rayzee 8.8.2 → 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 (104) hide show
  1. package/README.md +210 -20
  2. package/dist/{NeuralRetouch-CYOJ21Wn.js → NeuralRetouch-BRquqfmZ.js} +1 -1
  3. package/dist/{NeuralSuperRes-Dc6jRYke.js → NeuralSuperRes-CbNvQjCw.js} +5 -2
  4. package/dist/{ToneMapGPU-BVE852iN.js → ToneMapGPU-DpUZDMbQ.js} +2 -1
  5. package/dist/rayzee.es.js +10795 -5324
  6. package/dist/rayzee.umd.js +279 -268
  7. package/package.json +1 -1
  8. package/src/AssetConfig.js +4 -0
  9. package/src/EngineDefaults.js +60 -76
  10. package/src/EngineEvents.js +7 -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/Passes/OIDNTemporalHistory.js +25 -7
  16. package/src/PathTracerApp.js +313 -25
  17. package/src/Processor/ArchiveCache.js +120 -0
  18. package/src/Processor/ArchiveReader.js +179 -32
  19. package/src/Processor/AssetLoader.js +523 -76
  20. package/src/Processor/AtmosphereModel.js +465 -0
  21. package/src/Processor/BVHBuilder.js +4 -1
  22. package/src/Processor/BVHLeafFold.js +100 -0
  23. package/src/Processor/BVHRefitter.js +98 -214
  24. package/src/Processor/ChunkedRecords.js +189 -4
  25. package/src/Processor/EmissiveTriangleBuilder.js +8 -0
  26. package/src/Processor/GeometryExtractor.js +238 -40
  27. package/src/Processor/InstanceTable.js +55 -37
  28. package/src/Processor/PBRT/PBRTCurves.js +104 -27
  29. package/src/Processor/PBRT/PBRTParser.js +132 -22
  30. package/src/Processor/PBRT/PBRTSceneBuilder.js +210 -55
  31. package/src/Processor/PBRT/buffers.js +27 -0
  32. package/src/Processor/PBRT/index.js +55 -15
  33. package/src/Processor/PackedRayBuffer.js +10 -13
  34. package/src/Processor/ParallelBVHBuilder.js +36 -15
  35. package/src/Processor/PhysicalSky.js +308 -0
  36. package/src/Processor/QueueManager.js +1 -0
  37. package/src/Processor/SceneProcessor.js +1132 -181
  38. package/src/Processor/SplitBLAS.js +249 -0
  39. package/src/Processor/SunPosition.js +41 -0
  40. package/src/Processor/TextureCreator.js +21 -0
  41. package/src/Processor/Workers/AIUpscalerWorker.js +79 -52
  42. package/src/Processor/Workers/BVHWorker.js +17 -13
  43. package/src/Processor/ZipReader.js +207 -0
  44. package/src/RenderSettings.js +49 -8
  45. package/src/SceneState/SceneState.js +367 -0
  46. package/src/SceneState/portable.js +52 -0
  47. package/src/Stages/MotionVector.js +8 -42
  48. package/src/Stages/NRD.js +30 -21
  49. package/src/Stages/NormalDepth.js +12 -23
  50. package/src/Stages/PathTracer.js +114 -6
  51. package/src/Stages/PathTracerStage.js +251 -104
  52. package/src/Storage/BLASCache.js +190 -0
  53. package/src/Storage/CDFCache.js +77 -0
  54. package/src/Storage/DownloadCache.js +266 -0
  55. package/src/Storage/GeometrySpill.js +217 -0
  56. package/src/Storage/SceneGraphCodec.js +503 -0
  57. package/src/Storage/SpillStore.js +112 -0
  58. package/src/Storage/StorageManager.js +934 -0
  59. package/src/Storage/StorageOps.js +137 -0
  60. package/src/Storage/StorageWorker.js +37 -0
  61. package/src/Storage/events.js +29 -0
  62. package/src/Storage/identity.js +77 -0
  63. package/src/Storage/index.js +9 -0
  64. package/src/Storage/inlineTransport.js +24 -0
  65. package/src/Storage/locks.js +99 -0
  66. package/src/Storage/openStorage.js +87 -0
  67. package/src/Storage/sceneCachePolicy.js +12 -0
  68. package/src/Storage/shared.js +9 -0
  69. package/src/Storage/transport.js +62 -0
  70. package/src/TSL/Atmosphere.js +684 -0
  71. package/src/TSL/BVHTraversal.js +288 -239
  72. package/src/TSL/CameraRay.js +99 -14
  73. package/src/TSL/Common.js +11 -2
  74. package/src/TSL/DebugKernel.js +2 -2
  75. package/src/TSL/Displacement.js +6 -6
  76. package/src/TSL/EmissiveSampling.js +5 -5
  77. package/src/TSL/EnvironmentCDF.js +176 -0
  78. package/src/TSL/GenerateKernel.js +5 -5
  79. package/src/TSL/HitFacet.js +4 -4
  80. package/src/TSL/LightsDirect.js +8 -8
  81. package/src/TSL/LightsSampling.js +55 -37
  82. package/src/TSL/Random.js +1 -0
  83. package/src/TSL/ShadeKernel.js +75 -19
  84. package/src/TSL/Sun.js +40 -0
  85. package/src/TSL/TextureSampling.js +6 -7
  86. package/src/index.js +22 -0
  87. package/src/managers/CameraManager.js +412 -56
  88. package/src/managers/DenoisingManager.js +4 -3
  89. package/src/managers/EnvironmentManager.js +263 -33
  90. package/src/managers/GoboManager.js +19 -8
  91. package/src/managers/IESManager.js +3 -3
  92. package/src/managers/InteractionManager.js +30 -2
  93. package/src/managers/LightManager.js +98 -5
  94. package/src/managers/MaterialDataManager.js +22 -2
  95. package/src/managers/TransformManager.js +26 -0
  96. package/src/managers/UniformManager.js +11 -7
  97. package/src/managers/VideoRenderManager.js +38 -17
  98. package/src/managers/ViewCamera.js +77 -0
  99. package/src/managers/WalkControls.js +262 -0
  100. package/src/managers/helpers/OutlineHelper.js +22 -2
  101. package/src/managers/timeline/CameraTrack.js +235 -0
  102. package/src/managers/timeline/TimelineManager.js +146 -0
  103. package/src/version.js +3 -0
  104. package/src/Processor/ProceduralSky.js +0 -291
