rayzee 11.2.0 → 11.3.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 (63) hide show
  1. package/README.md +62 -33
  2. package/dist/ArchiveImporter-D735xX-4.js +7860 -0
  3. package/dist/BidirectionalIntegrator-M89c0AlP.js +954 -0
  4. package/dist/{BufferLayout-D_IBo2ci.js → BufferLayout-xPRH9jYD.js} +41 -5
  5. package/dist/{NeuralRetouch-DFGbnmLb.js → NeuralRetouch-DiBH63kz.js} +1 -1
  6. package/dist/{NeuralSuperRes-BtXvyJQl.js → NeuralSuperRes-CZNQP13W.js} +1 -1
  7. package/dist/{PathTracer-BHXGJtPN.js → PathTracer-BXCCVrPK.js} +4371 -4612
  8. package/dist/{PhysicalSky-DN6u8UX4.js → PhysicalSky-DqmaS0lb.js} +2 -2
  9. package/dist/{RayzeeRenderer-Dw1x8mVL.js → RayzeeRenderer-DUSMFhmc.js} +426 -363
  10. package/dist/SolarLimb-bZE6y63S.js +762 -0
  11. package/dist/addons/archives.es.js +1 -1
  12. package/dist/addons/bidirectional.es.js +1 -1
  13. package/dist/addons/physical-sky.es.js +1 -1
  14. package/dist/rayzee-core.es.js +7 -7
  15. package/dist/rayzee.es.js +472 -468
  16. package/dist/rayzee.umd.js +106 -105
  17. package/dist/sceneCachePolicy-BAib5fCp.js +1602 -0
  18. package/package.json +1 -1
  19. package/src/EngineDefaults.js +1 -1
  20. package/src/PathTracerApp.js +4 -3
  21. package/src/Processor/ArchiveImporter.js +173 -94
  22. package/src/Processor/ArchiveReader.js +20 -23
  23. package/src/Processor/AssetLoader.js +48 -1
  24. package/src/Processor/BVHRefitter.js +109 -13
  25. package/src/Processor/BufferLayout.js +115 -0
  26. package/src/Processor/ChunkedRecords.js +3 -2
  27. package/src/Processor/EmissiveTriangleBuilder.js +6 -6
  28. package/src/Processor/GeometryExtractor.js +189 -79
  29. package/src/Processor/HostMemory.js +39 -9
  30. package/src/Processor/InstanceTable.js +714 -42
  31. package/src/Processor/PBRT/PBRTCurves.js +224 -6
  32. package/src/Processor/PBRT/PBRTMaterials.js +3 -0
  33. package/src/Processor/PBRT/PBRTSceneBuilder.js +82 -25
  34. package/src/Processor/PackedRayBuffer.js +5 -2
  35. package/src/Processor/SceneProcessor.js +747 -141
  36. package/src/Processor/TLASBuilder.js +32 -17
  37. package/src/Processor/USD/USDLayer.js +1149 -0
  38. package/src/Processor/USD/USDScene.js +1655 -0
  39. package/src/Processor/USD/USDStage.js +682 -0
  40. package/src/Processor/USD/USDText.js +674 -0
  41. package/src/Processor/USD/index.js +138 -0
  42. package/src/Processor/Workers/BVHRefitWorker.js +3 -3
  43. package/src/RayzeeRenderer.js +168 -27
  44. package/src/Stages/NormalDepth.js +7 -12
  45. package/src/Stages/PathTracer.js +12 -17
  46. package/src/Stages/PathTracerStage.js +273 -96
  47. package/src/Storage/GeometrySpill.js +46 -14
  48. package/src/Storage/SceneGraphCodec.js +17 -3
  49. package/src/Storage/SpillStore.js +9 -0
  50. package/src/TSL/BVHTraversal.js +183 -7
  51. package/src/TSL/Common.js +57 -13
  52. package/src/TSL/ShadeKernel.js +3 -2
  53. package/src/TSL/TextureSampling.js +11 -8
  54. package/src/TSL/patches.js +84 -0
  55. package/src/integrators/BidirectionalIntegrator.js +20 -2
  56. package/src/managers/DenoisingManager.js +4 -4
  57. package/src/managers/InteractionManager.js +19 -6
  58. package/src/managers/MaterialDataManager.js +22 -5
  59. package/src/managers/UniformManager.js +3 -0
  60. package/dist/ArchiveImporter-eNlu0_6F.js +0 -5198
  61. package/dist/BidirectionalIntegrator-D1-6UySB.js +0 -946
  62. package/dist/SolarLimb-CPaeZVwz.js +0 -721
  63. package/dist/sceneCachePolicy-DA7iGoL4.js +0 -485
@@ -9,10 +9,17 @@
9
9
  * denormalised one. World bounds and the inverse transform are derived on demand.
10
10
  */
11
11
 
12
- import { TRIANGLE_DATA_LAYOUT, BVH_LEAF_MARKERS, assertBVHIndexFits, bvhIndexView } from './BufferLayout.js';
12
+ import {
13
+ TRIANGLE_DATA_LAYOUT, BVH_LEAF_MARKERS, BVH_EMPTY_BOX, assertBVHIndexFits, bvhIndexView,
14
+ CLUSTER_SIZE, CLUSTER_FIRST_MASK, CLUSTER_COUNT_SHIFT, CLUSTER_COPY_HIDDEN, RECORD_VEC4, TLAS_LEAF_IDENTITY,
15
+ encodeClusterBoxes as encodeClusterBoxBytes,
16
+ } from './BufferLayout.js';
13
17
 
