rayzee 7.31.1 → 8.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 (51) hide show
  1. package/README.md +164 -0
  2. package/dist/rayzee.es.js +6366 -4361
  3. package/dist/rayzee.umd.js +122 -73
  4. package/package.json +1 -1
  5. package/src/EngineDefaults.js +168 -13
  6. package/src/EngineIssues.js +5 -0
  7. package/src/PathTracerApp.js +120 -19
  8. package/src/Processor/ArchiveReader.js +538 -0
  9. package/src/Processor/AssetLoader.js +265 -23
  10. package/src/Processor/BVHBuilder.js +48 -32
  11. package/src/Processor/BVHRefitter.js +244 -99
  12. package/src/Processor/ChunkedRecords.js +302 -0
  13. package/src/Processor/EmissiveTriangleBuilder.js +48 -21
  14. package/src/Processor/GeometryExtractor.js +577 -92
  15. package/src/Processor/HostMemory.js +222 -0
  16. package/src/Processor/InstanceTable.js +548 -97
  17. package/src/Processor/PBRT/EqualAreaOctahedral.js +32 -8
  18. package/src/Processor/PBRT/PBRTCurves.js +249 -0
  19. package/src/Processor/PBRT/PBRTMath.js +19 -0
  20. package/src/Processor/PBRT/PBRTParser.js +187 -41
  21. package/src/Processor/PBRT/PBRTSceneBuilder.js +586 -42
  22. package/src/Processor/PBRT/PBRTTokenizer.js +205 -65
  23. package/src/Processor/PBRT/index.js +257 -50
  24. package/src/Processor/PackedRayBuffer.js +29 -18
  25. package/src/Processor/ParallelBVHBuilder.js +9 -8
  26. package/src/Processor/QueueManager.js +5 -3
  27. package/src/Processor/SceneProcessor.js +1046 -365
  28. package/src/Processor/TLASBuilder.js +546 -201
  29. package/src/Processor/TextureCreator.js +11 -9
  30. package/src/Processor/VRAMTracker.js +12 -3
  31. package/src/Processor/Workers/BVHRefitWorker.js +16 -11
  32. package/src/Processor/Workers/BVHWorker.js +12 -12
  33. package/src/Processor/Workers/TLASWorker.js +37 -0
  34. package/src/Stages/NormalDepth.js +6 -2
  35. package/src/Stages/PathTracer.js +5 -5
  36. package/src/Stages/PathTracerStage.js +163 -41
  37. package/src/TSL/BVHTraversal.js +208 -90
  38. package/src/TSL/Common.js +135 -2
  39. package/src/TSL/Displacement.js +21 -11
  40. package/src/TSL/EmissiveSampling.js +49 -25
  41. package/src/TSL/ExtendKernel.js +3 -2
  42. package/src/TSL/LightBVHSampling.js +7 -4
  43. package/src/TSL/LightsDirect.js +30 -29
  44. package/src/TSL/ShadeKernel.js +8 -6
  45. package/src/TSL/Struct.js +3 -0
  46. package/src/TSL/TextureSampling.js +24 -9
  47. package/src/TSL/patches.js +128 -3
  48. package/src/index.js +3 -0
  49. package/src/managers/AnimationManager.js +86 -76
  50. package/src/managers/MaterialDataManager.js +29 -23
  51. package/src/managers/TransformManager.js +9 -192
@@ -1,5 +1,5 @@
1
1
  import { DataArrayTexture, RGBAFormat, LinearFilter, UnsignedByteType, SRGBColorSpace, LinearSRGBColorSpace, RepeatWrapping } from "three";
2
- import { alignBucketWidth, TEXTURE_CONSTANTS, MEMORY_CONSTANTS, DEFAULT_TEXTURE_MATRIX, MATERIAL_DATA_LAYOUT, normalizeAttenuationDistance } from '../EngineDefaults.js';
2
+ import { alignBucketWidth, TEXTURE_CONSTANTS, MEMORY_CONSTANTS, DEFAULT_TEXTURE_MATRIX, MATERIAL_DATA_LAYOUT, normalizeAttenuationDistance, BVH_LEAF_MARKERS, assertBVHIndexFits, bvhIndexView } from '../EngineDefaults.js';
3
3
  import TexturesWorker from './Workers/TexturesWorker.js?worker&inline';
4
4
  import { ISSUE_CODES } from '../EngineIssues.js';
5
5
  import { linearToSRGB } from './ToneMapCPU.js';
