Selaa lähdekoodia

Fix banding by reading from a downsampled depth buffer

Marco Fugaro 1 kuukausi sitten
vanhempi
sitoutus
c010301449
1 muutettua tiedostoa jossa 137 lisäystä ja 16 poistoa
  1. 137 16
      examples/jsm/tsl/display/GTAONode.js

+ 137 - 16
examples/jsm/tsl/display/GTAONode.js

@@ -1,5 +1,5 @@
-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, int, dot, max, pow, abs, If, textureSize, sin, cos, PI, texture, passTexture, mat3, add, normalize, mul, cross, mix, acos, clamp } from 'three/tsl';
+import { DataTexture, RenderTarget, RepeatWrapping, Vector2, Vector3, TempNode, QuadMesh, NodeMaterial, RendererUtils, RedFormat, FloatType, NearestFilter } 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, mul, cross, mix, acos, clamp } from 'three/tsl';
 
 const _quadMesh = /*@__PURE__*/ new QuadMesh();
 const _size = /*@__PURE__*/ new Vector2();
@@ -71,13 +71,12 @@ class GTAONode extends TempNode {
 		this.normalNode = normalNode;
 
 		/**
-		 * The resolution scale. By default the effect is rendered in full resolution
-		 * for best quality but a value of `0.5` should be sufficient for most scenes.
+		 * Backing field for {@link GTAONode#resolutionScale}.
 		 *
+		 * @private
 		 * @type {number}
-		 * @default 1
 		 */
-		this.resolutionScale = 1;
+		this._resolutionScale = 1;
 
 		/**
 		 * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders
@@ -97,6 +96,14 @@ 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;
+
 		// uniforms
 
 		/**
@@ -114,6 +121,15 @@ 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.
 		 *
@@ -226,6 +242,9 @@ class GTAONode extends TempNode {
 		this._material = new NodeMaterial();
 		this._material.name = 'GTAO';
 
+		this._depthDownsampleMaterial = new NodeMaterial();
+		this._depthDownsampleMaterial.name = 'GTAO.DepthDownsample';
+
 		/**
 		 * The result of the effect is represented as a separate texture node.
 		 *
@@ -247,6 +266,38 @@ 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).
+	 *
+	 * @type {number}
+	 * @default 1
+	 */
+	get resolutionScale() {
+
+		return this._resolutionScale;
+
+	}
+
+	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;
+
+		}
+
+	}
+
 	/**
 	 * Sets the size of the effect.
 	 *
@@ -255,11 +306,22 @@ class GTAONode extends TempNode {
 	 */
 	setSize( width, height ) {
 
-		width = Math.round( this.resolutionScale * width );
-		height = Math.round( this.resolutionScale * 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 ) );
+
+		this.resolution.value.set( lowW, lowH );
+		this._aoRenderTarget.setSize( lowW, lowH );
 
-		this.resolution.value.set( width, height );
-		this._aoRenderTarget.setSize( width, height );
+		// 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 );
+
+		}
 
 	}
 
@@ -293,15 +355,27 @@ class GTAONode extends TempNode {
 		const size = renderer.getDrawingBufferSize( _size );
 		this.setSize( size.width, size.height );
 
-		_quadMesh.material = this._material;
-		_quadMesh.name = 'AO';
-
 		// clear
 
 		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
 
+		_quadMesh.material = this._material;
+		_quadMesh.name = 'AO';
 		renderer.setRenderTarget( this._aoRenderTarget );
 		_quadMesh.render( renderer );
 
@@ -321,9 +395,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 sampleDepth = ( uv ) => {
 
-			const depth = this.depthNode.sample( uv ).r;
+			const depth = depthSourceNode.sample( uv ).r;
 
 			if ( builder.renderer.logarithmicDepthBuffer === true ) {
 
@@ -338,7 +421,9 @@ class GTAONode extends TempNode {
 		};
 
 		const sampleNoise = ( uv ) => this._noiseNode.sample( uv );
-		const sampleNormal = ( uv ) => ( this.normalNode !== null ) ? this.normalNode.sample( uv ).rgb.normalize() : getNormalFromDepth( uv, this.depthNode.value, this._cameraProjectionMatrixInverse );
+		const sampleNormal = ( uv ) => ( normalSourceNode !== null )
+			? normalSourceNode.sample( uv ).rgb.normalize()
+			: getNormalFromDepth( uv, fallbackDepthForNormal, this._cameraProjectionMatrixInverse );
 
 		const ao = Fn( () => {
 
@@ -486,7 +571,41 @@ class GTAONode extends TempNode {
 		this._material.fragmentNode = ao().context( builder.getSharedContext() );
 		this._material.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;
 
@@ -499,8 +618,10 @@ class GTAONode extends TempNode {
 	dispose() {
 
 		this._aoRenderTarget.dispose();
+		this._depthHalfRT.dispose();
 
 		this._material.dispose();
+		this._depthDownsampleMaterial.dispose();
 
 	}
 

粤ICP备19079148号