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/src/PathTracerApp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { WebGPURenderer, RectAreaLightNode, SRGBColorSpace } from 'three/webgpu';
|
|
2
2
|
import { texture as _tslTexture, cubeTexture as _tslCubeTexture } from 'three/tsl';
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
Scene, EventDispatcher, Box3
|
|
5
5
|
} from 'three';
|
|
6
6
|
import { RectAreaLightTexturesLib } from 'three/addons/lights/RectAreaLightTexturesLib.js';
|
|
7
7
|
import { SceneHelpers } from './SceneHelpers.js';
|
|
@@ -16,12 +16,15 @@ import { AutoExposure } from './Stages/AutoExposure.js';
|
|
|
16
16
|
import { Compositor } from './Stages/Compositor.js';
|
|
17
17
|
import { RenderPipeline } from './Pipeline/RenderPipeline.js';
|
|
18
18
|
import { CompletionTracker } from './Pipeline/CompletionTracker.js';
|
|
19
|
-
import { ENGINE_DEFAULTS as DEFAULT_STATE, PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, MAX_STORAGE_TEXTURE_SIZE, MAX_RESERVABLE_RENDER_SIZE, setReservedRenderSize } from './EngineDefaults.js';
|
|
19
|
+
import { ENGINE_DEFAULTS as DEFAULT_STATE, PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, MAX_STORAGE_TEXTURE_SIZE, MAX_RESERVABLE_RENDER_SIZE, setReservedRenderSize, getRenderProfile } from './EngineDefaults.js';
|
|
20
20
|
import { updateStats, updateLoading, resetLoading, setStatusCallback, getDisplaySamples, disposeObjectFromMemory, disposeRenderer } from './Processor/utils.js';
|
|
21
21
|
import { BuildTimer } from './Processor/BuildTimer.js';
|
|
22
22
|
import { createLogger, fmt } from './utils/Logger.js';
|
|
23
23
|
import { InteractionManager } from './managers/InteractionManager.js';
|
|
24
24
|
import { EngineEvents } from './EngineEvents.js';
|
|
25
|
+
import { IssueLog, ISSUE_CODES } from './EngineIssues.js';
|
|
26
|
+
import { SETTING_SOURCE } from './RenderSettings.js';
|
|
27
|
+
import { toneMapToRGBA8 } from './Processor/ToneMapCPU.js';
|
|
25
28
|
import { AssetLoader } from './Processor/AssetLoader.js';
|
|
26
29
|
import { SceneProcessor } from './Processor/SceneProcessor.js';
|
|
27
30
|
|
|
@@ -80,6 +83,32 @@ function attachDeviceLostHandler( device, holder ) {
|
|
|
80
83
|
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
const SOFTWARE_ADAPTER = /swiftshader|llvmpipe|lavapipe|basic render|microsoft basic|warp/i;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Flags software rasterizers: correct output, ~100x slower, invisible in the image.
|
|
90
|
+
* `isFallbackAdapter` only covers adapters we asked to be fallbacks, so the strings matter.
|
|
91
|
+
*
|
|
92
|
+
* @param {GPUAdapter} adapter
|
|
93
|
+
* @returns {{vendor:string, architecture:string, device:string, description:string, isSoftware:boolean}}
|
|
94
|
+
*/
|
|
95
|
+
export function describeAdapter( adapter ) {
|
|
96
|
+
|
|
97
|
+
const info = adapter.info ?? {};
|
|
98
|
+
const identity = `${info.vendor ?? ''} ${info.architecture ?? ''} ${info.device ?? ''} ${info.description ?? ''}`;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
vendor: info.vendor ?? '',
|
|
102
|
+
architecture: info.architecture ?? '',
|
|
103
|
+
device: info.device ?? '',
|
|
104
|
+
description: info.description ?? '',
|
|
105
|
+
isSoftware: info.isFallbackAdapter === true
|
|
106
|
+
|| adapter.isFallbackAdapter === true
|
|
107
|
+
|| SOFTWARE_ADAPTER.test( identity ),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
}
|
|
111
|
+
|
|
83
112
|
export class PathTracerApp extends EventDispatcher {
|
|
84
113
|
|
|
85
114
|
/**
|
|
@@ -88,6 +117,10 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
88
117
|
* @param {boolean} [options.autoResize=true] - Automatically listen for window resize events
|
|
89
118
|
* @param {HTMLElement} [options.container] - Single DOM parent the engine mounts all auxiliary
|
|
90
119
|
* elements into (HUD overlay, denoiser canvas). Defaults to `canvas.parentNode`.
|
|
120
|
+
* @param {boolean} [options.strict=false] - Throw at the point of degradation instead of
|
|
121
|
+
* rendering a plausible wrong image. See EngineIssues.js; read `app.issues` when off.
|
|
122
|
+
* @param {string} [options.profile='viewer'] - Which tuning to apply where the viewer's
|
|
123
|
+
* product decisions differ from the physical answer. See RENDER_PROFILES.
|
|
91
124
|
*
|
|
92
125
|
* The engine dispatches `EngineEvents.FRAME` after each animate() iteration so hosts can
|
|
93
126
|
* tick external instrumentation (e.g. a stats panel) without coupling the engine to it.
|
|
@@ -115,8 +148,24 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
115
148
|
this._applySceneMetadataEnabled = options.applySceneMetadata !== false;
|
|
116
149
|
this._applyingSceneMetadata = false;
|
|
117
150
|
|
|
151
|
+
// Before the settings: the profile supplies some of their defaults.
|
|
152
|
+
this._profile = getRenderProfile( options.profile );
|
|
153
|
+
|
|
154
|
+
// First, so no subsystem can degrade unrecorded.
|
|
155
|
+
this._issues = new IssueLog( {
|
|
156
|
+
strict: options.strict === true,
|
|
157
|
+
onIssue: ( issue ) => this.dispatchEvent( { type: EngineEvents.ISSUE, issue } ),
|
|
158
|
+
} );
|
|
159
|
+
|
|
118
160
|
// ── Settings (single source of truth for all render parameters) ──
|
|
119
|
-
this.settings = new RenderSettings(
|
|
161
|
+
this.settings = new RenderSettings(
|
|
162
|
+
{
|
|
163
|
+
...DEFAULT_STATE,
|
|
164
|
+
environmentRotation: this._profile.environmentRotation,
|
|
165
|
+
saturation: this._profile.saturation,
|
|
166
|
+
},
|
|
167
|
+
{ issues: this._issues }
|
|
168
|
+
);
|
|
120
169
|
|
|
121
170
|
// ── Core objects (populated in init) ──
|
|
122
171
|
this.renderer = null;
|
|
@@ -197,6 +246,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
197
246
|
this._dispatchPinned = false;
|
|
198
247
|
this._deterministicRestore = null;
|
|
199
248
|
|
|
249
|
+
/** @type {?{vendor:string, architecture:string, device:string, description:string, isSoftware:boolean}} */
|
|
250
|
+
this.adapterInfo = null;
|
|
251
|
+
|
|
200
252
|
}
|
|
201
253
|
|
|
202
254
|
/**
|
|
@@ -501,6 +553,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
501
553
|
this._removeTrackedListeners();
|
|
502
554
|
setStatusCallback( null );
|
|
503
555
|
|
|
556
|
+
this._issues.detach(); // onIssue captures `this`; see IssueLog.detach()
|
|
557
|
+
|
|
504
558
|
this.interactionManager?.deselect?.();
|
|
505
559
|
this.transformManager?.detach?.();
|
|
506
560
|
|
|
@@ -915,8 +969,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
915
969
|
* the 'load' event (_onAssetLoaded). Appending a model never applies it — an appended
|
|
916
970
|
* model must not hijack the scene's environment.
|
|
917
971
|
*
|
|
918
|
-
* Resolves to null
|
|
919
|
-
* environment. Returns null outright when there is nothing to apply.
|
|
972
|
+
* Resolves to null rather than rejecting: a failed fetch leaves the model on the current
|
|
973
|
+
* environment. Returns null outright when there is nothing to apply. Under `strict` the
|
|
974
|
+
* recorded issue throws instead, and the rejection surfaces where loadSceneData awaits it.
|
|
920
975
|
* Opt out per app with `new PathTracerApp( canvas, { applySceneMetadata: false } )`.
|
|
921
976
|
*
|
|
922
977
|
* @returns {Promise<import('three').Texture|null>|null}
|
|
@@ -935,7 +990,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
935
990
|
return this.assetLoader.loadEnvironment( env.sourceFile )
|
|
936
991
|
.catch( error => {
|
|
937
992
|
|
|
938
|
-
|
|
993
|
+
this._issues.record(
|
|
994
|
+
ISSUE_CODES.ENVIRONMENT_LOAD_FAILED,
|
|
995
|
+
`authored environment "${env.sourceFile}" failed to load — lighting falls back to the previous environment`,
|
|
996
|
+
{ sourceFile: env.sourceFile, cause: String( error?.message ?? error ) }
|
|
997
|
+
);
|
|
939
998
|
return null;
|
|
940
999
|
|
|
941
1000
|
} )
|
|
@@ -969,10 +1028,10 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
969
1028
|
|
|
970
1029
|
if ( env.rotation !== undefined ) updates.environmentRotation = env.rotation;
|
|
971
1030
|
|
|
972
|
-
this.settings.setMany( updates, { reset: false } );
|
|
1031
|
+
this.settings.setMany( updates, { reset: false, source: SETTING_SOURCE.SCENE_METADATA } );
|
|
973
1032
|
if ( this.scene ) this.scene.background = texture;
|
|
974
1033
|
|
|
975
|
-
this.dispatchEvent( { type:
|
|
1034
|
+
this.dispatchEvent( { type: EngineEvents.SCENE_METADATA_APPLIED, metadata, environment: { ...env } } );
|
|
976
1035
|
|
|
977
1036
|
}
|
|
978
1037
|
|
|
@@ -1016,7 +1075,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1016
1075
|
this._applySceneMetadataSettings( texture );
|
|
1017
1076
|
|
|
1018
1077
|
} )
|
|
1019
|
-
.catch( error =>
|
|
1078
|
+
.catch( error => this._issues.record(
|
|
1079
|
+
ISSUE_CODES.ENVIRONMENT_LOAD_FAILED,
|
|
1080
|
+
'authored environment failed to install — the scene is lit by whatever was already loaded',
|
|
1081
|
+
{ cause: String( error?.message ?? error ) }
|
|
1082
|
+
) )
|
|
1020
1083
|
.finally( () => timer.end( 'Environment fetch + CDF (concurrent)' ) );
|
|
1021
1084
|
|
|
1022
1085
|
} else if ( environmentTexture?.image?.data ) {
|
|
@@ -1571,7 +1634,12 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1571
1634
|
|
|
1572
1635
|
if ( applied < pending.requestedPx ) {
|
|
1573
1636
|
|
|
1574
|
-
|
|
1637
|
+
this._issues.warn(
|
|
1638
|
+
ISSUE_CODES.RENDER_RESERVE_CAPPED,
|
|
1639
|
+
`reserved render size ${fmt.n( pending.requestedPx )}px was capped to ${fmt.n( applied )}px by this device's limits — ` +
|
|
1640
|
+
`renders above ${fmt.n( applied )}px will be declined`,
|
|
1641
|
+
{ requested: pending.requestedPx, applied }
|
|
1642
|
+
);
|
|
1575
1643
|
|
|
1576
1644
|
}
|
|
1577
1645
|
|
|
@@ -1599,7 +1667,12 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1599
1667
|
|
|
1600
1668
|
if ( width > MAX_STORAGE_TEXTURE_SIZE || height > MAX_STORAGE_TEXTURE_SIZE ) {
|
|
1601
1669
|
|
|
1602
|
-
|
|
1670
|
+
this._issues.record(
|
|
1671
|
+
ISSUE_CODES.RENDER_SIZE_DECLINED,
|
|
1672
|
+
`render resolution ${width}×${height} exceeds the ${MAX_STORAGE_TEXTURE_SIZE}px storage reserve — resize ignored, ` +
|
|
1673
|
+
`so output stays at the previous size. Raise it with setReservedRenderResolution( ${Math.max( width, height )} ) before init().`,
|
|
1674
|
+
{ width, height, reserve: MAX_STORAGE_TEXTURE_SIZE }
|
|
1675
|
+
);
|
|
1603
1676
|
return false;
|
|
1604
1677
|
|
|
1605
1678
|
}
|
|
@@ -1704,7 +1777,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1704
1777
|
usePixelFreeze: config.usePixelFreeze ?? false,
|
|
1705
1778
|
pixelFreezeThreshold: config.pixelFreezeThreshold ?? DEFAULT_STATE.pixelFreezeThreshold,
|
|
1706
1779
|
pixelFreezeStability: config.pixelFreezeStability ?? DEFAULT_STATE.pixelFreezeStability,
|
|
1707
|
-
}, { silent: true } );
|
|
1780
|
+
}, { silent: true, source: SETTING_SOURCE.MODE_PRESET } );
|
|
1708
1781
|
|
|
1709
1782
|
// renderMode has no SETTING_ROUTES entry
|
|
1710
1783
|
this.stages.pathTracer?.setUniform( 'renderMode', parseInt( config.renderMode ) );
|
|
@@ -1923,6 +1996,27 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1923
1996
|
|
|
1924
1997
|
}
|
|
1925
1998
|
|
|
1999
|
+
/** What the engine survived rather than failed on, oldest first. @returns {Object[]} copies */
|
|
2000
|
+
get issues() {
|
|
2001
|
+
|
|
2002
|
+
return this._issues.list;
|
|
2003
|
+
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
/** Non-empty means: do not publish this frame. */
|
|
2007
|
+
get issueErrors() {
|
|
2008
|
+
|
|
2009
|
+
return this._issues.errors;
|
|
2010
|
+
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
/** Scopes the log to one render on a reused app. */
|
|
2014
|
+
clearIssues() {
|
|
2015
|
+
|
|
2016
|
+
this._issues.clear();
|
|
2017
|
+
|
|
2018
|
+
}
|
|
2019
|
+
|
|
1926
2020
|
/**
|
|
1927
2021
|
* Whether output is currently bit-reproducible. False when the dispatch heuristics
|
|
1928
2022
|
* were left active via `pinDispatch: false`, since those consume async readbacks.
|
|
@@ -1939,14 +2033,18 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1939
2033
|
* Awaits the STBN atlases first — until they land the sampler reads a constant-0.5
|
|
1940
2034
|
* placeholder that gets baked permanently into the accumulation buffer.
|
|
1941
2035
|
*
|
|
2036
|
+
* A frame retired by adaptive sampling stops advancing `frameCount`, so a fixed-count loop
|
|
2037
|
+
* can never reach `count`. `allowEarlyRetire` makes that an outcome instead of a throw.
|
|
2038
|
+
*
|
|
1942
2039
|
* @param {number} count - samples to accumulate
|
|
1943
2040
|
* @param {Object} [options]
|
|
1944
2041
|
* @param {boolean} [options.reset=true] - restart accumulation from sample 0 first
|
|
1945
2042
|
* @param {number} [options.yieldEvery=8] - yield to the event loop every N passes (0 disables)
|
|
1946
2043
|
* @param {function(number): void} [options.onProgress] - called with the running sample count
|
|
1947
|
-
* @
|
|
2044
|
+
* @param {boolean} [options.allowEarlyRetire=false]
|
|
2045
|
+
* @returns {Promise<number>} samples accumulated; below `count` only when retired early
|
|
1948
2046
|
*/
|
|
1949
|
-
async renderFrames( count, { reset = true, yieldEvery = 8, onProgress } = {} ) {
|
|
2047
|
+
async renderFrames( count, { reset = true, yieldEvery = 8, onProgress, allowEarlyRetire = false } = {} ) {
|
|
1950
2048
|
|
|
1951
2049
|
const stage = this.stages.pathTracer;
|
|
1952
2050
|
if ( ! stage ) throw new Error( 'renderFrames: app is not initialized' );
|
|
@@ -1970,6 +2068,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1970
2068
|
const maxPasses = count + 64;
|
|
1971
2069
|
let passes = 0;
|
|
1972
2070
|
|
|
2071
|
+
let retiredEarly = false;
|
|
2072
|
+
|
|
1973
2073
|
while ( stage.frameCount < target && passes < maxPasses ) {
|
|
1974
2074
|
|
|
1975
2075
|
if ( this._deviceLost ) throw new Error( 'renderFrames: WebGPU device lost' );
|
|
@@ -1978,6 +2078,14 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1978
2078
|
this.pipeline.render();
|
|
1979
2079
|
passes ++;
|
|
1980
2080
|
|
|
2081
|
+
// render() no-ops once complete; spinning would burn the budget for nothing.
|
|
2082
|
+
if ( stage.isComplete && stage.frameCount < target ) {
|
|
2083
|
+
|
|
2084
|
+
retiredEarly = true;
|
|
2085
|
+
break;
|
|
2086
|
+
|
|
2087
|
+
}
|
|
2088
|
+
|
|
1981
2089
|
onProgress?.( stage.frameCount );
|
|
1982
2090
|
|
|
1983
2091
|
if ( yieldEvery > 0 && passes % yieldEvery === 0 ) {
|
|
@@ -1988,11 +2096,15 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1988
2096
|
|
|
1989
2097
|
}
|
|
1990
2098
|
|
|
1991
|
-
if ( stage.frameCount < target ) {
|
|
2099
|
+
if ( stage.frameCount < target && ! ( retiredEarly && allowEarlyRetire ) ) {
|
|
2100
|
+
|
|
2101
|
+
const cause = retiredEarly
|
|
2102
|
+
? 'adaptive sampling retired the frame at the convergence threshold — pass ' +
|
|
2103
|
+
'`allowEarlyRetire: true`, or disable it with setDeterministicMode()'
|
|
2104
|
+
: 'something retired the render (maxSamples, a stray reset, or a canvas resize)';
|
|
1992
2105
|
|
|
1993
2106
|
throw new Error(
|
|
1994
|
-
`renderFrames:
|
|
1995
|
-
'something retired the render (maxSamples, a stray reset, or a canvas resize)'
|
|
2107
|
+
`renderFrames: stopped at ${stage.frameCount}/${target} samples after ${passes} passes — ${cause}`
|
|
1996
2108
|
);
|
|
1997
2109
|
|
|
1998
2110
|
}
|
|
@@ -2001,6 +2113,53 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2001
2113
|
|
|
2002
2114
|
}
|
|
2003
2115
|
|
|
2116
|
+
/**
|
|
2117
|
+
* The path tracer's accumulation as pixels, read from its storage target rather than the
|
|
2118
|
+
* canvas — so it works headless, works while the page is hidden, and cannot pick up a
|
|
2119
|
+
* helper overlay.
|
|
2120
|
+
*
|
|
2121
|
+
* This is `pathtracer:color`, NOT the Compositor's resolved output: denoising, bloom and
|
|
2122
|
+
* edge filtering are downstream and are absent here. Use getCanvas() when you want what
|
|
2123
|
+
* the viewport shows.
|
|
2124
|
+
*
|
|
2125
|
+
* @param {Object} [options]
|
|
2126
|
+
* @param {'linear'|'srgb'} [options.colorSpace='srgb']
|
|
2127
|
+
* @param {boolean} [options.preserveAlpha=false] - srgb only
|
|
2128
|
+
* @returns {Promise<{data: Float32Array|Uint8ClampedArray, width: number, height: number, colorSpace: string}>}
|
|
2129
|
+
*/
|
|
2130
|
+
async renderToBuffer( { colorSpace = 'srgb', preserveAlpha = false } = {} ) {
|
|
2131
|
+
|
|
2132
|
+
if ( colorSpace !== 'linear' && colorSpace !== 'srgb' ) {
|
|
2133
|
+
|
|
2134
|
+
throw new Error( `renderToBuffer: colorSpace must be 'linear' or 'srgb', got "${colorSpace}"` );
|
|
2135
|
+
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
const stage = this.stages?.pathTracer;
|
|
2139
|
+
const target = stage?.storageTextures?.readTarget;
|
|
2140
|
+
if ( ! target ) throw new Error( 'renderToBuffer: no render target — call init() and render at least one sample' );
|
|
2141
|
+
|
|
2142
|
+
// The pool over-allocates to the reserve, so the texture is larger than the frame.
|
|
2143
|
+
const { width, height } = stage;
|
|
2144
|
+
|
|
2145
|
+
const linear = await this.renderer.readRenderTargetPixelsAsync( target, 0, 0, width, height, 0 );
|
|
2146
|
+
|
|
2147
|
+
if ( colorSpace === 'linear' ) return { data: linear, width, height, colorSpace };
|
|
2148
|
+
|
|
2149
|
+
return {
|
|
2150
|
+
data: toneMapToRGBA8( linear, {
|
|
2151
|
+
exposure: this.renderer.toneMappingExposure,
|
|
2152
|
+
toneMapping: this.renderer.toneMapping,
|
|
2153
|
+
saturation: this.settings.get( 'saturation' ) ?? 1,
|
|
2154
|
+
preserveAlpha,
|
|
2155
|
+
} ),
|
|
2156
|
+
width,
|
|
2157
|
+
height,
|
|
2158
|
+
colorSpace,
|
|
2159
|
+
};
|
|
2160
|
+
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2004
2163
|
/**
|
|
2005
2164
|
* Enables WebGPU timestamp queries so {@link PathTracerApp#getGPUTimings} reports real
|
|
2006
2165
|
* GPU time. Off by default because the queries themselves cost time. No-ops when the
|
|
@@ -2580,6 +2739,20 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2580
2739
|
|
|
2581
2740
|
}
|
|
2582
2741
|
|
|
2742
|
+
this.adapterInfo = describeAdapter( adapter );
|
|
2743
|
+
if ( this.adapterInfo.isSoftware ) {
|
|
2744
|
+
|
|
2745
|
+
// Warning: the image is correct, only the cost is wrong.
|
|
2746
|
+
this._issues.warn(
|
|
2747
|
+
ISSUE_CODES.ADAPTER_SOFTWARE,
|
|
2748
|
+
`software GPU adapter "${this.adapterInfo.description || this.adapterInfo.vendor}" — ` +
|
|
2749
|
+
'renders are correct but orders of magnitude slower than hardware',
|
|
2750
|
+
{ ...this.adapterInfo }
|
|
2751
|
+
);
|
|
2752
|
+
log.warn( `software GPU adapter — check app.adapterInfo.isSoftware. ${this.adapterInfo.description}` );
|
|
2753
|
+
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2583
2756
|
const adapterLimits = adapter.limits;
|
|
2584
2757
|
|
|
2585
2758
|
this.renderer = new WebGPURenderer( {
|
|
@@ -2596,6 +2769,17 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2596
2769
|
|
|
2597
2770
|
await this.renderer.init();
|
|
2598
2771
|
|
|
2772
|
+
// WebGPURenderer swaps in WebGL2 on failure with only a warn(). The wavefront path is
|
|
2773
|
+
// compute-only, so every frame would fail against an empty canvas.
|
|
2774
|
+
if ( ! this.renderer.backend?.isWebGPUBackend ) {
|
|
2775
|
+
|
|
2776
|
+
throw new Error(
|
|
2777
|
+
'WebGPU backend unavailable — three.js fell back to WebGL2, which cannot run the ' +
|
|
2778
|
+
'path tracer\'s compute kernels. Check GPU process flags and driver support.'
|
|
2779
|
+
);
|
|
2780
|
+
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2599
2783
|
// Detect GPU device loss (dGPU/iGPU switch, driver reset, TDR watchdog on heavy
|
|
2600
2784
|
// compute). Without this the rAF loop keeps calling render() on a dead device,
|
|
2601
2785
|
// spewing errors forever. reason 'destroyed' during dispose() is intentional teardown.
|
|
@@ -2621,7 +2805,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2621
2805
|
RectAreaLightNode.setLTC( RectAreaLightTexturesLib.init() );
|
|
2622
2806
|
|
|
2623
2807
|
this.renderer.outputColorSpace = SRGBColorSpace;
|
|
2624
|
-
this.renderer.toneMapping =
|
|
2808
|
+
this.renderer.toneMapping = this._profile.toneMapping;
|
|
2625
2809
|
this.renderer.toneMappingExposure = 1.0;
|
|
2626
2810
|
this.renderer.setPixelRatio( 1.0 );
|
|
2627
2811
|
|
|
@@ -2643,8 +2827,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2643
2827
|
|
|
2644
2828
|
_initAssetPipeline() {
|
|
2645
2829
|
|
|
2646
|
-
this._sdf = new SceneProcessor();
|
|
2647
|
-
this.assetLoader = new AssetLoader(
|
|
2830
|
+
this._sdf = new SceneProcessor( { issues: this._issues } );
|
|
2831
|
+
this.assetLoader = new AssetLoader(
|
|
2832
|
+
this.meshScene, this.cameraManager.camera, this.cameraManager.controls,
|
|
2833
|
+
{ issues: this._issues, profile: this._profile }
|
|
2834
|
+
);
|
|
2648
2835
|
this.assetLoader.setRenderer( this.renderer );
|
|
2649
2836
|
this.assetLoader.createFloorPlane();
|
|
2650
2837
|
|
|
@@ -2662,7 +2849,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2662
2849
|
this._createStages();
|
|
2663
2850
|
|
|
2664
2851
|
const { clientWidth: w, clientHeight: h } = this.canvas;
|
|
2665
|
-
this.pipeline = new RenderPipeline( this.renderer, w || 1, h || 1 );
|
|
2852
|
+
this.pipeline = new RenderPipeline( this.renderer, w || 1, h || 1, { issues: this._issues } );
|
|
2666
2853
|
|
|
2667
2854
|
this.pipeline.addStage( this.stages.pathTracer );
|
|
2668
2855
|
this.pipeline.addStage( this.stages.normalDepth );
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PipelineContext } from './PipelineContext.js';
|
|
2
2
|
import { EventDispatcher } from './EventDispatcher.js';
|
|
3
3
|
import { auditStageRender } from './BindingAudit.js';
|
|
4
|
+
import { ISSUE_CODES } from '../EngineIssues.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* RenderPipeline - Orchestrates execution of pipeline stages
|
|
@@ -36,15 +37,42 @@ import { auditStageRender } from './BindingAudit.js';
|
|
|
36
37
|
*/
|
|
37
38
|
export class RenderPipeline {
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* A failed stage leaves a half-composited frame that still reaches the canvas — the exact
|
|
42
|
+
* plausible-wrong-image case a batch host must not publish.
|
|
43
|
+
* @private
|
|
44
|
+
*/
|
|
45
|
+
_reportStageFailure( name, phase, error ) {
|
|
46
|
+
|
|
47
|
+
console.error( `[Pipeline] Error in stage '${name}' (${phase}):`, error );
|
|
48
|
+
|
|
49
|
+
const key = `${name}:${phase}`;
|
|
50
|
+
if ( this._reportedStageFailures.has( key ) ) return;
|
|
51
|
+
this._reportedStageFailures.add( key );
|
|
52
|
+
|
|
53
|
+
this._issues?.record(
|
|
54
|
+
ISSUE_CODES.STAGE_RENDER_FAILED,
|
|
55
|
+
`stage "${name}" threw during ${phase} — the frame is incomplete`,
|
|
56
|
+
{ stage: name, phase, cause: String( error?.message ?? error ) }
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
/**
|
|
40
62
|
* Create a new pipeline
|
|
41
63
|
* @param {THREE.WebGLRenderer} renderer - Three.js renderer
|
|
42
64
|
* @param {number} width - Viewport width
|
|
43
65
|
* @param {number} height - Viewport height
|
|
66
|
+
* @param {Object} [options]
|
|
67
|
+
* @param {import('../EngineIssues.js').IssueLog} [options.issues] - records stage failures
|
|
44
68
|
*/
|
|
45
|
-
constructor( renderer, width, height ) {
|
|
69
|
+
constructor( renderer, width, height, { issues = null } = {} ) {
|
|
46
70
|
|
|
47
71
|
this.renderer = renderer;
|
|
72
|
+
this._issues = issues;
|
|
73
|
+
|
|
74
|
+
// A stage that throws does so every frame; record the first per stage, not 60/second.
|
|
75
|
+
this._reportedStageFailures = new Set();
|
|
48
76
|
this.width = width;
|
|
49
77
|
this.height = height;
|
|
50
78
|
|
|
@@ -215,8 +243,7 @@ export class RenderPipeline {
|
|
|
215
243
|
|
|
216
244
|
} catch ( error ) {
|
|
217
245
|
|
|
218
|
-
|
|
219
|
-
// Continue pipeline execution despite error
|
|
246
|
+
this._reportStageFailure( stage.name, 'render', error );
|
|
220
247
|
|
|
221
248
|
}
|
|
222
249
|
|
|
@@ -278,7 +305,7 @@ export class RenderPipeline {
|
|
|
278
305
|
|
|
279
306
|
} catch ( error ) {
|
|
280
307
|
|
|
281
|
-
|
|
308
|
+
this._reportStageFailure( stage.name, 'reset', error );
|
|
282
309
|
|
|
283
310
|
}
|
|
284
311
|
|
|
@@ -16,6 +16,8 @@ import { BuildTimer } from './BuildTimer.js';
|
|
|
16
16
|
import { getAssetConfig } from '../AssetConfig.js';
|
|
17
17
|
import { loadPBRTScene, pickEntryPath } from './PBRT/index.js';
|
|
18
18
|
import { extractSceneMetadata } from './SceneMetadata.js';
|
|
19
|
+
import { ISSUE_CODES } from '../EngineIssues.js';
|
|
20
|
+
import { getRenderProfile } from '../EngineDefaults.js';
|
|
19
21
|
|
|
20
22
|
// Define supported file formats
|
|
21
23
|
const SUPPORTED_FORMATS = {
|
|
@@ -54,7 +56,7 @@ function standInForSplit( source ) {
|
|
|
54
56
|
*/
|
|
55
57
|
export class AssetLoader extends EventDispatcher {
|
|
56
58
|
|
|
57
|
-
constructor( scene, camera, controls ) {
|
|
59
|
+
constructor( scene, camera, controls, { issues = null, profile = null } = {} ) {
|
|
58
60
|
|
|
59
61
|
super();
|
|
60
62
|
this.scene = scene;
|
|
@@ -78,6 +80,22 @@ export class AssetLoader extends EventDispatcher {
|
|
|
78
80
|
this._loadingManager = new LoadingManager();
|
|
79
81
|
this._loadCancelled = false;
|
|
80
82
|
|
|
83
|
+
this._issues = issues;
|
|
84
|
+
this._profile = profile ?? getRenderProfile();
|
|
85
|
+
|
|
86
|
+
// A glTF whose external texture 404s still loads. Only place the engine sees the URL.
|
|
87
|
+
// ZIP paths build their own managers and are not covered.
|
|
88
|
+
this._loadingManager.onError = ( url ) => {
|
|
89
|
+
|
|
90
|
+
if ( this._loadCancelled ) return;
|
|
91
|
+
this._issues?.record(
|
|
92
|
+
ISSUE_CODES.ASSET_UNREACHABLE,
|
|
93
|
+
`asset "${url}" could not be fetched — anything depending on it renders without it`,
|
|
94
|
+
{ url }
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
};
|
|
98
|
+
|
|
81
99
|
}
|
|
82
100
|
|
|
83
101
|
/**
|
|
@@ -1744,9 +1762,10 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1744
1762
|
// Intensity is authored in Watts (Blender-style); emitted radiance
|
|
1745
1763
|
// is computed at render time as power/(π·area). Blender emission
|
|
1746
1764
|
// defaults: power-normalized, full Lambertian spread (π), rectangle.
|
|
1765
|
+
// Viewer tuning, not physics — see RENDER_PROFILES.
|
|
1747
1766
|
const light = new RectAreaLight(
|
|
1748
1767
|
new Color( ...userData.color ),
|
|
1749
|
-
userData.intensity *
|
|
1768
|
+
userData.intensity * this._profile.areaLightIntensityScale,
|
|
1750
1769
|
userData.width,
|
|
1751
1770
|
userData.height
|
|
1752
1771
|
);
|
|
@@ -1879,6 +1898,10 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1879
1898
|
// Clear the internal listener map directly so handlers don't retain references.
|
|
1880
1899
|
this._listeners = undefined;
|
|
1881
1900
|
|
|
1901
|
+
// onError captures `this`, and a manager outlives the loader via an in-flight fetch.
|
|
1902
|
+
this._loadingManager.onError = undefined;
|
|
1903
|
+
this._issues = null;
|
|
1904
|
+
|
|
1882
1905
|
this.releaseTargetModel();
|
|
1883
1906
|
|
|
1884
1907
|
}
|
|
@@ -33,6 +33,7 @@ export class SceneProcessor {
|
|
|
33
33
|
* @param {boolean} [options.useFloat32Array=true] - Use Float32Array for triangle data
|
|
34
34
|
* @param {string} [options.textureQuality='adaptive'] - Texture quality mode
|
|
35
35
|
* @param {boolean} [options.enableTextureCache=true] - Enable texture caching
|
|
36
|
+
* @param {import('../EngineIssues.js').IssueLog} [options.issues] - forwarded to TextureCreator
|
|
36
37
|
*/
|
|
37
38
|
constructor( options = {} ) {
|
|
38
39
|
|
|
@@ -160,7 +161,7 @@ export class SceneProcessor {
|
|
|
160
161
|
} );
|
|
161
162
|
|
|
162
163
|
// Create and configure texture creator
|
|
163
|
-
this.textureCreator = new TextureCreator( { maxTextureSize: this.config.maxTextureSize } );
|
|
164
|
+
this.textureCreator = new TextureCreator( { maxTextureSize: this.config.maxTextureSize, issues: this.config.issues } );
|
|
164
165
|
// The optimized TextureCreator will auto-detect capabilities and select optimal methods
|
|
165
166
|
|
|
166
167
|
// Create emissive triangle builder for direct lighting
|