rayzee 8.4.1 → 8.6.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 (65) hide show
  1. package/README.md +61 -9
  2. package/dist/{DLSSNeural-BNSJ8V3z.js → DLSSNeural-BhBCBgKf.js} +5 -5
  3. package/dist/{DLSSSuperRes-BoEW89Cl.js → DLSSSuperRes-BCA7F_k5.js} +5 -5
  4. package/dist/ToneMapGPU-BzcLheoR.js +533 -0
  5. package/dist/rayzee.es.js +27278 -9133
  6. package/dist/rayzee.umd.js +255 -187
  7. package/package.json +1 -1
  8. package/src/AssetConfig.js +15 -2
  9. package/src/Color/BakedViews.js +134 -0
  10. package/src/Color/BuiltinViews.js +261 -0
  11. package/src/Color/ColorManagement.js +1130 -0
  12. package/src/Color/ColorSpaces.js +432 -0
  13. package/src/Color/Displays.js +19 -0
  14. package/src/Color/InputColorSpaces.js +243 -0
  15. package/src/Color/LutBake.js +371 -0
  16. package/src/Color/OcioRuntime.js +338 -0
  17. package/src/Color/OcioViews.js +419 -0
  18. package/src/Color/ViewTransforms.js +323 -0
  19. package/src/Color/WorkingMatrix.js +86 -0
  20. package/src/EngineDefaults.js +84 -4
  21. package/src/EngineIssues.js +3 -1
  22. package/src/Passes/AIUpscaler.js +16 -7
  23. package/src/PathTracerApp.js +318 -35
  24. package/src/Processor/AssetLoader.js +47 -55
  25. package/src/Processor/CameraOptimizer.js +22 -6
  26. package/src/Processor/EmissiveTriangleBuilder.js +34 -2
  27. package/src/Processor/GeometryExtractor.js +151 -138
  28. package/src/Processor/LightSerializer.js +17 -20
  29. package/src/Processor/MaterialPacking.js +168 -0
  30. package/src/Processor/PackedRayBuffer.js +6 -2
  31. package/src/Processor/ProceduralSky.js +4 -0
  32. package/src/Processor/SceneProcessor.js +25 -1
  33. package/src/Processor/SimpleSky.js +8 -0
  34. package/src/Processor/TextureCreator.js +137 -91
  35. package/src/Processor/TextureReadback.js +92 -0
  36. package/src/Processor/ToneMapCPU.js +80 -165
  37. package/src/Processor/ToneMapGPU.js +122 -151
  38. package/src/RenderSettings.js +10 -1
  39. package/src/Stages/Compositor.js +11 -2
  40. package/src/Stages/PathTracer.js +21 -4
  41. package/src/Stages/PathTracerStage.js +11 -18
  42. package/src/TSL/Common.js +15 -0
  43. package/src/TSL/Debugger.js +2 -3
  44. package/src/TSL/EmissiveSampling.js +6 -8
  45. package/src/TSL/ExtendKernel.js +9 -0
  46. package/src/TSL/Fresnel.js +23 -1
  47. package/src/TSL/HitFacet.js +102 -0
  48. package/src/TSL/LightsDirect.js +12 -33
  49. package/src/TSL/LightsSampling.js +15 -7
  50. package/src/TSL/MaterialEvaluation.js +26 -19
  51. package/src/TSL/MaterialProperties.js +74 -37
  52. package/src/TSL/MaterialTransmission.js +2 -3
  53. package/src/TSL/ShadeKernel.js +34 -21
  54. package/src/TSL/ShadowTerminator.js +125 -0
  55. package/src/TSL/Struct.js +9 -0
  56. package/src/TSL/Subsurface.js +2 -3
  57. package/src/index.js +29 -2
  58. package/src/managers/DenoisingManager.js +3 -7
  59. package/src/managers/EnvironmentManager.js +20 -0
  60. package/src/managers/LightManager.js +17 -15
  61. package/src/managers/MaterialDataManager.js +118 -208
  62. package/src/managers/UniformManager.js +1 -0
  63. package/src/managers/VideoRenderManager.js +9 -9
  64. package/dist/ToneMapGPU-DzqSyzTx.js +0 -341
  65. package/src/LightUnits.js +0 -122
