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
@@ -1,14 +1,21 @@
1
- import { EventDispatcher, PerspectiveCamera, Vector3 } from 'three';
1
+ import { EventDispatcher, MathUtils, OrthographicCamera, PerspectiveCamera, Quaternion, Vector3 } from 'three';
2
2
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
3
3
  import { EngineEvents, } from '../EngineEvents.js';
4
- import { AF_DEFAULTS } from '../EngineDefaults.js';
4
+ import { AF_DEFAULTS, CAMERA_PROJECTION_IDS } from '../EngineDefaults.js';
5
+ import { viewDepth } from './InteractionManager.js';
6
+ import { ViewCamera } from './ViewCamera.js';
7
+ import { WalkControls } from './WalkControls.js';
8
+ import { toPortable, fromPortable } from '../SceneState/portable.js';
5
9
 
6
10
  const DEFAULT_CAMERA_SCALE = Object.freeze( new Vector3( 1, 1, 1 ) );
7
11
 
12
+ /** The height of an orthographic camera's view in world units, or null for any other camera. */
13
+ const orthoHeightOf = camera => camera?.isOrthographicCamera ? ( camera.top - camera.bottom ) / camera.zoom : null;
14
+
8
15
  /**
9
16
  * Manages camera creation, switching, auto-focus, and AF point placement.
10
17
  *
11
- * Owns the PerspectiveCamera and OrbitControls instances.
18
+ * Owns the {@link ViewCamera} and OrbitControls instances.
12
19
  * Dispatches events that PathTracerApp relays to external consumers.
13
20
  */
