Line2NodeMaterial.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. import NodeMaterial, { addNodeMaterial } from './NodeMaterial.js';
  2. import { temp } from '../core/VarNode.js';
  3. import { varying } from '../core/VaryingNode.js';
  4. import { property } from '../core/PropertyNode.js';
  5. import { attribute } from '../core/AttributeNode.js';
  6. import { cameraProjectionMatrix } from '../accessors/CameraNode.js';
  7. import { materialColor } from '../accessors/MaterialNode.js';
  8. import { modelViewMatrix } from '../accessors/ModelNode.js';
  9. import { positionGeometry } from '../accessors/PositionNode.js';
  10. import { abs, mix, mod, dot, clamp, smoothstep } from '../math/MathNode.js';
  11. import { tslFn, ShaderNode, float, vec2, vec3, vec4 } from '../shadernode/ShaderNode.js';
  12. import { uv } from '../accessors/UVNode.js';
  13. import { materialLineScale, materialLineDashSize, materialLineGapSize, materialLineDashOffset, materialLineWidth } from '../accessors/LineMaterialNode.js';
  14. import { viewport } from '../display/ViewportNode.js';
  15. import { dashSize, gapSize } from '../core/PropertyNode.js';
  16. import { LineDashedMaterial } from 'three';
  17. const defaultValues = new LineDashedMaterial();
  18. class Line2NodeMaterial extends NodeMaterial {
  19. constructor( params = {} ) {
  20. super();
  21. this.normals = false;
  22. this.lights = false;
  23. this.setDefaultValues( defaultValues );
  24. this.useAlphaToCoverage = true;
  25. this.useColor = params.vertexColors;
  26. this.useDash = params.dashed;
  27. this.useWorldUnits = false;
  28. this.dashOffset = 0;
  29. this.lineWidth = 1;
  30. this.lineColorNode = null;
  31. this.offsetNode = null;
  32. this.dashScaleNode = null;
  33. this.dashSizeNode = null;
  34. this.gapSizeNode = null;
  35. this.setupShaders();
  36. this.setValues( params );
  37. }
  38. setupShaders() {
  39. const useAlphaToCoverage = this.alphaToCoverage;
  40. const useColor = this.useColor;
  41. const useDash = this.dashed;
  42. const useWorldUnits = this.worldUnits;
  43. const trimSegment = tslFn( ( { start, end } ) => {
  44. const a = cameraProjectionMatrix.element( 2 ).element( 2 ); // 3nd entry in 3th column
  45. const b = cameraProjectionMatrix.element( 3 ).element( 2 ); // 3nd entry in 4th column
  46. const nearEstimate = b.mul( -0.5 ).div( a );
  47. const alpha = nearEstimate.sub( start.z ).div( end.z.sub( start.z ) );
  48. return vec4( mix( start.xyz, end.xyz, alpha ), end.w );
  49. } );
  50. this.vertexNode = new ShaderNode( ( stack ) => {
  51. stack.assign( varying( vec2(), 'vUv' ), uv() );
  52. const instanceStart = attribute( 'instanceStart' );
  53. const instanceEnd = attribute( 'instanceEnd' );
  54. // camera space
  55. const start = property( 'vec4', 'start' );
  56. const end = property( 'vec4', 'end' );
  57. stack.assign( start, modelViewMatrix.mul( vec4( instanceStart, 1.0 ) ) ); // force assignment into correct place in flow
  58. stack.assign( end, modelViewMatrix.mul( vec4( instanceEnd, 1.0 ) ) );
  59. if ( useWorldUnits ) {
  60. stack.assign( varying( vec3(), 'worldStart' ), start.xyz );
  61. stack.assign( varying( vec3(), 'worldEnd' ), end.xyz );
  62. }
  63. const aspect = viewport.z.div( viewport.w );
  64. // special case for perspective projection, and segments that terminate either in, or behind, the camera plane
  65. // clearly the gpu firmware has a way of addressing this issue when projecting into ndc space
  66. // but we need to perform ndc-space calculations in the shader, so we must address this issue directly
  67. // perhaps there is a more elegant solution -- WestLangley
  68. const perspective = cameraProjectionMatrix.element( 2 ).element( 3 ).equal( -1.0 ); // 4th entry in the 3rd column
  69. stack.if( perspective, ( stack ) => {
  70. stack.if( start.z.lessThan( 0.0 ).and( end.z.greaterThan( 0.0 ) ), ( stack ) => {
  71. stack.assign( end, trimSegment( { start: start, end: end } ) );
  72. } ).elseif( end.z.lessThan( 0.0 ).and( start.z.greaterThanEqual( 0.0 ) ), ( stack ) => {
  73. stack.assign( start, trimSegment( { start: end, end: start } ) );
  74. } );
  75. } );
  76. // clip space
  77. const clipStart = cameraProjectionMatrix.mul( start );
  78. const clipEnd = cameraProjectionMatrix.mul( end );
  79. // ndc space
  80. const ndcStart = clipStart.xyz.div( clipStart.w );
  81. const ndcEnd = clipEnd.xyz.div( clipEnd.w );
  82. // direction
  83. const dir = ndcEnd.xy.sub( ndcStart.xy );
  84. // account for clip-space aspect ratio
  85. stack.assign( dir.x, dir.x.mul( aspect ) );
  86. stack.assign( dir, dir.normalize() );
  87. const clip = temp( vec4() );
  88. if ( useWorldUnits ) {
  89. // get the offset direction as perpendicular to the view vector
  90. const worldDir = end.xyz.sub( start.xyz ).normalize();
  91. const offset = positionGeometry.y.lessThan( 0.5 ).cond(
  92. start.xyz.cross( worldDir ).normalize(),
  93. end.xyz.cross( worldDir ).normalize()
  94. );
  95. // sign flip
  96. stack.assign( offset, positionGeometry.x.lessThan( 0.0 ).cond( offset.negate(), offset ) );
  97. const forwardOffset = worldDir.dot( vec3( 0.0, 0.0, 1.0 ) );
  98. // don't extend the line if we're rendering dashes because we
  99. // won't be rendering the endcaps
  100. if ( ! useDash ) {
  101. // extend the line bounds to encompass endcaps
  102. stack.assign( start, start.sub( vec4( worldDir.mul( materialLineWidth ).mul( 0.5 ), 0 ) ) );
  103. stack.assign( end, end.add( vec4( worldDir.mul( materialLineWidth ).mul( 0.5 ), 0 ) ) );
  104. // shift the position of the quad so it hugs the forward edge of the line
  105. stack.assign( offset, offset.sub( vec3( dir.mul( forwardOffset ), 0 ) ) );
  106. stack.assign( offset.z, offset.z.add( 0.5 ) );
  107. }
  108. // endcaps
  109. stack.if( positionGeometry.y.greaterThan( 1.0 ).or( positionGeometry.y.lessThan( 0.0 ) ), ( stack ) => {
  110. stack.assign( offset, offset.add( vec3( dir.mul( 2.0 ).mul( forwardOffset ), 0 ) ) );
  111. } );
  112. // adjust for linewidth
  113. stack.assign( offset, offset.mul( materialLineWidth ).mul( 0.5 ) );
  114. // set the world position
  115. const worldPos = varying( vec4(), 'worldPos' );
  116. stack.assign( worldPos, positionGeometry.y.lessThan( 0.5 ).cond( start, end ) );
  117. stack.assign( worldPos, worldPos.add( vec4( offset, 0 ) ) );
  118. // project the worldpos
  119. stack.assign( clip, cameraProjectionMatrix.mul( worldPos ) );
  120. // shift the depth of the projected points so the line
  121. // segments overlap neatly
  122. const clipPose = temp( vec3() );
  123. stack.assign( clipPose, positionGeometry.y.lessThan( 0.5 ).cond( ndcStart, ndcEnd ) );
  124. stack.assign( clip.z, clipPose.z.mul( clip.w ) );
  125. } else {
  126. const offset = property( 'vec2', 'offset' );
  127. stack.assign( offset, vec2( dir.y, dir.x.negate() ) );
  128. // undo aspect ratio adjustment
  129. stack.assign( dir.x, dir.x.div( aspect ) );
  130. stack.assign( offset.x, offset.x.div( aspect ) );
  131. // sign flip
  132. stack.assign( offset, positionGeometry.x.lessThan( 0.0 ).cond( offset.negate(), offset ) );
  133. // endcaps
  134. stack.if( positionGeometry.y.lessThan( 0.0 ), ( stack ) => {
  135. stack.assign( offset, offset.sub( dir ) );
  136. } ).elseif( positionGeometry.y.greaterThan( 1.0 ), ( stack ) => {
  137. stack.assign( offset, offset.add( dir ) );
  138. } );
  139. // adjust for linewidth
  140. stack.assign( offset, offset.mul( materialLineWidth ) );
  141. // adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ...
  142. stack.assign( offset, offset.div( viewport.w ) );
  143. // select end
  144. stack.assign( clip, positionGeometry.y.lessThan( 0.5 ).cond( clipStart, clipEnd ) );
  145. // back to clip space
  146. stack.assign( offset, offset.mul( clip.w ) );
  147. stack.assign( clip, clip.add( vec4( offset, 0, 0 ) ) );
  148. }
  149. return clip;
  150. } );
  151. const closestLineToLine = tslFn( ( { p1, p2, p3, p4 } ) => {
  152. const p13 = p1.sub( p3 );
  153. const p43 = p4.sub( p3 );
  154. const p21 = p2.sub( p1 );
  155. const d1343 = dot( p13, p43 );
  156. const d4321 = dot( p43, p21 );
  157. const d1321 = dot( p13, p21 );
  158. const d4343 = dot( p43, p43 );
  159. const d2121 = dot( p21, p21 );
  160. const denom = d2121.mul( d4343 ).sub( d4321.mul( d4321 ) );
  161. const numer = d1343.mul( d4321 ).sub( d1321.mul( d4343 ) );
  162. const mua = clamp( numer.div( denom ), 0, 1 );
  163. const mub = clamp( d1343.add( d4321.mul( mua ) ).div( d4343 ), 0, 1 );
  164. return vec2( mua, mub );
  165. } );
  166. this.colorNode = new ShaderNode( ( stack ) => {
  167. const vUv = varying( vec2(), 'vUv' );
  168. if ( useDash ) {
  169. const offsetNode = this.offsetNode ? float( this.offsetNodeNode ) : materialLineDashOffset;
  170. const dashScaleNode = this.dashScaleNode ? float( this.dashScaleNode ) : materialLineScale;
  171. const dashSizeNode = this.dashSizeNode ? float( this.dashSizeNode ) : materialLineDashSize;
  172. const gapSizeNode = this.dashSizeNode ? float( this.dashGapNode ) : materialLineGapSize;
  173. stack.assign( dashSize, dashSizeNode );
  174. stack.assign( gapSize, gapSizeNode );
  175. const instanceDistanceStart = attribute( 'instanceDistanceStart' );
  176. const instanceDistanceEnd = attribute( 'instanceDistanceEnd' );
  177. const lineDistance = positionGeometry.y.lessThan( 0.5 ).cond( dashScaleNode.mul( instanceDistanceStart ), materialLineScale.mul( instanceDistanceEnd ) );
  178. const vLineDistance = varying( lineDistance.add( materialLineDashOffset ) );
  179. const vLineDistanceOffset = offsetNode ? vLineDistance.add( offsetNode ) : vLineDistance;
  180. stack.add( vUv.y.lessThan( - 1.0 ).or( vUv.y.greaterThan( 1.0 ) ).discard() ); // discard endcaps
  181. stack.add( mod( vLineDistanceOffset, dashSize.add( gapSize ) ).greaterThan( dashSize ).discard() ); // todo - FIX
  182. }
  183. // force assignment into correct place in flow
  184. const alpha = property( 'float', 'alpha' );
  185. stack.assign( alpha, 1 );
  186. if ( useWorldUnits ) {
  187. let worldStart = varying( vec3(), 'worldStart' );
  188. let worldEnd = varying( vec3(), 'worldEnd' );
  189. // Find the closest points on the view ray and the line segment
  190. const rayEnd = varying( vec4(), 'worldPos' ).xyz.normalize().mul( 1e5 );
  191. const lineDir = worldEnd.sub( worldStart );
  192. const params = closestLineToLine( { p1: worldStart, p2: worldEnd, p3: vec3( 0.0, 0.0, 0.0 ), p4: rayEnd } );
  193. const p1 = worldStart.add( lineDir.mul( params.x ) );
  194. const p2 = rayEnd.mul( params.y );
  195. const delta = p1.sub( p2 );
  196. const len = delta.length();
  197. const norm = len.div( materialLineWidth );
  198. if ( ! useDash ) {
  199. if ( useAlphaToCoverage ) {
  200. const dnorm = norm.fwidth();
  201. stack.assign( alpha, smoothstep( dnorm.negate().add( 0.5 ), dnorm.add( 0.5 ), norm ).oneMinus() );
  202. } else {
  203. stack.add( norm.greaterThan( 0.5 ).discard() );
  204. }
  205. }
  206. } else {
  207. // round endcaps
  208. if ( useAlphaToCoverage ) {
  209. const a = vUv.x;
  210. const b = vUv.y.greaterThan( 0.0 ).cond( vUv.y.sub( 1.0 ), vUv.y.add( 1.0 ) );
  211. const len2 = a.mul( a ).add( b.mul( b ) );
  212. // force assignment out of following 'if' statement - to avoid uniform control flow errors
  213. const dlen = property( 'float', 'dlen' );
  214. stack.assign( dlen, len2.fwidth() );
  215. stack.if( abs( vUv.y ).greaterThan( 1.0 ), ( stack ) => {
  216. stack.assign( alpha, smoothstep( dlen.oneMinus(), dlen.add( 1 ), len2 ).oneMinus() );
  217. } );
  218. } else {
  219. stack.if( abs( vUv.y ).greaterThan( 1.0 ), ( stack ) => {
  220. const a = vUv.x;
  221. const b = vUv.y.greaterThan( 0.0 ).cond( vUv.y.sub( 1.0 ), vUv.y.add( 1.0 ) );
  222. const len2 = a.mul( a ).add( b.mul( b ) );
  223. stack.add( len2.greaterThan( 1.0 ).discard() );
  224. } );
  225. }
  226. }
  227. let lineColorNode;
  228. if ( this.lineColorNode ) {
  229. lineColorNode = this.lineColorNode;
  230. } else {
  231. if ( useColor ) {
  232. const instanceColorStart = attribute( 'instanceColorStart' );
  233. const instanceColorEnd = attribute( 'instanceColorEnd' );
  234. lineColorNode = varying( positionGeometry.y.lessThan( 0.5 ).cond( instanceColorStart, instanceColorEnd ) );
  235. } else {
  236. lineColorNode = materialColor;
  237. }
  238. }
  239. return vec4( lineColorNode, alpha );
  240. } );
  241. this.needsUpdate = true;
  242. }
  243. get worldUnits() {
  244. return this.useWorldUnits;
  245. }
  246. set worldUnits( value ) {
  247. if ( this.useWorldUnits !== value ) {
  248. this.useWorldUnits = value;
  249. this.setupShaders();
  250. }
  251. }
  252. get dashed() {
  253. return this.useDash;
  254. }
  255. set dashed( value ) {
  256. if ( this.useDash !== value ) {
  257. this.useDash = value;
  258. this.setupShaders();
  259. }
  260. }
  261. get alphaToCoverage() {
  262. return this.useAlphaToCoverage;
  263. }
  264. set alphaToCoverage( value ) {
  265. if ( this.useAlphaToCoverage !== value ) {
  266. this.useAlphaToCoverage = value;
  267. this.setupShaders();
  268. }
  269. }
  270. }
  271. export default Line2NodeMaterial;
  272. addNodeMaterial( 'Line2NodeMaterial', Line2NodeMaterial );