rayzee 5.11.0 → 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 (41) hide show
  1. package/README.md +81 -24
  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 +1233 -1803
  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 +54 -65
  15. package/src/Processor/AssetLoader.js +5 -2
  16. package/src/Processor/Workers/AIUpscalerWorker.js +21 -6
  17. package/src/Stages/ASVGF.js +6 -27
  18. package/src/Stages/AdaptiveSampling.js +10 -26
  19. package/src/Stages/PathTracer.js +4 -5
  20. package/src/TSL/BVHTraversal.js +2 -18
  21. package/src/TSL/Clearcoat.js +1 -2
  22. package/src/TSL/Common.js +0 -13
  23. package/src/TSL/EmissiveSampling.js +0 -16
  24. package/src/TSL/Environment.js +0 -7
  25. package/src/TSL/LightsDirect.js +3 -379
  26. package/src/TSL/LightsSampling.js +0 -171
  27. package/src/TSL/MaterialEvaluation.js +3 -103
  28. package/src/TSL/MaterialProperties.js +1 -56
  29. package/src/TSL/MaterialSampling.js +2 -284
  30. package/src/TSL/MaterialTransmission.js +0 -93
  31. package/src/TSL/Random.js +0 -23
  32. package/src/TSL/Struct.js +0 -21
  33. package/src/TSL/TextureSampling.js +0 -69
  34. package/src/index.js +5 -2
  35. package/src/managers/DenoisingManager.js +13 -5
  36. package/src/managers/VideoRenderManager.js +4 -4
  37. package/dist/assets/AIUpscalerWorker-D58dcMrY.js +0 -2
  38. package/dist/assets/AIUpscalerWorker-D58dcMrY.js.map +0 -1
  39. package/src/Processor/createRenderTargetHelper.js +0 -521
  40. package/src/TSL/RayIntersection.js +0 -162
  41. 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.11.0",
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,11 +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
66
  * @param {HTMLElement} [options.container] - Single DOM parent the engine mounts all auxiliary
69
- * elements into (HUD overlay, denoiser canvas, stats). Defaults to `canvas.parentNode`.
70
- * @param {HTMLElement} [options.statsContainer] - Override mount target for the stats panel only.
71
- * Defaults to `options.container`.
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.
72
71
  */
