rayzee 9.1.2 → 9.2.1

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 (49) hide show
  1. package/README.md +176 -16
  2. package/dist/{NeuralRetouch-BRquqfmZ.js → NeuralRetouch-CYgkQY8Z.js} +1 -1
  3. package/dist/{NeuralSuperRes-CbNvQjCw.js → NeuralSuperRes-PkgDLQcD.js} +72 -40
  4. package/dist/{ToneMapGPU-DpUZDMbQ.js → ToneMapGPU-B6XIe3DU.js} +102 -92
  5. package/dist/rayzee.es.js +6209 -5571
  6. package/dist/rayzee.umd.js +137 -126
  7. package/package.json +3 -2
  8. package/src/AssetConfig.js +10 -5
  9. package/src/EngineEvents.js +41 -1
  10. package/src/EngineIssues.js +3 -0
  11. package/src/Headless.js +23 -7
  12. package/src/HeadlessCanvas.js +104 -0
  13. package/src/Passes/OIDNDenoiser.js +29 -4
  14. package/src/Passes/OIDNTemporalHistory.js +7 -0
  15. package/src/PathTracerApp.js +511 -136
  16. package/src/Platform.js +76 -0
  17. package/src/Processor/AssetLoader.js +66 -4
  18. package/src/Processor/BVHBuilder.js +2 -1
  19. package/src/Processor/EquirectHDRInfo.js +2 -1
  20. package/src/Processor/HostMemory.js +14 -0
  21. package/src/Processor/PBRT/PBRTSceneBuilder.js +26 -0
  22. package/src/Processor/ParallelBVHBuilder.js +7 -6
  23. package/src/Processor/PlatformImageLoader.js +118 -0
  24. package/src/Processor/ResampleRGBA8.js +102 -0
  25. package/src/Processor/SceneProcessor.js +7 -6
  26. package/src/Processor/TextureCreator.js +162 -13
  27. package/src/Processor/TextureReadback.js +26 -10
  28. package/src/Processor/ToneMapGPU.js +45 -15
  29. package/src/Processor/Workers/PackWorker.js +28 -0
  30. package/src/Stages/Compositor.js +25 -6
  31. package/src/Stages/PathTracer.js +142 -25
  32. package/src/Stages/PathTracerStage.js +6 -96
  33. package/src/Storage/DownloadCache.js +30 -7
  34. package/src/TSL/DebugKernel.js +1 -1
  35. package/src/TSL/GenerateKernel.js +2 -5
  36. package/src/TSL/Random.js +3 -120
  37. package/src/TSL/ShadeKernel.js +3 -3
  38. package/src/index.js +2 -1
  39. package/src/managers/CameraManager.js +2 -2
  40. package/src/managers/DenoisingManager.js +76 -7
  41. package/src/managers/GoboManager.js +8 -1
  42. package/src/managers/IESManager.js +13 -2
  43. package/src/managers/InteractionManager.js +1 -1
  44. package/src/managers/LightManager.js +31 -0
  45. package/src/managers/OverlayManager.js +11 -1
  46. package/src/managers/VideoRenderManager.js +8 -0
  47. package/src/node/NodeWorker.js +144 -0
  48. package/src/node/index.js +9 -0
  49. package/src/node/nodePlatform.js +86 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "9.1.2",
3
+ "version": "9.2.1",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -9,7 +9,8 @@
9
9
  ".": {
10
10
  "import": "./dist/rayzee.es.js",
11
11
  "require": "./dist/rayzee.umd.js"
12
- }
12
+ },
13
+ "./node": "./src/node/index.js"
13
14
  },
