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,101 +1,553 @@
1
1
  /**
2
- * InstanceTable — Per-mesh BLAS metadata for the two-level BVH (TLAS/BLAS).
2
+ * InstanceTable — per-placement transforms plus per-template BLAS metadata for the two-level BVH.
3
3
  *
4
- * Tracks each mesh's BLAS location within the combined BVH buffer,
5
- * its triangle range in the global triangle buffer, world-space AABB,
6
- * and per-BLAS triangle reorder map for refit.
4
+ * Columns, not objects, and the per-template half is stored once rather than once per placement:
5
+ * millions of placements still share a few thousand geometries, so triangle ranges, node counts
6
+ * and object-space bounds live on the template. `sourceMesh` doubles as the template id.
7
+ *
8
+ * 75 bytes a placement, against 418 for the array-of-objects version and 211 for the flat but
9
+ * denormalised one. World bounds and the inverse transform are derived on demand.
7
10
  */
8
11
 
9
- import { TRIANGLE_DATA_LAYOUT } from '../EngineDefaults.js';
12
+ import { BVH_LEAF_MARKERS, TRIANGLE_DATA_LAYOUT, assertBVHIndexFits, bvhIndexView } from '../EngineDefaults.js';
13
+
14
+ const IDENTITY = Float64Array.from( [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] );
15
+
16
+ /** Determinant of the upper-left 3x3 at `off`. Negative means the transform mirrors. */
17
+ export function determinant3At( m, off ) {
18
+
19
+ return m[ off ] * ( m[ off + 5 ] * m[ off + 10 ] - m[ off + 6 ] * m[ off + 9 ] )
20
+ - m[ off + 4 ] * ( m[ off + 1 ] * m[ off + 10 ] - m[ off + 2 ] * m[ off + 9 ] )
21
+ + m[ off + 8 ] * ( m[ off + 1 ] * m[ off + 6 ] - m[ off + 2 ] * m[ off + 5 ] );
22
+
23
+ }
24
+
25
+ /** Writes the inverse of the affine column-major 4x4 at `off` into `out`. */
26
+ export function invertAffineInto( m, off, out ) {
27
+
28
+ const det = determinant3At( m, off );
29
+ if ( ! Number.isFinite( det ) || Math.abs( det ) < 1e-20 ) {
30
+
31
+ out.set( IDENTITY );
32
+ return out;
33
+
34
+ }
35
+
36
+ const s = 1 / det;
37
+
38
+ out[ 0 ] = ( m[ off + 5 ] * m[ off + 10 ] - m[ off + 6 ] * m[ off + 9 ] ) * s;
39
+ out[ 1 ] = ( m[ off + 2 ] * m[ off + 9 ] - m[ off + 1 ] * m[ off + 10 ] ) * s;
40
+ out[ 2 ] = ( m[ off + 1 ] * m[ off + 6 ] - m[ off + 2 ] * m[ off + 5 ] ) * s;
41
+ out[ 3 ] = 0;
42
+ out[ 4 ] = ( m[ off + 6 ] * m[ off + 8 ] - m[ off + 4 ] * m[ off + 10 ] ) * s;
43
+ out[ 5 ] = ( m[ off ] * m[ off + 10 ] - m[ off + 2 ] * m[ off + 8 ] ) * s;
44
+ out[ 6 ] = ( m[ off + 2 ] * m[ off + 4 ] - m[ off ] * m[ off + 6 ] ) * s;
45
+ out[ 7 ] = 0;
46
+ out[ 8 ] = ( m[ off + 4 ] * m[ off + 9 ] - m[ off + 5 ] * m[ off + 8 ] ) * s;
47
+ out[ 9 ] = ( m[ off + 1 ] * m[ off + 8 ] - m[ off ] * m[ off + 9 ] ) * s;
48
+ out[ 10 ] = ( m[ off ] * m[ off + 5 ] - m[ off + 1 ] * m[ off + 4 ] ) * s;
49
+ out[ 11 ] = 0;
50
+
51
+ out[ 12 ] = - ( out[ 0 ] * m[ off + 12 ] + out[ 4 ] * m[ off + 13 ] + out[ 8 ] * m[ off + 14 ] );
52
+ out[ 13 ] = - ( out[ 1 ] * m[ off + 12 ] + out[ 5 ] * m[ off + 13 ] + out[ 9 ] * m[ off + 14 ] );
53
+ out[ 14 ] = - ( out[ 2 ] * m[ off + 12 ] + out[ 6 ] * m[ off + 13 ] + out[ 10 ] * m[ off + 14 ] );
54
+ out[ 15 ] = 1;
55
+ return out;
56
+
57
+ }
58
+
59
+ /** `out = a * b`, column-major affine 4x4s; `b` is read at `bOff`. Matches how the extractor
60
+ * composes an InstancedMesh's host matrix with each instance matrix. */
61
+ export function multiplyAffine( a, b, bOff, out ) {
62
+
63
+ for ( let c = 0; c < 4; c ++ ) {
64
+
65
+ const o = bOff + c * 4;
66
+ const b0 = b[ o ], b1 = b[ o + 1 ], b2 = b[ o + 2 ], b3 = b[ o + 3 ];
67
+
68
+ for ( let r = 0; r < 4; r ++ ) {
69
+
70
+ out[ c * 4 + r ] = a[ r ] * b0 + a[ 4 + r ] * b1 + a[ 8 + r ] * b2 + a[ 12 + r ] * b3;
71
+
72
+ }
73
+
74
+ }
75
+
76
+ return out;
77
+
78
+ }
79
+
80
+ /** True when the matrix leaves points untouched — lets refit skip the transform entirely. */
81
+ export function isIdentity( m ) {
82
+
83
+ for ( let i = 0; i < 16; i ++ ) if ( m[ i ] !== IDENTITY[ i ] ) return false;
84
+ return true;
85
+
86
+ }
87
+
88
+ /** Axis-aligned bounds of an AABB carried through an affine transform (all 8 corners). */
89
+ export function transformAABB( aabb, m ) {
90
+
91
+ if ( ! aabb ) return aabb;
92
+
93
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
94
+ let maxX = - Infinity, maxY = - Infinity, maxZ = - Infinity;
95
+
96
+ for ( let c = 0; c < 8; c ++ ) {
97
+
98
+ const x = c & 1 ? aabb.maxX : aabb.minX;
99
+ const y = c & 2 ? aabb.maxY : aabb.minY;
100
+ const z = c & 4 ? aabb.maxZ : aabb.minZ;
101
+
102
+ const wx = m[ 0 ] * x + m[ 4 ] * y + m[ 8 ] * z + m[ 12 ];
103
+ const wy = m[ 1 ] * x + m[ 5 ] * y + m[ 9 ] * z + m[ 13 ];
104
+ const wz = m[ 2 ] * x + m[ 6 ] * y + m[ 10 ] * z + m[ 14 ];
105
+
106
+ if ( wx < minX ) minX = wx; if ( wx > maxX ) maxX = wx;
107
+ if ( wy < minY ) minY = wy; if ( wy > maxY ) maxY = wy;
108
+ if ( wz < minZ ) minZ = wz; if ( wz > maxZ ) maxZ = wz;
109
+
110
+ }
111
+
112
+ return { minX, minY, minZ, maxX, maxY, maxZ };
113
+
114
+ }
115
+
116
+ /** Writes an AABB carried through an affine transform into `dst` at `dstOff`. */
117
+ export function transformAABBInto( src, srcOff, m, mOff, dst, dstOff ) {
118
+
119
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
120
+ let maxX = - Infinity, maxY = - Infinity, maxZ = - Infinity;
121
+
122
+ for ( let c = 0; c < 8; c ++ ) {
123
+
124
+ const x = src[ srcOff + ( c & 1 ? 3 : 0 ) ];
125
+ const y = src[ srcOff + ( c & 2 ? 4 : 1 ) ];
126
+ const z = src[ srcOff + ( c & 4 ? 5 : 2 ) ];
127
+
128
+ const wx = m[ mOff ] * x + m[ mOff + 4 ] * y + m[ mOff + 8 ] * z + m[ mOff + 12 ];
129
+ const wy = m[ mOff + 1 ] * x + m[ mOff + 5 ] * y + m[ mOff + 9 ] * z + m[ mOff + 13 ];
130
+ const wz = m[ mOff + 2 ] * x + m[ mOff + 6 ] * y + m[ mOff + 10 ] * z + m[ mOff + 14 ];
131
+
132
+ if ( wx < minX ) minX = wx; if ( wx > maxX ) maxX = wx;
133
+ if ( wy < minY ) minY = wy; if ( wy > maxY ) maxY = wy;
134
+ if ( wz < minZ ) minZ = wz; if ( wz > maxZ ) maxZ = wz;
135
+
136
+ }
137
+
138
+ dst[ dstOff ] = minX; dst[ dstOff + 1 ] = minY; dst[ dstOff + 2 ] = minZ;
139
+ dst[ dstOff + 3 ] = maxX; dst[ dstOff + 4 ] = maxY; dst[ dstOff + 5 ] = maxZ;
140
+
141
+ }
142
+
143
+ /** True when the 16 floats at `off` leave points untouched — lets refit skip the transform. */
144
+ export function isIdentityAt( m, off ) {
145
+
146
+ for ( let i = 0; i < 16; i ++ ) if ( m[ off + i ] !== IDENTITY[ i ] ) return false;
147
+ return true;
148
+
149
+ }
10
150
 
