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,12 +1,10 @@
1
1
  import { WebGPURenderer, RectAreaLightNode } from 'three/webgpu';
2
2
  import {
3
- ACESFilmicToneMapping, PerspectiveCamera, Scene, EventDispatcher,
4
- Mesh, CircleGeometry, MeshPhysicalMaterial, TimestampQuery
3
+ ACESFilmicToneMapping, Scene, EventDispatcher, TimestampQuery
5
4
  } from 'three';
6
5
  import { RectAreaLightTexturesLib } from 'three/addons/lights/RectAreaLightTexturesLib.js';
7
- import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
8
6
  import { SceneHelpers } from './SceneHelpers.js';
9
- import Stats from 'stats-gl';
7
+ import { createStats } from './managers/helpers/StatsHelper.js';
10
8
  import { PathTracer } from './Stages/PathTracer.js';
11
9
  import { NormalDepth } from './Stages/NormalDepth.js';
12
10
  import { MotionVector } from './Stages/MotionVector.js';
@@ -19,6 +17,7 @@ import { AutoExposure } from './Stages/AutoExposure.js';
19
17
  import { SSRC } from './Stages/SSRC.js';
20
18
  import { Display } from './Stages/Display.js';
21
19
  import { RenderPipeline } from './Pipeline/RenderPipeline.js';
20
+ import { CompletionTracker } from './Pipeline/CompletionTracker.js';
22
21
  import { ENGINE_DEFAULTS as DEFAULT_STATE, FINAL_RENDER_CONFIG, PREVIEW_RENDER_CONFIG } from './EngineDefaults.js';
23
22
  import { updateStats, updateLoading, resetLoading, setStatusCallback, getDisplaySamples } from './Processor/utils.js';
24
23
  import { BuildTimer } from './Processor/BuildTimer.js';
@@ -27,17 +26,6 @@ import { EngineEvents } from './EngineEvents.js';
27
26
  import { AssetLoader } from './Processor/AssetLoader.js';
28
27
  import { SceneProcessor } from './Processor/SceneProcessor.js';
29
28
 
30
- // Sub-API facades
31
- import { OutputAPI } from './api/OutputAPI.js';
32
- import { LightsAPI } from './api/LightsAPI.js';
33
- import { AnimationAPI } from './api/AnimationAPI.js';
34
- import { SelectionAPI } from './api/SelectionAPI.js';
35
- import { TransformAPI } from './api/TransformAPI.js';
36
- import { CameraAPI } from './api/CameraAPI.js';
37
- import { EnvironmentAPI } from './api/EnvironmentAPI.js';
38
- import { MaterialsAPI } from './api/MaterialsAPI.js';
39
- import { DenoisingAPI } from './api/DenoisingAPI.js';
40
-
41
29
  // Managers
42
30
  import { RenderSettings } from './RenderSettings.js';
43
31
  import { CameraManager } from './managers/CameraManager.js';
@@ -46,18 +34,22 @@ import { DenoisingManager } from './managers/DenoisingManager.js';
46
34
  import { OverlayManager } from './managers/OverlayManager.js';
47
35
  import { AnimationManager } from './managers/AnimationManager.js';
48
36
  import { TransformManager } from './managers/TransformManager.js';
49
- import { TileHelper } from './managers/helpers/TileHelper.js';
50
- import { OutlineHelper } from './managers/helpers/OutlineHelper.js';
51
37
 
52
38
 
53
39
  /**
54
40
  * WebGPU Path Tracer Application.
55
41
  *
56
- * Thin facade that delegates to focused managers:
57
- * - {@link RenderSettings} single source of truth for all render parameters
58
- * - {@link CameraManager} — camera switching, auto-focus, DOF
59
- * - {@link LightManager} — light CRUD, helpers, GPU transfer
60
- * - {@link DenoisingManager} — denoiser strategy, OIDN, AI upscaler
42
+ * Managers are exposed as direct public properties (Three.js style):
43
+ * - `app.cameraManager` — {@link CameraManager} (camera, controls, auto-focus, DOF)
44
+ * - `app.lightManager` — {@link LightManager} (CRUD, helpers, GPU transfer)
45
+ * - `app.denoisingManager` — {@link DenoisingManager} (strategy, OIDN, AI upscaler)
46
+ * - `app.animationManager` — {@link AnimationManager} (playback, clips, speed)
47
+ * - `app.transformManager` — {@link TransformManager} (gizmo, drag, BVH refit)
48
+ * - `app.interactionManager` — {@link InteractionManager} (selection, focus, context menu)
49
+ * - `app.overlayManager` — {@link OverlayManager} (HUD, helpers)
50
+ * - `app.environmentManager` — EnvironmentManager (HDRI, procedural sky, mode switching)
51
+ * - `app.settings` — {@link RenderSettings} (all render parameters)
52
+ * - `app.stages` — Named pipeline stages for advanced control
61
53
  *
62
54
  * Extends EventDispatcher for event-driven communication with stores/UI.
63
55
  */
