rayzee 7.27.1 → 7.28.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/dist/rayzee.es.js +3607 -3394
- package/dist/rayzee.umd.js +49 -49
- package/package.json +1 -1
- package/src/EngineDefaults.js +42 -3
- package/src/EngineEvents.js +6 -0
- package/src/EngineIssues.js +93 -0
- package/src/Headless.js +114 -0
- package/src/PathTracerApp.js +208 -21
- package/src/Pipeline/RenderPipeline.js +31 -4
- package/src/Processor/AssetLoader.js +25 -2
- package/src/Processor/SceneProcessor.js +2 -1
- package/src/Processor/TextureCreator.js +37 -95
- package/src/Processor/ToneMapCPU.js +39 -0
- package/src/RenderSettings.js +94 -25
- package/src/index.js +12 -1
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -4,6 +4,45 @@
|
|
|
4
4
|
* and should not depend on any UI framework or external modules.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Product decisions for a real-time viewer, not physical constants. Both are invisible from
|
|
9
|
+
* outside and both move output away from a reference renderer. `viewer` is the default.
|
|
10
|
+
*/
|
|
11
|
+
export const RENDER_PROFILES = Object.freeze( {
|
|
12
|
+
viewer: Object.freeze( {
|
|
13
|
+
areaLightIntensityScale: 0.1, // authored RectAreaLight watts from glTF placeholders
|
|
14
|
+
environmentRotation: 270.0, // degrees
|
|
15
|
+
toneMapping: 4, // ACESFilmicToneMapping
|
|
16
|
+
saturation: 1.2, // compensates for ACES desaturation — see Compositor
|
|
17
|
+
} ),
|
|
18
|
+
physical: Object.freeze( {
|
|
19
|
+
areaLightIntensityScale: 1.0,
|
|
20
|
+
environmentRotation: 0.0,
|
|
21
|
+
// AgX, matching Blender/Cycles' default view transform. ACES crushes shadows on this
|
|
22
|
+
// engine's own corpus (shade 1.48 vs AgX 3.01 from identical radiance).
|
|
23
|
+
toneMapping: 6,
|
|
24
|
+
saturation: 1.0, // no grade
|
|
25
|
+
} ),
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} [name] - a RENDER_PROFILES key
|
|
30
|
+
* @returns {{areaLightIntensityScale: number, environmentRotation: number}}
|
|
31
|
+
* @throws {Error} on an unknown name — a typo must not silently select viewer tuning
|
|
32
|
+
*/
|
|
33
|
+
export function getRenderProfile( name = 'viewer' ) {
|
|
34
|
+
|
|
35
|
+
const profile = RENDER_PROFILES[ name ];
|
|
36
|
+
if ( ! profile ) {
|
|
37
|
+
|
|
38
|
+
throw new Error( `unknown render profile "${name}" — expected one of ${Object.keys( RENDER_PROFILES ).join( ', ' )}` );
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return profile;
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
|
|
7
46
|
export const ENGINE_DEFAULTS = {
|
|
8
47
|
// Canvas output
|
|
9
48
|
resolution: 512,
|
|
@@ -15,9 +54,9 @@ export const ENGINE_DEFAULTS = {
|
|
|
15
54
|
// to TEXTURE_CONSTANTS.MAX_TEXTURE_SIZE (hardware ceiling). Applied at scene load.
|
|
16
55
|
maxTextureSize: 4096,
|
|
17
56
|
|
|
18
|
-
toneMapping:
|
|
57
|
+
toneMapping: RENDER_PROFILES.viewer.toneMapping,
|
|
19
58
|
exposure: 1,
|
|
20
|
-
saturation:
|
|
59
|
+
saturation: RENDER_PROFILES.viewer.saturation,
|
|
21
60
|
enableEnvironment: true,
|
|
22
61
|
showBackground: true,
|
|
23
62
|
transparentBackground: false,
|
|
@@ -30,7 +69,7 @@ export const ENGINE_DEFAULTS = {
|
|
|
30
69
|
// primary-ray env lookup; lighting/reflections stay sharp. Samples = taps/frame (noise vs cost).
|
|
31
70
|
backgroundBlurriness: 0,
|
|
32
71
|
backgroundBlurSamples: 8,
|
|
33
|
-
environmentRotation:
|
|
72
|
+
environmentRotation: RENDER_PROFILES.viewer.environmentRotation,
|
|
34
73
|
groundProjectionEnabled: false,
|
|
35
74
|
groundProjectionRadius: 100,
|
|
36
75
|
groundProjectionHeight: 15,
|
package/src/EngineEvents.js
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured record of what the engine degraded rather than failed on. Lenient hosts read
|
|
3
|
+
* `app.issues`; strict hosts throw at the point of degradation.
|
|
4
|
+
*
|
|
5
|
+
* Codes are API surface — add, never rename or repurpose.
|
|
6
|
+
*/
|
|
7
|
+
export const ISSUE_CODES = Object.freeze( {
|
|
8
|
+
ADAPTER_SOFTWARE: 'adapter.software',
|
|
9
|
+
ASSET_UNREACHABLE: 'asset.unreachable',
|
|
10
|
+
TEXTURE_BUILD_FAILED: 'texture.build_failed',
|
|
11
|
+
TEXTURE_PROCESSING_FALLBACK: 'texture.processing_fallback',
|
|
12
|
+
ENVIRONMENT_LOAD_FAILED: 'environment.load_failed',
|
|
13
|
+
SETTING_UNKNOWN_KEY: 'setting.unknown_key',
|
|
14
|
+
RENDER_SIZE_DECLINED: 'render.size_declined',
|
|
15
|
+
STAGE_RENDER_FAILED: 'stage.render_failed',
|
|
16
|
+
RENDER_RESERVE_CAPPED: 'render.reserve_capped',
|
|
17
|
+
} );
|
|
18
|
+
|
|
19
|
+
/** Strict throws on ERROR only. */
|
|
20
|
+
export const ISSUE_SEVERITY = Object.freeze( { ERROR: 'error', WARNING: 'warning' } );
|
|
21
|
+
|
|
22
|
+
export class EngineIssueError extends Error {
|
|
23
|
+
|
|
24
|
+
constructor( issue ) {
|
|
25
|
+
|
|
26
|
+
super( `[${issue.code}] ${issue.message}` );
|
|
27
|
+
this.name = 'EngineIssueError';
|
|
28
|
+
this.code = issue.code;
|
|
29
|
+
this.detail = issue.detail;
|
|
30
|
+
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class IssueLog {
|
|
36
|
+
|
|
37
|
+
/** @param {{strict?: boolean, onIssue?: function(Object): void}} [options] */
|
|
38
|
+
constructor( { strict = false, onIssue = null } = {} ) {
|
|
39
|
+
|
|
40
|
+
this.strict = strict === true;
|
|
41
|
+
this._onIssue = onIssue;
|
|
42
|
+
this._entries = [];
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @throws {EngineIssueError} when strict and severity is 'error' */
|
|
47
|
+
record( code, message, detail = {}, severity = ISSUE_SEVERITY.ERROR ) {
|
|
48
|
+
|
|
49
|
+
const issue = Object.freeze( { code, message, detail, severity, at: Date.now() } );
|
|
50
|
+
this._entries.push( issue );
|
|
51
|
+
|
|
52
|
+
this._onIssue?.( issue );
|
|
53
|
+
|
|
54
|
+
if ( this.strict && severity === ISSUE_SEVERITY.ERROR ) throw new EngineIssueError( issue );
|
|
55
|
+
|
|
56
|
+
return issue;
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
warn( code, message, detail ) {
|
|
61
|
+
|
|
62
|
+
return this.record( code, message, detail, ISSUE_SEVERITY.WARNING );
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @returns {Object[]} a copy */
|
|
67
|
+
get list() {
|
|
68
|
+
|
|
69
|
+
return this._entries.slice();
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get errors() {
|
|
74
|
+
|
|
75
|
+
return this._entries.filter( ( issue ) => issue.severity === ISSUE_SEVERITY.ERROR );
|
|
76
|
+
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
clear() {
|
|
80
|
+
|
|
81
|
+
this._entries.length = 0;
|
|
82
|
+
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Call from dispose(): onIssue captures the app, and this log outlives it via injectees. */
|
|
86
|
+
detach() {
|
|
87
|
+
|
|
88
|
+
this._onIssue = null;
|
|
89
|
+
this.strict = false;
|
|
90
|
+
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
}
|
package/src/Headless.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The supported way to render without a person watching. Defaults are the batch renderer's —
|
|
3
|
+
* strict, physical profile, deterministic — all reversible, none reversible by accident.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* const shot = await renderHeadless( { canvas, model: url, width: 1920, height: 1080, samples: 256 } );
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { PathTracerApp } from './PathTracerApp.js';
|
|
10
|
+
/**
|
|
11
|
+
* One render, start to finish, disposing the app afterwards.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} options
|
|
14
|
+
* @param {HTMLCanvasElement} options.canvas - needed for the WebGPU surface, not for output
|
|
15
|
+
* @param {string} [options.model] - URL to load before rendering
|
|
16
|
+
* @param {number} [options.width=1920]
|
|
17
|
+
* @param {number} [options.height=1080]
|
|
18
|
+
* @param {number} [options.samples=64] - samples to accumulate
|
|
19
|
+
* @param {'linear'|'srgb'} [options.colorSpace='srgb']
|
|
20
|
+
* @param {boolean} [options.strict=true]
|
|
21
|
+
* @param {string} [options.profile='physical'] - see RENDER_PROFILES
|
|
22
|
+
* @param {boolean} [options.deterministic=true]
|
|
23
|
+
* @param {boolean} [options.allowEarlyRetire=false] - needs `deterministic: false` to be reachable
|
|
24
|
+
* @param {Object} [options.settings] - applied after the model loads
|
|
25
|
+
* @param {function(number): void} [options.onProgress] - running sample count
|
|
26
|
+
* @returns {Promise<{data: Float32Array|Uint8ClampedArray, width: number, height: number,
|
|
27
|
+
* colorSpace: string, samples: number, retiredBy: string, issues: Object[], adapter: Object}>}
|
|
28
|
+
*/
|
|
29
|
+
export async function renderHeadless( options ) {
|
|
30
|
+
|
|
31
|
+
const app = await openHeadless( options );
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
|
|
35
|
+
return await captureHeadless( app, options );
|
|
36
|
+
|
|
37
|
+
} finally {
|
|
38
|
+
|
|
39
|
+
app.dispose();
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Accumulate and read back, against an app {@link openHeadless} already set up.
|
|
47
|
+
* @param {PathTracerApp} app
|
|
48
|
+
* @returns {Promise<Object>} the frame plus what the engine survived to produce it
|
|
49
|
+
*/
|
|
50
|
+
export async function captureHeadless( app, {
|
|
51
|
+
samples = 64,
|
|
52
|
+
colorSpace = 'srgb',
|
|
53
|
+
allowEarlyRetire = false,
|
|
54
|
+
onProgress = undefined,
|
|
55
|
+
} = {} ) {
|
|
56
|
+
|
|
57
|
+
const accumulated = await app.renderFrames( samples, { onProgress, allowEarlyRetire } );
|
|
58
|
+
const frame = await app.renderToBuffer( { colorSpace } );
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
...frame,
|
|
62
|
+
samples: accumulated,
|
|
63
|
+
retiredBy: accumulated < samples ? 'converged' : 'count',
|
|
64
|
+
issues: app.issues,
|
|
65
|
+
adapter: app.adapterInfo,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Setup only, keeping the app alive for several frames. The caller MUST dispose it.
|
|
72
|
+
* @param {Object} options - as {@link renderHeadless}, minus `samples`
|
|
73
|
+
* @returns {Promise<PathTracerApp>}
|
|
74
|
+
*/
|
|
75
|
+
export async function openHeadless( {
|
|
76
|
+
canvas,
|
|
77
|
+
model = null,
|
|
78
|
+
width = 1920,
|
|
79
|
+
height = 1080,
|
|
80
|
+
strict = true,
|
|
81
|
+
profile = 'physical',
|
|
82
|
+
deterministic = true,
|
|
83
|
+
settings = null,
|
|
84
|
+
} = {} ) {
|
|
85
|
+
|
|
86
|
+
if ( ! canvas ) throw new Error( 'openHeadless: a canvas is required' );
|
|
87
|
+
|
|
88
|
+
const app = new PathTracerApp( canvas, { autoResize: false, strict, profile } );
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
|
|
92
|
+
// Before init(): replayed once the device exists but before the stages allocate, so a
|
|
93
|
+
// >2048 render builds the storage pool once instead of building then re-building it.
|
|
94
|
+
app.setReservedRenderResolution( Math.max( width, height ) );
|
|
95
|
+
|
|
96
|
+
await app.init();
|
|
97
|
+
app.setCanvasSize( width, height );
|
|
98
|
+
|
|
99
|
+
if ( model ) await app.loadModel( model );
|
|
100
|
+
if ( settings ) app.settings.setMany( settings, { silent: true } );
|
|
101
|
+
|
|
102
|
+
// After the model: scene metadata can turn adaptive sampling back on.
|
|
103
|
+
if ( deterministic ) app.setDeterministicMode( true );
|
|
104
|
+
|
|
105
|
+
return app;
|
|
106
|
+
|
|
107
|
+
} catch ( error ) {
|
|
108
|
+
|
|
109
|
+
app.dispose(); // a half-built app still holds GPU memory
|
|
110
|
+
throw error;
|
|
111
|
+
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
}
|