rayzee 11.12.0 → 11.13.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 (29) hide show
  1. package/dist/{ArchiveImporter-vUowipjh.js → ArchiveImporter-DQU55mit.js} +1322 -1290
  2. package/dist/{BidirectionalIntegrator-DPh6PSYb.js → BidirectionalIntegrator-CLmFjSN3.js} +2 -2
  3. package/dist/{NeuralRetouch-wwAwssFM.js → NeuralRetouch-CEkBhI1M.js} +1 -1
  4. package/dist/{NeuralSuperRes-DqV4v_gQ.js → NeuralSuperRes-Drjws9nE.js} +1 -1
  5. package/dist/{PathTracer-DW-ElMON.js → PathTracer-7a7L3cGF.js} +3448 -3404
  6. package/dist/{PhysicalSky-Ispr77Lf.js → PhysicalSky-DUIbTE93.js} +1 -1
  7. package/dist/{RayzeeRenderer-CPbY683A.js → RayzeeRenderer-CjjGT2e0.js} +340 -340
  8. package/dist/{SceneMetadata-BNA22-m3.js → SceneMetadata-CoAHTAmo.js} +1 -1
  9. package/dist/addons/archives.es.js +1 -1
  10. package/dist/addons/bidirectional.es.js +1 -1
  11. package/dist/addons/physical-sky.es.js +1 -1
  12. package/dist/rayzee-core.es.js +4 -4
  13. package/dist/rayzee.es.js +13 -13
  14. package/dist/rayzee.umd.js +110 -110
  15. package/dist/{sceneCachePolicy-BLvdjkVh.js → sceneCachePolicy-DavHt6Ii.js} +217 -185
  16. package/package.json +1 -1
  17. package/src/Processor/ArchiveImporter.js +25 -3
  18. package/src/Processor/AssetLoader.js +6 -4
  19. package/src/Processor/InstanceTable.js +36 -11
  20. package/src/Processor/PBRT/PBRTCurves.js +6 -3
  21. package/src/Processor/PBRT/PBRTSceneBuilder.js +26 -3
  22. package/src/Processor/PBRT/index.js +1 -0
  23. package/src/Processor/SceneProcessor.js +43 -5
  24. package/src/Processor/TLASBuilder.js +5 -4
  25. package/src/Processor/USD/USDScene.js +6 -1
  26. package/src/Processor/USD/index.js +2 -2
  27. package/src/Processor/utils.js +80 -1
  28. package/src/Stages/PathTracerStage.js +17 -6
  29. package/src/TSL/BVHTraversal.js +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "11.12.0",
3
+ "version": "11.13.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -21,7 +21,7 @@ import { ENGINE_AREAS } from '../Storage/areas.js';
21
21
  import { encodeSceneGraph, writeSceneGraph, decodeSceneGraph, SceneGraphUnsupported, SCENE_GRAPH_FORMAT, ARCHIVE_PATH, ARCHIVE_LOADER } from '../Storage/SceneGraphCodec.js';
22
22
  import { worthStoring } from '../Storage/sceneCachePolicy.js';
23
23
  import { VERSION } from '../version.js';
24
- import { updateLoading } from './utils';
24
+ import { timeSlicer, updateLoading } from './utils';
25
25
  import { loadPBRTScene, pickEntryPath, VirtualFS, PBRT_BUILD_REVISION } from './PBRT/index.js';
26
26
  import { loadUSDScene, listUSDRootLayers, listUSDParts, USDFiles, USDStage, USD_LAYER } from './USD/index.js';
27
27
  import { pfmTexture } from './PBRT/PFM.js';
