Explorar el Código

GTAONode: Interleaved gradient noise default + clip-space ray stepping

Default the slice rotation and step jitter to per-pixel interleaved
gradient noise (no texture fetch, no CPU generation). A tileable noise
texture can still be supplied via `noiseNode` for a blue-noise spectrum.

Project the slice direction into clip space once per slice so each
ray-march step becomes a multiply-add plus perspective divide rather than
a full matrix transform per sample, and hoist the per-pixel view direction
out of the slice loop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mr.doob hace 1 mes
padre
commit
686d89656f
Se han modificado 1 ficheros con 47 adiciones y 29 borrados
  1. 47 29
      examples/jsm/tsl/display/GTAONode.js

+ 47 - 29
examples/jsm/tsl/display/GTAONode.js

@@ -1,6 +1,5 @@
 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';
+import { reference, logarithmicDepthToViewZ, viewZToPerspectiveDepth, getNormalFromDepth, 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, interleavedGradientNoise, screenCoordinate } from 'three/tsl';
 
 const _quadMesh = /*@__PURE__*/ new QuadMesh();
 const _size = /*@__PURE__*/ new Vector2();
@@ -224,16 +223,16 @@ class GTAONode extends TempNode {
 		this.temporalAccumulationAlpha = uniform( 0.1 );
 
 		/**
-		 * Blue-noise texture sampled for the slice rotation and step jitter.
-		 * Generated by Ulichney's void-and-cluster method (see
-		 * {@link generateBlueNoiseTexture}); 64×64 two-channel, tileable. Channel
-		 * R drives the slice rotation, G drives the per-step phase jitter — both
-		 * read from a single texture fetch.
+		 * Optional tileable noise texture driving the slice rotation (channel R) and
+		 * per-step phase jitter (channel G). When `null` (default), the AO uses cheap
+		 * per-pixel interleaved gradient noise — no texture fetch, no CPU generation.
+		 * Inject a two-channel blue-noise texture here for a higher-quality spectrum
+		 * when a temporal resolve (TRAA / accumulation) is available.
 		 *
-		 * @private
-		 * @type {TextureNode}
+		 * @type {?TextureNode}
+		 * @default null
 		 */
-		this._noiseNode = texture( generateBlueNoiseTexture( 64, 2 ) );
+		this.noiseNode = null;
 
 		/**
 		 * Represents the projection matrix of the scene's camera.
@@ -450,7 +449,6 @@ class GTAONode extends TempNode {
 
 		};
 
-		const sampleNoise = ( uv ) => this._noiseNode.sample( uv );
 		const sampleNormal = ( uv ) => ( normalSourceNode !== null )
 			? normalSourceNode.sample( uv ).rgb.normalize()
 			: getNormalFromDepth( uv, fallbackDepthForNormal, this._cameraProjectionMatrixInverse );
@@ -466,19 +464,26 @@ class GTAONode extends TempNode {
 
 			const radiusToUse = this.radius;
 
-			// Tile the blue-noise texture across the screen at its native resolution.
-			const noiseResolution = textureSize( this._noiseNode, 0 );
-			const noiseUv = uvNode.mul( this.resolution.div( noiseResolution ) );
+			// Per-pixel sampling noise. `noise1` → slice rotation, `noise2` → per-step
+			// phase jitter. Both are scrolled per frame by the R2 jitter offset (zero
+			// when `jitter` is off) so each frame is a decorrelated realization for the
+			// temporal resolve. Default: cheap interleaved gradient noise (pure ALU, no
+			// fetch). When a noise texture is injected, sample its R/G channels instead.
+			let noise1, noise2;
+
+			if ( this.noiseNode !== null ) {
+
+				const noiseUv = uvNode.mul( this.resolution.div( textureSize( this.noiseNode, 0 ) ) );
+				const noiseSample = this.noiseNode.sample( noiseUv );
+				noise1 = fract( noiseSample.r.add( this._jitterOffset.x ) );
+				noise2 = fract( noiseSample.g.add( this._jitterOffset.y ) );
+
+			} else {
 
-			// R and G are independent blue-noise patterns generated with different
-			// seeds, so one fetch gives a decorrelated pair. `noise1` → slice
-			// rotation, `noise2` → per-step phase jitter. Each is scrolled per frame
-			// by its R2 golden-ratio offset (zero when temporal filtering is off), so
-			// successive frames are maximally decorrelated for the temporal
-			// accumulator while every frame keeps its blue-noise spectrum.
-			const noiseSample = sampleNoise( noiseUv );
-			const noise1 = fract( noiseSample.r.add( this._jitterOffset.x ) );
-			const noise2 = fract( noiseSample.g.add( this._jitterOffset.y ) );
+				noise1 = fract( interleavedGradientNoise( screenCoordinate ).add( this._jitterOffset.x ) );
+				noise2 = fract( interleavedGradientNoise( screenCoordinate.add( vec2( 97.0, 71.0 ) ) ).add( this._jitterOffset.y ) );
+
+			}
 
 			// Random tangent direction from noise1, used to rotate the per-slice azimuth.
 			const tangentAngle = noise1.mul( PI ).mul( 2.0 );
@@ -497,13 +502,22 @@ class GTAONode extends TempNode {
 			// (Activision GTAO slides 86, 92–93 "Noise Distribution".)
 			const stepJitter = noise2.toVar();
 
+			// Loop-invariant (constant per pixel): view direction and the clip-space ray
+			// origin. Projecting the slice direction once per slice (clipStep, below) turns
+			// each ray-march step into a clip-space multiply-add instead of a full matrix
+			// transform per tap.
+			const viewDir = normalize( viewPosition.xyz.negate() ).toVar();
+			const clipCenter = this._cameraProjectionMatrix.mul( vec4( viewPosition.xyz, 1.0 ) ).toVar();
+
 			Loop( { start: int( 0 ), end: DIRECTIONS, type: 'int', condition: '<' }, ( { i } ) => {
 
 				const angle = float( i ).div( float( DIRECTIONS ) ).mul( PI ).toVar();
 				const sampleDir = vec3( cos( angle ), sin( angle ), 0 ).toVar();
 				sampleDir.assign( normalize( kernelMatrix.mul( sampleDir ) ) );
 
-				const viewDir = normalize( viewPosition.xyz.negate() ).toVar();
+				// Clip-space step direction for this slice (see clipCenter above).
+				const clipStep = this._cameraProjectionMatrix.mul( vec4( sampleDir, 0.0 ) ).toVar();
+
 				const sliceBitangent = normalize( cross( sampleDir, viewDir ) ).toVar();
 				const sliceTangent = cross( sliceBitangent, viewDir ).toVar();
 
@@ -531,13 +545,15 @@ class GTAONode extends TempNode {
 					// 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 );
+					const offsetScale = radiusToUse.mul( sampleDist ).toVar();
 
 					// The loop marches in two opposite directions (x and y) along the slice's line to find the horizon on both sides.
 
-					// x
+					// x — clip-space ray step ( = getScreenPosition( viewPosition + sampleDir·offsetScale ) )
 
-					const sampleScreenPositionX = getScreenPosition( viewPosition.add( sampleViewOffset ), this._cameraProjectionMatrix ).toVar();
+					const clipX = clipCenter.add( clipStep.mul( offsetScale ) ).toVar();
+					const sampleUvX = clipX.xy.div( clipX.w ).mul( 0.5 ).add( 0.5 ).toVar();
+					const sampleScreenPositionX = vec2( sampleUvX.x, sampleUvX.y.oneMinus() ).toVar();
 					const sampleDepthX = sampleDepth( sampleScreenPositionX ).toVar();
 					const sampleSceneViewPositionX = getViewPosition( sampleScreenPositionX, sampleDepthX, this._cameraProjectionMatrixInverse ).toVar();
 					const viewDeltaX = sampleSceneViewPositionX.sub( viewPosition ).toVar();
@@ -559,9 +575,11 @@ class GTAONode extends TempNode {
 
 					} );
 
-					// y
+					// y — opposite clip-space ray step (reuses clipStep)
 
-					const sampleScreenPositionY = getScreenPosition( viewPosition.sub( sampleViewOffset ), this._cameraProjectionMatrix ).toVar();
+					const clipY = clipCenter.sub( clipStep.mul( offsetScale ) ).toVar();
+					const sampleUvY = clipY.xy.div( clipY.w ).mul( 0.5 ).add( 0.5 ).toVar();
+					const sampleScreenPositionY = vec2( sampleUvY.x, sampleUvY.y.oneMinus() ).toVar();
 					const sampleDepthY = sampleDepth( sampleScreenPositionY ).toVar();
 					const sampleSceneViewPositionY = getViewPosition( sampleScreenPositionY, sampleDepthY, this._cameraProjectionMatrixInverse ).toVar();
 					const viewDeltaY = sampleSceneViewPositionY.sub( viewPosition ).toVar();

粤ICP备19079148号