GodraysNode.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. import { Frustum, Matrix4, RenderTarget, Vector2, RendererUtils, QuadMesh, TempNode, NodeMaterial, NodeUpdateType, Vector3, Plane } 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 uvNode = uv();
  279. const lightPos = lightPosition( this._light );
  280. const sampleDepth = ( uv ) => {
  281. const depth = this.depthNode.sample( uv ).r;
  282. if ( builder.renderer.logarithmicDepthBuffer === true ) {
  283. const viewZ = logarithmicDepthToViewZ( depth, this._cameraNear, this._cameraFar );
  284. return viewZToPerspectiveDepth( viewZ, this._cameraNear, this._cameraFar );
  285. }
  286. return depth;
  287. };
  288. const sdPlane = ( p, n, h ) => {
  289. return dot( p, n ).add( h );
  290. };
  291. const intersectRayPlane = ( rayOrigin, rayDirection, planeNormal, planeDistance ) => {
  292. const denom = dot( planeNormal, rayDirection );
  293. return sdPlane( rayOrigin, planeNormal, planeDistance ).div( denom ).negate();
  294. };
  295. const computeShadowCoord = ( worldPos ) => {
  296. const shadowPosition = lightShadowMatrix( this._light ).mul( worldPos );
  297. const shadowCoord = shadowPosition.xyz.div( shadowPosition.w );
  298. return vec3( shadowCoord.x, shadowCoord.y.oneMinus(), shadowCoord.z );
  299. };
  300. const inShadow = ( worldPos ) => {
  301. if ( this._light.isPointLight ) {
  302. const lightToPos = worldPos.sub( lightPos ).toConst();
  303. const shadowPositionAbs = lightToPos.abs().toConst();
  304. const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ).negate();
  305. const depth = viewZToPerspectiveDepth( viewZ, this._shadowCameraNear, this._shadowCameraFar );
  306. const result = cubeTexture( this._light.shadow.map.depthTexture, lightToPos ).compare( depth ).r;
  307. return vec2( result.oneMinus().add( 0.005 ), viewZ.negate() );
  308. } else if ( this._light.isDirectionalLight ) {
  309. const shadowCoord = computeShadowCoord( worldPos ).toConst();
  310. const frustumTest = shadowCoord.x.greaterThanEqual( 0 )
  311. .and( shadowCoord.x.lessThanEqual( 1 ) )
  312. .and( shadowCoord.y.greaterThanEqual( 0 ) )
  313. .and( shadowCoord.y.lessThanEqual( 1 ) )
  314. .and( shadowCoord.z.greaterThanEqual( 0 ) )
  315. .and( shadowCoord.z.lessThanEqual( 1 ) );
  316. const output = vec2( 1, 0 );
  317. If( frustumTest.equal( true ), () => {
  318. const result = texture( this._light.shadow.map.depthTexture, shadowCoord.xy ).compare( shadowCoord.z ).r;
  319. const viewZ = perspectiveDepthToViewZ( shadowCoord.z, this._shadowCameraNear, this._shadowCameraFar );
  320. output.assign( vec2( result.oneMinus(), viewZ.negate() ) );
  321. } );
  322. return output;
  323. } else {
  324. throw new Error( 'GodraysNode: Unsupported light type.' );
  325. }
  326. };
  327. const godrays = Fn( () => {
  328. const output = vec4( 0, 0, 0, 1 ).toVar();
  329. const isEarlyOut = bool( false );
  330. const depth = sampleDepth( uvNode ).toConst();
  331. const viewPosition = getViewPosition( uvNode, depth, this._cameraProjectionMatrixInverse ).toConst();
  332. const worldPosition = this._cameraMatrixWorld.mul( viewPosition );
  333. const inBoxDist = float( - 10000.0 ).toVar();
  334. Loop( 6, ( { i } ) => {
  335. inBoxDist.assign( max( inBoxDist, sdPlane( this._cameraPosition, this._fNormals.element( i ), this._fConstants.element( i ) ) ) );
  336. } );
  337. const startPosition = this._cameraPosition.toVar();
  338. If( inBoxDist.lessThan( 0 ), () => {
  339. // If the ray target is outside the shadow box, move it to the nearest
  340. // point on the box to avoid marching through unlit space
  341. Loop( 6, ( { i } ) => {
  342. If( sdPlane( worldPosition, this._fNormals.element( i ), this._fConstants.element( i ) ).greaterThan( 0 ), () => {
  343. const direction = worldPosition.sub( this._cameraPosition ).toConst();
  344. const t = intersectRayPlane( this._cameraPosition, direction, this._fNormals.element( i ), this._fConstants.element( i ) );
  345. worldPosition.assign( this._cameraPosition.add( t.mul( direction ) ) );
  346. } );
  347. } );
  348. } ).Else( () => {
  349. // Find the first point where the ray intersects the shadow box (startPos)
  350. const direction = worldPosition.sub( this._cameraPosition ).toConst();
  351. const minT = float( 10000 ).toVar();
  352. Loop( 6, ( { i } ) => {
  353. const t = intersectRayPlane( this._cameraPosition, direction, this._fNormals.element( i ), this._fConstants.element( i ) );
  354. If( t.lessThan( minT ).and( t.greaterThan( 0 ) ), () => {
  355. minT.assign( t );
  356. } );
  357. } );
  358. If( minT.equal( 10000 ), () => {
  359. isEarlyOut.assign( true );
  360. } ).Else( () => {
  361. startPosition.assign( this._cameraPosition.add( minT.add( 0.001 ).mul( direction ) ) );
  362. // If the ray target is outside the shadow box, move it to the nearest
  363. // point on the box to avoid marching through unlit space
  364. const endInBoxDist = float( - 10000 ).toVar();
  365. Loop( 6, ( { i } ) => {
  366. endInBoxDist.assign( max( endInBoxDist, sdPlane( worldPosition, this._fNormals.element( i ), this._fConstants.element( i ) ) ) );
  367. } );
  368. If( endInBoxDist.greaterThanEqual( 0 ), () => {
  369. const minT = float( 10000 ).toVar();
  370. Loop( 6, ( { i } ) => {
  371. If( sdPlane( worldPosition, this._fNormals.element( i ), this._fConstants.element( i ) ).greaterThan( 0 ), () => {
  372. const t = intersectRayPlane( startPosition, direction, this._fNormals.element( i ), this._fConstants.element( i ) );
  373. If( t.lessThan( minT ).and( t.greaterThan( 0 ) ), () => {
  374. minT.assign( t );
  375. } );
  376. } );
  377. } );
  378. If( minT.lessThan( worldPosition.distance( startPosition ) ), () => {
  379. worldPosition.assign( startPosition.add( minT.mul( direction ) ) );
  380. } );
  381. } );
  382. } );
  383. } );
  384. If( isEarlyOut.equal( false ), () => {
  385. const illum = float( 0 ).toVar();
  386. const noise = interleavedGradientNoise( screenCoordinate ).toConst();
  387. const samplesFloat = round( add( this.raymarchSteps, mul( this.raymarchSteps.div( 8 ).add( 2 ), noise ) ) ).toConst();
  388. const samples = uint( samplesFloat ).toConst();
  389. Loop( samples, ( { i } ) => {
  390. const samplePos = mix( startPosition, worldPosition, float( i ).div( samplesFloat ) ).toConst();
  391. const shadowInfo = inShadow( samplePos );
  392. const shadowAmount = shadowInfo.x.oneMinus().toConst();
  393. illum.addAssign( shadowAmount.mul( distance( startPosition, worldPosition ).mul( this.density.div( 100 ) ) ).mul( pow( shadowInfo.y.div( this._shadowCameraFar ).oneMinus(), this.distanceAttenuation ) ) );
  394. } );
  395. illum.divAssign( samplesFloat );
  396. output.assign( vec4( vec3( clamp( exp( illum.negate() ).oneMinus(), 0, this.maxDensity ) ), depth ) );
  397. } );
  398. return output;
  399. } );
  400. this._material.fragmentNode = godrays().context( builder.getSharedContext() );
  401. this._material.needsUpdate = true;
  402. return this._textureNode;
  403. }
  404. /**
  405. * Frees internal resources. This method should be called
  406. * when the effect is no longer required.
  407. */
  408. dispose() {
  409. this._godraysRenderTarget.dispose();
  410. this._material.dispose();
  411. }
  412. }
  413. export default GodraysNode;
  414. /**
  415. * TSL function for creating a Godrays effect.
  416. *
  417. * @tsl
  418. * @function
  419. * @param {TextureNode} depthNode - A texture node that represents the scene's depth.
  420. * @param {Camera} camera - The camera the scene is rendered with.
  421. * @param {(DirectionalLight|PointLight)} light - The light the godrays are rendered for.
  422. * @returns {GodraysNode}
  423. */
  424. export const godrays = ( depthNode, camera, light ) => new GodraysNode( depthNode, camera, light );
粤ICP备19079148号