|
|
@@ -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();
|