Line2NodeMaterial.js 12 KB

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