14
21
  export class CameraManager extends EventDispatcher {
@@ -23,7 +30,7 @@ export class CameraManager extends EventDispatcher {
23
30
  const width = canvas.clientWidth;
24
31
  const height = canvas.clientHeight;
25
32
 
26
- this.camera = new PerspectiveCamera( 60, width / height || 1, 0.01, 1000 );
33
+ this.camera = new ViewCamera( 60, width / height || 1, 0.01, 1000 );
27
34
  this.camera.position.set( 0, 0, 5 );
28
35
 
29
36
  this.controls = new OrbitControls( this.camera, canvas );
@@ -31,9 +38,16 @@ export class CameraManager extends EventDispatcher {
31
38
  this.controls.zoomToCursor = true;
32
39
  this.controls.saveState();
33
40
 
41
+ // The wheel zooms an orthographic view: report its height.
42
+ this._reportedOrthoHeight = null;
43
+ this._reportOrthoHeight = this._reportOrthoHeight.bind( this );
44
+ this.controls.addEventListener( 'change', this._reportOrthoHeight );
45
+
46
+ this.walkControls = new WalkControls( this.camera, canvas, this.controls );
47
+
34
48
  this.interactionManager = null;
35
49
 
36
- /** @type {import('three').PerspectiveCamera[]} */
50
+ /** @type {import('three').Camera[]} */
37
51
  this.cameras = [ this.camera ];
38
52
  this.currentCameraIndex = 0;
39
53
 
@@ -62,19 +76,20 @@ export class CameraManager extends EventDispatcher {
62
76
 
63
77
  /**
64
78
  * Sets the list of available cameras (default + extracted from model).
65
- * @param {import('three').PerspectiveCamera[]} cameras
79
+ * @param {import('three').Camera[]} cameras - perspective or orthographic
66
80
  */
67
81
  setCameras( cameras ) {
68
82
 
69
83
  this.cameras = cameras;
70
84
  this._userCameraCounter = 0;
85
+ this.resetAutoFocus();
71
86
 
72
87
  }
73
88
 
74
89
  /**
75
90
  * Adds a new user camera that snapshots the current render camera's pose,
76
- * FOV, clip planes, and orbit target. The snapshot is a standalone template
77
- * appended to the list; switching to it restores this exact framing.
91
+ * FOV or orthographic size, clip planes, and orbit target. The snapshot is a standalone
92
+ * template appended to the list; switching to it restores this exact framing.
78
93
  *
79
94
  * @param {string} [name] - Optional display name (auto-generated otherwise).
80
95
  * @returns {number} The index of the newly added camera.
@@ -82,7 +97,10 @@ export class CameraManager extends EventDispatcher {
82
97
  addCameraFromView( name ) {
83
98
 
84
99
  const src = this.camera;
85
- const cam = new PerspectiveCamera( src.fov, src.aspect, src.near, src.far );
100
+ const cam = src.orthographic
101
+ ? new OrthographicCamera( src.left, src.right, src.top, src.bottom, src.near, src.far )
102
+ : new PerspectiveCamera( src.fov, src.aspect, src.near, src.far );
103
+ cam.zoom = src.zoom;
86
104
  cam.position.copy( src.position );
87
105
  cam.quaternion.copy( src.quaternion );
88
106
  cam.scale.copy( src.scale );
@@ -193,8 +211,10 @@ export class CameraManager extends EventDispatcher {
193
211
  focalLength: get( 'focalLength' ),
194
212
  apertureScale: get( 'apertureScale' ),
195
213
  anamorphicRatio: get( 'anamorphicRatio' ),
214
+ dofBlur: get( 'dofBlur' ),
196
215
  autoFocusMode: this.autoFocusMode,
197
216
  afScreenPoint: { ...this.afScreenPoint },
217
+ orthoHeight: this.orthoHeight,
198
218
  };
199
219
 
200
220
  }
@@ -214,6 +234,7 @@ export class CameraManager extends EventDispatcher {
214
234
  focalLength: eff.focalLength,
215
235
  apertureScale: eff.apertureScale,
216
236
  anamorphicRatio: eff.anamorphicRatio,
237
+ dofBlur: eff.dofBlur,
217
238
  } );
218
239
 
219
240
  if ( eff.autoFocusMode !== undefined ) this.setAutoFocusMode( eff.autoFocusMode );
@@ -221,13 +242,6 @@ export class CameraManager extends EventDispatcher {
221
242
 
222
243
  }
223
244
 
224
- /** Scene scale used to convert scaled focusDistance to UI-facing units. */
225
- _getSceneScale() {
226
-
227
- return this._afContext?.assetLoader?.getSceneScale?.() || 1;
228
-
229
- }
230
-
231
245
  /**
232
246
  * Switches the active camera by index.
233
247
  * Uses stored callbacks from initCallbacks() for resize/reset.
@@ -305,7 +319,7 @@ export class CameraManager extends EventDispatcher {
305
319
  // quaternion can express — a pbrt scene's `Scale -1 1 1`, for one. Copying
306
320
  // pose alone would silently un-mirror the view.
307
321
  this.camera.scale.copy( sourceCamera.scale );
308
- this.camera.fov = sourceCamera.fov;
322
+ if ( sourceCamera.isPerspectiveCamera ) this.camera.fov = sourceCamera.fov;
309
323
  this.camera.near = sourceCamera.near;
310
324
  this.camera.far = sourceCamera.far;
311
325
  this.camera.updateProjectionMatrix();
@@ -334,18 +348,121 @@ export class CameraManager extends EventDispatcher {
334
348
 
335
349
  }
336
350
 
351
+ this.resetAutoFocus();
352
+
353
+ // A camera left orthographic comes back orthographic; one never visited is what it was made as.
354
+ const incoming = this.cameras[ index ];
355
+ const saved = incoming?.userData?.__rayzeeEffects;
356
+ this._showOrthographic( saved ? saved.orthoHeight : orthoHeightOf( incoming ) );
357
+
337
358
  // Restore the incoming camera's own DOF/focus effects (if it has any saved;
338
359
  // otherwise it inherits the current global config).
339
- this._applyEffects( this.cameras[ index ]?.userData?.__rayzeeEffects );
360
+ this._applyEffects( saved );
340
361
 
341
362
  onResize?.();
342
363
  onReset?.();
343
364
 
344
- // Emit UI-facing effects: focusDistance unscaled (matching AUTO_FOCUS_UPDATED),
345
- // so the adapter is a plain passthrough with no scene-scale awareness.
346
- const effects = this._captureEffects();
347
- if ( effects ) effects.focusDistance /= this._getSceneScale();
348
- this.dispatchEvent( { type: 'CameraSwitched', cameraIndex: index, effects, fov: this.camera.fov } );
365
+ this.dispatchEvent( {
366
+ type: 'CameraSwitched', cameraIndex: index, effects: this._captureEffects(), fov: this.camera.fov,
367
+ cameraProjection: this._getSettings?.( 'cameraProjection' ),
368
+ } );
369
+
370
+ }
371
+
372
+ /**
373
+ * Makes the camera orthographic at `height`, or perspective for null, then hands the `cameraProjection`
374
+ * setting the outcome — after, so its handler finds the camera already there. A 360° panorama stays on
375
+ * across perspective cameras.
376
+ * @param {?number} height - in world units
377
+ */
378
+ _showOrthographic( height ) {
379
+
380
+ if ( height > 0 ) this._setView( true, height / 2 );
381
+ else this._setView( false );
382
+
383
+ const current = this._getSettings?.( 'cameraProjection' );
384
+ const next = height > 0 ? 'orthographic' : current === 'orthographic' ? 'perspective' : current;
385
+ if ( next !== current ) this._applySettings?.( { cameraProjection: next } );
386
+
387
+ }
388
+
389
+ /**
390
+ * The camera's side of the `cameraProjection` setting: set that, not this. Turning orthographic
391
+ * keeps what the view shows at the orbit target, and turning back moves the camera to keep it.
392
+ * @param {'perspective' | 'orthographic' | 'equirectangular'} projection
393
+ */
394
+ applyProjection( projection ) {
395
+
396
+ const camera = this.camera;
397
+ const orthographic = projection === 'orthographic';
398
+ if ( camera.orthographic === orthographic ) return;
399
+
400
+ if ( orthographic ) {
401
+
402
+ this._setView( true, this._fittedHalfHeight() );
403
+
404
+ } else {
405
+
406
+ const target = this.controls.target;
407
+ const distance = this.orthoHeight / 2 / Math.tan( MathUtils.degToRad( camera.fov ) / 2 );
408
+ camera.position.sub( target ).setLength( distance ).add( target );
409
+ this._setView( false );
410
+
411
+ }
412
+
413
+ this.controls.update();
414
+
415
+ }
416
+
417
+ /** The height of the view in world units while orthographic, the wheel's zoom included; null otherwise. */
418
+ get orthoHeight() {
419
+
420
+ return orthoHeightOf( this.camera );
421
+
422
+ }
423
+
424
+ /** @param {number} height - the orthographic view's height, in world units */
425
+ setOrthoHeight( height ) {
426
+
427
+ if ( ! ( height > 0 ) ) return;
428
+ this._setView( this.camera.orthographic, height / 2 );
429
+ this._onReset?.();
430
+
431
+ }
432
+
433
+ /** Sizes an orthographic view to show what a perspective one would at the orbit target. */
434
+ fitOrthographic() {
435
+
436
+ if ( this.camera.orthographic ) this.setOrthoHeight( 2 * this._fittedHalfHeight() );
437
+
438
+ }
439
+
440
+ // The one writer of the camera's projection. The wheel's zoom starts over.
441
+ _setView( orthographic, halfHeight = this.camera.orthoHalfHeight ) {
442
+
443
+ const camera = this.camera;
444
+ camera.orthographic = orthographic;
445
+ camera.orthoHalfHeight = halfHeight;
446
+ camera.zoom = 1;
447
+ camera.updateProjectionMatrix();
448
+ this._reportOrthoHeight();
449
+
450
+ }
451
+
452
+ // Half of what a perspective view shows at the orbit target.
453
+ _fittedHalfHeight() {
454
+
455
+ return this.camera.position.distanceTo( this.controls.target ) * Math.tan( MathUtils.degToRad( this.camera.fov ) / 2 );
456
+
457
+ }
458
+
459
+ _reportOrthoHeight() {
460
+
461
+ if ( ! this.camera.orthographic ) return;
462
+ const height = this.orthoHeight;
463
+ if ( height === this._reportedOrthoHeight ) return;
464
+ this._reportedOrthoHeight = height;
465
+ this.dispatchEvent( { type: EngineEvents.ORTHO_HEIGHT_UPDATED, height } );
349
466
 
350
467
  }
351
468
 
@@ -362,9 +479,255 @@ export class CameraManager extends EventDispatcher {
362
479
 
363
480
  }
364
481
 
482
+ /**
483
+ * Orbit around a target, or walk through the scene first-person (see {@link WalkControls}).
484
+ * Leaving walk mode orbits around the surface at the centre of the view.
485
+ * @param {'orbit' | 'walk'} mode
486
+ */
487
+ setNavigationMode( mode ) {
488
+
489
+ const walk = mode === 'walk';
490
+ if ( this.walkControls.enabled === walk ) return;
491
+
492
+ const controls = this.controls;
493
+ controls.enableRotate = controls.enablePan = controls.enableZoom = ! walk;
494
+ this.walkControls.enabled = walk;
495
+ if ( walk ) return;
496
+
497
+ this.walkControls.release();
498
+ const hit = this.interactionManager?.pickSurface( 0, 0 );
499
+ if ( ! hit ) return;
500
+
501
+ const distance = MathUtils.clamp( hit.distance, controls.minDistance, controls.maxDistance );
502
+ controls.target.copy( this.camera.position ).addScaledVector( this.camera.getWorldDirection( new Vector3() ), distance );
503
+ controls.update();
504
+
505
+ }
506
+
507
+ /** @returns {'orbit' | 'walk'} */
508
+ get navigationMode() {
509
+
510
+ return this.walkControls.enabled ? 'walk' : 'orbit';
511
+
512
+ }
513
+
514
+ /** Per frame, before rendering. */
515
+ updateControls() {
516
+
517
+ this.walkControls.update();
518
+ this.controls.update();
519
+
520
+ }
521
+
522
+ /**
523
+ * The current view as a pose: where the camera is, what it looks at, and how much it shows — the
524
+ * height an orthographic view covers, or what a perspective one covers at its orbit target.
525
+ * @returns {import('./timeline/CameraTrack.js').CameraPose}
526
+ */
527
+ captureView() {
528
+
529
+ const camera = this.camera;
530
+ const orthoHeight = this.orthoHeight ?? 2 * this._fittedHalfHeight();
531
+ return { position: camera.position.clone(), target: this.controls.target.clone(), fov: camera.fov, orthoHeight };
532
+
533
+ }
534
+
535
+ /**
536
+ * Puts the camera at a pose, looking at its target, sized for the projection in use.
537
+ * @param {import('./timeline/CameraTrack.js').CameraPose} pose
538
+ */
539
+ applyPose( pose ) {
540
+
541
+ const camera = this.camera;
542
+ camera.position.copy( pose.position );
543
+ camera.lookAt( pose.target );
544
+ this.controls.target.copy( pose.target );
545
+
546
+ if ( camera.orthographic ) {
547
+
548
+ this._setView( true, pose.orthoHeight / 2 );
549
+
550
+ } else {
551
+
552
+ camera.fov = pose.fov;
553
+ camera.updateProjectionMatrix();
554
+
555
+ }
556
+
557
+ camera.updateMatrixWorld();
558
+
559
+ }
560
+
561
+ // ── Saved sessions ────────────────────────────────────────────
562
+
563
+ /**
564
+ * The live view, every camera's own effects, the cameras the user added and which one is
565
+ * active, as plain data. The active camera's effects are the render settings themselves.
566
+ * @returns {Object}
567
+ */
568
+ serialize() {
569
+
570
+ const camera = this.camera;
571
+ const saved = this._defaultCameraState;
572
+
573
+ return {
574
+ current: this.currentCameraIndex,
575
+ view: {
576
+ position: camera.position.toArray(),
577
+ quaternion: camera.quaternion.toArray(),
578
+ scale: camera.scale.toArray(),
579
+ fov: camera.fov,
580
+ near: camera.near,
581
+ far: camera.far,
582
+ zoom: camera.zoom,
583
+ orthographic: !! camera.orthographic,
584
+ orthoHalfHeight: camera.orthoHalfHeight,
585
+ target: this.controls.target.toArray(),
586
+ },
587
+ defaultView: saved ? {
588
+ position: saved.position.toArray(),
589
+ quaternion: saved.quaternion.toArray(),
590
+ scale: saved.scale.toArray(),
591
+ fov: saved.fov,
592
+ near: saved.near,
593
+ far: saved.far,
594
+ target: saved.target?.toArray() ?? null,
595
+ } : null,
596
+ navigationMode: this.navigationMode,
597
+ autoFocus: { mode: this.autoFocusMode, point: { ...this.afScreenPoint } },
598
+ cameras: this.cameras.map( ( cam, index ) => {
599
+
600
+ const entry = { index, name: cam.name ?? '', effects: toPortable( cam.userData?.__rayzeeEffects ) ?? null };
601
+ if ( index === 0 || ! cam.userData?.__rayzeeUserCamera ) return entry;
602
+
603
+ return {
604
+ ...entry,
605
+ user: true,
606
+ orthographic: !! cam.isOrthographicCamera,
607
+ frustum: cam.isOrthographicCamera ? [ cam.left, cam.right, cam.top, cam.bottom ] : null,
608
+ fov: cam.fov ?? null,
609
+ aspect: cam.aspect ?? null,
610
+ near: cam.near,
611
+ far: cam.far,
612
+ zoom: cam.zoom,
613
+ position: cam.position.toArray(),
614
+ quaternion: cam.quaternion.toArray(),
615
+ scale: cam.scale.toArray(),
616
+ orbitTarget: cam.userData.__rayzeeOrbitTarget?.toArray() ?? null,
617
+ };
618
+
619
+ } ),
620
+ };
621
+
622
+ }
623
+
624
+ /**
625
+ * Puts back what {@link serialize} recorded, against the cameras this load produced. A model
626
+ * camera is matched by index and name; one that no longer matches keeps its own effects.
627
+ * Restore render settings first: this sets the exact view after any projection change they made.
628
+ * @param {Object} state
629
+ * @returns {{mismatched: string[]}} model cameras whose saved effects were not applied
630
+ */
631
+ restore( state ) {
632
+
633
+ const mismatched = [];
634
+ if ( ! state ) return { mismatched };
635
+
636
+ this.cameras = this.cameras.filter( cam => ! cam.userData?.__rayzeeUserCamera );
637
+ const indexMap = new Map();
638
+
639
+ for ( const entry of state.cameras ?? [] ) {
640
+
641
+ if ( entry.user ) {
642
+
643
+ const cam = entry.orthographic
644
+ ? new OrthographicCamera( ...entry.frustum, entry.near, entry.far )
645
+ : new PerspectiveCamera( entry.fov, entry.aspect, entry.near, entry.far );
646
+ cam.name = entry.name;
647
+ cam.zoom = entry.zoom ?? 1;
648
+ cam.position.fromArray( entry.position );
649
+ cam.quaternion.fromArray( entry.quaternion );
650
+ cam.scale.fromArray( entry.scale );
651
+ cam.updateProjectionMatrix();
652
+ cam.updateMatrixWorld( true );
653
+ cam.userData.__rayzeeUserCamera = true;
654
+ cam.userData.__rayzeeEffects = fromPortable( entry.effects );
655
+ if ( entry.orbitTarget ) cam.userData.__rayzeeOrbitTarget = new Vector3().fromArray( entry.orbitTarget );
656
+ indexMap.set( entry.index, this.cameras.length );
657
+ this.cameras.push( cam );
658
+ continue;
659
+
660
+ }
661
+
662
+ const cam = this.cameras[ entry.index ];
663
+ if ( ! cam || ( entry.index > 0 && ( cam.name ?? '' ) !== entry.name ) ) {
664
+
665
+ mismatched.push( entry.name );
666
+ continue;
667
+
668
+ }
669
+
670
+ indexMap.set( entry.index, entry.index );
671
+ if ( entry.effects ) cam.userData.__rayzeeEffects = fromPortable( entry.effects );
672
+
673
+ }
674
+
675
+ this._userCameraCounter = this.cameras.filter( cam => cam.userData?.__rayzeeUserCamera ).length;
676
+ this.currentCameraIndex = indexMap.get( state.current ) ?? 0;
677
+
678
+ const d = state.defaultView;
679
+ this._defaultCameraState = d ? {
680
+ position: new Vector3().fromArray( d.position ),
681
+ quaternion: new Quaternion().fromArray( d.quaternion ),
682
+ scale: new Vector3().fromArray( d.scale ),
683
+ fov: d.fov,
684
+ near: d.near,
685
+ far: d.far,
686
+ target: d.target ? new Vector3().fromArray( d.target ) : null,
687
+ } : null;
688
+
689
+ if ( state.navigationMode ) this.setNavigationMode( state.navigationMode );
690
+
691
+ const v = state.view;
692
+ if ( v ) {
693
+
694
+ const camera = this.camera;
695
+ camera.position.fromArray( v.position );
696
+ camera.quaternion.fromArray( v.quaternion );
697
+ camera.scale.fromArray( v.scale );
698
+ camera.fov = v.fov;
699
+ camera.near = v.near;
700
+ camera.far = v.far;
701
+ this._setView( v.orthographic, v.orthoHalfHeight );
702
+ camera.zoom = v.zoom ?? 1;
703
+ camera.updateProjectionMatrix();
704
+ camera.updateMatrixWorld( true );
705
+ this.controls.target.fromArray( v.target );
706
+ this.controls.update();
707
+
708
+ }
709
+
710
+ if ( state.autoFocus ) {
711
+
712
+ this.setAutoFocusMode( state.autoFocus.mode );
713
+ this.setAFScreenPoint( state.autoFocus.point.x, state.autoFocus.point.y );
714
+
715
+ }
716
+
717
+ this.resetAutoFocus();
718
+ this._reportOrthoHeight();
719
+ this.dispatchEvent( {
720
+ type: 'CameraSwitched', cameraIndex: this.currentCameraIndex, effects: this._captureEffects(), fov: this.camera.fov,
721
+ cameraProjection: this._getSettings?.( 'cameraProjection' ),
722
+ } );
723
+
724
+ return { mismatched };
725
+
726
+ }
727
+
365
728
  // ── Aliases (match Sub-API surface) ───────────────────────────
366
729
 
367
- /** The active Three.js PerspectiveCamera. */
730
+ /** The active camera, a {@link ViewCamera}. */
368
731
  get active() {
369
732
 
370
733
  return this.camera;
@@ -393,6 +756,15 @@ export class CameraManager extends EventDispatcher {
393
756
 
394
757
  }
395
758
 
759
+ /** Forget what auto-focus last measured: it belonged to another model or viewpoint. */
760
+ resetAutoFocus() {
761
+
762
+ this._lastValidFocusDistance = null;
763
+ this._smoothedFocusDistance = null;
764
+ this._afPointDirty = true;
765
+
766
+ }
767
+
396
768
  setAFScreenPoint( x, y ) {
397
769
 
398
770
  this.afScreenPoint = { x, y };
@@ -420,9 +792,7 @@ export class CameraManager extends EventDispatcher {
420
792
  * Per-frame auto-focus update. Called in animate() before pipeline.render().
421
793
  *
422
794
  * @param {Object} params
423
- * @param {import('three').Scene} params.meshScene
424
795
  * @param {Object} params.assetLoader
425
- * @param {import('three').Mesh} params.floorPlane
426
796
  * @param {number} params.currentFocusDistance
427
797
  * @param {import('../Stages/PathTracer.js').PathTracer} params.pathTracer
428
798
  * @param {Function} params.setFocusDistance - Callback to update uniform + settings
@@ -431,16 +801,18 @@ export class CameraManager extends EventDispatcher {
431
801
  */
432
802
  updateAutoFocus( ctx ) {
433
803
 
434
- const { meshScene, assetLoader, floorPlane, currentFocusDistance, pathTracer, setFocusDistance, softReset, hardReset } = ctx || this._afContext || {};
435
- if ( ! meshScene ) return;
804
+ const context = ctx || this._afContext;
805
+ if ( ! context || ! this.interactionManager ) return;
806
+ const { assetLoader, currentFocusDistance, pathTracer, setFocusDistance, softReset, hardReset } = context;
436
807
 
437
808
  if ( this.autoFocusMode === 'manual' ) return;
438
809
 
439
810
  // Depth-of-field is the only consumer of the auto-focus distance. With DOF
440
811
  // off (the default) the per-frame scene raycast is pure waste, so skip it.
441
- // Re-snap on the frame DOF turns back on so focus is correct immediately
812
+ // A panorama pauses it too: Raycaster.setFromCamera only knows a frustum.
813
+ // Re-snap on the frame it resumes so focus is correct immediately
442
814
  // rather than racking from a stale smoothed value.
443
- if ( ! pathTracer?.enableDOF?.value ) {
815
+ if ( ! pathTracer?.enableDOF?.value || pathTracer.cameraProjection?.value === CAMERA_PROJECTION_IDS.equirectangular ) {
444
816
 
445
817
  this._afSuspended = true;
446
818
  return;
@@ -461,27 +833,13 @@ export class CameraManager extends EventDispatcher {
461
833
  && stage.frameCount > 0
462
834
  && ! stage.isComplete ) return;
463
835
 
464
- // Convert AF screen point (normalized 0-1) to NDC (-1 to 1)
465
- const ndcX = this.afScreenPoint.x * 2 - 1;
466
- const ndcY = - ( this.afScreenPoint.y * 2 - 1 );
467
-
468
- const raycaster = this.interactionManager?.raycaster;
469
- if ( ! raycaster ) return;
470
-
471
- raycaster.setFromCamera( { x: ndcX, y: ndcY }, this.camera );
472
- const intersects = raycaster.intersectObjects( meshScene.children, true );
473
-
474
- const validHit = intersects.find( hit =>
475
- hit.object !== this.interactionManager?.focusPointIndicator &&
476
- hit.object !== floorPlane &&
477
- ! hit.object.name.includes( 'Helper' ) &&
478
- hit.object.type === 'Mesh'
479
- );
836
+ // AF screen point (0 to 1, y down) to NDC.
837
+ const validHit = this.interactionManager.pickSurface( this.afScreenPoint.x * 2 - 1, 1 - this.afScreenPoint.y * 2 );
480
838
 
481
839
  let rawDistance;
482
840
  if ( validHit ) {
483
841
 
484
- rawDistance = validHit.distance;
842
+ rawDistance = viewDepth( validHit.point, this.camera );
485
843
  this._lastValidFocusDistance = rawDistance;
486
844
 
487
845
  } else {
@@ -492,8 +850,9 @@ export class CameraManager extends EventDispatcher {
492
850
 
493
851
  } else {
494
852
 
495
- const scale = assetLoader?.getSceneScale() || 1.0;
496
- rawDistance = AF_DEFAULTS.FALLBACK_DISTANCE * scale;
853
+ // Nothing under the AF point on a fresh view: focus where the camera orbits.
854
+ const depth = this.controls ? viewDepth( this.controls.target, this.camera ) : 0;
855
+ rawDistance = depth > 0 ? depth : AF_DEFAULTS.FALLBACK_DISTANCE * ( assetLoader?.getSceneScale() || 1.0 );
497
856
  this._lastValidFocusDistance = rawDistance;
498
857
 
499
858
  }
@@ -533,9 +892,8 @@ export class CameraManager extends EventDispatcher {
533
892
 
534
893
  setFocusDistance( newFocus );
535
894
 
536
- // Update store for UI display (unscaled value)
537
895
  const scale = assetLoader?.getSceneScale() || 1.0;
538
- this.dispatchEvent( { type: EngineEvents.AUTO_FOCUS_UPDATED, distance: newFocus / scale } );
896
+ this.dispatchEvent( { type: EngineEvents.AUTO_FOCUS_UPDATED, distance: newFocus / scale, worldDistance: newFocus } );
539
897
 
540
898
  const changeRatio = Math.abs( newFocus - prevFocus ) / Math.max( prevFocus, 0.001 );
541
899
  if ( forceReset ) {
@@ -569,20 +927,16 @@ export class CameraManager extends EventDispatcher {
569
927
  * and `updateAutoFocus()` reads from it each frame — no per-frame allocation.
570
928
  *
571
929
  * @param {Object} deps
572
- * @param {import('three').Scene} deps.meshScene
573
930
  * @param {import('../Processor/AssetLoader.js').AssetLoader} deps.assetLoader
574
- * @param {import('three').Mesh} deps.floorPlane
575
931
  * @param {import('../Stages/PathTracer.js').PathTracer} deps.pathTracer
576
932
  * @param {import('../RenderSettings.js').RenderSettings} deps.settings
577
933
  * @param {Function} deps.softReset
578
934
  * @param {Function} deps.hardReset
579
935
  */
580
- initAutoFocus( { meshScene, assetLoader, floorPlane, pathTracer, settings, softReset, hardReset } ) {
936
+ initAutoFocus( { assetLoader, pathTracer, settings, softReset, hardReset } ) {
581
937
 
582
938
  this._afContext = {
583
- meshScene,
584
939
  assetLoader,
585
- floorPlane,
586
940
  pathTracer,
587
941
  setFocusDistance: ( d ) => settings.set( 'focusDistance', d, { silent: true } ),
588
942
  softReset,
@@ -598,6 +952,8 @@ export class CameraManager extends EventDispatcher {
598
952
 
599
953
  dispose() {
600
954
 
955
+ this.walkControls?.dispose();
956
+ this.controls?.removeEventListener( 'change', this._reportOrthoHeight );
601
957
  this.controls?.dispose();
602
958
 
603
959
  }
@@ -8,7 +8,7 @@ import { createLogger } from '../utils/Logger.js';
8
8
  // The neural passes live in `../neural/` but report through the manager that drives them, so they
9
9
  // share one namespace: `rayzee.log.only( 'neural' )` shows the whole chain.
10
10
  const neuralLog = createLogger( 'neural' );
11
- import { ENGINE_DEFAULTS as DEFAULT_STATE, ASVGF_QUALITY_PRESETS, NRD_DEFAULTS, NRD_QUALITY_PRESETS, NRD_PRESET_KEYS } from '../EngineDefaults.js';
11
+ import { ENGINE_DEFAULTS as DEFAULT_STATE, ASVGF_QUALITY_PRESETS, NRD_DEFAULTS, NRD_QUALITY_PRESETS, NRD_PRESET_KEYS, CAMERA_PROJECTION_IDS } from '../EngineDefaults.js';
12
12
 
13
13
  // A refresh slower than this is a slideshow, not a live view, so the cadence swaps to a cheaper
14
14
  // model and puts the chosen one back for the finished image. Resolution-aware by construction:
@@ -164,7 +164,7 @@ export class DenoisingManager extends EventDispatcher {
164
164
  this._knownResetCount = 0;
165
165
  this._seenTracedFrames = 0;
166
166
  this._viewProj = new Matrix4();
167
- this._historyCamera = { world: null, projInv: null, viewProj: null };
167
+ this._historyCamera = { world: null, projInv: null, viewProj: null, orthographic: false };
168
168
  // TLAS leaf → { world, offset, prev }: placements moved since the last traced frame.
169
169
  this._movedPlacements = new Map();
170
170
  this._movedUpload = { count: 0, leaves: new Uint32Array( 0 ), toPrev: new Float32Array( 0 ) };
@@ -870,7 +870,7 @@ export class DenoisingManager extends EventDispatcher {
870
870
 
871
871
  const pt = this._stages.pathTracer;
872
872
  return this.historyWanted && ! this._cadenceSuspended && !! this._stages.normalDepth?.enabled
873
- && ( pt?.uniforms?.get( 'cameraProjection' )?.value ?? 0 ) === 0
873
+ && pt?.uniforms?.get( 'cameraProjection' )?.value !== CAMERA_PROJECTION_IDS.equirectangular
874
874
  && ! ( pt?.visMode?.value > 0 );
875
875
 
876
876
  }
@@ -1043,6 +1043,7 @@ export class DenoisingManager extends EventDispatcher {
1043
1043
  camera.world = pt.cameraWorldMatrix.value.elements;
1044
1044
  camera.projInv = pt.cameraProjectionMatrixInverse.value.elements;
1045
1045
  camera.viewProj = this._viewProj.elements;
1046
+ camera.orthographic = pt.uniforms?.get( 'cameraProjection' )?.value === CAMERA_PROJECTION_IDS.orthographic;
1046
1047
  return camera;
1047
1048
 
1048
1049
  }