| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625 |
- import NodeMaterial from './NodeMaterial.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 { 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';
- 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 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;
- }
- /**
- * 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;
|