rayzee 5.8.1 → 5.8.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "5.8.1",
3
+ "version": "5.8.2",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -1,58 +1,19 @@
1
- import { Fn, wgslFn, vec3, vec4, float, int, uint, ivec2, uvec2, uniform,
1
+ import { Fn, vec3, vec4, float, int, uint, ivec2, uvec2, uniform,
2
2
  If, dot, max, min, abs, mix,
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 } from 'three';
6
6
  import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
7
7
  import { createRenderTargetHelper } from '../Processor/createRenderTargetHelper.js';
8
- import { luminance, normalDepthWeight } from '../TSL/Common.js';
9
-
10
- // ── wgslFn helpers ──────────────────────────────────────────
8
+ import { luminance } from '../TSL/Common.js';
11
9
 
12
10
  /**
13
- * Gradient-adaptive temporal blending alpha.
11
+ * WebGPU ASVGF Stage — temporal denoising via motion-vector reprojection
12
+ * + 3×3 colour-distance disocclusion rejection. Ping-pong StorageTextures.
14
13
  *
15
- * Maps temporal gradient to effective alpha:
16
- * low gradient → baseAlpha (more accumulation)
17
- * high gradient → up to 1.0 (fast adaptation)
18
- */
19
- const gradientAdaptiveAlpha = /*@__PURE__*/ wgslFn( `
20
- fn gradientAdaptiveAlpha(
21
- gradient: f32,
22
- baseAlpha: f32,
23
- scale: f32,
24
- gMin: f32,
25
- gMax: f32
26
- ) -> f32 {
27
-
28
- let remapped = clamp( ( gradient - gMin ) / max( gMax - gMin, 0.001 ), 0.0, 1.0 );
29
- return clamp( baseAlpha + ( 1.0 - baseAlpha ) * remapped * scale, baseAlpha, 1.0 );
30
-
31
- }
32
- ` );
33
-
34
- /**
35
- * WebGPU ASVGF Stage (Compute Shader)
36
- *
37
- * Adaptive Spatio-Temporal Variance-Guided Filtering for real-time denoising.
38
- * Two compute passes per frame with ping-pong StorageTextures.
39
- *
40
- * Algorithm:
41
- * 1. Gradient (compute): Cooperative tile load → 3×3 brightest search
42
- * → motion reprojection → normalized luminance gradient
43
- * 2. Temporal accumulation (compute): Motion validity check
44
- * → normal/depth edge-stopping → 3×3 variance clipping
45
- * → gradient-adaptive alpha → temporal blend + history tracking
46
- * → write current ND to prevND for next frame
47
- *
48
- * Execution: PER_CYCLE
49
- *
50
- * Events listened:
51
- * asvgf:reset, asvgf:setTemporal, asvgf:updateParameters,
52
- * camera:moved, pipeline:reset
53
- *
54
- * Textures published: asvgf:output, asvgf:temporalColor
55
- * Textures read: pathtracer:color, pathtracer:normalDepth, motionVector:screenSpace
14
+ * Events: asvgf:reset, asvgf:setTemporal, asvgf:updateParameters
15
+ * Publishes: asvgf:output, asvgf:temporalColor
16
+ * Reads: pathtracer:color, motionVector:screenSpace
56
17
  */
57
18
  export class ASVGF extends RenderStage {
58
19
 
@@ -66,41 +27,25 @@ export class ASVGF extends RenderStage {
66
27
  this.renderer = renderer;
67
28
  this.debugContainer = options.debugContainer || null;
68
29
 
69
- // Parameters
70
30
  this.temporalAlpha = uniform( options.temporalAlpha ?? 0.1 );
71
- this.gradientScale = uniform( options.gradientScale ?? 2.0 );
72
- this.gradientMin = uniform( options.gradientMin ?? 0.01 );
73
- this.gradientMax = uniform( options.gradientMax ?? 0.5 );
74
31
  this.phiColor = uniform( options.phiColor ?? 10.0 );
75
- this.phiNormal = uniform( options.phiNormal ?? 128.0 );
76
- this.phiDepth = uniform( options.phiDepth ?? 1.0 );
77
32
  this.maxAccumFrames = uniform( options.maxAccumFrames ?? 32.0 );
78
33
  this.varianceClip = uniform( options.varianceClip ?? 1.0 );
79
34
 
80
- // Resolution
81
35
  this.resW = uniform( options.width || 1 );
82
36
  this.resH = uniform( options.height || 1 );
83
37
 
84
- // Temporal control
85
38
  this.temporalEnabled = true;
86
- this.temporalEnabledU = uniform( 1.0 ); // 1.0 = enabled
39
+ this.temporalEnabledU = uniform( 1.0 );
87
40
 
88
- // Input texture nodes (context textures — always regular Textures)
89
41
  this._colorTexNode = new TextureNode();
90
- this._normalDepthTexNode = new TextureNode();
91
42
  this._motionTexNode = new TextureNode();
92
-
93
- // Read-side TextureNode wrappers (pre-compile with EmptyTexture,
94
- // then set to StorageTextures at runtime)
95
43
  this._readTemporalTexNode = new TextureNode();
96
- this._readPrevNDTexNode = new TextureNode();
97
- this._gradientReadTexNode = new TextureNode();
98
44
 
99
- // Ping-pong StorageTextures
100
45
  const w = options.width || 1;
101
46
  const h = options.height || 1;
102
47
 
103
- // LinearFilter for textureLoad codegen compatibility
48
+ // LinearFilter required for textureLoad codegen.
104
49
  this._temporalTexA = new StorageTexture( w, h );
105
50
  this._temporalTexA.type = HalfFloatType;
106
51
  this._temporalTexA.format = RGBAFormat;
@@ -113,18 +58,6 @@ export class ASVGF extends RenderStage {
113
58
  this._temporalTexB.minFilter = LinearFilter;
114
59
  this._temporalTexB.magFilter = LinearFilter;
115
60
 
116
- this._prevNDTexA = new StorageTexture( w, h );
117
- this._prevNDTexA.type = HalfFloatType;
118
- this._prevNDTexA.format = RGBAFormat;
119
- this._prevNDTexA.minFilter = LinearFilter;
120
- this._prevNDTexA.magFilter = LinearFilter;
121
-
122
- this._prevNDTexB = new StorageTexture( w, h );
123
- this._prevNDTexB.type = HalfFloatType;
124
- this._prevNDTexB.format = RGBAFormat;
125
- this._prevNDTexB.minFilter = LinearFilter;
126
- this._prevNDTexB.magFilter = LinearFilter;
127
-
128
61
  this._gradientStorageTex = new StorageTexture( w, h );
129
62
  this._gradientStorageTex.type = HalfFloatType;
130
63
  this._gradientStorageTex.format = RGBAFormat;
@@ -134,16 +67,12 @@ export class ASVGF extends RenderStage {
134
67
  this.currentMoments = 0; // 0 = write A, read B; 1 = write B, read A
135
68
  this._compiled = false;
136
69
 
137
- // Dispatch dimensions
138
70
  this._dispatchX = Math.ceil( w / 8 );
139
71
  this._dispatchY = Math.ceil( h / 8 );
140
72
 
141
- // Build compute nodes
142
73
  this._buildGradientCompute();
143
74
  this._buildTemporalCompute();
144
75
 
145
- // ── Heatmap debug visualization (compute shader) ──
146
-
147
76
  this.showHeatmap = false;
148
77
  this.debugMode = uniform( 0, 'int' );
149
78
 
@@ -162,7 +91,7 @@ export class ASVGF extends RenderStage {
162
91
  stencilBuffer: false
163
92
  } );
164
93
 
165
- // Separate heatmap texture nodes (avoid interference with compute pipeline)
94
+ // Separate from temporal-pass nodes to avoid binding interference.
166
95
  this._heatmapRawColorTexNode = new TextureNode();
167
96
  this._heatmapColorTexNode = new TextureNode();
168
97
  this._heatmapTemporalTexNode = new TextureNode();
@@ -187,18 +116,9 @@ export class ASVGF extends RenderStage {
187
116
 
188
117
  }
189
118
 
190
- // ──────────────────────────────────────────────────
191
- // Compute pass 1: Temporal Gradient
192
- // ──────────────────────────────────────────────────
193
-
194
- /**
195
- * Build gradient compute node with shared memory 3×3 brightest search.
196
- *
197
- * Workgroup [8,8,1] loads a 10×10 luminance tile into shared memory.
198
- * Each thread finds the brightest pixel in its 3×3 neighbourhood,
199
- * reads the motion vector there, reprojects, and computes the
200
- * normalized luminance gradient against the previous frame.
201
- */
119
+ // Temporal-luminance gradient (heatmap mode 5 only).
120
+ // 10×10 shared tile; each thread does a 3×3 brightest search, reprojects,
121
+ // and emits the normalized luminance delta against the previous frame.
202
122
  _buildGradientCompute() {
203
123
 
204
124
  const colorTex = this._colorTexNode;
@@ -225,9 +145,7 @@ export class ASVGF extends RenderStage {
225
145
  const tileOriginX = int( workgroupId.x ).mul( WG_SIZE ).sub( 1 );
226
146
  const tileOriginY = int( workgroupId.y ).mul( WG_SIZE ).sub( 1 );
227
147
 
228
- // ── Cooperative tile loading ─────────────────────
229
-
230
- // Load #1: all 64 threads load positions 0-63
148
+ // All 64 threads load positions 0–63; threads 0–35 then load 64–99.
231
149
  const sx1 = linearIdx.mod( TILE_W );
232
150
  const sy1 = linearIdx.div( TILE_W );
233
151
  const gx1 = tileOriginX.add( int( sx1 ) ).clamp( int( 0 ), int( resW ).sub( 1 ) );
@@ -235,7 +153,6 @@ export class ASVGF extends RenderStage {
235
153
  const sColor1 = textureLoad( colorTex, ivec2( gx1, gy1 ) ).xyz;
236
154
  sharedLum.element( linearIdx ).assign( luminance( sColor1 ) );
237
155
 
238
- // Load #2: threads 0-35 load positions 64-99
239
156
  If( linearIdx.lessThan( uint( EXTRA_LOAD ) ), () => {
240
157
 
241
158
  const idx2 = linearIdx.add( uint( WG_THREADS ) );
@@ -250,14 +167,11 @@ export class ASVGF extends RenderStage {
250
167
 
251
168
  workgroupBarrier();
252
169
 
253
- // ── Per-thread gradient computation ──────────────
254
-
255
170
  const gx = int( workgroupId.x ).mul( WG_SIZE ).add( int( lx ) );
256
171
  const gy = int( workgroupId.y ).mul( WG_SIZE ).add( int( ly ) );
257
172
 
258
173
  If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
259
174
 
260
- // Find brightest in 3×3 from shared memory
261
175
  const bestLum = float( - 1.0 ).toVar();
262
176
  const bestDx = int( 0 ).toVar();
263
177
  const bestDy = int( 0 ).toVar();
@@ -282,22 +196,19 @@ export class ASVGF extends RenderStage {
282
196
 
283
197
  }
284
198
 
285
- // Read motion at brightest pixel
286
199
  const bestGx = gx.add( bestDx ).clamp( int( 0 ), int( resW ).sub( 1 ) );
287
200
  const bestGy = gy.add( bestDy ).clamp( int( 0 ), int( resH ).sub( 1 ) );
288
201
  const motion = textureLoad( motionTex, ivec2( bestGx, bestGy ) );
289
202
 
290
- // Reproject via motion vector (UV-space → pixel coords)
203
+ // Reproject via motion vector (UV-space → pixel coords).
291
204
  const prevXf = float( bestGx ).sub( motion.x.mul( resW ) );
292
205
  const prevYf = float( bestGy ).sub( motion.y.mul( resH ) );
293
206
  const prevX = int( prevXf ).clamp( int( 0 ), int( resW ).sub( 1 ) );
294
207
  const prevY = int( prevYf ).clamp( int( 0 ), int( resH ).sub( 1 ) );
295
208
 
296
- // Previous frame luminance at reprojected position
297
209
  const prevColor = textureLoad( prevTemporalTex, ivec2( prevX, prevY ) ).xyz;
298
210
  const prevLum = luminance( prevColor );
299
211
 
300
- // Temporal gradient = normalized luminance difference
301
212
  const gradient = abs( bestLum.sub( prevLum ) )
302
213
  .div( max( bestLum, float( 0.001 ) ) )
303
214
  .clamp( 0.0, 1.0 );
@@ -319,43 +230,21 @@ export class ASVGF extends RenderStage {
319
230
 
320
231
  }
321
232
 
322
- // ──────────────────────────────────────────────────
323
- // Compute pass 2: Temporal Accumulation
324
- // ──────────────────────────────────────────────────
325
-
326
- /**
327
- * Build two temporal compute nodes — one for each ping-pong direction.
328
- *
329
- * Each node writes to its temporal StorageTexture (accumulated color + history)
330
- * AND its prevND StorageTexture (current ND saved for next frame),
331
- * eliminating the need for a separate copy pass.
332
- */
233
+ // One temporal node per ping-pong direction.
333
234
  _buildTemporalCompute() {
334
235
 
335
- this._temporalNodeA = this._buildTemporalForDirection(
336
- this._temporalTexA, this._prevNDTexA
337
- );
338
- this._temporalNodeB = this._buildTemporalForDirection(
339
- this._temporalTexB, this._prevNDTexB
340
- );
236
+ this._temporalNodeA = this._buildTemporalForDirection( this._temporalTexA );
237
+ this._temporalNodeB = this._buildTemporalForDirection( this._temporalTexB );
341
238
 
342
239
  }
343
240
 
344
- _buildTemporalForDirection( writeTemporalTex, writePrevNDTex ) {
241
+ _buildTemporalForDirection( writeTemporalTex ) {
345
242
 
346
243
  const colorTex = this._colorTexNode;
347
- const ndTex = this._normalDepthTexNode;
348
244
  const motionTex = this._motionTexNode;
349
245
  const prevTemporalTex = this._readTemporalTexNode;
350
- const prevNDTex = this._readPrevNDTexNode;
351
- const gradientTex = this._gradientReadTexNode;
352
246
 
353
247
  const temporalAlpha = this.temporalAlpha;
354
- const gradientScale = this.gradientScale;
355
- const gradientMinU = this.gradientMin;
356
- const gradientMaxU = this.gradientMax;
357
- const phiNormal = this.phiNormal;
358
- const phiDepth = this.phiDepth;
359
248
  const maxAccumFrames = this.maxAccumFrames;
360
249
  const varianceClipU = this.varianceClip;
361
250
  const temporalEnabledU = this.temporalEnabledU;
@@ -373,18 +262,15 @@ export class ASVGF extends RenderStage {
373
262
 
374
263
  const coord = ivec2( gx, gy );
375
264
  const currentColor = textureLoad( colorTex, coord ).xyz;
376
- const currentND = textureLoad( ndTex, coord );
377
265
 
378
- // Default: pass through with history = 1
266
+ // Default: history = 1, no blend (used when temporal off or reprojection invalid).
379
267
  const result = vec4( currentColor, 1.0 ).toVar();
380
268
 
381
269
  If( temporalEnabledU.greaterThan( 0.5 ), () => {
382
270
 
383
- // Read motion vector
384
271
  const motion = textureLoad( motionTex, coord );
385
272
  const motionValid = motion.w.greaterThan( 0.5 );
386
273
 
387
- // Reprojected pixel coords
388
274
  const prevXf = float( gx ).sub( motion.x.mul( resW ) );
389
275
  const prevYf = float( gy ).sub( motion.y.mul( resH ) );
390
276
  const prevOnScreen = prevXf.greaterThanEqual( 0.0 )
@@ -398,29 +284,16 @@ export class ASVGF extends RenderStage {
398
284
  const prevY = int( prevYf ).clamp( int( 0 ), int( resH ).sub( 1 ) );
399
285
  const prevCoord = ivec2( prevX, prevY );
400
286
 
401
- // Normal/depth similarity check
402
- const currentNormal = currentND.xyz.mul( 2.0 ).sub( 1.0 );
403
- const prevND = textureLoad( prevNDTex, prevCoord );
404
- const prevNormal = prevND.xyz.mul( 2.0 ).sub( 1.0 );
405
-
406
- const similarity = normalDepthWeight(
407
- currentNormal, prevNormal,
408
- currentND.w, prevND.w,
409
- phiNormal, phiDepth
410
- );
411
-
412
- // Previous frame colour + history length
413
287
  const prevData = textureLoad( prevTemporalTex, prevCoord );
414
288
  const prevColor = prevData.xyz;
415
289
  const historyLength = prevData.w;
416
290
 
417
- // History confidence: 0 (fresh) → 1 (fully converged)
418
- const historyConfidence = historyLength.div( maxAccumFrames ).clamp( 0.0, 1.0 );
419
-
420
- // 3×3 neighbourhood colour clamping (variance clipping)
421
- const nMin = vec3( 1e10 ).toVar();
422
- const nMax = vec3( - 1e10 ).toVar();
291
+ // Euclidean colour-distance gate. Per-channel clipping
292
+ // fails at silhouettes where wall/box colours overlap
293
+ // in per-channel ranges. A normal/depth gate would be
294
+ // stronger but shading normals jitter too much for it.
423
295
  const nMean = vec3( 0.0 ).toVar();
296
+ const nMeanSq = vec3( 0.0 ).toVar();
424
297
 
425
298
  for ( let dy = - 1; dy <= 1; dy ++ ) {
426
299
 
@@ -429,67 +302,53 @@ export class ASVGF extends RenderStage {
429
302
  const sx = gx.add( dx ).clamp( int( 0 ), int( resW ).sub( 1 ) );
430
303
  const sy = gy.add( dy ).clamp( int( 0 ), int( resH ).sub( 1 ) );
431
304
  const s = textureLoad( colorTex, ivec2( sx, sy ) ).xyz;
432
- nMin.assign( min( nMin, s ) );
433
- nMax.assign( max( nMax, s ) );
434
305
  nMean.addAssign( s );
306
+ nMeanSq.addAssign( s.mul( s ) );
435
307
 
436
308
  }
437
309
 
438
310
  }
439
311
 
440
312
  nMean.divAssign( 9.0 );
441
-
442
- // Expand bounding box by variance clip factor
443
- // Widen box for high-history pixels: noisy current shouldn't clamp converged previous
444
- const historyExpand = float( 1.0 ).add( historyConfidence.mul( 3.0 ) );
445
- const boxExtent = nMax.sub( nMin ).mul( varianceClipU ).mul( historyExpand );
446
- const clampMin = nMin.sub( boxExtent );
447
- const clampMax = nMax.add( boxExtent );
448
- const clampedPrev = prevColor.clamp( clampMin, clampMax );
449
-
450
- // History-adaptive alpha: 1/(N+1), floored at temporalAlpha.
451
- // Standard SVGF approach — gives optimal noise reduction for
452
- // temporal accumulation. Variance clipping above handles
453
- // disocclusion; gradient not used here because with 1 SPP
454
- // input the gradient is dominated by Monte Carlo noise, not
455
- // scene changes, which drives alpha toward 1.0 and kills
456
- // accumulation.
457
- const effectiveAlpha = max(
313
+ nMeanSq.divAssign( 9.0 );
314
+ const nVariance = max( nMeanSq.sub( nMean.mul( nMean ) ), vec3( 0.0 ) );
315
+ const sigmaSq = dot( nVariance, vec3( 1.0 ) );
316
+
317
+ // reject [0,1]: 0 = matches mean (keep history),
318
+ // 1 = >k·σ away (force fresh sample). Squared form skips sqrt.
319
+ const diff = prevColor.sub( nMean );
320
+ const distSq = dot( diff, diff );
321
+ const sigmaSqK = sigmaSq.mul( varianceClipU.mul( varianceClipU ) );
322
+ const reject = distSq.div( max( sigmaSqK, float( 1e-6 ) ) ).clamp( 0.0, 1.0 );
323
+
324
+ const baseAlpha = max(
458
325
  float( 1.0 ).div( historyLength.add( 1.0 ) ),
459
326
  temporalAlpha
460
327
  );
461
-
462
- // Blend
463
- const blended = mix( clampedPrev, currentColor, effectiveAlpha );
464
-
465
- // Update history length
466
- const newHistory = min( historyLength.add( 1.0 ), maxAccumFrames );
328
+ const effectiveAlpha = mix( baseAlpha, float( 1.0 ), reject );
329
+ const blended = mix( prevColor, currentColor, effectiveAlpha );
330
+ const newHistory = mix(
331
+ min( historyLength.add( 1.0 ), maxAccumFrames ),
332
+ float( 1.0 ),
333
+ reject
334
+ );
467
335
 
468
336
  result.assign( vec4( blended, newHistory ) );
469
337
 
470
338
  } ).Else( () => {
471
339
 
472
- // No valid reprojection — use current colour, reset history
473
340
  result.assign( vec4( currentColor, 1.0 ) );
474
341
 
475
342
  } );
476
343
 
477
344
  } );
478
345
 
479
- // Write temporal result (colour + history in .w)
480
346
  textureStore(
481
347
  writeTemporalTex,
482
348
  uvec2( uint( gx ), uint( gy ) ),
483
349
  result
484
350
  ).toWriteOnly();
485
351
 
486
- // Write current ND to prevND for next frame
487
- textureStore(
488
- writePrevNDTex,
489
- uvec2( uint( gx ), uint( gy ) ),
490
- currentND
491
- ).toWriteOnly();
492
-
493
352
  } );
494
353
 
495
354
  } );
@@ -501,10 +360,6 @@ export class ASVGF extends RenderStage {
501
360
 
502
361
  }
503
362
 
504
- // ──────────────────────────────────────────────────
505
- // Heatmap debug visualization (compute shader)
506
- // ──────────────────────────────────────────────────
507
-
508
363
  _buildHeatmapCompute() {
509
364
 
510
365
  const rawColorTex = this._heatmapRawColorTexNode;
@@ -530,11 +385,10 @@ export class ASVGF extends RenderStage {
530
385
  const coord = ivec2( gx, gy );
531
386
  const result = vec4( 0.0, 0.0, 0.0, 1.0 ).toVar();
532
387
 
533
- // Use If/ElseIf/Else chain — separate If() blocks cause TSL
534
- // to generate non-exclusive WGSL branches where texture samples
535
- // from inactive branches can contaminate the output.
388
+ // Must be chained If/ElseIf — separate If blocks let inactive-
389
+ // branch texture samples contaminate the output.
536
390
 
537
- // Mode 0: Beauty (denoised output)
391
+ // 0: beauty
538
392
  If( mode.equal( int( 0 ) ), () => {
539
393
 
540
394
  const c = textureLoad( colorTex, coord ).xyz;
@@ -542,9 +396,7 @@ export class ASVGF extends RenderStage {
542
396
 
543
397
  } ).ElseIf( mode.equal( int( 1 ) ), () => {
544
398
 
545
- // Mode 1: Spatial luminance variance of raw path tracer input.
546
- // Computes E[L²] - E[L]² over a 3×3 neighbourhood, which correctly
547
- // highlights noisy regions regardless of accumulation state.
399
+ // 1: 3×3 luminance variance of raw path-tracer input
548
400
  const meanLum = float( 0.0 ).toVar();
549
401
  const meanLumSq = float( 0.0 ).toVar();
550
402
 
@@ -566,9 +418,7 @@ export class ASVGF extends RenderStage {
566
418
  meanLum.divAssign( 9.0 );
567
419
  meanLumSq.divAssign( 9.0 );
568
420
  const variance = max( meanLumSq.sub( meanLum.mul( meanLum ) ), float( 0.0 ) );
569
-
570
- // Relative variance (normalise by mean to handle HDR range),
571
- // then scale into 0-1 for the heatmap.
421
+ // Normalise by mean for HDR, scale to [0,1] for ramp.
572
422
  const relVar = variance.div( max( meanLum.mul( meanLum ), float( 0.0001 ) ) );
573
423
  const t = relVar.mul( 10.0 ).clamp( 0.0, 1.0 );
574
424
 
@@ -582,14 +432,14 @@ export class ASVGF extends RenderStage {
582
432
 
583
433
  } ).ElseIf( mode.equal( int( 2 ) ), () => {
584
434
 
585
- // Mode 2: History length
435
+ // 2: history length
586
436
  const historyLength = textureLoad( temporalTex, coord ).w;
587
437
  const t = historyLength.div( 32.0 ).clamp( 0.0, 1.0 );
588
438
  result.assign( vec4( float( 1.0 ).sub( t ), t, float( 0.2 ), 1.0 ) );
589
439
 
590
440
  } ).ElseIf( mode.equal( int( 3 ) ), () => {
591
441
 
592
- // Mode 3: Motion vectors
442
+ // 3: motion vectors
593
443
  const motion = textureLoad( motionTex, coord );
594
444
  const mx = abs( motion.x ).mul( 100.0 ).clamp( 0.0, 1.0 );
595
445
  const my = abs( motion.y ).mul( 100.0 ).clamp( 0.0, 1.0 );
@@ -598,13 +448,13 @@ export class ASVGF extends RenderStage {
598
448
 
599
449
  } ).ElseIf( mode.equal( int( 4 ) ), () => {
600
450
 
601
- // Mode 4: Normals
451
+ // 4: normals
602
452
  const nd = textureLoad( ndTex, coord );
603
453
  result.assign( vec4( nd.xyz, 1.0 ) );
604
454
 
605
455
  } ).Else( () => {
606
456
 
607
- // Mode 5: Temporal gradient
457
+ // 5: temporal-luminance gradient
608
458
  const grad = textureLoad( gradientTex, coord ).x;
609
459
  const t = grad.mul( 5.0 ).clamp( 0.0, 1.0 );
610
460
  result.assign( vec4( t, t.mul( 0.5 ), float( 1.0 ).sub( t ), 1.0 ) );
@@ -628,10 +478,6 @@ export class ASVGF extends RenderStage {
628
478
 
629
479
  }
630
480
 
631
- // ──────────────────────────────────────────────────
632
- // Pipeline lifecycle
633
- // ──────────────────────────────────────────────────
634
-
635
481
  setupEventListeners() {
636
482
 
637
483
  this.on( 'asvgf:reset', () => this.resetTemporalData() );
@@ -651,10 +497,7 @@ export class ASVGF extends RenderStage {
651
497
 
652
498
  if ( ! data ) return;
653
499
  if ( data.temporalAlpha !== undefined ) this.temporalAlpha.value = data.temporalAlpha;
654
- if ( data.gradientScale !== undefined ) this.gradientScale.value = data.gradientScale;
655
500
  if ( data.phiColor !== undefined ) this.phiColor.value = data.phiColor;
656
- if ( data.phiNormal !== undefined ) this.phiNormal.value = data.phiNormal;
657
- if ( data.phiDepth !== undefined ) this.phiDepth.value = data.phiDepth;
658
501
 
659
502
  } );
660
503
 
@@ -664,14 +507,12 @@ export class ASVGF extends RenderStage {
664
507
 
665
508
  if ( ! this.enabled ) return;
666
509
 
667
- // Read inputs from context
668
510
  const colorTex = context.getTexture( 'pathtracer:color' );
669
511
  const normalDepthTex = context.getTexture( 'pathtracer:normalDepth' );
670
512
  const motionTex = context.getTexture( 'motionVector:screenSpace' );
671
513
 
672
514
  if ( ! colorTex ) return;
673
515
 
674
- // Auto-size
675
516
  const img = colorTex.image;
676
517
  if ( img && img.width > 0 && img.height > 0 ) {
677
518
 
@@ -684,14 +525,11 @@ export class ASVGF extends RenderStage {
684
525
 
685
526
  }
686
527
 
687
- // Update context texture nodes
688
528
  this._colorTexNode.value = colorTex;
689
- if ( normalDepthTex ) this._normalDepthTexNode.value = normalDepthTex;
690
529
  if ( motionTex ) this._motionTexNode.value = motionTex;
691
530
 
692
- // Force-compile all compute nodes on first frame while TextureNode
693
- // wrappers still hold EmptyTexture. This ensures textureLoad codegen
694
- // includes the required level parameter.
531
+ // Force first-frame compile while TextureNodes still hold EmptyTexture,
532
+ // so textureLoad codegen emits the required `level` parameter.
695
533
  if ( ! this._compiled ) {
696
534
 
697
535
  this.renderer.compute( this._gradientNode );
@@ -701,35 +539,26 @@ export class ASVGF extends RenderStage {
701
539
 
702
540
  }
703
541
 
704
- // Ping-pong direction: read from opposite side, write to current side
542
+ // Ping-pong: read opposite, write current
705
543
  const readTemporal = this.currentMoments === 0
706
544
  ? this._temporalTexB : this._temporalTexA;
707
- const readPrevND = this.currentMoments === 0
708
- ? this._prevNDTexB : this._prevNDTexA;
709
545
  const writeNode = this.currentMoments === 0
710
546
  ? this._temporalNodeA : this._temporalNodeB;
711
547
  const writeTemporal = this.currentMoments === 0
712
548
  ? this._temporalTexA : this._temporalTexB;
713
549
 
714
- // Pass 1: Temporal gradient (shared memory 3×3 brightest search)
715
550
  this._readTemporalTexNode.value = readTemporal;
716
- this.renderer.compute( this._gradientNode );
717
-
718
- // Pass 2: Temporal accumulation + prevND write
719
- this._gradientReadTexNode.value = this._gradientStorageTex;
720
- this._readPrevNDTexNode.value = readPrevND;
721
551
  this.renderer.compute( writeNode );
722
552
 
723
- // Publish outputs
724
553
  context.setTexture( 'asvgf:output', writeTemporal );
725
554
  context.setTexture( 'asvgf:temporalColor', writeTemporal );
726
555
 
727
- // Swap for next frame
728
556
  this.currentMoments = 1 - this.currentMoments;
729
557
 
730
- // Render heatmap debug overlay if enabled
731
558
  if ( this.showHeatmap ) {
732
559
 
560
+ this.renderer.compute( this._gradientNode );
561
+
733
562
  this._heatmapRawColorTexNode.value = colorTex;
734
563
  this._heatmapColorTexNode.value = writeTemporal;
735
564
  this._heatmapTemporalTexNode.value = writeTemporal;
@@ -772,10 +601,7 @@ export class ASVGF extends RenderStage {
772
601
 
773
602
  if ( ! params ) return;
774
603
  if ( params.temporalAlpha !== undefined ) this.temporalAlpha.value = params.temporalAlpha;
775
- if ( params.gradientScale !== undefined ) this.gradientScale.value = params.gradientScale;
776
604
  if ( params.phiColor !== undefined ) this.phiColor.value = params.phiColor;
777
- if ( params.phiNormal !== undefined ) this.phiNormal.value = params.phiNormal;
778
- if ( params.phiDepth !== undefined ) this.phiDepth.value = params.phiDepth;
779
605
  if ( params.debugMode !== undefined ) this.debugMode.value = params.debugMode;
780
606
 
781
607
  }
@@ -791,8 +617,6 @@ export class ASVGF extends RenderStage {
791
617
 
792
618
  this._temporalTexA.setSize( width, height );
793
619
  this._temporalTexB.setSize( width, height );
794
- this._prevNDTexA.setSize( width, height );
795
- this._prevNDTexB.setSize( width, height );
796
620
  this._gradientStorageTex.setSize( width, height );
797
621
  this._heatmapStorageTex.setSize( width, height );
798
622
  this.heatmapTarget.setSize( width, height );
@@ -800,7 +624,6 @@ export class ASVGF extends RenderStage {
800
624
  this.resW.value = width;
801
625
  this.resH.value = height;
802
626
 
803
- // Update dispatch dimensions
804
627
  this._dispatchX = Math.ceil( width / 8 );
805
628
  this._dispatchY = Math.ceil( height / 8 );
806
629
  this._gradientNode.dispatchSize = [ this._dispatchX, this._dispatchY, 1 ];
@@ -812,10 +635,8 @@ export class ASVGF extends RenderStage {
812
635
 
813
636
  reset() {
814
637
 
815
- // Intentionally does NOT reset temporal data.
816
- // ASVGF uses motion vectors to maintain temporal coherence across
817
- // camera movement. Only explicit 'asvgf:reset' events (scene change,
818
- // render mode switch) should clear temporal history.
638
+ // No-op: motion vectors handle camera moves; only explicit
639
+ // 'asvgf:reset' (scene change, render-mode switch) clears history.
819
640
 
820
641
  }
821
642
 
@@ -826,22 +647,15 @@ export class ASVGF extends RenderStage {
826
647
  this._temporalNodeB?.dispose();
827
648
  this._temporalTexA?.dispose();
828
649
  this._temporalTexB?.dispose();
829
- this._prevNDTexA?.dispose();
830
- this._prevNDTexB?.dispose();
831
650
  this._gradientStorageTex?.dispose();
832
651
  this._heatmapComputeNode?.dispose();
833
652
  this._heatmapStorageTex?.dispose();
834
653
  this.heatmapTarget?.dispose();
835
654
 
836
- // Dispose input TextureNode objects
837
655
  this._colorTexNode?.dispose();
838
- this._normalDepthTexNode?.dispose();
839
656
  this._motionTexNode?.dispose();
840
657
  this._readTemporalTexNode?.dispose();
841
- this._readPrevNDTexNode?.dispose();
842
- this._gradientReadTexNode?.dispose();
843
658
 
844
- // Dispose heatmap TextureNode objects
845
659
  this._heatmapRawColorTexNode?.dispose();
846
660
  this._heatmapColorTexNode?.dispose();
847
661
  this._heatmapTemporalTexNode?.dispose();
@@ -849,7 +663,7 @@ export class ASVGF extends RenderStage {
849
663
  this._heatmapMotionTexNode?.dispose();
850
664
  this._heatmapGradientTexNode?.dispose();
851
665
 
852
- // dispose() also removes the DOM node
666
+ // also removes the DOM node
853
667
  this.heatmapHelper?.dispose();
854
668
 
855
669
  }