@driftengine/core 4.8.7 → 4.9.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 (163) hide show
  1. package/CHANGELOG.json +61 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +5 -0
  4. package/dist/index.js +7 -0
  5. package/dist/render/atmosphere.d.ts +13 -0
  6. package/dist/render/atmosphere.js +9 -1
  7. package/dist/render/backend/api.d.ts +9 -1
  8. package/dist/render/backend/createRenderer.d.ts +6 -0
  9. package/dist/render/backend/createRenderer.js +4 -1
  10. package/dist/render/backend/webgl2/renderer.d.ts +90 -2
  11. package/dist/render/backend/webgl2/renderer.js +297 -16
  12. package/dist/render/backend/webgpu/device.d.ts +7 -1
  13. package/dist/render/backend/webgpu/device.js +31 -3
  14. package/dist/render/backend/webgpu/flatPass.d.ts +16 -1
  15. package/dist/render/backend/webgpu/flatPass.js +58 -26
  16. package/dist/render/backend/webgpu/postPass.d.ts +51 -9
  17. package/dist/render/backend/webgpu/postPass.js +16 -2
  18. package/dist/render/backend/webgpu/reflectionSurfacePass.d.ts +30 -0
  19. package/dist/render/backend/webgpu/reflectionSurfacePass.js +100 -0
  20. package/dist/render/backend/webgpu/renderer.d.ts +131 -2
  21. package/dist/render/backend/webgpu/renderer.js +586 -70
  22. package/dist/render/displayRange.d.ts +31 -0
  23. package/dist/render/displayRange.js +47 -0
  24. package/dist/render/filmicCurve.d.ts +74 -0
  25. package/dist/render/filmicCurve.js +203 -0
  26. package/dist/render/fog.d.ts +4 -1
  27. package/dist/render/fog.js +12 -2
  28. package/dist/render/frame/resources.d.ts +2 -2
  29. package/dist/render/frame/resources.js +6 -0
  30. package/dist/render/frameReflections.d.ts +40 -0
  31. package/dist/render/frameReflections.js +48 -0
  32. package/dist/render/pass.d.ts +49 -0
  33. package/dist/render/reflectionSurfaceTargets.d.ts +28 -0
  34. package/dist/render/reflectionSurfaceTargets.js +99 -0
  35. package/dist/render/renderQuality.d.ts +39 -8
  36. package/dist/render/renderQuality.js +4 -0
  37. package/dist/render/sceneTarget.d.ts +5 -0
  38. package/dist/render/sceneTarget.js +24 -1
  39. package/dist/render/screenSpaceReflection.d.ts +5 -0
  40. package/dist/render/screenSpaceReflection.js +5 -0
  41. package/dist/render/shaders/arcane.d.ts +1 -1
  42. package/dist/render/shaders/bolt.d.ts +1 -1
  43. package/dist/render/shaders/caustics.d.ts +1 -1
  44. package/dist/render/shaders/film.d.ts +1 -1
  45. package/dist/render/shaders/filmLook.js +2 -1
  46. package/dist/render/shaders/fire.d.ts +1 -1
  47. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  48. package/dist/render/shaders/flat/glassTint.js +2 -0
  49. package/dist/render/shaders/flat/index.d.ts +10 -0
  50. package/dist/render/shaders/flat/index.js +6 -0
  51. package/dist/render/shaders/flat/main.d.ts +1 -1
  52. package/dist/render/shaders/flat/main.js +28 -0
  53. package/dist/render/shaders/flat/overlay.d.ts +16 -0
  54. package/dist/render/shaders/flat/overlay.js +117 -0
  55. package/dist/render/shaders/flat/pointShadow.js +1 -1
  56. package/dist/render/shaders/flat/reflectionSurface.d.ts +21 -0
  57. package/dist/render/shaders/flat/reflectionSurface.js +61 -0
  58. package/dist/render/shaders/fog.d.ts +1 -1
  59. package/dist/render/shaders/fog.js +9 -2
  60. package/dist/render/shaders/fringe.d.ts +29 -0
  61. package/dist/render/shaders/fringe.js +55 -0
  62. package/dist/render/shaders/generated/arcane.wgsl.d.ts +1 -1
  63. package/dist/render/shaders/generated/arcane.wgsl.js +1 -1
  64. package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
  65. package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
  66. package/dist/render/shaders/generated/caustics.wgsl.d.ts +1 -1
  67. package/dist/render/shaders/generated/caustics.wgsl.js +1 -1
  68. package/dist/render/shaders/generated/film.wgsl.d.ts +1 -1
  69. package/dist/render/shaders/generated/film.wgsl.js +1 -1
  70. package/dist/render/shaders/generated/fire.wgsl.d.ts +1 -1
  71. package/dist/render/shaders/generated/fire.wgsl.js +1 -1
  72. package/dist/render/shaders/generated/flat.wgsl.d.ts +176 -16
  73. package/dist/render/shaders/generated/flat.wgsl.js +214 -54
  74. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  75. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  76. package/dist/render/shaders/generated/particle.wgsl.d.ts +4 -4
  77. package/dist/render/shaders/generated/particle.wgsl.js +4 -4
  78. package/dist/render/shaders/generated/rush.wgsl.d.ts +56 -26
  79. package/dist/render/shaders/generated/rush.wgsl.js +56 -26
  80. package/dist/render/shaders/generated/scatter.wgsl.d.ts +1 -1
  81. package/dist/render/shaders/generated/scatter.wgsl.js +1 -1
  82. package/dist/render/shaders/generated/smoke.wgsl.d.ts +1 -1
  83. package/dist/render/shaders/generated/smoke.wgsl.js +1 -1
  84. package/dist/render/shaders/generated/ssrResolve.wgsl.d.ts +29 -0
  85. package/dist/render/shaders/generated/ssrResolve.wgsl.js +29 -0
  86. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +22 -2
  87. package/dist/render/shaders/generated/ssrTrace.wgsl.js +22 -2
  88. package/dist/render/shaders/generated/water.wgsl.d.ts +1 -1
  89. package/dist/render/shaders/generated/water.wgsl.js +1 -1
  90. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +2 -2
  91. package/dist/render/shaders/gpudriven/fog.wgsl.js +11 -3
  92. package/dist/render/shaders/line.d.ts +1 -1
  93. package/dist/render/shaders/particle.d.ts +3 -3
  94. package/dist/render/shaders/rush.js +54 -11
  95. package/dist/render/shaders/scatter.d.ts +1 -1
  96. package/dist/render/shaders/smoke.d.ts +1 -1
  97. package/dist/render/shaders/ssrResolve.d.ts +17 -0
  98. package/dist/render/shaders/ssrResolve.js +51 -0
  99. package/dist/render/shaders/ssrTrace.js +26 -0
  100. package/dist/render/shaders/water.d.ts +1 -1
  101. package/dist/render/ssrPass.d.ts +16 -0
  102. package/dist/render/ssrPass.js +106 -1
  103. package/dist/render/surfaceOverlay.d.ts +132 -0
  104. package/dist/render/surfaceOverlay.js +85 -0
  105. package/dist/render/surfaceTexture.d.ts +19 -0
  106. package/dist/render/surfaceTexture.js +69 -0
  107. package/dist/render/temporalAa.d.ts +14 -0
  108. package/dist/render/temporalAa.js +20 -0
  109. package/dist/render/vertexDefaults.d.ts +17 -0
  110. package/dist/render/vertexDefaults.js +24 -0
  111. package/package.json +6 -6
  112. package/src/index.ts +16 -0
  113. package/src/render/atmosphere.ts +22 -1
  114. package/src/render/backend/api.ts +9 -0
  115. package/src/render/backend/createRenderer.ts +18 -1
  116. package/src/render/backend/webgl2/renderer.ts +357 -13
  117. package/src/render/backend/webgpu/device.ts +42 -3
  118. package/src/render/backend/webgpu/flatPass.ts +63 -26
  119. package/src/render/backend/webgpu/postPass.ts +18 -0
  120. package/src/render/backend/webgpu/reflectionSurfacePass.ts +114 -0
  121. package/src/render/backend/webgpu/renderer.ts +735 -83
  122. package/src/render/displayRange.ts +61 -0
  123. package/src/render/filmicCurve.ts +229 -0
  124. package/src/render/fog.ts +19 -3
  125. package/src/render/frame/resources.ts +6 -0
  126. package/src/render/frameReflections.ts +72 -0
  127. package/src/render/pass.ts +53 -1
  128. package/src/render/reflectionSurfaceTargets.ts +105 -0
  129. package/src/render/renderQuality.ts +44 -8
  130. package/src/render/sceneTarget.ts +30 -2
  131. package/src/render/screenSpaceReflection.ts +5 -0
  132. package/src/render/shaders/filmLook.ts +2 -1
  133. package/src/render/shaders/flat/glassTint.ts +5 -1
  134. package/src/render/shaders/flat/index.ts +16 -0
  135. package/src/render/shaders/flat/main.ts +28 -0
  136. package/src/render/shaders/flat/overlay.ts +117 -0
  137. package/src/render/shaders/flat/pointShadow.ts +1 -1
  138. package/src/render/shaders/flat/reflectionSurface.ts +61 -0
  139. package/src/render/shaders/fog.ts +9 -2
  140. package/src/render/shaders/fringe.ts +59 -0
  141. package/src/render/shaders/generated/arcane.wgsl.ts +1 -1
  142. package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
  143. package/src/render/shaders/generated/caustics.wgsl.ts +1 -1
  144. package/src/render/shaders/generated/film.wgsl.ts +1 -1
  145. package/src/render/shaders/generated/fire.wgsl.ts +1 -1
  146. package/src/render/shaders/generated/flat.wgsl.ts +214 -54
  147. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  148. package/src/render/shaders/generated/particle.wgsl.ts +4 -4
  149. package/src/render/shaders/generated/rush.wgsl.ts +56 -26
  150. package/src/render/shaders/generated/scatter.wgsl.ts +1 -1
  151. package/src/render/shaders/generated/smoke.wgsl.ts +1 -1
  152. package/src/render/shaders/generated/ssrResolve.wgsl.ts +30 -0
  153. package/src/render/shaders/generated/ssrTrace.wgsl.ts +22 -2
  154. package/src/render/shaders/generated/water.wgsl.ts +1 -1
  155. package/src/render/shaders/gpudriven/fog.wgsl.ts +11 -3
  156. package/src/render/shaders/rush.ts +54 -11
  157. package/src/render/shaders/ssrResolve.ts +52 -0
  158. package/src/render/shaders/ssrTrace.ts +26 -0
  159. package/src/render/ssrPass.ts +138 -1
  160. package/src/render/surfaceOverlay.ts +195 -0
  161. package/src/render/surfaceTexture.ts +67 -0
  162. package/src/render/temporalAa.ts +20 -0
  163. package/src/render/vertexDefaults.ts +28 -0