@@ -687,7 +687,7 @@ export class ArchiveImporter {
687
687
  : null;
688
688
 
689
689
  const stored = graphKey ? await this._loadStoredGraph( graphKey, source, { imageFromBytes, envFromBytes } ) : null;
690
- const built = stored ?? await loadPBRTScene( { vfs: zip, source, entryPath, plyParser, imageFromBytes, envFromBytes, ...shape } );
690
+ const built = stored ?? await loadPBRTScene( { vfs: zip, source, entryPath, plyParser, imageFromBytes, envFromBytes, ...shape, pause: this._buildPause( timeSlicer() ) } );
691
691
  this.loader._throwDeferred();
692
692
 
693
693
  const { group, environment, animations, report, warnings, meshCount, entryPath: loadedEntry, candidates,
@@ -819,7 +819,7 @@ export class ArchiveImporter {
819
819
  const url = ext === 'pfm' ? null : URL.createObjectURL( blob );
820
820
  try {
821
821
 
822
- const texture = url ? await this.loader.loadEnvironmentByExtension( url, ext, { loader: ext === 'exr' ? this._exrLoader() : null } ) : pfmTexture( bytes );
822
+ const texture = url ? await this.loader.loadEnvironmentByExtension( url, ext, { loader: ext === 'exr' ? this._exrLoader() : null, quiet: true } ) : pfmTexture( bytes );
823
823
  texture.mapping = EquirectangularReflectionMapping;
824
824
  setEnvironmentSource( texture, `bytes:${await sampleHash( blob )}` );
825
825
  texture.userData[ ARCHIVE_PATH ] = fname;
@@ -863,8 +863,17 @@ export class ArchiveImporter {
863
863
  if ( chosen.length === 0 ) await this._requireUSDPartChoice( filename, files, entryPath, options.listing ?? paths.map( path => ( { path, size: entries[ path ]?.size ?? entries[ path ]?.byteLength ?? 0 } ) ), options.promptBytes );
864
864
 
865
865
  const start = performance.now();
866
+ const slice = timeSlicer();
866
867
  const built = await loadUSDScene( {
867
868
  files, entryPath,
869
+ pauseRead: ( prims, counting ) => {
870
+
871
+ const turn = slice();
872
+ if ( turn ) updateLoading( { status: `${counting ? 'Counting' : 'Reading'} USD scene... ${prims.toLocaleString()} prims`, progress: counting ? 5 : 6 } );
873
+ return turn;
874
+
875
+ },
876
+ pauseBuild: this._buildPause( slice ),
868
877
  resolveImage: async path => {
869
878
 
870
879
  const bytes = await files.read( path );
@@ -918,6 +927,19 @@ export class ArchiveImporter {
918
927
 
919
928
  }
920
929
 
930
+ /** The scene builder's pause: a turn for the page whenever `slice` gives one, with how far the build is. @private */
931
+ _buildPause( slice ) {
932
+
933
+ return ( done, total ) => {
934
+
935
+ const turn = slice();
936
+ if ( turn ) updateLoading( { status: `Building scene... ${done.toLocaleString()} of ${total.toLocaleString()} shapes`, progress: 8 } );
937
+ return turn;
938
+
939
+ };
940
+
941
+ }
942
+
921
943
  /** Asks which parts of a large USD scene to load. @private */
922
944
  async _requireUSDPartChoice( filename, files, entryPath, listing, promptBytes = USD_ELEMENT_PROMPT_BYTES ) {
923
945
 
@@ -9,7 +9,7 @@ import { clone as cloneWithSkeletons } from 'three/addons/utils/SkeletonUtils.js
9
9
  import { DownloadCache, nameFromUrl, cachedObjectURL } from '../Storage/DownloadCache.js';
10
10
  import { setEnvironmentSource } from '../Storage/CDFCache.js';
11
11
  import { fileIdentity, identityKey } from '../Storage/identity.js';
12
- import { disposeEngineOwnedResources, disposeObjectFromMemory, updateLoading } from './utils';
12
+ import { computeInstanceBoundingBoxes, disposeEngineOwnedResources, disposeObjectFromMemory, updateLoading } from './utils';
13
13
  import { BuildTimer } from './BuildTimer.js';
14
14
  import { getPlatform, hasImageDecoder, withHostWorker } from '../Platform.js';
15
15
  import { loadPlatformImage, platformImagesPlugin, missingImageDecoderPlugin } from './PlatformImageLoader.js';
@@ -723,7 +723,8 @@ export class AssetLoader extends EventDispatcher {
723
723
  * @param {string} extension
724
724
  * @param {{loader?: Object}} [options] - a three.js loader to read the file with, for a format nothing registered
725
725
  */
726
- async loadEnvironmentByExtension( url, extension, { loader = null } = {} ) {
726
+ /** `quiet` leaves the loading bar to a larger load this environment is part of. */
727
+ async loadEnvironmentByExtension( url, extension, { loader = null, quiet = false } = {} ) {
727
728
 
728
729
  const format = this._formats.get( extension );
729
730
  if ( ! loader && format?.type === 'environment' ) loader = this.loaderCache[ extension ] ??= await format.createLoader( this._loadingManager );
@@ -733,7 +734,7 @@ export class AssetLoader extends EventDispatcher {
733
734
  const cancelable = AssetLoader._isNetworkUrl( url );
734
735
  const status = "Downloading Environment...";
735
736
  const source = await this._viaCache( url, status, cancelable );
736
- const onProgress = source.cached ? undefined : this._downloadProgress( status, cancelable );
737
+ const onProgress = source.cached || quiet ? undefined : this._downloadProgress( status, cancelable );
737
738
 
738
739
  let texture;
739
740
  try {
@@ -763,7 +764,7 @@ export class AssetLoader extends EventDispatcher {
763
764
 
764
765
  }
765
766
 
766
- this._downloadComplete( "Processing Environment...", 62 );
767
+ if ( ! quiet ) this._downloadComplete( "Processing Environment...", 62 );
767
768
 
768
769
  if ( this.maxEnvironmentWidth ) limitEnvironmentWidth( texture, this.maxEnvironmentWidth );
769
770
  texture.mapping = EquirectangularReflectionMapping;
@@ -1042,6 +1043,7 @@ export class AssetLoader extends EventDispatcher {
1042
1043
 
1043
1044
  // Center model and adjust camera
1044
1045
  buildTimer.start( 'Camera setup' );
1046
+ computeInstanceBoundingBoxes( model );
1045
1047
  const box = new Box3().setFromObject( model );
1046
1048
  const center = box.getCenter( new Vector3() );
1047
1049
  const size = box.getSize( new Vector3() );
@@ -550,10 +550,20 @@ export class InstanceTable {
550
550
  */
551
551
  formClusters( world ) {
552
552
 
553
+ const steps = this.clusterSteps( world );
554
+ let step;
555
+ do step = steps.next(); while ( ! step.done );
556
+ return step.value;
557
+
558
+ }
559
+
560
+ /** {@link formClusters} in steps: yields how many entries have their cluster, and returns what it returns. */
561
+ * clusterSteps( world ) {
562
+
553
563
  const n = this.entryCount;
554
564
  const sorted = new Int32Array( n );
555
565
  for ( let e = 0; e < n; e ++ ) sorted[ e ] = e;
556
- splitIntoRuns( sorted, 0, n, world );
566
+ yield * splitIntoRuns( sorted, 0, n, world );
557
567
 
558
568
  const bounds = new Float64Array( this.clusterCount * 6 );
559
569
  for ( let c = 0; c < this.clusterCount; c ++ ) {
@@ -936,31 +946,31 @@ export class InstanceTable {
936
946
  }
937
947
 
938
948
  /**
939
- * Every TLAS entry's world-space bounds, 6 floats each — what the TLAS sorts on.
949
+ * Every TLAS entry's world-space bounds, 6 floats each — what the TLAS sorts on; entries `[from, to)` only, if given.
940
950
  * @param {Float64Array|Float32Array} out
941
951
  */
942
- writeEntryWorldAABBs( out ) {
952
+ writeEntryWorldAABBs( out, from = 0, to = this.entryCount ) {
943
953
 
944
954
  if ( ! this.entryRep ) {
945
955
 
946
- this.writeWorldAABBs( out );
956
+ this.writeWorldAABBs( out, from, to );
947
957
  return;
948
958
 
949
959
  }
950
960
 
951
- for ( let e = 0; e < this.entryCount; e ++ ) this.writeEntryWorldAABB( e, out, e * 6 );
961
+ for ( let e = from; e < to; e ++ ) this.writeEntryWorldAABB( e, out, e * 6 );
952
962
 
953
963
  }
954
964
 
955
965
  /**
956
- * Every placement's world-space bounds, 6 floats each.
966
+ * Every placement's world-space bounds, 6 floats each; placements `[from, to)` only, if given.
957
967
  * @param {Float64Array|Float32Array} out
958
968
  */
959
- writeWorldAABBs( out ) {
969
+ writeWorldAABBs( out, from = 0, to = this.count ) {
960
970
 
961
971
  const aabb = this.tplObjectAABB, src = this.sourceMesh, set = this.isSet;
962
972
 
963
- for ( let i = 0; i < this.count; i ++ ) {
973
+ for ( let i = from; i < to; i ++ ) {
964
974
 
965
975
  const o = i * 6;
966
976
 
@@ -1204,15 +1214,30 @@ const _recordInverse = new Float64Array( 16 );
1204
1214
 
1205
1215
  /**
1206
1216
  * 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⌉.
1217
+ * axis, the lower part a whole number of runs, which keeps the run count ⌈n / CLUSTER_SIZE⌉. Yields how many entries
1218
+ * are in their run, about every million entries of work.
1208
1219
  */
1209
- function splitIntoRuns( sorted, lo, hi, world ) {
1220
+ function * splitIntoRuns( sorted, lo, hi, world ) {
1210
1221
 
1211
1222
  const stack = [ lo, hi ];
1223
+ let placed = 0, work = 0;
1212
1224
  while ( stack.length ) {
1213
1225
 
1214
1226
  const end = stack.pop(), start = stack.pop();
1215
- if ( end - start <= CLUSTER_SIZE ) continue;
1227
+ if ( end - start <= CLUSTER_SIZE ) {
1228
+
1229
+ placed += end - start;
1230
+ continue;
1231
+
1232
+ }
1233
+
1234
+ work += end - start;
1235
+ if ( work >= 1 << 20 ) {
1236
+
1237
+ work = 0;
1238
+ yield placed;
1239
+
1240
+ }
1216
1241
 
1217
1242
  let axis = 0, widest = - 1;
1218
1243
  for ( let a = 0; a < 3; a ++ ) {
@@ -467,9 +467,9 @@ function withIndex( P, widths, index, basis ) {
467
467
 
468
468
  /**
469
469
  * A USD BasisCurves prim's curves (one point array, a count per curve) as one strip mesh. Modes and types are USD's;
470
- * the first normal of a curve orients its ribbon.
470
+ * the first normal of a curve orients its ribbon. `pause()` is awaited every 1024 curves, and again as they are joined.
471
471
  */
472
- export function tessellateCurves( spec ) {
472
+ export async function tessellateCurves( spec, pause = null ) {
473
473
 
474
474
  const { P, counts, widths = null, widthMode = 'constant', N = null, normalMode = 'vertex', type = 'cubic', basis = 'bspline', wrap = 'nonperiodic' } = spec;
475
475
  const pieces = [];
@@ -523,6 +523,7 @@ export function tessellateCurves( spec ) {
523
523
  }
524
524
 
525
525
  start += n;
526
+ if ( pause && ( c & 1023 ) === 1023 ) await pause();
526
527
 
527
528
  }
528
529
 
@@ -530,12 +531,14 @@ export function tessellateCurves( spec ) {
530
531
  const positions = new Float32Array( vertices * 3 );
531
532
  const indices = new Uint32Array( indexCount );
532
533
  let v = 0, i = 0;
533
- for ( const piece of pieces ) {
534
+ for ( let p = 0; p < pieces.length; p ++ ) {
534
535
 
536
+ const piece = pieces[ p ];
535
537
  positions.set( piece.positions, v * 3 );
536
538
  for ( let k = 0; k < piece.indices.length; k ++ ) indices[ i + k ] = piece.indices[ k ] + v;
537
539
  v += piece.positions.length / 3;
538
540
  i += piece.indices.length;
541
+ if ( pause && ( p & 1023 ) === 1023 ) await pause();
539
542
 
540
543
  }
541
544
 
@@ -31,6 +31,7 @@ import { octahedralToEquirect } from './EqualAreaOctahedral.js';
31
31
  import { tessellateCurve, tessellateCurves } from './PBRTCurves.js';
32
32
  import * as M from './PBRTMath.js';
33
33
  import { packUnitAttribute } from '../GeometryExtractor.js';
34
+ import { computeInstanceBoundingBoxes } from '../utils.js';
34
35
 
35
36
  const LAMP_TYPES = new Set( [ 'distant', 'point', 'spot' ] );
36
37
 
@@ -590,6 +591,8 @@ export class PBRTSceneBuilder {
590
591
  * @param {(filename:string)=>Promise<import('three').Texture>} [resolvers.resolveImage]
591
592
  * @param {(filename:string)=>Promise<import('three').Texture>} [resolvers.resolveEnvironment]
592
593
  * @param {boolean} [resolvers.convertHandedness=false]
594
+ * @param {(done:number, total:number)=>?Promise} [resolvers.pause] - called as shapes are built; a promise it
595
+ * returns is waited on, which lets the host redraw
593
596
  */
594
597
  constructor( resolvers = {} ) {
595
598
 
@@ -604,6 +607,9 @@ export class PBRTSceneBuilder {
604
607
  this.resolveEnvironment = resolvers.resolveEnvironment || resolvers.resolveImage || ( async () => null );
605
608
  this.convertHandedness = resolvers.convertHandedness === true;
606
609
  this._usage = resolvers.usage ?? null;
610
+ this.pause = resolvers.pause ?? null;
611
+ this._shapesDone = 0;
612
+ this._shapeTotal = 0;
607
613
 
608
614
  this.warnings = [];
609
615
  this.report = []; // per-mesh diagnostics for debugging imports
@@ -629,6 +635,13 @@ export class PBRTSceneBuilder {
629
635
 
630
636
  }
631
637
 
638
+ /** @private */
639
+ _tick() {
640
+
641
+ return this.pause ? this.pause( this._shapesDone, this._shapeTotal ) : null;
642
+
643
+ }
644
+
632
645
  /**
633
646
  * @param {object} ir - output of PBRTParser
634
647
  * @returns {Promise<{group:Group, camera:PerspectiveCamera|OrthographicCamera|null, environment:object|null, warnings:string[]}>}
@@ -671,6 +684,9 @@ export class PBRTSceneBuilder {
671
684
  this.mergedShapes = 0;
672
685
  this.droppedNoTemplate = 0;
673
686
  this._animated = []; // { node, motion }
687
+ this._shapesDone = 0;
688
+ this._shapeTotal = ir.shapes.length;
689
+ for ( const name of ir.instances.keys() ) this._shapeTotal += ir.objects.get( name )?.length ?? 0;
674
690
 
675
691
  // Shapes (direct + instanced)
676
692
  for ( let i = 0; i < ir.shapes.length; i ++ ) {
@@ -682,6 +698,8 @@ export class PBRTSceneBuilder {
682
698
 
683
699
  }
684
700
 
701
+ this._shapesDone ++;
702
+ await this._tick();
685
703
  const shape = ir.shapes[ i ];
686
704
  const [ geometry, surface ] = await Promise.all( [
687
705
  this._batches && ! shape.shared ? this._createGeometry( shape ) : this._buildGeometry( shape ),
@@ -843,6 +861,9 @@ export class PBRTSceneBuilder {
843
861
 
844
862
  }
845
863
 
864
+ // While the matrices are still in memory: the scene's framing reads every box.
865
+ computeInstanceBoundingBoxes( mesh );
866
+
846
867
  group.add( mesh );
847
868
 
848
869
  this.reportedMeshes += count;
@@ -888,6 +909,7 @@ export class PBRTSceneBuilder {
888
909
 
889
910
  for ( const shape of template ) {
890
911
 
912
+ this._shapesDone ++;
891
913
  if ( this._overBudget() ) {
892
914
 
893
915
  this.skippedForBudget += list.count;
@@ -895,6 +917,7 @@ export class PBRTSceneBuilder {
895
917
 
896
918
  }
897
919
 
920
+ await this._tick();
898
921
  const [ built, sharedMaterial ] = await Promise.all( [
899
922
  this._buildGeometry( shape ),
900
923
  this._getMaterial( shape )
@@ -1576,9 +1599,9 @@ export class PBRTSceneBuilder {
1576
1599
  }
1577
1600
 
1578
1601
  /** Many curves on one point array, as USD writes them. */
1579
- _curves( params ) {
1602
+ async _curves( params ) {
1580
1603
 
1581
- const built = tessellateCurves( {
1604
+ const built = await tessellateCurves( {
1582
1605
  P: param( params, 'P' ).value,
1583
1606
  counts: param( params, 'counts' ).value,
1584
1607
  widths: param( params, 'widths' )?.value ?? null,
@@ -1591,7 +1614,7 @@ export class PBRTSceneBuilder {
1591
1614
  steps: this.curveSteps,
1592
1615
  sides: this.curveSides ?? 1,
1593
1616
  tolerance: this.curveTolerance,
1594
- } );
1617
+ }, () => this._tick() );
1595
1618
 
1596
1619
  if ( ! built ) {
1597
1620
 
@@ -493,6 +493,7 @@ export async function loadPBRTScene( args ) {
493
493
  curveSteps: args.curveSteps,
494
494
  curveSides: args.curveSides,
495
495
  curveTolerance: args.curveTolerance,
496
+ pause: args.pause,
496
497
  usage,
497
498
  resolvePLY: async ( filename ) => {
498
499
 
@@ -12,7 +12,7 @@ import {
12
12
  import { TextureCreator } from './TextureCreator.js';
13
13
  import { GeometryExtractor, geometryBytesOf, usesTextureCoordinates } from './GeometryExtractor.js';
14
14
  import { EmissiveTriangleBuilder } from './EmissiveTriangleBuilder.js';
15
- import { updateLoading } from '../Processor/utils.js';
15
+ import { timeSlicer, updateLoading } from '../Processor/utils.js';
16
16
  import { BuildTimer } from './BuildTimer.js';
17
17
  import { createLogger, fmt, workerLogLevel } from '../utils/Logger.js';
18
18
  import { Box3, Matrix4, SRGBColorSpace, Sphere, Vector3 } from 'three';
@@ -38,6 +38,8 @@ import { createWorker, hardwareThreads } from '../Platform.js';
38
38
 
39
39
  // Under this the TLAS build is a few ms; the worker round trip would cost more than it saves.
40
40
  const TLAS_WORKER_MIN_ENTRIES = 50_000;
41
+ // Per-copy passes run in slices this long, with a turn for the page between them when one is due.
42
+ const SLICE_RECORDS = 1 << 16;
41
43
 
42
44
  // Meshes past this build across every core, one at a time.
43
45
  const LARGE_MESH_TRIANGLES = 200000;
@@ -934,9 +936,11 @@ export class SceneProcessor {
934
936
  */
935
937
  async _buildBVH() {
936
938
 
939
+ // A streamed build has reported its BLASes up to 35 % already.
940
+ const startProgress = this._streamed ? 35 : 25;
937
941
  updateLoading( {
938
942
  status: "Building BVH...",
939
- progress: 25
943
+ progress: startProgress
940
944
  } );
941
945
 
942
946
  if ( this.triangleCount === 0 ) {
@@ -948,6 +952,7 @@ export class SceneProcessor {
948
952
  this._log( 'Building two-level BVH (TLAS/BLAS)' );
949
953
  this.memory.mark( 'blas' );
950
954
  const startTime = performance.now();
955
+ this._slice = timeSlicer();
951
956
 
952
957
  try {
953
958
 
@@ -996,6 +1001,7 @@ export class SceneProcessor {
996
1001
 
997
1002
  for ( let m = 0; m < meshCount; m ++ ) {
998
1003
 
1004
+ if ( ( m & 0xffff ) === 0xffff ) await this._turn( 'Matching copies to their shapes', m, meshCount, startProgress );
999
1005
  const range = ranges[ sourceOf( m ) ];
1000
1006
  if ( ! range || range.count === 0 ) continue;
1001
1007
 
@@ -1115,6 +1121,7 @@ export class SceneProcessor {
1115
1121
 
1116
1122
  for ( let m = 0; m < meshCount; m ++ ) {
1117
1123
 
1124
+ if ( ( m & 0xffff ) === 0xffff ) await this._turn( 'Placing copies', m, meshCount, 70 );
1118
1125
  const owner = aliasOf[ m ];
1119
1126
  if ( owner < 0 ) continue;
1120
1127
  this.instanceTable.setAlias(
@@ -1149,8 +1156,17 @@ export class SceneProcessor {
1149
1156
 
1150
1157
  // Each entry's world box picks its cluster and sizes its byte box; dropped once the leaves are written.
1151
1158
  table.clusterWorld = new Float32Array( table.entryCount * 6 );
1152
- table.writeEntryWorldAABBs( table.clusterWorld );
1153
- table.clusterBounds = table.formClusters( table.clusterWorld );
1159
+ for ( let e = 0; e < table.entryCount; e += SLICE_RECORDS ) {
1160
+
1161
+ table.writeEntryWorldAABBs( table.clusterWorld, e, Math.min( table.entryCount, e + SLICE_RECORDS ) );
1162
+ await this._turn( 'Measuring copies', e, table.entryCount, 72 );
1163
+
1164
+ }
1165
+
1166
+ const steps = table.clusterSteps( table.clusterWorld );
1167
+ let step;
1168
+ while ( ! ( step = steps.next() ).done ) await this._turn( 'Grouping copies', step.value, table.entryCount, 72 );
1169
+ table.clusterBounds = step.value;
1154
1170
 
1155
1171
  }
1156
1172
 
@@ -1218,6 +1234,7 @@ export class SceneProcessor {
1218
1234
 
1219
1235
  for ( let t = 0; t < table.templateCount; t ++ ) {
1220
1236
 
1237
+ await this._turn( 'Assembling the scene', t, table.templateCount, 74 );
1221
1238
  // Only owning templates hold one; spilled, they are read back one at a time.
1222
1239
  const blas = spill ? await this._readSpilledBLAS( spill, t ) : table.blasData.get( t );
1223
1240
  if ( ! blas ) continue;
@@ -1283,10 +1300,23 @@ export class SceneProcessor {
1283
1300
  } );
1284
1301
  throw error;
1285
1302
 
1303
+ } finally {
1304
+
1305
+ this._slice = null;
1306
+
1286
1307
  }
1287
1308
 
1288
1309
  }
1289
1310
 
1311
+ /** During a build, a turn for the page once a slice of it has passed, with how far `what` has got. @private */
1312
+ _turn( what, done, total, progress ) {
1313
+
1314
+ const turn = this._slice?.();
1315
+ if ( turn ) updateLoading( { status: `${what}: ${done.toLocaleString()} of ${total.toLocaleString()}`, progress } );
1316
+ return turn;
1317
+
1318
+ }
1319
+
1290
1320
  /** Worker config shared by all BLAS builds. @private */
1291
1321
  _blasWorkerOpts() {
1292
1322
 
@@ -3889,6 +3919,7 @@ export class SceneProcessor {
3889
3919
  const used = Math.ceil( count * RECORD_VEC4 / 4 );
3890
3920
  this.bvh.setRecords( at, used === nodes ? stage : stage.subarray( 0, used * 16 ) );
3891
3921
  await flushBVH?.( at + used );
3922
+ await this._turn( 'Writing copy records', r, n, 74 );
3892
3923
 
3893
3924
  }
3894
3925
 
@@ -3907,8 +3938,15 @@ export class SceneProcessor {
3907
3938
  // The worker needs a buffer it can own, and world bounds are derived anyway.
3908
3939
  const aabbs = clustered ? table.clusterBounds : new Float64Array( n * 6 );
3909
3940
  if ( ! clustered ) table.writeEntryWorldAABBs( aabbs );
3941
+ updateLoading( { status: 'Building the scene tree...', progress: 73 } );
3910
3942
  const { tlasData, nodeCount } = await this._runTLASWorker( aabbs, n );
3911
- TLASBuilder.fillLeaves( tlasData, nodeCount, table );
3943
+ for ( let node = 0; node < nodeCount; node += SLICE_RECORDS ) {
3944
+
3945
+ TLASBuilder.fillLeaves( tlasData, nodeCount, table, node, Math.min( nodeCount, node + SLICE_RECORDS ) );
3946
+ await this._turn( 'Writing the scene tree', node, nodeCount, 73 );
3947
+
3948
+ }
3949
+
3912
3950
  return tlasData;
3913
3951
 
3914
3952
  } catch ( error ) {
@@ -227,21 +227,22 @@ export class TLASBuilder {
227
227
 
228
228
  /**
229
229
  * Fill in each leaf's BLAS pointer, visibility and world-to-object matrix, and record where
230
- * the leaf landed. One O(n) pass, so it stays on the main thread while the SAH tree does not.
230
+ * the leaf landed. One O(n) pass, so it stays on the main thread while the SAH tree does not;
231
+ * nodes `[from, to)` only, if given.
231
232
  *
232
233
  * A node is 16 floats and the BLAS pointer needs 4, so the affine inverse fits in the
233
234
  * remaining 12: the ray moves into object space with no second binding, which matters
234
235
  * because Shade is already at the 10 storage buffers Metal allows. The inverse is derived
235
236
  * here rather than stored: a column of it costs 64 bytes a placement.
236
237
  */
237
- static fillLeaves( data, nodeCount, table ) {
238
+ static fillLeaves( data, nodeCount, table, from = 0, to = nodeCount ) {
238
239
 
239
240
  const idx = bvhIndexView( data );
240
241
 
241
242
  if ( table.clusterCount > 0 ) {
242
243
 
243
244
  // Built over clusters: each leaf holds one, its copies' transforms in records (table.writeRecord).
244
- for ( let node = 0; node < nodeCount; node ++ ) {
245
+ for ( let node = from; node < to; node ++ ) {
245
246
 
246
247
  const o = node * FLOATS_PER_NODE;
247
248
  if ( idx[ o + 3 ] === BVH_LEAF_MARKERS.BLAS_POINTER_LEAF ) table.writeClusterLeaf( data, idx, o, idx[ o + 1 ], table.clusterWorld, node );
@@ -252,7 +253,7 @@ export class TLASBuilder {
252
253
 
253
254
  }
254
255
 
255
- for ( let node = 0; node < nodeCount; node ++ ) {
256
+ for ( let node = from; node < to; node ++ ) {
256
257
 
257
258
  const o = node * FLOATS_PER_NODE;
258
259
  if ( idx[ o + 3 ] !== BVH_LEAF_MARKERS.BLAS_POINTER_LEAF ) continue;
@@ -380,9 +380,12 @@ function thinCurves( curve, rate, salt ) {
380
380
 
381
381
  export class USDSceneReader {
382
382
 
383
- constructor( stage, { maxTriangles = Infinity, maxPlacements = Infinity, curveSteps = 2, resolveImage = async () => null, warn = () => {} } = {} ) {
383
+ /** `pause( prims, counting )` is awaited every 256 prims, `counting` during the counting pass. */
384
+ constructor( stage, { maxTriangles = Infinity, maxPlacements = Infinity, curveSteps = 2, resolveImage = async () => null, warn = () => {}, pause = null } = {} ) {
384
385
 
385
386
  this.stage = stage;
387
+ this.pause = pause;
388
+ this._visited = 0;
386
389
  this.maxTriangles = maxTriangles;
387
390
  this.maxPlacements = maxPlacements;
388
391
  this.curveSteps = curveSteps;
@@ -430,6 +433,7 @@ export class USDSceneReader {
430
433
 
431
434
  async walk() {
432
435
 
436
+ this._visited = 0;
433
437
  const root = this.stage.pseudoRoot;
434
438
  const top = this.stage.upAxis === 'Z' ? new Matrix4().makeRotationX( - Math.PI / 2 ) : new Matrix4();
435
439
  for ( const name of root.childNames() ) {
@@ -527,6 +531,7 @@ export class USDSceneReader {
527
531
 
528
532
  async visit( prim, parent, out, ctx ) {
529
533
 
534
+ if ( this.pause && ( ++ this._visited & 255 ) === 0 ) await this.pause( this._visited, this.dry );
530
535
  if ( ! prim.isDefined || prim.meta( 'active' ) === false ) return;
531
536
  if ( prim.value( 'visibility' ) === 'invisible' ) return;
532
537
  const purpose = prim.value( 'purpose' );
@@ -96,7 +96,7 @@ export async function loadUSDScene( args ) {
96
96
  const warn = message => warnings.push( message );
97
97
  const builder = new PBRTSceneBuilder( {
98
98
  maxTriangles: args.maxTriangles, maxPlacements: args.maxPlacements, mergeShapesAbove: args.mergeShapesAbove,
99
- curveSteps: args.curveSteps, curveSides: args.curveSides, curveTolerance: args.curveTolerance,
99
+ curveSteps: args.curveSteps, curveSides: args.curveSides, curveTolerance: args.curveTolerance, pause: args.pauseBuild,
100
100
  } );
101
101
 
102
102
  const parseStart = performance.now();
@@ -104,7 +104,7 @@ export async function loadUSDScene( args ) {
104
104
  await stage.open( args.entryPath );
105
105
  const reader = new USDSceneReader( stage, {
106
106
  maxTriangles: builder.maxTriangles, maxPlacements: builder.maxPlacements, curveSteps: builder.curveSteps,
107
- resolveImage: args.resolveImage, warn,
107
+ resolveImage: args.resolveImage, warn, pause: args.pauseRead,
108
108
  } );
109
109
  const read = await reader.read();
110
110
  if ( stage.omittedArcs > 0 ) warn( `${stage.omittedArcs} reference(s) into the parts left out of this load skipped` );
@@ -1,4 +1,4 @@
1
- import { SphereGeometry, Mesh, MeshPhysicalMaterial, Group, Object3D, ShaderMaterial, Vector2, Matrix4, GLSL3 } from 'three';
1
+ import { SphereGeometry, Mesh, MeshPhysicalMaterial, Group, Object3D, ShaderMaterial, Vector2, Matrix4, GLSL3, Box3 } from 'three';
2
2
  import { EngineEvents } from '../EngineEvents.js';
3
3
 
4
4
  let _statusCallback = null;
@@ -27,6 +27,85 @@ export const updateStats = ( statsUpdate ) => {
27
27
 
28
28
  };
29
29
 
30
+ // A message: unlike a timer it is not throttled in a background tab, and unlike scheduler.yield it queues behind the
31
+ // page's own timers and posted renders instead of running ahead of them.
32
+ function nextTask() {
33
+
34
+ if ( typeof MessageChannel === 'undefined' ) return new Promise( resolve => setTimeout( resolve, 0 ) );
35
+ return new Promise( resolve => {
36
+
37
+ const { port1, port2 } = new MessageChannel();
38
+ port1.onmessage = () => {
39
+
40
+ port1.close();
41
+ resolve();
42
+
43
+ };
44
+
45
+ port2.postMessage( null );
46
+
47
+ } );
48
+
49
+ }
50
+
51
+ /**
52
+ * For a long loop on the main thread: the returned function gives the page one task turn, to redraw, once `ms` have
53
+ * passed since the last, and returns null in between.
54
+ */
55
+ export function timeSlicer( ms = 50 ) {
56
+
57
+ let since = performance.now();
58
+ const resume = () => {
59
+
60
+ since = performance.now();
61
+
62
+ };
63
+
64
+ return () => ( performance.now() - since < ms ? null : nextTask().then( resume ) );
65
+
66
+ }
67
+
68
+ /** InstancedMesh.computeBoundingBox, bit for bit, for each instanced mesh under `object` without a box yet. */
69
+ export function computeInstanceBoundingBoxes( object ) {
70
+
71
+ object.traverse( mesh => {
72
+
73
+ if ( ! mesh.isInstancedMesh || mesh.boundingBox !== null ) return;
74
+ const geometry = mesh.geometry;
75
+ if ( geometry.boundingBox === null ) geometry.computeBoundingBox();
76
+ const box = mesh.boundingBox = new Box3();
77
+ const local = geometry.boundingBox;
78
+ if ( local.isEmpty() ) return;
79
+
80
+ const xs = [ local.min.x, local.max.x ], ys = [ local.min.y, local.max.y ], zs = [ local.min.z, local.max.z ];
81
+ const m = mesh.instanceMatrix.array;
82
+ let x0 = Infinity, y0 = Infinity, z0 = Infinity, x1 = - Infinity, y1 = - Infinity, z1 = - Infinity;
83
+ for ( let i = 0, o = 0; i < mesh.count; i ++, o += 16 ) {
84
+
85
+ const e0 = m[ o ], e1 = m[ o + 1 ], e2 = m[ o + 2 ], e3 = m[ o + 3 ], e4 = m[ o + 4 ], e5 = m[ o + 5 ], e6 = m[ o + 6 ], e7 = m[ o + 7 ];
86
+ const e8 = m[ o + 8 ], e9 = m[ o + 9 ], e10 = m[ o + 10 ], e11 = m[ o + 11 ], e12 = m[ o + 12 ], e13 = m[ o + 13 ], e14 = m[ o + 14 ], e15 = m[ o + 15 ];
87
+ for ( let c = 0; c < 8; c ++ ) {
88
+
89
+ const x = xs[ c >> 2 ], y = ys[ ( c >> 1 ) & 1 ], z = zs[ c & 1 ];
90
+ // Vector3.applyMatrix4's operations, in its order.
91
+ const w = 1 / ( e3 * x + e7 * y + e11 * z + e15 );
92
+ const px = ( e0 * x + e4 * y + e8 * z + e12 ) * w;
93
+ const py = ( e1 * x + e5 * y + e9 * z + e13 ) * w;
94
+ const pz = ( e2 * x + e6 * y + e10 * z + e14 ) * w;
95
+ x0 = Math.min( x0, px ); y0 = Math.min( y0, py ); z0 = Math.min( z0, pz );
96
+ x1 = Math.max( x1, px ); y1 = Math.max( y1, py ); z1 = Math.max( z1, pz );
97
+
98
+ }
99
+
100
+ }
101
+
102
+ box.min.set( x0, y0, z0 );
103
+ box.max.set( x1, y1, z1 );
104
+
105
+ } );
106
+
107
+ }
108
+
30
109
  // Raw frameCount is the user-facing sample count (one full-frame pass per frame).
31
110
  export function getDisplaySamples( pathTracerStage ) {
32
111