Browse Source

Replace depth downsample pass with textureGather sampling

Marco Fugaro 1 month ago
parent
commit
0aaa6a33d7
1 changed files with 13 additions and 104 deletions
  1. 13 104
      examples/jsm/tsl/display/GTAONode.js

+ 13 - 104
examples/jsm/tsl/display/GTAONode.js

@@ -1,4 +1,4 @@
-import { RenderTarget, Vector2, TempNode, QuadMesh, NodeMaterial, RendererUtils, RedFormat, FloatType, NearestFilter, LinearFilter } from 'three/webgpu';
+import { RenderTarget, Vector2, TempNode, QuadMesh, NodeMaterial, RendererUtils, RedFormat, LinearFilter } from 'three/webgpu';
 import { reference, logarithmicDepthToViewZ, viewZToPerspectiveDepth, getNormalFromDepth, getScreenPosition, getViewPosition, nodeObject, Fn, float, NodeUpdateType, uv, uniform, Loop, vec2, vec3, vec4, int, dot, max, min, pow, abs, If, textureSize, sin, cos, PI, texture, passTexture, mat3, add, normalize, cross, mix, acos, clamp, fract } from 'three/tsl';
 import { generateBlueNoiseTexture } from '../../math/BlueNoise.js';
 
@@ -117,14 +117,6 @@ class GTAONode extends TempNode {
 		this._aoRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false, format: RedFormat } );
 		this._aoRenderTarget.texture.name = 'GTAONode.AO';
 
-		// Half-res depth target, populated by the downsample pass when
-		// resolutionScale < 1. Depth uses Float32 because perspective depth values
-		// cluster near 1.0 where Float16 quantizes horizon angles into visible rings.
-		this._depthHalfRT = new RenderTarget( 1, 1, { depthBuffer: false, format: RedFormat, type: FloatType } );
-		this._depthHalfRT.texture.name = 'GTAONode.DepthHalf';
-		this._depthHalfRT.texture.minFilter = NearestFilter;
-		this._depthHalfRT.texture.magFilter = NearestFilter;
-
 		// History + resolve targets for the temporal accumulation pass. LinearFilter
 		// so the reprojected history fetch (fractional UV) is bilinear, and so the
 		// output upsamples bilinearly to full-res instead of blocky nearest texels.
