rayzee 7.24.0 → 7.26.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 +1 -1
- package/dist/rayzee.es.js +1921 -1755
- package/dist/rayzee.umd.js +7 -7
- package/package.json +2 -2
- package/src/Passes/AIUpscaler.js +3 -3
- package/src/Passes/OIDNDenoiser.js +2 -2
- package/src/PathTracerApp.js +206 -19
- package/src/Pipeline/CompletionTracker.js +45 -6
- package/src/Processor/AssetLoader.js +88 -12
- package/src/Processor/SceneMetadata.js +123 -0
- package/src/Processor/ToneMapCPU.js +37 -7
- package/src/RenderSettings.js +1 -0
- package/src/Stages/PathTracerStage.js +6 -16
- package/src/index.js +3 -0
- package/src/managers/EnvironmentManager.js +34 -2
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SceneMetadata.js
|
|
3
|
+
* Scene-level authoring metadata carried inside an asset file.
|
|
4
|
+
*
|
|
5
|
+
* glTF/GLB carries it in `extras`, which three's GLTFLoader surfaces in three
|
|
6
|
+
* places depending on where the exporter wrote it: top-level `extras` becomes
|
|
7
|
+
* `gltf.userData`, `asset.extras` stays on `gltf.asset.extras`, and a scene's
|
|
8
|
+
* `extras` becomes `gltf.scene.userData`. All three are scanned, and the payload
|
|
9
|
+
* may sit at the root or be namespaced under a `rayzee` key.
|
|
10
|
+
*
|
|
11
|
+
* Recognised payload:
|
|
12
|
+
* {
|
|
13
|
+
* "environment": {
|
|
14
|
+
* "sourceFile": "https://.../brown_photostudio_02_1k.hdr",
|
|
15
|
+
* "rotation": 0, // degrees around Y, matches the environmentRotation setting
|
|
16
|
+
* "intensity": 1 // drives both environmentIntensity and backgroundIntensity
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* Values may also arrive as JSON strings — DCC custom-property round-trips often
|
|
21
|
+
* stringify nested objects — so every container is parsed leniently.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const ENV_URL_KEYS = [ 'sourceFile', 'url', 'file' ];
|
|
25
|
+
|
|
26
|
+
function asObject( value ) {
|
|
27
|
+
|
|
28
|
+
if ( typeof value === 'string' ) {
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
|
|
32
|
+
const parsed = JSON.parse( value );
|
|
33
|
+
return ( parsed && typeof parsed === 'object' ) ? parsed : null;
|
|
34
|
+
|
|
35
|
+
} catch {
|
|
36
|
+
|
|
37
|
+
return null;
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return ( value && typeof value === 'object' ) ? value : null;
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function asNumber( value ) {
|
|
48
|
+
|
|
49
|
+
const n = typeof value === 'string' ? parseFloat( value ) : value;
|
|
50
|
+
return Number.isFinite( n ) ? n : undefined;
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function pickPayload( container ) {
|
|
55
|
+
|
|
56
|
+
const obj = asObject( container );
|
|
57
|
+
if ( ! obj ) return null;
|
|
58
|
+
|
|
59
|
+
const scoped = asObject( obj.rayzee );
|
|
60
|
+
if ( scoped && scoped.environment !== undefined ) return scoped;
|
|
61
|
+
|
|
62
|
+
return obj.environment !== undefined ? obj : null;
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeEnvironment( raw ) {
|
|
67
|
+
|
|
68
|
+
const env = asObject( raw );
|
|
69
|
+
if ( ! env ) return null;
|
|
70
|
+
|
|
71
|
+
const sourceFile = ENV_URL_KEYS
|
|
72
|
+
.map( key => env[ key ] )
|
|
73
|
+
.find( value => typeof value === 'string' && value.trim() );
|
|
74
|
+
if ( ! sourceFile ) return null;
|
|
75
|
+
|
|
76
|
+
const rotation = asNumber( env.rotation );
|
|
77
|
+
const intensity = asNumber( env.intensity );
|
|
78
|
+
|
|
79
|
+
const normalized = { sourceFile: sourceFile.trim() };
|
|
80
|
+
if ( rotation !== undefined ) normalized.rotation = rotation;
|
|
81
|
+
if ( intensity !== undefined ) normalized.intensity = Math.max( 0, intensity );
|
|
82
|
+
|
|
83
|
+
return normalized;
|
|
84
|
+
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Reads scene metadata out of one `extras`-shaped container.
|
|
89
|
+
* @param {Object|string|null} container
|
|
90
|
+
* @returns {{ environment?: { sourceFile: string, rotation?: number, intensity?: number } }|null}
|
|
91
|
+
*/
|
|
92
|
+
export function parseSceneMetadata( container ) {
|
|
93
|
+
|
|
94
|
+
const payload = pickPayload( container );
|
|
95
|
+
if ( ! payload ) return null;
|
|
96
|
+
|
|
97
|
+
const environment = normalizeEnvironment( payload.environment );
|
|
98
|
+
return environment ? { environment } : null;
|
|
99
|
+
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Reads scene metadata out of a GLTFLoader parse result.
|
|
104
|
+
* @param {Object} gltf - The object GLTFLoader resolves with
|
|
105
|
+
* @returns {{ environment?: { sourceFile: string, rotation?: number, intensity?: number } }|null}
|
|
106
|
+
*/
|
|
107
|
+
export function extractSceneMetadata( gltf ) {
|
|
108
|
+
|
|
109
|
+
if ( ! gltf ) return null;
|
|
110
|
+
|
|
111
|
+
const containers = [ gltf.userData, gltf.asset?.extras, gltf.scene?.userData ];
|
|
112
|
+
if ( Array.isArray( gltf.scenes ) ) containers.push( ...gltf.scenes.map( scene => scene?.userData ) );
|
|
113
|
+
|
|
114
|
+
for ( const container of containers ) {
|
|
115
|
+
|
|
116
|
+
const metadata = parseSceneMetadata( container );
|
|
117
|
+
if ( metadata ) return metadata;
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
|
|
123
|
+
}
|
|
@@ -140,15 +140,26 @@ function neutralToneMap( r, g, b, exposure, out ) {
|
|
|
140
140
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Three.js clamps the fragment output with `.max( 0 )` *before* tone mapping
|
|
145
|
+
* (NodeMaterial.js, "force unsigned floats"), so the GPU curves never see a negative
|
|
146
|
+
* channel. Compositor's saturation grade (default 1.2) drives complementary channels
|
|
147
|
+
* below zero on a third of a typical frame, and AgX/Neutral mix those negatives across
|
|
148
|
+
* channels instead of clipping them — measured 7-10 levels of shadow error against the
|
|
149
|
+
* viewport until the readback clamps the same way.
|
|
150
|
+
*/
|
|
151
|
+
const clampNegative = fn => ( r, g, b, exposure, out ) =>
|
|
152
|
+
fn( r > 0 ? r : 0, g > 0 ? g : 0, b > 0 ? b : 0, exposure, out );
|
|
153
|
+
|
|
143
154
|
/** Look-up table mapping Three.js ToneMapping constants to CPU functions. */
|
|
144
155
|
export const TONE_MAP_FNS = new Map( [
|
|
145
|
-
[ NoToneMapping, noToneMap ],
|
|
146
|
-
[ LinearToneMapping, linearToneMap ],
|
|
147
|
-
[ ReinhardToneMapping, reinhardToneMap ],
|
|
148
|
-
[ CineonToneMapping, cineonToneMap ],
|
|
149
|
-
[ ACESFilmicToneMapping, acesFilmicToneMap ],
|
|
150
|
-
[ AgXToneMapping, agxToneMap ],
|
|
151
|
-
[ NeutralToneMapping, neutralToneMap ]
|
|
156
|
+
[ NoToneMapping, clampNegative( noToneMap ) ],
|
|
157
|
+
[ LinearToneMapping, clampNegative( linearToneMap ) ],
|
|
158
|
+
[ ReinhardToneMapping, clampNegative( reinhardToneMap ) ],
|
|
159
|
+
[ CineonToneMapping, clampNegative( cineonToneMap ) ],
|
|
160
|
+
[ ACESFilmicToneMapping, clampNegative( acesFilmicToneMap ) ],
|
|
161
|
+
[ AgXToneMapping, clampNegative( agxToneMap ) ],
|
|
162
|
+
[ NeutralToneMapping, clampNegative( neutralToneMap ) ]
|
|
152
163
|
] );
|
|
153
164
|
|
|
154
165
|
/** sRGB gamma (1/2.2) — fast pow approximation. Prefer `linearToSRGB` when matching Three.js's output. */
|
|
@@ -165,6 +176,25 @@ export function linearToSRGB( c ) {
|
|
|
165
176
|
|
|
166
177
|
}
|
|
167
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Exposure as the WebGPU output pass applies it.
|
|
181
|
+
*
|
|
182
|
+
* Three.js applies `toneMappingExposure` *inside* ToneMappingNode's tone-mapping branch, and
|
|
183
|
+
* that branch returns the colour untouched for NoToneMapping (ToneMappingNode.js: `if
|
|
184
|
+
* ( toneMapping === NoToneMapping ) return colorNode`). So exposure is a no-op on screen there,
|
|
185
|
+
* and a CPU readback that applies it anyway paints brighter than the viewport it replaces
|
|
186
|
+
* (measured +33.6 % at exposure 2).
|
|
187
|
+
*
|
|
188
|
+
* @param {number} exposure - renderer.toneMappingExposure
|
|
189
|
+
* @param {number} toneMapping - Three.js ToneMapping constant
|
|
190
|
+
* @returns {number}
|
|
191
|
+
*/
|
|
192
|
+
export function effectiveExposure( exposure, toneMapping ) {
|
|
193
|
+
|
|
194
|
+
return toneMapping === NoToneMapping ? 1.0 : exposure;
|
|
195
|
+
|
|
196
|
+
}
|
|
197
|
+
|
|
168
198
|
/** Rec.709 luminance coefficients (same as Display / Common.js). */
|
|
169
199
|
const LUM_R = 0.2126, LUM_G = 0.7152, LUM_B = 0.0722;
|
|
170
200
|
|
package/src/RenderSettings.js
CHANGED
|
@@ -1185,25 +1185,15 @@ export class PathTracerStage extends RenderStage {
|
|
|
1185
1185
|
}
|
|
1186
1186
|
|
|
1187
1187
|
/**
|
|
1188
|
-
* Update completion threshold based on render mode
|
|
1188
|
+
* Update completion threshold based on render mode. The ceiling holds in every limit mode —
|
|
1189
|
+
* uncapping it under a time budget lets a generous deadline silently slow every render.
|
|
1189
1190
|
*/
|
|
1190
1191
|
updateCompletionThreshold() {
|
|
1191
1192
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
this.completionThreshold = Infinity;
|
|
1198
|
-
|
|
1199
|
-
} else {
|
|
1200
|
-
|
|
1201
|
-
this.completionThreshold = updateCompletionThreshold(
|
|
1202
|
-
renderMode,
|
|
1203
|
-
maxFrames
|
|
1204
|
-
);
|
|
1205
|
-
|
|
1206
|
-
}
|
|
1193
|
+
this.completionThreshold = updateCompletionThreshold(
|
|
1194
|
+
this.renderMode.value,
|
|
1195
|
+
this.maxSamples.value
|
|
1196
|
+
);
|
|
1207
1197
|
|
|
1208
1198
|
}
|
|
1209
1199
|
|
package/src/index.js
CHANGED
|
@@ -17,6 +17,9 @@ export { EngineEvents } from './EngineEvents.js';
|
|
|
17
17
|
// Asset URL / cache namespace overrides (call before constructing PathTracerApp)
|
|
18
18
|
export { configureAssets, getAssetConfig } from './AssetConfig.js';
|
|
19
19
|
|
|
20
|
+
// Scene-level authoring metadata embedded in model files (glTF `extras`)
|
|
21
|
+
export { extractSceneMetadata, parseSceneMetadata } from './Processor/SceneMetadata.js';
|
|
22
|
+
|
|
20
23
|
// Logging — leveled/namespaced console output shared with the workers
|
|
21
24
|
export { Logger, createLogger, fmt, LOG_LEVELS } from './utils/Logger.js';
|
|
22
25
|
|
|
@@ -360,8 +360,11 @@ export class EnvironmentManager {
|
|
|
360
360
|
/**
|
|
361
361
|
* Set environment map, build CDF, and update shader texture nodes.
|
|
362
362
|
* @param {import('three').Texture|null} envMap
|
|
363
|
+
* @param {Object} [options]
|
|
364
|
+
* @param {boolean} [options.buildCDF=true] - Skip when the caller builds the CDF itself
|
|
365
|
+
* (PathTracerApp.loadSceneData builds it in parallel with the BVH, then calls applyCDFResults).
|
|
363
366
|
*/
|
|
364
|
-
async setEnvironmentMap( envMap ) {
|
|
367
|
+
async setEnvironmentMap( envMap, { buildCDF = true } = {} ) {
|
|
365
368
|
|
|
366
369
|
// Free the outgoing env texture's GPU memory before it is orphaned. Skip: the
|
|
367
370
|
// reusable placeholder, an idempotent re-set of the same texture, and the HDRI
|
|
@@ -379,7 +382,7 @@ export class EnvironmentManager {
|
|
|
379
382
|
|
|
380
383
|
if ( envMap ) {
|
|
381
384
|
|
|
382
|
-
await this.buildEnvironmentCDF();
|
|
385
|
+
if ( buildCDF ) await this.buildEnvironmentCDF();
|
|
383
386
|
|
|
384
387
|
} else {
|
|
385
388
|
|
|
@@ -411,6 +414,35 @@ export class EnvironmentManager {
|
|
|
411
414
|
|
|
412
415
|
}
|
|
413
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Enter 'hdri' mode ahead of an async HDRI install, and drop the texture stashed by a
|
|
419
|
+
* previous setMode( 'gradient' | 'color' | 'procedural' ): a new HDRI makes it
|
|
420
|
+
* unreachable, so leaving it in place both leaks it and lets a setMode( 'hdri' )
|
|
421
|
+
* arriving mid-download restore it over the map we are about to install.
|
|
422
|
+
* @param {import('three').Texture} [incoming] - Never disposed, even if it is the stash.
|
|
423
|
+
*/
|
|
424
|
+
beginHDRI( incoming = null ) {
|
|
425
|
+
|
|
426
|
+
if ( this._previousHDRI && this._previousHDRI !== incoming ) this._previousHDRI.dispose?.();
|
|
427
|
+
|
|
428
|
+
this._previousHDRI = null;
|
|
429
|
+
this.envParams.mode = 'hdri';
|
|
430
|
+
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Install an HDRI as the active environment, switching the mode state machine to 'hdri'.
|
|
435
|
+
* @param {import('three').Texture} envMap
|
|
436
|
+
* @param {Object} [options]
|
|
437
|
+
* @param {boolean} [options.buildCDF=true]
|
|
438
|
+
*/
|
|
439
|
+
async applyHDRI( envMap, { buildCDF = true } = {} ) {
|
|
440
|
+
|
|
441
|
+
this.beginHDRI( envMap );
|
|
442
|
+
await this.setEnvironmentMap( envMap, { buildCDF } );
|
|
443
|
+
|
|
444
|
+
}
|
|
445
|
+
|
|
414
446
|
// ===== SKY GENERATORS =====
|
|
415
447
|
|
|
416
448
|
/**
|