rayzee 5.4.2 → 5.5.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": "5.4.2",
3
+ "version": "5.5.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",
@@ -1,4 +1,5 @@
1
1
  import { WebGPURenderer, RectAreaLightNode } from 'three/webgpu';
2
+ import { texture as _tslTexture, cubeTexture as _tslCubeTexture } from 'three/tsl';
2
3
  import {
3
4
  ACESFilmicToneMapping, Scene, EventDispatcher, TimestampQuery
4
5
  } from 'three';
@@ -400,22 +401,100 @@ export class PathTracerApp extends EventDispatcher {
400
401
 
401
402
  this.stopAnimation();
402
403
  clearTimeout( this._resizeDebounceTimer );
404
+ this._resizeDebounceTimer = null;
403
405
 
404
- // Remove all tracked listeners (app-owned subscriptions across managers/DOM)
405
406
  this._removeTrackedListeners();
406
-
407
407
  setStatusCallback( null );
408
408
 
409
+ this.interactionManager?.deselect?.();
410
+ this.transformManager?.detach?.();
411
+
409
412
  this.animationManager?.dispose();
410
413
  this.transformManager?.dispose();
411
414
  this.overlayManager?.dispose();
412
- this._sceneHelpers?.clear();
413
415
  this.lightManager?.dispose();
414
416
  this.denoisingManager?.dispose();
415
- this.pipeline?.dispose();
416
417
  this.interactionManager?.dispose();
417
418
  this.cameraManager?.dispose();
419
+
420
+ this.pipeline?.dispose();
421
+
422
+ // _sdf + assetLoader own the heaviest GPU allocations (material texture arrays,
423
+ // BVH/triangle buffers, loaded GLTF resources, BVH refit worker, loader caches).
424
+ // They are not referenced by the pipeline, so pipeline.dispose() does not reach them.
425
+ this._sdf?.dispose();
426
+ this._sdf = null;
427
+
428
+ this.assetLoader?.dispose();
429
+ this.assetLoader = null;
430
+
431
+ if ( this.meshScene ) {
432
+
433
+ this.meshScene.environment?.dispose();
434
+ this.meshScene.environment = null;
435
+
436
+ for ( const child of [ ...this.meshScene.children ] ) {
437
+
438
+ disposeObjectFromMemory( child );
439
+
440
+ }
441
+
442
+ this.meshScene.clear();
443
+ this.meshScene = null;
444
+
445
+ }
446
+
447
+ this._sceneHelpers?.clear();
448
+ this._sceneHelpers = null;
449
+
450
+ this.scene?.clear();
451
+ this.scene = null;
452
+
453
+ // Three.js 0.184 leaks (confirmed via heap-snapshot retainer analysis):
454
+ //
455
+ // 1) Renderer.dispose() does not remove the 'resize' listener it installs on
456
+ // _canvasTarget. The bound handler closes over the renderer, pinning the
457
+ // entire WebGPU graph (Backend, Nodes, Bindings, Pipelines, GPUDevice,
458
+ // every TSL node) alive indefinitely.
459
+ // See three/src/renderers/common/Renderer.js:292 (attach) and
460
+ // :2503 (dispose — missing removal).
461
+ //
462
+ // 2) Textures manager (one per renderer) registers a per-texture 'dispose'
463
+ // listener that closes over `this = Textures` — which transitively
464
+ // captures backend → renderer. These listeners are removed only when
465
+ // the texture itself is destroyed. For module-level singletons like
466
+ // EmptyTexture (new Texture in TextureNode.js) and its CubeTexture
467
+ // counterpart, the texture is never destroyed, so every renderer ever
468
+ // created leaks through the singleton's listener array.
469
+ //
470
+ // Both workarounds are safe when only a single PathTracerApp is active at a
471
+ // time. If you run multiple in parallel, reset listeners only on the renderer
472
+ // being disposed (not the shared singletons).
473
+ if ( this.renderer?._canvasTarget && this.renderer._onCanvasTargetResize ) {
474
+
475
+ this.renderer._canvasTarget.removeEventListener(
476
+ 'resize',
477
+ this.renderer._onCanvasTargetResize
478
+ );
479
+
480
+ }
481
+
482
+ try {
483
+
484
+ const emptyTex = _tslTexture().value;
485
+ const emptyCube = _tslCubeTexture().value;
486
+ if ( emptyTex?._listeners?.dispose ) emptyTex._listeners.dispose.length = 0;
487
+ if ( emptyCube?._listeners?.dispose ) emptyCube._listeners.dispose.length = 0;
488
+
489
+ } catch ( err ) {
490
+
491
+ console.warn( 'PathTracerApp: failed to clear TSL texture singleton listeners', err );
492
+
493
+ }
494
+
418
495
  this.renderer?.dispose();
496
+ if ( this.renderer ) this.renderer._canvasTarget = null;
497
+ this.renderer = null;
419
498
 
420
499
  if ( this._stats ) {
421
500
 
@@ -424,6 +503,7 @@ export class PathTracerApp extends EventDispatcher {
424
503
 
425
504
  }
426
505
 
506
+ this.stages = {};
427
507
  this.isInitialized = false;
428
508
 
429
509
  }
@@ -460,13 +540,9 @@ export class PathTracerApp extends EventDispatcher {
460
540
  this.interactionManager?.deselect();
461
541
  this.transformManager?.detach?.();
462
542
 
463
- // Dispose the loaded model (geometries, materials, textures)
464
- if ( this.assetLoader?.targetModel ) {
465
-
466
- disposeObjectFromMemory( this.assetLoader.targetModel );
467
- this.assetLoader.targetModel = null;
468
-
469
- }
543
+ // Release the loaded model. If loaded via loadObject3D(), the caller owns it —
544
+ // we only detach it from the scene. Otherwise dispose geometries/materials/textures.
545
+ this.assetLoader?.releaseTargetModel();
470
546
 
471
547
  // Clear lights in the WebGPU light scene
472
548
  this.lightManager?.clearLights?.();
@@ -1111,8 +1187,6 @@ export class PathTracerApp extends EventDispatcher {
1111
1187
  }
1112
1188
  } );
