Просмотр исходного кода

TRAANode: Reduce smearing (#32322)

Co-authored-by: Michael Herzog <michael.herzog@human-interactive.org>
Shota Matsuda 3 месяцев назад
Родитель
Сommit
3acd6600c3
2 измененных файлов с 191 добавлено и 64 удалено
  1. 188 62
      examples/jsm/tsl/display/TRAANode.js
  2. 3 2
      examples/webgpu_postprocessing_ao.html

+ 188 - 62
examples/jsm/tsl/display/TRAANode.js

@@ -1,5 +1,5 @@
 import { HalfFloatType, Vector2, RenderTarget, RendererUtils, QuadMesh, NodeMaterial, TempNode, NodeUpdateType, Matrix4, DepthTexture } from 'three/webgpu';
-import { add, float, If, Loop, int, Fn, min, max, clamp, nodeObject, texture, uniform, uv, vec2, vec4, luminance, convertToTexture, passTexture, velocity, getViewPosition, viewZToPerspectiveDepth } from 'three/tsl';
+import { add, float, If, Fn, max, nodeObject, texture, uniform, uv, vec2, vec4, luminance, convertToTexture, passTexture, velocity, getViewPosition, viewZToPerspectiveDepth, struct, ivec2, mix } from 'three/tsl';
 
 const _quadMesh = /*@__PURE__*/ new QuadMesh();
 const _size = /*@__PURE__*/ new Vector2();
@@ -77,26 +77,45 @@ class TRAANode extends TempNode {
 		this.velocityNode = velocityNode;
 
 		/**
-		 *  The camera the scene is rendered with.
+		 * 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.
+		 * 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.0001;
+		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.0001;
+		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;
+
+		/**
+		 * Whether to decrease the weight on the current frame when the velocity is more subpixel.
+		 * This reduces blurriness under motion, but can introduce a square pattern artifact.
+		 *
+		 * @type {boolean}
+		 * @default true
+		 */
+		this.useSubpixelCorrection = true;
 
 		/**
 		 * The jitter index selects the current camera offset value.
@@ -436,11 +455,50 @@ class TRAANode extends TempNode {
 
 		}
 
-		const historyTexture = texture( this._historyRenderTarget.texture );
-		const sampleTexture = this.beautyNode;
-		const depthTexture = this.depthNode;
-		const velocityTexture = this.velocityNode;
+		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;
@@ -451,95 +509,163 @@ class TRAANode extends TempNode {
 
 		};
 
-		const resolve = Fn( () => {
+		// 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' }
+			]
+		} );
 
-			const uvNode = uv();
+		// Performs variance clipping.
+		// See: https://developer.download.nvidia.com/gameworks/events/GDC2016/msalvi_temporal_supersampling.pdf
+		const varianceClipping = Fn( ( [ positionTexel, currentColor, historyColor, gamma ] ) => {
 
-			const minColor = vec4( 10000 ).toVar();
-			const maxColor = vec4( - 10000 ).toVar();
-			const closestDepth = float( 2 ).toVar();
-			const farthestDepth = float( - 1 ).toVar();
-			const closestDepthPixelPosition = vec2( 0 ).toVar();
+			const offsets = [
+				[ - 1, - 1 ],
+				[ - 1, 1 ],
+				[ 1, - 1 ],
+				[ 1, 1 ],
+				[ 1, 0 ],
+				[ 0, - 1 ],
+				[ 0, 1 ],
+				[ - 1, 0 ]
+			];
 
-			// sample a 3x3 neighborhood to create a box in color space
-			// clamping the history color with the resulting min/max colors mitigates ghosting
+			const moment1 = currentColor.toVar();
+			const moment2 = currentColor.pow2().toVar();
 
-			Loop( { start: int( - 1 ), end: int( 1 ), type: 'int', condition: '<=', name: 'x' }, ( { x } ) => {
+			for ( const [ x, y ] of offsets ) {
 
-				Loop( { start: int( - 1 ), end: int( 1 ), type: 'int', condition: '<=', name: 'y' }, ( { y } ) => {
+				// Use max() to prevent NaN values from propagating.
+				const neighbor = this.beautyNode.offset( ivec2( x, y ) ).load( positionTexel ).max( 0 );
+				moment1.addAssign( neighbor );
+				moment2.addAssign( neighbor.pow2() );
 
-					const uvNeighbor = uvNode.add( vec2( float( x ), float( y ) ).mul( this._invSize ) ).toVar();
-					const colorNeighbor = max( vec4( 0 ), sampleTexture.sample( uvNeighbor ) ).toVar(); // use max() to avoid propagate garbage values
+			}
 
-					minColor.assign( min( minColor, colorNeighbor ) );
-					maxColor.assign( max( maxColor, colorNeighbor ) );
+			const N = float( offsets.length + 1 );
+			const mean = moment1.div( N );
+			const variance = moment2.div( N ).sub( mean.pow2() ).max( 0 ).sqrt().mul( gamma );
+			const minColor = mean.sub( variance );
+			const maxColor = mean.add( variance );
 
-					const currentDepth = depthTexture.sample( uvNeighbor ).r.toVar();
+			return clipAABB( mean.clamp( minColor, maxColor ), historyColor, minColor, maxColor );
 
-					// find the sample position of the closest depth in the neighborhood (used for velocity)
+		} );
 
-					If( currentDepth.lessThan( closestDepth ), () => {
+		// Returns the amount of subpixel (expressed within [0, 1]) in the velocity.
+		const subpixelCorrection = Fn( ( [ velocityUV, textureSize ] ) => {
+
+			const velocityTexel = velocityUV.mul( textureSize );
+			const phase = velocityTexel.fract().abs();
+			const weight = max( phase, phase.oneMinus() );
+			return weight.x.mul( weight.y ).oneMinus().div( 0.75 );
+
+		} ).setLayout( {
+			name: 'subpixelCorrection',
+			type: 'float',
+			inputs: [
+				{ name: 'velocityUV', type: 'vec2' },
+				{ name: 'textureSize', type: 'ivec2' }
+			]
+		} );
 
-						closestDepth.assign( currentDepth );
-						closestDepthPixelPosition.assign( uvNeighbor );
+		// 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 ) ) ) );
 
-					// find the farthest depth in the neighborhood (used to preserve edge anti-aliasing)
+			const luminanceCurrent = luminance( compressedCurrent.rgb );
+			const luminanceHistory = luminance( compressedHistory.rgb );
 
-					If( currentDepth.greaterThan( farthestDepth ), () => {
+			currentWeight.mulAssign( float( 1 ).div( luminanceCurrent.add( 1 ) ) );
+			historyWeight.mulAssign( float( 1 ).div( luminanceHistory.add( 1 ) ) );
 
-						farthestDepth.assign( currentDepth );
+			return add( currentColor.mul( currentWeight ), historyColor.mul( historyWeight ) ).div( max( currentWeight.add( historyWeight ), 0.00001 ) ).toVar();
 
-					} );
+		} );
 
-				} );
+		const historyNode = texture( this._historyRenderTarget.texture );
 
-			} );
+		const resolve = Fn( () => {
 
-			// sampling/reprojection
+			const uvNode = uv();
+			const textureSize = this.beautyNode.size(); // Assumes all the buffers share the same size.
+			const positionTexel = uvNode.mul( textureSize );
 
-			const offset = velocityTexture.sample( closestDepthPixelPosition ).xy.mul( vec2( 0.5, - 0.5 ) ); // NDC to uv offset
+			// sample the closest and farthest depths in the current buffer
 
-			const currentColor = sampleTexture.sample( uvNode );
-			const historyColor = historyTexture.sample( uvNode.sub( offset ) );
+			const currentDepth = sampleCurrentDepth( positionTexel );
+			const closestDepth = currentDepth.get( 'closestDepth' );
+			const closestPositionTexel = currentDepth.get( 'closestPositionTexel' );
+			const farthestDepth = currentDepth.get( 'farthestDepth' );
 
-			// clamping
+			// convert the NDC offset to UV offset
 
-			const clampedHistoryColor = clamp( historyColor, minColor, maxColor );
+			const offsetUV = this.velocityNode.load( closestPositionTexel ).xy.mul( vec2( 0.5, - 0.5 ) );
 
-			// sample the current and previous depths
+			// sample the previous depth
 
-			const currentDepth = depthTexture.sample( uvNode ).r;
-			const historyUV = uvNode.sub( offset );
+			const historyUV = uvNode.sub( offsetUV );
 			const previousDepth = samplePreviousDepth( historyUV );
 
-			// disocclusion except on edges
+			// history is considered valid when the UV is in range and there's no disocclusion except on edges
 
+			const isValidUV = historyUV.greaterThanEqual( 0 ).all().and( historyUV.lessThanEqual( 1 ).all() );
 			const isEdge = farthestDepth.sub( closestDepth ).greaterThan( this.edgeDepthDiff );
-			const isDisocclusion = currentDepth.sub( previousDepth ).greaterThan( this.depthThreshold ).and( isEdge.not() );
+			const isDisocclusion = closestDepth.sub( previousDepth ).greaterThan( this.depthThreshold );
+			const hasValidHistory = isValidUV.and( isEdge.or( isDisocclusion.not() ) );
 
-			// higher velocity = more weight on current frame
-			// zero out history weight where disocclusion
+			// sample the current and previous colors
 
-			const motionFactor = uvNode.sub( historyUV ).length().mul( 10 );
-			const currentWeight = isDisocclusion.select( 1, float( 0.05 ).add( motionFactor ).saturate() ).toVar();
-			const historyWeight = currentWeight.oneMinus().toVar();
+			const currentColor = this.beautyNode.sample( uvNode );
+			const historyColor = historyNode.sample( uvNode.sub( offsetUV ) );
 
-			// flicker reduction based on luminance weighing
+			// increase the weight towards the current frame under motion
 
-			const compressedCurrent = currentColor.mul( float( 1 ).div( ( max( currentColor.r, currentColor.g, currentColor.b ).add( 1.0 ) ) ) );
-			const compressedHistory = clampedHistoryColor.mul( float( 1 ).div( ( max( clampedHistoryColor.r, clampedHistoryColor.g, clampedHistoryColor.b ).add( 1.0 ) ) ) );
+			const motionFactor = uvNode.sub( historyUV ).mul( textureSize ).length().div( this.maxVelocityLength ).saturate();
+			const currentWeight = float( 0.05 ).toVar(); // A minimum weight
 
-			const luminanceCurrent = luminance( compressedCurrent.rgb );
-			const luminanceHistory = luminance( compressedHistory.rgb );
+			if ( this.useSubpixelCorrection ) {
 
-			currentWeight.mulAssign( float( 1 ).div( luminanceCurrent.add( 1 ) ) );
-			historyWeight.mulAssign( float( 1 ).div( luminanceHistory.add( 1 ) ) );
+				// Increase the minimum weight towards the current frame when the velocity is more subpixel.
+				currentWeight.addAssign( subpixelCorrection( offsetUV, textureSize ).mul( 0.25 ) );
+
+			}
+
+			currentWeight.assign( hasValidHistory.select( currentWeight.add( motionFactor ).saturate(), 1 ) );
+
+			// Perform neighborhood clipping/clamping. We use variance clipping here.
+
+			const varianceGamma = mix( 0.5, 1, motionFactor.oneMinus().pow2() ); // Reasonable gamma range is [0.75, 2]
+			const clippedHistoryColor = varianceClipping( positionTexel, currentColor, historyColor, varianceGamma );
+
+			// flicker reduction based on luminance weighing
 
-			const smoothedOutput = add( currentColor.mul( currentWeight ), clampedHistoryColor.mul( historyWeight ) ).div( max( currentWeight.add( historyWeight ), 0.00001 ) ).toVar();
+			const output = flickerReduction( currentColor, clippedHistoryColor, currentWeight );
 
-			return smoothedOutput;
+			return output;
 
 		} );
 

+ 3 - 2
examples/webgpu_postprocessing_ao.html

@@ -17,8 +17,8 @@
 
 			<small>
 				Ambient Occlusion based on GTAO.<br />
-				<a href="https://skfb.ly/oCnNx" target="_blank" rel="noopener">Minimalistic Modern Bedroom</a> by 
-				<a href="https://sketchfab.com/dylanheyes" target="_blank" rel="noopener">dylanheyes</a> is licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener">Creative Commons Attribution</a>.	
+				<a href="https://skfb.ly/oCnNx" target="_blank" rel="noopener">Minimalistic Modern Bedroom</a> by
+				<a href="https://sketchfab.com/dylanheyes" target="_blank" rel="noopener">dylanheyes</a> is licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener">Creative Commons Attribution</a>.
 			</small>
 		</div>
 
@@ -149,6 +149,7 @@
 				// final output + traa
 
 				traaPass = traa( scenePass, prePassDepth, prePassVelocity, camera );
+				traaPass.useSubpixelCorrection = false;
 
 				postProcessing.outputNode = traaPass;
 

粤ICP备19079148号