rayzee 7.18.0 → 7.19.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 +1 -1
- package/dist/rayzee.es.js +2665 -2559
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +49 -49
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/PathTracerApp.js +74 -61
- package/src/Processor/utils.js +30 -0
- package/src/SceneHelpers.js +11 -1
- package/src/Stages/PathTracer.js +5 -2
- package/src/managers/OverlayManager.js +109 -48
- package/src/managers/helpers/OutlineHelper.js +42 -40
- package/src/managers/helpers/TransformGizmoHelper.js +61 -0
- package/src/managers/helpers/ViewOverlayRenderer.js +140 -0
- package/src/managers/helpers/overlaySurface.js +53 -0
package/package.json
CHANGED
package/src/PathTracerApp.js
CHANGED
|
@@ -17,7 +17,7 @@ import { Compositor } from './Stages/Compositor.js';
|
|
|
17
17
|
import { RenderPipeline } from './Pipeline/RenderPipeline.js';
|
|
18
18
|
import { CompletionTracker } from './Pipeline/CompletionTracker.js';
|
|
19
19
|
import { ENGINE_DEFAULTS as DEFAULT_STATE, PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, MAX_STORAGE_TEXTURE_SIZE, MAX_RESERVABLE_RENDER_SIZE, setReservedRenderSize } from './EngineDefaults.js';
|
|
20
|
-
import { updateStats, updateLoading, resetLoading, setStatusCallback, getDisplaySamples, disposeObjectFromMemory } from './Processor/utils.js';
|
|
20
|
+
import { updateStats, updateLoading, resetLoading, setStatusCallback, getDisplaySamples, disposeObjectFromMemory, disposeRenderer } from './Processor/utils.js';
|
|
21
21
|
import { BuildTimer } from './Processor/BuildTimer.js';
|
|
22
22
|
import { createLogger, fmt } from './utils/Logger.js';
|
|
23
23
|
import { InteractionManager } from './managers/InteractionManager.js';
|
|
@@ -35,6 +35,7 @@ import { DenoisingManager } from './managers/DenoisingManager.js';
|
|
|
35
35
|
import { OverlayManager } from './managers/OverlayManager.js';
|
|
36
36
|
import { AnimationManager } from './managers/AnimationManager.js';
|
|
37
37
|
import { TransformManager } from './managers/TransformManager.js';
|
|
38
|
+
import { TransformGizmoHelper } from './managers/helpers/TransformGizmoHelper.js';
|
|
38
39
|
|
|
39
40
|
// One app per canvas — auto-dispose a prior owner if the caller double-
|
|
40
41
|
// instantiates (StrictMode, HMR, etc.) so its rAF loop can't burn CPU.
|
|
@@ -216,7 +217,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
216
217
|
this._initScenes();
|
|
217
218
|
this._initAssetPipeline();
|
|
218
219
|
this._initPipeline();
|
|
219
|
-
this._initManagers();
|
|
220
|
+
await this._initManagers();
|
|
220
221
|
this._wireEvents();
|
|
221
222
|
|
|
222
223
|
// Seed path tracer with minimal empty scene data
|
|
@@ -511,35 +512,19 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
511
512
|
this.scene?.clear();
|
|
512
513
|
this.scene = null;
|
|
513
514
|
|
|
514
|
-
// Three.js 0.184
|
|
515
|
+
// Three.js 0.184 leak (confirmed via heap-snapshot retainer analysis): the
|
|
516
|
+
// Textures manager (one per renderer) registers a per-texture 'dispose'
|
|
517
|
+
// listener that closes over `this = Textures` — which transitively captures
|
|
518
|
+
// backend → renderer. These listeners are removed only when the texture
|
|
519
|
+
// itself is destroyed. For module-level singletons like EmptyTexture (new
|
|
520
|
+
// Texture in TextureNode.js) and its CubeTexture counterpart, the texture is
|
|
521
|
+
// never destroyed, so every renderer ever created leaks through the
|
|
522
|
+
// singleton's listener array.
|
|
515
523
|
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
// See three/src/renderers/common/Renderer.js:292 (attach) and
|
|
521
|
-
// :2503 (dispose — missing removal).
|
|
522
|
-
//
|
|
523
|
-
// 2) Textures manager (one per renderer) registers a per-texture 'dispose'
|
|
524
|
-
// listener that closes over `this = Textures` — which transitively
|
|
525
|
-
// captures backend → renderer. These listeners are removed only when
|
|
526
|
-
// the texture itself is destroyed. For module-level singletons like
|
|
527
|
-
// EmptyTexture (new Texture in TextureNode.js) and its CubeTexture
|
|
528
|
-
// counterpart, the texture is never destroyed, so every renderer ever
|
|
529
|
-
// created leaks through the singleton's listener array.
|
|
530
|
-
//
|
|
531
|
-
// Both workarounds are safe when only a single PathTracerApp is active at a
|
|
532
|
-
// time. If you run multiple in parallel, reset listeners only on the renderer
|
|
533
|
-
// being disposed (not the shared singletons).
|
|
534
|
-
if ( this.renderer?._canvasTarget && this.renderer._onCanvasTargetResize ) {
|
|
535
|
-
|
|
536
|
-
this.renderer._canvasTarget.removeEventListener(
|
|
537
|
-
'resize',
|
|
538
|
-
this.renderer._onCanvasTargetResize
|
|
539
|
-
);
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
|
|
524
|
+
// Safe when only a single PathTracerApp is active at a time. If you run
|
|
525
|
+
// multiple in parallel, reset listeners only on the renderer being disposed
|
|
526
|
+
// (not the shared singletons). The sibling _canvasTarget leak is handled by
|
|
527
|
+
// disposeRenderer().
|
|
543
528
|
try {
|
|
544
529
|
|
|
545
530
|
const emptyTex = _tslTexture().value;
|
|
@@ -553,8 +538,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
553
538
|
|
|
554
539
|
}
|
|
555
540
|
|
|
556
|
-
this.renderer
|
|
557
|
-
if ( this.renderer ) this.renderer._canvasTarget = null;
|
|
541
|
+
disposeRenderer( this.renderer );
|
|
558
542
|
this.renderer = null;
|
|
559
543
|
|
|
560
544
|
this.stages = {};
|
|
@@ -1231,10 +1215,12 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1231
1215
|
*
|
|
1232
1216
|
* MUST be called before the pipeline/stages are constructed (they pre-allocate at this value and cannot
|
|
1233
1217
|
* resize). Calling it after init only takes effect on the next full pipeline (re)build.
|
|
1234
|
-
* @param {number}
|
|
1218
|
+
* @param {number} requestedPx desired reserved size (longest edge)
|
|
1219
|
+
* @param {Object} [opts]
|
|
1220
|
+
* @param {boolean} [opts.allowLower=false] permit lowering, paying a rebuild, to reclaim VRAM
|
|
1235
1221
|
* @returns {number} the applied reserved size
|
|
1236
1222
|
*/
|
|
1237
|
-
setReservedRenderResolution( requestedPx ) {
|
|
1223
|
+
setReservedRenderResolution( requestedPx, { allowLower = false } = {} ) {
|
|
1238
1224
|
|
|
1239
1225
|
const limits = this.renderer?.backend?.device?.limits;
|
|
1240
1226
|
const maxBinding = limits?.maxStorageBufferBindingSize || ( 128 * 1024 * 1024 );
|
|
@@ -1243,7 +1229,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1243
1229
|
const deviceSafeMax = ( deviceMemGB >= 8 && maxBinding >= 1024 * 1024 * 1024 )
|
|
1244
1230
|
? MAX_RESERVABLE_RENDER_SIZE : 2048;
|
|
1245
1231
|
const prev = MAX_STORAGE_TEXTURE_SIZE;
|
|
1246
|
-
|
|
1232
|
+
|
|
1233
|
+
// Monotonic up: UI-driven callers request whatever the current view needs, so honouring decreases made
|
|
1234
|
+
// the reserve oscillate on every preview↔render switch and paid a full kernel rebuild each time.
|
|
1235
|
+
const target = allowLower ? requestedPx : Math.max( requestedPx, prev );
|
|
1236
|
+
const applied = setReservedRenderSize( Math.min( target, deviceSafeMax ) );
|
|
1247
1237
|
|
|
1248
1238
|
// If the pipeline is already built and the reserved size changed, re-init the reserved GPU storage in
|
|
1249
1239
|
// place: each stage recreates its pre-allocated StorageTextures at the new size + rebuilds its compute
|
|
@@ -1256,6 +1246,17 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1256
1246
|
this.pauseRendering = true;
|
|
1257
1247
|
try {
|
|
1258
1248
|
|
|
1249
|
+
// Lowering below the live backing store makes copyToReadTargets read past the end of the
|
|
1250
|
+
// (now smaller) write textures — a GPUValidationError. Shrink the backing store first.
|
|
1251
|
+
// renderer.setSize, not setCanvasSize/onResize: those also rewrite camera.aspect.
|
|
1252
|
+
const backing = this.renderer.domElement;
|
|
1253
|
+
if ( backing.width > applied || backing.height > applied ) {
|
|
1254
|
+
|
|
1255
|
+
this.renderer.setSize(
|
|
1256
|
+
Math.min( backing.width, applied ), Math.min( backing.height, applied ), false );
|
|
1257
|
+
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1259
1260
|
for ( const stage of Object.values( this.stages ) ) stage?.reallocateReservedStorage?.();
|
|
1260
1261
|
|
|
1261
1262
|
} finally {
|
|
@@ -1273,6 +1274,16 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1273
1274
|
|
|
1274
1275
|
}
|
|
1275
1276
|
|
|
1277
|
+
/**
|
|
1278
|
+
* The current reserved (pre-allocated) square render size in px.
|
|
1279
|
+
* @returns {number}
|
|
1280
|
+
*/
|
|
1281
|
+
getReservedRenderResolution() {
|
|
1282
|
+
|
|
1283
|
+
return MAX_STORAGE_TEXTURE_SIZE;
|
|
1284
|
+
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1276
1287
|
/**
|
|
1277
1288
|
* Guard against render resolutions the compute pipeline can't support.
|
|
1278
1289
|
* Per-resolution StorageTextures are pre-allocated at MAX_STORAGE_TEXTURE_SIZE
|
|
@@ -1304,13 +1315,6 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1304
1315
|
this.cameraManager.camera.aspect = width / height;
|
|
1305
1316
|
this.cameraManager.camera.updateProjectionMatrix();
|
|
1306
1317
|
|
|
1307
|
-
// Overlay helpers always render at display resolution
|
|
1308
|
-
const dpr = window.devicePixelRatio || 1;
|
|
1309
|
-
this.overlayManager?.setSize(
|
|
1310
|
-
Math.round( width * dpr ),
|
|
1311
|
-
Math.round( height * dpr )
|
|
1312
|
-
);
|
|
1313
|
-
|
|
1314
1318
|
const lastW = this.denoisingManager?._lastRenderWidth ?? 0;
|
|
1315
1319
|
const lastH = this.denoisingManager?._lastRenderHeight ?? 0;
|
|
1316
1320
|
if ( width === lastW && height === lastH ) return;
|
|
@@ -1367,28 +1371,27 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1367
1371
|
|
|
1368
1372
|
this.cameraManager.controls.enabled = ! isProduction;
|
|
1369
1373
|
|
|
1370
|
-
//
|
|
1374
|
+
// Anything with a SETTING_ROUTES entry must go through settings, not setUniform: set() early-returns on
|
|
1375
|
+
// `prev === value`, so a uniform written behind the map leaves it stale and the next set() silently no-ops.
|
|
1371
1376
|
this.settings.setMany( {
|
|
1372
1377
|
maxSamples: config.maxSamples,
|
|
1373
1378
|
maxBounces: config.bounces,
|
|
1374
1379
|
transmissiveBounces: config.transmissiveBounces,
|
|
1375
1380
|
maxSubsurfaceSteps: config.maxSubsurfaceSteps,
|
|
1381
|
+
enableAlphaShadows: config.enableAlphaShadows ?? false,
|
|
1382
|
+
// Tier-1 convergence early-stop
|
|
1383
|
+
useAdaptiveSampling: config.useAdaptiveSampling ?? false,
|
|
1384
|
+
noiseThreshold: config.noiseThreshold ?? DEFAULT_STATE.noiseThreshold,
|
|
1385
|
+
adaptiveStopFraction: config.adaptiveStopFraction ?? DEFAULT_STATE.adaptiveStopFraction,
|
|
1386
|
+
adaptiveMinSamples: config.adaptiveMinSamples ?? DEFAULT_STATE.adaptiveMinSamples,
|
|
1387
|
+
// Tier-2 per-pixel freeze
|
|
1388
|
+
usePixelFreeze: config.usePixelFreeze ?? false,
|
|
1389
|
+
pixelFreezeThreshold: config.pixelFreezeThreshold ?? DEFAULT_STATE.pixelFreezeThreshold,
|
|
1390
|
+
pixelFreezeStability: config.pixelFreezeStability ?? DEFAULT_STATE.pixelFreezeStability,
|
|
1376
1391
|
}, { silent: true } );
|
|
1377
1392
|
|
|
1393
|
+
// renderMode has no SETTING_ROUTES entry
|
|
1378
1394
|
this.stages.pathTracer?.setUniform( 'renderMode', parseInt( config.renderMode ) );
|
|
1379
|
-
this.stages.pathTracer?.setUniform( 'enableAlphaShadows', config.enableAlphaShadows ?? false );
|
|
1380
|
-
|
|
1381
|
-
// Tier-1 convergence early-stop (live uniforms, no kernel rebuild). Threshold/min-samples fall back to
|
|
1382
|
-
// engine defaults when a config doesn't override them.
|
|
1383
|
-
this.stages.pathTracer?.setUniform( 'useAdaptiveSampling', config.useAdaptiveSampling ?? false );
|
|
1384
|
-
this.stages.pathTracer?.setUniform( 'noiseThreshold', config.noiseThreshold ?? DEFAULT_STATE.noiseThreshold );
|
|
1385
|
-
this.stages.pathTracer?.setUniform( 'adaptiveStopFraction', config.adaptiveStopFraction ?? DEFAULT_STATE.adaptiveStopFraction );
|
|
1386
|
-
this.stages.pathTracer?.setUniform( 'adaptiveMinSamples', config.adaptiveMinSamples ?? DEFAULT_STATE.adaptiveMinSamples );
|
|
1387
|
-
|
|
1388
|
-
// Tier-2 per-pixel freeze (per-mode; falls back off when a config doesn't set it).
|
|
1389
|
-
this.stages.pathTracer?.setUniform( 'usePixelFreeze', config.usePixelFreeze ?? false );
|
|
1390
|
-
this.stages.pathTracer?.setUniform( 'pixelFreezeThreshold', config.pixelFreezeThreshold ?? DEFAULT_STATE.pixelFreezeThreshold );
|
|
1391
|
-
this.stages.pathTracer?.setUniform( 'pixelFreezeStability', config.pixelFreezeStability ?? DEFAULT_STATE.pixelFreezeStability );
|
|
1392
1395
|
|
|
1393
1396
|
this.stages.pathTracer?.updateCompletionThreshold?.();
|
|
1394
1397
|
|
|
@@ -1905,7 +1908,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1905
1908
|
|
|
1906
1909
|
}
|
|
1907
1910
|
|
|
1908
|
-
_initManagers() {
|
|
1911
|
+
async _initManagers() {
|
|
1909
1912
|
|
|
1910
1913
|
this.interactionManager = new InteractionManager( {
|
|
1911
1914
|
scene: this.meshScene,
|
|
@@ -1929,7 +1932,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1929
1932
|
onReset: () => this.reset(),
|
|
1930
1933
|
} );
|
|
1931
1934
|
this._setupDenoisingManager();
|
|
1932
|
-
this._setupOverlayManager();
|
|
1935
|
+
await this._setupOverlayManager();
|
|
1933
1936
|
|
|
1934
1937
|
this.transformManager = new TransformManager( {
|
|
1935
1938
|
camera: this.cameraManager.camera,
|
|
@@ -1938,6 +1941,10 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1938
1941
|
app: this,
|
|
1939
1942
|
} );
|
|
1940
1943
|
|
|
1944
|
+
// The gizmo is part of the scene overlay layer, so it draws on the same
|
|
1945
|
+
// view-resolution surface as the light helpers and the outline.
|
|
1946
|
+
this.overlayManager.register( 'transform', new TransformGizmoHelper( this.transformManager ) );
|
|
1947
|
+
|
|
1941
1948
|
// Wire cross-manager dependencies
|
|
1942
1949
|
this.interactionManager.setDependencies( {
|
|
1943
1950
|
overlayManager: this.overlayManager,
|
|
@@ -2209,13 +2216,12 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2209
2216
|
|
|
2210
2217
|
this.scene.updateMatrixWorld();
|
|
2211
2218
|
this.overlayManager?.render();
|
|
2212
|
-
this.transformManager?.render( this.renderer );
|
|
2213
2219
|
|
|
2214
2220
|
}
|
|
2215
2221
|
|
|
2216
|
-
_setupOverlayManager() {
|
|
2222
|
+
async _setupOverlayManager() {
|
|
2217
2223
|
|
|
2218
|
-
this.overlayManager = new OverlayManager( this.
|
|
2224
|
+
this.overlayManager = new OverlayManager( this.cameraManager.camera );
|
|
2219
2225
|
this.overlayManager.setupDefaultHelpers( {
|
|
2220
2226
|
helperScene: this._sceneHelpers,
|
|
2221
2227
|
meshScene: this.meshScene,
|
|
@@ -2226,6 +2232,13 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2226
2232
|
renderHeight: this.denoisingManager?._lastRenderHeight || this.canvas.clientHeight || 1,
|
|
2227
2233
|
} );
|
|
2228
2234
|
|
|
2235
|
+
// Helpers draw at the size the canvas is displayed at, not the size it is
|
|
2236
|
+
// rendered at. Shares the main device, so this is a swapchain, not a context.
|
|
2237
|
+
await this.overlayManager.initViewRenderer( {
|
|
2238
|
+
device: this.renderer.backend?.device,
|
|
2239
|
+
sizeSource: this.canvas,
|
|
2240
|
+
} );
|
|
2241
|
+
|
|
2229
2242
|
this._container = this._container || this.canvas.parentNode || null;
|
|
2230
2243
|
this.overlayManager.mount( this._container );
|
|
2231
2244
|
|
package/src/Processor/utils.js
CHANGED
|
@@ -102,6 +102,36 @@ export function distroyBuffers( { material, geometry, children } ) {
|
|
|
102
102
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Disposes a renderer without leaking it.
|
|
107
|
+
*
|
|
108
|
+
* Three.js 0.184–0.185: `Renderer.dispose()` does not remove the 'resize' listener
|
|
109
|
+
* it installs on `_canvasTarget`. The bound handler closes over the renderer,
|
|
110
|
+
* pinning the entire WebGPU graph (Backend, Nodes, Bindings, Pipelines, GPUDevice,
|
|
111
|
+
* every TSL node) alive indefinitely — confirmed via heap-snapshot retainer
|
|
112
|
+
* analysis. See three/src/renderers/common/Renderer.js:292 (attach) and :2503
|
|
113
|
+
* (dispose — missing removal).
|
|
114
|
+
*
|
|
115
|
+
* Renderers constructed with an external `device` do not destroy it here; three
|
|
116
|
+
* only destroys a device it created itself.
|
|
117
|
+
*
|
|
118
|
+
* @param {import('three/webgpu').WebGPURenderer} renderer
|
|
119
|
+
*/
|
|
120
|
+
export function disposeRenderer( renderer ) {
|
|
121
|
+
|
|
122
|
+
if ( ! renderer ) return;
|
|
123
|
+
|
|
124
|
+
if ( renderer._canvasTarget && renderer._onCanvasTargetResize ) {
|
|
125
|
+
|
|
126
|
+
renderer._canvasTarget.removeEventListener( 'resize', renderer._onCanvasTargetResize );
|
|
127
|
+
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
renderer.dispose();
|
|
131
|
+
renderer._canvasTarget = null;
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
105
135
|
export function disposeObjectFromMemory( object, exeptions = [] ) {
|
|
106
136
|
|
|
107
137
|
if ( ! object ) return;
|
package/src/SceneHelpers.js
CHANGED
|
@@ -65,6 +65,16 @@ export class SceneHelpers {
|
|
|
65
65
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Whether a render() would actually put anything on screen. `visible` alone
|
|
70
|
+
* is the enable flag and can be true with no lights in the scene.
|
|
71
|
+
*/
|
|
72
|
+
get isDrawing() {
|
|
73
|
+
|
|
74
|
+
return this.visible && this._helpers.size > 0;
|
|
75
|
+
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
/**
|
|
69
79
|
* Syncs all helpers with their source objects.
|
|
70
80
|
*/
|
|
@@ -132,7 +142,7 @@ export class SceneHelpers {
|
|
|
132
142
|
*/
|
|
133
143
|
render( renderer, camera ) {
|
|
134
144
|
|
|
135
|
-
if ( ! this.
|
|
145
|
+
if ( ! this.isDrawing ) return;
|
|
136
146
|
|
|
137
147
|
const prevAutoClear = renderer.autoClear;
|
|
138
148
|
renderer.autoClear = false;
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -769,8 +769,11 @@ export class PathTracer extends PathTracerStage {
|
|
|
769
769
|
|
|
770
770
|
if ( ! this._packedBuffers || ! ( this.materialData?.materialCount > 0 ) ) return;
|
|
771
771
|
|
|
772
|
-
|
|
773
|
-
|
|
772
|
+
// Clamp into the (possibly just-lowered) reserve: create() sizes the write StorageTextures at
|
|
773
|
+
// MAX_STORAGE_TEXTURE_SIZE but the readTarget at what's passed here, and the tracked render size still
|
|
774
|
+
// holds the pre-change value — unclamped, copyToReadTargets reads past the write textures.
|
|
775
|
+
const w = Math.min( this.storageTextures.renderWidth || 1, MAX_STORAGE_TEXTURE_SIZE );
|
|
776
|
+
const h = Math.min( this.storageTextures.renderHeight || 1, MAX_STORAGE_TEXTURE_SIZE );
|
|
774
777
|
|
|
775
778
|
// Recreate the MRT pool at the new reserved size (create() reallocates the write StorageTextures at the
|
|
776
779
|
// live MAX_STORAGE_TEXTURE_SIZE; the read RenderTarget follows the current render size).
|
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
import { TileHelper } from './helpers/TileHelper.js';
|
|
2
2
|
import { OutlineHelper } from './helpers/OutlineHelper.js';
|
|
3
|
+
import { ViewOverlayRenderer } from './helpers/ViewOverlayRenderer.js';
|
|
4
|
+
import { createOverlayCanvas, viewPixelSize } from './helpers/overlaySurface.js';
|
|
5
|
+
import { createLogger } from '../utils/Logger.js';
|
|
6
|
+
|
|
7
|
+
const log = createLogger( 'overlay' );
|
|
8
|
+
|
|
9
|
+
/** Whether a helper wants to draw into the given layer this frame. */
|
|
10
|
+
const drawsOn = ( helper, layer ) => helper.visible && helper.layer === layer && helper.render;
|
|
3
11
|
|
|
4
12
|
/**
|
|
5
13
|
* OverlayManager — Unified overlay system for visual helpers.
|
|
6
14
|
*
|
|
7
|
-
* Two rendering layers
|
|
8
|
-
*
|
|
9
|
-
*
|
|
15
|
+
* Two rendering layers, both sized to the **view** (what the user sees on
|
|
16
|
+
* screen) rather than to the path tracer's render resolution:
|
|
17
|
+
* 1. **Scene layer** — Three.js content (light gizmos, transform gizmo,
|
|
18
|
+
* selection outline) drawn by {@link ViewOverlayRenderer} into its own
|
|
19
|
+
* transparent canvas stacked over the main one.
|
|
10
20
|
* 2. **HUDCanvas** — A 2D `<canvas>` element overlaid via CSS for screen-space
|
|
11
|
-
* elements (
|
|
12
|
-
*
|
|
21
|
+
* elements (denoise/upscale tile progress).
|
|
22
|
+
*
|
|
23
|
+
* Neither layer touches the main canvas, so helpers can never be captured in a
|
|
24
|
+
* saved image.
|
|
13
25
|
*
|
|
14
26
|
* Helpers are registered by name and implement a simple interface:
|
|
15
27
|
* { update?(), render?(ctx, w, h), show(), hide(), dispose(), visible, layer }
|
|
16
28
|
*
|
|
17
29
|
* @example
|
|
18
|
-
* const overlay = new OverlayManager(
|
|
30
|
+
* const overlay = new OverlayManager( camera );
|
|
31
|
+
* await overlay.initViewRenderer( { device, sizeSource: canvas } );
|
|
19
32
|
* overlay.register( 'outline', new OutlineHelper() );
|
|
20
33
|
* overlay.show( 'outline' );
|
|
21
34
|
* // in animate():
|
|
@@ -24,25 +37,51 @@ import { OutlineHelper } from './helpers/OutlineHelper.js';
|
|
|
24
37
|
export class OverlayManager {
|
|
25
38
|
|
|
26
39
|
/**
|
|
27
|
-
* @param {import('three/webgpu').WebGPURenderer} renderer
|
|
28
40
|
* @param {import('three').PerspectiveCamera} camera
|
|
29
41
|
*/
|
|
30
|
-
constructor(
|
|
42
|
+
constructor( camera ) {
|
|
31
43
|
|
|
32
|
-
this.renderer = renderer;
|
|
33
44
|
this.camera = camera;
|
|
34
45
|
|
|
35
46
|
/** @type {Map<string, Object>} */
|
|
36
47
|
this._helpers = new Map();
|
|
37
48
|
|
|
38
49
|
// ── HUD Canvas (2D overlay) ──
|
|
39
|
-
this._hudCanvas =
|
|
40
|
-
this._hudCanvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;';
|
|
50
|
+
this._hudCanvas = createOverlayCanvas();
|
|
41
51
|
this._hudCtx = this._hudCanvas.getContext( '2d' );
|
|
42
52
|
|
|
43
53
|
// ── HelperScene reference (set via setHelperScene) ──
|
|
44
54
|
this._helperScene = null;
|
|
45
55
|
|
|
56
|
+
/** @type {ViewOverlayRenderer|null} */
|
|
57
|
+
this._viewOverlay = null;
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Creates the view-resolution surface the scene layer draws into. On failure
|
|
63
|
+
* the scene layer stays off rather than degrading onto the main backbuffer —
|
|
64
|
+
* drawing there would bake helpers into saved images.
|
|
65
|
+
*
|
|
66
|
+
* @param {Object} config
|
|
67
|
+
* @param {GPUDevice} config.device - Shared with the main renderer
|
|
68
|
+
* @param {HTMLElement} config.sizeSource - Element whose on-screen box defines the size
|
|
69
|
+
*/
|
|
70
|
+
async initViewRenderer( { device, sizeSource } ) {
|
|
71
|
+
|
|
72
|
+
if ( ! device || ! sizeSource ) return;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
|
|
76
|
+
this._viewOverlay = await new ViewOverlayRenderer().init( { device, sizeSource } );
|
|
77
|
+
|
|
78
|
+
} catch ( err ) {
|
|
79
|
+
|
|
80
|
+
log.error( 'overlay surface unavailable — light gizmos, transform gizmo and outlines are disabled', err );
|
|
81
|
+
this._viewOverlay = null;
|
|
82
|
+
|
|
83
|
+
}
|
|
84
|
+
|
|
46
85
|
}
|
|
47
86
|
|
|
48
87
|
/**
|
|
@@ -67,14 +106,19 @@ export class OverlayManager {
|
|
|
67
106
|
}
|
|
68
107
|
|
|
69
108
|
/**
|
|
70
|
-
* Mounts the
|
|
71
|
-
* multiple times; re-mounts only when the parent differs.
|
|
109
|
+
* Mounts the overlay canvases into the given container. Idempotent — safe to
|
|
110
|
+
* call multiple times; re-mounts only when the parent differs. The 3D surface
|
|
111
|
+
* goes in first so the HUD stacks above it.
|
|
72
112
|
* @param {HTMLElement} container
|
|
73
113
|
*/
|
|
74
114
|
mount( container ) {
|
|
75
115
|
|
|
76
|
-
if ( ! container
|
|
77
|
-
|
|
116
|
+
if ( ! container ) return;
|
|
117
|
+
|
|
118
|
+
const viewCanvas = this._viewOverlay?.canvas;
|
|
119
|
+
if ( viewCanvas && viewCanvas.parentElement !== container ) container.appendChild( viewCanvas );
|
|
120
|
+
|
|
121
|
+
if ( this._hudCanvas.parentElement !== container ) container.appendChild( this._hudCanvas );
|
|
78
122
|
|
|
79
123
|
}
|
|
80
124
|
|
|
@@ -116,7 +160,7 @@ export class OverlayManager {
|
|
|
116
160
|
this._wireDenoiserTileEvents( tileHelper, denoisingManager );
|
|
117
161
|
|
|
118
162
|
// ── Outline helper ──
|
|
119
|
-
const outlineHelper = new OutlineHelper(
|
|
163
|
+
const outlineHelper = new OutlineHelper( meshScene, this.camera );
|
|
120
164
|
this.register( 'outline', outlineHelper );
|
|
121
165
|
|
|
122
166
|
}
|
|
@@ -170,7 +214,7 @@ export class OverlayManager {
|
|
|
170
214
|
|
|
171
215
|
if ( this._helpers.has( name ) ) {
|
|
172
216
|
|
|
173
|
-
|
|
217
|
+
log.warn( `helper "${name}" already registered — replacing.` );
|
|
174
218
|
this._helpers.get( name ).dispose?.();
|
|
175
219
|
|
|
176
220
|
}
|
|
@@ -259,44 +303,56 @@ export class OverlayManager {
|
|
|
259
303
|
*/
|
|
260
304
|
render() {
|
|
261
305
|
|
|
262
|
-
|
|
263
|
-
if ( this._helperScene ) {
|
|
264
|
-
|
|
265
|
-
this._helperScene.render( this.renderer, this.camera );
|
|
306
|
+
const overlay = this._viewOverlay;
|
|
266
307
|
|
|
267
|
-
|
|
308
|
+
if ( overlay ) {
|
|
268
309
|
|
|
269
|
-
|
|
270
|
-
for ( const helper of this._helpers.values() ) {
|
|
310
|
+
const hasSceneOverlay = this._hasVisibleSceneOverlay();
|
|
271
311
|
|
|
272
|
-
if (
|
|
312
|
+
if ( hasSceneOverlay || overlay.isVisible ) {
|
|
273
313
|
|
|
274
|
-
|
|
314
|
+
overlay.syncSize();
|
|
315
|
+
// Doubles as the wipe of last frame's content when parking.
|
|
316
|
+
overlay.begin();
|
|
317
|
+
if ( hasSceneOverlay ) this._renderSceneLayer( overlay.renderer );
|
|
318
|
+
overlay.setVisible( hasSceneOverlay );
|
|
275
319
|
|
|
276
320
|
}
|
|
277
321
|
|
|
278
322
|
}
|
|
279
323
|
|
|
280
|
-
// 3. Draw HUD canvas (2D helpers)
|
|
281
324
|
this.refreshHUD();
|
|
282
325
|
|
|
283
326
|
}
|
|
284
327
|
|
|
285
|
-
/**
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
*/
|
|
290
|
-
setSize( width, height ) {
|
|
328
|
+
/** Draws the helper scene + every visible scene-layer helper. */
|
|
329
|
+
_renderSceneLayer( renderer ) {
|
|
330
|
+
|
|
331
|
+
this._helperScene?.render( renderer, this.camera );
|
|
291
332
|
|
|
292
333
|
for ( const helper of this._helpers.values() ) {
|
|
293
334
|
|
|
294
|
-
helper.
|
|
335
|
+
if ( drawsOn( helper, 'scene' ) ) helper.render( renderer, this.camera );
|
|
295
336
|
|
|
296
337
|
}
|
|
297
338
|
|
|
298
339
|
}
|
|
299
340
|
|
|
341
|
+
/** Whether anything at all wants to draw into the scene layer this frame. */
|
|
342
|
+
_hasVisibleSceneOverlay() {
|
|
343
|
+
|
|
344
|
+
if ( this._helperScene?.isDrawing ) return true;
|
|
345
|
+
|
|
346
|
+
for ( const helper of this._helpers.values() ) {
|
|
347
|
+
|
|
348
|
+
if ( drawsOn( helper, 'scene' ) ) return true;
|
|
349
|
+
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return false;
|
|
353
|
+
|
|
354
|
+
}
|
|
355
|
+
|
|
300
356
|
/**
|
|
301
357
|
* Redraws the HUD canvas. Safe to call outside the animation loop
|
|
302
358
|
* (e.g. during async OIDN tile progress).
|
|
@@ -310,7 +366,7 @@ export class OverlayManager {
|
|
|
310
366
|
let hasVisibleHUD = false;
|
|
311
367
|
for ( const helper of this._helpers.values() ) {
|
|
312
368
|
|
|
313
|
-
if (
|
|
369
|
+
if ( drawsOn( helper, 'hud' ) ) {
|
|
314
370
|
|
|
315
371
|
hasVisibleHUD = true;
|
|
316
372
|
break;
|
|
@@ -326,30 +382,32 @@ export class OverlayManager {
|
|
|
326
382
|
|
|
327
383
|
}
|
|
328
384
|
|
|
329
|
-
|
|
385
|
+
// Un-park BEFORE measuring: a display:none canvas reports clientWidth 0, which returns early below and
|
|
386
|
+
// skips the un-park at the end — so once parked the HUD could never come back.
|
|
387
|
+
if ( canvas.style.display !== '' ) canvas.style.display = '';
|
|
388
|
+
|
|
389
|
+
// Helpers draw in CSS-pixel coordinates of the unscaled layout box; the
|
|
390
|
+
// backing store is sized off the on-screen box, so nothing is drawn below
|
|
391
|
+
// view resolution.
|
|
330
392
|
const displayW = canvas.clientWidth;
|
|
331
393
|
const displayH = canvas.clientHeight;
|
|
332
|
-
const
|
|
333
|
-
|
|
394
|
+
const size = displayW > 0 && displayH > 0 ? viewPixelSize( canvas ) : null;
|
|
395
|
+
if ( ! size ) return;
|
|
334
396
|
|
|
335
|
-
if ( canvas.width !==
|
|
397
|
+
if ( canvas.width !== size.width || canvas.height !== size.height ) {
|
|
336
398
|
|
|
337
|
-
canvas.width =
|
|
338
|
-
canvas.height =
|
|
399
|
+
canvas.width = size.width;
|
|
400
|
+
canvas.height = size.height;
|
|
339
401
|
|
|
340
402
|
}
|
|
341
403
|
|
|
342
|
-
ctx.clearRect( 0, 0,
|
|
404
|
+
ctx.clearRect( 0, 0, size.width, size.height );
|
|
343
405
|
ctx.save();
|
|
344
|
-
ctx.scale(
|
|
406
|
+
ctx.scale( size.width / displayW, size.height / displayH );
|
|
345
407
|
|
|
346
408
|
for ( const helper of this._helpers.values() ) {
|
|
347
409
|
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
helper.render( ctx, displayW, displayH );
|
|
351
|
-
|
|
352
|
-
}
|
|
410
|
+
if ( drawsOn( helper, 'hud' ) ) helper.render( ctx, displayW, displayH );
|
|
353
411
|
|
|
354
412
|
}
|
|
355
413
|
|
|
@@ -372,6 +430,9 @@ export class OverlayManager {
|
|
|
372
430
|
|
|
373
431
|
this._helpers.clear();
|
|
374
432
|
|
|
433
|
+
this._viewOverlay?.dispose();
|
|
434
|
+
this._viewOverlay = null;
|
|
435
|
+
|
|
375
436
|
if ( this._hudCanvas.parentElement ) {
|
|
376
437
|
|
|
377
438
|
this._hudCanvas.parentElement.removeChild( this._hudCanvas );
|