@@ -0,0 +1,323 @@
1
+ /**
2
+ * The live list of view transforms, and the shader generated from it.
3
+ *
4
+ * A view transform has to exist in four places at once: the TSL graph that paints the canvas, the
5
+ * WGSL compute pass that packs a frame for readback, the JavaScript that OIDN and the upscaler go
6
+ * through, and the menu a host builds. Keeping four hand-written lists in step is how a saved
7
+ * image ends up different from the viewport, so there is one list and the other three are derived
8
+ * from it.
9
+ *
10
+ * Adding an OCIO view at runtime therefore reaches every consumer, including ones compiled before
11
+ * the config was loaded — `getRegistryVersion()` moves and the shader is rebuilt.
12
+ *
13
+ * ## The two kinds of output
14
+ *
15
+ * A built-in curve returns **linear** colour; the renderer's output pass and the readback both
16
+ * apply the sRGB transfer afterwards. An OCIO view returns colour already encoded for its display
17
+ * — sRGB, Rec.1886, or PQ — and applying a transfer on top of that would encode it twice.
18
+ * `outputEncoded` is what tells each consumer which it is holding.
19
+ */
20
+
21
+ import { NoToneMapping } from 'three';
22
+ import { BUILTIN_VIEWS } from './BuiltinViews.js';
23
+
24
+ /** First id an OCIO-derived view gets. Clear of three.js's own constants and room to grow. */
25
+ export const OCIO_VIEW_BASE = 200;
26
+
27
+ /**
28
+ * How many table-backed transforms may be registered at once.
29
+ *
30
+ * The readback binds every table in one shader and WebGPU only guarantees 16 sampled textures per
31
+ * stage. Without a cap the failure is a shader that will not compile, at the moment someone bakes
32
+ * one view too many — long after the mistake.
33
+ */
34
+ export const MAX_TABLE_TRANSFORMS = 12;
35
+
36
+ const transforms = [ ...BUILTIN_VIEWS ];
37
+ const byId = new Map( transforms.map( t => [ t.id, t ] ) );
38
+
39
+ let version = 1;
40
+ const listeners = new Set();
41
+
42
+ function announce() {
43
+
44
+ version ++;
45
+ for ( const fn of listeners ) fn();
46
+
47
+ }
48
+
49
+ /** Every registered transform, keyed by id. Read-only in spirit — mutate through the functions. */
50
+ export const VIEW_TRANSFORMS = byId;
51
+
52
+ /** Bumped whenever the list changes, so a compiled shader knows it is stale. */
53
+ export function getRegistryVersion() {
54
+
55
+ return version;
56
+
57
+ }
58
+
59
+ /** @returns {function(): void} unsubscribe */
60
+ export function onRegistryChange( listener ) {
61
+
62
+ listeners.add( listener );
63
+ return () => listeners.delete( listener );
64
+
65
+ }
66
+
67
+ export function getViewTransform( id ) {
68
+
69
+ return byId.get( id ) ?? null;
70
+
71
+ }
72
+
73
+ /** What a host menu should show, in registration order. */
74
+ export function listViewTransforms() {
75
+
76
+ return transforms.map( t => ( {
77
+ id: t.id,
78
+ name: t.name,
79
+ source: t.source,
80
+ display: t.ocio?.display ?? null,
81
+ view: t.ocio?.view ?? null,
82
+ look: t.ocio?.look ?? null,
83
+ } ) );
84
+
85
+ }
86
+
87
+ /** How many registered transforms need a texture binding. */
88
+ export function countTableTransforms() {
89
+
90
+ return transforms.filter( t => t.table ).length;
91
+
92
+ }
93
+
94
+ /** The lowest unused OCIO id. */
95
+ export function nextOcioId() {
96
+
97
+ let id = OCIO_VIEW_BASE;
98
+ while ( byId.has( id ) ) id ++;
99
+ return id;
100
+
101
+ }
102
+
103
+ /**
104
+ * Register a transform, replacing any with the same id.
105
+ *
106
+ * @param {Object} t - needs at least `id`, `name`, `wgslConst`, `cpu`
107
+ */
108
+ export function addViewTransform( t ) {
109
+
110
+ if ( typeof t?.id !== 'number' ) throw new Error( 'view transform needs a numeric id' );
111
+ if ( typeof t.cpu !== 'function' ) throw new Error( `view transform ${t.id} needs a cpu function` );
112
+ if ( ! t.name ) throw new Error( `view transform ${t.id} needs a name` );
113
+ if ( ! /^TM_[A-Z0-9_]+$/.test( t.wgslConst ?? '' ) ) {
114
+
115
+ throw new Error( `view transform ${t.id} needs a wgslConst like TM_SOMETHING` );
116
+
117
+ }
118
+
119
+ const existing = byId.get( t.id );
120
+ if ( ! existing && t.table && countTableTransforms() >= MAX_TABLE_TRANSFORMS ) {
121
+
122
+ throw new Error(
123
+ `cannot register "${t.name}": ${MAX_TABLE_TRANSFORMS} table-backed view transforms already ` +
124
+ 'registered, and WebGPU only guarantees 16 sampled textures per stage. Remove one first.'
125
+ );
126
+
127
+ }
128
+
129
+ const entry = { source: 'custom', outputEncoded: false, appliesExposure: true, ...t };
130
+
131
+ if ( existing ) {
132
+
133
+ transforms[ transforms.indexOf( existing ) ] = entry;
134
+
135
+ } else {
136
+
137
+ transforms.push( entry );
138
+
139
+ }
140
+
141
+ byId.set( entry.id, entry );
142
+ announce();
143
+ return entry;
144
+
145
+ }
146
+
147
+ /** @returns {boolean} whether anything was removed */
148
+ export function removeViewTransform( id ) {
149
+
150
+ const existing = byId.get( id );
151
+ if ( ! existing ) return false;
152
+
153
+ transforms.splice( transforms.indexOf( existing ), 1 );
154
+ byId.delete( id );
155
+ announce();
156
+ return true;
157
+
158
+ }
159
+
160
+ /** Drop every transform that came from a config, leaving the built-ins and the ids in `keep`. */
161
+ export function removeOcioViewTransforms( keep = null ) {
162
+
163
+ const doomed = transforms.filter( t => t.source === 'ocio' && ! keep?.has( t.id ) ).map( t => t.id );
164
+ if ( doomed.length === 0 ) return 0;
165
+
166
+ for ( const id of doomed ) {
167
+
168
+ const t = byId.get( id );
169
+ transforms.splice( transforms.indexOf( t ), 1 );
170
+ byId.delete( id );
171
+
172
+ }
173
+
174
+ announce();
175
+ return doomed.length;
176
+
177
+ }
178
+
179
+ /**
180
+ * Hand every transform that has a TSL node to the renderer, so `renderer.toneMapping = id`
181
+ * works on the live canvas. Built-ins are already there — three.js registers its own.
182
+ */
183
+ export function registerWithRenderer( renderer ) {
184
+
185
+ const library = renderer?.library;
186
+ if ( ! library?.addToneMapping ) return 0;
187
+
188
+ let count = 0;
189
+ for ( const t of transforms ) {
190
+
191
+ if ( ! t.tsl ) continue;
192
+
193
+ // ⚠️ `addToneMapping` refuses to redefine an id — it warns and returns without replacing.
194
+ // A rebaked view keeps its id, so without dropping the old entry first the canvas would go
195
+ // on running the previous table while the readback used the new one. Two images, one
196
+ // setting, no error.
197
+ library.toneMappingNodes?.delete( t.id );
198
+ library.addToneMapping( t.tsl, t.id );
199
+ count ++;
200
+
201
+ }
202
+
203
+ return count;
204
+
205
+ }
206
+
207
+ /**
208
+ * The WGSL for every registered transform, plus the bindings its tables need.
209
+ *
210
+ * @param {Object} [options]
211
+ * @param {number} [options.group=0] - bind group the caller owns
212
+ * @param {number} [options.firstBinding=3] - first binding index free in that group
213
+ * @returns {{ wgsl: string, bindings: Array<{ index: number, transform: Object }> }}
214
+ */
215
+ export function buildToneMapWGSL( { group = 0, firstBinding = 3 } = {} ) {
216
+
217
+ const bindings = [];
218
+ let next = firstBinding;
219
+
220
+ const declarations = [];
221
+ const bodies = [];
222
+
223
+ for ( const t of transforms ) {
224
+
225
+ if ( t.table ) {
226
+
227
+ const texName = `${t.wgslConst.toLowerCase()}_tex`;
228
+ declarations.push( `@group(${group}) @binding(${next}) var ${texName}: texture_3d<f32>;` );
229
+ bindings.push( { index: next, transform: t, texName } );
230
+ next += 1;
231
+
232
+ }
233
+
234
+ if ( t.wgsl ) bodies.push( t.wgsl );
235
+
236
+ }
237
+
238
+ const constants = transforms.map( t => `const ${t.wgslConst}: u32 = ${t.id}u;` ).join( '\n' );
239
+
240
+ const branches = transforms
241
+ .filter( t => t.call )
242
+ .map( t => `\tif ( mode == ${t.wgslConst} ) { return ${t.call}; }` )
243
+ .join( '\n' );
244
+
245
+ // Generated rather than hardcoded: a transform that returns display-encoded colour must not
246
+ // have the sRGB transfer applied on top of it.
247
+ const encodedIds = transforms.filter( t => t.outputEncoded ).map( t => t.wgslConst );
248
+ const encodedTest = encodedIds.length
249
+ ? encodedIds.map( c => `mode == ${c}` ).join( ' || ' )
250
+ : 'false';
251
+
252
+ const noExposureIds = transforms.filter( t => ! t.appliesExposure ).map( t => t.wgslConst );
253
+ const noExposureTest = noExposureIds.length
254
+ ? noExposureIds.map( c => `mode == ${c}` ).join( ' || ' )
255
+ : 'false';
256
+
257
+ const wgsl = /* wgsl */ `
258
+ ${constants}
259
+
260
+ ${declarations.join( '\n' )}
261
+
262
+ ${bodies.join( '\n' )}
263
+
264
+ // Three.js clamps the fragment output with max(0) before tone mapping, so the curves never see a
265
+ // negative channel. The saturation grade drives channels below zero on much of a typical frame,
266
+ // and AgX/Neutral mix negatives across channels instead of clipping them.
267
+ fn rayzee_tone_curve( color: vec3<f32>, mode: u32 ) -> vec3<f32> {
268
+ let c = max( color, vec3<f32>( 0.0 ) );
269
+ ${branches}
270
+ return clamp( c, vec3<f32>( 0.0 ), vec3<f32>( 1.0 ) );
271
+ }
272
+
273
+ /** True when the curve already returned display-encoded colour. */
274
+ fn rayzee_output_encoded( mode: u32 ) -> bool {
275
+ return ${encodedTest};
276
+ }
277
+
278
+ fn rayzee_tone_map( linearRGB: vec3<f32>, mode: u32, exposure: f32, saturation: f32 ) -> vec3<f32> {
279
+ // Three.js returns early for NoToneMapping without applying exposure, so a readback that
280
+ // applied it would paint brighter than the viewport it replaces.
281
+ var c = linearRGB * select( exposure, 1.0, ${noExposureTest} );
282
+ if ( saturation != 1.0 ) {
283
+ let luma = vec3<f32>( dot( c, vec3<f32>( 0.2126, 0.7152, 0.0722 ) ) );
284
+ c = luma + ( c - luma ) * saturation;
285
+ }
286
+ return rayzee_tone_curve( c, mode );
287
+ }
288
+
289
+ fn rayzee_linear_to_srgb( c: vec3<f32> ) -> vec3<f32> {
290
+ return select(
291
+ 1.055 * pow( max( c, vec3<f32>( 0.0 ) ), vec3<f32>( 1.0 / 2.4 ) ) - vec3<f32>( 0.055 ),
292
+ 12.92 * c,
293
+ c <= vec3<f32>( 0.0031308 ) );
294
+ }
295
+
296
+ /** The transfer step, skipped for a curve that already encoded its own output. */
297
+ fn rayzee_encode( mapped: vec3<f32>, mode: u32 ) -> vec3<f32> {
298
+ if ( rayzee_output_encoded( mode ) ) { return clamp( mapped, vec3<f32>( 0.0 ), vec3<f32>( 1.0 ) ); }
299
+ return rayzee_linear_to_srgb( mapped );
300
+ }
301
+
302
+ fn rayzee_to_u8( srgb: vec3<f32> ) -> vec3<u32> {
303
+ return vec3<u32>( clamp( round( srgb * 255.0 + vec3<f32>( 0.5 ) ), vec3<f32>( 0.0 ), vec3<f32>( 255.0 ) ) );
304
+ }
305
+ `;
306
+
307
+ return { wgsl, bindings };
308
+
309
+ }
310
+
311
+ /** Put the registry back to the seven built-ins. Tests and config unload use this. */
312
+ export function resetViewTransforms() {
313
+
314
+ transforms.length = 0;
315
+ transforms.push( ...BUILTIN_VIEWS );
316
+ byId.clear();
317
+ for ( const t of transforms ) byId.set( t.id, t );
318
+ announce();
319
+
320
+ }
321
+
322
+ /** The id to fall back to when a chosen one disappears. */
323
+ export const FALLBACK_VIEW = NoToneMapping;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The primaries change from the engine's native linear Rec.709 into the adopted working space.
3
+ *
4
+ * A leaf module with no imports at all, deliberately. The two hottest consumers — the material
5
+ * buffer and the light serializer — sit far from colour management and are exercised by tests that
6
+ * mock three.js; pulling `ColorManagement` into them would drag the whole OCIO stack, and three.js
7
+ * with it, into places that have no business knowing either exists.
8
+ *
9
+ * Null means "not colour-managed", which is also the identity, so every consumer's fast path is a
10
+ * single null check.
11
+ */
12
+
13
+ let matrix = null;
14
+ let spaceName = null;
15
+
16
+ /**
17
+ * @param {?number[]} m - row-major 3×3, or null for no conversion
18
+ * @param {?string} [name] - the working space it converts into, for logging
19
+ */
20
+ export function setWorkingMatrix( m, name = null ) {
21
+
22
+ matrix = m;
23
+ spaceName = m ? name : null;
24
+
25
+ }
26
+
27
+ export function getWorkingMatrix() {
28
+
29
+ return matrix;
30
+
31
+ }
32
+
33
+ export function getWorkingMatrixSpace() {
34
+
35
+ return spaceName;
36
+
37
+ }
38
+
39
+ /**
40
+ * Convert one linear RGB triple in place.
41
+ * @param {Float32Array|number[]} rgb - read and written at [0..2]
42
+ * @returns {boolean} whether anything changed
43
+ */
44
+ export function convertLinearTriple( rgb ) {
45
+
46
+ if ( ! matrix ) return false;
47
+
48
+ const r = rgb[ 0 ], g = rgb[ 1 ], b = rgb[ 2 ];
49
+ rgb[ 0 ] = matrix[ 0 ] * r + matrix[ 1 ] * g + matrix[ 2 ] * b;
50
+ rgb[ 1 ] = matrix[ 3 ] * r + matrix[ 4 ] * g + matrix[ 5 ] * b;
51
+ rgb[ 2 ] = matrix[ 6 ] * r + matrix[ 7 ] * g + matrix[ 8 ] * b;
52
+ return true;
53
+
54
+ }
55
+
56
+ /**
57
+ * Convert interleaved linear triples inside a larger buffer.
58
+ *
59
+ * @param {Float32Array} data
60
+ * @param {number} stride - floats between one record and the next
61
+ * @param {number[]} offsets - where each triple starts within one stride
62
+ * @returns {number} how many triples were converted
63
+ */
64
+ export function convertLinearTriples( data, stride, offsets ) {
65
+
66
+ if ( ! matrix || offsets.length === 0 ) return 0;
67
+
68
+ let count = 0;
69
+ for ( let base = 0; base + stride <= data.length; base += stride ) {
70
+
71
+ for ( const off of offsets ) {
72
+
73
+ const i = base + off;
74
+ const r = data[ i ], g = data[ i + 1 ], b = data[ i + 2 ];
75
+ data[ i ] = matrix[ 0 ] * r + matrix[ 1 ] * g + matrix[ 2 ] * b;
76
+ data[ i + 1 ] = matrix[ 3 ] * r + matrix[ 4 ] * g + matrix[ 5 ] * b;
77
+ data[ i + 2 ] = matrix[ 6 ] * r + matrix[ 7 ] * g + matrix[ 8 ] * b;
78
+ count ++;
79
+
80
+ }
81
+
82
+ }
83
+
84
+ return count;
85
+
86
+ }
@@ -10,11 +10,13 @@
10
10
  */
