rayzee 6.5.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.
Files changed (51) hide show
  1. package/README.md +24 -5
  2. package/dist/rayzee.es.js +7624 -7063
  3. package/dist/rayzee.es.js.map +1 -1
  4. package/dist/rayzee.umd.js +157 -236
  5. package/dist/rayzee.umd.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/EngineDefaults.js +26 -9
  8. package/src/PathTracerApp.js +118 -26
  9. package/src/Pipeline/PipelineContext.js +1 -2
  10. package/src/Pipeline/RenderPipeline.js +1 -1
  11. package/src/Pipeline/RenderStage.js +1 -1
  12. package/src/Processor/CameraOptimizer.js +0 -5
  13. package/src/Processor/GeometryExtractor.js +6 -0
  14. package/src/Processor/KernelManager.js +277 -0
  15. package/src/Processor/PackedRayBuffer.js +291 -0
  16. package/src/Processor/QueueManager.js +173 -0
  17. package/src/Processor/SceneProcessor.js +1 -0
  18. package/src/Processor/ShaderBuilder.js +11 -317
  19. package/src/Processor/StorageTexturePool.js +29 -15
  20. package/src/Processor/VRAMTracker.js +169 -0
  21. package/src/Processor/utils.js +11 -110
  22. package/src/RenderSettings.js +0 -3
  23. package/src/Stages/ASVGF.js +151 -78
  24. package/src/Stages/BilateralFilter.js +34 -10
  25. package/src/Stages/EdgeFilter.js +2 -3
  26. package/src/Stages/MotionVector.js +16 -9
  27. package/src/Stages/NormalDepth.js +17 -5
  28. package/src/Stages/PathTracer.js +671 -1456
  29. package/src/Stages/PathTracerStage.js +1451 -0
  30. package/src/Stages/SSRC.js +32 -15
  31. package/src/Stages/Variance.js +35 -12
  32. package/src/TSL/CompactKernel.js +110 -0
  33. package/src/TSL/DebugKernel.js +98 -0
  34. package/src/TSL/Environment.js +13 -11
  35. package/src/TSL/ExtendKernel.js +75 -0
  36. package/src/TSL/FinalWriteKernel.js +121 -0
  37. package/src/TSL/GenerateKernel.js +111 -0
  38. package/src/TSL/LightsSampling.js +2 -2
  39. package/src/TSL/PathTracerCore.js +43 -1039
  40. package/src/TSL/ShadeKernel.js +876 -0
  41. package/src/TSL/patches.js +81 -4
  42. package/src/index.js +3 -0
  43. package/src/managers/CameraManager.js +1 -1
  44. package/src/managers/DenoisingManager.js +40 -75
  45. package/src/managers/EnvironmentManager.js +30 -39
  46. package/src/managers/OverlayManager.js +7 -22
  47. package/src/managers/UniformManager.js +0 -3
  48. package/src/managers/helpers/TileHelper.js +2 -2
  49. package/src/Stages/AdaptiveSampling.js +0 -483
  50. package/src/TSL/PathTracer.js +0 -384
  51. package/src/managers/TileManager.js +0 -298
@@ -1,20 +1,22 @@
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
- import { HalfFloatType, FloatType, RGBAFormat, NearestFilter, LinearFilter } from 'three';
5
+ import { HalfFloatType, FloatType, RGBAFormat, NearestFilter, LinearFilter, Box2, Vector2 } from 'three';
6
6
  import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
7
7
  import { luminance } from '../TSL/Common.js';
