|
|
@@ -1,8 +1,8 @@
|
|
|
import NodeMaterial from './NodeMaterial.js';
|
|
|
-import { dashSize, gapSize, varyingProperty } from '../../nodes/core/PropertyNode.js';
|
|
|
+import { dashSize, diffuseColor, 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 { materialLineScale, materialLineDashSize, materialLineGapSize, materialLineDashOffset, materialLineWidth } 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';
|
|
|
@@ -13,488 +13,542 @@ import { viewportOpaqueMipTexture } from '../../nodes/display/ViewportTextureNod
|
|
|
|
|
|
import { LineDashedMaterial } from '../LineDashedMaterial.js';
|
|
|
import { NoBlending } from '../../constants.js';
|
|
|
+import { warnOnce } from '../../utils.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
|
|
|
+ * Varying node representing the world position of the segment start in view space.
|
|
|
+ * Used for distance and coordinate calculations across the fragment shader.
|
|
|
+ * @type {VaryingNode<vec3>}
|
|
|
*/
|
|
|
-class Line2NodeMaterial extends NodeMaterial {
|
|
|
+const worldStart = varyingProperty( 'vec3', 'worldStart' );
|
|
|
|
|
|
- static get type() {
|
|
|
+/**
|
|
|
+ * Varying node representing the world position of the segment end in view space.
|
|
|
+ * Used for distance and coordinate calculations across the fragment shader.
|
|
|
+ * @type {VaryingNode<vec3>}
|
|
|
+ */
|
|
|
+const worldEnd = varyingProperty( 'vec3', 'worldEnd' );
|
|
|
|
|
|
- return 'Line2NodeMaterial';
|
|
|
+/**
|
|
|
+ * Varying node representing the accumulated distance along the line.
|
|
|
+ * Crucial for correctly computing dashed line intervals in fragment stage.
|
|
|
+ * @type {VaryingNode<float>}
|
|
|
+ */
|
|
|
+const lineDistance = varyingProperty( 'float', 'lineDistance' );
|
|
|
|
|
|
- }
|
|
|
+/**
|
|
|
+ * Varying node representing the interpolated world/view position of the current fragment.
|
|
|
+ * Used for line/ray distance checks under perspective projection.
|
|
|
+ * @type {VaryingNode<vec4>}
|
|
|
+ */
|
|
|
+const worldPos = varyingProperty( 'vec4', 'worldPos' );
|
|
|
|
|
|
- /**
|
|
|
- * Constructs a new node material for wide line rendering.
|
|
|
- *
|
|
|
- * @param {Object} [parameters={}] - The configuration parameter.
|
|
|
- */
|
|
|
- constructor( parameters = {} ) {
|
|
|
+/**
|
|
|
+ * Trims the line segment to avoid rendering behind the camera near plane.
|
|
|
+ * Computes an interpolation factor (alpha) to clamp the segment's coordinate.
|
|
|
+ *
|
|
|
+ * @param {Object} inputs
|
|
|
+ * @param {Node<vec4>} inputs.start - Segment start position in view space.
|
|
|
+ * @param {Node<vec4>} inputs.end - Segment end position in view space.
|
|
|
+ * @returns {Node<float>} The interpolation factor (alpha) to trim the segment.
|
|
|
+ */
|
|
|
+const trimSegmentAlpha = Fn( ( { start, end } ) => {
|
|
|
|
|
|
- super();
|
|
|
+ const a = cameraProjectionMatrix.element( 2 ).element( 2 ); // 3nd entry in 3th column
|
|
|
+ const b = cameraProjectionMatrix.element( 3 ).element( 2 ); // 3nd entry in 4th column
|
|
|
|
|
|
- /**
|
|
|
- * This flag can be used for type testing.
|
|
|
- *
|
|
|
- * @type {boolean}
|
|
|
- * @readonly
|
|
|
- * @default true
|
|
|
- */
|
|
|
- this.isLine2NodeMaterial = true;
|
|
|
+ // we need different nearEstimate formula for reversed and default depth buffer
|
|
|
+ // a is positive with a reversed depth buffer so it can be used for controlling the code flow
|
|
|
|
|
|
- this.setDefaultValues( _defaultValues );
|
|
|
+ const nearEstimate = a.greaterThan( 0 ).select( b.negate().div( a.add( 1 ) ), b.mul( - 0.5 ).div( a ) );
|
|
|
|
|
|
- /**
|
|
|
- * Whether vertex colors should be used or not.
|
|
|
- *
|
|
|
- * @type {boolean}
|
|
|
- * @default false
|
|
|
- */
|
|
|
- this.vertexColors = parameters.vertexColors;
|
|
|
+ return nearEstimate.sub( start.z ).div( end.z.sub( start.z ) );
|
|
|
|
|
|
- /**
|
|
|
- * The dash offset.
|
|
|
- *
|
|
|
- * @type {number}
|
|
|
- * @default 0
|
|
|
- */
|
|
|
- this.dashOffset = 0;
|
|
|
+}, { start: 'vec4', end: 'vec4', return: 'float' } );
|
|
|
|
|
|
- /**
|
|
|
- * Defines the lines color.
|
|
|
- *
|
|
|
- * @type {?Node<vec3>}
|
|
|
- * @default null
|
|
|
- */
|
|
|
- this.lineColorNode = null;
|
|
|
+/**
|
|
|
+ * Calculates the closest points on two 3D lines.
|
|
|
+ * Used for perspective-correct line rendering and coordinates interpolation.
|
|
|
+ *
|
|
|
+ * @param {Object} inputs
|
|
|
+ * @param {Node<vec3>} inputs.p1 - Start of line 1.
|
|
|
+ * @param {Node<vec3>} inputs.p2 - End of line 1.
|
|
|
+ * @param {Node<vec3>} inputs.p3 - Start of line 2.
|
|
|
+ * @param {Node<vec3>} inputs.p4 - End of line 2.
|
|
|
+ * @returns {Node<vec2>} A vec2 containing the parametric coordinates (mua, mub) of the closest points on line 1 and line 2.
|
|
|
+ */
|
|
|
+const closestLineToLine = Fn( ( { p1, p2, p3, p4 } ) => {
|
|
|
|
|
|
- /**
|
|
|
- * Defines the offset.
|
|
|
- *
|
|
|
- * @type {?Node<float>}
|
|
|
- * @default null
|
|
|
- */
|
|
|
- this.offsetNode = null;
|
|
|
+ const p13 = p1.sub( p3 );
|
|
|
+ const p43 = p4.sub( p3 );
|
|
|
|
|
|
- /**
|
|
|
- * Defines the dash scale.
|
|
|
- *
|
|
|
- * @type {?Node<float>}
|
|
|
- * @default null
|
|
|
- */
|
|
|
- this.dashScaleNode = null;
|
|
|
+ const p21 = p2.sub( p1 );
|
|
|
|
|
|
- /**
|
|
|
- * Defines the dash size.
|
|
|
- *
|
|
|
- * @type {?Node<float>}
|
|
|
- * @default null
|
|
|
- */
|
|
|
- this.dashSizeNode = null;
|
|
|
+ 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 );
|
|
|
|
|
|
- /**
|
|
|
- * Defines the gap size.
|
|
|
- *
|
|
|
- * @type {?Node<float>}
|
|
|
- * @default null
|
|
|
- */
|
|
|
- this.gapSizeNode = null;
|
|
|
+ const denom = d2121.mul( d4343 ).sub( d4321.mul( d4321 ) );
|
|
|
+ const numer = d1343.mul( d4321 ).sub( d1321.mul( d4343 ) );
|
|
|
|
|
|
- /**
|
|
|
- * Blending is set to `NoBlending` since transparency
|
|
|
- * is not supported, yet.
|
|
|
- *
|
|
|
- * @type {number}
|
|
|
- * @default 0
|
|
|
- */
|
|
|
- this.blending = NoBlending;
|
|
|
+ const mua = numer.div( denom ).clamp();
|
|
|
+ const mub = d1343.add( d4321.mul( mua ) ).div( d4343 ).clamp();
|
|
|
|
|
|
- this._useDash = parameters.dashed;
|
|
|
- this._useAlphaToCoverage = true;
|
|
|
- this._useWorldUnits = false;
|
|
|
+ return vec2( mua, mub );
|
|
|
|
|
|
- this.setValues( parameters );
|
|
|
+}, { p1: 'vec3', p2: 'vec3', p3: 'vec3', p4: 'vec3', return: 'vec2' } );
|
|
|
|
|
|
- }
|
|
|
+/**
|
|
|
+ * TSL node acting as a custom Model-View-Projection (MVP) for fat lines,
|
|
|
+ * expanding 3D segments into screen/world-facing ribbons of a specified width.
|
|
|
+ *
|
|
|
+ * @tsl
|
|
|
+ * @type {Node<vec4>}
|
|
|
+ */
|
|
|
+const mvpLine = Fn( ( { material } ) => {
|
|
|
|
|
|
- /**
|
|
|
- * Setups the vertex and fragment stage of this node material.
|
|
|
- *
|
|
|
- * @param {NodeBuilder} builder - The current node builder.
|
|
|
- */
|
|
|
- setup( builder ) {
|
|
|
+ const useDash = material._useDash;
|
|
|
+ const useWorldUnits = material._useWorldUnits;
|
|
|
|
|
|
- const { renderer } = builder;
|
|
|
+ const instanceStart = attribute( 'instanceStart' );
|
|
|
+ const instanceEnd = attribute( 'instanceEnd' );
|
|
|
|
|
|
- const useAlphaToCoverage = this._useAlphaToCoverage;
|
|
|
- const vertexColors = this.vertexColors;
|
|
|
- const useDash = this._useDash;
|
|
|
- const useWorldUnits = this._useWorldUnits;
|
|
|
+ // camera space
|
|
|
|
|
|
- const trimSegmentAlpha = Fn( ( { start, end } ) => {
|
|
|
+ const start = vec4( modelViewMatrix.mul( vec4( instanceStart, 1.0 ) ) ).toVar( 'start' );
|
|
|
+ const end = vec4( modelViewMatrix.mul( vec4( instanceEnd, 1.0 ) ) ).toVar( '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
|
|
|
+ let distanceStart, distanceEnd;
|
|
|
|
|
|
- // we need different nearEstimate formula for reversed and default depth buffer
|
|
|
- // a is positive with a reversed depth buffer so it can be used for controlling the code flow
|
|
|
+ if ( useDash ) {
|
|
|
|
|
|
- const nearEstimate = a.greaterThan( 0 ).select( b.negate().div( a.add( 1 ) ), b.mul( - 0.5 ).div( a ) );
|
|
|
+ distanceStart = float( attribute( 'instanceDistanceStart' ) ).toVar( 'distanceStart' );
|
|
|
+ distanceEnd = float( attribute( 'instanceDistanceEnd' ) ).toVar( 'distanceEnd' );
|
|
|
|
|
|
- return nearEstimate.sub( start.z ).div( end.z.sub( start.z ) );
|
|
|
+ }
|
|
|
|
|
|
- } ).setLayout( {
|
|
|
- name: 'trimSegmentAlpha',
|
|
|
- type: 'float',
|
|
|
- inputs: [
|
|
|
- { name: 'start', type: 'vec4' },
|
|
|
- { name: 'end', type: 'vec4' }
|
|
|
- ]
|
|
|
- } );
|
|
|
+ if ( useWorldUnits ) {
|
|
|
+
|
|
|
+ worldStart.assign( start.xyz );
|
|
|
+ worldEnd.assign( end.xyz );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ const aspect = viewport.z.div( viewport.w );
|
|
|
|
|
|
- this.vertexNode = Fn( () => {
|
|
|
+ // 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 instanceStart = attribute( 'instanceStart' );
|
|
|
- const instanceEnd = attribute( 'instanceEnd' );
|
|
|
+ const perspective = cameraProjectionMatrix.element( 2 ).element( 3 ).equal( - 1.0 ); // 4th entry in the 3rd column
|
|
|
|
|
|
- // camera space
|
|
|
+ If( perspective, () => {
|
|
|
|
|
|
- const start = vec4( modelViewMatrix.mul( vec4( instanceStart, 1.0 ) ) ).toVar( 'start' );
|
|
|
- const end = vec4( modelViewMatrix.mul( vec4( instanceEnd, 1.0 ) ) ).toVar( 'end' );
|
|
|
+ If( start.z.lessThan( 0.0 ).and( end.z.greaterThan( 0.0 ) ), () => {
|
|
|
|
|
|
- let distanceStart, distanceEnd;
|
|
|
+ const alpha = trimSegmentAlpha( { start, end } );
|
|
|
+ end.assign( vec4( mix( start.xyz, end.xyz, alpha ), end.w ) );
|
|
|
|
|
|
if ( useDash ) {
|
|
|
|
|
|
- distanceStart = float( attribute( 'instanceDistanceStart' ) ).toVar( 'distanceStart' );
|
|
|
- distanceEnd = float( attribute( 'instanceDistanceEnd' ) ).toVar( 'distanceEnd' );
|
|
|
+ distanceEnd.assign( mix( distanceStart, distanceEnd, alpha ) );
|
|
|
|
|
|
}
|
|
|
|
|
|
- if ( useWorldUnits ) {
|
|
|
+ } ).ElseIf( end.z.lessThan( 0.0 ).and( start.z.greaterThanEqual( 0.0 ) ), () => {
|
|
|
|
|
|
- varyingProperty( 'vec3', 'worldStart' ).assign( start.xyz );
|
|
|
- varyingProperty( 'vec3', 'worldEnd' ).assign( end.xyz );
|
|
|
+ const alpha = trimSegmentAlpha( { start: end, end: start } );
|
|
|
+ start.assign( vec4( mix( end.xyz, start.xyz, alpha ), start.w ) );
|
|
|
|
|
|
- }
|
|
|
+ if ( useDash ) {
|
|
|
|
|
|
- const aspect = viewport.z.div( viewport.w );
|
|
|
+ distanceStart.assign( mix( distanceEnd, distanceStart, alpha ) );
|
|
|
|
|
|
- // 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 ) ), () => {
|
|
|
+ if ( useDash ) {
|
|
|
|
|
|
- const alpha = trimSegmentAlpha( { start: start, end: end } );
|
|
|
- end.assign( vec4( mix( start.xyz, end.xyz, alpha ), end.w ) );
|
|
|
+ const dashScaleNode = material.dashScaleNode ? float( material.dashScaleNode ) : materialLineScale;
|
|
|
+ const offsetNode = material.offsetNode ? float( material.offsetNode ) : materialLineDashOffset;
|
|
|
|
|
|
- if ( useDash ) {
|
|
|
+ let lineDist = positionGeometry.y.lessThan( 0.5 ).select( dashScaleNode.mul( distanceStart ), dashScaleNode.mul( distanceEnd ) );
|
|
|
+ lineDist = lineDist.add( offsetNode );
|
|
|
|
|
|
- distanceEnd.assign( mix( distanceStart, distanceEnd, alpha ) );
|
|
|
+ lineDistance.assign( lineDist );
|
|
|
|
|
|
- }
|
|
|
+ }
|
|
|
|
|
|
- } ).ElseIf( end.z.lessThan( 0.0 ).and( start.z.greaterThanEqual( 0.0 ) ), () => {
|
|
|
+ // clip space
|
|
|
+ const clipStart = cameraProjectionMatrix.mul( start );
|
|
|
+ const clipEnd = cameraProjectionMatrix.mul( end );
|
|
|
|
|
|
- const alpha = trimSegmentAlpha( { start: end, end: start } );
|
|
|
- start.assign( vec4( mix( end.xyz, start.xyz, alpha ), start.w ) );
|
|
|
+ // ndc space
|
|
|
+ const ndcStart = clipStart.xyz.div( clipStart.w );
|
|
|
+ const ndcEnd = clipEnd.xyz.div( clipEnd.w );
|
|
|
|
|
|
- if ( useDash ) {
|
|
|
+ // direction
|
|
|
+ const dir = ndcEnd.xy.sub( ndcStart.xy ).toVar();
|
|
|
|
|
|
- distanceStart.assign( mix( distanceEnd, distanceStart, alpha ) );
|
|
|
+ // 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
|
|
|
|
|
|
- if ( useDash ) {
|
|
|
+ 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 dashScaleNode = this.dashScaleNode ? float( this.dashScaleNode ) : materialLineScale;
|
|
|
- const offsetNode = this.offsetNode ? float( this.offsetNode ) : materialLineDashOffset;
|
|
|
+ worldPos.assign( positionGeometry.y.lessThan( 0.5 ).select( start, end ) );
|
|
|
|
|
|
- let lineDistance = positionGeometry.y.lessThan( 0.5 ).select( dashScaleNode.mul( distanceStart ), dashScaleNode.mul( distanceEnd ) );
|
|
|
- lineDistance = lineDistance.add( offsetNode );
|
|
|
+ // 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 ) );
|
|
|
|
|
|
- varyingProperty( 'float', 'lineDistance' ).assign( lineDistance );
|
|
|
+ // 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 ) );
|
|
|
|
|
|
- // clip space
|
|
|
- const clipStart = cameraProjectionMatrix.mul( start );
|
|
|
- const clipEnd = cameraProjectionMatrix.mul( end );
|
|
|
+ // add width to the box
|
|
|
+ worldPos.addAssign( vec4( worldFwd.mul( hw ), 0 ) );
|
|
|
|
|
|
- // ndc space
|
|
|
- const ndcStart = clipStart.xyz.div( clipStart.w );
|
|
|
- const ndcEnd = clipEnd.xyz.div( clipEnd.w );
|
|
|
+ // endcaps
|
|
|
+ If( positionGeometry.y.greaterThan( 1.0 ).or( positionGeometry.y.lessThan( 0.0 ) ), () => {
|
|
|
|
|
|
- // direction
|
|
|
- const dir = ndcEnd.xy.sub( ndcStart.xy ).toVar();
|
|
|
+ worldPos.subAssign( vec4( worldFwd.mul( 2.0 ).mul( hw ), 0 ) );
|
|
|
|
|
|
- // account for clip-space aspect ratio
|
|
|
- dir.x.assign( dir.x.mul( aspect ) );
|
|
|
- dir.assign( dir.normalize() );
|
|
|
+ } );
|
|
|
|
|
|
- const clip = vec4().toVar();
|
|
|
+ }
|
|
|
|
|
|
- if ( useWorldUnits ) {
|
|
|
+ // project the worldpos
|
|
|
+ clip.assign( cameraProjectionMatrix.mul( worldPos ) );
|
|
|
|
|
|
- // get the offset direction as perpendicular to the view vector
|
|
|
+ // shift the depth of the projected points so the line
|
|
|
+ // segments overlap neatly
|
|
|
+ const clipPose = vec3().toVar();
|
|
|
|
|
|
- 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 );
|
|
|
+ clipPose.assign( positionGeometry.y.lessThan( 0.5 ).select( ndcStart, ndcEnd ) );
|
|
|
+ clip.z.assign( clipPose.z.mul( clip.w ) );
|
|
|
|
|
|
- const worldPos = varyingProperty( 'vec4', 'worldPos' );
|
|
|
+ } else {
|
|
|
|
|
|
- worldPos.assign( positionGeometry.y.lessThan( 0.5 ).select( start, end ) );
|
|
|
+ const offset = vec2( dir.y, dir.x.negate() ).toVar( 'offset' );
|
|
|
|
|
|
- // 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 ) );
|
|
|
+ // undo aspect ratio adjustment
|
|
|
+ dir.x.assign( dir.x.div( aspect ) );
|
|
|
+ offset.x.assign( offset.x.div( aspect ) );
|
|
|
|
|
|
- // don't extend the line if we're rendering dashes because we
|
|
|
- // won't be rendering the endcaps
|
|
|
- if ( ! useDash ) {
|
|
|
+ // sign flip
|
|
|
+ offset.assign( positionGeometry.x.lessThan( 0.0 ).select( offset.negate(), offset ) );
|
|
|
|
|
|
- // cap extension
|
|
|
- worldPos.addAssign( vec4( positionGeometry.y.lessThan( 0.5 ).select( worldDir.mul( hw ).negate(), worldDir.mul( hw ) ), 0 ) );
|
|
|
+ // endcaps
|
|
|
+ If( positionGeometry.y.lessThan( 0.0 ), () => {
|
|
|
|
|
|
- // add width to the box
|
|
|
- worldPos.addAssign( vec4( worldFwd.mul( hw ), 0 ) );
|
|
|
+ offset.assign( offset.sub( dir ) );
|
|
|
|
|
|
- // endcaps
|
|
|
- If( positionGeometry.y.greaterThan( 1.0 ).or( positionGeometry.y.lessThan( 0.0 ) ), () => {
|
|
|
+ } ).ElseIf( positionGeometry.y.greaterThan( 1.0 ), () => {
|
|
|
|
|
|
- worldPos.subAssign( vec4( worldFwd.mul( 2.0 ).mul( hw ), 0 ) );
|
|
|
+ offset.assign( offset.add( dir ) );
|
|
|
|
|
|
- } );
|
|
|
+ } );
|
|
|
|
|
|
- }
|
|
|
+ // adjust for linewidth
|
|
|
+ offset.assign( offset.mul( materialLineWidth ) );
|
|
|
|
|
|
- // project the worldpos
|
|
|
- clip.assign( cameraProjectionMatrix.mul( worldPos ) );
|
|
|
+ // adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ...
|
|
|
+ offset.assign( offset.div( viewport.w.div( screenDPR ) ) );
|
|
|
|
|
|
- // shift the depth of the projected points so the line
|
|
|
- // segments overlap neatly
|
|
|
- const clipPose = vec3().toVar();
|
|
|
+ // select end
|
|
|
+ clip.assign( positionGeometry.y.lessThan( 0.5 ).select( clipStart, clipEnd ) );
|
|
|
|
|
|
- clipPose.assign( positionGeometry.y.lessThan( 0.5 ).select( ndcStart, ndcEnd ) );
|
|
|
- clip.z.assign( clipPose.z.mul( clip.w ) );
|
|
|
+ // back to clip space
|
|
|
+ offset.assign( offset.mul( clip.w ) );
|
|
|
|
|
|
- } else {
|
|
|
+ clip.assign( clip.add( vec4( offset, 0, 0 ) ) );
|
|
|
|
|
|
- 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 ) );
|
|
|
+ return clip;
|
|
|
|
|
|
- // sign flip
|
|
|
- offset.assign( positionGeometry.x.lessThan( 0.0 ).select( offset.negate(), offset ) );
|
|
|
+} )();
|
|
|
|
|
|
- // endcaps
|
|
|
- If( positionGeometry.y.lessThan( 0.0 ), () => {
|
|
|
+/**
|
|
|
+ * TSL fragment node that computes the shape/coverage (alpha) of the fat line segment.
|
|
|
+ * Handles dash/gap generation, alpha-to-coverage rendering, and round endcaps.
|
|
|
+ *
|
|
|
+ * @tsl
|
|
|
+ * @type {Node<float>}
|
|
|
+ */
|
|
|
+const alphaLine = Fn( ( { material, renderer } ) => {
|
|
|
|
|
|
- offset.assign( offset.sub( dir ) );
|
|
|
+ const useAlphaToCoverage = material._useAlphaToCoverage;
|
|
|
+ const useDash = material._useDash;
|
|
|
+ const useWorldUnits = material._useWorldUnits;
|
|
|
|
|
|
- } ).ElseIf( positionGeometry.y.greaterThan( 1.0 ), () => {
|
|
|
+ const vUv = uv();
|
|
|
|
|
|
- offset.assign( offset.add( dir ) );
|
|
|
+ if ( useDash ) {
|
|
|
|
|
|
- } );
|
|
|
+ const dashSizeNode = material.dashSizeNode ? float( material.dashSizeNode ) : materialLineDashSize;
|
|
|
+ const gapSizeNode = material.gapSizeNode ? float( material.gapSizeNode ) : materialLineGapSize;
|
|
|
|
|
|
- // adjust for linewidth
|
|
|
- offset.assign( offset.mul( materialLineWidth ) );
|
|
|
+ dashSize.assign( dashSizeNode );
|
|
|
+ gapSize.assign( gapSizeNode );
|
|
|
|
|
|
- // adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ...
|
|
|
- offset.assign( offset.div( viewport.w.div( screenDPR ) ) );
|
|
|
+ vUv.y.lessThan( - 1.0 ).or( vUv.y.greaterThan( 1.0 ) ).discard(); // discard endcaps
|
|
|
+ lineDistance.mod( dashSize.add( gapSize ) ).greaterThan( dashSize ).discard(); // todo - FIX
|
|
|
|
|
|
- // select end
|
|
|
- clip.assign( positionGeometry.y.lessThan( 0.5 ).select( clipStart, clipEnd ) );
|
|
|
+ }
|
|
|
|
|
|
- // back to clip space
|
|
|
- offset.assign( offset.mul( clip.w ) );
|
|
|
+ const alpha = float( 1 ).toVar( 'alpha' );
|
|
|
|
|
|
- clip.assign( clip.add( vec4( offset, 0, 0 ) ) );
|
|
|
+ if ( useWorldUnits ) {
|
|
|
|
|
|
- }
|
|
|
+ // Find the closest points on the view ray and the line segment
|
|
|
+ const rayEnd = 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 } );
|
|
|
|
|
|
- return clip;
|
|
|
+ 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 ) {
|
|
|
|
|
|
- const closestLineToLine = Fn( ( { p1, p2, p3, p4 } ) => {
|
|
|
+ if ( useAlphaToCoverage && renderer.currentSamples > 0 ) {
|
|
|
|
|
|
- const p13 = p1.sub( p3 );
|
|
|
- const p43 = p4.sub( p3 );
|
|
|
+ const dnorm = norm.fwidth();
|
|
|
+ alpha.assign( smoothstep( dnorm.negate().add( 0.5 ), dnorm.add( 0.5 ), norm ).oneMinus() );
|
|
|
|
|
|
- const p21 = p2.sub( p1 );
|
|
|
+ } else {
|
|
|
|
|
|
- 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 );
|
|
|
+ norm.greaterThan( 0.5 ).discard();
|
|
|
|
|
|
- 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 );
|
|
|
+ } else {
|
|
|
|
|
|
- } ).setLayout( {
|
|
|
- name: 'closestLineToLine',
|
|
|
- type: 'vec2',
|
|
|
- inputs: [
|
|
|
- { name: 'p1', type: 'vec3' },
|
|
|
- { name: 'p2', type: 'vec3' },
|
|
|
- { name: 'p3', type: 'vec3' },
|
|
|
- { name: 'p4', type: 'vec3' }
|
|
|
- ]
|
|
|
- } );
|
|
|
+ // round endcaps
|
|
|
|
|
|
- this.colorNode = Fn( () => {
|
|
|
+ if ( useAlphaToCoverage && renderer.currentSamples > 0 ) {
|
|
|
|
|
|
- const vUv = uv();
|
|
|
+ const a = vUv.x;
|
|
|
+ const b = vUv.y.greaterThan( 0.0 ).select( vUv.y.sub( 1.0 ), vUv.y.add( 1.0 ) );
|
|
|
|
|
|
- if ( useDash ) {
|
|
|
+ const len2 = a.mul( a ).add( b.mul( b ) );
|
|
|
|
|
|
- const dashSizeNode = this.dashSizeNode ? float( this.dashSizeNode ) : materialLineDashSize;
|
|
|
- const gapSizeNode = this.gapSizeNode ? float( this.gapSizeNode ) : materialLineGapSize;
|
|
|
+ const dlen = float( len2.fwidth() ).toVar( 'dlen' );
|
|
|
|
|
|
- dashSize.assign( dashSizeNode );
|
|
|
- gapSize.assign( gapSizeNode );
|
|
|
+ If( vUv.y.abs().greaterThan( 1.0 ), () => {
|
|
|
|
|
|
- const vLineDistance = varyingProperty( 'float', 'lineDistance' );
|
|
|
+ alpha.assign( smoothstep( dlen.oneMinus(), dlen.add( 1 ), len2 ).oneMinus() );
|
|
|
|
|
|
- vUv.y.lessThan( - 1.0 ).or( vUv.y.greaterThan( 1.0 ) ).discard(); // discard endcaps
|
|
|
- vLineDistance.mod( dashSize.add( gapSize ) ).greaterThan( dashSize ).discard(); // todo - FIX
|
|
|
+ } );
|
|
|
|
|
|
- }
|
|
|
+ } else {
|
|
|
|
|
|
- const alpha = float( 1 ).toVar( 'alpha' );
|
|
|
+ If( vUv.y.abs().greaterThan( 1.0 ), () => {
|
|
|
|
|
|
- if ( useWorldUnits ) {
|
|
|
+ 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 worldStart = varyingProperty( 'vec3', 'worldStart' );
|
|
|
- const worldEnd = varyingProperty( 'vec3', 'worldEnd' );
|
|
|
+ len2.greaterThan( 1.0 ).discard();
|
|
|
|
|
|
- // 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.currentSamples > 0 ) {
|
|
|
+ return alpha;
|
|
|
|
|
|
- const dnorm = norm.fwidth();
|
|
|
- alpha.assign( smoothstep( dnorm.negate().add( 0.5 ), dnorm.add( 0.5 ), norm ).oneMinus() );
|
|
|
+} )();
|
|
|
|
|
|
- } else {
|
|
|
+/**
|
|
|
+ * 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 {
|
|
|
|
|
|
- norm.greaterThan( 0.5 ).discard();
|
|
|
+ static get type() {
|
|
|
|
|
|
- }
|
|
|
+ return 'Line2NodeMaterial';
|
|
|
|
|
|
- }
|
|
|
+ }
|
|
|
|
|
|
- } else {
|
|
|
+ /**
|
|
|
+ * Constructs a new node material for wide line rendering.
|
|
|
+ *
|
|
|
+ * @param {Object} [parameters={}] - The configuration parameter.
|
|
|
+ */
|
|
|
+ constructor( parameters = {} ) {
|
|
|
|
|
|
- // round endcaps
|
|
|
+ super();
|
|
|
|
|
|
- if ( useAlphaToCoverage && renderer.currentSamples > 0 ) {
|
|
|
+ /**
|
|
|
+ * This flag can be used for type testing.
|
|
|
+ *
|
|
|
+ * @type {boolean}
|
|
|
+ * @readonly
|
|
|
+ * @default true
|
|
|
+ */
|
|
|
+ this.isLine2NodeMaterial = true;
|
|
|
|
|
|
- const a = vUv.x;
|
|
|
- const b = vUv.y.greaterThan( 0.0 ).select( vUv.y.sub( 1.0 ), vUv.y.add( 1.0 ) );
|
|
|
+ this.setDefaultValues( _defaultValues );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Whether vertex colors should be used or not.
|
|
|
+ *
|
|
|
+ * @type {boolean}
|
|
|
+ * @default false
|
|
|
+ */
|
|
|
+ this.vertexColors = parameters.vertexColors;
|
|
|
|
|
|
- const len2 = a.mul( a ).add( b.mul( b ) );
|
|
|
+ /**
|
|
|
+ * The dash offset.
|
|
|
+ *
|
|
|
+ * @type {number}
|
|
|
+ * @default 0
|
|
|
+ */
|
|
|
+ this.dashOffset = 0;
|
|
|
|
|
|
- const dlen = float( len2.fwidth() ).toVar( 'dlen' );
|
|
|
+ /**
|
|
|
+ * Defines the offset.
|
|
|
+ *
|
|
|
+ * @type {?Node<float>}
|
|
|
+ * @default null
|
|
|
+ */
|
|
|
+ this.offsetNode = null;
|
|
|
|
|
|
- If( vUv.y.abs().greaterThan( 1.0 ), () => {
|
|
|
+ /**
|
|
|
+ * Defines the dash scale.
|
|
|
+ *
|
|
|
+ * @type {?Node<float>}
|
|
|
+ * @default null
|
|
|
+ */
|
|
|
+ this.dashScaleNode = null;
|
|
|
|
|
|
- alpha.assign( smoothstep( dlen.oneMinus(), dlen.add( 1 ), len2 ).oneMinus() );
|
|
|
+ /**
|
|
|
+ * Defines the dash size.
|
|
|
+ *
|
|
|
+ * @type {?Node<float>}
|
|
|
+ * @default null
|
|
|
+ */
|
|
|
+ this.dashSizeNode = null;
|
|
|
|
|
|
- } );
|
|
|
+ /**
|
|
|
+ * Defines the gap size.
|
|
|
+ *
|
|
|
+ * @type {?Node<float>}
|
|
|
+ * @default null
|
|
|
+ */
|
|
|
+ this.gapSizeNode = null;
|
|
|
|
|
|
- } else {
|
|
|
+ /**
|
|
|
+ * Blending is set to `NoBlending` since transparency
|
|
|
+ * is not supported, yet.
|
|
|
+ *
|
|
|
+ * @type {number}
|
|
|
+ * @default 0
|
|
|
+ */
|
|
|
+ this.blending = NoBlending;
|
|
|
|
|
|
- If( vUv.y.abs().greaterThan( 1.0 ), () => {
|
|
|
+ this._useDash = parameters.dashed;
|
|
|
+ this._useAlphaToCoverage = true;
|
|
|
+ this._useWorldUnits = false;
|
|
|
|
|
|
- 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 ) );
|
|
|
+ this.setValues( parameters );
|
|
|
|
|
|
- len2.greaterThan( 1.0 ).discard();
|
|
|
+ }
|
|
|
|
|
|
- } );
|
|
|
+ /**
|
|
|
+ * Setups the diffuse color of the line material in the fragment stage.
|
|
|
+ * Overrides the base setup to incorporate line/dash rendering and blending.
|
|
|
+ *
|
|
|
+ * @param {NodeBuilder} builder - The current node builder.
|
|
|
+ */
|
|
|
+ setupDiffuseColor( builder ) {
|
|
|
|
|
|
- }
|
|
|
+ super.setupDiffuseColor( builder );
|
|
|
|
|
|
- }
|
|
|
+ diffuseColor.a.mulAssign( alphaLine );
|
|
|
|
|
|
- let lineColorNode;
|
|
|
+ if ( this.vertexColors === true && builder.geometry.hasAttribute( 'instanceColorStart' ) ) {
|
|
|
|
|
|
- if ( this.lineColorNode ) {
|
|
|
+ const instanceColorStart = attribute( 'instanceColorStart' );
|
|
|
+ const instanceColorEnd = attribute( 'instanceColorEnd' );
|
|
|
|
|
|
- lineColorNode = this.lineColorNode;
|
|
|
+ const instanceColor = positionGeometry.y.lessThan( 0.5 ).select( instanceColorStart, instanceColorEnd );
|
|
|
|
|
|
- } else {
|
|
|
+ diffuseColor.rgb.mulAssign( instanceColor );
|
|
|
|
|
|
- if ( vertexColors ) {
|
|
|
+ }
|
|
|
|
|
|
- const instanceColorStart = attribute( 'instanceColorStart' );
|
|
|
- const instanceColorEnd = attribute( 'instanceColorEnd' );
|
|
|
+ if ( this.transparent ) {
|
|
|
|
|
|
- const instanceColor = positionGeometry.y.lessThan( 0.5 ).select( instanceColorStart, instanceColorEnd );
|
|
|
+ diffuseColor.rgb.assign( diffuseColor.rgb.mul( diffuseColor.a ).add( viewportOpaqueMipTexture().rgb.mul( diffuseColor.a.oneMinus() ) ) );
|
|
|
|
|
|
- lineColorNode = instanceColor.mul( materialColor );
|
|
|
+ }
|
|
|
|
|
|
- } else {
|
|
|
+ }
|
|
|
|
|
|
- lineColorNode = materialColor;
|
|
|
+ /**
|
|
|
+ * Setups the position in clip space for the vertex stage of the fat line.
|
|
|
+ * Overrides the default model-view-projection to return the expanded fat line vertex coordinates.
|
|
|
+ *
|
|
|
+ * @param {NodeBuilder} builder - The current node builder.
|
|
|
+ * @return {Node<vec4>} The position of the fat line vertex in clip space.
|
|
|
+ */
|
|
|
+ setupModelViewProjection( /*builder*/ ) {
|
|
|
|
|
|
- }
|
|
|
+ return mvpLine;
|
|
|
|
|
|
- }
|
|
|
+ }
|
|
|
|
|
|
- return vec4( lineColorNode, alpha );
|
|
|
+ /**
|
|
|
+ * Defines the lines color.
|
|
|
+ *
|
|
|
+ * @deprecated since r185. Use {@link NodeMaterial#colorNode} instead.
|
|
|
+ * @type {?Node<vec3>}
|
|
|
+ */
|
|
|
+ get lineColorNode() {
|
|
|
|
|
|
- } )();
|
|
|
+ warnOnce( 'Line2NodeMaterial: "lineColorNode" has been deprecated. Use "colorNode" instead.' ); // @deprecated r185
|
|
|
|
|
|
- if ( this.transparent ) {
|
|
|
+ return this.colorNode;
|
|
|
|
|
|
- const opacityNode = this.opacityNode ? float( this.opacityNode ) : materialOpacity;
|
|
|
+ }
|
|
|
|
|
|
- this.outputNode = vec4( this.colorNode.rgb.mul( opacityNode ).add( viewportOpaqueMipTexture().rgb.mul( opacityNode.oneMinus() ) ), this.colorNode.a );
|
|
|
+ set lineColorNode( value ) {
|
|
|
|
|
|
- }
|
|
|
+ warnOnce( 'Line2NodeMaterial: "lineColorNode" has been deprecated. Use "colorNode" instead.' ); // @deprecated r185
|
|
|
|
|
|
- super.setup( builder );
|
|
|
+ this.colorNode = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
@@ -581,7 +635,6 @@ class Line2NodeMaterial extends NodeMaterial {
|
|
|
this.vertexColors = source.vertexColors;
|
|
|
this.dashOffset = source.dashOffset;
|
|
|
|
|
|
- this.lineColorNode = source.lineColorNode;
|
|
|
this.offsetNode = source.offsetNode;
|
|
|
this.dashScaleNode = source.dashScaleNode;
|
|
|
this.dashSizeNode = source.dashSizeNode;
|