rayzee 7.0.0 → 7.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -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,
@@ -195,6 +198,17 @@ export const ASVGF_QUALITY_PRESETS = {
195
198
  }
196
199
  };
197
200
 
201
+ // Adaptive variants — same SVGF quality as the base preset plus the temporal-
202
+ // gradient anti-lag enabled (gradientStrength > 0). The gradient measures real
203
+ // change in units of noise σ (gradientSigmaScale), so a static scene reads ~0
204
+ // and convergence is unaffected; only moving lights/anim/disocclusion drop
205
+ // history. Mutating the exported object is fine (const binding, mutable object);
206
+ // spreading lets each inherit its base. See ASVGF._buildGradientCompute.
207
+ const ADAPTIVE_GRADIENT = { gradientSigmaScale: 2.5, gradientNoiseFloor: 0.05 };
208
+ ASVGF_QUALITY_PRESETS.low_adaptive = { ...ASVGF_QUALITY_PRESETS.low, gradientStrength: 0.8, ...ADAPTIVE_GRADIENT };
209
+ ASVGF_QUALITY_PRESETS.medium_adaptive = { ...ASVGF_QUALITY_PRESETS.medium, gradientStrength: 1.0, ...ADAPTIVE_GRADIENT };
210
+ ASVGF_QUALITY_PRESETS.high_adaptive = { ...ASVGF_QUALITY_PRESETS.high, gradientStrength: 1.0, ...ADAPTIVE_GRADIENT };
211
+
198
212
  export const CAMERA_RANGES = {
199
213
  fov: {
200
214
  min: 10,
@@ -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): 1 uvec4/pixel, half-precision packed (pack2x16, no f32 bitcast).
15
- // .x=packSnorm2x16(normal.xy) .y=packSnorm2x16(normal.z, depth) .z=packUnorm2x16(albedo.rg) .w=packUnorm2x16(albedo.b, 0)
16
- // Separate buffer from RAY (per-pixel, not per-ray×S) — written by Generate/Shade bounce-0, read only by FinalWrite.
17
- export const GBUFFER_STRIDE = 1;
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). 1 uvec4/pixel (element p), pack2x16 lanes. ──
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
 
