rayzee 9.1.2 → 9.2.1
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 +176 -16
- package/dist/{NeuralRetouch-BRquqfmZ.js → NeuralRetouch-CYgkQY8Z.js} +1 -1
- package/dist/{NeuralSuperRes-CbNvQjCw.js → NeuralSuperRes-PkgDLQcD.js} +72 -40
- package/dist/{ToneMapGPU-DpUZDMbQ.js → ToneMapGPU-B6XIe3DU.js} +102 -92
- package/dist/rayzee.es.js +6209 -5571
- package/dist/rayzee.umd.js +137 -126
- 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 +511 -136
- 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/TextureReadback.js +26 -10
- package/src/Processor/ToneMapGPU.js +45 -15
- 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/Storage/DownloadCache.js +30 -7
- 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,20 +23,23 @@ 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';
|
|
32
32
|
import { fileIdentity, identityKey } from './Storage/identity.js';
|
|
33
33
|
import { SETTING_SOURCE } from './RenderSettings.js';
|
|
34
34
|
import { toneMapToRGBA8 } from './Processor/ToneMapCPU.js';
|
|
35
|
+
import { PackedToneMapper } from './Processor/ToneMapGPU.js';
|
|
35
36
|
import { ColorManagement, setActiveColorManagement } from './Color/ColorManagement.js';
|
|
36
37
|
import { getViewTransform } from './Color/ViewTransforms.js';
|
|
37
38
|
import { AssetLoader } from './Processor/AssetLoader.js';
|
|
38
39
|
import { SPILL_TRIANGLE_BUDGET, SPILL_PLACEMENT_BUDGET } from './Processor/PBRT/index.js';
|
|
39
40
|
import { SceneProcessor } from './Processor/SceneProcessor.js';
|
|
41
|
+
import { deviceMemoryGB } from './Processor/HostMemory.js';
|
|
42
|
+
import { createHeadlessCanvas } from './HeadlessCanvas.js';
|
|
40
43
|
|
|
41
44
|
// Managers
|
|
42
45
|
import { RenderSettings } from './RenderSettings.js';
|
|
@@ -53,6 +56,7 @@ import { TransformGizmoHelper } from './managers/helpers/TransformGizmoHelper.js
|
|
|
53
56
|
import { captureSceneState, applySceneState } from './SceneState/SceneState.js';
|
|
54
57
|
import { TRIANGLE_PATCH_PROPERTIES } from './managers/MaterialDataManager.js';
|
|
55
58
|
import { VERSION } from './version.js';
|
|
59
|
+
import { toPortable } from './SceneState/portable.js';
|
|
56
60
|
|
|
57
61
|
export const RENDER_CHECKPOINT_VERSION = 1;
|
|
58
62
|
|
|
@@ -66,6 +70,29 @@ const partOf = ( { element, pbrtEntry } = {} ) => ( {
|
|
|
66
70
|
// instantiates (StrictMode, HMR, etc.) so its rAF loop can't burn CPU.
|
|
67
71
|
const _appsByCanvas = new WeakMap();
|
|
68
72
|
|
|
73
|
+
const KNOWN_EVENTS = new Set( [ ...Object.values( EngineEvents ), ...Object.values( LEGACY_EVENT_NAMES ) ] );
|
|
74
|
+
const _warnedEvents = new Set();
|
|
75
|
+
|
|
76
|
+
// three's Animation.start() calls self.requestAnimationFrame during init() with no guard, and
|
|
77
|
+
// outside a browser there is no `self`. A headless app never uses that loop, so it gets an inert one.
|
|
78
|
+
async function initWithoutFrameLoop( renderer, headless ) {
|
|
79
|
+
|
|
80
|
+
if ( ! headless || typeof self !== 'undefined' ) return renderer.init();
|
|
81
|
+
|
|
82
|
+
globalThis.self = { requestAnimationFrame: () => 0, cancelAnimationFrame: () => {} };
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
|
|
86
|
+
return await renderer.init();
|
|
87
|
+
|
|
88
|
+
} finally {
|
|
89
|
+
|
|
90
|
+
delete globalThis.self;
|
|
91
|
+
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
}
|
|
95
|
+
|
|
69
96
|
|
|
70
97
|
/**
|
|
71
98
|
* WebGPU Path Tracer Application.
|
|
@@ -137,6 +164,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
137
164
|
* @param {HTMLCanvasElement} canvas - Canvas element for rendering
|
|
138
165
|
* @param {Object} [options] - Engine options
|
|
139
166
|
* @param {boolean} [options.autoResize=true] - Automatically listen for window resize events
|
|
167
|
+
* @param {boolean} [options.headless=false] - a render nobody watches, and the default when no canvas
|
|
168
|
+
* is given: the engine supplies its own canvas (see HeadlessCanvas.js), runs no render loop — drive it
|
|
169
|
+
* with renderFrames() or renderUntilComplete() — and builds neither the overlay nor the gizmo
|
|
140
170
|
* @param {HTMLElement} [options.container] - Single DOM parent the engine mounts all auxiliary
|
|
141
171
|
* elements into (HUD overlay, denoiser canvas). Defaults to `canvas.parentNode`.
|
|
142
172
|
* @param {boolean} [options.strict=false] - Throw at the point of degradation instead of
|
|
@@ -146,8 +176,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
146
176
|
* @param {number} [options.maxSceneBytes] - refuse a scene whose estimated host memory is
|
|
147
177
|
* above this. The default refuses where the renderer process would be killed instead of
|
|
148
178
|
* throwing; raise it deliberately, on a fresh browser. See HostMemory.js.
|
|
179
|
+
* @param {number} [options.hostMemoryGB] - the host's memory, where `navigator.deviceMemory` is
|
|
180
|
+
* absent (anything but Chrome) or wrong; it sizes the render reserve and the path pool
|
|
149
181
|
* @param {false|'auto'|StorageManager} [options.storage] - on-disk storage; defaults to
|
|
150
|
-
* `configureAssets( { storage } )
|
|
182
|
+
* `configureAssets( { storage } )`, or off under `strict` unless that was set. A host-supplied
|
|
183
|
+
* manager stays the host's to dispose.
|
|
151
184
|
* @param {boolean} [options.memorySpill=false] - experimental: once a large static scene is
|
|
152
185
|
* on the GPU, move its triangle records and BLAS nodes to disk (see
|
|
153
186
|
* {@link ensureSceneResident}). Needs storage; skipped for animated scenes.
|
|
@@ -159,6 +192,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
159
192
|
|
|
160
193
|
super();
|
|
161
194
|
|
|
195
|
+
this._headless = options.headless === true || ! canvas;
|
|
196
|
+
canvas ??= createHeadlessCanvas();
|
|
197
|
+
|
|
162
198
|
try {
|
|
163
199
|
|
|
164
200
|
_appsByCanvas.get( canvas )?.dispose();
|
|
@@ -172,9 +208,10 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
172
208
|
_appsByCanvas.set( canvas, this );
|
|
173
209
|
|
|
174
210
|
this.canvas = canvas;
|
|
175
|
-
this._autoResize = options.autoResize !== false;
|
|
211
|
+
this._autoResize = ! this._headless && options.autoResize !== false;
|
|
176
212
|
// A scene budget the host may raise; read where SceneProcessor is built, well after this.
|
|
177
213
|
this._maxSceneBytes = options.maxSceneBytes;
|
|
214
|
+
this._hostMemoryGB = options.hostMemoryGB;
|
|
178
215
|
this._storageOption = options.storage;
|
|
179
216
|
this._memorySpill = options.memorySpill === true;
|
|
180
217
|
/** @type {?StorageManager} on-disk storage, null when off or unavailable */
|
|
@@ -187,6 +224,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
187
224
|
|
|
188
225
|
// Before the settings: the profile supplies some of their defaults.
|
|
189
226
|
this._profile = getRenderProfile( options.profile );
|
|
227
|
+
this._profileName = options.profile ?? 'viewer';
|
|
190
228
|
|
|
191
229
|
// First, so no subsystem can degrade unrecorded.
|
|
192
230
|
this._issues = new IssueLog( {
|
|
@@ -378,7 +416,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
378
416
|
|
|
379
417
|
async _initStorage() {
|
|
380
418
|
|
|
381
|
-
|
|
419
|
+
// A batch render must not be answered from an earlier run's cache unless the host asked for one.
|
|
420
|
+
const option = this._storageOption
|
|
421
|
+
?? ( this._issues.strict && ! isAssetConfigured( 'storage' ) ? false : getAssetConfig().storage );
|
|
382
422
|
if ( option === false ) return;
|
|
383
423
|
|
|
384
424
|
if ( option instanceof StorageManager ) {
|
|
@@ -418,11 +458,34 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
418
458
|
|
|
419
459
|
}
|
|
420
460
|
|
|
461
|
+
dispatchEvent( event ) {
|
|
462
|
+
|
|
463
|
+
super.dispatchEvent( event );
|
|
464
|
+
const legacy = LEGACY_EVENT_NAMES[ event.type ];
|
|
465
|
+
if ( legacy ) super.dispatchEvent( { ...event, type: legacy } );
|
|
466
|
+
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
addEventListener( type, listener ) {
|
|
470
|
+
|
|
471
|
+
if ( ! KNOWN_EVENTS.has( type ) && ! _warnedEvents.has( type ) ) {
|
|
472
|
+
|
|
473
|
+
_warnedEvents.add( type );
|
|
474
|
+
log.warn( `no engine event is named "${type}", so this listener will never fire — see EngineEvents` );
|
|
475
|
+
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
super.addEventListener( type, listener );
|
|
479
|
+
|
|
480
|
+
}
|
|
481
|
+
|
|
421
482
|
/**
|
|
422
483
|
* Starts the animation loop.
|
|
423
484
|
*/
|
|
424
485
|
animate() {
|
|
425
486
|
|
|
487
|
+
if ( this._headless ) throw new Error( 'a headless app has no render loop — drive it with renderFrames() or renderUntilComplete()' );
|
|
488
|
+
|
|
426
489
|
// Device lost: stop the loop rather than rescheduling render() on a dead device.
|
|
427
490
|
if ( this._deviceLost ) return;
|
|
428
491
|
|
|
@@ -523,42 +586,15 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
523
586
|
|
|
524
587
|
}
|
|
525
588
|
|
|
526
|
-
|
|
527
|
-
this.
|
|
589
|
+
// A lockstep readback is due and still in flight; the frame would trace nothing.
|
|
590
|
+
if ( this.stages.pathTracer.readbackWait() ) {
|
|
528
591
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
this.completion.updateTime();
|
|
532
|
-
this.denoisingManager?.tickContinuousDenoise( this.stages.pathTracer.frameCount );
|
|
592
|
+
this.dispatchEvent( { type: EngineEvents.FRAME } );
|
|
593
|
+
return;
|
|
533
594
|
|
|
534
595
|
}
|
|
535
596
|
|
|
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
|
-
}
|
|
597
|
+
this._traceFrame( { liveDenoise: true } );
|
|
562
598
|
|
|
563
599
|
// Render completion → denoise/upscale chain
|
|
564
600
|
if ( this.stages.pathTracer.isComplete && this.completion.markComplete() ) {
|
|
@@ -568,17 +604,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
568
604
|
context: this.pipeline?.context,
|
|
569
605
|
} );
|
|
570
606
|
|
|
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 } );
|
|
607
|
+
this.dispatchEvent( { type: EngineEvents.RENDER_COMPLETE, ...this._completionInfo() } );
|
|
582
608
|
|
|
583
609
|
}
|
|
584
610
|
|
|
@@ -589,6 +615,60 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
589
615
|
|
|
590
616
|
}
|
|
591
617
|
|
|
618
|
+
// One frame of a render and its bookkeeping, shared by animate() and renderUntilComplete().
|
|
619
|
+
_traceFrame( { liveDenoise = false } = {} ) {
|
|
620
|
+
|
|
621
|
+
const stage = this.stages.pathTracer;
|
|
622
|
+
this.pipeline.render();
|
|
623
|
+
this.denoisingManager?.afterTrace();
|
|
624
|
+
|
|
625
|
+
if ( ! stage.isComplete ) {
|
|
626
|
+
|
|
627
|
+
this.completion.updateTime();
|
|
628
|
+
if ( liveDenoise ) this.denoisingManager?.tickContinuousDenoise( stage.frameCount );
|
|
629
|
+
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
this._ensureVRAMWiring();
|
|
633
|
+
// VRAM is monotonic and only changes on allocation events (scene/env
|
|
634
|
+
// load, resize — each re-measures via _ensureVRAMWiring). Within an
|
|
635
|
+
// accumulation burst nothing reallocates, so re-walking every stage's
|
|
636
|
+
// textures each frame is wasted. Measure at burst start (catches any
|
|
637
|
+
// reset-triggered allocation) + a periodic backstop; read cached otherwise.
|
|
638
|
+
const tracker = stage.vramTracker;
|
|
639
|
+
const frame = stage.frameCount ?? 0;
|
|
640
|
+
if ( tracker && ( frame <= 1 || frame % 30 === 0 ) ) tracker.measure();
|
|
641
|
+
|
|
642
|
+
updateStats( {
|
|
643
|
+
timeElapsed: this.completion.timeElapsed,
|
|
644
|
+
samples: getDisplaySamples( stage ),
|
|
645
|
+
memoryUsed: tracker?.current ?? 0,
|
|
646
|
+
memoryPeak: tracker?.peak ?? 0,
|
|
647
|
+
} );
|
|
648
|
+
|
|
649
|
+
// Only the wall-clock stop — PathTracer.render() retires the ceiling and convergence
|
|
650
|
+
// itself, so whichever of the three arrives first wins.
|
|
651
|
+
if ( this.completion.isTimeLimitReached( stage, this.settings.get( 'renderLimitMode' ), this.settings.get( 'renderTimeLimit' ) ) ) {
|
|
652
|
+
|
|
653
|
+
stage.isComplete = true;
|
|
654
|
+
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
_completionInfo() {
|
|
660
|
+
|
|
661
|
+
return {
|
|
662
|
+
samples: this.stages.pathTracer.frameCount,
|
|
663
|
+
timeElapsed: this.completion.timeElapsed,
|
|
664
|
+
budgetOverrun: this.completion.budgetOverrun,
|
|
665
|
+
// null means isComplete was forced rather than earned (the reconcile below);
|
|
666
|
+
// a forced stop is closer to the ceiling than to convergence.
|
|
667
|
+
reason: this.completion.stopCondition( this.stages.pathTracer ) ?? 'samples',
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
}
|
|
671
|
+
|
|
592
672
|
/**
|
|
593
673
|
* Stops the animation loop.
|
|
594
674
|
*/
|
|
@@ -621,7 +701,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
621
701
|
/** Wakes the animation loop if it was stopped due to idle. */
|
|
622
702
|
wake() {
|
|
623
703
|
|
|
624
|
-
if ( this._deviceLost ) return;
|
|
704
|
+
if ( this._deviceLost || this._headless ) return;
|
|
625
705
|
if ( ! this.animationManagerId && this.isInitialized && ! this._paused ) this.animate();
|
|
626
706
|
|
|
627
707
|
}
|
|
@@ -638,7 +718,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
638
718
|
resume() {
|
|
639
719
|
|
|
640
720
|
this._paused = false;
|
|
641
|
-
if ( ! this.animationManagerId ) this.animate();
|
|
721
|
+
if ( ! this.animationManagerId && ! this._headless ) this.animate();
|
|
642
722
|
|
|
643
723
|
}
|
|
644
724
|
|
|
@@ -675,7 +755,6 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
675
755
|
|
|
676
756
|
this.completion.reset();
|
|
677
757
|
this.wake();
|
|
678
|
-
this.dispatchEvent( { type: 'RenderReset' } );
|
|
679
758
|
this.dispatchEvent( { type: EngineEvents.RENDER_RESET } );
|
|
680
759
|
|
|
681
760
|
}
|
|
@@ -708,6 +787,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
708
787
|
this.color = null;
|
|
709
788
|
this._textureReadback?.dispose();
|
|
710
789
|
this._textureReadback = null;
|
|
790
|
+
this._readbackToneMapper?.dispose();
|
|
791
|
+
this._readbackToneMapper = null;
|
|
711
792
|
|
|
712
793
|
this.interactionManager?.deselect?.();
|
|
713
794
|
this.transformManager?.detach?.();
|
|
@@ -859,7 +940,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
859
940
|
|
|
860
941
|
this.denoisingManager?.dropDisplay();
|
|
861
942
|
this.reset();
|
|
862
|
-
this.dispatchEvent( { type:
|
|
943
|
+
this.dispatchEvent( { type: EngineEvents.SCENE_UNLOADED } );
|
|
863
944
|
|
|
864
945
|
}
|
|
865
946
|
|
|
@@ -872,7 +953,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
872
953
|
|
|
873
954
|
await this._loadWithSceneRebuild(
|
|
874
955
|
() => this.assetLoader.loadModel( url, options ),
|
|
875
|
-
{ type:
|
|
956
|
+
{ type: EngineEvents.MODEL_LOADED, url },
|
|
876
957
|
{ kind: 'url', url, cacheKey: options.cacheKey ?? null }
|
|
877
958
|
);
|
|
878
959
|
|
|
@@ -897,7 +978,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
897
978
|
|
|
898
979
|
await this._loadWithSceneRebuild(
|
|
899
980
|
() => this.assetLoader.loadObject3D( object3d, name ),
|
|
900
|
-
{ type:
|
|
981
|
+
{ type: EngineEvents.OBJECT3D_LOADED, name },
|
|
901
982
|
{ kind: 'object3d', name }
|
|
902
983
|
);
|
|
903
984
|
|
|
@@ -939,7 +1020,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
939
1020
|
|
|
940
1021
|
this.pipeline?.eventBus.emit( 'autoexposure:resetHistory' );
|
|
941
1022
|
this.reset();
|
|
942
|
-
this.dispatchEvent( { type:
|
|
1023
|
+
this.dispatchEvent( { type: EngineEvents.ENVIRONMENT_LOADED, url } );
|
|
943
1024
|
|
|
944
1025
|
} finally {
|
|
945
1026
|
|
|
@@ -991,7 +1072,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
991
1072
|
|
|
992
1073
|
await this._loadWithSceneRebuild(
|
|
993
1074
|
() => this.assetLoader.loadAssetFromUrl( file, { ...options, filename } ),
|
|
994
|
-
{ type:
|
|
1075
|
+
{ type: EngineEvents.MODEL_LOADED, filename },
|
|
995
1076
|
{ kind: 'url', url: file, filename, cacheKey: options.cacheKey ?? null, ...partOf( options ) }
|
|
996
1077
|
);
|
|
997
1078
|
return;
|
|
@@ -1006,7 +1087,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1006
1087
|
const identity = fileIdentity( file ).catch( () => ( { name: file.name, size: file.size, lastModified: file.lastModified ?? 0, sample: null } ) );
|
|
1007
1088
|
await this._loadWithSceneRebuild(
|
|
1008
1089
|
() => this.assetLoader.loadAssetFromFile( file, options ),
|
|
1009
|
-
{ type:
|
|
1090
|
+
{ type: EngineEvents.MODEL_LOADED, filename: file.name },
|
|
1010
1091
|
async () => {
|
|
1011
1092
|
|
|
1012
1093
|
const id = await identity;
|
|
@@ -1037,7 +1118,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1037
1118
|
|
|
1038
1119
|
this.pipeline?.eventBus.emit( 'autoexposure:resetHistory' );
|
|
1039
1120
|
this.reset();
|
|
1040
|
-
this.dispatchEvent( { type:
|
|
1121
|
+
this.dispatchEvent( { type: EngineEvents.ENVIRONMENT_LOADED, filename: file.name } );
|
|
1041
1122
|
|
|
1042
1123
|
} finally {
|
|
1043
1124
|
|
|
@@ -1056,7 +1137,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1056
1137
|
|
|
1057
1138
|
await this._loadWithSceneRebuild(
|
|
1058
1139
|
() => this.assetLoader.loadExampleModels( index, modelFiles ),
|
|
1059
|
-
{ type:
|
|
1140
|
+
{ type: EngineEvents.MODEL_LOADED, index },
|
|
1060
1141
|
{ kind: 'url', url: modelFiles[ index ]?.url ?? null, cacheKey: null }
|
|
1061
1142
|
);
|
|
1062
1143
|
|
|
@@ -1106,7 +1187,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1106
1187
|
|
|
1107
1188
|
await this.loadSceneData();
|
|
1108
1189
|
this.reset();
|
|
1109
|
-
this.dispatchEvent( { type:
|
|
1190
|
+
this.dispatchEvent( { type: EngineEvents.TEXTURES_REPROCESSED, maxTextureSize: this._maxTextureSize } );
|
|
1110
1191
|
|
|
1111
1192
|
} finally {
|
|
1112
1193
|
|
|
@@ -1175,6 +1256,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1175
1256
|
// there is nothing to fall back to. Leaving it half-built would keep the last
|
|
1176
1257
|
// frame's buffers on screen under a scene that no longer exists.
|
|
1177
1258
|
if ( ! error || error.code !== 'LOAD_IN_PROGRESS' ) this._discardFailedLoad();
|
|
1259
|
+
|
|
1260
|
+
// Only the scene build reported its own failure; one in the parse left a host's loading
|
|
1261
|
+
// status spinning on its last step. A part prompt is not a failure, so it just ends it.
|
|
1262
|
+
if ( error?.code === 'ARCHIVE_NEEDS_ELEMENT' ) resetLoading();
|
|
1263
|
+
else if ( error?.code !== 'LOAD_IN_PROGRESS' ) updateLoading( { status: `Error: ${error?.message ?? error}`, failed: true, progress: 100 } );
|
|
1178
1264
|
throw error;
|
|
1179
1265
|
|
|
1180
1266
|
} finally {
|
|
@@ -1446,7 +1532,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1446
1532
|
|
|
1447
1533
|
this._initAnimationAndTransforms();
|
|
1448
1534
|
|
|
1449
|
-
this.dispatchEvent( { type:
|
|
1535
|
+
this.dispatchEvent( { type: EngineEvents.SCENE_REBUILD } );
|
|
1450
1536
|
return true;
|
|
1451
1537
|
|
|
1452
1538
|
}
|
|
@@ -1540,7 +1626,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1540
1626
|
root.userData.__rayzeeSourceUrl = url;
|
|
1541
1627
|
if ( cacheKey ) root.userData.__rayzeeCacheKey = cacheKey;
|
|
1542
1628
|
if ( name ) root.userData.__rayzeeName = name;
|
|
1543
|
-
await this._finishRebuildNoReframe( { type:
|
|
1629
|
+
await this._finishRebuildNoReframe( { type: EngineEvents.MODEL_ADDED, url, id: root.uuid } );
|
|
1544
1630
|
return root.uuid;
|
|
1545
1631
|
|
|
1546
1632
|
} finally {
|
|
@@ -1575,7 +1661,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1575
1661
|
const { root } = this.assetLoader.appendObject3D( object3d, name || 'object3d' );
|
|
1576
1662
|
root.userData.__rayzeeSceneObject = true;
|
|
1577
1663
|
if ( name ) root.userData.__rayzeeName = name;
|
|
1578
|
-
await this._finishRebuildNoReframe( { type:
|
|
1664
|
+
await this._finishRebuildNoReframe( { type: EngineEvents.MODEL_ADDED, id: root.uuid } );
|
|
1579
1665
|
return root.uuid;
|
|
1580
1666
|
|
|
1581
1667
|
} finally {
|
|
@@ -1627,7 +1713,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1627
1713
|
|
|
1628
1714
|
// Ground is permanent (removal refused above), so the scene always keeps
|
|
1629
1715
|
// renderable geometry — a full rebuild is always valid here.
|
|
1630
|
-
await this._finishRebuildNoReframe( { type:
|
|
1716
|
+
await this._finishRebuildNoReframe( { type: EngineEvents.SCENE_OBJECT_REMOVED, id } );
|
|
1631
1717
|
|
|
1632
1718
|
return true;
|
|
1633
1719
|
|
|
@@ -1745,7 +1831,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1745
1831
|
|
|
1746
1832
|
if ( ! this._memorySpill || ! this.storage || this.animationManager?.hasAnimations ) return;
|
|
1747
1833
|
this._sdf?.spillToDisk( this.storage )
|
|
1748
|
-
.then( result => result && this.dispatchEvent( { type:
|
|
1834
|
+
.then( result => result && this.dispatchEvent( { type: EngineEvents.SCENE_SPILLED, ...result } ) )
|
|
1749
1835
|
.catch( error => this._issues.warn( ISSUE_CODES.STORAGE_WRITE_FAILED, `memory spill failed: ${error.message}` ) );
|
|
1750
1836
|
|
|
1751
1837
|
}
|
|
@@ -1764,7 +1850,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1764
1850
|
_dispatchCamerasUpdated() {
|
|
1765
1851
|
|
|
1766
1852
|
this.dispatchEvent( {
|
|
1767
|
-
type:
|
|
1853
|
+
type: EngineEvents.CAMERAS_UPDATED,
|
|
1768
1854
|
cameras: this.cameraManager.cameras,
|
|
1769
1855
|
cameraNames: this.cameraManager.getCameraNames(),
|
|
1770
1856
|
} );
|
|
@@ -2060,12 +2146,24 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2060
2146
|
|
|
2061
2147
|
const limits = this.renderer.backend?.device?.limits;
|
|
2062
2148
|
const maxBinding = limits?.maxStorageBufferBindingSize || ( 128 * 1024 * 1024 );
|
|
2063
|
-
const
|
|
2149
|
+
const memory = deviceMemoryGB( this._hostMemoryGB );
|
|
2064
2150
|
// 4K reserve pins the accum MRT (~1.5 GB) + aux; only grant it on clearly-capable GPUs.
|
|
2065
|
-
const deviceSafeMax = (
|
|
2151
|
+
const deviceSafeMax = ( memory.gb >= 8 && maxBinding >= 1024 * 1024 * 1024 )
|
|
2066
2152
|
? MAX_RESERVABLE_RENDER_SIZE : 2048;
|
|
2067
2153
|
const applied = setReservedRenderSize( Math.min( target, deviceSafeMax ) );
|
|
2068
2154
|
|
|
2155
|
+
if ( applied < Math.floor( requestedPx ) ) {
|
|
2156
|
+
|
|
2157
|
+
this._issues.warn(
|
|
2158
|
+
ISSUE_CODES.RENDER_RESERVE_CAPPED,
|
|
2159
|
+
`reserved render size ${fmt.n( requestedPx )}px was capped to ${fmt.n( applied )}px by this device's limits — ` +
|
|
2160
|
+
`renders above ${fmt.n( applied )}px will be declined` +
|
|
2161
|
+
( memory.assumed && memory.gb < 8 ? ' (host memory unknown, 4 GB assumed: pass hostMemoryGB)' : '' ),
|
|
2162
|
+
{ requested: requestedPx, applied, deviceMemoryGB: memory.gb, assumed: memory.assumed, maxStorageBufferBindingSize: maxBinding }
|
|
2163
|
+
);
|
|
2164
|
+
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2069
2167
|
// Gate the realloc on the textures that EXIST, not on how the binding moved: a raise applied while
|
|
2070
2168
|
// nothing was allocated leaves `applied === prev` for every later call, so keying off that let the
|
|
2071
2169
|
// first ineffective call poison every effective one after it (issue #9). The path tracer's write MRT
|
|
@@ -2102,7 +2200,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2102
2200
|
}
|
|
2103
2201
|
|
|
2104
2202
|
this.reset();
|
|
2105
|
-
this.dispatchEvent( { type:
|
|
2203
|
+
this.dispatchEvent( { type: EngineEvents.RESERVED_RENDER_SIZE_CHANGED, size: applied } );
|
|
2106
2204
|
|
|
2107
2205
|
}
|
|
2108
2206
|
|
|
@@ -2124,19 +2222,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2124
2222
|
this._pendingReservedRenderSize = null;
|
|
2125
2223
|
|
|
2126
2224
|
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 } );
|
|
2225
|
+
this.dispatchEvent( { type: EngineEvents.RESERVED_RENDER_SIZE_CHANGED, size: applied } );
|
|
2140
2226
|
|
|
2141
2227
|
}
|
|
2142
2228
|
|
|
@@ -2206,7 +2292,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2206
2292
|
this.denoisingManager?.setRenderSize( renderWidth, renderHeight );
|
|
2207
2293
|
this.needsReset = true;
|
|
2208
2294
|
|
|
2209
|
-
this.dispatchEvent( { type:
|
|
2295
|
+
this.dispatchEvent( { type: EngineEvents.RESOLUTION_CHANGED, width: renderWidth, height: renderHeight } );
|
|
2210
2296
|
|
|
2211
2297
|
}
|
|
2212
2298
|
|
|
@@ -2382,6 +2468,43 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2382
2468
|
|
|
2383
2469
|
}
|
|
2384
2470
|
|
|
2471
|
+
// Draws once without waking the loop: a woken loop takes a finished render nothing marked
|
|
2472
|
+
// complete (renderFrames, a video export) as newly finished, and denoises it again.
|
|
2473
|
+
_presentDisplay() {
|
|
2474
|
+
|
|
2475
|
+
if ( this.animationManagerId ) {
|
|
2476
|
+
|
|
2477
|
+
this._needsDisplayRefresh = true;
|
|
2478
|
+
return;
|
|
2479
|
+
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
const context = this.pipeline?.context;
|
|
2483
|
+
if ( this._deviceLost || ! context ) return;
|
|
2484
|
+
this.stages.compositor?.render( context );
|
|
2485
|
+
this._renderHelperOverlay();
|
|
2486
|
+
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
/**
|
|
2490
|
+
* One OIDN pass over the current accumulation at the final tier, resolved once the denoised
|
|
2491
|
+
* picture is published. Does not start the render loop or the upscaler; read the result with
|
|
2492
|
+
* `renderToBuffer( { source: 'display' } )`.
|
|
2493
|
+
* @returns {Promise<boolean>} whether a denoised picture was published
|
|
2494
|
+
*/
|
|
2495
|
+
async runFinalDenoise() {
|
|
2496
|
+
|
|
2497
|
+
const dm = this.denoisingManager;
|
|
2498
|
+
if ( await dm?.denoiseOnce() ) return true;
|
|
2499
|
+
|
|
2500
|
+
const reason = ! dm?.denoiser ? 'the denoiser was not built'
|
|
2501
|
+
: ! dm.denoiser.enabled ? 'OIDN was off while accumulating — call denoisingManager.setOIDNEnabled( true ) first'
|
|
2502
|
+
: 'the denoise did not complete';
|
|
2503
|
+
this._issues.record( ISSUE_CODES.DENOISER_UNAVAILABLE, `final denoise produced no picture: ${reason}`, { reason } );
|
|
2504
|
+
return false;
|
|
2505
|
+
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2385
2508
|
// Aborts any in-flight denoise/upscale and puts the denoiser canvas back at base resolution (the
|
|
2386
2509
|
// upscaler leaves it enlarged), so the live canvas is what's on screen again.
|
|
2387
2510
|
_abortPostProcess( { keepDisplay = false } = {} ) {
|
|
@@ -2392,7 +2515,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2392
2515
|
|
|
2393
2516
|
const w = this.denoisingManager._lastRenderWidth;
|
|
2394
2517
|
const h = this.denoisingManager._lastRenderHeight;
|
|
2395
|
-
this.dispatchEvent( { type:
|
|
2518
|
+
this.dispatchEvent( { type: EngineEvents.RESOLUTION_CHANGED, width: w, height: h } );
|
|
2396
2519
|
|
|
2397
2520
|
}
|
|
2398
2521
|
|
|
@@ -2586,9 +2709,6 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2586
2709
|
/**
|
|
2587
2710
|
* Accumulates exactly `count` samples synchronously, bypassing the rAF loop.
|
|
2588
2711
|
*
|
|
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
2712
|
* A frame retired by adaptive sampling stops advancing `frameCount`, so a fixed-count loop
|
|
2593
2713
|
* can never reach `count`. `allowEarlyRetire` makes that an outcome instead of a throw.
|
|
2594
2714
|
*
|
|
@@ -2606,8 +2726,6 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2606
2726
|
if ( ! stage ) throw new Error( 'renderFrames: app is not initialized' );
|
|
2607
2727
|
if ( ! ( count > 0 ) ) throw new Error( `renderFrames: count must be positive, got ${count}` );
|
|
2608
2728
|
|
|
2609
|
-
await stage.blueNoiseReady;
|
|
2610
|
-
|
|
2611
2729
|
const target = ( reset ? 0 : stage.frameCount ) + count;
|
|
2612
2730
|
|
|
2613
2731
|
// completionThreshold is a cached JS number derived from maxSamples, so this must
|
|
@@ -2631,6 +2749,14 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2631
2749
|
if ( this._deviceLost ) throw new Error( 'renderFrames: WebGPU device lost' );
|
|
2632
2750
|
if ( ! stage.isReady ) throw new Error( 'renderFrames: path tracer stage is not ready' );
|
|
2633
2751
|
|
|
2752
|
+
const wait = stage.readbackWait();
|
|
2753
|
+
if ( wait ) {
|
|
2754
|
+
|
|
2755
|
+
await this._awaitReadback( wait );
|
|
2756
|
+
continue;
|
|
2757
|
+
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2634
2760
|
this.pipeline.render();
|
|
2635
2761
|
passes ++;
|
|
2636
2762
|
|
|
@@ -2669,6 +2795,156 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2669
2795
|
|
|
2670
2796
|
}
|
|
2671
2797
|
|
|
2798
|
+
/**
|
|
2799
|
+
* Renders until the render's own stop condition — the sample ceiling, adaptive convergence or the
|
|
2800
|
+
* time limit, whichever comes first — without requestAnimationFrame, then runs the closing denoise
|
|
2801
|
+
* once. The production counterpart of {@link renderFrames}: adaptive sampling stays on, and its
|
|
2802
|
+
* readbacks run in lockstep (see PathTracer.setLockstepReadbacks), so the same input renders the same
|
|
2803
|
+
* image however fast frames are submitted — except under a time limit, which is wall-clock by nature.
|
|
2804
|
+
* The upscaler does not run. `EngineEvents.RENDER_COMPLETE` fires as it does from the loop.
|
|
2805
|
+
*
|
|
2806
|
+
* @param {Object} [options]
|
|
2807
|
+
* @param {boolean} [options.reset=true] - restart accumulation from sample 0 first
|
|
2808
|
+
* @param {boolean} [options.denoise=true] - run the final OIDN pass when `finalDenoise` is on
|
|
2809
|
+
* @param {AbortSignal} [options.signal] - rejects with its reason once aborted
|
|
2810
|
+
* @param {number} [options.drainEvery=4] - every N frames, wait for the GPU to finish the frames
|
|
2811
|
+
* before the last N, so submissions stay at most 2N frames ahead without the GPU idling
|
|
2812
|
+
* @param {function(number): void} [options.onProgress] - called with the running sample count
|
|
2813
|
+
* @returns {Promise<{samples: number, retiredBy: 'samples'|'converged'|'timeLimit', timeElapsed: number,
|
|
2814
|
+
* budgetOverrun: boolean, denoised: boolean}>}
|
|
2815
|
+
*/
|
|
2816
|
+
async renderUntilComplete( { reset = true, denoise = true, signal, drainEvery = 4, onProgress } = {} ) {
|
|
2817
|
+
|
|
2818
|
+
const stage = this.stages.pathTracer;
|
|
2819
|
+
if ( ! stage ) throw new Error( 'renderUntilComplete: app is not initialized' );
|
|
2820
|
+
if ( this._loadingInProgress || this._sdf?.isProcessing ) throw new Error( 'renderUntilComplete: a scene is still loading' );
|
|
2821
|
+
|
|
2822
|
+
const lockstepWas = stage.lockstepReadbacks;
|
|
2823
|
+
stage.setLockstepReadbacks( true );
|
|
2824
|
+
// A 100 ms wall-clock mode for camera drags; frames in it do not count, so a batch render has no use for it.
|
|
2825
|
+
const interaction = this.settings.getEffective().interactionModeEnabled;
|
|
2826
|
+
this.settings.set( 'interactionModeEnabled', false, { silent: true, reset: false } );
|
|
2827
|
+
|
|
2828
|
+
try {
|
|
2829
|
+
|
|
2830
|
+
if ( reset ) this.reset();
|
|
2831
|
+
this.stopAnimation();
|
|
2832
|
+
|
|
2833
|
+
const queue = this.renderer.backend.device?.queue;
|
|
2834
|
+
let passes = 0;
|
|
2835
|
+
let previousBatch = null;
|
|
2836
|
+
|
|
2837
|
+
while ( ! stage.isComplete ) {
|
|
2838
|
+
|
|
2839
|
+
signal?.throwIfAborted();
|
|
2840
|
+
if ( this._deviceLost ) throw new Error( 'renderUntilComplete: WebGPU device lost' );
|
|
2841
|
+
if ( ! stage.isReady ) throw new Error( 'renderUntilComplete: path tracer stage is not ready' );
|
|
2842
|
+
|
|
2843
|
+
const wait = stage.readbackWait();
|
|
2844
|
+
if ( wait ) {
|
|
2845
|
+
|
|
2846
|
+
await this._awaitReadback( wait );
|
|
2847
|
+
continue;
|
|
2848
|
+
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
this.cameraManager.camera.updateMatrixWorld();
|
|
2852
|
+
const counted = stage.frameCount;
|
|
2853
|
+
this._traceFrame();
|
|
2854
|
+
onProgress?.( stage.frameCount );
|
|
2855
|
+
|
|
2856
|
+
// A frame that did not count, with no readback to wait on, must still give timers a turn.
|
|
2857
|
+
if ( stage.frameCount === counted && ! stage.isComplete ) await new Promise( ( resolve ) => setTimeout( resolve, 0 ) );
|
|
2858
|
+
|
|
2859
|
+
if ( queue && drainEvery > 0 && ++ passes % drainEvery === 0 ) {
|
|
2860
|
+
|
|
2861
|
+
const batch = queue.onSubmittedWorkDone();
|
|
2862
|
+
if ( previousBatch ) await previousBatch;
|
|
2863
|
+
previousBatch = batch;
|
|
2864
|
+
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
const info = this._completionInfo();
|
|
2870
|
+
if ( this.completion.markComplete() ) this.dispatchEvent( { type: EngineEvents.RENDER_COMPLETE, ...info } );
|
|
2871
|
+
|
|
2872
|
+
const denoised = denoise && this.denoisingManager?.finalDenoise ? await this.runFinalDenoise() : false;
|
|
2873
|
+
const { reason, ...rest } = info;
|
|
2874
|
+
return { ...rest, retiredBy: reason, denoised };
|
|
2875
|
+
|
|
2876
|
+
} finally {
|
|
2877
|
+
|
|
2878
|
+
stage.setLockstepReadbacks( lockstepWas );
|
|
2879
|
+
if ( interaction ) this.settings.set( 'interactionModeEnabled', interaction.value, { silent: true, reset: false, source: interaction.source } );
|
|
2880
|
+
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
/**
|
|
2886
|
+
* What produced the current image, as plain JSON, to keep beside a render: engine and three.js
|
|
2887
|
+
* versions, the profile by name and by value (a name's values can change between releases), the
|
|
2888
|
+
* adapter, every live setting with its source, the colour pipeline, and the modes that decide
|
|
2889
|
+
* whether the render reproduces.
|
|
2890
|
+
* @returns {Object}
|
|
2891
|
+
*/
|
|
2892
|
+
getProvenance() {
|
|
2893
|
+
|
|
2894
|
+
const stage = this.stages.pathTracer;
|
|
2895
|
+
const color = this.color?.status();
|
|
2896
|
+
|
|
2897
|
+
return toPortable( {
|
|
2898
|
+
engine: VERSION,
|
|
2899
|
+
three: REVISION,
|
|
2900
|
+
profile: { name: this._profileName, values: { ...this._profile } },
|
|
2901
|
+
adapter: this.adapterInfo ?? null,
|
|
2902
|
+
mode: {
|
|
2903
|
+
headless: this._headless,
|
|
2904
|
+
strict: this._issues.strict,
|
|
2905
|
+
deterministic: this.isDeterministic,
|
|
2906
|
+
lockstep: !! stage?.lockstepReadbacks,
|
|
2907
|
+
},
|
|
2908
|
+
render: stage ? {
|
|
2909
|
+
width: stage.width,
|
|
2910
|
+
height: stage.height,
|
|
2911
|
+
samples: stage.frameCount,
|
|
2912
|
+
complete: stage.isComplete,
|
|
2913
|
+
retiredBy: stage.isComplete ? this.completion.stopCondition( stage ) ?? 'samples' : null,
|
|
2914
|
+
} : null,
|
|
2915
|
+
color: color ? {
|
|
2916
|
+
config: color.config?.id ?? null,
|
|
2917
|
+
workingSpace: color.workingSpace,
|
|
2918
|
+
view: color.activeView ?? color.activeTransform?.id ?? null,
|
|
2919
|
+
context: color.context,
|
|
2920
|
+
} : null,
|
|
2921
|
+
scene: this.sceneSource,
|
|
2922
|
+
settings: this.settings.getEffective(),
|
|
2923
|
+
issues: this.issues.map( ( { code, severity } ) => ( { code, severity } ) ),
|
|
2924
|
+
} );
|
|
2925
|
+
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
/**
|
|
2929
|
+
* Lockstep readbacks for renders driven by the loop or by {@link renderFrames}:
|
|
2930
|
+
* {@link renderUntilComplete} turns them on for its own duration. See PathTracer.setLockstepReadbacks.
|
|
2931
|
+
* @param {boolean} [enabled=true]
|
|
2932
|
+
*/
|
|
2933
|
+
setLockstepReadbacks( enabled = true ) {
|
|
2934
|
+
|
|
2935
|
+
this.stages.pathTracer?.setLockstepReadbacks( enabled );
|
|
2936
|
+
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
// A readback that never lands would hold the render forever; a lost device is the one way it can't.
|
|
2940
|
+
async _awaitReadback( wait ) {
|
|
2941
|
+
|
|
2942
|
+
const lost = this.renderer.backend.device?.lost;
|
|
2943
|
+
const outcome = await ( lost ? Promise.race( [ wait.then( () => null ), lost ] ) : wait );
|
|
2944
|
+
if ( outcome ) throw new Error( `WebGPU device lost while awaiting a readback: ${outcome.message ?? outcome.reason}` );
|
|
2945
|
+
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2672
2948
|
/**
|
|
2673
2949
|
* A render in progress as data: the accumulated colour (and the aux buffers while they hold
|
|
2674
2950
|
* samples), the per-pixel convergence buffers, and the counters that choose the next sample.
|
|
@@ -2721,7 +2997,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2721
2997
|
* @param {'accumulation'|'display'} [options.source='accumulation'] - `'display'` reads what the
|
|
2722
2998
|
* viewport is showing — denoised when a denoiser has run, without bloom — instead of the raw
|
|
2723
2999
|
* accumulation
|
|
2724
|
-
* @returns {Promise<{data: Float32Array|Uint8ClampedArray, width: number, height: number, colorSpace: string
|
|
3000
|
+
* @returns {Promise<{data: Float32Array|Uint8ClampedArray, width: number, height: number, colorSpace: string,
|
|
3001
|
+
* source: string}>} `source` is what was read: `'accumulation'`, or the denoiser that published
|
|
3002
|
+
* it (`'oidn'`, `'asvgf'`, `'nrd'`, `'edgeFiltering'`, `'bilateralFiltering'`)
|
|
2725
3003
|
*/
|
|
2726
3004
|
async renderToBuffer( { colorSpace = 'srgb', preserveAlpha = false, source = 'accumulation' } = {} ) {
|
|
2727
3005
|
|
|
@@ -2741,39 +3019,75 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2741
3019
|
// The pool over-allocates to the reserve, so the texture is larger than the frame.
|
|
2742
3020
|
const { width, height } = stage;
|
|
2743
3021
|
|
|
2744
|
-
|
|
3022
|
+
if ( colorSpace === 'srgb' ) {
|
|
3023
|
+
|
|
3024
|
+
const shown = source === 'display' ? this._displaySource( target ) : null;
|
|
3025
|
+
const tone = {
|
|
3026
|
+
exposure: this.renderer.toneMappingExposure,
|
|
3027
|
+
toneMapping: this.renderer.toneMapping,
|
|
3028
|
+
saturation: this.settings.get( 'saturation' ) ?? 1,
|
|
3029
|
+
preserveAlpha,
|
|
3030
|
+
};
|
|
3031
|
+
|
|
3032
|
+
const data = await this._toneMapOnGPU( shown?.texture ?? null, target, width, height, tone )
|
|
3033
|
+
?? toneMapToRGBA8( shown
|
|
3034
|
+
? await this._readTexture( shown.texture, width, height )
|
|
3035
|
+
: await this.renderer.readRenderTargetPixelsAsync( target, 0, 0, width, height, 0 ), tone );
|
|
3036
|
+
return { data, width, height, colorSpace, source: shown?.source ?? 'accumulation' };
|
|
3037
|
+
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
const { data: linear, source: read } = source === 'display'
|
|
2745
3041
|
? await this._readDisplaySource( target, width, height )
|
|
2746
|
-
: await this.renderer.readRenderTargetPixelsAsync( target, 0, 0, width, height, 0 );
|
|
3042
|
+
: { data: await this.renderer.readRenderTargetPixelsAsync( target, 0, 0, width, height, 0 ), source: 'accumulation' };
|
|
2747
3043
|
|
|
2748
3044
|
// `colorSpace` stays 'linear' — callers branch on it. `workingSpace` is the new, additive
|
|
2749
3045
|
// answer to "linear in what primaries", which only means something once a config is loaded.
|
|
2750
3046
|
if ( colorSpace === 'linear' ) {
|
|
2751
3047
|
|
|
2752
|
-
return { data: linear, width, height, colorSpace, workingSpace: this.color?.workingSpace ?? null };
|
|
3048
|
+
return { data: linear, width, height, colorSpace, workingSpace: this.color?.workingSpace ?? null, source: read };
|
|
2753
3049
|
|
|
2754
3050
|
}
|
|
2755
3051
|
|
|
2756
3052
|
// A named space is a delivery buffer, not a picture: scene-referred float in whatever the
|
|
2757
3053
|
// config calls that space. Saving an EXR for a compositor and grading through an sRGB view
|
|
2758
3054
|
// on screen are different questions, and this is the one that answers the first.
|
|
2759
|
-
|
|
3055
|
+
const { rgba, colorSpace: got } = this.color.exportPixels( linear, colorSpace );
|
|
3056
|
+
return { data: rgba, width, height, colorSpace: got, source: read };
|
|
2760
3057
|
|
|
2761
|
-
|
|
2762
|
-
return { data: rgba, width, height, colorSpace: got };
|
|
3058
|
+
}
|
|
2763
3059
|
|
|
2764
|
-
|
|
3060
|
+
/**
|
|
3061
|
+
* Display bytes tone-mapped on the card, or null to do it on the CPU. On the CPU it took 1.4 s at
|
|
3062
|
+
* 4096×2160 on an M-series Mac and 10 s on a cloud L4's host; the GPU pass matches it within one
|
|
3063
|
+
* level (`tests/gpu/toneMapParity.test.js`).
|
|
3064
|
+
*/
|
|
3065
|
+
async _toneMapOnGPU( shown, accumulation, width, height, tone ) {
|
|
2765
3066
|
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
3067
|
+
const backend = this.renderer?.backend;
|
|
3068
|
+
if ( ! backend?.device ) return null;
|
|
3069
|
+
|
|
3070
|
+
try {
|
|
3071
|
+
|
|
3072
|
+
const texture = shown ? this._readbackPass().draw( shown, width, height ).texture : accumulation.textures[ 0 ];
|
|
3073
|
+
const gpuTexture = backend.get( texture ).texture;
|
|
3074
|
+
if ( ! gpuTexture ) return null;
|
|
3075
|
+
|
|
3076
|
+
this._readbackToneMapper ??= new PackedToneMapper( backend.device, 'rayzee:readback-tonemap', { input: 'texture' } );
|
|
3077
|
+
this._readbackToneMapper.ensureSize( width, height );
|
|
3078
|
+
return await this._readbackToneMapper.toRGBA8( gpuTexture, tone );
|
|
3079
|
+
|
|
3080
|
+
} catch ( error ) {
|
|
3081
|
+
|
|
3082
|
+
log.warn( `renderToBuffer: tone mapping on the GPU failed, doing it on the CPU: ${error?.message ?? error}` );
|
|
3083
|
+
return null;
|
|
3084
|
+
|
|
3085
|
+
} finally {
|
|
3086
|
+
|
|
3087
|
+
this._readbackToneMapper?.release();
|
|
3088
|
+
if ( shown ) this._textureReadback?.release();
|
|
3089
|
+
|
|
3090
|
+
}
|
|
2777
3091
|
|
|
2778
3092
|
}
|
|
2779
3093
|
|
|
@@ -3089,6 +3403,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3089
3403
|
*/
|
|
3090
3404
|
getMemoryInfo() {
|
|
3091
3405
|
|
|
3406
|
+
// Wired from the loop too, which renderFrames never enters: a headless host would miss these categories.
|
|
3407
|
+
this._ensureVRAMWiring();
|
|
3092
3408
|
return this.stages.pathTracer?.vramTracker?.measure() ?? { current: 0, peak: 0, byCategory: {} };
|
|
3093
3409
|
|
|
3094
3410
|
}
|
|
@@ -3121,11 +3437,19 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3121
3437
|
if ( ! tracker ) return; // stages not ready yet
|
|
3122
3438
|
|
|
3123
3439
|
tracker.register( 'stages', () => this._collectStageTextures() );
|
|
3440
|
+
tracker.register( 'denoiser', () => ( { bytes: this.denoisingManager?.gpuBytes() ?? 0 } ) );
|
|
3441
|
+
// One image per presented surface (a browser may keep one or two more), and the buffer
|
|
3442
|
+
// three's output pass tone-maps through.
|
|
3443
|
+
tracker.register( 'canvas', () => [
|
|
3444
|
+
{ bytes: this.canvas.width * this.canvas.height * 4 },
|
|
3445
|
+
...( this.renderer?._frameBufferTargets?.values() ?? [] ),
|
|
3446
|
+
...( this.overlayManager?.gpuResources() ?? [] ),
|
|
3447
|
+
] );
|
|
3124
3448
|
|
|
3125
3449
|
const remeasure = () => tracker.measure();
|
|
3126
|
-
this._addTrackedListener( this,
|
|
3127
|
-
this._addTrackedListener( this,
|
|
3128
|
-
this._addTrackedListener( this,
|
|
3450
|
+
this._addTrackedListener( this, EngineEvents.SCENE_REBUILD, remeasure );
|
|
3451
|
+
this._addTrackedListener( this, EngineEvents.ENVIRONMENT_LOADED, remeasure );
|
|
3452
|
+
this._addTrackedListener( this, EngineEvents.RESOLUTION_CHANGED, remeasure );
|
|
3129
3453
|
|
|
3130
3454
|
this._vramWired = true;
|
|
3131
3455
|
|
|
@@ -3368,6 +3692,35 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3368
3692
|
|
|
3369
3693
|
}
|
|
3370
3694
|
|
|
3695
|
+
/**
|
|
3696
|
+
* What a `'display'` read shows: the texture a denoiser published, or null for the raw
|
|
3697
|
+
* accumulation — recorded as a fallback when a denoiser is in use.
|
|
3698
|
+
* @private
|
|
3699
|
+
*/
|
|
3700
|
+
_displaySource( accumulation ) {
|
|
3701
|
+
|
|
3702
|
+
const context = this.pipeline?.context;
|
|
3703
|
+
const found = context && this.stages.compositor?.resolveLightSource( context );
|
|
3704
|
+
if ( found && found.key !== 'pathtracer:color' && found.texture !== accumulation.texture ) {
|
|
3705
|
+
|
|
3706
|
+
return { texture: found.texture, source: found.key.split( ':' )[ 0 ] };
|
|
3707
|
+
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
if ( this._denoiserInUse() ) {
|
|
3711
|
+
|
|
3712
|
+
this._issues.record(
|
|
3713
|
+
ISSUE_CODES.OUTPUT_SOURCE_FALLBACK,
|
|
3714
|
+
'renderToBuffer: \'display\' was asked for, but no denoiser has published a picture — returned the raw accumulation',
|
|
3715
|
+
{ requested: 'display', read: 'accumulation' }
|
|
3716
|
+
);
|
|
3717
|
+
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
return null;
|
|
3721
|
+
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3371
3724
|
/**
|
|
3372
3725
|
* What the Compositor is drawing from, as linear float. Falls back to the accumulation when no
|
|
3373
3726
|
* denoiser has published anything, which is also what the viewport shows then.
|
|
@@ -3375,16 +3728,29 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3375
3728
|
*/
|
|
3376
3729
|
async _readDisplaySource( accumulation, width, height ) {
|
|
3377
3730
|
|
|
3378
|
-
const
|
|
3379
|
-
|
|
3380
|
-
|
|
3731
|
+
const shown = this._displaySource( accumulation );
|
|
3732
|
+
if ( ! shown ) return { data: await this.renderer.readRenderTargetPixelsAsync( accumulation, 0, 0, width, height, 0 ), source: 'accumulation' };
|
|
3733
|
+
return { data: await this._readTexture( shown.texture, width, height ), source: shown.source };
|
|
3381
3734
|
|
|
3382
|
-
|
|
3735
|
+
}
|
|
3383
3736
|
|
|
3384
|
-
|
|
3737
|
+
_readbackPass() {
|
|
3385
3738
|
|
|
3386
|
-
this._textureReadback ??= new TextureReadback( this.renderer );
|
|
3387
|
-
|
|
3739
|
+
return ( this._textureReadback ??= new TextureReadback( this.renderer ) );
|
|
3740
|
+
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
_readTexture( texture, width, height ) {
|
|
3744
|
+
|
|
3745
|
+
return this._readbackPass().read( texture, width, height );
|
|
3746
|
+
|
|
3747
|
+
}
|
|
3748
|
+
|
|
3749
|
+
// With none, the raw accumulation is the display.
|
|
3750
|
+
_denoiserInUse() {
|
|
3751
|
+
|
|
3752
|
+
const dm = this.denoisingManager;
|
|
3753
|
+
return !! dm?.denoiser?.enabled || ( dm?.denoiserStrategy ?? 'none' ) !== 'none';
|
|
3388
3754
|
|
|
3389
3755
|
}
|
|
3390
3756
|
|
|
@@ -3548,9 +3914,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3548
3914
|
|
|
3549
3915
|
setStatusCallback( ( event ) => this.dispatchEvent( event ) );
|
|
3550
3916
|
|
|
3551
|
-
if ( ! navigator.gpu ) {
|
|
3917
|
+
if ( typeof navigator === 'undefined' || ! navigator.gpu ) {
|
|
3552
3918
|
|
|
3553
|
-
throw new Error( 'WebGPU is not
|
|
3919
|
+
throw new Error( 'WebGPU is not available — outside a browser, install one as navigator.gpu (for Node, the `webgpu` package)' );
|
|
3554
3920
|
|
|
3555
3921
|
}
|
|
3556
3922
|
|
|
@@ -3614,7 +3980,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3614
3980
|
requiredLimits,
|
|
3615
3981
|
} );
|
|
3616
3982
|
|
|
3617
|
-
await this.renderer.
|
|
3983
|
+
await initWithoutFrameLoop( this.renderer, this._headless );
|
|
3618
3984
|
|
|
3619
3985
|
// WebGPURenderer swaps in WebGL2 on failure with only a warn(). The wavefront path is
|
|
3620
3986
|
// compute-only, so every frame would fail against an empty canvas.
|
|
@@ -3764,23 +4130,31 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3764
4130
|
} );
|
|
3765
4131
|
this.goboManager = new GoboManager( this.stages.pathTracer, {
|
|
3766
4132
|
onReset: () => this.reset(),
|
|
4133
|
+
issues: this._issues,
|
|
3767
4134
|
} );
|
|
3768
4135
|
this.iesManager = new IESManager( this.stages.pathTracer, {
|
|
3769
4136
|
onReset: () => this.reset(),
|
|
4137
|
+
issues: this._issues,
|
|
3770
4138
|
} );
|
|
3771
4139
|
this._setupDenoisingManager();
|
|
3772
|
-
await this._setupOverlayManager();
|
|
3773
4140
|
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
4141
|
+
// A second renderer, a 2D canvas and a gizmo, all for pixels nobody sees.
|
|
4142
|
+
if ( ! this._headless ) {
|
|
4143
|
+
|
|
4144
|
+
await this._setupOverlayManager();
|
|
4145
|
+
|
|
4146
|
+
this.transformManager = new TransformManager( {
|
|
4147
|
+
camera: this.cameraManager.camera,
|
|
4148
|
+
canvas: this.canvas,
|
|
4149
|
+
orbitControls: this.cameraManager.controls,
|
|
4150
|
+
app: this,
|
|
4151
|
+
} );
|
|
3780
4152
|
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
4153
|
+
// The gizmo is part of the scene overlay layer, so it draws on the same
|
|
4154
|
+
// view-resolution surface as the light helpers and the outline.
|
|
4155
|
+
this.overlayManager.register( 'transform', new TransformGizmoHelper( this.transformManager ) );
|
|
4156
|
+
|
|
4157
|
+
}
|
|
3784
4158
|
|
|
3785
4159
|
// Wire cross-manager dependencies
|
|
3786
4160
|
this.interactionManager.setDependencies( {
|
|
@@ -3794,7 +4168,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3794
4168
|
this.denoisingManager.setOverlayManager( this.overlayManager );
|
|
3795
4169
|
this.denoisingManager.setResetCallback( () => this.reset() );
|
|
3796
4170
|
this.denoisingManager.setPostProcessRefreshCallback( () => this.requestPostProcessRefresh() );
|
|
3797
|
-
this.denoisingManager.setDisplayRefreshCallback( () => this.
|
|
4171
|
+
this.denoisingManager.setDisplayRefreshCallback( () => this._presentDisplay() );
|
|
3798
4172
|
this.denoisingManager.setSettings( this.settings );
|
|
3799
4173
|
|
|
3800
4174
|
// Expose environment manager (lives on pathTracer stage)
|
|
@@ -3808,7 +4182,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3808
4182
|
_wireEvents() {
|
|
3809
4183
|
|
|
3810
4184
|
// Forward manager events → app events
|
|
3811
|
-
this._addTrackedListener( this.cameraManager,
|
|
4185
|
+
this._addTrackedListener( this.cameraManager, EngineEvents.CAMERA_SWITCHED, ( e ) => this.dispatchEvent( e ) );
|
|
3812
4186
|
this._addTrackedListener( this.cameraManager, EngineEvents.AUTO_FOCUS_UPDATED, ( e ) => this.dispatchEvent( e ) );
|
|
3813
4187
|
this._addTrackedListener( this.cameraManager, EngineEvents.ORTHO_HEIGHT_UPDATED, ( e ) => this.dispatchEvent( e ) );
|
|
3814
4188
|
this._addTrackedListener( this.timeline, EngineEvents.TIMELINE_CHANGED, ( e ) => this.dispatchEvent( e ) );
|
|
@@ -3816,7 +4190,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3816
4190
|
this._forwardEvents( this.denoisingManager, [
|
|
3817
4191
|
EngineEvents.DENOISING_START, EngineEvents.DENOISING_END,
|
|
3818
4192
|
EngineEvents.UPSCALING_START, EngineEvents.UPSCALING_PROGRESS, EngineEvents.UPSCALING_END,
|
|
3819
|
-
|
|
4193
|
+
EngineEvents.RESOLUTION_CHANGED,
|
|
3820
4194
|
] );
|
|
3821
4195
|
|
|
3822
4196
|
this._setupAutoExposureListener();
|
|
@@ -3970,7 +4344,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3970
4344
|
|
|
3971
4345
|
_createStages() {
|
|
3972
4346
|
|
|
3973
|
-
this.stages.pathTracer = new PathTracer( this.renderer, this.scene, this.cameraManager.camera );
|
|
4347
|
+
this.stages.pathTracer = new PathTracer( this.renderer, this.scene, this.cameraManager.camera, { hostMemoryGB: this._hostMemoryGB } );
|
|
3974
4348
|
this.stages.normalDepth = new NormalDepth( this.renderer, {
|
|
3975
4349
|
pathTracer: this.stages.pathTracer
|
|
3976
4350
|
} );
|
|
@@ -4011,6 +4385,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
4011
4385
|
pipeline: this.pipeline,
|
|
4012
4386
|
getExposure: () => this.settings.get( 'exposure' ) ?? 1.0,
|
|
4013
4387
|
getSaturation: () => this.settings.get( 'saturation' ) ?? 1.0,
|
|
4388
|
+
issues: this._issues,
|
|
4014
4389
|
} );
|
|
4015
4390
|
|
|
4016
4391
|
this.denoisingManager.setupDenoiser();
|