rayzee 9.0.0 → 9.1.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 (94) hide show
  1. package/README.md +139 -8
  2. package/dist/{NeuralRetouch-CYOJ21Wn.js → NeuralRetouch-BRquqfmZ.js} +1 -1
  3. package/dist/{NeuralSuperRes-CQLCLpcC.js → NeuralSuperRes-CbNvQjCw.js} +3 -2
  4. package/dist/{ToneMapGPU-BVE852iN.js → ToneMapGPU-DpUZDMbQ.js} +2 -1
  5. package/dist/rayzee.es.js +10344 -5334
  6. package/dist/rayzee.umd.js +265 -265
  7. package/package.json +1 -1
  8. package/src/AssetConfig.js +4 -0
  9. package/src/EngineDefaults.js +38 -56
  10. package/src/EngineEvents.js +3 -0
  11. package/src/EngineIssues.js +6 -0
  12. package/src/Headless.js +3 -1
  13. package/src/Passes/AIUpscaler.js +2 -1
  14. package/src/Passes/OIDNDenoiser.js +3 -6
  15. package/src/PathTracerApp.js +290 -19
  16. package/src/Processor/ArchiveCache.js +120 -0
  17. package/src/Processor/ArchiveReader.js +179 -32
  18. package/src/Processor/AssetLoader.js +514 -76
  19. package/src/Processor/AtmosphereModel.js +465 -0
  20. package/src/Processor/BVHBuilder.js +4 -1
  21. package/src/Processor/BVHLeafFold.js +100 -0
  22. package/src/Processor/BVHRefitter.js +98 -214
  23. package/src/Processor/ChunkedRecords.js +189 -4
  24. package/src/Processor/EmissiveTriangleBuilder.js +8 -0
  25. package/src/Processor/GeometryExtractor.js +238 -40
  26. package/src/Processor/InstanceTable.js +55 -37
  27. package/src/Processor/PBRT/PBRTCurves.js +104 -27
  28. package/src/Processor/PBRT/PBRTParser.js +132 -22
  29. package/src/Processor/PBRT/PBRTSceneBuilder.js +192 -49
  30. package/src/Processor/PBRT/buffers.js +27 -0
  31. package/src/Processor/PBRT/index.js +55 -15
  32. package/src/Processor/PackedRayBuffer.js +10 -13
  33. package/src/Processor/ParallelBVHBuilder.js +36 -15
  34. package/src/Processor/PhysicalSky.js +308 -0
  35. package/src/Processor/QueueManager.js +1 -0
  36. package/src/Processor/SceneProcessor.js +1132 -181
  37. package/src/Processor/SplitBLAS.js +249 -0
  38. package/src/Processor/SunPosition.js +41 -0
  39. package/src/Processor/TextureCreator.js +21 -0
  40. package/src/Processor/Workers/AIUpscalerWorker.js +79 -52
  41. package/src/Processor/Workers/BVHWorker.js +17 -13
  42. package/src/Processor/ZipReader.js +207 -0
  43. package/src/RenderSettings.js +38 -0
  44. package/src/SceneState/SceneState.js +367 -0
  45. package/src/SceneState/portable.js +52 -0
  46. package/src/Stages/NormalDepth.js +7 -6
  47. package/src/Stages/PathTracer.js +108 -4
  48. package/src/Stages/PathTracerStage.js +251 -104
  49. package/src/Storage/BLASCache.js +190 -0
  50. package/src/Storage/CDFCache.js +77 -0
  51. package/src/Storage/DownloadCache.js +266 -0
  52. package/src/Storage/GeometrySpill.js +217 -0
  53. package/src/Storage/SceneGraphCodec.js +503 -0
  54. package/src/Storage/SpillStore.js +112 -0
  55. package/src/Storage/StorageManager.js +934 -0
  56. package/src/Storage/StorageOps.js +137 -0
  57. package/src/Storage/StorageWorker.js +37 -0
  58. package/src/Storage/events.js +29 -0
  59. package/src/Storage/identity.js +77 -0
  60. package/src/Storage/index.js +9 -0
  61. package/src/Storage/inlineTransport.js +24 -0
  62. package/src/Storage/locks.js +99 -0
  63. package/src/Storage/openStorage.js +87 -0
  64. package/src/Storage/sceneCachePolicy.js +12 -0
  65. package/src/Storage/shared.js +9 -0
  66. package/src/Storage/transport.js +62 -0
  67. package/src/TSL/Atmosphere.js +684 -0
  68. package/src/TSL/BVHTraversal.js +288 -239
  69. package/src/TSL/Common.js +11 -2
  70. package/src/TSL/Displacement.js +6 -6
  71. package/src/TSL/EmissiveSampling.js +5 -5
  72. package/src/TSL/EnvironmentCDF.js +176 -0
  73. package/src/TSL/GenerateKernel.js +3 -3
  74. package/src/TSL/HitFacet.js +4 -4
  75. package/src/TSL/LightsDirect.js +8 -8
  76. package/src/TSL/LightsSampling.js +55 -37
  77. package/src/TSL/Random.js +1 -0
  78. package/src/TSL/ShadeKernel.js +75 -19
  79. package/src/TSL/Sun.js +40 -0
  80. package/src/TSL/TextureSampling.js +6 -7
  81. package/src/index.js +18 -0
  82. package/src/managers/CameraManager.js +169 -1
  83. package/src/managers/EnvironmentManager.js +263 -33
  84. package/src/managers/GoboManager.js +19 -8
  85. package/src/managers/IESManager.js +3 -3
  86. package/src/managers/LightManager.js +98 -5
  87. package/src/managers/MaterialDataManager.js +22 -2
  88. package/src/managers/TransformManager.js +26 -0
  89. package/src/managers/UniformManager.js +5 -3
  90. package/src/managers/VideoRenderManager.js +3 -1
  91. package/src/managers/timeline/CameraTrack.js +22 -0
  92. package/src/managers/timeline/TimelineManager.js +14 -0
  93. package/src/version.js +3 -0
  94. package/src/Processor/ProceduralSky.js +0 -291
