rayzee 6.4.0 → 7.0.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 (58) hide show
  1. package/README.md +24 -5
  2. package/dist/rayzee.es.js +4953 -4225
  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 +29 -13
  8. package/src/PathTracerApp.js +119 -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 +22 -1
  14. package/src/Processor/KernelManager.js +277 -0
  15. package/src/Processor/PackedRayBuffer.js +265 -0
  16. package/src/Processor/QueueManager.js +173 -0
  17. package/src/Processor/SceneProcessor.js +1 -0
  18. package/src/Processor/ShaderBuilder.js +11 -316
  19. package/src/Processor/StorageTexturePool.js +29 -15
  20. package/src/Processor/TextureCreator.js +6 -0
  21. package/src/Processor/VRAMTracker.js +169 -0
  22. package/src/Processor/utils.js +11 -110
  23. package/src/RenderSettings.js +1 -3
  24. package/src/Stages/ASVGF.js +76 -20
  25. package/src/Stages/BilateralFilter.js +34 -10
  26. package/src/Stages/EdgeFilter.js +2 -3
  27. package/src/Stages/MotionVector.js +16 -9
  28. package/src/Stages/NormalDepth.js +17 -5
  29. package/src/Stages/PathTracer.js +671 -1456
  30. package/src/Stages/PathTracerStage.js +1451 -0
  31. package/src/Stages/SSRC.js +32 -15
  32. package/src/Stages/Variance.js +35 -12
  33. package/src/TSL/BVHTraversal.js +7 -1
  34. package/src/TSL/Common.js +12 -2
  35. package/src/TSL/CompactKernel.js +110 -0
  36. package/src/TSL/DebugKernel.js +98 -0
  37. package/src/TSL/Environment.js +13 -11
  38. package/src/TSL/ExtendKernel.js +75 -0
  39. package/src/TSL/FinalWriteKernel.js +121 -0
  40. package/src/TSL/GenerateKernel.js +109 -0
  41. package/src/TSL/LightsSampling.js +2 -2
  42. package/src/TSL/MaterialTransmission.js +32 -2
  43. package/src/TSL/PathTracerCore.js +43 -912
  44. package/src/TSL/ShadeKernel.js +873 -0
  45. package/src/TSL/Struct.js +5 -0
  46. package/src/TSL/Subsurface.js +232 -0
  47. package/src/TSL/patches.js +81 -4
  48. package/src/index.js +3 -0
  49. package/src/managers/CameraManager.js +1 -1
  50. package/src/managers/DenoisingManager.js +40 -75
  51. package/src/managers/EnvironmentManager.js +30 -39
  52. package/src/managers/MaterialDataManager.js +60 -1
  53. package/src/managers/OverlayManager.js +7 -22
  54. package/src/managers/UniformManager.js +1 -3
  55. package/src/managers/helpers/TileHelper.js +2 -2
  56. package/src/Stages/AdaptiveSampling.js +0 -483
  57. package/src/TSL/PathTracer.js +0 -384
  58. package/src/managers/TileManager.js +0 -298
@@ -27,21 +27,10 @@ export const updateStats = ( statsUpdate ) => {
27
27
 
28
28
  };
29
29
 
30
- /**
31
- * Convert raw frameCount to user-facing sample count.
32
- * In tiled mode, one "sample pass" spans all tiles.
33
- */
30
+ // Raw frameCount is the user-facing sample count (one full-frame pass per frame).
34
31
  export function getDisplaySamples( pathTracerStage ) {
35
32
 
36
- const frameCount = pathTracerStage.frameCount || 0;
37
- if ( pathTracerStage.renderMode?.value === 1 && frameCount > 0 ) {
38
-
39
- const totalTiles = pathTracerStage.tileManager?.totalTilesCache || 1;
40
- return 1 + Math.floor( ( frameCount - 1 ) / totalTiles );
41
-
42
- }
43
-
44
- return frameCount;
33
+ return pathTracerStage.frameCount || 0;
45
34
 
46
35
  }