@@ -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,
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';
@@ -8,13 +8,15 @@ import { luminance } from '../TSL/Common.js';
8
8
  import { ALBEDO_EPS, MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
9
9
 
10
10
  /**
11
- * ASVGF — SVGF temporal + spatial denoising with albedo demodulation.
11
+ * ASVGF — SVGF temporal denoising with albedo demodulation + adaptive-α.
12
12
  *
13
- * Adaptive infrastructure (gradient compute, prev-color cache,
14
- * gradientStrength uniform) is in place but disabled by default —
15
- * gradientStrength=0 makes adaptiveBoost=0 so effectiveAlpha is the pure
16
- * EMA 1/(history+1). The fixed-noise-floor implementation misfires on
17
- * 1-SPP raw input; a per-pixel variance-aware floor is the proper fix.
13
+ * Adaptive temporal gradient (the "A"): the gradient kernel compares this
14
+ * frame's demodulated lighting against the reprojected accumulated history,
15
+ * floored by a per-pixel σ (max(Δ k·σ, 0)), max-normalised and squared
16
+ * (Q2RTX get_gradient). gradientStrength scales it into effectiveAlpha =
17
+ * mix(baseAlpha, 1, gradient·gradientStrength) high change drop history
18
+ * (anti-lag). gradientStrength=0 (base presets) keeps pure EMA; the
19
+ * *_adaptive presets enable it.
18
20
  *
19
21
  * Reads: pathtracer:color, pathtracer:albedo, pathtracer:normalDepth,
20
22
  * pathtracer:prevNormalDepth, motionVector:screenSpace
@@ -34,7 +36,10 @@ export class ASVGF extends RenderStage {
34
36
 
35
37
  this.temporalAlpha = uniform( options.temporalAlpha ?? 0.0 );
36
38
  this.gradientStrength = uniform( options.gradientStrength ?? 0.0 );
37
- this.gradientNoiseFloor = uniform( options.gradientNoiseFloor ?? 0.15 );
39
+ // σ multiplier for the per-pixel noise floor (NRD luminanceSigmaScale 2).
40
+ this.gradientSigmaScale = uniform( options.gradientSigmaScale ?? 2.0 );
41
+ // Secondary relative floor on the normalised gradient (0 = rely on σ alone).
42
+ this.gradientNoiseFloor = uniform( options.gradientNoiseFloor ?? 0.0 );
38
43
  this.maxAccumFrames = uniform( options.maxAccumFrames ?? 32.0 );
39
44
 
40
45
  this.resW = uniform( options.width || 1 );
@@ -43,7 +48,6 @@ export class ASVGF extends RenderStage {
43
48
  this.temporalEnabledU = uniform( 1.0 );
44
49
 
45
50
  this._colorTexNode = new TextureNode();
46
- this._prevColorTexNode = new TextureNode();
47
51
  this._albedoTexNode = new TextureNode();
48
52
  this._motionTexNode = new TextureNode();
49
53
  this._normalDepthTexNode = new TextureNode();
@@ -112,18 +116,6 @@ export class ASVGF extends RenderStage {
112
116
  stencilBuffer: false
113
117
  } );
114
118
 
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
119
  this.currentMoments = 0; // 0 = write A, read B; 1 = write B, read A
128
120
  this._compiled = false;
129
121
 
@@ -162,17 +154,24 @@ export class ASVGF extends RenderStage {
162
154
 
163
155
  }
164
156
 
165
- // Per-pixel adaptive-α signal: 5×5 spatial average of |currentLum − prevLum|
166
- // / meanLum, both raw single-SPP (noise-comparable), with noise-floor
167
- // subtraction. Currently gated off by gradientStrength=0 kept compiled
168
- // to drive heatmap mode 5 and as scaffolding for a proper variance-aware
169
- // implementation.
157
+ // Adaptive temporal gradient: per pixel, compare this frame's DEMODULATED
158
+ // lighting against the motion-reprojected accumulated history (low-noise),
159
+ // floored by a per-pixel σ from the 5×5 spatial neighbourhood. The σ floor
160
+ // is what the old fixed 0.15 constant couldn't give — on a static scene the
161
+ // 1-SPP sample sits within ±k·σ of the converged estimate so the gradient
162
+ // reads ~0 (no convergence penalty); only real change (moving light, anim,
163
+ // disocclusion) exceeds it. Demodulation (vs raw color) keeps albedo/texture
164
+ // edges from false-firing. Output .x feeds effectiveAlpha in the temporal pass.
170
165
  _buildGradientCompute() {
171
166
 
172
167
  const colorTex = this._colorTexNode;
173
- const prevColorTex = this._prevColorTexNode;
168
+ const albedoTex = this._albedoTexNode;
174
169
  const motionTex = this._motionTexNode;
170
+ // Accumulated history (demodulated lighting in .xyz, history count in .w).
171
+ // Same node the temporal pass reads; set to readTemporal before dispatch.
172
+ const histTex = this._readTemporalTexNode;
175
173
  const noiseFloor = this.gradientNoiseFloor;
174
+ const sigmaScale = this.gradientSigmaScale;
176
175
  const gradientStorageTex = this._gradientStorageTex;
177
176
  const resW = this.resW;
178
177
  const resH = this.resH;
@@ -185,7 +184,7 @@ export class ASVGF extends RenderStage {
185
184
  const WG_THREADS = WG_SIZE * WG_SIZE; // 64
186
185
 
187
186
  const sharedCurLum = workgroupArray( 'float', TILE_TOTAL );
188
- const sharedPrevLum = workgroupArray( 'float', TILE_TOTAL );
187
+ const sharedHistLum = workgroupArray( 'float', TILE_TOTAL );
189
188
 
190
189
  const computeFn = Fn( () => {
191
190
 
@@ -204,20 +203,26 @@ export class ASVGF extends RenderStage {
204
203
  const gxL = tileOriginX.add( int( sx ) ).clamp( int( 0 ), int( resW ).sub( 1 ) );
205
204
  const gyL = tileOriginY.add( int( sy ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
206
205
 
206
+ // Demodulated current lighting luminance (matches the temporal pass:
207
+ // safeAlbedo = max(albedo, ALBEDO_EPS) keeps sky/dark-material round-trip).
207
208
  const curColor = textureLoad( colorTex, ivec2( gxL, gyL ) ).xyz;
208
- sharedCurLum.element( k ).assign( luminance( curColor ) );
209
-
209
+ const curAlbedo = textureLoad( albedoTex, ivec2( gxL, gyL ) ).xyz;
210
+ const curLighting = curColor.div( max( curAlbedo, vec3( ALBEDO_EPS ) ) );
211
+ const curLum = luminance( curLighting ).toVar();
212
+ sharedCurLum.element( k ).assign( curLum );
213
+
214
+ // Reproject the accumulated history to this pixel; read its lighting
215
+ // luminance. Invalid motion → mirror current so the delta is 0
216
+ // (disocclusion handled by the temporal pass's geometric gate).
210
217
  const motion = textureLoad( motionTex, ivec2( gxL, gyL ) );
211
218
  const prevXf = float( gxL ).sub( motion.x.mul( resW ) );
212
219
  const prevYf = float( gyL ).sub( motion.y.mul( resH ) );
213
220
  const prevX = int( prevXf ).clamp( int( 0 ), int( resW ).sub( 1 ) );
214
221
  const prevY = int( prevYf ).clamp( int( 0 ), int( resH ).sub( 1 ) );
215
- // Invalid prev → mirror current so the diff contributes 0;
216
- // disocclusion is handled by the geometric gate downstream.
217
222
  const motionValid = motion.w.greaterThan( 0.5 );
218
- const prevColor = textureLoad( prevColorTex, ivec2( prevX, prevY ) ).xyz;
219
- const prevLum = motionValid.select( luminance( prevColor ), luminance( curColor ) );
220
- sharedPrevLum.element( k ).assign( prevLum );
223
+ const histLighting = textureLoad( histTex, ivec2( prevX, prevY ) ).xyz;
224
+ const histLum = motionValid.select( luminance( histLighting ), curLum );
225
+ sharedHistLum.element( k ).assign( histLum );
221
226
 
222
227
  };
223
228
 
@@ -245,8 +250,9 @@ export class ASVGF extends RenderStage {
245
250
 
246
251
  If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
247
252
 
248
- const sumDiff = float( 0.0 ).toVar();
249
- const sumMean = float( 0.0 ).toVar();
253
+ // Pass 1 per-pixel noise σ from the 5×5 (demodulated) neighbourhood.
254
+ const sumLum = float( 0.0 ).toVar();
255
+ const sumLumSq = float( 0.0 ).toVar();
250
256
 
251
257
  for ( let dy = - TILE_BORDER; dy <= TILE_BORDER; dy ++ ) {
252
258
 
@@ -256,26 +262,59 @@ export class ASVGF extends RenderStage {
256
262
  .mul( uint( TILE_W ) )
257
263
  .add( lx.add( uint( TILE_BORDER + dx ) ) );
258
264
  const cL = sharedCurLum.element( idx );
259
- const pL = sharedPrevLum.element( idx );
260
- sumDiff.addAssign( abs( cL.sub( pL ) ) );
261
- sumMean.addAssign( cL.add( pL ).mul( 0.5 ) );
265
+ sumLum.addAssign( cL );
266
+ sumLumSq.addAssign( cL.mul( cL ) );
262
267
 
263
268
  }
264
269
 
265
270
  }
266
271
 
267
- const rawGradient = sumDiff
268
- .div( max( sumMean, float( 0.001 ) ) )
269
- .clamp( 0.0, 1.0 );
270
- const oneMinusFloor = max( float( 1.0 ).sub( noiseFloor ), float( 0.0001 ) );
271
- const gradient = max( rawGradient.sub( noiseFloor ), float( 0.0 ) )
272
- .div( oneMinusFloor )
273
- .clamp( 0.0, 1.0 );
272
+ const meanLum = sumLum.div( 25.0 );
273
+ const variance = max( sumLumSq.div( 25.0 ).sub( meanLum.mul( meanLum ) ), float( 0.0 ) );
274
+ const sigmaFloor = sqrt( variance ).mul( sigmaScale ).toVar();
275
+
276
+ // Pass 2 5×5 average of the squared, σ-floored, max-normalised
277
+ // temporal change. Squaring (Q2RTX get_gradient) suppresses residual
278
+ // MC noise while staying reactive in high contrast.
279
+ const sumG = float( 0.0 ).toVar();
280
+
281
+ for ( let dy = - TILE_BORDER; dy <= TILE_BORDER; dy ++ ) {
282
+
283
+ for ( let dx = - TILE_BORDER; dx <= TILE_BORDER; dx ++ ) {
284
+
285
+ const idx = ly.add( uint( TILE_BORDER + dy ) )
286
+ .mul( uint( TILE_W ) )
287
+ .add( lx.add( uint( TILE_BORDER + dx ) ) );
288
+ const cL = sharedCurLum.element( idx );
289
+ const hL = sharedHistLum.element( idx );
290
+ const floored = max( abs( cL.sub( hL ) ).sub( sigmaFloor ), float( 0.0 ) );
291
+ const g = floored.div( max( max( cL, hL ), float( 0.001 ) ) );
292
+ // Optional secondary relative floor (noiseFloor=0 → no-op).
293
+ const gf = max( g.sub( noiseFloor ), float( 0.0 ) )
294
+ .div( max( float( 1.0 ).sub( noiseFloor ), float( 0.0001 ) ) );
295
+ sumG.addAssign( gf.mul( gf ) );
296
+
297
+ }
298
+
299
+ }
300
+
301
+ const gradientRaw = sumG.div( 25.0 ).clamp( 0.0, 1.0 ).toVar();
302
+
303
+ // Trust the gradient only once enough history has accumulated at the
304
+ // reprojected centre — early frames have noisy history → false fires.
305
+ const cMotion = textureLoad( motionTex, ivec2( gx, gy ) );
306
+ const cPrevX = int( float( gx ).sub( cMotion.x.mul( resW ) ) ).clamp( int( 0 ), int( resW ).sub( 1 ) );
307
+ const cPrevY = int( float( gy ).sub( cMotion.y.mul( resH ) ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
308
+ const histLen = cMotion.w.greaterThan( 0.5 )
309
+ .select( textureLoad( histTex, ivec2( cPrevX, cPrevY ) ).w, float( 0.0 ) );
310
+ const confidence = histLen.div( 4.0 ).clamp( 0.0, 1.0 );
311
+
312
+ const gradient = gradientRaw.mul( confidence );
274
313
 
275
314
  textureStore(
276
315
  gradientStorageTex,
277
316
  uvec2( uint( gx ), uint( gy ) ),
278
- vec4( gradient, rawGradient, sumMean.div( 25.0 ), 1.0 )
317
+ vec4( gradient, gradientRaw, sigmaFloor, 1.0 )
279
318
  ).toWriteOnly();
280
319
 
281
320
  } );
@@ -635,12 +674,6 @@ export class ASVGF extends RenderStage {
635
674
  const writeTemporal = this.currentMoments === 0
636
675
  ? this._temporalTexA : this._temporalTexB;
637
676
 
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
677
  // First-frame compile while StorageTexture-typed nodes still hold
645
678
  // EmptyTexture, so textureLoad codegen emits the required `level`
646
679
  // parameter. Binding StorageTextures only AFTER compile keeps the
@@ -670,15 +703,6 @@ export class ASVGF extends RenderStage {
670
703
 
671
704
  this.renderer.compute( writeNode );
672
705
 
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
- }
681
-
682
706
  // Copy active region out of the over-allocated StorageTextures into
683
707
  // right-sized RTs; downstream stages UV-sample these.
684
708
  this._srcRegion.max.set( this.resW.value, this.resH.value );
@@ -733,6 +757,7 @@ export class ASVGF extends RenderStage {
733
757
  if ( ! params ) return;
734
758
  if ( params.temporalAlpha !== undefined ) this.temporalAlpha.value = params.temporalAlpha;
735
759
  if ( params.gradientStrength !== undefined ) this.gradientStrength.value = params.gradientStrength;
760
+ if ( params.gradientSigmaScale !== undefined ) this.gradientSigmaScale.value = params.gradientSigmaScale;
736
761
  if ( params.gradientNoiseFloor !== undefined ) this.gradientNoiseFloor.value = params.gradientNoiseFloor;
737
762
  if ( params.maxAccumFrames !== undefined ) this.maxAccumFrames.value = params.maxAccumFrames;
738
763
  if ( params.debugMode !== undefined ) this.debugMode.value = params.debugMode;
@@ -742,8 +767,6 @@ export class ASVGF extends RenderStage {
742
767
  resetTemporalData() {
743
768
 
744
769
  this.currentMoments = 0;
745
- // Drop cache so post-reset frames don't see pre-reset prev color.
746
- this._prevColorReady = false;
747
770
 
748
771
  }
749
772
 
@@ -756,8 +779,6 @@ export class ASVGF extends RenderStage {
756
779
  this._outputRT.texture.needsUpdate = true;
757
780
  this._gradientRT.setSize( width, height );
758
781
  this._gradientRT.texture.needsUpdate = true;
759
- this._prevColorRT.setSize( width, height );
760
- this._prevColorRT.texture.needsUpdate = true;
761
782
  this.heatmapTarget.setSize( width, height );
762
783
  this.heatmapTarget.texture.needsUpdate = true;
763
784
  this.resW.value = width;
@@ -775,8 +796,6 @@ export class ASVGF extends RenderStage {
775
796
  // would dispatch both temporal ping-pong nodes while _readTemporalTexNode still
776
797
  // aliases one node's write target, producing a "write-only storage +
777
798
  // TextureBinding in same synchronization scope" validation error.
778
- // Only the size-dependent prev-color cache needs re-seeding.
779
- this._prevColorReady = false;
780
799
 
781
800
  }
782
801
 
@@ -799,13 +818,11 @@ export class ASVGF extends RenderStage {
799
818
  this._demodulatedRT?.dispose();
800
819
  this._outputRT?.dispose();
801
820
  this._gradientRT?.dispose();
802
- this._prevColorRT?.dispose();
803
821
  this._heatmapComputeNode?.dispose();
804
822
  this._heatmapStorageTex?.dispose();
805
823
  this.heatmapTarget?.dispose();
806
824
 
807
825
  this._colorTexNode?.dispose();
808
- this._prevColorTexNode?.dispose();
809
826
  this._albedoTexNode?.dispose();
810
827
  this._motionTexNode?.dispose();
811
828
  this._normalDepthTexNode?.dispose();
@@ -19,7 +19,7 @@ import { Ray } from './Struct.js';
19
19
  import { RAY_FLAG } from '../Processor/QueueManager.js';
20
20
  import {
21
21
  writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf,
22
- writeRayRadiance, writeGBuffer,
22
+ writeRayRadiance, writeGBuffer, writeGBufferSurfaceID,
23
23
  writeMediumStack,
24
24
  } from '../Processor/PackedRayBuffer.js';
25
25
 
@@ -91,6 +91,8 @@ export function buildGenerateKernel( params ) {
91
91
 
92
92
  // default: normal +Z, depth 1 (far), black albedo (background/miss)
93
93
  writeGBuffer( gBufferRW, uint( pixelIndex ), vec3( 0.0, 0.0, 1.0 ), float( 1.0 ), vec3( 0.0 ) );
94
+ // surface-ID lane defaults to invalid (valid=0); Shade overwrites it at the bounce-0 hit.
95
+ writeGBufferSurfaceID( gBufferRW, uint( pixelIndex ), uint( 0 ), uint( 0 ), float( 0.0 ), float( 0.0 ), uint( 0 ) );
94
96
 
95
97
  } );
96
98
 
@@ -48,9 +48,9 @@ import {
48
48
  readMediumStack, writeMediumStack, readMediumSigmaA, writeMediumSigmaA,
49
49
  readPathBounces, readSssSteps, readSSSMedium, writeSSSMedium,
50
50
  readHitDistance, readHitBarycentrics, readHitNormal,
51
- readHitMaterialIndex, readHitTriangleIndex,
51
+ readHitMaterialIndex, readHitTriangleIndex, readHitMeshIndex,
52
52
  writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf, writeRayRadiance,
53
- writeGBuffer, readGBuffer, gbDecodeNormalDepth,
53
+ writeGBuffer, writeGBufferSurfaceID, readGBuffer, gbDecodeNormalDepth,
54
54
  readRayRadiance,
55
55
  } from '../Processor/PackedRayBuffer.js';
56
56
 
@@ -374,6 +374,9 @@ export function buildShadeKernel( params ) {
374
374
  If( sampleIndex.equal( int( 0 ) ), () => {
375
375
 
376
376
  writeGBuffer( gBufferRW, pixelIndex, vec3( 0.0, 0.0, 1.0 ), linearDepth, vec3( 0.0 ) );
377
+ // Persist the primary-hit surface ID (Tier-1 A-SVGF correlated re-projection). Hit-only
378
+ // branch (misses Return above), so this marks the pixel valid; bary from the bounce-0 hit.
379
+ writeGBufferSurfaceID( gBufferRW, pixelIndex, hitTriIdx, readHitMeshIndex( hitBufferRO, rayID ), hitUV.x, hitUV.y, uint( 1 ) );
377
380
 
378
381
  } );
379
382