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
package/README.md CHANGED
@@ -22,9 +22,10 @@ A real-time WebGPU path tracing engine built on Three.js. Framework-agnostic —
22
22
  - [Configuring Assets (CDN URLs & cache namespace)](#configuring-assets-cdn-urls--cache-namespace)
23
23
  - [PathTracerApp](#pathtracerapp)
24
24
  - [engine.cameraManager](#enginecameramanager)
25
- - [Camera Projection (360° Panorama)](#camera-projection-360-panorama)
25
+ - [Camera Projection (Orthographic, 360° Panorama)](#camera-projection-orthographic-360-panorama)
26
26
  - [engine.lightManager](#enginelightmanager)
27
27
  - [engine.animationManager](#engineanimationmanager)
28
+ - [engine.timeline](#enginetimeline)
28
29
  - [Materials](#materials)
29
30
  - [Colour Management](#colour-management)
30
31
  - [engine.environmentManager](#engineenvironmentmanager)
@@ -293,9 +294,12 @@ configureAssets({
293
294
  ocioRuntimeFactory: () => import('@bb-studio/ocio'), // or: ocioRuntimeUrl: '/vendor/ocio/index.js'
294
295
  ocioWasmUrl: '/vendor/ocio/ocio-wasm.wasm', // optional override for the .wasm
295
296
 
296
- // Prefix for engine-managed IndexedDB stores. Set to a unique value if multiple
297
- // apps embed the engine on the same origin to avoid cache collisions.
297
+ // Names the engine's on-disk storage (an OPFS directory). Set a unique value if several
298
+ // apps embed the engine on the same origin, so their caches stay apart.
298
299
  cacheNamespace: 'my-app',
300
+
301
+ // On-disk storage: 'auto' (default) where the browser has it, false for none.
302
+ storage: 'auto',
299
303
  });
300
304
 
301
305
  const engine = new PathTracerApp(canvas);
@@ -320,6 +324,8 @@ const engine = new PathTracerApp(canvas, options?)
320
324
  | `options.strict` | `boolean` | Throw an `EngineIssueError` where the engine would otherwise degrade and carry on (default: `false`). See [Degradation contract](#degradation-contract). |
321
325
  | `options.profile` | `string` | `'viewer'` (default) or `'physical'` — product tuning that is not a physical constant: area-light scale, environment rotation, tone mapping, saturation. An unknown name throws. |
322
326
  | `options.maxSceneBytes` | `number` | Raise or lower the CPU memory ceiling a scene may need before the engine refuses it (default 9,216 MB). See [Memory monitoring](#memory-monitoring). |
327
+ | `options.storage` | `false \| 'auto' \| StorageManager` | On-disk storage (default: `configureAssets( { storage } )`). A manager you pass stays yours to dispose. See [On-disk storage](#on-disk-storage-opfs). |
328
+ | `options.memorySpill` | `boolean` | Experimental, default `false`: build a large static scene through disk — triangle records, BLAS nodes and the three.js geometry are written out as the build finishes with them — and raise the pbrt triangle and placement caps to 60M / 8M. See [On-disk storage](#on-disk-storage-opfs). |
323
329
 
324
330
  The engine creates and mounts everything it needs (denoiser canvas, tile/HUD overlay) into a single parent on `init()`. Performance HUDs (e.g. `stats-gl`) are not bundled — listen to `EngineEvents.FRAME` and tick your own panel.
325
331
 
@@ -391,10 +397,20 @@ try {
391
397
  }
392
398
  ```
393
399
 
394
- Per-load options for pbrt archives: `promptBytes` moves that 4 GB line, `maxTriangles` (default
395
- 45M) and `maxPlacements` (default 6M) cap the build — past either, placements are skipped and the
396
- build reports itself truncated. 45M is the highest rung measured to survive; raising it is a
397
- deliberate act on a fresh browser tab.
400
+ Per-load options for pbrt archives:
401
+
402
+ | Option | Default | Effect |
403
+ |---|---|---|
404
+ | `promptBytes` | 4 GB | moves the line past which a multi-element archive asks for elements |
405
+ | `maxTriangles` | 45M (60M with `memorySpill`) | past it, placements are skipped and the build reports itself truncated |
406
+ | `maxPlacements` | 6M (8M with `memorySpill`) | the same, for placements |
407
+ | `curveTolerance` | 0.05 | how far a curve segment may stray, × the curve's half-width; curves become strips with adaptive segments. `0` gives the old uniform strips bit for bit |
408
+ | `instanceIncludes` | `true` | a file included again under the same material, with no side effects, is placed as an instance of its first reading instead of being read and stored again |
409
+
410
+ 45M is the highest rung measured to survive without the spill; raising either cap is a deliberate
411
+ act on a fresh browser tab. With `memorySpill`, 80M stored triangles (4.35M placements) have loaded
412
+ and rendered; an 89M load ran out of memory while parsing, and WebGPU's 4 GB buffer limit stops the
413
+ triangle data at 89.5M in any case.
398
414
 
399
415
  #### Settings
400
416
 
@@ -428,16 +444,19 @@ Key settings:
428
444
  | `maxSubsurfaceSteps` | `number` | 8 | Max random-walk steps for subsurface scattering (raised to 64 by `configureForMode('production')`) |
429
445
  | `enableAlphaShadows` | `boolean` | false | Alpha-tested shadow rays (enabled by `configureForMode('production')`) |
430
446
  | `enableDOF` | `boolean` | false | Enable depth of field |
431
- | `focusDistance` | `number` | 0.8 | DOF focus distance |
432
- | `aperture` | `number` | 5.6 | DOF aperture (f-stop) |
433
- | `focalLength` | `number` | 50 | DOF focal length (mm) |
447
+ | `dofMode` | `string` | 'look' | `'look'`: the blur is set by `dofBlur`, the same at any scene scale; `'physical'`: a real lens set by `aperture`, `focalLength` and `unitsPerMetre`. The `physical` render profile defaults to `'physical'` |
448
+ | `dofBlur` | `number` | 0.05 | Look mode: how far a distant background blurs, as a fraction of the image height |
449
+ | `focusDistance` | `number` | 0.8 | DOF focus distance in scene units — depth along the view axis (the focal plane is flat) |
450
+ | `aperture` | `number` | 5.6 | Physical mode: f-stop |
451
+ | `focalLength` | `number` | 50 | Physical mode: focal length (mm) |
452
+ | `unitsPerMetre` | `number` | 1 | Physical mode: scene units per real metre, for files whose units are not metres. It carries over between model loads — reset it when the new file's units differ |
434
453
  | `transparentBackground` | `boolean` | false | Transparent canvas background |
435
454
  | `interactionModeEnabled` | `boolean` | true | Render at lower resolution while the camera moves, keeping the full bounce budget ("Fast Navigation" in the app) |
436
455
  | `interactionRenderScale` | `number` | 0.5 | Per-axis render scale while the camera moves (0.5 = a quarter of the pixels); `1` turns the drop off. Ignored while OIDN is the live denoiser |
437
456
  | `renderMode` | `number` | 0 | Internal preview(0)/production(1) flag driving accumulation & ASVGF behavior — normally set via `configureForMode()`, not written directly |
438
457
  | `visMode` | `number` | 0 | Debug visualization mode (0 = off) |
439
458
  | `environmentMode` | `string` | 'hdri' | Sky mode: `'hdri'` \| `'procedural'` \| `'gradient'` \| `'color'` — not routed through `engine.settings`; use `engine.environmentManager.setMode()` instead |
440
- | `cameraProjection` | `string` | 'perspective' | `'perspective'` \| `'equirectangular'` — see [Camera Projection](#camera-projection-360-panorama) |
459
+ | `cameraProjection` | `string` | 'perspective' | `'perspective'` \| `'orthographic'` \| `'equirectangular'` — see [Camera Projection](#camera-projection-orthographic-360-panorama) |
441
460
  | `panoramaLonRange` | `[number, number]` | `[-180, 180]` | Panorama longitude sweep, degrees, left→right |
442
461
  | `panoramaLatRange` | `[number, number]` | `[-90, 90]` | Panorama latitude sweep, degrees, bottom→top |
443
462
  | `panoramaLevelHorizon` | `boolean` | true | Yaw-only panorama basis, so orbit pitch/roll can't tilt the horizon |
@@ -467,21 +486,45 @@ To pause rendering for image-viewing UI, set `engine.pauseRendering = true` and
467
486
  Camera switching, auto-focus, DOF, and direct Three.js access.
468
487
 
469
488
  ```js
470
- engine.cameraManager.active // The active PerspectiveCamera
489
+ engine.cameraManager.active // The active camera: a PerspectiveCamera that can turn orthographic
471
490
  engine.cameraManager.controls // The OrbitControls instance
472
491
  engine.cameraManager.switchCamera(index) // Switch between scene cameras
473
492
  engine.cameraManager.getNames() // List available cameras
474
493
  engine.cameraManager.focusOn(center) // Focus orbit camera on a world-space point
475
494
  engine.cameraManager.setAutoFocusMode(mode) // 'auto' | 'manual'
476
495
  engine.cameraManager.setAFScreenPoint(x, y) // Set normalized AF screen point (0-1)
496
+ engine.cameraManager.setNavigationMode(mode) // 'orbit' | 'walk'
497
+ engine.cameraManager.walkControls.speed // Walk speed, scene units per second
498
+ engine.cameraManager.orthoHeight // Orthographic view height, scene units, wheel zoom included
499
+ engine.cameraManager.setOrthoHeight(height) // Set it
477
500
  ```
478
501
 
479
- ### Camera Projection (360° Panorama)
502
+ **Walk mode** is first-person navigation: drag to look, W A S D or the arrow keys to walk level, E and Q to
503
+ rise and sink, Shift faster, Alt slower. A new model resets `speed` so the walk crosses it in about eight
504
+ seconds. Keys are ignored while focus is in a text field, list or menu, and when a focused control has
505
+ already used the key. The mode obeys `controls.enabled`, so anything that locks the orbit camera locks walking
506
+ too. Switching back to `'orbit'` circles the surface at the centre of the view.
480
507
 
481
- Two camera models live behind the `cameraProjection` setting. Both branches are compiled into the same kernel, so switching writes a uniform and resets accumulation — it never recompiles WGSL.
508
+ ### Camera Projection (Orthographic, 360° Panorama)
509
+
510
+ Three camera models live behind the `cameraProjection` setting. All are compiled into the same kernel, so switching writes a uniform and resets accumulation — it never recompiles WGSL.
511
+
512
+ #### Orthographic
482
513
 
483
514
  ```js
484
- engine.settings.set('cameraProjection', 'equirectangular'); // 'perspective' (default) | 'equirectangular'
515
+ engine.settings.set('cameraProjection', 'orthographic');
516
+ engine.cameraManager.setOrthoHeight(12); // the view's height in scene units
517
+ engine.addEventListener(EngineEvents.ORTHO_HEIGHT_UPDATED, ({ height }) => {}); // the wheel changed it
518
+ ```
519
+
520
+ Rays are parallel and start on the camera's image plane, so nothing behind the camera is seen and nothing shrinks with distance. Switching keeps what the view shows at the orbit target: turning orthographic sizes the view from the orbit distance and field of view, and turning back moves the camera to match. The wheel then zooms by changing the view's size rather than moving the camera. A new model is framed the same way.
521
+
522
+ `engine.cameraManager.active` stays the same object — it switches its own projection and reports `isOrthographicCamera` — so picking, the transform gizmo and the overlays follow without anything being re-pointed. Imported orthographic cameras (glTF, and pbrt's `Camera "orthographic"`) switch the projection to orthographic at their own size, and a camera left orthographic comes back so; any other camera switches it back to perspective. Every denoiser, auto-focus and depth of field keep working. An environment at infinity is seen from a single direction, so the background is one colour.
523
+
524
+ #### 360° Panorama
525
+
526
+ ```js
527
+ engine.settings.set('cameraProjection', 'equirectangular');
485
528
 
486
529
  // Optional: crop the sweep. Degrees, [min, max].
487
530
  engine.settings.set('panoramaLonRange', [-90, 90]); // VR180
@@ -496,9 +539,9 @@ Depth of field still works: the lens plane is built from each ray's own frame, n
496
539
  Two features are incompatible with a non-frustum camera and the engine switches them off for you when panorama is enabled:
497
540
 
498
541
  - **ASVGF** falls back to the `edgeaware` denoiser — ASVGF's motion vectors unproject through the projection matrix, which is meaningless when every pixel is its own direction.
499
- - **Auto-focus** switches to `'manual'` — it raycasts via `Raycaster.setFromCamera`, which only understands a frustum.
542
+ - **Auto-focus** pauses and focus holds its last distance — it raycasts via `Raycaster.setFromCamera`, which only understands a frustum. It resumes when you leave the panorama.
500
543
 
501
- Read the outcome back rather than duplicating the rule (`engine.denoisingManager.denoiserStrategy`, `engine.cameraManager.autoFocusMode`). Neither is restored automatically when you switch back to `'perspective'`.
544
+ Read the denoiser outcome back rather than duplicating the rule (`engine.denoisingManager.denoiserStrategy`); it is not restored automatically when you leave the panorama.
502
545
 
503
546
  ### engine.lightManager
504
547
 
@@ -529,6 +572,34 @@ engine.animationManager.setLoop(true) // Enable/disable looping
529
572
  engine.animationManager.clips // Get available animation clips
530
573
  ```
531
574
 
575
+ ### engine.timeline
576
+
577
+ Authored animation: keyframed tracks on one time axis, in seconds. The camera's is the first track; the timeline is where lights and objects will join it. A model's own clips stay with `engine.animationManager`, and saved cameras (`engine.addCamera()`) stay cameras — a keyframe is not a camera.
578
+
579
+ ```js
580
+ const camera = engine.timeline.camera // the camera's track
581
+ const key = camera.addKey() // key the current view, 2 s after the last key
582
+ camera.addKey(5) // …or at a time
583
+ camera.updateKey(key.id) // give a key the current view
584
+ camera.setTime(key.id, 3.5) // retime it; keys stay in time order
585
+ camera.remove(key.id)
586
+ camera.keys // [{ id, time, position, target, fov, orthoHeight }]
587
+
588
+ engine.timeline.duration // seconds to the last key of any track
589
+ engine.timeline.animates // a track has two keys or more
590
+ engine.timeline.seek(2.5) // put the scene where the timeline has it at 2.5 s
591
+ await engine.timeline.play() // run it in the viewport, controls locked
592
+ engine.timeline.stop() // or stop early
593
+
594
+ // Keys changed ({ track: 'camera' }) or playback started or stopped ({ track: undefined })
595
+ engine.addEventListener(EngineEvents.TIMELINE_CHANGED, ({ track }) => {})
596
+
597
+ // A video of the move through a still scene; pass clipIndex too to move the camera during a clip
598
+ await new VideoRenderManager(engine).renderAnimation({ timeline: engine.timeline, fps: 30, onFrame });
599
+ ```
600
+
601
+ Between keys the camera glides along a smooth curve through their positions while looking along another through their targets, so a subject every key looks at stays in frame. It leaves the first key and reaches the last at rest, runs straight through the keys between, and holds still outside them. FOV, or an orthographic view's height, blends too, in the projection in use. The curves are three.js keyframe tracks (`InterpolateSmooth`). With auto-focus on, focus is measured again on every video frame, and the view is put back when the render ends. A new model clears the keys, as it clears saved cameras.
602
+
532
603
  ### Materials
533
604
 
534
605
  Material property updates and texture transforms — accessed as direct methods on the engine.
@@ -603,12 +674,29 @@ engine.environmentManager.texture // The loaded environment texture
603
674
  await engine.loadEnvironment(url) // Load HDR/EXR environment map (method on engine)
604
675
  await engine.environmentManager.setEnvironmentMap(tex) // Set a custom environment texture
605
676
  await engine.environmentManager.setMode(mode) // 'hdri' | 'procedural' | 'gradient' | 'color'
606
- await engine.environmentManager.generateProcedural() // Preetham-model sky
677
+ await engine.environmentManager.generateProcedural() // Physical sky: spectral, multiple scattering, analytic sun
607
678
  await engine.environmentManager.generateGradient() // Gradient sky
608
679
  await engine.environmentManager.generateSolid() // Solid color sky
609
680
  engine.environmentManager.markDirty() // Flag environment for GPU re-upload
610
681
  ```
611
682
 
683
+ The physical sky (mode `'procedural'`) is set through `params`, then baked. It is baked and
684
+ importance-sampled entirely on the GPU (~1.6 ms per sun move), so bake on every slider event rather
685
+ than debouncing: requests made in one task become one bake, and the promise resolves once the
686
+ environment has caught up. The sun is drawn and sampled as a light of its own, not from the texture.
687
+
688
+ ```js
689
+ import { sunPosition, dayOfYearForMonth } from 'rayzee';
690
+
691
+ const p = engine.environmentManager.params;
692
+ const { azimuth, elevation } = sunPosition( { hours: 17.5, dayOfYear: dayOfYearForMonth( 6 ), latitude: 40 } );
693
+ const az = ( 180 - azimuth ) * Math.PI / 180, el = elevation * Math.PI / 180; // north along −Z, east along +X
694
+ p.skySunDirection.set( Math.cos( el ) * Math.sin( az ), Math.sin( el ), Math.cos( el ) * Math.cos( az ) );
695
+ p.skyTurbidity = 3; // haze, 1–10; also skyOzone (Dobson units), skyAirDensity (× Earth's),
696
+ // skyGroundAlbedo (Color), skyAltitude (m), skySunSize (°), skySunStrength
697
+ await engine.environmentManager.generateProcedural();
698
+ ```
699
+
612
700
  ### engine.denoisingManager
613
701
 
614
702
  Denoiser strategy, ASVGF, OIDN, upscaler, and auto-exposure.
@@ -841,7 +929,8 @@ Before extraction the engine prices the scene and applies two lines, both record
841
929
 
842
930
  Raise or lower the hard line with `new PathTracerApp(canvas, { maxSceneBytes })`. The estimate runs
843
931
  low at the very top of its range, so the per-load `maxTriangles` cap (45M) is the more reliable
844
- guard on a scene of that size.
932
+ guard on a scene of that size. With `memorySpill` the estimate leaves out what the build keeps on
933
+ disk (the BVH, and triangle records past what a streamed build holds at once).
845
934
 
846
935
  ---
847
936
 
@@ -912,6 +1001,87 @@ precision, the model, kernel capabilities, tile state and resource counts — wh
912
1001
  confirm FP16 actually engaged on a given GPU rather than inferring it from the device's feature
913
1002
  list. Both need `timestamp-query`, and `getDenoiseProfile()` returns `null` when OIDN is not set up.
914
1003
 
1004
+ ### On-disk Storage (OPFS)
1005
+
1006
+ `engine.storage` is a `StorageManager` over the browser's origin private file system, or `null` where
1007
+ there is none (a private window, Node) — everything works without it, only slower. The engine keeps
1008
+ its caches there: downloads (models, skies, OIDN weights, STBN atlases — revalidated with `HEAD`),
1009
+ unpacked `.tar.gz` archives and archive indexes, built scenes (reopened without rebuilding BVHs when
1010
+ the first build took ≥ 10 s), and environment sampling tables. Caches share a budget of 30 % of the
1011
+ quota and are evicted least-recently-used; `engine.storage.usage()` reports each area.
1012
+
1013
+ A host keeps its own data beside them:
1014
+
1015
+ ```js
1016
+ import { STORAGE_KIND } from 'rayzee';
1017
+
1018
+ engine.storage.defineArea( 'renders', { kind: STORAGE_KIND.USER } ); // never evicted
1019
+ const writer = await engine.storage.area( 'renders' ).create( 'render:42', { label: 'Render 42' } );
1020
+ await writer.writeFile( 'image.png', blob );
1021
+ await writer.commit(); // meta.json last: all or nothing
1022
+
1023
+ const entry = await engine.storage.area( 'renders' ).open( 'render:42' );
1024
+ const file = await entry.file( 'image.png' );
1025
+ entry.release();
1026
+ ```
1027
+
1028
+ `loadFile( url )` and `loadModel( url, { cacheKey } )` download through the cache — pass `cacheKey` for
1029
+ a link that expires (a signed URL). `fetchFile( url, storage )` / `cachedObjectURL( url )` do the same
1030
+ for a host's own assets. Failures record `storage.*` issues and fall back to memory; storage never
1031
+ throws for being absent or full.
1032
+
1033
+ **Memory spill (experimental).** With `memorySpill: true`, a static scene of more than one 64 MB chunk
1034
+ is built through disk, so its large arrays are never all in memory at once:
1035
+
1036
+ - Extraction and BVH building run together. Each stored range of triangles goes to a BLAS worker as
1037
+ soon as it is written, and extraction waits while more than 1.5 GB of triangle records are held.
1038
+ On the 55.7M-triangle Moana subset the build peak fell from 6.6 to 4.1 GB.
1039
+ - The three.js geometry goes to disk after the build last reads it, and comes back when the build
1040
+ ends. On a 70M-triangle scene the page held 0.84 GB after extraction instead of 4.07 GB.
1041
+ - Each triangle chunk and BLAS goes to the GPU, then to disk. At 50M triangles the page settles at
1042
+ 7.2 GB instead of 9.1 GB.
1043
+
1044
+ With or without the spill, a mesh past 2M triangles is built as spatial pieces of ≤ 512k triangles
1045
+ on a worker pool and joined under the tree that split them, so no build holds a second copy of it.
1046
+
1047
+ The render is unchanged. Visibility and rigid moves need nothing read back. Material edits that
1048
+ rewrite triangles (side, transparency, emission), and refits, read it back first. `refitBLASes`
1049
+ throws on a spilled scene until `await engine.ensureSceneResident()`.
1050
+
1051
+ ### Saving Scene State
1052
+
1053
+ ```js
1054
+ const state = engine.exportSceneState(); // JSON-safe: settings, sky, colour, lights, cameras,
1055
+ // timeline keys, material edits, hidden and moved objects
1056
+ const source = engine.sceneSource; // { kind: 'url', url, cacheKey } | { kind: 'local-file', file } | ...
1057
+
1058
+ // later, after loading the same model again:
1059
+ const { skipped } = await engine.importSceneState( state, {
1060
+ resolve: async ( request ) => {
1061
+ // { kind: 'environment', source } → a File, a URL or null (the engine loads URL skies itself)
1062
+ // { kind: 'colorConfig', config } → true once you have loaded it (built-in configs load themselves)
1063
+ return null;
1064
+ },
1065
+ } );
1066
+ ```
1067
+
1068
+ Objects, materials and cameras are matched by position in the scene plus a name check — UUIDs
1069
+ change on every load — so a different model keeps its own objects and materials and the rest still
1070
+ applies; `skipped` lists what could not be put back. Texture swaps on materials are not included yet.
1071
+
1072
+ ### Render Checkpoints
1073
+
1074
+ ```js
1075
+ const checkpoint = await engine.captureRenderCheckpoint(); // accumulation + the counters that pick the next sample
1076
+ // ...a reload later, the same scene state restored and one frame rendered at the same size:
1077
+ engine.restoreRenderCheckpoint( checkpoint );
1078
+ engine.wake(); // or await engine.renderFrames( n, { reset: false } )
1079
+ ```
1080
+
1081
+ In deterministic mode the continued render is bit-identical to one that never stopped. A checkpoint
1082
+ is ~60 bytes a pixel (252 MB at 2048²): the accumulated colour and aux buffers as RGBA32F plus three
1083
+ per-pixel convergence buffers.
1084
+
915
1085
  ---
916
1086
 
917
1087
  ### Events
@@ -941,7 +1111,8 @@ engine.addEventListener(EngineEvents.RENDER_COMPLETE, (e) => {
941
1111
  | `TRANSFORM_MODE_CHANGED` | Gizmo mode changed |
942
1112
  | `SELECT_MODE_CHANGED` | Selection mode toggled |
943
1113
  | `SETTING_CHANGED` | A render setting is modified |
944
- | `AUTO_FOCUS_UPDATED` | Auto-focus recalculated |
1114
+ | `AUTO_FOCUS_UPDATED` | Auto-focus recalculated — `worldDistance` in scene units, `distance` divided by the model's size |
1115
+ | `ORTHO_HEIGHT_UPDATED` | An orthographic view's height changed — `height` in scene units |
945
1116
  | `AUTO_EXPOSURE_UPDATED` | Auto-exposure recalculated |
946
1117
  | `AF_POINT_PLACED` | Focus point placed on screen |
947
1118
  | `ANIMATION_STARTED` / `ANIMATION_PAUSED` / `ANIMATION_STOPPED` / `ANIMATION_FINISHED` | Animation lifecycle |
@@ -982,6 +1153,7 @@ import {
982
1153
  CAMERA_PRESETS,
983
1154
  CAMERA_RANGES,
984
1155
  SKY_PRESETS,
1156
+ DEFAULT_SUN_PATH,
985
1157
  AUTO_FOCUS_MODES,
986
1158
  AF_DEFAULTS,
987
1159
  TRIANGLE_DATA_LAYOUT,
@@ -1006,12 +1178,28 @@ import {
1006
1178
  displayCanvasFit,
1007
1179
  } from 'rayzee';
1008
1180
 
1181
+ // Where the sun stands for a solar time, date and latitude (degrees; azimuth clockwise from north)
1182
+ import { sunPosition, timeForSunElevation, dayOfYearForMonth } from 'rayzee';
1183
+
1009
1184
  // Leveled/namespaced logging, shared with the workers
1010
1185
  import { Logger, createLogger, fmt, LOG_LEVELS } from 'rayzee';
1011
1186
 
1012
1187
  // Asset URL / cache namespace overrides
1013
1188
  import { configureAssets, getAssetConfig } from 'rayzee';
1014
1189
 
1190
+ // On-disk storage (OPFS): caches, host areas, downloads, file identity
1191
+ import {
1192
+ openStorage, StorageManager, STORAGE_KIND, ENGINE_AREAS, acquireLock, heldLockNames,
1193
+ fileIdentity, identityKey, sameIdentity,
1194
+ DownloadCache, DOWNLOAD_POLICY, fetchFile, nameFromUrl, cachedObjectURL,
1195
+ } from 'rayzee';
1196
+
1197
+ // Archives read in place, one entry at a time
1198
+ import { openZip, readZipDirectory } from 'rayzee';
1199
+
1200
+ // Scene state as plain data (engine.exportSceneState), and the engine version as built
1201
+ import { SCENE_STATE_VERSION, toPortable, fromPortable, VERSION } from 'rayzee';
1202
+
1015
1203
  // Advanced: managers & pipeline
1016
1204
  import {
1017
1205
  RenderSettings,
@@ -1024,6 +1212,8 @@ import {
1024
1212
  AnimationManager,
1025
1213
  TransformManager,
1026
1214
  VideoRenderManager,
1215
+ TimelineManager,
1216
+ CameraTrack,
1027
1217
  InteractionManager,
1028
1218
  RenderPipeline,
1029
1219
  RenderStage,
@@ -1,5 +1,5 @@
1
1
  import { t as e } from "./rolldown-runtime-CAFD8bLK.js";
2
- import { S as t, t as n, y as r } from "./ToneMapGPU-BVE852iN.js";
2
+ import { S as t, t as n, y as r } from "./ToneMapGPU-DpUZDMbQ.js";
3
3
  //#region src/neural/NeuralRetouch.js
4
4
  var i = /* @__PURE__ */ e({
5
5
  NeuralRetouch: () => m,
@@ -1,5 +1,5 @@
1
1
  import { t as e } from "./rolldown-runtime-CAFD8bLK.js";
2
- import { S as t, t as n, y as r } from "./ToneMapGPU-BVE852iN.js";
2
+ import { S as t, t as n, y as r } from "./ToneMapGPU-DpUZDMbQ.js";
3
3
  //#region src/EngineEvents.js
4
4
  var i = {
5
5
  RENDER_COMPLETE: "engine:renderComplete",
@@ -21,6 +21,7 @@ var i = {
21
21
  OBJECT_TRANSFORM_END: "engine:objectTransformEnd",
22
22
  TRANSFORM_MODE_CHANGED: "engine:transformModeChanged",
23
23
  AUTO_FOCUS_UPDATED: "engine:autoFocusUpdated",
24
+ ORTHO_HEIGHT_UPDATED: "engine:orthoHeightUpdated",
24
25
  AUTO_EXPOSURE_UPDATED: "engine:autoExposureUpdated",
25
26
  AF_POINT_PLACED: "engine:afPointPlaced",
26
27
  SETTING_CHANGED: "engine:settingChanged",
@@ -28,12 +29,14 @@ var i = {
28
29
  ANIMATION_PAUSED: "engine:animationPaused",
29
30
  ANIMATION_STOPPED: "engine:animationStopped",
30
31
  ANIMATION_FINISHED: "engine:animationFinished",
32
+ TIMELINE_CHANGED: "engine:timelineChanged",
31
33
  VIDEO_RENDER_PROGRESS: "engine:videoRenderProgress",
32
34
  VIDEO_RENDER_COMPLETE: "engine:videoRenderComplete",
33
35
  DISPOSE: "engine:dispose",
34
36
  DEVICE_LOST: "engine:deviceLost",
35
37
  ISSUE: "engine:issue",
36
- SCENE_METADATA_APPLIED: "engine:sceneMetadataApplied"
38
+ SCENE_METADATA_APPLIED: "engine:sceneMetadataApplied",
39
+ STORAGE_CHANGED: "engine:storageChanged"
37
40
  }, a = /* @__PURE__ */ e({
38
41
  NeuralSuperRes: () => g,
39
42
  SR_MAX_INPUT: () => c,
@@ -110,7 +110,8 @@ var D = {
110
110
  upscalerModelBaseUrl: "https://huggingface.co/notaneimu/onnx-image-models/resolve/main/",
111
111
  neuralRuntimeUrl: "/neural/neural-runtime.js",
112
112
  neuralAssetBaseUrl: null,
113
- cacheNamespace: "rayzee"
113
+ cacheNamespace: "rayzee",
114
+ storage: "auto"
114
115
  };
115
116
  function O(e) {
116
117
  e && Object.assign(D, e);