8
- import { ALBEDO_EPS } from '../EngineDefaults.js';
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();
@@ -57,41 +61,60 @@ export class ASVGF extends RenderStage {
57
61
  // FloatType for ping-pong: demodulated lighting on dark materials
58
62
  // (lighting ≈ color/0.01) exceeds HalfFloat's 65k cap on HDR.
59
63
  // LinearFilter is required for textureLoad codegen on StorageTextures.
60
- this._temporalTexA = new StorageTexture( w, h );
64
+ this._temporalTexA = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
61
65
  this._temporalTexA.type = FloatType;
62
66
  this._temporalTexA.format = RGBAFormat;
63
67
  this._temporalTexA.minFilter = LinearFilter;
64
68
  this._temporalTexA.magFilter = LinearFilter;
65
69
 
66
- this._temporalTexB = new StorageTexture( w, h );
70
+ this._temporalTexB = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
67
71
  this._temporalTexB.type = FloatType;
68
72
  this._temporalTexB.format = RGBAFormat;
69
73
  this._temporalTexB.minFilter = LinearFilter;
70
74
  this._temporalTexB.magFilter = LinearFilter;
71
75
 
72
- this._outputModulatedTex = new StorageTexture( w, h );
76
+ this._outputModulatedTex = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
73
77
  this._outputModulatedTex.type = FloatType;
74
78
  this._outputModulatedTex.format = RGBAFormat;
75
79
  this._outputModulatedTex.minFilter = LinearFilter;
76
80
  this._outputModulatedTex.magFilter = LinearFilter;
77
81
 
78
- this._gradientStorageTex = new StorageTexture( w, h );
82
+ this._gradientStorageTex = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
79
83
  this._gradientStorageTex.type = HalfFloatType;
80
84
  this._gradientStorageTex.format = RGBAFormat;
81
85
  this._gradientStorageTex.minFilter = LinearFilter;
82
86
  this._gradientStorageTex.magFilter = LinearFilter;
83
87
 
84
- // FloatType to match pathtracer:color (PT MRT). copyTextureToTexture
85
- // requires identical formats.
86
- this._prevColorRT = new RenderTarget( w, h, {
88
+ // Over-allocated StorageTextures are sampled by UV downstream; copy the
89
+ // active region into right-sized RTs and publish those instead.
90
+ this._srcRegion = new Box2( new Vector2( 0, 0 ), new Vector2( 0, 0 ) );
91
+
92
+ this._demodulatedRT = new RenderTarget( w, h, {
87
93
  type: FloatType,
88
94
  format: RGBAFormat,
89
- minFilter: NearestFilter,
90
- magFilter: NearestFilter,
95
+ minFilter: LinearFilter,
96
+ magFilter: LinearFilter,
97
+ depthBuffer: false,
98
+ stencilBuffer: false
99
+ } );
100
+
101
+ this._outputRT = new RenderTarget( w, h, {
102
+ type: FloatType,
103
+ format: RGBAFormat,
104
+ minFilter: LinearFilter,
105
+ magFilter: LinearFilter,
106
+ depthBuffer: false,
107
+ stencilBuffer: false
108
+ } );
109
+
110
+ this._gradientRT = new RenderTarget( w, h, {
111
+ type: HalfFloatType,
112
+ format: RGBAFormat,
113
+ minFilter: LinearFilter,
114
+ magFilter: LinearFilter,
91
115
  depthBuffer: false,
92
116
  stencilBuffer: false
93
117
  } );
94
- this._prevColorReady = false;
95
118
 
96
119
  this.currentMoments = 0; // 0 = write A, read B; 1 = write B, read A
97
120
  this._compiled = false;
@@ -105,7 +128,7 @@ export class ASVGF extends RenderStage {
105
128
  this.showHeatmap = false;
106
129
  this.debugMode = uniform( 0, 'int' );
107
130
 
108
- this._heatmapStorageTex = new StorageTexture( w, h );
131
+ this._heatmapStorageTex = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
109
132
  this._heatmapStorageTex.type = FloatType;
110
133
  this._heatmapStorageTex.format = RGBAFormat;
111
134
  this._heatmapStorageTex.minFilter = NearestFilter;
@@ -131,17 +154,24 @@ export class ASVGF extends RenderStage {
131
154
 
132
155
  }
133
156
 
134
- // Per-pixel adaptive-α signal: 5×5 spatial average of |currentLum − prevLum|
135
- // / meanLum, both raw single-SPP (noise-comparable), with noise-floor
136
- // subtraction. Currently gated off by gradientStrength=0 kept compiled
137
- // to drive heatmap mode 5 and as scaffolding for a proper variance-aware
138
- // 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.
139
165
  _buildGradientCompute() {
140
166
 
141
167
  const colorTex = this._colorTexNode;
142
- const prevColorTex = this._prevColorTexNode;
168
+ const albedoTex = this._albedoTexNode;
143
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;
144
173
  const noiseFloor = this.gradientNoiseFloor;
174
+ const sigmaScale = this.gradientSigmaScale;
145
175
  const gradientStorageTex = this._gradientStorageTex;
146
176
  const resW = this.resW;
147
177
  const resH = this.resH;
@@ -154,7 +184,7 @@ export class ASVGF extends RenderStage {
154
184
  const WG_THREADS = WG_SIZE * WG_SIZE; // 64
155
185
 
156
186
  const sharedCurLum = workgroupArray( 'float', TILE_TOTAL );
157
- const sharedPrevLum = workgroupArray( 'float', TILE_TOTAL );
187
+ const sharedHistLum = workgroupArray( 'float', TILE_TOTAL );
158
188
 
159
189
  const computeFn = Fn( () => {
160
190
 
@@ -173,20 +203,26 @@ export class ASVGF extends RenderStage {
173
203
  const gxL = tileOriginX.add( int( sx ) ).clamp( int( 0 ), int( resW ).sub( 1 ) );
174
204
  const gyL = tileOriginY.add( int( sy ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
175
205
 
206
+ // Demodulated current lighting luminance (matches the temporal pass:
207
+ // safeAlbedo = max(albedo, ALBEDO_EPS) keeps sky/dark-material round-trip).
176
208
  const curColor = textureLoad( colorTex, ivec2( gxL, gyL ) ).xyz;
177
- sharedCurLum.element( k ).assign( luminance( curColor ) );
178
-
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).
179
217
  const motion = textureLoad( motionTex, ivec2( gxL, gyL ) );
180
218
  const prevXf = float( gxL ).sub( motion.x.mul( resW ) );
181
219
  const prevYf = float( gyL ).sub( motion.y.mul( resH ) );
182
220
  const prevX = int( prevXf ).clamp( int( 0 ), int( resW ).sub( 1 ) );
183
221
  const prevY = int( prevYf ).clamp( int( 0 ), int( resH ).sub( 1 ) );
184
- // Invalid prev → mirror current so the diff contributes 0;
185
- // disocclusion is handled by the geometric gate downstream.
186
222
  const motionValid = motion.w.greaterThan( 0.5 );
187
- const prevColor = textureLoad( prevColorTex, ivec2( prevX, prevY ) ).xyz;
188
- const prevLum = motionValid.select( luminance( prevColor ), luminance( curColor ) );
189
- 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 );
190
226
 
191
227
  };
192
228
 
@@ -214,8 +250,33 @@ export class ASVGF extends RenderStage {
214
250
 
215
251
  If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
216
252
 
217
- const sumDiff = float( 0.0 ).toVar();
218
- 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();
256
+
257
+ for ( let dy = - TILE_BORDER; dy <= TILE_BORDER; dy ++ ) {
258
+
259
+ for ( let dx = - TILE_BORDER; dx <= TILE_BORDER; dx ++ ) {
260
+
261
+ const idx = ly.add( uint( TILE_BORDER + dy ) )
262
+ .mul( uint( TILE_W ) )
263
+ .add( lx.add( uint( TILE_BORDER + dx ) ) );
264
+ const cL = sharedCurLum.element( idx );
265
+ sumLum.addAssign( cL );
266
+ sumLumSq.addAssign( cL.mul( cL ) );
267
+
268
+ }
269
+
270
+ }
271
+
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();
219
280
 
220
281
  for ( let dy = - TILE_BORDER; dy <= TILE_BORDER; dy ++ ) {
221
282
 
@@ -225,26 +286,35 @@ export class ASVGF extends RenderStage {
225
286
  .mul( uint( TILE_W ) )
226
287
  .add( lx.add( uint( TILE_BORDER + dx ) ) );
227
288
  const cL = sharedCurLum.element( idx );
228
- const pL = sharedPrevLum.element( idx );
229
- sumDiff.addAssign( abs( cL.sub( pL ) ) );
230
- sumMean.addAssign( cL.add( pL ).mul( 0.5 ) );
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 ) );
231
296
 
232
297
  }
233
298
 
234
299
  }
235
300
 
236
- const rawGradient = sumDiff
237
- .div( max( sumMean, float( 0.001 ) ) )
238
- .clamp( 0.0, 1.0 );
239
- const oneMinusFloor = max( float( 1.0 ).sub( noiseFloor ), float( 0.0001 ) );
240
- const gradient = max( rawGradient.sub( noiseFloor ), float( 0.0 ) )
241
- .div( oneMinusFloor )
242
- .clamp( 0.0, 1.0 );
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 );
243
313
 
244
314
  textureStore(
245
315
  gradientStorageTex,
246
316
  uvec2( uint( gx ), uint( gy ) ),
247
- vec4( gradient, rawGradient, sumMean.div( 25.0 ), 1.0 )
317
+ vec4( gradient, gradientRaw, sigmaFloor, 1.0 )
248
318
  ).toWriteOnly();
249
319
 
250
320
  } );
@@ -581,8 +651,9 @@ export class ASVGF extends RenderStage {
581
651
  const img = colorTex.image;
582
652
  if ( img && img.width > 0 && img.height > 0 ) {
583
653
 
584
- if ( img.width !== this._temporalTexA.image.width ||
585
- img.height !== this._temporalTexA.image.height ) {
654
+ // Compare against an active-size RT, not the fixed-2048 StorageTexture.
655
+ if ( img.width !== this._outputRT.width ||
656
+ img.height !== this._outputRT.height ) {
586
657
 
587
658
  this.setSize( img.width, img.height );
588
659
 
@@ -603,12 +674,6 @@ export class ASVGF extends RenderStage {
603
674
  const writeTemporal = this.currentMoments === 0
604
675
  ? this._temporalTexA : this._temporalTexB;
605
676
 
606
- // Before first copy seeds the cache, alias current so the gradient
607
- // sees zero diff (no false boost).
608
- this._prevColorTexNode.value = this._prevColorReady
609
- ? this._prevColorRT.texture
610
- : colorTex;
611
-
612
677
  // First-frame compile while StorageTexture-typed nodes still hold
613
678
  // EmptyTexture, so textureLoad codegen emits the required `level`
614
679
  // parameter. Binding StorageTextures only AFTER compile keeps the
@@ -638,18 +703,21 @@ export class ASVGF extends RenderStage {
638
703
 
639
704
  this.renderer.compute( writeNode );
640
705
 
641
- // Cache this frame's pathtracer:color for next frame's gradient if it's
642
- // active. Copy AFTER reads so we don't clobber the prev view.
706
+ // Copy active region out of the over-allocated StorageTextures into
707
+ // right-sized RTs; downstream stages UV-sample these.
708
+ this._srcRegion.max.set( this.resW.value, this.resH.value );
709
+
710
+ this.renderer.copyTextureToTexture( writeTemporal, this._demodulatedRT.texture, this._srcRegion );
711
+ this.renderer.copyTextureToTexture( this._outputModulatedTex, this._outputRT.texture, this._srcRegion );
643
712
  if ( needsGradient ) {
644
713
 
645
- this.renderer.copyTextureToTexture( colorTex, this._prevColorRT.texture );
646
- this._prevColorReady = true;
714
+ this.renderer.copyTextureToTexture( this._gradientStorageTex, this._gradientRT.texture, this._srcRegion );
647
715
 
648
716
  }
649
717
 
650
- context.setTexture( 'asvgf:demodulated', writeTemporal );
651
- context.setTexture( 'asvgf:output', this._outputModulatedTex );
652
- context.setTexture( 'asvgf:gradient', this._gradientStorageTex );
718
+ context.setTexture( 'asvgf:demodulated', this._demodulatedRT.texture );
719
+ context.setTexture( 'asvgf:output', this._outputRT.texture );
720
+ context.setTexture( 'asvgf:gradient', this._gradientRT.texture );
653
721
 
654
722
  this.currentMoments = 1 - this.currentMoments;
655
723
 
@@ -665,7 +733,8 @@ export class ASVGF extends RenderStage {
665
733
  this._heatmapGradientTexNode.value = this._gradientStorageTex;
666
734
 
667
735
  this.renderer.compute( this._heatmapComputeNode );
668
- this.renderer.copyTextureToTexture( this._heatmapStorageTex, this.heatmapTarget.texture );
736
+ this._srcRegion.max.set( this.heatmapTarget.width, this.heatmapTarget.height );
737
+ this.renderer.copyTextureToTexture( this._heatmapStorageTex, this.heatmapTarget.texture, this._srcRegion );
669
738
 
670
739
  }
671
740
 
@@ -688,6 +757,7 @@ export class ASVGF extends RenderStage {
688
757
  if ( ! params ) return;
689
758
  if ( params.temporalAlpha !== undefined ) this.temporalAlpha.value = params.temporalAlpha;
690
759
  if ( params.gradientStrength !== undefined ) this.gradientStrength.value = params.gradientStrength;
760
+ if ( params.gradientSigmaScale !== undefined ) this.gradientSigmaScale.value = params.gradientSigmaScale;
691
761
  if ( params.gradientNoiseFloor !== undefined ) this.gradientNoiseFloor.value = params.gradientNoiseFloor;
692
762
  if ( params.maxAccumFrames !== undefined ) this.maxAccumFrames.value = params.maxAccumFrames;
693
763
  if ( params.debugMode !== undefined ) this.debugMode.value = params.debugMode;
@@ -697,20 +767,20 @@ export class ASVGF extends RenderStage {
697
767
  resetTemporalData() {
698
768
 
699
769
  this.currentMoments = 0;
700
- // Drop cache so post-reset frames don't see pre-reset prev color.
701
- this._prevColorReady = false;
702
770
 
703
771
  }
704
772
 
705
773
  setSize( width, height ) {
706
774
 
707
- this._temporalTexA.setSize( width, height );
708
- this._temporalTexB.setSize( width, height );
709
- this._outputModulatedTex.setSize( width, height );
710
- this._gradientStorageTex.setSize( width, height );
711
- this._prevColorRT.setSize( width, height );
712
- this._heatmapStorageTex.setSize( width, height );
775
+ // StorageTextures stay at max alloc — see resize crash fix (three.js #33061).
776
+ this._demodulatedRT.setSize( width, height );
777
+ this._demodulatedRT.texture.needsUpdate = true;
778
+ this._outputRT.setSize( width, height );
779
+ this._outputRT.texture.needsUpdate = true;
780
+ this._gradientRT.setSize( width, height );
781
+ this._gradientRT.texture.needsUpdate = true;
713
782
  this.heatmapTarget.setSize( width, height );
783
+ this.heatmapTarget.texture.needsUpdate = true;
714
784
  this.resW.value = width;
715
785
  this.resH.value = height;
716
786
 
@@ -721,9 +791,11 @@ export class ASVGF extends RenderStage {
721
791
  this._temporalNodeB.dispatchSize = [ this._dispatchX, this._dispatchY, 1 ];
722
792
  this._heatmapComputeNode.dispatchSize = [ this._dispatchX, this._dispatchY, 1 ];
723
793
 
724
- // Buffers reallocated → re-run first-frame compile and re-seed cache.
725
- this._compiled = false;
726
- this._prevColorReady = false;
794
+ // StorageTextures are over-allocated (never reallocated on resize), so the
795
+ // compute kernels stay valid — do NOT reset _compiled. Re-running the warmup
796
+ // would dispatch both temporal ping-pong nodes while _readTemporalTexNode still
797
+ // aliases one node's write target, producing a "write-only storage +
798
+ // TextureBinding in same synchronization scope" validation error.
727
799
 
728
800
  }
729
801
 
@@ -743,13 +815,14 @@ export class ASVGF extends RenderStage {
743
815
  this._temporalTexB?.dispose();
744
816
  this._outputModulatedTex?.dispose();
745
817
  this._gradientStorageTex?.dispose();
746
- this._prevColorRT?.dispose();
818
+ this._demodulatedRT?.dispose();
819
+ this._outputRT?.dispose();
820
+ this._gradientRT?.dispose();
747
821
  this._heatmapComputeNode?.dispose();
748
822
  this._heatmapStorageTex?.dispose();
749
823
  this.heatmapTarget?.dispose();
750
824
 
751
825
  this._colorTexNode?.dispose();
752
- this._prevColorTexNode?.dispose();
753
826
  this._albedoTexNode?.dispose();
754
827
  this._motionTexNode?.dispose();
755
828
  this._normalDepthTexNode?.dispose();
@@ -1,10 +1,10 @@
1
1
  import { Fn, wgslFn, vec3, vec4, float, int, uint, ivec2, uvec2, uniform, If, max, sqrt,
2
2
  textureLoad, textureStore, localId, workgroupId } from 'three/tsl';
3
- import { TextureNode, StorageTexture } from 'three/webgpu';
4
- import { HalfFloatType, RGBAFormat, LinearFilter } from 'three';
3
+ import { RenderTarget, TextureNode, StorageTexture } from 'three/webgpu';
4
+ import { HalfFloatType, RGBAFormat, LinearFilter, Box2, Vector2 } from 'three';
5
5
  import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
6
6
  import { luminance } from '../TSL/Common.js';
7
- import { ALBEDO_EPS } from '../EngineDefaults.js';
7
+ import { ALBEDO_EPS, MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
8
8
 
9
9
  // SVGF bilateral edge-stopping weight. All three φ params are relative
10
10
  // tolerances (unitless fractions) so the filter is scale-invariant across
@@ -83,19 +83,35 @@ export class BilateralFilter extends RenderStage {
83
83
  const w = options.width || 1;
84
84
  const h = options.height || 1;
85
85
 
86
+ // Pre-allocate StorageTextures at max — defensive against three.js #33061
87
+ // (TSL compute pipeline keeps a stale GPUTextureView after setSize()).
88
+
86
89
  // LinearFilter required for textureLoad codegen on StorageTextures.
87
- this._storageTexA = new StorageTexture( w, h );
90
+ this._storageTexA = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
88
91
  this._storageTexA.type = HalfFloatType;
89
92
  this._storageTexA.format = RGBAFormat;
90
93
  this._storageTexA.minFilter = LinearFilter;
91
94
  this._storageTexA.magFilter = LinearFilter;
92
95
 
93
- this._storageTexB = new StorageTexture( w, h );
96
+ this._storageTexB = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
94
97
  this._storageTexB.type = HalfFloatType;
95
98
  this._storageTexB.format = RGBAFormat;
96
99
  this._storageTexB.minFilter = LinearFilter;
97
100
  this._storageTexB.magFilter = LinearFilter;
98
101
 
102
+ this._srcRegion = new Box2( new Vector2( 0, 0 ), new Vector2( 0, 0 ) );
103
+
104
+ // Active-size RT published downstream; over-allocated storage tex sampled
105
+ // by UV would read the wrong region.
106
+ this._outputTarget = new RenderTarget( w, h, {
107
+ type: HalfFloatType,
108
+ format: RGBAFormat,
109
+ minFilter: LinearFilter,
110
+ magFilter: LinearFilter,
111
+ depthBuffer: false,
112
+ stencilBuffer: false
113
+ } );
114
+
99
115
  this._compiled = false;
100
116
 
101
117
  this._dispatchX = Math.ceil( w / 8 );
@@ -245,8 +261,9 @@ export class BilateralFilter extends RenderStage {
245
261
  const img = inputTex.image;
246
262
  if ( img && img.width > 0 && img.height > 0 ) {
247
263
 
248
- if ( img.width !== this._storageTexA.image.width ||
249
- img.height !== this._storageTexA.image.height ) {
264
+ // Compare against an active-size RT, not the fixed-2048 StorageTexture.
265
+ if ( img.width !== this._outputTarget.width ||
266
+ img.height !== this._outputTarget.height ) {
250
267
 
251
268
  this.setSize( img.width, img.height );
252
269
 
@@ -295,7 +312,12 @@ export class BilateralFilter extends RenderStage {
295
312
 
296
313
  }
297
314
 
298
- context.setTexture( 'bilateralFiltering:output', readTex );
315
+ // Copy the final result out of the over-allocated StorageTexture into
316
+ // the active-size RenderTarget; downstream stages UV-sample the latter.
317
+ this._srcRegion.max.set( this._outputTarget.width, this._outputTarget.height );
318
+ this.renderer.copyTextureToTexture( readTex, this._outputTarget.texture, this._srcRegion );
319
+
320
+ context.setTexture( 'bilateralFiltering:output', this._outputTarget.texture );
299
321
 
300
322
  }
301
323
 
@@ -313,8 +335,9 @@ export class BilateralFilter extends RenderStage {
313
335
 
314
336
  setSize( width, height ) {
315
337
 
316
- this._storageTexA.setSize( width, height );
317
- this._storageTexB.setSize( width, height );
338
+ // StorageTextures stay at their max allocation (see constructor).
339
+ this._outputTarget.setSize( width, height );
340
+ this._outputTarget.texture.needsUpdate = true;
318
341
  this.resW.value = width;
319
342
  this.resH.value = height;
320
343
 
@@ -338,6 +361,7 @@ export class BilateralFilter extends RenderStage {
338
361
  this._computeNodeB?.dispose();
339
362
  this._storageTexA?.dispose();
340
363
  this._storageTexB?.dispose();
364
+ this._outputTarget?.dispose();
341
365
  this._readTexNode?.dispose();
342
366
  this._normalDepthTexNode?.dispose();
343
367
  this._albedoTexNode?.dispose();
@@ -5,6 +5,7 @@ import { RenderTarget, TextureNode, StorageTexture } from 'three/webgpu';
5
5
  import { HalfFloatType, RGBAFormat, NearestFilter, Box2, Vector2 } from 'three';
6
6
  import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
7
7
  import { REC709_LUMINANCE_COEFFICIENTS } from '../TSL/Common.js';
8
+ import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
8
9
 
9
10
  /**
10
11
  * WebGPU Edge-Aware Filtering Stage (Compute Shader).
@@ -50,11 +51,10 @@ export class EdgeFilter extends RenderStage {
50
51
 
51
52
  // Pre-allocate StorageTexture at max — defensive against three.js #33061
52
53
  // (TSL compute pipeline re-compile returns zeros after resize).
53
- const MAX_STORAGE_SIZE = 2048;
54
54
  const w = options.width || 1;
55
55
  const h = options.height || 1;
56
56
 
57
- this._outputStorageTex = new StorageTexture( MAX_STORAGE_SIZE, MAX_STORAGE_SIZE );
57
+ this._outputStorageTex = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
58
58
  this._outputStorageTex.type = HalfFloatType;
59
59
  this._outputStorageTex.format = RGBAFormat;
60
60
  this._outputStorageTex.minFilter = NearestFilter;
@@ -243,7 +243,6 @@ export class EdgeFilter extends RenderStage {
243
243
 
244
244
  // Copy out of the over-allocated StorageTexture into the right-sized
245
245
  // RenderTarget; downstream stages can sample the latter.
246
- this._srcRegion.min.set( 0, 0 );
247
246
  this._srcRegion.max.set( this.outputTarget.width, this.outputTarget.height );
248
247
  this.renderer.copyTextureToTexture( this._outputStorageTex, this.outputTarget.texture, this._srcRegion );
249
248
 
@@ -1,8 +1,9 @@
1
1
  import { Fn, vec2, vec3, vec4, float, int, uint, ivec2, uvec2, uniform, If, normalize, mat3,
2
2
  textureLoad, textureStore, workgroupId, localId } from 'three/tsl';
3
3
  import { RenderTarget, TextureNode, StorageTexture } from 'three/webgpu';
4
- import { HalfFloatType, RGBAFormat, NearestFilter, Matrix4 } from 'three';
4
+ import { HalfFloatType, RGBAFormat, NearestFilter, Matrix4, Box2, Vector2 } from 'three';
5
5
  import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
6
+ import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
6
7
 
7
8
  /**
8
9
  * WebGPU Motion Vector Stage (Compute Shader)
@@ -89,19 +90,24 @@ export class MotionVector extends RenderStage {
89
90
  // Input texture node (swappable — no shader recompile)
90
91
  this._normalDepthTexNode = new TextureNode();
91
92
 
92
- // Write-only StorageTextures (compute output)
93
- this._screenSpaceStorageTex = new StorageTexture( width, height );
93
+ // Write-only StorageTextures (compute output).
94
+ // Pre-allocate at max — StorageTexture.setSize() destroys the GPU texture
95
+ // while the compute bind group keeps the stale view (three.js #33061).
96
+ this._screenSpaceStorageTex = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
94
97
  this._screenSpaceStorageTex.type = HalfFloatType;
95
98
  this._screenSpaceStorageTex.format = RGBAFormat;
96
99
  this._screenSpaceStorageTex.minFilter = NearestFilter;
97
100
  this._screenSpaceStorageTex.magFilter = NearestFilter;
98
101
 
99
- this._worldSpaceStorageTex = new StorageTexture( width, height );
102
+ this._worldSpaceStorageTex = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
100
103
  this._worldSpaceStorageTex.type = HalfFloatType;
101
104
  this._worldSpaceStorageTex.format = RGBAFormat;
102
105
  this._worldSpaceStorageTex.minFilter = NearestFilter;
103
106
  this._worldSpaceStorageTex.magFilter = NearestFilter;
104
107
 
108
+ // Reused per-copy: copy only the active region out of the over-alloc texs.
109
+ this._srcRegion = new Box2( new Vector2( 0, 0 ), new Vector2( 0, 0 ) );
110
+
105
111
  // Readable RenderTargets (copy destinations — published to context)
106
112
  const rtOpts = {
107
113
  type: HalfFloatType,
@@ -464,9 +470,11 @@ export class MotionVector extends RenderStage {
464
470
  this.renderer.compute( this._worldSpaceComputeNode );
465
471
 
466
472
  // Copy StorageTextures → RenderTargets (cross-dispatch reads from
467
- // StorageTexture return zeros — must use RenderTarget for downstream stages)
468
- this.renderer.copyTextureToTexture( this._screenSpaceStorageTex, this.screenSpaceTarget.texture );
469
- this.renderer.copyTextureToTexture( this._worldSpaceStorageTex, this.worldSpaceTarget.texture );
473
+ // StorageTexture return zeros — must use RenderTarget for downstream stages).
474
+ // srcRegion = active size; StorageTextures are over-allocated at 2048.
475
+ this._srcRegion.max.set( this.screenSpaceTarget.width, this.screenSpaceTarget.height );
476
+ this.renderer.copyTextureToTexture( this._screenSpaceStorageTex, this.screenSpaceTarget.texture, this._srcRegion );
477
+ this.renderer.copyTextureToTexture( this._worldSpaceStorageTex, this.worldSpaceTarget.texture, this._srcRegion );
470
478
 
471
479
  // Publish RenderTarget textures to context
472
480
  context.setTexture( 'motionVector:screenSpace', this.screenSpaceTarget.texture );
@@ -501,8 +509,7 @@ export class MotionVector extends RenderStage {
501
509
 
502
510
  setSize( width, height ) {
503
511
 
504
- this._screenSpaceStorageTex.setSize( width, height );
505
- this._worldSpaceStorageTex.setSize( width, height );
512
+ // StorageTextures stay at their max allocation (see constructor).
506
513
  this.screenSpaceTarget.setSize( width, height );
507
514
  this.screenSpaceTarget.texture.needsUpdate = true;
508
515
  this.worldSpaceTarget.setSize( width, height );