Accueil / npm / three@0.186.1 / src / materials / nodes / Line2NodeMaterial.js

Line2NodeMaterial.js

import NodeMaterial from './NodeMaterial.js';
import { dashSize, diffuseColor, gapSize, varyingProperty } from '../../nodes/core/PropertyNode.js';
import { attribute } from '../../nodes/core/AttributeNode.js';
import { cameraProjectionMatrix, cameraProjectionMatrixInverse, cameraWorldMatrix } from '../../nodes/accessors/Camera.js';
import { materialLineScale, materialLineDashSize, materialLineGapSize, materialLineDashOffset, materialLineWidth } from '../../nodes/accessors/MaterialNode.js';
import { modelViewMatrix, modelWorldMatrixInverse } from '../../nodes/accessors/ModelNode.js';
import { positionGeometry, positionLocal, positionPrevious } 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 { screenDPR, viewport } from '../../nodes/display/ScreenNode.js';
import { viewportOpaqueMipTexture } from '../../nodes/display/ViewportTextureNode.js';

import { LineDashedMaterial } from '../LineDashedMaterial.js';
import { NoBlending } from '../../constants.js';
import { warnOnce } from '../../utils.js';

const _defaultValues = /*@__PURE__*/ new LineDashedMaterial();

/**
 * 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>}
 */
const worldStart = varyingProperty( 'vec3', 'worldStart' );

/**
 * 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' );

/**
 * 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' );

/**
 * 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 } ) => {

	const a = cameraProjectionMatrix.element( 2 ).element( 2 ); // 3nd entry in 3th column
	const b = cameraProjectionMatrix.element( 3 ).element( 2 ); // 3nd entry in 4th column

	// 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

	const nearEstimate = a.greaterThan( 0 ).select( b.negate().div( a.add( 1 ) ), b.mul( - 0.5 ).div( a ) );

	return nearEstimate.sub( start.z ).div( end.z.sub( start.z ) );

}, { start: 'vec4', end: 'vec4', return: 'float' } );

/**
 * 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 } ) => {

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

}, { 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 } ) => {

	const useDash = material._useDash;
	const useWorldUnits = material._useWorldUnits;

	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' );

	let distanceStart, distanceEnd;

	if ( useDash ) {

		distanceStart = float( attribute( 'instanceDistanceStart' ) ).toVar( 'distanceStart' );
		distanceEnd = float( attribute( 'instanceDistanceEnd' ) ).toVar( 'distanceEnd' );

	}

	if ( useWorldUnits ) {

		worldStart.assign( start.xyz );
		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 ) ), () => {

			const alpha = trimSegmentAlpha( { start, end } );
			end.assign( vec4( mix( start.xyz, end.xyz, alpha ), end.w ) );

			if ( useDash ) {

				distanceEnd.assign( mix( distanceStart, distanceEnd, alpha ) );

			}

		} ).ElseIf( end.z.lessThan( 0.0 ).and( start.z.greaterThanEqual( 0.0 ) ), () => {

			const alpha = trimSegmentAlpha( { start: end, end: start } );
			start.assign( vec4( mix( end.xyz, start.xyz, alpha ), start.w ) );

			if ( useDash ) {

				distanceStart.assign( mix( distanceEnd, distanceStart, alpha ) );

			}

		} );

	} );

	if ( useDash ) {

		const dashScaleNode = material.dashScaleNode ? float( material.dashScaleNode ) : materialLineScale;
		const offsetNode = material.offsetNode ? float( material.offsetNode ) : materialLineDashOffset;

		let lineDist = positionGeometry.y.lessThan( 0.5 ).select( dashScaleNode.mul( distanceStart ), dashScaleNode.mul( distanceEnd ) );
		lineDist = lineDist.add( offsetNode );

		lineDistance.assign( lineDist );

	}

	// 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 );

		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.div( screenDPR ) ) );

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

} )();

/**
 * 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 } ) => {

	const useAlphaToCoverage = material._useAlphaToCoverage;
	const useDash = material._useDash;
	const useWorldUnits = material._useWorldUnits;

	const vUv = uv();

	if ( useDash ) {

		const dashSizeNode = material.dashSizeNode ? float( material.dashSizeNode ) : materialLineDashSize;
		const gapSizeNode = material.gapSizeNode ? float( material.gapSizeNode ) : materialLineGapSize;

		dashSize.assign( dashSizeNode );
		gapSize.assign( gapSizeNode );

		vUv.y.lessThan( - 1.0 ).or( vUv.y.greaterThan( 1.0 ) ).discard(); // discard endcaps
		lineDistance.mod( dashSize.add( gapSize ) ).greaterThan( dashSize ).discard(); // todo - FIX

	}

	const alpha = float( 1 ).toVar( 'alpha' );

	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 } );

		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 ) {

				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.currentSamples > 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 ) );

			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();

			} );

		}

	}

	return alpha;

} )();

/**
 * 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.vertexColors = parameters.vertexColors;

		/**
		 * The dash offset.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.dashOffset = 0;

		/**
		 * 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 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 );

		if ( this.vertexColors === true && builder.geometry.hasAttribute( 'instanceColorStart' ) ) {

			const instanceColorStart = attribute( 'instanceColorStart' );
			const instanceColorEnd = attribute( 'instanceColorEnd' );

			const instanceColor = positionGeometry.y.lessThan( 0.5 ).select( instanceColorStart, instanceColorEnd );

			diffuseColor.rgb.mulAssign( instanceColor );

		}

		if ( this.transparent ) {

			diffuseColor.rgb.assign( diffuseColor.rgb.mul( diffuseColor.a ).add( viewportOpaqueMipTexture().rgb.mul( diffuseColor.a.oneMinus() ) ) );

		}

	}

	/**
	 * Setups the position of the expanded fat line vertex in local space.
	 *
	 * @param {NodeBuilder} builder - The current node builder.
	 * @return {Node<vec3>} The position of the fat line vertex in local space.
	 */
	setupPosition( builder ) {

		const localPosition = modelWorldMatrixInverse.mul( cameraWorldMatrix ).mul( cameraProjectionMatrixInverse ).mul( mvpLine );

		positionLocal.assign( localPosition.xyz.div( localPosition.w ) );

		if ( builder.needsPreviousData() ) {

			positionPrevious.assign( positionLocal );

		}

		return super.setupPosition( builder );

	}

	/**
	 * Defines the lines color.
	 *
	 * @deprecated since r185. Use {@link NodeMaterial#colorNode} instead.
	 * @type {?Node<vec3>}
	 */
	get lineColorNode() {

		return this.colorNode;

	}

	set lineColorNode( value ) {

		warnOnce( 'Line2NodeMaterial: "lineColorNode" has been deprecated. Use "colorNode" instead.' ); // @deprecated r185

		this.colorNode = value;

	}

	/**
	 * 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;

Exemple