@@ -5,6 +5,7 @@ import {
5
5
  import { ISSUE_CODES } from '../EngineIssues.js';
6
6
  import { ChunkedRecords, SHARED_MEMORY_AVAILABLE } from './ChunkedRecords.js';
7
7
  import { createLogger, fmt, warnOnce } from '../utils/Logger.js';
8
+ import { GeometrySpill } from '../Storage/GeometrySpill.js';
8
9
 
9
10
  const log = createLogger( 'geometry' );
10
11
 
@@ -257,6 +258,167 @@ export class GeometryExtractor {
257
258
 
258
259
  }
259
260
 
261
+ /**
262
+ * `extract()`, handing each stored range to `onRange( meshIndex, range )` as soon as it is
263
+ * written and awaiting `pause( triangles )` every `yieldEvery` triangles, so a build can work
264
+ * on the first meshes while the rest are read. The store is lazy and sized exactly
265
+ * ({@link _countStoredTriangles}): a chunk takes memory when first written, and the receiver
266
+ * may send it to disk once it is done with it. `releaseGeometry( geometry )` is handed each
267
+ * geometry after its last read, already compressed, unless something else still holds its arrays.
268
+ */
269
+ async extractStreaming( object, { onStore, onRange, pause, yieldEvery = 1 << 20, releaseGeometry = null } ) {
270
+
271
+ this.resetArrays();
272
+ this._triangleCapacity = Math.max( 1, this._countStoredTriangles( object ) );
273
+ this._allocateTriangles( this._triangleCapacity, true );
274
+ onStore?.( this.triangles );
275
+ this.currentTriangleIndex = 0;
276
+ this._allocatePlacements( this._countPlacements( object ) );
277
+ this._geometryUses = this._countGeometryUses( object );
278
+ this.bakeInverse = new Map();
279
+ this._onRange = onRange;
280
+ this._releaseAfterRead = releaseGeometry ? this._geometryReleaser( object, releaseGeometry ) : null;
281
+
282
+ try {
283
+
284
+ // Depth-first pre-order, as traverseObject: mesh indices follow the same walk.
285
+ const stack = [ object ];
286
+ let yielded = 0;
287
+ while ( stack.length ) {
288
+
289
+ const node = stack.pop();
290
+ this._visit( node );
291
+ if ( node.children ) for ( let i = node.children.length - 1; i >= 0; i -- ) stack.push( node.children[ i ] );
292
+ if ( this.currentTriangleIndex - yielded >= yieldEvery ) {
293
+
294
+ yielded = this.currentTriangleIndex;
295
+ await pause( this.currentTriangleIndex );
296
+
297
+ }
298
+
299
+ }
300
+
301
+ } finally {
302
+
303
+ this._onRange = null;
304
+ this._releaseAfterRead = null;
305
+
306
+ }
307
+
308
+ this._shareInstanceMatrices();
309
+ this._compressAttributes( object );
310
+ this.triangles.trimInPlace( this.currentTriangleIndex );
311
+ this._streamedStore = true;
312
+
313
+ this.logStats();
314
+ return this.getExtractedData();
315
+
316
+ }
317
+
318
+ /**
319
+ * Triangles `extract()` will store: a geometry shared under one material once, an emissive
320
+ * instanced mesh once per instance (it is expanded), everything else once per mesh.
321
+ * @private
322
+ */
323
+ _countStoredTriangles( object ) {
324
+
325
+ const seen = new Set();
326
+ let count = 0;
327
+ const walk = node => {
328
+
329
+ if ( node.isMesh && node.geometry && node.material ) {
330
+
331
+ const g = node.geometry;
332
+ const tris = Math.ceil( ( g.index ? g.index.count : ( g.attributes.position?.count ?? 0 ) ) / 3 );
333
+ const material = Array.isArray( node.material ) ? node.material[ 0 ] : node.material;
334
+ const e = material?.emissive;
335
+ const emissive = material?.emissiveIntensity > 0 && !! e && ( e.r > 0 || e.g > 0 || e.b > 0 );
336
+ const instances = node.isInstancedMesh ? ( node.count ?? 0 ) : 1;
337
+
338
+ if ( emissive && node.isInstancedMesh && ! isDeformable( node ) && tris * instances <= MAX_EXPANDED_EMISSIVE_TRIANGLES ) count += tris * instances;
339
+ else if ( emissive || isDeformable( node ) ) count += tris;
340
+ else {
341
+
342
+ const key = `${g.uuid}|${material?.uuid}`;
343
+ if ( ! seen.has( key ) ) {
344
+
345
+ seen.add( key );
346
+ count += tris;
347
+
348
+ }
349
+
350
+ }
351
+
352
+ }
353
+
354
+ if ( node.children ) for ( const child of node.children ) walk( child );
355
+
356
+ };
357
+
358
+ walk( object );
359
+ return count;
360
+
361
+ }
362
+
363
+ /**
364
+ * Counts each geometry's reads, and returns what to call after each mesh: on a geometry's
365
+ * last read it is compressed and handed to `release`. Never a host's geometry, a deforming
366
+ * one, one GeometrySpill cannot swap, or one sharing an array with another geometry.
367
+ * @private
368
+ */
369
+ _geometryReleaser( object, release ) {
370
+
371
+ const reads = new Map();
372
+ const holders = new Map();
373
+ const kept = new Set();
374
+
375
+ const walk = ( node, external ) => {
376
+
377
+ external ||= !! node.userData?.__rayzeeExternal;
378
+ const g = node.isMesh && node.material ? node.geometry : null;
379
+ if ( g ) {
380
+
381
+ if ( ! reads.has( g ) ) {
382
+
383
+ if ( ! GeometrySpill.canTake( g ) ) kept.add( g );
384
+ for ( const attribute of [ ...Object.values( g.attributes ), g.index ] ) {
385
+
386
+ const array = attribute?.array;
387
+ if ( ! array ) continue;
388
+ const holder = holders.get( array );
389
+ if ( holder && holder !== g ) kept.add( g ).add( holder );
390
+ else holders.set( array, g );
391
+
392
+ }
393
+
394
+ }
395
+
396
+ reads.set( g, ( reads.get( g ) ?? 0 ) + 1 );
397
+ if ( external || isDeformable( node ) ) kept.add( g );
398
+
399
+ }
400
+
401
+ if ( node.children ) for ( const child of node.children ) walk( child, external );
402
+
403
+ };
404
+
405
+ walk( object, false );
406
+
407
+ return mesh => {
408
+
409
+ const g = mesh.material ? mesh.geometry : null;
410
+ if ( ! g || ! reads.has( g ) ) return;
411
+ const left = reads.get( g ) - 1;
412
+ if ( left > 0 ) return void reads.set( g, left );
413
+ reads.delete( g );
414
+ if ( kept.has( g ) ) return;
415
+ this._compressGeometry( g );
416
+ release( g );
417
+
418
+ };
419
+
420
+ }
421
+
260
422
  /**
261
423
  * The sizes a memory preflight needs, without doing any of the work: what `extract()` would
262
424
  * store, plus the three.js geometry already resident behind it.
@@ -354,8 +516,6 @@ export class GeometryExtractor {
354
516
  */
355
517
  _compressAttributes( object ) {
356
518
 
357
- const done = this._compressed;
358
-
359
519
  // A host's own Object3D is rendered as a copy, but the copy shares its geometry by
360
520
  // reference, so rewriting an attribute there would rewrite the host's data. Its whole
361
521
  // subtree is skipped rather than the root alone.
@@ -369,44 +529,50 @@ export class GeometryExtractor {
369
529
 
370
530
  walk( object, o => {
371
531
 
372
- const g = o.geometry;
373
- if ( ! o.isMesh || ! g || done.has( g.uuid ) ) return;
374
- done.add( g.uuid );
532
+ if ( o.isMesh && o.geometry ) this._compressGeometry( o.geometry );
375
533
 
376
- for ( const name of [ 'normal', 'tangent', 'color' ] ) {
377
-
378
- const attr = g.getAttribute( name );
379
- if ( ! attr || attr.normalized || ! ( attr.array instanceof Float32Array ) ) continue;
380
- if ( g.morphAttributes?.[ name ]?.length ) continue;
534
+ } );
381
535
 
382
- const src = attr.array;
383
- let ok = true;
384
- for ( let i = 0; i < src.length; i ++ ) {
536
+ }
385
537
 
386
- const v = src[ i ];
387
- if ( ! ( v >= - 1.0001 && v <= 1.0001 ) ) {
538
+ /** One geometry's share of {@link _compressAttributes}; each geometry is done once. @private */
539
+ _compressGeometry( g ) {
388
540
 
389
- ok = false; break;
541
+ if ( this._compressed.has( g.uuid ) ) return;
542
+ this._compressed.add( g.uuid );
390
543
 
391
- }
544
+ for ( const name of [ 'normal', 'tangent', 'color' ] ) {
392
545
 
393
- }
546
+ const attr = g.getAttribute( name );
547
+ if ( ! attr || attr.normalized || ! ( attr.array instanceof Float32Array ) ) continue;
548
+ if ( g.morphAttributes?.[ name ]?.length ) continue;
394
549
 
395
- // Out of snorm range (or NaN) — leave it as floats rather than silently clamp.
396
- if ( ! ok ) continue;
550
+ const src = attr.array;
551
+ let ok = true;
552
+ for ( let i = 0; i < src.length; i ++ ) {
397
553
 
398
- const packed = new Int16Array( src.length );
399
- for ( let i = 0; i < src.length; i ++ ) {
554
+ const v = src[ i ];
555
+ if ( ! ( v >= - 1.0001 && v <= 1.0001 ) ) {
400
556
 
401
- packed[ i ] = Math.round( Math.max( - 1, Math.min( 1, src[ i ] ) ) * 32767 );
557
+ ok = false; break;
402
558
 
403
559
  }
404
560
 
405
- g.setAttribute( name, new BufferAttribute( packed, attr.itemSize, true ) );
561
+ }
562
+
563
+ // Out of snorm range (or NaN) — leave it as floats rather than silently clamp.
564
+ if ( ! ok ) continue;
565
+
566
+ const packed = new Int16Array( src.length );
567
+ for ( let i = 0; i < src.length; i ++ ) {
568
+
569
+ packed[ i ] = Math.round( Math.max( - 1, Math.min( 1, src[ i ] ) ) * 32767 );
406
570
 
407
571
  }
408
572
 
409
- } );
573
+ g.setAttribute( name, new BufferAttribute( packed, attr.itemSize, true ) );
574
+
575
+ }
410
576
 
411
577
  }
412
578
 
@@ -462,6 +628,15 @@ export class GeometryExtractor {
462
628
 
463
629
  if ( needed <= this._triangleCapacity ) return;
464
630
 
631
+ // A streamed store may already have chunks on disk: it grows where it stands.
632
+ if ( this._onRange ) {
633
+
634
+ this.triangles.grow( needed );
635
+ this._triangleCapacity = needed;
636
+ return;
637
+
638
+ }
639
+
465
640
  const previous = this.triangles;
466
641
  const used = this.currentTriangleIndex;
467
642
  this._allocateTriangles( needed );
@@ -486,11 +661,11 @@ export class GeometryExtractor {
486
661
  * V8 ArrayBuffer cap, well below what the GPU buffer can take.
487
662
  * @private
488
663
  */
489
- _allocateTriangles( capacity ) {
664
+ _allocateTriangles( capacity, lazy = false ) {
490
665
 
491
666
  // Shared-backed so a refit reads and writes them in place; copying them into shared memory
492
667
  // on first refit would be a second copy of the largest thing in the scene.
493
- this.triangles = new ChunkedRecords(
668
+ this.triangles = ( lazy ? ChunkedRecords.lazy : ( ...a ) => new ChunkedRecords( ...a ) )(
494
669
  capacity, TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE, Uint32Array,
495
670
  undefined, SHARED_MEMORY_AVAILABLE
496
671
  );
@@ -502,10 +677,28 @@ export class GeometryExtractor {
502
677
 
503
678
  traverseObject( object ) {
504
679
 
505
- // Process the current object
680
+ this._visit( object );
681
+
682
+ // Process children recursively
683
+ if ( object.children ) {
684
+
685
+ for ( const child of object.children ) {
686
+
687
+ this.traverseObject( child );
688
+
689
+ }
690
+
691
+ }
692
+
693
+ }
694
+
695
+ /** @private */
696
+ _visit( object ) {
697
+
506
698
  if ( object.isMesh ) {
507
699
 
508
700
  this.processMesh( object );
701
+ this._releaseAfterRead?.( object );
509
702
 
510
703
  } else if ( object.isDirectionalLight ) {
511
704
 
@@ -517,17 +710,6 @@ export class GeometryExtractor {
517
710
 
518
711
  }
519
712
 
520
- // Process children recursively
521
- if ( object.children ) {
522
-
523
- for ( const child of object.children ) {
524
-
525
- this.traverseObject( child );
526
-
527
- }
528
-
529
- }
530
-
531
713
  }
532
714
 
533
715
  processMesh( mesh ) {
@@ -602,6 +784,7 @@ export class GeometryExtractor {
602
784
  this.meshTriangleRanges.push( range );
603
785
  if ( range.count > 0 && ! bake && shareable ) this._geometryRanges.set( key, { ...range, meshIndex } );
604
786
  this._recordPlacements( mesh, meshIndex, bake, expand );
787
+ if ( range.count > 0 ) this._onRange?.( meshIndex, range );
605
788
 
606
789
  }
607
790
 
@@ -1209,10 +1392,24 @@ export class GeometryExtractor {
1209
1392
  }
1210
1393
 
1211
1394
  /** The filled triangle records, as one chunk when they fit and several when they do not. */
1395
+ /**
1396
+ * Lets go of the triangle store once its owner has taken it. The extractor's own reference
1397
+ * (and its float view) kept every chunk alive, so a spilled chunk never left memory.
1398
+ */
1399
+ releaseTriangles() {
1400
+
1401
+ this.triangles = null;
1402
+ this.triangleData = null;
1403
+ this.triangleFloatChunks = null;
1404
+ this.triangleFloats = null;
1405
+
1406
+ }
1407
+
1212
1408
  getTriangleData() {
1213
1409
 
1214
1410
  if ( ! this.triangles ) return null;
1215
- return this.triangles.trimTo( this.currentTriangleIndex );
1411
+ // A streamed store is trimmed where it stands: its receiver holds chunks of it on disk.
1412
+ return this._streamedStore ? this.triangles : this.triangles.trimTo( this.currentTriangleIndex );
1216
1413
 
1217
1414
  }
1218
1415
 
@@ -1308,6 +1505,7 @@ export class GeometryExtractor {
1308
1505
 
1309
1506
  // Reset triangle data
1310
1507
  this.triangles = null;
1508
+ this._streamedStore = false;
1311
1509
  this.triangleData = null;
1312
1510
  this.triangleFloatChunks = null;
1313
1511
  this.triangleFloats = null;
@@ -524,13 +524,18 @@ export class InstanceTable {
524
524
  *
525
525
  * @param {Float32Array} triangleData - Global triangle data (needed for leaf-root fallback)
526
526
  */
527
- computeAABBs( triangleData ) {
527
+ /**
528
+ * @param {Object} triangleData
529
+ * @param {{owners?: boolean}} [options] - `owners: false` when each owner's bounds were taken
530
+ * as its BLAS landed (a build that spills its BLASes keeps none of them here)
531
+ */
532
+ computeAABBs( triangleData, { owners = true } = {} ) {
528
533
 
529
- for ( let t = 0; t < this.templateCount; t ++ ) {
534
+ for ( let t = 0; owners && t < this.templateCount; t ++ ) {
530
535
 
531
536
  if ( this.tplOwner[ t ] < 0 ) continue;
532
537
  const blas = this.blasData.get( t );
533
- if ( blas ) this._readRootAABB( blas, t, triangleData, this.tplObjectAABB, t * 6 );
538
+ if ( blas ) this._readRootAABB( Array.isArray( blas ) ? blas[ 0 ] : blas, t, triangleData, this.tplObjectAABB, t * 6 );
534
539
 
535
540
  }
536
541
 
@@ -574,10 +579,17 @@ export class InstanceTable {
574
579
  */
575
580
  _readRootAABB( bvhData, template, triangleData, out, off ) {
576
581
 
582
+ InstanceTable.rootAABB( bvhData, triangleData, this.tplTriOffset[ template ], this.tplTriCount[ template ], out, off );
583
+
584
+ }
585
+
586
+ /** A BLAS's object-space bounds, from its root node or, for a leaf root, its triangles. */
587
+ static rootAABB( bvhData, triangleData, triOffset, triCount, out, off ) {
588
+
577
589
  if ( ! bvhData || bvhIndexView( bvhData )[ 3 ] === BVH_LEAF_MARKERS.TRIANGLE_LEAF ) {
578
590
 
579
591
  // Root is a leaf — very small mesh. Scan its triangles.
580
- this._computeAABBFromTriangles( template, triangleData, out, off );
592
+ aabbOfTriangles( triangleData, triOffset, triCount, out, off );
581
593
  return;
582
594
 
583
595
  }
@@ -598,39 +610,7 @@ export class InstanceTable {
598
610
  */
599
611
  _computeAABBFromTriangles( template, triangleData, out, off ) {
600
612
 
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 ) );
607
- let minX = Infinity, minY = Infinity, minZ = Infinity;
608
- let maxX = - Infinity, maxY = - Infinity, maxZ = - Infinity;
609
-
610
- const triOffset = this.tplTriOffset[ template ], triCount = this.tplTriCount[ template ];
611
- for ( let t = 0; t < triCount; t ++ ) {
612
-
613
- const gi = triOffset + t;
614
- const tri = chunked ? chunked.chunkFor( gi ) : flat;
615
- const base = chunked ? chunked.baseOf( gi ) : gi * FPT;
616
-
617
- // Positions A (offset 0), B (offset 4), C (offset 8)
618
- for ( let o = 0; o <= 8; o += 4 ) {
619
-
620
- const x = tri[ base + o ], y = tri[ base + o + 1 ], z = tri[ base + o + 2 ];
621
- if ( x < minX ) minX = x;
622
- if ( y < minY ) minY = y;
623
- if ( z < minZ ) minZ = z;
624
- if ( x > maxX ) maxX = x;
625
- if ( y > maxY ) maxY = y;
626
- if ( z > maxZ ) maxZ = z;
627
-
628
- }
629
-
630
- }
631
-
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;
613
+ aabbOfTriangles( triangleData, this.tplTriOffset[ template ], this.tplTriCount[ template ], out, off );
634
614
 
635
615
  }
636
616
 
@@ -683,3 +663,41 @@ export class InstanceTable {
683
663
  }
684
664
 
685
665
  }
666
+
667
+ /** Bounds of triangles [triOffset, triOffset + triCount) of a flat or chunked store, into out[off..off+5]. */
668
+ function aabbOfTriangles( triangleData, triOffset, triCount, out, off ) {
669
+
670
+ const FPT = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
671
+ // Flat array or ChunkedRecords: resolve the chunk once per triangle either way.
672
+ const chunked = triangleData && triangleData.chunks ? triangleData.viewAs( Float32Array ) : null;
673
+ const flat = chunked ? null : ( triangleData instanceof Float32Array
674
+ ? triangleData
675
+ : new Float32Array( triangleData.buffer, triangleData.byteOffset, triangleData.length ) );
676
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
677
+ let maxX = - Infinity, maxY = - Infinity, maxZ = - Infinity;
678
+
679
+ for ( let t = 0; t < triCount; t ++ ) {
680
+
681
+ const gi = triOffset + t;
682
+ const tri = chunked ? chunked.chunkFor( gi ) : flat;
683
+ const base = chunked ? chunked.baseOf( gi ) : gi * FPT;
684
+
685
+ // Positions A (offset 0), B (offset 4), C (offset 8)
686
+ for ( let o = 0; o <= 8; o += 4 ) {
687
+
688
+ const x = tri[ base + o ], y = tri[ base + o + 1 ], z = tri[ base + o + 2 ];
689
+ if ( x < minX ) minX = x;
690
+ if ( y < minY ) minY = y;
691
+ if ( z < minZ ) minZ = z;
692
+ if ( x > maxX ) maxX = x;
693
+ if ( y > maxY ) maxY = y;
694
+ if ( z > maxZ ) maxZ = z;
695
+
696
+ }
697
+
698
+ }
699
+
700
+ out[ off ] = minX; out[ off + 1 ] = minY; out[ off + 2 ] = minZ;
701
+ out[ off + 3 ] = maxX; out[ off + 4 ] = maxY; out[ off + 5 ] = maxZ;
702
+
703
+ }