@driftengine/core 4.8.6 → 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 (253) hide show
  1. package/CHANGELOG.json +132 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +7 -2
  4. package/dist/index.js +8 -1
  5. package/dist/render/anisotropicLobe.d.ts +10 -0
  6. package/dist/render/anisotropicLobe.js +19 -0
  7. package/dist/render/areaLights.d.ts +35 -0
  8. package/dist/render/areaLights.js +35 -0
  9. package/dist/render/atmosphere.d.ts +13 -0
  10. package/dist/render/atmosphere.js +9 -1
  11. package/dist/render/backend/api.d.ts +41 -3
  12. package/dist/render/backend/createRenderer.d.ts +6 -0
  13. package/dist/render/backend/createRenderer.js +4 -1
  14. package/dist/render/backend/webgl2/renderer.d.ts +140 -16
  15. package/dist/render/backend/webgl2/renderer.js +436 -44
  16. package/dist/render/backend/webgl2/sceneCaptureTarget.d.ts +29 -0
  17. package/dist/render/backend/webgl2/sceneCaptureTarget.js +96 -0
  18. package/dist/render/backend/webgpu/device.d.ts +7 -1
  19. package/dist/render/backend/webgpu/device.js +31 -3
  20. package/dist/render/backend/webgpu/flatPass.d.ts +16 -1
  21. package/dist/render/backend/webgpu/flatPass.js +58 -26
  22. package/dist/render/backend/webgpu/particlePass.js +2 -1
  23. package/dist/render/backend/webgpu/postPass.d.ts +51 -9
  24. package/dist/render/backend/webgpu/postPass.js +16 -2
  25. package/dist/render/backend/webgpu/reflectionSurfacePass.d.ts +30 -0
  26. package/dist/render/backend/webgpu/reflectionSurfacePass.js +100 -0
  27. package/dist/render/backend/webgpu/renderer.d.ts +166 -3
  28. package/dist/render/backend/webgpu/renderer.js +738 -83
  29. package/dist/render/backend/webgpu/sceneCaptureTarget.d.ts +24 -0
  30. package/dist/render/backend/webgpu/sceneCaptureTarget.js +59 -0
  31. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  32. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
  33. package/dist/render/backend/webgpu/surfaceTexturePass.js +34 -8
  34. package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
  35. package/dist/render/clusteredLights.d.ts +23 -5
  36. package/dist/render/clusteredLights.js +35 -8
  37. package/dist/render/displayRange.d.ts +31 -0
  38. package/dist/render/displayRange.js +47 -0
  39. package/dist/render/driftLight/bake.d.ts +2 -0
  40. package/dist/render/driftLight/bake.js +1 -1
  41. package/dist/render/driftLight/falloff.d.ts +4 -2
  42. package/dist/render/driftLight/falloff.js +8 -2
  43. package/dist/render/filmicCurve.d.ts +74 -0
  44. package/dist/render/filmicCurve.js +203 -0
  45. package/dist/render/fog.d.ts +4 -1
  46. package/dist/render/fog.js +12 -2
  47. package/dist/render/frame/resources.d.ts +2 -2
  48. package/dist/render/frame/resources.js +6 -0
  49. package/dist/render/frameReflections.d.ts +40 -0
  50. package/dist/render/frameReflections.js +48 -0
  51. package/dist/render/gi/bounceLights.d.ts +3 -2
  52. package/dist/render/gi/bounceLights.js +8 -3
  53. package/dist/render/instanceCull.js +5 -0
  54. package/dist/render/instances.d.ts +19 -5
  55. package/dist/render/instances.js +9 -3
  56. package/dist/render/lightBudget.d.ts +12 -1
  57. package/dist/render/lightBudget.js +23 -3
  58. package/dist/render/lightmap.d.ts +23 -9
  59. package/dist/render/lightmap.js +40 -18
  60. package/dist/render/particlePool.d.ts +21 -0
  61. package/dist/render/particlePool.js +7 -0
  62. package/dist/render/pass.d.ts +49 -0
  63. package/dist/render/pointLightSelection.d.ts +13 -0
  64. package/dist/render/pointLightSelection.js +3 -1
  65. package/dist/render/reflectionSurfaceTargets.d.ts +28 -0
  66. package/dist/render/reflectionSurfaceTargets.js +99 -0
  67. package/dist/render/renderQuality.d.ts +47 -15
  68. package/dist/render/renderQuality.js +4 -0
  69. package/dist/render/rendererHarness.js +3 -0
  70. package/dist/render/rgb9e5.d.ts +22 -0
  71. package/dist/render/rgb9e5.js +47 -0
  72. package/dist/render/sceneCapture.d.ts +29 -0
  73. package/dist/render/sceneCapture.js +36 -0
  74. package/dist/render/sceneTarget.d.ts +5 -0
  75. package/dist/render/sceneTarget.js +24 -1
  76. package/dist/render/screenSpaceReflection.d.ts +32 -1
  77. package/dist/render/screenSpaceReflection.js +14 -0
  78. package/dist/render/shaders/arcane.d.ts +1 -1
  79. package/dist/render/shaders/bolt.d.ts +1 -1
  80. package/dist/render/shaders/bolt.js +7 -1
  81. package/dist/render/shaders/caustics.d.ts +1 -1
  82. package/dist/render/shaders/depth.js +11 -0
  83. package/dist/render/shaders/film.d.ts +1 -1
  84. package/dist/render/shaders/filmLook.js +2 -1
  85. package/dist/render/shaders/fire.d.ts +1 -1
  86. package/dist/render/shaders/flat/anisotropic.d.ts +1 -1
  87. package/dist/render/shaders/flat/anisotropic.js +37 -3
  88. package/dist/render/shaders/flat/areaLight.d.ts +1 -1
  89. package/dist/render/shaders/flat/areaLight.js +46 -0
  90. package/dist/render/shaders/flat/glassTint.d.ts +3 -2
  91. package/dist/render/shaders/flat/glassTint.js +5 -1
  92. package/dist/render/shaders/flat/index.d.ts +15 -0
  93. package/dist/render/shaders/flat/index.js +22 -2
  94. package/dist/render/shaders/flat/lobes.d.ts +6 -0
  95. package/dist/render/shaders/flat/lobes.js +49 -43
  96. package/dist/render/shaders/flat/main.d.ts +1 -1
  97. package/dist/render/shaders/flat/main.js +85 -22
  98. package/dist/render/shaders/flat/models.d.ts +5 -2
  99. package/dist/render/shaders/flat/models.js +100 -16
  100. package/dist/render/shaders/flat/overlay.d.ts +16 -0
  101. package/dist/render/shaders/flat/overlay.js +117 -0
  102. package/dist/render/shaders/flat/pointShadow.js +1 -1
  103. package/dist/render/shaders/flat/preamble.js +10 -3
  104. package/dist/render/shaders/flat/reflectionSurface.d.ts +21 -0
  105. package/dist/render/shaders/flat/reflectionSurface.js +61 -0
  106. package/dist/render/shaders/fog.d.ts +1 -1
  107. package/dist/render/shaders/fog.js +9 -2
  108. package/dist/render/shaders/fringe.d.ts +29 -0
  109. package/dist/render/shaders/fringe.js +55 -0
  110. package/dist/render/shaders/generated/arcane.wgsl.d.ts +1 -1
  111. package/dist/render/shaders/generated/arcane.wgsl.js +1 -1
  112. package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
  113. package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
  114. package/dist/render/shaders/generated/caustics.wgsl.d.ts +1 -1
  115. package/dist/render/shaders/generated/caustics.wgsl.js +1 -1
  116. package/dist/render/shaders/generated/depth.wgsl.d.ts +4 -4
  117. package/dist/render/shaders/generated/depth.wgsl.js +4 -4
  118. package/dist/render/shaders/generated/film.wgsl.d.ts +1 -1
  119. package/dist/render/shaders/generated/film.wgsl.js +1 -1
  120. package/dist/render/shaders/generated/fire.wgsl.d.ts +1 -1
  121. package/dist/render/shaders/generated/fire.wgsl.js +1 -1
  122. package/dist/render/shaders/generated/flat.wgsl.d.ts +192 -16
  123. package/dist/render/shaders/generated/flat.wgsl.js +230 -54
  124. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  125. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  126. package/dist/render/shaders/generated/particle.wgsl.d.ts +4 -4
  127. package/dist/render/shaders/generated/particle.wgsl.js +4 -4
  128. package/dist/render/shaders/generated/rush.wgsl.d.ts +56 -26
  129. package/dist/render/shaders/generated/rush.wgsl.js +56 -26
  130. package/dist/render/shaders/generated/scatter.wgsl.d.ts +1 -1
  131. package/dist/render/shaders/generated/scatter.wgsl.js +1 -1
  132. package/dist/render/shaders/generated/smoke.wgsl.d.ts +1 -1
  133. package/dist/render/shaders/generated/smoke.wgsl.js +1 -1
  134. package/dist/render/shaders/generated/ssrResolve.wgsl.d.ts +29 -0
  135. package/dist/render/shaders/generated/ssrResolve.wgsl.js +29 -0
  136. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +32 -7
  137. package/dist/render/shaders/generated/ssrTrace.wgsl.js +32 -7
  138. package/dist/render/shaders/generated/water.wgsl.d.ts +23 -16
  139. package/dist/render/shaders/generated/water.wgsl.js +23 -16
  140. package/dist/render/shaders/gi/probeBake.wgsl.js +9 -5
  141. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +2 -2
  142. package/dist/render/shaders/gpudriven/fog.wgsl.js +11 -3
  143. package/dist/render/shaders/line.d.ts +1 -1
  144. package/dist/render/shaders/particle.d.ts +3 -3
  145. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  146. package/dist/render/shaders/recon/motion.wgsl.js +13 -2
  147. package/dist/render/shaders/rush.js +54 -11
  148. package/dist/render/shaders/scatter.d.ts +1 -1
  149. package/dist/render/shaders/smoke.d.ts +1 -1
  150. package/dist/render/shaders/ssrResolve.d.ts +17 -0
  151. package/dist/render/shaders/ssrResolve.js +51 -0
  152. package/dist/render/shaders/ssrTrace.js +46 -1
  153. package/dist/render/shaders/water.d.ts +1 -1
  154. package/dist/render/shaders/water.js +12 -3
  155. package/dist/render/skinScatter.d.ts +3 -1
  156. package/dist/render/skinScatter.js +3 -1
  157. package/dist/render/ssrPass.d.ts +16 -0
  158. package/dist/render/ssrPass.js +107 -1
  159. package/dist/render/surfaceModel.d.ts +29 -8
  160. package/dist/render/surfaceModel.js +33 -9
  161. package/dist/render/surfaceOverlay.d.ts +132 -0
  162. package/dist/render/surfaceOverlay.js +85 -0
  163. package/dist/render/surfaceTexture.d.ts +44 -1
  164. package/dist/render/surfaceTexture.js +96 -5
  165. package/dist/render/temporalAa.d.ts +14 -0
  166. package/dist/render/temporalAa.js +20 -0
  167. package/dist/render/vertexDefaults.d.ts +17 -0
  168. package/dist/render/vertexDefaults.js +24 -0
  169. package/package.json +6 -6
  170. package/src/index.ts +22 -2
  171. package/src/render/anisotropicLobe.ts +29 -0
  172. package/src/render/areaLights.ts +58 -0
  173. package/src/render/atmosphere.ts +22 -1
  174. package/src/render/backend/api.ts +58 -2
  175. package/src/render/backend/createRenderer.ts +18 -1
  176. package/src/render/backend/webgl2/renderer.ts +548 -50
  177. package/src/render/backend/webgl2/sceneCaptureTarget.ts +130 -0
  178. package/src/render/backend/webgpu/device.ts +42 -3
  179. package/src/render/backend/webgpu/flatPass.ts +63 -26
  180. package/src/render/backend/webgpu/particlePass.ts +2 -1
  181. package/src/render/backend/webgpu/postPass.ts +18 -0
  182. package/src/render/backend/webgpu/reflectionSurfacePass.ts +114 -0
  183. package/src/render/backend/webgpu/renderer.ts +927 -100
  184. package/src/render/backend/webgpu/sceneCaptureTarget.ts +63 -0
  185. package/src/render/backend/webgpu/surfaceTexturePass.ts +44 -14
  186. package/src/render/clusteredLights.ts +41 -8
  187. package/src/render/displayRange.ts +61 -0
  188. package/src/render/driftLight/bake.ts +3 -1
  189. package/src/render/driftLight/falloff.ts +13 -2
  190. package/src/render/filmicCurve.ts +229 -0
  191. package/src/render/fog.ts +19 -3
  192. package/src/render/frame/resources.ts +6 -0
  193. package/src/render/frameReflections.ts +72 -0
  194. package/src/render/gi/bounceLights.ts +12 -3
  195. package/src/render/instanceCull.ts +4 -0
  196. package/src/render/instances.ts +26 -6
  197. package/src/render/lightBudget.ts +40 -2
  198. package/src/render/lightmap.ts +64 -23
  199. package/src/render/particlePool.ts +23 -0
  200. package/src/render/pass.ts +53 -1
  201. package/src/render/pointLightSelection.ts +20 -1
  202. package/src/render/reflectionSurfaceTargets.ts +105 -0
  203. package/src/render/renderQuality.ts +52 -15
  204. package/src/render/rendererHarness.ts +3 -0
  205. package/src/render/rgb9e5.ts +52 -0
  206. package/src/render/sceneCapture.ts +56 -0
  207. package/src/render/sceneTarget.ts +30 -2
  208. package/src/render/screenSpaceReflection.ts +38 -1
  209. package/src/render/shaders/bolt.ts +7 -1
  210. package/src/render/shaders/depth.ts +11 -0
  211. package/src/render/shaders/filmLook.ts +2 -1
  212. package/src/render/shaders/flat/anisotropic.ts +37 -3
  213. package/src/render/shaders/flat/areaLight.ts +46 -0
  214. package/src/render/shaders/flat/glassTint.ts +13 -2
  215. package/src/render/shaders/flat/index.ts +37 -2
  216. package/src/render/shaders/flat/lobes.ts +50 -43
  217. package/src/render/shaders/flat/main.ts +85 -22
  218. package/src/render/shaders/flat/models.ts +100 -16
  219. package/src/render/shaders/flat/overlay.ts +117 -0
  220. package/src/render/shaders/flat/pointShadow.ts +1 -1
  221. package/src/render/shaders/flat/preamble.ts +10 -3
  222. package/src/render/shaders/flat/reflectionSurface.ts +61 -0
  223. package/src/render/shaders/fog.ts +9 -2
  224. package/src/render/shaders/fringe.ts +59 -0
  225. package/src/render/shaders/generated/arcane.wgsl.ts +1 -1
  226. package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
  227. package/src/render/shaders/generated/caustics.wgsl.ts +1 -1
  228. package/src/render/shaders/generated/depth.wgsl.ts +4 -4
  229. package/src/render/shaders/generated/film.wgsl.ts +1 -1
  230. package/src/render/shaders/generated/fire.wgsl.ts +1 -1
  231. package/src/render/shaders/generated/flat.wgsl.ts +230 -54
  232. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  233. package/src/render/shaders/generated/particle.wgsl.ts +4 -4
  234. package/src/render/shaders/generated/rush.wgsl.ts +56 -26
  235. package/src/render/shaders/generated/scatter.wgsl.ts +1 -1
  236. package/src/render/shaders/generated/smoke.wgsl.ts +1 -1
  237. package/src/render/shaders/generated/ssrResolve.wgsl.ts +30 -0
  238. package/src/render/shaders/generated/ssrTrace.wgsl.ts +32 -7
  239. package/src/render/shaders/generated/water.wgsl.ts +23 -16
  240. package/src/render/shaders/gi/probeBake.wgsl.ts +9 -5
  241. package/src/render/shaders/gpudriven/fog.wgsl.ts +11 -3
  242. package/src/render/shaders/recon/motion.wgsl.ts +13 -2
  243. package/src/render/shaders/rush.ts +54 -11
  244. package/src/render/shaders/ssrResolve.ts +52 -0
  245. package/src/render/shaders/ssrTrace.ts +46 -1
  246. package/src/render/shaders/water.ts +12 -3
  247. package/src/render/skinScatter.ts +3 -1
  248. package/src/render/ssrPass.ts +139 -1
  249. package/src/render/surfaceModel.ts +47 -10
  250. package/src/render/surfaceOverlay.ts +195 -0
  251. package/src/render/surfaceTexture.ts +136 -9
  252. package/src/render/temporalAa.ts +20 -0
  253. 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';
