rayzee 7.0.0 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/rayzee.es.js +1676 -1636
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +20 -20
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +16 -5
- package/src/PathTracerApp.js +10 -3
- package/src/Processor/PackedRayBuffer.js +33 -7
- package/src/Stages/ASVGF.js +122 -79
- package/src/Stages/BilateralFilter.js +23 -4
- package/src/Stages/NormalDepth.js +101 -7
- package/src/Stages/Variance.js +7 -3
- package/src/TSL/GenerateKernel.js +3 -1
- package/src/TSL/ShadeKernel.js +5 -2
- package/src/managers/CameraManager.js +19 -0
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -127,6 +127,9 @@ export const ENGINE_DEFAULTS = {
|
|
|
127
127
|
asvgfPhiDepth: 1.0,
|
|
128
128
|
asvgfVarianceBoost: 1.0,
|
|
129
129
|
asvgfMaxAccumFrames: 32,
|
|
130
|
+
asvgfGradientStrength: 0.0,
|
|
131
|
+
asvgfGradientSigmaScale: 2.0,
|
|
132
|
+
asvgfGradientNoiseFloor: 0.0,
|
|
130
133
|
asvgfDebugMode: 0,
|
|
131
134
|
asvgfQualityPreset: 'medium',
|
|
132
135
|
showAsvgfHeatmap: false,
|
|
@@ -158,11 +161,15 @@ export const MAX_STORAGE_TEXTURE_SIZE = 2048;
|
|
|
158
161
|
|
|
159
162
|
export const ASVGF_QUALITY_PRESETS = {
|
|
160
163
|
// phiColor / phiDepth are RELATIVE tolerances (fractions). Bigger = more
|
|
161
|
-
// permissive.
|
|
162
|
-
//
|
|
164
|
+
// permissive. The adaptive temporal gradient (gradientStrength > 0) is always
|
|
165
|
+
// on: it measures real change in units of noise σ (gradientSigmaScale), so a
|
|
166
|
+
// static scene reads ~0 (no convergence penalty) and only moving lights / anim
|
|
167
|
+
// / disocclusion drop history. See ASVGF._buildGradientCompute.
|
|
163
168
|
low: {
|
|
164
169
|
temporalAlpha: 0.1,
|
|
165
|
-
gradientStrength: 0.
|
|
170
|
+
gradientStrength: 0.8,
|
|
171
|
+
gradientSigmaScale: 2.5,
|
|
172
|
+
gradientNoiseFloor: 0.05,
|
|
166
173
|
atrousIterations: 3,
|
|
167
174
|
phiColor: 1.0,
|
|
168
175
|
phiNormal: 64.0,
|
|
@@ -173,7 +180,9 @@ export const ASVGF_QUALITY_PRESETS = {
|
|
|
173
180
|
},
|
|
174
181
|
medium: {
|
|
175
182
|
temporalAlpha: 0.03,
|
|
176
|
-
gradientStrength:
|
|
183
|
+
gradientStrength: 1.0,
|
|
184
|
+
gradientSigmaScale: 2.5,
|
|
185
|
+
gradientNoiseFloor: 0.05,
|
|
177
186
|
atrousIterations: 4,
|
|
178
187
|
phiColor: 0.5,
|
|
179
188
|
phiNormal: 128.0,
|
|
@@ -184,7 +193,9 @@ export const ASVGF_QUALITY_PRESETS = {
|
|
|
184
193
|
},
|
|
185
194
|
high: {
|
|
186
195
|
temporalAlpha: 0.0,
|
|
187
|
-
gradientStrength:
|
|
196
|
+
gradientStrength: 1.0,
|
|
197
|
+
gradientSigmaScale: 2.5,
|
|
198
|
+
gradientNoiseFloor: 0.05,
|
|
188
199
|
atrousIterations: 6,
|
|
189
200
|
phiColor: 0.3,
|
|
190
201
|
phiNormal: 256.0,
|
package/src/PathTracerApp.js
CHANGED
|
@@ -310,12 +310,19 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
this._ensureVRAMWiring();
|
|
313
|
-
|
|
313
|
+
// VRAM is monotonic and only changes on allocation events (scene/env
|
|
314
|
+
// load, resize — each re-measures via _ensureVRAMWiring). Within an
|
|
315
|
+
// accumulation burst nothing reallocates, so re-walking every stage's
|
|
316
|
+
// textures each frame is wasted. Measure at burst start (catches any
|
|
317
|
+
// reset-triggered allocation) + a periodic backstop; read cached otherwise.
|
|
318
|
+
const tracker = this.stages.pathTracer?.vramTracker;
|
|
319
|
+
const frame = this.stages.pathTracer?.frameCount ?? 0;
|
|
320
|
+
if ( tracker && ( frame <= 1 || frame % 30 === 0 ) ) tracker.measure();
|
|
314
321
|
updateStats( {
|
|
315
322
|
timeElapsed: this.completion.timeElapsed,
|
|
316
323
|
samples: getDisplaySamples( this.stages.pathTracer ),
|
|
317
|
-
memoryUsed:
|
|
318
|
-
memoryPeak:
|
|
324
|
+
memoryUsed: tracker?.current ?? 0,
|
|
325
|
+
memoryPeak: tracker?.peak ?? 0,
|
|
319
326
|
} );
|
|
320
327
|
|
|
321
328
|
// Check time limit
|
|
@@ -11,10 +11,13 @@ import { StorageInstancedBufferAttribute } from 'three/webgpu';
|
|
|
11
11
|
|
|
12
12
|
export const RAY_STRIDE = 7;
|
|
13
13
|
export const HIT_STRIDE = 2;
|
|
14
|
-
// Per-pixel G-buffer (first-hit MRT staging):
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
14
|
+
// Per-pixel G-buffer (first-hit MRT staging): 2 uvec4/pixel (AoS, element p*GBUFFER_STRIDE + lane).
|
|
15
|
+
// lane 0 — half-packed normal/depth/albedo (pack2x16, no f32 bitcast); read by FinalWrite:
|
|
16
|
+
// .x=packSnorm2x16(normal.xy) .y=packSnorm2x16(normal.z, depth) .z=packUnorm2x16(albedo.rg) .w=packUnorm2x16(albedo.b, 0)
|
|
17
|
+
// lane 1 — primary-hit surface ID for A-SVGF correlated-gradient re-projection (Tier 1); written at the
|
|
18
|
+
// bounce-0 hit, valid=0 on miss (Generate inits): .x=triIndex .y=meshIndex .z=packUnorm2x16(bary.u,bary.v) .w=valid
|
|
19
|
+
// Separate buffer from RAY (per-pixel, not per-ray×S) — written by Generate/Shade bounce-0.
|
|
20
|
+
export const GBUFFER_STRIDE = 2;
|
|
18
21
|
|
|
19
22
|
export const RAY = {
|
|
20
23
|
ORIGIN_META: 0, // vec4(origin.xyz, uintBitsToFloat(perRayBounces | sssSteps<<8)); pixelIndex+sampleIndex derived from rayID
|
|
@@ -146,17 +149,40 @@ export const readRayPdf = ( buf, id ) =>
|
|
|
146
149
|
export const readRayRadiance = ( buf, id ) =>
|
|
147
150
|
buf.element( soa( id, RAY.RADIANCE_ALPHA ) );
|
|
148
151
|
|
|
149
|
-
// ── Per-pixel G-buffer (first-hit MRT).
|
|
152
|
+
// ── Per-pixel G-buffer (first-hit MRT). 2 uvec4/pixel (AoS), pack2x16 lanes. ──
|
|
150
153
|
// normal: raw unit vec3; depth: linear [0,1]; albedo: vec3 [0,1]. Packed values live in u32 lanes
|
|
151
154
|
// verbatim (no f32 bitcast) so NaN-range bit patterns (snorm ±1 → 0x7FFF) survive store/load intact.
|
|
155
|
+
// gbLane resolves the AoS slot for a pixel (lane 0 = MRT, lane 1 = surface ID).
|
|
156
|
+
const gbLane = ( pixelIndex, lane ) => {
|
|
157
|
+
|
|
158
|
+
const base = uint( pixelIndex ).mul( GBUFFER_STRIDE );
|
|
159
|
+
return lane === 0 ? base : base.add( lane );
|
|
160
|
+
|
|
161
|
+
};
|
|
162
|
+
|
|
152
163
|
export const writeGBuffer = ( buf, pixelIndex, normal, depth, albedo ) =>
|
|
153
|
-
buf.element( pixelIndex ).assign( uvec4(
|
|
164
|
+
buf.element( gbLane( pixelIndex, 0 ) ).assign( uvec4(
|
|
154
165
|
packSnorm2x16( vec2( normal.x, normal.y ) ),
|
|
155
166
|
packSnorm2x16( vec2( normal.z, depth ) ),
|
|
156
167
|
packUnorm2x16( vec2( albedo.x, albedo.y ) ),
|
|
157
168
|
packUnorm2x16( vec2( albedo.z, 0.0 ) ),
|
|
158
169
|
) );
|
|
159
|
-
export const readGBuffer = ( buf, pixelIndex ) => buf.element( pixelIndex );
|
|
170
|
+
export const readGBuffer = ( buf, pixelIndex ) => buf.element( gbLane( pixelIndex, 0 ) );
|
|
171
|
+
|
|
172
|
+
// Lane 1 — primary-hit surface ID for A-SVGF correlated gradient re-projection (Tier 1).
|
|
173
|
+
// valid=0 marks a miss (no primary surface); bary packed unorm (both in [0,1]).
|
|
174
|
+
export const writeGBufferSurfaceID = ( buf, pixelIndex, triIndex, meshIndex, baryU, baryV, valid ) =>
|
|
175
|
+
buf.element( gbLane( pixelIndex, 1 ) ).assign( uvec4(
|
|
176
|
+
uint( triIndex ), uint( meshIndex ), packUnorm2x16( vec2( baryU, baryV ) ), uint( valid ),
|
|
177
|
+
) );
|
|
178
|
+
export const readGBufferSurfaceID = ( buf, pixelIndex ) => {
|
|
179
|
+
|
|
180
|
+
const p = buf.element( gbLane( pixelIndex, 1 ) );
|
|
181
|
+
const bary = unpackUnorm2x16( p.z );
|
|
182
|
+
return { triIndex: p.x, meshIndex: p.y, baryU: bary.x, baryV: bary.y, valid: p.w };
|
|
183
|
+
|
|
184
|
+
};
|
|
185
|
+
|
|
160
186
|
// Decode for FinalWrite. normalDepth.xyz matches the prior path (normal*0.5+0.5), .w = raw depth.
|
|
161
187
|
export const gbDecodeNormalDepth = ( packed ) => {
|
|
162
188
|
|
package/src/Stages/ASVGF.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fn, vec3, vec4, float, int, uint, ivec2, uvec2, uniform,
|
|
2
|
-
If, dot, max, min, abs, mix, pow, exp,
|
|
2
|
+
If, dot, max, min, abs, mix, pow, exp, sqrt, select,
|
|
3
3
|
textureLoad, textureStore, workgroupArray, workgroupBarrier, localId, workgroupId } from 'three/tsl';
|
|
4
4
|
import { RenderTarget, TextureNode, StorageTexture } from 'three/webgpu';
|
|
5
5
|
import { HalfFloatType, FloatType, RGBAFormat, NearestFilter, LinearFilter, Box2, Vector2 } from 'three';
|
|
@@ -7,14 +7,21 @@ import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
|
7
7
|
import { luminance } from '../TSL/Common.js';
|
|
8
8
|
import { ALBEDO_EPS, MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
|
|
9
9
|
|
|
10
|
+
// Replace NaN/±Inf with a bounded value so one firefly can't permanently poison the
|
|
11
|
+
// temporal EMA (mix() propagates NaN forever). Per-channel: NaN (x!=x) → 0, ±Inf → [0,1e7].
|
|
12
|
+
const sanitize1 = ( x ) => select( x.equal( x ), x, float( 0.0 ) ).clamp( 0.0, 1e7 );
|
|
13
|
+
const sanitizeRGB = ( c ) => vec3( sanitize1( c.x ), sanitize1( c.y ), sanitize1( c.z ) );
|
|
14
|
+
|
|
10
15
|
/**
|
|
11
|
-
* ASVGF — SVGF temporal
|
|
16
|
+
* ASVGF — SVGF temporal denoising with albedo demodulation + adaptive-α.
|
|
12
17
|
*
|
|
13
|
-
* Adaptive
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* 1
|
|
18
|
+
* Adaptive temporal gradient (the "A"): the gradient kernel compares this
|
|
19
|
+
* frame's demodulated lighting against the reprojected accumulated history,
|
|
20
|
+
* floored by a per-pixel σ (max(Δ − k·σ, 0)), max-normalised and squared
|
|
21
|
+
* (Q2RTX get_gradient). gradientStrength scales it into effectiveAlpha =
|
|
22
|
+
* mix(baseAlpha, 1, gradient·gradientStrength) — high change ⇒ drop history
|
|
23
|
+
* (anti-lag). All quality presets enable it (gradientStrength > 0); a static
|
|
24
|
+
* scene reads gradient ≈ 0, so convergence is unaffected.
|
|
18
25
|
*
|
|
19
26
|
* Reads: pathtracer:color, pathtracer:albedo, pathtracer:normalDepth,
|
|
20
27
|
* pathtracer:prevNormalDepth, motionVector:screenSpace
|
|
@@ -34,16 +41,21 @@ export class ASVGF extends RenderStage {
|
|
|
34
41
|
|
|
35
42
|
this.temporalAlpha = uniform( options.temporalAlpha ?? 0.0 );
|
|
36
43
|
this.gradientStrength = uniform( options.gradientStrength ?? 0.0 );
|
|
37
|
-
|
|
44
|
+
// σ multiplier for the per-pixel noise floor (NRD luminanceSigmaScale ≈ 2).
|
|
45
|
+
this.gradientSigmaScale = uniform( options.gradientSigmaScale ?? 2.0 );
|
|
46
|
+
// Secondary relative floor on the normalised gradient (0 = rely on σ alone).
|
|
47
|
+
this.gradientNoiseFloor = uniform( options.gradientNoiseFloor ?? 0.0 );
|
|
38
48
|
this.maxAccumFrames = uniform( options.maxAccumFrames ?? 32.0 );
|
|
39
49
|
|
|
40
50
|
this.resW = uniform( options.width || 1 );
|
|
41
51
|
this.resH = uniform( options.height || 1 );
|
|
42
52
|
|
|
43
53
|
this.temporalEnabledU = uniform( 1.0 );
|
|
54
|
+
// 1.0 for one frame after asvgf:reset → forces a fresh sample so stale pre-reset
|
|
55
|
+
// (wrong-scene) history isn't blended in (mirrors Variance's _needsWarmReset).
|
|
56
|
+
this.forceResetU = uniform( 0.0 );
|
|
44
57
|
|
|
45
58
|
this._colorTexNode = new TextureNode();
|
|
46
|
-
this._prevColorTexNode = new TextureNode();
|
|
47
59
|
this._albedoTexNode = new TextureNode();
|
|
48
60
|
this._motionTexNode = new TextureNode();
|
|
49
61
|
this._normalDepthTexNode = new TextureNode();
|
|
@@ -112,20 +124,9 @@ export class ASVGF extends RenderStage {
|
|
|
112
124
|
stencilBuffer: false
|
|
113
125
|
} );
|
|
114
126
|
|
|
115
|
-
// FloatType to match pathtracer:color (PT MRT). copyTextureToTexture
|
|
116
|
-
// requires identical formats.
|
|
117
|
-
this._prevColorRT = new RenderTarget( w, h, {
|
|
118
|
-
type: FloatType,
|
|
119
|
-
format: RGBAFormat,
|
|
120
|
-
minFilter: NearestFilter,
|
|
121
|
-
magFilter: NearestFilter,
|
|
122
|
-
depthBuffer: false,
|
|
123
|
-
stencilBuffer: false
|
|
124
|
-
} );
|
|
125
|
-
this._prevColorReady = false;
|
|
126
|
-
|
|
127
127
|
this.currentMoments = 0; // 0 = write A, read B; 1 = write B, read A
|
|
128
128
|
this._compiled = false;
|
|
129
|
+
this._needsWarmReset = false;
|
|
129
130
|
|
|
130
131
|
this._dispatchX = Math.ceil( w / 8 );
|
|
131
132
|
this._dispatchY = Math.ceil( h / 8 );
|
|
@@ -162,17 +163,24 @@ export class ASVGF extends RenderStage {
|
|
|
162
163
|
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
166
|
+
// Adaptive temporal gradient: per pixel, compare this frame's DEMODULATED
|
|
167
|
+
// lighting against the motion-reprojected accumulated history (low-noise),
|
|
168
|
+
// floored by a per-pixel σ from the 5×5 spatial neighbourhood. The σ floor
|
|
169
|
+
// is what the old fixed 0.15 constant couldn't give — on a static scene the
|
|
170
|
+
// 1-SPP sample sits within ±k·σ of the converged estimate so the gradient
|
|
171
|
+
// reads ~0 (no convergence penalty); only real change (moving light, anim,
|
|
172
|
+
// disocclusion) exceeds it. Demodulation (vs raw color) keeps albedo/texture
|
|
173
|
+
// edges from false-firing. Output .x feeds effectiveAlpha in the temporal pass.
|
|
170
174
|
_buildGradientCompute() {
|
|
171
175
|
|
|
172
176
|
const colorTex = this._colorTexNode;
|
|
173
|
-
const
|
|
177
|
+
const albedoTex = this._albedoTexNode;
|
|
174
178
|
const motionTex = this._motionTexNode;
|
|
179
|
+
// Accumulated history (demodulated lighting in .xyz, history count in .w).
|
|
180
|
+
// Same node the temporal pass reads; set to readTemporal before dispatch.
|
|
181
|
+
const histTex = this._readTemporalTexNode;
|
|
175
182
|
const noiseFloor = this.gradientNoiseFloor;
|
|
183
|
+
const sigmaScale = this.gradientSigmaScale;
|
|
176
184
|
const gradientStorageTex = this._gradientStorageTex;
|
|
177
185
|
const resW = this.resW;
|
|
178
186
|
const resH = this.resH;
|
|
@@ -185,7 +193,7 @@ export class ASVGF extends RenderStage {
|
|
|
185
193
|
const WG_THREADS = WG_SIZE * WG_SIZE; // 64
|
|
186
194
|
|
|
187
195
|
const sharedCurLum = workgroupArray( 'float', TILE_TOTAL );
|
|
188
|
-
const
|
|
196
|
+
const sharedHistLum = workgroupArray( 'float', TILE_TOTAL );
|
|
189
197
|
|
|
190
198
|
const computeFn = Fn( () => {
|
|
191
199
|
|
|
@@ -204,20 +212,28 @@ export class ASVGF extends RenderStage {
|
|
|
204
212
|
const gxL = tileOriginX.add( int( sx ) ).clamp( int( 0 ), int( resW ).sub( 1 ) );
|
|
205
213
|
const gyL = tileOriginY.add( int( sy ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
206
214
|
|
|
215
|
+
// Demodulated current lighting luminance (matches the temporal pass:
|
|
216
|
+
// safeAlbedo = max(albedo, ALBEDO_EPS) keeps sky/dark-material round-trip).
|
|
207
217
|
const curColor = textureLoad( colorTex, ivec2( gxL, gyL ) ).xyz;
|
|
208
|
-
|
|
209
|
-
|
|
218
|
+
const curAlbedo = textureLoad( albedoTex, ivec2( gxL, gyL ) ).xyz;
|
|
219
|
+
const curLighting = sanitizeRGB( curColor.div( max( curAlbedo, vec3( ALBEDO_EPS ) ) ) );
|
|
220
|
+
const curLum = luminance( curLighting ).toVar();
|
|
221
|
+
sharedCurLum.element( k ).assign( curLum );
|
|
222
|
+
|
|
223
|
+
// Reproject the accumulated history to this pixel; read its lighting
|
|
224
|
+
// luminance. Invalid motion → mirror current so the delta is 0
|
|
225
|
+
// (disocclusion handled by the temporal pass's geometric gate).
|
|
210
226
|
const motion = textureLoad( motionTex, ivec2( gxL, gyL ) );
|
|
211
227
|
const prevXf = float( gxL ).sub( motion.x.mul( resW ) );
|
|
212
228
|
const prevYf = float( gyL ).sub( motion.y.mul( resH ) );
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
229
|
+
// Round-to-nearest (not truncate) — the gradient does a nearest-neighbour
|
|
230
|
+
// history lookup; +0.5 removes the half-pixel floor bias vs the temporal tap.
|
|
231
|
+
const prevX = int( prevXf.add( 0.5 ) ).clamp( int( 0 ), int( resW ).sub( 1 ) );
|
|
232
|
+
const prevY = int( prevYf.add( 0.5 ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
217
233
|
const motionValid = motion.w.greaterThan( 0.5 );
|
|
218
|
-
const
|
|
219
|
-
const
|
|
220
|
-
|
|
234
|
+
const histLighting = textureLoad( histTex, ivec2( prevX, prevY ) ).xyz;
|
|
235
|
+
const histLum = motionValid.select( luminance( histLighting ), curLum );
|
|
236
|
+
sharedHistLum.element( k ).assign( histLum );
|
|
221
237
|
|
|
222
238
|
};
|
|
223
239
|
|
|
@@ -245,8 +261,33 @@ export class ASVGF extends RenderStage {
|
|
|
245
261
|
|
|
246
262
|
If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
|
|
247
263
|
|
|
248
|
-
|
|
249
|
-
const
|
|
264
|
+
// Pass 1 — per-pixel noise σ from the 5×5 (demodulated) neighbourhood.
|
|
265
|
+
const sumLum = float( 0.0 ).toVar();
|
|
266
|
+
const sumLumSq = float( 0.0 ).toVar();
|
|
267
|
+
|
|
268
|
+
for ( let dy = - TILE_BORDER; dy <= TILE_BORDER; dy ++ ) {
|
|
269
|
+
|
|
270
|
+
for ( let dx = - TILE_BORDER; dx <= TILE_BORDER; dx ++ ) {
|
|
271
|
+
|
|
272
|
+
const idx = ly.add( uint( TILE_BORDER + dy ) )
|
|
273
|
+
.mul( uint( TILE_W ) )
|
|
274
|
+
.add( lx.add( uint( TILE_BORDER + dx ) ) );
|
|
275
|
+
const cL = sharedCurLum.element( idx );
|
|
276
|
+
sumLum.addAssign( cL );
|
|
277
|
+
sumLumSq.addAssign( cL.mul( cL ) );
|
|
278
|
+
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const meanLum = sumLum.div( 25.0 );
|
|
284
|
+
const variance = max( sumLumSq.div( 25.0 ).sub( meanLum.mul( meanLum ) ), float( 0.0 ) );
|
|
285
|
+
const sigmaFloor = sqrt( variance ).mul( sigmaScale ).toVar();
|
|
286
|
+
|
|
287
|
+
// Pass 2 — 5×5 average of the squared, σ-floored, max-normalised
|
|
288
|
+
// temporal change. Squaring (Q2RTX get_gradient) suppresses residual
|
|
289
|
+
// MC noise while staying reactive in high contrast.
|
|
290
|
+
const sumG = float( 0.0 ).toVar();
|
|
250
291
|
|
|
251
292
|
for ( let dy = - TILE_BORDER; dy <= TILE_BORDER; dy ++ ) {
|
|
252
293
|
|
|
@@ -256,26 +297,35 @@ export class ASVGF extends RenderStage {
|
|
|
256
297
|
.mul( uint( TILE_W ) )
|
|
257
298
|
.add( lx.add( uint( TILE_BORDER + dx ) ) );
|
|
258
299
|
const cL = sharedCurLum.element( idx );
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
300
|
+
const hL = sharedHistLum.element( idx );
|
|
301
|
+
const floored = max( abs( cL.sub( hL ) ).sub( sigmaFloor ), float( 0.0 ) );
|
|
302
|
+
const g = floored.div( max( max( cL, hL ), float( 0.001 ) ) );
|
|
303
|
+
// Optional secondary relative floor (noiseFloor=0 → no-op).
|
|
304
|
+
const gf = max( g.sub( noiseFloor ), float( 0.0 ) )
|
|
305
|
+
.div( max( float( 1.0 ).sub( noiseFloor ), float( 0.0001 ) ) );
|
|
306
|
+
sumG.addAssign( gf.mul( gf ) );
|
|
262
307
|
|
|
263
308
|
}
|
|
264
309
|
|
|
265
310
|
}
|
|
266
311
|
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
312
|
+
const gradientRaw = sumG.div( 25.0 ).clamp( 0.0, 1.0 ).toVar();
|
|
313
|
+
|
|
314
|
+
// Trust the gradient only once enough history has accumulated at the
|
|
315
|
+
// reprojected centre — early frames have noisy history → false fires.
|
|
316
|
+
const cMotion = textureLoad( motionTex, ivec2( gx, gy ) );
|
|
317
|
+
const cPrevX = int( float( gx ).sub( cMotion.x.mul( resW ) ).add( 0.5 ) ).clamp( int( 0 ), int( resW ).sub( 1 ) );
|
|
318
|
+
const cPrevY = int( float( gy ).sub( cMotion.y.mul( resH ) ).add( 0.5 ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
319
|
+
const histLen = cMotion.w.greaterThan( 0.5 )
|
|
320
|
+
.select( textureLoad( histTex, ivec2( cPrevX, cPrevY ) ).w, float( 0.0 ) );
|
|
321
|
+
const confidence = histLen.div( 4.0 ).clamp( 0.0, 1.0 );
|
|
322
|
+
|
|
323
|
+
const gradient = gradientRaw.mul( confidence );
|
|
274
324
|
|
|
275
325
|
textureStore(
|
|
276
326
|
gradientStorageTex,
|
|
277
327
|
uvec2( uint( gx ), uint( gy ) ),
|
|
278
|
-
vec4( gradient,
|
|
328
|
+
vec4( gradient, gradientRaw, sigmaFloor, 1.0 )
|
|
279
329
|
).toWriteOnly();
|
|
280
330
|
|
|
281
331
|
} );
|
|
@@ -316,6 +366,7 @@ export class ASVGF extends RenderStage {
|
|
|
316
366
|
const temporalAlphaMin = this.temporalAlpha;
|
|
317
367
|
const gradientStrength = this.gradientStrength;
|
|
318
368
|
const temporalEnabledU = this.temporalEnabledU;
|
|
369
|
+
const forceResetU = this.forceResetU;
|
|
319
370
|
const resW = this.resW;
|
|
320
371
|
const resH = this.resH;
|
|
321
372
|
|
|
@@ -335,23 +386,28 @@ export class ASVGF extends RenderStage {
|
|
|
335
386
|
// Same safeAlbedo on both demod and re-mod sides → exact
|
|
336
387
|
// round-trip for sky/miss rays where albedo=0.
|
|
337
388
|
const safeAlbedo = max( currentAlbedo, vec3( ALBEDO_EPS ) );
|
|
338
|
-
const currentLighting = currentColor.div( safeAlbedo );
|
|
389
|
+
const currentLighting = sanitizeRGB( currentColor.div( safeAlbedo ) );
|
|
339
390
|
|
|
340
391
|
// Defaults = fresh sample (no temporal blend).
|
|
341
392
|
const demodResult = vec4( currentLighting, 1.0 ).toVar();
|
|
342
393
|
const modulatedResult = vec4( currentColor, 1.0 ).toVar();
|
|
343
394
|
|
|
344
|
-
|
|
395
|
+
// forceResetU skips the blend for one frame after asvgf:reset → re-anchors
|
|
396
|
+
// history to the current (post-reset) scene instead of the stale ping-pong.
|
|
397
|
+
If( temporalEnabledU.greaterThan( 0.5 ).and( forceResetU.lessThan( 0.5 ) ), () => {
|
|
345
398
|
|
|
346
399
|
const motion = textureLoad( motionTex, coord );
|
|
347
400
|
const motionValid = motion.w.greaterThan( 0.5 );
|
|
348
401
|
|
|
349
402
|
const prevXf = float( gx ).sub( motion.x.mul( resW ) );
|
|
350
403
|
const prevYf = float( gy ).sub( motion.y.mul( resH ) );
|
|
404
|
+
// Upper bound is the inclusive last pixel (< res, not < res-1): the 2×2 taps
|
|
405
|
+
// already clamp to [0,res-1] and wSum gates bad taps, so res-1 wrongly
|
|
406
|
+
// rejected the trailing column/row. Matches MotionVector's inclusive UV.
|
|
351
407
|
const prevOnScreen = prevXf.greaterThanEqual( 0.0 )
|
|
352
|
-
.and( prevXf.lessThan( float( resW )
|
|
408
|
+
.and( prevXf.lessThan( float( resW ) ) )
|
|
353
409
|
.and( prevYf.greaterThanEqual( 0.0 ) )
|
|
354
|
-
.and( prevYf.lessThan( float( resH )
|
|
410
|
+
.and( prevYf.lessThan( float( resH ) ) );
|
|
355
411
|
|
|
356
412
|
If( motionValid.and( prevOnScreen ), () => {
|
|
357
413
|
|
|
@@ -419,7 +475,7 @@ export class ASVGF extends RenderStage {
|
|
|
419
475
|
.add( p11.w.mul( v11 ) )
|
|
420
476
|
.mul( invWSum );
|
|
421
477
|
|
|
422
|
-
// adaptive α —
|
|
478
|
+
// adaptive α — gradient·gradientStrength boosts toward 1 on change.
|
|
423
479
|
const gradient = textureLoad( gradientTex, coord ).x;
|
|
424
480
|
const adaptiveBoost = gradient.mul( gradientStrength ).clamp( 0.0, 1.0 );
|
|
425
481
|
|
|
@@ -429,7 +485,9 @@ export class ASVGF extends RenderStage {
|
|
|
429
485
|
);
|
|
430
486
|
const effectiveAlpha = mix( baseAlpha, float( 1.0 ), adaptiveBoost );
|
|
431
487
|
|
|
432
|
-
|
|
488
|
+
// Sanitize the blend too: a NaN already baked into prevLighting
|
|
489
|
+
// would otherwise survive mix() and re-poison the write target.
|
|
490
|
+
const blendedLighting = sanitizeRGB( mix( prevLighting, currentLighting, effectiveAlpha ) );
|
|
433
491
|
const newHistory = min( prevHistory.add( 1.0 ), maxAccumFrames );
|
|
434
492
|
|
|
435
493
|
demodResult.assign( vec4( blendedLighting, newHistory ) );
|
|
@@ -635,12 +693,6 @@ export class ASVGF extends RenderStage {
|
|
|
635
693
|
const writeTemporal = this.currentMoments === 0
|
|
636
694
|
? this._temporalTexA : this._temporalTexB;
|
|
637
695
|
|
|
638
|
-
// Before first copy seeds the cache, alias current so the gradient
|
|
639
|
-
// sees zero diff (no false boost).
|
|
640
|
-
this._prevColorTexNode.value = this._prevColorReady
|
|
641
|
-
? this._prevColorRT.texture
|
|
642
|
-
: colorTex;
|
|
643
|
-
|
|
644
696
|
// First-frame compile while StorageTexture-typed nodes still hold
|
|
645
697
|
// EmptyTexture, so textureLoad codegen emits the required `level`
|
|
646
698
|
// parameter. Binding StorageTextures only AFTER compile keeps the
|
|
@@ -668,16 +720,11 @@ export class ASVGF extends RenderStage {
|
|
|
668
720
|
|
|
669
721
|
}
|
|
670
722
|
|
|
723
|
+
// One-shot fresh re-anchor after asvgf:reset, threaded through the SINGLE
|
|
724
|
+
// writeNode dispatch (a dual-node warmup would alias the read target — see setSize).
|
|
725
|
+
this.forceResetU.value = this._needsWarmReset ? 1.0 : 0.0;
|
|
671
726
|
this.renderer.compute( writeNode );
|
|
672
|
-
|
|
673
|
-
// Cache this frame's pathtracer:color for next frame's gradient if it's
|
|
674
|
-
// active. Copy AFTER reads so we don't clobber the prev view.
|
|
675
|
-
if ( needsGradient ) {
|
|
676
|
-
|
|
677
|
-
this.renderer.copyTextureToTexture( colorTex, this._prevColorRT.texture );
|
|
678
|
-
this._prevColorReady = true;
|
|
679
|
-
|
|
680
|
-
}
|
|
727
|
+
this._needsWarmReset = false;
|
|
681
728
|
|
|
682
729
|
// Copy active region out of the over-allocated StorageTextures into
|
|
683
730
|
// right-sized RTs; downstream stages UV-sample these.
|
|
@@ -733,6 +780,7 @@ export class ASVGF extends RenderStage {
|
|
|
733
780
|
if ( ! params ) return;
|
|
734
781
|
if ( params.temporalAlpha !== undefined ) this.temporalAlpha.value = params.temporalAlpha;
|
|
735
782
|
if ( params.gradientStrength !== undefined ) this.gradientStrength.value = params.gradientStrength;
|
|
783
|
+
if ( params.gradientSigmaScale !== undefined ) this.gradientSigmaScale.value = params.gradientSigmaScale;
|
|
736
784
|
if ( params.gradientNoiseFloor !== undefined ) this.gradientNoiseFloor.value = params.gradientNoiseFloor;
|
|
737
785
|
if ( params.maxAccumFrames !== undefined ) this.maxAccumFrames.value = params.maxAccumFrames;
|
|
738
786
|
if ( params.debugMode !== undefined ) this.debugMode.value = params.debugMode;
|
|
@@ -742,8 +790,9 @@ export class ASVGF extends RenderStage {
|
|
|
742
790
|
resetTemporalData() {
|
|
743
791
|
|
|
744
792
|
this.currentMoments = 0;
|
|
745
|
-
//
|
|
746
|
-
|
|
793
|
+
// Re-anchor history on the next frame — the ping-pong textures still hold
|
|
794
|
+
// pre-reset (wrong-scene) lighting + history count, which would otherwise blend in.
|
|
795
|
+
this._needsWarmReset = true;
|
|
747
796
|
|
|
748
797
|
}
|
|
749
798
|
|
|
@@ -756,8 +805,6 @@ export class ASVGF extends RenderStage {
|
|
|
756
805
|
this._outputRT.texture.needsUpdate = true;
|
|
757
806
|
this._gradientRT.setSize( width, height );
|
|
758
807
|
this._gradientRT.texture.needsUpdate = true;
|
|
759
|
-
this._prevColorRT.setSize( width, height );
|
|
760
|
-
this._prevColorRT.texture.needsUpdate = true;
|
|
761
808
|
this.heatmapTarget.setSize( width, height );
|
|
762
809
|
this.heatmapTarget.texture.needsUpdate = true;
|
|
763
810
|
this.resW.value = width;
|
|
@@ -775,8 +822,6 @@ export class ASVGF extends RenderStage {
|
|
|
775
822
|
// would dispatch both temporal ping-pong nodes while _readTemporalTexNode still
|
|
776
823
|
// aliases one node's write target, producing a "write-only storage +
|
|
777
824
|
// TextureBinding in same synchronization scope" validation error.
|
|
778
|
-
// Only the size-dependent prev-color cache needs re-seeding.
|
|
779
|
-
this._prevColorReady = false;
|
|
780
825
|
|
|
781
826
|
}
|
|
782
827
|
|
|
@@ -799,13 +844,11 @@ export class ASVGF extends RenderStage {
|
|
|
799
844
|
this._demodulatedRT?.dispose();
|
|
800
845
|
this._outputRT?.dispose();
|
|
801
846
|
this._gradientRT?.dispose();
|
|
802
|
-
this._prevColorRT?.dispose();
|
|
803
847
|
this._heatmapComputeNode?.dispose();
|
|
804
848
|
this._heatmapStorageTex?.dispose();
|
|
805
849
|
this.heatmapTarget?.dispose();
|
|
806
850
|
|
|
807
851
|
this._colorTexNode?.dispose();
|
|
808
|
-
this._prevColorTexNode?.dispose();
|
|
809
852
|
this._albedoTexNode?.dispose();
|
|
810
853
|
this._motionTexNode?.dispose();
|
|
811
854
|
this._normalDepthTexNode?.dispose();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fn, wgslFn, vec3, vec4, float, int, uint, ivec2, uvec2, uniform, If, max, sqrt,
|
|
1
|
+
import { Fn, wgslFn, vec3, vec4, float, int, uint, ivec2, uvec2, uniform, If, max, mix, sqrt,
|
|
2
2
|
textureLoad, textureStore, localId, workgroupId } from 'three/tsl';
|
|
3
3
|
import { RenderTarget, TextureNode, StorageTexture } from 'three/webgpu';
|
|
4
4
|
import { HalfFloatType, RGBAFormat, LinearFilter, Box2, Vector2 } from 'three';
|
|
@@ -59,6 +59,9 @@ export class BilateralFilter extends RenderStage {
|
|
|
59
59
|
this.renderer = renderer;
|
|
60
60
|
this.inputTextureName = options.inputTextureName || 'asvgf:demodulated';
|
|
61
61
|
this.normalDepthTextureName = options.normalDepthTextureName || 'pathtracer:normalDepth';
|
|
62
|
+
// Mapped (normal/bump-perturbed) normal for the normal edge-stop — geometric
|
|
63
|
+
// normals are flat across a normal-mapped surface so normW can't preserve bump detail.
|
|
64
|
+
this.shadingNormalTextureName = options.shadingNormalTextureName || 'pathtracer:shadingNormal';
|
|
62
65
|
this.albedoTextureName = options.albedoTextureName || 'pathtracer:albedo';
|
|
63
66
|
this.varianceTextureName = options.varianceTextureName || 'variance:output';
|
|
64
67
|
this.iterations = options.iterations ?? 4;
|
|
@@ -69,6 +72,10 @@ export class BilateralFilter extends RenderStage {
|
|
|
69
72
|
this.phiNormal = uniform( options.phiNormal ?? 128.0 );
|
|
70
73
|
this.phiDepth = uniform( options.phiDepth ?? 0.05 );
|
|
71
74
|
this.phiLuminance = uniform( options.phiLuminance ?? 4.0 );
|
|
75
|
+
// Blend Variance's spatial-variance channel into sigma_l (1 = max(temporal, spatial),
|
|
76
|
+
// 0 = temporal-only). Widens the luminance gate where history is thin but the
|
|
77
|
+
// neighbourhood is noisy (disocclusion). Default on — validated −1.7% RMSE @4spp.
|
|
78
|
+
this.spatialVarianceWeight = uniform( options.spatialVarianceWeight ?? 1.0 );
|
|
72
79
|
this.stepSizeU = uniform( 1, 'int' );
|
|
73
80
|
// 1 on the final iteration → multiply by albedo to remodulate.
|
|
74
81
|
this.isLastIterationU = uniform( 0, 'int' );
|
|
@@ -77,6 +84,7 @@ export class BilateralFilter extends RenderStage {
|
|
|
77
84
|
|
|
78
85
|
this._readTexNode = new TextureNode();
|
|
79
86
|
this._normalDepthTexNode = new TextureNode();
|
|
87
|
+
this._shadingNormalTexNode = new TextureNode();
|
|
80
88
|
this._albedoTexNode = new TextureNode();
|
|
81
89
|
this._varianceTexNode = new TextureNode();
|
|
82
90
|
|
|
@@ -133,12 +141,14 @@ export class BilateralFilter extends RenderStage {
|
|
|
133
141
|
|
|
134
142
|
const readTexNode = this._readTexNode;
|
|
135
143
|
const ndTexNode = this._normalDepthTexNode;
|
|
144
|
+
const snTexNode = this._shadingNormalTexNode;
|
|
136
145
|
const albedoTexNode = this._albedoTexNode;
|
|
137
146
|
const varTexNode = this._varianceTexNode;
|
|
138
147
|
const phiColor = this.phiColor;
|
|
139
148
|
const phiNormal = this.phiNormal;
|
|
140
149
|
const phiDepth = this.phiDepth;
|
|
141
150
|
const phiLuminance = this.phiLuminance;
|
|
151
|
+
const spatialVarianceWeight = this.spatialVarianceWeight;
|
|
142
152
|
const stepSize = this.stepSizeU;
|
|
143
153
|
const isLastIterationU = this.isLastIterationU;
|
|
144
154
|
const resW = this.resW;
|
|
@@ -165,7 +175,8 @@ export class BilateralFilter extends RenderStage {
|
|
|
165
175
|
const coord = ivec2( gx, gy );
|
|
166
176
|
const centerColor = textureLoad( readTexNode, coord ).xyz;
|
|
167
177
|
const centerND = textureLoad( ndTexNode, coord );
|
|
168
|
-
|
|
178
|
+
// Normal edge-stop reads the mapped (shading) normal; depth gate stays geometric.
|
|
179
|
+
const centerNormal = textureLoad( snTexNode, coord ).xyz.mul( 2.0 ).sub( 1.0 );
|
|
169
180
|
const centerDepth = centerND.w;
|
|
170
181
|
const centerLum = luminance( centerColor );
|
|
171
182
|
const centerSafeAlbedo = max( textureLoad( albedoTexNode, coord ).xyz, vec3( ALBEDO_EPS ) );
|
|
@@ -176,7 +187,10 @@ export class BilateralFilter extends RenderStage {
|
|
|
176
187
|
// from demodulation — otherwise dark materials get an
|
|
177
188
|
// under-estimated sigma → over-strict luminance gate → no
|
|
178
189
|
// blending → silhouette dark-outline artifact.
|
|
179
|
-
|
|
190
|
+
// .z = temporal variance, .w = spatial (3×3) variance. Blend toward
|
|
191
|
+
// max(temporal, spatial) so disoccluded/low-history pixels widen sigma_l.
|
|
192
|
+
const vSample = textureLoad( varTexNode, coord );
|
|
193
|
+
const variance = mix( vSample.z, max( vSample.z, vSample.w ), spatialVarianceWeight );
|
|
180
194
|
const sigmaL = phiLuminance
|
|
181
195
|
.mul( sqrt( max( variance, float( 0.0 ) ) ) )
|
|
182
196
|
.div( centerAlbedoLum )
|
|
@@ -201,7 +215,7 @@ export class BilateralFilter extends RenderStage {
|
|
|
201
215
|
|
|
202
216
|
const sColor = textureLoad( readTexNode, ivec2( sx, sy ) ).xyz;
|
|
203
217
|
const sND = textureLoad( ndTexNode, ivec2( sx, sy ) );
|
|
204
|
-
const sNormal =
|
|
218
|
+
const sNormal = textureLoad( snTexNode, ivec2( sx, sy ) ).xyz.mul( 2.0 ).sub( 1.0 );
|
|
205
219
|
const sDepth = sND.w;
|
|
206
220
|
const sLum = luminance( sColor );
|
|
207
221
|
|
|
@@ -253,6 +267,8 @@ export class BilateralFilter extends RenderStage {
|
|
|
253
267
|
|| context.getTexture( 'asvgf:output' )
|
|
254
268
|
|| context.getTexture( 'pathtracer:color' );
|
|
255
269
|
const ndTex = context.getTexture( this.normalDepthTextureName );
|
|
270
|
+
// Fall back to geometric normalDepth if the mapped normal isn't published.
|
|
271
|
+
const snTex = context.getTexture( this.shadingNormalTextureName ) || ndTex;
|
|
256
272
|
const albedoTex = context.getTexture( this.albedoTextureName );
|
|
257
273
|
const varTex = context.getTexture( this.varianceTextureName );
|
|
258
274
|
|
|
@@ -273,6 +289,7 @@ export class BilateralFilter extends RenderStage {
|
|
|
273
289
|
|
|
274
290
|
// RenderTarget textures — safe to bind before first-compile.
|
|
275
291
|
if ( ndTex ) this._normalDepthTexNode.value = ndTex;
|
|
292
|
+
if ( snTex ) this._shadingNormalTexNode.value = snTex;
|
|
276
293
|
if ( albedoTex ) this._albedoTexNode.value = albedoTex;
|
|
277
294
|
|
|
278
295
|
// First-frame compile while StorageTexture-typed nodes still hold
|
|
@@ -329,6 +346,7 @@ export class BilateralFilter extends RenderStage {
|
|
|
329
346
|
if ( params.phiNormal !== undefined ) this.phiNormal.value = params.phiNormal;
|
|
330
347
|
if ( params.phiDepth !== undefined ) this.phiDepth.value = params.phiDepth;
|
|
331
348
|
if ( params.phiLuminance !== undefined ) this.phiLuminance.value = params.phiLuminance;
|
|
349
|
+
if ( params.spatialVarianceWeight !== undefined ) this.spatialVarianceWeight.value = params.spatialVarianceWeight;
|
|
332
350
|
if ( params.atrousIterations !== undefined ) this.iterations = params.atrousIterations;
|
|
333
351
|
|
|
334
352
|
}
|
|
@@ -364,6 +382,7 @@ export class BilateralFilter extends RenderStage {
|
|
|
364
382
|
this._outputTarget?.dispose();
|
|
365
383
|
this._readTexNode?.dispose();
|
|
366
384
|
this._normalDepthTexNode?.dispose();
|
|
385
|
+
this._shadingNormalTexNode?.dispose();
|
|
367
386
|
this._albedoTexNode?.dispose();
|
|
368
387
|
this._varianceTexNode?.dispose();
|
|
369
388
|
|