@@ -17,7 +17,7 @@ import { AutoExposure } from './Stages/AutoExposure.js';
17
17
  import { Compositor } from './Stages/Compositor.js';
18
18
  import { RenderPipeline } from './Pipeline/RenderPipeline.js';
19
19
  import { CompletionTracker } from './Pipeline/CompletionTracker.js';
20
- import { ENGINE_DEFAULTS as DEFAULT_STATE, PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, modePresetSettings, MAX_STORAGE_TEXTURE_SIZE, MAX_RESERVABLE_RENDER_SIZE, setReservedRenderSize, getRenderProfile } from './EngineDefaults.js';
20
+ import { ENGINE_DEFAULTS as DEFAULT_STATE, TRIANGLE_DATA_LAYOUT, PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, modePresetSettings, MAX_STORAGE_TEXTURE_SIZE, MAX_RESERVABLE_RENDER_SIZE, setReservedRenderSize, getRenderProfile } from './EngineDefaults.js';
21
21
  import { updateStats, updateLoading, resetLoading, setStatusCallback, getDisplaySamples, disposeObjectFromMemory, disposeRenderer } from './Processor/utils.js';
22
22
  import { BuildTimer } from './Processor/BuildTimer.js';
23
23
  import { TextureReadback } from './Processor/TextureReadback.js';
@@ -25,16 +25,23 @@ import { createLogger, fmt } from './utils/Logger.js';
25
25
  import { InteractionManager } from './managers/InteractionManager.js';
26
26
  import { EngineEvents } from './EngineEvents.js';
27
27
  import { IssueLog, ISSUE_CODES } from './EngineIssues.js';
28
+ import { getAssetConfig } from './AssetConfig.js';
29
+ import { StorageManager } from './Storage/StorageManager.js';
30
+ import { acquireSharedStorage } from './Storage/openStorage.js';
31
+ import { nameFromUrl } from './Storage/DownloadCache.js';
32
+ import { fileIdentity, identityKey } from './Storage/identity.js';
28
33
  import { SETTING_SOURCE } from './RenderSettings.js';
29
34
  import { toneMapToRGBA8 } from './Processor/ToneMapCPU.js';
30
35
  import { ColorManagement, setActiveColorManagement } from './Color/ColorManagement.js';
31
36
  import { getViewTransform } from './Color/ViewTransforms.js';
32
37
  import { AssetLoader } from './Processor/AssetLoader.js';
38
+ import { SPILL_TRIANGLE_BUDGET, SPILL_PLACEMENT_BUDGET } from './Processor/PBRT/index.js';
33
39
  import { SceneProcessor } from './Processor/SceneProcessor.js';
34
40
 
35
41
  // Managers
36
42
  import { RenderSettings } from './RenderSettings.js';
37
43
  import { CameraManager } from './managers/CameraManager.js';
44
+ import { TimelineManager } from './managers/timeline/TimelineManager.js';
38
45
  import { LightManager } from './managers/LightManager.js';
39
46
  import { GoboManager } from './managers/GoboManager.js';
40
47
  import { IESManager } from './managers/IESManager.js';
@@ -43,6 +50,17 @@ import { OverlayManager } from './managers/OverlayManager.js';
43
50
  import { AnimationManager } from './managers/AnimationManager.js';
44
51
  import { TransformManager } from './managers/TransformManager.js';
45
52
  import { TransformGizmoHelper } from './managers/helpers/TransformGizmoHelper.js';
53
+ import { captureSceneState, applySceneState } from './SceneState/SceneState.js';
54
+ import { TRIANGLE_PATCH_PROPERTIES } from './managers/MaterialDataManager.js';
55
+ import { VERSION } from './version.js';
56
+
57
+ export const RENDER_CHECKPOINT_VERSION = 1;
58
+
59
+ // The part of an archive a load chose, for sceneSource.
60
+ const partOf = ( { element, pbrtEntry } = {} ) => ( {
61
+ ...( element !== undefined ? { element } : {} ),
62
+ ...( pbrtEntry !== undefined ? { pbrtEntry } : {} ),
63
+ } );
46
64
 
47
65
  // One app per canvas — auto-dispose a prior owner if the caller double-
48
66
  // instantiates (StrictMode, HMR, etc.) so its rAF loop can't burn CPU.