73
72
  constructor( canvas, options = {} ) {
74
73
 
@@ -88,9 +87,7 @@ export class PathTracerApp extends EventDispatcher {
88
87
 
89
88
  this.canvas = canvas;
90
89
  this._autoResize = options.autoResize !== false;
91
- this._showStats = options.showStats !== false;
92
90
  this._container = options.container || null;
93
- this._statsContainer = options.statsContainer || null;
94
91
 
95
92
  // ── Settings (single source of truth for all render parameters) ──
96
93
  this.settings = new RenderSettings( DEFAULT_STATE );
@@ -214,8 +211,6 @@ export class PathTracerApp extends EventDispatcher {
214
211
  this.stages.pathTracer.materialData.setMaterialData( new Float32Array( 16 ) );
215
212
  this.stages.pathTracer.setupMaterial();
216
213
 
217
- if ( this._showStats ) this._initStats();
218
-
219
214
  this.isInitialized = true;
220
215
  console.log( 'WebGPU Path Tracer App initialized' );
221
216
 
@@ -232,7 +227,7 @@ export class PathTracerApp extends EventDispatcher {
232
227
 
233
228
  if ( this._loadingInProgress || this._sdf?.isProcessing ) {
234
229
 
235
- this._stats?.update();
230
+ this.dispatchEvent( { type: EngineEvents.FRAME } );
236
231
  return;
237
232
 
238
233
  }
@@ -334,7 +329,7 @@ export class PathTracerApp extends EventDispatcher {
334
329
  }
335
330
 
336
331
  this._renderHelperOverlay();
337
- this._stats?.update();
332
+ this.dispatchEvent( { type: EngineEvents.FRAME } );
338
333
 
339
334
  this.renderer.resolveTimestampsAsync?.( TimestampQuery.RENDER );
340
335
  this.renderer.resolveTimestampsAsync?.( TimestampQuery.COMPUTE );
@@ -367,7 +362,6 @@ export class PathTracerApp extends EventDispatcher {
367
362
 
368
363
  this._paused = true;
369
364
  this.stopAnimation();
370
- if ( this._stats ) this._stats.dom.style.display = 'none';
371
365
 
372
366
  }
373
367
 
@@ -376,7 +370,6 @@ export class PathTracerApp extends EventDispatcher {
376
370
 
377
371
  this._paused = false;
378
372
  if ( ! this.animationManagerId ) this.animate();
379
- if ( this._stats ) this._stats.dom.style.display = '';
380
373
 
381
374
  }
382
375
 
@@ -517,13 +510,6 @@ export class PathTracerApp extends EventDispatcher {
517
510
  if ( this.renderer ) this.renderer._canvasTarget = null;
518
511
  this.renderer = null;
519
512
 
520
- if ( this._stats ) {
521
-
522
- this._stats.dom.remove();
523
- this._stats = null;
524
-
525
- }
526
-
527
513
  this.stages = {};
528
514
  this.isInitialized = false;
529
515
 
@@ -914,26 +900,16 @@ export class PathTracerApp extends EventDispatcher {
914
900
  // ═══════════════════════════════════════════════════════════════
915
901
 
916
902
  /**
917
- * Configures the engine for a specific rendering mode.
918
- * @param {string} mode - 'preview' | 'final-render' | 'results'
903
+ * Configures the engine for a specific rendering quality tier.
904
+ * @param {'interactive' | 'production'} mode
919
905
  * @param {Object} [options]
920
906
  */
921
907
  configureForMode( mode, options = {} ) {
922
908
 
923
- if ( mode === 'results' ) {
924
-
925
- this.pauseRendering = true;
926
- this.cameraManager.controls.enabled = false;
927
- this.renderer?.domElement && ( this.renderer.domElement.style.display = 'none' );
928
- this.denoisingManager?.denoiser?.output && ( this.denoisingManager.denoiser.output.style.display = 'none' );
929
- return;
930
-
931
- }
932
-
933
- const isFinal = mode === 'final-render';
934
- const config = isFinal ? FINAL_RENDER_CONFIG : PREVIEW_RENDER_CONFIG;
909
+ const isProduction = mode === 'production';
910
+ const config = isProduction ? PRODUCTION_RENDER_CONFIG : INTERACTIVE_RENDER_CONFIG;
935
911
 
936
- this.cameraManager.controls.enabled = ! isFinal;
912
+ this.cameraManager.controls.enabled = ! isProduction;
937
913
 
938
914
  // Batch uniform updates via settings
939
915
  this.settings.setMany( {
@@ -974,9 +950,6 @@ export class PathTracerApp extends EventDispatcher {
974
950
 
975
951
  }
976
952
 
977
- this.renderer?.domElement && ( this.renderer.domElement.style.display = 'block' );
978
- this.denoisingManager?.denoiser?.output && ( this.denoisingManager.denoiser.output.style.display = 'block' );
979
-
980
953
  this.needsReset = false;
981
954
  this.pauseRendering = false;
982
955
  this.reset();
@@ -1022,26 +995,21 @@ export class PathTracerApp extends EventDispatcher {
1022
995
  }
1023
996
 
1024
997
  /**
1025
- * 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>}
1026
1006
  */
1027
- screenshot() {
1007
+ screenshot( { type = 'image/png', quality } = {} ) {
1028
1008
 
1029
1009
  const canvas = this.getCanvas();
1030
- if ( ! canvas ) return;
1010
+ if ( ! canvas ) return Promise.resolve( null );
1031
1011
 
1032
- try {
1033
-
1034
- const data = canvas.toDataURL( 'image/png' );
1035
- const link = document.createElement( 'a' );
1036
- link.href = data;
1037
- link.download = 'screenshot.png';
1038
- link.click();
1039
-
1040
- } catch ( error ) {
1041
-
1042
- console.error( 'Screenshot failed:', error );
1043
-
1044
- }
1012
+ return new Promise( ( resolve ) => canvas.toBlob( resolve, type, quality ) );
1045
1013
 
1046
1014
  }
1047
1015
 
@@ -1150,6 +1118,34 @@ export class PathTracerApp extends EventDispatcher {
1150
1118
 
1151
1119
  }
1152
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
+
1153
1149
  /**
1154
1150
  * Updates a material's texture transform (offset, repeat, rotation).
1155
1151
  * @param {number} materialIndex
@@ -1541,9 +1537,9 @@ export class PathTracerApp extends EventDispatcher {
1541
1537
  stage.isComplete = false;
1542
1538
  this.completion.resumeFromPause();
1543
1539
 
1544
- this.canvas.style.opacity = '1';
1545
- const denoiserOutput = this.denoisingManager?.denoiser?.output;
1546
- 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 );
1547
1543
 
1548
1544
  this.dispatchEvent( { type: EngineEvents.RENDER_RESET } );
1549
1545
  this.wake();
@@ -1552,13 +1548,6 @@ export class PathTracerApp extends EventDispatcher {
1552
1548
 
1553
1549
  }
1554
1550
 
1555
- _initStats() {
1556
-
1557
- const container = this._statsContainer || this._container || this.canvas.parentElement || document.body;
1558
- this._stats = createStats( this.renderer, container );
1559
-
1560
- }
1561
-
1562
1551
  _setupAutoExposureListener() {
1563
1552
 
1564
1553
  if ( ! this.stages.autoExposure ) return;
@@ -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