rayzee 8.2.0 → 8.4.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.
@@ -0,0 +1,11 @@
1
+ //#region \0rolldown/runtime.js
2
+ var e = Object.defineProperty, t = (t, n) => {
3
+ let r = {};
4
+ for (var i in t) e(r, i, {
5
+ get: t[i],
6
+ enumerable: !0
7
+ });
8
+ return n || e(r, Symbol.toStringTag, { value: "Module" }), r;
9
+ };
10
+ //#endregion
11
+ export { t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "8.2.0",
3
+ "version": "8.4.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",
@@ -32,6 +32,13 @@ const config = {
32
32
  // AI upscaler ONNX model base URL. Quality presets resolve relative paths against this.
33
33
  upscalerModelBaseUrl: 'https://huggingface.co/notaneimu/onnx-image-models/resolve/main/',
34
34
 
35
+ // DLSS runtime bundle. A plain script the host serves, not a module: it installs
36
+ // `globalThis.DLSSRuntime`, so it cannot be imported and the host must publish it somewhere.
37
+ dlssRuntimeUrl: '/dlss/dlss-runtime.js',
38
+
39
+ // Base for the DLSS weight manifests the runtime fetches (`generated/...` beneath this).
40
+ dlssAssetBaseUrl: 'https://pub-6e048ff014374f11bef33d76b6c2b5ef.r2.dev/v1',
41
+
35
42
  // Prefix used when the engine writes to client-side stores (IndexedDB, etc).
36
43
  // Set to a unique value to avoid collisions when multiple apps embed the engine on the same origin.
37
44
  cacheNamespace: 'rayzee',
@@ -1079,7 +1079,11 @@ export const INTERACTIVE_RENDER_CONFIG = {
1079
1079
  renderMode: ENGINE_DEFAULTS.renderMode, enableAlphaShadows: ENGINE_DEFAULTS.enableAlphaShadows,
1080
1080
  transmissiveBounces: ENGINE_DEFAULTS.transmissiveBounces,
1081
1081
  maxSubsurfaceSteps: ENGINE_DEFAULTS.maxSubsurfaceSteps,
1082
- enableOIDN: false, oidnQuality: 'fast',
1082
+ // On, like production. The final pass costs one cheap denoise when the preview settles, and both
1083
+ // neural passes need a denoised frame to be worth running — on Monte-Carlo noise the upscaler
1084
+ // measured worse than a plain resize. ⚠️ `ENGINE_DEFAULTS.enableOIDN` stays false: the bench
1085
+ // renders against it and every quality golden would move.
1086
+ enableOIDN: true, oidnQuality: 'fast',
1083
1087
  interactionModeEnabled: true,
1084
1088
  useAdaptiveSampling: true, // idle refine stops early when converged; frozen during motion
1085
1089
  noiseThreshold: 0.1, // loose: preview wants a fast settle, not a clean one
@@ -2463,11 +2463,13 @@ export class PathTracerApp extends EventDispatcher {
2463
2463
 
2464
2464
  if ( ! this.renderer?.domElement ) return null;
2465
2465
 
2466
+ // Whatever is on the overlay is what the viewport shows, so it is also what a save must
2467
+ // write. Gating on `upscaler.enabled` instead missed the neural-rendering pass, which puts
2468
+ // a picture there without the ONNX upscaler being on at all — saves silently wrote the
2469
+ // un-enhanced render.
2466
2470
  const dm = this.denoisingManager;
2467
- const upscaled = dm?.upscaler?.enabled && dm?.upscalerCanvas
2468
- && dm.upscalerCanvas.style.display !== 'none';
2469
-
2470
- if ( upscaled ) return dm.upscalerCanvas;
2471
+ const overlay = dm?.upscalerCanvas;
2472
+ if ( overlay && overlay.style.display !== 'none' ) return overlay;
2471
2473
 
2472
2474
  // A presented WebGPU canvas only reads back what was drawn immediately before, so draw.
2473
2475
  // This also puts the denoised picture on it: the Compositor prefers it over the raw render.
@@ -146,6 +146,9 @@ export class SceneProcessor {
146
146
  geometryExtractionTime: 0,
147
147
  bvhBuildTime: 0,
148
148
  blasBuildTime: 0,
149
+ // Summed per-mesh build time reported by the workers. Divided by blasBuildTime it
150
+ // gives how many workers were actually busy — a health signal, not a cost.
151
+ blasWorkerTime: 0,
149
152
  tlasBuildTime: 0,
150
153
  bvhAssembleTime: 0,
151
154
  totalProcessingTime: 0
@@ -781,6 +784,7 @@ export class SceneProcessor {
781
784
  blasStats.failed += st.failedSplits ?? 0;
782
785
  blasStats.treeletsImproved += st.treeletsImproved ?? 0;
783
786
  blasStats.treeletsProcessed += st.treeletsProcessed ?? 0;
787
+ this.performanceMetrics.blasWorkerTime += st.totalBuildTime ?? 0;
784
788
 
785
789
  }
786
790
 
@@ -1456,6 +1460,9 @@ export class SceneProcessor {
1456
1460
  geometryExtractionTime: 0,
1457
1461
  bvhBuildTime: 0,
1458
1462
  blasBuildTime: 0,
1463
+ // Summed per-mesh build time reported by the workers. Divided by blasBuildTime it
1464
+ // gives how many workers were actually busy — a health signal, not a cost.
1465
+ blasWorkerTime: 0,
1459
1466
  tlasBuildTime: 0,
1460
1467
  bvhAssembleTime: 0,
1461
1468
  totalProcessingTime: 0
@@ -0,0 +1,317 @@
1
+ /**
2
+ * The engine's tone curve in WGSL, and a pass that turns a packed rgba16float buffer into RGBA
3
+ * bytes on the card.
4
+ *
5
+ * A mirror of `ToneMapCPU.js` — same curves, same order (exposure, saturation, curve, sRGB
6
+ * transfer), same rounding. It exists because reading half-floats back and converting them in
7
+ * JavaScript costs more than the neural upscale itself at production sizes. `bench:tonemap`
8
+ * compares the two implementations on a real device and fails on any drift.
9
+ *
10
+ * ⚠️ Rounding is deliberately bug-compatible. `toneMapToRGBA8` writes `srgb * 255 + 0.5` into a
11
+ * `Uint8ClampedArray`, which rounds again — so the CPU has always been half a level bright. WGSL's
12
+ * `round()` is round-half-to-even like the clamped array, so `round( srgb * 255 + 0.5 )` reproduces
13
+ * it exactly, including black staying at 0. Dropping the extra half would shift every image against
14
+ * the OIDN and Real-ESRGAN readbacks, which still go through the CPU function.
15
+ */
16
+
17
+ import {
18
+ NoToneMapping, LinearToneMapping, ReinhardToneMapping,
19
+ CineonToneMapping, ACESFilmicToneMapping, AgXToneMapping, NeutralToneMapping
20
+ } from 'three';
21
+
22
+ /**
23
+ * WGSL functions: `rayzee_tone_map( linearRGB, mode, exposure, saturation )` and
24
+ * `rayzee_linear_to_srgb( rgb )`, plus `rayzee_to_u8( channel )`.
25
+ *
26
+ * `mode` is the Three.js ToneMapping constant, interpolated in from the import above so the two
27
+ * sides cannot disagree about which number means which curve.
28
+ */
29
+ export const TONE_MAP_WGSL = /* wgsl */ `
30
+ const TM_NONE: u32 = ${NoToneMapping}u;
31
+ const TM_LINEAR: u32 = ${LinearToneMapping}u;
32
+ const TM_REINHARD: u32 = ${ReinhardToneMapping}u;
33
+ const TM_CINEON: u32 = ${CineonToneMapping}u;
34
+ const TM_ACES: u32 = ${ACESFilmicToneMapping}u;
35
+ const TM_AGX: u32 = ${AgXToneMapping}u;
36
+ const TM_NEUTRAL: u32 = ${NeutralToneMapping}u;
37
+
38
+ fn tm_reinhard( c: vec3<f32> ) -> vec3<f32> {
39
+ return clamp( c / ( c + vec3<f32>( 1.0 ) ), vec3<f32>( 0.0 ), vec3<f32>( 1.0 ) );
40
+ }
41
+
42
+ fn tm_cineon( c0: vec3<f32> ) -> vec3<f32> {
43
+ let c = max( c0 - vec3<f32>( 0.004 ), vec3<f32>( 0.0 ) );
44
+ let v = ( c * ( 6.2 * c + vec3<f32>( 0.5 ) ) ) / ( c * ( 6.2 * c + vec3<f32>( 1.7 ) ) + vec3<f32>( 0.06 ) );
45
+ return pow( max( v, vec3<f32>( 0.0 ) ), vec3<f32>( 2.2 ) );
46
+ }
47
+
48
+ fn tm_aces( c0: vec3<f32> ) -> vec3<f32> {
49
+ let c = c0 / 0.6;
50
+ let m_in = mat3x3<f32>(
51
+ vec3<f32>( 0.59719, 0.07600, 0.02840 ),
52
+ vec3<f32>( 0.35458, 0.90834, 0.13383 ),
53
+ vec3<f32>( 0.04823, 0.01566, 0.83777 ) );
54
+ let v = m_in * c;
55
+ let a = v * ( v + vec3<f32>( 0.0245786 ) ) - vec3<f32>( 0.000090537 );
56
+ let b = v * ( 0.983729 * v + vec3<f32>( 0.4329510 ) ) + vec3<f32>( 0.238081 );
57
+ let m_out = mat3x3<f32>(
58
+ vec3<f32>( 1.60475, -0.10208, -0.00327 ),
59
+ vec3<f32>( -0.53108, 1.10813, -0.07276 ),
60
+ vec3<f32>( -0.07367, -0.00605, 1.07602 ) );
61
+ return clamp( m_out * ( a / b ), vec3<f32>( 0.0 ), vec3<f32>( 1.0 ) );
62
+ }
63
+
64
+ fn tm_agx( c0: vec3<f32> ) -> vec3<f32> {
65
+ let m_in = mat3x3<f32>(
66
+ vec3<f32>( 0.6274, 0.0691, 0.0164 ),
67
+ vec3<f32>( 0.3293, 0.9195, 0.0880 ),
68
+ vec3<f32>( 0.0433, 0.0113, 0.8956 ) );
69
+ let m_agx = mat3x3<f32>(
70
+ vec3<f32>( 0.856627153315983, 0.137318972929847, 0.11189821299995 ),
71
+ vec3<f32>( 0.0951212405381588, 0.761241990602591, 0.0767994186031903 ),
72
+ vec3<f32>( 0.0482516061458583, 0.101439036467562, 0.811302368396859 ) );
73
+
74
+ var v = m_agx * ( m_in * c0 );
75
+
76
+ let minEv = -12.47393;
77
+ let maxEv = 4.026069;
78
+ v = clamp( ( log2( max( v, vec3<f32>( 1e-10 ) ) ) - vec3<f32>( minEv ) ) / ( maxEv - minEv ),
79
+ vec3<f32>( 0.0 ), vec3<f32>( 1.0 ) );
80
+
81
+ let x2 = v * v;
82
+ let x4 = x2 * x2;
83
+ v = 15.5 * x4 * x2 - 40.14 * x4 * v + 31.96 * x4 - 6.868 * x2 * v + 0.4298 * x2 + 0.1191 * v
84
+ - vec3<f32>( 0.00232 );
85
+
86
+ let m_out = mat3x3<f32>(
87
+ vec3<f32>( 1.1271005818144368, -0.1413297634984383, -0.14132976349843826 ),
88
+ vec3<f32>( -0.11060664309660323, 1.157823702216272, -0.11060664309660294 ),
89
+ vec3<f32>( -0.016493938717834573, -0.016493938717834257, 1.2519364065950405 ) );
90
+ let o = pow( max( m_out * v, vec3<f32>( 0.0 ) ), vec3<f32>( 2.2 ) );
91
+
92
+ let m_srgb = mat3x3<f32>(
93
+ vec3<f32>( 1.6605, -0.1246, -0.0182 ),
94
+ vec3<f32>( -0.5876, 1.1329, -0.1006 ),
95
+ vec3<f32>( -0.0728, -0.0083, 1.1187 ) );
96
+ return clamp( m_srgb * o, vec3<f32>( 0.0 ), vec3<f32>( 1.0 ) );
97
+ }
98
+
99
+ fn tm_neutral( c0: vec3<f32> ) -> vec3<f32> {
100
+ let startCompression = 0.8 - 0.04;
101
+ let desaturation = 0.15;
102
+
103
+ let x = min( c0.r, min( c0.g, c0.b ) );
104
+ var offset = 0.04;
105
+ if ( x < 0.08 ) { offset = x - 6.25 * x * x; }
106
+ var c = c0 - vec3<f32>( offset );
107
+
108
+ let peak = max( c.r, max( c.g, c.b ) );
109
+ if ( peak < startCompression ) { return c; }
110
+
111
+ let d = 1.0 - startCompression;
112
+ let newPeak = 1.0 - d * d / ( peak + d - startCompression );
113
+ c = c * ( newPeak / peak );
114
+ let gFactor = 1.0 - 1.0 / ( desaturation * ( peak - newPeak ) + 1.0 );
115
+ return mix( c, vec3<f32>( newPeak ), gFactor );
116
+ }
117
+
118
+ // Three.js clamps the fragment output with max(0) before tone mapping, so the curves never see a
119
+ // negative channel. The saturation grade drives channels below zero on much of a typical frame, and
120
+ // AgX/Neutral mix negatives across channels instead of clipping them.
121
+ fn rayzee_tone_curve( color: vec3<f32>, mode: u32 ) -> vec3<f32> {
122
+ let c = max( color, vec3<f32>( 0.0 ) );
123
+ if ( mode == TM_REINHARD ) { return tm_reinhard( c ); }
124
+ if ( mode == TM_CINEON ) { return tm_cineon( c ); }
125
+ if ( mode == TM_ACES ) { return tm_aces( c ); }
126
+ if ( mode == TM_AGX ) { return tm_agx( c ); }
127
+ if ( mode == TM_NEUTRAL ) { return tm_neutral( c ); }
128
+ return clamp( c, vec3<f32>( 0.0 ), vec3<f32>( 1.0 ) );
129
+ }
130
+
131
+ fn rayzee_tone_map( linearRGB: vec3<f32>, mode: u32, exposure: f32, saturation: f32 ) -> vec3<f32> {
132
+ // Three.js returns early for NoToneMapping without applying exposure, so a readback that applied
133
+ // it would paint brighter than the viewport it replaces.
134
+ var c = linearRGB * select( exposure, 1.0, mode == TM_NONE );
135
+ if ( saturation != 1.0 ) {
136
+ let luma = vec3<f32>( dot( c, vec3<f32>( 0.2126, 0.7152, 0.0722 ) ) );
137
+ c = luma + ( c - luma ) * saturation;
138
+ }
139
+ return rayzee_tone_curve( c, mode );
140
+ }
141
+
142
+ fn rayzee_linear_to_srgb( c: vec3<f32> ) -> vec3<f32> {
143
+ return select(
144
+ 1.055 * pow( max( c, vec3<f32>( 0.0 ) ), vec3<f32>( 1.0 / 2.4 ) ) - vec3<f32>( 0.055 ),
145
+ 12.92 * c,
146
+ c <= vec3<f32>( 0.0031308 ) );
147
+ }
148
+
149
+ fn rayzee_to_u8( srgb: vec3<f32> ) -> vec3<u32> {
150
+ return vec3<u32>( clamp( round( srgb * 255.0 + vec3<f32>( 0.5 ) ), vec3<f32>( 0.0 ), vec3<f32>( 255.0 ) ) );
151
+ }
152
+ `;
153
+
154
+ const PACKED_WGSL = /* wgsl */ `
155
+ struct Params {
156
+ width: u32,
157
+ height: u32,
158
+ mode: u32,
159
+ flipY: u32,
160
+ exposure: f32,
161
+ saturation: f32,
162
+ };
163
+
164
+ @group(0) @binding(0) var<storage, read> src: array<u32>;
165
+ @group(0) @binding(1) var<storage, read_write> dst: array<u32>;
166
+ @group(0) @binding(2) var<uniform> params: Params;
167
+
168
+ ${TONE_MAP_WGSL}
169
+
170
+ @compute @workgroup_size(8, 8)
171
+ fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
172
+ if ( gid.x >= params.width || gid.y >= params.height ) { return; }
173
+
174
+ var srcY = gid.y;
175
+ if ( params.flipY == 1u ) { srcY = params.height - 1u - gid.y; }
176
+
177
+ let a = ( srcY * params.width + gid.x ) * 2u;
178
+ let rg = unpack2x16float( src[ a ] );
179
+ let ba = unpack2x16float( src[ a + 1u ] );
180
+
181
+ let mapped = rayzee_tone_map( vec3<f32>( rg.x, rg.y, ba.x ), params.mode, params.exposure, params.saturation );
182
+ let b8 = rayzee_to_u8( rayzee_linear_to_srgb( mapped ) );
183
+ dst[ gid.y * params.width + gid.x ] = b8.x | ( b8.y << 8u ) | ( b8.z << 16u ) | ( 255u << 24u );
184
+ }
185
+ `;
186
+
187
+ /** Uniform blocks round up to 16 bytes; the struct above is 24. */
188
+ const PARAMS_BYTES = 32;
189
+
190
+ /**
191
+ * Tone-maps a tightly packed rgba16float buffer (two `u32` per pixel) into RGBA bytes.
192
+ *
193
+ * Bound to one device and one image size; `ensureSize()` reallocates when the size changes.
194
+ */
195
+ export class PackedToneMapper {
196
+
197
+ constructor( device, label = 'rayzee:tonemap' ) {
198
+
199
+ this.device = device;
200
+ this.label = label;
201
+ this.width = 0;
202
+ this.height = 0;
203
+ this._pipeline = null;
204
+ this._storage = null;
205
+ this._map = null;
206
+ this._params = null;
207
+ this._paramData = new ArrayBuffer( PARAMS_BYTES );
208
+ this._paramU32 = new Uint32Array( this._paramData );
209
+ this._paramF32 = new Float32Array( this._paramData );
210
+ this.disposed = false;
211
+
212
+ }
213
+
214
+ ensureSize( width, height ) {
215
+
216
+ if ( this.width === width && this.height === height && this._pipeline ) return;
217
+
218
+ this._releaseBuffers();
219
+
220
+ const bytes = width * height * 4;
221
+ this._storage = this.device.createBuffer( {
222
+ label: `${this.label}-out`,
223
+ size: bytes,
224
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
225
+ } );
226
+ this._map = this.device.createBuffer( {
227
+ label: `${this.label}-map`,
228
+ size: bytes,
229
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
230
+ } );
231
+ this._params = this.device.createBuffer( {
232
+ label: `${this.label}-params`,
233
+ size: PARAMS_BYTES,
234
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
235
+ } );
236
+
237
+ this._pipeline ??= this.device.createComputePipeline( {
238
+ label: this.label,
239
+ layout: 'auto',
240
+ compute: { module: this.device.createShaderModule( { label: this.label, code: PACKED_WGSL } ), entryPoint: 'main' },
241
+ } );
242
+
243
+ this.width = width;
244
+ this.height = height;
245
+
246
+ }
247
+
248
+ /**
249
+ * @param {GPUBuffer} src packed rgba16float, `width * height * 8` bytes, STORAGE-capable
250
+ * @param {object} tone
251
+ * @param {number} tone.exposure raw `renderer.toneMappingExposure`
252
+ * @param {number} tone.toneMapping Three.js ToneMapping constant
253
+ * @param {number} [tone.saturation=1]
254
+ * @param {boolean} [tone.flipY=false]
255
+ * @returns {Promise<Uint8ClampedArray>} RGBA bytes, `width * height * 4`
256
+ */
257
+ async toRGBA8( src, { exposure = 1, toneMapping = 0, saturation = 1, flipY = false } = {} ) {
258
+
259
+ if ( this.disposed ) throw new Error( 'PackedToneMapper: disposed' );
260
+ if ( ! this._pipeline ) throw new Error( 'PackedToneMapper: call ensureSize() first' );
261
+
262
+ const { width, height } = this;
263
+
264
+ this._paramU32[ 0 ] = width;
265
+ this._paramU32[ 1 ] = height;
266
+ this._paramU32[ 2 ] = toneMapping >>> 0;
267
+ this._paramU32[ 3 ] = flipY ? 1 : 0;
268
+ this._paramF32[ 4 ] = exposure;
269
+ this._paramF32[ 5 ] = saturation;
270
+ this.device.queue.writeBuffer( this._params, 0, this._paramData );
271
+
272
+ const group = this.device.createBindGroup( {
273
+ layout: this._pipeline.getBindGroupLayout( 0 ),
274
+ entries: [
275
+ { binding: 0, resource: { buffer: src, size: width * height * 8 } },
276
+ { binding: 1, resource: { buffer: this._storage } },
277
+ { binding: 2, resource: { buffer: this._params } },
278
+ ],
279
+ } );
280
+
281
+ const encoder = this.device.createCommandEncoder( { label: this.label } );
282
+ const pass = encoder.beginComputePass();
283
+ pass.setPipeline( this._pipeline );
284
+ pass.setBindGroup( 0, group );
285
+ pass.dispatchWorkgroups( Math.ceil( width / 8 ), Math.ceil( height / 8 ) );
286
+ pass.end();
287
+ encoder.copyBufferToBuffer( this._storage, 0, this._map, 0, width * height * 4 );
288
+ this.device.queue.submit( [ encoder.finish() ] );
289
+
290
+ await this._map.mapAsync( GPUMapMode.READ );
291
+ const bytes = new Uint8ClampedArray( this._map.getMappedRange().slice( 0 ) );
292
+ this._map.unmap();
293
+ return bytes;
294
+
295
+ }
296
+
297
+ _releaseBuffers() {
298
+
299
+ this._storage?.destroy();
300
+ this._map?.destroy();
301
+ this._params?.destroy();
302
+ this._storage = null;
303
+ this._map = null;
304
+ this._params = null;
305
+
306
+ }
307
+
308
+ dispose() {
309
+
310
+ if ( this.disposed ) return;
311
+ this.disposed = true;
312
+ this._releaseBuffers();
313
+ this._pipeline = null;
314
+
315
+ }
316
+
317
+ }