| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642 |
- import { RenderTarget, Vector2, TempNode, QuadMesh, NodeMaterial, RendererUtils, MathUtils } from 'three/webgpu';
- import { clamp, normalize, reference, nodeObject, Fn, NodeUpdateType, uniform, vec4, passTexture, uv, logarithmicDepthToViewZ, viewZToPerspectiveDepth, getViewPosition, screenCoordinate, float, sub, fract, dot, vec2, rand, vec3, Loop, mul, PI, cos, sin, uint, cross, acos, sign, pow, luminance, If, max, abs, Break, sqrt, HALF_PI, div, ceil, shiftRight, convertToTexture, bool, getNormalFromDepth, countOneBits, interleavedGradientNoise } from 'three/tsl';
- const _quadMesh = /*@__PURE__*/ new QuadMesh();
- const _size = /*@__PURE__*/ new Vector2();
- // From Activision GTAO paper: https://www.activision.com/cdn/research/s2016_pbs_activision_occlusion.pptx
- const _temporalRotations = [ 60, 300, 180, 240, 120, 0 ];
- const _spatialOffsets = [ 0, 0.5, 0.25, 0.75 ];
- let _rendererState;
- /**
- * Post processing node for applying Screen Space Global Illumination (SSGI) to a scene.
- *
- * References:
- * - {@link https://github.com/cdrinmatane/SSRT3}.
- * - {@link https://cdrinmatane.github.io/posts/ssaovb-code/}.
- * - {@link https://cdrinmatane.github.io/cgspotlight-slides/ssilvb_slides.pdf}.
- *
- * The quality and performance of the effect mainly depend on `sliceCount` and `stepCount`.
- * The total number of samples taken per pixel is `sliceCount` * `stepCount` * `2`. Here are some
- * recommended presets depending on whether temporal filtering is used or not.
- *
- * With temporal filtering (recommended):
- *
- * - Low: `sliceCount` of `1`, `stepCount` of `12`.
- * - Medium: `sliceCount` of `2`, `stepCount` of `8`.
- * - High: `sliceCount` of `3`, `stepCount` of `16`.
- *
- * Use for a higher slice count if you notice temporal instabilities like flickering. Reduce the sample
- * count then to mitigate the performance lost.
- *
- * Without temporal filtering:
- *
- * - Low: `sliceCount` of `2`, `stepCount` of `6`.
- * - Medium: `sliceCount` of `3`, `stepCount` of `8`.
- * - High: `sliceCount` of `4`, `stepCount` of `12`.
- *
- * @augments TempNode
- * @three_import import { ssgi } from 'three/addons/tsl/display/SSGINode.js';
- */
- class SSGINode extends TempNode {
- static get type() {
- return 'SSGINode';
- }
- /**
- * Constructs a new SSGI node.
- *
- * @param {TextureNode} beautyNode - A texture node that represents the beauty or scene pass.
- * @param {TextureNode} depthNode - A texture node that represents the scene's depth.
- * @param {TextureNode} normalNode - A texture node that represents the scene's normals.
- * @param {PerspectiveCamera} camera - The camera the scene is rendered with.
- */
- constructor( beautyNode, depthNode, normalNode, camera ) {
- super( 'vec4' );
- /**
- * A texture node that represents the beauty or scene pass.
- *
- * @type {TextureNode}
- */
- this.beautyNode = beautyNode;
- /**
- * A node that represents the scene's depth.
- *
- * @type {TextureNode}
- */
- this.depthNode = depthNode;
- /**
- * A node that represents the scene's normals. If no normals are passed to the
- * constructor (because MRT is not available), normals can be automatically
- * reconstructed from depth values in the shader.
- *
- * @type {TextureNode}
- */
- this.normalNode = normalNode;
- /**
- * 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;
- /**
- * Number of per-pixel hemisphere slices. This has a big performance cost and should be kept as low as possible.
- * Should be in the range `[1, 4]`.
- *
- * @type {UniformNode<uint>}
- * @default 1
- */
- this.sliceCount = uniform( 1, 'uint' );
- /**
- * Number of samples taken along one side of a given hemisphere slice. This has a big performance cost and should
- * be kept as low as possible. Should be in the range `[1, 32]`.
- *
- * @type {UniformNode<uint>}
- * @default 12
- */
- this.stepCount = uniform( 12, 'uint' );
- /**
- * Power function applied to AO to make it appear darker/lighter. Should be in the range `[0, 4]`.
- *
- * @type {UniformNode<float>}
- * @default 1
- */
- this.aoIntensity = uniform( 1, 'float' );
- /**
- * Intensity of the indirect diffuse light. Should be in the range `[0, 100]`.
- *
- * @type {UniformNode<float>}
- * @default 10
- */
- this.giIntensity = uniform( 10, 'float' );
- /**
- * Effective sampling radius in world space. AO and GI can only have influence within that radius.
- * Should be in the range `[1, 25]`.
- *
- * @type {UniformNode<float>}
- * @default 12
- */
- this.radius = uniform( 12, 'float' );
- /**
- * Makes the sample distance in screen space instead of world-space (helps having more detail up close).
- *
- * @type {UniformNode<bool>}
- * @default false
- */
- this.useScreenSpaceSampling = uniform( true, 'bool' );
- /**
- * Controls samples distribution. It's an exponent applied at each step get increasing step size over the distance.
- * Should be in the range `[1, 3]`.
- *
- * @type {UniformNode<float>}
- * @default 2
- */
- this.expFactor = uniform( 2, 'float' );
- /**
- * Constant thickness value of objects on the screen in world units. Allows light to pass behind surfaces past that thickness value.
- * Should be in the range `[0.01, 10]`.
- *
- * @type {UniformNode<float>}
- * @default 1
- */
- this.thickness = uniform( 1, 'float' );
- /**
- * Whether to increase thickness linearly over distance or not (avoid losing detail over the distance).
- *
- * @type {UniformNode<bool>}
- * @default false
- */
- this.useLinearThickness = uniform( false, 'bool' );
- /**
- * How much light backface surfaces emit.
- * Should be in the range `[0, 1]`.
- *
- * @type {UniformNode<float>}
- * @default 0
- */
- this.backfaceLighting = uniform( 0, 'float' );
- /**
- * Whether to use temporal filtering or not. Setting this property to
- * `true` requires the usage of `TRAANode`. This will help to reduce noise
- * although it introduces typical TAA artifacts like ghosting and temporal
- * instabilities.
- *
- * If setting this property to `false`, a manual denoise via `DenoiseNode`
- * is required.
- *
- * @type {boolean}
- * @default true
- */
- this.useTemporalFiltering = true;
- // private uniforms
- /**
- * The resolution of the effect.
- *
- * @private
- * @type {UniformNode<vec2>}
- */
- this._resolution = uniform( new Vector2() );
- /**
- * Used to compute the effective step radius when viewSpaceSampling is `false`.
- *
- * @private
- * @type {UniformNode<vec2>}
- */
- this._halfProjScale = uniform( 1 );
- /**
- * Temporal direction that influences the rotation angle for each slice.
- *
- * @private
- * @type {UniformNode<float>}
- */
- this._temporalDirection = uniform( 0 );
- /**
- * Temporal offset added to the initial ray step.
- *
- * @private
- * @type {UniformNode<float>}
- */
- this._temporalOffset = uniform( 0 );
- /**
- * 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 );
- /**
- * A reference to the scene's camera.
- *
- * @private
- * @type {PerspectiveCamera}
- */
- this._camera = camera;
- /**
- * The render target the GI is rendered into.
- *
- * @private
- * @type {RenderTarget}
- */
- this._ssgiRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false } );
- this._ssgiRenderTarget.texture.name = 'SSGI';
- /**
- * The material that is used to render the effect.
- *
- * @private
- * @type {NodeMaterial}
- */
- this._material = new NodeMaterial();
- this._material.name = 'SSGI';
- /**
- * The result of the effect is represented as a separate texture node.
- *
- * @private
- * @type {PassTextureNode}
- */
- this._textureNode = passTexture( this, this._ssgiRenderTarget.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 ) {
- this._resolution.value.set( width, height );
- this._ssgiRenderTarget.setSize( width, height );
- this._halfProjScale.value = height / ( Math.tan( this._camera.fov * MathUtils.DEG2RAD * 0.5 ) * 2 ) * 0.5;
- }
- /**
- * 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 );
- // update temporal uniforms
- if ( this.useTemporalFiltering === true ) {
- const frameId = frame.frameId;
- this._temporalDirection.value = _temporalRotations[ frameId % 6 ] / 360;
- this._temporalOffset.value = _spatialOffsets[ frameId % 4 ];
- } else {
- this._temporalDirection.value = 1;
- this._temporalOffset.value = 1;
- }
- //
- _quadMesh.material = this._material;
- _quadMesh.name = 'SSGI';
- // clear
- renderer.setClearColor( 0x000000, 1 );
- // gi
- renderer.setRenderTarget( this._ssgiRenderTarget );
- _quadMesh.render( renderer );
- // restore
- 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 MAX_RAY = uint( 32 );
- const globalOccludedBitfield = uint( 0 );
- 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 sampleNormal = ( uv ) => ( this.normalNode !== null ) ? this.normalNode.sample( uv ).rgb.normalize() : getNormalFromDepth( uv, this.depthNode.value, this._cameraProjectionMatrixInverse );
- const sampleBeauty = ( uv ) => this.beautyNode.sample( uv );
- // From Activision GTAO paper: https://www.activision.com/cdn/research/s2016_pbs_activision_occlusion.pptx
- const spatialOffsets = Fn( ( [ position ] ) => {
- return float( 0.25 ).mul( sub( position.y, position.x ).bitAnd( 3 ) );
- } ).setLayout( {
- name: 'spatialOffsets',
- type: 'float',
- inputs: [
- { name: 'position', type: 'vec2' }
- ]
- } );
- const GTAOFastAcos = Fn( ( [ value ] ) => {
- const outVal = abs( value ).mul( float( - 0.156583 ) ).add( HALF_PI );
- outVal.mulAssign( sqrt( abs( value ).oneMinus() ) );
- const x = value.x.greaterThanEqual( 0 ).select( outVal.x, PI.sub( outVal.x ) );
- const y = value.y.greaterThanEqual( 0 ).select( outVal.y, PI.sub( outVal.y ) );
- return vec2( x, y );
- } ).setLayout( {
- name: 'GTAOFastAcos',
- type: 'vec2',
- inputs: [
- { name: 'value', type: 'vec2' }
- ]
- } );
- const horizonSampling = Fn( ( [ directionIsRight, RADIUS, viewPosition, slideDirTexelSize, initialRayStep, uvNode, viewDir, viewNormal, n ] ) => {
- const STEP_COUNT = this.stepCount.toConst();
- const EXP_FACTOR = this.expFactor.toConst();
- const THICKNESS = this.thickness.toConst();
- const BACKFACE_LIGHTING = this.backfaceLighting.toConst();
- const stepRadius = float( 0 );
- If( this.useScreenSpaceSampling.equal( true ), () => {
- stepRadius.assign( RADIUS.mul( this._resolution.x.div( 2 ) ).div( float( 16 ) ) ); // SSRT3 has a bug where stepRadius is divided by STEP_COUNT twice; fix here
- } ).Else( () => {
- stepRadius.assign( max( RADIUS.mul( this._halfProjScale ).div( viewPosition.z.negate() ), float( STEP_COUNT ) ) ); // Port note: viewZ is negative so a negate is required
- } );
- stepRadius.divAssign( float( STEP_COUNT ).add( 1 ) );
- const radiusVS = max( 1, float( STEP_COUNT.sub( 1 ) ) ).mul( stepRadius );
- const uvDirection = directionIsRight.equal( true ).select( vec2( 1, - 1 ), vec2( - 1, 1 ) ); // Port note: Because of different uv conventions, uv-y has a different sign
- const samplingDirection = directionIsRight.equal( true ).select( 1, - 1 );
- const color = vec3( 0 );
- const lastSampleViewPosition = vec3( viewPosition ).toVar();
- Loop( { start: uint( 0 ), end: STEP_COUNT, type: 'uint', condition: '<' }, ( { i } ) => {
- const offset = pow( abs( mul( stepRadius, float( i ).add( initialRayStep ) ).div( radiusVS ) ), EXP_FACTOR ).mul( radiusVS ).toConst();
- const uvOffset = slideDirTexelSize.mul( max( offset, float( i ).add( 1 ) ) ).toConst();
- const sampleUV = uvNode.add( uvOffset.mul( uvDirection ) ).toConst();
- If( sampleUV.x.lessThanEqual( 0 ).or( sampleUV.y.lessThanEqual( 0 ) ).or( sampleUV.x.greaterThanEqual( 1 ) ).or( sampleUV.y.greaterThanEqual( 1 ) ), () => {
- Break();
- } );
- const sampleViewPosition = getViewPosition( sampleUV, sampleDepth( sampleUV ), this._cameraProjectionMatrixInverse ).toConst();
- const pixelToSample = sampleViewPosition.sub( viewPosition ).normalize().toConst();
- const linearThicknessMultiplier = this.useLinearThickness.equal( true ).select( sampleViewPosition.z.negate().div( this._cameraFar ).clamp().mul( 100 ), float( 1 ) );
- const pixelToSampleBackface = normalize( sampleViewPosition.sub( linearThicknessMultiplier.mul( viewDir ).mul( THICKNESS ) ).sub( viewPosition ) );
- let frontBackHorizon = vec2( dot( pixelToSample, viewDir ), dot( pixelToSampleBackface, viewDir ) );
- frontBackHorizon = GTAOFastAcos( clamp( frontBackHorizon, - 1, 1 ) );
- frontBackHorizon = clamp( div( mul( samplingDirection, frontBackHorizon.negate() ).sub( n.sub( HALF_PI ) ), PI ) ); // Port note: subtract half pi instead of adding it
- frontBackHorizon = directionIsRight.equal( true ).select( frontBackHorizon.yx, frontBackHorizon.xy ); // Front/Back get inverted depending on angle
- // inline ComputeOccludedBitfield() for easier debugging
- const minHorizon = frontBackHorizon.x.toConst();
- const maxHorizon = frontBackHorizon.y.toConst();
- const startHorizonInt = uint( frontBackHorizon.mul( float( MAX_RAY ) ) ).toConst();
- const angleHorizonInt = uint( ceil( maxHorizon.sub( minHorizon ).mul( float( MAX_RAY ) ) ) ).toConst();
- const angleHorizonBitfield = angleHorizonInt.greaterThan( uint( 0 ) ).select( uint( shiftRight( uint( 0xFFFFFFFF ), uint( 32 ).sub( MAX_RAY ).add( MAX_RAY.sub( angleHorizonInt ) ) ) ), uint( 0 ) ).toConst();
- let currentOccludedBitfield = angleHorizonBitfield.shiftLeft( startHorizonInt );
- currentOccludedBitfield = currentOccludedBitfield.bitAnd( globalOccludedBitfield.bitNot() );
- globalOccludedBitfield.assign( globalOccludedBitfield.bitOr( currentOccludedBitfield ) );
- const numOccludedZones = countOneBits( currentOccludedBitfield );
- //
- If( numOccludedZones.greaterThan( 0 ), () => { // If a ray hit the sample, that sample is visible from shading point
- const lightColor = sampleBeauty( sampleUV );
- If( luminance( lightColor ).greaterThan( 0.001 ), () => { // Continue if there is light at that location (intensity > 0)
- const lightDirectionVS = normalize( pixelToSample );
- const normalDotLightDirection = clamp( dot( viewNormal, lightDirectionVS ) );
- If( normalDotLightDirection.greaterThan( 0.001 ), () => { // Continue if light is facing surface normal
- const lightNormalVS = sampleNormal( sampleUV );
- // Intensity of outgoing light in the direction of the shading point
- let lightNormalDotLightDirection = dot( lightNormalVS, lightDirectionVS.negate() );
- const d = sign( lightNormalDotLightDirection ).lessThan( 0 ).select( abs( lightNormalDotLightDirection ).mul( BACKFACE_LIGHTING ), abs( lightNormalDotLightDirection ) );
- lightNormalDotLightDirection = BACKFACE_LIGHTING.greaterThan( 0 ).and( dot( lightNormalVS, viewDir ).greaterThan( 0 ) ).select( d, clamp( lightNormalDotLightDirection ) );
- color.rgb.addAssign( float( numOccludedZones ).div( float( MAX_RAY ) ).mul( lightColor ).mul( normalDotLightDirection ).mul( lightNormalDotLightDirection ) );
- } );
- } );
- } );
- lastSampleViewPosition.assign( sampleViewPosition );
- } );
- return vec3( color );
- } );
- const gi = Fn( () => {
- const depth = sampleDepth( uvNode ).toVar();
- depth.greaterThanEqual( 1.0 ).discard();
- const viewPosition = getViewPosition( uvNode, depth, this._cameraProjectionMatrixInverse ).toVar();
- const viewNormal = sampleNormal( uvNode ).toVar();
- const viewDir = normalize( viewPosition.xyz.negate() ).toVar();
- //
- const noiseOffset = spatialOffsets( screenCoordinate );
- const noiseDirection = interleavedGradientNoise( screenCoordinate );
- const noiseJitterIdx = this._temporalDirection.mul( 0.02 ); // Port: Add noiseJitterIdx here for slightly better noise convergence with TRAA (see #31890 for more details)
- const initialRayStep = fract( noiseOffset.add( this._temporalOffset ) ).add( rand( uvNode.add( noiseJitterIdx ).mul( 2 ).sub( 1 ) ) );
- const ao = float( 0 );
- const color = vec3( 0 );
- const ROTATION_COUNT = this.sliceCount.toConst();
- const AO_INTENSITY = this.aoIntensity.toConst();
- const GI_INTENSITY = this.giIntensity.toConst();
- const RADIUS = this.radius.toConst();
- Loop( { start: uint( 0 ), end: ROTATION_COUNT, type: 'uint', condition: '<' }, ( { i } ) => {
- const rotationAngle = mul( float( i ).add( noiseDirection ).add( this._temporalDirection ), PI.div( float( ROTATION_COUNT ) ) ).toConst();
- const sliceDir = vec3( vec2( cos( rotationAngle ), sin( rotationAngle ) ), 0 ).toConst();
- const slideDirTexelSize = sliceDir.xy.mul( float( 1 ).div( this._resolution ) ).toConst();
- const planeNormal = normalize( cross( sliceDir, viewDir ) ).toConst();
- const tangent = cross( viewDir, planeNormal ).toConst();
- const projectedNormal = viewNormal.sub( planeNormal.mul( dot( viewNormal, planeNormal ) ) ).toConst();
- const projectedNormalNormalized = normalize( projectedNormal ).toConst();
- const cos_n = clamp( dot( projectedNormalNormalized, viewDir ), - 1, 1 ).toConst();
- const n = sign( dot( projectedNormal, tangent ) ).negate().mul( acos( cos_n ) ).toConst();
- globalOccludedBitfield.assign( 0 );
- color.addAssign( horizonSampling( bool( true ), RADIUS, viewPosition, slideDirTexelSize, initialRayStep, uvNode, viewDir, viewNormal, n ) );
- color.addAssign( horizonSampling( bool( false ), RADIUS, viewPosition, slideDirTexelSize, initialRayStep, uvNode, viewDir, viewNormal, n ) );
- ao.addAssign( float( countOneBits( globalOccludedBitfield ) ).div( float( MAX_RAY ) ) );
- } );
- ao.divAssign( float( ROTATION_COUNT ) );
- ao.assign( pow( ao.clamp().oneMinus(), AO_INTENSITY ).clamp() );
- color.divAssign( float( ROTATION_COUNT ) );
- color.mulAssign( GI_INTENSITY );
- // scale color based on luminance
- const maxLuminance = float( 7 ).toConst(); // 7 represent a HDR luminance value
- const currentLuminance = luminance( color );
- const scale = currentLuminance.greaterThan( maxLuminance ).select( maxLuminance.div( currentLuminance ), float( 1 ) );
- color.mulAssign( scale );
- return vec4( color, ao );
- } );
- this._material.fragmentNode = gi().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._ssgiRenderTarget.dispose();
- this._material.dispose();
- }
- }
- export default SSGINode;
- /**
- * TSL function for creating a SSGI effect.
- *
- * @tsl
- * @function
- * @param {TextureNode} beautyNode - The texture node that represents the input of the effect.
- * @param {TextureNode} depthNode - A texture node that represents the scene's depth.
- * @param {TextureNode} normalNode - A texture node that represents the scene's normals.
- * @param {Camera} camera - The camera the scene is rendered with.
- * @returns {SSGINode}
- */
- export const ssgi = ( beautyNode, depthNode, normalNode, camera ) => nodeObject( new SSGINode( convertToTexture( beautyNode ), depthNode, normalNode, camera ) );
|