rayzee 4.8.14 → 5.0.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.
@@ -1,30 +1,38 @@
1
- import { EventDispatcher, Vector3 } from 'three';
1
+ import { EventDispatcher, PerspectiveCamera, Vector3 } from 'three';
2
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
2
3
  import { EngineEvents, } from '../EngineEvents.js';
3
4
  import { AF_DEFAULTS } from '../EngineDefaults.js';
4
5
 
5
6
  /**
6
- * Manages camera switching, auto-focus, and AF point placement.
7
+ * Manages camera creation, switching, auto-focus, and AF point placement.
7
8
  *
8
- * Extracted from PathTracerApp to keep the facade slim.
9
+ * Owns the PerspectiveCamera and OrbitControls instances.
9
10
  * Dispatches events that PathTracerApp relays to external consumers.
10
11
  */
11
12
  export class CameraManager extends EventDispatcher {
12
13
 
13
14
  /**
14
- * @param {import('three').PerspectiveCamera} camera
15
- * @param {import('three/addons/controls/OrbitControls.js').OrbitControls} controls
16
- * @param {import('./InteractionManager.js').InteractionManager} interactionManager
15
+ * @param {HTMLCanvasElement} canvas - Canvas element for orbit controls
17
16
  */
18
- constructor( camera, controls, interactionManager ) {
17
+ constructor( canvas ) {
19
18
 
20
19
  super();
21
20
 
22
- this.camera = camera;
23
- this.controls = controls;
24
- this.interactionManager = interactionManager;
21
+ const width = canvas.clientWidth;
22
+ const height = canvas.clientHeight;
23
+
24
+ this.camera = new PerspectiveCamera( 60, width / height || 1, 0.01, 1000 );
25
+ this.camera.position.set( 0, 0, 5 );
26
+
27
+ this.controls = new OrbitControls( this.camera, canvas );
28
+ this.controls.screenSpacePanning = true;
29
+ this.controls.zoomToCursor = true;
30
+ this.controls.saveState();
31
+
32
+ this.interactionManager = null;
25
33
 
26
34
  /** @type {import('three').PerspectiveCamera[]} */
27
- this.cameras = [ camera ];
35
+ this.cameras = [ this.camera ];
28
36
  this.currentCameraIndex = 0;
29
37
 
30
38
  // Auto-focus state
@@ -38,6 +46,11 @@ export class CameraManager extends EventDispatcher {
38
46
  // Saved state for default camera when switching to model cameras
39
47
  this._defaultCameraState = null;
40
48
 
49
+ // Callbacks injected by PathTracerApp
50
+ this._onResize = null;
51
+ this._onReset = null;
52
+ this._getSettings = null;
53
+
41
54
  }
42
55
 
43
56
  /**
@@ -67,15 +80,38 @@ export class CameraManager extends EventDispatcher {
67
80
 
68
81
  }
69
82
 
83
+ /**
84
+ * Stores callbacks for camera operations (resize, reset, settings access).
85
+ * Call once after all managers are ready.
86
+ *
87
+ * @param {Object} callbacks
88
+ * @param {Function} callbacks.onResize - Trigger viewport resize
89
+ * @param {Function} callbacks.onReset - Trigger accumulation reset
90
+ * @param {Function} callbacks.getSettings - (key) => value
91
+ */
92
+ initCallbacks( { onResize, onReset, getSettings } ) {
93
+
94
+ this._onResize = onResize;
95
+ this._onReset = onReset;
96
+ this._getSettings = getSettings;
97
+
98
+ }
99
+
70
100
  /**
71
101
  * Switches the active camera by index.
102
+ * Uses stored callbacks from initCallbacks() for resize/reset.
72
103
  * @param {number} index
73
- * @param {number} focusDistance - Current focus distance (for orbit target placement)
74
- * @param {Function} onResize - Callback to trigger resize after camera change
75
- * @param {Function} onReset - Callback to trigger accumulation reset
104
+ * @param {number} [focusDistance] - Override focus distance (falls back to settings)
105
+ * @param {Function} [onResize] - Override resize callback
106
+ * @param {Function} [onReset] - Override reset callback
76
107
  */
77
108
  switchCamera( index, focusDistance, onResize, onReset ) {
78
109
 
110
+ // Use stored callbacks if not provided (backward-compatible signature)
111
+ focusDistance = focusDistance ?? this._getSettings?.( 'focusDistance' );
112
+ onResize = onResize ?? this._onResize;
113
+ onReset = onReset ?? this._onReset;
114
+
79
115
  if ( ! this.cameras || this.cameras.length === 0 ) return;
80
116
 
81
117
  if ( index < 0 || index >= this.cameras.length ) {
@@ -150,6 +186,35 @@ export class CameraManager extends EventDispatcher {
150
186
 
151
187
  }
152
188
 
189
+ /**
190
+ * Focuses the orbit camera on a world-space point.
191
+ * @param {import('three').Vector3} center
192
+ */
193
+ focusOn( center ) {
194
+
195
+ if ( ! center || ! this.controls ) return;
196
+ this.controls.target.copy( center );
197
+ this.controls.update();
198
+ this._onReset?.();
199
+
200
+ }
201
+
202
+ // ── Aliases (match Sub-API surface) ───────────────────────────
203
+
204
+ /** The active Three.js PerspectiveCamera. */
205
+ get active() {
206
+
207
+ return this.camera;
208
+
209
+ }
210
+
211
+ /** @see getCameraNames */
212
+ getNames() {
213
+
214
+ return this.getCameraNames();
215
+
216
+ }
217
+
153
218
  // ── Auto-Focus ────────────────────────────────────────────────
154
219
 
155
220
  setAutoFocusMode( mode ) {
@@ -201,7 +266,10 @@ export class CameraManager extends EventDispatcher {
201
266
  * @param {Function} params.softReset - Callback for soft accumulation reset
202
267
  * @param {Function} params.hardReset - Callback for hard accumulation reset
203
268
  */
204
- updateAutoFocus( { meshScene, assetLoader, floorPlane, currentFocusDistance, pathTracer, setFocusDistance, softReset, hardReset } ) {
269
+ updateAutoFocus( ctx ) {
270
+
271
+ const { meshScene, assetLoader, floorPlane, currentFocusDistance, pathTracer, setFocusDistance, softReset, hardReset } = ctx || this._afContext || {};
272
+ if ( ! meshScene ) return;
205
273
 
206
274
  if ( this.autoFocusMode === 'manual' ) return;
207
275
 
@@ -303,4 +371,54 @@ export class CameraManager extends EventDispatcher {
303
371
 
304
372
  }
305
373
 
374
+ /**
375
+ * Deferred dependency injection — InteractionManager needs the camera
376
+ * in its constructor, so it can't be passed during CameraManager creation.
377
+ * @param {import('./InteractionManager.js').InteractionManager} interactionManager
378
+ */
379
+ setInteractionManager( interactionManager ) {
380
+
381
+ this.interactionManager = interactionManager;
382
+
383
+ }
384
+
385
+ /**
386
+ * Initialises the stable auto-focus context. Call once after all
387
+ * managers and stages are ready. CameraManager stores the context
388
+ * and `updateAutoFocus()` reads from it each frame — no per-frame allocation.
389
+ *
390
+ * @param {Object} deps
391
+ * @param {import('three').Scene} deps.meshScene
392
+ * @param {import('../Processor/AssetLoader.js').AssetLoader} deps.assetLoader
393
+ * @param {import('three').Mesh} deps.floorPlane
394
+ * @param {import('../Stages/PathTracer.js').PathTracer} deps.pathTracer
395
+ * @param {import('../RenderSettings.js').RenderSettings} deps.settings
396
+ * @param {Function} deps.softReset
397
+ * @param {Function} deps.hardReset
398
+ */
399
+ initAutoFocus( { meshScene, assetLoader, floorPlane, pathTracer, settings, softReset, hardReset } ) {
400
+
401
+ this._afContext = {
402
+ meshScene,
403
+ assetLoader,
404
+ floorPlane,
405
+ pathTracer,
406
+ setFocusDistance: ( d ) => settings.set( 'focusDistance', d, { silent: true } ),
407
+ softReset,
408
+ hardReset,
409
+ };
410
+
411
+ // Live getter — reads current value without allocation
412
+ Object.defineProperty( this._afContext, 'currentFocusDistance', {
413
+ get: () => settings.get( 'focusDistance' ),
414
+ } );
415
+
416
+ }
417
+
418
+ dispose() {
419
+
420
+ this.controls?.dispose();
421
+
422
+ }
423
+
306
424
  }
@@ -20,7 +20,7 @@ export class DenoisingManager extends EventDispatcher {
20
20
  /**
21
21
  * @param {Object} params
22
22
  * @param {import('three/webgpu').WebGPURenderer} params.renderer
23
- * @param {HTMLCanvasElement|null} params.denoiserCanvas
23
+ * @param {HTMLCanvasElement} params.mainCanvas - The primary rendering canvas
24
24
  * @param {import('three').Scene} params.scene
25
25
  * @param {import('three').PerspectiveCamera} params.camera
26
26
  * @param {Object} params.stages - Named references to pipeline stages
@@ -29,12 +29,13 @@ export class DenoisingManager extends EventDispatcher {
29
29
  * @param {Function} params.getSaturation - () => current saturation value
30
30
  * @param {Function} params.getTransparentBg - () => boolean
31
31
  */
32
- constructor( { renderer, denoiserCanvas, scene, camera, stages, pipeline, getExposure, getSaturation, getTransparentBg } ) {
32
+ constructor( { renderer, mainCanvas, scene, camera, stages, pipeline, getExposure, getSaturation, getTransparentBg } ) {
33
33
 
34
34
  super();
35
35
 
36
36
  this.renderer = renderer;
37
- this.denoiserCanvas = denoiserCanvas;
37
+ this.mainCanvas = mainCanvas;
38
+ this.denoiserCanvas = this._createDenoiserCanvas( mainCanvas );
38
39
  this.scene = scene;
39
40
  this.camera = camera;
40
41
  this.pipeline = pipeline;
@@ -49,6 +50,74 @@ export class DenoisingManager extends EventDispatcher {
49
50
  this.denoiser = null;
50
51
  this.upscaler = null;
51
52
 
53
+ // Resolution tracking — used for canvas restoration on reset
54
+ this._lastRenderWidth = 0;
55
+ this._lastRenderHeight = 0;
56
+
57
+ }
58
+
59
+ _createDenoiserCanvas( mainCanvas ) {
60
+
61
+ const parent = mainCanvas.parentNode;
62
+ if ( ! parent ) return null;
63
+
64
+ const dc = document.createElement( 'canvas' );
65
+ dc.width = mainCanvas.width;
66
+ dc.height = mainCanvas.height;
67
+ dc.style.width = `${mainCanvas.clientWidth}px`;
68
+ dc.style.height = `${mainCanvas.clientHeight}px`;
69
+
70
+ parent.insertBefore( dc, mainCanvas );
71
+ return dc;
72
+
73
+ }
74
+
75
+ /**
76
+ * Updates the render resolution and propagates to denoiser/upscaler.
77
+ * @param {number} width
78
+ * @param {number} height
79
+ */
80
+ setRenderSize( width, height ) {
81
+
82
+ this._lastRenderWidth = width;
83
+ this._lastRenderHeight = height;
84
+ this.denoiser?.setSize( width, height );
85
+ this.upscaler?.setBaseSize( width, height );
86
+
87
+ }
88
+
89
+ /**
90
+ * Syncs the denoiser canvas CSS dimensions to match display size.
91
+ * @param {number} width
92
+ * @param {number} height
93
+ */
94
+ syncCanvasStyle( width, height ) {
95
+
96
+ if ( this.denoiserCanvas ) {
97
+
98
+ this.denoiserCanvas.style.width = `${width}px`;
99
+ this.denoiserCanvas.style.height = `${height}px`;
100
+
101
+ }
102
+
103
+ }
104
+
105
+ /**
106
+ * Restores the denoiser canvas to base render resolution after upscaling.
107
+ * @returns {boolean} true if the canvas was resized
108
+ */
109
+ restoreBaseResolution() {
110
+
111
+ if ( ! this.denoiserCanvas || ! this._lastRenderWidth || ! this._lastRenderHeight ) return false;
112
+
113
+ const wasResized = this.denoiserCanvas.width !== this._lastRenderWidth
114
+ || this.denoiserCanvas.height !== this._lastRenderHeight;
115
+
116
+ this.denoiserCanvas.width = this._lastRenderWidth;
117
+ this.denoiserCanvas.height = this._lastRenderHeight;
118
+
119
+ return wasResized;
120
+
52
121
  }
53
122
 
54
123
  /**
@@ -374,10 +443,227 @@ export class DenoisingManager extends EventDispatcher {
374
443
 
375
444
  }
376
445
 
446
+ if ( this.denoiserCanvas?.parentNode ) {
447
+
448
+ this.denoiserCanvas.parentNode.removeChild( this.denoiserCanvas );
449
+ this.denoiserCanvas = null;
450
+
451
+ }
452
+
453
+ }
454
+
455
+ // ── Injected Dependencies (set after construction) ───────────
456
+
457
+ /** @param {import('./OverlayManager.js').OverlayManager} overlayManager */
458
+ setOverlayManager( overlayManager ) {
459
+
460
+ this._overlayManager = overlayManager;
461
+
462
+ }
463
+
464
+ /** @param {Function} fn - () => void, triggers accumulation reset */
465
+ setResetCallback( fn ) {
466
+
467
+ this._onReset = fn;
468
+
469
+ }
470
+
471
+ /** @param {import('../RenderSettings.js').RenderSettings} settings */
472
+ setSettings( settings ) {
473
+
474
+ this._settings = settings;
475
+
476
+ }
477
+
478
+ // ── Stage Parameter Forwarding ───────────────────────────────
479
+ // These methods match the DenoisingAPI surface so call sites need
480
+ // zero or minimal changes after facade removal.
481
+
482
+ /** Updates ASVGF stage parameters. */
483
+ setASVGFParams( params ) {
484
+
485
+ this._stages.asvgf?.updateParameters( params );
486
+
487
+ }
488
+
489
+ /** Toggles the ASVGF heatmap debug overlay. */
490
+ toggleASVGFHeatmap( enabled ) {
491
+
492
+ this._stages.asvgf?.toggleHeatmap?.( enabled );
493
+
494
+ }
495
+
496
+ /**
497
+ * Configures ASVGF for a specific render mode (multi-stage coordination).
498
+ * @param {Object} config - { enabled, temporalAlpha, atrousIterations, ... }
499
+ */
500
+ configureASVGFForMode( config ) {
501
+
502
+ if ( ! this._stages.asvgf ) return;
503
+
504
+ this._stages.asvgf.enabled = config.enabled;
505
+ if ( this._stages.variance ) this._stages.variance.enabled = config.enabled;
506
+ if ( this._stages.bilateralFilter ) this._stages.bilateralFilter.enabled = config.enabled;
507
+
508
+ if ( config.enabled ) {
509
+
510
+ this._stages.asvgf.updateParameters( config );
511
+
512
+ }
513
+
514
+ }
515
+
516
+ /** Updates SSRC stage parameters. */
517
+ setSSRCParams( params ) {
518
+
519
+ this._stages.ssrc?.updateParameters( params );
520
+
521
+ }
522
+
523
+ /** Updates edge-aware filtering parameters. */
524
+ setEdgeAwareParams( params ) {
525
+
526
+ this._stages.edgeFilter?.updateUniforms( params );
527
+
528
+ }
529
+
530
+ /** Updates auto-exposure stage parameters. */
531
+ setAutoExposureParams( params ) {
532
+
533
+ this._stages.autoExposure?.updateParameters( params );
534
+
535
+ }
536
+
537
+ /**
538
+ * Updates adaptive sampling parameters (with settings bridge).
539
+ * @param {Object} params
540
+ */
541
+ setAdaptiveSamplingParams( params ) {
542
+
543
+ if ( params.min !== undefined ) this._stages.pathTracer?.setAdaptiveSamplingMin( params.min );
544
+ if ( params.adaptiveSamplingMax !== undefined ) this._settings?.set( 'adaptiveSamplingMax', params.adaptiveSamplingMax );
545
+ this._stages.adaptiveSampling?.setAdaptiveSamplingParameters( params );
546
+
547
+ }
548
+
549
+ /** Toggles the adaptive sampling debug helper. */
550
+ toggleAdaptiveSamplingHelper( enabled ) {
551
+
552
+ this._stages.adaptiveSampling?.toggleHelper( enabled );
553
+
554
+ }
555
+
556
+ // ── OIDN ─────────────────────────────────────────────────────
557
+
558
+ /** Enables or disables Intel OIDN denoiser. */
559
+ setOIDNEnabled( enabled ) {
560
+
561
+ if ( this.denoiser ) this.denoiser.enabled = enabled;
562
+
563
+ }
564
+
565
+ /** Sets OIDN denoiser quality. */
566
+ setOIDNQuality( quality ) {
567
+
568
+ this.denoiser?.updateQuality( quality );
569
+
570
+ }
571
+
572
+ /** Enables or disables the OIDN tile helper overlay. */
573
+ setOIDNTileHelper( enabled ) {
574
+
575
+ this._setTileHelper( enabled );
576
+
577
+ }
578
+
579
+ /** Enables or disables the tile helper overlay. */
580
+ setTileHelperEnabled( enabled ) {
581
+
582
+ this._setTileHelper( enabled );
583
+
584
+ }
585
+
586
+ /** Enables or disables tile highlight. */
587
+ setTileHighlightEnabled( enabled ) {
588
+
589
+ this._setTileHelper( enabled );
590
+
591
+ }
592
+
593
+ // ── AI Upscaler ──────────────────────────────────────────────
594
+
595
+ /** Enables or disables the AI upscaler. */
596
+ setUpscalerEnabled( enabled ) {
597
+
598
+ if ( this.upscaler ) this.upscaler.enabled = enabled;
599
+
600
+ }
601
+
602
+ /** Sets the upscaler scale factor. */
603
+ setUpscalerScaleFactor( factor ) {
604
+
605
+ this.upscaler?.setScaleFactor( factor );
606
+
607
+ }
608
+
609
+ /** Sets the upscaler quality level. */
610
+ setUpscalerQuality( quality ) {
611
+
612
+ this.upscaler?.setQuality( quality );
613
+
614
+ }
615
+
616
+ // ── Convenience (match DenoisingAPI names with reset) ────────
617
+
618
+ /**
619
+ * Enables or disables auto-exposure (convenience wrapper).
620
+ * @param {boolean} enabled
621
+ */
622
+ setAutoExposure( enabled ) {
623
+
624
+ this.setAutoExposureEnabled( enabled, this._getExposure() );
625
+ this._onReset?.();
626
+
627
+ }
628
+
629
+ /**
630
+ * Enables or disables adaptive sampling (convenience wrapper with settings bridge).
631
+ * @param {boolean} enabled
632
+ */
633
+ setAdaptiveSampling( enabled ) {
634
+
635
+ this._settings?.set( 'useAdaptiveSampling', enabled );
636
+ this.setAdaptiveSamplingEnabled( enabled );
637
+
638
+ }
639
+
640
+ /**
641
+ * Switches strategy with automatic reset (convenience wrapper).
642
+ * @param {'none'|'asvgf'|'ssrc'|'edgeaware'} strategy
643
+ * @param {string} [asvgfPreset]
644
+ */
645
+ setStrategy( strategy, asvgfPreset ) {
646
+
647
+ this.setDenoiserStrategy( strategy, asvgfPreset );
648
+ this._onReset?.();
649
+
377
650
  }
378
651
 
379
652
  // ── Private ───────────────────────────────────────────────────
380
653
 
654
+ _setTileHelper( enabled ) {
655
+
656
+ const tileHelper = this._overlayManager?.getHelper( 'tiles' );
657
+ if ( tileHelper ) {
658
+
659
+ tileHelper.enabled = enabled;
660
+ if ( ! enabled ) tileHelper.hide();
661
+
662
+ }
663
+
664
+ }
665
+
666
+
381
667
  _getEffectiveExposure() {
382
668
 
383
669
  return this._stages.autoExposure?.enabled
@@ -78,10 +78,103 @@ export class EnvironmentManager {
78
78
 
79
79
  /**
80
80
  * Optional callbacks set by the owning stage.
81
- * @type {{ onReset?: Function, getSceneTextureNodes?: Function }}
81
+ * @type {{ onReset?: Function, onAutoExposureReset?: Function, getSceneTextureNodes?: Function }}
82
82
  */
83
83
  this.callbacks = {};
84
84
 
85
+ // Mode state machine (absorbed from EnvironmentAPI)
86
+ this._previousHDRI = null;
87
+
88
+ }
89
+
90
+ // ===== MODE STATE MACHINE =====
91
+
92
+ /**
93
+ * Switches the environment mode (hdri, gradient, color, procedural).
94
+ * Preserves the HDRI texture when switching away, restores when switching back.
95
+ * @param {'hdri'|'gradient'|'color'|'procedural'} mode
96
+ */
97
+ async setMode( mode ) {
98
+
99
+ const prev = this.envParams.mode;
100
+ this.envParams.mode = mode;
101
+
102
+ // Cache HDRI texture when leaving HDRI mode
103
+ if ( mode !== 'hdri' && prev === 'hdri' ) {
104
+
105
+ this._previousHDRI = this.environmentTexture;
106
+
107
+ }
108
+
109
+ if ( mode === 'gradient' ) {
110
+
111
+ await this.generateGradientTexture();
112
+
113
+ } else if ( mode === 'color' ) {
114
+
115
+ await this.generateSolidColorTexture();
116
+
117
+ } else if ( mode === 'procedural' ) {
118
+
119
+ await this.generateProceduralSkyTexture();
120
+
121
+ } else if ( mode === 'hdri' && this._previousHDRI ) {
122
+
123
+ await this.setEnvironmentMap( this._previousHDRI );
124
+ this._previousHDRI = null;
125
+
126
+ }
127
+
128
+ this.markDirty();
129
+ this.callbacks.onAutoExposureReset?.();
130
+ this._notifyReset();
131
+
132
+ }
133
+
134
+ /**
135
+ * Marks the environment texture as needing GPU re-upload on the next frame.
136
+ */
137
+ markDirty() {
138
+
139
+ if ( this.environmentTexture ) this.environmentTexture.needsUpdate = true;
140
+
141
+ }
142
+
143
+ // ===== Aliases (match Sub-API surface for zero-churn migration) =====
144
+
145
+ /** @see envParams */
146
+ get params() {
147
+
148
+ return this.envParams;
149
+
150
+ }
151
+
152
+ /** @see environmentTexture */
153
+ get texture() {
154
+
155
+ return this.environmentTexture;
156
+
157
+ }
158
+
159
+ /** @see generateGradientTexture */
160
+ generateGradient() {
161
+
162
+ return this.generateGradientTexture();
163
+
164
+ }
165
+
166
+ /** @see generateSolidColorTexture */
167
+ generateSolid() {
168
+
169
+ return this.generateSolidColorTexture();
170
+
171
+ }
172
+
173
+ /** @see generateProceduralSkyTexture */
174
+ generateProcedural() {
175
+
176
+ return this.generateProceduralSkyTexture();
177
+
85
178
  }
86
179
 
87
180
  // ===== CDF STORAGE BUFFERS =====