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
@@ -148,6 +148,26 @@ export function geometryBytesOf( object ) {
148
148
 
149
149
  }
150
150
 
151
+ /** Bytes of InstancedMesh matrix lists under `object`, each list once however many meshes share it. */
152
+ export function instanceBytesOf( object ) {
153
+
154
+ let bytes = 0;
155
+ const counted = new Set();
156
+ object.traverse?.( node => {
157
+
158
+ const array = node.isInstancedMesh ? node.instanceMatrix?.array : null;
159
+ if ( array && ! counted.has( array ) ) {
160
+
161
+ counted.add( array );
162
+ bytes += array.byteLength;
163
+
164
+ }
165
+
166
+ } );
167
+ return bytes;
168
+
169
+ }
170
+
151
171
  // Affine point and linear direction transforms straight off the matrix elements: no perspective
152
172
  // divide, no method dispatch, on the hottest per-vertex path of a baked mesh.
153
173
  function affinePoint( v, e ) {
@@ -187,6 +207,54 @@ function determinant3( e ) {
187
207
 
188
208
  }
189
209
 
210
+ /**
211
+ * Whether any material under `object` holds a texture, so the GPU needs the triangles' texture coordinates. A scene
212
+ * with none (Moana's USD: its Ptex is baked into colours) is uploaded without them.
213
+ */
214
+ export function usesTextureCoordinates( object ) {
215
+
216
+ let uses = false;
217
+ const seen = new Set();
218
+ object.traverse( node => {
219
+
220
+ if ( uses || ! node.isMesh || ! node.material ) return;
221
+ for ( const material of Array.isArray( node.material ) ? node.material : [ node.material ] ) {
222
+
223
+ if ( seen.has( material ) ) continue;
224
+ seen.add( material );
225
+ for ( const key in material ) if ( material[ key ]?.isTexture ) uses = true;
226
+
227
+ }
228
+
229
+ } );
230
+ return uses;
231
+
232
+ }
233
+
234
+ /**
235
+ * Stores a geometry's float attribute in [−1, 1] as normalized 16-bit integers, which three.js reads back as floats.
236
+ * Left as floats when out of that range (or NaN) rather than silently clamped, or when morph targets use it.
237
+ */
238
+ export function packUnitAttribute( g, name ) {
239
+
240
+ const attr = g.getAttribute( name );
241
+ if ( ! attr || attr.normalized || ! ( attr.array instanceof Float32Array ) ) return;
242
+ if ( g.morphAttributes?.[ name ]?.length ) return;
243
+
244
+ const src = attr.array;
245
+ for ( let i = 0; i < src.length; i ++ ) {
246
+
247
+ const v = src[ i ];
248
+ if ( ! ( v >= - 1.0001 && v <= 1.0001 ) ) return;
249
+
250
+ }
251
+
252
+ const packed = new Int16Array( src.length );
253
+ for ( let i = 0; i < src.length; i ++ ) packed[ i ] = Math.round( Math.max( - 1, Math.min( 1, src[ i ] ) ) * 32767 );
254
+ g.setAttribute( name, new BufferAttribute( packed, attr.itemSize, true ) );
255
+
256
+ }
257
+
190
258
  export class GeometryExtractor {
191
259
 
192
260
  /** @param {{issues?: import('../EngineIssues.js').IssueLog}} [options] */
@@ -252,7 +320,6 @@ export class GeometryExtractor {
252
320
 
253
321
  // Single traversal: extract geometry, materials, lights, and cameras
254
322
  this.traverseObject( object );
255
- this._shareInstanceMatrices();
256
323
  this._compressAttributes( object );
257
324
 
258
325
  this.logStats();
@@ -307,7 +374,6 @@ export class GeometryExtractor {
307
374
 
308
375
  }
309
376
 
310
- this._shareInstanceMatrices();
311
377
  this._compressAttributes( object );
312
378
  this.triangles.trimInPlace( this.currentTriangleIndex );
313
379
  this._streamedStore = true;
@@ -448,6 +514,7 @@ export class GeometryExtractor {
448
514
  placements: this._countPlacements( object ),
449
515
  meshes,
450
516
  geometryBytes: geometryBytesOf( object ),
517
+ instanceBytes: instanceBytesOf( object ),
451
518
  };
452
519
 
453
520
  }
@@ -502,9 +569,71 @@ export class GeometryExtractor {
502
569
  this.instanceCount = 0;
503
570
  this.instanceCapacity = count;
504
571
  this.instanceSource = new Int32Array( count );
505
- this.instanceMatrices = new Float32Array( count * 16 );
506
- this._shareable = [];
572
+ // Placement transforms as runs of rows (InstanceTable): an InstancedMesh's own matrix list read where it is,
573
+ // or rows of the extractor's own. A copy of every list was 3 GB beside them on the Moana island.
574
+ this._runs = { first: [], arrays: [], arrayOf: [], base: [], owned: [] };
575
+ this._runArrayIndex = new Map();
576
+ this._singles = null;
577
+ this._singlesUsed = 0;
578
+ this._singlesRun = null;
507
579
  this._compressed = new Set();
580
+ this._matrixLists = new Map();
581
+
582
+ }
583
+
584
+ /** Starts a run of rows at the next placement. @private */
585
+ _beginRun( array, base, owned ) {
586
+
587
+ const runs = this._runs;
588
+ let a = this._runArrayIndex.get( array );
589
+ if ( a === undefined ) this._runArrayIndex.set( array, a = runs.arrays.push( array ) - 1 );
590
+ runs.first.push( this.instanceCount );
591
+ runs.arrayOf.push( a );
592
+ runs.base.push( base );
593
+ runs.owned.push( owned );
594
+
595
+ }
596
+
597
+ /** The row for a placement of a mesh without instances, appended to the extractor's own array. @private */
598
+ _singleRow() {
599
+
600
+ const runs = this._runs;
601
+ if ( ! this._singles ) {
602
+
603
+ this._singles = new Float32Array( 1024 * 16 );
604
+ this._singlesIndex = runs.arrays.push( this._singles ) - 1;
605
+
606
+ }
607
+
608
+ if ( this._singlesUsed + 16 > this._singles.length ) {
609
+
610
+ const grown = new Float32Array( this._singles.length * 2 );
611
+ grown.set( this._singles );
612
+ this._singles = runs.arrays[ this._singlesIndex ] = grown;
613
+
614
+ }
615
+
616
+ if ( this._singlesRun !== runs.first.length - 1 ) {
617
+
618
+ runs.first.push( this.instanceCount );
619
+ runs.arrayOf.push( this._singlesIndex );
620
+ runs.base.push( this._singlesUsed );
621
+ runs.owned.push( 1 );
622
+ this._singlesRun = runs.first.length - 1;
623
+
624
+ }
625
+
626
+ const o = this._singlesUsed;
627
+ this._singlesUsed += 16;
628
+ return o;
629
+
630
+ }
631
+
632
+ /** The runs, closed at the placement count (InstanceTable.allocate). @private */
633
+ _matrixRuns() {
634
+
635
+ const runs = this._runs;
636
+ return { first: [ ...runs.first, this.instanceCount ], arrays: runs.arrays, arrayOf: runs.arrayOf, base: runs.base, owned: runs.owned };
508
637
 
509
638
  }
510
639
 
@@ -543,59 +672,7 @@ export class GeometryExtractor {
543
672
  if ( this._compressed.has( g.uuid ) ) return;
544
673
  this._compressed.add( g.uuid );
545
674
 
546
- for ( const name of [ 'normal', 'tangent', 'color' ] ) {
547
-
548
- const attr = g.getAttribute( name );
549
- if ( ! attr || attr.normalized || ! ( attr.array instanceof Float32Array ) ) continue;
550
- if ( g.morphAttributes?.[ name ]?.length ) continue;
551
-
552
- const src = attr.array;
553
- let ok = true;
554
- for ( let i = 0; i < src.length; i ++ ) {
555
-
556
- const v = src[ i ];
557
- if ( ! ( v >= - 1.0001 && v <= 1.0001 ) ) {
558
-
559
- ok = false; break;
560
-
561
- }
562
-
563
- }
564
-
565
- // Out of snorm range (or NaN) — leave it as floats rather than silently clamp.
566
- if ( ! ok ) continue;
567
-
568
- const packed = new Int16Array( src.length );
569
- for ( let i = 0; i < src.length; i ++ ) {
570
-
571
- packed[ i ] = Math.round( Math.max( - 1, Math.min( 1, src[ i ] ) ) * 32767 );
572
-
573
- }
574
-
575
- g.setAttribute( name, new BufferAttribute( packed, attr.itemSize, true ) );
576
-
577
- }
578
-
579
- }
580
-
581
- /**
582
- * Point each InstancedMesh's matrix attribute at the placement pool that duplicates it.
583
- *
584
- * With the host at the origin the two hold identical bytes — 366 MB apart at 6M instances.
585
- * Runs after the traversal, since growth reallocates the pool.
586
- * @private
587
- */
588
- _shareInstanceMatrices() {
589
-
590
- for ( const { mesh, start, count } of this._shareable ) {
591
-
592
- const attr = mesh.instanceMatrix;
593
- if ( ! attr || attr.array.length !== count * 16 ) continue;
594
- attr.array = this.instanceMatrices.subarray( start * 16, ( start + count ) * 16 );
595
-
596
- }
597
-
598
- this._shareable = [];
675
+ for ( const name of [ 'normal', 'tangent', 'color' ] ) packUnitAttribute( g, name );
599
676
 
600
677
  }
601
678
 
@@ -755,7 +832,7 @@ export class GeometryExtractor {
755
832
  };
756
833
  this.expandedTriangleCount += shared.count;
757
834
  this.meshTriangleRanges.push( range );
758
- this._recordPlacements( mesh, meshIndex );
835
+ this._recordPlacements( mesh, meshIndex, false, false, shared.count > 0 );
759
836
  return;
760
837
 
761
838
  }
@@ -785,7 +862,7 @@ export class GeometryExtractor {
785
862
  this.expandedTriangleCount += range.count;
786
863
  this.meshTriangleRanges.push( range );
787
864
  if ( range.count > 0 && ! bake && shareable ) this._geometryRanges.set( key, { ...range, meshIndex } );
788
- this._recordPlacements( mesh, meshIndex, bake, expand );
865
+ this._recordPlacements( mesh, meshIndex, bake, expand, range.count > 0 && shareable );
789
866
  if ( range.count > 0 ) this._onRange?.( meshIndex, range );
790
867
 
791
868
  }
@@ -796,10 +873,9 @@ export class GeometryExtractor {
796
873
  * a matrix each rather than a million Object3Ds.
797
874
  * @private
798
875
  */
799
- _recordPlacements( mesh, meshIndex, baked = false, expanded = false ) {
876
+ _recordPlacements( mesh, meshIndex, baked = false, expanded = false, groupable = false ) {
800
877
 
801
878
  const world = mesh.matrixWorld.elements;
802
- const dst = this.instanceMatrices;
803
879
  const src = this.instanceSource;
804
880
 
805
881
  // Expanded instances live in the triangles, so the mesh keeps a single placement. The
@@ -807,6 +883,8 @@ export class GeometryExtractor {
807
883
  // every instance equally, which is exactly what composing against it produces.
808
884
  if ( ! mesh.isInstancedMesh || ! mesh.instanceMatrix || expanded ) {
809
885
 
886
+ const d = this._singleRow();
887
+ const dst = this._singles;
810
888
  const p = this._nextPlacement();
811
889
  src[ p ] = meshIndex;
812
890
 
@@ -814,12 +892,12 @@ export class GeometryExtractor {
814
892
 
815
893
  // The triangles already carry this pose, so the placement starts at identity. A
816
894
  // later move composes against the inverse of what was baked in.
817
- for ( let k = 0; k < 16; k ++ ) dst[ p * 16 + k ] = k % 5 === 0 ? 1 : 0;
895
+ for ( let k = 0; k < 16; k ++ ) dst[ d + k ] = k % 5 === 0 ? 1 : 0;
818
896
  this.bakeInverse.set( meshIndex, Float32Array.from( this._matrixPool.mat4.copy( mesh.matrixWorld ).invert().elements ) );
819
897
 
820
898
  } else {
821
899
 
822
- for ( let k = 0; k < 16; k ++ ) dst[ p * 16 + k ] = world[ k ];
900
+ for ( let k = 0; k < 16; k ++ ) dst[ d + k ] = world[ k ];
823
901
 
824
902
  }
825
903
 
@@ -829,27 +907,28 @@ export class GeometryExtractor {
829
907
 
830
908
  const arr = mesh.instanceMatrix.array;
831
909
  const count = mesh.count ?? ( arr.length / 16 );
832
- // A pbrt archive bakes the CTM into every instance, so the host needs no multiply.
833
- const identityHost = isIdentityElements( world );
834
- if ( identityHost && arr.length === count * 16 ) {
910
+ if ( groupable && count > 0 ) {
835
911
 
836
- this._shareable.push( { mesh, start: this.instanceCount, count } );
912
+ let list = this._matrixLists.get( mesh.instanceMatrix );
913
+ if ( ! list ) this._matrixLists.set( mesh.instanceMatrix, list = [] );
914
+ list.push( { meshIndex, start: this.instanceCount, count, world: Float32Array.from( world ) } );
837
915
 
838
916
  }
839
917
 
918
+ if ( count <= 0 ) return;
919
+
920
+ // A pbrt archive bakes the CTM into every instance, so the host needs no multiply and the list is read in place.
921
+ const identityHost = isIdentityElements( world ) && arr.length >= count * 16;
922
+ const dst = identityHost ? arr : new Float32Array( count * 16 );
923
+ this._beginRun( identityHost ? mesh.instanceMatrix : dst, 0, identityHost ? 0 : 1 );
924
+
840
925
  for ( let i = 0; i < count; i ++ ) {
841
926
 
842
927
  const o = i * 16;
843
928
  const p = this._nextPlacement();
844
- const d = p * 16;
929
+ const d = i * 16;
845
930
  src[ p ] = meshIndex;
846
-
847
- if ( identityHost ) {
848
-
849
- for ( let k = 0; k < 16; k ++ ) dst[ d + k ] = arr[ o + k ];
850
- continue;
851
-
852
- }
931
+ if ( identityHost ) continue;
853
932
 
854
933
  // world = mesh.matrixWorld * instanceMatrix, both column-major.
855
934
  for ( let c = 0; c < 4; c ++ ) {
@@ -869,6 +948,39 @@ export class GeometryExtractor {
869
948
 
870
949
  }
871
950
 
951
+ /**
952
+ * Instanced meshes sharing one matrix attribute under the same host transform: one object traced once a copy
953
+ * (InstanceTable.setGroups). A scene builder shares the attribute between a template's shapes for this.
954
+ * @returns {Array<{members: number[], starts: number[], count: number}>}
955
+ * @private
956
+ */
957
+ _instanceGroups() {
958
+
959
+ const groups = [];
960
+ for ( const list of this._matrixLists.values() ) {
961
+
962
+ const open = [];
963
+ for ( const member of list ) {
964
+
965
+ const group = open.find( g => g.count === member.count && g.world.every( ( v, k ) => v === member.world[ k ] ) );
966
+ if ( group ) {
967
+
968
+ group.members.push( member.meshIndex );
969
+ group.starts.push( member.start );
970
+
971
+ } else open.push( { world: member.world, count: member.count, members: [ member.meshIndex ], starts: [ member.start ] } );
972
+
973
+ }
974
+
975
+ for ( const g of open ) if ( g.members.length >= 2 ) groups.push( { members: g.members, starts: g.starts, count: g.count } );
976
+
977
+ }
978
+
979
+ this._matrixLists.clear();
980
+ return groups;
981
+
982
+ }
983
+
872
984
  /** Next free placement slot, growing only if the pre-count was short. @private */
873
985
  _nextPlacement() {
874
986
 
@@ -876,11 +988,8 @@ export class GeometryExtractor {
876
988
 
877
989
  const grown = Math.max( 16, this.instanceCapacity * 2 );
878
990
  const srcCol = new Int32Array( grown );
879
- const matCol = new Float32Array( grown * 16 );
880
991
  srcCol.set( this.instanceSource );
881
- matCol.set( this.instanceMatrices );
882
992
  this.instanceSource = srcCol;
883
- this.instanceMatrices = matCol;
884
993
  this.instanceCapacity = grown;
885
994
 
886
995
  }
@@ -1566,8 +1675,9 @@ export class GeometryExtractor {
1566
1675
  meshTriangleRanges: this.meshTriangleRanges, // Per-mesh { start, count } for TLAS/BLAS
1567
1676
  expandedTriangleCount: this.expandedTriangleCount,
1568
1677
  instanceSource: this.instanceSource.subarray( 0, this.instanceCount ),
1569
- instanceMatrices: this.instanceMatrices.subarray( 0, this.instanceCount * 16 ),
1678
+ matrixRuns: this._matrixRuns(),
1570
1679
  instanceCount: this.instanceCount,
1680
+ instanceGroups: this._instanceGroups(),
1571
1681
  // meshIndex -> inverse of the pose baked into its triangles; only baked meshes appear.
1572
1682
  bakeInverse: this.bakeInverse,
1573
1683
  maps: this.maps,
@@ -29,8 +29,17 @@ const BVH_NODE_BYTES = 64;
29
29
  */
30
30
  const NODES_PER_TRIANGLE = 0.9;
31
31
 
32
- /** Bytes per placement in the instance table: a mat4 of world transform. */
33
- const PLACEMENT_BYTES = 64;
32
+ /**
33
+ * Bytes per placement in the instance table's own columns, measured on the Moana island (1,415 MB for 51.0M). Its
34
+ * transforms are read where the scene keeps them (an InstancedMesh's matrix list, `instanceBytes`), not copied.
35
+ */
36
+ const PLACEMENT_BYTES = 28;
37
+
38
+ /**
39
+ * Bytes per placement while a spilling build makes its TLAS: the tree, a quarter of it per copy once copies are
40
+ * clustered, plus the boxes it is built from. Measured on the island: ~2.3 GB at 51.0M.
41
+ */
42
+ const TLAS_PHASE_BYTES = 45;
34
43
 
35
44
  /** Bytes per triangle of per-template reorder map, one Uint32 each. */
36
45
  const ORDER_MAP_BYTES = 4;
@@ -59,11 +68,14 @@ export const SAFE_SCENE_BYTES = 7040 * 1024 * 1024;
59
68
  */
60
69
  export const MAX_SCENE_BYTES = 9216 * 1024 * 1024;
61
70
 
62
- // Scene-archive budgets with the memory spill on: the triangles and BVH leave the heap as the build goes, so the host
63
- // passes these instead of the importer's own. The whole 15-part Moana subset (55.7M / 7.0M) loads cold under them, at
64
- // an 8.1 GB preflight estimate; the preflight, not the budget, refuses anything larger.
65
- export const SPILL_TRIANGLE_BUDGET = 60_000_000;
66
- export const SPILL_PLACEMENT_BUDGET = 8_000_000;
71
+ // Scene-archive budgets with the memory spill on: the triangles, BVH and geometry leave the heap as the build goes, so
72
+ // the host passes these instead of the importer's own. The whole USD island (111.6M triangles, 51.0M placements) loads
73
+ // under them in Chrome on a 24 GB M-series: 304 s, tab peak ~13 GB, 13 GB of GPU buffers. The preflight, not the
74
+ // budget, refuses anything larger.
75
+ export const SPILL_TRIANGLE_BUDGET = 120_000_000;
76
+ export const SPILL_PLACEMENT_BUDGET = 60_000_000;
77
+ /** Widest environment kept with memory spill on; a wider one is box-filtered down (AssetLoader). */
78
+ export const SPILL_ENVIRONMENT_WIDTH = 8192;
67
79
 
68
80
  /**
69
81
  * The host's memory in GB: the host's own figure when it gives one, else Chrome's
@@ -96,21 +108,39 @@ const PROBE_CEILING_BYTES = 12 * 1024 * 1024 * 1024;
96
108
  * @returns {{total: number, triangles: number, bvh: number, geometry: number,
97
109
  * placements: number, orderMaps: number}} bytes
98
110
  */
99
- export function estimateSceneBytes( { triangles, placements, geometryBytes = 0 } ) {
111
+ export function estimateSceneBytes( { triangles, placements, geometryBytes = 0, instanceBytes = 0 } ) {
100
112
 
101
113
  const parts = {
102
114
  triangles: triangles * TRIANGLE_BYTES,
103
115
  bvh: Math.ceil( triangles * NODES_PER_TRIANGLE ) * BVH_NODE_BYTES,
104
116
  geometry: geometryBytes,
117
+ instances: instanceBytes,
105
118
  placements: placements * PLACEMENT_BYTES,
106
119
  orderMaps: triangles * ORDER_MAP_BYTES,
107
120
  };
108
121
 
109
- parts.total = parts.triangles + parts.bvh + parts.geometry + parts.placements + parts.orderMaps;
122
+ parts.total = parts.triangles + parts.bvh + parts.geometry + parts.instances + parts.placements + parts.orderMaps;
110
123
  return parts;
111
124
 
112
125
  }
113
126
 
127
+ /**
128
+ * A spilling, streamed build's peak: the larger of its two phases, since neither the triangles, the BLASes nor the
129
+ * three.js geometry are resident once each has been read. Extraction holds the geometry, the matrix lists and
130
+ * `residentTriangleBytes` of records; the TLAS phase the lists, the table, the tree being built and the order maps.
131
+ * @param {ReturnType<typeof estimateSceneBytes>} parts
132
+ * @param {number} placements
133
+ * @param {number} residentTriangleBytes
134
+ * @returns {number} bytes
135
+ */
136
+ export function spillingPeakBytes( parts, placements, residentTriangleBytes ) {
137
+
138
+ const extraction = parts.geometry + parts.instances + placements * 4 + residentTriangleBytes;
139
+ const tlas = parts.instances + parts.placements + placements * TLAS_PHASE_BYTES + parts.orderMaps;
140
+ return Math.max( extraction, tlas );
141
+
142
+ }
143
+
114
144
  /**
115
145
  * How many bytes this process can still place in chunk-sized pieces, right now.
116
146
  *