Line2NodeMaterial.js 12 KB

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