rayzee 9.1.1 → 9.2.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 +174 -15
- package/dist/{NeuralRetouch-BRquqfmZ.js → NeuralRetouch-DgnfldWO.js} +1 -1
- package/dist/{NeuralSuperRes-CbNvQjCw.js → NeuralSuperRes-Baa2w8VU.js} +72 -40
- package/dist/{ToneMapGPU-DpUZDMbQ.js → ToneMapGPU-CNgexVto.js} +75 -71
- package/dist/rayzee.es.js +4704 -4103
- package/dist/rayzee.umd.js +96 -96
- package/package.json +3 -2
- package/src/AssetConfig.js +10 -5
- package/src/EngineEvents.js +41 -1
- package/src/EngineIssues.js +3 -0
- package/src/Headless.js +23 -7
- package/src/HeadlessCanvas.js +104 -0
- package/src/Passes/OIDNDenoiser.js +29 -4
- package/src/Passes/OIDNTemporalHistory.js +7 -0
- package/src/PathTracerApp.js +432 -119
- package/src/Platform.js +76 -0
- package/src/Processor/AssetLoader.js +66 -4
- package/src/Processor/BVHBuilder.js +2 -1
- package/src/Processor/EquirectHDRInfo.js +2 -1
- package/src/Processor/HostMemory.js +14 -0
- package/src/Processor/IESParser.js +1 -2
- package/src/Processor/PBRT/PBRTSceneBuilder.js +26 -0
- package/src/Processor/ParallelBVHBuilder.js +7 -6
- package/src/Processor/PlatformImageLoader.js +118 -0
- package/src/Processor/ResampleRGBA8.js +102 -0
- package/src/Processor/SceneProcessor.js +8 -8
- package/src/Processor/TextureCreator.js +162 -13
- package/src/Processor/Workers/PackWorker.js +28 -0
- package/src/Stages/Compositor.js +25 -6
- package/src/Stages/PathTracer.js +143 -26
- package/src/Stages/PathTracerStage.js +7 -98
- package/src/TSL/DebugKernel.js +1 -1
- package/src/TSL/EmissiveSampling.js +1 -1
- package/src/TSL/GenerateKernel.js +3 -7
- package/src/TSL/Random.js +3 -120
- package/src/TSL/ShadeKernel.js +6 -6
- package/src/index.js +2 -1
- package/src/managers/CameraManager.js +2 -2
- package/src/managers/DenoisingManager.js +77 -8
- package/src/managers/GoboManager.js +8 -1
- package/src/managers/IESManager.js +13 -2
- package/src/managers/InteractionManager.js +1 -1
- package/src/managers/LightManager.js +31 -0
- package/src/managers/OverlayManager.js +11 -1
- package/src/managers/VideoRenderManager.js +8 -0
- package/src/node/NodeWorker.js +144 -0
- package/src/node/index.js +9 -0
- package/src/node/nodePlatform.js +86 -0
|
@@ -363,7 +363,7 @@ export class CameraManager extends EventDispatcher {
|
|
|
363
363
|
onReset?.();
|
|
364
364
|
|
|
365
365
|
this.dispatchEvent( {
|
|
366
|
-
type:
|
|
366
|
+
type: EngineEvents.CAMERA_SWITCHED, cameraIndex: index, effects: this._captureEffects(), fov: this.camera.fov,
|
|
367
367
|
cameraProjection: this._getSettings?.( 'cameraProjection' ),
|
|
368
368
|
} );
|
|
369
369
|
|
|
@@ -717,7 +717,7 @@ export class CameraManager extends EventDispatcher {
|
|
|
717
717
|
this.resetAutoFocus();
|
|
718
718
|
this._reportOrthoHeight();
|
|
719
719
|
this.dispatchEvent( {
|
|
720
|
-
type:
|
|
720
|
+
type: EngineEvents.CAMERA_SWITCHED, cameraIndex: this.currentCameraIndex, effects: this._captureEffects(), fov: this.camera.fov,
|
|
721
721
|
cameraProjection: this._getSettings?.( 'cameraProjection' ),
|
|
722
722
|
} );
|
|
723
723
|
|
|
@@ -3,6 +3,7 @@ import { OIDNDenoiser } from '../Passes/OIDNDenoiser.js';
|
|
|
3
3
|
import { OIDNTemporalHistory } from '../Passes/OIDNTemporalHistory.js';
|
|
4
4
|
import { AIUpscaler } from '../Passes/AIUpscaler.js';
|
|
5
5
|
import { EngineEvents } from '../EngineEvents.js';
|
|
6
|
+
import { ISSUE_CODES } from '../EngineIssues.js';
|
|
6
7
|
import { createLogger } from '../utils/Logger.js';
|
|
7
8
|
|
|
8
9
|
// The neural passes live in `../neural/` but report through the manager that drives them, so they
|
|
@@ -100,12 +101,14 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
100
101
|
* @param {import('../Pipeline/RenderPipeline.js').RenderPipeline} params.pipeline
|
|
101
102
|
* @param {Function} params.getExposure - () => current exposure value
|
|
102
103
|
* @param {Function} params.getSaturation - () => current saturation value
|
|
104
|
+
* @param {import('../EngineIssues.js').IssueLog} [params.issues]
|
|
103
105
|
*/
|
|
104
|
-
constructor( { renderer, mainCanvas, stages, pipeline, getExposure, getSaturation } ) {
|
|
106
|
+
constructor( { renderer, mainCanvas, stages, pipeline, getExposure, getSaturation, issues = null } ) {
|
|
105
107
|
|
|
106
108
|
super();
|
|
107
109
|
|
|
108
110
|
this.renderer = renderer;
|
|
111
|
+
this._issues = issues;
|
|
109
112
|
this.mainCanvas = mainCanvas;
|
|
110
113
|
this.upscalerCanvas = this._createUpscalerCanvas( mainCanvas );
|
|
111
114
|
this.pipeline = pipeline;
|
|
@@ -183,6 +186,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
183
186
|
this._denoiserStartHandler = null;
|
|
184
187
|
this._denoiserEndHandler = null;
|
|
185
188
|
this._denoiserTileHandler = null;
|
|
189
|
+
this._denoiserErrorHandler = null;
|
|
186
190
|
this._upscalerResChangedHandler = null;
|
|
187
191
|
this._upscalerStartHandler = null;
|
|
188
192
|
this._upscalerProgressHandler = null;
|
|
@@ -214,6 +218,23 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
214
218
|
|
|
215
219
|
}
|
|
216
220
|
|
|
221
|
+
/** GPU bytes of OIDN's inputs, output and motion history. */
|
|
222
|
+
gpuBytes() {
|
|
223
|
+
|
|
224
|
+
return ( this.denoiser?.gpuBytes?.() ?? 0 ) + ( this._history?.gpuBytes?.() ?? 0 );
|
|
225
|
+
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
_recordNoOverlayCanvas( pass ) {
|
|
229
|
+
|
|
230
|
+
this._issues?.record(
|
|
231
|
+
ISSUE_CODES.DENOISER_UNAVAILABLE,
|
|
232
|
+
`the ${pass} pass needs the render canvas to be in a document, and it is not — skipped`,
|
|
233
|
+
{ pass }
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
}
|
|
237
|
+
|
|
217
238
|
/**
|
|
218
239
|
* Updates the render resolution and propagates to denoiser/upscaler.
|
|
219
240
|
* @param {number} width
|
|
@@ -280,8 +301,6 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
280
301
|
*/
|
|
281
302
|
setupDenoiser() {
|
|
282
303
|
|
|
283
|
-
if ( ! this.upscalerCanvas ) return;
|
|
284
|
-
|
|
285
304
|
const pt = this._stages.pathTracer;
|
|
286
305
|
|
|
287
306
|
// No canvas: the denoiser hands its result to the pipeline as a picture, and the
|
|
@@ -335,9 +354,17 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
335
354
|
|
|
336
355
|
};
|
|
337
356
|
|
|
357
|
+
// Only the weight load names a URL; the init wrapper re-reports that same failure.
|
|
358
|
+
this._denoiserErrorHandler = e => e.url && this._issues?.record(
|
|
359
|
+
ISSUE_CODES.ASSET_UNREACHABLE,
|
|
360
|
+
`denoiser weights "${e.url}" could not be loaded — the render is not denoised`,
|
|
361
|
+
{ url: e.url, asset: 'oidn-weights', cause: String( e.error?.message ?? e.error ) }
|
|
362
|
+
);
|
|
363
|
+
|
|
338
364
|
this.denoiser.addEventListener( 'start', this._denoiserStartHandler );
|
|
339
365
|
this.denoiser.addEventListener( 'end', this._denoiserEndHandler );
|
|
340
366
|
this.denoiser.addEventListener( 'tileProgress', this._denoiserTileHandler );
|
|
367
|
+
this.denoiser.addEventListener( 'error', this._denoiserErrorHandler );
|
|
341
368
|
|
|
342
369
|
}
|
|
343
370
|
|
|
@@ -384,7 +411,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
384
411
|
|
|
385
412
|
// Forward lifecycle events (store refs for removal on re-setup / dispose)
|
|
386
413
|
this._upscalerResChangedHandler = ( e ) =>
|
|
387
|
-
this.dispatchEvent( { type:
|
|
414
|
+
this.dispatchEvent( { type: EngineEvents.RESOLUTION_CHANGED, width: e.width, height: e.height } );
|
|
388
415
|
this._upscalerStartHandler = () =>
|
|
389
416
|
this.dispatchEvent( { type: EngineEvents.UPSCALING_START } );
|
|
390
417
|
this._upscalerProgressHandler = ( e ) =>
|
|
@@ -1101,7 +1128,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
1101
1128
|
// The nominal size, not the path tracer's: that one is smaller while the camera moves.
|
|
1102
1129
|
if ( wasShowing && this._lastRenderWidth && this._lastRenderHeight ) {
|
|
1103
1130
|
|
|
1104
|
-
this.dispatchEvent( { type:
|
|
1131
|
+
this.dispatchEvent( { type: EngineEvents.RESOLUTION_CHANGED, width: this._lastRenderWidth, height: this._lastRenderHeight } );
|
|
1105
1132
|
|
|
1106
1133
|
}
|
|
1107
1134
|
|
|
@@ -1166,7 +1193,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
1166
1193
|
|
|
1167
1194
|
if ( ! this.upscalerCanvas ) {
|
|
1168
1195
|
|
|
1169
|
-
|
|
1196
|
+
this._recordNoOverlayCanvas( 'neural' );
|
|
1170
1197
|
return;
|
|
1171
1198
|
|
|
1172
1199
|
}
|
|
@@ -1282,7 +1309,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
1282
1309
|
if ( this.mainCanvas ) this.mainCanvas.style.opacity = '0';
|
|
1283
1310
|
|
|
1284
1311
|
// The host shows the output dimensions; without this they keep reading the render size.
|
|
1285
|
-
this.dispatchEvent( { type:
|
|
1312
|
+
this.dispatchEvent( { type: EngineEvents.RESOLUTION_CHANGED, width: image.width, height: image.height } );
|
|
1286
1313
|
|
|
1287
1314
|
} catch ( error ) {
|
|
1288
1315
|
|
|
@@ -1297,7 +1324,45 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
1297
1324
|
|
|
1298
1325
|
}
|
|
1299
1326
|
|
|
1300
|
-
|
|
1327
|
+
/**
|
|
1328
|
+
* One denoise of the accumulation at the final tier, resolved once its picture is published.
|
|
1329
|
+
* Unlike onRenderComplete(), leaves the render loop and the upscaler alone.
|
|
1330
|
+
* @returns {Promise<boolean>} whether a denoised picture was published
|
|
1331
|
+
*/
|
|
1332
|
+
async denoiseOnce() {
|
|
1333
|
+
|
|
1334
|
+
const dn = this.denoiser;
|
|
1335
|
+
if ( ! dn?.enabled ) return false;
|
|
1336
|
+
|
|
1337
|
+
this._cleanupCompletionListener();
|
|
1338
|
+
this._resetCadence();
|
|
1339
|
+
this._useFinalQuality();
|
|
1340
|
+
|
|
1341
|
+
// start() refuses during a run and only defers during a weight load.
|
|
1342
|
+
while ( dn.state.isLoading || dn.state.isDenoising ) {
|
|
1343
|
+
|
|
1344
|
+
await ( dn.state.isLoading ? dn.whenLoaded() : new Promise( ( resolve ) => {
|
|
1345
|
+
|
|
1346
|
+
const onEnd = () => {
|
|
1347
|
+
|
|
1348
|
+
dn.removeEventListener( 'end', onEnd );
|
|
1349
|
+
resolve();
|
|
1350
|
+
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
dn.addEventListener( 'end', onEnd );
|
|
1354
|
+
|
|
1355
|
+
} ) );
|
|
1356
|
+
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
const ok = await dn.start();
|
|
1360
|
+
if ( ok ) this._onDisplayRefresh?.();
|
|
1361
|
+
return ok && dn.hasOutput;
|
|
1362
|
+
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
onRenderComplete( { isStillComplete } ) {
|
|
1301
1366
|
|
|
1302
1367
|
// Remove any stale completion-chain listener from a previous render cycle
|
|
1303
1368
|
this._cleanupCompletionListener();
|
|
@@ -1459,6 +1524,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
1459
1524
|
if ( this._denoiserStartHandler ) this.denoiser.removeEventListener( 'start', this._denoiserStartHandler );
|
|
1460
1525
|
if ( this._denoiserEndHandler ) this.denoiser.removeEventListener( 'end', this._denoiserEndHandler );
|
|
1461
1526
|
if ( this._denoiserTileHandler ) this.denoiser.removeEventListener( 'tileProgress', this._denoiserTileHandler );
|
|
1527
|
+
if ( this._denoiserErrorHandler ) this.denoiser.removeEventListener( 'error', this._denoiserErrorHandler );
|
|
1462
1528
|
this.denoiser.dispose();
|
|
1463
1529
|
this.denoiser = null;
|
|
1464
1530
|
|
|
@@ -1482,6 +1548,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
1482
1548
|
this._denoiserStartHandler = null;
|
|
1483
1549
|
this._denoiserEndHandler = null;
|
|
1484
1550
|
this._denoiserTileHandler = null;
|
|
1551
|
+
this._denoiserErrorHandler = null;
|
|
1485
1552
|
this._upscalerResChangedHandler = null;
|
|
1486
1553
|
this._upscalerStartHandler = null;
|
|
1487
1554
|
this._upscalerProgressHandler = null;
|
|
@@ -1490,6 +1557,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
1490
1557
|
this._onReset = null;
|
|
1491
1558
|
this._onPostProcessRefresh = null;
|
|
1492
1559
|
this._onDisplayRefresh = null;
|
|
1560
|
+
this._issues = null;
|
|
1493
1561
|
|
|
1494
1562
|
if ( this.upscalerCanvas?.parentNode ) {
|
|
1495
1563
|
|
|
@@ -1669,6 +1737,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
1669
1737
|
setUpscalerEnabled( enabled ) {
|
|
1670
1738
|
|
|
1671
1739
|
if ( this.upscaler ) this.upscaler.enabled = enabled;
|
|
1740
|
+
else if ( enabled ) this._recordNoOverlayCanvas( 'upscaler' );
|
|
1672
1741
|
// The enlarged picture describes a setting that is no longer on.
|
|
1673
1742
|
if ( ! enabled ) this._restoreRenderDisplay();
|
|
1674
1743
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DataArrayTexture, LinearFilter, RGBAFormat, UnsignedByteType } from 'three';
|
|
2
2
|
import { cachedObjectURL } from '../Storage/DownloadCache.js';
|
|
3
|
+
import { ISSUE_CODES } from '../EngineIssues.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Manages projection masks ("gobos" / "cookies") for spot lights.
|
|
@@ -33,6 +34,7 @@ export class GoboManager {
|
|
|
33
34
|
|
|
34
35
|
this.pathTracer = pathTracer;
|
|
35
36
|
this._onReset = options.onReset || null;
|
|
37
|
+
this._issues = options.issues ?? null;
|
|
36
38
|
|
|
37
39
|
/** @type {DataArrayTexture | null} */
|
|
38
40
|
this.texture = null;
|
|
@@ -140,7 +142,11 @@ export class GoboManager {
|
|
|
140
142
|
const entry = this.entries.find( e => e.name === name );
|
|
141
143
|
if ( ! entry ) {
|
|
142
144
|
|
|
143
|
-
|
|
145
|
+
this._issues?.record(
|
|
146
|
+
ISSUE_CODES.ASSET_UNREACHABLE,
|
|
147
|
+
`no gobo named "${name}" is loaded — the light projects without one`,
|
|
148
|
+
{ name, asset: 'gobo' }
|
|
149
|
+
);
|
|
144
150
|
return false;
|
|
145
151
|
|
|
146
152
|
}
|
|
@@ -237,6 +243,7 @@ export class GoboManager {
|
|
|
237
243
|
this.entries = [];
|
|
238
244
|
this.pathTracer = null;
|
|
239
245
|
this._onReset = null;
|
|
246
|
+
this._issues = null;
|
|
240
247
|
|
|
241
248
|
}
|
|
242
249
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DataArrayTexture, LinearFilter, RGBAFormat, UnsignedByteType } from 'three';
|
|
2
2
|
import { parseIES, resampleIESToGrid, deriveIESBeamAngle, deriveIESPenumbra } from '../Processor/IESParser.js';
|
|
3
3
|
import { sharedDownloads } from '../Storage/DownloadCache.js';
|
|
4
|
+
import { ISSUE_CODES } from '../EngineIssues.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Manages IES photometric profiles for spot lights.
|
|
@@ -31,6 +32,7 @@ export class IESManager {
|
|
|
31
32
|
|
|
32
33
|
this.pathTracer = pathTracer;
|
|
33
34
|
this._onReset = options.onReset || null;
|
|
35
|
+
this._issues = options.issues ?? null;
|
|
34
36
|
|
|
35
37
|
/** @type {DataArrayTexture | null} */
|
|
36
38
|
this.texture = null;
|
|
@@ -72,7 +74,11 @@ export class IESManager {
|
|
|
72
74
|
|
|
73
75
|
} catch ( err ) {
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
this._issues?.warn(
|
|
78
|
+
ISSUE_CODES.ASSET_UNREACHABLE,
|
|
79
|
+
`IES profile "${it.name}" could not be loaded: ${err.message}`,
|
|
80
|
+
{ url: it.url, asset: 'ies', cause: err.message }
|
|
81
|
+
);
|
|
76
82
|
return null;
|
|
77
83
|
|
|
78
84
|
}
|
|
@@ -188,7 +194,11 @@ export class IESManager {
|
|
|
188
194
|
const entry = this.entries.find( e => e.name === name );
|
|
189
195
|
if ( ! entry ) {
|
|
190
196
|
|
|
191
|
-
|
|
197
|
+
this._issues?.record(
|
|
198
|
+
ISSUE_CODES.ASSET_UNREACHABLE,
|
|
199
|
+
`no IES profile named "${name}" is loaded — the light keeps its plain distribution`,
|
|
200
|
+
{ name, asset: 'ies' }
|
|
201
|
+
);
|
|
192
202
|
return empty;
|
|
193
203
|
|
|
194
204
|
}
|
|
@@ -249,6 +259,7 @@ export class IESManager {
|
|
|
249
259
|
this.entries = [];
|
|
250
260
|
this.pathTracer = null;
|
|
251
261
|
this._onReset = null;
|
|
262
|
+
this._issues = null;
|
|
252
263
|
|
|
253
264
|
}
|
|
254
265
|
|
|
@@ -809,7 +809,7 @@ export class InteractionManager extends EventDispatcher {
|
|
|
809
809
|
this.addEventListener( 'focusChanged', ( event ) => {
|
|
810
810
|
|
|
811
811
|
app.settings.set( 'focusDistance', event.worldDistance );
|
|
812
|
-
app.dispatchEvent( { type:
|
|
812
|
+
app.dispatchEvent( { type: EngineEvents.FOCUS_CHANGED, distance: event.distance } );
|
|
813
813
|
|
|
814
814
|
} );
|
|
815
815
|
|
|
@@ -109,6 +109,37 @@ export class LightManager extends EventDispatcher {
|
|
|
109
109
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* The light the path tracer traces, by UUID from getAll(). A model's own lights are copied in
|
|
114
|
+
* at load, so editing those does nothing: edit this one, then call sync().
|
|
115
|
+
* @param {string} uuid
|
|
116
|
+
* @returns {?import('three').Light}
|
|
117
|
+
*/
|
|
118
|
+
getLight( uuid ) {
|
|
119
|
+
|
|
120
|
+
const light = this.scene.getObjectByProperty( 'uuid', uuid );
|
|
121
|
+
return light?.isLight ? light : null;
|
|
122
|
+
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Sets a traced light's intensity (watts, or W/m² for directional — see the README) and uploads it.
|
|
127
|
+
* @param {string} uuid
|
|
128
|
+
* @param {number} intensity
|
|
129
|
+
* @returns {boolean} false when no traced light has that UUID
|
|
130
|
+
*/
|
|
131
|
+
setIntensity( uuid, intensity ) {
|
|
132
|
+
|
|
133
|
+
const light = this.getLight( uuid );
|
|
134
|
+
if ( ! light ) return false;
|
|
135
|
+
|
|
136
|
+
light.intensity = intensity;
|
|
137
|
+
this.updateLights();
|
|
138
|
+
this._onReset?.();
|
|
139
|
+
return true;
|
|
140
|
+
|
|
141
|
+
}
|
|
142
|
+
|
|
112
143
|
/**
|
|
113
144
|
* Removes a light by UUID.
|
|
114
145
|
* @param {string} uuid
|
|
@@ -3,6 +3,7 @@ import { OutlineHelper } from './helpers/OutlineHelper.js';
|
|
|
3
3
|
import { ViewOverlayRenderer } from './helpers/ViewOverlayRenderer.js';
|
|
4
4
|
import { createOverlayCanvas, viewPixelSize } from './helpers/overlaySurface.js';
|
|
5
5
|
import { createLogger } from '../utils/Logger.js';
|
|
6
|
+
import { EngineEvents } from '../EngineEvents.js';
|
|
6
7
|
|
|
7
8
|
const log = createLogger( 'overlay' );
|
|
8
9
|
|
|
@@ -84,6 +85,15 @@ export class OverlayManager {
|
|
|
84
85
|
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
/** The overlay surface while it is shown: one presented image and three's output buffers. */
|
|
89
|
+
gpuResources() {
|
|
90
|
+
|
|
91
|
+
const view = this._viewOverlay;
|
|
92
|
+
if ( ! view?.isVisible ) return [];
|
|
93
|
+
return [ { bytes: view.canvas.width * view.canvas.height * 4 }, ...( view.renderer?._frameBufferTargets?.values() ?? [] ) ];
|
|
94
|
+
|
|
95
|
+
}
|
|
96
|
+
|
|
87
97
|
/**
|
|
88
98
|
* Sets the SceneHelpers instance used for 3D overlay rendering.
|
|
89
99
|
* @param {import('../SceneHelpers.js').SceneHelpers} helperScene
|
|
@@ -149,7 +159,7 @@ export class OverlayManager {
|
|
|
149
159
|
|
|
150
160
|
tileHelper.setRenderSize( renderWidth || 1, renderHeight || 1 );
|
|
151
161
|
|
|
152
|
-
app.addEventListener(
|
|
162
|
+
app.addEventListener( EngineEvents.RESOLUTION_CHANGED, ( e ) => {
|
|
153
163
|
|
|
154
164
|
tileHelper.setRenderSize( e.width, e.height );
|
|
155
165
|
|
|
@@ -219,6 +219,14 @@ export class VideoRenderManager {
|
|
|
219
219
|
|
|
220
220
|
while ( ! pathTracer.isComplete && ! this._cancelled ) {
|
|
221
221
|
|
|
222
|
+
const wait = pathTracer.readbackWait();
|
|
223
|
+
if ( wait ) {
|
|
224
|
+
|
|
225
|
+
await wait;
|
|
226
|
+
continue;
|
|
227
|
+
|
|
228
|
+
}
|
|
229
|
+
|
|
222
230
|
app.cameraManager.camera.updateMatrixWorld();
|
|
223
231
|
app.pipeline.render();
|
|
224
232
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Worker as ThreadWorker } from 'node:worker_threads';
|
|
2
|
+
import { Buffer, resolveObjectURL } from 'node:buffer';
|
|
3
|
+
|
|
4
|
+
// Evaluated inside the thread: a dedicated worker's `self` over parentPort, then the module. Messages
|
|
5
|
+
// that arrive before the module has installed its handler are held, not dropped. No require(): a
|
|
6
|
+
// thread inherits --input-type=module, which evaluates this as an ES module.
|
|
7
|
+
const BOOTSTRAP = `
|
|
8
|
+
const { parentPort } = process.getBuiltinModule( 'node:worker_threads' );
|
|
9
|
+
const listeners = { message: new Set(), error: new Set(), messageerror: new Set() };
|
|
10
|
+
const held = [];
|
|
11
|
+
let ready = false;
|
|
12
|
+
|
|
13
|
+
const transferable = ( list ) => ( list ?? [] ).filter( ( item ) => item instanceof ArrayBuffer || item?.constructor?.name === 'MessagePort' );
|
|
14
|
+
const deliver = ( data ) => {
|
|
15
|
+
|
|
16
|
+
const event = { type: 'message', data };
|
|
17
|
+
globalThis.onmessage?.( event );
|
|
18
|
+
for ( const fn of listeners.message ) fn( event );
|
|
19
|
+
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
globalThis.self = globalThis;
|
|
23
|
+
globalThis.onmessage = null;
|
|
24
|
+
globalThis.postMessage = ( data, transfer ) => parentPort.postMessage( data, transferable( Array.isArray( transfer ) ? transfer : transfer?.transfer ) );
|
|
25
|
+
globalThis.addEventListener = ( type, fn ) => listeners[ type ]?.add( fn );
|
|
26
|
+
globalThis.removeEventListener = ( type, fn ) => listeners[ type ]?.delete( fn );
|
|
27
|
+
globalThis.close = () => process.exit( 0 );
|
|
28
|
+
|
|
29
|
+
parentPort.on( 'message', ( message ) => {
|
|
30
|
+
|
|
31
|
+
if ( message?.__rayzeeModule !== undefined ) {
|
|
32
|
+
|
|
33
|
+
const release = () => {
|
|
34
|
+
|
|
35
|
+
ready = true;
|
|
36
|
+
for ( const data of held.splice( 0 ) ) deliver( data );
|
|
37
|
+
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// A classic worker is a script in the global scope (three's Draco decoder is one).
|
|
41
|
+
if ( message.classic ) {
|
|
42
|
+
|
|
43
|
+
process.getBuiltinModule( 'node:vm' ).runInThisContext( message.__rayzeeModule );
|
|
44
|
+
release();
|
|
45
|
+
|
|
46
|
+
} else {
|
|
47
|
+
|
|
48
|
+
import( 'data:text/javascript;charset=utf-8,' + encodeURIComponent( message.__rayzeeModule ) ).then( release, ( error ) => { throw error; } );
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return;
|
|
53
|
+
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if ( ready ) deliver( message );
|
|
57
|
+
else held.push( message );
|
|
58
|
+
|
|
59
|
+
} );
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
const DATA_URL = /^data:([^,]*?)(;base64)?,(.*)$/s;
|
|
63
|
+
|
|
64
|
+
function readModuleSource( url ) {
|
|
65
|
+
|
|
66
|
+
const href = String( url );
|
|
67
|
+
|
|
68
|
+
if ( href.startsWith( 'blob:' ) ) {
|
|
69
|
+
|
|
70
|
+
const blob = resolveObjectURL( href );
|
|
71
|
+
if ( ! blob ) throw new Error( `NodeWorker: no blob behind ${href}` );
|
|
72
|
+
return blob.text();
|
|
73
|
+
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const match = DATA_URL.exec( href );
|
|
77
|
+
if ( ! match ) throw new Error( `NodeWorker: only data: and blob: worker URLs are supported, got ${href.slice( 0, 64 )}` );
|
|
78
|
+
const [ , , base64, body ] = match;
|
|
79
|
+
return base64 ? Buffer.from( body, 'base64' ).toString( 'utf8' ) : decodeURIComponent( body );
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The Web Worker API over `worker_threads`, for hosts with no browser (see nodePlatform). It runs
|
|
85
|
+
* `data:` and `blob:` workers, module or classic: the engine's own, which the published build inlines,
|
|
86
|
+
* and three.js's. Those call the global `Worker`, so a model using Draco or KTX2 also needs
|
|
87
|
+
* `globalThis.Worker = NodeWorker`. ArrayBuffers transfer and
|
|
88
|
+
* SharedArrayBuffers share as in a browser; an ImageBitmap cannot exist here to transfer.
|
|
89
|
+
*/
|
|
90
|
+
export class NodeWorker {
|
|
91
|
+
|
|
92
|
+
constructor( url, options = {} ) {
|
|
93
|
+
|
|
94
|
+
this.onmessage = null;
|
|
95
|
+
this.onerror = null;
|
|
96
|
+
this.onmessageerror = null;
|
|
97
|
+
this._listeners = { message: new Set(), error: new Set(), messageerror: new Set() };
|
|
98
|
+
|
|
99
|
+
this._thread = new ThreadWorker( BOOTSTRAP, { eval: true, name: options.name } );
|
|
100
|
+
this._thread.on( 'message', ( data ) => this._emit( 'message', { type: 'message', data } ) );
|
|
101
|
+
this._thread.on( 'messageerror', ( error ) => this._emit( 'messageerror', { type: 'messageerror', error } ) );
|
|
102
|
+
this._thread.on( 'error', ( error ) => this._emit( 'error', { type: 'error', error, message: error?.message ?? String( error ) } ) );
|
|
103
|
+
|
|
104
|
+
// Anything the caller posts before the module lands is held in the thread, in order.
|
|
105
|
+
const source = readModuleSource( url );
|
|
106
|
+
const load = ( code ) => this._thread.postMessage( { __rayzeeModule: code, classic: options.type !== 'module' } );
|
|
107
|
+
if ( typeof source === 'string' ) load( source );
|
|
108
|
+
else source.then( load, ( error ) => this._emit( 'error', { type: 'error', error, message: error.message } ) );
|
|
109
|
+
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
postMessage( data, transfer ) {
|
|
113
|
+
|
|
114
|
+
const list = Array.isArray( transfer ) ? transfer : transfer?.transfer;
|
|
115
|
+
this._thread.postMessage( data, ( list ?? [] ).filter( ( item ) => item instanceof ArrayBuffer ) );
|
|
116
|
+
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
addEventListener( type, fn ) {
|
|
120
|
+
|
|
121
|
+
this._listeners[ type ]?.add( fn );
|
|
122
|
+
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
removeEventListener( type, fn ) {
|
|
126
|
+
|
|
127
|
+
this._listeners[ type ]?.delete( fn );
|
|
128
|
+
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
terminate() {
|
|
132
|
+
|
|
133
|
+
this._thread.terminate();
|
|
134
|
+
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_emit( type, event ) {
|
|
138
|
+
|
|
139
|
+
this[ `on${type}` ]?.( event );
|
|
140
|
+
for ( const fn of this._listeners[ type ] ) fn( event );
|
|
141
|
+
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-only helpers, kept out of the main entry so a browser bundle never sees `node:` imports.
|
|
3
|
+
*
|
|
4
|
+
* import { configurePlatform, openHeadless } from 'rayzee';
|
|
5
|
+
* import { nodePlatform } from 'rayzee/node';
|
|
6
|
+
* configurePlatform( nodePlatform( { decodeImage } ) );
|
|
7
|
+
*/
|
|
8
|
+
export { NodeWorker } from './NodeWorker.js';
|
|
9
|
+
export { nodePlatform } from './nodePlatform.js';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { NodeWorker } from './NodeWorker.js';
|
|
2
|
+
|
|
3
|
+
// three.js's FileLoader constructs one on every chunk of a streamed download.
|
|
4
|
+
class NodeProgressEvent extends Event {
|
|
5
|
+
|
|
6
|
+
constructor( type, { lengthComputable = false, loaded = 0, total = 0 } = {} ) {
|
|
7
|
+
|
|
8
|
+
super( type );
|
|
9
|
+
this.lengthComputable = lengthComputable;
|
|
10
|
+
this.loaded = loaded;
|
|
11
|
+
this.total = total;
|
|
12
|
+
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const SHARED_UPLOAD_CHUNK = 64 * 2 ** 20;
|
|
18
|
+
const WRAPPED = Symbol.for( 'rayzee.sharedUploads' );
|
|
19
|
+
|
|
20
|
+
function sharedBytes( data, elementOffset = 0, elementCount = undefined ) {
|
|
21
|
+
|
|
22
|
+
const isView = ArrayBuffer.isView( data );
|
|
23
|
+
const backing = isView ? data.buffer : data;
|
|
24
|
+
if ( ! ( backing instanceof SharedArrayBuffer ) ) return null;
|
|
25
|
+
|
|
26
|
+
const size = isView ? data.BYTES_PER_ELEMENT ?? 1 : 1;
|
|
27
|
+
const start = ( isView ? data.byteOffset : 0 ) + elementOffset * size;
|
|
28
|
+
const length = elementCount !== undefined ? elementCount * size : ( isView ? data.byteLength : backing.byteLength ) - elementOffset * size;
|
|
29
|
+
return new Uint8Array( backing, start, length );
|
|
30
|
+
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// dawn.node (webgpu 0.6.1) segfaults writing from a SharedArrayBuffer, which WebGPU allows and the
|
|
34
|
+
// engine's triangle and BVH stores are. Such uploads are copied out first, 64 MB at a time.
|
|
35
|
+
function copySharedUploads() {
|
|
36
|
+
|
|
37
|
+
const Queue = globalThis.GPUQueue;
|
|
38
|
+
if ( ! Queue ) throw new Error( 'nodePlatform(): install the webgpu globals first — Object.assign( globalThis, globals )' );
|
|
39
|
+
if ( Queue.prototype[ WRAPPED ] ) return;
|
|
40
|
+
|
|
41
|
+
const { writeBuffer, writeTexture } = Queue.prototype;
|
|
42
|
+
|
|
43
|
+
Queue.prototype.writeBuffer = function ( ...args ) {
|
|
44
|
+
|
|
45
|
+
const [ buffer, bufferOffset, data, dataOffset, size ] = args;
|
|
46
|
+
const bytes = sharedBytes( data, dataOffset ?? 0, size );
|
|
47
|
+
if ( ! bytes ) return writeBuffer.apply( this, args );
|
|
48
|
+
|
|
49
|
+
for ( let at = 0; at < bytes.length; at += SHARED_UPLOAD_CHUNK ) {
|
|
50
|
+
|
|
51
|
+
writeBuffer.call( this, buffer, bufferOffset + at, bytes.slice( at, at + SHARED_UPLOAD_CHUNK ) );
|
|
52
|
+
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
Queue.prototype.writeTexture = function ( ...args ) {
|
|
58
|
+
|
|
59
|
+
const bytes = sharedBytes( args[ 1 ] );
|
|
60
|
+
if ( bytes ) args[ 1 ] = bytes.slice();
|
|
61
|
+
return writeTexture.apply( this, args );
|
|
62
|
+
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
Queue.prototype[ WRAPPED ] = true;
|
|
66
|
+
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Everything the engine needs from Node, for `configurePlatform( nodePlatform( { decodeImage } ) )`:
|
|
71
|
+
* the worker class and the host's image decoder — the engine carries none. It also defines
|
|
72
|
+
* `ProgressEvent` where the runtime lacks it, the one browser class three.js's loaders construct.
|
|
73
|
+
* WebGPU itself is the host's: install the `webgpu` package's globals and `navigator.gpu` first, since
|
|
74
|
+
* its queue is wrapped here to copy uploads out of shared memory.
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} [options]
|
|
77
|
+
* @param {( bytes: Uint8Array, mimeType: string ) => Promise<{data: Uint8Array, width: number, height: number}>} [options.decodeImage]
|
|
78
|
+
* PNG/JPEG/WebP bytes to RGBA8 pixels, top row first. Without it a model's textures cannot load.
|
|
79
|
+
*/
|
|
80
|
+
export function nodePlatform( { decodeImage } = {} ) {
|
|
81
|
+
|
|
82
|
+
globalThis.ProgressEvent ??= NodeProgressEvent;
|
|
83
|
+
copySharedUploads();
|
|
84
|
+
return { Worker: NodeWorker, decodeImage: decodeImage ?? null };
|
|
85
|
+
|
|
86
|
+
}
|