47
36
 
@@ -214,9 +203,9 @@ export function generateMaterialSpheres( rows = 5, columns = 5, spacing = 1.2 )
214
203
 
215
204
  // ── Path Tracer Utilities (formerly PathTracerUtils static class) ──
216
205
 
217
- export function updateCompletionThreshold( renderMode, maxFrames, totalTiles ) {
206
+ export function updateCompletionThreshold( renderMode, maxFrames ) {
218
207
 
219
- return renderMode === 1 ? totalTiles * maxFrames : maxFrames;
208
+ return maxFrames;
220
209
 
221
210
  }
222
211
 
@@ -335,33 +324,10 @@ export function areValuesEqual( a, b ) {
335
324
 
336
325
  }
337
326
 
338
- export function calculateAccumulationAlpha( frameValue, renderMode, totalTiles, isInteractionMode = false ) {
339
-
340
- if ( isInteractionMode ) {
341
-
342
- return 1.0;
343
-
344
- }
345
-
346
- if ( renderMode === 0 ) {
347
-
348
- return 1.0 / ( frameValue + 1 );
349
-
350
- } else {
351
-
352
- if ( frameValue === 0 ) {
353
-
354
- return 1.0;
355
-
356
- } else {
357
-
358
- const completedTileCycles = Math.floor( ( frameValue - 1 ) / totalTiles );
359
- const totalSamples = 1 + completedTileCycles;
360
- return 1.0 / ( totalSamples + 1 );
327
+ export function calculateAccumulationAlpha( frameValue, isInteractionMode = false ) {
361
328
 
362
- }
363
-
364
- }
329
+ // Full-frame progressive accumulation: each frame is one complete pass.
330
+ return isInteractionMode ? 1.0 : 1.0 / ( frameValue + 1 );
365
331
 
366
332
  }
367
333
 
@@ -416,12 +382,6 @@ export function optimizeShaderDefines( defines, state ) {
416
382
 
417
383
  const optimized = { ...defines };
418
384
 
419
- if ( ! state.useAdaptiveSampling ) {
420
-
421
- delete optimized.ENABLE_ADAPTIVE_SAMPLING;
422
-
423
- }
424
-
425
385
  if ( ! state.enableAccumulation ) {
426
386
 
427
387
  delete optimized.ENABLE_ACCUMULATION;
@@ -438,49 +398,6 @@ export function optimizeShaderDefines( defines, state ) {
438
398
 
439
399
  }
440
400
 
441
- export function calculateSpiralOrder( tiles, center = null ) {
442
-
443
- const totalTiles = tiles * tiles;
444
- const centerPoint = center || new Vector2( ( tiles - 1 ) / 2, ( tiles - 1 ) / 2 );
445
- const tilePositions = [];
446
-
447
- for ( let i = 0; i < totalTiles; i ++ ) {
448
-
449
- const x = i % tiles;
450
- const y = Math.floor( i / tiles );
451
- const distance = Math.sqrt(
452
- Math.pow( x - centerPoint.x, 2 ) +
453
- Math.pow( y - centerPoint.y, 2 )
454
- );
455
- const angle = Math.atan( y - centerPoint.y, x - centerPoint.x );
456
-
457
- tilePositions.push( {
458
- index: i,
459
- x,
460
- y,
461
- distance,
462
- angle
463
- } );
464
-
465
- }
466
-
467
- tilePositions.sort( ( a, b ) => {
468
-
469
- const distanceDiff = a.distance - b.distance;
470
- if ( Math.abs( distanceDiff ) < 0.01 ) {
471
-
472
- return a.angle - b.angle;
473
-
474
- }
475
-
476
- return distanceDiff;
477
-
478
- } );
479
-
480
- return tilePositions.map( pos => pos.index );
481
-
482
- }
483
-
484
401
  export function clamp( value, min, max ) {
485
402
 
486
403
  return Math.min( Math.max( value, min ), max );
@@ -493,31 +410,15 @@ export function lerp( a, b, t ) {
493
410
 
494
411
  }
495
412
 
496
- export function isRenderComplete( frameValue, renderMode, maxFrames, totalTiles ) {
497
-
498
- if ( renderMode === 0 ) {
499
-
500
- return frameValue >= maxFrames;
501
-
502
- } else {
503
-
504
- return frameValue >= maxFrames * totalTiles;
413
+ export function isRenderComplete( frameValue, renderMode, maxFrames ) {
505
414
 
506
- }
415
+ return frameValue >= maxFrames;
507
416
 
508
417
  }
509
418
 
510
- export function getCurrentSampleCount( frameValue, renderMode, totalTiles ) {
511
-
512
- if ( renderMode === 0 ) {
419
+ export function getCurrentSampleCount( frameValue ) {
513
420
 
514
- return frameValue;
515
-
516
- } else {
517
-
518
- return Math.floor( frameValue / totalTiles );
519
-
520
- }
421
+ return frameValue;
521
422
 
522
423
  }
523
424
 
@@ -18,6 +18,7 @@ const SETTING_ROUTES = {
18
18
  maxBounces: { uniform: 'maxBounces', reset: true },
19
19
  samplesPerPixel: { uniform: 'samplesPerPixel', reset: true },
20
20
  transmissiveBounces: { uniform: 'transmissiveBounces', reset: true },
21
+ maxSubsurfaceSteps: { uniform: 'maxSubsurfaceSteps', reset: true },
21
22
  environmentIntensity: { uniform: 'environmentIntensity', reset: true },
22
23
  backgroundIntensity: { uniform: 'backgroundIntensity', reset: true },
23
24
  showBackground: { uniform: 'showBackground', reset: true },
@@ -39,8 +40,6 @@ const SETTING_ROUTES = {
39
40
  emissiveBoost: { uniform: 'emissiveBoost', reset: true },
40
41
  visMode: { uniform: 'visMode', reset: true },
41
42
  debugVisScale: { uniform: 'debugVisScale', reset: true },
42
- useAdaptiveSampling: { uniform: 'useAdaptiveSampling', reset: true },
43
- adaptiveSamplingMax: { uniform: 'adaptiveSamplingMax', reset: true },
44
43
 
45
44
  // ── Multi-stage / special handling ────────────────────────────
46
45
 
@@ -62,7 +61,6 @@ const SETTING_ROUTES = {
62
61
  */
63
62
  const DEFAULTS_KEY_MAP = {
64
63
  bounces: 'maxBounces',
65
- adaptiveSampling: 'useAdaptiveSampling',
66
64
  debugMode: 'visMode',
67
65
  };
68
66
 
@@ -2,10 +2,10 @@ import { Fn, vec3, vec4, float, int, uint, ivec2, uvec2, uniform,
2
2
  If, dot, max, min, abs, mix, pow, exp,
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
11
  * ASVGF — SVGF temporal + spatial denoising with albedo demodulation.
@@ -57,30 +57,61 @@ export class ASVGF extends RenderStage {
57
57
  // FloatType for ping-pong: demodulated lighting on dark materials
58
58
  // (lighting ≈ color/0.01) exceeds HalfFloat's 65k cap on HDR.
59
59
  // LinearFilter is required for textureLoad codegen on StorageTextures.
60
- this._temporalTexA = new StorageTexture( w, h );
60
+ this._temporalTexA = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
61
61
  this._temporalTexA.type = FloatType;
62
62
  this._temporalTexA.format = RGBAFormat;
63
63
  this._temporalTexA.minFilter = LinearFilter;
64
64
  this._temporalTexA.magFilter = LinearFilter;
65
65
 
66
- this._temporalTexB = new StorageTexture( w, h );
66
+ this._temporalTexB = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
67
67
  this._temporalTexB.type = FloatType;
68
68
  this._temporalTexB.format = RGBAFormat;
69
69
  this._temporalTexB.minFilter = LinearFilter;
70
70
  this._temporalTexB.magFilter = LinearFilter;
71
71
 
72
- this._outputModulatedTex = new StorageTexture( w, h );
72
+ this._outputModulatedTex = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
73
73
  this._outputModulatedTex.type = FloatType;
74
74
  this._outputModulatedTex.format = RGBAFormat;
75
75
  this._outputModulatedTex.minFilter = LinearFilter;
76
76
  this._outputModulatedTex.magFilter = LinearFilter;
77
77
 
78
- this._gradientStorageTex = new StorageTexture( w, h );
78
+ this._gradientStorageTex = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
79
79
  this._gradientStorageTex.type = HalfFloatType;
80
80
  this._gradientStorageTex.format = RGBAFormat;
81
81
  this._gradientStorageTex.minFilter = LinearFilter;
82
82
  this._gradientStorageTex.magFilter = LinearFilter;
83
83
 
84
+ // Over-allocated StorageTextures are sampled by UV downstream; copy the
85
+ // active region into right-sized RTs and publish those instead.
86
+ this._srcRegion = new Box2( new Vector2( 0, 0 ), new Vector2( 0, 0 ) );
87
+
88
+ this._demodulatedRT = new RenderTarget( w, h, {
89
+ type: FloatType,
90
+ format: RGBAFormat,
91
+ minFilter: LinearFilter,
92
+ magFilter: LinearFilter,
93
+ depthBuffer: false,
94
+ stencilBuffer: false
95
+ } );
96
+
97
+ this._outputRT = new RenderTarget( w, h, {
98
+ type: FloatType,
99
+ format: RGBAFormat,
100
+ minFilter: LinearFilter,
101
+ magFilter: LinearFilter,
102
+ depthBuffer: false,
103
+ stencilBuffer: false
104
+ } );
105
+
106
+ this._gradientRT = new RenderTarget( w, h, {
107
+ type: HalfFloatType,
108
+ format: RGBAFormat,
109
+ minFilter: LinearFilter,
110
+ magFilter: LinearFilter,
111
+ depthBuffer: false,
112
+ stencilBuffer: false
113
+ } );
114
+
84
115
  // FloatType to match pathtracer:color (PT MRT). copyTextureToTexture
85
116
  // requires identical formats.
86
117
  this._prevColorRT = new RenderTarget( w, h, {
@@ -105,7 +136,7 @@ export class ASVGF extends RenderStage {
105
136
  this.showHeatmap = false;
106
137
  this.debugMode = uniform( 0, 'int' );
107
138
 
108
- this._heatmapStorageTex = new StorageTexture( w, h );
139
+ this._heatmapStorageTex = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
109
140
  this._heatmapStorageTex.type = FloatType;
110
141
  this._heatmapStorageTex.format = RGBAFormat;
111
142
  this._heatmapStorageTex.minFilter = NearestFilter;
@@ -581,8 +612,9 @@ export class ASVGF extends RenderStage {
581
612
  const img = colorTex.image;
582
613
  if ( img && img.width > 0 && img.height > 0 ) {
583
614
 
584
- if ( img.width !== this._temporalTexA.image.width ||
585
- img.height !== this._temporalTexA.image.height ) {
615
+ // Compare against an active-size RT, not the fixed-2048 StorageTexture.
616
+ if ( img.width !== this._outputRT.width ||
617
+ img.height !== this._outputRT.height ) {
586
618
 
587
619
  this.setSize( img.width, img.height );
588
620
 
@@ -647,9 +679,21 @@ export class ASVGF extends RenderStage {
647
679
 
648
680
  }
649
681
 
650
- context.setTexture( 'asvgf:demodulated', writeTemporal );
651
- context.setTexture( 'asvgf:output', this._outputModulatedTex );
652
- context.setTexture( 'asvgf:gradient', this._gradientStorageTex );
682
+ // Copy active region out of the over-allocated StorageTextures into
683
+ // right-sized RTs; downstream stages UV-sample these.
684
+ this._srcRegion.max.set( this.resW.value, this.resH.value );
685
+
686
+ this.renderer.copyTextureToTexture( writeTemporal, this._demodulatedRT.texture, this._srcRegion );
687
+ this.renderer.copyTextureToTexture( this._outputModulatedTex, this._outputRT.texture, this._srcRegion );
688
+ if ( needsGradient ) {
689
+
690
+ this.renderer.copyTextureToTexture( this._gradientStorageTex, this._gradientRT.texture, this._srcRegion );
691
+
692
+ }
693
+
694
+ context.setTexture( 'asvgf:demodulated', this._demodulatedRT.texture );
695
+ context.setTexture( 'asvgf:output', this._outputRT.texture );
696
+ context.setTexture( 'asvgf:gradient', this._gradientRT.texture );
653
697
 
654
698
  this.currentMoments = 1 - this.currentMoments;
655
699
 
@@ -665,7 +709,8 @@ export class ASVGF extends RenderStage {
665
709
  this._heatmapGradientTexNode.value = this._gradientStorageTex;
666
710
 
667
711
  this.renderer.compute( this._heatmapComputeNode );
668
- this.renderer.copyTextureToTexture( this._heatmapStorageTex, this.heatmapTarget.texture );
712
+ this._srcRegion.max.set( this.heatmapTarget.width, this.heatmapTarget.height );
713
+ this.renderer.copyTextureToTexture( this._heatmapStorageTex, this.heatmapTarget.texture, this._srcRegion );
669
714
 
670
715
  }
671
716
 
@@ -704,13 +749,17 @@ export class ASVGF extends RenderStage {
704
749
 
705
750
  setSize( width, height ) {
706
751
 
707
- this._temporalTexA.setSize( width, height );
708
- this._temporalTexB.setSize( width, height );
709
- this._outputModulatedTex.setSize( width, height );
710
- this._gradientStorageTex.setSize( width, height );
752
+ // StorageTextures stay at max alloc — see resize crash fix (three.js #33061).
753
+ this._demodulatedRT.setSize( width, height );
754
+ this._demodulatedRT.texture.needsUpdate = true;
755
+ this._outputRT.setSize( width, height );
756
+ this._outputRT.texture.needsUpdate = true;
757
+ this._gradientRT.setSize( width, height );
758
+ this._gradientRT.texture.needsUpdate = true;
711
759
  this._prevColorRT.setSize( width, height );
712
- this._heatmapStorageTex.setSize( width, height );
760
+ this._prevColorRT.texture.needsUpdate = true;
713
761
  this.heatmapTarget.setSize( width, height );
762
+ this.heatmapTarget.texture.needsUpdate = true;
714
763
  this.resW.value = width;
715
764
  this.resH.value = height;
716
765
 
@@ -721,8 +770,12 @@ export class ASVGF extends RenderStage {
721
770
  this._temporalNodeB.dispatchSize = [ this._dispatchX, this._dispatchY, 1 ];
722
771
  this._heatmapComputeNode.dispatchSize = [ this._dispatchX, this._dispatchY, 1 ];
723
772
 
724
- // Buffers reallocated → re-run first-frame compile and re-seed cache.
725
- this._compiled = false;
773
+ // StorageTextures are over-allocated (never reallocated on resize), so the
774
+ // compute kernels stay valid — do NOT reset _compiled. Re-running the warmup
775
+ // would dispatch both temporal ping-pong nodes while _readTemporalTexNode still
776
+ // aliases one node's write target, producing a "write-only storage +
777
+ // TextureBinding in same synchronization scope" validation error.
778
+ // Only the size-dependent prev-color cache needs re-seeding.
726
779
  this._prevColorReady = false;
727
780
 
728
781
  }
@@ -743,6 +796,9 @@ export class ASVGF extends RenderStage {
743
796
  this._temporalTexB?.dispose();
744
797
  this._outputModulatedTex?.dispose();
745
798
  this._gradientStorageTex?.dispose();
799
+ this._demodulatedRT?.dispose();
800
+ this._outputRT?.dispose();
801
+ this._gradientRT?.dispose();
746
802
  this._prevColorRT?.dispose();
747
803
  this._heatmapComputeNode?.dispose();
748
804
  this._heatmapStorageTex?.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 );
@@ -1,8 +1,9 @@
1
1
  import { Fn, vec3, vec4, float, int, uint, uvec2, uniform, normalize, mat3, storage, If,
2
2
  textureStore, workgroupId, localId } from 'three/tsl';
3
3
  import { RenderTarget, 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
  import { Ray, HitInfo } from '../TSL/Struct.js';
7
8
  import { traverseBVH } from '../TSL/BVHTraversal.js';
8
9
 
@@ -44,12 +45,15 @@ export class NormalDepth extends RenderStage {
44
45
  const w = options.width || 1;
45
46
  const h = options.height || 1;
46
47
 
47
- this._outputStorageTex = new StorageTexture( w, h );
48
+ // StorageTexture stays at max alloc — see resize crash fix (three.js #33061).
49
+ this._outputStorageTex = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
48
50
  this._outputStorageTex.type = HalfFloatType;
49
51
  this._outputStorageTex.format = RGBAFormat;
50
52
  this._outputStorageTex.minFilter = NearestFilter;
51
53
  this._outputStorageTex.magFilter = NearestFilter;
52
54
 
55
+ this._srcRegion = new Box2( new Vector2( 0, 0 ), new Vector2( 0, 0 ) );
56
+
53
57
  // Ping-pong RTs share format with the StorageTexture so copyTextureToTexture works.
54
58
  const rtOpts = {
55
59
  type: HalfFloatType,
@@ -252,13 +256,16 @@ export class NormalDepth extends RenderStage {
252
256
  const prevRT = this._currentIdx === 0 ? this._rtB : this._rtA;
253
257
 
254
258
  this.renderer.compute( this._computeNode );
255
- this.renderer.copyTextureToTexture( this._outputStorageTex, writeRT.texture );
259
+
260
+ // Copy only the active region out of the over-allocated StorageTexture.
261
+ this._srcRegion.max.set( writeRT.width, writeRT.height );
262
+ this.renderer.copyTextureToTexture( this._outputStorageTex, writeRT.texture, this._srcRegion );
256
263
 
257
264
  // First dispatch: seed prev from current so ASVGF doesn't see false
258
265
  // disocclusion on frame 1.
259
266
  if ( ! this._hasHistory ) {
260
267
 
261
- this.renderer.copyTextureToTexture( this._outputStorageTex, prevRT.texture );
268
+ this.renderer.copyTextureToTexture( this._outputStorageTex, prevRT.texture, this._srcRegion );
262
269
  this._hasHistory = true;
263
270
 
264
271
  }
@@ -279,9 +286,14 @@ export class NormalDepth extends RenderStage {
279
286
 
280
287
  setSize( width, height ) {
281
288
 
282
- this._outputStorageTex.setSize( width, height );
289
+ // StorageTexture stays at its max allocation (see constructor).
290
+ // RenderTarget.setSize() updates width/height but does NOT bump
291
+ // texture.version, so copyTextureToTexture's GPU texture would stay at
292
+ // the old size — needsUpdate forces the resize to take effect.
283
293
  this._rtA.setSize( width, height );
294
+ this._rtA.texture.needsUpdate = true;
284
295
  this._rtB.setSize( width, height );
296
+ this._rtB.texture.needsUpdate = true;
285
297
  this._hasHistory = false;
286
298
  this.resolutionWidth.value = width;
287
299
  this.resolutionHeight.value = height;