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.
- package/README.md +60 -4
- package/dist/{DLSSNeural-BNSJ8V3z.js → DLSSNeural-BhBCBgKf.js} +5 -5
- package/dist/{DLSSSuperRes-BoEW89Cl.js → DLSSSuperRes-BCA7F_k5.js} +5 -5
- package/dist/ToneMapGPU-BzcLheoR.js +533 -0
- package/dist/rayzee.es.js +27198 -9047
- package/dist/rayzee.umd.js +255 -187
- package/package.json +1 -1
- package/src/AssetConfig.js +15 -2
- package/src/Color/BakedViews.js +134 -0
- package/src/Color/BuiltinViews.js +261 -0
- package/src/Color/ColorManagement.js +1130 -0
- package/src/Color/ColorSpaces.js +432 -0
- package/src/Color/Displays.js +19 -0
- package/src/Color/InputColorSpaces.js +243 -0
- package/src/Color/LutBake.js +371 -0
- package/src/Color/OcioRuntime.js +338 -0
- package/src/Color/OcioViews.js +419 -0
- package/src/Color/ViewTransforms.js +323 -0
- package/src/Color/WorkingMatrix.js +86 -0
- package/src/EngineDefaults.js +55 -3
- package/src/EngineIssues.js +3 -0
- package/src/Passes/AIUpscaler.js +16 -7
- package/src/PathTracerApp.js +227 -7
- package/src/Processor/AssetLoader.js +1 -2
- package/src/Processor/EmissiveTriangleBuilder.js +34 -2
- package/src/Processor/GeometryExtractor.js +151 -138
- package/src/Processor/LightSerializer.js +8 -3
- package/src/Processor/MaterialPacking.js +168 -0
- package/src/Processor/PackedRayBuffer.js +6 -2
- package/src/Processor/ProceduralSky.js +4 -0
- package/src/Processor/SceneProcessor.js +25 -1
- package/src/Processor/SimpleSky.js +8 -0
- package/src/Processor/TextureCreator.js +137 -91
- package/src/Processor/TextureReadback.js +92 -0
- package/src/Processor/ToneMapCPU.js +80 -165
- package/src/Processor/ToneMapGPU.js +122 -151
- package/src/RenderSettings.js +1 -0
- package/src/Stages/Compositor.js +11 -2
- package/src/Stages/PathTracer.js +2 -0
- package/src/Stages/PathTracerStage.js +1 -1
- package/src/TSL/Common.js +15 -0
- package/src/TSL/Debugger.js +2 -3
- package/src/TSL/EmissiveSampling.js +6 -8
- package/src/TSL/ExtendKernel.js +9 -0
- package/src/TSL/Fresnel.js +23 -1
- package/src/TSL/HitFacet.js +102 -0
- package/src/TSL/LightsDirect.js +12 -33
- package/src/TSL/LightsSampling.js +15 -7
- package/src/TSL/MaterialEvaluation.js +26 -19
- package/src/TSL/MaterialProperties.js +74 -37
- package/src/TSL/MaterialTransmission.js +2 -3
- package/src/TSL/ShadeKernel.js +34 -21
- package/src/TSL/ShadowTerminator.js +125 -0
- package/src/TSL/Struct.js +9 -0
- package/src/TSL/Subsurface.js +2 -3
- package/src/index.js +29 -1
- package/src/managers/EnvironmentManager.js +20 -0
- package/src/managers/MaterialDataManager.js +118 -208
- package/src/managers/UniformManager.js +1 -0
- package/dist/ToneMapGPU-DzqSyzTx.js +0 -341
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moving colour between the config's named spaces.
|
|
3
|
+
*
|
|
4
|
+
* Three shapes of caller, three costs:
|
|
5
|
+
* - one colour (a material tint, a light colour) — cached, essentially free
|
|
6
|
+
* - a float buffer (an HDRI, a rendered frame) — one OCIO call per chunk
|
|
7
|
+
* - an 8-bit texture — expanded to float, converted, re-encoded
|
|
8
|
+
*
|
|
9
|
+
* The 8-bit path re-encodes with the sRGB transfer before quantizing rather than storing linear
|
|
10
|
+
* bytes. Linear 8-bit banding is visible in any dark gradient; the shader already undoes an sRGB
|
|
11
|
+
* curve on these pools, so the encoding costs nothing and the precision is no worse than the
|
|
12
|
+
* texture arrived with.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { getRuntime, getConfigInfo } from './OcioRuntime.js';
|
|
16
|
+
|
|
17
|
+
/** Pixels per chunk when converting a large buffer. 1M px = 16 MB of scratch. */
|
|
18
|
+
const CHUNK_PIXELS = 1 << 20;
|
|
19
|
+
|
|
20
|
+
const processorCache = new Map();
|
|
21
|
+
const colorCache = new Map();
|
|
22
|
+
const COLOR_CACHE_LIMIT = 4096;
|
|
23
|
+
const matrixCache = new Map();
|
|
24
|
+
|
|
25
|
+
function cacheKey( from, to, context ) {
|
|
26
|
+
|
|
27
|
+
return `${from}\u0000${to}\u0000${context ? JSON.stringify( context ) : ''}`;
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Drop every cached processor and result. Call when the config or context changes. */
|
|
32
|
+
export function clearColorCaches() {
|
|
33
|
+
|
|
34
|
+
for ( const p of processorCache.values() ) p.dispose?.();
|
|
35
|
+
processorCache.clear();
|
|
36
|
+
colorCache.clear();
|
|
37
|
+
matrixCache.clear();
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A processor for `from` → `to`, kept for reuse. Building one is the expensive part of OCIO;
|
|
43
|
+
* applying it is not.
|
|
44
|
+
* @returns {Object|null} null when no config is loaded
|
|
45
|
+
*/
|
|
46
|
+
export function getProcessor( from, to, { context = null } = {} ) {
|
|
47
|
+
|
|
48
|
+
const rt = getRuntime();
|
|
49
|
+
if ( ! rt ) return null;
|
|
50
|
+
if ( from === to ) return null;
|
|
51
|
+
|
|
52
|
+
const key = cacheKey( from, to, context );
|
|
53
|
+
let p = processorCache.get( key );
|
|
54
|
+
if ( p !== undefined ) return p;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
|
|
58
|
+
const config = configHandle();
|
|
59
|
+
p = config.createColorSpaceProcessor( from, to, context ? { context } : undefined );
|
|
60
|
+
if ( p.isNoOp ) {
|
|
61
|
+
|
|
62
|
+
p.dispose();
|
|
63
|
+
p = null;
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
} catch ( err ) {
|
|
68
|
+
|
|
69
|
+
throw new Error( `no OCIO transform from "${from}" to "${to}": ${err.message}` );
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
processorCache.set( key, p );
|
|
74
|
+
return p;
|
|
75
|
+
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The live `Config`, which is what builds processors — the runtime wrapper does not.
|
|
80
|
+
*
|
|
81
|
+
* The runtime exposes its own handle; that one belongs to the runtime and must never be disposed
|
|
82
|
+
* here, or every later call fails. Only a handle this module built is this module's to release.
|
|
83
|
+
*/
|
|
84
|
+
let configHandleCache = null;
|
|
85
|
+
let configHandleId = null;
|
|
86
|
+
let configHandleOwned = false;
|
|
87
|
+
|
|
88
|
+
function configHandle() {
|
|
89
|
+
|
|
90
|
+
const rt = getRuntime();
|
|
91
|
+
if ( ! rt ) throw new Error( 'no OCIO config loaded' );
|
|
92
|
+
|
|
93
|
+
if ( rt.config ) return rt.config;
|
|
94
|
+
|
|
95
|
+
const info = getConfigInfo();
|
|
96
|
+
if ( configHandleCache && configHandleId === info.id ) return configHandleCache;
|
|
97
|
+
|
|
98
|
+
if ( configHandleOwned ) configHandleCache?.dispose?.();
|
|
99
|
+
|
|
100
|
+
if ( ! info.id?.startsWith( 'ocio://' ) ) {
|
|
101
|
+
|
|
102
|
+
throw new Error( 'OCIO runtime exposes no Config handle for this config' );
|
|
103
|
+
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
configHandleCache = rt.ocio.createBuiltinConfig( info.id );
|
|
107
|
+
configHandleOwned = true;
|
|
108
|
+
configHandleId = info.id;
|
|
109
|
+
return configHandleCache;
|
|
110
|
+
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Forget the cached Config handle and every processor built from it. */
|
|
114
|
+
export function resetConfigHandle() {
|
|
115
|
+
|
|
116
|
+
if ( configHandleOwned ) configHandleCache?.dispose?.();
|
|
117
|
+
configHandleCache = null;
|
|
118
|
+
configHandleId = null;
|
|
119
|
+
configHandleOwned = false;
|
|
120
|
+
clearColorCaches();
|
|
121
|
+
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* One colour, converted. Repeated calls with the same colour are free — a scene with 4,000
|
|
126
|
+
* materials sharing 12 tints costs 12 conversions.
|
|
127
|
+
*
|
|
128
|
+
* @param {number[]|Float32Array} rgb
|
|
129
|
+
* @returns {number[]} a new triple; the input is untouched
|
|
130
|
+
*/
|
|
131
|
+
export function convertColor( rgb, from, to, { context = null } = {} ) {
|
|
132
|
+
|
|
133
|
+
const rt = getRuntime();
|
|
134
|
+
if ( ! rt || from === to ) return [ rgb[ 0 ], rgb[ 1 ], rgb[ 2 ] ];
|
|
135
|
+
|
|
136
|
+
const key = `${cacheKey( from, to, context )}\u0000${rgb[ 0 ]},${rgb[ 1 ]},${rgb[ 2 ]}`;
|
|
137
|
+
const hit = colorCache.get( key );
|
|
138
|
+
if ( hit ) return hit.slice();
|
|
139
|
+
|
|
140
|
+
const out = rt.transformRgb( from, to, [ rgb[ 0 ], rgb[ 1 ], rgb[ 2 ] ], context ? { context } : undefined );
|
|
141
|
+
// Bounded: a colour picker being dragged is a new colour every frame.
|
|
142
|
+
if ( colorCache.size >= COLOR_CACHE_LIMIT ) colorCache.clear();
|
|
143
|
+
colorCache.set( key, out );
|
|
144
|
+
return out.slice();
|
|
145
|
+
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A float RGBA buffer, converted in place. Alpha is left alone.
|
|
150
|
+
* @param {Float32Array} rgba - 4 floats per pixel
|
|
151
|
+
*/
|
|
152
|
+
export function convertPixelsF32( rgba, from, to, { context = null } = {} ) {
|
|
153
|
+
|
|
154
|
+
if ( rgba.length % 4 !== 0 ) throw new Error( 'convertPixelsF32 wants 4 floats per pixel' );
|
|
155
|
+
|
|
156
|
+
const processor = getProcessor( from, to, { context } );
|
|
157
|
+
if ( ! processor ) return rgba;
|
|
158
|
+
|
|
159
|
+
const pixels = rgba.length / 4;
|
|
160
|
+
if ( pixels <= CHUNK_PIXELS ) {
|
|
161
|
+
|
|
162
|
+
processor.applyRGBAF32( rgba );
|
|
163
|
+
return rgba;
|
|
164
|
+
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// A view over the same memory, so the chunking costs no copy.
|
|
168
|
+
for ( let start = 0; start < pixels; start += CHUNK_PIXELS ) {
|
|
169
|
+
|
|
170
|
+
const count = Math.min( CHUNK_PIXELS, pixels - start );
|
|
171
|
+
processor.applyRGBAF32( rgba.subarray( start * 4, ( start + count ) * 4 ) );
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return rgba;
|
|
176
|
+
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const SRGB_ALPHA = 0.055;
|
|
180
|
+
|
|
181
|
+
/** sRGB electro-optical transfer function — encoded [0,1] to linear. */
|
|
182
|
+
export function srgbToLinear( c ) {
|
|
183
|
+
|
|
184
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow( ( c + SRGB_ALPHA ) / ( 1 + SRGB_ALPHA ), 2.4 );
|
|
185
|
+
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** sRGB opto-electronic transfer function — linear to encoded [0,1]. */
|
|
189
|
+
export function linearToSrgb( c ) {
|
|
190
|
+
|
|
191
|
+
if ( c <= 0 ) return 0;
|
|
192
|
+
return c <= 0.0031308 ? 12.92 * c : ( 1 + SRGB_ALPHA ) * Math.pow( c, 1 / 2.4 ) - SRGB_ALPHA;
|
|
193
|
+
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const DECODE_255 = new Float32Array( 256 );
|
|
197
|
+
for ( let i = 0; i < 256; i ++ ) DECODE_255[ i ] = srgbToLinear( i / 255 );
|
|
198
|
+
|
|
199
|
+
const LINEAR_255 = new Float32Array( 256 );
|
|
200
|
+
for ( let i = 0; i < 256; i ++ ) LINEAR_255[ i ] = i / 255;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* How stored bytes relate to the values a colour space describes.
|
|
204
|
+
*
|
|
205
|
+
* 'raw' — the bytes ARE the space's own values; the space's transfer is part of the
|
|
206
|
+
* transform and must not be undone first
|
|
207
|
+
* 'srgb' — the bytes are sRGB-encoded and the space is the linear one underneath
|
|
208
|
+
* 'linear' — the bytes are linear
|
|
209
|
+
*
|
|
210
|
+
* Getting this wrong decodes a texture's transfer twice, which lifts every shadow and is easy to
|
|
211
|
+
* mistake for a lighting bug.
|
|
212
|
+
*/
|
|
213
|
+
function decodeTable( encoding ) {
|
|
214
|
+
|
|
215
|
+
if ( encoding === 'srgb' ) return DECODE_255;
|
|
216
|
+
return LINEAR_255;
|
|
217
|
+
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* An 8-bit RGBA texture, converted in place from `from` to `to`.
|
|
223
|
+
*
|
|
224
|
+
* @param {Uint8Array|Uint8ClampedArray} bytes - 4 bytes per pixel
|
|
225
|
+
* @param {Object} [options]
|
|
226
|
+
* @param {'raw'|'srgb'|'linear'} [options.inputEncoding='raw']
|
|
227
|
+
* @param {'srgb'|'linear'} [options.outputEncoding='srgb']
|
|
228
|
+
*/
|
|
229
|
+
export function convertEncodedRGBA8( bytes, from, to, {
|
|
230
|
+
context = null, inputEncoding = 'raw', outputEncoding = 'srgb',
|
|
231
|
+
} = {} ) {
|
|
232
|
+
|
|
233
|
+
if ( bytes.length % 4 !== 0 ) throw new Error( 'convertEncodedRGBA8 wants 4 bytes per pixel' );
|
|
234
|
+
|
|
235
|
+
const processor = getProcessor( from, to, { context } );
|
|
236
|
+
if ( ! processor ) return bytes;
|
|
237
|
+
|
|
238
|
+
const pixels = bytes.length / 4;
|
|
239
|
+
const decode = decodeTable( inputEncoding );
|
|
240
|
+
const encode = outputEncoding === 'srgb' ? encodeSrgbByte : encodeLinearByte;
|
|
241
|
+
const scratch = new Float32Array( Math.min( CHUNK_PIXELS, pixels ) * 4 );
|
|
242
|
+
|
|
243
|
+
for ( let start = 0; start < pixels; start += CHUNK_PIXELS ) {
|
|
244
|
+
|
|
245
|
+
const count = Math.min( CHUNK_PIXELS, pixels - start );
|
|
246
|
+
const view = count * 4 === scratch.length ? scratch : scratch.subarray( 0, count * 4 );
|
|
247
|
+
|
|
248
|
+
for ( let i = 0; i < count; i ++ ) {
|
|
249
|
+
|
|
250
|
+
const s = ( start + i ) * 4, d = i * 4;
|
|
251
|
+
view[ d ] = decode[ bytes[ s ] ];
|
|
252
|
+
view[ d + 1 ] = decode[ bytes[ s + 1 ] ];
|
|
253
|
+
view[ d + 2 ] = decode[ bytes[ s + 2 ] ];
|
|
254
|
+
view[ d + 3 ] = 1;
|
|
255
|
+
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
processor.applyRGBAF32( view );
|
|
259
|
+
|
|
260
|
+
for ( let i = 0; i < count; i ++ ) {
|
|
261
|
+
|
|
262
|
+
const s = ( start + i ) * 4, d = i * 4;
|
|
263
|
+
bytes[ s ] = encode( view[ d ] );
|
|
264
|
+
bytes[ s + 1 ] = encode( view[ d + 1 ] );
|
|
265
|
+
bytes[ s + 2 ] = encode( view[ d + 2 ] );
|
|
266
|
+
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return bytes;
|
|
272
|
+
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Linear [0,1] → sRGB byte, by table.
|
|
277
|
+
*
|
|
278
|
+
* Three `Math.pow` calls a pixel were most of the cost of converting a scene's textures — 3 s of
|
|
279
|
+
* frozen UI on the 642-texture test model. The table is indexed by the square root of the value
|
|
280
|
+
* because the sRGB curve is close to a square root, so its steps are near-even in the output. At
|
|
281
|
+
* 64K entries it is ~50× faster than the exact curve and 0.09 % of values land one level off —
|
|
282
|
+
* only those sitting on a rounding boundary.
|
|
283
|
+
*/
|
|
284
|
+
const ENCODE_STEPS = 65536;
|
|
285
|
+
const ENCODE_SRGB = new Uint8Array( ENCODE_STEPS + 1 );
|
|
286
|
+
for ( let i = 0; i <= ENCODE_STEPS; i ++ ) {
|
|
287
|
+
|
|
288
|
+
const t = i / ENCODE_STEPS;
|
|
289
|
+
ENCODE_SRGB[ i ] = Math.round( linearToSrgb( t * t ) * 255 );
|
|
290
|
+
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function encodeSrgbByte( v ) {
|
|
294
|
+
|
|
295
|
+
if ( ! ( v > 0 ) ) return 0;
|
|
296
|
+
if ( v >= 1 ) return 255;
|
|
297
|
+
return ENCODE_SRGB[ ( Math.sqrt( v ) * ENCODE_STEPS + 0.5 ) | 0 ];
|
|
298
|
+
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function encodeLinearByte( v ) {
|
|
302
|
+
|
|
303
|
+
if ( ! ( v > 0 ) ) return 0;
|
|
304
|
+
return v >= 1 ? 255 : ( v * 255 + 0.5 ) | 0;
|
|
305
|
+
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* A primaries change applied straight to 8-bit pixels, without a round trip through the OCIO
|
|
310
|
+
* runtime per chunk.
|
|
311
|
+
*
|
|
312
|
+
* This is the common case by far — a texture is sRGB or linear Rec.709 and the working space only
|
|
313
|
+
* differs by its primaries — and doing it here is roughly an order of magnitude faster than
|
|
314
|
+
* expanding a 4K texture to float and back through WebAssembly.
|
|
315
|
+
*
|
|
316
|
+
* @param {Uint8Array|Uint8ClampedArray} bytes
|
|
317
|
+
* @param {number[]} m - row-major 3×3, from `extractMatrix`
|
|
318
|
+
* @param {'srgb'|'linear'} [encoding='srgb'] - the encoding the bytes carry, in and out
|
|
319
|
+
*/
|
|
320
|
+
export function applyMatrixRGBA8( bytes, m, encoding = 'srgb' ) {
|
|
321
|
+
|
|
322
|
+
if ( ! m ) return bytes;
|
|
323
|
+
|
|
324
|
+
const decode = decodeTable( encoding );
|
|
325
|
+
const encode = encoding === 'srgb' ? encodeSrgbByte : encodeLinearByte;
|
|
326
|
+
const [ m0, m1, m2, m3, m4, m5, m6, m7, m8 ] = m;
|
|
327
|
+
|
|
328
|
+
for ( let i = 0; i < bytes.length; i += 4 ) {
|
|
329
|
+
|
|
330
|
+
const r = decode[ bytes[ i ] ], g = decode[ bytes[ i + 1 ] ], b = decode[ bytes[ i + 2 ] ];
|
|
331
|
+
|
|
332
|
+
bytes[ i ] = encode( m0 * r + m1 * g + m2 * b );
|
|
333
|
+
bytes[ i + 1 ] = encode( m3 * r + m4 * g + m5 * b );
|
|
334
|
+
bytes[ i + 2 ] = encode( m6 * r + m7 * g + m8 * b );
|
|
335
|
+
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return bytes;
|
|
339
|
+
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* The 3×3 matrix for `from` → `to`, row-major, or null when the transform is not a pure matrix.
|
|
344
|
+
*
|
|
345
|
+
* Tested rather than assumed: a transform is only a matrix if it is linear, so this checks
|
|
346
|
+
* superposition and scaling on probe colours instead of trusting the space's name. A primaries
|
|
347
|
+
* change passes; anything carrying a transfer curve, a tone curve or a gamut compressor does not.
|
|
348
|
+
*/
|
|
349
|
+
export function extractMatrix( from, to, { context = null, tolerance = 1e-4 } = {} ) {
|
|
350
|
+
|
|
351
|
+
const rt = getRuntime();
|
|
352
|
+
if ( ! rt ) return null;
|
|
353
|
+
if ( from === to ) return [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ];
|
|
354
|
+
|
|
355
|
+
const key = cacheKey( from, to, context );
|
|
356
|
+
if ( matrixCache.has( key ) ) return matrixCache.get( key );
|
|
357
|
+
|
|
358
|
+
const processor = getProcessor( from, to, { context } );
|
|
359
|
+
if ( ! processor ) {
|
|
360
|
+
|
|
361
|
+
matrixCache.set( key, [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ] );
|
|
362
|
+
return matrixCache.get( key );
|
|
363
|
+
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const apply = rgb => {
|
|
367
|
+
|
|
368
|
+
const buf = new Float32Array( [ rgb[ 0 ], rgb[ 1 ], rgb[ 2 ], 1 ] );
|
|
369
|
+
processor.applyRGBAF32( buf );
|
|
370
|
+
return [ buf[ 0 ], buf[ 1 ], buf[ 2 ] ];
|
|
371
|
+
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const e = [ apply( [ 1, 0, 0 ] ), apply( [ 0, 1, 0 ] ), apply( [ 0, 0, 1 ] ) ];
|
|
375
|
+
|
|
376
|
+
// Column j of the matrix is the image of basis vector j.
|
|
377
|
+
const m = [
|
|
378
|
+
e[ 0 ][ 0 ], e[ 1 ][ 0 ], e[ 2 ][ 0 ],
|
|
379
|
+
e[ 0 ][ 1 ], e[ 1 ][ 1 ], e[ 2 ][ 1 ],
|
|
380
|
+
e[ 0 ][ 2 ], e[ 1 ][ 2 ], e[ 2 ][ 2 ],
|
|
381
|
+
];
|
|
382
|
+
|
|
383
|
+
const predict = rgb => [
|
|
384
|
+
m[ 0 ] * rgb[ 0 ] + m[ 1 ] * rgb[ 1 ] + m[ 2 ] * rgb[ 2 ],
|
|
385
|
+
m[ 3 ] * rgb[ 0 ] + m[ 4 ] * rgb[ 1 ] + m[ 5 ] * rgb[ 2 ],
|
|
386
|
+
m[ 6 ] * rgb[ 0 ] + m[ 7 ] * rgb[ 1 ] + m[ 8 ] * rgb[ 2 ],
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
const probes = [
|
|
390
|
+
[ 0.18, 0.18, 0.18 ], [ 0.5, 0.25, 0.75 ], [ 2.0, 0.1, 0.4 ],
|
|
391
|
+
[ 0.02, 0.9, 0.3 ], [ 8.0, 4.0, 1.0 ], [ 0.001, 0.002, 0.004 ],
|
|
392
|
+
];
|
|
393
|
+
|
|
394
|
+
let linear = true;
|
|
395
|
+
for ( const probe of probes ) {
|
|
396
|
+
|
|
397
|
+
const got = apply( probe ), want = predict( probe );
|
|
398
|
+
const scale = Math.max( 1e-3, Math.abs( want[ 0 ] ), Math.abs( want[ 1 ] ), Math.abs( want[ 2 ] ) );
|
|
399
|
+
for ( let c = 0; c < 3; c ++ ) {
|
|
400
|
+
|
|
401
|
+
if ( Math.abs( got[ c ] - want[ c ] ) > tolerance * scale ) linear = false;
|
|
402
|
+
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if ( ! linear ) break;
|
|
406
|
+
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const result = linear ? m : null;
|
|
410
|
+
matrixCache.set( key, result );
|
|
411
|
+
return result;
|
|
412
|
+
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** True when the config marks this space as data (a normal map, a mask) — never colour-managed. */
|
|
416
|
+
export function isDataSpace( name ) {
|
|
417
|
+
|
|
418
|
+
const info = getConfigInfo();
|
|
419
|
+
if ( ! info ) return false;
|
|
420
|
+
const cs = info.colorSpaces.find( c => c.name === name || c.aliases?.includes( name ) );
|
|
421
|
+
return cs ? cs.isData : false;
|
|
422
|
+
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** True when the config knows this name (or an alias of it). */
|
|
426
|
+
export function hasColorSpace( name ) {
|
|
427
|
+
|
|
428
|
+
const info = getConfigInfo();
|
|
429
|
+
if ( ! info || ! name ) return false;
|
|
430
|
+
return info.colorSpaces.some( c => c.name === name || c.aliases?.includes( name ) );
|
|
431
|
+
|
|
432
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which OCIO displays the canvas can show as intended.
|
|
3
|
+
*
|
|
4
|
+
* Only a display whose encoding *is* a canvas colour space qualifies. sRGB, Rec.1886 and gamma 2.2
|
|
5
|
+
* Rec.709 are all shown on an sRGB canvas. "Display P3" is P3 primaries with the sRGB curve — exactly
|
|
6
|
+
* `display-p3`. "P3-D65" is the cinema variant with a 2.6 gamma, and Rec.2020 and the HDR displays
|
|
7
|
+
* have no canvas equivalent at all.
|
|
8
|
+
*
|
|
9
|
+
* @returns {'srgb'|'display-p3'|null}
|
|
10
|
+
*/
|
|
11
|
+
export function displayCanvasFit( display ) {
|
|
12
|
+
|
|
13
|
+
const name = String( display ?? '' );
|
|
14
|
+
if ( /(hdr|pq|hlg|2100|st2084)/i.test( name ) ) return null;
|
|
15
|
+
if ( /^display\s*p3\b/i.test( name ) ) return 'display-p3';
|
|
16
|
+
if ( /^(srgb|rec\.?\s*1886|bt\.?\s*1886|gamma\s*2\.2\s*rec\.?\s*709)\b(?!.*(p3|2020))/i.test( name ) ) return 'srgb';
|
|
17
|
+
return null;
|
|
18
|
+
|
|
19
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What colour space something arriving in the scene is already in.
|
|
3
|
+
*
|
|
4
|
+
* This is the half of colour management that is easy to skip and expensive to skip. A view
|
|
5
|
+
* transform makes the render look right; getting the input wrong makes it *be* wrong — a texture
|
|
6
|
+
* read as sRGB when it was authored in ACEScct is not a grading problem, it is a different image.
|
|
7
|
+
*
|
|
8
|
+
* Three answers, tried in order:
|
|
9
|
+
* 1. an explicit override the host set for that file or texture
|
|
10
|
+
* 2. the config's own file rules, which is the thing studios actually configure
|
|
11
|
+
* 3. what three.js already believes, mapped onto the config's roles
|
|
12
|
+
*
|
|
13
|
+
* With no config loaded every answer is null and the engine behaves exactly as it always has.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { SRGBColorSpace, LinearSRGBColorSpace, NoColorSpace } from 'three';
|
|
17
|
+
import { getRuntime, getConfigInfo } from './OcioRuntime.js';
|
|
18
|
+
import { hasColorSpace, isDataSpace } from './ColorSpaces.js';
|
|
19
|
+
|
|
20
|
+
/** Exact paths and regexes the host pinned, newest first. */
|
|
21
|
+
const overrides = [];
|
|
22
|
+
|
|
23
|
+
/** Bumped whenever an answer could change, so anything cached by input space knows it is stale. */
|
|
24
|
+
let inputVersion = 0;
|
|
25
|
+
|
|
26
|
+
export function getInputVersion() {
|
|
27
|
+
|
|
28
|
+
return inputVersion;
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Call when the config changes: its file rules are part of every answer. */
|
|
33
|
+
export function bumpInputVersion() {
|
|
34
|
+
|
|
35
|
+
inputVersion ++;
|
|
36
|
+
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Pin a colour space for a file, overriding the config's rules.
|
|
41
|
+
* @param {string|RegExp} match - an exact path, a substring, or a pattern
|
|
42
|
+
*/
|
|
43
|
+
export function setInputOverride( match, colorSpace ) {
|
|
44
|
+
|
|
45
|
+
overrides.unshift( { match, colorSpace } );
|
|
46
|
+
inputVersion ++;
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function clearInputOverrides() {
|
|
51
|
+
|
|
52
|
+
if ( overrides.length === 0 ) return;
|
|
53
|
+
overrides.length = 0;
|
|
54
|
+
inputVersion ++;
|
|
55
|
+
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function listInputOverrides() {
|
|
59
|
+
|
|
60
|
+
return overrides.map( o => ( { match: String( o.match ), colorSpace: o.colorSpace } ) );
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function matchOverride( filePath ) {
|
|
65
|
+
|
|
66
|
+
if ( ! filePath ) return null;
|
|
67
|
+
|
|
68
|
+
for ( const o of overrides ) {
|
|
69
|
+
|
|
70
|
+
if ( o.match instanceof RegExp ? o.match.test( filePath ) : filePath.includes( o.match ) ) {
|
|
71
|
+
|
|
72
|
+
return o.colorSpace;
|
|
73
|
+
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The config's file rules, as a host can show them. Empty without a config. */
|
|
83
|
+
export function listFileRules() {
|
|
84
|
+
|
|
85
|
+
return getConfigInfo()?.fileRules.map( r => ( {
|
|
86
|
+
index: r.index, name: r.name, colorSpace: r.colorSpace,
|
|
87
|
+
pattern: r.pattern, extension: r.extension, regex: r.regex,
|
|
88
|
+
} ) ) ?? [];
|
|
89
|
+
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A config space matching what three.js thinks the texture is.
|
|
94
|
+
*
|
|
95
|
+
* Roles first, because a config names its own spaces however it likes but the roles are fixed
|
|
96
|
+
* vocabulary. Only if the roles are absent does this fall back to matching names.
|
|
97
|
+
*/
|
|
98
|
+
function fromThreeColorSpace( threeColorSpace ) {
|
|
99
|
+
|
|
100
|
+
const info = getConfigInfo();
|
|
101
|
+
if ( ! info ) return null;
|
|
102
|
+
|
|
103
|
+
const role = name => info.roles.find( r => r.name === name )?.colorSpace ?? null;
|
|
104
|
+
|
|
105
|
+
if ( threeColorSpace === NoColorSpace ) return role( 'data' );
|
|
106
|
+
|
|
107
|
+
if ( threeColorSpace === SRGBColorSpace ) {
|
|
108
|
+
|
|
109
|
+
return role( 'texture_paint' ) ?? role( 'color_picking' )
|
|
110
|
+
?? info.colorSpaces.find( c => /^srgb\b/i.test( c.name ) && ! /display/i.test( c.name ) )?.name
|
|
111
|
+
?? null;
|
|
112
|
+
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if ( threeColorSpace === LinearSRGBColorSpace ) {
|
|
116
|
+
|
|
117
|
+
return info.colorSpaces.find( c => /^lin(ear)?[\s_-]*(rec\.?\s*)?709/i.test( c.name ) )?.name
|
|
118
|
+
?? info.colorSpaces.find( c => /^linear.*srgb/i.test( c.name ) )?.name
|
|
119
|
+
?? role( 'scene_linear' );
|
|
120
|
+
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return null;
|
|
124
|
+
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Work out what a file is in.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} filePath
|
|
131
|
+
* @param {Object} [options]
|
|
132
|
+
* @param {string} [options.threeColorSpace] - what three.js tagged it, used as the last resort
|
|
133
|
+
* @returns {{ colorSpace: string, via: 'override'|'rule'|'three', rule: ?Object }|null}
|
|
134
|
+
*/
|
|
135
|
+
export function resolveInputSpace( filePath, { threeColorSpace = null } = {} ) {
|
|
136
|
+
|
|
137
|
+
if ( ! getConfigInfo() ) return null;
|
|
138
|
+
|
|
139
|
+
const pinned = matchOverride( filePath );
|
|
140
|
+
if ( pinned ) return { colorSpace: pinned, via: 'override', rule: null };
|
|
141
|
+
|
|
142
|
+
const rt = getRuntime();
|
|
143
|
+
if ( rt && filePath ) {
|
|
144
|
+
|
|
145
|
+
const match = rt.matchFileRule( filePath );
|
|
146
|
+
|
|
147
|
+
// The default rule matches everything, so it is not evidence about this file. Prefer what
|
|
148
|
+
// three.js knows over a rule that only fired because nothing else did.
|
|
149
|
+
if ( match && ! match.isDefaultRule && hasColorSpace( match.colorSpace ) ) {
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
colorSpace: match.colorSpace,
|
|
153
|
+
via: 'rule',
|
|
154
|
+
rule: { index: match.ruleIndex, name: match.ruleName },
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const fromThree = fromThreeColorSpace( threeColorSpace );
|
|
162
|
+
if ( fromThree ) return { colorSpace: fromThree, via: 'three', rule: null };
|
|
163
|
+
|
|
164
|
+
const rt2 = getRuntime();
|
|
165
|
+
if ( rt2 && filePath ) {
|
|
166
|
+
|
|
167
|
+
const match = rt2.matchFileRule( filePath );
|
|
168
|
+
if ( match && hasColorSpace( match.colorSpace ) ) {
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
colorSpace: match.colorSpace,
|
|
172
|
+
via: 'rule',
|
|
173
|
+
rule: { index: match.ruleIndex, name: match.ruleName, isDefault: match.isDefaultRule },
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return null;
|
|
181
|
+
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Whether this texture should be colour-managed at all.
|
|
186
|
+
*
|
|
187
|
+
* A normal map, a roughness map or a mask carries numbers, not colour; running it through a
|
|
188
|
+
* primaries matrix corrupts it. three.js already flags these as `NoColorSpace`, and a config can
|
|
189
|
+
* say the same through its `data` role or an `isData` space.
|
|
190
|
+
*/
|
|
191
|
+
export function isDataTexture( texture ) {
|
|
192
|
+
|
|
193
|
+
if ( ! texture ) return false;
|
|
194
|
+
if ( texture.colorSpace === NoColorSpace ) return true;
|
|
195
|
+
|
|
196
|
+
const tagged = texture.userData?.ocioColorSpace;
|
|
197
|
+
if ( tagged && isDataSpace( tagged ) ) return true;
|
|
198
|
+
|
|
199
|
+
return false;
|
|
200
|
+
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The path a texture came from, as well as it can be told. */
|
|
204
|
+
function texturePath( texture, filePath ) {
|
|
205
|
+
|
|
206
|
+
return filePath ?? texture.userData?.sourcePath ?? ( texture.name || null ) ?? texture.image?.src ?? null;
|
|
207
|
+
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* What colour space a texture is in, and how that was decided.
|
|
212
|
+
*
|
|
213
|
+
* `via` is what lets a caller treat the answers differently: `'tag'`, `'override'` and `'rule'`
|
|
214
|
+
* are someone saying what the file is, while `'three'` only repeats what three.js already
|
|
215
|
+
* assumed — the engine's own pipeline has handled that case all along, and running a full OCIO
|
|
216
|
+
* transform over it is slower for the same result.
|
|
217
|
+
*
|
|
218
|
+
* @returns {?{ colorSpace: string, via: 'tag'|'override'|'rule'|'three', rule: ?Object }}
|
|
219
|
+
*/
|
|
220
|
+
export function resolveTextureSpace( texture, filePath = null ) {
|
|
221
|
+
|
|
222
|
+
if ( ! getConfigInfo() || ! texture ) return null;
|
|
223
|
+
|
|
224
|
+
const tagged = texture.userData?.ocioColorSpace;
|
|
225
|
+
if ( tagged ) return hasColorSpace( tagged ) ? { colorSpace: tagged, via: 'tag', rule: null } : null;
|
|
226
|
+
|
|
227
|
+
if ( isDataTexture( texture ) ) return null;
|
|
228
|
+
|
|
229
|
+
return resolveInputSpace( texturePath( texture, filePath ), { threeColorSpace: texture.colorSpace } );
|
|
230
|
+
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The colour space a texture is in, honouring an explicit `userData.ocioColorSpace` tag before
|
|
235
|
+
* anything is guessed.
|
|
236
|
+
*
|
|
237
|
+
* @returns {?string}
|
|
238
|
+
*/
|
|
239
|
+
export function textureInputSpace( texture, filePath = null ) {
|
|
240
|
+
|
|
241
|
+
return resolveTextureSpace( texture, filePath )?.colorSpace ?? null;
|
|
242
|
+
|
|
243
|
+
}
|