three
Version:
JavaScript 3D library
437 lines (337 loc) • 12.4 kB
JavaScript
import { RenderTarget, Vector2, TempNode, QuadMesh, NodeMaterial, RendererUtils, RedFormat } from 'three/webgpu';
import { reference, logarithmicDepthToViewZ, viewZToPerspectiveDepth, getViewPosition, getScreenPositionFromClip, vogelDiskSample, interleavedGradientNoise, nodeObject, Fn, float, NodeUpdateType, uv, uniform, Loop, vec4, int, dot, max, clamp, length, screenCoordinate, PI2, texture, passTexture, context } from 'three/tsl';
import { depthAwareBlur } from './depthAwareBlur.js';
const _quadMesh = /*@__PURE__*/ new QuadMesh();
const _size = /*@__PURE__*/ new Vector2();
let _rendererState;
/**
* Post processing node for a fast, screen-space ambient occlusion (SSAO).
*
* It point-samples a per-pixel rotated Vogel disk and estimates obscurance with a single depth tap
* per sample, trading the ground-truth accuracy of {@link GTAONode}'s horizon ray-marching for
* lower cost. A built-in separable, depth-aware blur denoises the result so it can be used without
* temporal accumulation.
* ```js
* const scenePass = pass( scene, camera );
* scenePass.setMRT( mrt( { output, normal: normalView } ) );
* const scenePassColor = scenePass.getTextureNode( 'output' );
* const scenePassDepth = scenePass.getTextureNode( 'depth' );
* const scenePassNormal = scenePass.getTextureNode( 'normal' );
*
* const aoPass = ssao( scenePassDepth, scenePassNormal, camera );
*
* renderPipeline.outputNode = scenePassColor.mul( aoPass.r );
* ```
*
* @augments TempNode
* @three_import import { ssao } from 'three/addons/tsl/display/SSAONode.js';
*/
class SSAONode extends TempNode {
static get type() {
return 'SSAONode';
}
/**
* Constructs a new SSAO node.
*
* @param {Node<float>} depthNode - A node that represents the scene's depth.
* @param {Node<vec3>} normalNode - A node that represents the scene's normals.
* @param {Camera} camera - The camera the scene is rendered with.
*/
constructor( depthNode, normalNode, camera ) {
super( 'float' );
/**
* A node that represents the scene's depth.
*
* @type {Node<float>}
*/
this.depthNode = depthNode;
/**
* A node that represents the scene's normals.
*
* @type {Node<vec3>}
*/
this.normalNode = normalNode;
/**
* The resolution scale. The effect renders at a fraction of the drawing buffer
* for extra speed; `0.5` is a good default for a low-frequency signal like AO.
*
* @type {number}
* @default 0.5
*/
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;
/**
* The world-space radius the occlusion is gathered within.
*
* @type {UniformNode<float>}
*/
this.radius = uniform( 0.5 );
/**
* The strength of the occlusion.
*
* @type {UniformNode<float>}
*/
this.intensity = uniform( 1 );
/**
* An angle bias that suppresses self-occlusion on near-flat surfaces.
*
* @type {UniformNode<float>}
*/
this.bias = uniform( 0.025 );
/**
* How many samples are used to estimate the occlusion. A higher value
* results in a smoother result at a higher runtime cost.
*
* @type {UniformNode<float>}
*/
this.samples = uniform( 16 );
/**
* Whether the depth-aware blur that denoises the raw AO is applied or not.
*
* @type {boolean}
* @default true
*/
this.blurEnabled = true;
/**
* How strongly the blur rejects samples across depth discontinuities,
* relative to the AO radius. A higher value keeps edges crisper.
*
* @type {UniformNode<float>}
*/
this.blurSharpness = uniform( 2 );
/**
* The resolution of the effect. Set from the drawing buffer size and `resolutionScale`.
*
* @type {Vector2}
*/
this.resolution = new Vector2();
/**
* The render target the raw ambient occlusion is rendered into. Also holds the
* final result after the separable blur has ping-ponged back into it.
*
* @private
* @type {RenderTarget}
*/
this._aoRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false, format: RedFormat } );
this._aoRenderTarget.texture.name = 'SSAONode.AO';
/**
* The render target the intermediate (horizontally blurred) result is rendered into.
*
* @private
* @type {RenderTarget}
*/
this._blurRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false, format: RedFormat } );
this._blurRenderTarget.texture.name = 'SSAONode.Blur';
/**
* Represents the projection matrix of the scene's camera.
*
* @private
* @type {UniformNode<mat4>}
*/
this._cameraProjectionMatrix = uniform( camera.projectionMatrix );
/**
* Represents the inverse projection matrix of the scene's camera.
*
* @private
* @type {UniformNode<mat4>}
*/
this._cameraProjectionMatrixInverse = uniform( camera.projectionMatrixInverse );
/**
* 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 camera the scene is rendered with.
*
* @private
* @type {Camera}
*/
this._camera = camera;
/**
* The input texture the blur material reads from. Swapped between the two render
* targets to run the horizontal and vertical passes with a single material.
*
* @private
* @type {TextureNode}
*/
this._blurInput = texture( this._aoRenderTarget.texture );
/**
* The blur direction (one texel along x or y).
*
* @private
* @type {UniformNode<vec2>}
*/
this._blurDirection = uniform( new Vector2() );
/**
* The material that computes the raw ambient occlusion.
*
* @private
* @type {NodeMaterial}
*/
this._aoMaterial = new NodeMaterial();
this._aoMaterial.name = 'SSAO.AO';
/**
* The material that applies the separable, depth-aware blur.
*
* @private
* @type {NodeMaterial}
*/
this._blurMaterial = new NodeMaterial();
this._blurMaterial.name = 'SSAO.Blur';
/**
* The result of the effect is represented as a separate texture node.
*
* @private
* @type {PassTextureNode}
*/
this._textureNode = passTexture( this, this._aoRenderTarget.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.max( 1, Math.round( this.resolutionScale * width ) );
height = Math.max( 1, Math.round( this.resolutionScale * height ) );
this.resolution.set( width, height );
this._aoRenderTarget.setSize( width, height );
this._blurRenderTarget.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 );
renderer.setClearColor( 0xffffff, 1 );
// ambient occlusion
_quadMesh.material = this._aoMaterial;
_quadMesh.name = 'SSAO.AO';
renderer.setRenderTarget( this._aoRenderTarget );
_quadMesh.render( renderer );
// separable, depth-aware blur: horizontal ( AO -> blur ) then vertical ( blur -> AO )
if ( this.blurEnabled === true ) {
_quadMesh.material = this._blurMaterial;
_quadMesh.name = 'SSAO.Blur';
this._blurInput.value = this._aoRenderTarget.texture;
this._blurDirection.value.set( 1 / this.resolution.x, 0 );
renderer.setRenderTarget( this._blurRenderTarget );
_quadMesh.render( renderer );
this._blurInput.value = this._blurRenderTarget.texture;
this._blurDirection.value.set( 0, 1 / this.resolution.y );
renderer.setRenderTarget( this._aoRenderTarget );
_quadMesh.render( renderer );
}
RendererUtils.restoreRendererState( renderer, _rendererState );
}
/**
* 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 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;
};
// ambient occlusion
const ao = Fn( () => {
const depth = sampleDepth( uvNode ).toVar();
depth.greaterThanEqual( 1.0 ).discard();
const viewPosition = getViewPosition( uvNode, depth, this._cameraProjectionMatrixInverse ).toVar();
const viewNormal = this.normalNode.sample( uvNode ).rgb.normalize().toVar();
// a low-discrepancy Vogel disk rotated per-pixel decorrelates the samples spatially,
// so a small blur is enough to hide the sampling pattern
const phi = interleavedGradientNoise( screenCoordinate ).mul( PI2 ).toVar();
const samples = this.samples;
// the fragment's clip position is loop-invariant; projection is linear, so each
// sample only has to project its (view-plane) offset and add it
const clipPosition = this._cameraProjectionMatrix.mul( vec4( viewPosition, 1.0 ) ).toVar();
const occlusion = float( 0 ).toVar();
Loop( { start: int( 0 ), end: samples, type: 'int', condition: '<' }, ( { i } ) => {
// disk offset in the view-aligned plane at the fragment's depth, then reprojected
const offset = vogelDiskSample( i, samples, phi ).mul( this.radius );
const clipOffset = this._cameraProjectionMatrix.mul( vec4( offset, 0.0, 0.0 ) );
const sampleUv = getScreenPositionFromClip( clipPosition.add( clipOffset ) );
const sampleViewPosition = getViewPosition( sampleUv, sampleDepth( sampleUv ), this._cameraProjectionMatrixInverse );
// normalized obscurance: only occluders above the tangent plane count, faded by distance
const v = sampleViewPosition.sub( viewPosition ).toVar();
const dist = length( v ).toVar();
const cosAngle = dot( v, viewNormal ).div( max( dist, float( 0.0001 ) ) );
const falloff = this.radius.div( this.radius.add( dist ) );
occlusion.addAssign( max( cosAngle.sub( this.bias ), 0.0 ).mul( falloff ) );
} );
return clamp( occlusion.div( samples ).mul( this.intensity ).oneMinus(), 0.0, 1.0 );
} );
const sharedContext = context( builder.getSharedContext() );
// compute ambient occlusion
this._aoMaterial.contextNode = sharedContext;
this._aoMaterial.fragmentNode = ao();
this._aoMaterial.needsUpdate = true;
// separable, depth-aware blur ( run horizontally then vertically via `_blurDirection` )
this._blurMaterial.contextNode = sharedContext;
this._blurMaterial.fragmentNode = depthAwareBlur( this._blurInput, this.depthNode, this._blurDirection, this._camera, this.blurSharpness, this.radius );
this._blurMaterial.needsUpdate = true;
return this._textureNode;
}
/**
* Frees internal resources. This method should be called
* when the effect is no longer required.
*/
dispose() {
super.dispose();
this._aoRenderTarget.dispose();
this._blurRenderTarget.dispose();
this._aoMaterial.dispose();
this._blurMaterial.dispose();
}
}
export default SSAONode;
/**
* TSL function for creating a fast screen-space ambient occlusion (SSAO) effect.
*
* @tsl
* @function
* @param {Node<float>} depthNode - A node that represents the scene's depth.
* @param {Node<vec3>} normalNode - A node that represents the scene's normals.
* @param {Camera} camera - The camera the scene is rendered with.
* @returns {SSAONode}
*/
export const ssao = ( depthNode, normalNode, camera ) => new SSAONode( nodeObject( depthNode ), nodeObject( normalNode ), camera );