rayzee 9.1.2 → 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 +4915 -4314
- 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/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 +7 -6
- 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 +142 -25
- package/src/Stages/PathTracerStage.js +6 -96
- package/src/TSL/DebugKernel.js +1 -1
- package/src/TSL/GenerateKernel.js +2 -5
- package/src/TSL/Random.js +3 -120
- package/src/TSL/ShadeKernel.js +3 -3
- package/src/index.js +2 -1
- package/src/managers/CameraManager.js +2 -2
- package/src/managers/DenoisingManager.js +76 -7
- 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
package/src/PathTracerApp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { WebGPURenderer, RectAreaLightNode, SRGBColorSpace, LinearSRGBColorSpace } from 'three/webgpu';
|
|
2
2
|
import { texture as _tslTexture, cubeTexture as _tslCubeTexture } from 'three/tsl';
|
|
3
3
|
import {
|
|
4
|
-
Scene, EventDispatcher, Box3, Vector3
|
|
4
|
+
Scene, EventDispatcher, Box3, Vector3, REVISION,
|
|
5
5
|
} from 'three';
|
|
6
6
|
import { RectAreaLightTexturesLib } from 'three/addons/lights/RectAreaLightTexturesLib.js';
|
|
7
7
|
import { SceneHelpers } from './SceneHelpers.js';
|
|
@@ -23,9 +23,9 @@ import { BuildTimer } from './Processor/BuildTimer.js';
|
|
|
23
23
|
import { TextureReadback } from './Processor/TextureReadback.js';
|
|
24
24
|
import { createLogger, fmt } from './utils/Logger.js';
|
|
25
25
|
import { InteractionManager } from './managers/InteractionManager.js';
|
|
26
|
-
import { EngineEvents } from './EngineEvents.js';
|
|
26
|
+
import { EngineEvents, LEGACY_EVENT_NAMES } from './EngineEvents.js';
|
|
27
27
|
import { IssueLog, ISSUE_CODES } from './EngineIssues.js';
|
|
28
|
-
import { getAssetConfig } from './AssetConfig.js';
|
|
28
|
+
import { getAssetConfig, isAssetConfigured } from './AssetConfig.js';
|
|
29
29
|
import { StorageManager } from './Storage/StorageManager.js';
|
|
30
30
|
import { acquireSharedStorage } from './Storage/openStorage.js';
|
|
31
31
|
import { nameFromUrl } from './Storage/DownloadCache.js';
|
|
@@ -37,6 +37,8 @@ import { getViewTransform } from './Color/ViewTransforms.js';
|
|
|
37
37
|
import { AssetLoader } from './Processor/AssetLoader.js';
|
|
38
38
|
import { SPILL_TRIANGLE_BUDGET, SPILL_PLACEMENT_BUDGET } from './Processor/PBRT/index.js';
|
|
39
39
|
import { SceneProcessor } from './Processor/SceneProcessor.js';
|
|
40
|
+
import { deviceMemoryGB } from './Processor/HostMemory.js';
|
|
41
|
+
import { createHeadlessCanvas } from './HeadlessCanvas.js';
|
|
40
42
|
|
|
41
43
|
// Managers
|
|
42
44
|
import { RenderSettings } from './RenderSettings.js';
|
|
@@ -53,6 +55,7 @@ import { TransformGizmoHelper } from './managers/helpers/TransformGizmoHelper.js
|
|
|
53
55
|
import { captureSceneState, applySceneState } from './SceneState/SceneState.js';
|
|
54
56
|
import { TRIANGLE_PATCH_PROPERTIES } from './managers/MaterialDataManager.js';
|
|
55
57
|
import { VERSION } from './version.js';
|
|
58
|
+
import { toPortable } from './SceneState/portable.js';
|
|
56
59
|
|
|
57
60
|
export const RENDER_CHECKPOINT_VERSION = 1;
|
|
58
61
|
|
|
@@ -66,6 +69,29 @@ const partOf = ( { element, pbrtEntry } = {} ) => ( {
|
|
|
66
69
|
// instantiates (StrictMode, HMR, etc.) so its rAF loop can't burn CPU.
|
|
67
70
|
const _appsByCanvas = new WeakMap();
|
|
68
71
|
|
|
72
|
+
const KNOWN_EVENTS = new Set( [ ...Object.values( EngineEvents ), ...Object.values( LEGACY_EVENT_NAMES ) ] );
|
|
73
|
+
const _warnedEvents = new Set();
|
|
74
|
+
|
|
75
|
+
// three's Animation.start() calls self.requestAnimationFrame during init() with no guard, and
|
|
76
|
+
// outside a browser there is no `self`. A headless app never uses that loop, so it gets an inert one.
|
|
77
|
+
async function initWithoutFrameLoop( renderer, headless ) {
|
|
78
|
+
|
|
79
|
+
if ( ! headless || typeof self !== 'undefined' ) return renderer.init();
|
|
80
|
+
|
|
81
|
+
globalThis.self = { requestAnimationFrame: () => 0, cancelAnimationFrame: () => {} };
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
|
|
85
|
+
return await renderer.init();
|
|
86
|
+
|
|
87
|
+
} finally {
|
|
88
|
+
|
|
89
|
+
delete globalThis.self;
|
|
90
|
+
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
}
|
|
94
|
+
|
|
69
95
|
|
|
70
96
|
/**
|
|
71
97
|
* WebGPU Path Tracer Application.
|
|
@@ -137,6 +163,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
137
163
|
* @param {HTMLCanvasElement} canvas - Canvas element for rendering
|
|
138
164
|
* @param {Object} [options] - Engine options
|
|
139
165
|
* @param {boolean} [options.autoResize=true] - Automatically listen for window resize events
|
|
166
|
+
* @param {boolean} [options.headless=false] - a render nobody watches, and the default when no canvas
|
|
167
|
+
* is given: the engine supplies its own canvas (see HeadlessCanvas.js), runs no render loop — drive it
|
|
168
|
+
* with renderFrames() or renderUntilComplete() — and builds neither the overlay nor the gizmo
|
|
140
169
|
* @param {HTMLElement} [options.container] - Single DOM parent the engine mounts all auxiliary
|
|
141
170
|
* elements into (HUD overlay, denoiser canvas). Defaults to `canvas.parentNode`.
|
|
142
171
|
* @param {boolean} [options.strict=false] - Throw at the point of degradation instead of
|
|
@@ -146,8 +175,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
146
175
|
* @param {number} [options.maxSceneBytes] - refuse a scene whose estimated host memory is
|
|
147
176
|
* above this. The default refuses where the renderer process would be killed instead of
|
|
148
177
|
* throwing; raise it deliberately, on a fresh browser. See HostMemory.js.
|
|
178
|
+
* @param {number} [options.hostMemoryGB] - the host's memory, where `navigator.deviceMemory` is
|
|
179
|
+
* absent (anything but Chrome) or wrong; it sizes the render reserve and the path pool
|
|
149
180
|
* @param {false|'auto'|StorageManager} [options.storage] - on-disk storage; defaults to
|
|
150
|
-
* `configureAssets( { storage } )
|
|
181
|
+
* `configureAssets( { storage } )`, or off under `strict` unless that was set. A host-supplied
|
|
182
|
+
* manager stays the host's to dispose.
|
|
151
183
|
* @param {boolean} [options.memorySpill=false] - experimental: once a large static scene is
|
|
152
184
|
* on the GPU, move its triangle records and BLAS nodes to disk (see
|
|
153
185
|
* {@link ensureSceneResident}). Needs storage; skipped for animated scenes.
|
|
@@ -159,6 +191,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
159
191
|
|
|
160
192
|
super();
|
|
161
193
|
|
|
194
|
+
this._headless = options.headless === true || ! canvas;
|
|
195
|
+
canvas ??= createHeadlessCanvas();
|
|
196
|
+
|
|
162
197
|
try {
|
|
163
198
|
|
|
164
199
|
_appsByCanvas.get( canvas )?.dispose();
|
|
@@ -172,9 +207,10 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
172
207
|
_appsByCanvas.set( canvas, this );
|
|
173
208
|
|
|
174
209
|
this.canvas = canvas;
|
|
175
|
-
this._autoResize = options.autoResize !== false;
|
|
210
|
+
this._autoResize = ! this._headless && options.autoResize !== false;
|
|
176
211
|
// A scene budget the host may raise; read where SceneProcessor is built, well after this.
|
|
177
212
|
this._maxSceneBytes = options.maxSceneBytes;
|
|
213
|
+
this._hostMemoryGB = options.hostMemoryGB;
|
|
178
214
|
this._storageOption = options.storage;
|
|
179
215
|
this._memorySpill = options.memorySpill === true;
|
|
180
216
|
/** @type {?StorageManager} on-disk storage, null when off or unavailable */
|
|
@@ -187,6 +223,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
187
223
|
|
|
188
224
|
// Before the settings: the profile supplies some of their defaults.
|
|
189
225
|
this._profile = getRenderProfile( options.profile );
|
|
226
|
+
this._profileName = options.profile ?? 'viewer';
|
|
190
227
|
|
|
191
228
|
// First, so no subsystem can degrade unrecorded.
|
|
192
229
|
this._issues = new IssueLog( {
|
|
@@ -378,7 +415,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
378
415
|
|
|
379
416
|
async _initStorage() {
|
|
380
417
|
|
|
381
|
-
|
|
418
|
+
// A batch render must not be answered from an earlier run's cache unless the host asked for one.
|
|
419
|
+
const option = this._storageOption
|
|
420
|
+
?? ( this._issues.strict && ! isAssetConfigured( 'storage' ) ? false : getAssetConfig().storage );
|
|
382
421
|
if ( option === false ) return;
|
|
383
422
|
|
|
384
423
|
if ( option instanceof StorageManager ) {
|
|
@@ -418,11 +457,34 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
418
457
|
|
|
419
458
|
}
|
|
420
459
|
|
|
460
|
+
dispatchEvent( event ) {
|
|
461
|
+
|
|
462
|
+
super.dispatchEvent( event );
|
|
463
|
+
const legacy = LEGACY_EVENT_NAMES[ event.type ];
|
|
464
|
+
if ( legacy ) super.dispatchEvent( { ...event, type: legacy } );
|
|
465
|
+
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
addEventListener( type, listener ) {
|
|
469
|
+
|
|
470
|
+
if ( ! KNOWN_EVENTS.has( type ) && ! _warnedEvents.has( type ) ) {
|
|
471
|
+
|
|
472
|
+
_warnedEvents.add( type );
|
|
473
|
+
log.warn( `no engine event is named "${type}", so this listener will never fire — see EngineEvents` );
|
|
474
|
+
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
super.addEventListener( type, listener );
|
|
478
|
+
|
|
479
|
+
}
|
|
480
|
+
|
|
421
481
|
/**
|
|
422
482
|
* Starts the animation loop.
|
|
423
483
|
*/
|
|
424
484
|
animate() {
|
|
425
485
|
|
|
486
|
+
if ( this._headless ) throw new Error( 'a headless app has no render loop — drive it with renderFrames() or renderUntilComplete()' );
|
|
487
|
+
|
|
426
488
|
// Device lost: stop the loop rather than rescheduling render() on a dead device.
|
|
427
489
|
if ( this._deviceLost ) return;
|
|
428
490
|
|
|
@@ -523,42 +585,15 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
523
585
|
|
|
524
586
|
}
|
|
525
587
|
|
|
526
|
-
|
|
527
|
-
this.
|
|
528
|
-
|
|
529
|
-
if ( ! this.stages.pathTracer.isComplete ) {
|
|
588
|
+
// A lockstep readback is due and still in flight; the frame would trace nothing.
|
|
589
|
+
if ( this.stages.pathTracer.readbackWait() ) {
|
|
530
590
|
|
|
531
|
-
this.
|
|
532
|
-
|
|
591
|
+
this.dispatchEvent( { type: EngineEvents.FRAME } );
|
|
592
|
+
return;
|
|
533
593
|
|
|
534
594
|
}
|
|
535
595
|
|
|
536
|
-
this.
|
|
537
|
-
// VRAM is monotonic and only changes on allocation events (scene/env
|
|
538
|
-
// load, resize — each re-measures via _ensureVRAMWiring). Within an
|
|
539
|
-
// accumulation burst nothing reallocates, so re-walking every stage's
|
|
540
|
-
// textures each frame is wasted. Measure at burst start (catches any
|
|
541
|
-
// reset-triggered allocation) + a periodic backstop; read cached otherwise.
|
|
542
|
-
const tracker = this.stages.pathTracer?.vramTracker;
|
|
543
|
-
const frame = this.stages.pathTracer?.frameCount ?? 0;
|
|
544
|
-
if ( tracker && ( frame <= 1 || frame % 30 === 0 ) ) tracker.measure();
|
|
545
|
-
|
|
546
|
-
updateStats( {
|
|
547
|
-
timeElapsed: this.completion.timeElapsed,
|
|
548
|
-
samples: getDisplaySamples( this.stages.pathTracer ),
|
|
549
|
-
memoryUsed: tracker?.current ?? 0,
|
|
550
|
-
memoryPeak: tracker?.peak ?? 0,
|
|
551
|
-
} );
|
|
552
|
-
|
|
553
|
-
// Only the wall-clock stop — PathTracer.render() retires the ceiling and convergence
|
|
554
|
-
// itself, so whichever of the three arrives first wins.
|
|
555
|
-
if ( this.completion.isTimeLimitReached(
|
|
556
|
-
this.stages.pathTracer, this.settings.get( 'renderLimitMode' ), this.settings.get( 'renderTimeLimit' )
|
|
557
|
-
) ) {
|
|
558
|
-
|
|
559
|
-
this.stages.pathTracer.isComplete = true;
|
|
560
|
-
|
|
561
|
-
}
|
|
596
|
+
this._traceFrame( { liveDenoise: true } );
|
|
562
597
|
|
|
563
598
|
// Render completion → denoise/upscale chain
|
|
564
599
|
if ( this.stages.pathTracer.isComplete && this.completion.markComplete() ) {
|
|
@@ -568,17 +603,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
568
603
|
context: this.pipeline?.context,
|
|
569
604
|
} );
|
|
570
605
|
|
|
571
|
-
|
|
572
|
-
samples: this.stages.pathTracer.frameCount,
|
|
573
|
-
timeElapsed: this.completion.timeElapsed,
|
|
574
|
-
budgetOverrun: this.completion.budgetOverrun,
|
|
575
|
-
// null means isComplete was forced rather than earned (the reconcile below);
|
|
576
|
-
// a forced stop is closer to the ceiling than to convergence.
|
|
577
|
-
reason: this.completion.stopCondition( this.stages.pathTracer ) ?? 'samples',
|
|
578
|
-
};
|
|
579
|
-
|
|
580
|
-
this.dispatchEvent( { type: 'RenderComplete', ...completionInfo } );
|
|
581
|
-
this.dispatchEvent( { type: EngineEvents.RENDER_COMPLETE, ...completionInfo } );
|
|
606
|
+
this.dispatchEvent( { type: EngineEvents.RENDER_COMPLETE, ...this._completionInfo() } );
|
|
582
607
|
|
|
583
608
|
}
|
|
584
609
|
|
|
@@ -589,6 +614,60 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
589
614
|
|
|
590
615
|
}
|
|
591
616
|
|
|
617
|
+
// One frame of a render and its bookkeeping, shared by animate() and renderUntilComplete().
|
|
618
|
+
_traceFrame( { liveDenoise = false } = {} ) {
|
|
619
|
+
|
|
620
|
+
const stage = this.stages.pathTracer;
|
|
621
|
+
this.pipeline.render();
|
|
622
|
+
this.denoisingManager?.afterTrace();
|
|
623
|
+
|
|
624
|
+
if ( ! stage.isComplete ) {
|
|
625
|
+
|
|
626
|
+
this.completion.updateTime();
|
|
627
|
+
if ( liveDenoise ) this.denoisingManager?.tickContinuousDenoise( stage.frameCount );
|
|
628
|
+
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
this._ensureVRAMWiring();
|
|
632
|
+
// VRAM is monotonic and only changes on allocation events (scene/env
|
|
633
|
+
// load, resize — each re-measures via _ensureVRAMWiring). Within an
|
|
634
|
+
// accumulation burst nothing reallocates, so re-walking every stage's
|
|
635
|
+
// textures each frame is wasted. Measure at burst start (catches any
|
|
636
|
+
// reset-triggered allocation) + a periodic backstop; read cached otherwise.
|
|
637
|
+
const tracker = stage.vramTracker;
|
|
638
|
+
const frame = stage.frameCount ?? 0;
|
|
639
|
+
if ( tracker && ( frame <= 1 || frame % 30 === 0 ) ) tracker.measure();
|
|
640
|
+
|
|
641
|
+
updateStats( {
|
|
642
|
+
timeElapsed: this.completion.timeElapsed,
|
|
643
|
+
samples: getDisplaySamples( stage ),
|
|
644
|
+
memoryUsed: tracker?.current ?? 0,
|
|
645
|
+
memoryPeak: tracker?.peak ?? 0,
|
|
646
|
+
} );
|
|
647
|
+
|
|
648
|
+
// Only the wall-clock stop — PathTracer.render() retires the ceiling and convergence
|
|
649
|
+
// itself, so whichever of the three arrives first wins.
|
|
650
|
+
if ( this.completion.isTimeLimitReached( stage, this.settings.get( 'renderLimitMode' ), this.settings.get( 'renderTimeLimit' ) ) ) {
|
|
651
|
+
|
|
652
|
+
stage.isComplete = true;
|
|
653
|
+
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
_completionInfo() {
|
|
659
|
+
|
|
660
|
+
return {
|
|
661
|
+
samples: this.stages.pathTracer.frameCount,
|
|
662
|
+
timeElapsed: this.completion.timeElapsed,
|
|
663
|
+
budgetOverrun: this.completion.budgetOverrun,
|
|
664
|
+
// null means isComplete was forced rather than earned (the reconcile below);
|
|
665
|
+
// a forced stop is closer to the ceiling than to convergence.
|
|
666
|
+
reason: this.completion.stopCondition( this.stages.pathTracer ) ?? 'samples',
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
}
|
|
670
|
+
|
|
592
671
|
/**
|
|
593
672
|
* Stops the animation loop.
|
|
594
673
|
*/
|
|
@@ -621,7 +700,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
621
700
|
/** Wakes the animation loop if it was stopped due to idle. */
|
|
622
701
|
wake() {
|
|
623
702
|
|
|
624
|
-
if ( this._deviceLost ) return;
|
|
703
|
+
if ( this._deviceLost || this._headless ) return;
|
|
625
704
|
if ( ! this.animationManagerId && this.isInitialized && ! this._paused ) this.animate();
|
|
626
705
|
|
|
627
706
|
}
|
|
@@ -638,7 +717,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
638
717
|
resume() {
|
|
639
718
|
|
|
640
719
|
this._paused = false;
|
|
641
|
-
if ( ! this.animationManagerId ) this.animate();
|
|
720
|
+
if ( ! this.animationManagerId && ! this._headless ) this.animate();
|
|
642
721
|
|
|
643
722
|
}
|
|
644
723
|
|
|
@@ -675,7 +754,6 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
675
754
|
|
|
676
755
|
this.completion.reset();
|
|
677
756
|
this.wake();
|
|
678
|
-
this.dispatchEvent( { type: 'RenderReset' } );
|
|
679
757
|
this.dispatchEvent( { type: EngineEvents.RENDER_RESET } );
|
|
680
758
|
|
|
681
759
|
}
|
|
@@ -859,7 +937,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
859
937
|
|
|
860
938
|
this.denoisingManager?.dropDisplay();
|
|
861
939
|
this.reset();
|
|
862
|
-
this.dispatchEvent( { type:
|
|
940
|
+
this.dispatchEvent( { type: EngineEvents.SCENE_UNLOADED } );
|
|
863
941
|
|
|
864
942
|
}
|
|
865
943
|
|
|
@@ -872,7 +950,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
872
950
|
|
|
873
951
|
await this._loadWithSceneRebuild(
|
|
874
952
|
() => this.assetLoader.loadModel( url, options ),
|
|
875
|
-
{ type:
|
|
953
|
+
{ type: EngineEvents.MODEL_LOADED, url },
|
|
876
954
|
{ kind: 'url', url, cacheKey: options.cacheKey ?? null }
|
|
877
955
|
);
|
|
878
956
|
|
|
@@ -897,7 +975,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
897
975
|
|
|
898
976
|
await this._loadWithSceneRebuild(
|
|
899
977
|
() => this.assetLoader.loadObject3D( object3d, name ),
|
|
900
|
-
{ type:
|
|
978
|
+
{ type: EngineEvents.OBJECT3D_LOADED, name },
|
|
901
979
|
{ kind: 'object3d', name }
|
|
902
980
|
);
|
|
903
981
|
|
|
@@ -939,7 +1017,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
939
1017
|
|
|
940
1018
|
this.pipeline?.eventBus.emit( 'autoexposure:resetHistory' );
|
|
941
1019
|
this.reset();
|
|
942
|
-
this.dispatchEvent( { type:
|
|
1020
|
+
this.dispatchEvent( { type: EngineEvents.ENVIRONMENT_LOADED, url } );
|
|
943
1021
|
|
|
944
1022
|
} finally {
|
|
945
1023
|
|
|
@@ -991,7 +1069,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
991
1069
|
|
|
992
1070
|
await this._loadWithSceneRebuild(
|
|
993
1071
|
() => this.assetLoader.loadAssetFromUrl( file, { ...options, filename } ),
|
|
994
|
-
{ type:
|
|
1072
|
+
{ type: EngineEvents.MODEL_LOADED, filename },
|
|
995
1073
|
{ kind: 'url', url: file, filename, cacheKey: options.cacheKey ?? null, ...partOf( options ) }
|
|
996
1074
|
);
|
|
997
1075
|
return;
|
|
@@ -1006,7 +1084,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1006
1084
|
const identity = fileIdentity( file ).catch( () => ( { name: file.name, size: file.size, lastModified: file.lastModified ?? 0, sample: null } ) );
|
|
1007
1085
|
await this._loadWithSceneRebuild(
|
|
1008
1086
|
() => this.assetLoader.loadAssetFromFile( file, options ),
|
|
1009
|
-
{ type:
|
|
1087
|
+
{ type: EngineEvents.MODEL_LOADED, filename: file.name },
|
|
1010
1088
|
async () => {
|
|
1011
1089
|
|
|
1012
1090
|
const id = await identity;
|
|
@@ -1037,7 +1115,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1037
1115
|
|
|
1038
1116
|
this.pipeline?.eventBus.emit( 'autoexposure:resetHistory' );
|
|
1039
1117
|
this.reset();
|
|
1040
|
-
this.dispatchEvent( { type:
|
|
1118
|
+
this.dispatchEvent( { type: EngineEvents.ENVIRONMENT_LOADED, filename: file.name } );
|
|
1041
1119
|
|
|
1042
1120
|
} finally {
|
|
1043
1121
|
|
|
@@ -1056,7 +1134,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1056
1134
|
|
|
1057
1135
|
await this._loadWithSceneRebuild(
|
|
1058
1136
|
() => this.assetLoader.loadExampleModels( index, modelFiles ),
|
|
1059
|
-
{ type:
|
|
1137
|
+
{ type: EngineEvents.MODEL_LOADED, index },
|
|
1060
1138
|
{ kind: 'url', url: modelFiles[ index ]?.url ?? null, cacheKey: null }
|
|
1061
1139
|
);
|
|
1062
1140
|
|
|
@@ -1106,7 +1184,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1106
1184
|
|
|
1107
1185
|
await this.loadSceneData();
|
|
1108
1186
|
this.reset();
|
|
1109
|
-
this.dispatchEvent( { type:
|
|
1187
|
+
this.dispatchEvent( { type: EngineEvents.TEXTURES_REPROCESSED, maxTextureSize: this._maxTextureSize } );
|
|
1110
1188
|
|
|
1111
1189
|
} finally {
|
|
1112
1190
|
|
|
@@ -1175,6 +1253,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1175
1253
|
// there is nothing to fall back to. Leaving it half-built would keep the last
|
|
1176
1254
|
// frame's buffers on screen under a scene that no longer exists.
|
|
1177
1255
|
if ( ! error || error.code !== 'LOAD_IN_PROGRESS' ) this._discardFailedLoad();
|
|
1256
|
+
|
|
1257
|
+
// Only the scene build reported its own failure; one in the parse left a host's loading
|
|
1258
|
+
// status spinning on its last step. A part prompt is not a failure, so it just ends it.
|
|
1259
|
+
if ( error?.code === 'ARCHIVE_NEEDS_ELEMENT' ) resetLoading();
|
|
1260
|
+
else if ( error?.code !== 'LOAD_IN_PROGRESS' ) updateLoading( { status: `Error: ${error?.message ?? error}`, failed: true, progress: 100 } );
|
|
1178
1261
|
throw error;
|
|
1179
1262
|
|
|
1180
1263
|
} finally {
|
|
@@ -1446,7 +1529,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1446
1529
|
|
|
1447
1530
|
this._initAnimationAndTransforms();
|
|
1448
1531
|
|
|
1449
|
-
this.dispatchEvent( { type:
|
|
1532
|
+
this.dispatchEvent( { type: EngineEvents.SCENE_REBUILD } );
|
|
1450
1533
|
return true;
|
|
1451
1534
|
|
|
1452
1535
|
}
|
|
@@ -1540,7 +1623,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1540
1623
|
root.userData.__rayzeeSourceUrl = url;
|
|
1541
1624
|
if ( cacheKey ) root.userData.__rayzeeCacheKey = cacheKey;
|
|
1542
1625
|
if ( name ) root.userData.__rayzeeName = name;
|
|
1543
|
-
await this._finishRebuildNoReframe( { type:
|
|
1626
|
+
await this._finishRebuildNoReframe( { type: EngineEvents.MODEL_ADDED, url, id: root.uuid } );
|
|
1544
1627
|
return root.uuid;
|
|
1545
1628
|
|
|
1546
1629
|
} finally {
|
|
@@ -1575,7 +1658,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1575
1658
|
const { root } = this.assetLoader.appendObject3D( object3d, name || 'object3d' );
|
|
1576
1659
|
root.userData.__rayzeeSceneObject = true;
|
|
1577
1660
|
if ( name ) root.userData.__rayzeeName = name;
|
|
1578
|
-
await this._finishRebuildNoReframe( { type:
|
|
1661
|
+
await this._finishRebuildNoReframe( { type: EngineEvents.MODEL_ADDED, id: root.uuid } );
|
|
1579
1662
|
return root.uuid;
|
|
1580
1663
|
|
|
1581
1664
|
} finally {
|
|
@@ -1627,7 +1710,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1627
1710
|
|
|
1628
1711
|
// Ground is permanent (removal refused above), so the scene always keeps
|
|
1629
1712
|
// renderable geometry — a full rebuild is always valid here.
|
|
1630
|
-
await this._finishRebuildNoReframe( { type:
|
|
1713
|
+
await this._finishRebuildNoReframe( { type: EngineEvents.SCENE_OBJECT_REMOVED, id } );
|
|
1631
1714
|
|
|
1632
1715
|
return true;
|
|
1633
1716
|
|
|
@@ -1745,7 +1828,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1745
1828
|
|
|
1746
1829
|
if ( ! this._memorySpill || ! this.storage || this.animationManager?.hasAnimations ) return;
|
|
1747
1830
|
this._sdf?.spillToDisk( this.storage )
|
|
1748
|
-
.then( result => result && this.dispatchEvent( { type:
|
|
1831
|
+
.then( result => result && this.dispatchEvent( { type: EngineEvents.SCENE_SPILLED, ...result } ) )
|
|
1749
1832
|
.catch( error => this._issues.warn( ISSUE_CODES.STORAGE_WRITE_FAILED, `memory spill failed: ${error.message}` ) );
|
|
1750
1833
|
|
|
1751
1834
|
}
|
|
@@ -1764,7 +1847,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1764
1847
|
_dispatchCamerasUpdated() {
|
|
1765
1848
|
|
|
1766
1849
|
this.dispatchEvent( {
|
|
1767
|
-
type:
|
|
1850
|
+
type: EngineEvents.CAMERAS_UPDATED,
|
|
1768
1851
|
cameras: this.cameraManager.cameras,
|
|
1769
1852
|
cameraNames: this.cameraManager.getCameraNames(),
|
|
1770
1853
|
} );
|
|
@@ -2060,12 +2143,24 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2060
2143
|
|
|
2061
2144
|
const limits = this.renderer.backend?.device?.limits;
|
|
2062
2145
|
const maxBinding = limits?.maxStorageBufferBindingSize || ( 128 * 1024 * 1024 );
|
|
2063
|
-
const
|
|
2146
|
+
const memory = deviceMemoryGB( this._hostMemoryGB );
|
|
2064
2147
|
// 4K reserve pins the accum MRT (~1.5 GB) + aux; only grant it on clearly-capable GPUs.
|
|
2065
|
-
const deviceSafeMax = (
|
|
2148
|
+
const deviceSafeMax = ( memory.gb >= 8 && maxBinding >= 1024 * 1024 * 1024 )
|
|
2066
2149
|
? MAX_RESERVABLE_RENDER_SIZE : 2048;
|
|
2067
2150
|
const applied = setReservedRenderSize( Math.min( target, deviceSafeMax ) );
|
|
2068
2151
|
|
|
2152
|
+
if ( applied < Math.floor( requestedPx ) ) {
|
|
2153
|
+
|
|
2154
|
+
this._issues.warn(
|
|
2155
|
+
ISSUE_CODES.RENDER_RESERVE_CAPPED,
|
|
2156
|
+
`reserved render size ${fmt.n( requestedPx )}px was capped to ${fmt.n( applied )}px by this device's limits — ` +
|
|
2157
|
+
`renders above ${fmt.n( applied )}px will be declined` +
|
|
2158
|
+
( memory.assumed && memory.gb < 8 ? ' (host memory unknown, 4 GB assumed: pass hostMemoryGB)' : '' ),
|
|
2159
|
+
{ requested: requestedPx, applied, deviceMemoryGB: memory.gb, assumed: memory.assumed, maxStorageBufferBindingSize: maxBinding }
|
|
2160
|
+
);
|
|
2161
|
+
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2069
2164
|
// Gate the realloc on the textures that EXIST, not on how the binding moved: a raise applied while
|
|
2070
2165
|
// nothing was allocated leaves `applied === prev` for every later call, so keying off that let the
|
|
2071
2166
|
// first ineffective call poison every effective one after it (issue #9). The path tracer's write MRT
|
|
@@ -2102,7 +2197,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2102
2197
|
}
|
|
2103
2198
|
|
|
2104
2199
|
this.reset();
|
|
2105
|
-
this.dispatchEvent( { type:
|
|
2200
|
+
this.dispatchEvent( { type: EngineEvents.RESERVED_RENDER_SIZE_CHANGED, size: applied } );
|
|
2106
2201
|
|
|
2107
2202
|
}
|
|
2108
2203
|
|
|
@@ -2124,19 +2219,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2124
2219
|
this._pendingReservedRenderSize = null;
|
|
2125
2220
|
|
|
2126
2221
|
const applied = this.setReservedRenderResolution( pending.requestedPx, { allowLower: pending.allowLower } );
|
|
2127
|
-
|
|
2128
|
-
if ( applied < pending.requestedPx ) {
|
|
2129
|
-
|
|
2130
|
-
this._issues.warn(
|
|
2131
|
-
ISSUE_CODES.RENDER_RESERVE_CAPPED,
|
|
2132
|
-
`reserved render size ${fmt.n( pending.requestedPx )}px was capped to ${fmt.n( applied )}px by this device's limits — ` +
|
|
2133
|
-
`renders above ${fmt.n( applied )}px will be declined`,
|
|
2134
|
-
{ requested: pending.requestedPx, applied }
|
|
2135
|
-
);
|
|
2136
|
-
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2139
|
-
this.dispatchEvent( { type: 'reserved_render_size_changed', size: applied } );
|
|
2222
|
+
this.dispatchEvent( { type: EngineEvents.RESERVED_RENDER_SIZE_CHANGED, size: applied } );
|
|
2140
2223
|
|
|
2141
2224
|
}
|
|
2142
2225
|
|
|
@@ -2206,7 +2289,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2206
2289
|
this.denoisingManager?.setRenderSize( renderWidth, renderHeight );
|
|
2207
2290
|
this.needsReset = true;
|
|
2208
2291
|
|
|
2209
|
-
this.dispatchEvent( { type:
|
|
2292
|
+
this.dispatchEvent( { type: EngineEvents.RESOLUTION_CHANGED, width: renderWidth, height: renderHeight } );
|
|
2210
2293
|
|
|
2211
2294
|
}
|
|
2212
2295
|
|
|
@@ -2382,6 +2465,43 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2382
2465
|
|
|
2383
2466
|
}
|
|
2384
2467
|
|
|
2468
|
+
// Draws once without waking the loop: a woken loop takes a finished render nothing marked
|
|
2469
|
+
// complete (renderFrames, a video export) as newly finished, and denoises it again.
|
|
2470
|
+
_presentDisplay() {
|
|
2471
|
+
|
|
2472
|
+
if ( this.animationManagerId ) {
|
|
2473
|
+
|
|
2474
|
+
this._needsDisplayRefresh = true;
|
|
2475
|
+
return;
|
|
2476
|
+
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
const context = this.pipeline?.context;
|
|
2480
|
+
if ( this._deviceLost || ! context ) return;
|
|
2481
|
+
this.stages.compositor?.render( context );
|
|
2482
|
+
this._renderHelperOverlay();
|
|
2483
|
+
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
/**
|
|
2487
|
+
* One OIDN pass over the current accumulation at the final tier, resolved once the denoised
|
|
2488
|
+
* picture is published. Does not start the render loop or the upscaler; read the result with
|
|
2489
|
+
* `renderToBuffer( { source: 'display' } )`.
|
|
2490
|
+
* @returns {Promise<boolean>} whether a denoised picture was published
|
|
2491
|
+
*/
|
|
2492
|
+
async runFinalDenoise() {
|
|
2493
|
+
|
|
2494
|
+
const dm = this.denoisingManager;
|
|
2495
|
+
if ( await dm?.denoiseOnce() ) return true;
|
|
2496
|
+
|
|
2497
|
+
const reason = ! dm?.denoiser ? 'the denoiser was not built'
|
|
2498
|
+
: ! dm.denoiser.enabled ? 'OIDN was off while accumulating — call denoisingManager.setOIDNEnabled( true ) first'
|
|
2499
|
+
: 'the denoise did not complete';
|
|
2500
|
+
this._issues.record( ISSUE_CODES.DENOISER_UNAVAILABLE, `final denoise produced no picture: ${reason}`, { reason } );
|
|
2501
|
+
return false;
|
|
2502
|
+
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2385
2505
|
// Aborts any in-flight denoise/upscale and puts the denoiser canvas back at base resolution (the
|
|
2386
2506
|
// upscaler leaves it enlarged), so the live canvas is what's on screen again.
|
|
2387
2507
|
_abortPostProcess( { keepDisplay = false } = {} ) {
|
|
@@ -2392,7 +2512,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2392
2512
|
|
|
2393
2513
|
const w = this.denoisingManager._lastRenderWidth;
|
|
2394
2514
|
const h = this.denoisingManager._lastRenderHeight;
|
|
2395
|
-
this.dispatchEvent( { type:
|
|
2515
|
+
this.dispatchEvent( { type: EngineEvents.RESOLUTION_CHANGED, width: w, height: h } );
|
|
2396
2516
|
|
|
2397
2517
|
}
|
|
2398
2518
|
|
|
@@ -2586,9 +2706,6 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2586
2706
|
/**
|
|
2587
2707
|
* Accumulates exactly `count` samples synchronously, bypassing the rAF loop.
|
|
2588
2708
|
*
|
|
2589
|
-
* Awaits the STBN atlases first — until they land the sampler reads a constant-0.5
|
|
2590
|
-
* placeholder that gets baked permanently into the accumulation buffer.
|
|
2591
|
-
*
|
|
2592
2709
|
* A frame retired by adaptive sampling stops advancing `frameCount`, so a fixed-count loop
|
|
2593
2710
|
* can never reach `count`. `allowEarlyRetire` makes that an outcome instead of a throw.
|
|
2594
2711
|
*
|
|
@@ -2606,8 +2723,6 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2606
2723
|
if ( ! stage ) throw new Error( 'renderFrames: app is not initialized' );
|
|
2607
2724
|
if ( ! ( count > 0 ) ) throw new Error( `renderFrames: count must be positive, got ${count}` );
|
|
2608
2725
|
|
|
2609
|
-
await stage.blueNoiseReady;
|
|
2610
|
-
|
|
2611
2726
|
const target = ( reset ? 0 : stage.frameCount ) + count;
|
|
2612
2727
|
|
|
2613
2728
|
// completionThreshold is a cached JS number derived from maxSamples, so this must
|
|
@@ -2631,6 +2746,14 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2631
2746
|
if ( this._deviceLost ) throw new Error( 'renderFrames: WebGPU device lost' );
|
|
2632
2747
|
if ( ! stage.isReady ) throw new Error( 'renderFrames: path tracer stage is not ready' );
|
|
2633
2748
|
|
|
2749
|
+
const wait = stage.readbackWait();
|
|
2750
|
+
if ( wait ) {
|
|
2751
|
+
|
|
2752
|
+
await this._awaitReadback( wait );
|
|
2753
|
+
continue;
|
|
2754
|
+
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2634
2757
|
this.pipeline.render();
|
|
2635
2758
|
passes ++;
|
|
2636
2759
|
|
|
@@ -2669,6 +2792,156 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2669
2792
|
|
|
2670
2793
|
}
|
|
2671
2794
|
|
|
2795
|
+
/**
|
|
2796
|
+
* Renders until the render's own stop condition — the sample ceiling, adaptive convergence or the
|
|
2797
|
+
* time limit, whichever comes first — without requestAnimationFrame, then runs the closing denoise
|
|
2798
|
+
* once. The production counterpart of {@link renderFrames}: adaptive sampling stays on, and its
|
|
2799
|
+
* readbacks run in lockstep (see PathTracer.setLockstepReadbacks), so the same input renders the same
|
|
2800
|
+
* image however fast frames are submitted — except under a time limit, which is wall-clock by nature.
|
|
2801
|
+
* The upscaler does not run. `EngineEvents.RENDER_COMPLETE` fires as it does from the loop.
|
|
2802
|
+
*
|
|
2803
|
+
* @param {Object} [options]
|
|
2804
|
+
* @param {boolean} [options.reset=true] - restart accumulation from sample 0 first
|
|
2805
|
+
* @param {boolean} [options.denoise=true] - run the final OIDN pass when `finalDenoise` is on
|
|
2806
|
+
* @param {AbortSignal} [options.signal] - rejects with its reason once aborted
|
|
2807
|
+
* @param {number} [options.drainEvery=4] - every N frames, wait for the GPU to finish the frames
|
|
2808
|
+
* before the last N, so submissions stay at most 2N frames ahead without the GPU idling
|
|
2809
|
+
* @param {function(number): void} [options.onProgress] - called with the running sample count
|
|
2810
|
+
* @returns {Promise<{samples: number, retiredBy: 'samples'|'converged'|'timeLimit', timeElapsed: number,
|
|
2811
|
+
* budgetOverrun: boolean, denoised: boolean}>}
|
|
2812
|
+
*/
|
|
2813
|
+
async renderUntilComplete( { reset = true, denoise = true, signal, drainEvery = 4, onProgress } = {} ) {
|
|
2814
|
+
|
|
2815
|
+
const stage = this.stages.pathTracer;
|
|
2816
|
+
if ( ! stage ) throw new Error( 'renderUntilComplete: app is not initialized' );
|
|
2817
|
+
if ( this._loadingInProgress || this._sdf?.isProcessing ) throw new Error( 'renderUntilComplete: a scene is still loading' );
|
|
2818
|
+
|
|
2819
|
+
const lockstepWas = stage.lockstepReadbacks;
|
|
2820
|
+
stage.setLockstepReadbacks( true );
|
|
2821
|
+
// A 100 ms wall-clock mode for camera drags; frames in it do not count, so a batch render has no use for it.
|
|
2822
|
+
const interaction = this.settings.getEffective().interactionModeEnabled;
|
|
2823
|
+
this.settings.set( 'interactionModeEnabled', false, { silent: true, reset: false } );
|
|
2824
|
+
|
|
2825
|
+
try {
|
|
2826
|
+
|
|
2827
|
+
if ( reset ) this.reset();
|
|
2828
|
+
this.stopAnimation();
|
|
2829
|
+
|
|
2830
|
+
const queue = this.renderer.backend.device?.queue;
|
|
2831
|
+
let passes = 0;
|
|
2832
|
+
let previousBatch = null;
|
|
2833
|
+
|
|
2834
|
+
while ( ! stage.isComplete ) {
|
|
2835
|
+
|
|
2836
|
+
signal?.throwIfAborted();
|
|
2837
|
+
if ( this._deviceLost ) throw new Error( 'renderUntilComplete: WebGPU device lost' );
|
|
2838
|
+
if ( ! stage.isReady ) throw new Error( 'renderUntilComplete: path tracer stage is not ready' );
|
|
2839
|
+
|
|
2840
|
+
const wait = stage.readbackWait();
|
|
2841
|
+
if ( wait ) {
|
|
2842
|
+
|
|
2843
|
+
await this._awaitReadback( wait );
|
|
2844
|
+
continue;
|
|
2845
|
+
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
this.cameraManager.camera.updateMatrixWorld();
|
|
2849
|
+
const counted = stage.frameCount;
|
|
2850
|
+
this._traceFrame();
|
|
2851
|
+
onProgress?.( stage.frameCount );
|
|
2852
|
+
|
|
2853
|
+
// A frame that did not count, with no readback to wait on, must still give timers a turn.
|
|
2854
|
+
if ( stage.frameCount === counted && ! stage.isComplete ) await new Promise( ( resolve ) => setTimeout( resolve, 0 ) );
|
|
2855
|
+
|
|
2856
|
+
if ( queue && drainEvery > 0 && ++ passes % drainEvery === 0 ) {
|
|
2857
|
+
|
|
2858
|
+
const batch = queue.onSubmittedWorkDone();
|
|
2859
|
+
if ( previousBatch ) await previousBatch;
|
|
2860
|
+
previousBatch = batch;
|
|
2861
|
+
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
const info = this._completionInfo();
|
|
2867
|
+
if ( this.completion.markComplete() ) this.dispatchEvent( { type: EngineEvents.RENDER_COMPLETE, ...info } );
|
|
2868
|
+
|
|
2869
|
+
const denoised = denoise && this.denoisingManager?.finalDenoise ? await this.runFinalDenoise() : false;
|
|
2870
|
+
const { reason, ...rest } = info;
|
|
2871
|
+
return { ...rest, retiredBy: reason, denoised };
|
|
2872
|
+
|
|
2873
|
+
} finally {
|
|
2874
|
+
|
|
2875
|
+
stage.setLockstepReadbacks( lockstepWas );
|
|
2876
|
+
if ( interaction ) this.settings.set( 'interactionModeEnabled', interaction.value, { silent: true, reset: false, source: interaction.source } );
|
|
2877
|
+
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
/**
|
|
2883
|
+
* What produced the current image, as plain JSON, to keep beside a render: engine and three.js
|
|
2884
|
+
* versions, the profile by name and by value (a name's values can change between releases), the
|
|
2885
|
+
* adapter, every live setting with its source, the colour pipeline, and the modes that decide
|
|
2886
|
+
* whether the render reproduces.
|
|
2887
|
+
* @returns {Object}
|
|
2888
|
+
*/
|
|
2889
|
+
getProvenance() {
|
|
2890
|
+
|
|
2891
|
+
const stage = this.stages.pathTracer;
|
|
2892
|
+
const color = this.color?.status();
|
|
2893
|
+
|
|
2894
|
+
return toPortable( {
|
|
2895
|
+
engine: VERSION,
|
|
2896
|
+
three: REVISION,
|
|
2897
|
+
profile: { name: this._profileName, values: { ...this._profile } },
|
|
2898
|
+
adapter: this.adapterInfo ?? null,
|
|
2899
|
+
mode: {
|
|
2900
|
+
headless: this._headless,
|
|
2901
|
+
strict: this._issues.strict,
|
|
2902
|
+
deterministic: this.isDeterministic,
|
|
2903
|
+
lockstep: !! stage?.lockstepReadbacks,
|
|
2904
|
+
},
|
|
2905
|
+
render: stage ? {
|
|
2906
|
+
width: stage.width,
|
|
2907
|
+
height: stage.height,
|
|
2908
|
+
samples: stage.frameCount,
|
|
2909
|
+
complete: stage.isComplete,
|
|
2910
|
+
retiredBy: stage.isComplete ? this.completion.stopCondition( stage ) ?? 'samples' : null,
|
|
2911
|
+
} : null,
|
|
2912
|
+
color: color ? {
|
|
2913
|
+
config: color.config?.id ?? null,
|
|
2914
|
+
workingSpace: color.workingSpace,
|
|
2915
|
+
view: color.activeView ?? color.activeTransform?.id ?? null,
|
|
2916
|
+
context: color.context,
|
|
2917
|
+
} : null,
|
|
2918
|
+
scene: this.sceneSource,
|
|
2919
|
+
settings: this.settings.getEffective(),
|
|
2920
|
+
issues: this.issues.map( ( { code, severity } ) => ( { code, severity } ) ),
|
|
2921
|
+
} );
|
|
2922
|
+
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
/**
|
|
2926
|
+
* Lockstep readbacks for renders driven by the loop or by {@link renderFrames}:
|
|
2927
|
+
* {@link renderUntilComplete} turns them on for its own duration. See PathTracer.setLockstepReadbacks.
|
|
2928
|
+
* @param {boolean} [enabled=true]
|
|
2929
|
+
*/
|
|
2930
|
+
setLockstepReadbacks( enabled = true ) {
|
|
2931
|
+
|
|
2932
|
+
this.stages.pathTracer?.setLockstepReadbacks( enabled );
|
|
2933
|
+
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
// A readback that never lands would hold the render forever; a lost device is the one way it can't.
|
|
2937
|
+
async _awaitReadback( wait ) {
|
|
2938
|
+
|
|
2939
|
+
const lost = this.renderer.backend.device?.lost;
|
|
2940
|
+
const outcome = await ( lost ? Promise.race( [ wait.then( () => null ), lost ] ) : wait );
|
|
2941
|
+
if ( outcome ) throw new Error( `WebGPU device lost while awaiting a readback: ${outcome.message ?? outcome.reason}` );
|
|
2942
|
+
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2672
2945
|
/**
|
|
2673
2946
|
* A render in progress as data: the accumulated colour (and the aux buffers while they hold
|
|
2674
2947
|
* samples), the per-pixel convergence buffers, and the counters that choose the next sample.
|
|
@@ -2721,7 +2994,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2721
2994
|
* @param {'accumulation'|'display'} [options.source='accumulation'] - `'display'` reads what the
|
|
2722
2995
|
* viewport is showing — denoised when a denoiser has run, without bloom — instead of the raw
|
|
2723
2996
|
* accumulation
|
|
2724
|
-
* @returns {Promise<{data: Float32Array|Uint8ClampedArray, width: number, height: number, colorSpace: string
|
|
2997
|
+
* @returns {Promise<{data: Float32Array|Uint8ClampedArray, width: number, height: number, colorSpace: string,
|
|
2998
|
+
* source: string}>} `source` is what was read: `'accumulation'`, or the denoiser that published
|
|
2999
|
+
* it (`'oidn'`, `'asvgf'`, `'nrd'`, `'edgeFiltering'`, `'bilateralFiltering'`)
|
|
2725
3000
|
*/
|
|
2726
3001
|
async renderToBuffer( { colorSpace = 'srgb', preserveAlpha = false, source = 'accumulation' } = {} ) {
|
|
2727
3002
|
|
|
@@ -2741,15 +3016,15 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2741
3016
|
// The pool over-allocates to the reserve, so the texture is larger than the frame.
|
|
2742
3017
|
const { width, height } = stage;
|
|
2743
3018
|
|
|
2744
|
-
const linear = source === 'display'
|
|
3019
|
+
const { data: linear, source: read } = source === 'display'
|
|
2745
3020
|
? await this._readDisplaySource( target, width, height )
|
|
2746
|
-
: await this.renderer.readRenderTargetPixelsAsync( target, 0, 0, width, height, 0 );
|
|
3021
|
+
: { data: await this.renderer.readRenderTargetPixelsAsync( target, 0, 0, width, height, 0 ), source: 'accumulation' };
|
|
2747
3022
|
|
|
2748
3023
|
// `colorSpace` stays 'linear' — callers branch on it. `workingSpace` is the new, additive
|
|
2749
3024
|
// answer to "linear in what primaries", which only means something once a config is loaded.
|
|
2750
3025
|
if ( colorSpace === 'linear' ) {
|
|
2751
3026
|
|
|
2752
|
-
return { data: linear, width, height, colorSpace, workingSpace: this.color?.workingSpace ?? null };
|
|
3027
|
+
return { data: linear, width, height, colorSpace, workingSpace: this.color?.workingSpace ?? null, source: read };
|
|
2753
3028
|
|
|
2754
3029
|
}
|
|
2755
3030
|
|
|
@@ -2759,7 +3034,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2759
3034
|
if ( named ) {
|
|
2760
3035
|
|
|
2761
3036
|
const { rgba, colorSpace: got } = this.color.exportPixels( linear, colorSpace );
|
|
2762
|
-
return { data: rgba, width, height, colorSpace: got };
|
|
3037
|
+
return { data: rgba, width, height, colorSpace: got, source: read };
|
|
2763
3038
|
|
|
2764
3039
|
}
|
|
2765
3040
|
|
|
@@ -2773,6 +3048,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2773
3048
|
width,
|
|
2774
3049
|
height,
|
|
2775
3050
|
colorSpace,
|
|
3051
|
+
source: read,
|
|
2776
3052
|
};
|
|
2777
3053
|
|
|
2778
3054
|
}
|
|
@@ -3089,6 +3365,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3089
3365
|
*/
|
|
3090
3366
|
getMemoryInfo() {
|
|
3091
3367
|
|
|
3368
|
+
// Wired from the loop too, which renderFrames never enters: a headless host would miss these categories.
|
|
3369
|
+
this._ensureVRAMWiring();
|
|
3092
3370
|
return this.stages.pathTracer?.vramTracker?.measure() ?? { current: 0, peak: 0, byCategory: {} };
|
|
3093
3371
|
|
|
3094
3372
|
}
|
|
@@ -3121,11 +3399,19 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3121
3399
|
if ( ! tracker ) return; // stages not ready yet
|
|
3122
3400
|
|
|
3123
3401
|
tracker.register( 'stages', () => this._collectStageTextures() );
|
|
3402
|
+
tracker.register( 'denoiser', () => ( { bytes: this.denoisingManager?.gpuBytes() ?? 0 } ) );
|
|
3403
|
+
// One image per presented surface (a browser may keep one or two more), and the buffer
|
|
3404
|
+
// three's output pass tone-maps through.
|
|
3405
|
+
tracker.register( 'canvas', () => [
|
|
3406
|
+
{ bytes: this.canvas.width * this.canvas.height * 4 },
|
|
3407
|
+
...( this.renderer?._frameBufferTargets?.values() ?? [] ),
|
|
3408
|
+
...( this.overlayManager?.gpuResources() ?? [] ),
|
|
3409
|
+
] );
|
|
3124
3410
|
|
|
3125
3411
|
const remeasure = () => tracker.measure();
|
|
3126
|
-
this._addTrackedListener( this,
|
|
3127
|
-
this._addTrackedListener( this,
|
|
3128
|
-
this._addTrackedListener( this,
|
|
3412
|
+
this._addTrackedListener( this, EngineEvents.SCENE_REBUILD, remeasure );
|
|
3413
|
+
this._addTrackedListener( this, EngineEvents.ENVIRONMENT_LOADED, remeasure );
|
|
3414
|
+
this._addTrackedListener( this, EngineEvents.RESOLUTION_CHANGED, remeasure );
|
|
3129
3415
|
|
|
3130
3416
|
this._vramWired = true;
|
|
3131
3417
|
|
|
@@ -3376,15 +3662,33 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3376
3662
|
async _readDisplaySource( accumulation, width, height ) {
|
|
3377
3663
|
|
|
3378
3664
|
const context = this.pipeline?.context;
|
|
3379
|
-
const
|
|
3380
|
-
if ( !
|
|
3665
|
+
const found = context && this.stages.compositor?.resolveLightSource( context );
|
|
3666
|
+
if ( ! found || found.key === 'pathtracer:color' || found.texture === accumulation.texture ) {
|
|
3667
|
+
|
|
3668
|
+
if ( this._denoiserInUse() ) {
|
|
3381
3669
|
|
|
3382
|
-
|
|
3670
|
+
this._issues.record(
|
|
3671
|
+
ISSUE_CODES.OUTPUT_SOURCE_FALLBACK,
|
|
3672
|
+
'renderToBuffer: \'display\' was asked for, but no denoiser has published a picture — returned the raw accumulation',
|
|
3673
|
+
{ requested: 'display', read: 'accumulation' }
|
|
3674
|
+
);
|
|
3675
|
+
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
return { data: await this.renderer.readRenderTargetPixelsAsync( accumulation, 0, 0, width, height, 0 ), source: 'accumulation' };
|
|
3383
3679
|
|
|
3384
3680
|
}
|
|
3385
3681
|
|
|
3386
3682
|
this._textureReadback ??= new TextureReadback( this.renderer );
|
|
3387
|
-
return await this._textureReadback.read( texture, width, height );
|
|
3683
|
+
return { data: await this._textureReadback.read( found.texture, width, height ), source: found.key.split( ':' )[ 0 ] };
|
|
3684
|
+
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
// With none, the raw accumulation is the display.
|
|
3688
|
+
_denoiserInUse() {
|
|
3689
|
+
|
|
3690
|
+
const dm = this.denoisingManager;
|
|
3691
|
+
return !! dm?.denoiser?.enabled || ( dm?.denoiserStrategy ?? 'none' ) !== 'none';
|
|
3388
3692
|
|
|
3389
3693
|
}
|
|
3390
3694
|
|
|
@@ -3548,9 +3852,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3548
3852
|
|
|
3549
3853
|
setStatusCallback( ( event ) => this.dispatchEvent( event ) );
|
|
3550
3854
|
|
|
3551
|
-
if ( ! navigator.gpu ) {
|
|
3855
|
+
if ( typeof navigator === 'undefined' || ! navigator.gpu ) {
|
|
3552
3856
|
|
|
3553
|
-
throw new Error( 'WebGPU is not
|
|
3857
|
+
throw new Error( 'WebGPU is not available — outside a browser, install one as navigator.gpu (for Node, the `webgpu` package)' );
|
|
3554
3858
|
|
|
3555
3859
|
}
|
|
3556
3860
|
|
|
@@ -3614,7 +3918,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3614
3918
|
requiredLimits,
|
|
3615
3919
|
} );
|
|
3616
3920
|
|
|
3617
|
-
await this.renderer.
|
|
3921
|
+
await initWithoutFrameLoop( this.renderer, this._headless );
|
|
3618
3922
|
|
|
3619
3923
|
// WebGPURenderer swaps in WebGL2 on failure with only a warn(). The wavefront path is
|
|
3620
3924
|
// compute-only, so every frame would fail against an empty canvas.
|
|
@@ -3764,23 +4068,31 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3764
4068
|
} );
|
|
3765
4069
|
this.goboManager = new GoboManager( this.stages.pathTracer, {
|
|
3766
4070
|
onReset: () => this.reset(),
|
|
4071
|
+
issues: this._issues,
|
|
3767
4072
|
} );
|
|
3768
4073
|
this.iesManager = new IESManager( this.stages.pathTracer, {
|
|
3769
4074
|
onReset: () => this.reset(),
|
|
4075
|
+
issues: this._issues,
|
|
3770
4076
|
} );
|
|
3771
4077
|
this._setupDenoisingManager();
|
|
3772
|
-
await this._setupOverlayManager();
|
|
3773
4078
|
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
canvas: this.canvas,
|
|
3777
|
-
orbitControls: this.cameraManager.controls,
|
|
3778
|
-
app: this,
|
|
3779
|
-
} );
|
|
4079
|
+
// A second renderer, a 2D canvas and a gizmo, all for pixels nobody sees.
|
|
4080
|
+
if ( ! this._headless ) {
|
|
3780
4081
|
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
4082
|
+
await this._setupOverlayManager();
|
|
4083
|
+
|
|
4084
|
+
this.transformManager = new TransformManager( {
|
|
4085
|
+
camera: this.cameraManager.camera,
|
|
4086
|
+
canvas: this.canvas,
|
|
4087
|
+
orbitControls: this.cameraManager.controls,
|
|
4088
|
+
app: this,
|
|
4089
|
+
} );
|
|
4090
|
+
|
|
4091
|
+
// The gizmo is part of the scene overlay layer, so it draws on the same
|
|
4092
|
+
// view-resolution surface as the light helpers and the outline.
|
|
4093
|
+
this.overlayManager.register( 'transform', new TransformGizmoHelper( this.transformManager ) );
|
|
4094
|
+
|
|
4095
|
+
}
|
|
3784
4096
|
|
|
3785
4097
|
// Wire cross-manager dependencies
|
|
3786
4098
|
this.interactionManager.setDependencies( {
|
|
@@ -3794,7 +4106,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3794
4106
|
this.denoisingManager.setOverlayManager( this.overlayManager );
|
|
3795
4107
|
this.denoisingManager.setResetCallback( () => this.reset() );
|
|
3796
4108
|
this.denoisingManager.setPostProcessRefreshCallback( () => this.requestPostProcessRefresh() );
|
|
3797
|
-
this.denoisingManager.setDisplayRefreshCallback( () => this.
|
|
4109
|
+
this.denoisingManager.setDisplayRefreshCallback( () => this._presentDisplay() );
|
|
3798
4110
|
this.denoisingManager.setSettings( this.settings );
|
|
3799
4111
|
|
|
3800
4112
|
// Expose environment manager (lives on pathTracer stage)
|
|
@@ -3808,7 +4120,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3808
4120
|
_wireEvents() {
|
|
3809
4121
|
|
|
3810
4122
|
// Forward manager events → app events
|
|
3811
|
-
this._addTrackedListener( this.cameraManager,
|
|
4123
|
+
this._addTrackedListener( this.cameraManager, EngineEvents.CAMERA_SWITCHED, ( e ) => this.dispatchEvent( e ) );
|
|
3812
4124
|
this._addTrackedListener( this.cameraManager, EngineEvents.AUTO_FOCUS_UPDATED, ( e ) => this.dispatchEvent( e ) );
|
|
3813
4125
|
this._addTrackedListener( this.cameraManager, EngineEvents.ORTHO_HEIGHT_UPDATED, ( e ) => this.dispatchEvent( e ) );
|
|
3814
4126
|
this._addTrackedListener( this.timeline, EngineEvents.TIMELINE_CHANGED, ( e ) => this.dispatchEvent( e ) );
|
|
@@ -3816,7 +4128,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3816
4128
|
this._forwardEvents( this.denoisingManager, [
|
|
3817
4129
|
EngineEvents.DENOISING_START, EngineEvents.DENOISING_END,
|
|
3818
4130
|
EngineEvents.UPSCALING_START, EngineEvents.UPSCALING_PROGRESS, EngineEvents.UPSCALING_END,
|
|
3819
|
-
|
|
4131
|
+
EngineEvents.RESOLUTION_CHANGED,
|
|
3820
4132
|
] );
|
|
3821
4133
|
|
|
3822
4134
|
this._setupAutoExposureListener();
|
|
@@ -3970,7 +4282,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3970
4282
|
|
|
3971
4283
|
_createStages() {
|
|
3972
4284
|
|
|
3973
|
-
this.stages.pathTracer = new PathTracer( this.renderer, this.scene, this.cameraManager.camera );
|
|
4285
|
+
this.stages.pathTracer = new PathTracer( this.renderer, this.scene, this.cameraManager.camera, { hostMemoryGB: this._hostMemoryGB } );
|
|
3974
4286
|
this.stages.normalDepth = new NormalDepth( this.renderer, {
|
|
3975
4287
|
pathTracer: this.stages.pathTracer
|
|
3976
4288
|
} );
|
|
@@ -4011,6 +4323,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
4011
4323
|
pipeline: this.pipeline,
|
|
4012
4324
|
getExposure: () => this.settings.get( 'exposure' ) ?? 1.0,
|
|
4013
4325
|
getSaturation: () => this.settings.get( 'saturation' ) ?? 1.0,
|
|
4326
|
+
issues: this._issues,
|
|
4014
4327
|
} );
|
|
4015
4328
|
|
|
4016
4329
|
this.denoisingManager.setupDenoiser();
|