11
151
  export class InstanceTable {
12
152
 
13
153
  constructor() {
14
154
 
15
- /** @type {InstanceEntry[]} */
16
- this.entries = [];
17
-
18
- /** Total BVH node count across all BLASes (excludes TLAS nodes) */
155
+ this.count = 0;
156
+ this.templateCount = 0;
19
157
  this.totalBLASNodes = 0;
20
-
21
- /** Number of TLAS nodes */
22
158
  this.tlasNodeCount = 0;
159
+ this.allocate( 0 );
23
160
 
24
161
  }
25
162
 
26
163
  /**
27
- * Pre-allocate entries array for a known mesh count.
28
- * Must be called before setEntry().
29
- * @param {number} count
164
+ * Pre-allocate the columns. Must be called before setEntry().
165
+ *
166
+ * @param {number} count - placements
167
+ * @param {number} [templateCount] - distinct source meshes; defaults to one per placement
168
+ * @param {Float32Array} [worldPool] - adopted as the transform column instead of allocating,
169
+ * so a caller that already built the matrices contiguously hands them over without a copy
170
+ * @param {Int32Array} [sourcePool] - likewise for the source-mesh column
30
171
  */
31
- allocate( count ) {
172
+ allocate( count, templateCount = count, worldPool = null, sourcePool = null ) {
173
+
174
+ this.count = count;
175
+ this.templateCount = templateCount;
176
+
177
+ this.isSet = new Uint8Array( count );
178
+ // Object3D this placement came from, and the template index for everything below.
179
+ this.sourceMesh = sourcePool && sourcePool.length >= count ? sourcePool : new Int32Array( count );
180
+ this.tlasLeafIndex = new Int32Array( count ).fill( - 1 ); // set by TLASBuilder
181
+ this.visible = new Uint8Array( count ).fill( 1 ); // baked into TLAS leaf slot [2]
182
+ // A mirroring transform reverses triangle winding, so front and back swap in object
183
+ // space. Traversal needs telling, or single-sided faces cull inside out.
184
+ this.flipWinding = new Uint8Array( count );
185
+ // template -> inverse of the pose baked into its triangles, for templates stored in
186
+ // world space. A move composes the new world matrix against this.
187
+ this.tplBakeInverse = null;
32
188
 
33
- this.entries = new Array( count ).fill( null );
189
+ if ( worldPool && worldPool.length >= count * 16 ) {
190
+
191
+ this.world = worldPool;
192
+ this.worldPooled = true;
193
+
194
+ } else {
195
+
196
+ this.world = new Float32Array( count * 16 );
197
+ this.worldPooled = false;
198
+
199
+ }
200
+
201
+ this.tplTriOffset = new Int32Array( templateCount );
202
+ this.tplTriCount = new Int32Array( templateCount );
203
+ this.tplExpandedStart = new Int32Array( templateCount );
204
+ this.tplNodeCount = new Int32Array( templateCount );
205
+ this.tplBlasOffset = new Int32Array( templateCount );
206
+ this.tplOwner = new Int32Array( templateCount ).fill( - 1 ); // placement that built the BLAS
207
+ this.tplObjectAABB = new Float32Array( templateCount * 6 );
208
+
209
+ this._placementRuns = null;
210
+
211
+ // Keyed by template, and only templates that own a BLAS carry them.
212
+ this.originalToBvhMap = new Map();
213
+ this.bvhToOriginal = new Map();
214
+ this.blasData = new Map();
215
+
216
+ }
217
+
218
+ /** Writes the placement half of an entry. @private */
219
+ _place( index, template, matrixWorld, matrixOffset ) {
220
+
221
+ this.isSet[ index ] = 1;
222
+ this.sourceMesh[ index ] = template;
223
+
224
+ const o = index * 16;
225
+
226
+ if ( ! matrixWorld ) {
227
+
228
+ this.world.set( IDENTITY, o );
229
+ this.flipWinding[ index ] = 0;
230
+ return;
231
+
232
+ }
233
+
234
+ // Already in the pool — adopting it is the whole point of passing it in.
235
+ if ( matrixWorld !== this.world ) {
236
+
237
+ for ( let k = 0; k < 16; k ++ ) this.world[ o + k ] = matrixWorld[ matrixOffset + k ];
238
+
239
+ }
240
+
241
+ this.flipWinding[ index ] = determinant3At( this.world, o ) < 0 ? 1 : 0;
34
242
 
35
243
  }
36
244
 
37
245
  /**
38
- * Set a mesh entry at a specific index (meshIndex) after its BLAS has been built.
39
- * Guarantees entries[meshIndex] maps to the correct mesh regardless of build order.
246
+ * Set a placement whose BLAS has just been built. Template metadata (triangle range, node
247
+ * count, reorder map) is recorded once against `sourceMesh`, not once per placement.
40
248
  *
41
249
  * @param {Object} params
42
- * @param {number} params.meshIndex - Index into the meshes array (also the slot index)
250
+ * @param {number} params.meshIndex - Placement slot
43
251
  * @param {number} params.blasNodeCount - Number of BVH nodes in this BLAS
44
252
  * @param {number} params.triOffset - Triangle index offset in global triangleData
45
253
  * @param {number} params.triCount - Number of triangles for this mesh
46
254
  * @param {Uint32Array} params.originalToBvhMap - Per-BLAS triangle reorder map
47
255
  * @param {Float32Array} params.bvhData - Raw BLAS BVH data (local indices, before assembly)
256
+ * @param {ArrayLike<number>} [params.matrixWorld] - object-to-world, column-major 16
257
+ * @param {number} [params.matrixOffset] - element offset into `matrixWorld`
258
+ */
259
+ setEntry( { meshIndex, blasNodeCount, triOffset, triCount, originalToBvhMap, bvhData, matrixWorld = null, matrixOffset = 0, expandedStart = null, sourceMesh = null } ) {
260
+
261
+ const t = sourceMesh ?? meshIndex;
262
+
263
+ this._place( meshIndex, t, matrixWorld, matrixOffset );
264
+
265
+ this.tplTriOffset[ t ] = triOffset;
266
+ this.tplTriCount[ t ] = triCount;
267
+ this.tplExpandedStart[ t ] = expandedStart ?? triOffset;
268
+ this.tplNodeCount[ t ] = blasNodeCount;
269
+ this.tplOwner[ t ] = meshIndex;
270
+
271
+ if ( originalToBvhMap ) this.originalToBvhMap.set( t, originalToBvhMap );
272
+ if ( bvhData ) this.blasData.set( t, bvhData );
273
+
274
+ }
275
+
276
+ /**
277
+ * Register a placement that reuses `ownerIndex`'s triangles and BLAS. Only the transform
278
+ * differs, so its template borrows the owner's node range and object-space bounds.
279
+ *
280
+ * @param {number} meshIndex
281
+ * @param {number} ownerIndex
282
+ * @param {ArrayLike<number>} [matrixWorld]
283
+ */
284
+ setAlias( meshIndex, ownerIndex, matrixWorld = null, expandedStart = null, sourceMesh = null, matrixOffset = 0 ) {
285
+
286
+ if ( ! this.isSet[ ownerIndex ] ) throw new Error( `InstanceTable.setAlias: owner ${ownerIndex} not built` );
287
+
288
+ const t = sourceMesh ?? meshIndex;
289
+ const ot = this.sourceMesh[ ownerIndex ];
290
+
291
+ this._place( meshIndex, t, matrixWorld, matrixOffset );
292
+
293
+ if ( t === ot ) return; // same template as the owner: nothing to mirror
294
+
295
+ this.tplTriOffset[ t ] = this.tplTriOffset[ ot ];
296
+ this.tplTriCount[ t ] = this.tplTriCount[ ot ];
297
+ this.tplNodeCount[ t ] = this.tplNodeCount[ ot ];
298
+ this.tplExpandedStart[ t ] = expandedStart ?? this.tplExpandedStart[ ot ];
299
+ this.tplOwner[ t ] = this.tplOwner[ ot ];
300
+
301
+ }
302
+
303
+ /** Placements actually built. */
304
+ get setCount() {
305
+
306
+ let n = 0;
307
+ for ( let i = 0; i < this.count; i ++ ) n += this.isSet[ i ];
308
+ return n;
309
+
310
+ }
311
+
312
+ /**
313
+ * Replace one placement's object-to-world transform. Its geometry is untouched, which is the
314
+ * whole point: a rigid move is a matrix change, and rewriting shared triangles instead would
315
+ * drag every other placement of the same geometry along with it.
316
+ */
317
+ setPlacementMatrix( index, matrixWorld, matrixOffset = 0 ) {
318
+
319
+ this._place( index, this.sourceMesh[ index ], matrixWorld, matrixOffset );
320
+
321
+ }
322
+
323
+ /**
324
+ * The contiguous run of placements recorded for `meshIndex`, or null. One object contributes
325
+ * one placement, or one per instance for an InstancedMesh, and the extractor emits them in
326
+ * mesh order — so the run is contiguous and worth indexing once rather than scanning per move.
327
+ *
328
+ * @returns {{start: number, count: number}|null}
329
+ */
330
+ placementRunOf( meshIndex ) {
331
+
332
+ if ( ! this._placementRuns ) {
333
+
334
+ const runs = this._placementRuns = new Map();
335
+ for ( let i = 0; i < this.count; i ++ ) {
336
+
337
+ const m = this.sourceMesh[ i ];
338
+ const run = runs.get( m );
339
+ if ( run ) run.count ++;
340
+ else runs.set( m, { start: i, count: 1 } );
341
+
342
+ }
343
+
344
+ }
345
+
346
+ return this._placementRuns.get( meshIndex ) ?? null;
347
+
348
+ }
349
+
350
+ /** True when this placement is the one whose BLAS the others alias. */
351
+ isOwner( index ) {
352
+
353
+ return this.tplOwner[ this.sourceMesh[ index ] ] === index;
354
+
355
+ }
356
+
357
+ triOffsetOf( index ) {
358
+
359
+ return this.tplTriOffset[ this.sourceMesh[ index ] ];
360
+
361
+ }
362
+
363
+ triCountOf( index ) {
364
+
365
+ return this.tplTriCount[ this.sourceMesh[ index ] ];
366
+
367
+ }
368
+
369
+ expandedStartOf( index ) {
370
+
371
+ return this.tplExpandedStart[ this.sourceMesh[ index ] ];
372
+
373
+ }
374
+
375
+ blasOffsetOf( index ) {
376
+
377
+ return this.tplBlasOffset[ this.sourceMesh[ index ] ];
378
+
379
+ }
380
+
381
+ blasNodeCountOf( index ) {
382
+
383
+ return this.tplNodeCount[ this.sourceMesh[ index ] ];
384
+
385
+ }
386
+
387
+ /** Record a rebuilt BLAS's node count. May shrink; never past what the build allocated. */
388
+ setBlasNodeCount( index, nodeCount ) {
389
+
390
+ this.tplNodeCount[ this.sourceMesh[ index ] ] = nodeCount;
391
+
392
+ }
393
+
394
+ /** Record the stored-order to caller-order map for a rebuilt BLAS. */
395
+ setBvhToOriginal( index, map ) {
396
+
397
+ this.bvhToOriginal.set( this.sourceMesh[ index ], map );
398
+
399
+ }
400
+
401
+ /**
402
+ * The per-BLAS triangle reorder map this placement traverses through. A placement that
403
+ * borrows another's BLAS borrows its map too — only the owner's template carries one.
404
+ */
405
+ bvhToOriginalOf( index ) {
406
+
407
+ const t = this.sourceMesh[ index ];
408
+ const own = this.bvhToOriginal.get( t );
409
+ if ( own ) return own;
410
+
411
+ const owner = this.tplOwner[ t ];
412
+ return ( owner >= 0 ? this.bvhToOriginal.get( this.sourceMesh[ owner ] ) : null ) || null;
413
+
414
+ }
415
+
416
+ /** A view of the 16 object-to-world floats for `index`. Cold paths only. */
417
+ matrixWorldOf( index ) {
418
+
419
+ return this.world.subarray( index * 16, index * 16 + 16 );
420
+
421
+ }
422
+
423
+ /**
424
+ * The 16 world-to-object floats for `index`, inverted on demand. Allocates — per-mesh work
425
+ * only; the TLAS build inverts into its own scratch.
48
426
  */
49
- setEntry( { meshIndex, blasNodeCount, triOffset, triCount, originalToBvhMap, bvhData } ) {
50
-
51
- this.entries[ meshIndex ] = {
52
- meshIndex,
53
- blasOffset: 0, // Set during assembly
54
- blasNodeCount,
55
- triOffset,
56
- triCount,
57
- worldAABB: null, // Computed from triangle data
58
- originalToBvhMap,
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
427
+ matrixInverseOf( index ) {
428
+
429
+ return invertAffineInto( this.world, index * 16, new Float64Array( 16 ) );
430
+
431
+ }
432
+
433
+ /** Writes this placement's world-space bounds into `out` at `off`. */
434
+ writeWorldAABB( index, out, off ) {
435
+
436
+ if ( ! this.isSet[ index ] ) {
437
+
438
+ for ( let k = 0; k < 6; k ++ ) out[ off + k ] = 0;
439
+ return;
440
+
441
+ }
442
+
443
+ transformAABBInto( this.tplObjectAABB, this.sourceMesh[ index ] * 6, this.world, index * 16, out, off );
444
+
445
+ }
446
+
447
+ /**
448
+ * Every placement's world-space bounds, 6 floats each — what the TLAS sorts on.
449
+ * @param {Float64Array|Float32Array} out
450
+ */
451
+ writeWorldAABBs( out ) {
452
+
453
+ const aabb = this.tplObjectAABB, world = this.world, src = this.sourceMesh, set = this.isSet;
454
+
455
+ for ( let i = 0; i < this.count; i ++ ) {
456
+
457
+ const o = i * 6;
458
+
459
+ if ( ! set[ i ] ) {
460
+
461
+ out[ o ] = 0; out[ o + 1 ] = 0; out[ o + 2 ] = 0;
462
+ out[ o + 3 ] = 0; out[ o + 4 ] = 0; out[ o + 5 ] = 0;
463
+ continue;
464
+
465
+ }
466
+
467
+ transformAABBInto( aabb, src[ i ] * 6, world, i * 16, out, o );
468
+
469
+ }
470
+
471
+ }
472
+
473
+ /**
474
+ * A plain snapshot of one placement, shaped like the old entry object.
475
+ *
476
+ * This allocates, so it is for per-mesh work (refit of a handful of meshes) only — never a
477
+ * loop over every placement, which is the cost this class exists to avoid.
478
+ *
479
+ * @param {number} i
480
+ * @returns {object|null}
481
+ */
482
+ entryAt( i ) {
483
+
484
+ if ( ! this.isSet[ i ] ) return null;
485
+
486
+ const t = this.sourceMesh[ i ];
487
+ const owner = this.tplOwner[ t ];
488
+
489
+ return {
490
+ meshIndex: i,
491
+ blasOffset: this.tplBlasOffset[ t ],
492
+ blasNodeCount: this.tplNodeCount[ t ],
493
+ triOffset: this.tplTriOffset[ t ],
494
+ triCount: this.tplTriCount[ t ],
495
+ expandedStart: this.tplExpandedStart[ t ],
496
+ sourceMesh: t,
497
+ sharedFrom: owner === i ? null : owner,
498
+ matrixWorld: this.matrixWorldOf( i ),
499
+ matrixInverse: this.matrixInverseOf( i ),
500
+ flipWinding: !! this.flipWinding[ i ],
501
+ bvhToOriginal: this.bvhToOriginal.get( t ) || null,
502
+ visible: !! this.visible[ i ],
503
+ tlasLeafIndex: this.tlasLeafIndex[ i ],
62
504
  };
63
505
 
64
506
  }
65
507
 
66
508
  /**
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.
509
+ * Set per-mesh visibility flag. Does NOT update the GPU buffer — caller must patch
510
+ * combinedBvhData[tlasLeafIndex*16 + 2] and mark the bvh attr dirty.
69
511
  *
70
512
  * @param {number} meshIndex
71
513
  * @param {boolean} visible
72
514
  */
73
515
  setVisibility( meshIndex, visible ) {
74
516
 
75
- const entry = this.entries[ meshIndex ];
76
- if ( entry ) entry.visible = visible;
517
+ if ( this.isSet[ meshIndex ] ) this.visible[ meshIndex ] = visible ? 1 : 0;
77
518
 
78
519
  }
79
520
 
80
521
  /**
81
- * Compute world-space AABBs for all entries from their BLAS root node data.
82
- * O(1) per mesh for inner roots; falls back to triangle scan for leaf roots (rare).
522
+ * Compute object-space AABBs for every template from its BLAS root node.
523
+ * O(1) per template for inner roots; falls back to a triangle scan for leaf roots (rare).
83
524
  *
84
525
  * @param {Float32Array} triangleData - Global triangle data (needed for leaf-root fallback)
85
526
  */
86
527
  computeAABBs( triangleData ) {
87
528
 
88
- for ( const entry of this.entries ) {
529
+ for ( let t = 0; t < this.templateCount; t ++ ) {
530
+
531
+ if ( this.tplOwner[ t ] < 0 ) continue;
532
+ const blas = this.blasData.get( t );
533
+ if ( blas ) this._readRootAABB( blas, t, triangleData, this.tplObjectAABB, t * 6 );
534
+
535
+ }
536
+
537
+ // Templates aliasing another's BLAS share its object bounds; only the transform differs.
538
+ for ( let t = 0; t < this.templateCount; t ++ ) {
89
539
 
90
- if ( ! entry ) continue;
91
- entry.worldAABB = this._readRootAABB( entry.bvhData, entry, triangleData );
540
+ const owner = this.tplOwner[ t ];
541
+ if ( owner < 0 || this.blasData.has( t ) ) continue;
542
+ const ot = this.sourceMesh[ owner ];
543
+ if ( ot !== t ) this.tplObjectAABB.copyWithin( t * 6, ot * 6, ot * 6 + 6 );
92
544
 
93
545
  }
94
546
 
95
547
  }
96
548
 
97
549
  /**
98
- * Recompute AABB for a single entry after BLAS refit.
550
+ * Recompute a placement's template bounds after its BLAS was refit.
99
551
  * Reads from the combined bvhData buffer at the BLAS root offset.
100
552
  *
101
553
  * @param {number} entryIndex
@@ -104,43 +556,39 @@ export class InstanceTable {
104
556
  */
105
557
  recomputeAABB( entryIndex, combinedBvhData, triangleData ) {
106
558
 
107
- const entry = this.entries[ entryIndex ];
108
- const rootData = combinedBvhData.subarray( entry.blasOffset * 16, entry.blasOffset * 16 + 16 );
109
- entry.worldAABB = this._readRootAABB( rootData, entry, triangleData );
559
+ const t = this.sourceMesh[ entryIndex ];
560
+ const n = this.tplBlasOffset[ t ];
561
+ // Flat buffer or ChunkedRecords: either way, take that one node's 16 floats.
562
+ const node = combinedBvhData.chunks
563
+ ? combinedBvhData.chunkFor( n ).subarray( combinedBvhData.baseOf( n ), combinedBvhData.baseOf( n ) + 16 )
564
+ : combinedBvhData.subarray( n * 16, n * 16 + 16 );
565
+ this._readRootAABB( node, t, triangleData, this.tplObjectAABB, t * 6 );
110
566
 
111
567
  }
112
568
 
113
569
  /**
114
- * Read the root node's AABB from a flat BVH data array.
570
+ * Read the root node's AABB from a flat BVH data array into `out` at `off`.
115
571
  * Inner root: union of left+right child AABBs (O(1)).
116
- * Leaf root: scan triangles (rare — only meshes with ≤maxLeafSize tris).
572
+ * Leaf root: scan triangles (rare — only meshes with <= maxLeafSize tris).
117
573
  * @private
118
574
  */
119
- _readRootAABB( bvhData, entry, triangleData ) {
575
+ _readRootAABB( bvhData, template, triangleData, out, off ) {
120
576
 
121
- const marker = bvhData[ 3 ];
122
-
123
- if ( marker === - 1 ) {
577
+ if ( ! bvhData || bvhIndexView( bvhData )[ 3 ] === BVH_LEAF_MARKERS.TRIANGLE_LEAF ) {
124
578
 
125
579
  // Root is a leaf — very small mesh. Scan its triangles.
126
- return this._computeAABBFromTriangles( entry, triangleData );
580
+ this._computeAABBFromTriangles( template, triangleData, out, off );
581
+ return;
127
582
 
128
583
  }
129
584
 
130
585
  // Inner node: [leftMin.xyz, leftChild] [leftMax.xyz, rightChild] [rightMin.xyz, 0] [rightMax.xyz, 0]
131
- const lMinX = bvhData[ 0 ], lMinY = bvhData[ 1 ], lMinZ = bvhData[ 2 ];
132
- const lMaxX = bvhData[ 4 ], lMaxY = bvhData[ 5 ], lMaxZ = bvhData[ 6 ];
133
- const rMinX = bvhData[ 8 ], rMinY = bvhData[ 9 ], rMinZ = bvhData[ 10 ];
134
- const rMaxX = bvhData[ 12 ], rMaxY = bvhData[ 13 ], rMaxZ = bvhData[ 14 ];
135
-
136
- return {
137
- minX: Math.min( lMinX, rMinX ),
138
- minY: Math.min( lMinY, rMinY ),
139
- minZ: Math.min( lMinZ, rMinZ ),
140
- maxX: Math.max( lMaxX, rMaxX ),
141
- maxY: Math.max( lMaxY, rMaxY ),
142
- maxZ: Math.max( lMaxZ, rMaxZ ),
143
- };
586
+ out[ off ] = Math.min( bvhData[ 0 ], bvhData[ 8 ] );
587
+ out[ off + 1 ] = Math.min( bvhData[ 1 ], bvhData[ 9 ] );
588
+ out[ off + 2 ] = Math.min( bvhData[ 2 ], bvhData[ 10 ] );
589
+ out[ off + 3 ] = Math.max( bvhData[ 4 ], bvhData[ 12 ] );
590
+ out[ off + 4 ] = Math.max( bvhData[ 5 ], bvhData[ 13 ] );
591
+ out[ off + 5 ] = Math.max( bvhData[ 6 ], bvhData[ 14 ] );
144
592
 
145
593
  }
146
594
 
@@ -148,22 +596,28 @@ export class InstanceTable {
148
596
  * Compute AABB by scanning triangle positions (fallback for leaf-root BLASes).
149
597
  * @private
150
598
  */
151
- _computeAABBFromTriangles( entry, triangleData ) {
599
+ _computeAABBFromTriangles( template, triangleData, out, off ) {
152
600
 
153
601
  const FPT = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
602
+ // Flat array or ChunkedRecords: resolve the chunk once per triangle either way.
603
+ const chunked = triangleData && triangleData.chunks ? triangleData.viewAs( Float32Array ) : null;
604
+ const flat = chunked ? null : ( triangleData instanceof Float32Array
605
+ ? triangleData
606
+ : new Float32Array( triangleData.buffer, triangleData.byteOffset, triangleData.length ) );
154
607
  let minX = Infinity, minY = Infinity, minZ = Infinity;
155
608
  let maxX = - Infinity, maxY = - Infinity, maxZ = - Infinity;
156
609
 
157
- for ( let t = 0; t < entry.triCount; t ++ ) {
610
+ const triOffset = this.tplTriOffset[ template ], triCount = this.tplTriCount[ template ];
611
+ for ( let t = 0; t < triCount; t ++ ) {
158
612
 
159
- const base = ( entry.triOffset + t ) * FPT;
613
+ const gi = triOffset + t;
614
+ const tri = chunked ? chunked.chunkFor( gi ) : flat;
615
+ const base = chunked ? chunked.baseOf( gi ) : gi * FPT;
160
616
 
161
- // Check positions A (offset 0), B (offset 4), C (offset 8)
162
- for ( let off = 0; off <= 8; off += 4 ) {
617
+ // Positions A (offset 0), B (offset 4), C (offset 8)
618
+ for ( let o = 0; o <= 8; o += 4 ) {
163
619
 
164
- const x = triangleData[ base + off ];
165
- const y = triangleData[ base + off + 1 ];
166
- const z = triangleData[ base + off + 2 ];
620
+ const x = tri[ base + o ], y = tri[ base + o + 1 ], z = tri[ base + o + 2 ];
167
621
  if ( x < minX ) minX = x;
168
622
  if ( y < minY ) minY = y;
169
623
  if ( z < minZ ) minZ = z;
@@ -175,14 +629,13 @@ export class InstanceTable {
175
629
 
176
630
  }
177
631
 
178
- return { minX, minY, minZ, maxX, maxY, maxZ };
632
+ out[ off ] = minX; out[ off + 1 ] = minY; out[ off + 2 ] = minZ;
633
+ out[ off + 3 ] = maxX; out[ off + 4 ] = maxY; out[ off + 5 ] = maxZ;
179
634
 
180
635
  }
181
636
 
182
637
  /**
183
- * Assign BLAS offsets in the combined BVH buffer.
184
- * Called after TLAS node count is known.
185
- *
638
+ * Assign BLAS offsets in the combined BVH buffer. Called once the TLAS node count is known.
186
639
  * @param {number} tlasNodeCount - Number of nodes in the TLAS
187
640
  */
188
641
  assignOffsets( tlasNodeCount ) {
@@ -190,42 +643,40 @@ export class InstanceTable {
190
643
  this.tlasNodeCount = tlasNodeCount;
191
644
  let offset = tlasNodeCount;
192
645
 
193
- for ( const entry of this.entries ) {
646
+ for ( let t = 0; t < this.templateCount; t ++ ) {
194
647
 
195
- if ( ! entry ) continue;
196
- entry.blasOffset = offset;
197
- offset += entry.blasNodeCount;
648
+ const owner = this.tplOwner[ t ];
649
+ if ( owner < 0 || this.sourceMesh[ owner ] !== t ) continue;
650
+ this.tplBlasOffset[ t ] = offset;
651
+ offset += this.tplNodeCount[ t ];
198
652
 
199
653
  }
200
654
 
201
- this.totalBLASNodes = offset - tlasNodeCount;
655
+ for ( let t = 0; t < this.templateCount; t ++ ) {
202
656
 
203
- }
657
+ const owner = this.tplOwner[ t ];
658
+ if ( owner < 0 ) continue;
659
+ const ot = this.sourceMesh[ owner ];
660
+ if ( ot !== t ) this.tplBlasOffset[ t ] = this.tplBlasOffset[ ot ];
204
661
 
205
- /**
206
- * Total node count (TLAS + all BLASes).
207
- */
208
- get totalNodeCount() {
662
+ }
209
663
 
210
- return this.tlasNodeCount + this.totalBLASNodes;
664
+ this.totalBLASNodes = offset - tlasNodeCount;
665
+ assertBVHIndexFits( offset, 'combined TLAS + BLAS node count' );
211
666
 
212
667
  }
213
668
 
214
- /**
215
- * Number of mesh instances.
216
- */
217
- get count() {
669
+ /** Total node count (TLAS + all BLASes). */
670
+ get totalNodeCount() {
218
671
 
219
- return this.entries.length;
672
+ return this.tlasNodeCount + this.totalBLASNodes;
220
673
 
221
674
  }
222
675
 
223
- /**
224
- * Reset all entries.
225
- */
676
+ /** Reset all columns. */
226
677
  clear() {
227
678
 
228
- this.entries = [];
679
+ this.allocate( 0 );
229
680
  this.totalBLASNodes = 0;
230
681
  this.tlasNodeCount = 0;
231
682