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
@@ -14,6 +14,7 @@
14
14
  * needed. Enable only if a scene comes out z-mirrored against a known reference.
15
15
  */
16
16
 
17
+ import { freeNow, resized } from './buffers.js';
17
18
  import {
18
19
  Group, Mesh, InstancedMesh, PerspectiveCamera, OrthographicCamera, Matrix4, Vector3, Quaternion,
19
20
  BufferGeometry, Float32BufferAttribute, Uint32BufferAttribute, SphereGeometry,
@@ -50,9 +51,7 @@ function grow( array, needed ) {
50
51
  if ( needed <= array.length ) return array;
51
52
  let length = array.length || 1;
52
53
  while ( length < needed ) length *= 2;
53
- const out = new array.constructor( length );
54
- out.set( array );
55
- return out;
54
+ return resized( array, length );
56
55
 
57
56
  }
58
57
 
@@ -76,6 +75,11 @@ const DEFAULT_TRIANGLE_BUDGET = 45_000_000;
76
75
  // Placements are their own budget: geometry is shared, but each costs a TLAS leaf and an
77
76
  // instance record. isCoastline's 5.09M loaded and rendered at 23 fps.
78
77
  const DEFAULT_PLACEMENT_BUDGET = 6_000_000;
78
+ // With the memory spill the triangles and BVH leave the heap as the build goes, so the host
79
+ // passes these instead. The whole 15-part Moana subset (55.7M / 7.0M) loads cold under them, at
80
+ // an 8.1 GB preflight estimate; the preflight, not the budget, refuses anything larger.
81
+ export const SPILL_TRIANGLE_BUDGET = 60_000_000;
82
+ export const SPILL_PLACEMENT_BUDGET = 8_000_000;
79
83
  // Non-instanced shapes merge into a few large meshes: isPalmRig declares 173,338 of them,
80
84
  // and one BufferGeometry + Mesh each measured 3.3 GB — the loader OOM'd before tracing.
81
85
  const DEFAULT_MERGE_MIN_SHAPES = 256;
@@ -86,6 +90,20 @@ const MERGE_BATCH_TRIANGLES = 500_000;
86
90
  // a crossed pair of ribbons is 16 triangles a curve.
87
91
  const DEFAULT_CURVE_STEPS = 2;
88
92
  const DEFAULT_CURVE_SIDES = { flat: 1, ribbon: 1, cylinder: 2 };
93
+ // A strip segment may stray 5 % of the half-width from the curve: 37 % of Moana's curve
94
+ // triangles go, at 3 % frame time in a view full of them. 0.1 took 48 % for 6 %.
95
+ const DEFAULT_CURVE_TOLERANCE = 0.05;
96
+
97
+ /** Bumped whenever the same scene files build a different graph, so a stored graph is not reused. */
98
+ export const PBRT_BUILD_REVISION = 4;
99
+
100
+ function samePlacements( a, b ) {
101
+
102
+ if ( a.count !== b.count ) return false;
103
+ for ( let i = 0, n = a.count * 16; i < n; i ++ ) if ( a.matrices[ i ] !== b.matrices[ i ] ) return false;
104
+ return true;
105
+
106
+ }
89
107
 
90
108
  /** Pixels out of an HTMLImageElement / ImageBitmap, which expose none of their own. */
91
109
  function pixelsFromDrawable( image, srgb ) {
@@ -160,6 +178,7 @@ export class PBRTSceneBuilder {
160
178
  this.mergeShapesAbove = resolvers.mergeShapesAbove ?? DEFAULT_MERGE_MIN_SHAPES;
161
179
  this.curveSteps = resolvers.curveSteps ?? DEFAULT_CURVE_STEPS;
162
180
  this.curveSides = resolvers.curveSides ?? null;
181
+ this.curveTolerance = resolvers.curveTolerance ?? DEFAULT_CURVE_TOLERANCE;
163
182
  this.resolvePLY = resolvers.resolvePLY || ( async () => null );
164
183
  this.resolveImage = resolvers.resolveImage || ( async () => null );
165
184
  this.resolveEnvironment = resolvers.resolveEnvironment || resolvers.resolveImage || ( async () => null );
@@ -189,6 +208,9 @@ export class PBRTSceneBuilder {
189
208
  async build( ir ) {
190
209
 
191
210
  this.ir = ir;
211
+ // Built again for their moving placements after the static ones, so never freed early.
212
+ this._keepShapes = new Set();
213
+ for ( const { name } of ir.animatedInstances ?? [] ) for ( const shape of ir.objects.get( name ) ?? [] ) this._keepShapes.add( shape );
192
214
  this._recoveredColors = 0;
193
215
  this.reportedMeshes = 0;
194
216
  this.triangleCount = 0; // stored triangles — a shared geometry counts once
@@ -333,9 +355,53 @@ export class PBRTSceneBuilder {
333
355
  const scratch = new Float64Array( 16 );
334
356
  const matrix = new Matrix4();
335
357
 
336
- for ( const [ name, list ] of ir.instances ) {
358
+ const place = ( list, geometry, material, rel, shapeType, materialType, tris ) => {
359
+
360
+ const affordable = Math.max( 0, this.maxPlacements - this.placementCount );
361
+ const count = Math.min( list.count, affordable );
362
+ if ( count < list.count ) this.skippedForBudget += list.count - count;
363
+ if ( count === 0 ) return;
364
+
365
+ const mesh = new InstancedMesh( geometry, material, count );
366
+ mesh.name = `instance_${n ++}`;
367
+ mesh.frustumCulled = false;
368
+
369
+ for ( let i = 0; i < count; i ++ ) {
370
+
371
+ if ( rel ) M.multiplyInto( scratch, list.matrices, i * 16, rel );
372
+ else for ( let e = 0; e < 16; e ++ ) scratch[ e ] = list.matrices[ i * 16 + e ];
373
+ mesh.setMatrixAt( i, matrix.fromArray( this.convertHandedness ? M.multiply( FLIP_Z, scratch ) : scratch ) );
374
+
375
+ }
376
+
377
+ mesh.instanceMatrix.needsUpdate = true;
378
+ group.add( mesh );
379
+
380
+ this.placementCount += count;
381
+ this.reportedMeshes += count;
382
+ if ( this.report.length < MAX_REPORT_ROWS ) this.report.push( {
383
+ mesh: `${mesh.name} ×${count}`,
384
+ shape: shapeType,
385
+ material: materialType,
386
+ color: '#' + material.color.getHexString(),
387
+ map: material.map ? 'yes' : '-',
388
+ uv: geometry.getAttribute( 'uv' ) ? 'yes' : 'NO',
389
+ normals: geometry.getAttribute( 'normal' ) ? 'yes' : 'NO',
390
+ emissive: material.emissiveIntensity > 0 ? `#${material.emissive.getHexString()}×${material.emissiveIntensity}` : '-',
391
+ size: `instanced`,
392
+ tris,
393
+ } );
394
+
395
+ };
396
+
397
+ const placeBatch = ( list, batch ) => {
398
+
399
+ if ( batch && batch.triangles > 0 ) place( list, this._batchGeometry( batch ), batch.material, null, 'merged', '-', batch.triangles );
400
+
401
+ };
402
+
403
+ for ( const { name, list, template } of this._samePlacementGroups( ir ) ) {
337
404
 
338
- const template = ir.objects.get( name );
339
405
  if ( ! template ) {
340
406
 
341
407
  // Counted, not just warned: these are placements that silently leave the scene,
@@ -346,6 +412,13 @@ export class PBRTSceneBuilder {
346
412
 
347
413
  }
348
414
 
415
+ // A template's own small shapes share its placements, so they merge in its space: the
416
+ // eight Pandanus trees in Moana hold 22,965 leaves each. A .ply may be shared with
417
+ // other templates and stays whole.
418
+ let inline = 0;
419
+ for ( const shape of template ) if ( shape.type !== 'plymesh' ) inline ++;
420
+ const batches = inline >= 2 ? new Map() : null;
421
+
349
422
  for ( const shape of template ) {
350
423
 
351
424
  if ( this._overBudget() ) {
@@ -361,47 +434,80 @@ export class PBRTSceneBuilder {
361
434
  ] );
362
435
  if ( ! geometry ) continue;
363
436
 
364
- const tris = this._accountGeometry( geometry );
365
- const affordable = Math.max( 0, this.maxPlacements - this.placementCount );
366
- const count = Math.min( list.count, affordable );
367
- if ( count < list.count ) this.skippedForBudget += list.count - count;
368
- if ( count === 0 ) continue;
437
+ if ( batches && shape.type !== 'plymesh' && geometry.getAttribute( 'position' ).count <= MERGE_VERTEX_LIMIT ) {
438
+
439
+ this._geometryCache.delete( shape );
440
+ const full = this._appendToBatch( batches, shape, geometry, sharedMaterial, shape.relativeCTM || shape.ctm );
441
+ this._releaseMerged( shape, geometry );
442
+ placeBatch( list, full );
443
+ continue;
369
444
 
445
+ }
446
+
447
+ const tris = this._accountGeometry( geometry );
370
448
  const material = this._materialForGeometry( shape, geometry, sharedMaterial, `instances_${name}` );
371
- const mesh = new InstancedMesh( geometry, material, count );
372
- mesh.name = `instance_${n ++}`;
373
- mesh.frustumCulled = false;
449
+ place( list, geometry, material, shape.relativeCTM || shape.ctm, shape.type, shape.material?.type || 'diffuse', tris );
374
450
 
375
- const rel = shape.relativeCTM || shape.ctm;
376
- for ( let i = 0; i < count; i ++ ) {
451
+ }
377
452
 
378
- M.multiplyInto( scratch, list.matrices, i * 16, rel );
379
- mesh.setMatrixAt( i, matrix.fromArray( this.convertHandedness ? M.multiply( FLIP_Z, scratch ) : scratch ) );
453
+ if ( batches ) for ( const slot of batches.values() ) {
380
454
 
381
- }
455
+ placeBatch( list, slot[ 0 ] );
456
+ placeBatch( list, slot[ 1 ] );
457
+
458
+ }
459
+
460
+ // Copied into each InstancedMesh: 576 MB at 9M placements, held to the end otherwise.
461
+ freeNow( list.matrices );
462
+ freeNow( list.matricesEnd );
463
+
464
+ }
465
+
466
+ }
467
+
468
+ /**
469
+ * Templates placed at exactly the same transforms, as one template. Each Pandanus tree
470
+ * includes ten leaf files; kept apart they were ten overlapping instances a ray entered
471
+ * one after another, and the tree rendered 60 % slower than with its leaves world-baked.
472
+ * @private
473
+ */
474
+ _samePlacementGroups( ir ) {
382
475
 
383
- mesh.instanceMatrix.needsUpdate = true;
384
- group.add( mesh );
385
-
386
- this.placementCount += count;
387
- this.reportedMeshes += count;
388
- if ( this.report.length < MAX_REPORT_ROWS ) this.report.push( {
389
- mesh: `${mesh.name} ×${count}`,
390
- shape: shape.type,
391
- material: shape.material?.type || 'diffuse',
392
- color: '#' + material.color.getHexString(),
393
- map: material.map ? 'yes' : '-',
394
- uv: geometry.getAttribute( 'uv' ) ? 'yes' : 'NO',
395
- normals: geometry.getAttribute( 'normal' ) ? 'yes' : 'NO',
396
- emissive: material.emissiveIntensity > 0 ? `#${material.emissive.getHexString()}×${material.emissiveIntensity}` : '-',
397
- size: `instanced`,
398
- tris,
399
- } );
476
+ const groups = [];
477
+ const buckets = new Map();
478
+
479
+ for ( const [ name, list ] of ir.instances ) {
480
+
481
+ const template = ir.objects.get( name );
482
+ if ( ! template || list.matricesEnd ) {
483
+
484
+ groups.push( { name, list, template } );
485
+ continue;
486
+
487
+ }
488
+
489
+ const key = `${list.count}:${Array.prototype.join.call( list.matrices.subarray( 0, 16 ), ',' )}`;
490
+ let bucket = buckets.get( key );
491
+ if ( ! bucket ) buckets.set( key, bucket = [] );
492
+
493
+ const same = bucket.find( g => samePlacements( g.list, list ) );
494
+ if ( same ) {
495
+
496
+ if ( same.template === ir.objects.get( same.name ) ) same.template = same.template.slice();
497
+ for ( const shape of template ) same.template.push( shape );
498
+ freeNow( list.matrices );
499
+ continue;
400
500
 
401
501
  }
402
502
 
503
+ const group = { name, list, template };
504
+ bucket.push( group );
505
+ groups.push( group );
506
+
403
507
  }
404
508
 
509
+ return groups;
510
+
405
511
  }
406
512
 
407
513
  /** Moving placements, one Group each: an InstancedMesh cannot move a single instance. */
@@ -637,6 +743,35 @@ export class PBRTSceneBuilder {
637
743
  */
638
744
  _mergeShape( shape, geometry, sharedMaterial, group ) {
639
745
 
746
+ this.reportedMeshes ++;
747
+ const world = this.convertHandedness ? M.multiply( FLIP_Z, shape.ctm ) : shape.ctm;
748
+ const full = this._appendToBatch( this._batches, shape, geometry, sharedMaterial, world );
749
+ this._releaseMerged( shape, geometry );
750
+ if ( full ) this._flushBatch( full, group );
751
+
752
+ }
753
+
754
+ /**
755
+ * A merged shape's arrays are in its batch now and nothing reads it again: freed at once, not at
756
+ * a GC the build reaches with every one still held — isIronwoodA1's leaves were 3.9 GB twice over.
757
+ * @private
758
+ */
759
+ _releaseMerged( shape, geometry ) {
760
+
761
+ if ( this._keepShapes?.has( shape ) ) return;
762
+ for ( const name in geometry.attributes ) freeNow( geometry.attributes[ name ].array );
763
+ freeNow( geometry.index?.array );
764
+ for ( const name in shape.params ) if ( ArrayBuffer.isView( shape.params[ name ]?.value ) ) freeNow( shape.params[ name ].value );
765
+
766
+ }
767
+
768
+ /**
769
+ * Copy one shape's triangles, moved by `transform`, into the batch for its material.
770
+ * @returns {object|null} the batch, taken out of `batches`, once it is full
771
+ * @private
772
+ */
773
+ _appendToBatch( batches, shape, geometry, sharedMaterial, transform ) {
774
+
640
775
  const material = this._materialForGeometry( shape, geometry, sharedMaterial, 'merged shape' );
641
776
  const position = geometry.getAttribute( 'position' );
642
777
  const normal = geometry.getAttribute( 'normal' );
@@ -646,11 +781,10 @@ export class PBRTSceneBuilder {
646
781
 
647
782
  // Charged directly: a merged shape stores its own copy even when two share one .ply.
648
783
  this.triangleCount += ( index ? index.count : vertices ) / 3;
649
- this.reportedMeshes ++;
650
784
  this.mergedShapes ++;
651
785
 
652
- let slot = this._batches.get( material );
653
- if ( ! slot ) this._batches.set( material, slot = [ null, null ] );
786
+ let slot = batches.get( material );
787
+ if ( ! slot ) batches.set( material, slot = [ null, null ] );
654
788
  const k = uv ? 1 : 0;
655
789
  let batch = slot[ k ];
656
790
  if ( ! batch ) slot[ k ] = batch = {
@@ -662,7 +796,7 @@ export class PBRTSceneBuilder {
662
796
  vertexCount: 0, indexCount: 0, triangles: 0
663
797
  };
664
798
 
665
- const world = this.convertHandedness ? M.multiply( FLIP_Z, shape.ctm ) : shape.ctm;
799
+ const world = transform;
666
800
  const inv = M.invert( world );
667
801
  const base = batch.vertexCount;
668
802
 
@@ -711,11 +845,26 @@ export class PBRTSceneBuilder {
711
845
 
712
846
  if ( batch.triangles >= MERGE_BATCH_TRIANGLES ) {
713
847
 
714
- this._flushBatch( batch, group );
715
848
  slot[ k ] = null;
849
+ return batch;
716
850
 
717
851
  }
718
852
 
853
+ return null;
854
+
855
+ }
856
+
857
+ /** A batch's triangles as one geometry; the batch's own arrays are released. @private */
858
+ _batchGeometry( batch ) {
859
+
860
+ const geometry = new BufferGeometry();
861
+ geometry.setAttribute( 'position', new Float32BufferAttribute( resized( batch.positions, batch.vertexCount * 3 ), 3 ) );
862
+ geometry.setAttribute( 'normal', new Float32BufferAttribute( resized( batch.normals, batch.vertexCount * 3 ), 3 ) );
863
+ if ( batch.uvs ) geometry.setAttribute( 'uv', new Float32BufferAttribute( resized( batch.uvs, batch.vertexCount * 2 ), 2 ) );
864
+ geometry.setIndex( new Uint32BufferAttribute( resized( batch.indices, batch.indexCount ), 1 ) );
865
+ batch.positions = batch.normals = batch.uvs = batch.indices = null;
866
+ return geometry;
867
+
719
868
  }
720
869
 
721
870
  /** Turn one accumulated batch into a single Mesh at the scene origin. @private */
@@ -723,12 +872,7 @@ export class PBRTSceneBuilder {
723
872
 
724
873
  if ( ! batch || batch.triangles === 0 ) return;
725
874
 
726
- const geometry = new BufferGeometry();
727
- geometry.setAttribute( 'position', new Float32BufferAttribute( batch.positions.slice( 0, batch.vertexCount * 3 ), 3 ) );
728
- geometry.setAttribute( 'normal', new Float32BufferAttribute( batch.normals.slice( 0, batch.vertexCount * 3 ), 3 ) );
729
- if ( batch.uvs ) geometry.setAttribute( 'uv', new Float32BufferAttribute( batch.uvs.slice( 0, batch.vertexCount * 2 ), 2 ) );
730
- geometry.setIndex( new Uint32BufferAttribute( batch.indices.slice( 0, batch.indexCount ), 1 ) );
731
-
875
+ const geometry = this._batchGeometry( batch );
732
876
  const mesh = new Mesh( geometry, batch.material );
733
877
  mesh.name = `merged_${this._mergedBatches ++}`;
734
878
  mesh.frustumCulled = false;
@@ -748,8 +892,6 @@ export class PBRTSceneBuilder {
748
892
  tris: batch.triangles
749
893
  } );
750
894
 
751
- batch.positions = batch.normals = batch.uvs = batch.indices = null;
752
-
753
895
  }
754
896
 
755
897
  _buildGeometry( shape ) {
@@ -833,7 +975,8 @@ export class PBRTSceneBuilder {
833
975
  width1: pFloat( params, 'width1', width ),
834
976
  N: params.N?.value || null,
835
977
  steps: this.curveSteps,
836
- sides: this.curveSides ?? DEFAULT_CURVE_SIDES[ type ] ?? 1
978
+ sides: this.curveSides ?? DEFAULT_CURVE_SIDES[ type ] ?? 1,
979
+ tolerance: this.curveTolerance
837
980
  } );
838
981
 
839
982
  if ( ! built ) {
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A dropped ArrayBuffer is freed at the next major GC, which a parse allocating gigabytes of
3
+ * scene text and growing arrays reaches late: the 80M-triangle Moana parse held ~4 GB of such
4
+ * garbage. Transferring a buffer away frees it at once.
5
+ */
6
+
7
+ const whole = view => view.byteOffset === 0 && view.byteLength === view.buffer.byteLength
8
+ && typeof view.buffer.transfer === 'function'
9
+ && ! ( typeof SharedArrayBuffer !== 'undefined' && view.buffer instanceof SharedArrayBuffer );
10
+
11
+ /** Frees a view's memory now. Only for a view that alone holds its whole buffer. */
12
+ export function freeNow( view ) {
13
+
14
+ if ( view && whole( view ) ) view.buffer.transfer( 0 );
15
+
16
+ }
17
+
18
+ /** `view` at `length` elements, the old memory let go at once; zero-filled when it grows. */
19
+ export function resized( view, length ) {
20
+
21
+ const Type = view.constructor;
22
+ if ( whole( view ) ) return new Type( view.buffer.transfer( length * Type.BYTES_PER_ELEMENT ) );
23
+ const out = new Type( length );
24
+ out.set( length < view.length ? view.subarray( 0, length ) : view );
25
+ return out;
26
+
27
+ }
@@ -14,12 +14,13 @@
14
14
  * await loadObject3D(group);
15
15
  */
16
16
 
17
+ import { freeNow } from './buffers.js';
17
18
  import { PBRTParser } from './PBRTParser.js';
18
19
  import { PBRTSceneBuilder } from './PBRTSceneBuilder.js';
19
20
  import { findFrameSequence, FrameSequenceMerger, motionFromShutter, SEQUENCE_FPS } from './PBRTAnimation.js';
20
21
 
21
22
  export { PBRTParser } from './PBRTParser.js';
22
- export { PBRTSceneBuilder } from './PBRTSceneBuilder.js';
23
+ export { PBRTSceneBuilder, PBRT_BUILD_REVISION, SPILL_TRIANGLE_BUDGET, SPILL_PLACEMENT_BUDGET } from './PBRTSceneBuilder.js';
23
24
  export { tokenize } from './PBRTTokenizer.js';
24
25
  export { findFrameSequence, SEQUENCE_FPS } from './PBRTAnimation.js';
25
26
 
@@ -105,6 +106,16 @@ export class VirtualFS {
105
106
 
106
107
  }
107
108
 
109
+ /** The first `bytes` of a record, reading no more of a lazy one than that when the source can. */
110
+ async readHead( rec, bytes ) {
111
+
112
+ if ( ! rec ) return null;
113
+ if ( rec.bytes ) return rec.bytes.subarray( 0, bytes );
114
+ if ( ! rec.lazy || ! this.source ) return null;
115
+ return this.source.readHead ? this.source.readHead( rec.key, bytes ) : this.source.read( rec.key );
116
+
117
+ }
118
+
108
119
  /** Resolve and read in one step; `dir` is joined first, as Include does. */
109
120
  async readPath( path, dir = null ) {
110
121
 
@@ -120,12 +131,15 @@ export class VirtualFS {
120
131
 
121
132
  }
122
133
 
123
- releasePath( path, dir = null ) {
134
+ /** `bytes`, when given, are what a read of it returned, freed now rather than at the next GC. */
135
+ releasePath( path, dir = null, bytes = null ) {
124
136
 
125
137
  const rec = ( dir !== null && this.findRecord( joinPath( dir, path ) ) ) || this.findRecord( path );
126
138
  // Only a lazy record, which can be read again. Releasing a resident one would drop a
127
139
  // fragment that a later Include still needs — isHibiscusYoung pulls one in six times.
128
- if ( rec?.lazy ) this.release( rec );
140
+ if ( ! rec?.lazy ) return;
141
+ this.release( rec );
142
+ freeNow( bytes );
129
143
 
130
144
  }
131
145
 
@@ -180,6 +194,7 @@ function findBytes( bytes, needle, from = 0 ) {
180
194
  }
181
195
 
182
196
  const ASCII = s => Uint8Array.from( s, c => c.charCodeAt( 0 ) );
197
+ const ENTRY_HEAD_BYTES = 4 * 1024 * 1024;
183
198
  const WORLD_BEGIN = ASCII( 'WorldBegin' );
184
199
  const INCLUDE_WORDS = [ ASCII( 'Include' ), ASCII( 'Import' ) ];
185
200
 
@@ -311,6 +326,11 @@ export async function pickEntryPathFrom( vfs ) {
311
326
  /**
312
327
  * Candidate scene files for a VirtualFS, best first. Each candidate is read to test it and
313
328
  * dropped again, so a lazily-indexed archive is never materialised just to rank its entries.
329
+ *
330
+ * WorldBegin may only follow the scene-wide options, so a scene declares it within its first
331
+ * megabytes: the heads decide, and when they name one scene no file is read whole — the full
332
+ * reads were 15 GB and 40 s for a 17-part Moana archive before its parse began. Heads that name
333
+ * none, or several, fall back to reading everything, as the ranking of several needs.
314
334
  */
315
335
  export async function listEntryPathsFrom( vfs ) {
316
336
 
@@ -318,18 +338,34 @@ export async function listEntryPathsFrom( vfs ) {
318
338
  if ( pbrts.length === 0 ) return [];
319
339
  if ( pbrts.length === 1 ) return [ pbrts[ 0 ].key ];
320
340
 
321
- const included = new Set();
322
- const worlds = new Map();
323
- for ( const rec of pbrts ) {
341
+ const scan = async ( read ) => {
324
342
 
325
- const bytes = await vfs.read( rec );
326
- if ( ! bytes ) continue;
327
- worlds.set( rec.key, hasWorldBegin( bytes ) );
328
- includedBasenames( bytes, included );
329
- if ( rec.lazy ) vfs.release( rec ); // read again later if it is actually used
343
+ const included = new Set();
344
+ const worlds = new Map();
345
+ let cut = false;
346
+ for ( const rec of pbrts ) {
330
347
 
331
- }
348
+ const bytes = await read( rec );
349
+ if ( ! bytes ) continue;
350
+ worlds.set( rec.key, hasWorldBegin( bytes ) );
351
+ includedBasenames( bytes, included );
352
+ if ( bytes.length >= ENTRY_HEAD_BYTES ) cut = true;
353
+ if ( rec.lazy ) {
354
+
355
+ vfs.release( rec ); // read again later if it is actually used
356
+ freeNow( bytes );
357
+
358
+ }
359
+
360
+ }
361
+
362
+ return { included, worlds, cut };
363
+
364
+ };
332
365
 
366
+ const heads = await scan( rec => vfs.readHead( rec, ENTRY_HEAD_BYTES ) );
367
+ const scenes = [ ...heads.worlds.values() ].filter( Boolean ).length;
368
+ const { included, worlds } = scenes === 1 || ! heads.cut ? heads : await scan( rec => vfs.read( rec ) );
333
369
  return rankEntryPaths( pbrts.map( r => r.key ), worlds, included );
334
370
 
335
371
  }
@@ -377,11 +413,14 @@ export async function loadPBRTScene( args ) {
377
413
  resolveInclude: ( include, currentDir ) => vfs.readPath( include, currentDir ),
378
414
  // Depth-first, so this keeps only the open include chain live rather than every
379
415
  // scene file at once — the difference between 5.7 GB resident and a few MB.
380
- releaseInclude: ( include, currentDir ) => vfs.releasePath( include, currentDir ),
381
- maxPlacements: args.maxPlacements
416
+ releaseInclude: ( include, currentDir, bytes ) => vfs.releasePath( include, currentDir, bytes ),
417
+ maxPlacements: args.maxPlacements,
418
+ instanceIncludes: args.instanceIncludes
382
419
  } );
383
420
 
384
- return parser.parse( bytes, path.includes( '/' ) ? path.slice( 0, path.lastIndexOf( '/' ) ) : '' );
421
+ const ir = await parser.parse( bytes, path.includes( '/' ) ? path.slice( 0, path.lastIndexOf( '/' ) ) : '' );
422
+ vfs.releasePath( path, null, bytes );
423
+ return ir;
385
424
 
386
425
  };
387
426
 
@@ -450,6 +489,7 @@ export async function loadPBRTScene( args ) {
450
489
  mergeShapesAbove: args.mergeShapesAbove,
451
490
  curveSteps: args.curveSteps,
452
491
  curveSides: args.curveSides,
492
+ curveTolerance: args.curveTolerance,
453
493
  resolvePLY: async ( filename ) => {
454
494
 
455
495
  const rec = vfs.findRecord( filename );
@@ -2,7 +2,7 @@
2
2
  * Packed buffer manager for wavefront path tracing — one storage buffer per data category.
3
3
  * RAY/HIT are SoA-within-a-buffer (field `slot` of element `id` lives at `id + slot*_cap`).
4
4
  *
5
- * All three are GPU-only: every lane is written by a kernel before anything reads it and none
5
+ * Both are GPU-only: every lane is written by a kernel before anything reads it and none
6
6
  * is ever read back, so they carry no CPU array (see gpuOnlyStorageAttribute).
7
7
  */
8
8
 
@@ -17,7 +17,7 @@ import { createLogger, fmt } from '../utils/Logger.js';
17
17
  const log = createLogger( 'gpu' );
18
18
 
19
19
  export const RAY_STRIDE = 7;
20
- export const HIT_STRIDE = 2;
20
+ export const HIT_STRIDE = 3;
21
21
  // Per-pixel G-buffer (first-hit MRT staging): 1 uvec4/pixel — half-packed normal/depth/albedo
22
22
  // (pack2x16, no f32 bitcast); read by FinalWrite:
23
23
  // .x=packSnorm2x16(normal.xy) .y=packSnorm2x16(normal.z, depth) .z=packUnorm2x16(albedo.rg)
@@ -42,6 +42,9 @@ export const HIT = {
42
42
  // The geometric normal rides as one oct16 word (it was interpolated from oct16 triangle
43
43
  // normals to begin with), which leaves the instance leaf a full 32-bit lane of its own.
44
44
  NORMAL_MAT: 1, // uvec4(octNormal, matIndex, instanceLeaf + 1, 0)
45
+ // The path's RNG state, here rather than in a buffer of its own: Shade was at the device's
46
+ // 10 storage buffers, and the second triangle buffer needed the slot. Extend never writes it.
47
+ RNG: 2, // uvec4(rngState, 0, 0, 0)
45
48
  };
46
49
 
47
50
  // SoA region stride, baked into the shader graph at build time; single instance, rebuilt on resize.
@@ -73,7 +76,6 @@ export class PackedRayBuffer {
73
76
 
74
77
  // Each: { rw: StorageBufferNode, ro: StorageBufferNode } over one shared GPU buffer.
75
78
  this.rayBuffer = null;
76
- this.rngBuffer = null;
77
79
  this.hitBuffer = null;
78
80
 
79
81
  if ( capacity > 0 ) this.allocate( capacity );
@@ -96,13 +98,6 @@ export class PackedRayBuffer {
96
98
  ro: storage( rayAttr, 'vec4' ).toReadOnly(),
97
99
  };
98
100
 
99
- const rngAttr = gpuOnlyStorageAttribute( capacity, 1, Uint32Array );
100
- this._attrs.rng = rngAttr;
101
- this.rngBuffer = {
102
- rw: storage( rngAttr, 'uint' ),
103
- ro: storage( rngAttr, 'uint' ).toReadOnly(),
104
- };
105
-
106
101
  const hitCount = capacity * HIT_STRIDE;
107
102
  const hitAttr = gpuOnlyStorageAttribute( hitCount, 4, Uint32Array );
108
103
  this._attrs.hit = hitAttr;
@@ -112,7 +107,7 @@ export class PackedRayBuffer {
112
107
  };
113
108
 
114
109
  // Kept for the [gpu] startup summary in PathTracer._buildWavefrontKernels.
115
- this.rayBytes = rayCount * 16 + capacity * 4;
110
+ this.rayBytes = rayCount * 16;
116
111
  this.hitBytes = hitCount * 16;
117
112
  this.totalBytes = this.rayBytes + this.hitBytes;
118
113
 
@@ -132,11 +127,9 @@ export class PackedRayBuffer {
132
127
  dispose() {
133
128
 
134
129
  freeStorageAttribute( this._renderer, this._attrs.ray );
135
- freeStorageAttribute( this._renderer, this._attrs.rng );
136
130
  freeStorageAttribute( this._renderer, this._attrs.hit );
137
131
  this._attrs = {};
138
132
  this.rayBuffer = null;
139
- this.rngBuffer = null;
140
133
  this.hitBuffer = null;
141
134
  this.capacity = 0;
142
135
 
@@ -285,6 +278,10 @@ export const readHitInstanceLeaf = ( buf, id ) =>
285
278
  export const readHitFacet = ( buf, id ) =>
286
279
  buf.element( soa( id, HIT.NORMAL_MAT ) ).w;
287
280
 
281
+ export const readRngState = ( buf, id ) => buf.element( soa( id, HIT.RNG ) ).x;
282
+
283
+ export const writeRngState = ( buf, id, state ) => buf.element( soa( id, HIT.RNG ) ).assign( uvec4( state, 0, 0, 0 ) );
284
+
288
285
  export const writeHitPacked = ( buf, id, distance, triIndex, baryU, baryV, normal, matIndex, instanceLeaf, facet = uint( 0 ) ) => {
289
286
 
290
287
  buf.element( soa( id, HIT.DIST_TRI_BARY ) )