441 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			441 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | import NodeMaterial, { addNodeMaterial } from './NodeMaterial.js'; | ||
|  | import { temp } from '../core/VarNode.js'; | ||
|  | import { varying } from '../core/VaryingNode.js'; | ||
|  | import { property, varyingProperty } from '../core/PropertyNode.js'; | ||
|  | import { attribute } from '../core/AttributeNode.js'; | ||
|  | import { cameraProjectionMatrix } from '../accessors/CameraNode.js'; | ||
|  | import { materialColor, materialLineScale, materialLineDashSize, materialLineGapSize, materialLineDashOffset, materialLineWidth } from '../accessors/MaterialNode.js'; | ||
|  | import { modelViewMatrix } from '../accessors/ModelNode.js'; | ||
|  | import { positionGeometry } from '../accessors/PositionNode.js'; | ||
|  | import { mix, smoothstep } from '../math/MathNode.js'; | ||
|  | import { tslFn, float, vec2, vec3, vec4, If } from '../shadernode/ShaderNode.js'; | ||
|  | import { uv } from '../accessors/UVNode.js'; | ||
|  | import { viewport } from '../display/ViewportNode.js'; | ||
|  | import { dashSize, gapSize } from '../core/PropertyNode.js'; | ||
|  | 
 | ||
|  | import { LineDashedMaterial } from 'three'; | ||
|  | 
 | ||
|  | const defaultValues = new LineDashedMaterial(); | ||
|  | 
 | ||
