rayzee 8.5.0 → 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 (60) hide show
  1. package/README.md +60 -4
  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 +27198 -9047
  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 +55 -3
  21. package/src/EngineIssues.js +3 -0
  22. package/src/Passes/AIUpscaler.js +16 -7
  23. package/src/PathTracerApp.js +227 -7
  24. package/src/Processor/AssetLoader.js +1 -2
  25. package/src/Processor/EmissiveTriangleBuilder.js +34 -2
  26. package/src/Processor/GeometryExtractor.js +151 -138
  27. package/src/Processor/LightSerializer.js +8 -3
  28. package/src/Processor/MaterialPacking.js +168 -0
  29. package/src/Processor/PackedRayBuffer.js +6 -2
  30. package/src/Processor/ProceduralSky.js +4 -0
  31. package/src/Processor/SceneProcessor.js +25 -1
  32. package/src/Processor/SimpleSky.js +8 -0
  33. package/src/Processor/TextureCreator.js +137 -91
  34. package/src/Processor/TextureReadback.js +92 -0
  35. package/src/Processor/ToneMapCPU.js +80 -165
  36. package/src/Processor/ToneMapGPU.js +122 -151
  37. package/src/RenderSettings.js +1 -0
  38. package/src/Stages/Compositor.js +11 -2
  39. package/src/Stages/PathTracer.js +2 -0
  40. package/src/Stages/PathTracerStage.js +1 -1
  41. package/src/TSL/Common.js +15 -0
  42. package/src/TSL/Debugger.js +2 -3
  43. package/src/TSL/EmissiveSampling.js +6 -8
  44. package/src/TSL/ExtendKernel.js +9 -0
  45. package/src/TSL/Fresnel.js +23 -1
  46. package/src/TSL/HitFacet.js +102 -0
  47. package/src/TSL/LightsDirect.js +12 -33
  48. package/src/TSL/LightsSampling.js +15 -7
  49. package/src/TSL/MaterialEvaluation.js +26 -19
  50. package/src/TSL/MaterialProperties.js +74 -37
  51. package/src/TSL/MaterialTransmission.js +2 -3
  52. package/src/TSL/ShadeKernel.js +34 -21
  53. package/src/TSL/ShadowTerminator.js +125 -0
  54. package/src/TSL/Struct.js +9 -0
  55. package/src/TSL/Subsurface.js +2 -3
  56. package/src/index.js +29 -1
  57. package/src/managers/EnvironmentManager.js +20 -0
  58. package/src/managers/MaterialDataManager.js +118 -208
  59. package/src/managers/UniformManager.js +1 -0
  60. package/dist/ToneMapGPU-DzqSyzTx.js +0 -341
