rayzee 5.4.0 → 5.4.1

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.0",
3
+ "version": "5.4.1",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -639,8 +639,7 @@ export class PathTracerApp extends EventDispatcher {
639
639
 
640
640
  if ( ! this._sdf.uploadToPathTracer( this.stages.pathTracer, this.lightManager, this.meshScene, environmentTexture ) ) return false;
641
641
 
642
- // Build per-mesh visibility buffer (must happen before setupMaterial so the
643
- // shader graph captures the storage node during compilation)
642
+ // Patch per-mesh visibility into the TLAS leaves we just uploaded
644
643
  this.stages.pathTracer._meshRefs = this.stages.pathTracer._collectMeshRefs( this.meshScene );
645
644
  this.stages.pathTracer.setMeshVisibilityData( this.stages.pathTracer._meshRefs );
646
645
 
@@ -472,20 +472,28 @@ export class AssetLoader extends EventDispatcher {
472
472
  const loader = await this.createGLTFLoader();
473
473
  loader.manager = manager;
474
474
 
475
- return await new Promise( ( resolve, reject ) => {
475
+ try {
476
476
 
477
- loader.parse( gltfContent, '',
478
- gltf => {
477
+ return await new Promise( ( resolve, reject ) => {
479
478
 
480
- if ( this.targetModel ) disposeObjectFromMemory( this.targetModel );
481
- this.targetModel = gltf.scene;
482
- this.onModelLoad( this.targetModel ).then( () => resolve( gltf ) );
479
+ loader.parse( gltfContent, '',
480
+ gltf => {
483
481
 
484
- },
485
- error => reject( error )
486
- );
482
+ if ( this.targetModel ) disposeObjectFromMemory( this.targetModel );
483
+ this.targetModel = gltf.scene;
484
+ this.onModelLoad( this.targetModel ).then( () => resolve( gltf ) );
487
485
 
488
- } );
486
+ },
487
+ error => reject( error )
488
+ );
489
+
490
+ } );
491
+
492
+ } finally {
493
+
494
+ this._disposeGLTFLoader( loader );
495
+
496
+ }
489
497
 
490
498
  } else {
491
499
 
@@ -697,11 +705,10 @@ export class AssetLoader extends EventDispatcher {
697
705
 
698
706
  }
699
707
 
700
- // Model loading methods
708
+ // Returns a fresh loader each call — DRACOLoader/KTX2Loader hold persistent
709
+ // worker pools. Callers must invoke _disposeGLTFLoader() to terminate them.
701
710
  async createGLTFLoader() {
702
711
 
703
- if ( this.loaderCache.gltf ) return this.loaderCache.gltf;
704
-
705
712
  const dracoLoader = new DRACOLoader();
706
713
  dracoLoader.setDecoderConfig( { type: 'js' } );
707
714
  dracoLoader.setDecoderPath( 'https://www.gstatic.com/draco/v1/decoders/' );
@@ -726,18 +733,23 @@ export class AssetLoader extends EventDispatcher {
726
733
 
727
734
  }
728
735
 
729
- this.loaderCache.ktx2 = ktx2Loader;
730
-
731
736
  const loader = new GLTFLoader();
732
737
  loader.setDRACOLoader( dracoLoader );
733
738
  loader.setKTX2Loader( ktx2Loader );
734
739
  loader.setMeshoptDecoder( MeshoptDecoder );
735
740
 
736
- this.loaderCache.gltf = loader;
737
741
  return loader;
738
742
 
739
743
  }
740
744
 
745
+ _disposeGLTFLoader( loader ) {
746
+
747
+ if ( ! loader ) return;
748
+ loader.dracoLoader?.dispose();
749
+ loader.ktx2Loader?.dispose();
750
+
751
+ }
752
+
741
753
  async loadExampleModels( index, modelFiles ) {
742
754
 
743
755
  if ( ! modelFiles || ! modelFiles[ index ] ) {
@@ -753,9 +765,10 @@ export class AssetLoader extends EventDispatcher {
753
765
 
754
766
  async loadModel( modelUrl ) {
755
767
 
768
+ const loader = await this.createGLTFLoader();
769
+
756
770
  try {
757
771
 
758
- const loader = await this.createGLTFLoader();
759
772
  updateLoading( { status: "Loading Model...", progress: 2 } );
760
773
  const data = await loader.loadAsync( modelUrl );
761
774
  updateLoading( { status: "Processing Data...", progress: 10 } );
@@ -774,15 +787,20 @@ export class AssetLoader extends EventDispatcher {
774
787
  this.dispatchEvent( { type: 'error', message: error.message, filename: modelUrl } );
775
788
  throw error;
776
789
 
790
+ } finally {
791
+
792
+ this._disposeGLTFLoader( loader );
793
+
777
794
  }
778
795
 
779
796
  }
780
797
 
781
798
  async loadGLBFromArrayBuffer( arrayBuffer, filename = 'model.glb' ) {
782
799
 
800
+ const loader = await this.createGLTFLoader();
801
+
783
802
  try {
784
803
 
785
- const loader = await this.createGLTFLoader();
786
804
  updateLoading( { isLoading: true, status: "Processing GLB Data...", progress: 5 } );
787
805
  await new Promise( r => setTimeout( r, 0 ) );
788
806
 
@@ -804,6 +822,10 @@ export class AssetLoader extends EventDispatcher {
804
822
  this.dispatchEvent( { type: 'error', message: error.message, filename } );
805
823
  throw error;
806
824
 
825
+ } finally {
826
+
827
+ this._disposeGLTFLoader( loader );
828
+
807
829
  }
808
830
 
809
831
  }
@@ -220,53 +220,62 @@ export class EquirectHDRInfo {
220
220
 
221
221
  const { floatData, width, height } = extractFloatData( hdr );
222
222
 
223
- // Reuse worker across calls; create on first use
224
- if ( ! this._worker ) {
223
+ // Fresh worker per call terminated in finally to avoid ~30 MB residency.
224
+ try {
225
225
 
226
- try {
226
+ this._worker = new Worker( CDF_WORKER_URL, { type: 'module' } );
227
227
 
228
- this._worker = new Worker( CDF_WORKER_URL, { type: 'module' } );
228
+ } catch ( e ) {
229
229
 
230
- } catch ( e ) {
230
+ if ( e.name !== 'SecurityError' ) throw e;
231
+ this._worker = await fetchAsWorker( CDF_WORKER_URL );
231
232
 
232
- if ( e.name !== 'SecurityError' ) throw e;
233
- this._worker = await fetchAsWorker( CDF_WORKER_URL );
233
+ }
234
234
 
235
- }
235
+ try {
236
236
 
237
- }
237
+ const result = await new Promise( ( resolve, reject ) => {
238
238
 
239
- const result = await new Promise( ( resolve, reject ) => {
239
+ this._worker.onmessage = ( e ) => {
240
240
 
241
- this._worker.onmessage = ( e ) => {
241
+ if ( e.data.error ) {
242
242
 
243
- if ( e.data.error ) {
243
+ reject( new Error( e.data.error ) );
244
244
 
245
- reject( new Error( e.data.error ) );
245
+ } else {
246
246
 
247
- } else {
247
+ resolve( e.data );
248
248
 
249
- resolve( e.data );
249
+ }
250
250
 
251
- }
251
+ };
252
252
 
253
- };
253
+ this._worker.onerror = reject;
254
254
 
255
- this._worker.onerror = reject;
255
+ // Transfer floatData to worker (zero-copy)
256
+ this._worker.postMessage(
257
+ { floatData, width, height },
258
+ [ floatData.buffer ]
259
+ );
256
260
 
257
- // Transfer floatData to worker (zero-copy)
258
- this._worker.postMessage(
259
- { floatData, width, height },
260
- [ floatData.buffer ]
261
- );
261
+ } );
262
262
 
263
- } );
263
+ this.marginalData = result.marginalData;
264
+ this.conditionalData = result.conditionalData;
265
+ this.totalSum = result.totalSum;
266
+ this.width = result.width;
267
+ this.height = result.height;
264
268
 
265
- this.marginalData = result.marginalData;
266
- this.conditionalData = result.conditionalData;
267
- this.totalSum = result.totalSum;
268
- this.width = result.width;
269
- this.height = result.height;
269
+ } finally {
270
+
271
+ if ( this._worker ) {
272
+
273
+ this._worker.terminate();
274
+ this._worker = null;
275
+
276
+ }
277
+
278
+ }
270
279
 
271
280
  }
272
281
 
@@ -57,10 +57,26 @@ export class InstanceTable {
57
57
  worldAABB: null, // Computed from triangle data
58
58
  originalToBvhMap,
59
59
  bvhData,
60
+ visible: true, // Per-mesh visibility (baked into TLAS leaf slot [2])
61
+ tlasLeafIndex: - 1, // Set by TLASBuilder.flatten() — enables in-place visibility patching
60
62
  };
61
63
 
62
64
  }
63
65
 
66
+ /**
67
+ * Set per-mesh visibility flag. Does NOT update the GPU buffer —
68
+ * caller must patch combinedBvhData[tlasLeafIndex*16 + 2] and mark bvh attr dirty.
69
+ *
70
+ * @param {number} meshIndex
71
+ * @param {boolean} visible
72
+ */
73
+ setVisibility( meshIndex, visible ) {
74
+
75
+ const entry = this.entries[ meshIndex ];
76
+ if ( entry ) entry.visible = visible;
77
+
78
+ }
79
+
64
80
  /**
65
81
  * Compute world-space AABBs for all entries from their BLAS root node data.
66
82
  * O(1) per mesh for inner roots; falls back to triangle scan for leaf roots (rare).
@@ -476,48 +476,36 @@ export class SceneProcessor {
476
476
 
477
477
  const validEntries = this.instanceTable.entries.filter( e => e !== null );
478
478
 
479
- if ( validEntries.length === 1 ) {
480
-
481
- // Single mesh use BLAS directly as flat BVH (no TLAS wrapper).
482
- // Avoids per-ray TLAS overhead and the extra branch in traversal.
483
- const entry = validEntries[ 0 ];
484
- this.bvhData = entry.bvhData;
485
- this.instanceTable.assignOffsets( 0 ); // BLAS at offset 0
486
- this._buildGlobalOriginalToBvhMap();
487
- entry.originalToBvhMap = null;
488
- entry.bvhData = null;
489
-
490
- } else {
491
-
492
- // Multi-mesh — build TLAS over mesh AABBs
493
- this.instanceTable.computeAABBs( this.triangleData );
494
- const { root: tlasRoot, nodeCount: tlasNodeCount } = this.tlasBuilder.build( validEntries );
479
+ // Always build a TLAS even for a single mesh — so the BLAS-pointer leaf
480
+ // carries packed per-mesh visibility in its slot [2]. The 1-node TLAS
481
+ // overhead (one extra leaf fetch per ray) is negligible and eliminates
482
+ // a dedicated visibility storage buffer binding.
483
+ this.instanceTable.computeAABBs( this.triangleData );
484
+ const { root: tlasRoot, nodeCount: tlasNodeCount } = this.tlasBuilder.build( validEntries );
495
485
 
496
- this.instanceTable.assignOffsets( tlasNodeCount );
497
- const totalNodes = this.instanceTable.totalNodeCount;
486
+ this.instanceTable.assignOffsets( tlasNodeCount );
487
+ const totalNodes = this.instanceTable.totalNodeCount;
498
488
 
499
- const tlasData = this.tlasBuilder.flatten( tlasRoot, validEntries );
489
+ const tlasData = this.tlasBuilder.flatten( tlasRoot, validEntries );
500
490
 
501
- // Assemble combined buffer: [TLAS][BLAS_0][BLAS_1]...[BLAS_M]
502
- this.bvhData = new Float32Array( totalNodes * 16 );
503
- this.bvhData.set( tlasData );
491
+ // Assemble combined buffer: [TLAS][BLAS_0][BLAS_1]...[BLAS_M]
492
+ this.bvhData = new Float32Array( totalNodes * 16 );
493
+ this.bvhData.set( tlasData );
504
494
 
505
- for ( const entry of validEntries ) {
495
+ for ( const entry of validEntries ) {
506
496
 
507
- const destOffset = entry.blasOffset * 16;
508
- this.bvhData.set( entry.bvhData, destOffset );
509
- this._offsetBLASInPlace( destOffset, entry.bvhData.length / 16, entry.blasOffset, entry.triOffset );
497
+ const destOffset = entry.blasOffset * 16;
498
+ this.bvhData.set( entry.bvhData, destOffset );
499
+ this._offsetBLASInPlace( destOffset, entry.bvhData.length / 16, entry.blasOffset, entry.triOffset );
510
500
 
511
- }
512
-
513
- this._buildGlobalOriginalToBvhMap();
501
+ }
514
502
 
515
- for ( const entry of validEntries ) {
503
+ this._buildGlobalOriginalToBvhMap();
516
504
 
517
- entry.originalToBvhMap = null;
518
- entry.bvhData = null;
505
+ for ( const entry of validEntries ) {
519
506
 
520
- }
507
+ entry.originalToBvhMap = null;
508
+ entry.bvhData = null;
521
509
 
522
510
  }
523
511
 
@@ -1384,6 +1372,7 @@ export class SceneProcessor {
1384
1372
  }
1385
1373
 
1386
1374
  pathTracer.setBVHData( this.bvhData );
1375
+ pathTracer.setInstanceTable( this.instanceTable );
1387
1376
 
1388
1377
  if ( this.materialData ) {
1389
1378
 
@@ -16,7 +16,6 @@ import { Fn, texture, vec2, float, int, uniform, If,
16
16
  import { TextureNode } from 'three/webgpu';
17
17
  import { LinearFilter, DataArrayTexture } from 'three';
18
18
  import { pathTracerMain } from '../TSL/PathTracer.js';
19
- import { setMeshVisibilityBuffer } from '../TSL/BVHTraversal.js';
20
19
  import { setShadowAlbedoMaps, setAlphaShadowsUniform } from '../TSL/LightsDirect.js';
21
20
  import { BuildTimer } from './BuildTimer.js';
22
21
 
@@ -234,11 +233,9 @@ export class ShaderBuilder {
234
233
  const triStorage = stage.triangleStorageNode;
235
234
  const bvhStorage = stage.bvhStorageNode;
236
235
  const matStorage = stage.materialData.materialStorageNode;
237
- const emissiveTriStorage = stage.emissiveTriangleStorageNode;
238
- const lightBVHStorage = stage.lightBVHStorageNode;
239
-
240
- // Set per-mesh visibility buffer (module-level in BVHTraversal.js, read during graph construction)
241
- setMeshVisibilityBuffer( stage.meshVisibilityStorageNode );
236
+ // Packed light buffer — [lightBVH | emissive triangles]. One node fed to both
237
+ // TSL params; emissive reads offset by stage.emissiveVec4Offset.
238
+ const lightBufferStorage = stage.lightStorageNode;
242
239
 
243
240
  // Set alpha-shadow uniform (module-level in LightsDirect.js, read at runtime)
244
241
  setAlphaShadowsUniform( stage.uniforms.get( 'enableAlphaShadows' ) );
@@ -249,9 +246,9 @@ export class ShaderBuilder {
249
246
  const adaptiveSamplingTex = new TextureNode();
250
247
  this.adaptiveSamplingTexNode = adaptiveSamplingTex;
251
248
 
252
- // Environment importance sampling CDF (storage buffers)
253
- const marginalCDFStorage = stage.environment.envMarginalStorageNode;
254
- const conditionalCDFStorage = stage.environment.envConditionalStorageNode;
249
+ // Environment importance sampling CDF — packed storage buffer
250
+ // Layout: [marginal (envResolution.y floats) | conditional (envResolution.x * envResolution.y floats)]
251
+ const envCDFStorage = stage.environment.envCDFStorageNode;
255
252
 
256
253
  // Previous-frame texture nodes — initialized from readTarget textures
257
254
  const readTextures = storageTextures.getReadTextures();
@@ -285,8 +282,8 @@ export class ShaderBuilder {
285
282
  setShadowAlbedoMaps( albedoMapsTex );
286
283
 
287
284
  const result = {
288
- triStorage, bvhStorage, matStorage, emissiveTriStorage, lightBVHStorage,
289
- envTex, adaptiveSamplingTex, marginalCDFStorage, conditionalCDFStorage,
285
+ triStorage, bvhStorage, matStorage, lightBufferStorage,
286
+ envTex, adaptiveSamplingTex, envCDFStorage,
290
287
  albedoMapsTex, normalMapsTex, bumpMapsTex,
291
288
  metalnessMapsTex, roughnessMapsTex, emissiveMapsTex, displacementMapsTex,
292
289
  };
@@ -304,8 +301,8 @@ export class ShaderBuilder {
304
301
  writeColorTex, writeNDTex, writeAlbedoTex ) {
305
302
 
306
303
  const {
307
- triStorage, bvhStorage, matStorage, emissiveTriStorage, lightBVHStorage,
308
- envTex, adaptiveSamplingTex, marginalCDFStorage, conditionalCDFStorage,
304
+ triStorage, bvhStorage, matStorage, lightBufferStorage,
305
+ envTex, adaptiveSamplingTex, envCDFStorage,
309
306
  albedoMapsTex, normalMapsTex, bumpMapsTex,
310
307
  metalnessMapsTex, roughnessMapsTex, emissiveMapsTex, displacementMapsTex,
311
308
  } = textureNodes;
@@ -366,8 +363,7 @@ export class ShaderBuilder {
366
363
  envTexture: envTex,
367
364
  environmentIntensity: stage.environmentIntensity,
368
365
  envMatrix: stage.environmentMatrix,
369
- envMarginalWeights: marginalCDFStorage,
370
- envConditionalWeights: conditionalCDFStorage,
366
+ envCDFBuffer: envCDFStorage,
371
367
  envTotalSum: stage.envTotalSum,
372
368
  envResolution: stage.envResolution,
373
369
  enableEnvironmentLight: stage.enableEnvironment,
@@ -381,11 +377,12 @@ export class ShaderBuilder {
381
377
  globalIlluminationIntensity: stage.globalIlluminationIntensity,
382
378
  totalTriangleCount: stage.totalTriangleCount,
383
379
  enableEmissiveTriangleSampling: stage.enableEmissiveTriangleSampling,
384
- emissiveTriangleBuffer: emissiveTriStorage,
380
+ emissiveTriangleBuffer: lightBufferStorage,
385
381
  emissiveTriangleCount: stage.emissiveTriangleCount,
386
382
  emissiveTotalPower: stage.emissiveTotalPower,
387
383
  emissiveBoost: stage.emissiveBoost,
388
- lightBVHBuffer: lightBVHStorage,
384
+ emissiveVec4Offset: stage.emissiveVec4Offset,
385
+ lightBVHBuffer: lightBufferStorage,
389
386
  lightBVHNodeCount: stage.lightBVHNodeCount,
390
387
  debugVisScale: stage.debugVisScale,
391
388
  enableAccumulation: stage.enableAccumulation,
@@ -199,10 +199,13 @@ export class TLASBuilder {
199
199
  /**
200
200
  * Flatten TLAS tree into Float32Array.
201
201
  * Inner nodes: same format as BVH.
202
- * Leaf nodes: [blasRootNodeIndex, 0, 0, -2] (BLAS-pointer marker).
202
+ * Leaf nodes: [blasRootNodeIndex, meshIndex, visibility, -2] (BLAS-pointer marker).
203
+ *
204
+ * Side effect: records each entry's flat leaf index on `entry.tlasLeafIndex` so that
205
+ * visibility can later be patched in place (combinedBvhData[tlasLeafIndex*16 + 2]).
203
206
  *
204
207
  * @param {TLASNode} root
205
- * @param {Array<{blasOffset: number}>} entries - Instance table entries with assigned blasOffsets
208
+ * @param {Array<{blasOffset: number, visible: boolean, tlasLeafIndex: number}>} entries
206
209
  * @returns {Float32Array}
207
210
  */
208
211
  flatten( root, entries ) {
@@ -268,10 +271,12 @@ export class TLASBuilder {
268
271
  // Leaf node — BLAS pointer
269
272
  const entry = entries[ n.entryIndex ];
270
273
  data[ o ] = entry.blasOffset; // Absolute node index of BLAS root in combined buffer
271
- data[ o + 1 ] = n.entryIndex; // meshIndex for per-mesh visibility check
272
- // data[o+2] = 0
274
+ data[ o + 1 ] = n.entryIndex; // meshIndex (kept for debug/ID traversal uses slot [2])
275
+ data[ o + 2 ] = entry.visible === false ? 0.0 : 1.0; // Per-mesh visibility (packed — frees a binding)
273
276
  data[ o + 3 ] = BVH_LEAF_MARKERS.BLAS_POINTER_LEAF; // -2 marker
274
277
 
278
+ entry.tlasLeafIndex = i;
279
+
275
280
  }
276
281
 
277
282
  }
@@ -180,17 +180,20 @@ export class PathTracer extends RenderStage {
180
180
  // Blue noise
181
181
  this.blueNoiseTexture = null;
182
182
 
183
- // Emissive triangles (storage buffer)initialized with dummy data so TSL compilation never sees null
184
- this.emissiveTriangleStorageAttr = new StorageInstancedBufferAttribute( new Float32Array( 4 ), 4 );
185
- this.emissiveTriangleStorageNode = storage( this.emissiveTriangleStorageAttr, 'vec4', 1 ).toReadOnly();
183
+ // Packed light buffer — [lightBVH nodes (4 vec4s each) | emissive triangles (2 vec4s each)]
184
+ // emissiveVec4Offset uniform tracks the vec4-count offset where emissive data starts.
185
+ // Initialized with dummy data so TSL compilation never sees null.
186
+ this.lightStorageAttr = new StorageInstancedBufferAttribute( new Float32Array( 16 ), 4 );
187
+ this.lightStorageNode = storage( this.lightStorageAttr, 'vec4', 1 ).toReadOnly();
186
188
 
187
- // Light BVH storage buffer initialized with dummy data
188
- this.lightBVHStorageAttr = new StorageInstancedBufferAttribute( new Float32Array( 16 ), 4 );
189
- this.lightBVHStorageNode = storage( this.lightBVHStorageAttr, 'vec4', 1 ).toReadOnly();
189
+ // Cached CPU-side data — rebuilt into the packed buffer whenever either source changes.
190
+ this._lbvhDataCache = null;
191
+ this._emissiveDataCache = null;
190
192
 
191
- // Per-mesh visibility (storage buffer for TLAS BLAS-pointer skip)
192
- this.meshVisibilityStorageAttr = new StorageInstancedBufferAttribute( new Float32Array( [ 1, 0, 0, 0 ] ), 4 );
193
- this.meshVisibilityStorageNode = storage( this.meshVisibilityStorageAttr, 'vec4', 1 ).toReadOnly();
193
+ // Per-mesh visibility is packed into the TLAS BLAS-pointer leaf's slot [2]
194
+ // (see TLASBuilder.flatten + BVHTraversal.js). The InstanceTable holds the
195
+ // tlasLeafIndex for each mesh so we can patch visibility in place.
196
+ this._instanceTable = null;
194
197
 
195
198
  // Adaptive sampling
196
199
  this.adaptiveSamplingTexture = null;
@@ -454,6 +457,7 @@ export class PathTracer extends RenderStage {
454
457
  // Set data references
455
458
  this.setTriangleData( this.sdfs.triangleData, this.sdfs.triangleCount );
456
459
  this.setBVHData( this.sdfs.bvhData );
460
+ this.setInstanceTable( this.sdfs.instanceTable );
457
461
  this.materialData.setMaterialData( this.sdfs.materialData );
458
462
 
459
463
  // Update triangle count
@@ -768,61 +772,78 @@ export class PathTracer extends RenderStage {
768
772
  }
769
773
 
770
774
  /**
771
- * Build per-mesh visibility storage buffer from mesh world-visibility.
772
- * Each mesh gets one float (1.0 = visible, 0.0 = hidden).
773
- * Padded to vec4 alignment for GPU storage buffer compatibility.
774
- * @param {Array} meshes - Array of Three.js mesh objects
775
+ * Bind the InstanceTable used to locate each mesh's TLAS leaf for in-place
776
+ * visibility patching. Called by SceneProcessor during upload.
777
+ * @param {import('../Processor/InstanceTable.js').InstanceTable} instanceTable
775
778
  */
776
- setMeshVisibilityData( meshes ) {
779
+ setInstanceTable( instanceTable ) {
780
+
781
+ this._instanceTable = instanceTable;
782
+
783
+ }
777
784
 
778
- if ( ! meshes || meshes.length === 0 ) return;
785
+ /**
786
+ * Initialize packed visibility for each mesh from current world-visibility.
787
+ * Patches the TLAS leaf slots in the combined BVH buffer that was just uploaded.
788
+ * @param {Array} meshes - Array of Three.js mesh objects, ordered by meshIndex
789
+ */
790
+ setMeshVisibilityData( meshes ) {
779
791
 
780
- const meshCount = meshes.length;
781
- // One vec4 per mesh — visibility stored in .x (simple indexing on GPU)
782
- const data = new Float32Array( meshCount * 4 );
792
+ if ( ! meshes || meshes.length === 0 || ! this._instanceTable ) return;
783
793
 
784
- for ( let i = 0; i < meshCount; i ++ ) {
794
+ for ( let i = 0; i < meshes.length; i ++ ) {
785
795
 
786
- data[ i * 4 ] = this._isWorldVisible( meshes[ i ] ) ? 1.0 : 0.0;
796
+ this._patchTLASLeafVisibility( i, this._isWorldVisible( meshes[ i ] ) );
787
797
 
788
798
  }
789
799
 
790
- this.meshVisibilityStorageAttr = new StorageInstancedBufferAttribute( data, 4 );
791
- this.meshVisibilityStorageNode.value = this.meshVisibilityStorageAttr;
792
- this.meshVisibilityStorageNode.bufferCount = meshCount;
800
+ if ( this.bvhStorageAttr ) this.bvhStorageAttr.needsUpdate = true;
793
801
 
794
802
  }
795
803
 
796
804
  /**
797
- * Update visibility for a single mesh in the GPU buffer (no rebuild).
805
+ * Update visibility for a single mesh by patching its TLAS leaf slot [2].
798
806
  * @param {number} meshIndex
799
807
  * @param {boolean} visible
800
808
  */
801
809
  updateMeshVisibility( meshIndex, visible ) {
802
810
 
803
- if ( ! this.meshVisibilityStorageAttr ) return;
804
-
805
- this.meshVisibilityStorageAttr.array[ meshIndex * 4 ] = visible ? 1.0 : 0.0;
806
- this.meshVisibilityStorageAttr.needsUpdate = true;
811
+ if ( ! this._patchTLASLeafVisibility( meshIndex, visible ) ) return;
812
+ if ( this.bvhStorageAttr ) this.bvhStorageAttr.needsUpdate = true;
807
813
 
808
814
  }
809
815
 
810
816
  /**
811
- * Recompute world-visibility for all meshes and update the GPU buffer.
817
+ * Recompute world-visibility for all meshes and patch TLAS leaves in place.
812
818
  * Call this when group visibility changes at runtime.
813
819
  */
814
820
  updateAllMeshVisibility() {
815
821
 
816
- if ( ! this._meshRefs || ! this.meshVisibilityStorageAttr ) return;
822
+ if ( ! this._meshRefs || ! this._instanceTable ) return;
817
823
 
818
- const data = this.meshVisibilityStorageAttr.array;
819
824
  for ( let i = 0; i < this._meshRefs.length; i ++ ) {
820
825
 
821
- data[ i * 4 ] = this._isWorldVisible( this._meshRefs[ i ] ) ? 1.0 : 0.0;
826
+ this._patchTLASLeafVisibility( i, this._isWorldVisible( this._meshRefs[ i ] ) );
822
827
 
823
828
  }
824
829
 
825
- this.meshVisibilityStorageAttr.needsUpdate = true;
830
+ if ( this.bvhStorageAttr ) this.bvhStorageAttr.needsUpdate = true;
831
+
832
+ }
833
+
834
+ /**
835
+ * Patch a single TLAS leaf's visibility flag in the combined BVH buffer.
836
+ * Returns true if the patch was applied.
837
+ * @private
838
+ */
839
+ _patchTLASLeafVisibility( meshIndex, visible ) {
840
+
841
+ const entry = this._instanceTable?.entries?.[ meshIndex ];
842
+ if ( ! entry || entry.tlasLeafIndex < 0 || ! this.bvhStorageAttr ) return false;
843
+
844
+ entry.visible = visible;
845
+ this.bvhStorageAttr.array[ entry.tlasLeafIndex * 16 + 2 ] = visible ? 1.0 : 0.0;
846
+ return true;
826
847
 
827
848
  }
828
849
 
@@ -1493,18 +1514,43 @@ export class PathTracer extends RenderStage {
1493
1514
 
1494
1515
  }
1495
1516
 
1496
- setEmissiveTriangleData( emissiveData, count, totalPower = 0 ) {
1517
+ /**
1518
+ * Rebuild the packed light buffer from cached lightBVH + emissive data.
1519
+ * Layout: [ lightBVH (LBVH_STRIDE vec4s per node) | emissive (EMISSIVE_STRIDE vec4s per entry) ].
1520
+ * Also updates `emissiveVec4Offset` uniform (in vec4 elements).
1521
+ * @private
1522
+ */
1523
+ _rebuildLightBuffer() {
1497
1524
 
1498
- if ( ! emissiveData ) return;
1525
+ const LBVH_STRIDE = 4; // vec4s per LBVH node — must match LightBVHSampling.js
1526
+ const lbvh = this._lbvhDataCache;
1527
+ const emis = this._emissiveDataCache;
1528
+ const lbvhLen = lbvh ? lbvh.length : 0;
1529
+ const emisLen = emis ? emis.length : 0;
1499
1530
 
1500
- const vec4Count = emissiveData.length / 4;
1531
+ // Ensure at least a minimal non-empty buffer so GPU allocation remains valid.
1532
+ const totalLen = Math.max( lbvhLen + emisLen, 4 );
1533
+ const combined = new Float32Array( totalLen );
1534
+ if ( lbvh ) combined.set( lbvh, 0 );
1535
+ if ( emis ) combined.set( emis, lbvhLen );
1501
1536
 
1502
- this.emissiveTriangleStorageAttr = new StorageInstancedBufferAttribute( emissiveData, 4 );
1503
- this.emissiveTriangleStorageNode.value = this.emissiveTriangleStorageAttr;
1504
- this.emissiveTriangleStorageNode.bufferCount = vec4Count;
1537
+ this.lightStorageAttr = new StorageInstancedBufferAttribute( combined, 4 );
1538
+ this.lightStorageNode.value = this.lightStorageAttr;
1539
+ this.lightStorageNode.bufferCount = combined.length / 4;
1540
+
1541
+ // Offset (in vec4 elements) where emissive data starts.
1542
+ this.emissiveVec4Offset.value = ( this.lightBVHNodeCount.value || 0 ) * LBVH_STRIDE;
1543
+
1544
+ }
1545
+
1546
+ setEmissiveTriangleData( emissiveData, count, totalPower = 0 ) {
1547
+
1548
+ if ( ! emissiveData ) return;
1505
1549
 
1550
+ this._emissiveDataCache = emissiveData;
1506
1551
  this.emissiveTriangleCount.value = count;
1507
1552
  this.emissiveTotalPower.value = totalPower;
1553
+ this._rebuildLightBuffer();
1508
1554
  console.log( `PathTracer: ${count} emissive triangles, totalPower=${totalPower.toFixed( 4 )} (storage buffer)` );
1509
1555
 
1510
1556
  }
@@ -1513,11 +1559,9 @@ export class PathTracer extends RenderStage {
1513
1559
 
1514
1560
  if ( ! nodeData ) return;
1515
1561
 
1516
- const vec4Count = nodeData.length / 4;
1517
- this.lightBVHStorageAttr = new StorageInstancedBufferAttribute( nodeData, 4 );
1518
- this.lightBVHStorageNode.value = this.lightBVHStorageAttr;
1519
- this.lightBVHStorageNode.bufferCount = vec4Count;
1562
+ this._lbvhDataCache = nodeData;
1520
1563
  this.lightBVHNodeCount.value = nodeCount;
1564
+ this._rebuildLightBuffer();
1521
1565
  console.log( `PathTracer: Light BVH ${nodeCount} nodes` );
1522
1566
 
1523
1567
  }