@@ -136,7 +154,12 @@ import {
136
154
  MORPH_DELTA_TEXTURE_UNIT,
137
155
  SKIN_PALETTE_TEXTURE_UNIT,
138
156
  } from '../../lightBudget.ts';
139
- import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel } from '../../surfaceModel.ts';
157
+ import {
158
+ MODEL_PARAM_FLOATS,
159
+ eyeAxisInWorld,
160
+ packModel,
161
+ skinProfileDistance,
162
+ } from '../../surfaceModel.ts';
140
163
  import type { SurfaceModelKind } from '../../surfaceModel.ts';
141
164
  import { lightmapRegionsBound, modelBound, skinHalfBound } from '../../shaders/flat/models.ts';
142
165
  import {
@@ -251,6 +274,8 @@ import type { IncrementalMesh } from '../../mesh.ts';
251
274
  import type { MeshData } from '../../mesh.ts';
252
275
  import type { MeshOptions } from '../api.ts';
253
276
  import { SurfaceTexture } from '../../surfaceTexture.ts';
277
+ import { sceneCaptureTexels } from '../../sceneCapture.ts';
278
+ import { GlSceneCaptureTarget } from './sceneCaptureTarget.ts';
254
279
  import { glCompressedFormats } from '../../glCompressed.ts';
255
280
  import type { CompressedTextureFormat, SurfaceSource } from '../../compressedSource.ts';
256
281
  import type { SurfaceMaterial, SurfaceTextureOptions } from '../../surfaceTexture.ts';
@@ -274,7 +299,7 @@ import { PlanarReflection } from '../../planarReflection.ts';
274
299
  import { InstancedMesh } from '../../instancedMesh.ts';
275
300
  import { ParticleBatch } from '../../particleBatch.ts';
276
301
  import type { ParticleBatchOptions } from '../../particleBatch.ts';
277
- import type { ParticleInstances } from '../../particlePool.ts';
302
+ import type { DeviceParticles, ParticleInstances } from '../../particlePool.ts';
278
303
  import { BoltBatch } from '../../boltBatch.ts';
279
304
  import type { BoltSegments } from '../../boltPool.ts';
280
305
  import { LineBatch } from '../../lineBatch.ts';
@@ -286,8 +311,22 @@ import { TextRenderer } from '../../textRenderer.ts';
286
311
  import { SdfTextRenderer } from '../../sdfTextRenderer.ts';
287
312
  import type { SdfFont } from '../../sdfFont.ts';
288
313
  import type { SdfTextStyle } from '../../sdfTextLayout.ts';
289
- import { MOTION_BLUR_MAX_UV, RUSH_REACH_UV } from '../../vertexDefaults.ts';
290
- 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';
291
330
  import type { RenderBackend } from '../api.ts';
292
331
  import { deviceSnappedCellSize } from '../../textLayout.ts';
293
332
  import type { TextStyle } from '../../textLayout.ts';
@@ -435,6 +474,11 @@ export interface Environment extends Atmosphere {
435
474
  lightDirections: Float32Array;
436
475
  /** Two per active light: the cosine of the inner cone angle, then of the outer. */
437
476
  lightConeCos: Float32Array;
477
+ /**
478
+ * One per active light: its own falloff exponent, or 0 for the frame's. Optional: absent is 0 for
479
+ * every light. See `PointLightSource.falloffExponent`.
480
+ */
481
+ lightFalloffExponents?: Float32Array;
438
482
  /** One per active light: a row of the photometric atlas, or −1 for none. */
439
483
  lightIesProfiles: Float32Array;
440
484
  /**
@@ -1336,6 +1380,10 @@ export class WebGL2Renderer implements RendererApi {
1336
1380
  private readonly reflectionQueue = new ReflectionQueue();
1337
1381
  /** Built on the first frame that submits a surface, and never for a scene with none. */
1338
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;
1339
1387
  /** Said once where a reflection cannot be traced at all. */
1340
1388
  private reflectionsRefused = false;
1341
1389
  /** Where the frame's camera stands, which is what gives a reconstructed normal its sign. */
@@ -1418,6 +1466,8 @@ export class WebGL2Renderer implements RendererApi {
1418
1466
  * exactly as it asked to be and nothing about it changes.
1419
1467
  */
1420
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);
1421
1471
  /**
1422
1472
  * This frame's veil colour, and how much of it to composite. See `setFrameVeil`.
1423
1473
  *
@@ -1619,8 +1669,26 @@ export class WebGL2Renderer implements RendererApi {
1619
1669
  private warnedNoPointShadowArray = false;
1620
1670
  /** Said once, for the retired ambient dial. */
1621
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;
1622
1681
  /** Whether a probe is being baked, so the passes that would recurse into one can tell. */
1623
1682
  private probePassActive = false;
1683
+ /**
1684
+ * Whether a scene capture is being drawn (`captureScene`): an off-screen pass like a bake, so the
1685
+ * frame's own view, its refraction copy and skin's spread are left alone, but lit as the frame is.
1686
+ */
1687
+ private capturePassActive = false;
1688
+ /** Each capture's framebuffer, beside the texture a material binds. See `createSceneCapture`. */
1689
+ private readonly captureTargets = new WeakMap<SurfaceTexture, GlSceneCaptureTarget>();
1690
+ /** Said once, where a driver refuses to draw into a capture. */
1691
+ private warnedCapture = false;
1624
1692
  /** Whether the bake under way lights its faces by the grid. `ProbeBakeOptions.bounce`. */
1625
1693
  private probeBounce = false;
1626
1694
  /** The clip-control extension, or null where this context does not offer it. */
@@ -1692,12 +1760,14 @@ export class WebGL2Renderer implements RendererApi {
1692
1760
  * the camera's matrix unchanged and multiplies nothing.
1693
1761
  */
1694
1762
  private viewProjFor(camera: { readonly viewProjection: ReadonlyMat4 }): ReadonlyMat4 {
1695
- const scene = this.probePassActive
1696
- ? (cubeFaceProjection(
1697
- this.cubeFaceViewProj,
1698
- this.sceneMatrix(camera.viewProjection) as unknown as ArrayLike<number>,
1699
- ) as unknown as ReadonlyMat4)
1700
- : this.sceneMatrix(camera.viewProjection);
1763
+ /* A capture's rows are flipped as a probe face's are, so it reads as an image does. */
1764
+ const scene =
1765
+ this.probePassActive || this.capturePassActive
1766
+ ? (cubeFaceProjection(
1767
+ this.cubeFaceViewProj,
1768
+ this.sceneMatrix(camera.viewProjection) as unknown as ArrayLike<number>,
1769
+ ) as unknown as ReadonlyMat4)
1770
+ : this.sceneMatrix(camera.viewProjection);
1701
1771
  /*
1702
1772
  * **The jitter goes on here and nowhere else.** This is the one funnel every geometry upload
1703
1773
  * passes through, so one line covers the mesh passes, the sky, the water and the effects
@@ -2275,6 +2345,8 @@ export class WebGL2Renderer implements RendererApi {
2275
2345
  gl: WebGL2RenderingContext;
2276
2346
  outputTransform: number;
2277
2347
  outputExposure: number;
2348
+ sceneColor: WebGLTexture | null;
2349
+ sceneDepth: WebGLTexture | null;
2278
2350
  } | null = null;
2279
2351
  private passDevice: PassDevice | null = null;
2280
2352
 
@@ -2286,6 +2358,9 @@ export class WebGL2Renderer implements RendererApi {
2286
2358
  * call. `init` runs now rather than at the first frame, because building a pipeline in the
2287
2359
  * frame loop is the allocation the house rules are about.
2288
2360
  */
2361
+ /** Each registered pass's declared reads, as a mask, resolved once at registration. */
2362
+ private readonly passReads = new Map<PassHandle, number>();
2363
+
2289
2364
  registerPass(definition: PassDefinition): PassHandle {
2290
2365
  const handle = registerIn(this.passes, definition);
2291
2366
  /*
@@ -2304,6 +2379,8 @@ export class WebGL2Renderer implements RendererApi {
2304
2379
  depthCorrection: correction,
2305
2380
  };
2306
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 ?? [])));
2307
2384
  definition.init?.(this.passDevice);
2308
2385
  return handle;
2309
2386
  }
@@ -2355,10 +2432,28 @@ export class WebGL2Renderer implements RendererApi {
2355
2432
  gl: this.gl,
2356
2433
  outputTransform: 0,
2357
2434
  outputExposure: 1,
2435
+ sceneColor: null,
2436
+ sceneDepth: null,
2358
2437
  };
2359
2438
  const context = this.passContext;
2360
2439
  context.outputTransform = this.gradeCode();
2361
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;
2362
2457
  definition.draw(context as PassContext);
2363
2458
  }
2364
2459
 
@@ -2366,6 +2461,7 @@ export class WebGL2Renderer implements RendererApi {
2366
2461
  unregisterPass(handle: PassHandle): void {
2367
2462
  const definition = unregisterIn(this.passes, handle);
2368
2463
  if (definition?.prepare !== undefined) this.preparingPasses--;
2464
+ this.passReads.delete(handle);
2369
2465
  if (definition !== undefined && this.passDevice !== null) definition.dispose?.(this.passDevice);
2370
2466
  }
2371
2467
 
@@ -2536,10 +2632,11 @@ export class WebGL2Renderer implements RendererApi {
2536
2632
  gl.bindTexture(gl.TEXTURE_2D, this.clusterTexture);
2537
2633
  gl.uniform1i(u['uClusterTable'] ?? null, CLUSTER_TABLE_TEXTURE_UNIT);
2538
2634
  gl.uniform1i(u['uClustered'] ?? null, on ? 1 : 0);
2539
- if (!on) return;
2540
2635
 
2541
2636
  /*
2542
- * 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`).
2543
2640
  *
2544
2641
  * A perspective matrix carries `1 / tan(fovY/2)` at [5] and that over the aspect at [0], and
2545
2642
  * the near and far planes in [14] and [10]. Deriving them here means the binner and the shader
@@ -2557,6 +2654,7 @@ export class WebGL2Renderer implements RendererApi {
2557
2654
  this.clusterFrustum[3] = aspect;
2558
2655
  gl.uniform4fv(u['uClusterFrustum'] ?? null, this.clusterFrustum);
2559
2656
  gl.uniformMatrix4fv(u['uView'] ?? null, false, camera.view);
2657
+ if (!on) return;
2560
2658
 
2561
2659
  this.clusterTable ??= createClusterTable();
2562
2660
  /* The rectangles past the fixed arm's go into the table. See `ClusterLightSet.areas`. */
@@ -2660,6 +2758,13 @@ export class WebGL2Renderer implements RendererApi {
2660
2758
  this.quality = resolveRenderQuality(qualityOptions);
2661
2759
  /* The grade this world asked for, until a frame says otherwise. See `setOutputExposure`. */
2662
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
+ }
2663
2768
  /*
2664
2769
  * Multisampling, but only when anything can use it.
2665
2770
  *
@@ -2958,6 +3063,29 @@ export class WebGL2Renderer implements RendererApi {
2958
3063
  this.quality.discardResolvedAttachments,
2959
3064
  )
2960
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');
2961
3089
  /* Screen-space skin needs the composite to add into; without one it is pre-integrated, said once. */
2962
3090
  if (this.quality.skinScattering === 'screen-space' && this.sceneTarget === null) {
2963
3091
  console.warn(
@@ -3499,6 +3627,92 @@ export class WebGL2Renderer implements RendererApi {
3499
3627
  return new SurfaceTexture(this.gl, lightmapTexels(page), { wrap: 'clamp', mipmap: false });
3500
3628
  }
3501
3629
 
3630
+ /**
3631
+ * A texture the scene can be drawn into, for a material to show. See `RendererApi` and
3632
+ * `sceneCapture.ts`. Half floats where the frame keeps range, as a probe does.
3633
+ */
3634
+ createSceneCapture(width: number, height: number): SurfaceTexture {
3635
+ const float = this.quality.hdrScene && this.gl.getExtension('EXT_color_buffer_float') !== null;
3636
+ const texture = new SurfaceTexture(this.gl, sceneCaptureTexels(width, height, float), {
3637
+ wrap: 'clamp',
3638
+ mipmap: false,
3639
+ });
3640
+ this.captureTargets.set(
3641
+ texture,
3642
+ new GlSceneCaptureTarget(
3643
+ this.gl,
3644
+ texture,
3645
+ width,
3646
+ height,
3647
+ this.sceneTarget?.sampleCount ?? 1,
3648
+ float,
3649
+ ),
3650
+ );
3651
+ return texture;
3652
+ }
3653
+
3654
+ /**
3655
+ * Draw the scene into a capture from `camera`: `draw` is handed the camera, its matrices updated
3656
+ * for the capture's shape, and submits the scene as it would to the screen. See `RendererApi`.
3657
+ *
3658
+ * **Radiance, as a probe bake stores it**: the output transform and exposure are held off for the
3659
+ * pass, so the frame grades a screen showing it once. No temporal jitter, which a capture has no
3660
+ * resolve to undo. Returns whether it drew: false for a texture that is not a capture, or one the
3661
+ * driver will not draw into.
3662
+ */
3663
+ captureScene(
3664
+ capture: SurfaceTexture,
3665
+ camera: Camera,
3666
+ clearColor: Vec3,
3667
+ draw: (camera: Camera) => void,
3668
+ ): boolean {
3669
+ if (this.contextLost) return false;
3670
+ const target = this.captureTargets.get(capture);
3671
+ if (target === undefined) return false;
3672
+ if (!target.usable) {
3673
+ if (!this.warnedCapture) {
3674
+ this.warnedCapture = true;
3675
+ console.warn(
3676
+ 'Renderer: this driver will not draw into a scene capture, so it keeps its clear. ' +
3677
+ 'Nothing else is affected.',
3678
+ );
3679
+ }
3680
+ return false;
3681
+ }
3682
+ camera.updateMatrices(target.width / target.height);
3683
+ const { gl } = this;
3684
+ const jittering = this.temporalJittering;
3685
+ this.temporalJittering = false;
3686
+ this.capturePassActive = true;
3687
+ this.useFlatProgram();
3688
+ gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, 0);
3689
+ gl.uniform1f(this.flatUniforms['uOutputExposure'] ?? null, 1);
3690
+ this.materials.dirty();
3691
+ target.begin(gl, clearColor, this.depthClear);
3692
+ /*
3693
+ * **Rows flipped, as a probe face's are.** A GL framebuffer's first row is its bottom and an
3694
+ * image's is its top, so a capture drawn upright would be shown upside down by every material
3695
+ * reading it as the image it is; `viewProjFor` negates clip y for the pass, and negating one axis
3696
+ * reverses every triangle's winding, which the front face follows. WebGPU's rows already run
3697
+ * the image's way.
3698
+ */
3699
+ gl.frontFace(gl.CW);
3700
+ try {
3701
+ draw(camera);
3702
+ } finally {
3703
+ target.end(gl);
3704
+ gl.frontFace(gl.CCW);
3705
+ this.useFlatProgram();
3706
+ gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, this.gradeCode());
3707
+ gl.uniform1f(this.flatUniforms['uOutputExposure'] ?? null, this.gradeExposure());
3708
+ this.materials.dirty();
3709
+ this.capturePassActive = false;
3710
+ this.temporalJittering = jittering;
3711
+ this.restoreViewport();
3712
+ }
3713
+ return true;
3714
+ }
3715
+
3502
3716
  /**
3503
3717
  * Replace a surface texture's pixels, keeping the GPU object and its sampler state.
3504
3718
  *
@@ -3514,11 +3728,16 @@ export class WebGL2Renderer implements RendererApi {
3514
3728
  updateSurfaceTexture(texture: SurfaceTexture, source: TexImageSource): void {
3515
3729
  if (this.contextLost) return;
3516
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);
3517
3733
  }
3518
3734
 
3519
3735
  disposeSurfaceTexture(texture: SurfaceTexture): void {
3520
3736
  if (this.contextLost) return;
3737
+ if (texture === this.overlayMaps) this.bindOverlayMaps(null);
3521
3738
  texture.dispose(this.gl);
3739
+ this.captureTargets.get(texture)?.dispose(this.gl);
3740
+ this.captureTargets.delete(texture);
3522
3741
  }
3523
3742
 
3524
3743
  /**
@@ -3751,9 +3970,9 @@ export class WebGL2Renderer implements RendererApi {
3751
3970
  if (model?.kind === 'skin' && this.skinScatter !== null) {
3752
3971
  this.skinScatter.setProfile(
3753
3972
  model.profile,
3754
- model.scatterColor[0] * model.radius,
3755
- model.scatterColor[1] * model.radius,
3756
- model.scatterColor[2] * model.radius,
3973
+ skinProfileDistance(model, 0),
3974
+ skinProfileDistance(model, 1),
3975
+ skinProfileDistance(model, 2),
3757
3976
  );
3758
3977
  }
3759
3978
  /*
@@ -3901,8 +4120,17 @@ export class WebGL2Renderer implements RendererApi {
3901
4120
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3902
4121
  if (modelMap !== null) modelMap.bind(gl, MODEL_MAP_TEXTURE_UNIT);
3903
4122
  gl.uniform1i(u['uModelMap'] ?? null, MODEL_MAP_TEXTURE_UNIT);
3904
- packModel(material?.model ?? null, modelMap !== null, this.modelParams);
4123
+ packModel(
4124
+ material?.model ?? null,
4125
+ modelMap !== null,
4126
+ this.modelParams,
4127
+ material?.physicalSpecular === true,
4128
+ );
4129
+ /* The first physical highlight asked for is when the lit programs start carrying one. */
4130
+ if (material?.physicalSpecular === true) this.askLit('PHYSICAL_SPECULAR');
3905
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);
3906
4134
 
3907
4135
  /* The albedo's effects table, or the stand-in the shader reads as none. Before the early
3908
4136
  return, so a material with no albedo still leaves a complete texture on the unit. */
@@ -3963,18 +4191,6 @@ export class WebGL2Renderer implements RendererApi {
3963
4191
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3964
4192
  }
3965
4193
 
3966
- /**
3967
- * A pass-level scale over the grain the geometry itself declared, 0 to 1.
3968
- *
3969
- * Which surfaces are mineral is now `MeshData.grain`, stated per vertex through
3970
- * `MeshBuilder.setGrain` — because it is a property of a material, and absent means none.
3971
- * This stays as the override for the case that has no answer to state: an imported model
3972
- * arrives with no grain information at all, and a caller drawing one decides for it.
3973
- *
3974
- * Pass state, like `setSurfaceTexture`, because a material covers many draws. The default
3975
- * is 1 and `bindMeshPass` restores it, so it scales geometry that declared grain and
3976
- * leaves geometry that did not exactly as it is.
3977
- */
3978
4194
  /**
3979
4195
  * The ambient the following draws take, as nine spherical-harmonic coefficients of incoming
3980
4196
  * radiance — red, green and blue of each, twenty-seven numbers — or `null` for the frame's own:
@@ -3986,16 +4202,6 @@ export class WebGL2Renderer implements RendererApi {
3986
4202
  * only — a glossy surface still reflects the probes — and it is pass state like the material,
3987
4203
  * which `bindMeshPass` clears, so a mirror or a probe bake drawn after does not inherit it.
3988
4204
  */
3989
- /**
3990
- * How much of the environment the following draws mirror, 0 to 1.
3991
- *
3992
- * Pass state beside the grain, and the counterpart to it: grain is a surface being uneven,
3993
- * this is a surface being smooth enough to carry an image. Polished paint, glass, chrome
3994
- * and still water want it; plaster and stone want none.
3995
- *
3996
- * Defaults to zero and bindMeshPass restores it, so a scene that never calls this looks
3997
- * exactly as it did.
3998
- */
3999
4205
  setAmbientSH(coefficients: ArrayLike<number> | null): void {
4000
4206
  if (this.contextLost) return;
4001
4207
  packAmbientSH(coefficients, this.ambientSH);
@@ -4007,6 +4213,16 @@ export class WebGL2Renderer implements RendererApi {
4007
4213
  this.useFlatProgram();
4008
4214
  }
4009
4215
 
4216
+ /**
4217
+ * How much of the environment the following draws mirror, 0 to 1.
4218
+ *
4219
+ * Pass state beside the grain, and the counterpart to it: grain is a surface being uneven,
4220
+ * this is a surface being smooth enough to carry an image. Polished paint, glass, chrome
4221
+ * and still water want it; plaster and stone want none.
4222
+ *
4223
+ * Defaults to zero and bindMeshPass restores it, so a scene that never calls this looks
4224
+ * exactly as it did.
4225
+ */
4010
4226
  setSurfaceReflectivity(amount: number): void {
4011
4227
  if (this.contextLost) return;
4012
4228
  this.materials.dirty();
@@ -4014,6 +4230,76 @@ export class WebGL2Renderer implements RendererApi {
4014
4230
  this.writeFlatVector('uEnvironmentDials', this.environmentDials);
4015
4231
  }
4016
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
+
4017
4303
  /**
4018
4304
  * How bright the environment the surfaces drawn next reflect is. 1 is the default and identity.
4019
4305
  *
@@ -4056,6 +4342,18 @@ export class WebGL2Renderer implements RendererApi {
4056
4342
  );
4057
4343
  }
4058
4344
 
4345
+ /**
4346
+ * A pass-level scale over the grain the geometry itself declared, 0 to 1.
4347
+ *
4348
+ * Which surfaces are mineral is now `MeshData.grain`, stated per vertex through
4349
+ * `MeshBuilder.setGrain` — because it is a property of a material, and absent means none.
4350
+ * This stays as the override for the case that has no answer to state: an imported model
4351
+ * arrives with no grain information at all, and a caller drawing one decides for it.
4352
+ *
4353
+ * Pass state, like `setSurfaceTexture`, because a material covers many draws. The default
4354
+ * is 1 and `bindMeshPass` restores it, so it scales geometry that declared grain and
4355
+ * leaves geometry that did not exactly as it is.
4356
+ */
4059
4357
  setSurfaceGrain(amount: number): void {
4060
4358
  if (this.contextLost) return;
4061
4359
  this.materials.dirty();
@@ -4437,8 +4735,11 @@ export class WebGL2Renderer implements RendererApi {
4437
4735
  if (coverage) gl.enable(gl.SAMPLE_ALPHA_TO_COVERAGE);
4438
4736
  if (ditheredBlend)
4439
4737
  gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(this.currentMaterial, true));
4738
+ const keeps = !blend && this.reflectionKeeps();
4739
+ if (keeps) this.reflectionTargets?.on();
4440
4740
  if (culling && cullsInstances(count, mesh.indexCount)) batch.drawCulled(gl, data, this.frustum);
4441
4741
  else batch.draw(gl, count);
4742
+ if (keeps) this.reflectionTargets?.off();
4442
4743
  if (coverage) gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
4443
4744
  if (ditheredBlend)
4444
4745
  gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(this.currentMaterial, false));
@@ -4485,7 +4786,7 @@ export class WebGL2Renderer implements RendererApi {
4485
4786
  const refraction = options.refraction ?? 0;
4486
4787
  const glassy = resolveGlass(options.glass, this.glassScratch);
4487
4788
  if (refraction <= 0 && !glassy) return false;
4488
- if (this.probePassActive || this.reflectionPassActive) return false;
4789
+ if (this.probePassActive || this.reflectionPassActive || this.capturePassActive) return false;
4489
4790
  const snapshot = this.sceneTarget?.snapshotColor(false, glassy) ?? null;
4490
4791
  if (snapshot === null) return false;
4491
4792
  gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
@@ -4509,7 +4810,8 @@ export class WebGL2Renderer implements RendererApi {
4509
4810
  const { gl } = this;
4510
4811
  gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
4511
4812
  gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
4512
- 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);
4513
4815
  }
4514
4816
 
4515
4817
  disposeMesh(mesh: Mesh): void {
@@ -4754,7 +5056,7 @@ export class WebGL2Renderer implements RendererApi {
4754
5056
  */
4755
5057
  private gradeCode(): number {
4756
5058
  return this.sceneTarget === null || !this.quality.hdrScene
4757
- ? OUTPUT_TRANSFORM_CODE[this.quality.outputTransform]
5059
+ ? forwardTransformCode(this.quality.outputTransform)
4758
5060
  : 0;
4759
5061
  }
4760
5062
 
@@ -4847,6 +5149,29 @@ export class WebGL2Renderer implements RendererApi {
4847
5149
  return new ParticleBatch(this.gl, capacity, options);
4848
5150
  }
4849
5151
 
5152
+ /**
5153
+ * Particles a caller's own compute wrote: WebGPU's alone, since this backend has no compute stage
5154
+ * to have written them (`computeSupported`). Said once, and nothing is drawn.
5155
+ */
5156
+ drawDeviceParticles(
5157
+ _batch: ParticleBatch,
5158
+ _particles: DeviceParticles,
5159
+ _camera: Camera,
5160
+ _env: Environment,
5161
+ _timeSeconds: number,
5162
+ ): void {
5163
+ if (this.warnedDeviceParticles) return;
5164
+ this.warnedDeviceParticles = true;
5165
+ console.warn(
5166
+ 'Renderer: drawDeviceParticles draws particles a compute shader wrote, and WebGL2 has no ' +
5167
+ 'compute stage, so nothing is drawn. Check `computeSupported`, and draw a pool with ' +
5168
+ '`drawParticles` where it is false.',
5169
+ );
5170
+ }
5171
+
5172
+ /** Said once: see `drawDeviceParticles`. */
5173
+ private warnedDeviceParticles = false;
5174
+
4850
5175
  /**
4851
5176
  * Draw a live particle pool.
4852
5177
  *
@@ -4913,7 +5238,12 @@ export class WebGL2Renderer implements RendererApi {
4913
5238
  const u = batch.uniforms;
4914
5239
  const scene = this.sceneTarget;
4915
5240
  let depth: WebGLTexture | null = null;
4916
- if (batch.softDepth > 0 && !this.reflectionPassActive && !this.probePassActive) {
5241
+ if (
5242
+ batch.softDepth > 0 &&
5243
+ !this.reflectionPassActive &&
5244
+ !this.probePassActive &&
5245
+ !this.capturePassActive
5246
+ ) {
4917
5247
  depth = scene?.snapshotDepth(true) ?? null;
4918
5248
  if (depth === null && !this.warnedSpriteDepth) {
4919
5249
  this.warnedSpriteDepth = true;
@@ -5970,7 +6300,7 @@ export class WebGL2Renderer implements RendererApi {
5970
6300
  * throws a sentence with the device's numbers in it rather than a bare driver string.
5971
6301
  */
5972
6302
  /** The lit fragment source this renderer's profile compiles, at a light budget. */
5973
- private flatFragAt(budget: LightBudget): string {
6303
+ private flatFragAt(budget: LightBudget, overlay = this.litOn.SURFACE_OVERLAY): string {
5974
6304
  return flatFrag({
5975
6305
  pointShadows: this.quality.pointShadows,
5976
6306
  directionalShadows: this.quality.directionalShadows,
@@ -5982,6 +6312,9 @@ export class WebGL2Renderer implements RendererApi {
5982
6312
  lightFixtures: this.litOn.LIGHT_FIXTURES,
5983
6313
  surfaceEffects: this.litOn.SURFACE_EFFECTS,
5984
6314
  driftLight: this.litOn.DRIFT_LIGHT,
6315
+ physicalSpecular: this.litOn.PHYSICAL_SPECULAR,
6316
+ surfaceOverlay: overlay,
6317
+ reflectionMaps: this.litOn.REFLECTION_MAPS,
5985
6318
  maxLights: budget.maxLights,
5986
6319
  maxAreaLights: budget.maxAreaLights,
5987
6320
  });
@@ -6807,11 +7140,22 @@ export class WebGL2Renderer implements RendererApi {
6807
7140
  * Set before `beginFrame` and held until changed, like `setSpeedRush`, which it sits
6808
7141
  * beside for the same reason: both describe how the finished image is presented rather
6809
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.
6810
7149
  */
6811
- setCameraMotionBlur(scale: number): void {
7150
+ setCameraMotionBlur(scale: number, maxShare = MOTION_BLUR_MAX_UV): void {
6812
7151
  this.motionBlurScale = Math.min(Math.max(scale, 0), 1);
7152
+ this.motionBlurMax = clampMotionShare(maxShare);
6813
7153
  }
6814
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
+
6815
7159
  /**
6816
7160
  * The camera has cut: this frame is a new shot, not the next moment of the last one.
6817
7161
  *
@@ -7023,6 +7367,47 @@ export class WebGL2Renderer implements RendererApi {
7023
7367
  : this.quality.outputExposure;
7024
7368
  }
7025
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
+
7026
7411
  /**
7027
7412
  * Composite a flat colour over the finished frame — for a cut dipping to white or to black.
7028
7413
  *
@@ -7114,6 +7499,21 @@ export class WebGL2Renderer implements RendererApi {
7114
7499
  this.sceneTarget.setVignette(strength);
7115
7500
  }
7116
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
+
7117
7517
  /**
7118
7518
  * Film grain, held until changed: an amplitude in display values (0 is none, 0.03 is a fine
7119
7519
  * grain) and this frame's seed. **The seed is the caller's**, because the engine takes time from
@@ -7304,6 +7704,8 @@ export class WebGL2Renderer implements RendererApi {
7304
7704
 
7305
7705
  beginFrame(clearColor: Vec3): void {
7306
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();
7307
7709
  this.skinScattered = false;
7308
7710
  this.skinScatter?.reset();
7309
7711
  if (this.litAsked) {
@@ -7373,6 +7775,8 @@ export class WebGL2Renderer implements RendererApi {
7373
7775
  }
7374
7776
 
7375
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);
7376
7780
  gl.clearColor(clearColor[0], clearColor[1], clearColor[2], 1);
7377
7781
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
7378
7782
  }
@@ -7429,7 +7833,7 @@ export class WebGL2Renderer implements RendererApi {
7429
7833
  motion = {
7430
7834
  reprojection: this.reprojection as Float32Array,
7431
7835
  strength: blur,
7432
- max: MOTION_BLUR_MAX_UV,
7836
+ max: this.motionBlurMax,
7433
7837
  };
7434
7838
  }
7435
7839
  }
@@ -7521,7 +7925,11 @@ export class WebGL2Renderer implements RendererApi {
7521
7925
 
7522
7926
  /* The grade travels to the resolve only where the mesh pass gave it up. See `bindMeshPass`. */
7523
7927
  const lateGrade = this.quality.hdrScene
7524
- ? { 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
+ }
7525
7933
  : { transform: 0, exposure: 1 };
7526
7934
  /*
7527
7935
  * Depth of field, on the same ceiling-and-dial split as bloom above: `depthOfField` decides
@@ -7597,6 +8005,49 @@ export class WebGL2Renderer implements RendererApi {
7597
8005
  * drawn over a reflective floor rather than under it, and before the post chain so bloom and
7598
8006
  * the grade see a reflection exactly as they would see any other part of the picture.
7599
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
+ }
7600
8051
  if (this.reflectionQueue.length > 0 && this.frameViewProj !== null) {
7601
8052
  const scene = this.sceneTarget?.colorAttachment() ?? null;
7602
8053
  const depth = this.sceneTarget?.snapshotDepth(true) ?? null;
@@ -7877,7 +8328,12 @@ export class WebGL2Renderer implements RendererApi {
7877
8328
  * other position, and the anti-flicker's period never started.
7878
8329
  */
7879
8330
  if (this.framePresented) this.temporalJittering = false;
7880
- if (!this.reflectionPassActive && !this.probePassActive && !this.framePresented) {
8331
+ if (
8332
+ !this.reflectionPassActive &&
8333
+ !this.probePassActive &&
8334
+ !this.capturePassActive &&
8335
+ !this.framePresented
8336
+ ) {
7881
8337
  this.frameViewProj = camera.viewProjection;
7882
8338
  /* Copied rather than referenced: a caller moves its camera in place, and the decal pass
7883
8339
  reads this at the end of the frame. */
@@ -7975,6 +8431,10 @@ export class WebGL2Renderer implements RendererApi {
7975
8431
  this.materialHasMaps = false;
7976
8432
  this.currentMaterial = null;
7977
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;
7978
8438
  /* Nor an ambient of its own: the frame's, until a draw asks. See `setAmbientSH`. */
7979
8439
  this.ambientSH.fill(0);
7980
8440
  /* No reflection and an environment at its own brightness; the emission the world asks for. */
@@ -8131,8 +8591,10 @@ export class WebGL2Renderer implements RendererApi {
8131
8591
  * unless a draw sets the strength above zero.
8132
8592
  */
8133
8593
  gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
8134
- gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
8594
+ gl.bindTexture(gl.TEXTURE_2D, this.overlayTexture ?? this.emptyTexture2D);
8135
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);
8136
8598
  /* Neither refracting nor glass, which is every draw that does not say otherwise. See
8137
8599
  `bindSeeThrough` for the four numbers in the one vector. */
8138
8600
  gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
@@ -8541,9 +9003,18 @@ export class WebGL2Renderer implements RendererApi {
8541
9003
  * here, so there is no motion target for this to be written into, and a mover passed here is
8542
9004
  * neither read nor advanced.
8543
9005
  */
8544
- _previousModel: ReadonlyMat4 | Mover | null = null,
9006
+ previousModel: ReadonlyMat4 | Mover | null = null,
8545
9007
  ): void {
8546
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
+ }
8547
9018
  /* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
8548
9019
  and `Mesh.indexCount`. */
8549
9020
  if (!mesh.complete || mesh.indexCount === 0) return;
@@ -8574,7 +9045,11 @@ export class WebGL2Renderer implements RendererApi {
8574
9045
  * light and its colour, each into the blur's own target.
8575
9046
  */
8576
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();
8577
9051
  this.drawMeshHalf(mesh, model, depthLayer, tint, splits ? 'scene' : 'whole');
9052
+ if (keeps) this.reflectionTargets?.off();
8578
9053
  if (splits && this.skinScatter !== null) {
8579
9054
  this.skinScatter.begin();
8580
9055
  this.drawMeshHalf(mesh, model, depthLayer, tint, 'diffuse');
@@ -8589,6 +9064,21 @@ export class WebGL2Renderer implements RendererApi {
8589
9064
  * skin yet, this is the frame's own picture — not a mirror's or a probe's, nor an overlay after
8590
9065
  * the frame — and the blur's target can be had.
8591
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
+
8592
9082
  private skinSplits(): boolean {
8593
9083
  const skin = this.skinScatter;
8594
9084
  const scene = this.sceneTarget;
@@ -8599,6 +9089,7 @@ export class WebGL2Renderer implements RendererApi {
8599
9089
  !this.skinScattered &&
8600
9090
  !this.reflectionPassActive &&
8601
9091
  !this.probePassActive &&
9092
+ !this.capturePassActive &&
8602
9093
  !this.framePresented &&
8603
9094
  skin.ready(scene)
8604
9095
  );
@@ -8616,10 +9107,17 @@ export class WebGL2Renderer implements RendererApi {
8616
9107
  const scene = this.sceneTarget;
8617
9108
  const projection = this.frameProjection;
8618
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;
8619
9117
  /* Latched whether or not skin was drawn: a skin drawn after this point is the whole surface. */
8620
9118
  this.skinScattered = true;
8621
9119
  if (!skin.pending) return;
8622
- if (this.reflectionPassActive || this.probePassActive || projection === null) return;
9120
+ if (projection === null) return;
8623
9121
  const depth = scene.snapshotDepth(true);
8624
9122
  mat4.invert(this.skinInverseProjection, projection);
8625
9123
  skin.spread(scene, depth, this.emptyTexture2D, projection, this.skinInverseProjection);