@@ -15,6 +15,11 @@ import {
15
15
  glShadowDepthFunc,
16
16
  } from '../../depthConvention.ts';
17
17
  import type { FrameView } from '../../frameView.ts';
18
+ import { maskOf } from '../../frame/resources.ts';
19
+
20
+ /** The two copies a registered pass may declare it reads. See `drawPass`. */
21
+ const COLOR_SNAPSHOT = maskOf('colorSnapshot');
22
+ const DEPTH_SNAPSHOT = maskOf('depthSnapshot');
18
23
  import type { Mover } from '../../recon/mover.ts';
19
24
  import { resolveGlass } from '../../glass.ts';
20
25
  import type { GlassOptions, ResolvedGlass } from '../../glass.ts';
@@ -31,6 +36,12 @@ import {
31
36
  } from '../../temporalAa.ts';
32
37
  import { DecalPass } from '../../decalPass.ts';
33
38
  import { SsrPass } from '../../ssrPass.ts';
39
+ import { ReflectionSurfaceTargets } from '../../reflectionSurfaceTargets.ts';
40
+ import {
41
+ frameReflectionsRefused,
42
+ resolveFrameReflections,
43
+ type ResolvedFrameReflections,
44
+ } from '../../frameReflections.ts';
34
45
  import { GlobalMediumPass } from '../../globalMediumPass.ts';
