rayzee 7.17.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.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/BVHSubtreeWorker-D7KMbrQ-.js +2 -0
  3. package/dist/assets/BVHSubtreeWorker-D7KMbrQ-.js.map +1 -0
  4. package/dist/assets/BVHWorker-DOaGn6uN.js +2 -0
  5. package/dist/assets/BVHWorker-DOaGn6uN.js.map +1 -0
  6. package/dist/rayzee.es.js +2388 -2138
  7. package/dist/rayzee.es.js.map +1 -1
  8. package/dist/rayzee.umd.js +49 -49
  9. package/dist/rayzee.umd.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/EngineDefaults.js +7 -6
  12. package/src/Passes/OIDNDenoiser.js +17 -14
  13. package/src/PathTracerApp.js +63 -56
  14. package/src/Processor/BVHBuilder.js +11 -7
  15. package/src/Processor/BuildTimer.js +20 -6
  16. package/src/Processor/EmissiveTriangleBuilder.js +11 -7
  17. package/src/Processor/GeometryExtractor.js +11 -5
  18. package/src/Processor/KernelManager.js +48 -1
  19. package/src/Processor/PackedRayBuffer.js +20 -12
  20. package/src/Processor/ParallelBVHBuilder.js +9 -6
  21. package/src/Processor/QueueManager.js +6 -8
  22. package/src/Processor/SceneProcessor.js +47 -21
  23. package/src/Processor/ShaderBuilder.js +4 -1
  24. package/src/Processor/StorageTexturePool.js +4 -1
  25. package/src/Processor/Workers/BVHWorker.js +12 -6
  26. package/src/Processor/utils.js +30 -0
  27. package/src/RenderSettings.js +1 -1
  28. package/src/SceneHelpers.js +11 -1
  29. package/src/Stages/PathTracer.js +15 -4
  30. package/src/Stages/PathTracerStage.js +17 -14
  31. package/src/TSL/FinalWriteKernel.js +11 -6
  32. package/src/TSL/ShadeKernel.js +46 -24
  33. package/src/TSL/TextureSampling.js +12 -2
  34. package/src/index.js +3 -0
  35. package/src/managers/EnvironmentManager.js +12 -8
  36. package/src/managers/MaterialDataManager.js +12 -9
  37. package/src/managers/OverlayManager.js +105 -48
  38. package/src/managers/UniformManager.js +1 -1
  39. package/src/managers/helpers/OutlineHelper.js +42 -40
  40. package/src/managers/helpers/TransformGizmoHelper.js +61 -0
  41. package/src/managers/helpers/ViewOverlayRenderer.js +140 -0
  42. package/src/managers/helpers/overlaySurface.js +53 -0
  43. package/src/utils/Logger.js +291 -0
  44. package/dist/assets/BVHSubtreeWorker-sNzvxn66.js +0 -2
  45. package/dist/assets/BVHSubtreeWorker-sNzvxn66.js.map +0 -1
  46. package/dist/assets/BVHWorker-CiVdFrwe.js +0 -2
  47. package/dist/assets/BVHWorker-CiVdFrwe.js.map +0 -1
