Browse Source

GTAONode: Add Quadratic ray stepping. (#33895)

Michael Herzog 1 week ago
parent
commit
ccc185ba06

+ 50 - 14
examples/jsm/tsl/display/GTAONode.js

@@ -1,11 +1,12 @@
 import { DataTexture, RenderTarget, RepeatWrapping, Vector2, Vector3, TempNode, QuadMesh, NodeMaterial, RendererUtils, RedFormat } from 'three/webgpu';
-import { reference, logarithmicDepthToViewZ, viewZToPerspectiveDepth, getNormalFromDepth, getScreenPosition, getViewPosition, nodeObject, Fn, float, NodeUpdateType, uv, uniform, Loop, vec2, vec3, vec4, int, dot, max, pow, abs, If, textureSize, sin, cos, PI, texture, passTexture, mat3, add, normalize, mul, cross, div, mix, acos, clamp } from 'three/tsl';
+import { reference, logarithmicDepthToViewZ, viewZToPerspectiveDepth, getNormalFromDepth, getScreenPosition, getViewPosition, nodeObject, Fn, float, NodeUpdateType, uv, uniform, Loop, vec2, vec3, int, dot, max, pow, abs, If, textureSize, sin, cos, PI, texture, passTexture, mat3, add, normalize, cross, mix, acos, clamp, interleavedGradientNoise, screenCoordinate, fract, rand } 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;
 
@@ -122,17 +123,18 @@ class GTAONode extends TempNode {
 		this.thickness = uniform( 1 );
 
 		/**
-		 * Another option to tweak the occlusion. The recommended range is
-		 * `[1,2]` for attenuating the AO.
+		 * @deprecated Since the switch to quadratic ray stepping with sphere falloff,
+		 * step distribution is fixed at `t²` and this uniform has no effect. Kept for
+		 * backward compatibility and will be removed in a future release.
 		 *
 		 * @type {UniformNode<float>}
 		 */
 		this.distanceExponent = uniform( 1 );
 
 		/**
-		 * The distance fall off value of the ambient occlusion.
-		 * A lower value leads to a larger AO effect. The value
-		 * should lie in the range `[0,1]`.
+		 * @deprecated Replaced by the sphere falloff `mix( max( h, sH ), h, (dist/radius)² )`,
+		 * which has no tunable parameter. Kept for backward compatibility and will be
+		 * removed in a future release.
 		 *
 		 * @type {UniformNode<float>}
 		 */
@@ -216,6 +218,15 @@ class GTAONode extends TempNode {
 		 */
 		this._temporalDirection = uniform( 0 );
 
+		/**
+		 * Temporal offset added to the initial ray step.
+		 *
+		 * @private
+		 * @type {UniformNode<float>}
+		 */
+		this._temporalOffset = uniform( 0 );
+
+
 		/**
 		 * The material that is used to render the effect.
 		 *
@@ -280,10 +291,12 @@ class GTAONode extends TempNode {
 			const frameId = frame.frameId;
 
 			this._temporalDirection.value = _temporalRotations[ frameId % 6 ] / 360;
+			this._temporalOffset.value = _spatialOffsets[ frameId % 4 ];
 
 		} else {
 
 			this._temporalDirection.value = 0;
+			this._temporalOffset.value = 1;
 
 		}
 
@@ -367,14 +380,18 @@ class GTAONode extends TempNode {
 
 			// Each iteration analyzes one vertical "slice" of the 3D space around the fragment.
 
+			// Per-step phase jitter for spatio-temporal decorrelation.
+			const noiseJitterIdx = this._temporalDirection.mul( 0.02 );
+			const stepJitter = fract( interleavedGradientNoise( screenCoordinate.add( this._temporalOffset ) ) ).add( rand( uvNode.add( noiseJitterIdx ).mul( 2 ).sub( 1 ) ) );
+
 			Loop( { start: int( 0 ), end: DIRECTIONS, type: 'int', condition: '<' }, ( { i } ) => {
 
 				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 ) );
+				const sampleDir = vec3( cos( angle ), sin( angle ), 0 ).toVar();
+				sampleDir.assign( normalize( kernelMatrix.mul( sampleDir ) ) );
 
 				const viewDir = normalize( viewPosition.xyz.negate() ).toVar();
-				const sliceBitangent = normalize( cross( sampleDir.xyz, viewDir ) ).toVar();
+				const sliceBitangent = normalize( cross( sampleDir, viewDir ) ).toVar();
 				const sliceTangent = cross( sliceBitangent, viewDir ).toVar();
 
 				// Project the view normal onto the slice plane (remove component along sliceBitangent).
@@ -397,7 +414,11 @@ class GTAONode extends TempNode {
 
 				Loop( { end: STEPS, type: 'int', name: 'j', condition: '<' }, ( { j } ) => {
 
-					const sampleViewOffset = sampleDir.xyz.mul( radiusToUse ).mul( sampleDir.w ).mul( pow( div( float( j ).add( 1.0 ), float( STEPS ) ), this.distanceExponent ) );
+					// Quadratic step distribution ( sampleDist = t² ) concentrates samples in the
+					// near-field. (Blender's Eevee adaptation)
+					const t = float( j ).add( 1.0 ).add( stepJitter ).div( STEPS ).toVar();
+					const sampleDist = t.mul( t );
+					const sampleViewOffset = sampleDir.mul( radiusToUse ).mul( sampleDist );
 
 					// The loop marches in two opposite directions (x and y) along the slice's line to find the horizon on both sides.
 
@@ -407,11 +428,21 @@ class GTAONode extends TempNode {
 					const sampleDepthX = sampleDepth( sampleScreenPositionX ).toVar();
 					const sampleSceneViewPositionX = getViewPosition( sampleScreenPositionX, sampleDepthX, this._cameraProjectionMatrixInverse ).toVar();
 					const viewDeltaX = sampleSceneViewPositionX.sub( viewPosition ).toVar();
+					const lenX = viewDeltaX.length().toVar();
+
+					// Manual normalize guards against zero-length delta.
+					const sHX = dot( viewDir, viewDeltaX.div( max( lenX, float( 0.0001 ) ) ) );
+
+					// Sphere falloff: ( dist / radius )² fades the sample's horizon contribution
+					// back toward the prior horizon as it approaches the radius boundary.
+					// (squared variant of the paper's near-field attenuation;
+					// Activision GTAO paper, Section 4.3 "Bounding the sampling area")
+					const distFacX = clamp( lenX.div( radiusToUse ), 0, 1 );
+					const distFacSqX = distFacX.mul( distFacX );
 
 					If( abs( viewDeltaX.z ).lessThan( this.thickness ), () => {
 
-						const sampleCosHorizon = dot( viewDir, normalize( viewDeltaX ) );
-						cosHorizons.x.addAssign( max( 0, mul( sampleCosHorizon.sub( cosHorizons.x ), mix( 1.0, float( 2.0 ).div( float( j ).add( 2 ) ), this.distanceFallOff ) ) ) );
+						cosHorizons.x.assign( mix( max( cosHorizons.x, sHX ), cosHorizons.x, distFacSqX ) );
 
 					} );
 
@@ -421,11 +452,16 @@ class GTAONode extends TempNode {
 					const sampleDepthY = sampleDepth( sampleScreenPositionY ).toVar();
 					const sampleSceneViewPositionY = getViewPosition( sampleScreenPositionY, sampleDepthY, this._cameraProjectionMatrixInverse ).toVar();
 					const viewDeltaY = sampleSceneViewPositionY.sub( viewPosition ).toVar();
+					const lenY = viewDeltaY.length().toVar();
+
+					const sHY = dot( viewDir, viewDeltaY.div( max( lenY, float( 0.0001 ) ) ) );
+
+					const distFacY = clamp( lenY.div( radiusToUse ), 0, 1 );
+					const distFacSqY = distFacY.mul( distFacY );
 
 					If( abs( viewDeltaY.z ).lessThan( this.thickness ), () => {
 
-						const sampleCosHorizon = dot( viewDir, normalize( viewDeltaY ) );
-						cosHorizons.y.addAssign( max( 0, mul( sampleCosHorizon.sub( cosHorizons.y ), mix( 1.0, float( 2.0 ).div( float( j ).add( 2 ) ), this.distanceFallOff ) ) ) );
+						cosHorizons.y.assign( mix( max( cosHorizons.y, sHY ), cosHorizons.y, distFacSqY ) );
 
 					} );
 

BIN
examples/screenshots/webgpu_postprocessing_ao.jpg


+ 2 - 8
examples/webgpu_postprocessing_ao.html

@@ -57,10 +57,8 @@
 
 			const params = {
 				samples: 16,
-				distanceExponent: 1,
-				distanceFallOff: 1,
-				radius: 0.25,
-				scale: 0.5,
+				radius: 0.5,
+				scale: 0.8,
 				thickness: 1,
 				aoOnly: false,
 				transparentOpacity: 0.3
@@ -499,8 +497,6 @@
 
 				const gui = renderer.inspector.createParameters( 'Settings' );
 				gui.add( params, 'samples', 4, 32, 1 ).onChange( updateParameters );
-				gui.add( params, 'distanceExponent', 1, 2 ).onChange( updateParameters );
-				gui.add( params, 'distanceFallOff', 0.01, 1 ).onChange( updateParameters );
 				gui.add( params, 'radius', 0.1, 1 ).onChange( updateParameters );
 				gui.add( params, 'scale', 0.01, 1 ).onChange( updateParameters );
 				gui.add( params, 'thickness', 0.01, 2 ).onChange( updateParameters );
@@ -531,8 +527,6 @@
 			function updateParameters() {
 
 				aoPass.samples.value = params.samples;
-				aoPass.distanceExponent.value = params.distanceExponent;
-				aoPass.distanceFallOff.value = params.distanceFallOff;
 				aoPass.radius.value = params.radius;
 				aoPass.scale.value = params.scale;
 				aoPass.thickness.value = params.thickness;

粤ICP备19079148号