@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
@@ -129,7 +129,7 @@ import {
129
129
  FACE_COUNT,
130
130
  createResolvedPointShadows,
131
131
  } from '../../pointShadowImage.ts';
132
- import { MAX_AREA_LIGHTS, type AreaLightSource } from '../../areaLights.ts';
132
+ import { MAX_AREA_LIGHTS, packAreaSizesAndDoors, type AreaLightSource } from '../../areaLights.ts';
133
133
  import {
134
134
  areaShadowLayerCount,
135
135
  castingRange,
@@ -175,12 +175,31 @@ import {
175
175
  import type { TextStyle } from '../../textLayout.ts';
176
176
  import { deviceSnappedCellSize, deviceSnappedOrigin } from '../../textLayout.ts';
177
177
  import { GpuSurfaceTexture } from './surfaceTexturePass.ts';
178
+ import { GpuSceneCaptureTarget } from './sceneCaptureTarget.ts';
179
+ import { sceneCaptureTexels } from '../../sceneCapture.ts';
178
180
  import { gpuCompressedFormats } from './compressedUpload.ts';
179
181
  import { GpuClothBinding, GpuClothParticles, clothStandIn } from './clothTextures.ts';
180
182
  import type { ClothViews } from './clothTextures.ts';
181
183
  import type { ComputeHost } from './computeHost.ts';
182
- import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel } from '../../surfaceModel.ts';
183
- import type { SurfaceModel } from '../../surfaceModel.ts';
184
+ import {
185
+ MODEL_PARAM_FLOATS,
186
+ eyeAxisInWorld,
187
+ packModel,
188
+ skinProfileDistance,
189
+ } from '../../surfaceModel.ts';
190
+ import type { SurfaceModel, SurfaceModelKind } from '../../surfaceModel.ts';
191
+ import {
192
+ OVERLAY_FLOATS,
193
+ overlayLays,
194
+ packSurfaceOverlay,
195
+ type SurfaceOverlay,
196
+ } from '../../surfaceOverlay.ts';
197
+ import {
198
+ frameReflectionsRefused,
199
+ resolveFrameReflections,
200
+ type ResolvedFrameReflections,
201
+ } from '../../frameReflections.ts';
202
+ import { ReflectionSurfacePass } from './reflectionSurfacePass.ts';
184
203
  import { validateClothBinding } from '../../clothBindingData.ts';
185
204
  import type { ClothBindingData } from '../../clothBindingData.ts';
186
205
  import type { CompressedTextureFormat, SurfaceSource } from '../../compressedSource.ts';
@@ -209,7 +228,24 @@ import type { SurfaceMaterial, SurfaceTextureOptions } from '../../surfaceTextur
209
228
  import type { RenderBackend } from '../api.ts';
210
229
  import { reflectionTargetSize, type ReflectionSize } from '../../planarReflectionDraw.ts';
211
230
  import { drawingBufferSize } from '../../drawingBuffer.ts';
212
- import { MOTION_BLUR_MAX_UV, OUTPUT_TRANSFORM_CODE, RUSH_REACH_UV } from '../../vertexDefaults.ts';
231
+ import {
232
+ MOTION_BLUR_MAX_UV,
233
+ OBJECT_BLUR_UNKEPT,
234
+ OUTPUT_TRANSFORM_CODE,
235
+ RUSH_REACH_UV,
236
+ clampMotionShare,
237
+ forwardTransformCode,
238
+ } from '../../vertexDefaults.ts';
239
+ import { FRINGE_FLOATS, resolveFringe } from '../../shaders/fringe.ts';
240
+ import { displayHeadroom, type DisplayRange } from '../../displayRange.ts';
241
+ import {
242
+ DEFAULT_FILMIC_CURVE,
243
+ FILMIC_CLAMPED,
244
+ FILMIC_CONSTANTS,
245
+ FILMIC_WITHOUT_COMPOSITE,
246
+ resolveFilmicCurve,
247
+ type FilmicCurve,
248
+ } from '../../filmicCurve.ts';
213
249
  import { ExposurePass } from './exposurePass.ts';
214
250
  import { AO_STORE } from '../../shaders/ambientOcclusion.ts';
215
251
  import { clampAutoExposure } from '../../shaders/exposure.ts';
@@ -274,9 +310,15 @@ import {
274
310
  DECAL_TEXTURES,
275
311
  DECAL_UNIFORMS,
276
312
  PREMULTIPLIED_OVER_BLEND,
313
+ FRAME_REFLECTION_BLEND,
277
314
  REFLECTION_OVER_BLEND,
278
315
  SSR_FRAG_FIELDS,
279
316
  SSR_FRAG_SIZE,
317
+ SSR_MATERIAL_FIELDS,
318
+ SSR_MATERIAL_RESOLVE_FRAG_WGSL,
319
+ SSR_MATERIAL_SIZE,
320
+ SSR_MATERIAL_TEXTURES,
321
+ SSR_MATERIAL_UNIFORMS,
280
322
  SSR_RESOLVE_FRAG_WGSL,
281
323
  SSR_RESOLVE_TEXTURES,
282
324
  SSR_TEXTURES,
@@ -337,7 +379,7 @@ import type { LineSegments } from '../../linePoints.ts';
337
379
  import { buildPlumeGeometry } from '../../plumeGeometry.ts';
338
380
  import type { ParticleMaterial } from '../../particleMaterial.ts';
339
381
  import { facingCode, type ParticleBatchOptions } from '../../particleBatch.ts';
340
- import type { ParticleInstances } from '../../particlePool.ts';
382
+ import type { DeviceParticles, ParticleInstances } from '../../particlePool.ts';
341
383
  import { PARTICLE_INDICES } from '../../shaders/particle.ts';
342
384
  import type { PlumeOptions, PlumePlacement } from '../../plumeRenderer.ts';
343
385
  import type { InstanceData } from '../../instancedMesh.ts';
@@ -385,6 +427,7 @@ import {
385
427
  type FlatVariant,
386
428
  flatPipeline,
387
429
  flatPipelineAsync,
430
+ probeStageCeilings,
388
431
  } from './flatPass.ts';
389
432
  import type { OitTarget } from './flatPass.ts';
390
433
  import {
@@ -687,34 +730,20 @@ const MAX_SKY_DRAWS = 64;
687
730
  /**
688
731
  * Whether the environment probe's permutation fits this device's per-stage binding limits.
689
732
  *
690
- * The widest flat variant declares one albedo, three directional shadow maps, twelve
691
- * point-shadow cubes and the probe: seventeen textures and seventeen samplers. WebGPU's default
692
- * is sixteen of each and an adapter is free to offer no more — this one offers 48 textures and
693
- * exactly 16 samplers, so the samplers are what decide it.
733
+ * **Two ceilings, counted separately**: sampled textures and samplers, since textures that share a
734
+ * sampler (`// wgsl:share` in `flat/preamble.ts`) count once against the second. The widest variant
735
+ * binds sixteen textures and twelve samplers, which is exactly WebGPU's default texture ceiling, and
736
+ * `select.ts` asks for the adapter's own — 48 here — so the probe does not depend on that request.
694
737
  */
695
738
  function probeFits(device: GPUDevice): boolean {
696
739
  /*
697
- * **Two ceilings, counted separately, and they are no longer the same number.**
698
- *
699
- * The widest permutation declares seventeen sampled textures — one albedo, three directional
700
- * shadow maps, the point-shadow array, a normal map and the probe's own cube — **seven**, where
701
- * it was seventeen while every point light's shadow was a `samplerCube` of its own. `select.ts` asks
702
- * for the adapter's ceiling, which is 48 on this machine against a device default of 16, so
703
- * six fits everywhere and the probe no longer depends on the request being granted.
704
- *
705
- * It used to declare seventeen *samplers* too, and that was the binding constraint: this
706
- * adapter offers exactly sixteen and no request can raise it, so the probe stood down and
707
- * reflective surfaces kept a gradient. The shadow bindings share one declaration — see
708
- * `// wgsl:share shadow` in `flat/preamble.ts`, which costs nothing because they were already
709
- * one object — so the count is four, and the fourth is the normal map's: it is a colour image
710
- * and wants filtering, which a non-filtering shadow sampler cannot give it.
711
- *
712
- * **Counted from the generated bindings rather than typed**: `FLAT_BINDINGS.flatFrag`'s
713
- * widest variant is `directionalShadows+environmentProbe+pointShadows`, seven textures and
714
- * four samplers. A stale number here reads as a device declining a feature it can run.
715
- */
716
- const TEXTURES = 7;
717
- const SAMPLERS = 4;
740
+ * **Counted from the generated bindings rather than typed** (`probeStageCeilings`). It was typed
741
+ * here as seven textures and four samplers while the widest variant grew to sixteen and twelve
742
+ * with the material maps, glass, the fixture atlas and DriftLight: a stale number reads as a
743
+ * device declining a feature it can run, or, the way this one had drifted, as one admitted to a
744
+ * permutation it cannot bind.
745
+ */
746
+ const { textures: TEXTURES, samplers: SAMPLERS } = probeStageCeilings();
718
747
  const fits =
719
748
  device.limits.maxSampledTexturesPerShaderStage >= TEXTURES &&
720
749
  device.limits.maxSamplersPerShaderStage >= SAMPLERS;
@@ -782,6 +811,7 @@ const VERB_PASS = 3;
782
811
  /** What the depth snapshot copies from, and what it copies into. */
783
812
  const DEPTH_SOURCE = maskOf('sceneDepth');
784
813
  const DEPTH_SNAPSHOT = maskOf('depthSnapshot');
814
+ const COLOR_SNAPSHOT = maskOf('colorSnapshot');
785
815
 
786
816
  /** The mirror's own colour and depth. */
787
817
  const MIRROR_TARGET = maskOf('mirrorColor', 'mirrorDepth');
@@ -1912,6 +1942,17 @@ export class WebGPURenderer implements RendererApi {
1912
1942
  */
1913
1943
  private refractSnapshot: GPUTexture | null = null;
1914
1944
  private refractSnapshotView: GPUTextureView | null = null;
1945
+ /**
1946
+ * The frame's colour as a registered pass reads it (`reads: ['colorSnapshot']`): the scene's own
1947
+ * size and format, refilled at each such pass's draw. Allocated only once a pass declares the
1948
+ * read, beside the frame's targets, and never in the frame loop. See `takePassColour`.
1949
+ */
1950
+ private passColour: GPUTexture | null = null;
1951
+ private passColourView: GPUTextureView | null = null;
1952
+ /** Registered passes that read the colour, queued this frame for after the upscale. */
1953
+ private readonly latePasses: PassHandle[] = [];
1954
+ /** A late pass draws unjittered, as every late draw does. */
1955
+ private readonly latePassJitter = new Float32Array(2);
1915
1956
  /** Whether this frame has taken it. One snapshot serves every refracting draw. */
1916
1957
  private refractSnapshotTaken = false;
1917
1958
  /**
@@ -2422,8 +2463,18 @@ export class WebGPURenderer implements RendererApi {
2422
2463
  private readonly reflectionQueue = new ReflectionQueue();
2423
2464
  private ssrLayout!: GPUBindGroupLayout;
2424
2465
  private ssrResolveLayout!: GPUBindGroupLayout;
2466
+ /** The frame's materials' reflections, resolved; null where off. See `frameReflections.ts`. */
2467
+ private readonly frameReflections: ResolvedFrameReflections | null;
2468
+ /** Their surface half, null where they are off or cannot be drawn: `reflectionSurfacePass.ts`. */
2469
+ private readonly reflectionSurface: ReflectionSurfacePass | null;
2470
+ /** The material resolve: its layout, its one block (the blur) and its group over the three maps. */
2471
+ private ssrMaterialLayout!: GPUBindGroupLayout;
2472
+ private ssrMaterialUniforms!: GPUBuffer;
2473
+ private readonly ssrMaterialFloats = new Float32Array(SSR_MATERIAL_SIZE / 4);
2474
+ private ssrMaterialGroup: GPUBindGroup | null = null;
2425
2475
  private ssrUniforms!: GPUBuffer;
2426
- private readonly ssrStaging = new ArrayBuffer(SSR_SLOT * MAX_REFLECTIVE_SURFACES);
2476
+ /* One slot a surface, and one more for the frame's materials' trace. */
2477
+ private readonly ssrStaging = new ArrayBuffer(SSR_SLOT * (MAX_REFLECTIVE_SURFACES + 1));
2427
2478
  private readonly ssrFloats = new Float32Array(this.ssrStaging);
2428
2479
  private ssrReflection: GPUTexture | null = null;
2429
2480
  private ssrReflectionView: GPUTextureView | null = null;
@@ -2948,6 +2999,117 @@ export class WebGPURenderer implements RendererApi {
2948
2999
  });
2949
3000
  }
2950
3001
 
3002
+ /**
3003
+ * A texture the scene can be drawn into, for a material to show. See `RendererApi` and
3004
+ * `sceneCapture.ts`. At the world's own format, which under `hdrScene` is half floats.
3005
+ */
3006
+ createSceneCapture(width: number, height: number): GpuSurfaceTexture {
3007
+ const texture = new GpuSurfaceTexture(
3008
+ this.surface.device,
3009
+ this.pipelines,
3010
+ sceneCaptureTexels(width, height, this.quality.hdrScene),
3011
+ { wrap: 'clamp', mipmap: false },
3012
+ );
3013
+ this.captureTargets.set(texture, new GpuSceneCaptureTarget(this.surface.device, width, height));
3014
+ return texture;
3015
+ }
3016
+
3017
+ /** Each capture's attachments, beside the texture a material binds. */
3018
+ private readonly captureTargets = new WeakMap<GpuSurfaceTexture, GpuSceneCaptureTarget>();
3019
+ /**
3020
+ * Whether a scene capture is being drawn: an off-screen pass as a probe bake is, so the frame's
3021
+ * camera, jitter, motion, refraction copy, skin's spread and late routing are left alone, but
3022
+ * lit by the probes and DriftLight as the frame is. See `captureScene`.
3023
+ */
3024
+ private capturePassActive = false;
3025
+
3026
+ /**
3027
+ * Draw the scene into a capture from `camera`. See `RendererApi.captureScene`.
3028
+ *
3029
+ * **Its own encoder, as a probe bake's**: the frame's pass is set aside and restored, the rings
3030
+ * are marked and rewound around the submit, and the frame graph is flushed at both boundaries so
3031
+ * no recorded draw crosses one. **Radiance**: the output transform and exposure are held off.
3032
+ */
3033
+ captureScene(
3034
+ capture: GpuSurfaceTexture,
3035
+ camera: Camera,
3036
+ clearColor: Vec3,
3037
+ draw: (camera: Camera) => void,
3038
+ ): boolean {
3039
+ if (this.surface.lost) return false;
3040
+ const target = this.captureTargets.get(capture);
3041
+ const resolve = capture.layerView();
3042
+ if (target === undefined || resolve === null) return false;
3043
+ camera.updateMatrices(target.width / target.height);
3044
+ const { device } = this.surface;
3045
+ const { depth, colorMsaa } = target.attachments(this.samples, this.pipelines.format);
3046
+
3047
+ /*
3048
+ * **Flushed, and only then set aside.** The flush replays what the frame has recorded, and
3049
+ * replaying opens the frame's pass where none is open yet; saved before it, the pass given back
3050
+ * afterwards was the null that stood there, while the one the flush opened was left open on the
3051
+ * frame's encoder and every later pass on it was refused — a black frame for any capture taken
3052
+ * after the frame had drawn something. `scripts/capture-check.mjs` takes one at three points.
3053
+ */
3054
+ if (this.quality.frameGraph) this.flushGraph();
3055
+ const savedPass = this.pass;
3056
+ const savedEncoder = this.encoder;
3057
+ this.markRings();
3058
+ const skinMark = this.skinPalettes.mark();
3059
+ const frameEpoch = this.encoderEpoch;
3060
+ this.encoderEpoch = ++this.encoderEpochs;
3061
+ const encoder = device.createCommandEncoder({ label: 'capture' });
3062
+ this.encoder = encoder;
3063
+ this.capturePassActive = true;
3064
+ const heldGrade = this.perFrameInts[this.materialField('uOutputTransform')] ?? 0;
3065
+ const heldExposure = this.perFrameFloats[this.materialField('uOutputExposure')] ?? 1;
3066
+ this.perFrameInts[this.materialField('uOutputTransform')] = 0;
3067
+ this.perFrameFloats[this.materialField('uOutputExposure')] = 1;
3068
+ this.materials.dirty();
3069
+ const pass = encoder.beginRenderPass({
3070
+ label: 'capture',
3071
+ colorAttachments: [
3072
+ {
3073
+ view: colorMsaa ?? resolve,
3074
+ resolveTarget: colorMsaa === null ? undefined : resolve,
3075
+ loadOp: 'clear',
3076
+ storeOp: resolvedStoreOp(
3077
+ colorMsaa !== null,
3078
+ true,
3079
+ this.quality.discardResolvedAttachments,
3080
+ ),
3081
+ clearValue: { r: clearColor[0], g: clearColor[1], b: clearColor[2], a: 1 },
3082
+ },
3083
+ ],
3084
+ depthStencilAttachment: {
3085
+ view: depth,
3086
+ depthClearValue: DEPTH_CLEAR,
3087
+ depthLoadOp: 'clear',
3088
+ depthStoreOp: 'discard',
3089
+ },
3090
+ });
3091
+ this.pass = pass;
3092
+ try {
3093
+ draw(camera);
3094
+ /* The callback's draws replayed into this pass before it ends: see `bakeProbe`. */
3095
+ if (this.quality.frameGraph) this.flushGraph();
3096
+ } finally {
3097
+ pass.end();
3098
+ this.pass = null;
3099
+ this.flushRings();
3100
+ device.queue.submit([encoder.finish()]);
3101
+ this.rewindRings(skinMark);
3102
+ this.encoderEpoch = frameEpoch;
3103
+ this.encoder = savedEncoder;
3104
+ this.pass = savedPass;
3105
+ this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
3106
+ this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
3107
+ this.materials.dirty();
3108
+ this.capturePassActive = false;
3109
+ }
3110
+ return true;
3111
+ }
3112
+
2951
3113
  /**
2952
3114
  * GPU textures an `updateSurfaceTexture` replaced, destroyed when the next frame begins.
2953
3115
  *
@@ -2994,8 +3156,11 @@ export class WebGPURenderer implements RendererApi {
2994
3156
  if (this.ormMap === texture) this.ormMap = null;
2995
3157
  if (this.emissiveMap === texture) this.emissiveMap = null;
2996
3158
  if (this.modelMap === texture) this.modelMap = null;
3159
+ if (this.overlayMaps === texture) this.overlayMaps = null;
2997
3160
  this.forgetBindingsOf(texture);
2998
3161
  texture.dispose();
3162
+ this.captureTargets.get(texture)?.dispose();
3163
+ this.captureTargets.delete(texture);
2999
3164
  }
3000
3165
 
3001
3166
  /**
@@ -3010,6 +3175,7 @@ export class WebGPURenderer implements RendererApi {
3010
3175
  /* Same reasoning, for whichever SDF labels were bound against this atlas. */
3011
3176
  this.sdfTextBindGroups.delete(texture);
3012
3177
  this.flatBindGroups.clear();
3178
+ this.overlayGroups.clear();
3013
3179
  /*
3014
3180
  * The blank one is built with the albedo held aside, because `buildFlatBindGroup` reads
3015
3181
  * `this.albedo` for that slot — leaving it in place would put a real colour map into the
@@ -3207,9 +3373,9 @@ export class WebGPURenderer implements RendererApi {
3207
3373
  if (model?.kind === 'skin' && this.skinScatter !== null) {
3208
3374
  this.skinScatter.setProfile(
3209
3375
  model.profile,
3210
- model.scatterColor[0] * model.radius,
3211
- model.scatterColor[1] * model.radius,
3212
- model.scatterColor[2] * model.radius,
3376
+ skinProfileDistance(model, 0),
3377
+ skinProfileDistance(model, 1),
3378
+ skinProfileDistance(model, 2),
3213
3379
  );
3214
3380
  }
3215
3381
  this.material((f, i) => {
@@ -3259,7 +3425,14 @@ export class WebGPURenderer implements RendererApi {
3259
3425
  f[emissive + 1] = asked?.[1] ?? 1;
3260
3426
  f[emissive + 2] = asked?.[2] ?? 1;
3261
3427
  /* The model's numbers and whether its map is bound. See `packModel`. */
3262
- packModel(material?.model ?? null, modelMap !== null, this.modelParams);
3428
+ packModel(
3429
+ material?.model ?? null,
3430
+ modelMap !== null,
3431
+ this.modelParams,
3432
+ material?.physicalSpecular === true,
3433
+ );
3434
+ /* The first physical highlight asked for is when the lit pipelines start carrying one. */
3435
+ if (material?.physicalSpecular === true) this.enableLit('PHYSICAL_SPECULAR');
3263
3436
  f.set(this.modelParams, this.materialField('uModelParams'));
3264
3437
  });
3265
3438
  /*
@@ -3396,6 +3569,20 @@ export class WebGPURenderer implements RendererApi {
3396
3569
  this.material((f) => f.set(this.ambientSH, this.materialField('uAmbientSH')));
3397
3570
  }
3398
3571
 
3572
+ /**
3573
+ * The overlay the following draws wear, or null for none: pass state, put back to none by
3574
+ * `bindMeshPass`. See `renderer.ts` and `surfaceOverlay.ts`. The numbers go in the material block,
3575
+ * which every variant declares; the atlas goes in a twin of each flat group (`overlayGroup`).
3576
+ */
3577
+ setSurfaceOverlay(overlay: SurfaceOverlay | null): void {
3578
+ if (this.surface.lost) return;
3579
+ packSurfaceOverlay(overlay, this.overlayFloats);
3580
+ /* The first overlay laid is when the lit pipelines start carrying one. */
3581
+ if (overlayLays(overlay)) this.enableLit('SURFACE_OVERLAY');
3582
+ this.overlayMaps = (overlay?.maps ?? null) as GpuSurfaceTexture | null;
3583
+ this.material((f) => f.set(this.overlayFloats, this.materialField('uOverlay')));
3584
+ }
3585
+
3399
3586
  /** How reflective the surfaces drawn next are. Clamped, as `renderer.ts` clamps it. */
3400
3587
  setSurfaceReflectivity(amount: number): void {
3401
3588
  this.material((f) => {
@@ -4140,7 +4327,7 @@ export class WebGPURenderer implements RendererApi {
4140
4327
  */
4141
4328
  private gradeCode(): number {
4142
4329
  return !this.hasComposite || !this.quality.hdrScene
4143
- ? (OUTPUT_TRANSFORM_CODE[this.quality.outputTransform] ?? 0)
4330
+ ? forwardTransformCode(this.quality.outputTransform)
4144
4331
  : 0;
4145
4332
  }
4146
4333
 
@@ -4175,7 +4362,43 @@ export class WebGPURenderer implements RendererApi {
4175
4362
 
4176
4363
  const device = this.surface.device;
4177
4364
  device.queue.writeBuffer(batch.instances, 0, batch.staging, 0, live * PARTICLE_INSTANCE_FLOATS);
4365
+ this.drawParticleInstances(batch, batch.instances, live, camera, env, timeSeconds);
4366
+ }
4178
4367
 
4368
+ /**
4369
+ * Draw particles a caller's own compute wrote, where they lie. See `DeviceParticles`.
4370
+ *
4371
+ * **The pool's draw with its instance stream swapped**: the same material, blend, flipbook, soft
4372
+ * edge and fog, reading the caller's buffer in place of the one the pool uploads into — so nothing
4373
+ * crosses back to the CPU. Not sorted, and a count past what the buffer holds is cut to it rather
4374
+ * than handed to a draw the device would refuse with the frame.
4375
+ */
4376
+ drawDeviceParticles(
4377
+ batch: GpuParticles,
4378
+ particles: DeviceParticles,
4379
+ camera: Camera,
4380
+ env: Environment,
4381
+ timeSeconds: number,
4382
+ ): void {
4383
+ const fits = Math.floor(particles.buffer.size / (PARTICLE_INSTANCE_FLOATS * 4));
4384
+ const live = Math.min(Math.max(0, Math.floor(particles.count)), fits);
4385
+ /* Not above zero covers a NaN as well: a count or a size that is no number draws nothing. */
4386
+ if (!this.canDraw() || !(live > 0)) return;
4387
+ if (batch.drawnInEncoder === this.encoderEpoch) return;
4388
+ batch.drawnInEncoder = this.encoderEpoch;
4389
+ this.drawParticleInstances(batch, particles.buffer, live, camera, env, timeSeconds);
4390
+ }
4391
+
4392
+ /** A pool's uniforms for this view, and its draw from `instances`: both entry points' tail. */
4393
+ private drawParticleInstances(
4394
+ batch: GpuParticles,
4395
+ instances: GPUBuffer,
4396
+ live: number,
4397
+ camera: Camera,
4398
+ env: Environment,
4399
+ timeSeconds: number,
4400
+ ): void {
4401
+ const device = this.surface.device;
4179
4402
  const v = batch.vertFloats;
4180
4403
  const atV = (name: string): number => (PARTICLE_VERT_FIELDS[name]?.offset ?? -4) / 4;
4181
4404
  mat4.multiply(this.correctedViewProj, CLIP_CORRECTION, camera.viewProjection);
@@ -4255,7 +4478,7 @@ export class WebGPURenderer implements RendererApi {
4255
4478
  particleCommand.pipeline = particlePipe;
4256
4479
  particleCommand.bindGroup = group;
4257
4480
  particleCommand.vertexBuffers[0] = batch.corners;
4258
- particleCommand.vertexBuffers[1] = batch.instances;
4481
+ particleCommand.vertexBuffers[1] = instances;
4259
4482
  particleCommand.vertexCount = 2;
4260
4483
  particleCommand.indexBuffer = batch.indexBuffer;
4261
4484
  particleCommand.indexed = true;
@@ -4267,7 +4490,7 @@ export class WebGPURenderer implements RendererApi {
4267
4490
  pass.setPipeline(particlePipe);
4268
4491
  pass.setBindGroup(0, group);
4269
4492
  pass.setVertexBuffer(0, batch.corners);
4270
- pass.setVertexBuffer(1, batch.instances);
4493
+ pass.setVertexBuffer(1, instances);
4271
4494
  pass.setIndexBuffer(batch.indexBuffer, 'uint32');
4272
4495
  pass.drawIndexed(PARTICLE_INDICES, live);
4273
4496
  }
@@ -4286,7 +4509,12 @@ export class WebGPURenderer implements RendererApi {
4286
4509
  const fields = batch.fields;
4287
4510
  const atF = (name: string): number => (fields[name]?.offset ?? -4) / 4;
4288
4511
  let depth: GPUTextureView | null = null;
4289
- if (batch.softDepth > 0 && !this.reflectionPassActive && !this.probePassActive) {
4512
+ if (
4513
+ batch.softDepth > 0 &&
4514
+ !this.reflectionPassActive &&
4515
+ !this.probePassActive &&
4516
+ !this.capturePassActive
4517
+ ) {
4290
4518
  if (this.openPass() !== null && this.takeVolumeDepth()) depth = this.resolvedDepthView;
4291
4519
  if (depth === null && !this.warnedSpriteDepth) {
4292
4520
  this.warnedSpriteDepth = true;
@@ -4660,6 +4888,7 @@ export class WebGPURenderer implements RendererApi {
4660
4888
  scatterInto(f, WATER_FRAG_FIELDS['uLightRadius'], lights.radii, 1);
4661
4889
  scatterInto(f, WATER_FRAG_FIELDS['uLightSourceRadius'], lights.sourceRadii, 1);
4662
4890
  scatterInto(f, WATER_FRAG_FIELDS['uLightWeight'], lights.weights, 1);
4891
+ scatterInto(f, WATER_FRAG_FIELDS['uLightCone'], lights.conesAndFalloffs, 4);
4663
4892
 
4664
4893
  const medium = resolveAtmosphere(
4665
4894
  env,
@@ -5617,8 +5846,11 @@ export class WebGPURenderer implements RendererApi {
5617
5846
  cookies: new Float32Array(0),
5618
5847
  heldSourceRadii: new Float32Array(MAX_POINT_LIGHTS),
5619
5848
  axesAndChannels: new Float32Array(MAX_POINT_LIGHTS * 4),
5849
+ conesAndFalloffs: new Float32Array(MAX_POINT_LIGHTS * 4),
5620
5850
  heldWeights: new Float32Array(MAX_POINT_LIGHTS),
5621
5851
  };
5852
+ /** The fixed arm's rectangles as `uAreaLightSize` holds them. See `packAreaSizesAndDoors`. */
5853
+ private readonly areaSizesAndDoors = new Float32Array(MAX_AREA_LIGHTS * 4);
5622
5854
  /** Refilled per frame rather than allocated; see `resolveAtmosphere`. */
5623
5855
  private readonly medium: ResolvedAtmosphere = {
5624
5856
  fogColor: new Float32Array(3),
@@ -6414,6 +6646,10 @@ export class WebGPURenderer implements RendererApi {
6414
6646
  this.skinPalettes = new SkinPaletteRing(quality.drawsPerFrame);
6415
6647
  this.maxDrawingBufferPixels = quality.maxDrawingBufferPixels;
6416
6648
  this.exposure = quality.outputExposure;
6649
+ resolveFilmicCurve(DEFAULT_FILMIC_CURVE, this.filmConstants);
6650
+ if (quality.outputTransform === 'filmic' && !(quality.screenEffects && quality.hdrScene)) {
6651
+ console.warn(FILMIC_WITHOUT_COMPOSITE);
6652
+ }
6417
6653
  this.flushSchedule =
6418
6654
  quality.frameGraph && quality.identifierGraph
6419
6655
  ? createFlushSchedule(MAX_DRAWS_PER_FRAME)
@@ -6491,6 +6727,26 @@ export class WebGPURenderer implements RendererApi {
6491
6727
  );
6492
6728
  }
6493
6729
  this.skinScatter = skinScreen ? new SkinScatterPass(device) : null;
6730
+ /*
6731
+ * The frame's materials' reflections, where the frame can carry them: scene light to swap the
6732
+ * probe's share in, and one sample, which the trace's depth copy and both maps are. Said once
6733
+ * where it cannot. See `frameReflections.ts`.
6734
+ */
6735
+ this.frameReflections = resolveFrameReflections(quality.screenSpaceReflections);
6736
+ const reflectionsRefusal =
6737
+ this.frameReflections === null
6738
+ ? null
6739
+ : !quality.screenEffects || !quality.hdrScene
6740
+ ? 'the frame keeps no scene light to swap the probe in (`screenEffects` and `hdrScene`)'
6741
+ : this.samples > 1
6742
+ ? 'a multisampled frame has no single depth or colour to march through'
6743
+ : null;
6744
+ if (reflectionsRefusal !== null)
6745
+ console.warn(`WebGPU: ${frameReflectionsRefused(reflectionsRefusal)}`);
6746
+ this.reflectionSurface =
6747
+ this.frameReflections !== null && reflectionsRefusal === null
6748
+ ? new ReflectionSurfacePass(device)
6749
+ : null;
6494
6750
  this.skinPipelines = skinScreen
6495
6751
  ? new PipelineCache(
6496
6752
  device,
@@ -6881,6 +7137,11 @@ export class WebGPURenderer implements RendererApi {
6881
7137
  addressModeU: 'clamp-to-edge',
6882
7138
  addressModeV: 'clamp-to-edge',
6883
7139
  });
7140
+ /* Or a surface overlay's atlas, in the twin a draw wearing one binds: `overlayGroup`. */
7141
+ const maps = this.overlayBinding ? this.overlayMaps : null;
7142
+ if (maps !== null) {
7143
+ return { view: maps.layerView() ?? blankView, sampler: this.refractSampler };
7144
+ }
6884
7145
  return { view: this.refractSnapshotView ?? blankView, sampler: this.refractSampler };
6885
7146
  }
6886
7147
  /*
@@ -7057,6 +7318,8 @@ export class WebGPURenderer implements RendererApi {
7057
7318
  { binding: RUSH_TEXTURES.uExposureHeld },
7058
7319
  /* Local exposure's grid, read filtered between tiles. See `localExposure.ts`. */
7059
7320
  { binding: RUSH_TEXTURES.uExposureLocal },
7321
+ /* Each mover's own motion: the reconstruction's target, or the scene where there is none. */
7322
+ { binding: RUSH_TEXTURES.uMotion },
7060
7323
  ]);
7061
7324
  this.rushUniforms = device.createBuffer({
7062
7325
  label: 'post.rushUniforms',
@@ -7074,11 +7337,29 @@ export class WebGPURenderer implements RendererApi {
7074
7337
  this.ssrLayout = createPostStageLayout(device, SSR_UNIFORMS, SSR_FRAG_SIZE, [
7075
7338
  { binding: SSR_TEXTURES.uSsrDepth, filterable: false },
7076
7339
  { binding: SSR_TEXTURES.uSsrScene },
7340
+ /* The frame's materials, for the trace that reads them: `frameReflections.ts`. */
7341
+ { binding: SSR_TEXTURES.uSsrProbeMap },
7342
+ { binding: SSR_TEXTURES.uSsrTintMap },
7077
7343
  ]);
7078
7344
  this.ssrResolveLayout = createSsrResolveLayout(device);
7345
+ this.ssrMaterialLayout = createPostStageLayout(
7346
+ device,
7347
+ SSR_MATERIAL_UNIFORMS,
7348
+ SSR_MATERIAL_SIZE,
7349
+ [
7350
+ { binding: SSR_MATERIAL_TEXTURES.uSsrReflection },
7351
+ { binding: SSR_MATERIAL_TEXTURES.uSsrProbeMap },
7352
+ { binding: SSR_MATERIAL_TEXTURES.uSsrTintMap },
7353
+ ],
7354
+ );
7355
+ this.ssrMaterialUniforms = device.createBuffer({
7356
+ label: 'post.ssrMaterialUniforms',
7357
+ size: SSR_MATERIAL_SIZE,
7358
+ usage: USAGE_UNIFORM_DST,
7359
+ });
7079
7360
  this.ssrUniforms = device.createBuffer({
7080
7361
  label: 'post.ssrUniforms',
7081
- size: SSR_SLOT * MAX_REFLECTIVE_SURFACES,
7362
+ size: SSR_SLOT * (MAX_REFLECTIVE_SURFACES + 1),
7082
7363
  usage: USAGE_UNIFORM_DST,
7083
7364
  });
7084
7365
  this.decalUniforms = device.createBuffer({
@@ -7782,7 +8063,11 @@ export class WebGPURenderer implements RendererApi {
7782
8063
  /* Last frame's snapshot is last frame's scene. */
7783
8064
  this.refractSnapshotTaken = false;
7784
8065
  this.skinScattered = false;
8066
+ /* One frame of the jitter's sequence however many times the frame binds its camera. */
8067
+ this.temporalHistory.nextFrame();
8068
+ this.latePasses.length = 0;
7785
8069
  this.skinScatter?.reset();
8070
+ this.reflectionSurface?.reset();
7786
8071
  this.plumeVerts.reset();
7787
8072
  this.plumeFrags.reset();
7788
8073
  this.waterVerts.reset();
@@ -8217,7 +8502,7 @@ export class WebGPURenderer implements RendererApi {
8217
8502
  * the viewer sees, and bounding it through a mirrored camera puts it in the wrong half of the
8218
8503
  * screen.
8219
8504
  */
8220
- if (!this.reflectionPassActive) {
8505
+ if (!this.reflectionPassActive && !this.capturePassActive) {
8221
8506
  mat4.copy(this.frameRawViewProj, camera.viewProjection);
8222
8507
  /* The eye in world space, which the resolve's normals are turned to face. */
8223
8508
  this.reconEye.set(camera.position);
@@ -8251,7 +8536,8 @@ export class WebGPURenderer implements RendererApi {
8251
8536
  * second temporal frame stepped the sequence twice a frame, so the world saw every other
8252
8537
  * position and the anti-flicker's period never started.
8253
8538
  */
8254
- if (this.framePresented) this.viewProj = this.correctedViewProj;
8539
+ /* A capture has no resolve to undo a jitter and is no frame of the sequence. */
8540
+ if (this.framePresented || this.capturePassActive) this.viewProj = this.correctedViewProj;
8255
8541
  else this.jitterFrame();
8256
8542
  /*
8257
8543
  * The frustum, once a frame, and from the **uncorrected** matrix.
@@ -8391,6 +8677,9 @@ export class WebGPURenderer implements RendererApi {
8391
8677
  f[at('uWriteMode') + 1] = 0;
8392
8678
  /* 1, not 0: a multiplier's identity is one. See `setEnvironmentGain`. */
8393
8679
  f[at('uEnvironmentDials') + 2] = 1;
8680
+ /* No overlay until a draw sets one. See `setSurfaceOverlay`. */
8681
+ this.overlayFloats.fill(0);
8682
+ f.fill(0, at('uOverlay'), at('uOverlay') + OVERLAY_FLOATS);
8394
8683
  /* The frame's own ambient until a draw asks for its own. See `setAmbientSH`. */
8395
8684
  f.fill(0, at('uAmbientSH'), at('uAmbientSH') + AMBIENT_SH_FLOATS);
8396
8685
  /*
@@ -8593,7 +8882,8 @@ export class WebGPURenderer implements RendererApi {
8593
8882
  * uniform added on one backend is bound on both in the same change.
8594
8883
  */
8595
8884
  scatterInto(f, this.fragment.fields['uLightDir'], lights.directions, 3);
8596
- scatterInto(f, this.fragment.fields['uLightCone'], lights.coneCos, 2);
8885
+ /* The cone and, in z, the light's own falloff exponent. See `conesAndFalloffs`. */
8886
+ scatterInto(f, this.fragment.fields['uLightCone'], lights.conesAndFalloffs, 4);
8597
8887
  scatterInto(f, this.fragment.fields['uLightIesProfile'], lights.iesProfiles, 1);
8598
8888
  /* The azimuth and, in w, the light's channels. See `axesAndChannels`. */
8599
8889
  scatterInto(f, this.fragment.fields['uLightIesAxis'], lights.axesAndChannels, 4);
@@ -8615,7 +8905,9 @@ export class WebGPURenderer implements RendererApi {
8615
8905
  scatterInto(f, this.fragment.fields['uAreaLightColor'], area.colors, 3);
8616
8906
  scatterInto(f, this.fragment.fields['uAreaLightRight'], area.right, 3);
8617
8907
  scatterInto(f, this.fragment.fields['uAreaLightUp'], area.up, 3);
8618
- scatterInto(f, this.fragment.fields['uAreaLightSize'], area.sizes, 2);
8908
+ /* The half extents and, in zw, the barn doors. See `packAreaSizesAndDoors`. */
8909
+ packAreaSizesAndDoors(area, this.areaSizesAndDoors);
8910
+ scatterInto(f, this.fragment.fields['uAreaLightSize'], this.areaSizesAndDoors, 4);
8619
8911
  scatterInto(f, this.fragment.fields['uAreaLightTwoSided'], area.twoSided, 1);
8620
8912
  }
8621
8913
 
@@ -8780,6 +9072,8 @@ export class WebGPURenderer implements RendererApi {
8780
9072
  this.emissiveMap = null;
8781
9073
  this.modelMap = null;
8782
9074
  this.bindGroup = this.blankAlbedoBindGroup;
9075
+ /* Nor an overlay; its numbers are zeroed with the block's other defaults. */
9076
+ this.overlayMaps = null;
8783
9077
  this.uvScale[0] = 1;
8784
9078
  this.uvScale[1] = 1;
8785
9079
  this.uvOffset[0] = 0;
@@ -8867,6 +9161,9 @@ export class WebGPURenderer implements RendererApi {
8867
9161
  * where it happened to pass, write motion for a surface no pixel of the frame belongs to.
8868
9162
  */
8869
9163
  if (this.tracksMotionNow()) this.recordDrawMotion(mesh, model, previousModel);
9164
+ else if (previousModel !== null && this.quality.cameraMotionBlur * this.motionBlurScale > 0) {
9165
+ this.warnObjectBlur();
9166
+ }
8870
9167
  noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
8871
9168
  /* An opaque draw meets the medium as the dial says: `submitMesh` reads an absent `fog` as
8872
9169
  true, which is a translucent draw's default and would put every opaque draw back in the fog
@@ -9286,6 +9583,26 @@ export class WebGPURenderer implements RendererApi {
9286
9583
  material,
9287
9584
  );
9288
9585
  }
9586
+ /* And its surface half, for the frame's materials' reflections. See `keepReflectionSurface`. */
9587
+ if (!blend && lit && depthWrite && this.reflectionSurfaceKeeps()) {
9588
+ this.keepReflectionSurface(
9589
+ mesh,
9590
+ key,
9591
+ base,
9592
+ skinned,
9593
+ morphed,
9594
+ deltas,
9595
+ layer,
9596
+ eight,
9597
+ clothed,
9598
+ modelKind,
9599
+ slot,
9600
+ material,
9601
+ null,
9602
+ 1,
9603
+ null,
9604
+ );
9605
+ }
9289
9606
  }
9290
9607
  /* Back to the defaults for whatever is drawn next, exactly as `renderer.ts` restores them —
9291
9608
  unconditionally, because the scratch state above is shared by every draw after this one
@@ -9437,6 +9754,7 @@ export class WebGPURenderer implements RendererApi {
9437
9754
  const refraction = options.refraction ?? 0;
9438
9755
  const glassy = resolveGlass(options.glass, this.glassScratch);
9439
9756
  if ((refraction <= 0 && !glassy) || !this.takeRefractSnapshot()) return false;
9757
+ this.seeThroughBound = true;
9440
9758
  const glass = this.glassScratch;
9441
9759
  /* Strength, path thickness, transmission, frost: one vector, for the budget the shader states. */
9442
9760
  const seeAt = this.materialField('uSeeThrough');
@@ -9458,6 +9776,7 @@ export class WebGPURenderer implements RendererApi {
9458
9776
 
9459
9777
  /** Put the scratch back after `bindSeeThrough`: refraction and glass off. */
9460
9778
  private releaseSeeThrough(): void {
9779
+ this.seeThroughBound = false;
9461
9780
  const seeAt = this.materialField('uSeeThrough');
9462
9781
  this.perFrameFloats[seeAt] = 0;
9463
9782
  this.perFrameFloats[seeAt + 2] = 0;
@@ -9658,6 +9977,26 @@ export class WebGPURenderer implements RendererApi {
9658
9977
  if (cut === null) pass.drawIndexed(mesh.indexCount, count);
9659
9978
  else pass.drawIndexedIndirect(cut.args, 0);
9660
9979
  }
9980
+ /* And its surface half, for the frame's materials' reflections, with the same survivors. */
9981
+ if (!blend && lit && depthWrite && this.reflectionSurfaceKeeps()) {
9982
+ this.keepReflectionSurface(
9983
+ mesh,
9984
+ key,
9985
+ base,
9986
+ false,
9987
+ false,
9988
+ null,
9989
+ layer,
9990
+ false,
9991
+ false,
9992
+ modelKind,
9993
+ slot,
9994
+ material,
9995
+ instanceBuffer,
9996
+ count,
9997
+ cut === null ? null : cut.args,
9998
+ );
9999
+ }
9661
10000
  }
9662
10001
  /* Back to the defaults, unconditionally and for the reason `drawMesh` gives at length: this
9663
10002
  scratch block is cumulative state shared by every draw after this one, and an early return
@@ -10365,6 +10704,20 @@ export class WebGPURenderer implements RendererApi {
10365
10704
  private warnedNoPointShadowArray = false;
10366
10705
  /** Said once, for the retired ambient dial. */
10367
10706
  private warnedEnvironmentAmbient = false;
10707
+ /** The overlay the draws wear, `uOverlay`: see `setSurfaceOverlay`. */
10708
+ private readonly overlayFloats = new Float32Array(OVERLAY_FLOATS);
10709
+ /** Its atlas, bound where the refraction copy goes in a twin of each flat group. */
10710
+ private overlayMaps: GpuSurfaceTexture | null = null;
10711
+ /**
10712
+ * Each flat group's twin with the atlas in the refraction slot, by the group and the atlas, built
10713
+ * on the first draw that wears it and dropped wherever `flatBindGroups` is.
10714
+ */
10715
+ private readonly overlayGroups = new Map<GPUBindGroup, Map<GpuSurfaceTexture, GPUBindGroup>>();
10716
+ /** True while a twin is built: what `flatTextures` hands the refraction slot then. */
10717
+ private overlayBinding = false;
10718
+ /** True between `bindSeeThrough` and `releaseSeeThrough`: that draw reads the copy, not the atlas. */
10719
+ private seeThroughBound = false;
10720
+ private warnedOverlayRefraction = false;
10368
10721
  private readonly castingWorldIndices = new Int32Array(POINT_SHADOW_POOL).fill(-1);
10369
10722
  private readonly resolvedPointShadows = createResolvedPointShadows(MAX_POINT_LIGHTS);
10370
10723
  /** One face's view-projection, rebuilt per face rather than allocated. */
@@ -10813,6 +11166,43 @@ export class WebGPURenderer implements RendererApi {
10813
11166
  );
10814
11167
  }
10815
11168
 
11169
+ /**
11170
+ * The open material's flat group, or its twin with the surface overlay's atlas in the refraction
11171
+ * slot where the draw wears one and does not refract. A twin is built from the live material,
11172
+ * which `bindGroup` always is, and kept by the group and the atlas.
11173
+ *
11174
+ * **A draw that refracts, or is glass, keeps the frame's copy** and its overlay reads no image —
11175
+ * the shader asks the same of `uSeeThrough` (`overlayImagesBound`) — said once.
11176
+ */
11177
+ private overlayGroup(): GPUBindGroup {
11178
+ const maps = this.overlayMaps;
11179
+ if (maps === null) return this.bindGroup;
11180
+ if (this.seeThroughBound) {
11181
+ if (!this.warnedOverlayRefraction) {
11182
+ this.warnedOverlayRefraction = true;
11183
+ console.warn(
11184
+ 'WebGPU: a draw that refracts or is glass wears a surface overlay with maps. The frame ' +
11185
+ "copy it reads takes the overlay's slot, so its rim, dissolve and wrinkles read no image.",
11186
+ );
11187
+ }
11188
+ return this.bindGroup;
11189
+ }
11190
+ let byMaps = this.overlayGroups.get(this.bindGroup);
11191
+ if (byMaps === undefined) {
11192
+ byMaps = new Map();
11193
+ this.overlayGroups.set(this.bindGroup, byMaps);
11194
+ }
11195
+ let twin = byMaps.get(maps);
11196
+ if (twin === undefined) {
11197
+ this.overlayBinding = true;
11198
+ twin = this.buildFlatBindGroup();
11199
+ this.overlayBinding = false;
11200
+ this.bindGroupBudget.ask();
11201
+ byMaps.set(maps, twin);
11202
+ }
11203
+ return twin;
11204
+ }
11205
+
10816
11206
  /**
10817
11207
  * The bind group a draw uses, skinned or not.
10818
11208
  *
@@ -10829,7 +11219,8 @@ export class WebGPURenderer implements RendererApi {
10829
11219
  morphed: boolean,
10830
11220
  deltas: GPUTextureView | null,
10831
11221
  ): GPUBindGroup {
10832
- if (!skinned && !morphed) return this.bindGroup;
11222
+ const base = this.overlayGroup();
11223
+ if (!skinned && !morphed) return base;
10833
11224
  const palette = skinned ? this.skinPalettes.view(this.skinPaletteSlot) : null;
10834
11225
  /* Slot `k` caches at `k + 1`; index 0 is the morphed draw that is not skinned and has no
10835
11226
  slot of its own. See `skinnedGroups`. */
@@ -10838,12 +11229,14 @@ export class WebGPURenderer implements RendererApi {
10838
11229
  const cloth = skinned ? this.clothViews() : null;
10839
11230
  if (
10840
11231
  (this.skinnedGroups[at] ?? null) === null ||
10841
- this.skinnedGroupsBeside[at] !== this.bindGroup ||
11232
+ this.skinnedGroupsBeside[at] !== base ||
10842
11233
  this.skinnedGroupsPalette[at] !== palette ||
10843
11234
  this.skinnedGroupsDeltas[at] !== deltas ||
10844
11235
  this.skinnedGroupsCloth[at] !== (cloth?.particles ?? null) ||
10845
11236
  this.skinnedGroupsClothBinding[at] !== (cloth?.binding ?? null)
10846
11237
  ) {
11238
+ /* Built from the live material, with the overlay's atlas where `base` is its twin. */
11239
+ this.overlayBinding = base !== this.bindGroup;
10847
11240
  this.skinnedGroups[at] = createFlatBindGroup(
10848
11241
  this.surface.device,
10849
11242
  this.flatLayoutFor(skinned, morphed),
@@ -10855,7 +11248,8 @@ export class WebGPURenderer implements RendererApi {
10855
11248
  deltas,
10856
11249
  cloth,
10857
11250
  );
10858
- this.skinnedGroupsBeside[at] = this.bindGroup;
11251
+ this.overlayBinding = false;
11252
+ this.skinnedGroupsBeside[at] = base;
10859
11253
  this.skinnedGroupsPalette[at] = palette;
10860
11254
  this.skinnedGroupsDeltas[at] = deltas;
10861
11255
  this.skinnedGroupsCloth[at] = cloth?.particles ?? null;
@@ -10921,6 +11315,7 @@ export class WebGPURenderer implements RendererApi {
10921
11315
  */
10922
11316
  private rebuildFlatGroupsForNewRings(): void {
10923
11317
  this.flatBindGroups.clear();
11318
+ this.overlayGroups.clear();
10924
11319
  this.flatEnvironment = this.flatTextures('uEnvironment').view;
10925
11320
  this.skinnedGroups.length = 0;
10926
11321
 
@@ -10984,6 +11379,7 @@ export class WebGPURenderer implements RendererApi {
10984
11379
  */
10985
11380
  private rebuildFlatBindGroup(): void {
10986
11381
  this.flatBindGroups.clear();
11382
+ this.overlayGroups.clear();
10987
11383
  this.flatEnvironment = this.flatTextures('uEnvironment').view;
10988
11384
  if (
10989
11385
  this.albedo === null &&
@@ -11225,9 +11621,14 @@ export class WebGPURenderer implements RendererApi {
11225
11621
  ? (OUTPUT_TRANSFORM_CODE[this.quality.outputTransform] ?? 0)
11226
11622
  : 0;
11227
11623
  f[at('uOutputExposure')] = graded ? this.exposure : 1;
11624
+ f.set(this.filmA, at('uFilmA'));
11625
+ f.set(this.filmB, at('uFilmB'));
11626
+ f[at('uDisplayHeadroom')] = this.headroom;
11228
11627
  f.set(this.reprojection, at('uReprojection'));
11229
11628
  f[at('uMotionStrength')] = motionStrength;
11230
- f[at('uMotionMax')] = MOTION_BLUR_MAX_UV;
11629
+ f[at('uMotionMax')] = this.motionBlurMax;
11630
+ /* Each mover's own motion, where the reconstruction drew its target this frame. */
11631
+ f[at('uObjectMotion')] = this.reconstructing && this.reconMotionView !== null ? 1 : 0;
11231
11632
  f[at('uAoStrength')] = this.frameProjection === null ? 0 : aoStrength;
11232
11633
  /*
11233
11634
  * The occlusion read where this frame's jitter put it (`uAoOffset`): measured from the jittered
@@ -11271,6 +11672,7 @@ export class WebGPURenderer implements RendererApi {
11271
11672
  f[at('uGradeSize')] = this.gradeSize;
11272
11673
  /* The lens and the print. See `setVignette` and `setFilmGrain`. */
11273
11674
  f[at('uVignette')] = this.vignette;
11675
+ f.set(this.fringe, at('uFringe'));
11274
11676
  f[at('uAutoExposure')] = adapting ? this.autoExposure : 0;
11275
11677
  f[at('uLocalExposure')] = measuring && localising ? this.localExposure : 0;
11276
11678
  f[at('uGrain')] = this.grain;
@@ -11316,7 +11718,11 @@ export class WebGPURenderer implements RendererApi {
11316
11718
  private rebuildRushBindGroup(): void {
11317
11719
  if (this.sceneColorView === null || this.resolvedDepthView === null) return;
11318
11720
  if (this.aoTargetView === null) return;
11319
- this.rushBindGroup = this.buildRushBindGroup('post.rushBindGroup', this.sceneColorView);
11721
+ this.rushBindGroup = this.buildRushBindGroup(
11722
+ 'post.rushBindGroup',
11723
+ this.sceneColorView,
11724
+ this.sceneColorView,
11725
+ );
11320
11726
  /*
11321
11727
  * **And the reconstruction's twin, from the same three call sites.** A colour grade replaces
11322
11728
  * the lookup table and a resize replaces the depth and the occlusion target, and both groups
@@ -11324,12 +11730,21 @@ export class WebGPURenderer implements RendererApi {
11324
11730
  * come to read a stale view on exactly the frames a consumer changed something.
11325
11731
  */
11326
11732
  if (this.reconShownView !== null) {
11327
- this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
11733
+ this.reconRushGroup = this.buildRushBindGroup(
11734
+ 'recon.rushBindGroup',
11735
+ this.reconShownView,
11736
+ this.reconMotionView ?? this.reconShownView,
11737
+ );
11328
11738
  }
11329
11739
  }
11330
11740
 
11331
11741
  /** The composite's group over one scene source. See `rebuildRushBindGroup`. */
11332
- private buildRushBindGroup(label: string, sceneView: GPUTextureView): GPUBindGroup | null {
11742
+ private buildRushBindGroup(
11743
+ label: string,
11744
+ sceneView: GPUTextureView,
11745
+ /** Each mover's own motion, or a stand-in the shader is told not to read. */
11746
+ motionView: GPUTextureView,
11747
+ ): GPUBindGroup | null {
11333
11748
  const { device } = this.surface;
11334
11749
  if (this.resolvedDepthView === null || this.aoTargetView === null) return null;
11335
11750
  const entry = (
@@ -11361,6 +11776,7 @@ export class WebGPURenderer implements RendererApi {
11361
11776
  and a declared binding still needs a complete texture, as the bloom fallback says. */
11362
11777
  ...entry(RUSH_TEXTURES.uExposureHeld, this.exposurePass?.heldView ?? sceneView),
11363
11778
  ...entry(RUSH_TEXTURES.uExposureLocal, this.exposurePass?.localView ?? sceneView),
11779
+ ...entry(RUSH_TEXTURES.uMotion, motionView),
11364
11780
  ],
11365
11781
  layout: this.rushLayout,
11366
11782
  });
@@ -11644,7 +12060,11 @@ export class WebGPURenderer implements RendererApi {
11644
12060
  );
11645
12061
  }
11646
12062
  /* The composite's own group, over the resolved picture instead of the scene target. */
11647
- this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
12063
+ this.reconRushGroup = this.buildRushBindGroup(
12064
+ 'recon.rushBindGroup',
12065
+ this.reconShownView,
12066
+ this.reconMotionView ?? this.reconShownView,
12067
+ );
11648
12068
  /* And the scene's, because `ensureComposite` built it before this one existed. */
11649
12069
  this.rebuildRushBindGroup();
11650
12070
  /* And the bloom's, which blooms the resolved picture: see `rebuildBloomGroups`. */
@@ -11723,7 +12143,7 @@ export class WebGPURenderer implements RendererApi {
11723
12143
  routesLate(
11724
12144
  this.reconstructing,
11725
12145
  this.reflectionPassActive,
11726
- this.probePassActive,
12146
+ this.probePassActive || this.capturePassActive,
11727
12147
  this.framePresented,
11728
12148
  )
11729
12149
  );
@@ -11758,7 +12178,7 @@ export class WebGPURenderer implements RendererApi {
11758
12178
  const depth = this.depthView;
11759
12179
  if (!this.reconstructing || late === null || shown === null || depth === null) return;
11760
12180
  const oitSet = this.oitActive && this.translucentQueue.length > 0;
11761
- if (late.pending === 0 && !oitSet) return;
12181
+ if (late.pending === 0 && !oitSet && this.latePasses.length === 0) return;
11762
12182
  const width = Math.max(1, this.surface.canvas.width);
11763
12183
  const height = Math.max(1, this.surface.canvas.height);
11764
12184
  /* The order-independent set tests against this depth too, so it is made for either. */
@@ -11782,20 +12202,64 @@ export class WebGPURenderer implements RendererApi {
11782
12202
  this.refractSnapshotTaken = true;
11783
12203
  }
11784
12204
  this.lateRefractWanted = false;
11785
- if (late.pending === 0) return;
11786
- const pass = encoder.beginRenderPass({
11787
- label: 'recon.late',
11788
- timestampWrites: this.gpuTimer.writesFor(),
11789
- colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
11790
- depthStencilAttachment: {
11791
- view: late.depthView(width, height),
11792
- depthLoadOp: 'load',
11793
- /* Kept: the order-independent set replays against it after this pass. */
11794
- depthStoreOp: 'store',
11795
- },
11796
- });
11797
- late.replay(pass, this.issueBound);
11798
- pass.end();
12205
+ if (late.pending > 0) {
12206
+ const pass = encoder.beginRenderPass({
12207
+ label: 'recon.late',
12208
+ timestampWrites: this.gpuTimer.writesFor(),
12209
+ colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
12210
+ depthStencilAttachment: {
12211
+ view: late.depthView(width, height),
12212
+ depthLoadOp: 'load',
12213
+ /* Kept: the order-independent set replays against it after this pass. */
12214
+ depthStoreOp: 'store',
12215
+ },
12216
+ });
12217
+ late.replay(pass, this.issueBound);
12218
+ pass.end();
12219
+ }
12220
+ this.runLatePasses(encoder, shown, late.depthView(width, height));
12221
+ }
12222
+
12223
+ /**
12224
+ * The registered passes `drawPass` queued for after the upscale: each handed a fresh copy of the
12225
+ * reconstructed picture with every late draw in it, in the output-size texture the late
12226
+ * refraction keeps (its own reads are done by now: they were recorded before this copy), then
12227
+ * drawn over the picture in a pass of its own.
12228
+ */
12229
+ private runLatePasses(
12230
+ encoder: GPUCommandEncoder,
12231
+ shown: GPUTextureView,
12232
+ depth: GPUTextureView,
12233
+ ): void {
12234
+ const copy = this.refractSnapshot;
12235
+ const source = this.reconShown;
12236
+ for (const handle of this.latePasses) {
12237
+ const definition = passAt(this.passes, handle);
12238
+ if (definition === undefined) continue;
12239
+ if (copy !== null && source !== null) {
12240
+ encoder.copyTextureToTexture({ texture: source }, { texture: copy }, [
12241
+ copy.width,
12242
+ copy.height,
12243
+ ]);
12244
+ }
12245
+ const pass = encoder.beginRenderPass({
12246
+ label: 'recon.latePass',
12247
+ timestampWrites: this.gpuTimer.writesFor(),
12248
+ colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
12249
+ depthStencilAttachment: { view: depth, depthLoadOp: 'load', depthStoreOp: 'store' },
12250
+ });
12251
+ const context = this.passContext;
12252
+ context.pass = pass;
12253
+ context.outputTransform = this.gradeCode();
12254
+ context.outputExposure = this.gradeExposure();
12255
+ context.jitter = this.latePassJitter;
12256
+ context.format = RECON_HISTORY_FORMAT;
12257
+ context.depthFormat = DEPTH_FORMAT;
12258
+ context.samples = 1;
12259
+ definition.draw(context as PassContext);
12260
+ context.jitter = this.passJitter;
12261
+ pass.end();
12262
+ }
11799
12263
  }
11800
12264
 
11801
12265
  /** Whether this draw lands in the reconstructed picture. See `lateRouting.ts`. */
@@ -11803,7 +12267,7 @@ export class WebGPURenderer implements RendererApi {
11803
12267
  return tracksMotion(
11804
12268
  this.reconstructing,
11805
12269
  this.reflectionPassActive,
11806
- this.probePassActive,
12270
+ this.probePassActive || this.capturePassActive,
11807
12271
  this.framePresented,
11808
12272
  );
11809
12273
  }
@@ -12222,7 +12686,10 @@ export class WebGPURenderer implements RendererApi {
12222
12686
  * other is the disagreement the parity rule exists to prevent. Said once rather than dropped.
12223
12687
  */
12224
12688
  private runReflections(encoder: GPUCommandEncoder): void {
12225
- if (this.reflectionQueue.length === 0) return;
12689
+ const frameReflections = this.frameReflections;
12690
+ const surfaceHalf = this.reflectionSurface;
12691
+ const materials = frameReflections !== null && surfaceHalf !== null;
12692
+ if (this.reflectionQueue.length === 0 && !materials) return;
12226
12693
  const scene = this.sceneColorView;
12227
12694
  const reflection = this.ssrReflectionView;
12228
12695
  const group = this.ssrBindGroup;
@@ -12260,14 +12727,88 @@ export class WebGPURenderer implements RendererApi {
12260
12727
  f.set(surface.axis, base + at('uSsrAxis'));
12261
12728
  f.set(surface.tint, base + at('uSsrTint'));
12262
12729
  f[base + at('uSsrStrength')] = surface.strength;
12730
+ f[base + at('uSsrFresnel')] = surface.fresnel ? 1 : 0;
12731
+ f[base + at('uSsrFresnel') + 1] = surface.roughness;
12263
12732
  f[base + at('uSsrFacingCos')] = surface.facingCos;
12264
12733
  f[base + at('uSsrReach')] = surface.reachM;
12265
12734
  f[base + at('uSsrThickness')] = surface.thicknessM;
12266
12735
  f[base + at('uSsrSteps')] = surface.steps;
12267
12736
  f[base + at('uSsrEdgeFade')] = REFLECTION_EDGE_FADE;
12737
+ f[base + at('uSsrMaterial')] = 0;
12268
12738
  slot++;
12269
12739
  });
12740
+ const regions = slot;
12741
+ /* The frame's materials' trace takes the slot after the surfaces': the box, its axis and its
12742
+ strength unread, the march and the ceiling its own. See `frameReflections.ts`. */
12743
+ if (materials) {
12744
+ const base = (slot * SSR_SLOT) / 4;
12745
+ f.set(this.ssrDepthToWorld, base + at('uSsrDepthToWorld'));
12746
+ f.set(this.ssrViewProj, base + at('uSsrViewProj'));
12747
+ f.set(this.frameEye, base + at('uSsrEye'));
12748
+ f.fill(0, base + at('uSsrAxis'), base + at('uSsrAxis') + 3);
12749
+ f.fill(1, base + at('uSsrTint'), base + at('uSsrTint') + 3);
12750
+ f[base + at('uSsrStrength')] = 1;
12751
+ f[base + at('uSsrFresnel')] = 0;
12752
+ f[base + at('uSsrFacingCos')] = -1;
12753
+ f[base + at('uSsrReach')] = frameReflections.reachM;
12754
+ f[base + at('uSsrThickness')] = frameReflections.thicknessM;
12755
+ f[base + at('uSsrSteps')] = frameReflections.steps;
12756
+ f[base + at('uSsrEdgeFade')] = REFLECTION_EDGE_FADE;
12757
+ f[base + at('uSsrMaterial')] = 1;
12758
+ f[base + at('uSsrMaxRoughness')] = frameReflections.maxRoughness;
12759
+ slot++;
12760
+ }
12270
12761
  device.queue.writeBuffer(this.ssrUniforms, 0, this.ssrStaging, 0, slot * SSR_SLOT);
12762
+ const tracePipeline = postPipeline(
12763
+ this.pipelines,
12764
+ device,
12765
+ this.ssrLayout,
12766
+ 'post.ssrTrace',
12767
+ SSR_TRACE_FRAG_WGSL,
12768
+ this.pipelines.format,
12769
+ PREMULTIPLIED_OVER_BLEND,
12770
+ );
12771
+
12772
+ if (materials && this.depthView !== null && this.ssrMaterialGroup !== null) {
12773
+ /* The frame's opaque lit draws again, into what the trace and the resolve read. */
12774
+ surfaceHalf.draw(encoder, this.depthView, this.issueBound);
12775
+ const trace = encoder.beginRenderPass({
12776
+ label: 'post.ssr.materialTrace',
12777
+ colorAttachments: [
12778
+ { view: reflection, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
12779
+ ],
12780
+ });
12781
+ trace.setPipeline(tracePipeline);
12782
+ trace.setBindGroup(0, group, [regions * SSR_SLOT]);
12783
+ trace.draw(3);
12784
+ trace.end();
12785
+ /* The blur's texel and its radius at roughness 1, as a share of the frame's height. */
12786
+ const m = this.ssrMaterialFloats;
12787
+ const blurAt = this.postField(SSR_MATERIAL_FIELDS, 'uSsrBlur');
12788
+ m[blurAt] = 1 / Math.max(width, 1);
12789
+ m[blurAt + 1] = 1 / Math.max(height, 1);
12790
+ m[blurAt + 2] = frameReflections.blur * height;
12791
+ device.queue.writeBuffer(this.ssrMaterialUniforms, 0, m);
12792
+ const resolve = encoder.beginRenderPass({
12793
+ label: 'post.ssr.materialResolve',
12794
+ colorAttachments: [{ view: scene, loadOp: 'load', storeOp: 'store' }],
12795
+ });
12796
+ resolve.setPipeline(
12797
+ postPipeline(
12798
+ this.pipelines,
12799
+ device,
12800
+ this.ssrMaterialLayout,
12801
+ 'post.ssrMaterialResolve',
12802
+ SSR_MATERIAL_RESOLVE_FRAG_WGSL,
12803
+ this.pipelines.format,
12804
+ FRAME_REFLECTION_BLEND,
12805
+ ),
12806
+ );
12807
+ resolve.setBindGroup(0, this.ssrMaterialGroup, [0]);
12808
+ resolve.draw(3);
12809
+ resolve.end();
12810
+ }
12811
+ if (regions === 0) return;
12271
12812
 
12272
12813
  /* Cleared whole and then scissored per surface: the resolve reads every pixel, and one left
12273
12814
  from the last frame would composite that reflection over this frame's scene. */
@@ -12277,17 +12818,7 @@ export class WebGPURenderer implements RendererApi {
12277
12818
  { view: reflection, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
12278
12819
  ],
12279
12820
  });
12280
- trace.setPipeline(
12281
- postPipeline(
12282
- this.pipelines,
12283
- device,
12284
- this.ssrLayout,
12285
- 'post.ssrTrace',
12286
- SSR_TRACE_FRAG_WGSL,
12287
- this.pipelines.format,
12288
- PREMULTIPLIED_OVER_BLEND,
12289
- ),
12290
- );
12821
+ trace.setPipeline(tracePipeline);
12291
12822
  slot = 0;
12292
12823
  this.reflectionQueue.replay((surface) => {
12293
12824
  const index = slot++;
@@ -12963,7 +13494,7 @@ export class WebGPURenderer implements RendererApi {
12963
13494
  * glass, and a frame that refracts nothing breaks no pass at all.
12964
13495
  */
12965
13496
  private takeRefractSnapshot(): boolean {
12966
- if (this.reflectionPassActive || this.probePassActive) return false;
13497
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive) return false;
12967
13498
  if (this.refractSnapshotTaken) return true;
12968
13499
  /* A late draw refracts the reconstructed picture, which does not exist until the resolve has
12969
13500
  run; `runLate` takes the copy then, once, before any late draw is replayed. */
@@ -13044,12 +13575,97 @@ export class WebGPURenderer implements RendererApi {
13044
13575
  !this.skinScattered &&
13045
13576
  !this.reflectionPassActive &&
13046
13577
  !this.probePassActive &&
13578
+ !this.capturePassActive &&
13579
+ !this.framePresented &&
13580
+ !this.overlayActive &&
13581
+ this.oitMode === 'none'
13582
+ );
13583
+ }
13584
+
13585
+ /** Whether a draw now is the frame's own, and so has a surface half to keep. */
13586
+ private reflectionSurfaceKeeps(): boolean {
13587
+ return (
13588
+ this.reflectionSurface !== null &&
13589
+ !this.reflectionPassActive &&
13590
+ !this.probePassActive &&
13591
+ !this.capturePassActive &&
13047
13592
  !this.framePresented &&
13048
13593
  !this.overlayActive &&
13049
13594
  this.oitMode === 'none'
13050
13595
  );
13051
13596
  }
13052
13597
 
13598
+ /**
13599
+ * An opaque lit draw's surface half, kept for the frame's materials' reflections: the draw again,
13600
+ * with the same slots, buffers and bind group, through the pipeline with `REFLECTION_SURFACE` on —
13601
+ * which writes what the pixel reflects of its environment into the two targets
13602
+ * `reflectionSurfacePass.ts` holds and tests for the depth its frame draw wrote. Replayed when the
13603
+ * frame's pass has ended, before the trace reads them. See `shaders/flat/reflectionSurface.ts`.
13604
+ */
13605
+ private keepReflectionSurface(
13606
+ mesh: GpuMesh,
13607
+ frameKey: string,
13608
+ base: string,
13609
+ skinned: boolean,
13610
+ morphed: boolean,
13611
+ deltas: GPUTextureView | null,
13612
+ layer: number,
13613
+ eight: boolean,
13614
+ clothed: boolean,
13615
+ modelKind: SurfaceModelKind | null,
13616
+ slot: number,
13617
+ material: number,
13618
+ instanceBuffer: GPUBuffer | null,
13619
+ instances: number,
13620
+ indirect: GPUBuffer | null,
13621
+ ): void {
13622
+ const surface = this.reflectionSurface;
13623
+ const present = this.meshPresent.get(base);
13624
+ if (surface === null || present === undefined) return;
13625
+ const key = `${frameKey}|rs`;
13626
+ const pipeline =
13627
+ this.pipelines.peek(key) ??
13628
+ flatPipeline(
13629
+ this.pipelines,
13630
+ this.surface.device,
13631
+ this.flatLayoutFor(skinned, morphed),
13632
+ this.variant,
13633
+ key,
13634
+ present,
13635
+ false,
13636
+ skinned,
13637
+ morphed,
13638
+ true,
13639
+ layer,
13640
+ instanceBuffer !== null,
13641
+ 'none',
13642
+ this.materialDoubleSided,
13643
+ false,
13644
+ eight,
13645
+ clothed,
13646
+ modelKind,
13647
+ 'whole',
13648
+ true,
13649
+ );
13650
+ const command = surface.take();
13651
+ command.pipeline = pipeline;
13652
+ command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
13653
+ command.offsetA = slot;
13654
+ command.offsetB = material;
13655
+ command.offsetCount = 2;
13656
+ let index = 0;
13657
+ for (; index < mesh.vertexBuffers.length; index++) {
13658
+ command.vertexBuffers[index] = mesh.vertexBuffers[index] as GPUBuffer;
13659
+ }
13660
+ if (instanceBuffer !== null) command.vertexBuffers[index++] = instanceBuffer;
13661
+ command.vertexCount = index;
13662
+ command.indexBuffer = mesh.indexBuffer;
13663
+ command.indexed = true;
13664
+ command.count = mesh.indexCount;
13665
+ command.instances = instances;
13666
+ command.indirect = indirect;
13667
+ }
13668
+
13053
13669
  /**
13054
13670
  * A skin draw's diffuse and colour halves, kept for the spread: the frame's draw again, twice,
13055
13671
  * with the same slots, buffers and bind group, through the pipelines that write the diffuse's
@@ -13129,10 +13745,17 @@ export class WebGPURenderer implements RendererApi {
13129
13745
  private runSkinScatter(): void {
13130
13746
  const skin = this.skinScatter;
13131
13747
  if (skin === null || this.skinScattered) return;
13748
+ /*
13749
+ * **Not the frame's picture, so not the frame's moment**: a blended draw in a mirror, a probe
13750
+ * bake or a scene capture lands over none of the frame's skin. Latching there switched the
13751
+ * spread off for the frame, so a capture with a glowing sign in it left every face drawn after
13752
+ * it flat. `scripts/capture-check.mjs`.
13753
+ */
13754
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive) return;
13132
13755
  /* Latched whether or not skin was kept: a skin drawn after this point is the whole surface. */
13133
13756
  this.skinScattered = true;
13134
13757
  if (!skin.pending) return;
13135
- if (this.reflectionPassActive || this.probePassActive || this.oitReplaying) return;
13758
+ if (this.oitReplaying) return;
13136
13759
  const encoder = this.encoder;
13137
13760
  const depth = this.depthView;
13138
13761
  const resolved = this.resolvedDepthView;
@@ -13201,7 +13824,7 @@ export class WebGPURenderer implements RendererApi {
13201
13824
  * own terms: `resolvedDepthView` holds the *frame's* depth, and clamping a mirrored beam
13202
13825
  * against it would stop it at geometry that is not in front of it.
13203
13826
  */
13204
- if (this.reflectionPassActive) return false;
13827
+ if (this.reflectionPassActive || this.capturePassActive) return false;
13205
13828
  if (this.resolvedDepthView === null) return false;
13206
13829
  if (this.volumeDepthTaken) return true;
13207
13830
  const encoder = this.encoder;
@@ -13257,6 +13880,65 @@ export class WebGPURenderer implements RendererApi {
13257
13880
  return true;
13258
13881
  }
13259
13882
 
13883
+ /** The colour copy a registered pass reads, at the scene's size and format. See `passColour`. */
13884
+ private allocatePassColour(): void {
13885
+ const source = this.sceneColor;
13886
+ if (source === null || this.passColour !== null) return;
13887
+ this.passColour = this.surface.device.createTexture({
13888
+ label: 'pass.sceneColor',
13889
+ size: [source.width, source.height],
13890
+ format: this.pipelines.format,
13891
+ usage: 0x4 | 0x2, // TEXTURE_BINDING | COPY_DST
13892
+ });
13893
+ this.passColourView = this.passColour.createView();
13894
+ }
13895
+
13896
+ /**
13897
+ * The frame's colour so far, into `passColour`, for a registered pass that reads it: the
13898
+ * boundary `takeRefractSnapshot` describes — flush, end the pass, copy what its end resolved,
13899
+ * reopen with `load` — and refused where that one is, in a mirror, a probe bake or a capture,
13900
+ * whose pictures are not the frame's. **Not latched**, unlike refraction's: each reading pass
13901
+ * sees the passes drawn before it, at a full-frame copy each.
13902
+ */
13903
+ private takePassColour(): void {
13904
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive) return;
13905
+ const copy = this.passColour;
13906
+ const source = this.sceneColor;
13907
+ const encoder = this.encoder;
13908
+ if (copy === null || source === null || encoder === null || this.pass === null) return;
13909
+ if (this.quality.frameGraph) this.flushGraph();
13910
+ this.pass.end();
13911
+ encoder.copyTextureToTexture({ texture: source }, { texture: copy }, [copy.width, copy.height]);
13912
+ this.pass = this.reopenFramePass('pass.colourTaken');
13913
+ }
13914
+
13915
+ /**
13916
+ * The frame's pass again after a copy between passes, loading everything already drawn: the same
13917
+ * attachments the copy's caller ended, with nothing cleared.
13918
+ */
13919
+ private reopenFramePass(label: string): GPURenderPassEncoder | null {
13920
+ const encoder = this.encoder;
13921
+ const target = this.compositeTarget();
13922
+ if (encoder === null || target === null) return null;
13923
+ const multisampled = this.compositeMsaa() !== null;
13924
+ return encoder.beginRenderPass({
13925
+ label,
13926
+ timestampWrites: this.gpuTimer.writesFor(),
13927
+ colorAttachments: [
13928
+ {
13929
+ view: this.compositeMsaa() ?? target,
13930
+ resolveTarget: multisampled ? target : undefined,
13931
+ loadOp: 'load',
13932
+ storeOp: resolvedStoreOp(multisampled, false, this.quality.discardResolvedAttachments),
13933
+ },
13934
+ ],
13935
+ depthStencilAttachment:
13936
+ this.depthView === null
13937
+ ? undefined
13938
+ : { view: this.depthView, depthLoadOp: 'load', depthStoreOp: 'store' },
13939
+ });
13940
+ }
13941
+
13260
13942
  /**
13261
13943
  * The pass to record into, reopening one on the canvas if the frame has already gone.
13262
13944
  *
@@ -13448,6 +14130,8 @@ export class WebGPURenderer implements RendererApi {
13448
14130
  /* And a march reads the same copy, for the same reason and with the same failure if it is
13449
14131
  discarded: a depth of zero is the far plane reversed, so every ray finds nothing. */
13450
14132
  this.reflectionQueue.length > 0 ||
14133
+ /* And the frame's materials' reflections, whose surface half tests against the depth too. */
14134
+ this.reflectionSurface !== null ||
13451
14135
  /* And the medium's own march, which stops where the frame's depth says a surface is. Left
13452
14136
  out of this, every ray runs to `maxDistance` through the walls and the fog is drawn on
13453
14137
  top of the room rather than in it. */
@@ -13761,7 +14445,9 @@ export class WebGPURenderer implements RendererApi {
13761
14445
  * any future branch added to one belongs in the other.
13762
14446
  */
13763
14447
  private targetPipelines(): PipelineCache {
13764
- if (this.probePassActive || this.reflectionPassActive) return this.pipelines;
14448
+ if (this.probePassActive || this.reflectionPassActive || this.capturePassActive) {
14449
+ return this.pipelines;
14450
+ }
13765
14451
  return this.overlayActive || this.framePresented ? this.overlayPipelines : this.pipelines;
13766
14452
  }
13767
14453
 
@@ -14310,6 +14996,8 @@ export class WebGPURenderer implements RendererApi {
14310
14996
  const reads = maskOf(...(definition.reads ?? []));
14311
14997
  this.passReads.set(handle, reads);
14312
14998
  this.passReadsUnion |= reads;
14999
+ /* The colour copy a reader needs, now rather than at its first draw: see `passColour`. */
15000
+ if ((reads & COLOR_SNAPSHOT) !== 0 && this.passColour === null) this.allocatePassColour();
14313
15001
  if (definition.prepare !== undefined) this.preparingPasses++;
14314
15002
  definition.init?.(this.passDevice());
14315
15003
  return handle;
@@ -14329,12 +15017,16 @@ export class WebGPURenderer implements RendererApi {
14329
15017
  environment: PassEnvironment | null;
14330
15018
  distanceField: ComposedField | null;
14331
15019
  jitter: Float32Array;
15020
+ sceneDepth: GPUTextureView | null;
15021
+ sceneColor: GPUTextureView | null;
14332
15022
  } = {
14333
15023
  backend: 'webgpu',
14334
15024
  encoder: null,
14335
15025
  environment: null,
14336
15026
  distanceField: null,
14337
15027
  jitter: this.passJitter,
15028
+ sceneDepth: null,
15029
+ sceneColor: null,
14338
15030
  };
14339
15031
  /**
14340
15032
  * The object handed to a pass as `PrepareContext.environment`, rebuilt only when it changes.
@@ -14432,6 +15124,15 @@ export class WebGPURenderer implements RendererApi {
14432
15124
  * than merely what happens.
14433
15125
  */
14434
15126
  this.prepareContext.distanceField = this.fieldComposer?.field ?? null;
15127
+ /* Settled for the frame: `ensureComposite` has run, so the view stays until the pass draws. */
15128
+ this.prepareContext.sceneDepth = this.resolvedDepthView;
15129
+ /*
15130
+ * Under a reconstruction a colour reader draws after the upscale (see `drawPass`), so it reads
15131
+ * the output-size copy the late refraction keeps rather than the render's.
15132
+ */
15133
+ this.prepareContext.sceneColor = this.reconstructionWanted
15134
+ ? this.refractSnapshotView
15135
+ : this.passColourView;
14435
15136
  for (const definition of this.passes.definitions) {
14436
15137
  definition?.prepare?.(this.prepareContext as PrepareContext);
14437
15138
  }
@@ -14448,20 +15149,42 @@ export class WebGPURenderer implements RendererApi {
14448
15149
  if (!this.canDraw()) return;
14449
15150
  const definition = passAt(this.passes, handle);
14450
15151
  if (definition === undefined) return;
15152
+ const reads = this.passReads.get(handle) ?? 0;
15153
+ /*
15154
+ * **Under a reconstruction, a pass that reads the frame's colour draws after the blended draws
15155
+ * do.** Those land after the upscale, at the output's size, so a pass drawn among them at the
15156
+ * render's would read a frame without them and then be covered by them — a full-screen effect
15157
+ * with the hit sparks drawn sharp over it. Queued instead, and drawn by `runLate` over the
15158
+ * reconstructed picture, after every late draw, whatever order it was asked in.
15159
+ */
15160
+ if ((reads & COLOR_SNAPSHOT) !== 0 && this.drawsLate()) {
15161
+ this.latePasses.push(handle);
15162
+ return;
15163
+ }
15164
+ /*
15165
+ * A pass that reads the depth copy gets it with everything drawn so far: the copy is taken
15166
+ * here, once a frame, as a soft-edged sprite takes it. Before `openPass` below, because taking
15167
+ * it ends the frame's pass and opens another.
15168
+ */
15169
+ if ((reads & DEPTH_SNAPSHOT) !== 0 && !this.framePresented && this.openPass() !== null) {
15170
+ this.takeVolumeDepth();
15171
+ }
15172
+ /*
15173
+ * And one that reads the colour gets it afresh, with everything drawn before it — a full-screen
15174
+ * pass drawn last reads the whole frame and an effect drawn mid-frame reads the frame so far.
15175
+ * A copy a reading pass, where the depth above is one a frame: what a colour reader draws is
15176
+ * itself part of what the next one should see.
15177
+ */
15178
+ if ((reads & COLOR_SNAPSHOT) !== 0 && !this.framePresented && this.openPass() !== null) {
15179
+ this.takePassColour();
15180
+ }
14451
15181
  if (!this.quality.frameGraph) {
14452
15182
  const pass = this.openPass();
14453
15183
  if (pass === null) return;
14454
15184
  definition.draw(this.fillPassContext(pass));
14455
15185
  return;
14456
15186
  }
14457
- recordNode(
14458
- this.arena,
14459
- VERB_PASS,
14460
- this.passReads.get(handle) ?? 0,
14461
- this.currentTarget(),
14462
- handle,
14463
- 0,
14464
- );
15187
+ recordNode(this.arena, VERB_PASS, reads, this.currentTarget(), handle, 0);
14465
15188
  if (this.framePresented) this.queueOverlayFlush();
14466
15189
  }
14467
15190
 
@@ -14582,8 +15305,9 @@ export class WebGPURenderer implements RendererApi {
14582
15305
  ): void {
14583
15306
  const binner = this.clusterBinner;
14584
15307
  i[at('uClustered')] = binner === null ? 0 : 1;
14585
- if (binner === null) return;
14586
15308
 
15309
+ /* Written whether or not the lights are clustered: a surface overlay's rim finds its place on
15310
+ the screen through the same two (\`overlayScreen\`). */
14587
15311
  const p = camera.projection;
14588
15312
  const tanHalfFovY = 1 / (p[5] ?? 1);
14589
15313
  const aspect = (p[5] ?? 1) / (p[0] ?? 1);
@@ -14599,6 +15323,7 @@ export class WebGPURenderer implements RendererApi {
14599
15323
  f[frustumAt + 2] = tanHalfFovY;
14600
15324
  f[frustumAt + 3] = aspect;
14601
15325
  f.set(camera.view as Float32Array, at('uView'));
15326
+ if (binner === null) return;
14602
15327
 
14603
15328
  /* The rectangles past the fixed arm's go into the table. See `ClusterLightSet.areas`. */
14604
15329
  const set = fillClusterLightSet(env, this.clusterLights, this.shadedAreaLights);
@@ -14923,6 +15648,49 @@ export class WebGPURenderer implements RendererApi {
14923
15648
  /** This frame's exposure into the tone curve. Starts at the profile's own. */
14924
15649
  private exposure: number;
14925
15650
 
15651
+ /**
15652
+ * The `filmic` curve every later frame is graded through, until it is set again. See
15653
+ * `RendererApi.setFilmicCurve`; the constants are solved here once, not per frame or per pixel.
15654
+ */
15655
+ setFilmicCurve(curve: FilmicCurve): void {
15656
+ this.filmCurve = curve;
15657
+ if (resolveFilmicCurve(curve, this.filmConstants, this.headroom) && !this.warnedFilmClamped) {
15658
+ this.warnedFilmClamped = true;
15659
+ console.warn(FILMIC_CLAMPED);
15660
+ }
15661
+ }
15662
+
15663
+ private warnedFilmClamped = false;
15664
+ /** The curve last set, so a new headroom can solve it again. */
15665
+ private filmCurve: FilmicCurve = DEFAULT_FILMIC_CURVE;
15666
+ /** The display's peak over paper white; 1 on a standard range. See `setDisplayLuminance`. */
15667
+ private headroom = 1;
15668
+
15669
+ /** The range the frame goes out in. See `RendererApi.displayRange`. */
15670
+ get displayRange(): DisplayRange {
15671
+ return this.surface.displayRange;
15672
+ }
15673
+
15674
+ /** Why it is that range, in words. */
15675
+ get displayRangeReason(): string {
15676
+ return this.surface.displayRangeReason;
15677
+ }
15678
+
15679
+ /**
15680
+ * How bright paper white and the display's peak are, in any one unit (nits, usually), held until
15681
+ * changed: their ratio is how far above white the highlights may run on a high range display,
15682
+ * and nothing changes on a standard one. See `RendererApi.setDisplayLuminance`.
15683
+ */
15684
+ setDisplayLuminance(paperWhite: number, peak: number): void {
15685
+ this.headroom = displayHeadroom(this.surface.displayRange, paperWhite, peak);
15686
+ resolveFilmicCurve(this.filmCurve, this.filmConstants, this.headroom);
15687
+ }
15688
+
15689
+ /** The curve's eight constants, and the two views the composite uploads them through. */
15690
+ private readonly filmConstants = new Float32Array(FILMIC_CONSTANTS);
15691
+ private readonly filmA = this.filmConstants.subarray(0, 4);
15692
+ private readonly filmB = this.filmConstants.subarray(4, 8);
15693
+
14926
15694
  /*
14927
15695
  * -----------------------------------------------------------------------------------------
14928
15696
  * The three screen effects, which this backend cannot yet honour — and says so.
@@ -14967,6 +15735,8 @@ export class WebGPURenderer implements RendererApi {
14967
15735
  private readonly veilColor: Vec3 = [0, 0, 0];
14968
15736
  private veilAlpha = 0;
14969
15737
  private vignette = 0;
15738
+ /** The lens's colour fringe: red's and green's pull and where it begins. See `fringe.ts`. */
15739
+ private readonly fringe = new Float32Array(FRINGE_FLOATS);
14970
15740
  private grain = 0;
14971
15741
  private grainSeed = 0;
14972
15742
  private warnedLookWithoutComposite = false;
@@ -14990,6 +15760,21 @@ export class WebGPURenderer implements RendererApi {
14990
15760
  this.vignette = clampVignette(strength);
14991
15761
  }
14992
15762
 
15763
+ /**
15764
+ * The lens's colour fringe, held until changed: `intensity` is a percentage, 0 is none, and red
15765
+ * and green are pulled toward the centre by their wavelength's distance from blue, growing from
15766
+ * `start` — a share of the half-frame, 0 the centre — to the whole of it at the edge. Applied to
15767
+ * scene light at the composite's first read. Needs `screenEffects`, and says so once rather than
15768
+ * doing nothing. See `shaders/fringe.ts`.
15769
+ */
15770
+ setChromaticAberration(intensity: number, start = 0): void {
15771
+ if (!this.hasComposite) {
15772
+ this.warnLookWithoutComposite(intensity);
15773
+ return;
15774
+ }
15775
+ resolveFringe(intensity, start, this.fringe);
15776
+ }
15777
+
14993
15778
  /**
14994
15779
  * Film grain, held until changed: an amplitude in display values (0 is none, 0.03 is a fine
14995
15780
  * grain) and this frame's seed. **The seed is the caller's**, because the engine takes time from
@@ -15141,9 +15926,27 @@ export class WebGPURenderer implements RendererApi {
15141
15926
  this.rushStrength = Math.min(Math.max(strength, 0), 1);
15142
15927
  }
15143
15928
 
15144
- /** How much of the frame's camera motion blur to apply, 0 to 1. */
15145
- setCameraMotionBlur(scale: number): void {
15929
+ /**
15930
+ * How much of the frame's motion blur to apply, 0 to 1, and the longest smear as a share of the
15931
+ * frame, both held until changed. See `RendererApi.setCameraMotionBlur`.
15932
+ */
15933
+ setCameraMotionBlur(scale: number, maxShare = MOTION_BLUR_MAX_UV): void {
15146
15934
  this.motionBlurScale = Math.min(Math.max(scale, 0), 1);
15935
+ this.motionBlurMax = clampMotionShare(maxShare);
15936
+ }
15937
+
15938
+ /** The longest smear, as a share of the frame. See `setCameraMotionBlur`. */
15939
+ private motionBlurMax = MOTION_BLUR_MAX_UV;
15940
+ private warnedObjectBlur = false;
15941
+
15942
+ /**
15943
+ * Said once: a draw named where it was last frame while the frame blurs, and this frame keeps no
15944
+ * motion target to blur it by — the camera's motion alone smears it. See `OBJECT_BLUR_UNKEPT`.
15945
+ */
15946
+ private warnObjectBlur(): void {
15947
+ if (this.warnedObjectBlur) return;
15948
+ this.warnedObjectBlur = true;
15949
+ console.warn(OBJECT_BLUR_UNKEPT);
15147
15950
  }
15148
15951
  /**
15149
15952
  * The camera has cut: this frame is a new shot, not the next moment of the last one.
@@ -15752,17 +16555,18 @@ export class WebGPURenderer implements RendererApi {
15752
16555
  * `bindMeshPass` and `drawMesh`, which record into whatever `this.pass` is. That is the same
15753
16556
  * arrangement `bakeFace` uses for a point light's cube, and it is why a bake must not be
15754
16557
  * opened inside a frame's pass by a caller who then expects the frame to continue.
15755
- */
15756
- const savedPass = this.pass;
15757
- /*
16558
+ *
15758
16559
  * **A bake is a boundary, and boundaries flush.** The mirror says why in its own words: what
15759
16560
  * has been recorded and not yet replayed belongs to the pass that is about to end, and leaving
15760
16561
  * it replays it into whichever pass opens next — a draw in the wrong target rather than a
15761
16562
  * missing one. This is the fourth such boundary in the backend and the only one that was never
15762
16563
  * given the treatment, which is what let a bake carry the frame's pending draws into a cube
15763
- * face.
16564
+ * face. **The pass is saved after the flush**, which opens it where none was: saved before,
16565
+ * the null given back left the flushed pass open and the frame's encoder locked. See
16566
+ * `captureScene`.
15764
16567
  */
15765
16568
  if (this.quality.frameGraph) this.flushGraph();
16569
+ const savedPass = this.pass;
15766
16570
  const savedEncoder = this.encoder;
15767
16571
  /*
15768
16572
  * **The rings are marked here and given back after the submit below**, because this bake is its
@@ -16454,6 +17258,7 @@ export class WebGPURenderer implements RendererApi {
16454
17258
  this.sceneColor,
16455
17259
  this.sceneColorMsaa,
16456
17260
  this.refractSnapshot,
17261
+ this.passColour,
16457
17262
  this.resolvedDepth,
16458
17263
  this.aoTarget,
16459
17264
  this.aoScratch,
@@ -16523,6 +17328,9 @@ export class WebGPURenderer implements RendererApi {
16523
17328
  usage: 0x4 | 0x2 | 0x10, // TEXTURE_BINDING | COPY_DST | RENDER_ATTACHMENT
16524
17329
  });
16525
17330
  this.refractSnapshotView = this.refractSnapshot.createView();
17331
+ this.passColour = null;
17332
+ this.passColourView = null;
17333
+ if ((this.passReadsUnion & COLOR_SNAPSHOT) !== 0) this.allocatePassColour();
16526
17334
  this.refractMips = new SnapshotMips(
16527
17335
  device,
16528
17336
  this.pipelines,
@@ -16751,6 +17559,10 @@ export class WebGPURenderer implements RendererApi {
16751
17559
  usage: USAGE,
16752
17560
  });
16753
17561
  this.ssrReflectionView = this.ssrReflection.createView();
17562
+ /* The frame's materials, at the scene's size; a stand-in where nothing traces them. */
17563
+ this.reflectionSurface?.size(width, height);
17564
+ const probeMap = this.reflectionSurface?.probeView ?? this.blankView;
17565
+ const tintMap = this.reflectionSurface?.tintView ?? this.blankView;
16754
17566
  this.ssrBindGroup =
16755
17567
  this.resolvedDepthView === null || this.sceneColorView === null
16756
17568
  ? null
@@ -16764,8 +17576,23 @@ export class WebGPURenderer implements RendererApi {
16764
17576
  },
16765
17577
  ...entry(SSR_TEXTURES.uSsrDepth, this.resolvedDepthView, true),
16766
17578
  ...entry(SSR_TEXTURES.uSsrScene, this.sceneColorView),
17579
+ ...entry(SSR_TEXTURES.uSsrProbeMap, probeMap),
17580
+ ...entry(SSR_TEXTURES.uSsrTintMap, tintMap),
16767
17581
  ],
16768
17582
  });
17583
+ this.ssrMaterialGroup = device.createBindGroup({
17584
+ label: 'post.ssrMaterialGroup',
17585
+ layout: this.ssrMaterialLayout,
17586
+ entries: [
17587
+ {
17588
+ binding: SSR_MATERIAL_UNIFORMS,
17589
+ resource: { buffer: this.ssrMaterialUniforms, size: SSR_MATERIAL_SIZE },
17590
+ },
17591
+ ...entry(SSR_MATERIAL_TEXTURES.uSsrReflection, this.ssrReflectionView),
17592
+ ...entry(SSR_MATERIAL_TEXTURES.uSsrProbeMap, probeMap),
17593
+ ...entry(SSR_MATERIAL_TEXTURES.uSsrTintMap, tintMap),
17594
+ ],
17595
+ });
16769
17596
  this.ssrResolveGroup = device.createBindGroup({
16770
17597
  label: 'post.ssrResolveGroup',
16771
17598
  layout: this.ssrResolveLayout,