1113
1189
 
1114
- window.renderer = this.renderer; // For debugging
1115
-
1116
1190
  await this.renderer.init();
1117
1191
 
1118
1192
  RectAreaLightNode.setLTC( RectAreaLightTexturesLib.init() );
@@ -37,6 +37,7 @@ export class AssetLoader extends EventDispatcher {
37
37
  this.camera = camera;
38
38
  this.controls = controls;
39
39
  this.targetModel = null;
40
+ this._externalModel = null;
40
41
  this.floorPlane = null;
41
42
  this.sceneScale = 1.0;
42
43
  this.loaderCache = {};
@@ -46,6 +47,31 @@ export class AssetLoader extends EventDispatcher {
46
47
 
47
48
  }
48
49
 
50
+ /**
51
+ * Releases the current targetModel. If it was supplied by the caller via
52
+ * loadObject3D(), we only detach it from its parent — the caller still owns
53
+ * that Object3D and may reuse it. Otherwise we disposeObjectFromMemory() to
54
+ * free geometry/material/texture GPU resources.
55
+ */
56
+ releaseTargetModel() {
57
+
58
+ if ( ! this.targetModel ) return;
59
+
60
+ if ( this.targetModel === this._externalModel ) {
61
+
62
+ this.targetModel.parent?.remove( this.targetModel );
63
+
64
+ } else {
65
+
66
+ disposeObjectFromMemory( this.targetModel );
67
+
68
+ }
69
+
70
+ this.targetModel = null;
71
+ this._externalModel = null;
72
+
73
+ }
74
+
49
75
  setRenderer( renderer ) {
50
76
 
51
77
  this.renderer = renderer;
@@ -173,7 +199,11 @@ export class AssetLoader extends EventDispatcher {
173
199
 
174
200
  } else {
175
201
 
176
- const extension = envUrl.split( '.' ).pop().toLowerCase();
202
+ // Strip query string + fragment before extracting extension, otherwise
203
+ // URLs like ".../foo.hdr?v=2" get mis-detected and fall through to the
204
+ // regular TextureLoader, which can't parse HDR/EXR binary data.
205
+ const cleanPath = envUrl.split( /[?#]/ )[ 0 ];
206
+ const extension = cleanPath.split( '.' ).pop().toLowerCase();
177
207
  texture = await this.loadEnvironmentByExtension( envUrl, extension );
178
208
 
179
209
  }
@@ -479,7 +509,7 @@ export class AssetLoader extends EventDispatcher {
479
509
  loader.parse( gltfContent, '',
480
510
  gltf => {
481
511
 
482
- if ( this.targetModel ) disposeObjectFromMemory( this.targetModel );
512
+ this.releaseTargetModel();
483
513
  this.targetModel = gltf.scene;
484
514
  this.onModelLoad( this.targetModel ).then( () => resolve( gltf ) );
485
515
 
@@ -522,7 +552,7 @@ export class AssetLoader extends EventDispatcher {
522
552
  const object = objLoader.parse( objContent );
523
553
  object.name = filePath;
524
554
 
525
- if ( this.targetModel ) disposeObjectFromMemory( this.targetModel );
555
+ this.releaseTargetModel();
526
556
  this.targetModel = object;
527
557
  await this.onModelLoad( this.targetModel );
528
558
  return object;
@@ -603,7 +633,7 @@ export class AssetLoader extends EventDispatcher {
603
633
  const objContent = strFromU8( objFile.content );
604
634
  const object = objLoader.parse( objContent );
605
635
 
606
- if ( this.targetModel ) disposeObjectFromMemory( this.targetModel );
636
+ this.releaseTargetModel();
607
637
  this.targetModel = object;
608
638
  await this.onModelLoad( this.targetModel );
609
639
 
@@ -773,7 +803,7 @@ export class AssetLoader extends EventDispatcher {
773
803
  const data = await loader.loadAsync( modelUrl );
774
804
  updateLoading( { status: "Processing Data...", progress: 10 } );
775
805
 
776
- if ( this.targetModel ) disposeObjectFromMemory( this.targetModel );
806
+ this.releaseTargetModel();
777
807
 
778
808
  this.targetModel = data.scene;
779
809
  this.animations = data.animations || [];
@@ -806,7 +836,7 @@ export class AssetLoader extends EventDispatcher {
806
836
 
807
837
  const data = await loader.parseAsync( arrayBuffer, '' );
808
838
 
809
- if ( this.targetModel ) disposeObjectFromMemory( this.targetModel );
839
+ this.releaseTargetModel();
810
840
 
811
841
  this.targetModel = data.scene;
812
842
  this.animations = data.animations || [];
@@ -845,7 +875,7 @@ export class AssetLoader extends EventDispatcher {
845
875
  }
846
876
 
847
877
  const object = this.loaderCache.fbx.parse( arrayBuffer );
848
- if ( this.targetModel ) disposeObjectFromMemory( this.targetModel );
878
+ this.releaseTargetModel();
849
879
  this.targetModel = object;
850
880
 
851
881
  updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
@@ -882,7 +912,7 @@ export class AssetLoader extends EventDispatcher {
882
912
  const object = this.loaderCache.obj.parse( contents );
883
913
  object.name = filename;
884
914
 
885
- if ( this.targetModel ) disposeObjectFromMemory( this.targetModel );
915
+ this.releaseTargetModel();
886
916
  this.targetModel = object;
887
917
 
888
918
  updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
@@ -920,7 +950,7 @@ export class AssetLoader extends EventDispatcher {
920
950
  const mesh = new Mesh( geometry, material );
921
951
  mesh.name = filename;
922
952
 
923
- if ( this.targetModel ) disposeObjectFromMemory( this.targetModel );
953
+ this.releaseTargetModel();
924
954
  this.targetModel = mesh;
925
955
 
926
956
  updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
@@ -970,7 +1000,7 @@ export class AssetLoader extends EventDispatcher {
970
1000
  }
971
1001
 
972
1002
  object.name = filename;
973
- if ( this.targetModel ) disposeObjectFromMemory( this.targetModel );
1003
+ this.releaseTargetModel();
974
1004
  this.targetModel = object;
975
1005
 
976
1006
  updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
@@ -1007,7 +1037,7 @@ export class AssetLoader extends EventDispatcher {
1007
1037
  const collada = this.loaderCache.collada.parse( contents );
1008
1038
  collada.scene.name = filename;
1009
1039
 
1010
- if ( this.targetModel ) disposeObjectFromMemory( this.targetModel );
1040
+ this.releaseTargetModel();
1011
1041
  this.targetModel = collada.scene;
1012
1042
 
1013
1043
  updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
@@ -1042,7 +1072,7 @@ export class AssetLoader extends EventDispatcher {
1042
1072
 
1043
1073
  const object = this.loaderCache.threemf.parse( arrayBuffer );
1044
1074
 
1045
- if ( this.targetModel ) disposeObjectFromMemory( this.targetModel );
1075
+ this.releaseTargetModel();
1046
1076
  this.targetModel = object;
1047
1077
 
1048
1078
  updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
@@ -1078,7 +1108,7 @@ export class AssetLoader extends EventDispatcher {
1078
1108
  const object = this.loaderCache.usdz.parse( arrayBuffer );
1079
1109
  object.name = filename;
1080
1110
 
1081
- if ( this.targetModel ) disposeObjectFromMemory( this.targetModel );
1111
+ this.releaseTargetModel();
1082
1112
  this.targetModel = object;
1083
1113
 
1084
1114
  updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
@@ -1101,8 +1131,9 @@ export class AssetLoader extends EventDispatcher {
1101
1131
 
1102
1132
  object3d.name = object3d.name || name;
1103
1133
 
1104
- if ( this.targetModel ) disposeObjectFromMemory( this.targetModel );
1134
+ this.releaseTargetModel();
1105
1135
  this.targetModel = object3d;
1136
+ this._externalModel = object3d;
1106
1137
 
1107
1138
  updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
1108
1139
  await this.onModelLoad( this.targetModel );
@@ -1379,23 +1410,18 @@ export class AssetLoader extends EventDispatcher {
1379
1410
  }
1380
1411
 
1381
1412
  this.loaderCache = {};
1382
- super.dispose(); // Use EventDispatcher's dispose method
1383
-
1384
- if ( this.targetModel ) {
1385
1413
 
1386
- disposeObjectFromMemory( this.targetModel );
1387
- this.targetModel = null;
1388
-
1389
- }
1414
+ // Three.js EventDispatcher exposes no dispose()/removeAllEventListeners().
1415
+ // Clear the internal listener map directly so handlers don't retain references.
1416
+ this._listeners = undefined;
1390
1417
 
1391
- console.log( 'AssetLoader resources disposed' );
1418
+ this.releaseTargetModel();
1392
1419
 
1393
1420
  }
1394
1421
 
1395
1422
  removeAllEventListeners() {
1396
1423
 
1397
- // Use EventDispatcher's dispose method for backward compatibility
1398
- super.dispose();
1424
+ this._listeners = undefined;
1399
1425
 
1400
1426
  }
1401
1427
 
@@ -206,32 +206,33 @@ export function createRenderTargetHelper( renderer, renderTargetOrTexture, optio
206
206
 
207
207
  } );
208
208
 
209
- window.addEventListener( 'pointermove', ( e ) => {
209
+ function onPointerMove( e ) {
210
210
 
211
211
  if ( ! isDragging ) return;
212
212
 
213
213
  const newLeft = e.clientX - dragOffsetX;
214
214
  const newTop = e.clientY - dragOffsetY;
215
215
 
216
- // Keep within window bounds
217
216
  const maxX = window.innerWidth - container.offsetWidth;
218
217
  const maxY = window.innerHeight - container.offsetHeight;
219
218
 
220
219
  container.style.left = `${Math.max( 0, Math.min( newLeft, maxX ) )}px`;
221
220
  container.style.top = `${Math.max( 0, Math.min( newTop, maxY ) )}px`;
222
221
 
223
- // Reset position properties that would otherwise take precedence
224
222
  container.style.bottom = 'auto';
225
223
  container.style.right = 'auto';
226
224
 
227
- } );
225
+ }
228
226
 
229
- window.addEventListener( 'pointerup', () => {
227
+ function onPointerUp() {
230
228
 
231
229
  isDragging = false;
232
230
  document.body.style.userSelect = '';
233
231
 
234
- } );
232
+ }
233
+
234
+ window.addEventListener( 'pointermove', onPointerMove );
235
+ window.addEventListener( 'pointerup', onPointerUp );
235
236
 
236
237
  // Optimize resize handling
237
238
  function handleResize() {
@@ -353,19 +354,20 @@ export function createRenderTargetHelper( renderer, renderTargetOrTexture, optio
353
354
 
354
355
  };
355
356
 
356
- // Handle resize events
357
- container.addEventListener( 'mousedown', () => {
357
+ function onContainerMouseDown() {
358
358
 
359
359
  window.addEventListener( 'mousemove', handleResize );
360
360
 
361
- } );
361
+ }
362
362
 
363
- window.addEventListener( 'mouseup', () => {
363
+ function onMouseUp() {
364
364
 
365
365
  window.removeEventListener( 'mousemove', handleResize );
366
366
 
367
- } );
367
+ }
368
368
 
369
+ container.addEventListener( 'mousedown', onContainerMouseDown );
370
+ window.addEventListener( 'mouseup', onMouseUp );
369
371
  window.addEventListener( 'resize', handleResize );
370
372
 
371
373
  // Auto-update animation frame
@@ -456,20 +458,45 @@ export function createRenderTargetHelper( renderer, renderTargetOrTexture, optio
456
458
  */
457
459
  container.dispose = function dispose() {
458
460
 
459
- if ( config.autoUpdate && animFrameId ) {
461
+ if ( animFrameId ) {
460
462
 
461
463
  cancelAnimationFrame( animFrameId );
464
+ animFrameId = null;
462
465
 
463
466
  }
464
467
 
465
- // Remove from DOM if attached
468
+ // Remove window listeners these close over renderer/renderTarget/container
469
+ // and pin the entire helper graph alive until the page unloads if not cleaned up.
470
+ window.removeEventListener( 'pointermove', onPointerMove );
471
+ window.removeEventListener( 'pointerup', onPointerUp );
472
+ window.removeEventListener( 'mouseup', onMouseUp );
473
+ window.removeEventListener( 'mousemove', handleResize );
474
+ window.removeEventListener( 'resize', handleResize );
475
+
466
476
  if ( container.parentNode ) {
467
477
 
468
478
  container.parentNode.removeChild( container );
469
479
 
470
480
  }
471
481
 
472
- // Clear references
482
+ // Drop closures attached to the container. These close over `renderer`,
483
+ // `renderTargetOrTexture`, and the canvas — keeping them around after the
484
+ // owning stage has been disposed retains the entire Three.js WebGPU graph.
485
+ container.startAutoUpdate = null;
486
+ container.stopAutoUpdate = null;
487
+ container.show = null;
488
+ container.hide = null;
489
+ container.toggle = null;
490
+ container.update = null;
491
+ container.dispose = null;
492
+
493
+ if ( domCanvas ) {
494
+
495
+ domCanvas.width = 0;
496
+ domCanvas.height = 0;
497
+
498
+ }
499
+
473
500
  clampedPixels = null;
474
501
 
475
502
  };
@@ -485,6 +485,13 @@ export class AdaptiveSampling extends RenderStage {
485
485
  this.heatmapTarget?.dispose();
486
486
  this.helper?.dispose();
487
487
 
488
+ this._computeNode = null;
489
+ this._heatmapComputeNode = null;
490
+ this._heatmapStorageTex = null;
491
+ this._outputStorageTex = null;
492
+ this.heatmapTarget = null;
493
+ this.helper = null;
494
+
488
495
  }
489
496
 
490
497
  }
@@ -2,9 +2,9 @@ import { storage } from 'three/tsl';
2
2
  import { StorageInstancedBufferAttribute } from 'three/webgpu';
3
3
  import {
4
4
  NearestFilter, Vector2, Matrix4,
5
- TextureLoader, RepeatWrapping, FloatType
5
+ TextureLoader, RepeatWrapping
6
6
  } from 'three';
7
- import { blueNoiseTextureNode } from '../TSL/Random.js';
7
+ import { stbnScalarTextureNode, stbnVec2TextureNode } from '../TSL/Random.js';
8
8
 
9
9
  // Pipeline system
10
10
  import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
@@ -26,8 +26,9 @@ import { LightSerializer } from '../Processor/LightSerializer';
26
26
  // Constants
27
27
  import { ENGINE_DEFAULTS as DEFAULT_STATE } from '../EngineDefaults.js';
28
28
 
29
- // Blue noise (loaded at runtime from CDN — not inlined to keep bundle small)
30
- const blueNoiseImage = 'https://assets.rayzee.atulmourya.com/noise/simple_bluenoise.png';
29
+ // STBN atlases - original source: https://github.com/NVIDIA-RTX/STBN/blob/main/Assets/STBN.zip
30
+ const stbnScalarAtlas = 'https://assets.rayzee.atulmourya.com/noise/stbn_scalar_atlas.png';
31
+ const stbnVec2Atlas = 'https://assets.rayzee.atulmourya.com/noise/stbn_vec2_atlas.png';
31
32
 
32
33
  /**
33
34
  * Data layout constants
@@ -190,8 +191,9 @@ export class PathTracer extends RenderStage {
190
191
  this.spotLightsData = null;
191
192
  this.areaLightsData = null;
192
193
 
193
- // Blue noise
194
- this.blueNoiseTexture = null;
194
+ // STBN noise textures
195
+ this.stbnScalarTexture = null;
196
+ this.stbnVec2Texture = null;
195
197
 
196
198
  // Packed light buffer — [lightBVH nodes (4 vec4s each) | emissive triangles (2 vec4s each)]
197
199
  // emissiveVec4Offset uniform tracks the vec4-count offset where emissive data starts.
@@ -376,25 +378,38 @@ export class PathTracer extends RenderStage {
376
378
  }
377
379
 
378
380
  /**
379
- * Setup blue noise texture
381
+ * Load STBN (Spatiotemporal Blue Noise) atlas textures.
382
+ * Each atlas is 1024×1024: 8×8 grid of 128×128 tiles, 64 temporal slices.
380
383
  */
381
384
  setupBlueNoise() {
382
385
 
383
386
  const loader = new TextureLoader();
384
387
  loader.setCrossOrigin( 'anonymous' );
385
- loader.load( blueNoiseImage, ( texture ) => {
386
388
 
387
- texture.minFilter = NearestFilter;
388
- texture.magFilter = NearestFilter;
389
- texture.wrapS = RepeatWrapping;
390
- texture.wrapT = RepeatWrapping;
391
- texture.type = FloatType;
392
- texture.generateMipmaps = false;
389
+ const configure = ( tex ) => {
393
390
 
394
- this.blueNoiseTexture = texture;
395
- blueNoiseTextureNode.value = texture;
391
+ tex.minFilter = NearestFilter;
392
+ tex.magFilter = NearestFilter;
393
+ tex.wrapS = RepeatWrapping;
394
+ tex.wrapT = RepeatWrapping;
395
+ tex.generateMipmaps = false;
396
+ return tex;
396
397
 
397
- console.log( `PathTracer: Blue noise loaded ${texture.image.width}x${texture.image.height}` );
398
+ };
399
+
400
+ loader.load( stbnScalarAtlas, ( tex ) => {
401
+
402
+ this.stbnScalarTexture = configure( tex );
403
+ stbnScalarTextureNode.value = tex;
404
+ console.log( `PathTracer: STBN scalar atlas loaded ${tex.image.width}x${tex.image.height}` );
405
+
406
+ } );
407
+
408
+ loader.load( stbnVec2Atlas, ( tex ) => {
409
+
410
+ this.stbnVec2Texture = configure( tex );
411
+ stbnVec2TextureNode.value = tex;
412
+ console.log( `PathTracer: STBN vec2 atlas loaded ${tex.image.width}x${tex.image.height}` );
398
413
 
399
414
  } );
400
415
 
@@ -1521,9 +1536,9 @@ export class PathTracer extends RenderStage {
1521
1536
 
1522
1537
  setBlueNoiseTexture( tex ) {
1523
1538
 
1524
- this.blueNoiseTexture = tex;
1525
- // Update the shared Random.js texture node so TSL shader graph uses the real texture
1526
- if ( tex ) blueNoiseTextureNode.value = tex;
1539
+ // Legacy API — sets the scalar STBN atlas texture
1540
+ this.stbnScalarTexture = tex;
1541
+ if ( tex ) stbnScalarTextureNode.value = tex;
1527
1542
 
1528
1543
  }
1529
1544
 
@@ -1676,7 +1691,8 @@ export class PathTracer extends RenderStage {
1676
1691
  this.storageTextures?.dispose();
1677
1692
 
1678
1693
  // Dispose textures
1679
- this.blueNoiseTexture?.dispose();
1694
+ this.stbnScalarTexture?.dispose();
1695
+ this.stbnVec2Texture?.dispose();
1680
1696
  this.placeholderTexture?.dispose();
1681
1697
 
1682
1698
  // Clear data references
@@ -28,12 +28,11 @@ import {
28
28
  texture,
29
29
  } from 'three/tsl';
30
30
 
31
- import { Ray, ShadowMaterial, HitInfo, DirectionSample, MaterialCache } from './Struct.js';
31
+ import { Ray, ShadowMaterial, HitInfo } from './Struct.js';
32
32
  import { PI, TWO_PI, EPSILON, REC709_LUMINANCE_COEFFICIENTS, powerHeuristic, getShadowMaterial, getDatafromStorageBuffer } from './Common.js';
33
33
  import { fresnelSchlickFloat } from './Fresnel.js';
34
34
  import { iorToFresnel0 } from './Fresnel.js';
35
35
  import {
36
- DirectionalLight, AreaLight, PointLight, SpotLight,
37
36
  sampleCone, intersectAreaLight,
38
37
  } from './LightsCore.js';
39
38
  import { calculateBeerLawAbsorption, calculateShadowTransmittance } from './MaterialTransmission.js';