diff --git a/src/materials/nodes/Line2NodeMaterial.js b/src/materials/nodes/Line2NodeMaterial.js index 4d4db8c9d66ebc..480c16a2cf0123 100644 --- a/src/materials/nodes/Line2NodeMaterial.js +++ b/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} */ -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} + */ +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} + */ +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} + */ +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} inputs.start - Segment start position in view space. + * @param {Node} inputs.end - Segment end position in view space. + * @returns {Node} 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} - * @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} inputs.p1 - Start of line 1. + * @param {Node} inputs.p2 - End of line 1. + * @param {Node} inputs.p3 - Start of line 2. + * @param {Node} inputs.p4 - End of line 2. + * @returns {Node} 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} - * @default null - */ - this.offsetNode = null; + const p13 = p1.sub( p3 ); + const p43 = p4.sub( p3 ); - /** - * Defines the dash scale. - * - * @type {?Node} - * @default null - */ - this.dashScaleNode = null; + const p21 = p2.sub( p1 ); - /** - * Defines the dash size. - * - * @type {?Node} - * @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} - * @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} + */ +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} + */ +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} + * @default null + */ + this.offsetNode = null; - If( vUv.y.abs().greaterThan( 1.0 ), () => { + /** + * Defines the dash scale. + * + * @type {?Node} + * @default null + */ + this.dashScaleNode = null; - alpha.assign( smoothstep( dlen.oneMinus(), dlen.add( 1 ), len2 ).oneMinus() ); + /** + * Defines the dash size. + * + * @type {?Node} + * @default null + */ + this.dashSizeNode = null; - } ); + /** + * Defines the gap size. + * + * @type {?Node} + * @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} 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} + */ + 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;