| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615 |
- import { Frustum, Matrix4, RenderTarget, Vector2, RendererUtils, QuadMesh, TempNode, NodeMaterial, NodeUpdateType, Vector3, Plane } from 'three/webgpu';
- 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';
- const _quadMesh = /*@__PURE__*/ new QuadMesh();
- const _size = /*@__PURE__*/ new Vector2();
- const _DIRECTIONS = [
- new Vector3( 1, 0, 0 ),
- new Vector3( - 1, 0, 0 ),
- new Vector3( 0, 1, 0 ),
- new Vector3( 0, - 1, 0 ),
- new Vector3( 0, 0, 1 ),
- new Vector3( 0, 0, - 1 ),
- ];
- const _PLANES = _DIRECTIONS.map( () => new Plane() );
- const _SCRATCH_VECTOR = new Vector3();
- const _SCRATCH_MAT4 = new Matrix4();
- const _SCRATCH_FRUSTUM = new Frustum();
- let _rendererState;
- /**
- * Post-Processing node for apply Screen-space raymarched godrays to a scene.
- *
- * After the godrays have been computed, it's recommened to apply a Bilateral
- * Blur to the result to mitigate raymarching and noise artifacts.
- *
- * The composite with the scene pass is ideally done with `depthAwareBlend()`,
- * which mitigates aliasing and light leaking.
- *
- * ```js
- * const godraysPass = godrays( scenePassDepth, camera, light );
- *
- * const blurPass = bilateralBlur( godraysPassColor ); // optional blur
- *
- * const outputBlurred = depthAwareBlend( scenePassColor, blurPassColor, scenePassDepth, camera, { blendColor, edgeRadius, edgeStrength } ); // composite
- * ```
- *
- * Limitations:
- *
- * - Only point and directional lights are currently supported.
- * - The effect requires a full shadow setup. Meaning shadows must be enabled in the renderer,
- * 3D objects must cast and receive shadows and the main light must cast shadows.
- *
- * Reference: This Node is a part of [three-good-godrays](https://github.com/Ameobea/three-good-godrays).
- *
- * @augments TempNode
- * @three_import import { godrays } from 'three/addons/tsl/display/GodraysNode.js';
- */
- class GodraysNode extends TempNode {
- static get type() {
- return 'GodraysNode';
- }
- /**
- * Constructs a new Godrays node.
- *
- * @param {TextureNode} depthNode - A texture node that represents the scene's depth.
- * @param {Camera} camera - The camera the scene is rendered with.
- * @param {(DirectionalLight|PointLight)} light - The light the godrays are rendered for.
- */
- constructor( depthNode, camera, light ) {
- super( 'vec4' );
- /**
- * A node that represents the beauty pass's depth.
- *
- * @type {TextureNode}
- */
- this.depthNode = depthNode;
- /**
- * The number of raymarching steps
- *
- * @type {UniformNode<uint>}
- * @default 60
- */
- this.raymarchSteps = uniform( uint( 60 ) );
- /**
- * The rate of accumulation for the godrays. Higher values roughly equate to more humid air/denser fog.
- *
- * @type {UniformNode<float>}
- * @default 0.7
- */
- this.density = uniform( float( 0.7 ) );
- /**
- * The maximum density of the godrays. Limits the maximum brightness of the godrays.
- *
- * @type {UniformNode<float>}
- * @default 0.5
- */
- this.maxDensity = uniform( float( 0.5 ) );
- /**
- * Higher values decrease the accumulation of godrays the further away they are from the light source.
- *
- * @type {UniformNode<float>}
- * @default 2
- */
- this.distanceAttenuation = uniform( float( 2 ) );
- /**
- * The resolution scale.
- *
- * @type {number}
- */
- this.resolutionScale = 0.5;
- /**
- * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders
- * its effect once per frame in `updateBefore()`.
- *
- * @type {string}
- * @default 'frame'
- */
- this.updateBeforeType = NodeUpdateType.FRAME;
- // private uniforms
- /**
- * Represents the world matrix of the scene's camera.
- *
- * @private
- * @type {UniformNode<mat4>}
- */
- this._cameraMatrixWorld = uniform( camera.matrixWorld );
- /**
- * Represents the inverse projection matrix of the scene's camera.
- *
- * @private
- * @type {UniformNode<mat4>}
- */
- this._cameraProjectionMatrixInverse = uniform( camera.projectionMatrixInverse );
- /**
- * Represents the inverse projection matrix of the scene's camera.
- *
- * @private
- * @type {UniformNode<mat4>}
- */
- this._premultipliedLightCameraMatrix = uniform( new Matrix4() );
- /**
- * Represents the world position of the scene's camera.
- *
- * @private
- * @type {UniformNode<mat4>}
- */
- this._cameraPosition = uniform( new Vector3() );
- /**
- * Represents the near value of the scene's camera.
- *
- * @private
- * @type {ReferenceNode<float>}
- */
- this._cameraNear = reference( 'near', 'float', camera );
- /**
- * Represents the far value of the scene's camera.
- *
- * @private
- * @type {ReferenceNode<float>}
- */
- this._cameraFar = reference( 'far', 'float', camera );
- /**
- * The near value of the shadow camera.
- *
- * @private
- * @type {ReferenceNode<float>}
- */
- this._shadowCameraNear = reference( 'near', 'float', light.shadow.camera );
- /**
- * The far value of the shadow camera.
- *
- * @private
- * @type {ReferenceNode<float>}
- */
- this._shadowCameraFar = reference( 'far', 'float', light.shadow.camera );
- this._fNormals = uniformArray( _DIRECTIONS.map( () => new Vector3() ) );
- this._fConstants = uniformArray( _DIRECTIONS.map( () => 0 ) );
- /**
- * The light the godrays are rendered for.
- *
- * @private
- * @type {(DirectionalLight|PointLight)}
- */
- this._light = light;
- /**
- * The camera the scene is rendered with.
- *
- * @private
- * @type {Camera}
- */
- this._camera = camera;
- /**
- * The render target the godrays are rendered into.
- *
- * @private
- * @type {RenderTarget}
- */
- this._godraysRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false } );
- this._godraysRenderTarget.texture.name = 'Godrays';
- /**
- * The material that is used to render the effect.
- *
- * @private
- * @type {NodeMaterial}
- */
- this._material = new NodeMaterial();
- this._material.name = 'Godrays';
- /**
- * The result of the effect is represented as a separate texture node.
- *
- * @private
- * @type {PassTextureNode}
- */
- this._textureNode = passTexture( this, this._godraysRenderTarget.texture );
- }
- /**
- * Returns the result of the effect as a texture node.
- *
- * @return {PassTextureNode} A texture node that represents the result of the effect.
- */
- getTextureNode() {
- return this._textureNode;
- }
- /**
- * Sets the size of the effect.
- *
- * @param {number} width - The width of the effect.
- * @param {number} height - The height of the effect.
- */
- setSize( width, height ) {
- width = Math.round( this.resolutionScale * width );
- height = Math.round( this.resolutionScale * height );
- this._godraysRenderTarget.setSize( width, height );
- }
- /**
- * This method is used to render the effect once per frame.
- *
- * @param {NodeFrame} frame - The current node frame.
- */
- updateBefore( frame ) {
- const { renderer } = frame;
- _rendererState = RendererUtils.resetRendererState( renderer, _rendererState );
- //
- const size = renderer.getDrawingBufferSize( _size );
- this.setSize( size.width, size.height );
- //
- _quadMesh.material = this._material;
- _quadMesh.name = 'Godrays';
- this._updateLightParams();
- this._cameraPosition.value.setFromMatrixPosition( this._camera.matrixWorld );
- // clear
- renderer.setClearColor( 0xffffff, 1 );
- // godrays
- renderer.setRenderTarget( this._godraysRenderTarget );
- _quadMesh.render( renderer );
- // restore
- RendererUtils.restoreRendererState( renderer, _rendererState );
- }
- _updateLightParams() {
- const light = this._light;
- const shadowCamera = light.shadow.camera;
- this._premultipliedLightCameraMatrix.value.multiplyMatrices( shadowCamera.projectionMatrix, shadowCamera.matrixWorldInverse );
- if ( light.isPointLight ) {
- for ( let i = 0; i < _DIRECTIONS.length; i ++ ) {
- const direction = _DIRECTIONS[ i ];
- const plane = _PLANES[ i ];
- _SCRATCH_VECTOR.copy( light.position );
- _SCRATCH_VECTOR.addScaledVector( direction, shadowCamera.far );
- plane.setFromNormalAndCoplanarPoint( direction, _SCRATCH_VECTOR );
- this._fNormals.array[ i ].copy( plane.normal );
- this._fConstants.array[ i ] = plane.constant;
- }
- } else if ( light.isDirectionalLight ) {
- _SCRATCH_MAT4.multiplyMatrices( shadowCamera.projectionMatrix, shadowCamera.matrixWorldInverse );
- _SCRATCH_FRUSTUM.setFromProjectionMatrix( _SCRATCH_MAT4 );
- for ( let i = 0; i < 6; i ++ ) {
- const plane = _SCRATCH_FRUSTUM.planes[ i ];
- this._fNormals.array[ i ].copy( plane.normal ).multiplyScalar( - 1 );
- this._fConstants.array[ i ] = plane.constant * - 1;
- }
- }
- }
- /**
- * This method is used to setup the effect's TSL code.
- *
- * @param {NodeBuilder} builder - The current node builder.
- * @return {PassTextureNode}
- */
- setup( builder ) {
- const uvNode = uv();
- const lightPos = lightPosition( this._light );
- const sampleDepth = ( uv ) => {
- const depth = this.depthNode.sample( uv ).r;
- if ( builder.renderer.logarithmicDepthBuffer === true ) {
- const viewZ = logarithmicDepthToViewZ( depth, this._cameraNear, this._cameraFar );
- return viewZToPerspectiveDepth( viewZ, this._cameraNear, this._cameraFar );
- }
- return depth;
- };
- const sdPlane = ( p, n, h ) => {
- return dot( p, n ).add( h );
- };
- const intersectRayPlane = ( rayOrigin, rayDirection, planeNormal, planeDistance ) => {
- const denom = dot( planeNormal, rayDirection );
- return sdPlane( rayOrigin, planeNormal, planeDistance ).div( denom ).negate();
- };
- const computeShadowCoord = ( worldPos ) => {
- const shadowPosition = lightShadowMatrix( this._light ).mul( worldPos );
- const shadowCoord = shadowPosition.xyz.div( shadowPosition.w );
- return vec3( shadowCoord.x, shadowCoord.y.oneMinus(), shadowCoord.z );
- };
- const inShadow = ( worldPos ) => {
- if ( this._light.isPointLight ) {
- const lightToPos = worldPos.sub( lightPos ).toConst();
- const shadowPositionAbs = lightToPos.abs().toConst();
- const viewZ = shadowPositionAbs.x.max( shadowPositionAbs.y ).max( shadowPositionAbs.z ).negate();
- const depth = viewZToPerspectiveDepth( viewZ, this._shadowCameraNear, this._shadowCameraFar );
- const result = cubeTexture( this._light.shadow.map.depthTexture, lightToPos ).compare( depth ).r;
- return vec2( result.oneMinus().add( 0.005 ), viewZ.negate() );
- } else if ( this._light.isDirectionalLight ) {
- const shadowCoord = computeShadowCoord( worldPos ).toConst();
- const frustumTest = shadowCoord.x.greaterThanEqual( 0 )
- .and( shadowCoord.x.lessThanEqual( 1 ) )
- .and( shadowCoord.y.greaterThanEqual( 0 ) )
- .and( shadowCoord.y.lessThanEqual( 1 ) )
- .and( shadowCoord.z.greaterThanEqual( 0 ) )
- .and( shadowCoord.z.lessThanEqual( 1 ) );
- const output = vec2( 1, 0 );
- If( frustumTest.equal( true ), () => {
- const result = texture( this._light.shadow.map.depthTexture, shadowCoord.xy ).compare( shadowCoord.z ).r;
- const viewZ = perspectiveDepthToViewZ( shadowCoord.z, this._shadowCameraNear, this._shadowCameraFar );
- output.assign( vec2( result.oneMinus(), viewZ.negate() ) );
- } );
- return output;
- } else {
- throw new Error( 'GodraysNode: Unsupported light type.' );
- }
- };
- const godrays = Fn( () => {
- const output = vec4( 0, 0, 0, 1 ).toVar();
- const isEarlyOut = bool( false );
- const depth = sampleDepth( uvNode ).toConst();
- const viewPosition = getViewPosition( uvNode, depth, this._cameraProjectionMatrixInverse ).toConst();
- const worldPosition = this._cameraMatrixWorld.mul( viewPosition );
- const inBoxDist = float( - 10000.0 ).toVar();
- Loop( 6, ( { i } ) => {
- inBoxDist.assign( max( inBoxDist, sdPlane( this._cameraPosition, this._fNormals.element( i ), this._fConstants.element( i ) ) ) );
- } );
- const startPosition = this._cameraPosition.toVar();
- If( inBoxDist.lessThan( 0 ), () => {
- // If the ray target is outside the shadow box, move it to the nearest
- // point on the box to avoid marching through unlit space
- Loop( 6, ( { i } ) => {
- If( sdPlane( worldPosition, this._fNormals.element( i ), this._fConstants.element( i ) ).greaterThan( 0 ), () => {
- const direction = worldPosition.sub( this._cameraPosition ).toConst();
- const t = intersectRayPlane( this._cameraPosition, direction, this._fNormals.element( i ), this._fConstants.element( i ) );
- worldPosition.assign( this._cameraPosition.add( t.mul( direction ) ) );
- } );
- } );
- } ).Else( () => {
- // Find the first point where the ray intersects the shadow box (startPos)
- const direction = worldPosition.sub( this._cameraPosition ).toConst();
- const minT = float( 10000 ).toVar();
- Loop( 6, ( { i } ) => {
- const t = intersectRayPlane( this._cameraPosition, direction, this._fNormals.element( i ), this._fConstants.element( i ) );
- If( t.lessThan( minT ).and( t.greaterThan( 0 ) ), () => {
- minT.assign( t );
- } );
- } );
- If( minT.equal( 10000 ), () => {
- isEarlyOut.assign( true );
- } ).Else( () => {
- startPosition.assign( this._cameraPosition.add( minT.add( 0.001 ).mul( direction ) ) );
- // If the ray target is outside the shadow box, move it to the nearest
- // point on the box to avoid marching through unlit space
- const endInBoxDist = float( - 10000 ).toVar();
- Loop( 6, ( { i } ) => {
- endInBoxDist.assign( max( endInBoxDist, sdPlane( worldPosition, this._fNormals.element( i ), this._fConstants.element( i ) ) ) );
- } );
- If( endInBoxDist.greaterThanEqual( 0 ), () => {
- const minT = float( 10000 ).toVar();
- Loop( 6, ( { i } ) => {
- If( sdPlane( worldPosition, this._fNormals.element( i ), this._fConstants.element( i ) ).greaterThan( 0 ), () => {
- const t = intersectRayPlane( startPosition, direction, this._fNormals.element( i ), this._fConstants.element( i ) );
- If( t.lessThan( minT ).and( t.greaterThan( 0 ) ), () => {
- minT.assign( t );
- } );
- } );
- } );
- If( minT.lessThan( worldPosition.distance( startPosition ) ), () => {
- worldPosition.assign( startPosition.add( minT.mul( direction ) ) );
- } );
- } );
- } );
- } );
- If( isEarlyOut.equal( false ), () => {
- const illum = float( 0 ).toVar();
- const noise = interleavedGradientNoise( screenCoordinate ).toConst();
- const samplesFloat = round( add( this.raymarchSteps, mul( this.raymarchSteps.div( 8 ).add( 2 ), noise ) ) ).toConst();
- const samples = uint( samplesFloat ).toConst();
- Loop( samples, ( { i } ) => {
- const samplePos = mix( startPosition, worldPosition, float( i ).div( samplesFloat ) ).toConst();
- const shadowInfo = inShadow( samplePos );
- const shadowAmount = shadowInfo.x.oneMinus().toConst();
- illum.addAssign( shadowAmount.mul( distance( startPosition, worldPosition ).mul( this.density.div( 100 ) ) ).mul( pow( shadowInfo.y.div( this._shadowCameraFar ).oneMinus(), this.distanceAttenuation ) ) );
- } );
- illum.divAssign( samplesFloat );
- output.assign( vec4( vec3( clamp( exp( illum.negate() ).oneMinus(), 0, this.maxDensity ) ), depth ) );
- } );
- return output;
- } );
- this._material.fragmentNode = godrays().context( builder.getSharedContext() );
- this._material.needsUpdate = true;
- return this._textureNode;
- }
- /**
- * Frees internal resources. This method should be called
- * when the effect is no longer required.
- */
- dispose() {
- this._godraysRenderTarget.dispose();
- this._material.dispose();
- }
- }
- export default GodraysNode;
- /**
- * TSL function for creating a Godrays effect.
- *
- * @tsl
- * @function
- * @param {TextureNode} depthNode - A texture node that represents the scene's depth.
- * @param {Camera} camera - The camera the scene is rendered with.
- * @param {(DirectionalLight|PointLight)} light - The light the godrays are rendered for.
- * @returns {GodraysNode}
- */
- export const godrays = ( depthNode, camera, light ) => new GodraysNode( depthNode, camera, light );
|