11
11
  export const RENDER_PROFILES = Object.freeze( {
12
12
  viewer: Object.freeze( {
13
- environmentRotation: 270.0, // degrees
14
- toneMapping: 4, // ACESFilmicToneMapping
15
- saturation: 1.2, // compensates for ACES desaturation — see Compositor
13
+ areaLightIntensityScale: 0.1, // scales glTF placeholder area-light power (viewer tuning)
14
+ environmentRotation: 0.0, // degrees — the HDRI as authored, as Blender shows it
15
+ toneMapping: 6, // AgXToneMapping
16
+ saturation: 1.0, // no grade
16
17
  } ),
17
18
  physical: Object.freeze( {
19
+ areaLightIntensityScale: 1.0,
18
20
  environmentRotation: 0.0,
19
21
  // AgX, matching Blender/Cycles' default view transform. ACES crushes shadows on this
20
22
  // engine's own corpus (shade 1.48 vs AgX 3.01 from identical radiance).
@@ -25,7 +27,7 @@ export const RENDER_PROFILES = Object.freeze( {
25
27
 
26
28
  /**
27
29
  * @param {string} [name] - a RENDER_PROFILES key
28
- * @returns {{environmentRotation: number, toneMapping: number, saturation: number}}
30
+ * @returns {{areaLightIntensityScale: number, environmentRotation: number}}
29
31
  * @throws {Error} on an unknown name — a typo must not silently select viewer tuning
30
32
  */
31
33
  export function getRenderProfile( name = 'viewer' ) {
@@ -156,6 +158,9 @@ export const ENGINE_DEFAULTS = {
156
158
  emissiveBoost: 1.0,
157
159
 
158
160
  fireflyThreshold: 3.0,
161
+ // Cycles' Shadow Terminator → Geometry Offset, and its default: light shadow rays leave a
162
+ // smooth-shaded triangle from the smooth surface near the terminator. 0 disables.
163
+ shadowTerminatorOffset: 0.1,
159
164
  // Wavefront material-coherence sort: global counting-sort of entering rays by material before
160
165
  // Shade (material-pure workgroups), under dynamic dispatch. Measured −8% at 1024²/8b. Gated on
161
166
  // material count > 8; the histogram bin count is sized per-scene to the material count.
@@ -208,6 +213,8 @@ export const ENGINE_DEFAULTS = {
208
213
  bloomRadius: 0.15,
209
214
  bloomThreshold: 0.85,
210
215
  interactionModeEnabled: true,
216
+ // Per-axis render scale while the camera moves (0.5 = a quarter of the pixels); 1 turns it off.
217
+ interactionRenderScale: 0.5,
211
218
  debugVisScale: 100,
212
219
 
213
220
  // Denoising strategy
@@ -746,6 +753,55 @@ export const MATERIAL_DATA_LAYOUT = {
746
753
  // collapses both spellings to 0 so no shader divides by Inf.
747
754
  export const normalizeAttenuationDistance = d => ( Number.isFinite( d ) && d > 0 ? d : 0 );
748
755
 
756
+ // The only fallback for a property a three.js material doesn't carry: MeshPhysicalMaterial's own
757
+ // default (glTF with the extension absent), or the value that turns an engine-only feature off.
758
+ export const MATERIAL_DEFAULTS = deepFreeze( {
759
+ color: [ 1, 1, 1 ],
760
+ emissive: [ 0, 0, 0 ],
761
+ emissiveIntensity: 1,
762
+ roughness: 1,
763
+ metalness: 0,
764
+ ior: 1.5,
765
+ opacity: 1,
766
+ transmission: 0,
767
+ thickness: 0,
768
+ attenuationColor: [ 1, 1, 1 ],
769
+ attenuationDistance: Infinity,
770
+ dispersion: 0,
771
+ sheen: 0,
772
+ sheenRoughness: 1,
773
+ sheenColor: [ 0, 0, 0 ],
774
+ specularIntensity: 1,
775
+ specularColor: [ 1, 1, 1 ],
776
+ clearcoat: 0,
777
+ clearcoatRoughness: 0,
778
+ iridescence: 0,
779
+ iridescenceIOR: 1.3,
780
+ iridescenceThicknessRange: [ 100, 400 ],
781
+ normalScale: [ 1, 1 ],
782
+ bumpScale: 1,
783
+ displacementScale: 1,
784
+ transparent: 0,
785
+ alphaTest: 0,
786
+ alphaMode: 0,
787
+ side: 0,
788
+ depthWrite: 1,
789
+ subsurface: 0,
790
+ subsurfaceColor: [ 1, 1, 1 ],
791
+ subsurfaceRadius: [ 1, 0.2, 0.1 ],
792
+ subsurfaceRadiusScale: 1,
793
+ subsurfaceAnisotropy: 0,
794
+ anisotropy: 0,
795
+ anisotropyRotation: 0,
796
+ } );
797
+
798
+ function deepFreeze( object ) {
799
+
800
+ for ( const value of Object.values( object ) ) if ( typeof value === 'object' ) Object.freeze( value );
801
+ return Object.freeze( object );
802
+
803
+ }
804
+
749
805
  // BVH node leaf markers
750
806
  /**
751
807
  * Node tags, written into slot [3] of a BVH node as a raw u32 bit pattern.
@@ -1090,6 +1146,30 @@ export const INTERACTIVE_RENDER_CONFIG = {
1090
1146
  usePixelFreeze: true, // speeds up idle refinement on heavy/high-res views; inert while moving (freeze resets)
1091
1147
  };
1092
1148
 
1149
+ // The RenderSettings a mode preset owns. configureForMode applies these, and anything that borrows a
1150
+ // mode for a while (the video renderer) restores exactly these keys.
1151
+ export function modePresetSettings( config ) {
1152
+
1153
+ return {
1154
+ maxSamples: config.maxSamples,
1155
+ maxBounces: config.bounces,
1156
+ transmissiveBounces: config.transmissiveBounces,
1157
+ maxSubsurfaceSteps: config.maxSubsurfaceSteps,
1158
+ enableAlphaShadows: config.enableAlphaShadows ?? false,
1159
+ // Tier-1 convergence early-stop
1160
+ useAdaptiveSampling: config.useAdaptiveSampling ?? false,
1161
+ noiseThreshold: config.noiseThreshold ?? ENGINE_DEFAULTS.noiseThreshold,
1162
+ adaptiveStopFraction: config.adaptiveStopFraction ?? ENGINE_DEFAULTS.adaptiveStopFraction,
1163
+ adaptiveMinSamples: config.adaptiveMinSamples ?? ENGINE_DEFAULTS.adaptiveMinSamples,
1164
+ // Tier-2 per-pixel freeze
1165
+ usePixelFreeze: config.usePixelFreeze ?? false,
1166
+ pixelFreezeThreshold: config.pixelFreezeThreshold ?? ENGINE_DEFAULTS.pixelFreezeThreshold,
1167
+ pixelFreezeStability: config.pixelFreezeStability ?? ENGINE_DEFAULTS.pixelFreezeStability,
1168
+ interactionModeEnabled: config.interactionModeEnabled ?? ENGINE_DEFAULTS.interactionModeEnabled,
1169
+ };
1170
+
1171
+ }
1172
+
1093
1173
  // Memory management constants
1094
1174
  export const MEMORY_CONSTANTS = {
1095
1175
  MAX_BUFFER_MEMORY: 1024 * 1024 * 1024,
@@ -19,9 +19,11 @@ export const ISSUE_CODES = Object.freeze( {
19
19
  STAGE_RENDER_FAILED: 'stage.render_failed',
20
20
  RENDER_RESERVE_CAPPED: 'render.reserve_capped',
21
21
  SCENE_MEMORY_BUDGET: 'scene.memory_budget',
22
- LIGHT_PLACEHOLDER_INVALID: 'light.placeholder_invalid',
23
22
  EMISSIVE_INSTANCES_COLLAPSED: 'emissive.instances_collapsed',
24
23
  REFIT_SHARED_GEOMETRY: 'refit.shared_geometry',
24
+ VIEW_TRANSFORM_DISPLAY_MISMATCH: 'viewTransform.display_mismatch',
25
+ VIEW_TRANSFORM_BAKE_FAILED: 'viewTransform.bake_failed',
26
+ COLOR_CONFIG_LOAD_FAILED: 'color.config_load_failed',
25
27
  } );
26
28
 
27
29
  /** Strict throws on ERROR only. */
@@ -1,9 +1,15 @@
1
1
  import { EventDispatcher, ACESFilmicToneMapping } from 'three';
2
- import { TONE_MAP_FNS, SRGB_GAMMA, applySaturation, effectiveExposure } from '../Processor/ToneMapCPU.js';
2
+ import { TONE_MAP_FNS, SRGB_GAMMA, applySaturation, effectiveExposure, isOutputEncoded } from '../Processor/ToneMapCPU.js';
3
3
  import { getAssetConfig } from '../AssetConfig.js';
4
4
  import AIUpscalerWorker from '../Processor/Workers/AIUpscalerWorker.js?worker&inline';
5
5
 
6
6
 
7
+ // An OCIO view already returned colour encoded for its display, so the gamma step below would
8
+ // encode it twice. A built-in curve returns linear and does want it.
9
+ const displayEncoder = toneMapping => isOutputEncoded( toneMapping )
10
+ ? ( c => ( c > 1 ? 1 : c < 0 ? 0 : c ) )
11
+ : ( c => Math.pow( c, SRGB_GAMMA ) );
12
+
7
13
  // ─── Model Configuration ───────────────────────────────────────────────────────
8
14
  // Quality presets reference relative paths against asset-config `upscalerModelBaseUrl`.
9
15
 
@@ -400,6 +406,7 @@ export class AIUpscaler extends EventDispatcher {
400
406
  if ( sourceImageData.isHDR ) {
401
407
 
402
408
  this._hdrToneMapFn = TONE_MAP_FNS.get( this.getToneMapping() ) || TONE_MAP_FNS.get( ACESFilmicToneMapping );
409
+ this._hdrEncode = displayEncoder( this.getToneMapping() );
403
410
  this._hdrExposure = effectiveExposure( this.getExposure(), this.getToneMapping() );
404
411
  this._hdrSaturation = this.getSaturation();
405
412
  this._tmOut = new Float32Array( 3 );
@@ -597,9 +604,10 @@ export class AIUpscaler extends EventDispatcher {
597
604
  }
598
605
 
599
606
  tmFn( er, eg, eb, 1.0, this._tmOut );
600
- floats[ dstIdx ] = Math.pow( this._tmOut[ 0 ], SRGB_GAMMA );
601
- floats[ pixelCount + dstIdx ] = Math.pow( this._tmOut[ 1 ], SRGB_GAMMA );
602
- floats[ 2 * pixelCount + dstIdx ] = Math.pow( this._tmOut[ 2 ], SRGB_GAMMA );
607
+ const encode = this._hdrEncode;
608
+ floats[ dstIdx ] = encode( this._tmOut[ 0 ] );
609
+ floats[ pixelCount + dstIdx ] = encode( this._tmOut[ 1 ] );
610
+ floats[ 2 * pixelCount + dstIdx ] = encode( this._tmOut[ 2 ] );
603
611
 
604
612
  } else {
605
613
 
@@ -794,6 +802,7 @@ export class AIUpscaler extends EventDispatcher {
794
802
  const pixels = imageData.data;
795
803
  const tmFn = TONE_MAP_FNS.get( this.getToneMapping() ) || TONE_MAP_FNS.get( ACESFilmicToneMapping );
796
804
  const exposure = effectiveExposure( this.getExposure(), this.getToneMapping() );
805
+ const encode = displayEncoder( this.getToneMapping() );
797
806
  const saturation = this.getSaturation();
798
807
  const out = new Float32Array( 3 );
799
808
 
@@ -810,9 +819,9 @@ export class AIUpscaler extends EventDispatcher {
810
819
  }
811
820
 
812
821
  tmFn( er, eg, eb, 1.0, out );
813
- pixels[ si ] = ( Math.pow( out[ 0 ], SRGB_GAMMA ) * 255 + 0.5 ) | 0;
814
- pixels[ si + 1 ] = ( Math.pow( out[ 1 ], SRGB_GAMMA ) * 255 + 0.5 ) | 0;
815
- pixels[ si + 2 ] = ( Math.pow( out[ 2 ], SRGB_GAMMA ) * 255 + 0.5 ) | 0;
822
+ pixels[ si ] = ( encode( out[ 0 ] ) * 255 + 0.5 ) | 0;
823
+ pixels[ si + 1 ] = ( encode( out[ 1 ] ) * 255 + 0.5 ) | 0;
824
+ pixels[ si + 2 ] = ( encode( out[ 2 ] ) * 255 + 0.5 ) | 0;
816
825
  pixels[ si + 3 ] = 255;
817
826
 
818
827
  }