@@ -75,7 +67,6 @@ export class PathTracerApp extends EventDispatcher {
75
67
  super();
76
68
 
77
69
  this.canvas = canvas;
78
- this.denoiserCanvas = null;
79
70
  this._autoResize = options.autoResize !== false;
80
71
  this._showStats = options.showStats !== false;
81
72
  this._statsContainer = options.statsContainer || null;
@@ -85,16 +76,13 @@ export class PathTracerApp extends EventDispatcher {
85
76
 
86
77
  // ── Core objects (populated in init) ──
87
78
  this.renderer = null;
88
- this._camera = null;
89
79
  this.scene = null;
90
80
  this.meshScene = null;
91
81
  this._sceneHelpers = null;
92
- this._controls = null;
93
82
 
94
83
  // ── Asset pipeline ──
95
84
  this.assetLoader = null;
96
85
  this._sdf = null;
97
- this.animationManager = new AnimationManager();
98
86
  this._animRefitInFlight = false;
99
87
 
100
88
  // ── Pipeline & stages ──
@@ -107,169 +95,40 @@ export class PathTracerApp extends EventDispatcher {
107
95
  */
108
96
  this.stages = {};
109
97
 
110
- // ── Managers (populated in init) ──
98
+ // ── Managers (direct public access) ──
99
+ /** @type {CameraManager} */
111
100
  this.cameraManager = null;
101
+ /** @type {LightManager} */
112
102
  this.lightManager = null;
103
+ /** @type {DenoisingManager} */
113
104
  this.denoisingManager = null;
105
+ /** @type {OverlayManager} */
114
106
  this.overlayManager = null;
107
+ /** @type {InteractionManager} */
108
+ this.interactionManager = null;
109
+ /** @type {TransformManager} */
110
+ this.transformManager = null;
111
+ /** @type {AnimationManager} */
112
+ this.animationManager = new AnimationManager();
113
+ /** @type {import('./managers/EnvironmentManager.js').EnvironmentManager} */
114
+ this.environmentManager = null;
115
115
 
116
116
  // ── State ──
117
117
  this.isInitialized = false;
118
118
  this.pauseRendering = false;
119
119
  this.pathTracerEnabled = true;
120
- this.animationId = null;
120
+ this.animationManagerId = null;
121
121
  this.needsReset = false;
122
- this._renderCompleteDispatched = false;
123
122
  this._loadingInProgress = false;
124
123
  this._needsDisplayRefresh = false;
125
124
  this._paused = false;
126
125
 
127
- // Stats tracking
128
- this.lastResetTime = performance.now();
129
- this.timeElapsed = 0;
126
+ // Render completion tracking
127
+ this.completion = new CompletionTracker();
130
128
 
131
129
  // Resolution state
132
- this._lastRenderWidth = 0;
133
- this._lastRenderHeight = 0;
134
130
  this._resizeDebounceTimer = null;
135
131
 
136
- // ── Sub-API facade instances (lazily created via getters) ──
137
- this._outputAPI = null;
138
- this._lightsAPI = null;
139
- this._animationAPI = null;
140
- this._selectionAPI = null;
141
- this._transformAPI = null;
142
- this._cameraAPI = null;
143
- this._environmentAPI = null;
144
- this._materialsAPI = null;
145
- this._denoisingAPI = null;
146
-
147
- }
148
-
149
- // ═══════════════════════════════════════════════════════════════
150
- // Settings API — unified parameter access
151
- // ═══════════════════════════════════════════════════════════════
152
-
153
- /**
154
- * Sets a render parameter. Replaces all individual setXxx() methods.
155
- * @param {string} key - Setting key (e.g. 'maxBounces', 'exposure')
156
- * @param {*} value - New value
157
- * @param {Object} [options]
158
- * @param {boolean} [options.reset] - Override default reset behavior
159
- * @param {boolean} [options.silent] - Suppress settingChanged event
160
- */
161
- set( key, value, options ) {
162
-
163
- this.settings.set( key, value, options );
164
-
165
- }
166
-
167
- /**
168
- * Batch-update multiple settings. Only resets once.
169
- * @param {Object} updates - Key/value pairs
170
- * @param {Object} [options]
171
- */
172
- setMany( updates, options ) {
173
-
174
- this.settings.setMany( updates, options );
175
-
176
- }
177
-
178
- /**
179
- * Reads the current value of a setting.
180
- * @param {string} key
181
- * @returns {*}
182
- */
183
- get( key ) {
184
-
185
- return this.settings.get( key );
186
-
187
- }
188
-
189
- /**
190
- * Returns a snapshot of all current settings.
191
- * @returns {Object}
192
- */
193
- getAll() {
194
-
195
- return this.settings.getAll();
196
-
197
- }
198
-
199
- // ═══════════════════════════════════════════════════════════════
200
- // Sub-API Facades — namespaced access to grouped functionality
201
- // ═══════════════════════════════════════════════════════════════
202
-
203
- /** Canvas output, screenshots, resize, and scene statistics. */
204
- get output() {
205
-
206
- if ( ! this._outputAPI ) this._outputAPI = new OutputAPI( this );
207
- return this._outputAPI;
208
-
209
- }
210
-
211
- /** Light CRUD, helpers, and GPU sync. */
212
- get lights() {
213
-
214
- if ( ! this._lightsAPI ) this._lightsAPI = new LightsAPI( this );
215
- return this._lightsAPI;
216
-
217
- }
218
-
219
- /** Animation playback controls. */
220
- get animation() {
221
-
222
- if ( ! this._animationAPI ) this._animationAPI = new AnimationAPI( this );
223
- return this._animationAPI;
224
-
225
- }
226
-
227
- /** Object selection and interaction modes. */
228
- get selection() {
229
-
230
- if ( ! this._selectionAPI ) this._selectionAPI = new SelectionAPI( this );
231
- return this._selectionAPI;
232
-
233
- }
234
-
235
- /** Transform gizmo mode and space. */
236
- get transform() {
237
-
238
- if ( ! this._transformAPI ) this._transformAPI = new TransformAPI( this );
239
- return this._transformAPI;
240
-
241
- }
242
-
243
- /** Camera switching, auto-focus, and DOF — also exposes raw Three.js objects via .active and .controls. */
244
- get camera() {
245
-
246
- if ( ! this._cameraAPI ) this._cameraAPI = new CameraAPI( this );
247
- return this._cameraAPI;
248
-
249
- }
250
-
251
- /** Environment maps, sky modes, and procedural generation. */
252
- get environment() {
253
-
254
- if ( ! this._environmentAPI ) this._environmentAPI = new EnvironmentAPI( this );
255
- return this._environmentAPI;
256
-
257
- }
258
-
259
- /** Material property updates and texture transforms. */
260
- get materials() {
261
-
262
- if ( ! this._materialsAPI ) this._materialsAPI = new MaterialsAPI( this );
263
- return this._materialsAPI;
264
-
265
- }
266
-
267
- /** Denoiser strategy, ASVGF, OIDN, upscaler, adaptive sampling, and auto-exposure. */
268
- get denoising() {
269
-
270
- if ( ! this._denoisingAPI ) this._denoisingAPI = new DenoisingAPI( this );
271
- return this._denoisingAPI;
272
-
273
132
  }
274
133
 
275
134
  // ═══════════════════════════════════════════════════════════════
@@ -281,226 +140,13 @@ export class PathTracerApp extends EventDispatcher {
281
140
  */
282
141
  async init() {
283
142
 
284
- // Wire loading/stats utilities to dispatch events through this app instance
285
- setStatusCallback( ( event ) => this.dispatchEvent( event ) );
286
-
287
- // Check WebGPU support
288
- if ( ! navigator.gpu ) {
289
-
290
- throw new Error( 'WebGPU is not supported in this browser' );
291
-
292
- }
293
-
294
- const adapter = await navigator.gpu.requestAdapter( { powerPreference: 'high-performance' } );
295
- if ( ! adapter ) {
296
-
297
- throw new Error( 'Failed to get WebGPU adapter' );
298
-
299
- }
300
-
301
- const adapterLimits = adapter.limits;
302
-
303
- // Create and initialize WebGPU renderer
304
- this.renderer = new WebGPURenderer( {
305
- canvas: this.canvas,
306
- alpha: true,
307
- powerPreference: 'high-performance',
308
- requiredLimits: {
309
- maxBufferSize: adapterLimits.maxBufferSize,
310
- maxStorageBufferBindingSize: adapterLimits.maxStorageBufferBindingSize,
311
- maxColorAttachmentBytesPerSample: 128,
312
- }
313
- } );
314
-
315
- window.renderer = this.renderer; // For debugging
316
-
317
- await this.renderer.init();
318
-
319
- // Initialize LTC textures required by RectAreaLight in WebGPU renderer
320
- RectAreaLightNode.setLTC( RectAreaLightTexturesLib.init() );
321
-
322
- this.renderer.toneMapping = ACESFilmicToneMapping;
323
- this.renderer.toneMappingExposure = 1.0;
324
-
325
- const width = this.canvas.clientWidth;
326
- const height = this.canvas.clientHeight;
327
- this.renderer.setPixelRatio( 1.0 );
328
-
329
- // Setup camera
330
- this._camera = new PerspectiveCamera( 60, width / height || 1, 0.01, 1000 );
331
- this._camera.position.set( 0, 0, 5 );
332
-
333
- // Create scenes
334
- this.scene = new Scene();
335
- this.meshScene = new Scene();
336
- this._sceneHelpers = new SceneHelpers();
337
-
338
- // Setup orbit controls
339
- this._controls = new OrbitControls( this._camera, this.canvas );
340
- this._controls.screenSpacePanning = true;
341
- this._controls.zoomToCursor = true;
342
- this._controls.saveState();
343
-
344
- // Asset pipeline
345
- this._sdf = new SceneProcessor();
346
- this.assetLoader = new AssetLoader( this.meshScene, this._camera, this._controls );
347
- this._setupFloorPlane();
348
- this.assetLoader.setFloorPlane( this._floorPlane );
349
-
350
- // Track camera movement for reset
351
- this._controls.addEventListener( 'change', () => {
352
-
353
- this.needsReset = true;
354
- this.wake();
355
-
356
- } );
357
-
358
- // ── Create pipeline stages ──
359
- this._createStages();
360
-
361
- // ── Pipeline orchestration ──
362
- const { clientWidth: w, clientHeight: h } = this.canvas;
363
- this.pipeline = new RenderPipeline( this.renderer, w || 1, h || 1 );
364
- this.pipeline.addStage( this.stages.pathTracer );
365
- this.pipeline.addStage( this.stages.normalDepth );
366
- this.pipeline.addStage( this.stages.motionVector );
367
- this.pipeline.addStage( this.stages.ssrc );
368
- this.pipeline.addStage( this.stages.asvgf );
369
- this.pipeline.addStage( this.stages.variance );
370
- this.pipeline.addStage( this.stages.bilateralFilter );
371
- this.pipeline.addStage( this.stages.adaptiveSampling );
372
- this.pipeline.addStage( this.stages.edgeFilter );
373
- this.pipeline.addStage( this.stages.autoExposure );
374
- this.pipeline.addStage( this.stages.display );
375
-
376
- // Set initial render dimensions
377
- const initRenderW = width || 1;
378
- const initRenderH = height || 1;
379
- this.pipeline.setSize( initRenderW, initRenderH );
380
- this._lastRenderWidth = initRenderW;
381
- this._lastRenderHeight = initRenderH;
382
-
383
- // ── Interaction manager ──
384
- this._interactionManager = new InteractionManager( {
385
- scene: this.meshScene,
386
- camera: this._camera,
387
- canvas: this.canvas,
388
- assetLoader: this.assetLoader,
389
- pathTracer: null,
390
- floorPlane: this._floorPlane
391
- } );
392
- this._setupInteractionListeners();
393
-
394
- // ── Managers ──
395
- this.cameraManager = new CameraManager( this._camera, this._controls, this._interactionManager );
396
- this.lightManager = new LightManager( this.scene, this._sceneHelpers, this.stages.pathTracer );
397
- this._createDenoiserCanvas();
398
- this._setupDenoisingManager();
399
- this._setupOverlayManager();
400
-
401
- // ── Transform controls ──
402
- this._transformManager = new TransformManager( {
403
- camera: this._camera,
404
- canvas: this.canvas,
405
- orbitControls: this._controls,
406
- app: this,
407
- } );
408
-
409
- // Wire CameraManager events → app events
410
- this.cameraManager.addEventListener( 'CameraSwitched', ( e ) => this.dispatchEvent( e ) );
411
- this.cameraManager.addEventListener( EngineEvents.AUTO_FOCUS_UPDATED, ( e ) => this.dispatchEvent( e ) );
412
-
413
- // Wire DenoisingManager events → app events
414
- this._forwardEvents( this.denoisingManager, [
415
- EngineEvents.DENOISING_START, EngineEvents.DENOISING_END,
416
- EngineEvents.UPSCALING_START, EngineEvents.UPSCALING_PROGRESS, EngineEvents.UPSCALING_END,
417
- 'resolution_changed',
418
- ] );
419
-
420
- // Set up auto-exposure event listener
421
- this._setupAutoExposureListener();
422
-
423
- // ── Stable auto-focus context (avoid per-frame allocation) ──
424
- this._autoFocusContext = {
425
- meshScene: this.meshScene,
426
- assetLoader: this.assetLoader,
427
- floorPlane: this._floorPlane,
428
- get currentFocusDistance() {
429
-
430
- return null;
431
-
432
- }, // replaced below
433
- pathTracer: this.stages.pathTracer,
434
- setFocusDistance: ( d ) => this.settings.set( 'focusDistance', d, { silent: true } ),
435
- softReset: () => this.reset( true ),
436
- hardReset: () => this.reset(),
437
- };
438
-
439
- // Use a getter so currentFocusDistance reads live value without allocation
440
- const settingsRef = this.settings;
441
- Object.defineProperty( this._autoFocusContext, 'currentFocusDistance', {
442
- get: () => settingsRef.get( 'focusDistance' ),
443
- } );
444
-
445
- // ── Bind RenderSettings ──
446
- this.settings.bind( {
447
- pathTracer: this.stages.pathTracer,
448
- resetCallback: () => this.reset(),
449
- handlers: this._buildSettingsHandlers(),
450
- delegates: {},
451
- } );
452
-
453
- // ── Resize handling ──
454
- this.onResize();
455
- this.resizeHandler = () => this.onResize();
456
- if ( this._autoResize ) {
457
-
458
- window.addEventListener( 'resize', this.resizeHandler );
459
-
460
- }
461
-
462
- // ── Asset load events ──
463
- this._onAssetLoaded = async ( event ) => {
464
-
465
- if ( this._loadingInProgress ) return;
466
-
467
- if ( event.model ) {
468
-
469
- await this.loadSceneData();
470
-
471
- } else if ( event.texture ) {
472
-
473
- const envTexture = this.meshScene.environment;
474
- if ( envTexture && this.stages.pathTracer ) {
475
-
476
- await this.stages.pathTracer.environment.setEnvironmentMap( envTexture );
477
-
478
- }
479
-
480
- resetLoading();
481
-
482
- }
483
-
484
- this.pauseRendering = false;
485
- this.reset();
486
-
487
- };
488
-
489
- this.assetLoader.addEventListener( 'load', this._onAssetLoaded );
490
-
491
- this.assetLoader.addEventListener( 'modelProcessed', ( event ) => {
492
-
493
- const cameras = [ this._camera, ...( event.cameras || [] ) ];
494
- this.cameraManager.setCameras( cameras );
495
-
496
- this._floorPlane = this.assetLoader.floorPlane;
497
- if ( this._interactionManager ) {
498
-
499
- this._interactionManager.floorPlane = this._floorPlane;
500
-
501
- }
502
-
503
- } );
143
+ await this._initRenderer();
144
+ this._initCameraManager();
145
+ this._initScenes();
146
+ this._initAssetPipeline();
147
+ this._initPipeline();
148
+ this._initManagers();
149
+ this._wireEvents();
504
150
 
505
151
  // Seed path tracer with minimal empty scene data
506
152
  this.stages.pathTracer.setTriangleData( new Float32Array( 32 ), 0 );
@@ -508,7 +154,6 @@ export class PathTracerApp extends EventDispatcher {
508
154
  this.stages.pathTracer.materialData.setMaterialData( new Float32Array( 16 ) );
509
155
  this.stages.pathTracer.setupMaterial();
510
156
 
511
- // Setup stats panel
512
157
  if ( this._showStats ) this._initStats();
513
158
 
514
159
  this.isInitialized = true;
@@ -523,7 +168,7 @@ export class PathTracerApp extends EventDispatcher {
523
168
  */
524
169
  animate() {
525
170
 
526
- this.animationId = requestAnimationFrame( () => this.animate() );
171
+ this.animationManagerId = requestAnimationFrame( () => this.animate() );
527
172
 
528
173
  if ( this._loadingInProgress || this._sdf?.isProcessing ) {
529
174
 
@@ -532,7 +177,7 @@ export class PathTracerApp extends EventDispatcher {
532
177
 
533
178
  }
534
179
 
535
- if ( this._controls ) this._controls.update();
180
+ if ( this.cameraManager.controls ) this.cameraManager.controls.update();
536
181
 
537
182
  // Animation playback: compute skinned positions and refit BVH.
538
183
  // Guard prevents overlapping async refits (fire-and-forget with 1-frame latency).
@@ -561,12 +206,12 @@ export class PathTracerApp extends EventDispatcher {
561
206
 
562
207
  }
563
208
 
564
- this._camera.updateMatrixWorld();
209
+ this.cameraManager.camera.updateMatrixWorld();
565
210
 
566
211
  // Raster fallback when path tracer is disabled
567
212
  if ( ! this.pathTracerEnabled ) {
568
213
 
569
- this.renderer.render( this.meshScene, this._camera );
214
+ this.renderer.render( this.meshScene, this.cameraManager.camera );
570
215
  this._renderHelperOverlay();
571
216
  return;
572
217
 
@@ -575,12 +220,12 @@ export class PathTracerApp extends EventDispatcher {
575
220
  if ( this.pauseRendering ) return;
576
221
 
577
222
  // Auto-focus: compute focus distance before rendering
578
- this.cameraManager.updateAutoFocus( this._autoFocusContext );
223
+ this.cameraManager.updateAutoFocus();
579
224
 
580
225
  // Render path tracing
581
226
  if ( this.stages.pathTracer?.isReady ) {
582
227
 
583
- if ( this.stages.pathTracer.isComplete && this._renderCompleteDispatched ) {
228
+ if ( this.stages.pathTracer.isComplete && this.completion.renderCompleteDispatched ) {
584
229
 
585
230
  if ( this._needsDisplayRefresh ) {
586
231
 
@@ -600,28 +245,24 @@ export class PathTracerApp extends EventDispatcher {
600
245
 
601
246
  if ( ! this.stages.pathTracer.isComplete ) {
602
247
 
603
- this.timeElapsed = ( performance.now() - this.lastResetTime ) / 1000;
248
+ this.completion.updateTime();
604
249
 
605
250
  }
606
251
 
607
- updateStats( { timeElapsed: this.timeElapsed, samples: getDisplaySamples( this.stages.pathTracer ) } );
252
+ updateStats( { timeElapsed: this.completion.timeElapsed, samples: getDisplaySamples( this.stages.pathTracer ) } );
608
253
 
609
254
  // Check time limit
610
- const renderLimitMode = this.settings.get( 'renderLimitMode' );
611
- const renderTimeLimit = this.settings.get( 'renderTimeLimit' );
612
- if ( renderLimitMode === 'time' && renderTimeLimit > 0 && this.timeElapsed >= renderTimeLimit ) {
255
+ if ( this.completion.isTimeLimitReached( this.settings.get( 'renderLimitMode' ), this.settings.get( 'renderTimeLimit' ) ) ) {
613
256
 
614
257
  this.stages.pathTracer.isComplete = true;
615
258
 
616
259
  }
617
260
 
618
261
  // Render completion → denoise/upscale chain
619
- if ( this.stages.pathTracer.isComplete && ! this._renderCompleteDispatched ) {
620
-
621
- this._renderCompleteDispatched = true;
262
+ if ( this.stages.pathTracer.isComplete && this.completion.markComplete() ) {
622
263
 
623
264
  this.denoisingManager.onRenderComplete( {
624
- isStillComplete: () => this._renderCompleteDispatched,
265
+ isStillComplete: () => this.completion.renderCompleteDispatched,
625
266
  context: this.pipeline?.context,
626
267
  } );
627
268
 
@@ -645,10 +286,10 @@ export class PathTracerApp extends EventDispatcher {
645
286
  */
646
287
  stopAnimation() {
647
288
 
648
- if ( this.animationId ) {
289
+ if ( this.animationManagerId ) {
649
290
 
650
- cancelAnimationFrame( this.animationId );
651
- this.animationId = null;
291
+ cancelAnimationFrame( this.animationManagerId );
292
+ this.animationManagerId = null;
652
293
 
653
294
  }
654
295
 
@@ -657,7 +298,7 @@ export class PathTracerApp extends EventDispatcher {
657
298
  /** Wakes the animation loop if it was stopped due to idle. */
658
299
  wake() {
659
300
 
660
- if ( ! this.animationId && this.isInitialized && ! this._paused ) this.animate();
301
+ if ( ! this.animationManagerId && this.isInitialized && ! this._paused ) this.animate();
661
302
 
662
303
  }
663
304
 
@@ -674,7 +315,7 @@ export class PathTracerApp extends EventDispatcher {
674
315
  resume() {
675
316
 
676
317
  this._paused = false;
677
- if ( ! this.animationId ) this.animate();
318
+ if ( ! this.animationManagerId ) this.animate();
678
319
  if ( this._stats ) this._stats.dom.style.display = '';
679
320
 
680
321
  }
@@ -692,29 +333,18 @@ export class PathTracerApp extends EventDispatcher {
692
333
 
693
334
  }
694
335
 
695
- // Abort post-processing
336
+ // Abort post-processing and restore denoiser canvas resolution
696
337
  this.denoisingManager?.abort( this.canvas );
697
338
 
698
- // Restore denoiser canvas to base render resolution
699
- if ( this.denoiserCanvas && this._lastRenderWidth && this._lastRenderHeight ) {
700
-
701
- const wasResized = this.denoiserCanvas.width !== this._lastRenderWidth
702
- || this.denoiserCanvas.height !== this._lastRenderHeight;
703
-
704
- this.denoiserCanvas.width = this._lastRenderWidth;
705
- this.denoiserCanvas.height = this._lastRenderHeight;
339
+ if ( this.denoisingManager?.restoreBaseResolution() ) {
706
340
 
707
- if ( wasResized ) {
708
-
709
- this.dispatchEvent( { type: 'resolution_changed', width: this._lastRenderWidth, height: this._lastRenderHeight } );
710
-
711
- }
341
+ const w = this.denoisingManager._lastRenderWidth;
342
+ const h = this.denoisingManager._lastRenderHeight;
343
+ this.dispatchEvent( { type: 'resolution_changed', width: w, height: h } );
712
344
 
713
345
  }
714
346
 
715
- this.timeElapsed = 0;
716
- this.lastResetTime = performance.now();
717
- this._renderCompleteDispatched = false;
347
+ this.completion.reset();
718
348
  this.wake();
719
349
  this.dispatchEvent( { type: 'RenderReset' } );
720
350
  this.dispatchEvent( { type: EngineEvents.RENDER_RESET } );
@@ -736,21 +366,13 @@ export class PathTracerApp extends EventDispatcher {
736
366
 
737
367
  }
738
368
 
739
- this._transformManager?.dispose();
369
+ this.transformManager?.dispose();
740
370
  this.overlayManager?.dispose();
741
371
  this._sceneHelpers?.clear();
742
372
  this.denoisingManager?.dispose();
743
-
744
- if ( this.denoiserCanvas?.parentNode ) {
745
-
746
- this.denoiserCanvas.parentNode.removeChild( this.denoiserCanvas );
747
- this.denoiserCanvas = null;
748
-
749
- }
750
-
751
373
  this.pipeline?.dispose();
752
- this._interactionManager?.dispose();
753
- this._controls?.dispose();
374
+ this.interactionManager?.dispose();
375
+ this.cameraManager?.dispose();
754
376
  this.renderer?.dispose();
755
377
 
756
378
  if ( this._stats ) {
@@ -878,6 +500,9 @@ export class PathTracerApp extends EventDispatcher {
878
500
  */
879
501
  async loadSceneData() {
880
502
 
503
+ // Clear selection before rebuilding — the old object leaves the scene graph
504
+ this.interactionManager?.deselect();
505
+
881
506
  // Stop any running animation before rebuilding scene data
882
507
  this.animationManager.dispose();
883
508
  this._animRefitInFlight = false;
@@ -901,90 +526,24 @@ export class PathTracerApp extends EventDispatcher {
901
526
  await this._sdf.buildBVH( this.meshScene );
902
527
  timer.end( 'BVH build (SceneProcessor)' );
903
528
 
904
- const { triangleData, triangleCount, bvhData, materialData } = this._sdf;
905
-
906
- if ( ! triangleData ) {
907
-
908
- console.error( 'PathTracerApp: Failed to get triangle data' );
909
- return false;
910
-
911
- }
912
-
529
+ // Transfer geometry, materials, and textures to GPU
913
530
  updateLoading( { status: "Transferring data to GPU...", progress: 86 } );
914
531
  await new Promise( r => setTimeout( r, 0 ) );
915
532
  timer.start( 'GPU data transfer' );
916
- this.stages.pathTracer.setTriangleData( triangleData, triangleCount );
917
533
 
918
- if ( ! bvhData ) {
534
+ if ( ! this._sdf.uploadToPathTracer( this.stages.pathTracer, this.lightManager, this.meshScene, environmentTexture ) ) return false;
919
535
 
920
- console.error( 'PathTracerApp: Failed to get BVH data' );
921
- return false;
536
+ timer.end( 'GPU data transfer' );
922
537
 
923
- }
538
+ // Compile shaders
539
+ updateLoading( { status: "Compiling shaders...", progress: 90 } );
540
+ await new Promise( r => setTimeout( r, 0 ) );
541
+ timer.start( 'Material setup (TSL compile)' );
542
+ this.stages.pathTracer.setupMaterial();
543
+ timer.end( 'Material setup (TSL compile)' );
924
544
 
925
- this.stages.pathTracer.setBVHData( bvhData );
926
-
927
- if ( materialData ) {
928
-
929
- this.stages.pathTracer.materialData.setMaterialData( materialData );
930
-
931
- } else {
932
-
933
- console.warn( 'PathTracerApp: No material data, using defaults' );
934
-
935
- }
936
-
937
- if ( environmentTexture ) {
938
-
939
- this.stages.pathTracer.environment.setEnvironmentTexture( environmentTexture );
940
-
941
- }
942
-
943
- // Transfer material texture arrays
944
- this.stages.pathTracer.materialData.setMaterialTextures( {
945
- albedoMaps: this._sdf.albedoTextures,
946
- normalMaps: this._sdf.normalTextures,
947
- bumpMaps: this._sdf.bumpTextures,
948
- roughnessMaps: this._sdf.roughnessTextures,
949
- metalnessMaps: this._sdf.metalnessTextures,
950
- emissiveMaps: this._sdf.emissiveTextures,
951
- displacementMaps: this._sdf.displacementTextures,
952
- } );
953
-
954
- // Emissive triangle data
955
- if ( this._sdf.emissiveTriangleData ) {
956
-
957
- this.stages.pathTracer.setEmissiveTriangleData(
958
- this._sdf.emissiveTriangleData,
959
- this._sdf.emissiveTriangleCount,
960
- this._sdf.emissiveTotalPower,
961
- );
962
-
963
- }
964
-
965
- // Light BVH data
966
- if ( this._sdf.lightBVHNodeData ) {
967
-
968
- this.stages.pathTracer.setLightBVHData(
969
- this._sdf.lightBVHNodeData,
970
- this._sdf.lightBVHNodeCount,
971
- );
972
-
973
- }
974
-
975
- // Transfer lights
976
- this.lightManager.transferSceneLights( this.meshScene );
977
- timer.end( 'GPU data transfer' );
978
-
979
- // Compile shaders
980
- updateLoading( { status: "Compiling shaders...", progress: 90 } );
981
- await new Promise( r => setTimeout( r, 0 ) );
982
- timer.start( 'Material setup (TSL compile)' );
983
- this.stages.pathTracer.setupMaterial();
984
- timer.end( 'Material setup (TSL compile)' );
985
-
986
- // Wait for CDF
987
- if ( cdfPromise ) {
545
+ // Wait for CDF
546
+ if ( cdfPromise ) {
988
547
 
989
548
  updateLoading( { status: "Finalizing environment map...", progress: 95 } );
990
549
  await cdfPromise;
@@ -1001,25 +560,7 @@ export class PathTracerApp extends EventDispatcher {
1001
560
  timer.print();
1002
561
  resetLoading();
1003
562
 
1004
- // Initialize animation manager if GLTF has animation clips.
1005
- // scene = meshScene (for full matrixWorld updates including parent chain)
1006
- // mixerRoot = targetModel (GLTF model root, for animation track name resolution)
1007
- const animations = this.assetLoader?.animations || [];
1008
- if ( animations.length > 0 ) {
1009
-
1010
- const mixerRoot = this.assetLoader?.targetModel || this.meshScene;
1011
- this.animationManager.init( this.meshScene, mixerRoot, this._sdf.meshes, animations, this._sdf.triangleCount );
1012
- this.animationManager.onFinished = () => {
1013
-
1014
- this._animRefitInFlight = false;
1015
- this.dispatchEvent( { type: EngineEvents.ANIMATION_FINISHED } );
1016
-
1017
- };
1018
-
1019
- }
1020
-
1021
- // Initialize transform manager mesh data for BVH refit on object transforms
1022
- this._transformManager?.setMeshData( this._sdf.meshes, this._sdf.triangleCount );
563
+ this._initAnimationAndTransforms();
1023
564
 
1024
565
  this.dispatchEvent( { type: 'SceneRebuild' } );
1025
566
  return true;
@@ -1066,36 +607,7 @@ export class PathTracerApp extends EventDispatcher {
1066
607
 
1067
608
  const result = this._sdf.refitBLASes( affectedMeshIndices, newPositions, newNormals );
1068
609
 
1069
- // Compute dirty ranges for partial GPU upload instead of full buffer copy
1070
- const instanceTable = this._sdf.instanceTable;
1071
- const triRanges = [];
1072
- const bvhRanges = [];
1073
- const FPT = 32; // FLOATS_PER_TRIANGLE
1074
- const FPN = 16; // FLOATS_PER_NODE
1075
-
1076
- for ( const meshIdx of affectedMeshIndices ) {
1077
-
1078
- const entry = instanceTable.entries[ meshIdx ];
1079
- if ( ! entry ) continue;
1080
-
1081
- triRanges.push( {
1082
- offset: entry.triOffset * FPT,
1083
- count: entry.triCount * FPT
1084
- } );
1085
-
1086
- bvhRanges.push( {
1087
- offset: entry.blasOffset * FPN,
1088
- count: entry.blasNodeCount * FPN
1089
- } );
1090
-
1091
- }
1092
-
1093
- // Always include TLAS range (rebuilt on every refit)
1094
- bvhRanges.push( {
1095
- offset: 0,
1096
- count: instanceTable.tlasNodeCount * FPN
1097
- } );
1098
-
610
+ const { triRanges, bvhRanges } = this._sdf.computeBLASDirtyRanges( affectedMeshIndices );
1099
611
  this.stages.pathTracer.updateBufferRanges( triRanges, bvhRanges );
1100
612
  this.reset();
1101
613
 
@@ -1113,95 +625,10 @@ export class PathTracerApp extends EventDispatcher {
1113
625
 
1114
626
  }
1115
627
 
1116
- /**
1117
- * Start playing a GLTF animation clip.
1118
- * @param {number} [clipIndex=0] - Clip index, or -1 to play all
1119
- */
1120
- /** @internal Use engine.animation.play() */
1121
- playAnimation( clipIndex = 0 ) {
1122
-
1123
- if ( ! this.animationManager?.hasAnimations ) {
1124
-
1125
- console.warn( 'playAnimation: No animation clips available' );
1126
- return;
1127
-
1128
- }
1129
-
1130
- this.animationManager.play( clipIndex );
1131
- this.wake();
1132
- this.dispatchEvent( { type: EngineEvents.ANIMATION_STARTED, clipIndex } );
1133
-
1134
- }
1135
-
1136
- /**
1137
- * @internal Use engine.animation.pause()
1138
- * Pause animation — preserves current time position.
1139
- */
1140
- pauseAnimation() {
1141
-
1142
- this.animationManager?.pause();
1143
- this._animRefitInFlight = false;
1144
- this.dispatchEvent( { type: EngineEvents.ANIMATION_PAUSED } );
1145
-
1146
- }
1147
-
1148
- /**
1149
- * @internal Use engine.animation.resume()
1150
- */
1151
- resumeAnimation() {
1152
-
1153
- this.animationManager?.resume();
1154
- this.wake();
1155
- this.dispatchEvent( { type: EngineEvents.ANIMATION_STARTED } );
1156
-
1157
- }
1158
-
1159
- /**
1160
- * @internal Use engine.animation.stop()
1161
- */
1162
- stopAnimationPlayback() {
1163
-
1164
- this.animationManager?.stop();
1165
- this._animRefitInFlight = false;
1166
- this.dispatchEvent( { type: EngineEvents.ANIMATION_STOPPED } );
1167
-
1168
- }
1169
-
1170
- /**
1171
- * @internal Use engine.animation.setSpeed()
1172
- * @param {number} speed
1173
- */
1174
- setAnimationSpeed( speed ) {
1175
-
1176
- this.animationManager?.setSpeed( speed );
1177
-
1178
- }
1179
-
1180
- /**
1181
- * @internal Use engine.animation.setLoop()
1182
- * @param {boolean} loop
1183
- */
1184
- setAnimationLoop( loop ) {
1185
-
1186
- this.animationManager?.setLoop( loop );
1187
-
1188
- }
1189
-
1190
- /**
1191
- * @internal Use engine.animation.clips
1192
- * @returns {{ index: number, name: string, duration: number }[]}
1193
- */
1194
- get animationClips() {
1195
-
1196
- return this.animationManager?.clips || [];
1197
-
1198
- }
1199
-
1200
628
  // ═══════════════════════════════════════════════════════════════
1201
629
  // Resize
1202
630
  // ═══════════════════════════════════════════════════════════════
1203
631
 
1204
- /** @internal Use engine.output.resize() */
1205
632
  onResize() {
1206
633
 
1207
634
  const width = this.canvas.clientWidth;
@@ -1210,15 +637,10 @@ export class PathTracerApp extends EventDispatcher {
1210
637
 
1211
638
  this.renderer.setPixelRatio( 1.0 );
1212
639
  this.renderer.setSize( width, height, false );
1213
- this._camera.aspect = width / height;
1214
- this._camera.updateProjectionMatrix();
640
+ this.cameraManager.camera.aspect = width / height;
641
+ this.cameraManager.camera.updateProjectionMatrix();
1215
642
 
1216
- if ( this.denoiserCanvas ) {
1217
-
1218
- this.denoiserCanvas.style.width = `${width}px`;
1219
- this.denoiserCanvas.style.height = `${height}px`;
1220
-
1221
- }
643
+ this.denoisingManager?.syncCanvasStyle( width, height );
1222
644
 
1223
645
  // Overlay helpers always render at display resolution
1224
646
  const dpr = window.devicePixelRatio || 1;
@@ -1227,7 +649,9 @@ export class PathTracerApp extends EventDispatcher {
1227
649
  Math.round( height * dpr )
1228
650
  );
1229
651
 
1230
- if ( width === this._lastRenderWidth && height === this._lastRenderHeight ) return;
652
+ const lastW = this.denoisingManager?._lastRenderWidth ?? 0;
653
+ const lastH = this.denoisingManager?._lastRenderHeight ?? 0;
654
+ if ( width === lastW && height === lastH ) return;
1231
655
 
1232
656
  clearTimeout( this._resizeDebounceTimer );
1233
657
  this._resizeDebounceTimer = setTimeout( () => {
@@ -1240,37 +664,26 @@ export class PathTracerApp extends EventDispatcher {
1240
664
 
1241
665
  _applyRenderResize( renderWidth, renderHeight ) {
1242
666
 
1243
- this._lastRenderWidth = renderWidth;
1244
- this._lastRenderHeight = renderHeight;
1245
-
1246
667
  this.pipeline?.setSize( renderWidth, renderHeight );
1247
- this.denoisingManager?.denoiser?.setSize( renderWidth, renderHeight );
1248
- this.denoisingManager?.upscaler?.setBaseSize( renderWidth, renderHeight );
668
+ this.denoisingManager?.setRenderSize( renderWidth, renderHeight );
1249
669
  this.needsReset = true;
1250
670
 
1251
671
  this.dispatchEvent( { type: 'resolution_changed', width: renderWidth, height: renderHeight } );
1252
672
 
1253
673
  }
1254
674
 
1255
- /** @internal Use engine.output.setSize() */
1256
675
  setCanvasSize( width, height ) {
1257
676
 
1258
677
  this.canvas.style.width = `${width}px`;
1259
678
  this.canvas.style.height = `${height}px`;
1260
-
1261
- if ( this.denoiserCanvas ) {
1262
-
1263
- this.denoiserCanvas.style.width = `${width}px`;
1264
- this.denoiserCanvas.style.height = `${height}px`;
1265
-
1266
- }
679
+ this.denoisingManager?.syncCanvasStyle( width, height );
1267
680
 
1268
681
  if ( width === 0 || height === 0 ) return;
1269
682
 
1270
683
  this.renderer.setPixelRatio( 1.0 );
1271
684
  this.renderer.setSize( width, height, false );
1272
- this._camera.aspect = width / height;
1273
- this._camera.updateProjectionMatrix();
685
+ this.cameraManager.camera.aspect = width / height;
686
+ this.cameraManager.camera.updateProjectionMatrix();
1274
687
 
1275
688
  clearTimeout( this._resizeDebounceTimer );
1276
689
  this._applyRenderResize( width, height );
@@ -1291,7 +704,7 @@ export class PathTracerApp extends EventDispatcher {
1291
704
  if ( mode === 'results' ) {
1292
705
 
1293
706
  this.pauseRendering = true;
1294
- this._controls.enabled = false;
707
+ this.cameraManager.controls.enabled = false;
1295
708
  this.renderer?.domElement && ( this.renderer.domElement.style.display = 'none' );
1296
709
  this.denoisingManager?.denoiser?.output && ( this.denoisingManager.denoiser.output.style.display = 'none' );
1297
710
  return;
@@ -1301,7 +714,7 @@ export class PathTracerApp extends EventDispatcher {
1301
714
  const isFinal = mode === 'final-render';
1302
715
  const config = isFinal ? FINAL_RENDER_CONFIG : PREVIEW_RENDER_CONFIG;
1303
716
 
1304
- this._controls.enabled = ! isFinal;
717
+ this.cameraManager.controls.enabled = ! isFinal;
1305
718
 
1306
719
  // Batch uniform updates via settings
1307
720
  this.settings.setMany( {
@@ -1311,9 +724,17 @@ export class PathTracerApp extends EventDispatcher {
1311
724
  transmissiveBounces: config.transmissiveBounces,
1312
725
  }, { silent: true } );
1313
726
 
1314
- this.setRenderMode( config.renderMode );
1315
- this.setTileCount( config.tiles );
1316
- this.setTileHelperEnabled( config.tilesHelper );
727
+ this.stages.pathTracer?.setUniform( 'renderMode', parseInt( config.renderMode ) );
728
+ this.stages.pathTracer?.tileManager?.setTileCount( config.tiles );
729
+
730
+ const tileHelper = this.overlayManager?.getHelper( 'tiles' );
731
+ if ( tileHelper ) {
732
+
733
+ tileHelper.enabled = config.tilesHelper;
734
+ if ( ! config.tilesHelper ) tileHelper.hide();
735
+
736
+ }
737
+
1317
738
  this.stages.pathTracer?.updateCompletionThreshold?.();
1318
739
 
1319
740
  const denoiser = this.denoisingManager?.denoiser;
@@ -1342,751 +763,438 @@ export class PathTracerApp extends EventDispatcher {
1342
763
 
1343
764
  }
1344
765
 
766
+ refreshFrame() {
767
+
768
+ this._needsDisplayRefresh = true;
769
+ this.wake();
770
+
771
+ }
772
+
1345
773
  // ═══════════════════════════════════════════════════════════════
1346
- // Delegated APIs Camera
774
+ // Output (absorbed from OutputAPI)
1347
775
  // ═══════════════════════════════════════════════════════════════
1348
776
 
1349
- /** @internal Use engine.cameraAPI.switch() */
1350
- switchCamera( index ) {
777
+ /**
778
+ * Returns the canvas element with the final rendered image.
779
+ * Chooses the post-processing canvas when denoiser/upscaler are active.
780
+ * @returns {HTMLCanvasElement|null}
781
+ */
782
+ getCanvas() {
1351
783
 
1352
- this.cameraManager.switchCamera(
1353
- index,
1354
- this.settings.get( 'focusDistance' ),
1355
- () => this.onResize(),
1356
- () => this.reset()
1357
- );
784
+ if ( ! this.renderer?.domElement ) return null;
1358
785
 
1359
- }
786
+ const dm = this.denoisingManager;
787
+ const usePostProcess = ( dm?.denoiser?.enabled || dm?.upscaler?.enabled )
788
+ && dm?.denoiserCanvas
789
+ && this.stages.pathTracer?.isComplete;
790
+
791
+ if ( usePostProcess ) return dm.denoiserCanvas;
792
+
793
+ // Re-render display stage so the WebGPU canvas has valid content
794
+ if ( this.stages.display && this.pipeline?.context ) {
795
+
796
+ this.stages.display.render( this.pipeline.context );
1360
797
 
1361
- /** @internal Use engine.cameraAPI.getNames() */
1362
- getCameraNames() {
798
+ }
1363
799
 
1364
- return this.cameraManager.getCameraNames();
800
+ return this.renderer.domElement;
1365
801
 
1366
802
  }
1367
803
 
1368
- // ═══════════════════════════════════════════════════════════════
1369
- // Delegated APIs Lights
1370
- // ═══════════════════════════════════════════════════════════════
804
+ /**
805
+ * Downloads a PNG screenshot of the current render.
806
+ */
807
+ screenshot() {
1371
808
 
1372
- /** @internal Use engine.lights.add() */
1373
- addLight( type ) {
809
+ const canvas = this.getCanvas();
810
+ if ( ! canvas ) return;
1374
811
 
1375
- const descriptor = this.lightManager.addLight( type );
1376
- this.reset();
1377
- return descriptor;
812
+ try {
1378
813
 
1379
- }
814
+ const data = canvas.toDataURL( 'image/png' );
815
+ const link = document.createElement( 'a' );
816
+ link.href = data;
817
+ link.download = 'screenshot.png';
818
+ link.click();
1380
819
 
1381
- /** @internal Use engine.lights.remove() */
1382
- removeLight( uuid ) {
820
+ } catch ( error ) {
821
+
822
+ console.error( 'Screenshot failed:', error );
1383
823
 
1384
- const removed = this.lightManager.removeLight( uuid );
1385
- if ( removed ) this.reset();
1386
- return removed;
824
+ }
1387
825
 
1388
826
  }
1389
827
 
1390
- /** @internal Use engine.lights.clear() */
1391
- clearLights() {
828
+ /**
829
+ * Returns scene statistics (triangle count, mesh count, etc.).
830
+ * @returns {Object|null}
831
+ */
832
+ getStatistics() {
833
+
834
+ try {
1392
835
 
1393
- this.lightManager.clearLights();
1394
- this.reset();
836
+ return this._sdf?.getStatistics?.() ?? null;
1395
837
 
1396
- }
838
+ } catch {
1397
839
 
1398
- /** @internal Use engine.lights.getAll() */
1399
- getLights() {
840
+ return null;
1400
841
 
1401
- return this.lightManager.getLights();
842
+ }
1402
843
 
1403
844
  }
1404
845
 
1405
- /** @internal Use engine.lights.sync() */
1406
- updateLights() {
846
+ /**
847
+ * Whether the path tracer has finished converging.
848
+ * @returns {boolean}
849
+ */
850
+ isComplete() {
1407
851
 
1408
- this.lightManager.updateLights();
852
+ return this.stages.pathTracer?.isComplete ?? false;
1409
853
 
1410
854
  }
1411
855
 
1412
- /** @internal Use engine.lights.showHelpers() */
1413
- setShowLightHelper( show ) {
856
+ /**
857
+ * Returns the current accumulated frame/sample count.
858
+ * @returns {number}
859
+ */
860
+ getFrameCount() {
1414
861
 
1415
- this.lightManager.setShowLightHelper( show );
862
+ return this.stages.pathTracer?.frameCount || 0;
1416
863
 
1417
864
  }
1418
865
 
1419
866
  // ═══════════════════════════════════════════════════════════════
1420
- // Delegated APIs Denoiser
867
+ // Materials (absorbed from MaterialsAPI)
1421
868
  // ═══════════════════════════════════════════════════════════════
1422
869
 
1423
- /** @internal Use engine.denoising.setStrategy() */
1424
- setDenoiserStrategy( strategy, asvgfPreset ) {
870
+ /**
871
+ * Updates a single material property and triggers emissive rebuild if needed.
872
+ * @param {number} materialIndex
873
+ * @param {string} property
874
+ * @param {*} value
875
+ */
876
+ setMaterialProperty( materialIndex, property, value ) {
1425
877
 
1426
- this.denoisingManager.setDenoiserStrategy( strategy, asvgfPreset );
1427
- this.reset();
878
+ this.stages.pathTracer?.materialData.updateMaterialProperty( materialIndex, property, value );
1428
879
 
1429
- }
880
+ const emissiveAffectingProps = [ 'emissive', 'emissiveIntensity', 'visible' ];
881
+ if ( emissiveAffectingProps.includes( property )
882
+ && this.stages.pathTracer?.enableEmissiveTriangleSampling?.value ) {
1430
883
 
1431
- /** @internal Use engine.denoising.setASVGFEnabled() */
1432
- setASVGFEnabled( enabled, qualityPreset ) {
884
+ const result = this._sdf.updateMaterialEmissive( materialIndex, property, value );
885
+ if ( result ) {
1433
886
 
1434
- this.denoisingManager.setASVGFEnabled( enabled, qualityPreset );
1435
- this.reset();
887
+ this.stages.pathTracer.setEmissiveTriangleData(
888
+ result.rawData, result.emissiveCount, result.totalPower,
889
+ );
1436
890
 
1437
- }
891
+ }
1438
892
 
1439
- /** @internal Use engine.denoising.applyASVGFPreset() */
1440
- applyASVGFPreset( presetName ) {
893
+ }
1441
894
 
1442
- this.denoisingManager.applyASVGFPreset( presetName );
1443
895
  this.reset();
1444
896
 
1445
897
  }
1446
898
 
1447
- /** @internal Use engine.denoising.setAutoExposure() */
1448
- setAutoExposureEnabled( enabled ) {
899
+ /**
900
+ * Updates a material's texture transform (offset, repeat, rotation).
901
+ * @param {number} materialIndex
902
+ * @param {string} textureName
903
+ * @param {Object} transform
904
+ */
905
+ setTextureTransform( materialIndex, textureName, transform ) {
1449
906
 
1450
- this.denoisingManager.setAutoExposureEnabled( enabled, this.settings.get( 'exposure' ) );
907
+ this.stages.pathTracer?.materialData.updateTextureTransform( materialIndex, textureName, transform );
1451
908
  this.reset();
1452
909
 
1453
910
  }
1454
911
 
1455
- /** @internal Use engine.denoising.setAdaptiveSampling() */
1456
- setAdaptiveSamplingEnabled( enabled ) {
912
+ /**
913
+ * Full material rebuild (required after texture changes).
914
+ * @param {import('three').Scene} [scene]
915
+ */
916
+ async rebuildMaterials( scene ) {
1457
917
 
1458
- this.settings.set( 'useAdaptiveSampling', enabled );
1459
- this.denoisingManager.setAdaptiveSamplingEnabled( enabled );
918
+ await this.stages.pathTracer?.rebuildMaterials( scene || this.meshScene );
919
+ this.reset();
1460
920
 
1461
921
  }
1462
922
 
1463
923
  // ═══════════════════════════════════════════════════════════════
1464
- // Delegated APIs Interaction
924
+ // PrivateInitialization
1465
925
  // ═══════════════════════════════════════════════════════════════
1466
926
 
1467
- /** @internal Use engine.selection.select() */
1468
- selectObject( object ) {
927
+ async _initRenderer() {
928
+
929
+ setStatusCallback( ( event ) => this.dispatchEvent( event ) );
1469
930
 
1470
- const outlineHelper = this.overlayManager?.getHelper( 'outline' );
1471
- if ( outlineHelper ) {
931
+ if ( ! navigator.gpu ) {
1472
932
 
1473
- outlineHelper.setSelectedObjects( object ? [ object ] : [] );
933
+ throw new Error( 'WebGPU is not supported in this browser' );
1474
934
 
1475
935
  }
1476
936
 
1477
- if ( this._interactionManager ) {
937
+ const adapter = await navigator.gpu.requestAdapter( { powerPreference: 'high-performance' } );
938
+ if ( ! adapter ) {
1478
939
 
1479
- this._interactionManager.selectedObject = object || null;
940
+ throw new Error( 'Failed to get WebGPU adapter' );
1480
941
 
1481
942
  }
1482
943
 
1483
- // Attach/detach transform gizmo
1484
- if ( this._transformManager ) {
1485
-
1486
- if ( object ) {
1487
-
1488
- this._transformManager.attach( object );
944
+ const adapterLimits = adapter.limits;
1489
945
 
1490
- } else {
946
+ this.renderer = new WebGPURenderer( {
947
+ canvas: this.canvas,
948
+ alpha: true,
949
+ powerPreference: 'high-performance',
950
+ requiredLimits: {
951
+ maxBufferSize: adapterLimits.maxBufferSize,
952
+ maxStorageBufferBindingSize: adapterLimits.maxStorageBufferBindingSize,
953
+ maxColorAttachmentBytesPerSample: 128,
954
+ }
955
+ } );
1491
956
 
1492
- this._transformManager.detach();
957
+ window.renderer = this.renderer; // For debugging
1493
958
 
1494
- }
959
+ await this.renderer.init();
1495
960
 
1496
- }
961
+ RectAreaLightNode.setLTC( RectAreaLightTexturesLib.init() );
1497
962
 
1498
- this.dispatchEvent( { type: EngineEvents.OBJECT_SELECTED, object: object || null } );
963
+ this.renderer.toneMapping = ACESFilmicToneMapping;
964
+ this.renderer.toneMappingExposure = 1.0;
965
+ this.renderer.setPixelRatio( 1.0 );
1499
966
 
1500
967
  }
1501
968
 
1502
- /** @internal Use engine.selection.toggleFocusMode() */
1503
- toggleFocusMode() {
969
+ _initCameraManager() {
1504
970
 
1505
- if ( ! this._interactionManager ) return false;
1506
- const enabled = this._interactionManager.toggleFocusMode();
1507
- if ( this._controls ) this._controls.enabled = ! enabled;
1508
- return enabled;
971
+ this.cameraManager = new CameraManager( this.canvas );
1509
972
 
1510
973
  }
1511
974
 
1512
- /** @internal Use engine.selection.toggleMode() */
1513
- toggleSelectMode() {
975
+ _initScenes() {
1514
976
 
1515
- if ( ! this._interactionManager ) return false;
1516
- return this._interactionManager.toggleSelectMode();
977
+ this.scene = new Scene();
978
+ this.meshScene = new Scene();
979
+ this._sceneHelpers = new SceneHelpers();
1517
980
 
1518
981
  }
1519
982
 
1520
- /** @internal Use engine.selection.disableMode() */
1521
- disableSelectMode() {
983
+ _initAssetPipeline() {
1522
984
 
1523
- this._interactionManager?.disableSelectMode();
1524
- this._transformManager?.detach();
1525
-
1526
- }
985
+ this._sdf = new SceneProcessor();
986
+ this.assetLoader = new AssetLoader( this.meshScene, this.cameraManager.camera, this.cameraManager.controls );
987
+ this.assetLoader.createFloorPlane();
1527
988
 
1528
- // ═══════════════════════════════════════════════════════════════
1529
- // Delegated APIs — Transform
1530
- // ═══════════════════════════════════════════════════════════════
989
+ this.cameraManager.controls.addEventListener( 'change', () => {
1531
990
 
1532
- /** @internal Use engine.transform.setMode() */
1533
- setTransformMode( mode ) {
991
+ this.needsReset = true;
992
+ this.wake();
1534
993
 
1535
- this._transformManager?.setMode( mode );
1536
- this.dispatchEvent( { type: EngineEvents.TRANSFORM_MODE_CHANGED, mode } );
994
+ } );
1537
995
 
1538
996
  }
1539
997
 
1540
- /** @internal Use engine.transform.setSpace() */
1541
- setTransformSpace( space ) {
998
+ _initPipeline() {
1542
999
 
1543
- this._transformManager?.setSpace( space );
1000
+ this._createStages();
1544
1001
 
1545
- }
1002
+ const { clientWidth: w, clientHeight: h } = this.canvas;
1003
+ this.pipeline = new RenderPipeline( this.renderer, w || 1, h || 1 );
1546
1004
 
1547
- /** @internal Use engine.transform.manager */
1548
- get transformManager() {
1005
+ this.pipeline.addStage( this.stages.pathTracer );
1006
+ this.pipeline.addStage( this.stages.normalDepth );
1007
+ this.pipeline.addStage( this.stages.motionVector );
1008
+ this.pipeline.addStage( this.stages.ssrc );
1009
+ this.pipeline.addStage( this.stages.asvgf );
1010
+ this.pipeline.addStage( this.stages.variance );
1011
+ this.pipeline.addStage( this.stages.bilateralFilter );
1012
+ this.pipeline.addStage( this.stages.adaptiveSampling );
1013
+ this.pipeline.addStage( this.stages.edgeFilter );
1014
+ this.pipeline.addStage( this.stages.autoExposure );
1015
+ this.pipeline.addStage( this.stages.display );
1549
1016
 
1550
- return this._transformManager;
1017
+ const initRenderW = this.canvas.clientWidth || 1;
1018
+ const initRenderH = this.canvas.clientHeight || 1;
1019
+ this.pipeline.setSize( initRenderW, initRenderH );
1551
1020
 
1552
1021
  }
1553
1022
 
1554
- refreshFrame() {
1023
+ _initManagers() {
1555
1024
 
1556
- this._needsDisplayRefresh = true;
1557
- this.wake();
1558
-
1559
- }
1560
-
1561
- // ═══════════════════════════════════════════════════════════════
1562
- // Delegated APIs — Environment
1563
- // ═══════════════════════════════════════════════════════════════
1564
-
1565
- /** @internal Use engine.environment.params */
1566
- getEnvParams() {
1567
-
1568
- return this.stages.pathTracer?.environment?.envParams ?? null;
1569
-
1570
- }
1571
-
1572
- /** @internal Use engine.environment.texture */
1573
- getEnvironmentTexture() {
1574
-
1575
- return this.stages.pathTracer?.environment?.environmentTexture ?? null;
1576
-
1577
- }
1578
-
1579
- /** @internal */
1580
- getEnvironmentCDF() {
1581
-
1582
- return null;
1025
+ this.interactionManager = new InteractionManager( {
1026
+ scene: this.meshScene,
1027
+ camera: this.cameraManager.camera,
1028
+ canvas: this.canvas,
1029
+ assetLoader: this.assetLoader,
1030
+ pathTracer: null,
1031
+ floorPlane: this.assetLoader.floorPlane
1032
+ } );
1583
1033
 
1584
- }
1034
+ this.interactionManager.wireAppEvents( this );
1585
1035
 
1586
- /** @internal Use engine.environment.generateProcedural() */
1587
- async generateProceduralSkyTexture() {
1036
+ this.cameraManager.setInteractionManager( this.interactionManager );
1037
+ this.lightManager = new LightManager( this.scene, this._sceneHelpers, this.stages.pathTracer, {
1038
+ onReset: () => this.reset(),
1039
+ } );
1040
+ this._setupDenoisingManager();
1041
+ this._setupOverlayManager();
1588
1042
 
1589
- return this.stages.pathTracer?.environment.generateProceduralSkyTexture();
1043
+ this.transformManager = new TransformManager( {
1044
+ camera: this.cameraManager.camera,
1045
+ canvas: this.canvas,
1046
+ orbitControls: this.cameraManager.controls,
1047
+ app: this,
1048
+ } );
1590
1049
 
1591
- }
1050
+ // Wire cross-manager dependencies
1051
+ this.interactionManager.setDependencies( {
1052
+ overlayManager: this.overlayManager,
1053
+ transformManager: this.transformManager,
1054
+ appDispatch: ( e ) => this.dispatchEvent( e ),
1055
+ orbitControls: this.cameraManager.controls,
1056
+ } );
1592
1057
 
1593
- /** @internal Use engine.environment.generateGradient() */
1594
- async generateGradientTexture() {
1058
+ this.denoisingManager.setOverlayManager( this.overlayManager );
1059
+ this.denoisingManager.setResetCallback( () => this.reset() );
1060
+ this.denoisingManager.setSettings( this.settings );
1595
1061
 
1596
- return this.stages.pathTracer?.environment.generateGradientTexture();
1062
+ // Expose environment manager (lives on pathTracer stage)
1063
+ this.environmentManager = this.stages.pathTracer.environment;
1064
+ this.environmentManager.callbacks.onAutoExposureReset = () => this.pipeline.eventBus.emit( 'autoexposure:resetHistory' );
1597
1065
 
1598
1066
  }
1599
1067
 
1600
- /** @internal Use engine.environment.generateSolid() */
1601
- async generateSolidColorTexture() {
1068
+ _wireEvents() {
1602
1069
 
1603
- return this.stages.pathTracer?.environment.generateSolidColorTexture();
1604
-
1605
- }
1606
-
1607
- /** @internal Use engine.environment.setTexture() */
1608
- async setEnvironmentMap( texture ) {
1070
+ // Forward manager events → app events
1071
+ this.cameraManager.addEventListener( 'CameraSwitched', ( e ) => this.dispatchEvent( e ) );
1072
+ this.cameraManager.addEventListener( EngineEvents.AUTO_FOCUS_UPDATED, ( e ) => this.dispatchEvent( e ) );
1609
1073
 
1610
- if ( ! this.stages.pathTracer ) {
1074
+ this._forwardEvents( this.denoisingManager, [
1075
+ EngineEvents.DENOISING_START, EngineEvents.DENOISING_END,
1076
+ EngineEvents.UPSCALING_START, EngineEvents.UPSCALING_PROGRESS, EngineEvents.UPSCALING_END,
1077
+ 'resolution_changed',
1078
+ ] );
1611
1079
 
1612
- console.warn( 'PathTracerApp: PathTracer not initialized' );
1613
- return;
1080
+ this._setupAutoExposureListener();
1614
1081
 
1615
- }
1082
+ // Animation lifecycle → wake + refit flag
1083
+ this.animationManager.wakeCallback = () => this.wake();
1084
+ this._forwardEvents( this.animationManager, [
1085
+ EngineEvents.ANIMATION_STARTED,
1086
+ EngineEvents.ANIMATION_PAUSED,
1087
+ EngineEvents.ANIMATION_STOPPED,
1088
+ ] );
1089
+ this.animationManager.addEventListener( EngineEvents.ANIMATION_PAUSED, () => {
1616
1090
 
1617
- await this.stages.pathTracer.environment.setEnvironmentMap( texture );
1618
- this.reset();
1091
+ this._animRefitInFlight = false;
1619
1092
 
1620
- }
1093
+ } );
1094
+ this.animationManager.addEventListener( EngineEvents.ANIMATION_STOPPED, () => {
1621
1095
 
1622
- /** @internal Use engine.environment.markDirty() */
1623
- markEnvironmentNeedsUpdate() {
1096
+ this._animRefitInFlight = false;
1624
1097
 
1625
- const tex = this.stages.pathTracer?.environment?.environmentTexture;
1626
- if ( tex ) tex.needsUpdate = true;
1098
+ } );
1627
1099
 
1628
- }
1100
+ // Camera callbacks for switchCamera / focusOn
1101
+ this.cameraManager.initCallbacks( {
1102
+ onResize: () => this.onResize(),
1103
+ onReset: () => this.reset(),
1104
+ getSettings: ( k ) => this.settings.get( k ),
1105
+ } );
1629
1106
 
1630
- /** @internal Use engine.environment.setMode() */
1631
- async setEnvironmentMode( mode ) {
1107
+ // Auto-focus context CameraManager stores it, reads it each frame
1108
+ this.cameraManager.initAutoFocus( {
1109
+ meshScene: this.meshScene,
1110
+ assetLoader: this.assetLoader,
1111
+ floorPlane: this.assetLoader.floorPlane,
1112
+ pathTracer: this.stages.pathTracer,
1113
+ settings: this.settings,
1114
+ softReset: () => this.reset( true ),
1115
+ hardReset: () => this.reset(),
1116
+ } );
1632
1117
 
1633
- const previousMode = this._environmentMode || 'hdri';
1634
- this._environmentMode = mode;
1118
+ // Bind settings to pipeline stages
1119
+ this.settings.bind( {
1120
+ stages: this.stages,
1121
+ resetCallback: () => this.reset(),
1122
+ reconcileCompletion: () => this._reconcileCompletion(),
1123
+ } );
1635
1124
 
1636
- if ( mode !== 'hdri' && previousMode === 'hdri' ) {
1125
+ // Resize handling
1126
+ this.onResize();
1127
+ this.resizeHandler = () => this.onResize();
1128
+ if ( this._autoResize ) {
1637
1129
 
1638
- this._previousHDRI = this.getEnvironmentTexture();
1639
- this._previousCDF = this.getEnvironmentCDF();
1130
+ window.addEventListener( 'resize', this.resizeHandler );
1640
1131
 
1641
1132
  }
1642
1133
 
1643
- if ( mode === 'gradient' ) {
1134
+ // Asset load events
1135
+ this._onAssetLoaded = async ( event ) => {
1644
1136
 
1645
- await this.generateGradientTexture();
1137
+ if ( this._loadingInProgress ) return;
1646
1138
 
1647
- } else if ( mode === 'color' ) {
1139
+ if ( event.model ) {
1648
1140
 
1649
- await this.generateSolidColorTexture();
1141
+ await this.loadSceneData();
1650
1142
 
1651
- } else if ( mode === 'procedural' ) {
1143
+ } else if ( event.texture ) {
1652
1144
 
1653
- await this.generateProceduralSkyTexture();
1145
+ const envTexture = this.meshScene.environment;
1146
+ if ( envTexture && this.stages.pathTracer ) {
1654
1147
 
1655
- } else if ( mode === 'hdri' ) {
1148
+ await this.stages.pathTracer.environment.setEnvironmentMap( envTexture );
1656
1149
 
1657
- if ( this._previousHDRI ) {
1150
+ }
1658
1151
 
1659
- await this.setEnvironmentMap( this._previousHDRI );
1660
- this._previousHDRI = null;
1661
- this._previousCDF = null;
1152
+ resetLoading();
1662
1153
 
1663
1154
  }
1664
1155
 
1665
- }
1666
-
1667
- const envParams = this.getEnvParams();
1668
- if ( envParams ) envParams.mode = mode;
1669
-
1670
- this.markEnvironmentNeedsUpdate();
1671
- this.pipeline?.eventBus.emit( 'autoexposure:resetHistory' );
1672
- this.reset();
1673
-
1674
- }
1675
-
1676
- // ═══════════════════════════════════════════════════════════════
1677
- // Read-Only Accessors
1678
- // ═══════════════════════════════════════════════════════════════
1679
-
1680
- /** @internal Use engine.output.isComplete() */
1681
- isComplete() {
1682
-
1683
- return this.stages.pathTracer?.isComplete ?? false;
1684
-
1685
- }
1686
-
1687
- /** @internal Use engine.output.getFrameCount() */
1688
- getFrameCount() {
1689
-
1690
- return this.stages.pathTracer?.frameCount || 0;
1691
-
1692
- }
1693
-
1694
- /**
1695
- * Convenience alias for the raw Three.js PerspectiveCamera.
1696
- * Prefer engine.camera.active for consistency with the sub-API pattern.
1697
- */
1698
- get activeCamera() {
1699
-
1700
- return this.cameraManager?.camera ?? this._camera;
1701
-
1702
- }
1703
-
1704
- /** @internal Use engine.output.getStatistics() */
1705
- getSceneStatistics() {
1706
-
1707
- try {
1708
-
1709
- return this._sdf?.getStatistics?.() ?? null;
1710
-
1711
- } catch {
1712
-
1713
- return null;
1714
-
1715
- }
1716
-
1717
- }
1718
-
1719
- /**
1720
- * @internal Use engine.output.getCanvas()
1721
- * Returns the canvas element suitable for reading pixels from.
1722
- * Ensures the WebGPU canvas has fresh content if it's the source.
1723
- * Use this instead of directly accessing renderer.domElement / denoiserCanvas.
1724
- * @returns {HTMLCanvasElement|null}
1725
- */
1726
- getOutputCanvas() {
1727
-
1728
- if ( ! this.renderer?.domElement ) return null;
1729
-
1730
- const denoiser = this.denoisingManager?.denoiser;
1731
- const upscaler = this.denoisingManager?.upscaler;
1732
- const usePostProcess = ( denoiser?.enabled || upscaler?.enabled )
1733
- && this.denoiserCanvas
1734
- && this.stages.pathTracer?.isComplete;
1735
-
1736
- if ( usePostProcess ) return this.denoiserCanvas;
1737
-
1738
- // Re-render display stage so the WebGPU canvas has valid content
1739
- if ( this.stages.display && this.pipeline?.context ) {
1740
-
1741
- this.stages.display.render( this.pipeline.context );
1742
-
1743
- }
1744
-
1745
- return this.renderer.domElement;
1746
-
1747
- }
1748
-
1749
- /**
1750
- * @internal Use engine.cameraAPI.focusOn()
1751
- * @param {import('three').Vector3} center
1752
- */
1753
- focusOnPoint( center ) {
1754
-
1755
- if ( ! center || ! this._controls ) return;
1756
- this._controls.target.copy( center );
1757
- this._controls.update();
1758
- this.reset();
1759
-
1760
- }
1761
-
1762
- /**
1763
- * @internal Use engine.selection.dispatchEvent()
1764
- * @param {Object} event
1765
- */
1766
- dispatchInteractionEvent( event ) {
1767
-
1768
- this._interactionManager?.dispatchEvent( event );
1769
-
1770
- }
1771
-
1772
- /**
1773
- * @internal Use engine.selection.on()
1774
- * @param {string} type
1775
- * @param {Function} handler
1776
- * @returns {Function} unsubscribe function
1777
- */
1778
- onInteractionEvent( type, handler ) {
1779
-
1780
- this._interactionManager?.addEventListener( type, handler );
1781
- return () => this._interactionManager?.removeEventListener( type, handler );
1782
-
1783
- }
1784
-
1785
- /** @internal Use engine.output.screenshot() */
1786
- takeScreenshot() {
1787
-
1788
- const canvas = this.getOutputCanvas();
1789
- if ( ! canvas ) return;
1790
-
1791
- try {
1792
-
1793
- const screenshot = canvas.toDataURL( 'image/png' );
1794
- const link = document.createElement( 'a' );
1795
- link.href = screenshot;
1796
- link.download = 'screenshot.png';
1797
- link.click();
1798
-
1799
- } catch ( error ) {
1800
-
1801
- console.error( 'PathTracerApp: Screenshot failed:', error );
1802
-
1803
- }
1804
-
1805
- }
1806
-
1807
- setPathTracerEnabled( val ) {
1808
-
1809
- this.pathTracerEnabled = val;
1810
-
1811
- }
1812
- setAccumulationEnabled( val ) {
1813
-
1814
- this.stages.pathTracer?.setAccumulationEnabled( val );
1815
-
1816
- }
1817
- setRenderMode( mode ) {
1818
-
1819
- this.stages.pathTracer?.setUniform( 'renderMode', parseInt( mode ) );
1820
-
1821
- }
1822
- setTileCount( val ) {
1823
-
1824
- this.stages.pathTracer?.tileManager?.setTileCount( val );
1825
-
1826
- }
1827
- setInteractionModeEnabled( val ) {
1828
-
1829
- this.stages.pathTracer?.setInteractionModeEnabled( val );
1830
-
1831
- }
1832
-
1833
- /** @internal Use engine.denoising.setAdaptiveSamplingParams() */
1834
- setAdaptiveSamplingParameters( params ) {
1835
-
1836
- if ( params.min !== undefined ) this.stages.pathTracer?.setAdaptiveSamplingMin( params.min );
1837
- if ( params.adaptiveSamplingMax !== undefined ) this.settings.set( 'adaptiveSamplingMax', params.adaptiveSamplingMax );
1838
- this.stages.adaptiveSampling?.setAdaptiveSamplingParameters( params );
1839
-
1840
- }
1841
-
1842
- /** @internal Use engine.materials.setProperty() */
1843
- updateMaterialProperty( materialIndex, property, value ) {
1844
-
1845
- this.stages.pathTracer?.materialData.updateMaterialProperty( materialIndex, property, value );
1846
-
1847
- const emissiveAffectingProps = [ 'emissive', 'emissiveIntensity', 'visible' ];
1848
- if ( emissiveAffectingProps.includes( property )
1849
- && this._sdf?.emissiveTriangleBuilder
1850
- && this.stages.pathTracer?.enableEmissiveTriangleSampling?.value ) {
1156
+ this.pauseRendering = false;
1157
+ this.reset();
1851
1158
 
1852
- const mat = this._sdf.materials[ materialIndex ];
1853
- if ( mat ) {
1159
+ };
1854
1160
 
1855
- if ( property === 'emissive' ) mat.emissive = value;
1856
- else if ( property === 'emissiveIntensity' ) mat.emissiveIntensity = value;
1857
- else if ( property === 'visible' ) mat.visible = value;
1161
+ this.assetLoader.addEventListener( 'load', this._onAssetLoaded );
1858
1162
 
1859
- const changed = this._sdf.emissiveTriangleBuilder.updateMaterialEmissive(
1860
- materialIndex, mat,
1861
- this._sdf.triangleData, this._sdf.materials, this._sdf.triangleCount,
1862
- );
1163
+ this.assetLoader.addEventListener( 'modelProcessed', ( event ) => {
1863
1164
 
1864
- if ( changed ) {
1165
+ const cameras = [ this.cameraManager.camera, ...( event.cameras || [] ) ];
1166
+ this.cameraManager.setCameras( cameras );
1865
1167
 
1866
- const emissiveRawData = this._sdf.emissiveTriangleBuilder.createEmissiveRawData();
1867
- this.stages.pathTracer.setEmissiveTriangleData(
1868
- emissiveRawData,
1869
- this._sdf.emissiveTriangleBuilder.emissiveCount,
1870
- this._sdf.emissiveTriangleBuilder.totalEmissivePower,
1871
- );
1168
+ if ( this.interactionManager ) {
1872
1169
 
1873
- }
1170
+ this.interactionManager.floorPlane = this.assetLoader.floorPlane;
1874
1171
 
1875
1172
  }
1876
1173
 
1877
- }
1878
-
1879
- this.reset();
1880
-
1881
- }
1882
-
1883
- /** @internal Use engine.materials.setTextureTransform() */
1884
- updateTextureTransform( materialIndex, textureName, transform ) {
1885
-
1886
- this.stages.pathTracer?.materialData.updateTextureTransform( materialIndex, textureName, transform );
1887
- this.reset();
1888
-
1889
- }
1890
-
1891
- /** @internal Use engine.materials.refresh() */
1892
- refreshMaterial() {
1893
-
1894
- this.reset();
1895
-
1896
- }
1897
-
1898
- /** @internal Use engine.materials.replace() */
1899
- updateMaterial( materialIndex, material ) {
1900
-
1901
- this.stages.pathTracer?.materialData.updateMaterial( materialIndex, material );
1902
-
1903
- }
1904
-
1905
- /** @internal Use engine.materials.rebuild() */
1906
- async rebuildMaterials( scene ) {
1907
-
1908
- await this.stages.pathTracer?.rebuildMaterials( scene || this.meshScene );
1909
-
1910
- }
1911
-
1912
- // ═══════════════════════════════════════════════════════════════
1913
- // Stage Parameter Facade — hides direct stage access from store
1914
- // ═══════════════════════════════════════════════════════════════
1915
-
1916
- // ── ASVGF ──
1917
-
1918
- /** @internal Use engine.denoising.setASVGFParams() */
1919
- updateASVGFParameters( params ) {
1920
-
1921
- this.stages.asvgf?.updateParameters( params );
1922
-
1923
- }
1924
-
1925
- /** @internal Use engine.denoising.toggleASVGFHeatmap() */
1926
- toggleASVGFHeatmap( enabled ) {
1927
-
1928
- this.stages.asvgf?.toggleHeatmap?.( enabled );
1174
+ } );
1929
1175
 
1930
1176
  }
1931
1177
 
1932
1178
  /**
1933
- * @internal Use engine.denoising.configureASVGFForMode()
1934
- * @param {Object} config
1179
+ * Initializes animation manager and transform manager after scene rebuild.
1935
1180
  */
1936
- configureASVGFForMode( config ) {
1937
-
1938
- if ( ! this.stages.asvgf ) return;
1939
-
1940
- this.stages.asvgf.enabled = config.enabled;
1941
- if ( this.stages.variance ) this.stages.variance.enabled = config.enabled;
1942
- if ( this.stages.bilateralFilter ) this.stages.bilateralFilter.enabled = config.enabled;
1943
-
1944
- if ( config.enabled ) {
1945
-
1946
- this.stages.asvgf.updateParameters( config );
1947
-
1948
- }
1949
-
1950
- }
1951
-
1952
- // ── SSRC ──
1953
-
1954
- /** @internal Use engine.denoising.setSSRCParams() */
1955
- updateSSRCParameters( params ) {
1956
-
1957
- this.stages.ssrc?.updateParameters( params );
1958
-
1959
- }
1960
-
1961
- // ── EdgeAware Filtering ──
1962
-
1963
- /** @internal Use engine.denoising.setEdgeAwareParams() */
1964
- updateEdgeAwareUniforms( params ) {
1965
-
1966
- this.stages.edgeFilter?.updateUniforms( params );
1967
-
1968
- }
1969
-
1970
- // ── Auto Exposure ──
1971
-
1972
- /** @internal Use engine.denoising.setAutoExposureParams() */
1973
- updateAutoExposureParameters( params ) {
1974
-
1975
- this.stages.autoExposure?.updateParameters( params );
1976
-
1977
- }
1978
-
1979
- // ── Adaptive Sampling ──
1980
-
1981
- /** @internal Use engine.denoising.setAdaptiveSamplingParams() */
1982
- updateAdaptiveSamplingParameters( params ) {
1983
-
1984
- this.stages.adaptiveSampling?.setAdaptiveSamplingParameters( params );
1985
-
1986
- }
1987
-
1988
- /** @internal Use engine.denoising.setAdaptiveSamplingParams({ varianceThreshold }) */
1989
- setAdaptiveSamplingVarianceThreshold( v ) {
1990
-
1991
- this.stages.adaptiveSampling?.setVarianceThreshold( v );
1992
-
1993
- }
1994
-
1995
- /** @internal Use engine.denoising.setAdaptiveSamplingParams({ materialBias }) */
1996
- setAdaptiveSamplingMaterialBias( v ) {
1997
-
1998
- this.stages.adaptiveSampling?.setMaterialBias( v );
1999
-
2000
- }
2001
-
2002
- /** @internal Use engine.denoising.setAdaptiveSamplingParams({ edgeBias }) */
2003
- setAdaptiveSamplingEdgeBias( v ) {
2004
-
2005
- this.stages.adaptiveSampling?.setEdgeBias( v );
2006
-
2007
- }
2008
-
2009
- /** @internal Use engine.denoising.setAdaptiveSamplingParams({ convergenceSpeed }) */
2010
- setAdaptiveSamplingConvergenceSpeed( v ) {
2011
-
2012
- this.stages.adaptiveSampling?.setConvergenceSpeed( v );
2013
-
2014
- }
2015
-
2016
- /** @internal Use engine.denoising.toggleAdaptiveSamplingHelper() */
2017
- toggleAdaptiveSamplingHelper( enabled ) {
2018
-
2019
- this.stages.adaptiveSampling?.toggleHelper( enabled );
2020
-
2021
- }
2022
-
2023
- // ── Tile Highlight ──
2024
-
2025
- /** @internal Use engine.denoising.setTileHighlightEnabled() */
2026
- setTileHighlightEnabled( enabled ) {
2027
-
2028
- this.setTileHelperEnabled( enabled );
2029
-
2030
- }
2031
-
2032
- // ── OIDN Denoiser ──
2033
-
2034
- /** @internal Use engine.denoising.setOIDNEnabled() */
2035
- setOIDNEnabled( enabled ) {
2036
-
2037
- const d = this.denoisingManager?.denoiser;
2038
- if ( d ) d.enabled = enabled;
2039
-
2040
- }
1181
+ _initAnimationAndTransforms() {
2041
1182
 
2042
- /** @internal Use engine.denoising.setOIDNQuality() */
2043
- updateOIDNQuality( quality ) {
2044
-
2045
- this.denoisingManager?.denoiser?.updateQuality( quality );
2046
-
2047
- }
2048
-
2049
- /** @internal Use engine.denoising.setOIDNTileHelper() */
2050
- setOIDNTileHelper( enabled ) {
2051
-
2052
- this.setTileHelperEnabled( enabled );
2053
-
2054
- }
1183
+ const animations = this.assetLoader?.animations || [];
1184
+ if ( animations.length > 0 ) {
2055
1185
 
2056
- /** @internal Use engine.denoising.setTileHelperEnabled() */
2057
- setTileHelperEnabled( enabled ) {
1186
+ const mixerRoot = this.assetLoader?.targetModel || this.meshScene;
1187
+ this.animationManager.init( this.meshScene, mixerRoot, this._sdf.meshes, animations, this._sdf.triangleCount );
1188
+ this.animationManager.onFinished = () => {
2058
1189
 
2059
- const tileHelper = this.overlayManager?.getHelper( 'tiles' );
2060
- if ( tileHelper ) {
1190
+ this._animRefitInFlight = false;
1191
+ this.dispatchEvent( { type: EngineEvents.ANIMATION_FINISHED } );
2061
1192
 
2062
- tileHelper.enabled = enabled;
2063
- if ( ! enabled ) tileHelper.hide();
1193
+ };
2064
1194
 
2065
1195
  }
2066
1196
 
2067
- }
2068
-
2069
- // ── AI Upscaler ──
2070
-
2071
- /** @internal Use engine.denoising.setUpscalerEnabled() */
2072
- setUpscalerEnabled( enabled ) {
2073
-
2074
- const u = this.denoisingManager?.upscaler;
2075
- if ( u ) u.enabled = enabled;
2076
-
2077
- }
2078
-
2079
- /** @internal Use engine.denoising.setUpscalerScaleFactor() */
2080
- setUpscalerScaleFactor( factor ) {
2081
-
2082
- this.denoisingManager?.upscaler?.setScaleFactor( factor );
2083
-
2084
- }
2085
-
2086
- /** @internal Use engine.denoising.setUpscalerQuality() */
2087
- setUpscalerQuality( quality ) {
2088
-
2089
- this.denoisingManager?.upscaler?.setQuality( quality );
1197
+ this.transformManager?.setMeshData( this._sdf.meshes, this._sdf.triangleCount );
2090
1198
 
2091
1199
  }
2092
1200
 
@@ -2099,11 +1207,11 @@ export class PathTracerApp extends EventDispatcher {
2099
1207
  const adaptiveSamplingMax = this.settings.get( 'adaptiveSamplingMax' );
2100
1208
  const useAdaptiveSampling = this.settings.get( 'useAdaptiveSampling' );
2101
1209
 
2102
- this.stages.pathTracer = new PathTracer( this.renderer, this.scene, this._camera );
1210
+ this.stages.pathTracer = new PathTracer( this.renderer, this.scene, this.cameraManager.camera );
2103
1211
  this.stages.normalDepth = new NormalDepth( this.renderer, {
2104
1212
  pathTracer: this.stages.pathTracer
2105
1213
  } );
2106
- this.stages.motionVector = new MotionVector( this.renderer, this._camera, {
1214
+ this.stages.motionVector = new MotionVector( this.renderer, this.cameraManager.camera, {
2107
1215
  pathTracer: this.stages.pathTracer
2108
1216
  } );
2109
1217
  this.stages.ssrc = new SSRC( this.renderer, { enabled: false } );
@@ -2124,31 +1232,13 @@ export class PathTracerApp extends EventDispatcher {
2124
1232
 
2125
1233
  }
2126
1234
 
2127
- _createDenoiserCanvas() {
2128
-
2129
- if ( this.denoiserCanvas ) return; // guard against double init
2130
-
2131
- const parent = this.canvas.parentNode;
2132
- if ( ! parent ) return; // headless / detached canvas — skip
2133
-
2134
- const dc = document.createElement( 'canvas' );
2135
- dc.width = this.canvas.width;
2136
- dc.height = this.canvas.height;
2137
- dc.style.width = `${this.canvas.clientWidth}px`;
2138
- dc.style.height = `${this.canvas.clientHeight}px`;
2139
-
2140
- parent.insertBefore( dc, this.canvas );
2141
- this.denoiserCanvas = dc;
2142
-
2143
- }
2144
-
2145
1235
  _setupDenoisingManager() {
2146
1236
 
2147
1237
  this.denoisingManager = new DenoisingManager( {
2148
1238
  renderer: this.renderer,
2149
- denoiserCanvas: this.denoiserCanvas,
1239
+ mainCanvas: this.canvas,
2150
1240
  scene: this.scene,
2151
- camera: this._camera,
1241
+ camera: this.cameraManager.camera,
2152
1242
  stages: {
2153
1243
  pathTracer: this.stages.pathTracer,
2154
1244
  asvgf: this.stages.asvgf,
@@ -2169,82 +1259,10 @@ export class PathTracerApp extends EventDispatcher {
2169
1259
  this.denoisingManager.setupDenoiser();
2170
1260
  this.denoisingManager.setupUpscaler();
2171
1261
 
2172
- }
2173
-
2174
- /**
2175
- * Builds handler functions for multi-stage settings that can't
2176
- * be routed with a simple uniform forward.
2177
- */
2178
- _buildSettingsHandlers() {
2179
-
2180
- return {
2181
-
2182
- handleTransparentBackground: ( value ) => {
2183
-
2184
- this.stages.pathTracer?.setUniform( 'transparentBackground', value );
2185
- this.stages.display?.setTransparentBackground( value );
2186
-
2187
- },
2188
-
2189
- handleExposure: ( value ) => {
2190
-
2191
- if ( ! this.stages.autoExposure?.enabled ) {
2192
-
2193
- this.stages.display?.setExposure( value );
2194
-
2195
- }
2196
-
2197
- },
2198
-
2199
- handleSaturation: ( value ) => {
2200
-
2201
- this.stages.display?.setSaturation( value );
2202
-
2203
- },
2204
-
2205
- handleRenderLimitMode: ( value ) => {
2206
-
2207
- if ( this.stages.pathTracer?.setRenderLimitMode ) {
2208
-
2209
- this.stages.pathTracer.setRenderLimitMode( value );
2210
-
2211
- }
2212
-
2213
- },
2214
-
2215
- handleMaxSamples: ( value ) => {
2216
-
2217
- this.stages.pathTracer?.setUniform( 'maxSamples', value );
2218
- this.stages.pathTracer?.updateCompletionThreshold();
2219
- this._reconcileCompletion();
2220
-
2221
- },
2222
-
2223
- handleRenderTimeLimit: () => {
2224
-
2225
- this._reconcileCompletion();
2226
-
2227
- },
2228
-
2229
- handleRenderMode: ( value ) => {
2230
-
2231
- this.stages.pathTracer?.setUniform( 'renderMode', parseInt( value ) );
2232
-
2233
- },
2234
-
2235
- handleEnvironmentRotation: ( value ) => {
2236
-
2237
- this.stages.pathTracer?.environment.setEnvironmentRotation( value );
2238
-
2239
- },
2240
-
2241
- handleInteractionModeEnabled: ( value ) => {
2242
-
2243
- this.setInteractionModeEnabled( value );
2244
-
2245
- },
2246
-
2247
- };
1262
+ // Set initial render resolution
1263
+ const initW = this.canvas.clientWidth || 1;
1264
+ const initH = this.canvas.clientHeight || 1;
1265
+ this.denoisingManager.setRenderSize( initW, initH );
2248
1266
 
2249
1267
  }
2250
1268
 
@@ -2253,7 +1271,9 @@ export class PathTracerApp extends EventDispatcher {
2253
1271
  const stage = this.stages.pathTracer;
2254
1272
  if ( ! stage ) return;
2255
1273
 
2256
- const shouldBeComplete = this._isRenderLimitReached();
1274
+ const shouldBeComplete = this.completion.isLimitReached(
1275
+ stage, this.settings.get( 'renderLimitMode' ), this.settings.get( 'renderTimeLimit' )
1276
+ );
2257
1277
 
2258
1278
  if ( shouldBeComplete && ! stage.isComplete ) {
2259
1279
 
@@ -2262,147 +1282,23 @@ export class PathTracerApp extends EventDispatcher {
2262
1282
  } else if ( ! shouldBeComplete && stage.isComplete ) {
2263
1283
 
2264
1284
  stage.isComplete = false;
2265
- this._renderCompleteDispatched = false;
2266
-
2267
- // Adjust lastResetTime so timeElapsed continues from where it paused
2268
- // rather than including idle time spent while completed
2269
- this.lastResetTime = performance.now() - this.timeElapsed * 1000;
1285
+ this.completion.resumeFromPause();
2270
1286
 
2271
1287
  this.canvas.style.opacity = '1';
2272
1288
  const denoiserOutput = this.denoisingManager?.denoiser?.output;
2273
1289
  if ( denoiserOutput ) denoiserOutput.style.display = 'none';
2274
1290
 
2275
1291
  this.dispatchEvent( { type: EngineEvents.RENDER_RESET } );
2276
-
2277
- // Restart the animation loop (it was stopped when render completed)
2278
1292
  this.wake();
2279
1293
 
2280
1294
  }
2281
1295
 
2282
1296
  }
2283
1297
 
2284
- _isRenderLimitReached() {
2285
-
2286
- const stage = this.stages.pathTracer;
2287
- if ( ! stage ) return false;
2288
-
2289
- if ( this.settings.get( 'renderLimitMode' ) === 'time' ) {
2290
-
2291
- const limit = this.settings.get( 'renderTimeLimit' );
2292
- return limit > 0 && this.timeElapsed >= limit;
2293
-
2294
- }
2295
-
2296
- return stage.frameCount >= stage.completionThreshold;
2297
-
2298
- }
2299
-
2300
- _setupFloorPlane() {
2301
-
2302
- this._floorPlane = new Mesh(
2303
- new CircleGeometry(),
2304
- new MeshPhysicalMaterial( {
2305
- transparent: false,
2306
- color: 0x303030,
2307
- roughness: 1,
2308
- metalness: 0,
2309
- opacity: 0,
2310
- transmission: 0,
2311
- } )
2312
- );
2313
- this._floorPlane.name = "Ground";
2314
- this._floorPlane.visible = false;
2315
- this.meshScene.add( this._floorPlane );
2316
-
2317
- }
2318
-
2319
1298
  _initStats() {
2320
1299
 
2321
- this._stats = new Stats( { horizontal: true, trackGPU: true } );
2322
- this._stats.dom.style.position = 'absolute';
2323
- this._stats.dom.style.top = 'unset';
2324
- this._stats.dom.style.bottom = '48px';
2325
-
2326
- this._stats.init( this.renderer );
2327
1300
  const container = this._statsContainer || this.canvas.parentElement || document.body;
2328
- container.appendChild( this._stats.dom );
2329
-
2330
- const foregroundColor = '#ffffff';
2331
- const backgroundColor = '#1e293b';
2332
-
2333
- const gradient = this._stats.fpsPanel.context.createLinearGradient( 0, this._stats.fpsPanel.GRAPH_Y, 0, this._stats.fpsPanel.GRAPH_Y + this._stats.fpsPanel.GRAPH_HEIGHT );
2334
- gradient.addColorStop( 0, foregroundColor );
2335
-
2336
- this._stats.fpsPanel.fg = this._stats.msPanel.fg = foregroundColor;
2337
- this._stats.fpsPanel.bg = this._stats.msPanel.bg = backgroundColor;
2338
- this._stats.fpsPanel.gradient = this._stats.msPanel.gradient = gradient;
2339
-
2340
- if ( this._stats.gpuPanel ) {
2341
-
2342
- this._stats.gpuPanel.fg = foregroundColor;
2343
- this._stats.gpuPanel.bg = backgroundColor;
2344
- this._stats.gpuPanel.gradient = gradient;
2345
-
2346
- }
2347
-
2348
- this._stats.dom.style.display = '';
2349
-
2350
- }
2351
-
2352
- _setupInteractionListeners() {
2353
-
2354
- if ( ! this._interactionManager ) return;
2355
-
2356
- this._interactionManager.addEventListener( 'objectSelected', ( event ) => {
2357
-
2358
- this.selectObject( event.object );
2359
- this.refreshFrame();
2360
- this.dispatchEvent( { type: 'objectSelected', object: event.object, uuid: event.uuid } );
2361
-
2362
- } );
2363
-
2364
- this._interactionManager.addEventListener( 'objectDeselected', ( event ) => {
2365
-
2366
- this.selectObject( null );
2367
- this.refreshFrame();
2368
- this.dispatchEvent( { type: 'objectDeselected', object: event.object, uuid: event.uuid } );
2369
-
2370
- } );
2371
-
2372
- this._interactionManager.addEventListener( 'selectModeChanged', ( event ) => {
2373
-
2374
- this.dispatchEvent( { type: EngineEvents.SELECT_MODE_CHANGED, enabled: event.enabled } );
2375
-
2376
- } );
2377
-
2378
- this._interactionManager.addEventListener( 'objectDoubleClicked', ( event ) => {
2379
-
2380
- this.selectObject( event.object );
2381
- this.refreshFrame();
2382
- this.dispatchEvent( { type: EngineEvents.OBJECT_DOUBLE_CLICKED, object: event.object, uuid: event.uuid } );
2383
-
2384
- } );
2385
-
2386
- this._interactionManager.addEventListener( 'focusChanged', ( event ) => {
2387
-
2388
- this.settings.set( 'focusDistance', event.worldDistance );
2389
- this.dispatchEvent( { type: 'focusChanged', distance: event.distance } );
2390
-
2391
- } );
2392
-
2393
- this._interactionManager.addEventListener( 'focusModeChanged', ( event ) => {
2394
-
2395
- if ( ! event.enabled && this._controls ) this._controls.enabled = true;
2396
-
2397
- } );
2398
-
2399
- this._interactionManager.addEventListener( 'afPointPlaced', ( event ) => {
2400
-
2401
- this.cameraManager.setAFScreenPoint( event.point.x, event.point.y );
2402
- if ( this._controls ) this._controls.enabled = true;
2403
- this.dispatchEvent( { type: EngineEvents.AF_POINT_PLACED, point: event.point } );
2404
-
2405
- } );
1301
+ this._stats = createStats( this.renderer, container );
2406
1302
 
2407
1303
  }
2408
1304
 
@@ -2426,91 +1322,30 @@ export class PathTracerApp extends EventDispatcher {
2426
1322
 
2427
1323
  this.scene.updateMatrixWorld();
2428
1324
  this.overlayManager?.render();
2429
- this._transformManager?.render( this.renderer );
1325
+ this.transformManager?.render( this.renderer );
2430
1326
 
2431
1327
  }
2432
1328
 
2433
1329
  _setupOverlayManager() {
2434
1330
 
2435
- this.overlayManager = new OverlayManager( this.renderer, this._camera );
2436
- this.overlayManager.setHelperScene( this._sceneHelpers );
2437
-
2438
- // ── Tile helper (shared across path tracer, OIDN, upscaler) ──
2439
- const tileHelper = new TileHelper();
2440
- this.overlayManager.register( 'tiles', tileHelper );
2441
-
2442
- // Sync render size
2443
- tileHelper.setRenderSize( this._lastRenderWidth || 1, this._lastRenderHeight || 1 );
2444
- this.addEventListener( 'resolution_changed', ( e ) => {
2445
-
2446
- tileHelper.setRenderSize( e.width, e.height );
2447
-
2448
- } );
2449
-
2450
- // ── Path tracer tile events ──
2451
- this.pipeline.eventBus.on( 'tile:changed', ( e ) => {
2452
-
2453
- if ( e.renderMode === 1 && e.tileBounds ) {
2454
-
2455
- tileHelper.setActiveTile( e.tileBounds );
2456
- tileHelper.show();
2457
-
2458
- }
2459
-
1331
+ this.overlayManager = new OverlayManager( this.renderer, this.cameraManager.camera );
1332
+ this.overlayManager.setupDefaultHelpers( {
1333
+ helperScene: this._sceneHelpers,
1334
+ meshScene: this.meshScene,
1335
+ pipeline: this.pipeline,
1336
+ denoisingManager: this.denoisingManager,
1337
+ app: this,
1338
+ renderWidth: this.denoisingManager?._lastRenderWidth || this.canvas.clientWidth || 1,
1339
+ renderHeight: this.denoisingManager?._lastRenderHeight || this.canvas.clientHeight || 1,
2460
1340
  } );
2461
1341
 
2462
- this.pipeline.eventBus.on( 'pipeline:reset', () => tileHelper.hide() );
2463
- this.addEventListener( EngineEvents.RENDER_COMPLETE, () => tileHelper.hide() );
2464
-
2465
- // ── OIDN denoiser tile events ──
2466
- this._setupDenoiserTileHelper( tileHelper );
2467
-
2468
- // ── Outline helper (renders at display resolution, not render resolution) ──
2469
- const outlineHelper = new OutlineHelper( this.renderer, this.meshScene, this._camera );
2470
- this.overlayManager.register( 'outline', outlineHelper );
2471
-
2472
- }
2473
-
2474
- _setupDenoiserTileHelper( tileHelper ) {
2475
-
2476
- // OIDN/upscaler tile events fire while the animation loop is stopped
2477
- // (render completed → stopAnimation → async denoise). We must manually
2478
- // trigger HUD redraws since overlayManager.render() isn't being called.
2479
- const sources = [ this.denoisingManager?.denoiser, this.denoisingManager?.upscaler ];
2480
-
2481
- for ( const source of sources ) {
2482
-
2483
- if ( ! source ) continue;
2484
-
2485
- source.addEventListener( 'tileProgress', ( e ) => {
2486
-
2487
- if ( e.tile ) {
2488
-
2489
- tileHelper.setRenderSize( e.imageWidth, e.imageHeight );
2490
- tileHelper.setActiveTile( e.tile );
2491
- tileHelper.show();
2492
- this.overlayManager?.refreshHUD();
2493
-
2494
- }
2495
-
2496
- } );
2497
-
2498
- source.addEventListener( 'end', () => {
2499
-
2500
- tileHelper.hide();
2501
- this.overlayManager?.refreshHUD();
2502
-
2503
- } );
2504
-
2505
- }
2506
-
2507
1342
  }
2508
1343
 
2509
1344
 
2510
1345
  _syncControlsAfterLoad() {
2511
1346
 
2512
- this._controls.saveState();
2513
- this._controls.update();
1347
+ this.cameraManager.controls.saveState();
1348
+ this.cameraManager.controls.update();
2514
1349
 
2515
1350
  }
2516
1351