rayzee 5.10.2 → 6.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.
Files changed (43) hide show
  1. package/README.md +82 -22
  2. package/dist/assets/AIUpscalerWorker-AXN-lKWN.js +2 -0
  3. package/dist/assets/AIUpscalerWorker-AXN-lKWN.js.map +1 -0
  4. package/dist/rayzee.es.js +1299 -1843
  5. package/dist/rayzee.es.js.map +1 -1
  6. package/dist/rayzee.umd.js +50 -74
  7. package/dist/rayzee.umd.js.map +1 -1
  8. package/package.json +1 -4
  9. package/src/AssetConfig.js +56 -0
  10. package/src/EngineDefaults.js +5 -3
  11. package/src/EngineEvents.js +1 -0
  12. package/src/Passes/AIUpscaler.js +44 -22
  13. package/src/Passes/OIDNDenoiser.js +4 -104
  14. package/src/PathTracerApp.js +59 -63
  15. package/src/Processor/AssetLoader.js +5 -2
  16. package/src/Processor/TextureCreator.js +42 -15
  17. package/src/Processor/Workers/AIUpscalerWorker.js +21 -6
  18. package/src/Stages/ASVGF.js +6 -27
  19. package/src/Stages/AdaptiveSampling.js +10 -26
  20. package/src/Stages/PathTracer.js +4 -5
  21. package/src/TSL/BVHTraversal.js +2 -18
  22. package/src/TSL/Clearcoat.js +1 -2
  23. package/src/TSL/Common.js +0 -13
  24. package/src/TSL/EmissiveSampling.js +0 -16
  25. package/src/TSL/Environment.js +0 -7
  26. package/src/TSL/LightsDirect.js +3 -379
  27. package/src/TSL/LightsSampling.js +0 -171
  28. package/src/TSL/MaterialEvaluation.js +3 -103
  29. package/src/TSL/MaterialProperties.js +1 -56
  30. package/src/TSL/MaterialSampling.js +2 -284
  31. package/src/TSL/MaterialTransmission.js +0 -93
  32. package/src/TSL/Random.js +0 -23
  33. package/src/TSL/Struct.js +0 -21
  34. package/src/TSL/TextureSampling.js +0 -69
  35. package/src/index.js +5 -2
  36. package/src/managers/DenoisingManager.js +13 -5
  37. package/src/managers/OverlayManager.js +14 -2
  38. package/src/managers/VideoRenderManager.js +4 -4
  39. package/dist/assets/AIUpscalerWorker-D58dcMrY.js +0 -2
  40. package/dist/assets/AIUpscalerWorker-D58dcMrY.js.map +0 -1
  41. package/src/Processor/createRenderTargetHelper.js +0 -521
  42. package/src/TSL/RayIntersection.js +0 -162
  43. package/src/managers/helpers/StatsHelper.js +0 -45
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "5.10.2",
3
+ "version": "6.0.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -36,9 +36,6 @@
36
36
  "peerDependencies": {
37
37
  "three": ">=0.184.0"
38
38
  },
