diff --git a/examples/jsm/tsl/display/GTAONode.js b/examples/jsm/tsl/display/GTAONode.js index 4080a0f22a1a4b..20963b493587cc 100644 --- a/examples/jsm/tsl/display/GTAONode.js +++ b/examples/jsm/tsl/display/GTAONode.js @@ -4,6 +4,9 @@ import { reference, logarithmicDepthToViewZ, viewZToPerspectiveDepth, getNormalF 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 ]; + let _rendererState; /** @@ -150,6 +153,20 @@ class GTAONode extends TempNode { */ this.samples = uniform( 16 ); + /** + * 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` + * might be required. + * + * @type {boolean} + * @default false + */ + this.useTemporalFiltering = false; + /** * The node represents the internal noise texture used by the AO. * @@ -190,6 +207,13 @@ class GTAONode extends TempNode { */ this._cameraFar = reference( 'far', 'float', camera ); + /** + * Temporal direction that influences the rotation angle for each slice. + * + * @type {UniformNode} + */ + this._temporalDirection = uniform( 0 ); + /** * The material that is used to render the effect. * @@ -247,6 +271,20 @@ class GTAONode extends TempNode { _rendererState = RendererUtils.resetRendererState( renderer, _rendererState ); + // update temporal uniforms + + if ( this.useTemporalFiltering === true ) { + + const frameId = frame.frameId; + + this._temporalDirection.value = _temporalRotations[ frameId % 6 ] / 360; + + } else { + + this._temporalDirection.value = 0; + + } + // const size = renderer.getDrawingBufferSize( _size ); @@ -313,6 +351,7 @@ class GTAONode extends TempNode { const noiseResolution = textureSize( this._noiseNode, 0 ); let noiseUv = vec2( uvNode.x, uvNode.y.oneMinus() ); noiseUv = noiseUv.mul( this.resolution.div( noiseResolution ) ); + const noiseTexel = sampleNoise( noiseUv ); const randomVec = noiseTexel.xyz.mul( 2.0 ).sub( 1.0 ); const tangent = vec3( randomVec.xy, 0.0 ).normalize(); @@ -328,7 +367,7 @@ class GTAONode extends TempNode { Loop( { start: int( 0 ), end: DIRECTIONS, type: 'int', condition: '<' }, ( { i } ) => { - const angle = float( i ).div( float( DIRECTIONS ) ).mul( PI ).toVar(); + const angle = float( i ).div( float( DIRECTIONS ) ).mul( PI ).add( this._temporalDirection ).toVar(); const sampleDir = vec4( cos( angle ), sin( angle ), 0., add( 0.5, mul( 0.5, noiseTexel.w ) ) ); sampleDir.xyz = normalize( kernelMatrix.mul( sampleDir.xyz ) ); diff --git a/examples/webgpu_postprocessing_ao.html b/examples/webgpu_postprocessing_ao.html index ba1510499c70fd..228f580e5b5b65 100644 --- a/examples/webgpu_postprocessing_ao.html +++ b/examples/webgpu_postprocessing_ao.html @@ -121,6 +121,7 @@ aoPass = ao( scenePassDepth, scenePassNormal, camera ).toInspector( 'AO' ); aoPass.resolutionScale = 0.5; // running AO in half resolution is often sufficient + aoPass.useTemporalFiltering = true; blendPassAO = vec4( scenePassColor.rgb.mul( aoPass.r ), scenePassColor.a ); // the AO is stored only in the red channel // traa @@ -167,6 +168,7 @@ gui.add( params, 'radius', 0.1, 1 ).onChange( updateParameters ); gui.add( params, 'scale', 0.01, 2 ).onChange( updateParameters ); gui.add( params, 'thickness', 0.01, 2 ).onChange( updateParameters ); + gui.add( aoPass, 'useTemporalFiltering' ).name( 'temporal filtering' ); gui.add( params, 'aoOnly' ).onChange( ( value ) => { if ( value === true ) {