35
46
  import {
36
47
  DEFAULT_GLOBAL_MEDIUM,
@@ -81,11 +92,18 @@ import { isWeakGpuFamily, webgl2RendererName } from '../../gpuCapability.ts';
81
92
  import { GpuTimer } from '../../gpuTimer.ts';
82
93
  import { compileProgram, uniformLocations } from '../../shader.ts';
83
94
  import {
95
+ countUniformVectors,
84
96
  FULL_LIGHT_BUDGET,
85
97
  type LightBudget,
86
98
  nextLightBudget,
87
99
  planLightBudget,
88
100
  } from '../../uniformVectorBudget.ts';
101
+ import {
102
+ OVERLAY_FLOATS,
103
+ overlayLays,
104
+ packSurfaceOverlay,
105
+ type SurfaceOverlay,
106
+ } from '../../surfaceOverlay.ts';
89
107
  import { FILM_FRAG, FILM_VERT } from '../../shaders/film.ts';
90
108
  import { SceneTarget } from '../../sceneTarget.ts';
91
109
  import type { ColourGradeLut } from '../../colourGrade.ts';
@@ -293,8 +311,22 @@ import { TextRenderer } from '../../textRenderer.ts';
293
311
  import { SdfTextRenderer } from '../../sdfTextRenderer.ts';
294
312
  import type { SdfFont } from '../../sdfFont.ts';
295
313
  import type { SdfTextStyle } from '../../sdfTextLayout.ts';
296
- import { MOTION_BLUR_MAX_UV, RUSH_REACH_UV } from '../../vertexDefaults.ts';
297
- import { OUTPUT_TRANSFORM_CODE } from '../../vertexDefaults.ts';
314
+ import {
315
+ MOTION_BLUR_MAX_UV,
316
+ OBJECT_BLUR_UNKEPT,
317
+ RUSH_REACH_UV,
318
+ clampMotionShare,
319
+ } from '../../vertexDefaults.ts';
320
+ import { OUTPUT_TRANSFORM_CODE, forwardTransformCode } from '../../vertexDefaults.ts';
321
+ import type { DisplayRange } from '../../displayRange.ts';
322
+ import {
323
+ DEFAULT_FILMIC_CURVE,
324
+ FILMIC_CLAMPED,
325
+ FILMIC_CONSTANTS,
326
+ FILMIC_WITHOUT_COMPOSITE,
327
+ resolveFilmicCurve,
328
+ type FilmicCurve,
329
+ } from '../../filmicCurve.ts';
298
330
  import type { RenderBackend } from '../api.ts';
299
331
  import { deviceSnappedCellSize } from '../../textLayout.ts';
300
332
  import type { TextStyle } from '../../textLayout.ts';
@@ -1348,6 +1380,10 @@ export class WebGL2Renderer implements RendererApi {
1348
1380
  private readonly reflectionQueue = new ReflectionQueue();
1349
1381
  /** Built on the first frame that submits a surface, and never for a scene with none. */
1350
1382
  private ssr: SsrPass | null = null;
1383
+ /** The frame's materials' reflections, resolved; null where off. See `frameReflections.ts`. */
1384
+ private readonly frameReflections: ResolvedFrameReflections | null;
1385
+ /** Their two maps on the scene's framebuffer, null where they cannot be drawn. */
1386
+ private readonly reflectionTargets: ReflectionSurfaceTargets | null;
1351
1387
  /** Said once where a reflection cannot be traced at all. */
1352
1388
  private reflectionsRefused = false;
1353
1389
  /** Where the frame's camera stands, which is what gives a reconstructed normal its sign. */
@@ -1430,6 +1466,8 @@ export class WebGL2Renderer implements RendererApi {
1430
1466
  * exactly as it asked to be and nothing about it changes.
1431
1467
  */
1432
1468
  private exposure: number;
1469
+ /** The `filmic` curve's eight constants, held until `setFilmicCurve` is called again. */
1470
+ private readonly filmConstants = new Float32Array(FILMIC_CONSTANTS);
1433
1471
  /**
1434
1472
  * This frame's veil colour, and how much of it to composite. See `setFrameVeil`.
1435
1473
  *
@@ -1631,6 +1669,15 @@ export class WebGL2Renderer implements RendererApi {
1631
1669
  private warnedNoPointShadowArray = false;
1632
1670
  /** Said once, for the retired ambient dial. */
1633
1671
  private warnedEnvironmentAmbient = false;
1672
+ /** The overlay the draws wear, `uOverlay`: see `setSurfaceOverlay`. */
1673
+ private readonly overlayFloats = new Float32Array(OVERLAY_FLOATS);
1674
+ /** Its atlas, and the 2D copy of it bound where the refraction copy goes. */
1675
+ private overlayMaps: SurfaceTexture | null = null;
1676
+ private overlayTexture: WebGLTexture | null = null;
1677
+ /** Whether this device's lit stage has room for the overlay's vectors: asked once. */
1678
+ private overlayRoom: boolean | null = null;
1679
+ private warnedOverlayRoom = false;
1680
+ private warnedOverlayBlocks = false;
1634
1681
  /** Whether a probe is being baked, so the passes that would recurse into one can tell. */
1635
1682
  private probePassActive = false;
1636
1683
  /**
@@ -2298,6 +2345,8 @@ export class WebGL2Renderer implements RendererApi {
2298
2345
  gl: WebGL2RenderingContext;
2299
2346
  outputTransform: number;
2300
2347
  outputExposure: number;
2348
+ sceneColor: WebGLTexture | null;
2349
+ sceneDepth: WebGLTexture | null;
2301
2350
  } | null = null;
2302
2351
  private passDevice: PassDevice | null = null;
2303
2352
 
@@ -2309,6 +2358,9 @@ export class WebGL2Renderer implements RendererApi {
2309
2358
  * call. `init` runs now rather than at the first frame, because building a pipeline in the
2310
2359
  * frame loop is the allocation the house rules are about.
2311
2360
  */
2361
+ /** Each registered pass's declared reads, as a mask, resolved once at registration. */
2362
+ private readonly passReads = new Map<PassHandle, number>();
2363
+
2312
2364
  registerPass(definition: PassDefinition): PassHandle {
2313
2365
  const handle = registerIn(this.passes, definition);
2314
2366
  /*
@@ -2327,6 +2379,8 @@ export class WebGL2Renderer implements RendererApi {
2327
2379
  depthCorrection: correction,
2328
2380
  };
2329
2381
  if (definition.prepare !== undefined) this.preparingPasses++;
2382
+ /* Resolved once: `maskOf` builds an array, and the frame path may not. */
2383
+ this.passReads.set(handle, maskOf(...(definition.reads ?? [])));
2330
2384
  definition.init?.(this.passDevice);
2331
2385
  return handle;
2332
2386
  }
@@ -2378,10 +2432,28 @@ export class WebGL2Renderer implements RendererApi {
2378
2432
  gl: this.gl,
2379
2433
  outputTransform: 0,
2380
2434
  outputExposure: 1,
2435
+ sceneColor: null,
2436
+ sceneDepth: null,
2381
2437
  };
2382
2438
  const context = this.passContext;
2383
2439
  context.outputTransform = this.gradeCode();
2384
2440
  context.outputExposure = this.gradeExposure();
2441
+ /*
2442
+ * The copies a pass declared it reads, taken here with everything drawn before it: colour
2443
+ * afresh at each reading pass, as WebGPU's `takePassColour` does, so a full-screen effect drawn
2444
+ * last reads the whole frame; depth afresh too, since a blit is the whole cost of either here.
2445
+ * Not in a mirror, a probe's face or a capture, whose pictures are not the frame's.
2446
+ */
2447
+ const reads = this.passReads.get(handle) ?? 0;
2448
+ const own = !this.reflectionPassActive && !this.probePassActive && !this.capturePassActive;
2449
+ context.sceneColor =
2450
+ own && (reads & COLOR_SNAPSHOT) !== 0
2451
+ ? (this.sceneTarget?.snapshotColor(true) ?? null)
2452
+ : null;
2453
+ context.sceneDepth =
2454
+ own && (reads & DEPTH_SNAPSHOT) !== 0
2455
+ ? (this.sceneTarget?.snapshotDepth(true) ?? null)
2456
+ : null;
2385
2457
  definition.draw(context as PassContext);
2386
2458
  }
2387
2459
 
@@ -2389,6 +2461,7 @@ export class WebGL2Renderer implements RendererApi {
2389
2461
  unregisterPass(handle: PassHandle): void {
2390
2462
  const definition = unregisterIn(this.passes, handle);
2391
2463
  if (definition?.prepare !== undefined) this.preparingPasses--;
2464
+ this.passReads.delete(handle);
2392
2465
  if (definition !== undefined && this.passDevice !== null) definition.dispose?.(this.passDevice);
2393
2466
  }
2394
2467
 
@@ -2559,10 +2632,11 @@ export class WebGL2Renderer implements RendererApi {
2559
2632
  gl.bindTexture(gl.TEXTURE_2D, this.clusterTexture);
2560
2633
  gl.uniform1i(u['uClusterTable'] ?? null, CLUSTER_TABLE_TEXTURE_UNIT);
2561
2634
  gl.uniform1i(u['uClustered'] ?? null, on ? 1 : 0);
2562
- if (!on) return;
2563
2635
 
2564
2636
  /*
2565
- * The frustum, read off the projection rather than taken as a parameter.
2637
+ * The frustum, read off the projection rather than taken as a parameter — **and with the view,
2638
+ * written whether or not the lights are clustered**: a surface overlay's rim finds its place on
2639
+ * the screen through the same two (`overlayScreen`).
2566
2640
  *
2567
2641
  * A perspective matrix carries `1 / tan(fovY/2)` at [5] and that over the aspect at [0], and
2568
2642
  * the near and far planes in [14] and [10]. Deriving them here means the binner and the shader
@@ -2580,6 +2654,7 @@ export class WebGL2Renderer implements RendererApi {
2580
2654
  this.clusterFrustum[3] = aspect;
2581
2655
  gl.uniform4fv(u['uClusterFrustum'] ?? null, this.clusterFrustum);
2582
2656
  gl.uniformMatrix4fv(u['uView'] ?? null, false, camera.view);
2657
+ if (!on) return;
2583
2658
 
2584
2659
  this.clusterTable ??= createClusterTable();
2585
2660
  /* The rectangles past the fixed arm's go into the table. See `ClusterLightSet.areas`. */
@@ -2683,6 +2758,13 @@ export class WebGL2Renderer implements RendererApi {
2683
2758
  this.quality = resolveRenderQuality(qualityOptions);
2684
2759
  /* The grade this world asked for, until a frame says otherwise. See `setOutputExposure`. */
2685
2760
  this.exposure = this.quality.outputExposure;
2761
+ resolveFilmicCurve(DEFAULT_FILMIC_CURVE, this.filmConstants);
2762
+ if (
2763
+ this.quality.outputTransform === 'filmic' &&
2764
+ !(this.quality.screenEffects && this.quality.hdrScene)
2765
+ ) {
2766
+ console.warn(FILMIC_WITHOUT_COMPOSITE);
2767
+ }
2686
2768
  /*
2687
2769
  * Multisampling, but only when anything can use it.
2688
2770
  *
@@ -2981,6 +3063,29 @@ export class WebGL2Renderer implements RendererApi {
2981
3063
  this.quality.discardResolvedAttachments,
2982
3064
  )
2983
3065
  : null;
3066
+ /*
3067
+ * The frame's materials' reflections, where the frame can carry them: scene light to swap the
3068
+ * probe's share in, and one sample, which the trace's depth copy and both maps are. Said once
3069
+ * where it cannot, in the words the other backend uses. See `frameReflections.ts`.
3070
+ */
3071
+ this.frameReflections = resolveFrameReflections(this.quality.screenSpaceReflections);
3072
+ const reflectionsRefusal =
3073
+ this.frameReflections === null
3074
+ ? null
3075
+ : this.sceneTarget === null || !this.sceneTarget.keepsRange
3076
+ ? 'the frame keeps no scene light to swap the probe in (`screenEffects` and `hdrScene`)'
3077
+ : this.sceneTarget.sampleCount > 1
3078
+ ? 'a multisampled frame has no single depth or colour to march through'
3079
+ : null;
3080
+ if (reflectionsRefusal !== null) {
3081
+ console.warn(`WebGL2: ${frameReflectionsRefused(reflectionsRefusal)}`);
3082
+ }
3083
+ this.reflectionTargets =
3084
+ this.frameReflections !== null && reflectionsRefusal === null
3085
+ ? new ReflectionSurfaceTargets(gl)
3086
+ : null;
3087
+ /* The lit programs write the maps from the first frame they are rebuilt in. See `litOn`. */
3088
+ if (this.reflectionTargets !== null) this.askLit('REFLECTION_MAPS');
2984
3089
  /* Screen-space skin needs the composite to add into; without one it is pre-integrated, said once. */
2985
3090
  if (this.quality.skinScattering === 'screen-space' && this.sceneTarget === null) {
2986
3091
  console.warn(
@@ -3623,10 +3728,13 @@ export class WebGL2Renderer implements RendererApi {
3623
3728
  updateSurfaceTexture(texture: SurfaceTexture, source: TexImageSource): void {
3624
3729
  if (this.contextLost) return;
3625
3730
  texture.update(this.gl, source);
3731
+ /* New pixels are a new copy for the overlay's slot: see `SurfaceTexture.flatLayer`. */
3732
+ if (texture === this.overlayMaps) this.bindOverlayMaps(texture);
3626
3733
  }
3627
3734
 
3628
3735
  disposeSurfaceTexture(texture: SurfaceTexture): void {
3629
3736
  if (this.contextLost) return;
3737
+ if (texture === this.overlayMaps) this.bindOverlayMaps(null);
3630
3738
  texture.dispose(this.gl);
3631
3739
  this.captureTargets.get(texture)?.dispose(this.gl);
3632
3740
  this.captureTargets.delete(texture);
@@ -4021,6 +4129,8 @@ export class WebGL2Renderer implements RendererApi {
4021
4129
  /* The first physical highlight asked for is when the lit programs start carrying one. */
4022
4130
  if (material?.physicalSpecular === true) this.askLit('PHYSICAL_SPECULAR');
4023
4131
  gl.uniform4fv(u['uModelParams'] ?? null, this.modelParams);
4132
+ /* The draw's overlay, which a program born mid-pass must wear too: see `setSurfaceOverlay`. */
4133
+ gl.uniform4fv(u['uOverlay'] ?? null, this.overlayFloats);
4024
4134
 
4025
4135
  /* The albedo's effects table, or the stand-in the shader reads as none. Before the early
4026
4136
  return, so a material with no albedo still leaves a complete texture on the unit. */
@@ -4120,6 +4230,76 @@ export class WebGL2Renderer implements RendererApi {
4120
4230
  this.writeFlatVector('uEnvironmentDials', this.environmentDials);
4121
4231
  }
4122
4232
 
4233
+ /**
4234
+ * The overlay the following draws wear — a rim of light, a dissolve, wrinkles by region — or null
4235
+ * for none. Pass state, as the material is: `bindMeshPass` puts it back to none. See
4236
+ * `surfaceOverlay.ts` for what it lays and `shaders/flat/overlay.ts` for how.
4237
+ *
4238
+ * **Refused, said once, where the lit stage has no room for it.** Fifteen vectors more than the
4239
+ * program this device fits: counted from the source as `fitFlatProgram` counts, and on a device
4240
+ * whose limit is the 256 an Adreno 740 reports, the eight-light build at 254 has none. The draws
4241
+ * then wear nothing, which is the picture a game without the feature draws. Its atlas is bound as
4242
+ * a 2D copy of its first layer, in the refraction copy's unit (`SurfaceTexture.flatLayer`); a
4243
+ * compressed atlas has no copy and is read as none, also said once.
4244
+ */
4245
+ setSurfaceOverlay(overlay: SurfaceOverlay | null): void {
4246
+ if (this.contextLost) return;
4247
+ let wanted = overlay;
4248
+ if (overlayLays(wanted) && !this.overlayFits()) {
4249
+ if (!this.warnedOverlayRoom) {
4250
+ this.warnedOverlayRoom = true;
4251
+ console.warn(
4252
+ 'Renderer: setSurfaceOverlay is refused on this device. The overlay takes fifteen ' +
4253
+ 'fragment uniform vectors and the lit shader already uses all but ' +
4254
+ `${this.overlayRoomLeft()} of the ${this.fragmentVectorLimit()} this GPU offers; ` +
4255
+ 'draws wear no overlay. A lower `maxLights` makes the room, or WebGPU has it.',
4256
+ );
4257
+ }
4258
+ wanted = null;
4259
+ }
4260
+ packSurfaceOverlay(wanted, this.overlayFloats);
4261
+ if (overlayLays(wanted)) this.askLit('SURFACE_OVERLAY');
4262
+ this.materials.dirty();
4263
+ this.writeFlatVector('uOverlay', this.overlayFloats);
4264
+ this.bindOverlayMaps((wanted?.maps ?? null) as SurfaceTexture | null);
4265
+ }
4266
+
4267
+ /** The overlay's atlas into the refraction copy's unit, as a 2D copy, or the empty texture. */
4268
+ private bindOverlayMaps(maps: SurfaceTexture | null): void {
4269
+ const { gl } = this;
4270
+ this.overlayMaps = maps;
4271
+ this.overlayTexture = maps === null ? null : maps.flatLayer(gl);
4272
+ if (maps !== null && this.overlayTexture === null && !this.warnedOverlayBlocks) {
4273
+ this.warnedOverlayBlocks = true;
4274
+ console.warn(
4275
+ "Renderer: a surface overlay's maps are a compressed texture, which WebGL2 cannot copy " +
4276
+ 'into the 2D image the lit stage reads them from. The overlay is drawn without its ' +
4277
+ 'images; upload the atlas from an image instead.',
4278
+ );
4279
+ }
4280
+ gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
4281
+ gl.bindTexture(gl.TEXTURE_2D, this.overlayTexture ?? this.emptyTexture2D);
4282
+ }
4283
+
4284
+ /** The most fragment uniform vectors this GPU offers a stage. */
4285
+ private fragmentVectorLimit(): number {
4286
+ return this.gl.getParameter(this.gl.MAX_FRAGMENT_UNIFORM_VECTORS) as number;
4287
+ }
4288
+
4289
+ /** What the lit shader at this renderer's budget leaves of the limit, with no overlay. */
4290
+ private overlayRoomLeft(): number {
4291
+ return (
4292
+ this.fragmentVectorLimit() - countUniformVectors(this.flatFragAt(this.lightBudget, false))
4293
+ );
4294
+ }
4295
+
4296
+ /** Whether the lit shader with the overlay compiled in fits this GPU: counted once. */
4297
+ private overlayFits(): boolean {
4298
+ this.overlayRoom ??=
4299
+ countUniformVectors(this.flatFragAt(this.lightBudget, true)) <= this.fragmentVectorLimit();
4300
+ return this.overlayRoom;
4301
+ }
4302
+
4123
4303
  /**
4124
4304
  * How bright the environment the surfaces drawn next reflect is. 1 is the default and identity.
4125
4305
  *
@@ -4555,8 +4735,11 @@ export class WebGL2Renderer implements RendererApi {
4555
4735
  if (coverage) gl.enable(gl.SAMPLE_ALPHA_TO_COVERAGE);
4556
4736
  if (ditheredBlend)
4557
4737
  gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(this.currentMaterial, true));
4738
+ const keeps = !blend && this.reflectionKeeps();
4739
+ if (keeps) this.reflectionTargets?.on();
4558
4740
  if (culling && cullsInstances(count, mesh.indexCount)) batch.drawCulled(gl, data, this.frustum);
4559
4741
  else batch.draw(gl, count);
4742
+ if (keeps) this.reflectionTargets?.off();
4560
4743
  if (coverage) gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
4561
4744
  if (ditheredBlend)
4562
4745
  gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(this.currentMaterial, false));
@@ -4627,7 +4810,8 @@ export class WebGL2Renderer implements RendererApi {
4627
4810
  const { gl } = this;
4628
4811
  gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
4629
4812
  gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
4630
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
4813
+ /* The overlay's atlas, where one is held: it shares the unit. See `setSurfaceOverlay`. */
4814
+ gl.bindTexture(gl.TEXTURE_2D, this.overlayTexture ?? this.emptyTexture2D);
4631
4815
  }
4632
4816
 
4633
4817
  disposeMesh(mesh: Mesh): void {
@@ -4872,7 +5056,7 @@ export class WebGL2Renderer implements RendererApi {
4872
5056
  */
4873
5057
  private gradeCode(): number {
4874
5058
  return this.sceneTarget === null || !this.quality.hdrScene
4875
- ? OUTPUT_TRANSFORM_CODE[this.quality.outputTransform]
5059
+ ? forwardTransformCode(this.quality.outputTransform)
4876
5060
  : 0;
4877
5061
  }
4878
5062
 
@@ -6116,7 +6300,7 @@ export class WebGL2Renderer implements RendererApi {
6116
6300
  * throws a sentence with the device's numbers in it rather than a bare driver string.
6117
6301
  */
6118
6302
  /** The lit fragment source this renderer's profile compiles, at a light budget. */
6119
- private flatFragAt(budget: LightBudget): string {
6303
+ private flatFragAt(budget: LightBudget, overlay = this.litOn.SURFACE_OVERLAY): string {
6120
6304
  return flatFrag({
6121
6305
  pointShadows: this.quality.pointShadows,
6122
6306
  directionalShadows: this.quality.directionalShadows,
@@ -6129,6 +6313,8 @@ export class WebGL2Renderer implements RendererApi {
6129
6313
  surfaceEffects: this.litOn.SURFACE_EFFECTS,
6130
6314
  driftLight: this.litOn.DRIFT_LIGHT,
6131
6315
  physicalSpecular: this.litOn.PHYSICAL_SPECULAR,
6316
+ surfaceOverlay: overlay,
6317
+ reflectionMaps: this.litOn.REFLECTION_MAPS,
6132
6318
  maxLights: budget.maxLights,
6133
6319
  maxAreaLights: budget.maxAreaLights,
6134
6320
  });
@@ -6954,11 +7140,22 @@ export class WebGL2Renderer implements RendererApi {
6954
7140
  * Set before `beginFrame` and held until changed, like `setSpeedRush`, which it sits
6955
7141
  * beside for the same reason: both describe how the finished image is presented rather
6956
7142
  * than anything in the world.
7143
+ *
7144
+ * `maxShare` is the longest smear, as a share of the frame (0.03 unless given, at most half): a
7145
+ * fast turn softens the frame rather than erasing it. **Each drawn object's own motion is
7146
+ * followed only under a reconstruction on WebGPU**, which draws the motion target it reads; here,
7147
+ * and on WebGPU without one, the camera's motion alone smears the frame, and a draw that names
7148
+ * where it was last frame is told so once.
6957
7149
  */
6958
- setCameraMotionBlur(scale: number): void {
7150
+ setCameraMotionBlur(scale: number, maxShare = MOTION_BLUR_MAX_UV): void {
6959
7151
  this.motionBlurScale = Math.min(Math.max(scale, 0), 1);
7152
+ this.motionBlurMax = clampMotionShare(maxShare);
6960
7153
  }
6961
7154
 
7155
+ /** The longest smear, as a share of the frame. See `setCameraMotionBlur`. */
7156
+ private motionBlurMax = MOTION_BLUR_MAX_UV;
7157
+ private warnedObjectBlur = false;
7158
+
6962
7159
  /**
6963
7160
  * The camera has cut: this frame is a new shot, not the next moment of the last one.
6964
7161
  *
@@ -7170,6 +7367,47 @@ export class WebGL2Renderer implements RendererApi {
7170
7367
  : this.quality.outputExposure;
7171
7368
  }
7172
7369
 
7370
+ /**
7371
+ * The `filmic` curve this and every later frame is graded through, until it is set again: a
7372
+ * grading volume's five numbers, solved once here into the constants the resolve reads. Ignored
7373
+ * unless `outputTransform` is `filmic`. A number out of range is clamped into it and said once,
7374
+ * since a frame loop must not throw. See `filmicCurve.ts`.
7375
+ */
7376
+ setFilmicCurve(curve: FilmicCurve): void {
7377
+ if (resolveFilmicCurve(curve, this.filmConstants) && !this.warnedFilmClamped) {
7378
+ this.warnedFilmClamped = true;
7379
+ console.warn(FILMIC_CLAMPED);
7380
+ }
7381
+ }
7382
+
7383
+ private warnedFilmClamped = false;
7384
+
7385
+ /**
7386
+ * The range the frame goes out in: `'high'` where the profile asked for `highDynamicRange`, the
7387
+ * frame holds light past white and the display shows it — on WebGPU — and `'standard'` otherwise.
7388
+ * **Always standard here**: this backend draws into a canvas of eight bits a channel, which has
7389
+ * nothing above white to show. Typed as the question rather than this backend's answer, because
7390
+ * the shared surface is derived from this class.
7391
+ */
7392
+ readonly displayRange: DisplayRange = 'standard';
7393
+
7394
+ /** Why it is that range, in words, for a game choosing its look and for a bug report. */
7395
+ get displayRangeReason(): string {
7396
+ return this.quality.highDynamicRange
7397
+ ? 'WebGL2 draws into an eight-bit canvas, which has no range above white'
7398
+ : 'highDynamicRange was not asked for';
7399
+ }
7400
+
7401
+ /**
7402
+ * How bright paper white and the display's peak are, in one unit, held until changed: their
7403
+ * ratio is how far above white the highlights may run where `displayRange` is `'high'`, and
7404
+ * nothing changes where it is standard, which is always here.
7405
+ */
7406
+ setDisplayLuminance(paperWhite: number, peak: number): void {
7407
+ void paperWhite;
7408
+ void peak;
7409
+ }
7410
+
7173
7411
  /**
7174
7412
  * Composite a flat colour over the finished frame — for a cut dipping to white or to black.
7175
7413
  *
@@ -7261,6 +7499,21 @@ export class WebGL2Renderer implements RendererApi {
7261
7499
  this.sceneTarget.setVignette(strength);
7262
7500
  }
7263
7501
 
7502
+ /**
7503
+ * The lens's colour fringe, held until changed: `intensity` is a percentage, 0 is none, and red
7504
+ * and green are pulled toward the centre by their wavelength's distance from blue, growing from
7505
+ * `start` — a share of the half-frame, 0 the centre — to the whole of it at the edge. Applied to
7506
+ * scene light at the composite's first read. Needs `screenEffects`, and says so once rather than
7507
+ * doing nothing. See `shaders/fringe.ts`.
7508
+ */
7509
+ setChromaticAberration(intensity: number, start = 0): void {
7510
+ if (this.sceneTarget === null) {
7511
+ this.warnLookWithoutComposite(intensity);
7512
+ return;
7513
+ }
7514
+ this.sceneTarget.setChromaticAberration(intensity, start);
7515
+ }
7516
+
7264
7517
  /**
7265
7518
  * Film grain, held until changed: an amplitude in display values (0 is none, 0.03 is a fine
7266
7519
  * grain) and this frame's seed. **The seed is the caller's**, because the engine takes time from
@@ -7451,6 +7704,8 @@ export class WebGL2Renderer implements RendererApi {
7451
7704
 
7452
7705
  beginFrame(clearColor: Vec3): void {
7453
7706
  if (this.contextLost) return;
7707
+ /* One frame of the jitter's sequence however many times the frame binds its camera. */
7708
+ this.temporalHistory.nextFrame();
7454
7709
  this.skinScattered = false;
7455
7710
  this.skinScatter?.reset();
7456
7711
  if (this.litAsked) {
@@ -7520,6 +7775,8 @@ export class WebGL2Renderer implements RendererApi {
7520
7775
  }
7521
7776
 
7522
7777
  this.sceneTarget?.begin(this.canvas.width, this.canvas.height);
7778
+ /* The materials' maps on the scene's framebuffer, cleared and disabled: `reflectionKeeps`. */
7779
+ this.reflectionTargets?.begin(this.canvas.width, this.canvas.height);
7523
7780
  gl.clearColor(clearColor[0], clearColor[1], clearColor[2], 1);
7524
7781
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
7525
7782
  }
@@ -7576,7 +7833,7 @@ export class WebGL2Renderer implements RendererApi {
7576
7833
  motion = {
7577
7834
  reprojection: this.reprojection as Float32Array,
7578
7835
  strength: blur,
7579
- max: MOTION_BLUR_MAX_UV,
7836
+ max: this.motionBlurMax,
7580
7837
  };
7581
7838
  }
7582
7839
  }
@@ -7668,7 +7925,11 @@ export class WebGL2Renderer implements RendererApi {
7668
7925
 
7669
7926
  /* The grade travels to the resolve only where the mesh pass gave it up. See `bindMeshPass`. */
7670
7927
  const lateGrade = this.quality.hdrScene
7671
- ? { transform: OUTPUT_TRANSFORM_CODE[this.quality.outputTransform], exposure: this.exposure }
7928
+ ? {
7929
+ transform: OUTPUT_TRANSFORM_CODE[this.quality.outputTransform] ?? 0,
7930
+ exposure: this.exposure,
7931
+ film: this.filmConstants,
7932
+ }
7672
7933
  : { transform: 0, exposure: 1 };
7673
7934
  /*
7674
7935
  * Depth of field, on the same ceiling-and-dial split as bloom above: `depthOfField` decides
@@ -7744,6 +8005,49 @@ export class WebGL2Renderer implements RendererApi {
7744
8005
  * drawn over a reflective floor rather than under it, and before the post chain so bloom and
7745
8006
  * the grade see a reflection exactly as they would see any other part of the picture.
7746
8007
  */
8008
+ /*
8009
+ * **The frame's materials' reflections first**, on the same terms: every pixel the opaque lit
8010
+ * draws left a reflection for, marched and swapped for the probe's share. The maps leave the
8011
+ * scene's framebuffer before the resolve draws into it while sampling them.
8012
+ */
8013
+ const reflectionTargets = this.reflectionTargets;
8014
+ const probeMap = reflectionTargets?.probeTexture ?? null;
8015
+ const tintMap = reflectionTargets?.tintTexture ?? null;
8016
+ if (
8017
+ reflectionTargets !== null &&
8018
+ this.frameReflections !== null &&
8019
+ probeMap !== null &&
8020
+ tintMap !== null &&
8021
+ this.frameViewProj !== null &&
8022
+ this.sceneTarget !== null
8023
+ ) {
8024
+ const scene = this.sceneTarget.colorAttachment();
8025
+ const depth = this.sceneTarget.snapshotDepth(true);
8026
+ if (scene !== null && depth !== null) {
8027
+ this.ssr ??= new SsrPass(this.gl, this.sceneTarget.keepsRange);
8028
+ this.ssr.traceMaterials(
8029
+ scene,
8030
+ depth,
8031
+ probeMap,
8032
+ tintMap,
8033
+ this.frameViewProj,
8034
+ this.frameEye,
8035
+ this.canvas.width,
8036
+ this.canvas.height,
8037
+ REFLECTION_EDGE_FADE,
8038
+ this.frameReflections,
8039
+ );
8040
+ this.sceneTarget.bind();
8041
+ reflectionTargets.detach();
8042
+ this.ssr.resolveMaterials(
8043
+ probeMap,
8044
+ tintMap,
8045
+ this.canvas.width,
8046
+ this.canvas.height,
8047
+ this.frameReflections,
8048
+ );
8049
+ }
8050
+ }
7747
8051
  if (this.reflectionQueue.length > 0 && this.frameViewProj !== null) {
7748
8052
  const scene = this.sceneTarget?.colorAttachment() ?? null;
7749
8053
  const depth = this.sceneTarget?.snapshotDepth(true) ?? null;
@@ -8127,6 +8431,10 @@ export class WebGL2Renderer implements RendererApi {
8127
8431
  this.materialHasMaps = false;
8128
8432
  this.currentMaterial = null;
8129
8433
  this.currentSurfaceTexture = null;
8434
+ /* Nor an overlay: the atlas's unit is given the empty texture with the refraction copy's. */
8435
+ this.overlayFloats.fill(0);
8436
+ this.overlayMaps = null;
8437
+ this.overlayTexture = null;
8130
8438
  /* Nor an ambient of its own: the frame's, until a draw asks. See `setAmbientSH`. */
8131
8439
  this.ambientSH.fill(0);
8132
8440
  /* No reflection and an environment at its own brightness; the emission the world asks for. */
@@ -8283,8 +8591,10 @@ export class WebGL2Renderer implements RendererApi {
8283
8591
  * unless a draw sets the strength above zero.
8284
8592
  */
8285
8593
  gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
8286
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
8594
+ gl.bindTexture(gl.TEXTURE_2D, this.overlayTexture ?? this.emptyTexture2D);
8287
8595
  gl.uniform1i(u['uRefractScene'] ?? null, REFRACT_SCENE_TEXTURE_UNIT);
8596
+ /* The draws' overlay, none at a pass's start, and its atlas above: see `setSurfaceOverlay`. */
8597
+ gl.uniform4fv(u['uOverlay'] ?? null, this.overlayFloats);
8288
8598
  /* Neither refracting nor glass, which is every draw that does not say otherwise. See
8289
8599
  `bindSeeThrough` for the four numbers in the one vector. */
8290
8600
  gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
@@ -8693,9 +9003,18 @@ export class WebGL2Renderer implements RendererApi {
8693
9003
  * here, so there is no motion target for this to be written into, and a mover passed here is
8694
9004
  * neither read nor advanced.
8695
9005
  */
8696
- _previousModel: ReadonlyMat4 | Mover | null = null,
9006
+ previousModel: ReadonlyMat4 | Mover | null = null,
8697
9007
  ): void {
8698
9008
  if (this.contextLost) return;
9009
+ /* A mover's own motion is not blurred here: see `setCameraMotionBlur`. Said once. */
9010
+ if (
9011
+ previousModel !== null &&
9012
+ !this.warnedObjectBlur &&
9013
+ this.quality.cameraMotionBlur * this.motionBlurScale > 0
9014
+ ) {
9015
+ this.warnedObjectBlur = true;
9016
+ console.warn(OBJECT_BLUR_UNKEPT);
9017
+ }
8699
9018
  /* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
8700
9019
  and `Mesh.indexCount`. */
8701
9020
  if (!mesh.complete || mesh.indexCount === 0) return;
@@ -8726,7 +9045,11 @@ export class WebGL2Renderer implements RendererApi {
8726
9045
  * light and its colour, each into the blur's own target.
8727
9046
  */
8728
9047
  const splits = this.skinSplits();
9048
+ /* An opaque lit draw into the frame writes its material's reflection too: see `reflectionKeeps`. */
9049
+ const keeps = this.reflectionKeeps();
9050
+ if (keeps) this.reflectionTargets?.on();
8729
9051
  this.drawMeshHalf(mesh, model, depthLayer, tint, splits ? 'scene' : 'whole');
9052
+ if (keeps) this.reflectionTargets?.off();
8730
9053
  if (splits && this.skinScatter !== null) {
8731
9054
  this.skinScatter.begin();
8732
9055
  this.drawMeshHalf(mesh, model, depthLayer, tint, 'diffuse');
@@ -8741,6 +9064,21 @@ export class WebGL2Renderer implements RendererApi {
8741
9064
  * skin yet, this is the frame's own picture — not a mirror's or a probe's, nor an overlay after
8742
9065
  * the frame — and the blur's target can be had.
8743
9066
  */
9067
+ /**
9068
+ * Whether a draw now is the frame's own, opaque and lit, and so writes the two maps beside its
9069
+ * colour: not a mirror's, a probe's or a capture's, whose framebuffers have no maps, and not after
9070
+ * the frame. See `reflectionSurfaceTargets.ts`.
9071
+ */
9072
+ private reflectionKeeps(): boolean {
9073
+ return (
9074
+ this.reflectionTargets !== null &&
9075
+ !this.reflectionPassActive &&
9076
+ !this.probePassActive &&
9077
+ !this.capturePassActive &&
9078
+ !this.framePresented
9079
+ );
9080
+ }
9081
+
8744
9082
  private skinSplits(): boolean {
8745
9083
  const skin = this.skinScatter;
8746
9084
  const scene = this.sceneTarget;
@@ -8769,10 +9107,16 @@ export class WebGL2Renderer implements RendererApi {
8769
9107
  const scene = this.sceneTarget;
8770
9108
  const projection = this.frameProjection;
8771
9109
  if (skin === null || scene === null || this.skinScattered) return;
9110
+ /*
9111
+ * **Not the frame's picture, so not the frame's moment**: a blended draw in a mirror, a probe
9112
+ * bake or a scene capture lands over none of the frame's skin. Latching there switched the
9113
+ * spread off for the frame, so a capture with a glowing sign in it left every face drawn after
9114
+ * it flat. `scripts/capture-check.mjs`.
9115
+ */
9116
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive) return;
8772
9117
  /* Latched whether or not skin was drawn: a skin drawn after this point is the whole surface. */
8773
9118
  this.skinScattered = true;
8774
9119
  if (!skin.pending) return;
8775
- if (this.reflectionPassActive || this.probePassActive || this.capturePassActive) return;
8776
9120
  if (projection === null) return;
8777
9121
  const depth = scene.snapshotDepth(true);
8778
9122
  mat4.invert(this.skinInverseProjection, projection);