@@ -1148,12 +1148,15 @@ export class TextureCreator {
1148
1148
 
1149
1149
  flattenBVH( bvhRoot );
1150
1150
 
1151
- // Layout: 4 vec4 per node (16 floats)
1151
+ // Layout: 4 vec4 per node (16 floats). Bounds are floats; every index field is a u32
1152
+ // bit pattern (see EngineDefaults.BVH_LEAF_MARKERS).
1152
1153
  // Inner: [leftMin.xyz, leftChild] [leftMax.xyz, rightChild] [rightMin.xyz, 0] [rightMax.xyz, 0]
1153
- // Leaf: [triOffset, triCount, 0, -1] [0,0,0,0] [0,0,0,0] [0,0,0,0]
1154
+ // Leaf: [triOffset, triCount, 0, TRIANGLE_LEAF] [0,0,0,0] [0,0,0,0] [0,0,0,0]
1154
1155
  const floatsPerNode = TEXTURE_CONSTANTS.VEC4_PER_BVH_NODE * TEXTURE_CONSTANTS.FLOATS_PER_VEC4;
1156
+ assertBVHIndexFits( nodes.length, 'BVH node count' );
1155
1157
  const size = nodes.length * floatsPerNode;
1156
1158
  const data = new Float32Array( size );
1159
+ const idx = bvhIndexView( data ); // index fields are u32 bit patterns
1157
1160
 
1158
1161
  for ( let i = 0; i < nodes.length; i ++ ) {
1159
1162
 
@@ -1171,12 +1174,12 @@ export class TextureCreator {
1171
1174
  data[ stride ] = left.boundsMin.x;
1172
1175
  data[ stride + 1 ] = left.boundsMin.y;
1173
1176
  data[ stride + 2 ] = left.boundsMin.z;
1174
- data[ stride + 3 ] = leftIdx;
1177
+ idx[ stride + 3 ] = leftIdx;
1175
1178
 
1176
1179
  data[ stride + 4 ] = left.boundsMax.x;
1177
1180
  data[ stride + 5 ] = left.boundsMax.y;
1178
1181
  data[ stride + 6 ] = left.boundsMax.z;
1179
- data[ stride + 7 ] = rightIdx;
1182
+ idx[ stride + 7 ] = rightIdx;
1180
1183
 
1181
1184
  data[ stride + 8 ] = right.boundsMin.x;
1182
1185
  data[ stride + 9 ] = right.boundsMin.y;
@@ -1188,10 +1191,9 @@ export class TextureCreator {
1188
1191
 
1189
1192
  } else {
1190
1193
 
1191
- // Leaf node
1192
- data[ stride ] = node.triangleOffset;
1193
- data[ stride + 1 ] = node.triangleCount;
1194
- data[ stride + 3 ] = - 1; // Leaf marker
1194
+ idx[ stride ] = node.triangleOffset;
1195
+ idx[ stride + 1 ] = node.triangleCount;
1196
+ idx[ stride + 3 ] = BVH_LEAF_MARKERS.TRIANGLE_LEAF;
1195
1197
 
1196
1198
  }
1197
1199
 
@@ -32,10 +32,10 @@ function texelBytes( tex ) {
32
32
 
33
33
  }
34
34
 
35
- /** Exact byte size of a storage/buffer attribute's backing typed array. */
35
+ /** Exact GPU byte size of a buffer attribute. GPU-only ones carry no backing array. */
36
36
  export function bufferBytes( attr ) {
37
37
 
38
- return attr?.array?.byteLength || 0;
38
+ return attr?.gpuByteLength ?? attr?.array?.byteLength ?? 0;
39
39
 
40
40
  }
41
41
 
@@ -141,7 +141,16 @@ export class VRAMTracker {
141
141
  // synthetic { bytes } (e.g. attributeArray-backed histograms)
142
142
  if ( typeof r.bytes === 'number' && ! r.isTexture && ! r.isRenderTarget ) return r.bytes;
143
143
 
144
- // buffer attribute — dedupe by backing array (rw/ro nodes share one buffer)
144
+ // buffer attribute — dedupe by backing array, or by the attribute itself when it is
145
+ // GPU-only and has none (rw/ro nodes share one buffer either way)
146
+ if ( r.gpuByteLength != null ) {
147
+
148
+ if ( seen.has( r ) ) return 0;
149
+ seen.add( r );
150
+ return r.gpuByteLength;
151
+
152
+ }
153
+
145
154
  if ( r.array && r.array.byteLength != null ) {
146
155
 
147
156
  if ( seen.has( r.array ) ) return 0;
@@ -2,20 +2,21 @@
2
2
  * BVHRefitWorker — Off-main-thread BVH refit using SharedArrayBuffer.
3
3
  *
4
4
  * Protocol:
5
- * 'init' → receives SharedArrayBuffers + index map (once per scene)
6
- * 'refit' → reads shared positions, writes shared bvh/tri data (per frame)
5
+ * 'init' → receives the shared triangle and node buffers (once per scene)
6
+ * 'refit' → recomputes every node's AABB from the triangles already written there
7
+ *
8
+ * Positions never reach here: the main thread scatters each mesh into the shared triangle
9
+ * records as it reads it, so no buffer the size of the scene exists on either side.
7
10
  */
8
11
 
9
12
  import { BVHRefitter } from '../BVHRefitter.js';
13
+ import { ChunkedRecords } from '../ChunkedRecords.js';
10
14
 
11
- const FLOATS_PER_NODE = 16;
12
15
  const refitter = new BVHRefitter();
13
16
 
14
17
  // Cached shared memory views (set once on 'init', reused every frame)
15
18
  let bvhData = null;
16
19
  let triData = null;
17
- let posData = null;
18
- let bvhToOriginal = null;
19
20
  let nodeCount = 0;
20
21
 
21
22
  self.onmessage = function ( e ) {
@@ -24,11 +25,16 @@ self.onmessage = function ( e ) {
24
25
 
25
26
  if ( type === 'init' ) {
26
27
 
27
- bvhData = new Float32Array( e.data.sharedBvhBuf );
28
- triData = new Float32Array( e.data.sharedTriBuf );
29
- posData = new Float32Array( e.data.sharedPosBuf );
30
- bvhToOriginal = e.data.bvhToOriginal; // transferred Uint32Array
31
- nodeCount = bvhData.length / FLOATS_PER_NODE;
28
+ bvhData = ChunkedRecords.adopt(
29
+ e.data.sharedBvhBufs.map( buf => new Float32Array( buf ) ),
30
+ e.data.bvhRecordCount, 16, e.data.bvhRecordsPerChunk
31
+ );
32
+ // Triangles arrive as one shared buffer per chunk; a single-chunk scene is the usual case.
33
+ triData = ChunkedRecords.adopt(
34
+ e.data.sharedTriBufs.map( buf => new Uint32Array( buf ) ),
35
+ e.data.triRecordCount, e.data.triLanesPerRecord, e.data.triRecordsPerChunk
36
+ );
37
+ nodeCount = bvhData.recordCount;
32
38
  return;
33
39
 
34
40
  }
@@ -39,7 +45,6 @@ self.onmessage = function ( e ) {
39
45
 
40
46
  const startTime = performance.now();
41
47
 
42
- refitter.updateTrianglePositions( triData, posData, bvhToOriginal );
43
48
  refitter.refit( bvhData, triData, nodeCount );
44
49
 
45
50
  self.postMessage( {
@@ -3,7 +3,7 @@ import { createLogger, fmt, applyWorkerLogLevel } from '../../utils/Logger.js';
3
3
 
4
4
  const log = createLogger( 'bvh' );
5
5
 
6
- const FPT = 32; // FLOATS_PER_TRIANGLE
6
+ const FPT = 20; // FLOATS_PER_TRIANGLE — the record is uint lanes, not floats
7
7
 
8
8
  // --- Message dispatcher ---
9
9
 
@@ -60,7 +60,7 @@ function handlePhase1( data ) {
60
60
  } : null;
61
61
 
62
62
  // Attach shared buffer views
63
- builder.triangles = new Float32Array( sharedTriangleData );
63
+ builder.triangles = new Uint32Array( sharedTriangleData );
64
64
  builder.centroids = new Float32Array( sharedCentroids );
65
65
  builder.bMin = new Float32Array( sharedBMin );
66
66
  builder.bMax = new Float32Array( sharedBMax );
@@ -148,8 +148,8 @@ function handleAssemble( data ) {
148
148
 
149
149
  // Reorder triangles using final indices from SharedArrayBuffer
150
150
  const indices = new Uint32Array( sharedIndices );
151
- const src = new Float32Array( sharedTriangleData );
152
- const dst = new Float32Array( sharedReorderBuffer );
151
+ const src = new Uint32Array( sharedTriangleData );
152
+ const dst = new Uint32Array( sharedReorderBuffer );
153
153
 
154
154
  for ( let i = 0; i < triangleCount; i ++ ) {
155
155
 
@@ -219,11 +219,11 @@ function handleFullBuild( data ) {
219
219
  } : null;
220
220
 
221
221
  const inputTriangles = triangleByteOffset !== undefined
222
- ? new Float32Array( triangleData, triangleByteOffset, triangleByteLength / 4 )
223
- : new Float32Array( triangleData );
222
+ ? new Uint32Array( triangleData, triangleByteOffset, triangleByteLength / 4 )
223
+ : new Uint32Array( triangleData );
224
224
 
225
225
  const reorderTarget = sharedReorderBuffer
226
- ? new Float32Array( sharedReorderBuffer )
226
+ ? new Uint32Array( sharedReorderBuffer )
227
227
  : null;
228
228
 
229
229
  const bvhRoot = builder.buildSync( inputTriangles, depth, progressCallback, reorderTarget );
@@ -243,21 +243,21 @@ function handleFullBuild( data ) {
243
243
  self.postMessage( {
244
244
  bvhData,
245
245
  originalToBvh,
246
- triangleCount: inputTriangles.length / 32,
246
+ triangleCount: inputTriangles.length / FPT,
247
247
  treeletStats: builder.splitStats
248
248
  }, transferables );
249
249
 
250
250
  } else {
251
251
 
252
- const reorderedFloat32Array = builder.reorderedTriangleData;
253
- const triangleCount = reorderedFloat32Array.byteLength / ( 32 * 4 );
252
+ const reordered = builder.reorderedTriangleData;
253
+ const triangleCount = reordered.byteLength / ( FPT * 4 );
254
254
 
255
- const transferables = [ bvhData.buffer, reorderedFloat32Array.buffer ];
255
+ const transferables = [ bvhData.buffer, reordered.buffer ];
256
256
  if ( originalToBvh ) transferables.push( originalToBvh.buffer );
257
257
 
258
258
  self.postMessage( {
259
259
  bvhData,
260
- triangles: reorderedFloat32Array,
260
+ triangles: reordered,
261
261
  originalToBvh,
262
262
  triangleCount,
263
263
  treeletStats: builder.splitStats
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Builds the TLAS off the main thread.
3
+ *
4
+ * Only the SAH tree runs here — it is the O(n log n) half, and at 6M placements it pinned the
5
+ * main thread for 25 s. The leaf payloads (BLAS pointer, visibility, world-to-object matrix)
6
+ * are filled in by the caller afterwards: that pass is O(n) and cheap, and keeping it out of
7
+ * the message avoids shipping a 16-float matrix per instance across the boundary.
8
+ */
9
+
10
+ import { TLASBuilder } from '../TLASBuilder.js';
11
+ import { createLogger, fmt, applyWorkerLogLevel } from '../../utils/Logger.js';
12
+
13
+ const log = createLogger( 'bvh' );
14
+
15
+ self.onmessage = function ( e ) {
16
+
17
+ const { aabbs, count, logLevel } = e.data;
18
+ applyWorkerLogLevel( logLevel );
19
+
20
+ try {
21
+
22
+ const start = performance.now();
23
+ // A fresh builder per message sizes its buffer exactly, so the result's ArrayBuffer can
24
+ // be transferred whole rather than copied out of a reused, oversized one.
25
+ const { data, nodeCount } = new TLASBuilder().buildStructure( new Float64Array( aabbs ), count );
26
+ log.debug( `TLAS ${fmt.n( nodeCount )} nodes in ${fmt.ms( performance.now() - start )}` );
27
+
28
+ self.postMessage( { tlasData: data, nodeCount }, [ data.buffer ] );
29
+
30
+ } catch ( error ) {
31
+
32
+ log.error( 'TLAS build failed:', error );
33
+ self.postMessage( { error: error.message } );
34
+
35
+ }
36
+
37
+ };
@@ -167,8 +167,9 @@ export class NormalDepth extends RenderStage {
167
167
 
168
168
  if ( pt.triangleStorageAttr && ! this._triStorageNode ) {
169
169
 
170
+ // uvec4, matching PathTracerStage: packed lanes must keep their exact bit pattern.
170
171
  this._triStorageNode = storage(
171
- pt.triangleStorageAttr, 'vec4', pt.triangleStorageAttr.count
172
+ pt.triangleStorageAttr, 'uvec4', pt.triangleStorageAttr.count
172
173
  ).toReadOnly();
173
174
 
174
175
  }
@@ -270,7 +271,10 @@ export class NormalDepth extends RenderStage {
270
271
  const uvTangent = vec4( 0.0 ).toVar();
271
272
  If( material.normalMapIndex.greaterThanEqual( int( 0 ) ), () => {
272
273
 
273
- uvTangent.assign( triangleUVTangent( triStorage, hit.triangleIndex, hit.normal, material.normalTransform ) );
274
+ uvTangent.assign( triangleUVTangent(
275
+ triStorage, hit.triangleIndex, hit.normal, material.normalTransform,
276
+ bvhStorage, hit.instanceLeaf
277
+ ) );
274
278
 
275
279
  } );
276
280
 
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { uniform, texture, storage } from 'three/tsl';
8
- import { StorageInstancedBufferAttribute } from 'three/webgpu';
8
+ import { gpuOnlyStorageAttribute } from '../TSL/patches.js';
9
9
  import { PathTracerStage } from './PathTracerStage.js';
10
10
  import { PackedRayBuffer, GBUFFER_STRIDE, RAY_STRIDE, HIT_STRIDE, freeStorageAttribute } from '../Processor/PackedRayBuffer.js';
11
11
  import { QueueManager, COUNTER, ENERGY_SCALE } from '../Processor/QueueManager.js';
@@ -993,7 +993,7 @@ export class PathTracer extends PathTracerStage {
993
993
  // canonicalize; u32 stores the bits verbatim.
994
994
  const gBufferVec4s = B * GBUFFER_STRIDE;
995
995
  freeStorageAttribute( this.renderer, this._gBufferAttr );
996
- this._gBufferAttr = new StorageInstancedBufferAttribute( new Uint32Array( gBufferVec4s * 4 ), 4 );
996
+ this._gBufferAttr = gpuOnlyStorageAttribute( gBufferVec4s, 4, Uint32Array );
997
997
  const gBufferRW = storage( this._gBufferAttr, 'uvec4' );
998
998
  const gBufferRO = storage( this._gBufferAttr, 'uvec4' ).toReadOnly();
999
999
 
@@ -1009,15 +1009,15 @@ export class PathTracer extends PathTracerStage {
1009
1009
 
1010
1010
  // Tier-1 convergence: per-pixel running mean of luminance² (Welford second moment), read+written by FinalWrite.
1011
1011
  freeStorageAttribute( this.renderer, this._m2Attr );
1012
- this._m2Attr = new StorageInstancedBufferAttribute( new Float32Array( maxPixels ), 1 );
1012
+ this._m2Attr = gpuOnlyStorageAttribute( maxPixels, 1 );
1013
1013
 
1014
1014
  // Tier-2: per-pixel freeze-candidate streak. FinalWrite writes (RW), buildActivePixels reads (RO).
1015
1015
  freeStorageAttribute( this.renderer, this._streakAttr );
1016
- this._streakAttr = new StorageInstancedBufferAttribute( new Uint32Array( maxPixels ), 1 );
1016
+ this._streakAttr = gpuOnlyStorageAttribute( maxPixels, 1, Uint32Array );
1017
1017
 
1018
1018
  // Tier-2: dilated frozen mask (1 = skip). buildActivePixels writes; active-list + FinalWrite read → race-free.
1019
1019
  freeStorageAttribute( this.renderer, this._frozenMaskAttr );
1020
- this._frozenMaskAttr = new StorageInstancedBufferAttribute( new Uint32Array( maxPixels ), 1 );
1020
+ this._frozenMaskAttr = gpuOnlyStorageAttribute( maxPixels, 1, Uint32Array );
1021
1021
 
1022
1022
  }
1023
1023
 
@@ -1,4 +1,5 @@
1
1
  import { storage } from 'three/tsl';
2
+ import { gpuOnlyStorageAttribute, uploadStorageChunkRange, uploadStorageChunks } from '../TSL/patches.js';
2
3
  import { StorageInstancedBufferAttribute } from 'three/webgpu';
3
4
  import {
4
5
  NearestFilter, Vector2, Matrix4,
@@ -109,17 +110,21 @@ export class PathTracerStage extends RenderStage {
109
110
  // Initialize material data manager
110
111
  this.materialData = new MaterialDataManager( this.sdfs );
111
112
  this.materialData.callbacks.onReset = () => this.reset();
112
- // Triangle data carries the per-triangle `side` flag (NORMAL_C.w). The
113
- // authoritative CPU array is triangleStorageAttr.array (not sdfs.triangleData,
114
- // which isn't populated on the PathTracerApp build path). The patch mutates
115
- // the array in place — only a dirty flag is needed for GPU re-upload.
113
+ // Triangle data carries the per-triangle `side` flag (NORMAL_C.w). The authoritative
114
+ // CPU copy is `_triangleRecords` when the scene is chunked and triangleStorageAttr.array
115
+ // otherwise (not sdfs.triangleData, which isn't populated on the PathTracerApp build
116
+ // path). The patch mutates it in place, then the GPU copy is refreshed.
116
117
  this.materialData.callbacks.getTriangleData = () => ( {
117
118
  array: this.triangleStorageAttr?.array,
119
+ records: this._triangleRecords,
118
120
  count: this.triangleCount,
119
121
  } );
120
122
  this.materialData.callbacks.onTriangleDataChanged = () => {
121
123
 
122
- if ( this.triangleStorageAttr ) this.triangleStorageAttr.needsUpdate = true;
124
+ if ( ! this.triangleStorageAttr ) return;
125
+ // A chunked attribute owns no CPU array for three.js to re-upload.
126
+ if ( this._triangleRecords ) uploadStorageChunks( this.renderer, this.triangleStorageAttr, this._triangleRecords.chunks );
127
+ else this.triangleStorageAttr.needsUpdate = true;
123
128
 
124
129
  };
125
130
 
@@ -171,11 +176,13 @@ export class PathTracerStage extends RenderStage {
171
176
 
172
177
  // Triangle data (storage buffer for WebGPU)
173
178
  this.triangleStorageAttr = null;
179
+ this._triangleRecords = null;
174
180
  this.triangleStorageNode = null;
175
181
  this.triangleCount = 0;
176
182
 
177
183
  // BVH data (storage buffer for WebGPU)
178
184
  this.bvhStorageAttr = null;
185
+ this._bvhRecords = null;
179
186
  this.bvhStorageNode = null;
180
187
  this.bvhNodeCount = 0;
181
188
 
@@ -704,19 +711,28 @@ export class PathTracerStage extends RenderStage {
704
711
  * On first call, creates the storage buffer and node.
705
712
  * On subsequent calls, creates a new attribute with the correct size
706
713
  * and updates the storage node's value to preserve shader graph references.
707
- * @param {Float32Array} triangleData - Raw triangle data
714
+ * @param {Uint32Array} triangleData - Packed triangle records (uvec4 lanes)
708
715
  * @param {number} triangleCount - Number of triangles
709
716
  */
710
717
  setTriangleData( triangleData, triangleCount ) {
711
718
 
712
719
  if ( ! triangleData ) return;
713
720
 
714
- const vec4Count = triangleData.length / 4;
721
+ // Past the ~2 GB array cap the records arrive as several chunks; they still become one
722
+ // GPU buffer, written at their running byte offsets, so no binding or shader changes.
723
+ const chunked = triangleData.chunks && triangleData.chunks.length > 1 ? triangleData : null;
724
+ const flat = chunked ? null : ( triangleData.chunks ? triangleData.chunks[ 0 ] : triangleData );
725
+ const lanes = chunked ? chunked.recordCount * chunked.lanesPerRecord : flat.length;
726
+ const vec4Count = lanes / 4;
727
+
728
+ const makeAttr = () => chunked
729
+ ? gpuOnlyStorageAttribute( vec4Count, 4, Uint32Array )
730
+ : new StorageInstancedBufferAttribute( flat, 4 );
715
731
 
716
732
  if ( this.triangleStorageNode ) {
717
733
 
718
734
  // Create new attribute with correct size (old one is GC'd, backend WeakMap cleans up GPU buffer)
719
- this.triangleStorageAttr = new StorageInstancedBufferAttribute( triangleData, 4 );
735
+ this.triangleStorageAttr = makeAttr();
720
736
 
721
737
  // Update storage node references (preserves compiled shader graph)
722
738
  this.triangleStorageNode.value = this.triangleStorageAttr;
@@ -725,11 +741,14 @@ export class PathTracerStage extends RenderStage {
725
741
  } else {
726
742
 
727
743
  // First time: create storage buffer and node
728
- this.triangleStorageAttr = new StorageInstancedBufferAttribute( triangleData, 4 );
729
- this.triangleStorageNode = storage( this.triangleStorageAttr, 'vec4', vec4Count ).toReadOnly();
744
+ this.triangleStorageAttr = makeAttr();
745
+ this.triangleStorageNode = storage( this.triangleStorageAttr, 'uvec4', vec4Count ).toReadOnly();
730
746
 
731
747
  }
732
748
 
749
+ this._triangleRecords = chunked;
750
+ if ( chunked ) uploadStorageChunks( this.renderer, this.triangleStorageAttr, chunked.chunks );
751
+
733
752
  this.triangleCount = triangleCount;
734
753
 
735
754
  log.debug( `${fmt.n( this.triangleCount )} triangles (storage buffer)` );
@@ -744,21 +763,33 @@ export class PathTracerStage extends RenderStage {
744
763
 
745
764
  if ( ! bvhImageData ) return;
746
765
 
747
- const vec4Count = bvhImageData.length / 4;
766
+ // Same story as the triangles: past the ~2 GB array cap the nodes arrive as several
767
+ // chunks and still become one GPU buffer.
768
+ const chunked = bvhImageData.chunks && bvhImageData.chunks.length > 1 ? bvhImageData : null;
769
+ const flat = chunked ? null : ( bvhImageData.chunks ? bvhImageData.chunks[ 0 ] : bvhImageData );
770
+ const lanes = chunked ? chunked.recordCount * chunked.lanesPerRecord : flat.length;
771
+ const vec4Count = lanes / 4;
772
+
773
+ const makeAttr = () => chunked
774
+ ? gpuOnlyStorageAttribute( vec4Count, 4, Float32Array )
775
+ : new StorageInstancedBufferAttribute( flat, 4 );
748
776
 
749
777
  if ( this.bvhStorageNode ) {
750
778
 
751
- this.bvhStorageAttr = new StorageInstancedBufferAttribute( bvhImageData, 4 );
779
+ this.bvhStorageAttr = makeAttr();
752
780
  this.bvhStorageNode.value = this.bvhStorageAttr;
753
781
  this.bvhStorageNode.bufferCount = vec4Count;
754
782
 
755
783
  } else {
756
784
 
757
- this.bvhStorageAttr = new StorageInstancedBufferAttribute( bvhImageData, 4 );
785
+ this.bvhStorageAttr = makeAttr();
758
786
  this.bvhStorageNode = storage( this.bvhStorageAttr, 'vec4', vec4Count ).toReadOnly();
759
787
 
760
788
  }
761
789
 
790
+ this._bvhRecords = chunked;
791
+ if ( chunked ) uploadStorageChunks( this.renderer, this.bvhStorageAttr, chunked.chunks );
792
+
762
793
  this.bvhNodeCount = Math.floor( vec4Count / BVH_VEC4_PER_NODE );
763
794
  log.debug( `${fmt.n( this.bvhNodeCount )} BVH nodes (storage buffer)` );
764
795
 
@@ -784,13 +815,31 @@ export class PathTracerStage extends RenderStage {
784
815
 
785
816
  if ( ! meshes || meshes.length === 0 || ! this._instanceTable ) return;
786
817
 
787
- for ( let i = 0; i < meshes.length; i ++ ) {
818
+ this._patchVisibilityFromMeshes( meshes );
819
+ this._flushBVHEdits();
788
820
 
789
- this._patchTLASLeafVisibility( i, this._isWorldVisible( meshes[ i ] ) );
821
+ }
790
822
 
791
- }
823
+ /**
824
+ * Resolve each placement's visibility from the object it came from. Entries are per
825
+ * instance, so an InstancedMesh covers a whole run of them with one authored flag.
826
+ * @private
827
+ */
828
+ _patchVisibilityFromMeshes( meshes ) {
792
829
 
793
- if ( this.bvhStorageAttr ) this.bvhStorageAttr.needsUpdate = true;
830
+ const table = this._instanceTable;
831
+ const cache = new Map();
832
+
833
+ for ( let i = 0; i < table.count; i ++ ) {
834
+
835
+ if ( ! table.isSet[ i ] ) continue;
836
+
837
+ const src = table.sourceMesh[ i ];
838
+ let visible = cache.get( src );
839
+ if ( visible === undefined ) cache.set( src, visible = this._isWorldVisible( meshes[ src ] ) );
840
+ this._patchTLASLeafVisibility( i, visible );
841
+
842
+ }
794
843
 
795
844
  }
796
845
 
@@ -802,7 +851,7 @@ export class PathTracerStage extends RenderStage {
802
851
  updateMeshVisibility( meshIndex, visible ) {
803
852
 
804
853
  if ( ! this._patchTLASLeafVisibility( meshIndex, visible ) ) return;
805
- if ( this.bvhStorageAttr ) this.bvhStorageAttr.needsUpdate = true;
854
+ this._flushBVHEdits();
806
855
 
807
856
  }
808
857
 
@@ -814,13 +863,8 @@ export class PathTracerStage extends RenderStage {
814
863
 
815
864
  if ( ! this._meshRefs || ! this._instanceTable ) return;
816
865
 
817
- for ( let i = 0; i < this._meshRefs.length; i ++ ) {
818
-
819
- this._patchTLASLeafVisibility( i, this._isWorldVisible( this._meshRefs[ i ] ) );
820
-
821
- }
822
-
823
- if ( this.bvhStorageAttr ) this.bvhStorageAttr.needsUpdate = true;
866
+ this._patchVisibilityFromMeshes( this._meshRefs );
867
+ this._flushBVHEdits();
824
868
 
825
869
  }
826
870
 
@@ -831,15 +875,51 @@ export class PathTracerStage extends RenderStage {
831
875
  */
832
876
  _patchTLASLeafVisibility( meshIndex, visible ) {
833
877
 
834
- const entry = this._instanceTable?.entries?.[ meshIndex ];
835
- if ( ! entry || entry.tlasLeafIndex < 0 || ! this.bvhStorageAttr ) return false;
878
+ const table = this._instanceTable;
879
+ if ( ! table || ! table.isSet?.[ meshIndex ] || ! this.bvhStorageAttr ) return false;
836
880
 
837
- entry.visible = visible;
838
- this.bvhStorageAttr.array[ entry.tlasLeafIndex * 16 + 2 ] = visible ? 1.0 : 0.0;
881
+ const leaf = table.tlasLeafIndex[ meshIndex ];
882
+ if ( leaf < 0 ) return false;
883
+
884
+ table.visible[ meshIndex ] = visible ? 1 : 0;
885
+
886
+ // A chunked attribute owns no CPU array; write into the chunk that holds this leaf.
887
+ if ( this._bvhRecords ) this._bvhRecords.chunkFor( leaf )[ this._bvhRecords.baseOf( leaf ) + 2 ] = visible ? 1.0 : 0.0;
888
+ else this.bvhStorageAttr.array[ leaf * 16 + 2 ] = visible ? 1.0 : 0.0;
889
+
890
+ this._dirtyBVHLeaves ??= new Set();
891
+ this._dirtyBVHLeaves.add( leaf );
839
892
  return true;
840
893
 
841
894
  }
842
895
 
896
+ /**
897
+ * Push BVH edits to the GPU. A chunked attribute cannot go through three.js's dirty flag,
898
+ * so the touched leaves are written straight into the buffer.
899
+ * @private
900
+ */
901
+ _flushBVHEdits() {
902
+
903
+ if ( ! this.bvhStorageAttr ) return;
904
+
905
+ if ( ! this._bvhRecords ) {
906
+
907
+ this.bvhStorageAttr.needsUpdate = true;
908
+ this._dirtyBVHLeaves?.clear();
909
+ return;
910
+
911
+ }
912
+
913
+ for ( const leaf of this._dirtyBVHLeaves ?? [] ) {
914
+
915
+ uploadStorageChunkRange( this.renderer, this.bvhStorageAttr, this._bvhRecords, leaf * 16, 16 );
916
+
917
+ }
918
+
919
+ this._dirtyBVHLeaves?.clear();
920
+
921
+ }
922
+
843
923
  /**
844
924
  * Collect mesh references from scene, ordered by meshIndex (assigned during extraction).
845
925
  * @param {Object3D} scene
@@ -903,14 +983,28 @@ export class PathTracerStage extends RenderStage {
903
983
  /** Update triangle positions in the existing GPU buffer (full). */
904
984
  updateTriangleData( triangleData ) {
905
985
 
906
- this._updateStorageBuffer( this.triangleStorageAttr, triangleData );
986
+ if ( this._triangleRecords ) {
987
+
988
+ uploadStorageChunks( this.renderer, this.triangleStorageAttr, this._triangleRecords.chunks );
989
+ return;
990
+
991
+ }
992
+
993
+ this._updateStorageBuffer( this.triangleStorageAttr, triangleData?.chunks ? triangleData.chunks[ 0 ] : triangleData );
907
994
 
908
995
  }
909
996
 
910
997
  /** Update BVH node data in the existing GPU buffer (full). */
911
998
  updateBVHData( bvhData ) {
912
999
 
913
- this._updateStorageBuffer( this.bvhStorageAttr, bvhData );
1000
+ if ( this._bvhRecords ) {
1001
+
1002
+ uploadStorageChunks( this.renderer, this.bvhStorageAttr, this._bvhRecords.chunks );
1003
+ return;
1004
+
1005
+ }
1006
+
1007
+ this._updateStorageBuffer( this.bvhStorageAttr, bvhData?.chunks ? bvhData.chunks[ 0 ] : bvhData );
914
1008
 
915
1009
  }
916
1010
 
@@ -925,29 +1019,55 @@ export class PathTracerStage extends RenderStage {
925
1019
 
926
1020
  if ( this.triangleStorageAttr && triRanges.length > 0 ) {
927
1021
 
928
- this.triangleStorageAttr.clearUpdateRanges();
1022
+ if ( this._triangleRecords ) {
929
1023
 
930
- for ( const r of triRanges ) {
1024
+ // Chunked: three.js cannot re-upload an attribute with no CPU array, so write
1025
+ // the dirty lanes straight into the GPU buffer.
1026
+ for ( const r of triRanges ) {
931
1027
 
932
- this.triangleStorageAttr.addUpdateRange( r.offset, r.count );
1028
+ uploadStorageChunkRange( this.renderer, this.triangleStorageAttr, this._triangleRecords, r.offset, r.count );
933
1029
 
934
- }
1030
+ }
1031
+
1032
+ } else {
1033
+
1034
+ this.triangleStorageAttr.clearUpdateRanges();
935
1035
 
936
- this.triangleStorageAttr.version ++;
1036
+ for ( const r of triRanges ) {
1037
+
1038
+ this.triangleStorageAttr.addUpdateRange( r.offset, r.count );
1039
+
1040
+ }
1041
+
1042
+ this.triangleStorageAttr.version ++;
1043
+
1044
+ }
937
1045
 
938
1046
  }
939
1047
 
940
1048
  if ( this.bvhStorageAttr && bvhRanges.length > 0 ) {
941
1049
 
942
- this.bvhStorageAttr.clearUpdateRanges();
1050
+ if ( this._bvhRecords ) {
943
1051
 
944
- for ( const r of bvhRanges ) {
1052
+ for ( const r of bvhRanges ) {
945
1053
 
946
- this.bvhStorageAttr.addUpdateRange( r.offset, r.count );
1054
+ uploadStorageChunkRange( this.renderer, this.bvhStorageAttr, this._bvhRecords, r.offset, r.count );
947
1055
 
948
- }
1056
+ }
949
1057
 
950
- this.bvhStorageAttr.version ++;
1058
+ } else {
1059
+
1060
+ this.bvhStorageAttr.clearUpdateRanges();
1061
+
1062
+ for ( const r of bvhRanges ) {
1063
+
1064
+ this.bvhStorageAttr.addUpdateRange( r.offset, r.count );
1065
+
1066
+ }
1067
+
1068
+ this.bvhStorageAttr.version ++;
1069
+
1070
+ }
951
1071
 
952
1072
  }
953
1073
 
@@ -1449,8 +1569,10 @@ export class PathTracerStage extends RenderStage {
1449
1569
 
1450
1570
  // Clear data references
1451
1571
  this.triangleStorageAttr = null;
1572
+ this._triangleRecords = null;
1452
1573
  this.triangleStorageNode = null;
1453
1574
  this.bvhStorageAttr = null;
1575
+ this._bvhRecords = null;
1454
1576
  this.bvhStorageNode = null;
1455
1577
  this.placeholderTexture = null;
1456
1578