39
- "dependencies": {
40
- "stats-gl": "^4.0.2"
41
- },
42
39
  "optionalDependencies": {
43
40
  "oidn-web": ">=0.3.0",
44
41
  "onnxruntime-web": ">=1.0.0"
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Engine asset configuration. CDN URLs and cache namespaces are configurable so
3
+ * downstream consumers aren't pinned to the upstream Rayzee deployment's defaults.
4
+ *
5
+ * Usage:
6
+ * import { configureAssets } from 'rayzee';
7
+ * configureAssets({
8
+ * stbnScalarAtlas: '/assets/stbn_scalar_atlas.png',
9
+ * dracoDecoderPath: '/draco/',
10
+ * cacheNamespace: 'my-app',
11
+ * });
12
+ *
13
+ * Call before constructing PathTracerApp. Per-key partial overrides are supported.
14
+ */
15
+
16
+ const config = {
17
+ // STBN blue-noise atlases (NVIDIA-RTX/STBN). Decoded as Float32 textures.
18
+ stbnScalarAtlas: 'https://assets.rayzee.atulmourya.com/noise/stbn_scalar_atlas.png',
19
+ stbnVec2Atlas: 'https://assets.rayzee.atulmourya.com/noise/stbn_vec2_atlas.png',
20
+
21
+ // onnxruntime-web (loaded lazily by AI upscaler worker via dynamic import).
22
+ ortRuntimeUrl: 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.24.3/dist/ort.webgpu.bundle.min.mjs',
23
+ ortWasmPaths: 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.24.3/dist/',
24
+
25
+ // Draco / KTX2 decoder paths for GLTFLoader.
26
+ dracoDecoderPath: 'https://www.gstatic.com/draco/v1/decoders/',
27
+ ktx2TranscoderPath: 'https://cdn.jsdelivr.net/npm/three@0.183.2/examples/jsm/libs/basis/',
28
+
29
+ // OIDN denoiser model weights (oidn-web tza files).
30
+ oidnWeightsBaseUrl: 'https://cdn.jsdelivr.net/npm/denoiser/tzas/',
31
+
32
+ // AI upscaler ONNX model base URL. Quality presets resolve relative paths against this.
33
+ upscalerModelBaseUrl: 'https://huggingface.co/notaneimu/onnx-image-models/resolve/main/',
34
+
35
+ // Prefix used when the engine writes to client-side stores (IndexedDB, etc).
36
+ // Set to a unique value to avoid collisions when multiple apps embed the engine on the same origin.
37
+ cacheNamespace: 'rayzee',
38
+ };
39
+
40
+ /**
41
+ * Override asset URLs and cache namespace. Partial — only provided keys are replaced.
42
+ * @param {Partial<typeof config>} overrides
43
+ */
44
+ export function configureAssets( overrides ) {
45
+
46
+ if ( ! overrides ) return;
47
+ Object.assign( config, overrides );
48
+
49
+ }
50
+
51
+ /** Returns a snapshot of current asset config. */
52
+ export function getAssetConfig() {
53
+
54
+ return { ...config };
55
+
56
+ }
@@ -436,15 +436,17 @@ export const TEXTURE_CONSTANTS = {
436
436
  // Default texture matrix for materials
437
437
  export const DEFAULT_TEXTURE_MATRIX = [ 0, 0, 1, 1, 0, 0, 0, 1 ];
438
438
 
439
- // Render mode configurations
440
- export const FINAL_RENDER_CONFIG = {
439
+ // Render quality configurations.
440
+ // 'interactive' low-sample, bounded bounces, no offline denoising, controls enabled.
441
+ // 'production' — high-sample, deep bounces, OIDN enabled, controls disabled.
442
+ export const PRODUCTION_RENDER_CONFIG = {
441
443
  maxSamples: 30, bounces: 20, transmissiveBounces: 8, samplesPerPixel: 1,
442
444
  renderMode: 1, enableAlphaShadows: true, tiles: 3, tilesHelper: true,
443
445
  enableOIDN: true, oidnQuality: 'balance',
444
446
  interactionModeEnabled: false,
445
447
  };
446
448
 
447
- export const PREVIEW_RENDER_CONFIG = {
449
+ export const INTERACTIVE_RENDER_CONFIG = {
448
450
  maxSamples: ENGINE_DEFAULTS.maxSamples, bounces: ENGINE_DEFAULTS.bounces,
449
451
  samplesPerPixel: ENGINE_DEFAULTS.samplesPerPixel, renderMode: ENGINE_DEFAULTS.renderMode, enableAlphaShadows: ENGINE_DEFAULTS.enableAlphaShadows,
450
452
  transmissiveBounces: ENGINE_DEFAULTS.transmissiveBounces,
@@ -7,6 +7,7 @@ export const EngineEvents = {
7
7
  // Render lifecycle
8
8
  RENDER_COMPLETE: 'engine:renderComplete',
9
9
  RENDER_RESET: 'engine:renderReset',
10
+ FRAME: 'engine:frame',
10
11
 
11
12
  // Denoiser
12
13
  DENOISING_START: 'engine:denoisingStart',
@@ -1,35 +1,53 @@
1
1
  import { EventDispatcher, ACESFilmicToneMapping } from 'three';
2
2
  import { TONE_MAP_FNS, SRGB_GAMMA, applySaturation } from '../Processor/ToneMapCPU.js';
3
3
  import { fetchAsWorker } from '../Processor/Workers/fetchAsWorker.js';
4
+ import { getAssetConfig } from '../AssetConfig.js';
4
5
  import AI_UPSCALER_WORKER_URL from '../Processor/Workers/AIUpscalerWorker.js?worker&url';
5
6
 
6
7
 
7
8
  // ─── Model Configuration ───────────────────────────────────────────────────────
9
+ // Quality presets reference relative paths against asset-config `upscalerModelBaseUrl`.
8
10
 
9
- const HF_BASE = 'https://huggingface.co/notaneimu/onnx-image-models/resolve/main/';
11
+ const QUALITY_PRESET_PATHS = {
12
+ fast: {
13
+ 2: '2x-spanx2-ch48.onnx',
14
+ 4: '4xNomos8k_span_otf_strong_fp32_opset17.onnx'
15
+ },
16
+ balanced: {
17
+ 2: '2xNomosUni_compact_otf_medium.onnx',
18
+ 4: 'RealESRGAN_x4plus.onnx'
19
+ },
20
+ quality: {
21
+ 2: '2x-realesrgan-x2plus.onnx',
22
+ 4: '4xNomos2_hq_mosr_fp32.onnx'
23
+ }
24
+ };
10
25
 
11
- const MODEL_CONFIG = {
26
+ function _resolveQualityPresets() {
27
+
28
+ const { upscalerModelBaseUrl } = getAssetConfig();
29
+ const out = {};
30
+ for ( const q in QUALITY_PRESET_PATHS ) {
31
+
32
+ out[ q ] = {};
33
+ for ( const s in QUALITY_PRESET_PATHS[ q ] ) {
34
+
35
+ out[ q ][ s ] = upscalerModelBaseUrl + QUALITY_PRESET_PATHS[ q ][ s ];
12
36
 
13
- // Quality presets: each has a 2x and 4x model (all NCHW, dynamic input dims)
14
- QUALITY_PRESETS: {
15
- fast: {
16
- // 1.6MB — SPAN
17
- 2: HF_BASE + '2x-spanx2-ch48.onnx',
18
- // 1.6MB — SPAN
19
- 4: HF_BASE + '4xNomos8k_span_otf_strong_fp32_opset17.onnx'
20
- },
21
- balanced: {
22
- // 2.4MB — SRVGGNetCompact
23
- 2: HF_BASE + '2xNomosUni_compact_otf_medium.onnx',
24
- // 4.9MB — SRVGGNetCompact
25
- 4: HF_BASE + 'RealESRGAN_x4plus.onnx'
26
- },
27
- quality: {
28
- // 67MB — RRDBNet
29
- 2: HF_BASE + '2x-realesrgan-x2plus.onnx',
30
- // 16.5MB — MoSR
31
- 4: HF_BASE + '4xNomos2_hq_mosr_fp32.onnx'
32
37
  }
38
+
39
+ }
40
+
41
+ return out;
42
+
43
+ }
44
+
45
+ const MODEL_CONFIG = {
46
+
47
+ get QUALITY_PRESETS() {
48
+
49
+ return _resolveQualityPresets();
50
+
33
51
  },
34
52
 
35
53
  // Larger tiles = fewer GPU dispatches = faster. 512 works on most GPUs with 4GB+ VRAM.
@@ -189,11 +207,15 @@ export class AIUpscaler extends EventDispatcher {
189
207
 
190
208
  };
191
209
 
210
+ const { ortRuntimeUrl, ortWasmPaths, cacheNamespace } = getAssetConfig();
192
211
  this._worker.addEventListener( 'message', handler );
193
212
  this._worker.postMessage( {
194
213
  type: 'load',
195
214
  url,
196
- sessionOptions: MODEL_CONFIG.SESSION_OPTIONS
215
+ sessionOptions: MODEL_CONFIG.SESSION_OPTIONS,
216
+ ortRuntimeUrl,
217
+ ortWasmPaths,
218
+ cacheNamespace,
197
219
  } );
198
220
 
199
221
  } );
@@ -42,14 +42,13 @@ function removeOidnTfjsBackend() {
42
42
 
43
43
  }
44
44
 
45
- import { createRenderTargetHelper } from '../Processor/createRenderTargetHelper.js';
46
45
  import { TONE_MAP_FNS, linearToSRGB, applySaturation } from '../Processor/ToneMapCPU.js';
46
+ import { getAssetConfig } from '../AssetConfig.js';
47
47
 
48
48
  /** Reusable RGB output buffer (avoids per-pixel allocation). */
49
49
  const _tmOut = new Float32Array( 3 );
50
50
 
51
51
  const MODEL_CONFIG = {
52
- BASE_URL: 'https://cdn.jsdelivr.net/npm/denoiser/tzas/',
53
52
  // clean-aux models — first-hit albedo/normal are deterministic per pixel
54
53
  QUALITY_MODELS: {
55
54
  fast: 'rt_hdr_alb_nrm_small',
@@ -82,7 +81,6 @@ export class OIDNDenoiser extends EventDispatcher {
82
81
  this.camera = camera;
83
82
  this.input = renderer.domElement;
84
83
  this.output = output;
85
- this.debugContainer = options.debugContainer || null;
86
84
  this.extractGBufferData = options.extractGBufferData || null;
87
85
  this.getMRTRenderTarget = options.getMRTRenderTarget || null;
88
86
 
@@ -143,11 +141,6 @@ export class OIDNDenoiser extends EventDispatcher {
143
141
  this.currentTZAUrl = null;
144
142
  this.unet = null;
145
143
 
146
- // For debug visualization
147
- this.debugHelpers = null;
148
- this._lastAlbedoTexture = null;
149
- this._lastNormalTexture = null;
150
-
151
144
  // Initialize asynchronously
152
145
  this._initialize().catch( error => {
153
146
 
@@ -163,7 +156,6 @@ export class OIDNDenoiser extends EventDispatcher {
163
156
  try {
164
157
 
165
158
  this._setupCanvas();
166
- this._initDebugVisualization();
167
159
  await this._setupUNetDenoiser();
168
160
 
169
161
  } catch ( error ) {
@@ -174,14 +166,6 @@ export class OIDNDenoiser extends EventDispatcher {
174
166
 
175
167
  }
176
168
 
177
- _initDebugVisualization() {
178
-
179
- // Note: Debug helpers will be created lazily when MRT textures are available
180
- // This avoids creating helpers without proper texture references
181
- this.debugHelpers = null;
182
-
183
- }
184
-
185
169
  _setupCanvas() {
186
170
 
187
171
  if ( ! this.output.getContext ) {
@@ -277,9 +261,10 @@ export class OIDNDenoiser extends EventDispatcher {
277
261
 
278
262
  _generateTzaUrl() {
279
263
 
280
- const { BASE_URL, QUALITY_MODELS } = MODEL_CONFIG;
264
+ const { oidnWeightsBaseUrl } = getAssetConfig();
265
+ const { QUALITY_MODELS } = MODEL_CONFIG;
281
266
  const modelName = QUALITY_MODELS[ this.quality ] || QUALITY_MODELS.balance;
282
- return `${BASE_URL}${modelName}.tza`;
267
+ return `${oidnWeightsBaseUrl}${modelName}.tza`;
283
268
 
284
269
  }
285
270
 
@@ -900,78 +885,6 @@ export class OIDNDenoiser extends EventDispatcher {
900
885
 
901
886
  }
902
887
 
903
- /**
904
- * Update debug visualization using MRT textures directly
905
- * @param {RenderTarget} mrtRenderTarget - The MRT render target containing albedo and normal
906
- */
907
- _updateDebugVisualization( mrtRenderTarget ) {
908
-
909
- if ( ! mrtRenderTarget?.textures || mrtRenderTarget.textures.length < 3 ) {
910
-
911
- return;
912
-
913
- }
914
-
915
- // Check if textures have changed (render target was recreated)
916
- const texturesChanged = this.debugHelpers &&
917
- ( this._lastAlbedoTexture !== mrtRenderTarget.textures[ 2 ] ||
918
- this._lastNormalTexture !== mrtRenderTarget.textures[ 1 ] );
919
-
920
- // Create or recreate helpers when textures change
921
- if ( ! this.debugHelpers || texturesChanged ) {
922
-
923
- // Dispose existing helpers if they exist
924
- if ( this.debugHelpers ) {
925
-
926
- this.debugHelpers.albedo?.dispose();
927
- this.debugHelpers.normal?.dispose();
928
- console.log( 'OIDNDenoiser: Recreating debug helpers due to texture change' );
929
-
930
- }
931
-
932
- // Pass full MRT render target with textureIndex for async readback
933
- this.debugHelpers = {
934
- albedo: createRenderTargetHelper( this.renderer, mrtRenderTarget, {
935
- width: 250,
936
- height: 250,
937
- position: 'bottom-right',
938
- theme: 'dark',
939
- title: 'OIDN Albedo',
940
- autoUpdate: false,
941
- textureIndex: 2
942
- } ),
943
- normal: createRenderTargetHelper( this.renderer, mrtRenderTarget, {
944
- width: 250,
945
- height: 250,
946
- position: 'bottom-left',
947
- theme: 'dark',
948
- title: 'OIDN Normal',
949
- autoUpdate: false,
950
- textureIndex: 1
951
- } )
952
- };
953
-
954
- // Store references to track texture changes
955
- this._lastAlbedoTexture = mrtRenderTarget.textures[ 2 ];
956
- this._lastNormalTexture = mrtRenderTarget.textures[ 1 ];
957
-
958
- // Add helpers to DOM
959
- const container = this.debugContainer || document.body;
960
- container.appendChild( this.debugHelpers.albedo );
961
- container.appendChild( this.debugHelpers.normal );
962
-
963
- // Hide by default (visibility state will be restored by calling code)
964
- this.debugHelpers.albedo.hide();
965
- this.debugHelpers.normal.hide();
966
-
967
- }
968
-
969
- // Update the displays
970
- this.debugHelpers.albedo.update();
971
- this.debugHelpers.normal.update();
972
-
973
- }
974
-
975
888
  _destroyPendingStagingBuffers() {
976
889
 
977
890
  for ( const buf of this._pendingStagingBuffers ) {
@@ -999,19 +912,6 @@ export class OIDNDenoiser extends EventDispatcher {
999
912
  removeOidnTfjsBackend();
1000
913
  this._destroyGPUInputBuffers();
1001
914
 
1002
- // Dispose debug helpers
1003
- if ( this.debugHelpers ) {
1004
-
1005
- this.debugHelpers.albedo?.dispose();
1006
- this.debugHelpers.normal?.dispose();
1007
- this.debugHelpers = null;
1008
-
1009
- }
1010
-
1011
- // Clear texture references
1012
- this._lastAlbedoTexture = null;
1013
- this._lastNormalTexture = null;
1014
-
1015
915
  // Clean up DOM
1016
916
  if ( this.output?.parentNode ) {
1017
917
 
@@ -5,7 +5,6 @@ import {
5
5
  } from 'three';
6
6
  import { RectAreaLightTexturesLib } from 'three/addons/lights/RectAreaLightTexturesLib.js';
7
7
  import { SceneHelpers } from './SceneHelpers.js';
8
- import { createStats } from './managers/helpers/StatsHelper.js';
9
8
  import { PathTracer } from './Stages/PathTracer.js';
10
9
  import { NormalDepth } from './Stages/NormalDepth.js';
11
10
  import { MotionVector } from './Stages/MotionVector.js';
@@ -19,7 +18,7 @@ import { SSRC } from './Stages/SSRC.js';
19
18
  import { Compositor } from './Stages/Compositor.js';
20
19
  import { RenderPipeline } from './Pipeline/RenderPipeline.js';
21
20
  import { CompletionTracker } from './Pipeline/CompletionTracker.js';
22
- import { ENGINE_DEFAULTS as DEFAULT_STATE, FINAL_RENDER_CONFIG, PREVIEW_RENDER_CONFIG } from './EngineDefaults.js';
21
+ import { ENGINE_DEFAULTS as DEFAULT_STATE, PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG } from './EngineDefaults.js';
23
22
  import { updateStats, updateLoading, resetLoading, setStatusCallback, getDisplaySamples, disposeObjectFromMemory } from './Processor/utils.js';
24
23
  import { BuildTimer } from './Processor/BuildTimer.js';
25
24
  import { InteractionManager } from './managers/InteractionManager.js';
@@ -64,8 +63,11 @@ export class PathTracerApp extends EventDispatcher {
64
63
  * @param {HTMLCanvasElement} canvas - Canvas element for rendering
65
64
  * @param {Object} [options] - Engine options
66
65
  * @param {boolean} [options.autoResize=true] - Automatically listen for window resize events
67
- * @param {boolean} [options.showStats=true] - Show the performance stats panel
68
- * @param {HTMLElement} [options.statsContainer] - DOM element to append the stats panel to (defaults to document.body)
66
+ * @param {HTMLElement} [options.container] - Single DOM parent the engine mounts all auxiliary
67
+ * elements into (HUD overlay, denoiser canvas). Defaults to `canvas.parentNode`.
68
+ *
69
+ * The engine dispatches `EngineEvents.FRAME` after each animate() iteration so hosts can
70
+ * tick external instrumentation (e.g. a stats panel) without coupling the engine to it.
69
71
  */
70
72
  constructor( canvas, options = {} ) {
71
73
 
@@ -85,8 +87,7 @@ export class PathTracerApp extends EventDispatcher {
85
87
 
86
88
  this.canvas = canvas;
87
89
  this._autoResize = options.autoResize !== false;
88
- this._showStats = options.showStats !== false;
89
- this._statsContainer = options.statsContainer || null;
90
+ this._container = options.container || null;
90
91
 
91
92
  // ── Settings (single source of truth for all render parameters) ──
92
93
  this.settings = new RenderSettings( DEFAULT_STATE );
@@ -210,8 +211,6 @@ export class PathTracerApp extends EventDispatcher {
210
211
  this.stages.pathTracer.materialData.setMaterialData( new Float32Array( 16 ) );
211
212
  this.stages.pathTracer.setupMaterial();
212
213
 
213
- if ( this._showStats ) this._initStats();
214
-
215
214
  this.isInitialized = true;
216
215
  console.log( 'WebGPU Path Tracer App initialized' );
217
216
 
@@ -228,7 +227,7 @@ export class PathTracerApp extends EventDispatcher {
228
227
 
229
228
  if ( this._loadingInProgress || this._sdf?.isProcessing ) {
230
229
 
231
- this._stats?.update();
230
+ this.dispatchEvent( { type: EngineEvents.FRAME } );
232
231
  return;
233
232
 
234
233
  }
@@ -330,7 +329,7 @@ export class PathTracerApp extends EventDispatcher {
330
329
  }
331
330
 
332
331
  this._renderHelperOverlay();
333
- this._stats?.update();
332
+ this.dispatchEvent( { type: EngineEvents.FRAME } );
334
333
 
335
334
  this.renderer.resolveTimestampsAsync?.( TimestampQuery.RENDER );
336
335
  this.renderer.resolveTimestampsAsync?.( TimestampQuery.COMPUTE );
@@ -363,7 +362,6 @@ export class PathTracerApp extends EventDispatcher {
363
362
 
364
363
  this._paused = true;
365
364
  this.stopAnimation();
366
- if ( this._stats ) this._stats.dom.style.display = 'none';
367
365
 
368
366
  }
369
367
 
@@ -372,7 +370,6 @@ export class PathTracerApp extends EventDispatcher {
372
370
 
373
371
  this._paused = false;
374
372
  if ( ! this.animationManagerId ) this.animate();
375
- if ( this._stats ) this._stats.dom.style.display = '';
376
373
 
377
374
  }
378
375
 
@@ -513,13 +510,6 @@ export class PathTracerApp extends EventDispatcher {
513
510
  if ( this.renderer ) this.renderer._canvasTarget = null;
514
511
  this.renderer = null;
515
512
 
516
- if ( this._stats ) {
517
-
518
- this._stats.dom.remove();
519
- this._stats = null;
520
-
521
- }
522
-
523
513
  this.stages = {};
524
514
  this.isInitialized = false;
525
515
 
@@ -910,26 +900,16 @@ export class PathTracerApp extends EventDispatcher {
910
900
  // ═══════════════════════════════════════════════════════════════
911
901
 
912
902
  /**
913
- * Configures the engine for a specific rendering mode.
914
- * @param {string} mode - 'preview' | 'final-render' | 'results'
903
+ * Configures the engine for a specific rendering quality tier.
904
+ * @param {'interactive' | 'production'} mode
915
905
  * @param {Object} [options]
916
906
  */
917
907
  configureForMode( mode, options = {} ) {
918
908
 
919
- if ( mode === 'results' ) {
920
-
921
- this.pauseRendering = true;
922
- this.cameraManager.controls.enabled = false;
923
- this.renderer?.domElement && ( this.renderer.domElement.style.display = 'none' );
924
- this.denoisingManager?.denoiser?.output && ( this.denoisingManager.denoiser.output.style.display = 'none' );
925
- return;
926
-
927
- }
928
-
929
- const isFinal = mode === 'final-render';
930
- const config = isFinal ? FINAL_RENDER_CONFIG : PREVIEW_RENDER_CONFIG;
909
+ const isProduction = mode === 'production';
910
+ const config = isProduction ? PRODUCTION_RENDER_CONFIG : INTERACTIVE_RENDER_CONFIG;
931
911
 
932
- this.cameraManager.controls.enabled = ! isFinal;
912
+ this.cameraManager.controls.enabled = ! isProduction;
933
913
 
934
914
  // Batch uniform updates via settings
935
915
  this.settings.setMany( {
@@ -970,9 +950,6 @@ export class PathTracerApp extends EventDispatcher {
970
950
 
971
951
  }
972
952
 
973
- this.renderer?.domElement && ( this.renderer.domElement.style.display = 'block' );
974
- this.denoisingManager?.denoiser?.output && ( this.denoisingManager.denoiser.output.style.display = 'block' );
975
-
976
953
  this.needsReset = false;
977
954
  this.pauseRendering = false;
978
955
  this.reset();
@@ -1018,26 +995,21 @@ export class PathTracerApp extends EventDispatcher {
1018
995
  }
1019
996
 
1020
997
  /**
1021
- * Downloads a PNG screenshot of the current render.
998
+ * Captures the current render as a Blob. Returns null if no canvas is
999
+ * available. The host is responsible for downloading or otherwise
1000
+ * consuming the result.
1001
+ *
1002
+ * @param {Object} [options]
1003
+ * @param {string} [options.type='image/png'] - MIME type for the encoded image
1004
+ * @param {number} [options.quality] - 0–1 quality hint for lossy formats
1005
+ * @returns {Promise<Blob|null>}
1022
1006
  */
1023
- screenshot() {
1007
+ screenshot( { type = 'image/png', quality } = {} ) {
1024
1008
 
1025
1009
  const canvas = this.getCanvas();
1026
- if ( ! canvas ) return;
1027
-
1028
- try {
1029
-
1030
- const data = canvas.toDataURL( 'image/png' );
1031
- const link = document.createElement( 'a' );
1032
- link.href = data;
1033
- link.download = 'screenshot.png';
1034
- link.click();
1035
-
1036
- } catch ( error ) {
1037
-
1038
- console.error( 'Screenshot failed:', error );
1010
+ if ( ! canvas ) return Promise.resolve( null );
1039
1011
 
1040
- }
1012
+ return new Promise( ( resolve ) => canvas.toBlob( resolve, type, quality ) );
1041
1013
 
1042
1014
  }
1043
1015
 
@@ -1146,6 +1118,34 @@ export class PathTracerApp extends EventDispatcher {
1146
1118
 
1147
1119
  }
1148
1120
 
1121
+ /**
1122
+ * The active mesh-bearing scene. Prefer this over reading `scene`/`meshScene`
1123
+ * directly — the engine may swap the underlying scene between rebuilds.
1124
+ * @returns {import('three').Scene}
1125
+ */
1126
+ getScene() {
1127
+
1128
+ return this.meshScene || this.scene;
1129
+
1130
+ }
1131
+
1132
+ // Sets when `visible` is a boolean; toggles when it's an updater (prev) => next.
1133
+ /**
1134
+ * @param {string} uuid
1135
+ * @param {boolean | ((prev: boolean) => boolean)} visible
1136
+ * @returns {boolean | null} new visibility, or null if the mesh wasn't found
1137
+ */
1138
+ setMeshVisibilityByUuid( uuid, visible ) {
1139
+
1140
+ const object = this.getScene()?.getObjectByProperty( 'uuid', uuid );
1141
+ if ( ! object ) return null;
1142
+ const next = typeof visible === 'function' ? !! visible( object.visible ) : !! visible;
1143
+ object.visible = next;
1144
+ this.updateAllMeshVisibility();
1145
+ return next;
1146
+
1147
+ }
1148
+
1149
1149
  /**
1150
1150
  * Updates a material's texture transform (offset, repeat, rotation).
1151
1151
  * @param {number} materialIndex
@@ -1537,9 +1537,9 @@ export class PathTracerApp extends EventDispatcher {
1537
1537
  stage.isComplete = false;
1538
1538
  this.completion.resumeFromPause();
1539
1539
 
1540
- this.canvas.style.opacity = '1';
1541
- const denoiserOutput = this.denoisingManager?.denoiser?.output;
1542
- if ( denoiserOutput ) denoiserOutput.style.display = 'none';
1540
+ // Restore live preview: abort() on the denoising manager already
1541
+ // handles canvas opacity, denoiser output visibility, and upscaler reset.
1542
+ this.denoisingManager?.abort( this.canvas );
1543
1543
 
1544
1544
  this.dispatchEvent( { type: EngineEvents.RENDER_RESET } );
1545
1545
  this.wake();
@@ -1548,13 +1548,6 @@ export class PathTracerApp extends EventDispatcher {
1548
1548
 
1549
1549
  }
1550
1550
 
1551
- _initStats() {
1552
-
1553
- const container = this._statsContainer || this.canvas.parentElement || document.body;
1554
- this._stats = createStats( this.renderer, container );
1555
-
1556
- }
1557
-
1558
1551
  _setupAutoExposureListener() {
1559
1552
 
1560
1553
  if ( ! this.stages.autoExposure ) return;
@@ -1592,6 +1585,9 @@ export class PathTracerApp extends EventDispatcher {
1592
1585
  renderHeight: this.denoisingManager?._lastRenderHeight || this.canvas.clientHeight || 1,
1593
1586
  } );
1594
1587
 
1588
+ this._container = this._container || this.canvas.parentNode || null;
1589
+ this.overlayManager.mount( this._container );
1590
+
1595
1591
  }
1596
1592
 
1597
1593
 
@@ -11,6 +11,7 @@ import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
11
11
  import { unzipSync, strFromU8 } from 'three/addons/libs/fflate.module.js';
12
12
  import { disposeObjectFromMemory, updateLoading } from './utils';
13
13
  import { BuildTimer } from './BuildTimer.js';
14
+ import { getAssetConfig } from '../AssetConfig.js';
14
15
 
15
16
  // Define supported file formats
16
17
  const SUPPORTED_FORMATS = {
@@ -739,12 +740,14 @@ export class AssetLoader extends EventDispatcher {
739
740
  // worker pools. Callers must invoke _disposeGLTFLoader() to terminate them.
740
741
  async createGLTFLoader() {
741
742
 
743
+ const { dracoDecoderPath, ktx2TranscoderPath } = getAssetConfig();
744
+
742
745
  const dracoLoader = new DRACOLoader();
743
746
  dracoLoader.setDecoderConfig( { type: 'js' } );
744
- dracoLoader.setDecoderPath( 'https://www.gstatic.com/draco/v1/decoders/' );
747
+ dracoLoader.setDecoderPath( dracoDecoderPath );
745
748
 
746
749
  const ktx2Loader = new KTX2Loader();
747
- ktx2Loader.setTranscoderPath( 'https://cdn.jsdelivr.net/npm/three@0.183.2/examples/jsm/libs/basis/' );
750
+ ktx2Loader.setTranscoderPath( ktx2TranscoderPath );
748
751
 
749
752
  if ( this.renderer ) {
750
753