From 291280b6a55b2e17526352745e84b62a66140055 Mon Sep 17 00:00:00 2001 From: sunag Date: Sat, 11 Apr 2026 00:18:26 -0300 Subject: [PATCH 1/3] WebGPUTextureUtils: Fix `copyTextureToBuffer()` dispose buffer (#33366) --- src/renderers/webgpu/utils/WebGPUTextureUtils.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/renderers/webgpu/utils/WebGPUTextureUtils.js b/src/renderers/webgpu/utils/WebGPUTextureUtils.js index 5191db62c63ba0..de2cf587acc18b 100644 --- a/src/renderers/webgpu/utils/WebGPUTextureUtils.js +++ b/src/renderers/webgpu/utils/WebGPUTextureUtils.js @@ -692,7 +692,9 @@ class WebGPUTextureUtils { await readBuffer.mapAsync( GPUMapMode.READ ); - const buffer = readBuffer.getMappedRange(); + const buffer = readBuffer.getMappedRange().slice(); + + readBuffer.destroy(); return new typedArrayType( buffer ); From 893add1de9472714c456a7975faeb85496c37e9c Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Sat, 11 Apr 2026 12:04:24 +0200 Subject: [PATCH 2/3] TAAUNode: Adding new node for TAA with Upsampling. (#33368) --- examples/files.json | 1 + examples/jsm/tsl/display/SharpenNode.js | 283 ++++++ examples/jsm/tsl/display/TAAUNode.js | 835 ++++++++++++++++++ .../screenshots/webgpu_upscaling_taau.jpg | Bin 0 -> 51554 bytes examples/webgpu_upscaling_fsr1.html | 2 - examples/webgpu_upscaling_taau.html | 205 +++++ 6 files changed, 1324 insertions(+), 2 deletions(-) create mode 100644 examples/jsm/tsl/display/SharpenNode.js create mode 100644 examples/jsm/tsl/display/TAAUNode.js create mode 100644 examples/screenshots/webgpu_upscaling_taau.jpg create mode 100644 examples/webgpu_upscaling_taau.html diff --git a/examples/files.json b/examples/files.json index 7d41857ce5a9a1..6c4650c977ad6d 100644 --- a/examples/files.json +++ b/examples/files.json @@ -493,6 +493,7 @@ "webgpu_tsl_vfx_tornado", "webgpu_tsl_wood", "webgpu_upscaling_fsr1", + "webgpu_upscaling_taau", "webgpu_video_frame", "webgpu_video_panorama", "webgpu_volume_caustics", diff --git a/examples/jsm/tsl/display/SharpenNode.js b/examples/jsm/tsl/display/SharpenNode.js new file mode 100644 index 00000000000000..90535795160ac2 --- /dev/null +++ b/examples/jsm/tsl/display/SharpenNode.js @@ -0,0 +1,283 @@ +import { HalfFloatType, RenderTarget, Vector2, NodeMaterial, RendererUtils, QuadMesh, TempNode, NodeUpdateType } from 'three/webgpu'; +import { Fn, float, vec3, vec4, ivec2, int, uv, floor, abs, max, min, exp2, nodeObject, passTexture, textureSize, textureLoad, convertToTexture } from 'three/tsl'; + +const _quadMesh = /*@__PURE__*/ new QuadMesh(); +const _size = /*@__PURE__*/ new Vector2(); + +let _rendererState; + +/** + * Post processing node for contrast-adaptive sharpening (RCAS). + * + * Reference: {@link https://gpuopen.com/fidelityfx-superresolution/}. + * + * @augments TempNode + * @three_import import { sharpen } from 'three/addons/tsl/display/SharpenNode.js'; + */ +class SharpenNode extends TempNode { + + static get type() { + + return 'SharpenNode'; + + } + + /** + * Constructs a new sharpen node. + * + * @param {TextureNode} textureNode - The texture node that represents the input of the effect. + * @param {Node} [sharpness=0.2] - Sharpening strength. 0 = maximum sharpening, 2 = no sharpening. + * @param {Node} [denoise=false] - Whether to attenuate sharpening in noisy areas. + */ + constructor( textureNode, sharpness = 0.2, denoise = false ) { + + super( 'vec4' ); + + /** + * This flag can be used for type testing. + * + * @type {boolean} + * @readonly + * @default true + */ + this.isSharpenNode = true; + + /** + * The texture node that represents the input of the effect. + * + * @type {TextureNode} + */ + this.textureNode = textureNode; + + /** + * Sharpening strength. 0 = maximum, 2 = none. + * + * @type {Node} + * @default 0.2 + */ + this.sharpness = nodeObject( sharpness ); + + /** + * Whether to attenuate sharpening in noisy areas. + * + * @type {Node} + * @default false + */ + this.denoise = nodeObject( denoise ); + + /** + * The render target for the sharpening pass. + * + * @private + * @type {RenderTarget} + */ + this._renderTarget = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } ); + this._renderTarget.texture.name = 'SharpenNode.output'; + + /** + * The result of the effect as a texture node. + * + * @private + * @type {PassTextureNode} + */ + this._textureNode = passTexture( this, this._renderTarget.texture ); + + /** + * The material for the sharpening pass. + * + * @private + * @type {?NodeMaterial} + */ + this._material = null; + + /** + * 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; + + } + + /** + * Sets the output size of the effect. + * + * @param {number} width - The width in pixels. + * @param {number} height - The height in pixels. + */ + setSize( width, height ) { + + this._renderTarget.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 ); + + // + + renderer.getDrawingBufferSize( _size ); + this.setSize( _size.x, _size.y ); + + renderer.setRenderTarget( this._renderTarget ); + + _quadMesh.material = this._material; + _quadMesh.name = 'Sharpen [ RCAS ]'; + _quadMesh.render( renderer ); + + // + + RendererUtils.restoreRendererState( renderer, _rendererState ); + + } + + /** + * 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; + + } + + /** + * This method is used to setup the effect's TSL code. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {PassTextureNode} + */ + setup( builder ) { + + const textureNode = this.textureNode; + const inputTex = textureNode.value; + + // RCAS: Robust Contrast-Adaptive Sharpening. + // + // Ported from AMD FidelityFX FSR 1 (ffx_fsr1.h). Uses a 5-tap + // cross pattern (center + up/down/left/right) to compute a + // per-pixel sharpening weight that is automatically limited by + // local contrast to avoid ringing. An optional noise-attenuation + // factor reduces sharpening in noisy areas. + + const rcas = Fn( () => { + + const targetUV = uv(); + const texSize = textureSize( textureLoad( inputTex ) ); + + const p = ivec2( int( floor( targetUV.x.mul( texSize.x ) ) ), int( floor( targetUV.y.mul( texSize.y ) ) ) ).toConst(); + + const e = textureLoad( inputTex, p ); + const b = textureLoad( inputTex, p.add( ivec2( 0, - 1 ) ) ); + const d = textureLoad( inputTex, p.add( ivec2( - 1, 0 ) ) ); + const f = textureLoad( inputTex, p.add( ivec2( 1, 0 ) ) ); + const h = textureLoad( inputTex, p.add( ivec2( 0, 1 ) ) ); + + // Approximate luminance (luma times 2). + + const luma = ( s ) => s.g.add( s.b.add( s.r ).mul( 0.5 ) ); + + const bL = luma( b ); + const dL = luma( d ); + const eL = luma( e ); + const fL = luma( f ); + const hL = luma( h ); + + // Sharpening amount from user parameter. + + const con = exp2( this.sharpness.negate() ).toConst(); + + // Min and max of ring. + + const mn4 = min( min( b.rgb, d.rgb ), min( f.rgb, h.rgb ) ).toConst(); + const mx4 = max( max( b.rgb, d.rgb ), max( f.rgb, h.rgb ) ).toConst(); + + // Compute adaptive lobe weight. + // Limiters based on how much sharpening the local contrast can tolerate. + + const RCAS_LIMIT = float( 0.25 - 1.0 / 16.0 ).toConst(); + + const hitMin = min( mn4, e.rgb ).div( mx4.mul( 4.0 ) ).toConst(); + const hitMax = vec3( 1.0 ).sub( max( mx4, e.rgb ) ).div( mn4.mul( 4.0 ).sub( 4.0 ) ).toConst(); + const lobeRGB = max( hitMin.negate(), hitMax ).toConst(); + + const lobe = max( + RCAS_LIMIT.negate(), + min( max( lobeRGB.r, max( lobeRGB.g, lobeRGB.b ) ), float( 0.0 ) ) + ).mul( con ).toConst(); + + // Noise attenuation. + + const nz = bL.add( dL ).add( fL ).add( hL ).mul( 0.25 ).sub( eL ).toConst(); + const nzRange = max( max( bL, dL ), max( eL, max( fL, hL ) ) ).sub( min( min( bL, dL ), min( eL, min( fL, hL ) ) ) ).toConst(); + const nzFactor = float( 1.0 ).sub( abs( nz ).div( max( nzRange, float( 1.0 / 65536.0 ) ) ).saturate().mul( 0.5 ) ).toConst(); + + const effectiveLobe = this.denoise.equal( true ).select( lobe.mul( nzFactor ), lobe ).toConst(); + + // Resolve: weighted blend of cross neighbors and center. + + const result = b.rgb.add( d.rgb ).add( f.rgb ).add( h.rgb ).mul( effectiveLobe ).add( e.rgb ) + .div( effectiveLobe.mul( 4.0 ).add( 1.0 ) ).toConst(); + + return vec4( result, e.a ); + + } ); + + // + + const context = builder.getSharedContext(); + + const material = this._material || ( this._material = new NodeMaterial() ); + material.fragmentNode = rcas().context( context ); + material.name = 'Sharpen_RCAS'; + material.needsUpdate = true; + + // + + const properties = builder.getNodeProperties( this ); + properties.textureNode = textureNode; + + // + + return this._textureNode; + + } + + /** + * Frees internal resources. This method should be called + * when the effect is no longer required. + */ + dispose() { + + this._renderTarget.dispose(); + + if ( this._material !== null ) this._material.dispose(); + + } + +} + +export default SharpenNode; + +/** + * TSL function for creating a sharpen node for post processing. + * + * @tsl + * @function + * @param {Node} node - The node that represents the input of the effect. + * @param {(number|Node)} [sharpness=0.2] - Sharpening strength. 0 = maximum, 2 = none. + * @param {(boolean|Node)} [denoise=false] - Whether to attenuate sharpening in noisy areas. + * @returns {SharpenNode} + */ +export const sharpen = ( node, sharpness, denoise ) => new SharpenNode( convertToTexture( node ), sharpness, denoise ); diff --git a/examples/jsm/tsl/display/TAAUNode.js b/examples/jsm/tsl/display/TAAUNode.js new file mode 100644 index 00000000000000..cb66854671639b --- /dev/null +++ b/examples/jsm/tsl/display/TAAUNode.js @@ -0,0 +1,835 @@ +import { HalfFloatType, Vector2, RenderTarget, RendererUtils, QuadMesh, NodeMaterial, TempNode, NodeUpdateType, Matrix4, DepthTexture } from 'three/webgpu'; +import { add, exp, float, If, Fn, max, texture, uniform, uv, vec2, vec4, luminance, convertToTexture, passTexture, velocity, getViewPosition, viewZToPerspectiveDepth, struct, ivec2, mix, property, outputStruct } from 'three/tsl'; + +const _quadMesh = /*@__PURE__*/ new QuadMesh(); +const _size = /*@__PURE__*/ new Vector2(); + +let _rendererState; + + +/** + * A special node that performs Temporal Anti-Aliasing Upscaling (TAAU). + * + * Like TRAA, the node accumulates jittered samples over multiple frames and + * reprojects history with motion vectors. Unlike TRAA, the input buffers + * (beauty, depth, velocity) are expected to be rendered at a lower resolution + * than the renderer's drawing buffer — typically by lowering the upstream + * pass's resolution via {@link PassNode#setResolutionScale} — and the resolve + * pass reconstructs an output-resolution image using a 9-tap Blackman-Harris + * filter (Gaussian approximation) over the jittered input samples. The result + * is an alternative to FSR2/3 that does anti-aliasing and upscaling in a + * single pass. + * + * References: + * - Karis, "High Quality Temporal Supersampling", SIGGRAPH 2014, {@link https://advances.realtimerendering.com/s2014/} + * - Riley/Arcila, FidelityFX Super Resolution 2, GDC 2022, {@link https://gpuopen.com/download/GDC_FidelityFX_Super_Resolution_2_0.pdf} + * + * Note: MSAA must be disabled when TAAU is in use. + * + * @augments TempNode + * @three_import import { taau } from 'three/addons/tsl/display/TAAUNode.js'; + */ +class TAAUNode extends TempNode { + + static get type() { + + return 'TAAUNode'; + + } + + /** + * Constructs a new TAAU node. + * + * @param {TextureNode} beautyNode - The texture node that represents the input of the effect. + * @param {TextureNode} depthNode - A node that represents the scene's depth. + * @param {TextureNode} velocityNode - A node that represents the scene's velocity. + * @param {Camera} camera - The camera the scene is rendered with. + */ + constructor( beautyNode, depthNode, velocityNode, camera ) { + + super( 'vec4' ); + + /** + * This flag can be used for type testing. + * + * @type {boolean} + * @readonly + * @default true + */ + this.isTAAUNode = true; + + /** + * 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 texture node that represents the input of the effect. + * + * @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 velocity. + * + * @type {TextureNode} + */ + this.velocityNode = velocityNode; + + /** + * The camera the scene is rendered with. + * + * @type {Camera} + */ + this.camera = camera; + + /** + * When the difference between the current and previous depth goes above this threshold, + * the history is considered invalid. + * + * @type {number} + * @default 0.0005 + */ + this.depthThreshold = 0.0005; + + /** + * The depth difference within the 3×3 neighborhood to consider a pixel as an edge. + * + * @type {number} + * @default 0.001 + */ + this.edgeDepthDiff = 0.001; + + /** + * The history becomes invalid as the pixel length of the velocity approaches this value. + * + * @type {number} + * @default 128 + */ + this.maxVelocityLength = 128; + + /** + * Baseline weight applied to the current frame in the resolve. Lower + * values produce smoother results with longer accumulation but slower + * convergence on disoccluded regions; the motion factor is added on + * top, so fast-moving pixels still respond quickly. + * + * @type {number} + * @default 0.025 + */ + this.currentFrameWeight = 0.025; + + /** + * The jitter index selects the current camera offset value. + * + * @private + * @type {number} + * @default 0 + */ + this._jitterIndex = 0; + + /** + * A uniform node holding the current jitter offset in input-pixel + * units. The shader needs this to know where each input sample was + * actually rendered when computing per-tap reconstruction weights. + * + * @private + * @type {UniformNode} + */ + this._jitterOffset = uniform( new Vector2() ); + + /** + * The render target that represents the history of frame data. + * Sized to the renderer's drawing buffer (the output resolution). + * + * @private + * @type {?RenderTarget} + */ + this._historyRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType, count: 2 } ); + this._historyRenderTarget.textures[ 0 ].name = 'TAAUNode.history.color'; + this._historyRenderTarget.textures[ 1 ].name = 'TAAUNode.history.lock'; + + /** + * The render target for the resolve. Sized to the renderer's drawing + * buffer (the output resolution). + * + * @private + * @type {?RenderTarget} + */ + this._resolveRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } ); + this._resolveRenderTarget.texture.name = 'TAAUNode.resolve'; + + /** + * Render target whose depth attachment holds the previous frame's + * depth buffer. The depth texture must be owned by a render target + * so that `copyTextureToTexture` can copy into it on the WebGL + * backend, which uses a framebuffer blit and therefore needs the + * destination depth texture to be attached to a framebuffer. This + * render target is sized independently of the history target so it + * can match the (lower-resolution) input depth texture. + * + * @private + * @type {RenderTarget} + */ + this._previousDepthRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false, depthTexture: new DepthTexture() } ); + this._previousDepthRenderTarget.depthTexture.name = 'TAAUNode.previousDepth'; + + /** + * Material used for the resolve step. + * + * @private + * @type {NodeMaterial} + */ + this._resolveMaterial = new NodeMaterial(); + this._resolveMaterial.name = 'TAAU.resolve'; + + /** + * Material used to seed the history render target on resize. It + * performs a bilinear upscale of the current beauty buffer into the + * output-sized history target so that the first frames after a + * resize do not fade in from black. + * + * @private + * @type {NodeMaterial} + */ + this._seedMaterial = new NodeMaterial(); + this._seedMaterial.name = 'TAAU.seed'; + + /** + * The result of the effect is represented as a separate texture node. + * + * @private + * @type {PassTextureNode} + */ + this._textureNode = passTexture( this, this._resolveRenderTarget.texture ); + + /** + * Used to save the original/unjittered projection matrix. + * + * @private + * @type {Matrix4} + */ + this._originalProjectionMatrix = new Matrix4(); + + /** + * A uniform node holding the camera's near and far. + * + * @private + * @type {UniformNode} + */ + this._cameraNearFar = uniform( new Vector2() ); + + /** + * A uniform node holding the camera world matrix. + * + * @private + * @type {UniformNode} + */ + this._cameraWorldMatrix = uniform( new Matrix4() ); + + /** + * A uniform node holding the camera world matrix inverse. + * + * @private + * @type {UniformNode} + */ + this._cameraWorldMatrixInverse = uniform( new Matrix4() ); + + /** + * A uniform node holding the camera projection matrix inverse. + * + * @private + * @type {UniformNode} + */ + this._cameraProjectionMatrixInverse = uniform( new Matrix4() ); + + /** + * A uniform node holding the previous frame's view matrix. + * + * @private + * @type {UniformNode} + */ + this._previousCameraWorldMatrix = uniform( new Matrix4() ); + + /** + * A uniform node holding the previous frame's projection matrix inverse. + * + * @private + * @type {UniformNode} + */ + this._previousCameraProjectionMatrixInverse = uniform( new Matrix4() ); + + /** + * A texture node for the previous depth buffer. + * + * @private + * @type {TextureNode} + */ + this._previousDepthNode = texture( this._previousDepthRenderTarget.depthTexture ); + + /** + * Sync the post processing stack with the TAAU node. + * + * @private + * @type {boolean} + */ + this._needsPostProcessingSync = false; + + } + + /** + * 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 output size of the effect (history and resolve targets). The + * previous-depth texture is sized independently in `updateBefore()` to + * track the scene's current depth texture. + * + * @param {number} outputWidth - The output width (drawing buffer width). + * @param {number} outputHeight - The output height (drawing buffer height). + */ + setSize( outputWidth, outputHeight ) { + + this._historyRenderTarget.setSize( outputWidth, outputHeight ); + this._resolveRenderTarget.setSize( outputWidth, outputHeight ); + + } + + /** + * Defines the TAAU's current jitter as a view offset to the scene's + * camera. The jitter is shrunk to one *output* pixel (rather than one + * input pixel) so that the halton sequence gradually fills the output + * sub-pixel grid over multiple frames. + * + * @param {number} inputWidth - The width of the input buffers the camera renders into. + * @param {number} inputHeight - The height of the input buffers the camera renders into. + */ + setViewOffset( inputWidth, inputHeight ) { + + // save original/unjittered projection matrix for velocity pass + + this.camera.updateProjectionMatrix(); + this._originalProjectionMatrix.copy( this.camera.projectionMatrix ); + + velocity.setProjectionMatrix( this._originalProjectionMatrix ); + + // The jitter range must span one output pixel (not one input pixel), + // so we shrink the input-pixel-unit offset by the ratio of input to + // output resolution. + + const haltonOffset = _haltonOffsets[ this._jitterIndex ]; + const jitterX = ( haltonOffset[ 0 ] - 0.5 ); + const jitterY = ( haltonOffset[ 1 ] - 0.5 ); + + this._jitterOffset.value.set( jitterX, jitterY ); + + this.camera.setViewOffset( + + inputWidth, inputHeight, + + jitterX, jitterY, + + inputWidth, inputHeight + + ); + + } + + /** + * Clears the view offset from the scene's camera. + */ + clearViewOffset() { + + this.camera.clearViewOffset(); + + velocity.setProjectionMatrix( null ); + + // update jitter index + + this._jitterIndex ++; + this._jitterIndex = this._jitterIndex % ( _haltonOffsets.length - 1 ); + + } + + /** + * This method is used to render the effect once per frame. + * + * @param {NodeFrame} frame - The current node frame. + */ + updateBefore( frame ) { + + const { renderer } = frame; + + // store previous frame matrices before updating current ones + + this._previousCameraWorldMatrix.value.copy( this._cameraWorldMatrix.value ); + this._previousCameraProjectionMatrixInverse.value.copy( this._cameraProjectionMatrixInverse.value ); + + // update camera matrices uniforms + + this._cameraNearFar.value.set( this.camera.near, this.camera.far ); + this._cameraWorldMatrix.value.copy( this.camera.matrixWorld ); + this._cameraWorldMatrixInverse.value.copy( this.camera.matrixWorldInverse ); + this._cameraProjectionMatrixInverse.value.copy( this.camera.projectionMatrixInverse ); + + // extract input dimensions from the beauty buffer and output + // dimensions from the renderer's drawing buffer + + const beautyRenderTarget = ( this.beautyNode.isRTTNode ) ? this.beautyNode.renderTarget : this.beautyNode.passNode.renderTarget; + + const inputWidth = beautyRenderTarget.texture.width; + const inputHeight = beautyRenderTarget.texture.height; + + const drawingBufferSize = renderer.getDrawingBufferSize( _size ); + const outputWidth = drawingBufferSize.width; + const outputHeight = drawingBufferSize.height; + + // + + _rendererState = RendererUtils.resetRendererState( renderer, _rendererState ); + + // + + const needsRestart = + this._historyRenderTarget.width !== outputWidth || + this._historyRenderTarget.height !== outputHeight; + + this.setSize( outputWidth, outputHeight ); + + // every time the dimensions change we need fresh history data + + if ( needsRestart === true ) { + + // make sure render targets are initialized after the resize which triggers a dispose() + + renderer.initRenderTarget( this._historyRenderTarget ); + renderer.initRenderTarget( this._resolveRenderTarget ); + + // Seed the history with a bilinear upscale of the current beauty + // buffer. Without this the first frames after a resize fade in + // from black because the history target was cleared. The seed + // material is a quad pass that samples beauty at output UVs, so + // it produces an output-sized image regardless of the input size. + + renderer.setRenderTarget( this._historyRenderTarget ); + _quadMesh.material = this._seedMaterial; + _quadMesh.name = 'TAAU.seed'; + _quadMesh.render( renderer ); + renderer.setRenderTarget( null ); + + } + + // must run after needsRestart so it does not affect the seed reset + + if ( this._needsPostProcessingSync === true ) { + + this.setViewOffset( inputWidth, inputHeight ); + + this._needsPostProcessingSync = false; + + } + + // resolve + + renderer.setRenderTarget( this._resolveRenderTarget ); + _quadMesh.material = this._resolveMaterial; + _quadMesh.name = 'TAAU'; + _quadMesh.render( renderer ); + renderer.setRenderTarget( null ); + + // update history + + renderer.copyTextureToTexture( this._resolveRenderTarget.texture, this._historyRenderTarget.texture ); + + // Copy the current scene depth into the previous-depth texture. We + // keep the destination size locked to the source's actual dimensions + // so that any one-frame timing mismatch between the scene pass's depth + // attachment and the beauty render target's bookkeeping cannot + // produce a copy with mismatched extents (which WebGPU rejects for + // depth/stencil formats). + + const currentDepth = this.depthNode.value; + const srcW = currentDepth.image !== null && currentDepth.image !== undefined ? currentDepth.image.width : 0; + const srcH = currentDepth.image !== null && currentDepth.image !== undefined ? currentDepth.image.height : 0; + + if ( srcW > 0 && srcH > 0 ) { + + if ( this._previousDepthRenderTarget.width !== srcW || this._previousDepthRenderTarget.height !== srcH ) { + + this._previousDepthRenderTarget.setSize( srcW, srcH ); + renderer.initRenderTarget( this._previousDepthRenderTarget ); + + } + + const dstDepth = this._previousDepthRenderTarget.depthTexture; + renderer.copyTextureToTexture( currentDepth, dstDepth ); + this._previousDepthNode.value = dstDepth; + + } + + // restore + + RendererUtils.restoreRendererState( renderer, _rendererState ); + + } + + /** + * This method is used to setup the effect's render targets and TSL code. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {PassTextureNode} + */ + setup( builder ) { + + const renderPipeline = builder.context.renderPipeline; + + if ( renderPipeline ) { + + this._needsPostProcessingSync = true; + + renderPipeline.context.onBeforeRenderPipeline = () => { + + const beautyRenderTarget = ( this.beautyNode.isRTTNode ) ? this.beautyNode.renderTarget : this.beautyNode.passNode.renderTarget; + + const inputWidth = beautyRenderTarget.texture.width; + const inputHeight = beautyRenderTarget.texture.height; + + this.setViewOffset( inputWidth, inputHeight ); + + }; + + renderPipeline.context.onAfterRenderPipeline = () => { + + this.clearViewOffset(); + + }; + + } + + const currentDepthStruct = struct( { + + closestDepth: 'float', + closestPositionTexel: 'vec2', + farthestDepth: 'float', + + } ); + + // Samples 3×3 neighborhood pixels and returns the closest and farthest depths. + const sampleCurrentDepth = Fn( ( [ positionTexel ] ) => { + + const closestDepth = float( 2 ).toVar(); + const closestPositionTexel = vec2( 0 ).toVar(); + const farthestDepth = float( - 1 ).toVar(); + + for ( let x = - 1; x <= 1; ++ x ) { + + for ( let y = - 1; y <= 1; ++ y ) { + + const neighbor = positionTexel.add( vec2( x, y ) ).toVar(); + const depth = this.depthNode.load( neighbor ).r.toVar(); + + If( depth.lessThan( closestDepth ), () => { + + closestDepth.assign( depth ); + closestPositionTexel.assign( neighbor ); + + } ); + + If( depth.greaterThan( farthestDepth ), () => { + + farthestDepth.assign( depth ); + + } ); + + } + + } + + return currentDepthStruct( closestDepth, closestPositionTexel, farthestDepth ); + + } ); + + // Samples a previous depth and reproject it using the current camera matrices. + const samplePreviousDepth = ( uv ) => { + + const depth = this._previousDepthNode.sample( uv ).r; + const positionView = getViewPosition( uv, depth, this._previousCameraProjectionMatrixInverse ); + const positionWorld = this._previousCameraWorldMatrix.mul( vec4( positionView, 1 ) ).xyz; + const viewZ = this._cameraWorldMatrixInverse.mul( vec4( positionWorld, 1 ) ).z; + return viewZToPerspectiveDepth( viewZ, this._cameraNearFar.x, this._cameraNearFar.y ); + + }; + + // Optimized version of AABB clipping. + // Reference: https://github.com/playdeadgames/temporal + const clipAABB = Fn( ( [ currentColor, historyColor, minColor, maxColor ] ) => { + + const pClip = maxColor.rgb.add( minColor.rgb ).mul( 0.5 ); + const eClip = maxColor.rgb.sub( minColor.rgb ).mul( 0.5 ).add( 1e-7 ); + const vClip = historyColor.sub( vec4( pClip, currentColor.a ) ); + const vUnit = vClip.xyz.div( eClip ); + const absUnit = vUnit.abs(); + const maxUnit = max( absUnit.x, absUnit.y, absUnit.z ); + return maxUnit.greaterThan( 1 ).select( + vec4( pClip, currentColor.a ).add( vClip.div( maxUnit ) ), + historyColor + ); + + } ).setLayout( { + name: 'clipAABB', + type: 'vec4', + inputs: [ + { name: 'currentColor', type: 'vec4' }, + { name: 'historyColor', type: 'vec4' }, + { name: 'minColor', type: 'vec4' }, + { name: 'maxColor', type: 'vec4' } + ] + } ); + + // Flicker reduction based on luminance weighing. + const flickerReduction = Fn( ( [ currentColor, historyColor, currentWeight ] ) => { + + const historyWeight = currentWeight.oneMinus(); + const compressedCurrent = currentColor.mul( float( 1 ).div( ( max( currentColor.r, currentColor.g, currentColor.b ).add( 1 ) ) ) ); + const compressedHistory = historyColor.mul( float( 1 ).div( ( max( historyColor.r, historyColor.g, historyColor.b ).add( 1 ) ) ) ); + + const luminanceCurrent = luminance( compressedCurrent.rgb ); + const luminanceHistory = luminance( compressedHistory.rgb ); + + currentWeight.mulAssign( float( 1 ).div( luminanceCurrent.add( 1 ) ) ); + historyWeight.mulAssign( float( 1 ).div( luminanceHistory.add( 1 ) ) ); + + return add( currentColor.mul( currentWeight ), historyColor.mul( historyWeight ) ).div( max( currentWeight.add( historyWeight ), 0.00001 ) ).toVar(); + + } ); + + const historyNode = texture( this._historyRenderTarget.textures[ 0 ] ); + const lockNode = texture( this._historyRenderTarget.textures[ 1 ] ); + + // --- TAAU resolve --- + // + // For each output pixel, we map its position into input-pixel space, + // find the closest jittered input sample, and reconstruct the current + // color as a weighted sum of the 3×3 neighborhood around that sample. + // Each tap's weight is a Gaussian approximation of a Blackman-Harris + // window evaluated at the distance between the tap's (jittered) + // sample center and the output pixel center. The same neighborhood + // also supplies the moments used for variance clipping of the + // reprojected history, so no second neighborhood read is needed. + + const colorOutput = property( 'vec4' ); + const lockOutput = property( 'vec4' ); + + const outputNode = outputStruct( colorOutput, lockOutput ); + + const resolve = Fn( () => { + + const uvNode = uv(); + const inputSize = this.beautyNode.size(); // ivec2 + const inputSizeF = vec2( inputSize ); + + // output pixel center in input-pixel coordinates + + const pIn = uvNode.mul( inputSizeF ); + + // the input sample at integer texel (m, n) was rendered at world + // position (m + 0.5 + jitter). Solving for the closest tap gives: + + const closestTapF = pIn.sub( vec2( 0.5 ).add( this._jitterOffset ) ).round(); + const closestTap = ivec2( closestTapF ); + + // depth dilation around the closest input tap + + const currentDepth = sampleCurrentDepth( closestTapF ); + const closestDepth = currentDepth.get( 'closestDepth' ); + const closestPositionTexel = currentDepth.get( 'closestPositionTexel' ); + const farthestDepth = currentDepth.get( 'farthestDepth' ); + + // reproject using the velocity sampled at the dilated depth tap + + const offsetUV = this.velocityNode.load( closestPositionTexel ).xy.mul( vec2( 0.5, - 0.5 ) ); + const historyUV = uvNode.sub( offsetUV ); + const previousDepth = samplePreviousDepth( historyUV ); + + // history validity + + const isValidUV = historyUV.greaterThanEqual( 0 ).all().and( historyUV.lessThanEqual( 1 ).all() ); + const isEdge = farthestDepth.sub( closestDepth ).greaterThan( this.edgeDepthDiff ); + const isDisocclusion = closestDepth.sub( previousDepth ).greaterThan( this.depthThreshold ); + const hasValidHistory = isValidUV.and( isEdge.or( isDisocclusion.not() ) ); + + // 9-tap Blackman-Harris (Gaussian approximation) reconstruction + // of the current frame color, plus moment accumulation for the + // variance clip of the history. + + const sumColor = vec4( 0 ).toVar(); + const sumWeight = float( 0 ).toVar(); + const moment1 = vec4( 0 ).toVar(); + const moment2 = vec4( 0 ).toVar(); + + const offsets = [ + [ - 1, - 1 ], [ 0, - 1 ], [ 1, - 1 ], + [ - 1, 0 ], [ 0, 0 ], [ 1, 0 ], + [ - 1, 1 ], [ 0, 1 ], [ 1, 1 ] + ]; + + for ( const [ x, y ] of offsets ) { + + const tap = closestTap.add( ivec2( x, y ) ); + const tapCenter = vec2( tap ).add( vec2( 0.5 ).add( this._jitterOffset ) ); + const delta = pIn.sub( tapCenter ); + const d2 = delta.dot( delta ); + const w = exp( d2.mul( - 2.29 ) ); + + // Use max() to prevent NaN values from propagating. + const c = this.beautyNode.load( tap ).max( 0 ); + + sumColor.addAssign( c.mul( w ) ); + sumWeight.addAssign( w ); + + moment1.addAssign( c ); + moment2.addAssign( c.pow2() ); + + } + + const currentColor = sumColor.div( sumWeight.max( 1e-5 ) ); + + // variance clipping using the moments we just gathered + + const N = float( offsets.length ); + const mean = moment1.div( N ); + const motionFactor = uvNode.sub( historyUV ).mul( inputSizeF ).length().div( this.maxVelocityLength ).saturate(); + const varianceGamma = mix( 0.5, 1, motionFactor.oneMinus().pow2() ); + const variance = moment2.div( N ).sub( mean.pow2() ).max( 0 ).sqrt().mul( varianceGamma ); + const minColor = mean.sub( variance ); + const maxColor = mean.add( variance ); + + const historyColor = historyNode.sample( historyUV ); + const clippedHistoryColor = clipAABB( mean.clamp( minColor, maxColor ), historyColor, minColor, maxColor ); + + // Current weight. Under TAAU a single input frame covers less of + // the output grid, so the baseline current weight is lower than + // in standard TRAA to give the accumulator more frames to fill + // in sub-pixel detail. Motion still biases toward the current + // frame to keep disoccluded and fast-moving pixels responsive. + + const currentLuma = luminance( currentColor.rgb ); + const meanLuma = luminance( mean.rgb ).toConst(); + const thinFeature = currentLuma.sub( meanLuma ).abs().div( meanLuma ).smoothstep( 0, 0.2 ); + + // Gate the lock by a two-sided depth change check. The + // existing `isDisocclusion` is one-sided (only fires when + // the scene moves farther), but new geometry appearing + // closer also makes the history stale. + const isDepthChanged = closestDepth.sub( previousDepth ).abs().greaterThan( this.depthThreshold ); + const canLock = isValidUV.and( isDepthChanged.not() ); + const gatedThinFeature = canLock.select( thinFeature, float( 0 ) ); + + const decay = isDisocclusion.select( 0, 0.5 ); + const lock = max( gatedThinFeature, lockNode.r.mul( decay ) ).saturate(); + const lockedHistoryColor = mix( clippedHistoryColor, historyColor, lock ); + + const currentWeight = float( this.currentFrameWeight ).toVar(); + currentWeight.assign( hasValidHistory.select( currentWeight.add( motionFactor ).saturate(), 1 ) ); + + const output = flickerReduction( currentColor, lockedHistoryColor, currentWeight ); + + colorOutput.assign( output ); + lockOutput.assign( lock ); + + return vec4( 0 ); // temporary solution until TSL does not complain anymore + + } ); + + // materials + + this._resolveMaterial.colorNode = resolve(); + this._resolveMaterial.outputNode = outputNode; + + this._seedMaterial.colorNode = Fn( () => { + + colorOutput.assign( this.beautyNode.sample( uv() ) ); + lockOutput.assign( 0 ); + + return vec4( 0 ); + + } )(); + + this._seedMaterial.outputNode = outputNode; + + return this._textureNode; + + } + + /** + * Frees internal resources. This method should be called + * when the effect is no longer required. + */ + dispose() { + + this._historyRenderTarget.dispose(); + this._resolveRenderTarget.dispose(); + this._previousDepthRenderTarget.dispose(); + + this._resolveMaterial.dispose(); + this._seedMaterial.dispose(); + + } + +} + +export default TAAUNode; + +function _halton( index, base ) { + + let fraction = 1; + let result = 0; + while ( index > 0 ) { + + fraction /= base; + result += fraction * ( index % base ); + index = Math.floor( index / base ); + + } + + return result; + +} + +const _haltonOffsets = /*@__PURE__*/ Array.from( + { length: 32 }, + ( _, index ) => [ _halton( index + 1, 2 ), _halton( index + 1, 3 ) ] +); + +/** + * TSL function for creating a TAAU node for Temporal Anti-Aliasing Upscaling. + * + * @tsl + * @function + * @param {TextureNode} beautyNode - The texture node that represents the input of the effect. + * @param {TextureNode} depthNode - A node that represents the scene's depth. + * @param {TextureNode} velocityNode - A node that represents the scene's velocity. + * @param {Camera} camera - The camera the scene is rendered with. + * @returns {TAAUNode} + */ +export const taau = ( beautyNode, depthNode, velocityNode, camera ) => new TAAUNode( convertToTexture( beautyNode ), depthNode, velocityNode, camera ); diff --git a/examples/screenshots/webgpu_upscaling_taau.jpg b/examples/screenshots/webgpu_upscaling_taau.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a1c68a3ec951eb2f089728a592497892f566c6bc GIT binary patch literal 51554 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

;!2ET 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 literal 0 HcmV?d00001 diff --git a/examples/webgpu_upscaling_fsr1.html b/examples/webgpu_upscaling_fsr1.html index c96bea73312ee8..6271e487ea1b44 100644 --- a/examples/webgpu_upscaling_fsr1.html +++ b/examples/webgpu_upscaling_fsr1.html @@ -80,8 +80,6 @@ mixer = new THREE.AnimationMixer( model ); mixer.clipAction( gltf.animations[ 0 ] ).play(); - renderer.setAnimationLoop( animate ); - } ); // renderer diff --git a/examples/webgpu_upscaling_taau.html b/examples/webgpu_upscaling_taau.html new file mode 100644 index 00000000000000..1984f3317f9970 --- /dev/null +++ b/examples/webgpu_upscaling_taau.html @@ -0,0 +1,205 @@ + + + + three.js webgpu - postprocessing taau + + + + + + +

+ + +
+ three.jsTAAU +
+ + + Temporal Reprojection Anti-Aliasing with Upsampling.
+ Model: Littlest Tokyo by Glen Fox, CC Attribution. +
+
+ + + + + + From 9be5d73560c602109f6f542bbb06429d0462f1a3 Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Sat, 11 Apr 2026 12:07:53 +0200 Subject: [PATCH 3/3] Update FSR1Node.js Add note about anti-aliasing. --- examples/jsm/tsl/display/FSR1Node.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/jsm/tsl/display/FSR1Node.js b/examples/jsm/tsl/display/FSR1Node.js index 1376ba2cccec6e..c2966621407514 100644 --- a/examples/jsm/tsl/display/FSR1Node.js +++ b/examples/jsm/tsl/display/FSR1Node.js @@ -18,7 +18,8 @@ let _rendererState; * * Note: Only use FSR 1 if your application is fragment-shader bound and cannot afford to render * at full resolution. FSR 1 adds its own overhead, so simply shaded scenes will render faster - * at native resolution without it. + * at native resolution without it. Besides, FSR 1 should always be used with an anti-aliased + * source image. * * Reference: {@link https://gpuopen.com/fidelityfx-superresolution/}. *