625 lines
16 KiB
JavaScript
625 lines
16 KiB
JavaScript
import { Frustum, Matrix4, RenderTarget, Vector2, RendererUtils, QuadMesh, TempNode, NodeMaterial, NodeUpdateType, Vector3, Plane, WebGPUCoordinateSystem } 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 { renderer } = 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 );
|
|
let coordZ = shadowCoord.z;
|
|
|
|
if ( renderer.coordinateSystem === WebGPUCoordinateSystem ) {
|
|
|
|
coordZ = coordZ.mul( 2 ).sub( 1 ); // WebGPU: Conversion [ 0, 1 ] to [ - 1, 1 ]
|
|
|
|
}
|
|
|
|
return vec3( shadowCoord.x, shadowCoord.y.oneMinus(), coordZ );
|
|
|
|
};
|
|
|
|
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 );
|