rayzee 7.18.0 → 7.19.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 +1 -1
- package/dist/rayzee.es.js +2600 -2508
- 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 +31 -44
- package/src/Processor/utils.js +30 -0
- package/src/SceneHelpers.js +11 -1
- package/src/managers/OverlayManager.js +105 -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 = {};
|
|
@@ -1304,13 +1288,6 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1304
1288
|
this.cameraManager.camera.aspect = width / height;
|
|
1305
1289
|
this.cameraManager.camera.updateProjectionMatrix();
|
|
1306
1290
|
|
|
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
1291
|
const lastW = this.denoisingManager?._lastRenderWidth ?? 0;
|
|
1315
1292
|
const lastH = this.denoisingManager?._lastRenderHeight ?? 0;
|
|
1316
1293
|
if ( width === lastW && height === lastH ) return;
|
|
@@ -1905,7 +1882,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1905
1882
|
|
|
1906
1883
|
}
|
|
1907
1884
|
|
|
1908
|
-
_initManagers() {
|
|
1885
|
+
async _initManagers() {
|
|
1909
1886
|
|
|
1910
1887
|
this.interactionManager = new InteractionManager( {
|
|
1911
1888
|
scene: this.meshScene,
|
|
@@ -1929,7 +1906,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1929
1906
|
onReset: () => this.reset(),
|
|
1930
1907
|
} );
|
|
1931
1908
|
this._setupDenoisingManager();
|
|
1932
|
-
this._setupOverlayManager();
|
|
1909
|
+
await this._setupOverlayManager();
|
|
1933
1910
|
|
|
1934
1911
|
this.transformManager = new TransformManager( {
|
|
1935
1912
|
camera: this.cameraManager.camera,
|
|
@@ -1938,6 +1915,10 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1938
1915
|
app: this,
|
|
1939
1916
|
} );
|
|
1940
1917
|
|
|
1918
|
+
// The gizmo is part of the scene overlay layer, so it draws on the same
|
|
1919
|
+
// view-resolution surface as the light helpers and the outline.
|
|
1920
|
+
this.overlayManager.register( 'transform', new TransformGizmoHelper( this.transformManager ) );
|
|
1921
|
+
|
|
1941
1922
|
// Wire cross-manager dependencies
|
|
1942
1923
|
this.interactionManager.setDependencies( {
|
|
1943
1924
|
overlayManager: this.overlayManager,
|
|
@@ -2209,13 +2190,12 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2209
2190
|
|
|
2210
2191
|
this.scene.updateMatrixWorld();
|
|
2211
2192
|
this.overlayManager?.render();
|
|
2212
|
-
this.transformManager?.render( this.renderer );
|
|
2213
2193
|
|
|
2214
2194
|
}
|
|
2215
2195
|
|
|
2216
|
-
_setupOverlayManager() {
|
|
2196
|
+
async _setupOverlayManager() {
|
|
2217
2197
|
|
|
2218
|
-
this.overlayManager = new OverlayManager( this.
|
|
2198
|
+
this.overlayManager = new OverlayManager( this.cameraManager.camera );
|
|
2219
2199
|
this.overlayManager.setupDefaultHelpers( {
|
|
2220
2200
|
helperScene: this._sceneHelpers,
|
|
2221
2201
|
meshScene: this.meshScene,
|
|
@@ -2226,6 +2206,13 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2226
2206
|
renderHeight: this.denoisingManager?._lastRenderHeight || this.canvas.clientHeight || 1,
|
|
2227
2207
|
} );
|
|
2228
2208
|
|
|
2209
|
+
// Helpers draw at the size the canvas is displayed at, not the size it is
|
|
2210
|
+
// rendered at. Shares the main device, so this is a swapchain, not a context.
|
|
2211
|
+
await this.overlayManager.initViewRenderer( {
|
|
2212
|
+
device: this.renderer.backend?.device,
|
|
2213
|
+
sizeSource: this.canvas,
|
|
2214
|
+
} );
|
|
2215
|
+
|
|
2229
2216
|
this._container = this._container || this.canvas.parentNode || null;
|
|
2230
2217
|
this.overlayManager.mount( this._container );
|
|
2231
2218
|
|
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;
|
|
@@ -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,42 +303,54 @@ export class OverlayManager {
|
|
|
259
303
|
*/
|
|
260
304
|
render() {
|
|
261
305
|
|
|
262
|
-
|
|
263
|
-
if ( this._helperScene ) {
|
|
306
|
+
const overlay = this._viewOverlay;
|
|
264
307
|
|
|
265
|
-
|
|
308
|
+
if ( overlay ) {
|
|
266
309
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// 2. Render scene-layer helpers (outline, etc.)
|
|
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
|
-
|
|
328
|
+
/** Draws the helper scene + every visible scene-layer helper. */
|
|
329
|
+
_renderSceneLayer( renderer ) {
|
|
330
|
+
|
|
331
|
+
this._helperScene?.render( renderer, this.camera );
|
|
332
|
+
|
|
333
|
+
for ( const helper of this._helpers.values() ) {
|
|
334
|
+
|
|
335
|
+
if ( drawsOn( helper, 'scene' ) ) helper.render( renderer, this.camera );
|
|
336
|
+
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
}
|
|
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;
|
|
291
345
|
|
|
292
346
|
for ( const helper of this._helpers.values() ) {
|
|
293
347
|
|
|
294
|
-
|
|
348
|
+
if ( drawsOn( helper, 'scene' ) ) return true;
|
|
295
349
|
|
|
296
350
|
}
|
|
297
351
|
|
|
352
|
+
return false;
|
|
353
|
+
|
|
298
354
|
}
|
|
299
355
|
|
|
300
356
|
/**
|
|
@@ -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,28 @@ export class OverlayManager {
|
|
|
326
382
|
|
|
327
383
|
}
|
|
328
384
|
|
|
329
|
-
|
|
385
|
+
// Helpers draw in CSS-pixel coordinates of the unscaled layout box; the
|
|
386
|
+
// backing store is sized off the on-screen box, so nothing is drawn below
|
|
387
|
+
// view resolution.
|
|
330
388
|
const displayW = canvas.clientWidth;
|
|
331
389
|
const displayH = canvas.clientHeight;
|
|
332
|
-
const
|
|
333
|
-
|
|
390
|
+
const size = displayW > 0 && displayH > 0 ? viewPixelSize( canvas ) : null;
|
|
391
|
+
if ( ! size ) return;
|
|
334
392
|
|
|
335
|
-
if ( canvas.width !==
|
|
393
|
+
if ( canvas.width !== size.width || canvas.height !== size.height ) {
|
|
336
394
|
|
|
337
|
-
canvas.width =
|
|
338
|
-
canvas.height =
|
|
395
|
+
canvas.width = size.width;
|
|
396
|
+
canvas.height = size.height;
|
|
339
397
|
|
|
340
398
|
}
|
|
341
399
|
|
|
342
|
-
ctx.clearRect( 0, 0,
|
|
400
|
+
ctx.clearRect( 0, 0, size.width, size.height );
|
|
343
401
|
ctx.save();
|
|
344
|
-
ctx.scale(
|
|
402
|
+
ctx.scale( size.width / displayW, size.height / displayH );
|
|
345
403
|
|
|
346
404
|
for ( const helper of this._helpers.values() ) {
|
|
347
405
|
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
helper.render( ctx, displayW, displayH );
|
|
351
|
-
|
|
352
|
-
}
|
|
406
|
+
if ( drawsOn( helper, 'hud' ) ) helper.render( ctx, displayW, displayH );
|
|
353
407
|
|
|
354
408
|
}
|
|
355
409
|
|
|
@@ -372,6 +426,9 @@ export class OverlayManager {
|
|
|
372
426
|
|
|
373
427
|
this._helpers.clear();
|
|
374
428
|
|
|
429
|
+
this._viewOverlay?.dispose();
|
|
430
|
+
this._viewOverlay = null;
|
|
431
|
+
|
|
375
432
|
if ( this._hudCanvas.parentElement ) {
|
|
376
433
|
|
|
377
434
|
this._hudCanvas.parentElement.removeChild( this._hudCanvas );
|
|
@@ -1,49 +1,43 @@
|
|
|
1
1
|
import { outline } from 'three/addons/tsl/display/OutlineNode.js';
|
|
2
2
|
import { MeshBasicNodeMaterial, QuadMesh } from 'three/webgpu';
|
|
3
|
-
import {
|
|
3
|
+
import { AddEquation, Color, CustomBlending, OneFactor } from 'three';
|
|
4
4
|
import { uniform, vec4 } from 'three/tsl';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* OutlineHelper — Renders selection outlines as a post-pipeline overlay.
|
|
8
8
|
*
|
|
9
9
|
* Uses Three.js OutlineNode internally but renders to a separate fullscreen
|
|
10
|
-
* quad
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* quad, composited on top of the path traced image. Drawn by OverlayManager's
|
|
11
|
+
* scene layer, which runs at **view resolution** — so outlines stay a crisp
|
|
12
|
+
* screen-space width regardless of the path tracer's render resolution.
|
|
13
|
+
*
|
|
14
|
+
* The quad is additive, but writes coverage into alpha (rather than a constant
|
|
15
|
+
* 1.0) so it composites correctly onto the transparent overlay surface instead
|
|
16
|
+
* of turning the whole frame opaque.
|
|
13
17
|
*
|
|
14
18
|
* Layer: 'scene' (rendered by OverlayManager's 3D pass, not HUD canvas).
|
|
15
19
|
*
|
|
16
20
|
* @example
|
|
17
|
-
* const outlineHelper = new OutlineHelper(
|
|
21
|
+
* const outlineHelper = new OutlineHelper( meshScene, camera );
|
|
18
22
|
* overlayManager.register( 'outline', outlineHelper );
|
|
19
23
|
* outlineHelper.setSelectedObjects( [ mesh ] );
|
|
20
24
|
*/
|
|
21
25
|
export class OutlineHelper {
|
|
22
26
|
|
|
23
|
-
constructor(
|
|
27
|
+
constructor( scene, camera ) {
|
|
24
28
|
|
|
25
29
|
this.layer = 'scene';
|
|
26
|
-
this.
|
|
30
|
+
this._enabled = true;
|
|
27
31
|
|
|
28
|
-
// Outline node (handles its own multi-pass rendering via updateBefore)
|
|
32
|
+
// Outline node (handles its own multi-pass rendering via updateBefore).
|
|
33
|
+
// It sizes its render targets from the drawing buffer of whatever renderer
|
|
34
|
+
// draws it — the view overlay surface — so no size override is needed.
|
|
29
35
|
this._outlineNode = outline( scene, camera, {
|
|
30
36
|
selectedObjects: [],
|
|
31
37
|
edgeThickness: uniform( 1.0 ),
|
|
32
38
|
edgeGlow: uniform( 0.0 ),
|
|
33
39
|
} );
|
|
34
40
|
|
|
35
|
-
// OutlineNode calls its own setSize() internally during updateBefore()
|
|
36
|
-
// with the renderer's current render target size. Override to force
|
|
37
|
-
// display resolution so outlines stay sharp at any render scale.
|
|
38
|
-
this._displayWidth = 1;
|
|
39
|
-
this._displayHeight = 1;
|
|
40
|
-
const origSetSize = this._outlineNode.setSize.bind( this._outlineNode );
|
|
41
|
-
this._outlineNode.setSize = () => {
|
|
42
|
-
|
|
43
|
-
origSetSize( this._displayWidth, this._displayHeight );
|
|
44
|
-
|
|
45
|
-
};
|
|
46
|
-
|
|
47
41
|
// Build the outline color from visible + hidden edges
|
|
48
42
|
const edgeStrength = uniform( 3.0 );
|
|
49
43
|
const visibleEdgeColor = uniform( new Color( 0xffffff ) );
|
|
@@ -53,10 +47,21 @@ export class OutlineHelper {
|
|
|
53
47
|
.add( hiddenEdge.mul( hiddenEdgeColor ) )
|
|
54
48
|
.mul( edgeStrength );
|
|
55
49
|
|
|
56
|
-
|
|
50
|
+
const coverage = outlineColorNode.r.max( outlineColorNode.g ).max( outlineColorNode.b ).clamp( 0.0, 1.0 );
|
|
51
|
+
|
|
52
|
+
// Fullscreen quad, premultiplied-additive: dst.rgb += src.rgb, dst.a += src.a.
|
|
53
|
+
// Spelled out rather than AdditiveBlending + premultipliedAlpha, because the
|
|
54
|
+
// latter also makes NodeMaterial multiply rgb by alpha — squaring a soft edge.
|
|
57
55
|
this._material = new MeshBasicNodeMaterial();
|
|
58
|
-
this._material.colorNode = vec4( outlineColorNode,
|
|
59
|
-
this._material.
|
|
56
|
+
this._material.colorNode = vec4( outlineColorNode, coverage );
|
|
57
|
+
this._material.transparent = true;
|
|
58
|
+
this._material.blending = CustomBlending;
|
|
59
|
+
this._material.blendEquation = AddEquation;
|
|
60
|
+
this._material.blendSrc = OneFactor;
|
|
61
|
+
this._material.blendDst = OneFactor;
|
|
62
|
+
this._material.blendEquationAlpha = AddEquation;
|
|
63
|
+
this._material.blendSrcAlpha = OneFactor;
|
|
64
|
+
this._material.blendDstAlpha = OneFactor;
|
|
60
65
|
this._material.toneMapped = false;
|
|
61
66
|
this._material.depthTest = false;
|
|
62
67
|
this._material.depthWrite = false;
|
|
@@ -75,13 +80,23 @@ export class OutlineHelper {
|
|
|
75
80
|
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Nothing selected means nothing to draw — reported here so the overlay
|
|
85
|
+
* surface can stay parked instead of clearing every frame.
|
|
86
|
+
*/
|
|
87
|
+
get visible() {
|
|
88
|
+
|
|
89
|
+
return this._enabled && this._outlineNode.selectedObjects.length > 0;
|
|
90
|
+
|
|
91
|
+
}
|
|
92
|
+
|
|
78
93
|
/**
|
|
79
94
|
* Renders the outline overlay onto the current backbuffer.
|
|
80
95
|
* Called by OverlayManager after Display has rendered.
|
|
81
96
|
*/
|
|
82
97
|
render( renderer ) {
|
|
83
98
|
|
|
84
|
-
if ( this.
|
|
99
|
+
if ( ! this.visible ) return;
|
|
85
100
|
|
|
86
101
|
const prevAutoClear = renderer.autoClear;
|
|
87
102
|
renderer.autoClear = false;
|
|
@@ -91,34 +106,21 @@ export class OutlineHelper {
|
|
|
91
106
|
|
|
92
107
|
}
|
|
93
108
|
|
|
94
|
-
/**
|
|
95
|
-
* Updates internal render target sizes.
|
|
96
|
-
* @param {number} width - Display width in pixels
|
|
97
|
-
* @param {number} height - Display height in pixels
|
|
98
|
-
*/
|
|
99
|
-
setSize( width, height ) {
|
|
100
|
-
|
|
101
|
-
this._displayWidth = width;
|
|
102
|
-
this._displayHeight = height;
|
|
103
|
-
this._outlineNode.setSize( width, height );
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
109
|
show() {
|
|
108
110
|
|
|
109
|
-
this.
|
|
111
|
+
this._enabled = true;
|
|
110
112
|
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
hide() {
|
|
114
116
|
|
|
115
|
-
this.
|
|
117
|
+
this._enabled = false;
|
|
116
118
|
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
dispose() {
|
|
120
122
|
|
|
121
|
-
this.
|
|
123
|
+
this._enabled = false;
|
|
122
124
|
this._outlineNode?.dispose();
|
|
123
125
|
this._material?.dispose();
|
|
124
126
|
// QuadMesh extends Mesh — no dispose method on the mesh itself;
|