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.
- package/README.md +61 -9
- 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 +27278 -9133
- 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 +84 -4
- package/src/EngineIssues.js +3 -1
- package/src/Passes/AIUpscaler.js +16 -7
- package/src/PathTracerApp.js +318 -35
- package/src/Processor/AssetLoader.js +47 -55
- package/src/Processor/CameraOptimizer.js +22 -6
- package/src/Processor/EmissiveTriangleBuilder.js +34 -2
- package/src/Processor/GeometryExtractor.js +151 -138
- package/src/Processor/LightSerializer.js +17 -20
- 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 +10 -1
- package/src/Stages/Compositor.js +11 -2
- package/src/Stages/PathTracer.js +21 -4
- package/src/Stages/PathTracerStage.js +11 -18
- 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 -2
- package/src/managers/DenoisingManager.js +3 -7
- package/src/managers/EnvironmentManager.js +20 -0
- package/src/managers/LightManager.js +17 -15
- package/src/managers/MaterialDataManager.js +118 -208
- package/src/managers/UniformManager.js +1 -0
- package/src/managers/VideoRenderManager.js +9 -9
- package/dist/ToneMapGPU-DzqSyzTx.js +0 -341
- 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
|
+
}
|
package/src/EngineDefaults.js
CHANGED
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export const RENDER_PROFILES = Object.freeze( {
|
|
12
12
|
viewer: Object.freeze( {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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 {{
|
|
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,
|
package/src/EngineIssues.js
CHANGED
|
@@ -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. */
|
package/src/Passes/AIUpscaler.js
CHANGED
|
@@ -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
|
-
|
|
601
|
-
floats[
|
|
602
|
-
floats[
|
|
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 ] = (
|
|
814
|
-
pixels[ si + 1 ] = (
|
|
815
|
-
pixels[ si + 2 ] = (
|
|
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
|
}
|