GodraysNode.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. import { Frustum, Matrix4, RenderTarget, Vector2, RendererUtils, QuadMesh, TempNode, NodeMaterial, NodeUpdateType, Vector3, Plane, WebGPUCoordinateSystem } from 'three/webgpu';
  2. import { cubeTexture, clamp, viewZToPerspectiveDepth, logarithmicDepthToViewZ, float, Loop, max, Fn, passTexture, uv, dot, uniformArray, If, getViewPosition, uniform, vec4, add, interleavedGradientNoise, screenCoordinate, round, mul, uint, mix, exp, vec3, distance, pow, reference, lightPosition, vec2, bool, texture, perspectiveDepthToViewZ, lightShadowMatrix } from 'three/tsl';
  3. const _quadMesh = /*@__PURE__*/ new QuadMesh();
  4. const _size = /*@__PURE__*/ new Vector2();
  5. const _DIRECTIONS = [
  6. new Vector3( 1, 0, 0 ),
  7. new Vector3( - 1, 0, 0 ),
  8. new Vector3( 0, 1, 0 ),
  9. new Vector3( 0, - 1, 0 ),
  10. new Vector3( 0, 0, 1 ),
  11. new Vector3( 0, 0, - 1 ),
  12. ];
  13. const _PLANES = _DIRECTIONS.map( () => new Plane() );
  14. const _SCRATCH_VECTOR = new Vector3();
  15. const _SCRATCH_MAT4 = new Matrix4();
  16. const _SCRATCH_FRUSTUM = new Frustum();
  17. let _rendererState;
  18. /**
  19. * Post-Processing node for apply Screen-space raymarched godrays to a scene.
  20. *
  21. * After the godrays have been computed, it's recommened to apply a Bilateral
  22. * Blur to the result to mitigate raymarching and noise artifacts.
  23. *
  24. * The composite with the scene pass is ideally done with `depthAwareBlend()`,
  25. * which mitigates aliasing and light leaking.
  26. *
  27. * ```js
  28. * const godraysPass = godrays( scenePassDepth, camera, light );
  29. *
  30. * const blurPass = bilateralBlur( godraysPassColor ); // optional blur
  31. *
  32. * const outputBlurred = depthAwareBlend( scenePassColor, blurPassColor, scenePassDepth, camera, { blendColor, edgeRadius, edgeStrength } ); // composite
  33. * ```
  34. *
  35. * Limitations:
  36. *
  37. * - Only point and directional lights are currently supported.
  38. * - The effect requires a full shadow setup. Meaning shadows must be enabled in the renderer,
  39. * 3D objects must cast and receive shadows and the main light must cast shadows.
  40. *
  41. * Reference: This Node is a part of [three-good-godrays](https://github.com/Ameobea/three-good-godrays).
  42. *
  43. * @augments TempNode
  44. * @three_import import { godrays } from 'three/addons/tsl/display/GodraysNode.js';
  45. */
  46. class GodraysNode extends TempNode {
  47. static get type() {
  48. return 'GodraysNode';
  49. }
  50. /**
  51. * Constructs a new Godrays node.
  52. *
  53. * @param {TextureNode} depthNode - A texture node that represents the scene's depth.
  54. * @param {Camera} camera - The camera the scene is rendered with.
  55. * @param {(DirectionalLight|PointLight)} light - The light the godrays are rendered for.
  56. */
  57. constructor( depthNode, camera, light ) {
  58. super( 'vec4' );
  59. /**
  60. * A node that represents the beauty pass's depth.
  61. *
  62. * @type {TextureNode}
  63. */
  64. this.depthNode = depthNode;
  65. /**
  66. * The number of raymarching steps
  67. *
  68. * @type {UniformNode<uint>}
  69. * @default 60
  70. */
  71. this.raymarchSteps = uniform( uint( 60 ) );
  72. /**
  73. * The rate of accumulation for the godrays. Higher values roughly equate to more humid air/denser fog.
  74. *
  75. * @type {UniformNode<float>}
  76. * @default 0.7
  77. */
  78. this.density = uniform( float( 0.7 ) );
  79. /**
  80. * The maximum density of the godrays. Limits the maximum brightness of the godrays.
  81. *
  82. * @type {UniformNode<float>}
  83. * @default 0.5
  84. */
  85. this.maxDensity = uniform( float( 0.5 ) );
  86. /**
  87. * Higher values decrease the accumulation of godrays the further away they are from the light source.
  88. *
  89. * @type {UniformNode<float>}
  90. * @default 2
  91. */
  92. this.distanceAttenuation = uniform( float( 2 ) );
  93. /**
  94. * The resolution scale.
  95. *
  96. * @type {number}
  97. */
  98. this.resolutionScale = 0.5;
  99. /**
  100. * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders
  101. * its effect once per frame in `updateBefore()`.
  102. *
  103. * @type {string}
  104. * @default 'frame'
  105. */
  106. this.updateBeforeType = NodeUpdateType.FRAME;
  107. // private uniforms
  108. /**
  109. * Represents the world matrix of the scene's camera.
  110. *
  111. * @private
  112. * @type {UniformNode<mat4>}
  113. */
  114. this._cameraMatrixWorld = uniform( camera.matrixWorld );
  115. /**
  116. * Represents the inverse projection matrix of the scene's camera.
  117. *
  118. * @private
  119. * @type {UniformNode<mat4>}
  120. */
  121. this._cameraProjectionMatrixInverse = uniform( camera.projectionMatrixInverse );
  122. /**
  123. * Represents the inverse projection matrix of the scene's camera.
  124. *
  125. * @private
  126. * @type {UniformNode<mat4>}
  127. */
  128. this._premultipliedLightCameraMatrix = uniform( new Matrix4() );
  129. /**
  130. * Represents the world position of the scene's camera.
  131. *
  132. * @private
  133. * @type {UniformNode<mat4>}
  134. */
  135. this._cameraPosition = uniform( new Vector3() );
  136. /**
  137. * Represents the near value of the scene's camera.
  138. *
  139. * @private
  140. * @type {ReferenceNode<float>}
  141. */
  142. this._cameraNear = reference( 'near', 'float', camera );
  143. /**
  144. * Represents the far value of the scene's camera.
  145. *
  146. * @private
  147. * @type {ReferenceNode<float>}
  148. */
  149. this._cameraFar = reference( 'far', 'float', camera );
  150. /**
  151. * The near value of the shadow camera.
  152. *
  153. * @private
  154. * @type {ReferenceNode<float>}
  155. */
  156. this._shadowCameraNear = reference( 'near', 'float', light.shadow.camera );
  157. /**
  158. * The far value of the shadow camera.
  159. *
  160. * @private
  161. * @type {ReferenceNode<float>}
  162. */
  163. this._shadowCameraFar = reference( 'far', 'float', light.shadow.camera );
  164. this._fNormals = uniformArray( _DIRECTIONS.map( () => new Vector3() ) );
  165. this._fConstants = uniformArray( _DIRECTIONS.map( () => 0 ) );
  166. /**
  167. * The light the godrays are rendered for.
  168. *
  169. * @private
  170. * @type {(DirectionalLight|PointLight)}
  171. */
  172. this._light = light;
  173. /**
  174. * The camera the scene is rendered with.
  175. *
  176. * @private
  177. * @type {Camera}
  178. */
  179. this._camera = camera;
  180. /**
  181. * The render target the godrays are rendered into.
  182. *
  183. * @private
  184. * @type {RenderTarget}
  185. */
  186. this._godraysRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false } );
  187. this._godraysRenderTarget.texture.name = 'Godrays';
  188. /**
  189. * The material that is used to render the effect.
  190. *
  191. * @private
  192. * @type {NodeMaterial}
  193. */
  194. this._material = new NodeMaterial();
  195. this._material.name = 'Godrays';
  196. /**
  197. * The result of the effect is represented as a separate texture node.
  198. *
  199. * @private
  200. * @type {PassTextureNode}
  201. */
  202. this._textureNode = passTexture( this, this._godraysRenderTarget.texture );
  203. }
  204. /**
  205. * Returns the result of the effect as a texture node.
  206. *
  207. * @return {PassTextureNode} A texture node that represents the result of the effect.
  208. */
  209. getTextureNode() {
  210. return this._textureNode;
  211. }
  212. /**
  213. * Sets the size of the effect.
  214. *
  215. * @param {number} width - The width of the effect.
  216. * @param {number} height - The height of the effect.
  217. */
  218. setSize( width, height ) {
  219. width = Math.round( this.resolutionScale * width );
  220. height = Math.round( this.resolutionScale * height );
  221. this._godraysRenderTarget.setSize( width, height );
  222. }
  223. /**
  224. * This method is used to render the effect once per frame.
  225. *
  226. * @param {NodeFrame} frame - The current node frame.
  227. */
  228. updateBefore( frame ) {
  229. const { renderer } = frame;
  230. _rendererState = RendererUtils.resetRendererState( renderer, _rendererState );
  231. //
  232. const size = renderer.getDrawingBufferSize( _size );
  233. this.setSize( size.width, size.height );
  234. //
  235. _quadMesh.material = this._material;
  236. _quadMesh.name = 'Godrays';
  237. this._updateLightParams();
  238. this._cameraPosition.value.setFromMatrixPosition( this._camera.matrixWorld );
  239. // clear
  240. renderer.setClearColor( 0xffffff, 1 );
  241. // godrays
  242. renderer.setRenderTarget( this._godraysRenderTarget );
  243. _quadMesh.render( renderer );
  244. // restore
  245. RendererUtils.restoreRendererState( renderer, _rendererState );
  246. }
  247. _updateLightParams() {
  248. const light = this._light;
  249. const shadowCamera = light.shadow.camera;
  250. this._premultipliedLightCameraMatrix.value.multiplyMatrices( shadowCamera.projectionMatrix, shadowCamera.matrixWorldInverse );
  251. if ( light.isPointLight ) {
  252. for ( let i = 0; i < _DIRECTIONS.length; i ++ ) {
  253. const direction = _DIRECTIONS[ i ];
  254. const plane = _PLANES[ i ];
  255. _SCRATCH_VECTOR.copy( light.position );
  256. _SCRATCH_VECTOR.addScaledVector( direction, shadowCamera.far );
  257. plane.setFromNormalAndCoplanarPoint( direction, _SCRATCH_VECTOR );
  258. this._fNormals.array[ i ].copy( plane.normal );
  259. this._fConstants.array[ i ] = plane.constant;
  260. }
  261. } else if ( light.isDirectionalLight ) {
  262. _SCRATCH_MAT4.multiplyMatrices( shadowCamera.projectionMatrix, shadowCamera.matrixWorldInverse );
  263. _SCRATCH_FRUSTUM.setFromProjectionMatrix( _SCRATCH_MAT4 );
  264. for ( let i = 0; i < 6; i ++ ) {
  265. const plane = _SCRATCH_FRUSTUM.planes[ i ];
  266. this._fNormals.array[ i ].copy( plane.normal ).multiplyScalar( - 1 );
  267. this._fConstants.array[ i ] = plane.constant * - 1;
  268. }
  269. }
  270. }
  271. /**
  272. * This method is used to setup the effect's TSL code.
  273. *
  274. * @param {NodeBuilder} builder - The current node builder.
  275. * @return {PassTextureNode}
  276. */
  277. setup( builder ) {
  278. const { renderer } = builder;
  279. const uvNode = uv();
  280. const lightPos = lightPosition( this._light );
  281. const sampleDepth = ( uv ) => {
  282. const depth = this.depthNode.sample( uv ).r;
  283. if ( builder.renderer.logarithmicDepthBuffer === true ) {
  284. const viewZ = logarithmicDepthToViewZ( depth, this._cameraNear, this._cameraFar );
  285. return viewZToPerspectiveDepth( viewZ, this._cameraNear, this._cameraFar );
  286. }
  287. return depth;
  288. };
  289. const sdPlane = ( p, n, h ) => {
  290. return dot( p, n ).add( h );
  291. };
  292. const intersectRayPlane = ( rayOrigin, rayDirection, planeNormal, planeDistance ) => {
  293. const denom = dot( planeNormal, rayDirection );
  294. return sdPlane( rayOrigin, planeNormal, planeDistance ).div( denom ).negate();
  295. };
  296. const computeShadowCoord = ( worldPos ) => {
  297. const shadowPosition = lightShadowMatrix( this._light ).mul( worldPos );
  298. const shadowCoord = shadowPosition.xyz.div( shadowPosition.w );
  299. let coordZ = shadowCoord.z;
  300. if ( renderer.coordinateSystem === WebGPUCoordinateSystem ) {
  301. coordZ = coordZ.mul( 2 ).sub( 1 ); // WebGPU: Conversion [ 0, 1 ] to [ - 1, 1 ]
  302. }
  303. return vec3( shadowCoord.x, shadowCoord.y.oneMinus(), coordZ );
  304. };
  305. const inShadow = ( worldPos ) => {
  306. if ( this._light.isPointLight ) {
  307. const lightToPos = worldPos.sub( lightPos ).toConst();
  308. const shadowPositionAbs = lightToPos.abs().toConst();
  309. const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ).negate();
  310. const depth = viewZToPerspectiveDepth( viewZ, this._shadowCameraNear, this._shadowCameraFar );
  311. const result = cubeTexture( this._light.shadow.map.depthTexture, lightToPos ).compare( depth ).r;
  312. return vec2( result.oneMinus().add( 0.005 ), viewZ.negate() );
  313. } else if ( this._light.isDirectionalLight ) {
  314. const shadowCoord = computeShadowCoord( worldPos ).toConst();
  315. const frustumTest = shadowCoord.x.greaterThanEqual( 0 )
  316. .and( shadowCoord.x.lessThanEqual( 1 ) )
  317. .and( shadowCoord.y.greaterThanEqual( 0 ) )
  318. .and( shadowCoord.y.lessThanEqual( 1 ) )
  319. .and( shadowCoord.z.greaterThanEqual( 0 ) )
  320. .and( shadowCoord.z.lessThanEqual( 1 ) );
  321. const output = vec2( 1, 0 );
  322. If( frustumTest.equal( true ), () => {
  323. const result = texture( this._light.shadow.map.depthTexture, shadowCoord.xy ).compare( shadowCoord.z ).r;
  324. const viewZ = perspectiveDepthToViewZ( shadowCoord.z, this._shadowCameraNear, this._shadowCameraFar );
  325. output.assign( vec2( result.oneMinus(), viewZ.negate() ) );
  326. } );
  327. return output;
  328. } else {
  329. throw new Error( 'GodraysNode: Unsupported light type.' );
  330. }
  331. };
  332. const godrays = Fn( () => {
  333. const output = vec4( 0, 0, 0, 1 ).toVar();
  334. const isEarlyOut = bool( false );
  335. const depth = sampleDepth( uvNode ).toConst();
  336. const viewPosition = getViewPosition( uvNode, depth, this._cameraProjectionMatrixInverse ).toConst();
  337. const worldPosition = this._cameraMatrixWorld.mul( viewPosition );
  338. const inBoxDist = float( - 10000.0 ).toVar();
  339. Loop( 6, ( { i } ) => {
  340. inBoxDist.assign( max( inBoxDist, sdPlane( this._cameraPosition, this._fNormals.element( i ), this._fConstants.element( i ) ) ) );
  341. } );
  342. const startPosition = this._cameraPosition.toVar();
  343. If( inBoxDist.lessThan( 0 ), () => {
  344. // If the ray target is outside the shadow box, move it to the nearest
  345. // point on the box to avoid marching through unlit space
  346. Loop( 6, ( { i } ) => {
  347. If( sdPlane( worldPosition, this._fNormals.element( i ), this._fConstants.element( i ) ).greaterThan( 0 ), () => {
  348. const direction = worldPosition.sub( this._cameraPosition ).toConst();
  349. const t = intersectRayPlane( this._cameraPosition, direction, this._fNormals.element( i ), this._fConstants.element( i ) );
  350. worldPosition.assign( this._cameraPosition.add( t.mul( direction ) ) );
  351. } );
  352. } );
  353. } ).Else( () => {
  354. // Find the first point where the ray intersects the shadow box (startPos)
  355. const direction = worldPosition.sub( this._cameraPosition ).toConst();
  356. const minT = float( 10000 ).toVar();
  357. Loop( 6, ( { i } ) => {
  358. const t = intersectRayPlane( this._cameraPosition, direction, this._fNormals.element( i ), this._fConstants.element( i ) );
  359. If( t.lessThan( minT ).and( t.greaterThan( 0 ) ), () => {
  360. minT.assign( t );
  361. } );
  362. } );
  363. If( minT.equal( 10000 ), () => {
  364. isEarlyOut.assign( true );
  365. } ).Else( () => {
  366. startPosition.assign( this._cameraPosition.add( minT.add( 0.001 ).mul( direction ) ) );
  367. // If the ray target is outside the shadow box, move it to the nearest
  368. // point on the box to avoid marching through unlit space
  369. const endInBoxDist = float( - 10000 ).toVar();
  370. Loop( 6, ( { i } ) => {
  371. endInBoxDist.assign( max( endInBoxDist, sdPlane( worldPosition, this._fNormals.element( i ), this._fConstants.element( i ) ) ) );
  372. } );
  373. If( endInBoxDist.greaterThanEqual( 0 ), () => {
  374. const minT = float( 10000 ).toVar();
  375. Loop( 6, ( { i } ) => {
  376. If( sdPlane( worldPosition, this._fNormals.element( i ), this._fConstants.element( i ) ).greaterThan( 0 ), () => {
  377. const t = intersectRayPlane( startPosition, direction, this._fNormals.element( i ), this._fConstants.element( i ) );
  378. If( t.lessThan( minT ).and( t.greaterThan( 0 ) ), () => {
  379. minT.assign( t );
  380. } );
  381. } );
  382. } );
  383. If( minT.lessThan( worldPosition.distance( startPosition ) ), () => {
  384. worldPosition.assign( startPosition.add( minT.mul( direction ) ) );
  385. } );
  386. } );
  387. } );
  388. } );
  389. If( isEarlyOut.equal( false ), () => {
  390. const illum = float( 0 ).toVar();
  391. const noise = interleavedGradientNoise( screenCoordinate ).toConst();
  392. const samplesFloat = round( add( this.raymarchSteps, mul( this.raymarchSteps.div( 8 ).add( 2 ), noise ) ) ).toConst();
  393. const samples = uint( samplesFloat ).toConst();
  394. Loop( samples, ( { i } ) => {
  395. const samplePos = mix( startPosition, worldPosition, float( i ).div( samplesFloat ) ).toConst();
  396. const shadowInfo = inShadow( samplePos );
  397. const shadowAmount = shadowInfo.x.oneMinus().toConst();
  398. illum.addAssign( shadowAmount.mul( distance( startPosition, worldPosition ).mul( this.density.div( 100 ) ) ).mul( pow( shadowInfo.y.div( this._shadowCameraFar ).oneMinus(), this.distanceAttenuation ) ) );
  399. } );
  400. illum.divAssign( samplesFloat );
  401. output.assign( vec4( vec3( clamp( exp( illum.negate() ).oneMinus(), 0, this.maxDensity ) ), depth ) );
  402. } );
  403. return output;
  404. } );
  405. this._material.fragmentNode = godrays().context( builder.getSharedContext() );
  406. this._material.needsUpdate = true;
  407. return this._textureNode;
  408. }
  409. /**
  410. * Frees internal resources. This method should be called
  411. * when the effect is no longer required.
  412. */
  413. dispose() {
  414. this._godraysRenderTarget.dispose();
  415. this._material.dispose();
  416. }
  417. }
  418. export default GodraysNode;
  419. /**
  420. * TSL function for creating a Godrays effect.
  421. *
  422. * @tsl
  423. * @function
  424. * @param {TextureNode} depthNode - A texture node that represents the scene's depth.
  425. * @param {Camera} camera - The camera the scene is rendered with.
  426. * @param {(DirectionalLight|PointLight)} light - The light the godrays are rendered for.
  427. * @returns {GodraysNode}
  428. */
  429. export const godrays = ( depthNode, camera, light ) => new GodraysNode( depthNode, camera, light );
粤ICP备19079148号