@@ -128,6 +146,11 @@ export class PathTracerApp extends EventDispatcher {
128
146
  * @param {number} [options.maxSceneBytes] - refuse a scene whose estimated host memory is
129
147
  * above this. The default refuses where the renderer process would be killed instead of
130
148
  * throwing; raise it deliberately, on a fresh browser. See HostMemory.js.
149
+ * @param {false|'auto'|StorageManager} [options.storage] - on-disk storage; defaults to
150
+ * `configureAssets( { storage } )`. A host-supplied manager stays the host's to dispose.
151
+ * @param {boolean} [options.memorySpill=false] - experimental: once a large static scene is
152
+ * on the GPU, move its triangle records and BLAS nodes to disk (see
153
+ * {@link ensureSceneResident}). Needs storage; skipped for animated scenes.
131
154
  *
132
155
  * The engine dispatches `EngineEvents.FRAME` after each animate() iteration so hosts can
133
156
  * tick external instrumentation (e.g. a stats panel) without coupling the engine to it.
@@ -152,6 +175,11 @@ export class PathTracerApp extends EventDispatcher {
152
175
  this._autoResize = options.autoResize !== false;
153
176
  // A scene budget the host may raise; read where SceneProcessor is built, well after this.
154
177
  this._maxSceneBytes = options.maxSceneBytes;
178
+ this._storageOption = options.storage;
179
+ this._memorySpill = options.memorySpill === true;
180
+ /** @type {?StorageManager} on-disk storage, null when off or unavailable */
181
+ this.storage = null;
182
+ this._storageRelease = null;
155
183
  this._container = options.container || null;
156
184
  // Apply the environment authored into a model file's metadata on load. See _beginSceneMetadataEnvironment().
157
185
  this._applySceneMetadataEnabled = options.applySceneMetadata !== false;
@@ -181,6 +209,7 @@ export class PathTracerApp extends EventDispatcher {
181
209
  ...DEFAULT_STATE,
182
210
  environmentRotation: this._profile.environmentRotation,
183
211
  saturation: this._profile.saturation,
212
+ dofMode: this._profile.dofMode,
184
213
  },
185
214
  { issues: this._issues }
186
215
  );
@@ -214,6 +243,8 @@ export class PathTracerApp extends EventDispatcher {
214
243
  // ── Managers (direct public access) ──
215
244
  /** @type {CameraManager} */
216
245
  this.cameraManager = null;
246
+ /** @type {TimelineManager} Authored animation: keyframed tracks. */
247
+ this.timeline = null;
217
248
  /** @type {LightManager} */
218
249
  this.lightManager = null;
219
250
  /** @type {GoboManager} */
@@ -320,6 +351,7 @@ export class PathTracerApp extends EventDispatcher {
320
351
  */
321
352
  async init() {
322
353
 
354
+ const storageReady = this._initStorage();
323
355
  await this._initRenderer();
324
356
  this._applyPendingReservedRenderSize();
325
357
  this._initCameraManager();
@@ -328,9 +360,11 @@ export class PathTracerApp extends EventDispatcher {
328
360
  this._initPipeline();
329
361
  await this._initManagers();
330
362
  this._wireEvents();
363
+ await storageReady;
364
+ this._attachStorage();
331
365
 
332
366
  // Seed path tracer with minimal empty scene data
333
- this.stages.pathTracer.setTriangleData( new Float32Array( 32 ), 0 );
367
+ this.stages.pathTracer.setTriangleData( new Uint32Array( TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE ), 0 );
334
368
  this.stages.pathTracer.setBVHData( new Float32Array( 16 ) );
335
369
  this.stages.pathTracer.materialData.setMaterialData( new Float32Array( 16 ) );
336
370
  this.stages.pathTracer.setupMaterial();
@@ -342,6 +376,48 @@ export class PathTracerApp extends EventDispatcher {
342
376
 
343
377
  }
344
378
 
379
+ async _initStorage() {
380
+
381
+ const option = this._storageOption ?? getAssetConfig().storage;
382
+ if ( option === false ) return;
383
+
384
+ if ( option instanceof StorageManager ) {
385
+
386
+ this.storage = option;
387
+
388
+ } else {
389
+
390
+ const { storage, reason, release } = await acquireSharedStorage( getAssetConfig().cacheNamespace );
391
+ if ( this._disposed ) {
392
+
393
+ release();
394
+ return;
395
+
396
+ }
397
+
398
+ if ( ! storage ) {
399
+
400
+ this._issues.warn( ISSUE_CODES.STORAGE_UNAVAILABLE, `on-disk storage is off: ${reason}`, { reason } );
401
+ return;
402
+
403
+ }
404
+
405
+ this.storage = storage;
406
+ this._storageRelease = release;
407
+
408
+ }
409
+
410
+ this._addTrackedListener( this.storage, 'change', ( e ) => this.dispatchEvent( { type: EngineEvents.STORAGE_CHANGED, area: e.area } ) );
411
+ this._addTrackedListener( this.storage, 'issue', ( e ) => this._issues.warn( e.code, e.message, e.detail ) );
412
+
413
+ }
414
+
415
+ _attachStorage() {
416
+
417
+ if ( this.assetLoader ) this.assetLoader.storage = this.storage;
418
+
419
+ }
420
+
345
421
  /**
346
422
  * Starts the animation loop.
347
423
  */
@@ -359,7 +435,8 @@ export class PathTracerApp extends EventDispatcher {
359
435
 
360
436
  }
361
437
 
362
- if ( this.cameraManager.controls ) this.cameraManager.controls.update();
438
+ this.timeline.update();
439
+ this.cameraManager.updateControls();
363
440
 
364
441
  this._applyPendingRenderScale();
365
442
 
@@ -619,6 +696,11 @@ export class PathTracerApp extends EventDispatcher {
619
696
  this._removeTrackedListeners();
620
697
  setStatusCallback( null );
621
698
 
699
+ this._storageRelease?.();
700
+ this._storageRelease = null;
701
+ this.storage = null;
702
+ this._sceneSource = this._sceneSourceFile = null;
703
+
622
704
  this._issues.detach(); // onIssue captures `this`; see IssueLog.detach()
623
705
 
624
706
  // Holds the renderer and the issue log, both of which outlive it otherwise.
@@ -638,6 +720,7 @@ export class PathTracerApp extends EventDispatcher {
638
720
  this.iesManager?.dispose();
639
721
  this.denoisingManager?.dispose();
640
722
  this.interactionManager?.dispose();
723
+ this.timeline?.dispose();
641
724
  this.cameraManager?.dispose();
642
725
 
643
726
  this.pipeline?.dispose();
@@ -758,6 +841,7 @@ export class PathTracerApp extends EventDispatcher {
758
841
  this.transformManager?.detach?.();
759
842
 
760
843
  this.assetLoader?.releaseTargetModel();
844
+ this._sceneSource = this._sceneSourceFile = null;
761
845
 
762
846
  // Clear lights in the WebGPU light scene
763
847
  this.lightManager?.clearLights?.();
@@ -765,7 +849,7 @@ export class PathTracerApp extends EventDispatcher {
765
849
  // Seed path tracer with empty data (matches the init-time seed)
766
850
  if ( this.stages.pathTracer ) {
767
851
 
768
- this.stages.pathTracer.setTriangleData( new Float32Array( 32 ), 0 );
852
+ this.stages.pathTracer.setTriangleData( new Uint32Array( TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE ), 0 );
769
853
  this.stages.pathTracer.setBVHData( new Float32Array( 16 ) );
770
854
  this.stages.pathTracer.materialData.setMaterialData( new Float32Array( 16 ) );
771
855
  this.stages.pathTracer.setEmissiveTriangleData?.( new Float32Array( 0 ), 0, 0 );
@@ -782,12 +866,14 @@ export class PathTracerApp extends EventDispatcher {
782
866
  /**
783
867
  * Loads a model, builds BVH, and uploads scene data.
784
868
  * @param {string} url - Model URL
869
+ * @param {{cacheKey?: string}} [options] - download-cache key for a URL that expires
785
870
  */
786
- async loadModel( url ) {
871
+ async loadModel( url, options = {} ) {
787
872
 
788
873
  await this._loadWithSceneRebuild(
789
- () => this.assetLoader.loadModel( url ),
790
- { type: 'ModelLoaded', url }
874
+ () => this.assetLoader.loadModel( url, options ),
875
+ { type: 'ModelLoaded', url },
876
+ { kind: 'url', url, cacheKey: options.cacheKey ?? null }
791
877
  );
792
878
 
793
879
  }
@@ -811,7 +897,8 @@ export class PathTracerApp extends EventDispatcher {
811
897
 
812
898
  await this._loadWithSceneRebuild(
813
899
  () => this.assetLoader.loadObject3D( object3d, name ),
814
- { type: 'Object3DLoaded', name }
900
+ { type: 'Object3DLoaded', name },
901
+ { kind: 'object3d', name }
815
902
  );
816
903
 
817
904
  }
@@ -885,22 +972,49 @@ export class PathTracerApp extends EventDispatcher {
885
972
  * tracer rendering buffers whose geometry has been freed. Here a concurrent call throws
886
973
  * LOAD_IN_PROGRESS before anything is touched.
887
974
  *
888
- * @param {File} file
975
+ * @param {File|string} file - a File, or a URL to download (through the download cache)
889
976
  * @param {object} [options] - forwarded to the archive loader: `element` to load one
890
- * subtree of a multi-part scene, `pbrtEntry` to choose among several .pbrt scenes.
977
+ * subtree of a multi-part scene, `pbrtEntry` to choose among several .pbrt scenes. For a
978
+ * URL also `filename` and `cacheKey` (see AssetLoader.loadAssetFromUrl).
891
979
  * @returns {Promise<void>}
892
980
  */
893
981
  async loadFile( file, options = {} ) {
894
982
 
983
+ options = this._sceneBudgets( options );
984
+
985
+ if ( typeof file === 'string' ) {
986
+
987
+ const filename = options.filename ?? nameFromUrl( file );
988
+ const format = this.assetLoader?.getFileFormat( filename );
989
+ if ( ! format ) throw new Error( `Unsupported file format: ${filename}` );
990
+ if ( format.type === 'environment' || format.type === 'image' ) return this.loadEnvironment( file );
991
+
992
+ await this._loadWithSceneRebuild(
993
+ () => this.assetLoader.loadAssetFromUrl( file, { ...options, filename } ),
994
+ { type: 'ModelLoaded', filename },
995
+ { kind: 'url', url: file, filename, cacheKey: options.cacheKey ?? null, ...partOf( options ) }
996
+ );
997
+ return;
998
+
999
+ }
1000
+
895
1001
  const format = this.assetLoader?.getFileFormat( file?.name || '' );
896
1002
  if ( ! format ) throw new Error( `Unsupported file format: ${file?.name}` );
897
1003
 
898
1004
  if ( format.type !== 'environment' && format.type !== 'image' ) {
899
1005
 
1006
+ const identity = fileIdentity( file ).catch( () => ( { name: file.name, size: file.size, lastModified: file.lastModified ?? 0, sample: null } ) );
900
1007
  await this._loadWithSceneRebuild(
901
1008
  () => this.assetLoader.loadAssetFromFile( file, options ),
902
- { type: 'ModelLoaded', filename: file.name }
1009
+ { type: 'ModelLoaded', filename: file.name },
1010
+ async () => {
1011
+
1012
+ const id = await identity;
1013
+ return { kind: 'local-file', file: id, key: id.sample ? identityKey( id ) : null, ...partOf( options ) };
1014
+
1015
+ }
903
1016
  );
1017
+ this._sceneSourceFile = file;
904
1018
  return;
905
1019
 
906
1020
  }
@@ -942,7 +1056,8 @@ export class PathTracerApp extends EventDispatcher {
942
1056
 
943
1057
  await this._loadWithSceneRebuild(
944
1058
  () => this.assetLoader.loadExampleModels( index, modelFiles ),
945
- { type: 'ModelLoaded', index }
1059
+ { type: 'ModelLoaded', index },
1060
+ { kind: 'url', url: modelFiles[ index ]?.url ?? null, cacheKey: null }
946
1061
  );
947
1062
 
948
1063
  }
@@ -1003,8 +1118,28 @@ export class PathTracerApp extends EventDispatcher {
1003
1118
 
1004
1119
  }
1005
1120
 
1121
+ /**
1122
+ * Where the model on screen came from, as plain data — what a saved session reopens:
1123
+ * `{ kind: 'url', url, cacheKey, filename?, element?, pbrtEntry? }`,
1124
+ * `{ kind: 'local-file', file: {name, size, lastModified, sample}, key, element?, pbrtEntry? }` or
1125
+ * `{ kind: 'object3d', name }`. Null before a load and after {@link unloadScene}.
1126
+ * @returns {?Object}
1127
+ */
1128
+ get sceneSource() {
1129
+
1130
+ return this._sceneSource ?? null;
1131
+
1132
+ }
1133
+
1134
+ /** The File a local load came from, so a host can save a project that carries it; null otherwise. */
1135
+ get sceneSourceFile() {
1136
+
1137
+ return this._sceneSourceFile ?? null;
1138
+
1139
+ }
1140
+
1006
1141
  /** Shared pipeline: load asset → sync controls → build BVH → reset → dispatch events */
1007
- async _loadWithSceneRebuild( loadFn, eventPayload ) {
1142
+ async _loadWithSceneRebuild( loadFn, eventPayload, source = null ) {
1008
1143
 
1009
1144
  if ( this._loadingInProgress ) throw this._busyError( 'PathTracerApp' );
1010
1145
 
@@ -1013,6 +1148,8 @@ export class PathTracerApp extends EventDispatcher {
1013
1148
  try {
1014
1149
 
1015
1150
  await loadFn();
1151
+ this._sceneSource = typeof source === 'function' ? await source() : source;
1152
+ this._sceneSourceFile = null;
1016
1153
  // A fresh model re-establishes the emissive-sampling auto-default (incremental
1017
1154
  // rebuilds — add/remove object, texture reprocess — preserve the user's choice).
1018
1155
  this._emissiveSamplingUserSet = false;
@@ -1022,6 +1159,7 @@ export class PathTracerApp extends EventDispatcher {
1022
1159
  this._clearAppendedModels();
1023
1160
  this._syncControlsAfterLoad();
1024
1161
  await this.loadSceneData( { pendingEnvironment: this._beginSceneMetadataEnvironment() } );
1162
+ this._maybeSpill();
1025
1163
  this.pipeline?.eventBus.emit( 'autoexposure:resetHistory' );
1026
1164
  // Not held: the first denoise of a new scene lands ~1.1 s after the load (upload and
1027
1165
  // shader compilation come first), and a second of the previous model reads as a bug.
@@ -1054,6 +1192,8 @@ export class PathTracerApp extends EventDispatcher {
1054
1192
  */
1055
1193
  _discardFailedLoad() {
1056
1194
 
1195
+ this._sceneSource = this._sceneSourceFile = null;
1196
+
1057
1197
  try {
1058
1198
 
1059
1199
  this._clearAppendedModels();
@@ -1166,7 +1306,7 @@ export class PathTracerApp extends EventDispatcher {
1166
1306
  * CDF'd concurrently with the BVH build rather than before it.
1167
1307
  * @returns {boolean}
1168
1308
  */
1169
- async loadSceneData( { pendingEnvironment = null } = {} ) {
1309
+ async loadSceneData( { pendingEnvironment = null, keepUserLights = false } = {} ) {
1170
1310
 
1171
1311
  // Clear selection before rebuilding — the old object leaves the scene graph.
1172
1312
  // Skipped on the append path (addModel): the selected object persists, so its
@@ -1182,7 +1322,9 @@ export class PathTracerApp extends EventDispatcher {
1182
1322
  this._tagPrimarySceneObject();
1183
1323
 
1184
1324
  const timer = new BuildTimer( '', { namespace: 'scene', level: 'info' } );
1185
- const environmentTexture = this.meshScene.environment;
1325
+ // meshScene.environment is the HDRI slot: a sky, gradient or colour in use outlives the model.
1326
+ const hdri = () => this.stages.pathTracer.environment.envParams.mode === 'hdri' ? this.meshScene.environment : null;
1327
+ const environmentTexture = hdri();
1186
1328
 
1187
1329
  // Environment CDF build in parallel with BVH
1188
1330
  let cdfPromise = null;
@@ -1225,7 +1367,8 @@ export class PathTracerApp extends EventDispatcher {
1225
1367
  // Build BVH
1226
1368
  timer.start( 'BVH build (SceneProcessor)' );
1227
1369
  this._sdf.setMaxTextureSize( this._maxTextureSize );
1228
- await this._sdf.buildBVH( this.meshScene );
1370
+ await this._sdf.buildBVH( this.meshScene, { sceneKey: this.assetLoader?.sceneSourceKey ?? null, progressive: this._progressiveSpill() } );
1371
+ this.assetLoader?.flushPendingGraph( this._sdf.performanceMetrics.totalProcessingTime );
1229
1372
  timer.end( 'BVH build (SceneProcessor)' );
1230
1373
 
1231
1374
  // Transfer geometry, materials, and textures to GPU
@@ -1235,7 +1378,7 @@ export class PathTracerApp extends EventDispatcher {
1235
1378
 
1236
1379
  // Re-read rather than reusing the snapshot above: a pendingEnvironment may have landed
1237
1380
  // during the BVH build, and the snapshot is then a disposed texture.
1238
- if ( ! this._sdf.uploadToPathTracer( this.stages.pathTracer, this.lightManager, this.meshScene, this.meshScene.environment ) ) return false;
1381
+ if ( ! this._sdf.uploadToPathTracer( this.stages.pathTracer, this.lightManager, this.meshScene, hdri(), { keepUserLights } ) ) return false;
1239
1382
 
1240
1383
  // Patch per-mesh visibility into the TLAS leaves we just uploaded
1241
1384
  this.stages.pathTracer._meshRefs = this.stages.pathTracer._collectMeshRefs( this.meshScene );
@@ -1363,7 +1506,8 @@ export class PathTracerApp extends EventDispatcher {
1363
1506
  /** Reframe-free rebuild sequence. Assumes the _loadingInProgress guard is already held. */
1364
1507
  async _finishRebuildNoReframe( eventPayload ) {
1365
1508
 
1366
- await this.loadSceneData(); // emits 'SceneRebuild'
1509
+ await this.loadSceneData( { keepUserLights: true } ); // emits 'SceneRebuild'
1510
+ this._maybeSpill();
1367
1511
  this._recalibrateControlLimits(); // scene bounds changed — retune zoom limits + near/far (no camera move)
1368
1512
  this.pipeline?.eventBus.emit( 'autoexposure:resetHistory' );
1369
1513
  this.reset();
@@ -1379,7 +1523,7 @@ export class PathTracerApp extends EventDispatcher {
1379
1523
  * @param {string} [opts.name] - Display name for the scene-object list.
1380
1524
  * @returns {Promise<string>} the new object's id (Object3D uuid).
1381
1525
  */
1382
- async addModel( url, { name } = {} ) {
1526
+ async addModel( url, { name, cacheKey } = {} ) {
1383
1527
 
1384
1528
  if ( this._loadingInProgress ) {
1385
1529
 
@@ -1391,8 +1535,10 @@ export class PathTracerApp extends EventDispatcher {
1391
1535
  this._preserveSelectionOnRebuild = true;
1392
1536
  try {
1393
1537
 
1394
- const { root } = await this.assetLoader.appendModel( url );
1538
+ const { root } = await this.assetLoader.appendModel( url, { cacheKey } );
1395
1539
  root.userData.__rayzeeSceneObject = true;
1540
+ root.userData.__rayzeeSourceUrl = url;
1541
+ if ( cacheKey ) root.userData.__rayzeeCacheKey = cacheKey;
1396
1542
  if ( name ) root.userData.__rayzeeName = name;
1397
1543
  await this._finishRebuildNoReframe( { type: 'ModelAdded', url, id: root.uuid } );
1398
1544
  return root.uuid;
@@ -1545,6 +1691,75 @@ export class PathTracerApp extends EventDispatcher {
1545
1691
 
1546
1692
  }
1547
1693
 
1694
+ /**
1695
+ * Everything changed since the scene loaded — render settings, environment, colour, lights,
1696
+ * cameras, timeline keys, material edits, hidden and moved objects — as JSON-safe data. The
1697
+ * model is not in it: {@link importSceneState} applies it once the host has loaded that again.
1698
+ * @returns {Object}
1699
+ */
1700
+ exportSceneState() {
1701
+
1702
+ return captureSceneState( this );
1703
+
1704
+ }
1705
+
1706
+ /**
1707
+ * Applies {@link exportSceneState}'s data to the scene now loaded. Objects, materials and
1708
+ * cameras are matched by position plus name; a scene that differs keeps its own objects and
1709
+ * materials and the rest still applies.
1710
+ * @param {Object} state
1711
+ * @param {Object} [options]
1712
+ * @param {function(Object): Promise<*>} [options.resolve] - supplies what the engine cannot
1713
+ * reach: `{ kind: 'environment', source }` → File | URL | null, `{ kind: 'colorConfig', config }`
1714
+ * → true once loaded. Without it, URL environments and built-in colour configs still restore.
1715
+ * @returns {Promise<{skipped: Array<{section: string, reason: string}>}>}
1716
+ */
1717
+ async importSceneState( state, options ) {
1718
+
1719
+ return await applySceneState( this, state, options );
1720
+
1721
+ }
1722
+
1723
+ /** Triangle and placement budgets for a scene archive: raised when the build can spill. @private */
1724
+ _sceneBudgets( options ) {
1725
+
1726
+ if ( ! this._memorySpill || ! this.storage ) return options;
1727
+ return { maxTriangles: SPILL_TRIANGLE_BUDGET, maxPlacements: SPILL_PLACEMENT_BUDGET, ...options };
1728
+
1729
+ }
1730
+
1731
+ /**
1732
+ * Experimental memory spill, during the build: static scenes only, since a deforming clip
1733
+ * refits every frame. @private
1734
+ */
1735
+ _progressiveSpill() {
1736
+
1737
+ if ( ! this._memorySpill || ! this.storage || ( this.assetLoader?.animations?.length ?? 0 ) > 0 ) return null;
1738
+ const stage = this.stages.pathTracer;
1739
+ return { storage: this.storage, uploader: ( records, kind ) => kind === 'triangles' ? stage.createTriangleChunkUploader( records ) : stage.createChunkUploader( records ) };
1740
+
1741
+ }
1742
+
1743
+ /** Experimental memory spill for a static scene, in the background. @private */
1744
+ _maybeSpill() {
1745
+
1746
+ if ( ! this._memorySpill || ! this.storage || this.animationManager?.hasAnimations ) return;
1747
+ this._sdf?.spillToDisk( this.storage )
1748
+ .then( result => result && this.dispatchEvent( { type: 'SceneSpilled', ...result } ) )
1749
+ .catch( error => this._issues.warn( ISSUE_CODES.STORAGE_WRITE_FAILED, `memory spill failed: ${error.message}` ) );
1750
+
1751
+ }
1752
+
1753
+ /**
1754
+ * Reads back what {@link options.memorySpill} moved to disk. The engine does this itself before
1755
+ * its own readers; a host calling {@link refitBLASes} on a spilled scene awaits it first.
1756
+ */
1757
+ async ensureSceneResident() {
1758
+
1759
+ if ( this._sdf?.spilled ) await this._sdf.ensureResident();
1760
+
1761
+ }
1762
+
1548
1763
  /** Notify consumers that the camera list changed (names / count). */
1549
1764
  _dispatchCamerasUpdated() {
1550
1765
 
@@ -1623,6 +1838,7 @@ export class PathTracerApp extends EventDispatcher {
1623
1838
  */
1624
1839
  async refitBVH( newPositions, newNormals ) {
1625
1840
 
1841
+ await this.ensureSceneResident();
1626
1842
  const result = await this._sdf.refitBVH( newPositions, newNormals );
1627
1843
 
1628
1844
  this.stages.pathTracer.updateTriangleData( this._sdf.triangles );
@@ -1645,6 +1861,7 @@ export class PathTracerApp extends EventDispatcher {
1645
1861
  */
1646
1862
  refitBLASes( affectedMeshIndices, newPositions, newNormals ) {
1647
1863
 
1864
+ if ( this._sdf?.spilled ) throw new Error( 'refitBLASes: the scene is spilled to disk — await app.ensureSceneResident() first' );
1648
1865
  const result = this._sdf.refitBLASes( affectedMeshIndices, newPositions, newNormals );
1649
1866
 
1650
1867
  const { triRanges, bvhRanges } = this._sdf.computeBLASDirtyRanges( affectedMeshIndices );
@@ -1725,6 +1942,13 @@ export class PathTracerApp extends EventDispatcher {
1725
1942
  _refreshMovedEmitters() {
1726
1943
 
1727
1944
  if ( ! this._emittersMoved ) return;
1945
+ if ( this._sdf?.spilled ) {
1946
+
1947
+ this.ensureSceneResident().then( () => this._refreshMovedEmitters() );
1948
+ return;
1949
+
1950
+ }
1951
+
1728
1952
  this._emittersMoved = false;
1729
1953
  this._uploadEmissivePayload( this._sdf?.refreshEmissiveTransforms() ?? null );
1730
1954
 
@@ -2088,6 +2312,7 @@ export class PathTracerApp extends EventDispatcher {
2088
2312
  // refresh landing in the middle of that raced the renderer's own output pass.
2089
2313
  this.denoisingManager?.setCadenceSuspended( isProduction );
2090
2314
 
2315
+ this.timeline.stop();
2091
2316
  this.cameraManager.controls.enabled = ! isProduction;
2092
2317
 
2093
2318
  // Anything with a SETTING_ROUTES entry must go through settings, not setUniform: set() early-returns on
@@ -2097,6 +2322,10 @@ export class PathTracerApp extends EventDispatcher {
2097
2322
  // renderMode has no SETTING_ROUTES entry
2098
2323
  this.stages.pathTracer?.setUniform( 'renderMode', parseInt( config.renderMode ) );
2099
2324
 
2325
+ // A move just before this left the moving-camera drop in force or queued; a final render
2326
+ // never runs at it.
2327
+ if ( isProduction ) this._applyRenderScale( 1 );
2328
+
2100
2329
  this.stages.pathTracer?.updateCompletionThreshold?.();
2101
2330
 
2102
2331
  const denoiser = this.denoisingManager?.denoiser;
@@ -2440,6 +2669,41 @@ export class PathTracerApp extends EventDispatcher {
2440
2669
 
2441
2670
  }
2442
2671
 
2672
+ /**
2673
+ * A render in progress as data: the accumulated colour (and the aux buffers while they hold
2674
+ * samples), the per-pixel convergence buffers, and the counters that choose the next sample.
2675
+ * {@link restoreRenderCheckpoint} continues it — bit-for-bit in deterministic mode, since the
2676
+ * random sequence is a pure function of pixel, frame and seed tick. At 4K it is ~500 MB.
2677
+ * @returns {Promise<?Object>} null before the path tracer exists
2678
+ */
2679
+ async captureRenderCheckpoint() {
2680
+
2681
+ const stage = this.stages.pathTracer;
2682
+ if ( ! stage ) return null;
2683
+ const accumulation = await stage.captureAccumulation();
2684
+ return { v: RENDER_CHECKPOINT_VERSION, engine: VERSION, savedAt: Date.now(), samples: accumulation.state.frameCount, ...accumulation };
2685
+
2686
+ }
2687
+
2688
+ /**
2689
+ * Continues a {@link captureRenderCheckpoint}. The scene, settings and render size must be the
2690
+ * ones it was taken with (restore the scene state first), and the scene must have rendered a
2691
+ * frame at that size since it last changed. Anything that resets accumulation afterwards
2692
+ * discards it. Rendering is not restarted: call {@link wake} or {@link renderFrames}.
2693
+ * @param {Object} checkpoint
2694
+ */
2695
+ restoreRenderCheckpoint( checkpoint ) {
2696
+
2697
+ if ( checkpoint?.v !== RENDER_CHECKPOINT_VERSION ) throw new Error( `render checkpoint version ${checkpoint?.v} is not ${RENDER_CHECKPOINT_VERSION}` );
2698
+ const stage = this.stages.pathTracer;
2699
+ if ( ! stage ) throw new Error( 'restoreRenderCheckpoint: app is not initialized' );
2700
+
2701
+ stage.restoreAccumulation( checkpoint );
2702
+ this.needsReset = false;
2703
+ this.dispatchEvent( { type: EngineEvents.RENDER_RESET, restored: checkpoint.samples } );
2704
+
2705
+ }
2706
+
2443
2707
  /**
2444
2708
  * The path tracer's accumulation as pixels, read from its storage target rather than the
2445
2709
  * canvas — so it works headless, works while the page is hidden, and cannot pick up a
@@ -2945,6 +3209,14 @@ export class PathTracerApp extends EventDispatcher {
2945
3209
 
2946
3210
  setMaterialProperty( materialIndex, property, value ) {
2947
3211
 
3212
+ // These rewrite triangle records, which a spilled scene has to read back first.
3213
+ if ( this._sdf?.spilled && TRIANGLE_PATCH_PROPERTIES.has( property ) ) {
3214
+
3215
+ this.ensureSceneResident().then( () => this.setMaterialProperty( materialIndex, property, value ) );
3216
+ return;
3217
+
3218
+ }
3219
+
2948
3220
  this.stages.pathTracer?.materialData.updateMaterialProperty( materialIndex, property, value );
2949
3221
 
2950
3222
  // Keep the emissive-NEE structure in sync unconditionally (not gated on the
@@ -3248,6 +3520,9 @@ export class PathTracerApp extends EventDispatcher {
3248
3520
  }
3249
3521
 
3250
3522
  if ( envChanged ) await this.environmentManager?.buildEnvironmentCDF?.();
3523
+ // Its pixels never leave the GPU, so it is baked again in the new space rather than converted.
3524
+ if ( env?._isPhysicalSky ) await this.environmentManager.generateProceduralSkyTexture();
3525
+ this.environmentManager?.refreshSun?.();
3251
3526
 
3252
3527
  this.reset();
3253
3528
 
@@ -3259,6 +3534,7 @@ export class PathTracerApp extends EventDispatcher {
3259
3534
  */
3260
3535
  async rebuildMaterials( scene ) {
3261
3536
 
3537
+ await this.ensureSceneResident();
3262
3538
  await this.stages.pathTracer?.rebuildMaterials( scene || this.meshScene );
3263
3539
  this.reset();
3264
3540
 
@@ -3390,6 +3666,7 @@ export class PathTracerApp extends EventDispatcher {
3390
3666
  _initCameraManager() {
3391
3667
 
3392
3668
  this.cameraManager = new CameraManager( this.canvas );
3669
+ this.timeline = new TimelineManager( { cameraManager: this.cameraManager, onReset: () => this.reset() } );
3393
3670
 
3394
3671
  }
3395
3672
 
@@ -3415,14 +3692,19 @@ export class PathTracerApp extends EventDispatcher {
3415
3692
  this.assetLoader.setRenderer( this.renderer );
3416
3693
  this.assetLoader.createFloorPlane();
3417
3694
 
3418
- this._addTrackedListener( this.cameraManager.controls, 'change', () => {
3695
+ const onCameraMoved = () => {
3419
3696
 
3420
3697
  this.needsReset = true;
3421
3698
  // Here rather than in render(), so the first frame of the move is already at the lower resolution.
3422
3699
  this.stages.pathTracer?.enterInteractionMode();
3423
3700
  this.wake();
3424
3701
 
3425
- } );
3702
+ };
3703
+
3704
+ this._addTrackedListener( this.cameraManager.controls, 'change', onCameraMoved );
3705
+ this._addTrackedListener( this.cameraManager.walkControls, 'change', onCameraMoved );
3706
+ // A held key moves the camera from the frame loop, which may be asleep.
3707
+ this._addTrackedListener( this.cameraManager.walkControls, 'start', () => this.wake() );
3426
3708
 
3427
3709
  }
3428
3710
 
@@ -3518,6 +3800,8 @@ export class PathTracerApp extends EventDispatcher {
3518
3800
  // Expose environment manager (lives on pathTracer stage)
3519
3801
  this.environmentManager = this.stages.pathTracer.environment;
3520
3802
  this.environmentManager.callbacks.onAutoExposureReset = () => this.pipeline.eventBus.emit( 'autoexposure:resetHistory' );
3803
+ // A whole-app reset, not the stage's: a sky bake lands after its input, often once the loop is idle.
3804
+ this.environmentManager.callbacks.onReset = () => this.reset();
3521
3805
 
3522
3806
  }
3523
3807
 
@@ -3526,6 +3810,8 @@ export class PathTracerApp extends EventDispatcher {
3526
3810
  // Forward manager events → app events
3527
3811
  this._addTrackedListener( this.cameraManager, 'CameraSwitched', ( e ) => this.dispatchEvent( e ) );
3528
3812
  this._addTrackedListener( this.cameraManager, EngineEvents.AUTO_FOCUS_UPDATED, ( e ) => this.dispatchEvent( e ) );
3813
+ this._addTrackedListener( this.cameraManager, EngineEvents.ORTHO_HEIGHT_UPDATED, ( e ) => this.dispatchEvent( e ) );
3814
+ this._addTrackedListener( this.timeline, EngineEvents.TIMELINE_CHANGED, ( e ) => this.dispatchEvent( e ) );
3529
3815
 
3530
3816
  this._forwardEvents( this.denoisingManager, [
3531
3817
  EngineEvents.DENOISING_START, EngineEvents.DENOISING_END,
@@ -3566,9 +3852,7 @@ export class PathTracerApp extends EventDispatcher {
3566
3852
 
3567
3853
  // Auto-focus context — CameraManager stores it, reads it each frame
3568
3854
  this.cameraManager.initAutoFocus( {
3569
- meshScene: this.meshScene,
3570
3855
  assetLoader: this.assetLoader,
3571
- floorPlane: this.assetLoader.floorPlane,
3572
3856
  pathTracer: this.stages.pathTracer,
3573
3857
  settings: this.settings,
3574
3858
  softReset: () => this.reset( true ),
@@ -3582,12 +3866,12 @@ export class PathTracerApp extends EventDispatcher {
3582
3866
  resetCallback: () => this.reset(),
3583
3867
  reconcileCompletion: () => this._reconcileCompletion(),
3584
3868
  denoisingManager: this.denoisingManager,
3585
- cameraManager: this.cameraManager,
3586
3869
  onInteractionRenderScale: () => {
3587
3870
 
3588
3871
  if ( this.stages.pathTracer?.interactionMode ) this._requestRenderScale( this._interactionRenderScale() );
3589
3872
 
3590
3873
  },
3874
+ onCameraProjection: ( value ) => this.cameraManager.applyProjection( value ),
3591
3875
  } );
3592
3876
 
3593
3877
  this.renderer.toneMappingExposure = this.settings.get( 'exposure' ) ?? 1.0;
@@ -3643,6 +3927,8 @@ export class PathTracerApp extends EventDispatcher {
3643
3927
 
3644
3928
  const cameras = [ this.cameraManager.camera, ...( event.cameras || [] ) ];
3645
3929
  this.cameraManager.setCameras( cameras );
3930
+ // Keys are poses in the scene being replaced, like the cameras saved in it.
3931
+ this.timeline.clear();
3646
3932
 
3647
3933
  if ( this.interactionManager ) {
3648
3934
 
@@ -3821,8 +4107,10 @@ export class PathTracerApp extends EventDispatcher {
3821
4107
 
3822
4108
  _syncControlsAfterLoad() {
3823
4109
 
4110
+ this.cameraManager.fitOrthographic();
3824
4111
  this.cameraManager.controls.saveState();
3825
4112
  this.cameraManager.controls.update();
4113
+ this.cameraManager.walkControls.fitSpeed( this.assetLoader.getSceneScale() );
3826
4114
 
3827
4115
  }
3828
4116