@@ -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
- * 1. **HelperScene** — A Three.js Scene rendered on top of the WebGPU backbuffer
9
- * (light gizmos, bounding boxes, outlines). Renders at display resolution.
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 (AF points, debug labels). Completely separate
12
- * from the WebGPU canvas, so it is never captured in saved images.
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( renderer, camera );
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( renderer, camera ) {
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 = document.createElement( 'canvas' );
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 HUD canvas into the given container. Idempotent — safe to call
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 || this._hudCanvas.parentElement === container ) return;
77
- container.appendChild( this._hudCanvas );
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( this.renderer, meshScene, this.camera );
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
- console.warn( `OverlayManager: helper "${name}" already registered — replacing.` );
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
- // 1. Render 3D HelperScene overlay (light gizmos, etc.)
263
- if ( this._helperScene ) {
306
+ const overlay = this._viewOverlay;
264
307
 
265
- this._helperScene.render( this.renderer, this.camera );
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 ( helper.visible && helper.layer === 'scene' && helper.render ) {
312
+ if ( hasSceneOverlay || overlay.isVisible ) {
273
313
 
274
- helper.render( this.renderer, this.camera );
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
- * Forwards display dimensions to helpers that need resize.
287
- * @param {number} width - Display width in pixels
288
- * @param {number} height - Display height in pixels
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 );
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
- helper.setSize?.( width, height );
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 ( helper.visible && helper.layer === 'hud' && helper.render ) {
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
- const dpr = window.devicePixelRatio || 1;
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 pixelW = Math.round( displayW * dpr );
333
- const pixelH = Math.round( displayH * dpr );
390
+ const size = displayW > 0 && displayH > 0 ? viewPixelSize( canvas ) : null;
391
+ if ( ! size ) return;
334
392
 
335
- if ( canvas.width !== pixelW || canvas.height !== pixelH ) {
393
+ if ( canvas.width !== size.width || canvas.height !== size.height ) {
336
394
 
337
- canvas.width = pixelW;
338
- canvas.height = pixelH;
395
+ canvas.width = size.width;
396
+ canvas.height = size.height;
339
397
 
340
398
  }
341
399
 
342
- ctx.clearRect( 0, 0, pixelW, pixelH );
400
+ ctx.clearRect( 0, 0, size.width, size.height );
343
401
  ctx.save();
344
- ctx.scale( dpr, dpr );
402
+ ctx.scale( size.width / displayW, size.height / displayH );
345
403
 
346
404
  for ( const helper of this._helpers.values() ) {
347
405
 
348
- if ( helper.visible && helper.layer === 'hud' && helper.render ) {
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 );
@@ -164,13 +164,13 @@ export class UniformManager {
164
164
  u( 'maxSamples', DEFAULT_STATE.maxSamples, 'int' );
165
165
  u( 'transmissiveBounces', DEFAULT_STATE.transmissiveBounces, 'int' );
166
166
  u( 'maxSubsurfaceSteps', DEFAULT_STATE.maxSubsurfaceSteps, 'int' );
167
+ u( 'maxTransparentBounces', DEFAULT_STATE.maxTransparentBounces, 'int' );
167
168
  u( 'visMode', DEFAULT_STATE.debugMode, 'int' );
168
169
  u( 'debugVisScale', DEFAULT_STATE.debugVisScale, 'float' );
169
170
 
170
171
  // Tier-1 convergence early-stop (FinalWrite reads these; live-toggled, no shader rebuild)
171
172
  ub( 'useAdaptiveSampling', DEFAULT_STATE.useAdaptiveSampling );
172
173
  u( 'noiseThreshold', DEFAULT_STATE.noiseThreshold, 'float' );
173
- u( 'darkNoiseFloor', DEFAULT_STATE.darkNoiseFloor, 'float' );
174
174
  u( 'adaptiveMinSamples', DEFAULT_STATE.adaptiveMinSamples, 'int' );
175
175
  // CPU-only (read in PathTracer._isConvergedComplete, not bound to any kernel); registered here for
176
176
  // the settings/configureForMode plumbing + the _defineUniformGetters accessor.
@@ -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 { AdditiveBlending, Color } from 'three';
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 with additive blending, composited on top of the Display output.
11
- * Renders at **display resolution** (not render resolution), so outlines
12
- * stay sharp regardless of path tracer resolution scaling.
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( renderer, meshScene, camera );
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( renderer, scene, camera ) {
27
+ constructor( scene, camera ) {
24
28
 
25
29
  this.layer = 'scene';
26
- this.visible = true;
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
- // Fullscreen quad with additive blending — composites outline on top
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, 1.0 );
59
- this._material.blending = AdditiveBlending;
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._outlineNode.selectedObjects.length === 0 ) return;
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.visible = true;
111
+ this._enabled = true;
110
112
 
111
113
  }
112
114
 
113
115
  hide() {
114
116
 
115
- this.visible = false;
117
+ this._enabled = false;
116
118
 
117
119
  }
118
120
 
119
121
  dispose() {
120
122
 
121
- this.visible = false;
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;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * TransformGizmoHelper — adapts {@link TransformManager} to the OverlayManager
3
+ * helper interface so the translate/rotate/scale gizmo draws on the same
4
+ * view-resolution surface as the light gizmos and the selection outline.
5
+ *
6
+ * An adapter rather than the interface on TransformManager itself: the manager
7
+ * is public engine API with its own lifecycle (PathTracerApp owns dispose), and
8
+ * the overlay must not dispose it a second time.
9
+ *
10
+ * Layer: 'scene'.
11
+ *
12
+ * @example
13
+ * overlayManager.register( 'transform', new TransformGizmoHelper( app.transformManager ) );
14
+ */
15
+ export class TransformGizmoHelper {
16
+
17
+ /**
18
+ * @param {import('../TransformManager.js').TransformManager} transformManager
19
+ */
20
+ constructor( transformManager ) {
21
+
22
+ this.layer = 'scene';
23
+ this._transformManager = transformManager;
24
+ this._enabled = true;
25
+
26
+ }
27
+
28
+ /** Nothing to draw unless the gizmo is attached to something. */
29
+ get visible() {
30
+
31
+ return this._enabled && this._transformManager.attachedObject !== null;
32
+
33
+ }
34
+
35
+ render( renderer ) {
36
+
37
+ this._transformManager.render( renderer );
38
+
39
+ }
40
+
41
+ show() {
42
+
43
+ this._enabled = true;
44
+
45
+ }
46
+
47
+ hide() {
48
+
49
+ this._enabled = false;
50
+
51
+ }
52
+
53
+ dispose() {
54
+
55
+ // Only stops drawing — TransformManager is owned by the app, which
56
+ // disposes it separately.
57
+ this._enabled = false;
58
+
59
+ }
60
+
61
+ }
@@ -0,0 +1,140 @@
1
+ import { WebGPURenderer } from 'three/webgpu';
2
+ import { SRGBColorSpace } from 'three';
3
+ import { disposeRenderer } from '../../Processor/utils.js';
4
+ import { createOverlayCanvas, viewPixelSize, MAX_VIEW_DPR } from './overlaySurface.js';
5
+
6
+ /** Device-pixel ceiling per axis — guards against huge surfaces at high viewport zoom. */
7
+ const MAX_OVERLAY_DIM = 4096;
8
+
9
+ /**
10
+ * ViewOverlayRenderer — a transparent canvas sized to what the user actually
11
+ * sees, driven by its own WebGPURenderer.
12
+ *
13
+ * The main canvas' backing store is the *render* resolution (512 in preview,
14
+ * up to 4K for a final render), and CSS stretches it to fit the viewport. Any
15
+ * helper drawn into that backbuffer inherits the render resolution and is then
16
+ * rescaled by the browser — soft gizmos at low render scale, wasted pixels at
17
+ * high. This surface instead matches the on-screen size (bounding rect × DPR,
18
+ * so the viewport zoom is included), so gizmos and outlines stay crisp and
19
+ * screen-space-correct at any render resolution.
20
+ *
21
+ * The renderer shares the main renderer's `GPUDevice`, so this costs a
22
+ * swapchain, not a second WebGPU context. Helpers rendered here are also
23
+ * physically absent from the main canvas, so they can never leak into a saved
24
+ * image.
25
+ *
26
+ * @example
27
+ * const overlay = new ViewOverlayRenderer();
28
+ * await overlay.init( { device: renderer.backend.device, sizeSource: canvas } );
29
+ * container.appendChild( overlay.canvas );
30
+ * // per frame:
31
+ * overlay.syncSize();
32
+ * overlay.begin();
33
+ * helperScene.render( overlay.renderer, camera );
34
+ */
35
+ export class ViewOverlayRenderer {
36
+
37
+ constructor() {
38
+
39
+ // Starts parked: three allocates the output-pass framebuffer (~30 MiB at
40
+ // 1600²) on the first clear, so a session that never shows a helper never
41
+ // pays for it.
42
+ this.canvas = createOverlayCanvas( true );
43
+ this._visible = false;
44
+
45
+ /** @type {import('three/webgpu').WebGPURenderer|null} */
46
+ this.renderer = null;
47
+
48
+ this._sizeSource = null;
49
+
50
+ }
51
+
52
+ /**
53
+ * Creates the renderer on the shared device.
54
+ *
55
+ * @param {Object} config
56
+ * @param {GPUDevice} config.device - Device to share with the main renderer
57
+ * @param {HTMLElement} config.sizeSource - Element whose on-screen box defines the overlay size
58
+ * @returns {Promise<ViewOverlayRenderer>}
59
+ */
60
+ async init( { device, sizeSource } ) {
61
+
62
+ this._sizeSource = sizeSource;
63
+
64
+ // A second backend claims the device's single uncaptured-error slot during
65
+ // init(). Hand it back so the owner's handler keeps receiving errors.
66
+ const deviceErrorHandler = device.onuncapturederror;
67
+
68
+ // At DPR ≥ 2 the browser's downscale already supersamples the edges, so MSAA
69
+ // would only add a multisampled attachment for no visible gain. Below that
70
+ // there is nothing to hide jaggies, so pay for it.
71
+ const renderer = new WebGPURenderer( {
72
+ canvas: this.canvas,
73
+ device,
74
+ alpha: true,
75
+ antialias: ( window.devicePixelRatio || 1 ) < MAX_VIEW_DPR,
76
+ } );
77
+
78
+ await renderer.init();
79
+ device.onuncapturederror = deviceErrorHandler;
80
+
81
+ renderer.outputColorSpace = SRGBColorSpace;
82
+ renderer.setPixelRatio( 1.0 );
83
+ renderer.setClearColor( 0x000000, 0 );
84
+ // Every frame starts with an explicit clear() in begin(); helpers then
85
+ // stack on top of each other without wiping what came before.
86
+ renderer.autoClear = false;
87
+
88
+ this.renderer = renderer;
89
+ this.syncSize();
90
+
91
+ return this;
92
+
93
+ }
94
+
95
+ get isVisible() {
96
+
97
+ return this._visible;
98
+
99
+ }
100
+
101
+ /** Matches the backing store to the size source's on-screen box. */
102
+ syncSize() {
103
+
104
+ if ( ! this.renderer ) return;
105
+
106
+ const size = viewPixelSize( this._sizeSource, MAX_OVERLAY_DIM );
107
+ if ( ! size || ( size.width === this.canvas.width && size.height === this.canvas.height ) ) return;
108
+
109
+ this.renderer.setSize( size.width, size.height, false );
110
+
111
+ }
112
+
113
+ /** Clears the surface to fully transparent. Call once before drawing helpers. */
114
+ begin() {
115
+
116
+ if ( ! this.renderer ) return;
117
+
118
+ this.renderer.setRenderTarget( null );
119
+ this.renderer.clear();
120
+
121
+ }
122
+
123
+ setVisible( visible ) {
124
+
125
+ if ( this._visible === visible ) return;
126
+
127
+ this._visible = visible;
128
+ this.canvas.style.display = visible ? '' : 'none';
129
+
130
+ }
131
+
132
+ dispose() {
133
+
134
+ disposeRenderer( this.renderer );
135
+ this.renderer = null;
136
+ this.canvas.parentElement?.removeChild( this.canvas );
137
+
138
+ }
139
+
140
+ }