Browse Source

Line2NodeMaterial: Refactoring, use native node material hooks (#33689)

sunag 1 month ago
parent
commit
a4b2603d31
1 changed files with 371 additions and 318 deletions
  1. 371 318
      src/materials/nodes/Line2NodeMaterial.js

+ 371 - 318
src/materials/nodes/Line2NodeMaterial.js

@@ -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;

粤ICP备19079148号