@@ -0,0 +1,419 @@
1
+ /**
2
+ * A display, a view and optionally a look, turned into something the engine can render with.
3
+ *
4
+ * This is the output half of colour management. The config says what "sRGB - Display / ACES 2.0 -
5
+ * SDR 100 nits" means; this bakes that meaning into a table (see `LutBake.js`) and registers it so
6
+ * `renderer.toneMapping = entry.id` just works — on the canvas, in a saved PNG, and through OIDN.
7
+ *
8
+ * ⚠️ An OCIO view returns colour **already encoded for its display**. The renderer must therefore
9
+ * not encode it again: `ColorManagement` sets `renderer.outputColorSpace` to linear while one is
10
+ * active, and the readback skips its sRGB step via `outputEncoded`. Get that wrong and every
11
+ * image is encoded twice — washed out, with crushed blacks.
12
+ */
13
+
14
+ import { Data3DTexture, RGBAFormat, HalfFloatType, NearestFilter, ClampToEdgeWrapping } from 'three';
15
+ import {
16
+ Fn, vec3, ivec3, float, floor, clamp, log2, max, If, texture3DLoad,
17
+ } from 'three/tsl';
18
+
19
+ import { ISSUE_CODES } from '../EngineIssues.js';
20
+ import { displayCanvasFit } from './Displays.js';
21
+ import { getRuntime, getConfigInfo } from './OcioRuntime.js';
22
+ import { bakeLut, makeCpuSampler, packHalf, lutWgsl, measureBakeError } from './LutBake.js';
23
+ import { addViewTransform, nextOcioId, VIEW_TRANSFORMS } from './ViewTransforms.js';
24
+
25
+ /** 25 stops, which is wider than any renderer output worth showing. */
26
+ export const DEFAULT_MIN_EV = - 12.47393;
27
+ export const DEFAULT_MAX_EV = 12.5260688117;
28
+
29
+ /** 65³ measures a mean of 0.11 code values against OCIO itself, for 2.1 MB. */
30
+ export const DEFAULT_LUT_SIZE = 65;
31
+
32
+
33
+ let issueLog = null;
34
+
35
+ /**
36
+ * The texture and TSL node already built for each registered id.
37
+ *
38
+ * Rebaking a view — a different look, a new `$SHOT`, a changed working space — must not hand the
39
+ * renderer a *different* node for the same id. three.js caches the compiled graph, and its node
40
+ * library refuses to redefine an id at all, so a new node either does nothing or forces a shader
41
+ * rebuild. Swapping the pixels inside the texture the graph already points at avoids both, and is
42
+ * the same pattern `UniformManager` uses: build the node once, mutate its value.
43
+ */
44
+ const liveViews = new Map();
45
+
46
+ /** Wire the engine's degradation log in, so a mismatch is recorded rather than only warned. */
47
+ export function setOcioIssueLog( log ) {
48
+
49
+ issueLog = log;
50
+
51
+ }
52
+
53
+ function configHandle() {
54
+
55
+ const rt = getRuntime();
56
+ if ( ! rt ) throw new Error( 'load an OCIO config first' );
57
+ if ( ! rt.config ) throw new Error( 'OCIO runtime exposes no Config handle' );
58
+ return rt.config;
59
+
60
+ }
61
+
62
+ /**
63
+ * The processor for one view, including a look when asked for.
64
+ *
65
+ * A look is applied as a separate step before the display transform, not through the view's own
66
+ * `looks` field — that field is whatever the config author attached, and a host choosing a look
67
+ * from the menu means "also this one".
68
+ */
69
+ function createViewProcessor( { source, display, view, look, context } ) {
70
+
71
+ const config = configHandle();
72
+ const options = context ? { context } : undefined;
73
+
74
+ if ( ! look ) {
75
+
76
+ return config.createDisplayViewProcessor( { source, display, view, ...( context ? { context } : {} ) } );
77
+
78
+ }
79
+
80
+ return config.createGroupTransformProcessor( [
81
+ { type: 'look', source, destination: source, looks: look },
82
+ { type: 'displayView', source, display, view },
83
+ ], options );
84
+
85
+ }
86
+
87
+ /** The TSL half of the table: the same shaper and the same six tetrahedra, as a node graph. */
88
+ function buildTslNode( { texture, size, minEv, maxEv } ) {
89
+
90
+ const last = size - 1;
91
+ const span = maxEv - minEv;
92
+
93
+ const fetch = coord => texture3DLoad(
94
+ texture,
95
+ clamp( coord, ivec3( 0 ), ivec3( last ) )
96
+ ).rgb;
97
+
98
+ return Fn( ( [ color, exposure ] ) => {
99
+
100
+ const c = max( color.mul( exposure ), vec3( 0.0 ) );
101
+
102
+ const p = clamp(
103
+ log2( max( c, vec3( 1e-10 ) ) ).sub( float( minEv ) ).div( float( span ) ),
104
+ vec3( 0.0 ), vec3( 1.0 )
105
+ ).mul( float( last ) ).toVar();
106
+
107
+ const i0 = clamp( ivec3( floor( p ) ), ivec3( 0 ), ivec3( size - 2 ) ).toVar();
108
+ const f = clamp( p.sub( vec3( i0 ) ), vec3( 0.0 ), vec3( 1.0 ) ).toVar();
109
+
110
+ // Declared before the branches: a sibling branch reading a var declared inside another
111
+ // reads an unassigned value, which is silent and wrong rather than a compile error.
112
+ const w = vec3( 0.0 ).toVar();
113
+ const o1 = ivec3( 0 ).toVar();
114
+ const o2 = ivec3( 0 ).toVar();
115
+ const o3 = ivec3( 1 ).toVar();
116
+
117
+ If( f.x.greaterThan( f.y ), () => {
118
+
119
+ If( f.y.greaterThan( f.z ), () => {
120
+
121
+ w.assign( vec3( f.x, f.y, f.z ) );
122
+ o1.assign( ivec3( 1, 0, 0 ) ); o2.assign( ivec3( 1, 1, 0 ) ); o3.assign( ivec3( 1, 1, 1 ) );
123
+
124
+ } ).ElseIf( f.x.greaterThan( f.z ), () => {
125
+
126
+ w.assign( vec3( f.x, f.z, f.y ) );
127
+ o1.assign( ivec3( 1, 0, 0 ) ); o2.assign( ivec3( 1, 0, 1 ) ); o3.assign( ivec3( 1, 1, 1 ) );
128
+
129
+ } ).Else( () => {
130
+
131
+ w.assign( vec3( f.z, f.x, f.y ) );
132
+ o1.assign( ivec3( 0, 0, 1 ) ); o2.assign( ivec3( 1, 0, 1 ) ); o3.assign( ivec3( 1, 1, 1 ) );
133
+
134
+ } );
135
+
136
+ } ).Else( () => {
137
+
138
+ If( f.z.greaterThan( f.y ), () => {
139
+
140
+ w.assign( vec3( f.z, f.y, f.x ) );
141
+ o1.assign( ivec3( 0, 0, 1 ) ); o2.assign( ivec3( 0, 1, 1 ) ); o3.assign( ivec3( 1, 1, 1 ) );
142
+
143
+ } ).ElseIf( f.z.greaterThan( f.x ), () => {
144
+
145
+ w.assign( vec3( f.y, f.z, f.x ) );
146
+ o1.assign( ivec3( 0, 1, 0 ) ); o2.assign( ivec3( 0, 1, 1 ) ); o3.assign( ivec3( 1, 1, 1 ) );
147
+
148
+ } ).Else( () => {
149
+
150
+ w.assign( vec3( f.y, f.x, f.z ) );
151
+ o1.assign( ivec3( 0, 1, 0 ) ); o2.assign( ivec3( 1, 1, 0 ) ); o3.assign( ivec3( 1, 1, 1 ) );
152
+
153
+ } );
154
+
155
+ } );
156
+
157
+ const c000 = fetch( i0 ).toVar();
158
+ const v1 = fetch( i0.add( o1 ) ).toVar();
159
+ const v2 = fetch( i0.add( o2 ) ).toVar();
160
+ const v3 = fetch( i0.add( o3 ) ).toVar();
161
+
162
+ return c000
163
+ .add( v1.sub( c000 ).mul( w.x ) )
164
+ .add( v2.sub( v1 ).mul( w.y ) )
165
+ .add( v3.sub( v2 ).mul( w.z ) );
166
+
167
+ } );
168
+
169
+ }
170
+
171
+ /**
172
+ * Bake one view into a registry entry, without registering it.
173
+ *
174
+ * @param {Object} options
175
+ * @param {string} options.display
176
+ * @param {string} options.view
177
+ * @param {string} [options.look] - a look name from the config
178
+ * @param {Object} [options.context] - context variables, e.g. `{ SHOT: '010' }`
179
+ * @param {string} [options.source] - the space being rendered in; defaults to the scene_linear role
180
+ * @param {number} [options.size=65]
181
+ * @returns {Object} a registry entry, with `error` holding what the table cost
182
+ */
183
+ export function buildOcioView( {
184
+ display, view, look = null, context = null, source = null,
185
+ size = DEFAULT_LUT_SIZE, minEv = DEFAULT_MIN_EV, maxEv = DEFAULT_MAX_EV,
186
+ id = null, name = null,
187
+ } = {} ) {
188
+
189
+ const info = getConfigInfo();
190
+ if ( ! info ) throw new Error( 'load an OCIO config first' );
191
+
192
+ const sceneLinear = info.roles.find( r => r.name === 'scene_linear' )?.colorSpace;
193
+ const from = source ?? sceneLinear;
194
+ if ( ! from ) throw new Error( 'config has no scene_linear role — pass an explicit source space' );
195
+
196
+ if ( ! info.displays.includes( display ) ) {
197
+
198
+ throw new Error( `config has no display "${display}" — has ${info.displays.join( ', ' )}` );
199
+
200
+ }
201
+
202
+ const views = ( info.viewsByDisplay[ display ] ?? [] ).map( v => v.name );
203
+ if ( ! views.includes( view ) ) {
204
+
205
+ throw new Error( `display "${display}" has no view "${view}" — has ${views.join( ', ' )}` );
206
+
207
+ }
208
+
209
+ if ( look && ! info.looks.some( l => l.name === look ) ) {
210
+
211
+ throw new Error( `config has no look "${look}" — has ${info.looks.map( l => l.name ).join( ', ' )}` );
212
+
213
+ }
214
+
215
+ if ( displayCanvasFit( display ) === null ) {
216
+
217
+ issueLog?.warn(
218
+ ISSUE_CODES.VIEW_TRANSFORM_DISPLAY_MISMATCH,
219
+ `"${display}" is not an sRGB-class SDR display, which is all the canvas is configured for — ` +
220
+ 'its view will be shown clipped or in the wrong gamut on screen, though a saved buffer is right',
221
+ { display, view }
222
+ );
223
+
224
+ }
225
+
226
+ const processor = createViewProcessor( { source: from, display, view, look, context } );
227
+ const apply = buffer => processor.applyRGBAF32( buffer );
228
+
229
+ // The JavaScript sampler reads the same half-precision values the GPU does, not the float
230
+ // bake they came from — otherwise the readback and the viewport differ by the half rounding,
231
+ // and "one table in four places" would only be nearly true. The error is measured on that
232
+ // same table, so it reports what actually renders.
233
+ let half, error;
234
+ try {
235
+
236
+ half = packHalf( bakeLut( { size, minEv, maxEv, apply } ) );
237
+ error = measureBakeError( { sampler: makeCpuSampler( { data: half, size, minEv, maxEv } ), apply } );
238
+
239
+ } finally {
240
+
241
+ processor.dispose();
242
+
243
+ }
244
+
245
+ return tableEntry( {
246
+ id, half, error,
247
+ name: name ?? ( look ? `${view} + ${look} (${display})` : `${view} (${display})` ),
248
+ ocio: { display, view, look, context, source: from, configId: info.id, size, minEv, maxEv },
249
+ } );
250
+
251
+ }
252
+
253
+ /**
254
+ * A registry entry for a view baked earlier and saved (`BakedViews.js`), without the runtime.
255
+ * @param {Object} baked - from `decodeBakedView`
256
+ */
257
+ export function buildBakedView( baked, id = null ) {
258
+
259
+ const { name, data, error, configId, display, view, look, context, source, size, minEv, maxEv, fingerprint, ocioVersion } = baked;
260
+
261
+ return tableEntry( {
262
+ id, name, error, half: data,
263
+ ocio: {
264
+ display, view, look: look ?? null, context: context ?? null, source, configId, size, minEv, maxEv,
265
+ baked: { fingerprint: fingerprint ?? null, ocioVersion: ocioVersion ?? null },
266
+ },
267
+ } );
268
+
269
+ }
270
+
271
+ function tableEntry( { id, name, half, error, ocio } ) {
272
+
273
+ const { size, minEv, maxEv } = ocio;
274
+ const sampler = makeCpuSampler( { data: half, size, minEv, maxEv } );
275
+ const entryId = id ?? nextOcioId();
276
+ const wgslConst = `TM_OCIO_${entryId}`;
277
+ const fnName = `tm_ocio_${entryId}`;
278
+
279
+ const held = liveViews.get( entryId );
280
+ const reusable = held && held.size === size && held.minEv === minEv && held.maxEv === maxEv;
281
+
282
+ let texture, tsl;
283
+ if ( reusable ) {
284
+
285
+ texture = held.texture;
286
+ texture.image.data = half;
287
+ texture.needsUpdate = true;
288
+ tsl = held.tsl;
289
+
290
+ } else {
291
+
292
+ held?.texture.dispose();
293
+
294
+ texture = new Data3DTexture( half, size, size, size );
295
+ texture.format = RGBAFormat;
296
+ texture.type = HalfFloatType;
297
+ texture.minFilter = NearestFilter;
298
+ texture.magFilter = NearestFilter;
299
+ texture.wrapS = texture.wrapT = texture.wrapR = ClampToEdgeWrapping;
300
+ texture.needsUpdate = true;
301
+
302
+ tsl = buildTslNode( { texture, size, minEv, maxEv } );
303
+
304
+ }
305
+
306
+ liveViews.set( entryId, { texture, tsl, size, minEv, maxEv } );
307
+
308
+ return {
309
+ id: entryId,
310
+ name,
311
+ wgslConst,
312
+ source: 'ocio',
313
+ outputEncoded: true,
314
+ appliesExposure: true,
315
+
316
+ cpu: ( r, g, b, exposure, out ) => sampler( r * exposure, g * exposure, b * exposure, out ),
317
+
318
+ wgsl: lutWgsl( { fnName, texName: `${wgslConst.toLowerCase()}_tex`, size, minEv, maxEv } ),
319
+ call: `${fnName}( c )`,
320
+ tsl,
321
+
322
+ table: { data: half, size, texture },
323
+ error,
324
+ ocio,
325
+ };
326
+
327
+ }
328
+
329
+ /** Bake a view and put it in the registry. */
330
+ export function addOcioView( options ) {
331
+
332
+ return addViewTransform( buildOcioView( options ) );
333
+
334
+ }
335
+
336
+ /**
337
+ * Bake every view of one display — what a host wants after loading a config, so the menu is
338
+ * populated without the user naming anything.
339
+ *
340
+ * Views that are pure data (`Raw`) are skipped: colour-managing a data view is meaningless, and
341
+ * the config marks them.
342
+ *
343
+ * @returns {{ added: Object[], skipped: Array<{ view: string, reason: string }> }}
344
+ */
345
+ export function addAllOcioViews( { display = null, look = null, context = null, source = null, size = DEFAULT_LUT_SIZE } = {} ) {
346
+
347
+ const info = getConfigInfo();
348
+ if ( ! info ) throw new Error( 'load an OCIO config first' );
349
+
350
+ const target = display ?? info.defaultDisplay;
351
+ const views = info.viewsByDisplay[ target ] ?? [];
352
+
353
+ const added = [];
354
+ const skipped = [];
355
+
356
+ for ( const v of views ) {
357
+
358
+ if ( v.colorSpace && v.colorSpace !== '<USE_DISPLAY_NAME>' ) {
359
+
360
+ const cs = info.colorSpaces.find( c => c.name === v.colorSpace );
361
+ if ( cs?.isData ) {
362
+
363
+ skipped.push( { view: v.name, reason: 'data view' } );
364
+ continue;
365
+
366
+ }
367
+
368
+ }
369
+
370
+ try {
371
+
372
+ added.push( addOcioView( { display: target, view: v.name, look, context, source, size } ) );
373
+
374
+ } catch ( err ) {
375
+
376
+ skipped.push( { view: v.name, reason: err.message } );
377
+ issueLog?.warn( ISSUE_CODES.VIEW_TRANSFORM_BAKE_FAILED, `could not bake "${v.name}" (${target}): ${err.message}`, { display: target, view: v.name } );
378
+
379
+ }
380
+
381
+ }
382
+
383
+ return { added, skipped };
384
+
385
+ }
386
+
387
+ /**
388
+ * Forget one view's texture and node, and free the texture.
389
+ *
390
+ * Ids are reused — `nextOcioId()` hands out the lowest free one — so a view removed without this
391
+ * would leave its disposed texture waiting to be "reused" by whatever is baked into that id next.
392
+ */
393
+ export function forgetOcioView( id ) {
394
+
395
+ const held = liveViews.get( id );
396
+ if ( ! held ) return;
397
+ held.texture.dispose();
398
+ liveViews.delete( id );
399
+
400
+ }
401
+
402
+ /** Free the GPU texture behind every OCIO entry currently registered, except the ids in `keep`. */
403
+ export function disposeOcioViewTextures( keep = null ) {
404
+
405
+ for ( const t of VIEW_TRANSFORMS.values() ) {
406
+
407
+ if ( t.source === 'ocio' && ! keep?.has( t.id ) ) t.table?.texture?.dispose?.();
408
+
409
+ }
410
+
411
+ for ( const [ id, held ] of liveViews ) {
412
+
413
+ if ( keep?.has( id ) ) continue;
414
+ held.texture.dispose();
415
+ liveViews.delete( id );
416
+
417
+ }
418
+
419
+ }