Line2NodeMaterial.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  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 tmpFwd = mix( start.xyz, end.xyz, 0.5 ).normalize();
  91. const worldUp = worldDir.cross( tmpFwd ).normalize();
  92. const worldFwd = worldDir.cross( worldUp );
  93. const worldPos = varyingProperty( 'vec4', 'worldPos' );
  94. worldPos.assign( positionGeometry.y.lessThan( 0.5 ).cond( start, end) );
  95. // height offset
  96. const hw = materialLineWidth.mul( 0.5 );
  97. worldPos.addAssign( vec4( positionGeometry.x.lessThan( 0.0 ).cond( worldUp.mul( hw ), worldUp.mul( hw ).negate() ), 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. // cap extension
  102. worldPos.addAssign( vec4( positionGeometry.y.lessThan( 0.5 ).cond( worldDir.mul( hw ).negate(), worldDir.mul( hw ) ), 0 ) );
  103. // add width to the box
  104. worldPos.addAssign( vec4( worldFwd.mul( hw ), 0 ) );
  105. // endcaps
  106. If( positionGeometry.y.greaterThan( 1.0 ).or( positionGeometry.y.lessThan( 0.0 ) ), () => {
  107. worldPos.subAssign( vec4( worldFwd.mul( 2.0 ).mul( hw ), 0 ) );
  108. } );
  109. }
  110. // project the worldpos
  111. clip.assign( cameraProjectionMatrix.mul( worldPos ) );
  112. // shift the depth of the projected points so the line
  113. // segments overlap neatly
  114. const clipPose = temp( vec3() );
  115. clipPose.assign( positionGeometry.y.lessThan( 0.5 ).cond( ndcStart, ndcEnd ) );
  116. clip.z.assign( clipPose.z.mul( clip.w ) );
  117. } else {
  118. const offset = property( 'vec2', 'offset' );
  119. offset.assign( vec2( dir.y, dir.x.negate() ) );
  120. // undo aspect ratio adjustment
  121. dir.x.assign( dir.x.div( aspect ) );
  122. offset.x.assign( offset.x.div( aspect ) );
  123. // sign flip
  124. offset.assign( positionGeometry.x.lessThan( 0.0 ).cond( offset.negate(), offset ) );
  125. // endcaps
  126. If( positionGeometry.y.lessThan( 0.0 ), () => {
  127. offset.assign( offset.sub( dir ) );
  128. } ).elseif( positionGeometry.y.greaterThan( 1.0 ), () => {
  129. offset.assign( offset.add( dir ) );
  130. } );
  131. // adjust for linewidth
  132. offset.assign( offset.mul( materialLineWidth ) );
  133. // adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ...
  134. offset.assign( offset.div( viewport.w ) );
  135. // select end
  136. clip.assign( positionGeometry.y.lessThan( 0.5 ).cond( clipStart, clipEnd ) );
  137. // back to clip space
  138. offset.assign( offset.mul( clip.w ) );
  139. clip.assign( clip.add( vec4( offset, 0, 0 ) ) );
  140. }
  141. return clip;
  142. } )();
  143. const closestLineToLine = tslFn( ( { p1, p2, p3, p4 } ) => {
  144. const p13 = p1.sub( p3 );
  145. const p43 = p4.sub( p3 );
  146. const p21 = p2.sub( p1 );
  147. const d1343 = p13.dot( p43 );
  148. const d4321 = p43.dot( p21 );
  149. const d1321 = p13.dot( p21 );
  150. const d4343 = p43.dot( p43 );
  151. const d2121 = p21.dot( p21 );
  152. const denom = d2121.mul( d4343 ).sub( d4321.mul( d4321 ) );
  153. const numer = d1343.mul( d4321 ).sub( d1321.mul( d4343 ) );
  154. const mua = numer.div( denom ).clamp();
  155. const mub = d1343.add( d4321.mul( mua ) ).div( d4343 ).clamp();
  156. return vec2( mua, mub );
  157. } );
  158. this.fragmentNode = tslFn( () => {
  159. const vUv = varyingProperty( 'vec2', 'vUv' );
  160. if ( useDash ) {
  161. const offsetNode = this.offsetNode ? float( this.offsetNodeNode ) : materialLineDashOffset;
  162. const dashScaleNode = this.dashScaleNode ? float( this.dashScaleNode ) : materialLineScale;
  163. const dashSizeNode = this.dashSizeNode ? float( this.dashSizeNode ) : materialLineDashSize;
  164. const gapSizeNode = this.dashSizeNode ? float( this.dashGapNode ) : materialLineGapSize;
  165. dashSize.assign( dashSizeNode );
  166. gapSize.assign( gapSizeNode );
  167. const instanceDistanceStart = attribute( 'instanceDistanceStart' );
  168. const instanceDistanceEnd = attribute( 'instanceDistanceEnd' );
  169. const lineDistance = positionGeometry.y.lessThan( 0.5 ).cond( dashScaleNode.mul( instanceDistanceStart ), materialLineScale.mul( instanceDistanceEnd ) );
  170. const vLineDistance = varying( lineDistance.add( materialLineDashOffset ) );
  171. const vLineDistanceOffset = offsetNode ? vLineDistance.add( offsetNode ) : vLineDistance;
  172. vUv.y.lessThan( - 1.0 ).or( vUv.y.greaterThan( 1.0 ) ).discard(); // discard endcaps
  173. vLineDistanceOffset.mod( dashSize.add( gapSize ) ).greaterThan( dashSize ).discard(); // todo - FIX
  174. }
  175. // force assignment into correct place in flow
  176. const alpha = property( 'float', 'alpha' );
  177. alpha.assign( 1 );
  178. if ( useWorldUnits ) {
  179. const worldStart = varyingProperty( 'vec3', 'worldStart' );
  180. const worldEnd = varyingProperty( 'vec3', 'worldEnd' );
  181. // Find the closest points on the view ray and the line segment
  182. const rayEnd = varyingProperty( 'vec4', 'worldPos' ).xyz.normalize().mul( 1e5 );
  183. const lineDir = worldEnd.sub( worldStart );
  184. const params = closestLineToLine( { p1: worldStart, p2: worldEnd, p3: vec3( 0.0, 0.0, 0.0 ), p4: rayEnd } );
  185. const p1 = worldStart.add( lineDir.mul( params.x ) );
  186. const p2 = rayEnd.mul( params.y );
  187. const delta = p1.sub( p2 );
  188. const len = delta.length();
  189. const norm = len.div( materialLineWidth );
  190. if ( ! useDash ) {
  191. if ( useAlphaToCoverage ) {
  192. const dnorm = norm.fwidth();
  193. alpha.assign( smoothstep( dnorm.negate().add( 0.5 ), dnorm.add( 0.5 ), norm ).oneMinus() );
  194. } else {
  195. norm.greaterThan( 0.5 ).discard();
  196. }
  197. }
  198. } else {
  199. // round endcaps
  200. if ( useAlphaToCoverage ) {
  201. const a = vUv.x;
  202. const b = vUv.y.greaterThan( 0.0 ).cond( vUv.y.sub( 1.0 ), vUv.y.add( 1.0 ) );
  203. const len2 = a.mul( a ).add( b.mul( b ) );
  204. // force assignment out of following 'if' statement - to avoid uniform control flow errors
  205. const dlen = property( 'float', 'dlen' );
  206. dlen.assign( len2.fwidth() );
  207. If( vUv.y.abs().greaterThan( 1.0 ), () => {
  208. alpha.assign( smoothstep( dlen.oneMinus(), dlen.add( 1 ), len2 ).oneMinus() );
  209. } );
  210. } else {
  211. If( vUv.y.abs().greaterThan( 1.0 ), () => {
  212. const a = vUv.x;
  213. const b = vUv.y.greaterThan( 0.0 ).cond( vUv.y.sub( 1.0 ), vUv.y.add( 1.0 ) );
  214. const len2 = a.mul( a ).add( b.mul( b ) );
  215. len2.greaterThan( 1.0 ).discard();
  216. } );
  217. }
  218. }
  219. let lineColorNode;
  220. if ( this.lineColorNode ) {
  221. lineColorNode = this.lineColorNode;
  222. } else {
  223. if ( useColor ) {
  224. const instanceColorStart = attribute( 'instanceColorStart' );
  225. const instanceColorEnd = attribute( 'instanceColorEnd' );
  226. const instanceColor = positionGeometry.y.lessThan( 0.5 ).cond( instanceColorStart, instanceColorEnd );
  227. lineColorNode = instanceColor.mul( materialColor );
  228. } else {
  229. lineColorNode = materialColor;
  230. }
  231. }
  232. return vec4( lineColorNode, alpha );
  233. } )();
  234. this.needsUpdate = true;
  235. }
  236. get worldUnits() {
  237. return this.useWorldUnits;
  238. }
  239. set worldUnits( value ) {
  240. if ( this.useWorldUnits !== value ) {
  241. this.useWorldUnits = value;
  242. this.setupShaders();
  243. }
  244. }
  245. get dashed() {
  246. return this.useDash;
  247. }
  248. set dashed( value ) {
  249. if ( this.useDash !== value ) {
  250. this.useDash = value;
  251. this.setupShaders();
  252. }
  253. }
  254. get alphaToCoverage() {
  255. return this.useAlphaToCoverage;
  256. }
  257. set alphaToCoverage( value ) {
  258. if ( this.useAlphaToCoverage !== value ) {
  259. this.useAlphaToCoverage = value;
  260. this.setupShaders();
  261. }
  262. }
  263. }
  264. export default Line2NodeMaterial;
  265. addNodeMaterial( 'Line2NodeMaterial', Line2NodeMaterial );