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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "7.27.1",
3
+ "version": "7.28.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -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: 4,
57
+ toneMapping: RENDER_PROFILES.viewer.toneMapping,
19
58
  exposure: 1,
20
- saturation: 1.2,
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: 270.0,
72
+ environmentRotation: RENDER_PROFILES.viewer.environmentRotation,
34
73
  groundProjectionEnabled: false,
35
74
  groundProjectionRadius: 100,
36
75
  groundProjectionHeight: 15,
@@ -55,4 +55,10 @@ export const EngineEvents = {
55
55
  // Lifecycle
56
56
  DISPOSE: 'engine:dispose',
57
57
  DEVICE_LOST: 'engine:deviceLost',
58
+
59
+ // Degradation
60
+ ISSUE: 'engine:issue',
61
+
62
+ // Scene metadata
63
+ SCENE_METADATA_APPLIED: 'engine:sceneMetadataApplied',
58
64
  };
@@ -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
+ }
@@ -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
+ }