|  | class Line2NodeMaterial extends NodeMaterial { | ||
|  | 
 | ||
|  | 	constructor( params = {} ) { | ||
|  | 
 | ||
|  | 		super(); | ||
|  | 
 | ||
|  | 		this.normals = false; | ||
|  | 		this.lights = false; | ||
|  | 
 | ||
|  | 		this.setDefaultValues( defaultValues ); | ||
|  | 
 | ||
|  | 		this.useAlphaToCoverage = true; | ||
|  | 		this.useColor = params.vertexColors; | ||
|  | 		this.useDash = params.dashed; | ||
|  | 		this.useWorldUnits = false; | ||
|  | 
 | ||
|  | 		this.dashOffset = 0; | ||
|  | 		this.lineWidth = 1; | ||
|  | 
 | ||
|  | 		this.lineColorNode = null; | ||
|  | 
 | ||
|  | 		this.offsetNode = null; | ||
|  | 		this.dashScaleNode = null; | ||
|  | 		this.dashSizeNode = null; | ||
|  | 		this.gapSizeNode = null; | ||
|  | 
 | ||
|  | 		this.setValues( params ); | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	setup( builder ) { | ||
|  | 
 | ||
|  | 		this.setupShaders(); | ||
|  | 
 | ||
|  | 		super.setup( builder ); | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	setupShaders() { | ||
|  | 
 | ||
|  | 		const useAlphaToCoverage = this.alphaToCoverage; | ||
|  | 		const useColor = this.useColor; | ||
|  | 		const useDash = this.dashed; | ||
|  | 		const useWorldUnits = this.worldUnits; | ||
|  | 
 | ||
|  | 		const trimSegment = tslFn( ( { 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 ); | ||
|  | 
 | ||
|  | 		} ); | ||
|  | 
 | ||
|  | 		this.vertexNode = tslFn( () => { | ||
|  | 
 | ||
|  | 			varyingProperty( 'vec2', 'vUv' ).assign( uv() ); | ||
|  | 
 | ||
|  | 			const instanceStart = attribute( 'instanceStart' ); | ||
|  | 			const instanceEnd = attribute( 'instanceEnd' ); | ||
|  | 
 | ||
|  | 			// camera space
 | ||
|  | 
 | ||
|  | 			const start = property( 'vec4', 'start' ); | ||
|  | 			const end = property( 'vec4', 'end' ); | ||
|  | 
 | ||
|  | 			start.assign( modelViewMatrix.mul( vec4( instanceStart, 1.0 ) ) ); // force assignment into correct place in flow
 | ||
|  | 			end.assign( modelViewMatrix.mul( vec4( instanceEnd, 1.0 ) ) ); | ||
|  | 
 | ||
|  | 			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 ).temp(); | ||
|  | 
 | ||
|  | 			// account for clip-space aspect ratio
 | ||
|  | 			dir.x.assign( dir.x.mul( aspect ) ); | ||
|  | 			dir.assign( dir.normalize() ); | ||
|  | 
 | ||
|  | 			const clip = temp( vec4() ); | ||
|  | 
 | ||
|  | 			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 ).cond( start, end) ); | ||
|  | 
 | ||
|  | 				// height offset
 | ||
|  | 				const hw = materialLineWidth.mul( 0.5 ); | ||
|  | 				worldPos.addAssign( vec4( positionGeometry.x.lessThan( 0.0 ).cond( 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 ).cond( 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 = temp( vec3() ); | ||
|  | 
 | ||
|  | 				clipPose.assign( positionGeometry.y.lessThan( 0.5 ).cond( ndcStart, ndcEnd ) ); | ||
|  | 				clip.z.assign( clipPose.z.mul( clip.w ) ); | ||
|  | 
 | ||
|  | 			} else { | ||
|  | 
 | ||
|  | 				const offset = property( 'vec2', 'offset' ); | ||
|  | 
 | ||
|  | 				offset.assign( vec2( dir.y, dir.x.negate() ) ); | ||
|  | 
 | ||
|  | 				// 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 ).cond( 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 ).cond( clipStart, clipEnd ) ); | ||
|  | 
 | ||
|  | 				// back to clip space
 | ||
|  | 				offset.assign( offset.mul( clip.w ) ); | ||
|  | 
 | ||
|  | 				clip.assign( clip.add( vec4( offset, 0, 0 ) ) ); | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 			return clip; | ||
|  | 
 | ||
|  | 		} )(); | ||
|  | 
 | ||
|  | 		const closestLineToLine = tslFn( ( { 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.fragmentNode = tslFn( () => { | ||
|  | 
 | ||
|  | 			const vUv = varyingProperty( 'vec2', 'vUv' ); | ||
|  | 
 | ||
|  | 			if ( useDash ) { | ||
|  | 
 | ||
|  | 				const offsetNode = this.offsetNode ? float( this.offsetNodeNode ) : materialLineDashOffset; | ||
|  | 				const dashScaleNode = this.dashScaleNode ? float( this.dashScaleNode ) : materialLineScale; | ||
|  | 				const dashSizeNode = this.dashSizeNode ? float( this.dashSizeNode ) : materialLineDashSize; | ||
|  | 				const gapSizeNode = this.dashSizeNode ? float( this.dashGapNode ) : materialLineGapSize; | ||
|  | 
 | ||
|  | 				dashSize.assign( dashSizeNode ); | ||
|  | 				gapSize.assign( gapSizeNode ); | ||
|  | 
 | ||
|  | 				const instanceDistanceStart = attribute( 'instanceDistanceStart' ); | ||
|  | 				const instanceDistanceEnd = attribute( 'instanceDistanceEnd' ); | ||
|  | 
 | ||
|  | 				const lineDistance = positionGeometry.y.lessThan( 0.5 ).cond( dashScaleNode.mul( instanceDistanceStart ), materialLineScale.mul( instanceDistanceEnd ) ); | ||
|  | 
 | ||
|  | 				const vLineDistance = varying( lineDistance.add( materialLineDashOffset ) ); | ||
|  | 				const vLineDistanceOffset = offsetNode ? vLineDistance.add( offsetNode ) : vLineDistance; | ||
|  | 
 | ||
|  | 				vUv.y.lessThan( - 1.0 ).or( vUv.y.greaterThan( 1.0 ) ).discard(); // discard endcaps
 | ||
|  | 				vLineDistanceOffset.mod( dashSize.add( gapSize ) ).greaterThan( dashSize ).discard(); // todo - FIX
 | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 			 // force assignment into correct place in flow
 | ||
|  | 			const alpha = property( 'float', 'alpha' ); | ||
|  | 			alpha.assign( 1 ); | ||
|  | 
 | ||
|  | 			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 ) { | ||
|  | 
 | ||
|  | 						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 ) { | ||
|  | 
 | ||
|  | 					const a = vUv.x; | ||
|  | 					const b = vUv.y.greaterThan( 0.0 ).cond( vUv.y.sub( 1.0 ), vUv.y.add( 1.0 ) ); | ||
|  | 
 | ||
|  | 					const len2 = a.mul( a ).add( b.mul( b ) ); | ||
|  | 
 | ||
|  | 					// force assignment out of following 'if' statement - to avoid uniform control flow errors
 | ||
|  | 					const dlen = property( 'float', 'dlen' ); | ||
|  | 					dlen.assign( len2.fwidth() ); | ||
|  | 
 | ||
|  | 					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 ).cond( 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 ).cond( instanceColorStart, instanceColorEnd ); | ||
|  | 
 | ||
|  | 					lineColorNode = instanceColor.mul( materialColor ); | ||
|  | 
 | ||
|  | 				} else { | ||
|  | 
 | ||
|  | 					lineColorNode = materialColor; | ||
|  | 
 | ||
|  | 				} | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 			return vec4( lineColorNode, alpha ); | ||
|  | 
 | ||
|  | 		} )(); | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 
 | ||
|  | 	get worldUnits() { | ||
|  | 
 | ||
|  | 		return this.useWorldUnits; | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	set worldUnits( value ) { | ||
|  | 
 | ||
|  | 		if ( this.useWorldUnits !== value ) { | ||
|  | 
 | ||
|  | 			this.useWorldUnits = value; | ||
|  | 			this.needsUpdate = true; | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 
 | ||
|  | 	get dashed() { | ||
|  | 
 | ||
|  | 		return this.useDash; | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	set dashed( value ) { | ||
|  | 
 | ||
|  | 		if ( this.useDash !== value ) { | ||
|  | 
 | ||
|  | 			this.useDash = value; | ||
|  | 			this.needsUpdate = true; | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 
 | ||
|  | 	get alphaToCoverage() { | ||
|  | 
 | ||
|  | 		return this.useAlphaToCoverage; | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	set alphaToCoverage( value ) { | ||
|  | 
 | ||
|  | 		if ( this.useAlphaToCoverage !== value ) { | ||
|  | 
 | ||
|  | 			this.useAlphaToCoverage = value; | ||
|  | 			this.needsUpdate = true; | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | } | ||
|  | 
 | ||
|  | export default Line2NodeMaterial; | ||
|  | 
 | ||
|  | addNodeMaterial( 'Line2NodeMaterial', Line2NodeMaterial ); |