14
18
  const IDENTITY = Float64Array.from( [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] );
15
19
 
20
+ /** Passed as a placement's matrix when its row already holds it (a pooled build). */
21
+ export const IN_PLACE = Object.freeze( [] );
22
+
16
23
  /** Determinant of the upper-left 3x3 at `off`. Negative means the transform mirrors. */
17
24
  export function determinant3At( m, off ) {
18
25
 
@@ -165,8 +172,8 @@ export class InstanceTable {
165
172
  *
166
173
  * @param {number} count - placements
167
174
  * @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
175
+ * @param {Float32Array|MatrixRuns} [worldPool] - the transforms, adopted rather than copied: one array of a row a
176
+ * placement, or runs of rows (GeometryExtractor) reading an InstancedMesh's own matrix list where it already is
170
177
  * @param {Int32Array} [sourcePool] - likewise for the source-mesh column
171
178
  */
172
179
  allocate( count, templateCount = count, worldPool = null, sourcePool = null ) {
@@ -186,17 +193,8 @@ export class InstanceTable {
186
193
  // world space. A move composes the new world matrix against this.
187
194
  this.tplBakeInverse = null;
188
195
 
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
- }
196
+ if ( worldPool?.first ) this._adoptRuns( worldPool );
197
+ else this._adoptRuns( { first: [ 0, count ], arrays: [ worldPool && worldPool.length >= count * 16 ? worldPool : new Float32Array( count * 16 ) ], arrayOf: [ 0 ], base: [ 0 ], owned: [ worldPool ? 0 : 1 ] } );
200
198
 
201
199
  this.tplTriOffset = new Int32Array( templateCount );
202
200
  this.tplTriCount = new Int32Array( templateCount );
@@ -208,6 +206,26 @@ export class InstanceTable {
208
206
 
209
207
  this._placementRuns = null;
210
208
 
209
+ // TLAS entries: one a placement, or one a copy of a group — instanced meshes placed by one shared matrix list,
210
+ // traced as one object through a small tree over their BLASes. Null maps are the identity.
211
+ this.groups = [];
212
+ this.groupOfTemplate = null;
213
+ this.memberSlot = null;
214
+ this.entryCount = count;
215
+ this.entryRep = null;
216
+ this.entryGroup = null;
217
+ this.placementEntry = null;
218
+ this.groupNodeStart = 0;
219
+ this.groupNodeCount = 0;
220
+
221
+ // Copy clusters (planClusters / formClusters): the TLAS is built over clusters of entries, and each entry's
222
+ // transform is a record behind the BLASes. Cluster c owns the CLUSTER_SIZE records from c · CLUSTER_SIZE.
223
+ this.clusterCount = 0;
224
+ this.recordEntry = null;
225
+ this.clusterLeaf = null;
226
+ this.recordNodeStart = 0;
227
+ this.recordNodeCount = 0;
228
+
211
229
  // Keyed by template, and only templates that own a BLAS carry them.
212
230
  this.originalToBvhMap = new Map();
213
231
  this.bvhToOriginal = new Map();
@@ -221,24 +239,9 @@ export class InstanceTable {
221
239
  this.isSet[ index ] = 1;
222
240
  this.sourceMesh[ index ] = template;
223
241
 
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;
242
+ if ( matrixWorld !== IN_PLACE ) this._writeRow( index, matrixWorld ?? IDENTITY, matrixWorld ? matrixOffset : 0 );
243
+ const o = this.matrixRow( index );
244
+ this.flipWinding[ index ] = determinant3At( this.rowArray, o ) < 0 ? 1 : 0;
242
245
 
243
246
  }
244
247
 
@@ -300,6 +303,364 @@ export class InstanceTable {
300
303
 
301
304
  }
302
305
 
306
+ /**
307
+ * Groups instanced meshes placed by one shared matrix list: each copy becomes one TLAS entry, whose subtree is a
308
+ * small tree over the members' BLAS roots in the copies' shared object space. Every member keeps its placements
309
+ * (transforms, visibility, refit stay per mesh); only the TLAS sees one entry where it saw one per member.
310
+ *
311
+ * @param {Array<{members: ArrayLike<number>, starts: ArrayLike<number>, count: number}>} groups - members are
312
+ * templates, `starts` the first placement of each, all `count` long
313
+ */
314
+ setGroups( groups ) {
315
+
316
+ this.groups = [];
317
+ this.groupOfTemplate = null;
318
+ this.memberSlot = null;
319
+ this.entryCount = this.count;
320
+ this.entryRep = null;
321
+ this.entryGroup = null;
322
+ this.placementEntry = null;
323
+ this.groupNodeCount = 0;
324
+
325
+ const valid = ( groups ?? [] ).filter( g => g.members.length >= 2 && Array.from( g.starts ).every( s => s >= 0 && s + g.count <= this.count ) );
326
+ if ( valid.length === 0 ) return;
327
+
328
+ const ofTemplate = new Int32Array( this.templateCount ).fill( - 1 );
329
+ const slot = new Int32Array( this.templateCount );
330
+ let nodes = 1; // the empty leaf a hidden member points at
331
+
332
+ for ( const g of valid ) {
333
+
334
+ const id = this.groups.length;
335
+ const members = Int32Array.from( g.members );
336
+ this.groups.push( { members, starts: Int32Array.from( g.starts ), count: g.count, offset: - 1, nodes: members.length - 1, aabb: new Float32Array( 6 ) } );
337
+ for ( let j = 0; j < members.length; j ++ ) {
338
+
339
+ ofTemplate[ members[ j ] ] = id;
340
+ slot[ members[ j ] ] = j;
341
+
342
+ }
343
+
344
+ nodes += members.length - 1;
345
+
346
+ }
347
+
348
+ const n = this.count, src = this.sourceMesh;
349
+ const entryOf = new Int32Array( n );
350
+ let entries = 0;
351
+ for ( let p = 0; p < n; p ++ ) {
352
+
353
+ const t = src[ p ];
354
+ if ( ofTemplate[ t ] < 0 || slot[ t ] === 0 ) entryOf[ p ] = entries ++;
355
+
356
+ }
357
+
358
+ const rep = new Int32Array( entries );
359
+ const group = new Int32Array( entries ).fill( - 1 );
360
+ for ( let p = 0; p < n; p ++ ) {
361
+
362
+ const t = src[ p ];
363
+ const g = ofTemplate[ t ];
364
+ if ( g >= 0 && slot[ t ] !== 0 ) continue;
365
+ rep[ entryOf[ p ] ] = p;
366
+ if ( g >= 0 ) group[ entryOf[ p ] ] = g;
367
+
368
+ }
369
+
370
+ for ( const g of this.groups ) {
371
+
372
+ const first = g.starts[ 0 ];
373
+ for ( let j = 1; j < g.members.length; j ++ ) entryOf.set( entryOf.subarray( first, first + g.count ), g.starts[ j ] );
374
+
375
+ }
376
+
377
+ this.groupOfTemplate = ofTemplate;
378
+ this.memberSlot = slot;
379
+ this.entryCount = entries;
380
+ this.entryRep = rep;
381
+ this.entryGroup = group;
382
+ this.placementEntry = entryOf;
383
+ this.groupNodeCount = nodes;
384
+
385
+ }
386
+
387
+ /** The placement whose transform an entry's TLAS leaf carries. */
388
+ repOf( entry ) {
389
+
390
+ return this.entryRep ? this.entryRep[ entry ] : entry;
391
+
392
+ }
393
+
394
+ /** The TLAS entry a placement is traced through. */
395
+ entryOf( placement ) {
396
+
397
+ return this.placementEntry ? this.placementEntry[ placement ] : placement;
398
+
399
+ }
400
+
401
+ /** Group of an entry, or -1. */
402
+ groupOfEntry( entry ) {
403
+
404
+ return this.entryGroup ? this.entryGroup[ entry ] : - 1;
405
+
406
+ }
407
+
408
+ /** The node an entry's leaf points at: its template's BLAS root, or its group's tree. */
409
+ entryRoot( entry ) {
410
+
411
+ const g = this.groupOfEntry( entry );
412
+ return g >= 0 ? this.groups[ g ].offset : this.tplBlasOffset[ this.sourceMesh[ this.repOf( entry ) ] ];
413
+
414
+ }
415
+
416
+ /** An entry is visible while any member placement of it is. */
417
+ entryVisible( entry ) {
418
+
419
+ const g = this.groupOfEntry( entry );
420
+ if ( g < 0 ) return this.visible[ this.repOf( entry ) ] === 1;
421
+ const { starts } = this.groups[ g ];
422
+ const copy = this.repOf( entry ) - starts[ 0 ];
423
+ for ( let j = 0; j < starts.length; j ++ ) if ( this.visible[ starts[ j ] + copy ] ) return true;
424
+ return false;
425
+
426
+ }
427
+
428
+ /** Records `node` as the TLAS leaf of every placement traced through `entry`. */
429
+ setLeafOf( entry, node ) {
430
+
431
+ const g = this.groupOfEntry( entry );
432
+ const p = this.repOf( entry );
433
+ if ( g < 0 ) {
434
+
435
+ this.tlasLeafIndex[ p ] = node;
436
+ return;
437
+
438
+ }
439
+
440
+ const { starts } = this.groups[ g ];
441
+ const copy = p - starts[ 0 ];
442
+ for ( let j = 0; j < starts.length; j ++ ) this.tlasLeafIndex[ starts[ j ] + copy ] = node;
443
+
444
+ }
445
+
446
+ /** Each group's object-space bounds: every member's, hidden or not, so a toggle never moves the TLAS. */
447
+ computeGroupAABBs() {
448
+
449
+ for ( const g of this.groups ) {
450
+
451
+ const box = g.aabb;
452
+ box.fill( Infinity, 0, 3 );
453
+ box.fill( - Infinity, 3, 6 );
454
+ for ( const t of g.members ) {
455
+
456
+ if ( this.tplOwner[ t ] < 0 ) continue;
457
+ const o = t * 6;
458
+ for ( let a = 0; a < 3; a ++ ) {
459
+
460
+ box[ a ] = Math.min( box[ a ], this.tplObjectAABB[ o + a ] );
461
+ box[ 3 + a ] = Math.max( box[ 3 + a ], this.tplObjectAABB[ o + 3 + a ] );
462
+
463
+ }
464
+
465
+ }
466
+
467
+ if ( box[ 0 ] > box[ 3 ] ) box.fill( 0 );
468
+
469
+ }
470
+
471
+ }
472
+
473
+ /** The empty leaf a hidden group member points at: the group region's last node. */
474
+ get emptyGroupLeaf() {
475
+
476
+ return this.groupNodeStart + this.groupNodeCount - 1;
477
+
478
+ }
479
+
480
+ /**
481
+ * The group region's nodes, from the members' bounds, BLAS offsets and visibility: each group's tree, then the
482
+ * empty leaf a hidden member points at. Pre-order, so a parent precedes its children, as refit expects.
483
+ * @returns {Float32Array} groupNodeCount nodes of 16 floats, for node groupNodeStart on
484
+ */
485
+ groupNodeBlock() {
486
+
487
+ const block = new Float32Array( this.groupNodeCount * 16 );
488
+ if ( ! this.groups.length ) return block;
489
+
490
+ const idx = new Uint32Array( block.buffer );
491
+ idx[ ( this.groupNodeCount - 1 ) * 16 + 3 ] = BVH_LEAF_MARKERS.TRIANGLE_LEAF;
492
+ for ( const g of this.groups ) this._writeGroupTree( g, block, idx );
493
+ return block;
494
+
495
+ }
496
+
497
+ /** @private */
498
+ _writeGroupTree( g, block, idx ) {
499
+
500
+ const base = this.groupNodeStart;
501
+ const empty = this.emptyGroupLeaf;
502
+ const aabb = this.tplObjectAABB;
503
+ let next = g.offset;
504
+
505
+ const child = ( list ) => {
506
+
507
+ if ( list.length === 1 ) {
508
+
509
+ const j = list[ 0 ];
510
+ const t = g.members[ j ];
511
+ const shown = this.tplOwner[ t ] >= 0 && this.visible[ g.starts[ j ] ] === 1;
512
+ return shown ? { ref: this.tplBlasOffset[ t ], box: aabb.subarray( t * 6, t * 6 + 6 ) } : { ref: empty, box: null };
513
+
514
+ }
515
+
516
+ const node = next ++;
517
+ const [ left, right ] = splitMembers( list, g.members, aabb );
518
+ const a = child( left );
519
+ const b = child( right );
520
+ const o = ( node - base ) * 16;
521
+ writeChildBox( block, o, a.box );
522
+ writeChildBox( block, o + 8, b.box );
523
+ idx[ o + 3 ] = a.ref;
524
+ idx[ o + 7 ] = b.ref;
525
+ return { ref: node, box: unionBoxes( a.box, b.box ) };
526
+
527
+ };
528
+
529
+ child( Array.from( g.members, ( _, j ) => j ) );
530
+
531
+ }
532
+
533
+ /**
534
+ * Plans copy clusters: ⌈entries / CLUSTER_SIZE⌉ of them. Only how many, which is fixed before any bounds exist (the
535
+ * BVH layout and the BLAS cache need it then); formClusters picks the members.
536
+ */
537
+ planClusters() {
538
+
539
+ this.clusterCount = Math.ceil( this.entryCount / CLUSTER_SIZE );
540
+ this.recordNodeCount = Math.ceil( this.entryCount * RECORD_VEC4 / 4 );
541
+ if ( this.entryCount >= CLUSTER_FIRST_MASK ) throw new RangeError( `${this.entryCount.toLocaleString()} copies is past what a copy cluster can address` );
542
+
543
+ }
544
+
545
+ /**
546
+ * Picks each cluster's members: the entries halved at the centre median of their widest axis until runs of
547
+ * CLUSTER_SIZE remain, so a cluster holds near neighbours, whatever each draws. Records follow cluster order.
548
+ * @param {Float32Array} world - each entry's world bounds, entryCount × 6
549
+ * @returns {Float64Array} each cluster's world bounds, clusterCount × 6, what the TLAS sorts on
550
+ */
551
+ formClusters( world ) {
552
+
553
+ const n = this.entryCount;
554
+ const sorted = new Int32Array( n );
555
+ for ( let e = 0; e < n; e ++ ) sorted[ e ] = e;
556
+ splitIntoRuns( sorted, 0, n, world );
557
+
558
+ const bounds = new Float64Array( this.clusterCount * 6 );
559
+ for ( let c = 0; c < this.clusterCount; c ++ ) {
560
+
561
+ const r0 = c * CLUSTER_SIZE, end = Math.min( r0 + CLUSTER_SIZE, n );
562
+ const b = c * 6;
563
+ bounds[ b ] = bounds[ b + 1 ] = bounds[ b + 2 ] = Infinity;
564
+ bounds[ b + 3 ] = bounds[ b + 4 ] = bounds[ b + 5 ] = - Infinity;
565
+ for ( let r = r0; r < end; r ++ ) {
566
+
567
+ const o = sorted[ r ] * 6;
568
+ for ( let a = 0; a < 3; a ++ ) {
569
+
570
+ if ( world[ o + a ] < bounds[ b + a ] ) bounds[ b + a ] = world[ o + a ];
571
+ if ( world[ o + 3 + a ] > bounds[ b + 3 + a ] ) bounds[ b + 3 + a ] = world[ o + 3 + a ];
572
+
573
+ }
574
+
575
+ }
576
+
577
+ }
578
+
579
+ this.recordEntry = sorted;
580
+ this.clusterLeaf = new Int32Array( this.clusterCount ).fill( - 1 );
581
+ return bounds;
582
+
583
+ }
584
+
585
+ /**
586
+ * Writes cluster `c`'s TLAS leaf at float offset `o` of `data` (node `node`), its copies' boxes from `world`, and
587
+ * makes each copy's record its placements' instance id (`tlasLeafIndex`).
588
+ */
589
+ writeClusterLeaf( data, idx, o, c, world, node ) {
590
+
591
+ const first = c * CLUSTER_SIZE, count = Math.min( CLUSTER_SIZE, this.entryCount - first );
592
+ idx[ o ] = ( first | ( count - 1 ) << CLUSTER_COUNT_SHIFT ) >>> 0;
593
+ idx[ o + 3 ] = BVH_LEAF_MARKERS.CLUSTER_LEAF;
594
+ for ( let k = 0; k < CLUSTER_SIZE; k ++ ) idx[ o + 12 + k ] = k < count ? this.copyWord( this.recordEntry[ first + k ] ) : 0;
595
+ this.encodeClusterBoxes( data, idx, o, c, world );
596
+ this.clusterLeaf[ c ] = node;
597
+ for ( let r = first; r < first + count; r ++ ) this.setLeafOf( this.recordEntry[ r ], r );
598
+
599
+ }
600
+
601
+ /** A cluster leaf's word for `entry` (BufferLayout): its root, whether its transform is identity, and hidden. */
602
+ copyWord( entry ) {
603
+
604
+ const o = this.matrixRow( this.repOf( entry ) );
605
+ const identity = isIdentityAt( this.rowArray, o ) ? TLAS_LEAF_IDENTITY : 0;
606
+ return ( this.entryRoot( entry ) | identity | ( this.entryVisible( entry ) ? 0 : CLUSTER_COPY_HIDDEN ) ) >>> 0;
607
+
608
+ }
609
+
610
+ /**
611
+ * Cluster `c`'s boxes into its leaf at `o` (BufferLayout `encodeClusterBoxes`): each copy's world box from `world`
612
+ * (entry-indexed, at build) or, without it, from its transform now. The union goes to `out` at `outOff`.
613
+ */
614
+ encodeClusterBoxes( data, idx, o, c, world = null, out = null, outOff = 0 ) {
615
+
616
+ const first = c * CLUSTER_SIZE, count = Math.min( CLUSTER_SIZE, this.entryCount - first );
617
+ const boxes = _copyBoxes;
618
+ for ( let k = 0; k < count; k ++ ) {
619
+
620
+ const e = this.recordEntry[ first + k ];
621
+ if ( world ) for ( let i = 0; i < 6; i ++ ) boxes[ k * 6 + i ] = world[ e * 6 + i ];
622
+ else this.writeEntryWorldAABB( e, boxes, k * 6 );
623
+
624
+ }
625
+
626
+ encodeClusterBoxBytes( data, idx, o, boxes, count, out, outOff );
627
+
628
+ }
629
+
630
+ /** A copy record: placement `p`'s world-to-object rows, laid out as a leaf's slots 4–15, at `off` of `out`. */
631
+ writeRecord( out, off, p ) {
632
+
633
+ const inv = _recordInverse;
634
+ const o = this.matrixRow( p );
635
+ invertAffineInto( this.rowArray, o, inv );
636
+ out[ off ] = inv[ 0 ]; out[ off + 1 ] = inv[ 4 ]; out[ off + 2 ] = inv[ 8 ]; out[ off + 3 ] = inv[ 12 ];
637
+ out[ off + 4 ] = inv[ 1 ]; out[ off + 5 ] = inv[ 5 ]; out[ off + 6 ] = inv[ 9 ]; out[ off + 7 ] = inv[ 13 ];
638
+ out[ off + 8 ] = inv[ 2 ]; out[ off + 9 ] = inv[ 6 ]; out[ off + 10 ] = inv[ 10 ]; out[ off + 11 ] = inv[ 14 ];
639
+
640
+ }
641
+
642
+ /** The TLAS leaf holding placement `p`: its cluster's when clustered. */
643
+ leafOfPlacement( p ) {
644
+
645
+ const id = this.tlasLeafIndex[ p ];
646
+ return id < 0 || ! this.clusterCount ? id : this.clusterLeaf[ this.clusterOf( id ) ];
647
+
648
+ }
649
+
650
+ /** The slot of record `r`'s word in its cluster leaf (`copyWord`). */
651
+ copyWordSlot( r ) {
652
+
653
+ return 12 + r % CLUSTER_SIZE;
654
+
655
+ }
656
+
657
+ /** The cluster holding record `r`. */
658
+ clusterOf( r ) {
659
+
660
+ return ( r / CLUSTER_SIZE ) | 0;
661
+
662
+ }
663
+
303
664
  /** Placements actually built. */
304
665
  get setCount() {
305
666
 
@@ -309,6 +670,109 @@ export class InstanceTable {
309
670
 
310
671
  }
311
672
 
673
+ /**
674
+ * Placement transforms are runs of rows: run r covers placements first[ r ] … first[ r + 1 ] − 1, whose 16 floats
675
+ * each follow from base[ r ] in arrays[ arrayOf[ r ] ] — an array of the table's own, or an InstancedMesh's matrix
676
+ * attribute, read through it so its array can go to disk and come back (`adoptedMatrices`). An adopted list is never
677
+ * written: the first write to its run copies the run into an array of the table's own.
678
+ * @private
679
+ */
680
+ _adoptRuns( { first, arrays, arrayOf, base, owned } ) {
681
+
682
+ this._runFirst = Int32Array.from( first );
683
+ this._runArrays = arrays.slice();
684
+ this._runArray = Int32Array.from( arrayOf );
685
+ this._runBase = Float64Array.from( base );
686
+ this._runOwned = Uint8Array.from( owned );
687
+ this._lastRun = 0;
688
+ this.rowArray = this._runArrays[ 0 ]?.array ?? this._runArrays[ 0 ] ?? null;
689
+
690
+ }
691
+
692
+ /** The run holding placement `p`. @private */
693
+ _runOf( p ) {
694
+
695
+ const first = this._runFirst;
696
+ let r = this._lastRun;
697
+ if ( p >= first[ r ] && p < first[ r + 1 ] ) return r;
698
+ let lo = 0, hi = first.length - 2;
699
+ while ( lo < hi ) {
700
+
701
+ const mid = ( lo + hi + 1 ) >> 1;
702
+ if ( first[ mid ] <= p ) lo = mid;
703
+ else hi = mid - 1;
704
+
705
+ }
706
+
707
+ r = this._lastRun = lo;
708
+ return r;
709
+
710
+ }
711
+
712
+ /** Where placement `p`'s 16 object-to-world floats start in `this.rowArray`, which this sets. */
713
+ matrixRow( p ) {
714
+
715
+ const r = this._runOf( p );
716
+ const a = this._runArrays[ this._runArray[ r ] ];
717
+ this.rowArray = a.array ?? a;
718
+ return this._runBase[ r ] + ( p - this._runFirst[ r ] ) * 16;
719
+
720
+ }
721
+
722
+ /** The transform array when a single run of the table's own holds every placement, else null. */
723
+ get world() {
724
+
725
+ const a = this._runFirst.length === 2 && this._runBase[ 0 ] === 0 ? this._runArrays[ this._runArray[ 0 ] ] : null;
726
+ return a?.array ?? a;
727
+
728
+ }
729
+
730
+ /** Bytes of transforms the table holds itself, not counting the matrix lists it reads in place. */
731
+ get matrixBytes() {
732
+
733
+ const own = new Set();
734
+ for ( let r = 0; r < this._runArray.length; r ++ ) if ( this._runOwned[ r ] ) own.add( this._runArrays[ this._runArray[ r ] ] );
735
+ let bytes = 0;
736
+ for ( const a of own ) bytes += a.byteLength;
737
+ return bytes;
738
+
739
+ }
740
+
741
+ /** The InstancedMesh matrix attributes the table reads in place. */
742
+ adoptedMatrices() {
743
+
744
+ const out = new Set();
745
+ for ( let r = 0; r < this._runArray.length; r ++ ) {
746
+
747
+ const a = this._runArrays[ this._runArray[ r ] ];
748
+ if ( ! this._runOwned[ r ] && a.isBufferAttribute ) out.add( a );
749
+
750
+ }
751
+
752
+ return [ ...out ];
753
+
754
+ }
755
+
756
+ /** @private */
757
+ _writeRow( p, m, mo ) {
758
+
759
+ const r = this._runOf( p );
760
+ if ( ! this._runOwned[ r ] ) {
761
+
762
+ const start = this._runBase[ r ], end = start + ( this._runFirst[ r + 1 ] - this._runFirst[ r ] ) * 16;
763
+ const a = this._runArrays[ this._runArray[ r ] ];
764
+ this._runArrays.push( ( a.array ?? a ).slice( start, end ) );
765
+ this._runArray[ r ] = this._runArrays.length - 1;
766
+ this._runBase[ r ] = 0;
767
+ this._runOwned[ r ] = 1;
768
+
769
+ }
770
+
771
+ const o = this.matrixRow( p ), w = this.rowArray;
772
+ for ( let k = 0; k < 16; k ++ ) w[ o + k ] = m[ mo + k ];
773
+
774
+ }
775
+
312
776
  /**
313
777
  * Replace one placement's object-to-world transform. Its geometry is untouched, which is the
314
778
  * whole point: a rigid move is a matrix change, and rewriting shared triangles instead would
@@ -416,7 +880,8 @@ export class InstanceTable {
416
880
  /** A view of the 16 object-to-world floats for `index`. Cold paths only. */
417
881
  matrixWorldOf( index ) {
418
882
 
419
- return this.world.subarray( index * 16, index * 16 + 16 );
883
+ const o = this.matrixRow( index );
884
+ return this.rowArray.subarray( o, o + 16 );
420
885
 
421
886
  }
422
887
 
@@ -426,7 +891,8 @@ export class InstanceTable {
426
891
  */
427
892
  matrixInverseOf( index ) {
428
893
 
429
- return invertAffineInto( this.world, index * 16, new Float64Array( 16 ) );
894
+ const o = this.matrixRow( index );
895
+ return invertAffineInto( this.rowArray, o, new Float64Array( 16 ) );
430
896
 
431
897
  }
432
898
 
@@ -440,17 +906,59 @@ export class InstanceTable {
440
906
 
441
907
  }
442
908
 
443
- transformAABBInto( this.tplObjectAABB, this.sourceMesh[ index ] * 6, this.world, index * 16, out, off );
909
+ const o = this.matrixRow( index );
910
+ transformAABBInto( this.tplObjectAABB, this.sourceMesh[ index ] * 6, this.rowArray, o, out, off );
911
+
912
+ }
913
+
914
+ /** Writes a TLAS entry's world-space bounds — its group's, for a group copy — into `out` at `off`. */
915
+ writeEntryWorldAABB( entry, out, off ) {
916
+
917
+ const g = this.groupOfEntry( entry );
918
+ if ( g < 0 ) {
919
+
920
+ this.writeWorldAABB( this.repOf( entry ), out, off );
921
+ return;
922
+
923
+ }
924
+
925
+ const p = this.repOf( entry );
926
+ if ( ! this.isSet[ p ] ) {
927
+
928
+ for ( let k = 0; k < 6; k ++ ) out[ off + k ] = 0;
929
+ return;
930
+
931
+ }
932
+
933
+ const o = this.matrixRow( p );
934
+ transformAABBInto( this.groups[ g ].aabb, 0, this.rowArray, o, out, off );
935
+
936
+ }
937
+
938
+ /**
939
+ * Every TLAS entry's world-space bounds, 6 floats each — what the TLAS sorts on.
940
+ * @param {Float64Array|Float32Array} out
941
+ */
942
+ writeEntryWorldAABBs( out ) {
943
+
944
+ if ( ! this.entryRep ) {
945
+
946
+ this.writeWorldAABBs( out );
947
+ return;
948
+
949
+ }
950
+
951
+ for ( let e = 0; e < this.entryCount; e ++ ) this.writeEntryWorldAABB( e, out, e * 6 );
444
952
 
445
953
  }
446
954
 
447
955
  /**
448
- * Every placement's world-space bounds, 6 floats each — what the TLAS sorts on.
956
+ * Every placement's world-space bounds, 6 floats each.
449
957
  * @param {Float64Array|Float32Array} out
450
958
  */
451
959
  writeWorldAABBs( out ) {
452
960
 
453
- const aabb = this.tplObjectAABB, world = this.world, src = this.sourceMesh, set = this.isSet;
961
+ const aabb = this.tplObjectAABB, src = this.sourceMesh, set = this.isSet;
454
962
 
455
963
  for ( let i = 0; i < this.count; i ++ ) {
456
964
 
@@ -464,7 +972,8 @@ export class InstanceTable {
464
972
 
465
973
  }
466
974
 
467
- transformAABBInto( aabb, src[ i ] * 6, world, i * 16, out, o );
975
+ const m = this.matrixRow( i );
976
+ transformAABBInto( aabb, src[ i ] * 6, this.rowArray, m, out, o );
468
977
 
469
978
  }
470
979
 
@@ -621,7 +1130,18 @@ export class InstanceTable {
621
1130
  assignOffsets( tlasNodeCount ) {
622
1131
 
623
1132
  this.tlasNodeCount = tlasNodeCount;
1133
+ this.groupNodeStart = tlasNodeCount;
624
1134
  let offset = tlasNodeCount;
1135
+ for ( const g of this.groups ) {
1136
+
1137
+ g.offset = offset;
1138
+ offset += g.nodes;
1139
+
1140
+ }
1141
+
1142
+ if ( this.groups.length ) offset ++; // the empty leaf, last: a full refit computes it before any group node reads it
1143
+ const blasBase = offset;
1144
+ this.recordNodeStart = 0;
625
1145
 
626
1146
  for ( let t = 0; t < this.templateCount; t ++ ) {
627
1147
 
@@ -641,15 +1161,30 @@ export class InstanceTable {
641
1161
 
642
1162
  }
643
1163
 
644
- this.totalBLASNodes = offset - tlasNodeCount;
645
- assertBVHIndexFits( offset, 'combined TLAS + BLAS node count' );
1164
+ this.totalBLASNodes = offset - blasBase;
1165
+ if ( this.clusterCount ) this.recordNodeStart = offset;
1166
+ assertBVHIndexFits( offset + ( this.clusterCount ? this.recordNodeCount : 0 ), 'combined TLAS + BLAS node count' );
1167
+
1168
+ }
1169
+
1170
+ /** Where the BLASes start: after the TLAS and the group trees. */
1171
+ get blasBase() {
1172
+
1173
+ return this.tlasNodeCount + this.groupNodeCount;
1174
+
1175
+ }
1176
+
1177
+ /** Tree nodes: TLAS, group trees, all BLASes. */
1178
+ get treeNodeCount() {
1179
+
1180
+ return this.blasBase + this.totalBLASNodes;
646
1181
 
647
1182
  }
648
1183
 
649
- /** Total node count (TLAS + all BLASes). */
1184
+ /** The whole buffer in nodes: the tree, then the copy records when clustered. */
650
1185
  get totalNodeCount() {
651
1186
 
652
- return this.tlasNodeCount + this.totalBLASNodes;
1187
+ return this.treeNodeCount + ( this.clusterCount ? this.recordNodeCount : 0 );
653
1188
 
654
1189
  }
655
1190
 
@@ -664,6 +1199,143 @@ export class InstanceTable {
664
1199
 
665
1200
  }
666
1201
 
1202
+ const _copyBoxes = new Float64Array( CLUSTER_SIZE * 6 );
1203
+ const _recordInverse = new Float64Array( 16 );
1204
+
1205
+ /**
1206
+ * Orders `sorted[ lo, hi )` so each run of CLUSTER_SIZE holds neighbours: halved at the centre median of the widest
1207
+ * axis, the lower part a whole number of runs, which keeps the run count ⌈n / CLUSTER_SIZE⌉.
1208
+ */
1209
+ function splitIntoRuns( sorted, lo, hi, world ) {
1210
+
1211
+ const stack = [ lo, hi ];
1212
+ while ( stack.length ) {
1213
+
1214
+ const end = stack.pop(), start = stack.pop();
1215
+ if ( end - start <= CLUSTER_SIZE ) continue;
1216
+
1217
+ let axis = 0, widest = - 1;
1218
+ for ( let a = 0; a < 3; a ++ ) {
1219
+
1220
+ let min = Infinity, max = - Infinity;
1221
+ for ( let i = start; i < end; i ++ ) {
1222
+
1223
+ const c = world[ sorted[ i ] * 6 + a ] + world[ sorted[ i ] * 6 + 3 + a ];
1224
+ if ( c < min ) min = c;
1225
+ if ( c > max ) max = c;
1226
+
1227
+ }
1228
+
1229
+ if ( max - min > widest ) {
1230
+
1231
+ widest = max - min;
1232
+ axis = a;
1233
+
1234
+ }
1235
+
1236
+ }
1237
+
1238
+ const mid = start + CLUSTER_SIZE * Math.ceil( ( end - start ) / ( 2 * CLUSTER_SIZE ) );
1239
+ selectNth( sorted, start, end, mid, world, axis );
1240
+ stack.push( start, mid, mid, end );
1241
+
1242
+ }
1243
+
1244
+ }
1245
+
1246
+ /** Partially sorts `sorted[ lo, hi )` by centre along `axis` so index `k` holds what a full sort would put there. */
1247
+ function selectNth( sorted, lo, hi, k, world, axis ) {
1248
+
1249
+ const key = ( i ) => world[ sorted[ i ] * 6 + axis ] + world[ sorted[ i ] * 6 + 3 + axis ];
1250
+ let l = lo, r = hi - 1;
1251
+ while ( r > l ) {
1252
+
1253
+ const m = ( l + r ) >>> 1;
1254
+ const a = key( l ), b = key( m ), c = key( r );
1255
+ const pivot = a < b ? ( b < c ? b : a < c ? c : a ) : ( a < c ? a : b < c ? c : b );
1256
+ let i = l, j = r;
1257
+ while ( i <= j ) {
1258
+
1259
+ while ( key( i ) < pivot ) i ++;
1260
+ while ( key( j ) > pivot ) j --;
1261
+ if ( i <= j ) {
1262
+
1263
+ const t = sorted[ i ];
1264
+ sorted[ i ] = sorted[ j ];
1265
+ sorted[ j ] = t;
1266
+ i ++;
1267
+ j --;
1268
+
1269
+ }
1270
+
1271
+ }
1272
+
1273
+ if ( k <= j ) r = j;
1274
+ else if ( k >= i ) l = i;
1275
+ else return;
1276
+
1277
+ }
1278
+
1279
+ }
1280
+
1281
+ /** Splits group member slots in two at the centroid median of their widest axis. */
1282
+ function splitMembers( list, members, aabb ) {
1283
+
1284
+ const centre = ( j, a ) => aabb[ members[ j ] * 6 + a ] + aabb[ members[ j ] * 6 + 3 + a ];
1285
+ let axis = 0, widest = - 1;
1286
+ for ( let a = 0; a < 3; a ++ ) {
1287
+
1288
+ let lo = Infinity, hi = - Infinity;
1289
+ for ( const j of list ) {
1290
+
1291
+ const c = centre( j, a );
1292
+ if ( c < lo ) lo = c;
1293
+ if ( c > hi ) hi = c;
1294
+
1295
+ }
1296
+
1297
+ if ( hi - lo > widest ) {
1298
+
1299
+ widest = hi - lo;
1300
+ axis = a;
1301
+
1302
+ }
1303
+
1304
+ }
1305
+
1306
+ const sorted = list.slice().sort( ( x, y ) => centre( x, axis ) - centre( y, axis ) || x - y );
1307
+ const mid = sorted.length >> 1;
1308
+ return [ sorted.slice( 0, mid ), sorted.slice( mid ) ];
1309
+
1310
+ }
1311
+
1312
+ /** A child's box into an inner node's slot at `off`; none is the far point box no ray enters. */
1313
+ function writeChildBox( block, off, box ) {
1314
+
1315
+ for ( let a = 0; a < 3; a ++ ) {
1316
+
1317
+ block[ off + a ] = box ? box[ a ] : BVH_EMPTY_BOX;
1318
+ block[ off + 4 + a ] = box ? box[ 3 + a ] : BVH_EMPTY_BOX;
1319
+
1320
+ }
1321
+
1322
+ }
1323
+
1324
+ function unionBoxes( a, b ) {
1325
+
1326
+ if ( ! a || ! b ) return a ?? b;
1327
+ const out = new Float32Array( 6 );
1328
+ for ( let k = 0; k < 3; k ++ ) {
1329
+
1330
+ out[ k ] = Math.min( a[ k ], b[ k ] );
1331
+ out[ 3 + k ] = Math.max( a[ 3 + k ], b[ 3 + k ] );
1332
+
1333
+ }
1334
+
1335
+ return out;
1336
+
1337
+ }
1338
+
667
1339
  /** Bounds of triangles [triOffset, triOffset + triCount) of a flat or chunked store, into out[off..off+5]. */
668
1340
  function aabbOfTriangles( triangleData, triOffset, triCount, out, off ) {
669
1341