@@ -155,15 +147,6 @@ class GTAONode extends TempNode {
 		 */
 		this.resolution = uniform( new Vector2() );
 
-		/**
-		 * Full screen resolution; used by the downsample passes to map each half-res
-		 * texel to its full-res footprint.
-		 *
-		 * @private
-		 * @type {UniformNode<vec2>}
-		 */
-		this._fullResolution = uniform( new Vector2() );
-
 		/**
 		 * The thickness of the ambient occlusion.
 		 *
@@ -308,9 +291,6 @@ class GTAONode extends TempNode {
 		this._material = new NodeMaterial();
 		this._material.name = 'GTAO';
 
-		this._depthDownsampleMaterial = new NodeMaterial();
-		this._depthDownsampleMaterial.name = 'GTAO.DepthDownsample';
-
 		this._accumulationMaterial = new NodeMaterial();
 		this._accumulationMaterial.name = 'GTAO.Accumulation';
 
@@ -340,11 +320,10 @@ class GTAONode extends TempNode {
 
 	/**
 	 * Render resolution as a fraction of the screen size (`1` = full, `0.5` = half).
-	 * At `< 1`, a min-Z depth downsample runs first and the AO shader reads from that
-	 * half-res target, which avoids the banding caused by silhouette-bleed when the
-	 * AO is computed at half-res against the full-res depth input. Normals are still
-	 * sampled at full resolution. Crossing the `1` ↔ `< 1` boundary triggers a shader
-	 * recompile (the texture binding is decided at compile time).
+	 * At `< 1` the AO is computed at lower resolution; depth is sampled via
+	 * `textureGather` so the min-Z foreground-priority footprint is resolved inline
+	 * at every sample, with no separate downsample pass. Normals are always sampled
+	 * at full resolution.
 	 *
 	 * @type {number}
 	 * @default 1
@@ -358,16 +337,8 @@ class GTAONode extends TempNode {
 	set resolutionScale( value ) {
 
 		if ( value === this._resolutionScale ) return;
-
-		const crossedBoundary = ( value === 1 ) !== ( this._resolutionScale === 1 );
 		this._resolutionScale = value;
 
-		if ( crossedBoundary && this._material ) {
-
-			this._material.needsUpdate = true;
-
-		}
-
 	}
 
 	/**
@@ -378,8 +349,6 @@ class GTAONode extends TempNode {
 	 */
 	setSize( width, height ) {
 
-		this._fullResolution.value.set( width, height );
-
 		const lowW = Math.max( 1, Math.round( this._resolutionScale * width ) );
 		const lowH = Math.max( 1, Math.round( this._resolutionScale * height ) );
 
@@ -388,15 +357,6 @@ class GTAONode extends TempNode {
 		this._historyRenderTarget.setSize( lowW, lowH );
 		this._accumulationRenderTarget.setSize( lowW, lowH );
 
-		// Only resize the half-res RT when we'll actually use it. Shrinking it
-		// here at scale=1 while the renderer holds cached bindings from a previous
-		// scale<1 compile would corrupt later frames.
-		if ( this._resolutionScale !== 1 ) {
-
-			this._depthHalfRT.setSize( lowW, lowH );
-
-		}
-
 	}
 
 	/**
@@ -440,19 +400,6 @@ class GTAONode extends TempNode {
 
 		renderer.setClearColor( 0xffffff, 1 );
 
-		// Downsample depth to half-res (min-Z foreground-priority) so the AO horizon
-		// search runs on a consistent low-res grid and doesn't band on silhouettes
-		// from bilinear bleed in the full-res depth input.
-
-		if ( this._resolutionScale !== 1 ) {
-
-			_quadMesh.material = this._depthDownsampleMaterial;
-			_quadMesh.name = 'GTAO.DepthDownsample';
-			renderer.setRenderTarget( this._depthHalfRT );
-			_quadMesh.render( renderer );
-
-		}
-
 		// AO horizon search. With temporal accumulation on, the AO is written to its
 		// own target and the accumulation pass resolves it into the output; with it
 		// off, the AO is written straight to the output target and the accumulation
@@ -495,18 +442,18 @@ class GTAONode extends TempNode {
 
 		const uvNode = uv();
 
-		// At scale<1 the AO shader reads depth from the half-res RT populated by the
-		// downsample pass; at scale=1 it samples the source node directly. The choice
-		// is compile-time — the resolutionScale setter triggers a recompile when
-		// crossing the boundary. Normals are always sampled at full-res.
-		const useHalfRes = this._resolutionScale !== 1;
-		const depthSourceNode = useHalfRes ? texture( this._depthHalfRT.texture ) : this.depthNode;
 		const normalSourceNode = this.normalNode;
-		const fallbackDepthForNormal = useHalfRes ? this._depthHalfRT.texture : this.depthNode.value;
+		const fallbackDepthForNormal = this.depthNode.value;
 
+		// Bilinear depth sampling blends values across silhouette edges, producing
+		// phantom mid-depth values that corrupt the horizon search and cause visible
+		// banding at low AO radius. textureGather returns the same 2×2 footprint
+		// unblended, so we can take the min (nearest surface wins) without a separate
+		// downsample pass.
 		const sampleDepth = ( uv ) => {
 
-			const depth = depthSourceNode.sample( uv ).r;
+			const g = this.depthNode.gather().sample( uv );
+			const depth = min( min( g.x, g.y ), min( g.z, g.w ) );
 
 			if ( builder.renderer.logarithmicDepthBuffer === true ) {
 
@@ -748,42 +695,6 @@ class GTAONode extends TempNode {
 		this._accumulationMaterial.fragmentNode = temporal().context( builder.getSharedContext() );
 		this._accumulationMaterial.needsUpdate = true;
 
-		// At scale=1 the downsample passes are skipped, so building their fragment
-		// nodes is dead work. Recompile when crossing the boundary picks the right path.
-		if ( ! useHalfRes ) {
-
-			return this._textureNode;
-
-		}
-
-		// Each half-res output texel covers ratio×ratio full-res texels (ratio =
-		// fullRes / lowRes). Sample at integer-pixel-center UVs so `.sample()` reads
-		// the exact texel with no bilinear bleed and stays valid on multisampled depth.
-		const computeFullResFootprint = () => {
-
-			const ratio = this._fullResolution.div( this.resolution );
-			const baseFull = uv().mul( this.resolution ).floor().mul( ratio );
-			const sampleAt = ( i, j ) => baseFull.add( vec2( i, j ).mul( ratio.sub( 1 ).max( 0 ) ) ).add( 0.5 ).div( this._fullResolution );
-			return { sampleAt };
-
-		};
-
-		// Depth: min-Z over the 2×2 footprint (foreground priority — half-res depth
-		// is always a real surface depth, never a phantom mid-silhouette value).
-		const depthDownsample = Fn( () => {
-
-			const { sampleAt } = computeFullResFootprint();
-			const d00 = this.depthNode.sample( sampleAt( 0, 0 ) ).r.toVar();
-			const d10 = this.depthNode.sample( sampleAt( 1, 0 ) ).r.toVar();
-			const d01 = this.depthNode.sample( sampleAt( 0, 1 ) ).r.toVar();
-			const d11 = this.depthNode.sample( sampleAt( 1, 1 ) ).r.toVar();
-			return vec4( min( min( d00, d10 ), min( d01, d11 ) ), 0, 0, 1 );
-
-		} );
-
-		this._depthDownsampleMaterial.fragmentNode = depthDownsample().context( builder.getSharedContext() );
-		this._depthDownsampleMaterial.needsUpdate = true;
-
 		return this._textureNode;
 
 	}
@@ -795,12 +706,10 @@ class GTAONode extends TempNode {
 	dispose() {
 
 		this._aoRenderTarget.dispose();
-		this._depthHalfRT.dispose();
 		this._historyRenderTarget.dispose();
 		this._accumulationRenderTarget.dispose();
 
 		this._material.dispose();
-		this._depthDownsampleMaterial.dispose();
 		this._accumulationMaterial.dispose();
 
 	}

粤ICP备19079148号