@openhps/core
Version:
Open Hybrid Positioning System - Core component
434 lines (393 loc) • 14.4 kB
JavaScript
import NodeMaterial from './NodeMaterial.js';
import { dashSize, gapSize, varyingProperty } from '../../nodes/core/PropertyNode.js';
import { attribute } from '../../nodes/core/AttributeNode.js';
import { cameraProjectionMatrix } from '../../nodes/accessors/Camera.js';
import { materialColor, materialLineScale, materialLineDashSize, materialLineGapSize, materialLineDashOffset, materialLineWidth, materialOpacity } from '../../nodes/accessors/MaterialNode.js';
import { modelViewMatrix } from '../../nodes/accessors/ModelNode.js';
import { positionGeometry } from '../../nodes/accessors/Position.js';
import { mix, smoothstep } from '../../nodes/math/MathNode.js';
import { Fn, float, vec2, vec3, vec4, If } from '../../nodes/tsl/TSLBase.js';
import { uv } from '../../nodes/accessors/UV.js';
import { viewport } from '../../nodes/display/ScreenNode.js';
import { viewportSharedTexture } from '../../nodes/display/ViewportSharedTextureNode.js';
import { LineDashedMaterial } from '../LineDashedMaterial.js';
import { NoBlending } from '../../constants.js';
const _defaultValues = /*@__PURE__*/new LineDashedMaterial();
/**
* This node material can be used to render lines with a size larger than one
* by representing them as instanced meshes.
*
* @augments NodeMaterial
*/
class Line2NodeMaterial extends NodeMaterial {
static get type() {
return 'Line2NodeMaterial';
}
/**
* Constructs a new node material for wide line rendering.
*
* @param {Object} [parameters={}] - The configuration parameter.
*/
constructor(parameters = {}) {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isLine2NodeMaterial = true;
this.setDefaultValues(_defaultValues);
/**
* Whether vertex colors should be used or not.
*
* @type {boolean}
* @default false
*/
this.useColor = parameters.vertexColors;
/**
* The dash offset.
*
* @type {number}
* @default 0
*/
this.dashOffset = 0;
/**
* The line width.
*
* @type {number}
* @default 0
*/
this.lineWidth = 1;
/**
* Defines the lines color.
*
* @type {?Node<vec3>}
* @default null
*/
this.lineColorNode = null;
/**
* Defines the offset.
*
* @type {?Node<float>}
* @default null
*/
this.offsetNode = null;
/**
* Defines the dash scale.
*
* @type {?Node<float>}
* @default null
*/
this.dashScaleNode = null;
/**
* Defines the dash size.
*
* @type {?Node<float>}
* @default null
*/
this.dashSizeNode = null;
/**
* Defines the gap size.
*
* @type {?Node<float>}
* @default null
*/
this.gapSizeNode = null;
/**
* Blending is set to `NoBlending` since transparency
* is not supported, yet.
*
* @type {number}
* @default 0
*/
this.blending = NoBlending;
this._useDash = parameters.dashed;
this._useAlphaToCoverage = true;
this._useWorldUnits = false;
this.setValues(parameters);
}
/**
* Setups the vertex and fragment stage of this node material.
*
* @param {NodeBuilder} builder - The current node builder.
*/
setup(builder) {
const {
renderer
} = builder;
const useAlphaToCoverage = this._useAlphaToCoverage;
const useColor = this.useColor;
const useDash = this._useDash;
const useWorldUnits = this._useWorldUnits;
const trimSegment = Fn(({
start,
end
}) => {
const a = cameraProjectionMatrix.element(2).element(2); // 3nd entry in 3th column
const b = cameraProjectionMatrix.element(3).element(2); // 3nd entry in 4th column
const nearEstimate = b.mul(-0.5).div(a);
const alpha = nearEstimate.sub(start.z).div(end.z.sub(start.z));
return vec4(mix(start.xyz, end.xyz, alpha), end.w);
}).setLayout({
name: 'trimSegment',
type: 'vec4',
inputs: [{
name: 'start',
type: 'vec4'
}, {
name: 'end',
type: 'vec4'
}]
});
this.vertexNode = Fn(() => {
const instanceStart = attribute('instanceStart');
const instanceEnd = attribute('instanceEnd');
// camera space
const start = vec4(modelViewMatrix.mul(vec4(instanceStart, 1.0))).toVar('start');
const end = vec4(modelViewMatrix.mul(vec4(instanceEnd, 1.0))).toVar('end');
if (useDash) {
const dashScaleNode = this.dashScaleNode ? float(this.dashScaleNode) : materialLineScale;
const offsetNode = this.offsetNode ? float(this.offsetNode) : materialLineDashOffset;
const instanceDistanceStart = attribute('instanceDistanceStart');
const instanceDistanceEnd = attribute('instanceDistanceEnd');
let lineDistance = positionGeometry.y.lessThan(0.5).select(dashScaleNode.mul(instanceDistanceStart), dashScaleNode.mul(instanceDistanceEnd));
lineDistance = lineDistance.add(offsetNode);
varyingProperty('float', 'lineDistance').assign(lineDistance);
}
if (useWorldUnits) {
varyingProperty('vec3', 'worldStart').assign(start.xyz);
varyingProperty('vec3', 'worldEnd').assign(end.xyz);
}
const aspect = viewport.z.div(viewport.w);
// special case for perspective projection, and segments that terminate either in, or behind, the camera plane
// clearly the gpu firmware has a way of addressing this issue when projecting into ndc space
// but we need to perform ndc-space calculations in the shader, so we must address this issue directly
// perhaps there is a more elegant solution -- WestLangley
const perspective = cameraProjectionMatrix.element(2).element(3).equal(-1.0); // 4th entry in the 3rd column
If(perspective, () => {
If(start.z.lessThan(0.0).and(end.z.greaterThan(0.0)), () => {
end.assign(trimSegment({
start: start,
end: end
}));
}).ElseIf(end.z.lessThan(0.0).and(start.z.greaterThanEqual(0.0)), () => {
start.assign(trimSegment({
start: end,
end: start
}));
});
});
// clip space
const clipStart = cameraProjectionMatrix.mul(start);
const clipEnd = cameraProjectionMatrix.mul(end);
// ndc space
const ndcStart = clipStart.xyz.div(clipStart.w);
const ndcEnd = clipEnd.xyz.div(clipEnd.w);
// direction
const dir = ndcEnd.xy.sub(ndcStart.xy).toVar();
// account for clip-space aspect ratio
dir.x.assign(dir.x.mul(aspect));
dir.assign(dir.normalize());
const clip = vec4().toVar();
if (useWorldUnits) {
// get the offset direction as perpendicular to the view vector
const worldDir = end.xyz.sub(start.xyz).normalize();
const tmpFwd = mix(start.xyz, end.xyz, 0.5).normalize();
const worldUp = worldDir.cross(tmpFwd).normalize();
const worldFwd = worldDir.cross(worldUp);
const worldPos = varyingProperty('vec4', 'worldPos');
worldPos.assign(positionGeometry.y.lessThan(0.5).select(start, end));
// height offset
const hw = materialLineWidth.mul(0.5);
worldPos.addAssign(vec4(positionGeometry.x.lessThan(0.0).select(worldUp.mul(hw), worldUp.mul(hw).negate()), 0));
// don't extend the line if we're rendering dashes because we
// won't be rendering the endcaps
if (!useDash) {
// cap extension
worldPos.addAssign(vec4(positionGeometry.y.lessThan(0.5).select(worldDir.mul(hw).negate(), worldDir.mul(hw)), 0));
// add width to the box
worldPos.addAssign(vec4(worldFwd.mul(hw), 0));
// endcaps
If(positionGeometry.y.greaterThan(1.0).or(positionGeometry.y.lessThan(0.0)), () => {
worldPos.subAssign(vec4(worldFwd.mul(2.0).mul(hw), 0));
});
}
// project the worldpos
clip.assign(cameraProjectionMatrix.mul(worldPos));
// shift the depth of the projected points so the line
// segments overlap neatly
const clipPose = vec3().toVar();
clipPose.assign(positionGeometry.y.lessThan(0.5).select(ndcStart, ndcEnd));
clip.z.assign(clipPose.z.mul(clip.w));
} else {
const offset = vec2(dir.y, dir.x.negate()).toVar('offset');
// undo aspect ratio adjustment
dir.x.assign(dir.x.div(aspect));
offset.x.assign(offset.x.div(aspect));
// sign flip
offset.assign(positionGeometry.x.lessThan(0.0).select(offset.negate(), offset));
// endcaps
If(positionGeometry.y.lessThan(0.0), () => {
offset.assign(offset.sub(dir));
}).ElseIf(positionGeometry.y.greaterThan(1.0), () => {
offset.assign(offset.add(dir));
});
// adjust for linewidth
offset.assign(offset.mul(materialLineWidth));
// adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ...
offset.assign(offset.div(viewport.w));
// select end
clip.assign(positionGeometry.y.lessThan(0.5).select(clipStart, clipEnd));
// back to clip space
offset.assign(offset.mul(clip.w));
clip.assign(clip.add(vec4(offset, 0, 0)));
}
return clip;
})();
const closestLineToLine = Fn(({
p1,
p2,
p3,
p4
}) => {
const p13 = p1.sub(p3);
const p43 = p4.sub(p3);
const p21 = p2.sub(p1);
const d1343 = p13.dot(p43);
const d4321 = p43.dot(p21);
const d1321 = p13.dot(p21);
const d4343 = p43.dot(p43);
const d2121 = p21.dot(p21);
const denom = d2121.mul(d4343).sub(d4321.mul(d4321));
const numer = d1343.mul(d4321).sub(d1321.mul(d4343));
const mua = numer.div(denom).clamp();
const mub = d1343.add(d4321.mul(mua)).div(d4343).clamp();
return vec2(mua, mub);
});
this.colorNode = Fn(() => {
const vUv = uv();
if (useDash) {
const dashSizeNode = this.dashSizeNode ? float(this.dashSizeNode) : materialLineDashSize;
const gapSizeNode = this.gapSizeNode ? float(this.gapSizeNode) : materialLineGapSize;
dashSize.assign(dashSizeNode);
gapSize.assign(gapSizeNode);
const vLineDistance = varyingProperty('float', 'lineDistance');
vUv.y.lessThan(-1.0).or(vUv.y.greaterThan(1.0)).discard(); // discard endcaps
vLineDistance.mod(dashSize.add(gapSize)).greaterThan(dashSize).discard(); // todo - FIX
}
const alpha = float(1).toVar('alpha');
if (useWorldUnits) {
const worldStart = varyingProperty('vec3', 'worldStart');
const worldEnd = varyingProperty('vec3', 'worldEnd');
// Find the closest points on the view ray and the line segment
const rayEnd = varyingProperty('vec4', 'worldPos').xyz.normalize().mul(1e5);
const lineDir = worldEnd.sub(worldStart);
const params = closestLineToLine({
p1: worldStart,
p2: worldEnd,
p3: vec3(0.0, 0.0, 0.0),
p4: rayEnd
});
const p1 = worldStart.add(lineDir.mul(params.x));
const p2 = rayEnd.mul(params.y);
const delta = p1.sub(p2);
const len = delta.length();
const norm = len.div(materialLineWidth);
if (!useDash) {
if (useAlphaToCoverage && renderer.samples > 1) {
const dnorm = norm.fwidth();
alpha.assign(smoothstep(dnorm.negate().add(0.5), dnorm.add(0.5), norm).oneMinus());
} else {
norm.greaterThan(0.5).discard();
}
}
} else {
// round endcaps
if (useAlphaToCoverage && renderer.samples > 1) {
const a = vUv.x;
const b = vUv.y.greaterThan(0.0).select(vUv.y.sub(1.0), vUv.y.add(1.0));
const len2 = a.mul(a).add(b.mul(b));
const dlen = float(len2.fwidth()).toVar('dlen');
If(vUv.y.abs().greaterThan(1.0), () => {
alpha.assign(smoothstep(dlen.oneMinus(), dlen.add(1), len2).oneMinus());
});
} else {
If(vUv.y.abs().greaterThan(1.0), () => {
const a = vUv.x;
const b = vUv.y.greaterThan(0.0).select(vUv.y.sub(1.0), vUv.y.add(1.0));
const len2 = a.mul(a).add(b.mul(b));
len2.greaterThan(1.0).discard();
});
}
}
let lineColorNode;
if (this.lineColorNode) {
lineColorNode = this.lineColorNode;
} else {
if (useColor) {
const instanceColorStart = attribute('instanceColorStart');
const instanceColorEnd = attribute('instanceColorEnd');
const instanceColor = positionGeometry.y.lessThan(0.5).select(instanceColorStart, instanceColorEnd);
lineColorNode = instanceColor.mul(materialColor);
} else {
lineColorNode = materialColor;
}
}
return vec4(lineColorNode, alpha);
})();
if (this.transparent) {
const opacityNode = this.opacityNode ? float(this.opacityNode) : materialOpacity;
this.outputNode = vec4(this.colorNode.rgb.mul(opacityNode).add(viewportSharedTexture().rgb.mul(opacityNode.oneMinus())), this.colorNode.a);
}
super.setup(builder);
}
/**
* Whether the lines should sized in world units or not.
* When set to `false` the unit is pixel.
*
* @type {boolean}
* @default false
*/
get worldUnits() {
return this._useWorldUnits;
}
set worldUnits(value) {
if (this._useWorldUnits !== value) {
this._useWorldUnits = value;
this.needsUpdate = true;
}
}
/**
* Whether the lines should be dashed or not.
*
* @type {boolean}
* @default false
*/
get dashed() {
return this._useDash;
}
set dashed(value) {
if (this._useDash !== value) {
this._useDash = value;
this.needsUpdate = true;
}
}
/**
* Whether alpha to coverage should be used or not.
*
* @type {boolean}
* @default true
*/
get alphaToCoverage() {
return this._useAlphaToCoverage;
}
set alphaToCoverage(value) {
if (this._useAlphaToCoverage !== value) {
this._useAlphaToCoverage = value;
this.needsUpdate = true;
}
}
}
export default Line2NodeMaterial;