14
15
  "files": [
15
16
  "dist",
@@ -5,7 +5,6 @@
5
5
  * Usage:
6
6
  * import { configureAssets } from 'rayzee';
7
7
  * configureAssets({
8
- * stbnScalarAtlas: '/assets/stbn_scalar_atlas.png',
9
8
  * dracoDecoderPath: '/draco/',
10
9
  * cacheNamespace: 'my-app',
11
10
  * });
@@ -14,10 +13,6 @@
14
13
  */
15
14
 
16
15
  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
16
  // onnxruntime-web (loaded lazily by AI upscaler worker via dynamic import).
22
17
  ortRuntimeUrl: 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.24.3/dist/ort.webgpu.bundle.min.mjs',
23
18
  ortWasmPaths: 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.24.3/dist/',
@@ -62,6 +57,8 @@ const config = {
62
57
  storage: 'auto',
63
58
  };
64
59
 
60
+ const configured = new Set();
61
+
65
62
  /**
66
63
  * Override asset URLs and cache namespace. Partial — only provided keys are replaced.
67
64
  * @param {Partial<typeof config>} overrides
@@ -70,6 +67,14 @@ export function configureAssets( overrides ) {
70
67
 
71
68
  if ( ! overrides ) return;
72
69
  Object.assign( config, overrides );
70
+ for ( const key of Object.keys( overrides ) ) configured.add( key );
71
+
72
+ }
73
+
74
+ /** Whether a host set this key, as opposed to it being the default. */
75
+ export function isAssetConfigured( key ) {
76
+
77
+ return configured.has( key );
73
78
 
74
79
  }
75
80
 
@@ -35,6 +35,9 @@ export const EngineEvents = {
35
35
  TRANSFORM_MODE_CHANGED: 'engine:transformModeChanged',
36
36
 
37
37
  // Camera
38
+ CAMERA_SWITCHED: 'engine:cameraSwitched',
39
+ CAMERAS_UPDATED: 'engine:camerasUpdated',
40
+ FOCUS_CHANGED: 'engine:focusChanged',
38
41
  AUTO_FOCUS_UPDATED: 'engine:autoFocusUpdated',
39
42
  ORTHO_HEIGHT_UPDATED: 'engine:orthoHeightUpdated',
40
43
  AUTO_EXPOSURE_UPDATED: 'engine:autoExposureUpdated',
@@ -63,9 +66,46 @@ export const EngineEvents = {
63
66
  // Degradation
64
67
  ISSUE: 'engine:issue',
65
68
 
66
- // Scene metadata
69
+ // Scene and assets
70
+ MODEL_LOADED: 'engine:modelLoaded',
71
+ OBJECT3D_LOADED: 'engine:object3dLoaded',
72
+ MODEL_ADDED: 'engine:modelAdded',
73
+ SCENE_OBJECT_REMOVED: 'engine:sceneObjectRemoved',
74
+ SCENE_UNLOADED: 'engine:sceneUnloaded',
75
+ SCENE_REBUILD: 'engine:sceneRebuild',
76
+ SCENE_SPILLED: 'engine:sceneSpilled',
67
77
  SCENE_METADATA_APPLIED: 'engine:sceneMetadataApplied',
78
+ ENVIRONMENT_LOADED: 'engine:environmentLoaded',
79
+ TEXTURES_REPROCESSED: 'engine:texturesReprocessed',
80
+
81
+ // Render size
82
+ RESOLUTION_CHANGED: 'engine:resolutionChanged',
83
+ RESERVED_RENDER_SIZE_CHANGED: 'engine:reservedRenderSizeChanged',
68
84
 
69
85
  // Storage (OPFS): usage or entries changed
70
86
  STORAGE_CHANGED: 'engine:storageChanged',
71
87
  };
88
+
89
+ /**
90
+ * The names these events had before they moved under `engine:`. The app dispatches each under both
91
+ * until the next major.
92
+ */
93
+ export const LEGACY_EVENT_NAMES = Object.freeze( {
94
+ [ EngineEvents.RENDER_COMPLETE ]: 'RenderComplete',
95
+ [ EngineEvents.RENDER_RESET ]: 'RenderReset',
96
+ [ EngineEvents.CAMERA_SWITCHED ]: 'CameraSwitched',
97
+ [ EngineEvents.CAMERAS_UPDATED ]: 'CamerasUpdated',
98
+ [ EngineEvents.FOCUS_CHANGED ]: 'focusChanged',
99
+ [ EngineEvents.MODEL_LOADED ]: 'ModelLoaded',
100
+ [ EngineEvents.OBJECT3D_LOADED ]: 'Object3DLoaded',
101
+ [ EngineEvents.MODEL_ADDED ]: 'ModelAdded',
102
+ [ EngineEvents.SCENE_OBJECT_REMOVED ]: 'SceneObjectRemoved',
103
+ [ EngineEvents.SCENE_UNLOADED ]: 'SceneUnloaded',
104
+ [ EngineEvents.SCENE_REBUILD ]: 'SceneRebuild',
105
+ [ EngineEvents.SCENE_SPILLED ]: 'SceneSpilled',
106
+ [ EngineEvents.SCENE_METADATA_APPLIED ]: 'SceneMetadataApplied',
107
+ [ EngineEvents.ENVIRONMENT_LOADED ]: 'EnvironmentLoaded',
108
+ [ EngineEvents.TEXTURES_REPROCESSED ]: 'TexturesReprocessed',
109
+ [ EngineEvents.RESOLUTION_CHANGED ]: 'resolution_changed',
110
+ [ EngineEvents.RESERVED_RENDER_SIZE_CHANGED ]: 'reserved_render_size_changed',
111
+ } );
@@ -30,6 +30,9 @@ export const ISSUE_CODES = Object.freeze( {
30
30
  STORAGE_READ_FAILED: 'storage.read_failed',
31
31
  STORAGE_ENTRY_CORRUPT: 'storage.entry_corrupt',
32
32
  STORAGE_CACHE_MISMATCH: 'storage.cache_mismatch',
33
+ DENOISER_UNAVAILABLE: 'denoiser.unavailable',
34
+ OUTPUT_SOURCE_FALLBACK: 'output.source_fallback',
35
+ LIGHT_PLACEHOLDER_SKIPPED: 'light.placeholder_skipped',
33
36
  } );
34
37
 
35
38
  /** Strict throws on ERROR only. */
package/src/Headless.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * strict, physical profile, deterministic — all reversible, none reversible by accident.
4
4
  *
5
5
  * @example
6
- * const shot = await renderHeadless( { canvas, model: url, width: 1920, height: 1080, samples: 256 } );
6
+ * const shot = await renderHeadless( { model: url, width: 1920, height: 1080, samples: 256 } );
7
7
  */
8
8
 
9
9
  import { PathTracerApp } from './PathTracerApp.js';
@@ -11,7 +11,9 @@ import { PathTracerApp } from './PathTracerApp.js';
11
11
  * One render, start to finish, disposing the app afterwards.
12
12
  *
13
13
  * @param {Object} options
14
- * @param {HTMLCanvasElement} options.canvas - needed for the WebGPU surface, not for output
14
+ * @param {HTMLCanvasElement} [options.canvas] - leave it out and the app runs headless, on a canvas of
15
+ * its own (see PathTracerApp's `headless` option); pass one only to also present to it
16
+ * @param {number} [options.hostMemoryGB] - see PathTracerApp; needed outside Chrome for renders above 2048
15
17
  * @param {string} [options.model] - URL to load before rendering
16
18
  * @param {number} [options.width=1920]
17
19
  * @param {number} [options.height=1080]
@@ -21,10 +23,12 @@ import { PathTracerApp } from './PathTracerApp.js';
21
23
  * @param {string} [options.profile='physical'] - see RENDER_PROFILES
22
24
  * @param {boolean} [options.deterministic=true]
23
25
  * @param {boolean} [options.allowEarlyRetire=false] - needs `deterministic: false` to be reachable
26
+ * @param {boolean} [options.denoise=false] - one final OIDN pass, read back instead of the raw accumulation
24
27
  * @param {Object} [options.settings] - applied after the model loads
25
28
  * @param {function(number): void} [options.onProgress] - running sample count
26
29
  * @returns {Promise<{data: Float32Array|Uint8ClampedArray, width: number, height: number,
27
- * colorSpace: string, samples: number, retiredBy: string, issues: Object[], adapter: Object}>}
30
+ * colorSpace: string, source: string, samples: number, retiredBy: string, issues: Object[], adapter: Object,
31
+ * provenance: Object}>} `provenance` is {@link PathTracerApp#getProvenance} at the moment of capture
28
32
  */
29
33
  export async function renderHeadless( options ) {
30
34
 
@@ -51,11 +55,23 @@ export async function captureHeadless( app, {
51
55
  samples = 64,
52
56
  colorSpace = 'srgb',
53
57
  allowEarlyRetire = false,
58
+ denoise = false,
54
59
  onProgress = undefined,
55
60
  } = {} ) {
56
61
 
62
+ // Before accumulating: OIDN reads the albedo and normal buffers, which are only written while it is
63
+ // on. Not setOIDNEnabled(), whose refresh would wake the loop; renderFrames resets anyway.
64
+ const dm = app.denoisingManager;
65
+ if ( denoise && dm && ! dm.finalDenoise ) {
66
+
67
+ dm.applyOIDNEnabled( true );
68
+ dm._syncGBufferStages();
69
+
70
+ }
71
+
57
72
  const accumulated = await app.renderFrames( samples, { onProgress, allowEarlyRetire } );
58
- const frame = await app.renderToBuffer( { colorSpace } );
73
+ if ( denoise ) await app.runFinalDenoise();
74
+ const frame = await app.renderToBuffer( { colorSpace, source: denoise ? 'display' : 'accumulation' } );
59
75
 
60
76
  return {
61
77
  ...frame,
@@ -63,6 +79,7 @@ export async function captureHeadless( app, {
63
79
  retiredBy: accumulated < samples ? 'converged' : 'count',
64
80
  issues: app.issues,
65
81
  adapter: app.adapterInfo,
82
+ provenance: app.getProvenance(),
66
83
  };
67
84
 
68
85
  }
@@ -82,12 +99,11 @@ export async function openHeadless( {
82
99
  deterministic = true,
83
100
  settings = null,
84
101
  storage = false,
102
+ hostMemoryGB = undefined,
85
103
  } = {} ) {
86
104
 
87
- if ( ! canvas ) throw new Error( 'openHeadless: a canvas is required' );
88
-
89
105
  // Off unless asked for: cached state from an earlier run must not change what a batch renders.
90
- const app = new PathTracerApp( canvas, { autoResize: false, strict, profile, storage } );
106
+ const app = new PathTracerApp( canvas ?? null, { autoResize: false, strict, profile, storage, hostMemoryGB } );
91
107
 
92
108
  try {
93
109
 
@@ -0,0 +1,104 @@
1
+ /**
2
+ * A canvas for a render nobody watches. three.js reads its size and asks it for a WebGPU context;
3
+ * the camera controls attach listeners to it. Here the context presents into a plain texture, and
4
+ * the listeners go nowhere, so no document, window or swapchain is needed. Read results with
5
+ * `renderToBuffer()`; there is no picture to capture from it.
6
+ */
7
+
8
+ const inert = {
9
+ addEventListener() {},
10
+ removeEventListener() {},
11
+ dispatchEvent() {
12
+
13
+ return true;
14
+
15
+ },
16
+ };
17
+
18
+ class HeadlessGPUContext {
19
+
20
+ constructor( canvas ) {
21
+
22
+ this.canvas = canvas;
23
+ this._configuration = null;
24
+ this._texture = null;
25
+
26
+ }
27
+
28
+ configure( configuration ) {
29
+
30
+ this.unconfigure();
31
+ this._configuration = configuration;
32
+
33
+ }
34
+
35
+ unconfigure() {
36
+
37
+ this._texture?.destroy();
38
+ this._texture = null;
39
+ this._configuration = null;
40
+
41
+ }
42
+
43
+ getConfiguration() {
44
+
45
+ return this._configuration;
46
+
47
+ }
48
+
49
+ getCurrentTexture() {
50
+
51
+ if ( ! this._configuration ) throw new Error( 'HeadlessGPUContext: getCurrentTexture() before configure()' );
52
+
53
+ const width = Math.max( 1, this.canvas.width );
54
+ const height = Math.max( 1, this.canvas.height );
55
+
56
+ if ( this._texture?.width !== width || this._texture?.height !== height ) {
57
+
58
+ const { device, format, usage } = this._configuration;
59
+ this._texture?.destroy();
60
+ this._texture = device.createTexture( { label: 'headless-canvas', size: [ width, height ], format, usage } );
61
+
62
+ }
63
+
64
+ return this._texture;
65
+
66
+ }
67
+
68
+ }
69
+
70
+ /**
71
+ * @param {number} [width=1]
72
+ * @param {number} [height=1]
73
+ */
74
+ export function createHeadlessCanvas( width = 1, height = 1 ) {
75
+
76
+ const ownerDocument = { ...inert, defaultView: null };
77
+
78
+ const canvas = {
79
+ ...inert,
80
+ isHeadlessCanvas: true,
81
+ width,
82
+ height,
83
+ clientWidth: 0,
84
+ clientHeight: 0,
85
+ style: {},
86
+ parentNode: null,
87
+ ownerDocument,
88
+ getRootNode: () => ownerDocument,
89
+ getBoundingClientRect() {
90
+
91
+ return { x: 0, y: 0, left: 0, top: 0, right: this.width, bottom: this.height, width: this.width, height: this.height };
92
+
93
+ },
94
+ setPointerCapture() {},
95
+ releasePointerCapture() {},
96
+ hasPointerCapture: () => false,
97
+ focus() {},
98
+ getContext: ( type ) => ( type === 'webgpu' ? context : null ),
99
+ };
100
+
101
+ const context = new HeadlessGPUContext( canvas );
102
+ return canvas;
103
+
104
+ }
@@ -12,7 +12,9 @@ async function getInitUNetFromBuffer() {
12
12
 
13
13
  if ( ! _initUNetFromBuffer ) {
14
14
 
15
- _initUNetFromBuffer = ( await import( 'oidn-web' ) ).initUNetFromBuffer;
15
+ // The bundle, not the package main: that is raw tsc output with extensionless imports,
16
+ // which Node's ESM resolver refuses.
17
+ _initUNetFromBuffer = ( await import( 'oidn-web/dist/oidn.js' ) ).initUNetFromBuffer;
16
18
 
17
19
  }
18
20
 
@@ -365,7 +367,7 @@ export class OIDNDenoiser extends EventDispatcher {
365
367
 
366
368
  }
367
369
 
368
- async _setupUNetDenoiser() {
370
+ _setupUNetDenoiser() {
369
371
 
370
372
  if ( this.state.isLoading ) {
371
373
 
@@ -373,10 +375,24 @@ export class OIDNDenoiser extends EventDispatcher {
373
375
  // nothing would ever fetch them. Reachable on every render now that the refreshes and
374
376
  // the finished image use different tiers. Flag it; the running load picks it up.
375
377
  this._reloadPending = true;
376
- return;
378
+ return Promise.resolve();
377
379
 
378
380
  }
379
381
 
382
+ this._loading = this._loadUNet();
383
+ return this._loading;
384
+
385
+ }
386
+
387
+ /** Resolves once no weights are loading, including a reload queued behind the current one. */
388
+ whenLoaded() {
389
+
390
+ return this.state.isLoading ? this._loading : Promise.resolve();
391
+
392
+ }
393
+
394
+ async _loadUNet() {
395
+
380
396
  do {
381
397
 
382
398
  this._reloadPending = false;
@@ -389,7 +405,7 @@ export class OIDNDenoiser extends EventDispatcher {
389
405
  } catch ( error ) {
390
406
 
391
407
  log.error( 'UNet weights failed to load:', error );
392
- this.dispatchEvent( { type: 'error', error: new Error( `Denoiser loading failed: ${error.message}` ) } );
408
+ this.dispatchEvent( { type: 'error', error: new Error( `Denoiser loading failed: ${error.message}` ), url: this._generateTzaUrl() } );
393
409
 
394
410
  } finally {
395
411
 
@@ -973,6 +989,15 @@ export class OIDNDenoiser extends EventDispatcher {
973
989
 
974
990
  }
975
991
 
992
+ /** Bytes of the GPU memory this pass allocates itself; oidn-web's network is not among them. */
993
+ gpuBytes() {
994
+
995
+ const { color, albedo, normal } = this._gpuInputBuffers;
996
+ const out = this._outGPUTexture ? this._outTexSize.width * this._outTexSize.height * 8 : 0; // rgba16float
997
+ return ( color?.size ?? 0 ) + ( albedo?.size ?? 0 ) + ( normal?.size ?? 0 ) + ( this._gpuInputPadBuffer?.size ?? 0 ) + out;
998
+
999
+ }
1000
+
976
1001
  /**
977
1002
  * Makes sure the output picture exists at the render size. A resize makes a new one rather than
978
1003
  * resizing in place: three.js caches the card-side handle against the wrapper, so the wrapper
@@ -423,6 +423,13 @@ export class OIDNTemporalHistory {
423
423
 
424
424
  }
425
425
 
426
+ gpuBytes() {
427
+
428
+ if ( ! this._sets ) return 0;
429
+ return this._sets.length * 3 * this.width * this.height * 16 + this._picked.size + this._copies.size;
430
+
431
+ }
432
+
426
433
  _ensure( width, height ) {
427
434
 
428
435
  if ( ! width || ! height ) return false;