Buckets:
| import NodeMaterial from './NodeMaterial.js'; | |
| import { dashSize, 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 { 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 { viewport } from '../../nodes/display/ScreenNode.js'; | |
| import { viewportSharedTexture } from '../../nodes/display/ViewportSharedTextureNode.js'; | |
| import { LineDashedMaterial } from '../LineDashedMaterial.js'; | |
| import { NoBlending } from '../../constants.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 | |
| */ | |
| 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.useColor = parameters.vertexColors; | |
| /** | |
| * The dash offset. | |
| * | |
| * @type {number} | |
| * @default 0 | |
| */ | |
| this.dashOffset = 0; | |
| /** | |
| * The line width. | |
| * | |
| * @type {number} | |
| * @default 0 | |
| */ | |
| this.lineWidth = 1; | |
| /** | |
| * Defines the lines color. | |
| * | |
| * @type {?Node<vec3>} | |
| * @default null | |
| */ | |
| this.lineColorNode = null; | |
| /** | |
| * 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 vertex and fragment stage of this node material. | |
| * | |
| * @param {NodeBuilder} builder - The current node builder. | |
| */ | |
| setup( builder ) { | |
| const { renderer } = builder; | |
| const useAlphaToCoverage = this._useAlphaToCoverage; | |
| const useColor = this.useColor; | |
| const useDash = this._useDash; | |
| const useWorldUnits = this._useWorldUnits; | |
| const trimSegment = 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 | |
| const nearEstimate = b.mul( - 0.5 ).div( a ); | |
| const alpha = nearEstimate.sub( start.z ).div( end.z.sub( start.z ) ); | |
| return vec4( mix( start.xyz, end.xyz, alpha ), end.w ); | |
| } ).setLayout( { | |
| name: 'trimSegment', | |
| type: 'vec4', | |
| inputs: [ | |
| { name: 'start', type: 'vec4' }, | |
| { name: 'end', type: 'vec4' } | |
| ] | |
| } ); | |
| this.vertexNode = Fn( () => { | |
| 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' ); | |
| if ( useDash ) { | |
| const dashScaleNode = this.dashScaleNode ? float( this.dashScaleNode ) : materialLineScale; | |
| const offsetNode = this.offsetNode ? float( this.offsetNode ) : materialLineDashOffset; | |
| const instanceDistanceStart = attribute( 'instanceDistanceStart' ); | |
| const instanceDistanceEnd = attribute( 'instanceDistanceEnd' ); | |
| let lineDistance = positionGeometry.y.lessThan( 0.5 ).select( dashScaleNode.mul( instanceDistanceStart ), dashScaleNode.mul( instanceDistanceEnd ) ); | |
| lineDistance = lineDistance.add( offsetNode ); | |
| varyingProperty( 'float', 'lineDistance' ).assign( lineDistance ); | |
| } | |
| if ( useWorldUnits ) { | |
| varyingProperty( 'vec3', 'worldStart' ).assign( start.xyz ); | |
| varyingProperty( 'vec3', '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 ) ), () => { | |
| end.assign( trimSegment( { start: start, end: end } ) ); | |
| } ).ElseIf( end.z.lessThan( 0.0 ).and( start.z.greaterThanEqual( 0.0 ) ), () => { | |
| start.assign( trimSegment( { start: end, end: start } ) ); | |
| } ); | |
| } ); | |
| // 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 ); | |
| const worldPos = varyingProperty( 'vec4', 'worldPos' ); | |
| 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 ) ); | |
| // 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; | |
| } )(); | |
| 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 ); | |
| } ); | |
| this.colorNode = Fn( () => { | |
| const vUv = uv(); | |
| if ( useDash ) { | |
| const dashSizeNode = this.dashSizeNode ? float( this.dashSizeNode ) : materialLineDashSize; | |
| const gapSizeNode = this.gapSizeNode ? float( this.gapSizeNode ) : materialLineGapSize; | |
| dashSize.assign( dashSizeNode ); | |
| gapSize.assign( gapSizeNode ); | |
| const vLineDistance = varyingProperty( 'float', 'lineDistance' ); | |
| vUv.y.lessThan( - 1.0 ).or( vUv.y.greaterThan( 1.0 ) ).discard(); // discard endcaps | |
| vLineDistance.mod( dashSize.add( gapSize ) ).greaterThan( dashSize ).discard(); // todo - FIX | |
| } | |
| const alpha = float( 1 ).toVar( 'alpha' ); | |
| if ( useWorldUnits ) { | |
| const worldStart = varyingProperty( 'vec3', 'worldStart' ); | |
| const worldEnd = varyingProperty( 'vec3', 'worldEnd' ); | |
| // 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.samples > 1 ) { | |
| 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.samples > 1 ) { | |
| 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(); | |
| } ); | |
| } | |
| } | |
| let lineColorNode; | |
| if ( this.lineColorNode ) { | |
| lineColorNode = this.lineColorNode; | |
| } else { | |
| if ( useColor ) { | |
| const instanceColorStart = attribute( 'instanceColorStart' ); | |
| const instanceColorEnd = attribute( 'instanceColorEnd' ); | |
| const instanceColor = positionGeometry.y.lessThan( 0.5 ).select( instanceColorStart, instanceColorEnd ); | |
| lineColorNode = instanceColor.mul( materialColor ); | |
| } else { | |
| lineColorNode = materialColor; | |
| } | |
| } | |
| return vec4( lineColorNode, alpha ); | |
| } )(); | |
| if ( this.transparent ) { | |
| const opacityNode = this.opacityNode ? float( this.opacityNode ) : materialOpacity; | |
| this.outputNode = vec4( this.colorNode.rgb.mul( opacityNode ).add( viewportSharedTexture().rgb.mul( opacityNode.oneMinus() ) ), this.colorNode.a ); | |
| } | |
| super.setup( builder ); | |
| } | |
| /** | |
| * 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; | |
Xet Storage Details
- Size:
- 13.9 kB
- Xet hash:
- 3cd9e7b4b957803df74c2b6f80c219902d6d067a3ff5cf023441f319c2a3bc9b
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.