@driftengine/core 4.8.7 → 4.10.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 (263) hide show
  1. package/CHANGELOG.json +123 -11
  2. package/README.md +43 -38
  3. package/dist/index.d.ts +8 -1
  4. package/dist/index.js +11 -3
  5. package/dist/render/ambientHarmonics.d.ts +1 -1
  6. package/dist/render/ambientHarmonics.js +1 -1
  7. package/dist/render/areaLights.d.ts +4 -4
  8. package/dist/render/areaLights.js +1 -1
  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 +19 -4
  12. package/dist/render/backend/createRenderer.d.ts +6 -0
  13. package/dist/render/backend/createRenderer.js +4 -1
  14. package/dist/render/backend/select.js +11 -5
  15. package/dist/render/backend/webgl2/boneAnimations.d.ts +29 -0
  16. package/dist/render/backend/webgl2/boneAnimations.js +80 -0
  17. package/dist/render/backend/webgl2/instanced.d.ts +5 -1
  18. package/dist/render/backend/webgl2/instanced.js +13 -1
  19. package/dist/render/backend/webgl2/renderer.d.ts +180 -4
  20. package/dist/render/backend/webgl2/renderer.js +550 -40
  21. package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +5 -3
  22. package/dist/render/backend/webgl2/skinPaletteTexture.js +6 -4
  23. package/dist/render/backend/webgpu/boneAnimations.d.ts +34 -0
  24. package/dist/render/backend/webgpu/boneAnimations.js +76 -0
  25. package/dist/render/backend/webgpu/buffers.d.ts +19 -4
  26. package/dist/render/backend/webgpu/buffers.js +18 -11
  27. package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
  28. package/dist/render/backend/webgpu/compressedUpload.js +53 -8
  29. package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
  30. package/dist/render/backend/webgpu/depthPass.js +49 -14
  31. package/dist/render/backend/webgpu/device.d.ts +7 -1
  32. package/dist/render/backend/webgpu/device.js +31 -3
  33. package/dist/render/backend/webgpu/drawCommand.d.ts +18 -4
  34. package/dist/render/backend/webgpu/drawCommand.js +10 -3
  35. package/dist/render/backend/webgpu/flatPass.d.ts +100 -42
  36. package/dist/render/backend/webgpu/flatPass.js +191 -43
  37. package/dist/render/backend/webgpu/instanced.d.ts +14 -1
  38. package/dist/render/backend/webgpu/instanced.js +24 -1
  39. package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
  40. package/dist/render/backend/webgpu/materialStore.js +265 -0
  41. package/dist/render/backend/webgpu/pipelineCache.d.ts +6 -0
  42. package/dist/render/backend/webgpu/pipelineCache.js +7 -0
  43. package/dist/render/backend/webgpu/postPass.d.ts +51 -9
  44. package/dist/render/backend/webgpu/postPass.js +16 -2
  45. package/dist/render/backend/webgpu/reflectionSurfacePass.d.ts +30 -0
  46. package/dist/render/backend/webgpu/reflectionSurfacePass.js +100 -0
  47. package/dist/render/backend/webgpu/renderer.d.ts +283 -37
  48. package/dist/render/backend/webgpu/renderer.js +1363 -202
  49. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
  50. package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
  51. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
  52. package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
  53. package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
  54. package/dist/render/backend/webgpu/staticBundles.js +241 -0
  55. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
  56. package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
  57. package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
  58. package/dist/render/backend/webgpu/uniformRing.js +9 -1
  59. package/dist/render/bloomChain.d.ts +3 -3
  60. package/dist/render/boneAnimation.d.ts +92 -0
  61. package/dist/render/boneAnimation.js +133 -0
  62. package/dist/render/compressedSource.d.ts +33 -13
  63. package/dist/render/compressedSource.js +70 -25
  64. package/dist/render/displayRange.d.ts +31 -0
  65. package/dist/render/displayRange.js +47 -0
  66. package/dist/render/filmicCurve.d.ts +74 -0
  67. package/dist/render/filmicCurve.js +203 -0
  68. package/dist/render/fog.d.ts +4 -1
  69. package/dist/render/fog.js +12 -2
  70. package/dist/render/frame/resources.d.ts +2 -2
  71. package/dist/render/frame/resources.js +6 -0
  72. package/dist/render/frameReflections.d.ts +40 -0
  73. package/dist/render/frameReflections.js +48 -0
  74. package/dist/render/glCompressed.js +83 -0
  75. package/dist/render/instances.d.ts +26 -0
  76. package/dist/render/lightBudget.d.ts +10 -0
  77. package/dist/render/lightBudget.js +10 -0
  78. package/dist/render/lightmap.d.ts +1 -1
  79. package/dist/render/mesh.d.ts +5 -0
  80. package/dist/render/mesh.js +6 -0
  81. package/dist/render/pass.d.ts +49 -0
  82. package/dist/render/pointLightSelection.d.ts +4 -4
  83. package/dist/render/reflectionSurfaceTargets.d.ts +28 -0
  84. package/dist/render/reflectionSurfaceTargets.js +99 -0
  85. package/dist/render/renderQuality.d.ts +39 -8
  86. package/dist/render/renderQuality.js +4 -0
  87. package/dist/render/sceneTarget.d.ts +5 -0
  88. package/dist/render/sceneTarget.js +24 -1
  89. package/dist/render/screenSpaceReflection.d.ts +7 -1
  90. package/dist/render/screenSpaceReflection.js +5 -0
  91. package/dist/render/shaders/arcane.d.ts +1 -1
  92. package/dist/render/shaders/bolt.d.ts +1 -1
  93. package/dist/render/shaders/boneAnimation.d.ts +19 -0
  94. package/dist/render/shaders/boneAnimation.js +63 -0
  95. package/dist/render/shaders/caustics.d.ts +1 -1
  96. package/dist/render/shaders/depth.d.ts +7 -0
  97. package/dist/render/shaders/depth.js +35 -10
  98. package/dist/render/shaders/film.d.ts +1 -1
  99. package/dist/render/shaders/filmLook.js +2 -1
  100. package/dist/render/shaders/fire.d.ts +1 -1
  101. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  102. package/dist/render/shaders/flat/glassTint.js +2 -0
  103. package/dist/render/shaders/flat/index.d.ts +16 -0
  104. package/dist/render/shaders/flat/index.js +33 -5
  105. package/dist/render/shaders/flat/main.d.ts +1 -1
  106. package/dist/render/shaders/flat/main.js +43 -5
  107. package/dist/render/shaders/flat/models.js +1 -1
  108. package/dist/render/shaders/flat/overlay.d.ts +16 -0
  109. package/dist/render/shaders/flat/overlay.js +117 -0
  110. package/dist/render/shaders/flat/pointShadow.js +3 -3
  111. package/dist/render/shaders/flat/preamble.js +38 -24
  112. package/dist/render/shaders/flat/reflectionSurface.d.ts +21 -0
  113. package/dist/render/shaders/flat/reflectionSurface.js +61 -0
  114. package/dist/render/shaders/fog.d.ts +1 -1
  115. package/dist/render/shaders/fog.js +9 -2
  116. package/dist/render/shaders/fringe.d.ts +29 -0
  117. package/dist/render/shaders/fringe.js +55 -0
  118. package/dist/render/shaders/generated/arcane.wgsl.d.ts +1 -1
  119. package/dist/render/shaders/generated/arcane.wgsl.js +1 -1
  120. package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
  121. package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
  122. package/dist/render/shaders/generated/caustics.wgsl.d.ts +1 -1
  123. package/dist/render/shaders/generated/caustics.wgsl.js +1 -1
  124. package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
  125. package/dist/render/shaders/generated/depth.wgsl.js +93 -0
  126. package/dist/render/shaders/generated/film.wgsl.d.ts +1 -1
  127. package/dist/render/shaders/generated/film.wgsl.js +1 -1
  128. package/dist/render/shaders/generated/fire.wgsl.d.ts +1 -1
  129. package/dist/render/shaders/generated/fire.wgsl.js +1 -1
  130. package/dist/render/shaders/generated/flat.wgsl.d.ts +3254 -2920
  131. package/dist/render/shaders/generated/flat.wgsl.js +3292 -2957
  132. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  133. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  134. package/dist/render/shaders/generated/particle.wgsl.d.ts +4 -4
  135. package/dist/render/shaders/generated/particle.wgsl.js +4 -4
  136. package/dist/render/shaders/generated/rush.wgsl.d.ts +56 -26
  137. package/dist/render/shaders/generated/rush.wgsl.js +56 -26
  138. package/dist/render/shaders/generated/scatter.wgsl.d.ts +1 -1
  139. package/dist/render/shaders/generated/scatter.wgsl.js +1 -1
  140. package/dist/render/shaders/generated/smoke.wgsl.d.ts +1 -1
  141. package/dist/render/shaders/generated/smoke.wgsl.js +1 -1
  142. package/dist/render/shaders/generated/ssrResolve.wgsl.d.ts +29 -0
  143. package/dist/render/shaders/generated/ssrResolve.wgsl.js +29 -0
  144. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +22 -2
  145. package/dist/render/shaders/generated/ssrTrace.wgsl.js +22 -2
  146. package/dist/render/shaders/generated/water.wgsl.d.ts +1 -1
  147. package/dist/render/shaders/generated/water.wgsl.js +1 -1
  148. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +2 -2
  149. package/dist/render/shaders/gpudriven/fog.wgsl.js +11 -3
  150. package/dist/render/shaders/line.d.ts +1 -1
  151. package/dist/render/shaders/outputTransform.d.ts +7 -0
  152. package/dist/render/shaders/outputTransform.js +28 -14
  153. package/dist/render/shaders/particle.d.ts +3 -3
  154. package/dist/render/shaders/rush.js +54 -11
  155. package/dist/render/shaders/scatter.d.ts +1 -1
  156. package/dist/render/shaders/smoke.d.ts +1 -1
  157. package/dist/render/shaders/ssrResolve.d.ts +17 -0
  158. package/dist/render/shaders/ssrResolve.js +51 -0
  159. package/dist/render/shaders/ssrTrace.js +26 -0
  160. package/dist/render/shaders/vertexChannel.d.ts +7 -0
  161. package/dist/render/shaders/vertexChannel.js +26 -0
  162. package/dist/render/shaders/water.d.ts +1 -1
  163. package/dist/render/ssrPass.d.ts +16 -0
  164. package/dist/render/ssrPass.js +106 -1
  165. package/dist/render/staticDraws.d.ts +47 -0
  166. package/dist/render/staticDraws.js +43 -0
  167. package/dist/render/surfaceOverlay.d.ts +132 -0
  168. package/dist/render/surfaceOverlay.js +85 -0
  169. package/dist/render/surfaceTexture.d.ts +57 -7
  170. package/dist/render/surfaceTexture.js +125 -17
  171. package/dist/render/temporalAa.d.ts +14 -0
  172. package/dist/render/temporalAa.js +20 -0
  173. package/dist/render/vertexDefaults.d.ts +17 -0
  174. package/dist/render/vertexDefaults.js +24 -0
  175. package/package.json +6 -6
  176. package/src/index.ts +24 -2
  177. package/src/render/ambientHarmonics.ts +1 -1
  178. package/src/render/areaLights.ts +4 -4
  179. package/src/render/atmosphere.ts +22 -1
  180. package/src/render/backend/api.ts +22 -3
  181. package/src/render/backend/createRenderer.ts +18 -1
  182. package/src/render/backend/select.ts +10 -5
  183. package/src/render/backend/webgl2/boneAnimations.ts +105 -0
  184. package/src/render/backend/webgl2/instanced.ts +20 -1
  185. package/src/render/backend/webgl2/renderer.ts +672 -37
  186. package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
  187. package/src/render/backend/webgpu/boneAnimations.ts +97 -0
  188. package/src/render/backend/webgpu/buffers.ts +42 -16
  189. package/src/render/backend/webgpu/compressedUpload.ts +59 -9
  190. package/src/render/backend/webgpu/depthPass.ts +51 -10
  191. package/src/render/backend/webgpu/device.ts +42 -3
  192. package/src/render/backend/webgpu/drawCommand.ts +22 -4
  193. package/src/render/backend/webgpu/flatPass.ts +250 -40
  194. package/src/render/backend/webgpu/instanced.ts +32 -1
  195. package/src/render/backend/webgpu/materialStore.ts +276 -0
  196. package/src/render/backend/webgpu/pipelineCache.ts +7 -0
  197. package/src/render/backend/webgpu/postPass.ts +18 -0
  198. package/src/render/backend/webgpu/reflectionSurfacePass.ts +114 -0
  199. package/src/render/backend/webgpu/renderer.ts +1655 -230
  200. package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
  201. package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
  202. package/src/render/backend/webgpu/staticBundles.ts +363 -0
  203. package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
  204. package/src/render/backend/webgpu/uniformRing.ts +10 -1
  205. package/src/render/bloomChain.ts +3 -3
  206. package/src/render/boneAnimation.ts +223 -0
  207. package/src/render/compressedSource.ts +121 -29
  208. package/src/render/displayRange.ts +61 -0
  209. package/src/render/filmicCurve.ts +229 -0
  210. package/src/render/fog.ts +19 -3
  211. package/src/render/frame/resources.ts +6 -0
  212. package/src/render/frameReflections.ts +72 -0
  213. package/src/render/glCompressed.ts +83 -0
  214. package/src/render/instances.ts +27 -0
  215. package/src/render/lightBudget.ts +11 -0
  216. package/src/render/lightmap.ts +1 -1
  217. package/src/render/mesh.ts +6 -0
  218. package/src/render/pass.ts +53 -1
  219. package/src/render/pointLightSelection.ts +4 -4
  220. package/src/render/reflectionSurfaceTargets.ts +105 -0
  221. package/src/render/renderQuality.ts +44 -8
  222. package/src/render/sceneTarget.ts +30 -2
  223. package/src/render/screenSpaceReflection.ts +7 -1
  224. package/src/render/shaders/boneAnimation.ts +63 -0
  225. package/src/render/shaders/depth.ts +45 -10
  226. package/src/render/shaders/filmLook.ts +2 -1
  227. package/src/render/shaders/flat/glassTint.ts +5 -1
  228. package/src/render/shaders/flat/index.ts +49 -5
  229. package/src/render/shaders/flat/main.ts +43 -5
  230. package/src/render/shaders/flat/models.ts +1 -1
  231. package/src/render/shaders/flat/overlay.ts +117 -0
  232. package/src/render/shaders/flat/pointShadow.ts +3 -3
  233. package/src/render/shaders/flat/preamble.ts +38 -24
  234. package/src/render/shaders/flat/reflectionSurface.ts +61 -0
  235. package/src/render/shaders/fog.ts +9 -2
  236. package/src/render/shaders/fringe.ts +59 -0
  237. package/src/render/shaders/generated/arcane.wgsl.ts +1 -1
  238. package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
  239. package/src/render/shaders/generated/caustics.wgsl.ts +1 -1
  240. package/src/render/shaders/generated/depth.wgsl.ts +95 -0
  241. package/src/render/shaders/generated/film.wgsl.ts +1 -1
  242. package/src/render/shaders/generated/fire.wgsl.ts +1 -1
  243. package/src/render/shaders/generated/flat.wgsl.ts +3292 -2957
  244. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  245. package/src/render/shaders/generated/particle.wgsl.ts +4 -4
  246. package/src/render/shaders/generated/rush.wgsl.ts +56 -26
  247. package/src/render/shaders/generated/scatter.wgsl.ts +1 -1
  248. package/src/render/shaders/generated/smoke.wgsl.ts +1 -1
  249. package/src/render/shaders/generated/ssrResolve.wgsl.ts +30 -0
  250. package/src/render/shaders/generated/ssrTrace.wgsl.ts +22 -2
  251. package/src/render/shaders/generated/water.wgsl.ts +1 -1
  252. package/src/render/shaders/gpudriven/fog.wgsl.ts +11 -3
  253. package/src/render/shaders/outputTransform.ts +30 -14
  254. package/src/render/shaders/rush.ts +54 -11
  255. package/src/render/shaders/ssrResolve.ts +52 -0
  256. package/src/render/shaders/ssrTrace.ts +26 -0
  257. package/src/render/shaders/vertexChannel.ts +27 -0
  258. package/src/render/ssrPass.ts +138 -1
  259. package/src/render/staticDraws.ts +95 -0
  260. package/src/render/surfaceOverlay.ts +195 -0
  261. package/src/render/surfaceTexture.ts +161 -27
  262. package/src/render/temporalAa.ts +20 -0
  263. package/src/render/vertexDefaults.ts +28 -0
@@ -187,10 +187,26 @@ import {
187
187
  packModel,
188
188
  skinProfileDistance,
189
189
  } from '../../surfaceModel.ts';
190
- import type { SurfaceModel } 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 { REFLECTION_SURFACE_FORMAT, ReflectionSurfacePass } from './reflectionSurfacePass.ts';
191
203
  import { validateClothBinding } from '../../clothBindingData.ts';
192
204
  import type { ClothBindingData } from '../../clothBindingData.ts';
193
- import type { CompressedTextureFormat, SurfaceSource } from '../../compressedSource.ts';
205
+ import type {
206
+ CompressedTextureFormat,
207
+ CompressedTextureSource,
208
+ SurfaceSource,
209
+ } from '../../compressedSource.ts';
194
210
  import {
195
211
  createSdfTextBindGroup,
196
212
  createSdfTextBindGroupLayout,
@@ -216,7 +232,24 @@ import type { SurfaceMaterial, SurfaceTextureOptions } from '../../surfaceTextur
216
232
  import type { RenderBackend } from '../api.ts';
217
233
  import { reflectionTargetSize, type ReflectionSize } from '../../planarReflectionDraw.ts';
218
234
  import { drawingBufferSize } from '../../drawingBuffer.ts';
219
- import { MOTION_BLUR_MAX_UV, OUTPUT_TRANSFORM_CODE, RUSH_REACH_UV } from '../../vertexDefaults.ts';
235
+ import {
236
+ MOTION_BLUR_MAX_UV,
237
+ OBJECT_BLUR_UNKEPT,
238
+ OUTPUT_TRANSFORM_CODE,
239
+ RUSH_REACH_UV,
240
+ clampMotionShare,
241
+ forwardTransformCode,
242
+ } from '../../vertexDefaults.ts';
243
+ import { FRINGE_FLOATS, resolveFringe } from '../../shaders/fringe.ts';
244
+ import { displayHeadroom, type DisplayRange } from '../../displayRange.ts';
245
+ import {
246
+ DEFAULT_FILMIC_CURVE,
247
+ FILMIC_CLAMPED,
248
+ FILMIC_CONSTANTS,
249
+ FILMIC_WITHOUT_COMPOSITE,
250
+ resolveFilmicCurve,
251
+ type FilmicCurve,
252
+ } from '../../filmicCurve.ts';
220
253
  import { ExposurePass } from './exposurePass.ts';
221
254
  import { AO_STORE } from '../../shaders/ambientOcclusion.ts';
222
255
  import { clampAutoExposure } from '../../shaders/exposure.ts';
@@ -281,9 +314,15 @@ import {
281
314
  DECAL_TEXTURES,
282
315
  DECAL_UNIFORMS,
283
316
  PREMULTIPLIED_OVER_BLEND,
317
+ FRAME_REFLECTION_BLEND,
284
318
  REFLECTION_OVER_BLEND,
285
319
  SSR_FRAG_FIELDS,
286
320
  SSR_FRAG_SIZE,
321
+ SSR_MATERIAL_FIELDS,
322
+ SSR_MATERIAL_RESOLVE_FRAG_WGSL,
323
+ SSR_MATERIAL_SIZE,
324
+ SSR_MATERIAL_TEXTURES,
325
+ SSR_MATERIAL_UNIFORMS,
287
326
  SSR_RESOLVE_FRAG_WGSL,
288
327
  SSR_RESOLVE_TEXTURES,
289
328
  SSR_TEXTURES,
@@ -377,23 +416,41 @@ import {
377
416
  SUN_STATIC_LAYER,
378
417
  } from '../../shadowMap.ts';
379
418
  import type { SceneCasterMaterial, ShadowCasterSink, ShadowCasters } from '../../shadowCasters.ts';
380
- import { VERTEX_LAYOUT, createGpuMesh, createGpuMeshIncremental, type GpuMesh } from './buffers.ts';
419
+ import {
420
+ VERTEX_LAYOUT,
421
+ createGpuMesh,
422
+ createGpuMeshIncremental,
423
+ type GpuMesh,
424
+ type StagedRewrite,
425
+ } from './buffers.ts';
381
426
  import type { GpuSurface } from './device.ts';
382
427
  import { SkinPaletteRing } from './skinPaletteRing.ts';
383
428
  import {
384
429
  DEPTH_FORMAT,
430
+ FLAT_ANIMATED_VERT_FIELDS,
385
431
  FLAT_VERT_FIELDS,
386
432
  FLAT_VERT_SIZE,
433
+ FLAT_VIEW_FIELDS,
434
+ FLAT_VIEW_SIZE,
387
435
  createFlatBindGroup,
388
436
  createFlatBindGroupLayout,
389
- flatFragmentBindings,
437
+ flatFragmentScratch,
390
438
  flatVariant,
391
439
  flatVertexBindings,
392
440
  type FlatVariant,
393
441
  flatPipeline,
394
442
  flatPipelineAsync,
443
+ probeStageCeilings,
395
444
  } from './flatPass.ts';
396
- import type { OitTarget } from './flatPass.ts';
445
+ import type { AnimationViews, FlatFragmentScratch, OitTarget } from './flatPass.ts';
446
+ import { GpuBoneAnimation } from './boneAnimations.ts';
447
+ import { packBoneAnimation } from '../../boneAnimation.ts';
448
+ import type { BoneAnimationClip, BoneAnimationHandle } from '../../boneAnimation.ts';
449
+ import { MaterialStore, hashWords } from './materialStore.ts';
450
+ import { GpuStaticDraws, encodeBundle, takeRecorded } from './staticBundles.ts';
451
+ import type { StaticRecording } from './staticBundles.ts';
452
+ import { captureStaticDraws, replayStaticDraws } from '../../staticDraws.ts';
453
+ import type { StaticDrawsHandle } from '../../staticDraws.ts';
397
454
  import {
398
455
  DEPTH_FRAG_BINDING,
399
456
  DEPTH_FRAG_SIZE,
@@ -401,6 +458,8 @@ import {
401
458
  DEPTH_PREVIOUS_BINDING,
402
459
  DEPTH_CUTOUT_VERT_FIELDS,
403
460
  DEPTH_CUTOUT_VERT_SIZE,
461
+ DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT_FIELDS,
462
+ DEPTH_INSTANCED_ANIMATED_VERT_FIELDS,
404
463
  DEPTH_INSTANCED_CUTOUT_VERT_FIELDS,
405
464
  DEPTH_VERT_FIELDS,
406
465
  DEPTH_VERT_SIZE,
@@ -694,34 +753,20 @@ const MAX_SKY_DRAWS = 64;
694
753
  /**
695
754
  * Whether the environment probe's permutation fits this device's per-stage binding limits.
696
755
  *
697
- * The widest flat variant declares one albedo, three directional shadow maps, twelve
698
- * point-shadow cubes and the probe: seventeen textures and seventeen samplers. WebGPU's default
699
- * is sixteen of each and an adapter is free to offer no more — this one offers 48 textures and
700
- * exactly 16 samplers, so the samplers are what decide it.
756
+ * **Two ceilings, counted separately**: sampled textures and samplers, since textures that share a
757
+ * sampler (`// wgsl:share` in `flat/preamble.ts`) count once against the second. The widest variant
758
+ * binds sixteen textures and twelve samplers, which is exactly WebGPU's default texture ceiling, and
759
+ * `select.ts` asks for the adapter's own — 48 here — so the probe does not depend on that request.
701
760
  */
702
761
  function probeFits(device: GPUDevice): boolean {
703
762
  /*
704
- * **Two ceilings, counted separately, and they are no longer the same number.**
705
- *
706
- * The widest permutation declares seventeen sampled textures — one albedo, three directional
707
- * shadow maps, the point-shadow array, a normal map and the probe's own cube — **seven**, where
708
- * it was seventeen while every point light's shadow was a `samplerCube` of its own. `select.ts` asks
709
- * for the adapter's ceiling, which is 48 on this machine against a device default of 16, so
710
- * six fits everywhere and the probe no longer depends on the request being granted.
711
- *
712
- * It used to declare seventeen *samplers* too, and that was the binding constraint: this
713
- * adapter offers exactly sixteen and no request can raise it, so the probe stood down and
714
- * reflective surfaces kept a gradient. The shadow bindings share one declaration — see
715
- * `// wgsl:share shadow` in `flat/preamble.ts`, which costs nothing because they were already
716
- * one object — so the count is four, and the fourth is the normal map's: it is a colour image
717
- * and wants filtering, which a non-filtering shadow sampler cannot give it.
718
- *
719
- * **Counted from the generated bindings rather than typed**: `FLAT_BINDINGS.flatFrag`'s
720
- * widest variant is `directionalShadows+environmentProbe+pointShadows`, seven textures and
721
- * four samplers. A stale number here reads as a device declining a feature it can run.
722
- */
723
- const TEXTURES = 7;
724
- const SAMPLERS = 4;
763
+ * **Counted from the generated bindings rather than typed** (`probeStageCeilings`). It was typed
764
+ * here as seven textures and four samplers while the widest variant grew to sixteen and twelve
765
+ * with the material maps, glass, the fixture atlas and DriftLight: a stale number reads as a
766
+ * device declining a feature it can run, or, the way this one had drifted, as one admitted to a
767
+ * permutation it cannot bind.
768
+ */
769
+ const { textures: TEXTURES, samplers: SAMPLERS } = probeStageCeilings();
725
770
  const fits =
726
771
  device.limits.maxSampledTexturesPerShaderStage >= TEXTURES &&
727
772
  device.limits.maxSamplersPerShaderStage >= SAMPLERS;
@@ -789,6 +834,7 @@ const VERB_PASS = 3;
789
834
  /** What the depth snapshot copies from, and what it copies into. */
790
835
  const DEPTH_SOURCE = maskOf('sceneDepth');
791
836
  const DEPTH_SNAPSHOT = maskOf('depthSnapshot');
837
+ const COLOR_SNAPSHOT = maskOf('colorSnapshot');
792
838
 
793
839
  /** The mirror's own colour and depth. */
794
840
  const MIRROR_TARGET = maskOf('mirrorColor', 'mirrorDepth');
@@ -886,6 +932,14 @@ const MAX_WATER_BODIES_PER_FRAME = 16;
886
932
  */
887
933
  const MAX_BATCHES_PER_FRAME = 8;
888
934
 
935
+ /**
936
+ * Mesh passes a frame may open before the ring grows: the frame's own, a mirror's, a capture's, and
937
+ * a probe face's, which gives its slot back once submitted. A frame past it loses the draws of the
938
+ * passes that found no slot, counted on the `materials` line, and the ring is grown for the next.
939
+ * Each slot is the pass's block, 3,328 to 6,656 bytes by variant, so the ring is 213 to 426 KB.
940
+ */
941
+ const PASSES_PER_FRAME = 64;
942
+
889
943
  /** Buffer usage flags, from the specification. See `buffers.ts` for why they are not read off the global. */
890
944
  const USAGE_UNIFORM_DST = 0x0040 | 0x0008;
891
945
 
@@ -1400,6 +1454,15 @@ interface SunTintTarget {
1400
1454
  readonly mips: readonly [SnapshotMips, SnapshotMips];
1401
1455
  }
1402
1456
 
1457
+ /** A skinned or morphed flat group, and what it was built with, compared by identity. */
1458
+ interface SkinnedTwin {
1459
+ readonly group: GPUBindGroup;
1460
+ readonly palette: GPUTextureView | null;
1461
+ readonly deltas: GPUTextureView | null;
1462
+ readonly cloth: GPUTextureView | null;
1463
+ readonly clothBinding: GPUTextureView | null;
1464
+ }
1465
+
1403
1466
  export class WebGPURenderer implements RendererApi {
1404
1467
  /**
1405
1468
  * Real timestamp queries, where the adapter offers them.
@@ -1483,6 +1546,8 @@ export class WebGPURenderer implements RendererApi {
1483
1546
  private readonly skinnedBindGroupLayout: GPUBindGroupLayout;
1484
1547
  private readonly morphedBindGroupLayout: GPUBindGroupLayout;
1485
1548
  private readonly bothBindGroupLayout: GPUBindGroupLayout;
1549
+ /** The instanced layout of a batch playing a bone animation. See `animatedGroup`. */
1550
+ private readonly animatedBindGroupLayout: GPUBindGroupLayout;
1486
1551
  /** The morph weights the following draws use, or null for none. */
1487
1552
  private morphWeights: Float32Array | null = null;
1488
1553
  /**
@@ -1495,14 +1560,15 @@ export class WebGPURenderer implements RendererApi {
1495
1560
  * group per character per frame — an allocation in the frame loop, which `AGENTS.md` forbids.
1496
1561
  * Keyed by slot instead, the groups are built once and reused for the life of the renderer.
1497
1562
  */
1498
- private readonly skinnedGroups: (GPUBindGroup | null)[] = [];
1499
- /** The unskinned group each was built beside, for identity-based invalidation. */
1500
- private readonly skinnedGroupsBeside: (GPUBindGroup | null)[] = [];
1501
- private readonly skinnedGroupsPalette: (GPUTextureView | null)[] = [];
1502
- private readonly skinnedGroupsDeltas: (GPUTextureView | null)[] = [];
1503
- /** The cloth views each skinned twin was built with, compared by identity like the palette. */
1504
- private readonly skinnedGroupsCloth: (GPUTextureView | null)[] = [];
1505
- private readonly skinnedGroupsClothBinding: (GPUTextureView | null)[] = [];
1563
+ /*
1564
+ * **And by material within a slot**, keyed by the unskinned group each was built beside. One
1565
+ * group a slot was rebuilt whenever the material beside it changed, so a character drawn in
1566
+ * thirty-odd sections, each its own material, built one a section a frame — about ninety a frame
1567
+ * for two characters, reported by a consumer. A slot's map holds one entry a material its rigs
1568
+ * are drawn in, and is emptied wherever the unskinned groups are, whose identities it is keyed
1569
+ * by.
1570
+ */
1571
+ private readonly skinnedGroups: (Map<GPUBindGroup, SkinnedTwin> | undefined)[] = [];
1506
1572
  /** The cloth the next skinned draws are placed by, or null. See `setCloth`. */
1507
1573
  private cloth: { binding: GpuClothBinding; particles: GpuClothParticles } | null = null;
1508
1574
  private clothStandCache: { texture: GPUTexture; views: ClothViews } | null = null;
@@ -1572,22 +1638,51 @@ export class WebGPURenderer implements RendererApi {
1572
1638
  /** Every distinct cache above, so a mesh's pipelines are built for each. One entry when aliased. */
1573
1639
  private readonly pipelineTargets: readonly PipelineCache[];
1574
1640
  private readonly bindGroupLayout: GPUBindGroupLayout;
1575
- /** One slot per draw, uploaded once a frame. See `UniformRing`. */
1576
- private readonly perDraw: UniformRing;
1577
- /** The lights, fog and camera, settled once per frame by `bindMeshPass`. */
1578
- private readonly perFrame: UniformRing;
1641
+ /** One slot per draw, uploaded once a frame. See `UniformRing`. Swapped while a static list records. */
1642
+ private perDraw: UniformRing;
1643
+ /**
1644
+ * The lights, fog and camera, a slot per mesh pass a frame opens: `bindMeshPass` writes them, and
1645
+ * the first draw after it takes a slot. A ring rather than one buffer because a frame opens more
1646
+ * than one pass — a mirror's, a capture's, a probe face's — and a queue write does not interleave
1647
+ * with the draws recorded between them. See `UniformRing`.
1648
+ */
1649
+ private passRing: UniformRing;
1650
+ /** Each material's block, kept across frames for as long as it does not change. */
1651
+ private readonly materialStore: MaterialStore;
1579
1652
  /**
1580
- * The material block as the CPU holds it, patched by the setters and copied into a slot.
1653
+ * Both blocks as the CPU holds them, the pass's then the material's, patched by name and copied
1654
+ * into a slot by the next draw after something changed them. See `FlatFragmentScratch`.
1581
1655
  *
1582
1656
  * Authoritative between draws: a setter changes one field here and marks the block dirty, and
1583
- * the next `drawMesh` takes a slot and copies the whole thing in. That keeps a setter free of
1584
- * any knowledge of slots, which is what lets it be called anywhere a scene wants.
1657
+ * the next `drawMesh` places it. That keeps a setter free of any knowledge of slots, which is
1658
+ * what lets it be called anywhere a scene wants.
1585
1659
  */
1586
1660
  private readonly perFrameStaging: ArrayBuffer;
1587
1661
  private readonly perFrameFloats: Float32Array;
1588
1662
  private readonly perFrameInts: Int32Array;
1663
+ /** The two halves of the scratch, as the words a slot is written from. */
1664
+ private readonly passInts: Int32Array;
1665
+ private readonly materialInts: Int32Array;
1589
1666
  /** The slot the open material occupies, and whether the next draw must take a new one. */
1590
1667
  private readonly materials = new MaterialChanges();
1668
+ /** The same for the pass's block: reopened by `bindMeshPass` and by nothing a material sets. */
1669
+ private readonly passBlock = new MaterialChanges();
1670
+ /**
1671
+ * The vertex stage's view block — the camera, the light's matrix, the wind — a slot a pass, and a
1672
+ * second, unjittered, for a blended draw landing after the upscale. Reopened with the pass, and by
1673
+ * `setWind` and `beginShadowPass`, which change what it holds.
1674
+ */
1675
+ private viewRing: UniformRing;
1676
+ private readonly viewBlock = new MaterialChanges();
1677
+ private readonly lateViewBlock = new MaterialChanges();
1678
+ /** The view slot the draw being submitted bound, read by the halves it keeps for later passes. */
1679
+ private drawViewOffset = 0;
1680
+ /** Pass slots asked for and refused this frame, and materials refused: what grows the two. */
1681
+ private passesAsked = 0;
1682
+ private passesRefused = 0;
1683
+ private viewsAsked = 0;
1684
+ private viewsRefused = 0;
1685
+ private materialsRefused = 0;
1591
1686
 
1592
1687
  /*
1593
1688
  * The frame graph's recording, and the machinery that replays it.
@@ -1856,8 +1951,8 @@ export class WebGPURenderer implements RendererApi {
1856
1951
  private readonly variant: FlatVariant;
1857
1952
  /** Whether this profile compiled the point-shadow path, and so has sentinels to write. */
1858
1953
  private readonly pointShadowsCompiled: boolean;
1859
- /** What the generator recorded about that permutation: sizes, offsets and bindings. */
1860
- private readonly fragment: ReturnType<typeof flatFragmentBindings>;
1954
+ /** What the generator recorded about that permutation, as the renderer's scratch lays it out. */
1955
+ private readonly fragment: FlatFragmentScratch;
1861
1956
 
1862
1957
  private depth: GPUTexture | null = null;
1863
1958
  private depthView: GPUTextureView | null = null;
@@ -1919,6 +2014,17 @@ export class WebGPURenderer implements RendererApi {
1919
2014
  */
1920
2015
  private refractSnapshot: GPUTexture | null = null;
1921
2016
  private refractSnapshotView: GPUTextureView | null = null;
2017
+ /**
2018
+ * The frame's colour as a registered pass reads it (`reads: ['colorSnapshot']`): the scene's own
2019
+ * size and format, refilled at each such pass's draw. Allocated only once a pass declares the
2020
+ * read, beside the frame's targets, and never in the frame loop. See `takePassColour`.
2021
+ */
2022
+ private passColour: GPUTexture | null = null;
2023
+ private passColourView: GPUTextureView | null = null;
2024
+ /** Registered passes that read the colour, queued this frame for after the upscale. */
2025
+ private readonly latePasses: PassHandle[] = [];
2026
+ /** A late pass draws unjittered, as every late draw does. */
2027
+ private readonly latePassJitter = new Float32Array(2);
1922
2028
  /** Whether this frame has taken it. One snapshot serves every refracting draw. */
1923
2029
  private refractSnapshotTaken = false;
1924
2030
  /**
@@ -2429,8 +2535,18 @@ export class WebGPURenderer implements RendererApi {
2429
2535
  private readonly reflectionQueue = new ReflectionQueue();
2430
2536
  private ssrLayout!: GPUBindGroupLayout;
2431
2537
  private ssrResolveLayout!: GPUBindGroupLayout;
2538
+ /** The frame's materials' reflections, resolved; null where off. See `frameReflections.ts`. */
2539
+ private readonly frameReflections: ResolvedFrameReflections | null;
2540
+ /** Their surface half, null where they are off or cannot be drawn: `reflectionSurfacePass.ts`. */
2541
+ private readonly reflectionSurface: ReflectionSurfacePass | null;
2542
+ /** The material resolve: its layout, its one block (the blur) and its group over the three maps. */
2543
+ private ssrMaterialLayout!: GPUBindGroupLayout;
2544
+ private ssrMaterialUniforms!: GPUBuffer;
2545
+ private readonly ssrMaterialFloats = new Float32Array(SSR_MATERIAL_SIZE / 4);
2546
+ private ssrMaterialGroup: GPUBindGroup | null = null;
2432
2547
  private ssrUniforms!: GPUBuffer;
2433
- private readonly ssrStaging = new ArrayBuffer(SSR_SLOT * MAX_REFLECTIVE_SURFACES);
2548
+ /* One slot a surface, and one more for the frame's materials' trace. */
2549
+ private readonly ssrStaging = new ArrayBuffer(SSR_SLOT * (MAX_REFLECTIVE_SURFACES + 1));
2434
2550
  private readonly ssrFloats = new Float32Array(this.ssrStaging);
2435
2551
  private ssrReflection: GPUTexture | null = null;
2436
2552
  private ssrReflectionView: GPUTextureView | null = null;
@@ -2580,7 +2696,7 @@ export class WebGPURenderer implements RendererApi {
2580
2696
  if (this.quality.frameGraph) recordNode(this.arena, VERB_SCOPE, 0, MIRROR_TARGET, 0, 0);
2581
2697
  this.pass?.end();
2582
2698
  this.flushRings();
2583
- this.surface.device.queue.submit([this.encoder.finish()]);
2699
+ this.submit([this.encoder.finish()]);
2584
2700
  this.encoder = this.surface.device.createCommandEncoder({ label: 'reflection' });
2585
2701
  /*
2586
2702
  * **Opened on demand, like the frame's own.** Nothing has been recorded into the mirror yet,
@@ -2640,7 +2756,8 @@ export class WebGPURenderer implements RendererApi {
2640
2756
  this.windStreakFrags,
2641
2757
  this.scatterVerts,
2642
2758
  this.scatterFrags,
2643
- this.perFrame,
2759
+ this.passRing,
2760
+ this.viewRing,
2644
2761
  this.filmVerts,
2645
2762
  this.filmFrags,
2646
2763
  this.insetUniforms,
@@ -2661,6 +2778,7 @@ export class WebGPURenderer implements RendererApi {
2661
2778
  /* Culls first: a submit that follows may carry a draw reading what they write. */
2662
2779
  this.instanceCulls?.flush();
2663
2780
  for (const ring of this.rings()) ring.flush();
2781
+ this.materialStore.flush();
2664
2782
  }
2665
2783
 
2666
2784
  /** Where every ring stands before work that submits an encoder of its own. */
@@ -2680,6 +2798,8 @@ export class WebGPURenderer implements RendererApi {
2680
2798
  private rewindRings(skinMark: number): void {
2681
2799
  const rings = this.rings();
2682
2800
  for (let i = 0; i < rings.length; i++) (rings[i] as UniformRing).rewind(this.ringMarks[i] ?? 0);
2801
+ /* The open pass may sit in a slot just given back, which the next pass would overwrite. */
2802
+ this.reopenPass();
2683
2803
  this.skinPalettes.rewind(skinMark);
2684
2804
  if (this.skinPaletteSlot >= skinMark) this.skinPaletteSlot = -1;
2685
2805
  }
@@ -2882,7 +3002,7 @@ export class WebGPURenderer implements RendererApi {
2882
3002
  * ones that move something *every* group holds: the shadow cubemaps, the probe fence, the cookie
2883
3003
  * atlas and the IES rows. None of those is a frame path.
2884
3004
  */
2885
- private readonly flatBindGroups = new Map<
3005
+ private flatBindGroups = new Map<
2886
3006
  GpuSurfaceTexture | null,
2887
3007
  Map<
2888
3008
  GpuSurfaceTexture | null,
@@ -3000,8 +3120,15 @@ export class WebGPURenderer implements RendererApi {
3000
3120
  const { device } = this.surface;
3001
3121
  const { depth, colorMsaa } = target.attachments(this.samples, this.pipelines.format);
3002
3122
 
3003
- const savedPass = this.pass;
3123
+ /*
3124
+ * **Flushed, and only then set aside.** The flush replays what the frame has recorded, and
3125
+ * replaying opens the frame's pass where none is open yet; saved before it, the pass given back
3126
+ * afterwards was the null that stood there, while the one the flush opened was left open on the
3127
+ * frame's encoder and every later pass on it was refused — a black frame for any capture taken
3128
+ * after the frame had drawn something. `scripts/capture-check.mjs` takes one at three points.
3129
+ */
3004
3130
  if (this.quality.frameGraph) this.flushGraph();
3131
+ const savedPass = this.pass;
3005
3132
  const savedEncoder = this.encoder;
3006
3133
  this.markRings();
3007
3134
  const skinMark = this.skinPalettes.mark();
@@ -3046,7 +3173,7 @@ export class WebGPURenderer implements RendererApi {
3046
3173
  pass.end();
3047
3174
  this.pass = null;
3048
3175
  this.flushRings();
3049
- device.queue.submit([encoder.finish()]);
3176
+ this.submit([encoder.finish()]);
3050
3177
  this.rewindRings(skinMark);
3051
3178
  this.encoderEpoch = frameEpoch;
3052
3179
  this.encoder = savedEncoder;
@@ -3054,6 +3181,7 @@ export class WebGPURenderer implements RendererApi {
3054
3181
  this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
3055
3182
  this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
3056
3183
  this.materials.dirty();
3184
+ this.reopenPass();
3057
3185
  this.capturePassActive = false;
3058
3186
  }
3059
3187
  return true;
@@ -3068,11 +3196,15 @@ export class WebGPURenderer implements RendererApi {
3068
3196
  */
3069
3197
  private readonly retiredTextures: GPUTexture[] = [];
3070
3198
 
3071
- updateSurfaceTexture(texture: GpuSurfaceTexture, source: TexImageSource): void {
3199
+ updateSurfaceTexture(
3200
+ texture: GpuSurfaceTexture,
3201
+ source: TexImageSource | CompressedTextureSource,
3202
+ ): void {
3072
3203
  if (this.surface.lost) return;
3073
- const replaced = texture.update(source);
3204
+ const replaced = texture.update(source, this.compressedFormats);
3074
3205
  if (replaced === null) return;
3075
- /* A new size is a new view, so every group holding the old one is dropped — see below. */
3206
+ /* A new size or format is a new view, so every group holding the old one is dropped — see
3207
+ below. */
3076
3208
  this.retiredTextures.push(replaced);
3077
3209
  this.forgetBindingsOf(texture);
3078
3210
  }
@@ -3105,6 +3237,7 @@ export class WebGPURenderer implements RendererApi {
3105
3237
  if (this.ormMap === texture) this.ormMap = null;
3106
3238
  if (this.emissiveMap === texture) this.emissiveMap = null;
3107
3239
  if (this.modelMap === texture) this.modelMap = null;
3240
+ if (this.overlayMaps === texture) this.overlayMaps = null;
3108
3241
  this.forgetBindingsOf(texture);
3109
3242
  texture.dispose();
3110
3243
  this.captureTargets.get(texture)?.dispose();
@@ -3123,6 +3256,9 @@ export class WebGPURenderer implements RendererApi {
3123
3256
  /* Same reasoning, for whichever SDF labels were bound against this atlas. */
3124
3257
  this.sdfTextBindGroups.delete(texture);
3125
3258
  this.flatBindGroups.clear();
3259
+ this.flatGroupEpoch += 1;
3260
+ this.overlayGroups.clear();
3261
+ this.skinnedGroups.length = 0;
3126
3262
  /*
3127
3263
  * The blank one is built with the albedo held aside, because `buildFlatBindGroup` reads
3128
3264
  * `this.albedo` for that slot — leaving it in place would put a real colour map into the
@@ -3314,6 +3450,13 @@ export class WebGPURenderer implements RendererApi {
3314
3450
  const emissiveMap = (material?.emissive ?? null) as GpuSurfaceTexture | null;
3315
3451
  const modelMap = (material?.modelMap ?? null) as GpuSurfaceTexture | null;
3316
3452
  this.materialModel = material?.model ?? null;
3453
+ const reflectivity = material?.reflectivity;
3454
+ const environmentGain = material?.environmentGain;
3455
+ this.materialReflectivity =
3456
+ reflectivity === undefined ? null : Math.min(1, Math.max(0, reflectivity));
3457
+ this.materialEnvironmentGain =
3458
+ environmentGain === undefined ? null : Math.max(0, environmentGain);
3459
+ this.writeEnvironmentDials();
3317
3460
  /* What the blur spreads this skin by: the last material to name a profile says. */
3318
3461
  const model = material?.model;
3319
3462
  warnUnpagedLightmap(model, modelMap);
@@ -3342,7 +3485,8 @@ export class WebGPURenderer implements RendererApi {
3342
3485
  const cut = this.materialField('uCutout');
3343
3486
  f[cut] = cutoff;
3344
3487
  f[cut + 1] = CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
3345
- f[cut + 2] = this.cutoutFrame;
3488
+ /* z unused: the dither's frame is the pass's, in `uHighlightMin.w`. See `uCutout`. */
3489
+ f[cut + 2] = 0;
3346
3490
  /* The material's lighting channels, which the lamps' are compared with. See `uCutout`. */
3347
3491
  f[cut + 3] = lightChannelsOf(material?.lightChannels);
3348
3492
  i[flags + 3] = material?.doubleSided === true ? 1 : 0;
@@ -3516,17 +3660,47 @@ export class WebGPURenderer implements RendererApi {
3516
3660
  this.material((f) => f.set(this.ambientSH, this.materialField('uAmbientSH')));
3517
3661
  }
3518
3662
 
3663
+ /**
3664
+ * The overlay the following draws wear, or null for none: pass state, put back to none by
3665
+ * `bindMeshPass`. See `renderer.ts` and `surfaceOverlay.ts`. The numbers go in the material block,
3666
+ * which every variant declares; the atlas goes in a twin of each flat group (`overlayGroup`).
3667
+ */
3668
+ setSurfaceOverlay(overlay: SurfaceOverlay | null): void {
3669
+ if (this.surface.lost) return;
3670
+ packSurfaceOverlay(overlay, this.overlayFloats);
3671
+ /* The first overlay laid is when the lit pipelines start carrying one. */
3672
+ if (overlayLays(overlay)) this.enableLit('SURFACE_OVERLAY');
3673
+ this.overlayMaps = (overlay?.maps ?? null) as GpuSurfaceTexture | null;
3674
+ this.material((f) => f.set(this.overlayFloats, this.materialField('uOverlay')));
3675
+ }
3676
+
3519
3677
  /** How reflective the surfaces drawn next are. Clamped, as `renderer.ts` clamps it. */
3520
3678
  setSurfaceReflectivity(amount: number): void {
3521
- this.material((f) => {
3522
- f[this.materialField('uEnvironmentDials') + 1] = Math.min(1, Math.max(0, amount));
3523
- });
3679
+ this.passReflectivity = Math.min(1, Math.max(0, amount));
3680
+ this.writeEnvironmentDials();
3524
3681
  }
3525
3682
 
3526
3683
  /** How bright the environment the surfaces drawn next reflect is. See `renderer.ts`. */
3527
3684
  setEnvironmentGain(gain: number): void {
3685
+ this.passEnvironmentGain = Math.max(0, gain);
3686
+ this.writeEnvironmentDials();
3687
+ }
3688
+
3689
+ /**
3690
+ * The reflectivity and environment gain the setters gave the pass, and the ones the material
3691
+ * set now states, or null where it states none (`SurfaceMaterial.reflectivity`). A draw wears the
3692
+ * material's where it has one and the pass's otherwise, the rule `renderer.ts` keeps too.
3693
+ */
3694
+ private passReflectivity = 0;
3695
+ private passEnvironmentGain = 1;
3696
+ private materialReflectivity: number | null = null;
3697
+ private materialEnvironmentGain: number | null = null;
3698
+
3699
+ private writeEnvironmentDials(): void {
3528
3700
  this.material((f) => {
3529
- f[this.materialField('uEnvironmentDials') + 2] = Math.max(0, gain);
3701
+ const dials = this.materialField('uEnvironmentDials');
3702
+ f[dials + 1] = this.materialReflectivity ?? this.passReflectivity;
3703
+ f[dials + 2] = this.materialEnvironmentGain ?? this.passEnvironmentGain;
3530
3704
  });
3531
3705
  }
3532
3706
 
@@ -4045,7 +4219,7 @@ export class WebGPURenderer implements RendererApi {
4045
4219
  }
4046
4220
  this.pass?.end();
4047
4221
  this.flushRings();
4048
- this.surface.device.queue.submit([this.encoder.finish()]);
4222
+ this.submit([this.encoder.finish()]);
4049
4223
  this.encoder = this.surface.device.createCommandEncoder({ label: 'frame.resumed' });
4050
4224
  this.reflectionPassActive = false;
4051
4225
  this.reflectionReady = true;
@@ -4260,7 +4434,7 @@ export class WebGPURenderer implements RendererApi {
4260
4434
  */
4261
4435
  private gradeCode(): number {
4262
4436
  return !this.hasComposite || !this.quality.hdrScene
4263
- ? (OUTPUT_TRANSFORM_CODE[this.quality.outputTransform] ?? 0)
4437
+ ? forwardTransformCode(this.quality.outputTransform)
4264
4438
  : 0;
4265
4439
  }
4266
4440
 
@@ -5728,6 +5902,7 @@ export class WebGPURenderer implements RendererApi {
5728
5902
  */
5729
5903
  setWind(windX: number, windZ: number, windGust: number, timeSeconds: number): void {
5730
5904
  resolveScatterDeform(windX, windZ, windGust, timeSeconds, null, this.frameWind);
5905
+ this.reopenViews();
5731
5906
  }
5732
5907
 
5733
5908
  /**
@@ -5747,22 +5922,6 @@ export class WebGPURenderer implements RendererApi {
5747
5922
  this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uWindSpatialPhase.offset, d.spatialPhase);
5748
5923
  }
5749
5924
 
5750
- /**
5751
- * The five wind fields of one per-draw slot.
5752
- *
5753
- * **Never called for an instanced draw.** That variant declares none of these, and
5754
- * `flatPass.ts` records what writing a field a variant does not declare costs: the block is
5755
- * shorter, so the write lands on whatever occupies that offset instead.
5756
- */
5757
- private writeWind(slot: number): void {
5758
- const d = this.frameWind;
5759
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uWindDirection.offset, d.direction);
5760
- this.perDraw.writeFloat(slot, FLAT_VERT_FIELDS.uWindSpeed.offset, d.bend);
5761
- this.perDraw.writeFloat(slot, FLAT_VERT_FIELDS.uWindGust.offset, d.gust);
5762
- this.perDraw.writeFloat(slot, FLAT_VERT_FIELDS.uWindTime.offset, d.time);
5763
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uWindSpatialPhase.offset, d.spatialPhase);
5764
- }
5765
-
5766
5925
  /** Refilled per frame rather than allocated; see `resolvePointLights`. */
5767
5926
  private readonly lights: ResolvedPointLights = {
5768
5927
  count: 0,
@@ -5917,6 +6076,9 @@ export class WebGPURenderer implements RendererApi {
5917
6076
  * at submit, and a texture's view is new when an update changes its size.
5918
6077
  */
5919
6078
  private readonly shadowCutoutLayout: GPUBindGroupLayout;
6079
+ /** A crowd's casters: the plain and cutout layouts with a bone animation's three textures. */
6080
+ private readonly shadowAnimatedLayout: GPUBindGroupLayout;
6081
+ private readonly shadowAnimatedCutoutLayout: GPUBindGroupLayout;
5920
6082
  private readonly cutoutGroups = new Map<GpuSurfaceTexture, GPUBindGroup>();
5921
6083
  private readonly cutoutPeelGroups = new Map<GpuSurfaceTexture, GPUBindGroup>();
5922
6084
  private shadowPeelPassUniforms: GPUBuffer | null = null;
@@ -6172,8 +6334,18 @@ export class WebGPURenderer implements RendererApi {
6172
6334
  const count = Math.min(data.count, gpuBatch.capacity);
6173
6335
  if (count === 0) return;
6174
6336
  const cutout = cutoutOf(material);
6337
+ /* A crowd casts where its clip has moved it, through the animated variant. */
6338
+ const animation = gpuBatch.animation === null ? null : this.animationViews(gpuBatch);
6339
+ if (gpuBatch.animation !== null && animation === null) return;
6340
+ const cutoutTexture =
6341
+ cutout === null ? null : (cutout.albedo as unknown as GpuSurfaceTexture);
6175
6342
  const cutoutGroup =
6176
- cutout === null ? null : this.cutoutGroup(cutout.albedo as unknown as GpuSurfaceTexture);
6343
+ animation !== null
6344
+ ? this.animatedDepthGroup(gpuBatch, animation, cutoutTexture)
6345
+ : cutoutTexture === null
6346
+ ? null
6347
+ : this.cutoutGroup(cutoutTexture);
6348
+ if (animation !== null && cutoutGroup === null) return;
6177
6349
 
6178
6350
  const slot = this.shadowDraws.allocate();
6179
6351
  if (slot === null) return;
@@ -6184,23 +6356,43 @@ export class WebGPURenderer implements RendererApi {
6184
6356
  DEPTH_VERT_FIELDS.uLightViewProj.offset,
6185
6357
  this.correctedLightViewProj,
6186
6358
  );
6187
- if (cutout !== null && cutoutGroup !== null) {
6359
+ const cuts = cutoutTexture !== null && cutoutGroup !== null;
6360
+ if (cutout !== null && cuts) {
6188
6361
  this.writeCutout(slot, cutout, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS);
6189
6362
  }
6363
+ const clip = gpuBatch.animation;
6364
+ if (animation !== null && clip !== null) {
6365
+ const fields = cuts
6366
+ ? DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT_FIELDS
6367
+ : DEPTH_INSTANCED_ANIMATED_VERT_FIELDS;
6368
+ this.shadowDraws.writeFloat(slot, fields.uSceneTime.offset, this.animationTime);
6369
+ this.shadowDraws.writeFloat(slot, fields.uBoneClip.offset, clip.framesPerSecond);
6370
+ this.shadowDraws.writeFloat(slot, fields.uBoneClip.offset + 4, clip.boneScale);
6371
+ }
6190
6372
 
6191
- const cuts = cutoutGroup !== null;
6192
6373
  const cull = material?.doubleSided === true ? 'none' : this.depthCullMode;
6374
+ const animated = animation !== null;
6193
6375
  pass.setPipeline(
6194
6376
  depthPipeline(
6195
6377
  this.pipelines,
6196
6378
  this.surface.device,
6197
- cuts ? this.shadowCutoutLayout : this.shadowLayout,
6198
- `${cuts ? 'depth-instanced-cutout' : 'depth-instanced'}|${cull}|${geometry.key ?? ''}`,
6379
+ animated
6380
+ ? cuts
6381
+ ? this.shadowAnimatedCutoutLayout
6382
+ : this.shadowAnimatedLayout
6383
+ : cuts
6384
+ ? this.shadowCutoutLayout
6385
+ : this.shadowLayout,
6386
+ `${cuts ? 'depth-instanced-cutout' : 'depth-instanced'}${animated ? '-animated' : ''}` +
6387
+ `|${cull}|${geometry.key ?? ''}`,
6199
6388
  this.presentOf(geometry),
6200
6389
  cull,
6201
6390
  false,
6202
6391
  true,
6203
6392
  cuts,
6393
+ false,
6394
+ false,
6395
+ animated,
6204
6396
  ),
6205
6397
  );
6206
6398
  pass.setBindGroup(0, cutoutGroup ?? this.shadowGroup(), [slot]);
@@ -6579,6 +6771,10 @@ export class WebGPURenderer implements RendererApi {
6579
6771
  this.skinPalettes = new SkinPaletteRing(quality.drawsPerFrame);
6580
6772
  this.maxDrawingBufferPixels = quality.maxDrawingBufferPixels;
6581
6773
  this.exposure = quality.outputExposure;
6774
+ resolveFilmicCurve(DEFAULT_FILMIC_CURVE, this.filmConstants);
6775
+ if (quality.outputTransform === 'filmic' && !(quality.screenEffects && quality.hdrScene)) {
6776
+ console.warn(FILMIC_WITHOUT_COMPOSITE);
6777
+ }
6582
6778
  this.flushSchedule =
6583
6779
  quality.frameGraph && quality.identifierGraph
6584
6780
  ? createFlushSchedule(MAX_DRAWS_PER_FRAME)
@@ -6656,6 +6852,26 @@ export class WebGPURenderer implements RendererApi {
6656
6852
  );
6657
6853
  }
6658
6854
  this.skinScatter = skinScreen ? new SkinScatterPass(device) : null;
6855
+ /*
6856
+ * The frame's materials' reflections, where the frame can carry them: scene light to swap the
6857
+ * probe's share in, and one sample, which the trace's depth copy and both maps are. Said once
6858
+ * where it cannot. See `frameReflections.ts`.
6859
+ */
6860
+ this.frameReflections = resolveFrameReflections(quality.screenSpaceReflections);
6861
+ const reflectionsRefusal =
6862
+ this.frameReflections === null
6863
+ ? null
6864
+ : !quality.screenEffects || !quality.hdrScene
6865
+ ? 'the frame keeps no scene light to swap the probe in (`screenEffects` and `hdrScene`)'
6866
+ : this.samples > 1
6867
+ ? 'a multisampled frame has no single depth or colour to march through'
6868
+ : null;
6869
+ if (reflectionsRefusal !== null)
6870
+ console.warn(`WebGPU: ${frameReflectionsRefused(reflectionsRefusal)}`);
6871
+ this.reflectionSurface =
6872
+ this.frameReflections !== null && reflectionsRefusal === null
6873
+ ? new ReflectionSurfacePass(device)
6874
+ : null;
6659
6875
  this.skinPipelines = skinScreen
6660
6876
  ? new PipelineCache(
6661
6877
  device,
@@ -6690,7 +6906,7 @@ export class WebGPURenderer implements RendererApi {
6690
6906
  pointShadows: quality.pointShadows,
6691
6907
  });
6692
6908
  this.pointShadowsCompiled = quality.pointShadows;
6693
- this.fragment = flatFragmentBindings(this.variant);
6909
+ this.fragment = flatFragmentScratch(this.variant);
6694
6910
  this.bindGroupLayout = createFlatBindGroupLayout(device, this.variant);
6695
6911
  /*
6696
6912
  * The skinned twin, built eagerly because it is a descriptor rather than a compile — WebGPU
@@ -6700,6 +6916,14 @@ export class WebGPURenderer implements RendererApi {
6700
6916
  this.skinnedBindGroupLayout = createFlatBindGroupLayout(device, this.variant, true);
6701
6917
  this.morphedBindGroupLayout = createFlatBindGroupLayout(device, this.variant, false, true);
6702
6918
  this.bothBindGroupLayout = createFlatBindGroupLayout(device, this.variant, true, true);
6919
+ /* And the instanced twin that plays a bone animation: its clip's and clocks' textures. */
6920
+ this.animatedBindGroupLayout = createFlatBindGroupLayout(
6921
+ device,
6922
+ this.variant,
6923
+ false,
6924
+ false,
6925
+ true,
6926
+ );
6703
6927
  this.perDraw = new UniformRing(
6704
6928
  device,
6705
6929
  FLAT_VERT_SIZE,
@@ -6708,30 +6932,48 @@ export class WebGPURenderer implements RendererApi {
6708
6932
  'flat.vertRing',
6709
6933
  this.drawBudget,
6710
6934
  );
6711
- this.perFrameStaging = new ArrayBuffer(this.fragment.uniformSize);
6935
+ const { passSize, materialSize } = this.fragment;
6936
+ this.perFrameStaging = new ArrayBuffer(passSize + materialSize);
6712
6937
  this.perFrameFloats = new Float32Array(this.perFrameStaging);
6713
6938
  this.perFrameInts = new Int32Array(this.perFrameStaging);
6939
+ this.passInts = new Int32Array(this.perFrameStaging, 0, passSize / 4);
6940
+ this.materialInts = new Int32Array(this.perFrameStaging, passSize, materialSize / 4);
6714
6941
  /*
6715
- * **A ring, because the block holds material state and a material changes between draws.**
6716
- *
6717
- * `setSurfaceTexture`, `setSurfaceGrain` and their siblings are pass state on both backends
6718
- * — a material covers many draws, so the alternative is rebinding the same values twenty
6719
- * times to say the same thing. On WebGL2 each is a `uniform1f` that lands immediately. Here
6720
- * they live in this block, and `queue.writeBuffer` does not interleave with recorded
6721
- * commands, so one buffer rewritten between two draws gives *both* the second value. Every
6722
- * surface in a scene would wear the last material set, which is bug 7's shape at pass scale.
6942
+ * **Two blocks, because they change at two rates.** `setSurfaceTexture`, `setSurfaceGrain` and
6943
+ * their siblings are pass state on both backends — a material covers many draws — and on WebGL2
6944
+ * each is a `uniform1f` that lands immediately. Here they live in a block, and
6945
+ * `queue.writeBuffer` does not interleave with recorded commands, so one buffer rewritten between
6946
+ * two draws gives *both* the second value: every surface would wear the last material set, which
6947
+ * is bug 7's shape at pass scale. So each draw binds a slot.
6723
6948
  *
6724
- * A slot per *material change* rather than per draw: `drawMesh` reuses the open slot until
6725
- * something dirties it, so a scene drawing a hundred meshes of one material spends one.
6949
+ * It was one block until 4.10.0, lights and all, copied whole into a ring slot per material
6950
+ * change and uploaded every frame: a stage of four hundred materials sent 4.8 MB a frame to say
6951
+ * what it had said the frame before. Now the pass's block takes a ring slot per pass, and a
6952
+ * material's block a slot of the store, where an unchanged material stays from frame to frame.
6726
6953
  */
6727
- this.perFrame = new UniformRing(
6954
+ this.passRing = new UniformRing(
6728
6955
  device,
6729
- this.fragment.uniformSize,
6956
+ passSize,
6957
+ PASSES_PER_FRAME,
6958
+ USAGE_UNIFORM_DST,
6959
+ 'flat.passRing',
6960
+ );
6961
+ this.materialStore = new MaterialStore(
6962
+ device,
6963
+ materialSize,
6730
6964
  this.quality.materialChangesPerFrame,
6731
6965
  USAGE_UNIFORM_DST,
6732
- 'flat.fragRing',
6966
+ 'flat.materials',
6733
6967
  this.materialBudget,
6734
6968
  );
6969
+ /* Two a pass at most: the jittered view and the unjittered one. */
6970
+ this.viewRing = new UniformRing(
6971
+ device,
6972
+ FLAT_VIEW_SIZE,
6973
+ 2 * PASSES_PER_FRAME,
6974
+ USAGE_UNIFORM_DST,
6975
+ 'flat.viewRing',
6976
+ );
6735
6977
 
6736
6978
  /*
6737
6979
  * A one-pixel stand-in for the albedo binding. The albedo's flag is zero for untextured
@@ -6880,7 +7122,7 @@ export class WebGPURenderer implements RendererApi {
6880
7122
  })
6881
7123
  .end();
6882
7124
  }
6883
- device.queue.submit([clear.finish()]);
7125
+ this.submit([clear.finish()]);
6884
7126
  /*
6885
7127
  * `nearest` on a depth texture sampled as an ordinary float, matching what
6886
7128
  * `shadowMap.ts` asks WebGL2 for. A filtering sampler cannot be used on a depth format
@@ -7046,6 +7288,11 @@ export class WebGPURenderer implements RendererApi {
7046
7288
  addressModeU: 'clamp-to-edge',
7047
7289
  addressModeV: 'clamp-to-edge',
7048
7290
  });
7291
+ /* Or a surface overlay's atlas, in the twin a draw wearing one binds: `overlayGroup`. */
7292
+ const maps = this.overlayBinding ? this.overlayMaps : null;
7293
+ if (maps !== null) {
7294
+ return { view: maps.layerView() ?? blankView, sampler: this.refractSampler };
7295
+ }
7049
7296
  return { view: this.refractSnapshotView ?? blankView, sampler: this.refractSampler };
7050
7297
  }
7051
7298
  /*
@@ -7222,6 +7469,8 @@ export class WebGPURenderer implements RendererApi {
7222
7469
  { binding: RUSH_TEXTURES.uExposureHeld },
7223
7470
  /* Local exposure's grid, read filtered between tiles. See `localExposure.ts`. */
7224
7471
  { binding: RUSH_TEXTURES.uExposureLocal },
7472
+ /* Each mover's own motion: the reconstruction's target, or the scene where there is none. */
7473
+ { binding: RUSH_TEXTURES.uMotion },
7225
7474
  ]);
7226
7475
  this.rushUniforms = device.createBuffer({
7227
7476
  label: 'post.rushUniforms',
@@ -7239,11 +7488,29 @@ export class WebGPURenderer implements RendererApi {
7239
7488
  this.ssrLayout = createPostStageLayout(device, SSR_UNIFORMS, SSR_FRAG_SIZE, [
7240
7489
  { binding: SSR_TEXTURES.uSsrDepth, filterable: false },
7241
7490
  { binding: SSR_TEXTURES.uSsrScene },
7491
+ /* The frame's materials, for the trace that reads them: `frameReflections.ts`. */
7492
+ { binding: SSR_TEXTURES.uSsrProbeMap },
7493
+ { binding: SSR_TEXTURES.uSsrTintMap },
7242
7494
  ]);
7243
7495
  this.ssrResolveLayout = createSsrResolveLayout(device);
7496
+ this.ssrMaterialLayout = createPostStageLayout(
7497
+ device,
7498
+ SSR_MATERIAL_UNIFORMS,
7499
+ SSR_MATERIAL_SIZE,
7500
+ [
7501
+ { binding: SSR_MATERIAL_TEXTURES.uSsrReflection },
7502
+ { binding: SSR_MATERIAL_TEXTURES.uSsrProbeMap },
7503
+ { binding: SSR_MATERIAL_TEXTURES.uSsrTintMap },
7504
+ ],
7505
+ );
7506
+ this.ssrMaterialUniforms = device.createBuffer({
7507
+ label: 'post.ssrMaterialUniforms',
7508
+ size: SSR_MATERIAL_SIZE,
7509
+ usage: USAGE_UNIFORM_DST,
7510
+ });
7244
7511
  this.ssrUniforms = device.createBuffer({
7245
7512
  label: 'post.ssrUniforms',
7246
- size: SSR_SLOT * MAX_REFLECTIVE_SURFACES,
7513
+ size: SSR_SLOT * (MAX_REFLECTIVE_SURFACES + 1),
7247
7514
  usage: USAGE_UNIFORM_DST,
7248
7515
  });
7249
7516
  this.decalUniforms = device.createBuffer({
@@ -7468,6 +7735,8 @@ export class WebGPURenderer implements RendererApi {
7468
7735
  this.shadowLayout = createDepthBindGroupLayout(device);
7469
7736
  this.shadowSkinnedLayout = createDepthBindGroupLayout(device, true);
7470
7737
  this.shadowCutoutLayout = createDepthBindGroupLayout(device, false, true);
7738
+ this.shadowAnimatedLayout = createDepthBindGroupLayout(device, false, false, true);
7739
+ this.shadowAnimatedCutoutLayout = createDepthBindGroupLayout(device, false, true, true);
7471
7740
  this.shadowDraws = new UniformRing(
7472
7741
  device,
7473
7742
  /* The cutout block is the plain one and two fields more, and a slot must hold either. */
@@ -7947,7 +8216,11 @@ export class WebGPURenderer implements RendererApi {
7947
8216
  /* Last frame's snapshot is last frame's scene. */
7948
8217
  this.refractSnapshotTaken = false;
7949
8218
  this.skinScattered = false;
8219
+ /* One frame of the jitter's sequence however many times the frame binds its camera. */
8220
+ this.temporalHistory.nextFrame();
8221
+ this.latePasses.length = 0;
7950
8222
  this.skinScatter?.reset();
8223
+ this.reflectionSurface?.reset();
7951
8224
  this.plumeVerts.reset();
7952
8225
  this.plumeFrags.reset();
7953
8226
  this.waterVerts.reset();
@@ -7975,9 +8248,18 @@ export class WebGPURenderer implements RendererApi {
7975
8248
  this.lineVerts.reset();
7976
8249
  this.lineFrags.reset();
7977
8250
  this.skyUniforms.reset();
7978
- /* The material ring, and the open slot with it: last frame's slots are gone. */
7979
- this.perFrame.reset();
8251
+ /* The pass ring, and the open slots with it: last frame's pass slots are gone. Its materials
8252
+ stay in the store, which a draw finds them in again. */
8253
+ this.passRing.reset();
8254
+ this.viewRing.reset();
8255
+ this.materialStore.beginFrame();
8256
+ this.passesAsked = 0;
8257
+ this.passesRefused = 0;
8258
+ this.viewsAsked = 0;
8259
+ this.viewsRefused = 0;
8260
+ this.materialsRefused = 0;
7980
8261
  this.materials.dirty();
8262
+ this.reopenPass();
7981
8263
  /* Spent by `runMotion` at the end of the last frame; a frame states its movers afresh. */
7982
8264
  this.motionPass?.reset();
7983
8265
  this.latePass?.reset();
@@ -8312,6 +8594,40 @@ export class WebGPURenderer implements RendererApi {
8312
8594
  };
8313
8595
  }
8314
8596
 
8597
+ /**
8598
+ * The copies this frame's mesh rewrites owe (`StagedRewrite`), recorded since the last submit and
8599
+ * sent at the head of the next one: one encoder however many meshes are rewritten. Null while
8600
+ * nothing is owed.
8601
+ */
8602
+ private pendingRewrites: GPUCommandEncoder | null = null;
8603
+ /** One record handed to every staged rewrite, refilled each call, so a rewrite allocates nothing. */
8604
+ private readonly stagedRewrite: StagedRewrite = {
8605
+ encoder: null as unknown as GPUCommandEncoder,
8606
+ staging: null as unknown as GPUBuffer,
8607
+ previous: null,
8608
+ };
8609
+
8610
+ private rewriteCopies(): GPUCommandEncoder {
8611
+ this.pendingRewrites ??= this.surface.device.createCommandEncoder({
8612
+ label: 'mesh.rewriteCopies',
8613
+ });
8614
+ return this.pendingRewrites;
8615
+ }
8616
+
8617
+ /**
8618
+ * Submit `buffers`, led by any copies the frame's mesh rewrites owe. **Every submit of this
8619
+ * renderer goes through here**, because any of them may draw a rewritten mesh — the frame's own,
8620
+ * a mirror's, a shadow's, a probe's — and the copies must be ahead of the first that does.
8621
+ */
8622
+ private submit(buffers: GPUCommandBuffer[]): void {
8623
+ const rewrites = this.pendingRewrites;
8624
+ if (rewrites !== null) {
8625
+ this.pendingRewrites = null;
8626
+ buffers.unshift(rewrites.finish());
8627
+ }
8628
+ this.surface.device.queue.submit(buffers);
8629
+ }
8630
+
8315
8631
  /**
8316
8632
  * Rewrite a mesh's positions, and its normals where the caller has them.
8317
8633
  *
@@ -8334,20 +8650,35 @@ export class WebGPURenderer implements RendererApi {
8334
8650
  * `changeFrames.ts`.
8335
8651
  */
8336
8652
  const motion = mesh.motion;
8337
- let previous: GPUBuffer | null = null;
8653
+ let staged: StagedRewrite | null = null;
8338
8654
  if (motion !== null && this.reconstructionWanted) {
8339
8655
  const frame = frameOfChange(this.moverFrame, this.inFrame);
8656
+ const size = mesh.vertexBuffers[0]?.size ?? 0;
8657
+ let previous: GPUBuffer | null = null;
8340
8658
  if (keepsPrevious(motion.changed, frame)) {
8341
8659
  motion.previous ??= this.surface.device.createBuffer({
8342
8660
  label: 'recon.dynamicPrevious',
8343
- size: mesh.vertexBuffers[0]?.size ?? 0,
8661
+ size,
8344
8662
  usage: 0x0020 | 0x0008, // VERTEX | COPY_DST
8345
8663
  });
8346
8664
  previous = motion.previous;
8347
8665
  }
8348
8666
  motion.changed = frame;
8667
+ /*
8668
+ * **Staged, and copied in behind the copy aside, in an encoder the frame's next submit leads
8669
+ * with**: no submit of the rewrite's own. See `StagedRewrite` and `submit`.
8670
+ */
8671
+ motion.staging ??= this.surface.device.createBuffer({
8672
+ label: 'mesh.stagedPositions',
8673
+ size,
8674
+ usage: 0x0004 | 0x0008, // COPY_SRC | COPY_DST
8675
+ });
8676
+ staged = this.stagedRewrite;
8677
+ staged.encoder = this.rewriteCopies();
8678
+ staged.staging = motion.staging;
8679
+ staged.previous = previous;
8349
8680
  }
8350
- mesh.update(this.surface.device, positions, normals, previous);
8681
+ mesh.update(this.surface.device, positions, normals, staged);
8351
8682
  }
8352
8683
 
8353
8684
  /**
@@ -8551,12 +8882,20 @@ export class WebGPURenderer implements RendererApi {
8551
8882
  * number, so this is a copy and is marked as one; the two must move together.
8552
8883
  */
8553
8884
  f[at('uGrain')] = 1;
8885
+ /* No reflection and an environment at its own brightness, from the pass and from no material. */
8886
+ this.passReflectivity = 0;
8887
+ this.passEnvironmentGain = 1;
8888
+ this.materialReflectivity = null;
8889
+ this.materialEnvironmentGain = null;
8554
8890
  f[at('uEnvironmentDials') + 1] = 0;
8555
8891
  /* No weighting and no dither until a pass or a crossfade asks. See `setDitherFade`. */
8556
8892
  f[at('uWriteMode')] = 0;
8557
8893
  f[at('uWriteMode') + 1] = 0;
8558
8894
  /* 1, not 0: a multiplier's identity is one. See `setEnvironmentGain`. */
8559
8895
  f[at('uEnvironmentDials') + 2] = 1;
8896
+ /* No overlay until a draw sets one. See `setSurfaceOverlay`. */
8897
+ this.overlayFloats.fill(0);
8898
+ f.fill(0, at('uOverlay'), at('uOverlay') + OVERLAY_FLOATS);
8560
8899
  /* The frame's own ambient until a draw asks for its own. See `setAmbientSH`. */
8561
8900
  f.fill(0, at('uAmbientSH'), at('uAmbientSH') + AMBIENT_SH_FLOATS);
8562
8901
  /*
@@ -8662,6 +9001,8 @@ export class WebGPURenderer implements RendererApi {
8662
9001
  */
8663
9002
  if (this.quality.nightEmissive) f[at('uNightEmissive')] = env.nightEmissive ?? 0;
8664
9003
  f.set(env.highlightMin, at('uHighlightMin'));
9004
+ /* The frame the cutout dither moves with, in the near corner's w. See `uHighlightMin`. */
9005
+ f[at('uHighlightMin') + 3] = this.cutoutFrame;
8665
9006
  f.set(env.highlightMax, at('uHighlightMax'));
8666
9007
  /* The gain in the far corner's w. See `uHighlightMax`. */
8667
9008
  f[at('uHighlightMax') + 3] = env.highlightGain;
@@ -8934,6 +9275,7 @@ export class WebGPURenderer implements RendererApi {
8934
9275
  * same terms here, which is why a pass cannot inherit a material from the one before it.
8935
9276
  */
8936
9277
  this.materials.dirty();
9278
+ this.reopenPass();
8937
9279
  /*
8938
9280
  * A pass starts with no material, exactly as `renderer.ts` resets these terms — **every map,
8939
9281
  * not the albedo alone**. `setMaterial` skips the lookup when the maps it is given are the ones
@@ -8949,6 +9291,8 @@ export class WebGPURenderer implements RendererApi {
8949
9291
  this.emissiveMap = null;
8950
9292
  this.modelMap = null;
8951
9293
  this.bindGroup = this.blankAlbedoBindGroup;
9294
+ /* Nor an overlay; its numbers are zeroed with the block's other defaults. */
9295
+ this.overlayMaps = null;
8952
9296
  this.uvScale[0] = 1;
8953
9297
  this.uvScale[1] = 1;
8954
9298
  this.uvOffset[0] = 0;
@@ -8956,31 +9300,114 @@ export class WebGPURenderer implements RendererApi {
8956
9300
  }
8957
9301
 
8958
9302
  /**
8959
- * The slot the next draw's material lives in, taking a new one when something dirtied it.
9303
+ * The slot the next draw's material lives in, placing it when something dirtied it, with the
9304
+ * pass's slot taken first where `bindMeshPass` reopened it: `passOffset` is that one's offset.
8960
9305
  *
8961
- * **A copy per material change, not per draw.** A scene drawing a hundred meshes of one
8962
- * material spends one slot and one copy; a scene switching material every draw spends one
8963
- * each, which is what it asked for.
9306
+ * **A placement per material change, not per draw.** A scene drawing a hundred meshes of one
9307
+ * material places it once; a scene switching material every draw places one each, and a material
9308
+ * the store already holds — the same numbers as a draw before it, this frame or an earlier one —
9309
+ * is bound where it is and uploads nothing.
8964
9310
  */
8965
9311
  private materialSlotForDraw(): number | null {
9312
+ if (!this.passBlock.open) {
9313
+ this.passesAsked += 1;
9314
+ const pass = this.passRing.allocate();
9315
+ if (pass === null) {
9316
+ this.passesRefused += 1;
9317
+ this.materialBudget.ask();
9318
+ this.materialBudget.drop();
9319
+ if (!this.warnedPassesFull) {
9320
+ this.warnedPassesFull = true;
9321
+ console.warn(
9322
+ `WebGPU: more than ${this.passRing.slots} mesh passes drew in one frame; the draws of the rest are skipped this frame and the ring grows for the next.`,
9323
+ );
9324
+ }
9325
+ return null;
9326
+ }
9327
+ this.passRing.writeBlock(pass, this.passInts);
9328
+ this.passBlock.slot = pass;
9329
+ }
8966
9330
  if (this.materials.open) return this.materials.slot;
8967
- const slot = this.perFrame.allocate();
9331
+ const slot = this.materialStore.place(this.materialInts);
8968
9332
  if (slot === null) {
8969
- if (!this.warnedMaterialsFull) {
8970
- this.warnedMaterialsFull = true;
9333
+ this.materialsRefused += 1;
9334
+ this.warnMaterialsFull();
9335
+ return null;
9336
+ }
9337
+ this.materials.slot = slot;
9338
+ return slot;
9339
+ }
9340
+
9341
+ /** The pass slot `materialSlotForDraw` left open, read beside the material's by every lit draw. */
9342
+ private get passOffset(): number {
9343
+ return this.passBlock.slot;
9344
+ }
9345
+
9346
+ /** A new pass: its block, and both its views, are taken afresh by the next draw. */
9347
+ private reopenPass(): void {
9348
+ this.passBlock.dirty();
9349
+ this.reopenViews();
9350
+ }
9351
+
9352
+ /** What a view holds changed — the wind, the light's matrix — so the next draw takes a new one. */
9353
+ private reopenViews(): void {
9354
+ this.viewBlock.dirty();
9355
+ this.lateViewBlock.dirty();
9356
+ }
9357
+
9358
+ /**
9359
+ * The view slot a lit draw reads its camera from: the pass's, or its unjittered twin for a blended
9360
+ * draw landing after the upscale. Taken by the first draw that needs it after the pass, the wind or
9361
+ * the light's matrix changed, and shared by every draw until then. Null when the ring is full, which
9362
+ * skips the draw and grows the ring for the next frame, as the pass ring does.
9363
+ */
9364
+ private viewSlotFor(late: boolean, viewProj: Float32Array): number | null {
9365
+ const block = late ? this.lateViewBlock : this.viewBlock;
9366
+ if (block.open) return block.slot;
9367
+ this.viewsAsked += 1;
9368
+ const slot = this.viewRing.allocate();
9369
+ if (slot === null) {
9370
+ this.viewsRefused += 1;
9371
+ this.materialBudget.ask();
9372
+ this.materialBudget.drop();
9373
+ if (!this.warnedPassesFull) {
9374
+ this.warnedPassesFull = true;
8971
9375
  console.warn(
8972
- `WebGPU: more than ${this.quality.materialChangesPerFrame} material changes in a frame; the draws asking for the rest are skipped. renderer.frameBudget names the line and the count.`,
9376
+ `WebGPU: more than ${this.viewRing.slots} views drew in one frame; the draws of the rest are skipped this frame and the ring grows for the next.`,
8973
9377
  );
8974
9378
  }
8975
9379
  return null;
8976
9380
  }
8977
- this.perFrame.writeBlock(slot, this.perFrameInts);
8978
- this.materials.slot = slot;
9381
+ this.writeView(this.viewRing, slot, viewProj);
9382
+ block.slot = slot;
8979
9383
  return slot;
8980
9384
  }
8981
9385
 
9386
+ /** A view block: the camera, the light's matrix and the frame's wind, into `ring` at `slot`. */
9387
+ private writeView(ring: UniformRing, slot: number, viewProj: Float32Array): void {
9388
+ ring.writeFloats(slot, FLAT_VIEW_FIELDS.uViewProj.offset, viewProj);
9389
+ ring.writeFloats(slot, FLAT_VIEW_FIELDS.uLightViewProj.offset, this.lightViewProj);
9390
+ /* The scene's clock, which a bone animation's instances read their moment from. */
9391
+ ring.writeFloat(slot, FLAT_VIEW_FIELDS.uSceneTime.offset, this.animationTime);
9392
+ const d = this.frameWind;
9393
+ ring.writeFloats(slot, FLAT_VIEW_FIELDS.uWindDirection.offset, d.direction);
9394
+ ring.writeFloat(slot, FLAT_VIEW_FIELDS.uWindSpeed.offset, d.bend);
9395
+ ring.writeFloat(slot, FLAT_VIEW_FIELDS.uWindGust.offset, d.gust);
9396
+ ring.writeFloat(slot, FLAT_VIEW_FIELDS.uWindTime.offset, d.time);
9397
+ ring.writeFloats(slot, FLAT_VIEW_FIELDS.uWindSpatialPhase.offset, d.spatialPhase);
9398
+ }
9399
+
9400
+ private warnMaterialsFull(): void {
9401
+ if (this.warnedMaterialsFull) return;
9402
+ this.warnedMaterialsFull = true;
9403
+ console.warn(
9404
+ `WebGPU: more than ${this.quality.materialChangesPerFrame} distinct materials in a frame; the draws asking for the rest are skipped. renderer.frameBudget names the line and the count.`,
9405
+ );
9406
+ }
9407
+
8982
9408
  /** Said once rather than every frame, for the reason `warnedFull` gives. */
8983
9409
  private warnedMaterialsFull = false;
9410
+ private warnedPassesFull = false;
8984
9411
  private warnedSkyFull = false;
8985
9412
  private warnedParticlesTwice = false;
8986
9413
  /**
@@ -9021,7 +9448,14 @@ export class WebGPURenderer implements RendererApi {
9021
9448
  * What this saves is the GPU's share, which for a heavy mesh is most of the cost and for a
9022
9449
  * trivial one is very little.
9023
9450
  */
9024
- if (this.quality.cullDraws && !this.visible(mesh.bounds, model as ReadonlyMat4)) return;
9451
+ /* A static list draws every entry in every view, since a bundle cannot leave one out. */
9452
+ if (
9453
+ this.quality.cullDraws &&
9454
+ this.staticRecording === null &&
9455
+ !this.visible(mesh.bounds, model as ReadonlyMat4)
9456
+ ) {
9457
+ return;
9458
+ }
9025
9459
  /*
9026
9460
  * And the other half of the same question, where the profile asked for one. Gated on
9027
9461
  * `cullDraws` alongside the frustum test, because both are the renderer doing a consumer's
@@ -9036,6 +9470,9 @@ export class WebGPURenderer implements RendererApi {
9036
9470
  * where it happened to pass, write motion for a surface no pixel of the frame belongs to.
9037
9471
  */
9038
9472
  if (this.tracksMotionNow()) this.recordDrawMotion(mesh, model, previousModel);
9473
+ else if (previousModel !== null && this.quality.cameraMotionBlur * this.motionBlurScale > 0) {
9474
+ this.warnObjectBlur();
9475
+ }
9039
9476
  noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
9040
9477
  /* An opaque draw meets the medium as the dial says: `submitMesh` reads an absent `fog` as
9041
9478
  true, which is a translucent draw's default and would put every opaque draw back in the fog
@@ -9209,21 +9646,16 @@ export class WebGPURenderer implements RendererApi {
9209
9646
  `TranslucentMeshOptions.reconstructed`. */
9210
9647
  const lands = blend && options.reconstructed !== true && this.drawsLate();
9211
9648
  const late = lands || (blend && this.oitReplaying && this.reconstructing);
9212
- this.perDraw.writeFloats(
9213
- slot,
9214
- FLAT_VERT_FIELDS.uViewProj.offset,
9215
- late ? this.correctedViewProj : this.viewProj,
9216
- );
9649
+ /* The camera, the light's matrix and the wind are the pass's, in its view block. */
9650
+ const view = this.viewSlotFor(late, late ? this.correctedViewProj : this.viewProj);
9651
+ if (view === null) return;
9652
+ this.drawViewOffset = view;
9217
9653
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uModel.offset, model);
9218
9654
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uTint.offset, tint ?? WHITE);
9219
- /* The frame's wind, for a mesh carrying a sway lane. Written on this path and not the
9220
- instanced one, which declares none of these fields. */
9221
- this.writeWind(slot);
9222
9655
  /* The material's repeats, not a constant: geometry authored in metres is textured at
9223
9656
  whatever density the material asks for without rebuilding it. */
9224
9657
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
9225
9658
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvOffset.offset, this.uvOffset);
9226
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
9227
9659
  /*
9228
9660
  * Whether this mesh's tangent frame is real. WebGPU has no disabled attribute, so location 10
9229
9661
  * is always backed — by the caller's data or by the constant buffer — which makes the binding
@@ -9420,8 +9852,10 @@ export class WebGPURenderer implements RendererApi {
9420
9852
  command.pipeline = pipeline;
9421
9853
  command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
9422
9854
  command.offsetA = slot;
9423
- command.offsetB = material;
9424
- command.offsetCount = 2;
9855
+ command.offsetB = this.passOffset;
9856
+ command.offsetC = material;
9857
+ command.offsetD = this.drawViewOffset;
9858
+ command.offsetCount = 4;
9425
9859
  for (let index = 0; index < mesh.vertexBuffers.length; index++) {
9426
9860
  command.vertexBuffers[index] = mesh.vertexBuffers[index] as GPUBuffer;
9427
9861
  }
@@ -9433,7 +9867,12 @@ export class WebGPURenderer implements RendererApi {
9433
9867
  const pass = this.openPass();
9434
9868
  if (pass === null) return;
9435
9869
  pass.setPipeline(pipeline);
9436
- pass.setBindGroup(0, this.flatBindGroupFor(skinned, morphed, deltas), [slot, material]);
9870
+ pass.setBindGroup(0, this.flatBindGroupFor(skinned, morphed, deltas), [
9871
+ slot,
9872
+ this.passOffset,
9873
+ material,
9874
+ this.drawViewOffset,
9875
+ ]);
9437
9876
  for (let index = 0; index < mesh.vertexBuffers.length; index++) {
9438
9877
  pass.setVertexBuffer(index, mesh.vertexBuffers[index] as GPUBuffer);
9439
9878
  }
@@ -9455,6 +9894,26 @@ export class WebGPURenderer implements RendererApi {
9455
9894
  material,
9456
9895
  );
9457
9896
  }
9897
+ /* And its surface half, for the frame's materials' reflections. See `keepReflectionSurface`. */
9898
+ if (!blend && lit && depthWrite && this.reflectionSurfaceKeeps()) {
9899
+ this.keepReflectionSurface(
9900
+ mesh,
9901
+ key,
9902
+ base,
9903
+ skinned,
9904
+ morphed,
9905
+ deltas,
9906
+ layer,
9907
+ eight,
9908
+ clothed,
9909
+ modelKind,
9910
+ slot,
9911
+ material,
9912
+ null,
9913
+ 1,
9914
+ null,
9915
+ );
9916
+ }
9458
9917
  }
9459
9918
  /* Back to the defaults for whatever is drawn next, exactly as `renderer.ts` restores them —
9460
9919
  unconditionally, because the scratch state above is shared by every draw after this one
@@ -9529,16 +9988,127 @@ export class WebGPURenderer implements RendererApi {
9529
9988
  'instanced pipeline.',
9530
9989
  );
9531
9990
  }
9991
+ const animation = (options.animation ?? null) as GpuBoneAnimation | null;
9992
+ /* The bone a vertex follows is its second coordinates' u, which a mesh carries in its grain
9993
+ lane; a mesh with none has nothing to name a bone by. See `boneAnimation.ts`. */
9994
+ if (animation !== null && !this.presentOf(mesh as GpuMesh & { key?: string })['grain']) {
9995
+ throw new Error(
9996
+ 'WebGPU: a bone animation reads each vertex’s bone from the mesh’s second coordinates ' +
9997
+ '(`MeshData.lightmapUvs`), and this mesh has none.',
9998
+ );
9999
+ }
9532
10000
  const batch = new GpuInstancedBatch(
9533
10001
  this.surface.device,
9534
10002
  mesh,
9535
10003
  capacity,
9536
10004
  `flat.instances:${(mesh as GpuMesh & { key?: string }).key ?? 'mesh'}`,
9537
10005
  options.cull ?? false,
10006
+ animation,
9538
10007
  );
9539
10008
  return batch;
9540
10009
  }
9541
10010
 
10011
+ /** See `Renderer.setAnimationTime`. Every view taken after it reads the new clock. */
10012
+ setAnimationTime(seconds: number): void {
10013
+ if (!Number.isFinite(seconds) || seconds === this.animationTime) return;
10014
+ this.animationTime = seconds;
10015
+ this.reopenViews();
10016
+ }
10017
+
10018
+ /** The clock `setAnimationTime` set, 0 until it is. */
10019
+ private animationTime = 0;
10020
+
10021
+ /** See `Renderer.createBoneAnimation`. */
10022
+ createBoneAnimation(clip: BoneAnimationClip): BoneAnimationHandle {
10023
+ return new GpuBoneAnimation(this.surface.device, packBoneAnimation(clip));
10024
+ }
10025
+
10026
+ /** See `Renderer.disposeBoneAnimation`. A batch still playing it draws nothing after. */
10027
+ disposeBoneAnimation(animation: BoneAnimationHandle): void {
10028
+ const clip = animation as GpuBoneAnimation;
10029
+ if (!clip.disposed) clip.dispose();
10030
+ }
10031
+
10032
+ /** An animated batch's clip and clocks, as its groups bind them. */
10033
+ private animationViews(batch: GpuInstancedBatch): AnimationViews | null {
10034
+ const animation = batch.animation;
10035
+ const clocks = batch.clocks;
10036
+ if (animation === null || clocks === null || animation.disposed) return null;
10037
+ return { places: animation.placesView, turns: animation.turnsView, clocks: clocks.view };
10038
+ }
10039
+
10040
+ /** An animated batch's groups, dropped wherever the renderer drops its own. */
10041
+ private freshAnimatedGroups(batch: GpuInstancedBatch): void {
10042
+ if (batch.groupEpoch === this.flatGroupEpoch) return;
10043
+ batch.litGroups.clear();
10044
+ batch.depthGroups.clear();
10045
+ batch.depthPeelGroups.clear();
10046
+ batch.groupEpoch = this.flatGroupEpoch;
10047
+ }
10048
+
10049
+ /**
10050
+ * The lit group an animated batch draws with: the material's group, or its overlay twin, again
10051
+ * with the clip's and the clocks' textures — kept on the batch by the group it was built beside,
10052
+ * as a skinned twin is kept by its palette slot.
10053
+ */
10054
+ private animatedGroup(batch: GpuInstancedBatch, views: AnimationViews): GPUBindGroup {
10055
+ this.freshAnimatedGroups(batch);
10056
+ const base = this.overlayGroup();
10057
+ let group = batch.litGroups.get(base);
10058
+ if (group === undefined) {
10059
+ this.overlayBinding = base !== this.bindGroup;
10060
+ group = createFlatBindGroup(
10061
+ this.surface.device,
10062
+ this.animatedBindGroupLayout,
10063
+ this.variant,
10064
+ this.perDraw.buffer,
10065
+ this.passRing.buffer,
10066
+ this.materialStore.buffer,
10067
+ this.viewRing.buffer,
10068
+ this.flatTextures,
10069
+ null,
10070
+ null,
10071
+ null,
10072
+ views,
10073
+ );
10074
+ this.overlayBinding = false;
10075
+ this.bindGroupBudget.ask();
10076
+ batch.litGroups.set(base, group);
10077
+ }
10078
+ return group;
10079
+ }
10080
+
10081
+ /** An animated caster's shadow group, by what it cuts with and whether this is the peel. */
10082
+ private animatedDepthGroup(
10083
+ batch: GpuInstancedBatch,
10084
+ views: AnimationViews,
10085
+ cutout: GpuSurfaceTexture | null,
10086
+ ): GPUBindGroup | null {
10087
+ this.freshAnimatedGroups(batch);
10088
+ const peel = this.shadowLayerIsPeel && this.shadowPeelPassUniforms !== null;
10089
+ const groups = peel ? batch.depthPeelGroups : batch.depthGroups;
10090
+ const held = groups.get(cutout);
10091
+ if (held !== undefined) return held;
10092
+ const passUniforms = peel ? this.shadowPeelPassUniforms : this.shadowPassUniforms;
10093
+ const previousView = peel ? this.sunStaticArrayView : this.shadowBlankView;
10094
+ const previousSampler = peel ? this.shadowPeelSampler : this.shadowBlankSampler;
10095
+ if (passUniforms === null || previousView === null || previousSampler === null) return null;
10096
+ const group = createDepthBindGroup(
10097
+ this.surface.device,
10098
+ cutout === null ? this.shadowAnimatedLayout : this.shadowAnimatedCutoutLayout,
10099
+ this.shadowDraws.buffer,
10100
+ passUniforms,
10101
+ previousView,
10102
+ previousSampler,
10103
+ null,
10104
+ cutout === null ? null : { view: cutout.view, sampler: cutout.sampler },
10105
+ null,
10106
+ views,
10107
+ );
10108
+ groups.set(cutout, group);
10109
+ return group;
10110
+ }
10111
+
9542
10112
  /** The device instance culls, built on the first culling draw. See `InstanceCullPass`. */
9543
10113
  private instanceCulls: InstanceCullPass | null = null;
9544
10114
  /** Counts frames, so a batch knows when its cull slots were last taken. */
@@ -9572,7 +10142,12 @@ export class WebGPURenderer implements RendererApi {
9572
10142
  this.submitInstanced(batch, data, 1, false, {});
9573
10143
  /* A batch that moved this frame pairs each slot with last frame's; the batch is the identity. */
9574
10144
  const previous = batch.previousBuffer;
9575
- if (previous !== null && batch.changed === this.moverFrame && this.tracksMotionNow()) {
10145
+ if (
10146
+ previous !== null &&
10147
+ this.staticRecording === null &&
10148
+ batch.changed === this.moverFrame &&
10149
+ this.tracksMotionNow()
10150
+ ) {
9576
10151
  const paired = pairedInstances(batch.previousCount, Math.min(data.count, batch.capacity));
9577
10152
  if (paired > 0) this.motionPass?.recordInstanced(batch.mesh, batch.buffer, previous, paired);
9578
10153
  }
@@ -9606,6 +10181,7 @@ export class WebGPURenderer implements RendererApi {
9606
10181
  const refraction = options.refraction ?? 0;
9607
10182
  const glassy = resolveGlass(options.glass, this.glassScratch);
9608
10183
  if ((refraction <= 0 && !glassy) || !this.takeRefractSnapshot()) return false;
10184
+ this.seeThroughBound = true;
9609
10185
  const glass = this.glassScratch;
9610
10186
  /* Strength, path thickness, transmission, frost: one vector, for the budget the shader states. */
9611
10187
  const seeAt = this.materialField('uSeeThrough');
@@ -9618,6 +10194,8 @@ export class WebGPURenderer implements RendererApi {
9618
10194
  this.perFrameFloats[tintAt] = refractTint?.[0] ?? 1;
9619
10195
  this.perFrameFloats[tintAt + 1] = refractTint?.[1] ?? 1;
9620
10196
  this.perFrameFloats[tintAt + 2] = refractTint?.[2] ?? 1;
10197
+ /* w: this framebuffer's first row is the top, so up the screen is down the rows. */
10198
+ this.perFrameFloats[tintAt + 3] = -1;
9621
10199
  const glassTintAt = this.materialField('uGlassTint');
9622
10200
  this.perFrameFloats[glassTintAt] = glass.tint[0];
9623
10201
  this.perFrameFloats[glassTintAt + 1] = glass.tint[1];
@@ -9627,6 +10205,7 @@ export class WebGPURenderer implements RendererApi {
9627
10205
 
9628
10206
  /** Put the scratch back after `bindSeeThrough`: refraction and glass off. */
9629
10207
  private releaseSeeThrough(): void {
10208
+ this.seeThroughBound = false;
9630
10209
  const seeAt = this.materialField('uSeeThrough');
9631
10210
  this.perFrameFloats[seeAt] = 0;
9632
10211
  this.perFrameFloats[seeAt + 2] = 0;
@@ -9692,10 +10271,16 @@ export class WebGPURenderer implements RendererApi {
9692
10271
  /* A culling batch's camera draw: the whole batch against the view and the occluders on the CPU,
9693
10272
  then instance by instance on the device into an indirect draw. Blended draws keep every
9694
10273
  instance and their order. See `InstancedOptions.cull`. */
9695
- const culling = !blend && batch.cull;
10274
+ const culling = !blend && batch.cull && this.staticRecording === null;
9696
10275
  if (culling && !batchBoxVisible(batch.box, this.frustum, this.occlusion)) return;
10276
+ /* A batch playing a bone animation reads its clocks by instance index, which a compaction
10277
+ would reorder: it is culled whole and never instance by instance. */
10278
+ const animation = batch.animation === null ? null : this.animationViews(batch);
10279
+ if (batch.animation !== null && animation === null) return;
9697
10280
  const cut =
9698
- culling && cullsInstances(count, mesh.indexCount) ? this.takeCullSlot(batch, count) : null;
10281
+ culling && animation === null && cullsInstances(count, mesh.indexCount)
10282
+ ? this.takeCullSlot(batch, count)
10283
+ : null;
9699
10284
  const instanceBuffer = cut === null ? batch.buffer : cut.instances;
9700
10285
 
9701
10286
  const slot = this.perDraw.allocate();
@@ -9711,15 +10296,22 @@ export class WebGPURenderer implements RendererApi {
9711
10296
 
9712
10297
  /* Late unless it moves with what it lies on. See `TranslucentMeshOptions.reconstructed`. */
9713
10298
  const lands = blend && options.reconstructed !== true && this.drawsLate();
9714
- this.perDraw.writeFloats(
9715
- slot,
9716
- FLAT_VERT_FIELDS.uViewProj.offset,
9717
- lands ? this.correctedViewProj : this.viewProj,
9718
- );
10299
+ const view = this.viewSlotFor(lands, lands ? this.correctedViewProj : this.viewProj);
10300
+ if (view === null) return;
10301
+ this.drawViewOffset = view;
9719
10302
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
9720
10303
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvOffset.offset, this.uvOffset);
9721
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
9722
10304
  this.perDraw.writeInt(slot, FLAT_VERT_FIELDS.uHasTangents.offset, mesh.hasTangents ? 1 : 0);
10305
+ /* The clip's speed and bone scale, read by the animated variant's own field. */
10306
+ const clip = batch.animation;
10307
+ if (animation !== null && clip !== null) {
10308
+ this.perDraw.writeFloat(
10309
+ slot,
10310
+ FLAT_ANIMATED_VERT_FIELDS.uBoneClip.offset,
10311
+ clip.framesPerSecond,
10312
+ );
10313
+ this.perDraw.writeFloat(slot, FLAT_ANIMATED_VERT_FIELDS.uBoneClip.offset + 4, clip.boneScale);
10314
+ }
9723
10315
 
9724
10316
  const dimmed = opacity < 1;
9725
10317
  if (dimmed) {
@@ -9757,7 +10349,7 @@ export class WebGPURenderer implements RendererApi {
9757
10349
  plain one would hand a draw the wrong layout. */
9758
10350
  const adds = blend && options.additive === true;
9759
10351
  const key =
9760
- `${base}|inst${blend ? (adds ? '|add' : '|blend') : ''}` +
10352
+ `${base}|inst${animation === null ? '' : '|anim'}${blend ? (adds ? '|add' : '|blend') : ''}` +
9761
10353
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
9762
10354
  `${this.materialDoubleSided ? '|2s' : ''}${coverage ? '|a2c' : ''}` +
9763
10355
  `${modelKind === null ? '' : `|m:${modelKind}`}`;
@@ -9772,7 +10364,7 @@ export class WebGPURenderer implements RendererApi {
9772
10364
  pipeline = flatPipeline(
9773
10365
  pipelines,
9774
10366
  this.surface.device,
9775
- this.flatLayoutFor(false, false),
10367
+ animation === null ? this.flatLayoutFor(false, false) : this.animatedBindGroupLayout,
9776
10368
  this.variant,
9777
10369
  key,
9778
10370
  present,
@@ -9788,6 +10380,9 @@ export class WebGPURenderer implements RendererApi {
9788
10380
  false,
9789
10381
  false,
9790
10382
  modelKind,
10383
+ 'whole',
10384
+ false,
10385
+ animation !== null,
9791
10386
  );
9792
10387
  }
9793
10388
 
@@ -9798,10 +10393,15 @@ export class WebGPURenderer implements RendererApi {
9798
10393
  : this.recordDraw(0, this.currentTarget());
9799
10394
  if (command !== null) {
9800
10395
  command.pipeline = pipeline;
9801
- command.bindGroup = this.flatBindGroupFor(false, false, null);
10396
+ command.bindGroup =
10397
+ animation === null
10398
+ ? this.flatBindGroupFor(false, false, null)
10399
+ : this.animatedGroup(batch, animation);
9802
10400
  command.offsetA = slot;
9803
- command.offsetB = material;
9804
- command.offsetCount = 2;
10401
+ command.offsetB = this.passOffset;
10402
+ command.offsetC = material;
10403
+ command.offsetD = this.drawViewOffset;
10404
+ command.offsetCount = 4;
9805
10405
  let index = 0;
9806
10406
  for (; index < mesh.vertexBuffers.length; index++) {
9807
10407
  command.vertexBuffers[index] = mesh.vertexBuffers[index] as GPUBuffer;
@@ -9817,7 +10417,13 @@ export class WebGPURenderer implements RendererApi {
9817
10417
  const pass = this.openPass();
9818
10418
  if (pass === null) return;
9819
10419
  pass.setPipeline(pipeline);
9820
- pass.setBindGroup(0, this.flatBindGroupFor(false, false, null), [slot, material]);
10420
+ pass.setBindGroup(
10421
+ 0,
10422
+ animation === null
10423
+ ? this.flatBindGroupFor(false, false, null)
10424
+ : this.animatedGroup(batch, animation),
10425
+ [slot, this.passOffset, material, this.drawViewOffset],
10426
+ );
9821
10427
  let index = 0;
9822
10428
  for (; index < mesh.vertexBuffers.length; index++) {
9823
10429
  pass.setVertexBuffer(index, mesh.vertexBuffers[index] as GPUBuffer);
@@ -9827,6 +10433,27 @@ export class WebGPURenderer implements RendererApi {
9827
10433
  if (cut === null) pass.drawIndexed(mesh.indexCount, count);
9828
10434
  else pass.drawIndexedIndirect(cut.args, 0);
9829
10435
  }
10436
+ /* And its surface half, for the frame's materials' reflections, with the same survivors. */
10437
+ if (!blend && lit && depthWrite && this.reflectionSurfaceKeeps()) {
10438
+ this.keepReflectionSurface(
10439
+ mesh,
10440
+ key,
10441
+ base,
10442
+ false,
10443
+ false,
10444
+ null,
10445
+ layer,
10446
+ false,
10447
+ false,
10448
+ modelKind,
10449
+ slot,
10450
+ material,
10451
+ instanceBuffer,
10452
+ count,
10453
+ cut === null ? null : cut.args,
10454
+ animation === null ? null : batch,
10455
+ );
10456
+ }
9830
10457
  }
9831
10458
  /* Back to the defaults, unconditionally and for the reason `drawMesh` gives at length: this
9832
10459
  scratch block is cumulative state shared by every draw after this one, and an early return
@@ -10037,6 +10664,7 @@ export class WebGPURenderer implements RendererApi {
10037
10664
  if (view === null) return false;
10038
10665
 
10039
10666
  this.lightViewProj.set(lightViewProj as Float32Array);
10667
+ this.reopenViews();
10040
10668
  mat4.multiply(this.correctedLightViewProj, SHADOW_CLIP_CORRECTION, lightViewProj);
10041
10669
  this.shadowLayerIsPeel = peel;
10042
10670
  this.shadowLayerIsDynamic = dynamic;
@@ -10149,6 +10777,264 @@ export class WebGPURenderer implements RendererApi {
10149
10777
  casters(this.sceneCasterSink);
10150
10778
  }
10151
10779
 
10780
+ /* -- Static draws ---------------------------------------------------------------- */
10781
+
10782
+ /** See `Renderer.createStaticDraws`. Recorded on its first draw, into each view it is drawn in. */
10783
+ createStaticDraws(casters: ShadowCasters): StaticDrawsHandle {
10784
+ return new GpuStaticDraws(
10785
+ this.surface.device,
10786
+ captureStaticDraws(casters),
10787
+ this.fragment.passSize,
10788
+ FLAT_VIEW_SIZE,
10789
+ FLAT_VERT_SIZE,
10790
+ );
10791
+ }
10792
+
10793
+ /**
10794
+ * See `Renderer.drawStaticDraws`. One bundle executed, after a pass block and a view block are
10795
+ * written for this view; the list is recorded first where it has no recording that is current for
10796
+ * this target and starting state. See `staticBundles.ts`.
10797
+ */
10798
+ drawStaticDraws(draws: StaticDrawsHandle): void {
10799
+ if (this.surface.lost) return;
10800
+ const list = draws as GpuStaticDraws;
10801
+ if (list.disposed) {
10802
+ this.warnStaticDisposed();
10803
+ return;
10804
+ }
10805
+ if (!this.canDraw() || this.viewProj === null) return;
10806
+ /* A list's draws are rigid and wear no overlay: put back to none, as `bindMeshPass` does. */
10807
+ this.setSkinPalette(null);
10808
+ this.setMorphWeights(null);
10809
+ this.setCloth(null);
10810
+ this.setSurfaceOverlay(null);
10811
+ this.setMaterial(null);
10812
+ this.sceneCasterMaterial = undefined;
10813
+ /* A probe face gives its slots back after every face; a list replays there draw by draw. */
10814
+ if (this.probePassActive) {
10815
+ this.replayStaticImmediately(list);
10816
+ return;
10817
+ }
10818
+ /* The copy first: taking one may grow the list's rings, which no recording before can bind. */
10819
+ const copy = list.takeCopy(this.moverFrame);
10820
+ const current = this.staticRecordingFor(list);
10821
+ if (current === null || current.immediate) {
10822
+ this.replayStaticImmediately(list);
10823
+ return;
10824
+ }
10825
+ current.usedIn = this.moverFrame;
10826
+ list.writePass(copy, this.passInts);
10827
+ this.writeView(list.views, list.viewOffset(copy), this.viewProj);
10828
+ list.uploadCopy(copy);
10829
+
10830
+ const cache = current.cache;
10831
+ let frame = current.frameBundles[copy];
10832
+ if (frame === undefined || frame === null) {
10833
+ frame = encodeBundle(
10834
+ this.surface.device,
10835
+ {
10836
+ colorFormats: [cache.format],
10837
+ depthStencilFormat: DEPTH_FORMAT,
10838
+ sampleCount: cache.sampleCount,
10839
+ depthReadOnly: false,
10840
+ },
10841
+ current.frame,
10842
+ list.passOffset(copy),
10843
+ list.viewOffset(copy),
10844
+ 'static.frame',
10845
+ );
10846
+ current.frameBundles[copy] = frame;
10847
+ }
10848
+ const command = this.recordDraw(0, this.currentTarget());
10849
+ if (command !== null) {
10850
+ command.bundles = frame;
10851
+ command.pipeline = null;
10852
+ command.bindGroup = null;
10853
+ } else {
10854
+ this.openPass()?.executeBundles(frame);
10855
+ }
10856
+ const surface = this.reflectionSurface;
10857
+ if (current.surfaceKept && surface !== null && current.surface.taken > 0) {
10858
+ let halves = current.surfaceBundles[copy];
10859
+ if (halves === undefined || halves === null) {
10860
+ halves = encodeBundle(
10861
+ this.surface.device,
10862
+ {
10863
+ colorFormats: [null, REFLECTION_SURFACE_FORMAT, REFLECTION_SURFACE_FORMAT],
10864
+ depthStencilFormat: DEPTH_FORMAT,
10865
+ sampleCount: 1,
10866
+ depthReadOnly: true,
10867
+ },
10868
+ current.surface,
10869
+ list.passOffset(copy),
10870
+ list.viewOffset(copy),
10871
+ 'static.surface',
10872
+ );
10873
+ current.surfaceBundles[copy] = halves;
10874
+ }
10875
+ const kept = surface.take();
10876
+ kept.bundles = halves;
10877
+ }
10878
+ /* Counted as the draws would have been, so a frame reports the same on both backends. */
10879
+ for (let draw = 0; draw < current.frame.taken; draw++) this.drawBudget.ask();
10880
+ if (!this.staticJustRecorded) {
10881
+ for (let ask = 0; ask < current.materialAsks; ask++) this.materialBudget.ask();
10882
+ }
10883
+ this.staticJustRecorded = false;
10884
+ }
10885
+
10886
+ /** A list drawn the way `drawSceneCasters` draws, a draw at a time, for where a bundle cannot be. */
10887
+ private replayStaticImmediately(list: GpuStaticDraws): void {
10888
+ this.staticJustRecorded = false;
10889
+ replayStaticDraws(list, this.sceneCasterSink);
10890
+ this.setMaterial(null);
10891
+ this.sceneCasterMaterial = undefined;
10892
+ }
10893
+
10894
+ /** See `Renderer.disposeStaticDraws`. Its pins let go; its meshes and materials are the caller's. */
10895
+ disposeStaticDraws(draws: StaticDrawsHandle): void {
10896
+ const list = draws as GpuStaticDraws;
10897
+ if (list.disposed) return;
10898
+ list.dispose(this.unpinFor(list));
10899
+ }
10900
+
10901
+ /** The recording to replay `list` from here, made now if none is current; null if it cannot be. */
10902
+ private staticRecordingFor(list: GpuStaticDraws): StaticRecording | null {
10903
+ /* Geometry still arriving is not drawn yet, so a recording now would leave it out for good. */
10904
+ for (const entry of list.entries) {
10905
+ const mesh = entry.kind === 'mesh' ? entry.mesh : (entry.batch as GpuInstancedBatch).mesh;
10906
+ if (!mesh.complete) return null;
10907
+ }
10908
+ const cache = this.targetPipelines();
10909
+ const start = this.materialInts;
10910
+ const startHash = hashWords(start, start.length);
10911
+ const surfaceKept = this.reflectionSurfaceKeeps();
10912
+ let recording = list.find(cache, start, startHash, this.cutoutResolveStaged, surfaceKept);
10913
+ if (
10914
+ recording !== null &&
10915
+ recording.cacheVersion === cache.version &&
10916
+ recording.groupEpoch === this.flatGroupEpoch &&
10917
+ recording.storeEpoch === this.materialStore.epoch &&
10918
+ recording.ringEpoch === list.ringEpoch
10919
+ ) {
10920
+ return recording;
10921
+ }
10922
+ const unpin = this.unpinFor(list);
10923
+ if (recording === null) {
10924
+ recording = list.open(cache, start, startHash, this.cutoutResolveStaged, surfaceKept, unpin);
10925
+ } else {
10926
+ list.clear(recording, recording.storeEpoch === this.materialStore.epoch ? unpin : () => {});
10927
+ }
10928
+ this.recordStatic(list, recording);
10929
+ return recording;
10930
+ }
10931
+
10932
+ /** How a list's pins are let go: through the store, unless it has forgotten them since. */
10933
+ private unpinFor(list: GpuStaticDraws): (offset: number) => void {
10934
+ const epoch = this.materialStore.epoch;
10935
+ return (offset) => {
10936
+ if (this.materialStore.epoch === epoch && list.pinEpoch === epoch) {
10937
+ this.materialStore.unpin(offset);
10938
+ }
10939
+ };
10940
+ }
10941
+
10942
+ /**
10943
+ * Record `list` into `recording`: its entries through the scene-caster path, with the list's
10944
+ * rings and group cache swapped in for the frame's and the pass and view slots held at the list's
10945
+ * first copy, so every command the path records binds the list's buffers and offsets the bundle
10946
+ * can rewrite per copy. Everything swapped is put back, whatever the path throws.
10947
+ */
10948
+ private recordStatic(list: GpuStaticDraws, recording: StaticRecording): void {
10949
+ if (list.groupEpoch !== this.flatGroupEpoch) {
10950
+ list.groups = new Map();
10951
+ list.blank = null;
10952
+ list.groupEpoch = this.flatGroupEpoch;
10953
+ }
10954
+ list.drawRing.reset();
10955
+ const perDraw = this.perDraw;
10956
+ const passRing = this.passRing;
10957
+ const viewRing = this.viewRing;
10958
+ const groups = this.flatBindGroups;
10959
+ const blank = this.blankAlbedoBindGroup;
10960
+ const bindGroup = this.bindGroup;
10961
+ const passSlot = this.passBlock.slot;
10962
+ const viewSlot = this.viewBlock.slot;
10963
+ const lateSlot = this.lateViewBlock.slot;
10964
+ const asked = this.materialBudget.used;
10965
+ this.perDraw = list.drawRing;
10966
+ this.passRing = list.passes;
10967
+ this.viewRing = list.views;
10968
+ this.flatBindGroups = list.groups as typeof groups;
10969
+ /* The blank group over the list's rings, built as the frame's is: with no maps held. */
10970
+ list.blank ??= this.buildFlatBindGroup();
10971
+ this.blankAlbedoBindGroup = list.blank;
10972
+ this.bindGroup = this.flatGroupForMaps(null, null, null, null, null);
10973
+ this.passBlock.slot = list.passOffset(0);
10974
+ this.viewBlock.slot = list.viewOffset(0);
10975
+ this.lateViewBlock.slot = list.viewOffset(0);
10976
+ this.materials.dirty();
10977
+ this.staticRecording = recording;
10978
+ try {
10979
+ this.sceneCasterMaterial = undefined;
10980
+ replayStaticDraws(list, this.sceneCasterSink);
10981
+ } finally {
10982
+ this.staticRecording = null;
10983
+ this.perDraw = perDraw;
10984
+ this.passRing = passRing;
10985
+ this.viewRing = viewRing;
10986
+ this.flatBindGroups = groups;
10987
+ this.blankAlbedoBindGroup = blank;
10988
+ this.bindGroup = bindGroup;
10989
+ this.passBlock.slot = passSlot;
10990
+ this.viewBlock.slot = viewSlot;
10991
+ this.lateViewBlock.slot = lateSlot;
10992
+ this.materials.dirty();
10993
+ this.sceneCasterMaterial = undefined;
10994
+ this.setMaterial(null);
10995
+ }
10996
+ recording.materialAsks = this.materialBudget.used - asked;
10997
+ const store = this.materialStore;
10998
+ recording.cacheVersion = recording.cache.version;
10999
+ recording.groupEpoch = this.flatGroupEpoch;
11000
+ recording.storeEpoch = store.epoch;
11001
+ recording.ringEpoch = list.ringEpoch;
11002
+ /* A skin split into halves is replayed draw by draw, so nothing recorded is kept or pinned. */
11003
+ if (recording.immediate) {
11004
+ resetPool(recording.frame);
11005
+ resetPool(recording.surface);
11006
+ return;
11007
+ }
11008
+ /* Every material the commands bind, pinned once, so nothing displaces what a bundle bakes. */
11009
+ for (const pool of [recording.frame, recording.surface]) {
11010
+ for (let i = 0; i < pool.taken; i++) {
11011
+ const offset = (pool.commands[i] as DrawCommand).offsetC;
11012
+ if (!recording.pins.includes(offset)) {
11013
+ store.pin(offset);
11014
+ recording.pins.push(offset);
11015
+ }
11016
+ }
11017
+ }
11018
+ list.pinEpoch = store.epoch;
11019
+ list.uploadDraws();
11020
+ this.staticJustRecorded = true;
11021
+ }
11022
+
11023
+ /** The recording under way, which `recordDraw` and the side passes take their commands into. */
11024
+ private staticRecording: StaticRecording | null = null;
11025
+ /** Whether the replay about to count its material changes recorded them, and so counted them. */
11026
+ private staticJustRecorded = false;
11027
+
11028
+ /** A disposed list drawn, said once: the frame loop does not throw. */
11029
+ private warnStaticDisposed(): void {
11030
+ if (this.warnedStaticDisposed) return;
11031
+ this.warnedStaticDisposed = true;
11032
+ console.warn('drawStaticDraws: this list was disposed, and draws nothing.');
11033
+ }
11034
+ private warnedStaticDisposed = false;
11035
+ /** Bumped wherever the lit groups are dropped, which a static list's own groups follow. */
11036
+ private flatGroupEpoch = 0;
11037
+
10152
11038
  drawShadowCasters(casters: ShadowCasters): void {
10153
11039
  if (this.surface.lost || this.shadowPass === null) return;
10154
11040
  casters(this.casterSink);
@@ -10494,7 +11380,7 @@ export class WebGPURenderer implements RendererApi {
10494
11380
  }
10495
11381
  this.shadowDraws.flush();
10496
11382
  this.scatterDepthDraws.flush();
10497
- this.surface.device.queue.submit([this.shadowEncoder.finish()]);
11383
+ this.submit([this.shadowEncoder.finish()]);
10498
11384
  /* A growth's old texture, copied out of by what was just submitted and needed no longer. */
10499
11385
  this.retiredSunTexture?.destroy();
10500
11386
  this.retiredSunTexture = null;
@@ -10534,6 +11420,20 @@ export class WebGPURenderer implements RendererApi {
10534
11420
  private warnedNoPointShadowArray = false;
10535
11421
  /** Said once, for the retired ambient dial. */
10536
11422
  private warnedEnvironmentAmbient = false;
11423
+ /** The overlay the draws wear, `uOverlay`: see `setSurfaceOverlay`. */
11424
+ private readonly overlayFloats = new Float32Array(OVERLAY_FLOATS);
11425
+ /** Its atlas, bound where the refraction copy goes in a twin of each flat group. */
11426
+ private overlayMaps: GpuSurfaceTexture | null = null;
11427
+ /**
11428
+ * Each flat group's twin with the atlas in the refraction slot, by the group and the atlas, built
11429
+ * on the first draw that wears it and dropped wherever `flatBindGroups` is.
11430
+ */
11431
+ private readonly overlayGroups = new Map<GPUBindGroup, Map<GpuSurfaceTexture, GPUBindGroup>>();
11432
+ /** True while a twin is built: what `flatTextures` hands the refraction slot then. */
11433
+ private overlayBinding = false;
11434
+ /** True between `bindSeeThrough` and `releaseSeeThrough`: that draw reads the copy, not the atlas. */
11435
+ private seeThroughBound = false;
11436
+ private warnedOverlayRefraction = false;
10537
11437
  private readonly castingWorldIndices = new Int32Array(POINT_SHADOW_POOL).fill(-1);
10538
11438
  private readonly resolvedPointShadows = createResolvedPointShadows(MAX_POINT_LIGHTS);
10539
11439
  /** One face's view-projection, rebuilt per face rather than allocated. */
@@ -10770,7 +11670,7 @@ export class WebGPURenderer implements RendererApi {
10770
11670
  * its own slot.
10771
11671
  */
10772
11672
  this.pointShadowArray?.flush();
10773
- this.surface.device.queue.submit([this.shadowEncoder.finish()]);
11673
+ this.submit([this.shadowEncoder.finish()]);
10774
11674
  this.shadowEncoder = null;
10775
11675
  this.shadowPass = null;
10776
11676
  this.gpuTimer.end();
@@ -10977,19 +11877,58 @@ export class WebGPURenderer implements RendererApi {
10977
11877
  this.bindGroupLayout,
10978
11878
  this.variant,
10979
11879
  this.perDraw.buffer,
10980
- this.perFrame.buffer,
11880
+ this.passRing.buffer,
11881
+ this.materialStore.buffer,
11882
+ this.viewRing.buffer,
10981
11883
  this.flatTextures,
10982
11884
  );
10983
11885
  }
10984
11886
 
11887
+ /**
11888
+ * The open material's flat group, or its twin with the surface overlay's atlas in the refraction
11889
+ * slot where the draw wears one and does not refract. A twin is built from the live material,
11890
+ * which `bindGroup` always is, and kept by the group and the atlas.
11891
+ *
11892
+ * **A draw that refracts, or is glass, keeps the frame's copy** and its overlay reads no image —
11893
+ * the shader asks the same of `uSeeThrough` (`overlayImagesBound`) — said once.
11894
+ */
11895
+ private overlayGroup(): GPUBindGroup {
11896
+ const maps = this.overlayMaps;
11897
+ if (maps === null) return this.bindGroup;
11898
+ if (this.seeThroughBound) {
11899
+ if (!this.warnedOverlayRefraction) {
11900
+ this.warnedOverlayRefraction = true;
11901
+ console.warn(
11902
+ 'WebGPU: a draw that refracts or is glass wears a surface overlay with maps. The frame ' +
11903
+ "copy it reads takes the overlay's slot, so its rim, dissolve and wrinkles read no image.",
11904
+ );
11905
+ }
11906
+ return this.bindGroup;
11907
+ }
11908
+ let byMaps = this.overlayGroups.get(this.bindGroup);
11909
+ if (byMaps === undefined) {
11910
+ byMaps = new Map();
11911
+ this.overlayGroups.set(this.bindGroup, byMaps);
11912
+ }
11913
+ let twin = byMaps.get(maps);
11914
+ if (twin === undefined) {
11915
+ this.overlayBinding = true;
11916
+ twin = this.buildFlatBindGroup();
11917
+ this.overlayBinding = false;
11918
+ this.bindGroupBudget.ask();
11919
+ byMaps.set(maps, twin);
11920
+ }
11921
+ return twin;
11922
+ }
11923
+
10985
11924
  /**
10986
11925
  * The bind group a draw uses, skinned or not.
10987
11926
  *
10988
- * **The skinned twin is cached against the two things that can invalidate it**, rather than
10989
- * against a flag somebody has to remember to clear at six call sites: the unskinned group it was
10990
- * built beside — which is rebuilt whenever the material or a shadow slot moves — and the palette
10991
- * view, which changes only when the texture is reallocated. Comparing object identity means a
10992
- * missed invalidation is not possible rather than merely unlikely, and it is why
11927
+ * **The skinned twin is kept by palette slot and by the unskinned group it was built beside**,
11928
+ * which is one a material, and compared against the views it holds — the palette's, which
11929
+ * changes only when the slot's texture grows, the morph deltas and the cloth's — rather than
11930
+ * against a flag somebody has to remember to clear at six call sites. Comparing object identity
11931
+ * means a missed invalidation is not possible rather than merely unlikely, and it is why
10993
11932
  * `SkinPaletteTexture.view()` caches: a fresh view every call never compares equal and would
10994
11933
  * rebuild this every draw.
10995
11934
  */
@@ -10998,39 +11937,53 @@ export class WebGPURenderer implements RendererApi {
10998
11937
  morphed: boolean,
10999
11938
  deltas: GPUTextureView | null,
11000
11939
  ): GPUBindGroup {
11001
- if (!skinned && !morphed) return this.bindGroup;
11940
+ const base = this.overlayGroup();
11941
+ if (!skinned && !morphed) return base;
11002
11942
  const palette = skinned ? this.skinPalettes.view(this.skinPaletteSlot) : null;
11003
11943
  /* Slot `k` caches at `k + 1`; index 0 is the morphed draw that is not skinned and has no
11004
11944
  slot of its own. See `skinnedGroups`. */
11005
11945
  const at = skinned ? this.skinPaletteSlot + 1 : 0;
11006
11946
  /* A skinned group carries the cloth's views, or the stand-in's: see `clothViews`. */
11007
11947
  const cloth = skinned ? this.clothViews() : null;
11948
+ let byMaterial = this.skinnedGroups[at];
11949
+ if (byMaterial === undefined) {
11950
+ byMaterial = new Map();
11951
+ this.skinnedGroups[at] = byMaterial;
11952
+ }
11953
+ let twin = byMaterial.get(base);
11008
11954
  if (
11009
- (this.skinnedGroups[at] ?? null) === null ||
11010
- this.skinnedGroupsBeside[at] !== this.bindGroup ||
11011
- this.skinnedGroupsPalette[at] !== palette ||
11012
- this.skinnedGroupsDeltas[at] !== deltas ||
11013
- this.skinnedGroupsCloth[at] !== (cloth?.particles ?? null) ||
11014
- this.skinnedGroupsClothBinding[at] !== (cloth?.binding ?? null)
11955
+ twin === undefined ||
11956
+ twin.palette !== palette ||
11957
+ twin.deltas !== deltas ||
11958
+ twin.cloth !== (cloth?.particles ?? null) ||
11959
+ twin.clothBinding !== (cloth?.binding ?? null)
11015
11960
  ) {
11016
- this.skinnedGroups[at] = createFlatBindGroup(
11961
+ /* Built from the live material, with the overlay's atlas where `base` is its twin. */
11962
+ this.overlayBinding = base !== this.bindGroup;
11963
+ const group = createFlatBindGroup(
11017
11964
  this.surface.device,
11018
11965
  this.flatLayoutFor(skinned, morphed),
11019
11966
  this.variant,
11020
11967
  this.perDraw.buffer,
11021
- this.perFrame.buffer,
11968
+ this.passRing.buffer,
11969
+ this.materialStore.buffer,
11970
+ this.viewRing.buffer,
11022
11971
  this.flatTextures,
11023
11972
  palette,
11024
11973
  deltas,
11025
11974
  cloth,
11026
11975
  );
11027
- this.skinnedGroupsBeside[at] = this.bindGroup;
11028
- this.skinnedGroupsPalette[at] = palette;
11029
- this.skinnedGroupsDeltas[at] = deltas;
11030
- this.skinnedGroupsCloth[at] = cloth?.particles ?? null;
11031
- this.skinnedGroupsClothBinding[at] = cloth?.binding ?? null;
11976
+ this.overlayBinding = false;
11977
+ twin = {
11978
+ group,
11979
+ palette,
11980
+ deltas,
11981
+ cloth: cloth?.particles ?? null,
11982
+ clothBinding: cloth?.binding ?? null,
11983
+ };
11984
+ byMaterial.set(base, twin);
11032
11985
  }
11033
- return this.skinnedGroups[at] as GPUBindGroup;
11986
+ return twin.group;
11034
11987
  }
11035
11988
 
11036
11989
  /** The layout matching a pair of vertex flags. A pipeline and its bind group must agree. */
@@ -11068,8 +12021,14 @@ export class WebGPURenderer implements RendererApi {
11068
12021
  if (this.drawBudget.dropped > 0) {
11069
12022
  grew = this.perDraw.growTo(roomFor(this.drawBudget.used)) || grew;
11070
12023
  }
11071
- if (this.materialBudget.dropped > 0) {
11072
- grew = this.perFrame.growTo(roomFor(this.materialBudget.used)) || grew;
12024
+ if (this.materialsRefused > 0) {
12025
+ grew = this.materialStore.growTo(roomFor(this.materialBudget.used)) || grew;
12026
+ }
12027
+ if (this.passesRefused > 0) {
12028
+ grew = this.passRing.growTo(roomFor(this.passesAsked)) || grew;
12029
+ }
12030
+ if (this.viewsRefused > 0) {
12031
+ grew = this.viewRing.growTo(roomFor(this.viewsAsked)) || grew;
11073
12032
  }
11074
12033
  if (grew) this.rebuildFlatGroupsForNewRings();
11075
12034
  }
@@ -11090,8 +12049,10 @@ export class WebGPURenderer implements RendererApi {
11090
12049
  */
11091
12050
  private rebuildFlatGroupsForNewRings(): void {
11092
12051
  this.flatBindGroups.clear();
11093
- this.flatEnvironment = this.flatTextures('uEnvironment').view;
12052
+ this.flatGroupEpoch += 1;
12053
+ this.overlayGroups.clear();
11094
12054
  this.skinnedGroups.length = 0;
12055
+ this.flatEnvironment = this.flatTextures('uEnvironment').view;
11095
12056
 
11096
12057
  const albedo = this.albedo;
11097
12058
  const normalMap = this.normalMap;
@@ -11119,6 +12080,7 @@ export class WebGPURenderer implements RendererApi {
11119
12080
  );
11120
12081
  /* The open material lives in a slot of a buffer that no longer exists. */
11121
12082
  this.materials.dirty();
12083
+ this.reopenPass();
11122
12084
  }
11123
12085
 
11124
12086
  /**
@@ -11153,6 +12115,9 @@ export class WebGPURenderer implements RendererApi {
11153
12115
  */
11154
12116
  private rebuildFlatBindGroup(): void {
11155
12117
  this.flatBindGroups.clear();
12118
+ this.flatGroupEpoch += 1;
12119
+ this.overlayGroups.clear();
12120
+ this.skinnedGroups.length = 0;
11156
12121
  this.flatEnvironment = this.flatTextures('uEnvironment').view;
11157
12122
  if (
11158
12123
  this.albedo === null &&
@@ -11394,9 +12359,14 @@ export class WebGPURenderer implements RendererApi {
11394
12359
  ? (OUTPUT_TRANSFORM_CODE[this.quality.outputTransform] ?? 0)
11395
12360
  : 0;
11396
12361
  f[at('uOutputExposure')] = graded ? this.exposure : 1;
12362
+ f.set(this.filmA, at('uFilmA'));
12363
+ f.set(this.filmB, at('uFilmB'));
12364
+ f[at('uDisplayHeadroom')] = this.headroom;
11397
12365
  f.set(this.reprojection, at('uReprojection'));
11398
12366
  f[at('uMotionStrength')] = motionStrength;
11399
- f[at('uMotionMax')] = MOTION_BLUR_MAX_UV;
12367
+ f[at('uMotionMax')] = this.motionBlurMax;
12368
+ /* Each mover's own motion, where the reconstruction drew its target this frame. */
12369
+ f[at('uObjectMotion')] = this.reconstructing && this.reconMotionView !== null ? 1 : 0;
11400
12370
  f[at('uAoStrength')] = this.frameProjection === null ? 0 : aoStrength;
11401
12371
  /*
11402
12372
  * The occlusion read where this frame's jitter put it (`uAoOffset`): measured from the jittered
@@ -11440,6 +12410,7 @@ export class WebGPURenderer implements RendererApi {
11440
12410
  f[at('uGradeSize')] = this.gradeSize;
11441
12411
  /* The lens and the print. See `setVignette` and `setFilmGrain`. */
11442
12412
  f[at('uVignette')] = this.vignette;
12413
+ f.set(this.fringe, at('uFringe'));
11443
12414
  f[at('uAutoExposure')] = adapting ? this.autoExposure : 0;
11444
12415
  f[at('uLocalExposure')] = measuring && localising ? this.localExposure : 0;
11445
12416
  f[at('uGrain')] = this.grain;
@@ -11485,7 +12456,11 @@ export class WebGPURenderer implements RendererApi {
11485
12456
  private rebuildRushBindGroup(): void {
11486
12457
  if (this.sceneColorView === null || this.resolvedDepthView === null) return;
11487
12458
  if (this.aoTargetView === null) return;
11488
- this.rushBindGroup = this.buildRushBindGroup('post.rushBindGroup', this.sceneColorView);
12459
+ this.rushBindGroup = this.buildRushBindGroup(
12460
+ 'post.rushBindGroup',
12461
+ this.sceneColorView,
12462
+ this.sceneColorView,
12463
+ );
11489
12464
  /*
11490
12465
  * **And the reconstruction's twin, from the same three call sites.** A colour grade replaces
11491
12466
  * the lookup table and a resize replaces the depth and the occlusion target, and both groups
@@ -11493,12 +12468,21 @@ export class WebGPURenderer implements RendererApi {
11493
12468
  * come to read a stale view on exactly the frames a consumer changed something.
11494
12469
  */
11495
12470
  if (this.reconShownView !== null) {
11496
- this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
12471
+ this.reconRushGroup = this.buildRushBindGroup(
12472
+ 'recon.rushBindGroup',
12473
+ this.reconShownView,
12474
+ this.reconMotionView ?? this.reconShownView,
12475
+ );
11497
12476
  }
11498
12477
  }
11499
12478
 
11500
12479
  /** The composite's group over one scene source. See `rebuildRushBindGroup`. */
11501
- private buildRushBindGroup(label: string, sceneView: GPUTextureView): GPUBindGroup | null {
12480
+ private buildRushBindGroup(
12481
+ label: string,
12482
+ sceneView: GPUTextureView,
12483
+ /** Each mover's own motion, or a stand-in the shader is told not to read. */
12484
+ motionView: GPUTextureView,
12485
+ ): GPUBindGroup | null {
11502
12486
  const { device } = this.surface;
11503
12487
  if (this.resolvedDepthView === null || this.aoTargetView === null) return null;
11504
12488
  const entry = (
@@ -11530,6 +12514,7 @@ export class WebGPURenderer implements RendererApi {
11530
12514
  and a declared binding still needs a complete texture, as the bloom fallback says. */
11531
12515
  ...entry(RUSH_TEXTURES.uExposureHeld, this.exposurePass?.heldView ?? sceneView),
11532
12516
  ...entry(RUSH_TEXTURES.uExposureLocal, this.exposurePass?.localView ?? sceneView),
12517
+ ...entry(RUSH_TEXTURES.uMotion, motionView),
11533
12518
  ],
11534
12519
  layout: this.rushLayout,
11535
12520
  });
@@ -11813,7 +12798,11 @@ export class WebGPURenderer implements RendererApi {
11813
12798
  );
11814
12799
  }
11815
12800
  /* The composite's own group, over the resolved picture instead of the scene target. */
11816
- this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
12801
+ this.reconRushGroup = this.buildRushBindGroup(
12802
+ 'recon.rushBindGroup',
12803
+ this.reconShownView,
12804
+ this.reconMotionView ?? this.reconShownView,
12805
+ );
11817
12806
  /* And the scene's, because `ensureComposite` built it before this one existed. */
11818
12807
  this.rebuildRushBindGroup();
11819
12808
  /* And the bloom's, which blooms the resolved picture: see `rebuildBloomGroups`. */
@@ -11927,7 +12916,7 @@ export class WebGPURenderer implements RendererApi {
11927
12916
  const depth = this.depthView;
11928
12917
  if (!this.reconstructing || late === null || shown === null || depth === null) return;
11929
12918
  const oitSet = this.oitActive && this.translucentQueue.length > 0;
11930
- if (late.pending === 0 && !oitSet) return;
12919
+ if (late.pending === 0 && !oitSet && this.latePasses.length === 0) return;
11931
12920
  const width = Math.max(1, this.surface.canvas.width);
11932
12921
  const height = Math.max(1, this.surface.canvas.height);
11933
12922
  /* The order-independent set tests against this depth too, so it is made for either. */
@@ -11951,20 +12940,64 @@ export class WebGPURenderer implements RendererApi {
11951
12940
  this.refractSnapshotTaken = true;
11952
12941
  }
11953
12942
  this.lateRefractWanted = false;
11954
- if (late.pending === 0) return;
11955
- const pass = encoder.beginRenderPass({
11956
- label: 'recon.late',
11957
- timestampWrites: this.gpuTimer.writesFor(),
11958
- colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
11959
- depthStencilAttachment: {
11960
- view: late.depthView(width, height),
11961
- depthLoadOp: 'load',
11962
- /* Kept: the order-independent set replays against it after this pass. */
11963
- depthStoreOp: 'store',
11964
- },
11965
- });
11966
- late.replay(pass, this.issueBound);
11967
- pass.end();
12943
+ if (late.pending > 0) {
12944
+ const pass = encoder.beginRenderPass({
12945
+ label: 'recon.late',
12946
+ timestampWrites: this.gpuTimer.writesFor(),
12947
+ colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
12948
+ depthStencilAttachment: {
12949
+ view: late.depthView(width, height),
12950
+ depthLoadOp: 'load',
12951
+ /* Kept: the order-independent set replays against it after this pass. */
12952
+ depthStoreOp: 'store',
12953
+ },
12954
+ });
12955
+ late.replay(pass, this.issueBound);
12956
+ pass.end();
12957
+ }
12958
+ this.runLatePasses(encoder, shown, late.depthView(width, height));
12959
+ }
12960
+
12961
+ /**
12962
+ * The registered passes `drawPass` queued for after the upscale: each handed a fresh copy of the
12963
+ * reconstructed picture with every late draw in it, in the output-size texture the late
12964
+ * refraction keeps (its own reads are done by now: they were recorded before this copy), then
12965
+ * drawn over the picture in a pass of its own.
12966
+ */
12967
+ private runLatePasses(
12968
+ encoder: GPUCommandEncoder,
12969
+ shown: GPUTextureView,
12970
+ depth: GPUTextureView,
12971
+ ): void {
12972
+ const copy = this.refractSnapshot;
12973
+ const source = this.reconShown;
12974
+ for (const handle of this.latePasses) {
12975
+ const definition = passAt(this.passes, handle);
12976
+ if (definition === undefined) continue;
12977
+ if (copy !== null && source !== null) {
12978
+ encoder.copyTextureToTexture({ texture: source }, { texture: copy }, [
12979
+ copy.width,
12980
+ copy.height,
12981
+ ]);
12982
+ }
12983
+ const pass = encoder.beginRenderPass({
12984
+ label: 'recon.latePass',
12985
+ timestampWrites: this.gpuTimer.writesFor(),
12986
+ colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
12987
+ depthStencilAttachment: { view: depth, depthLoadOp: 'load', depthStoreOp: 'store' },
12988
+ });
12989
+ const context = this.passContext;
12990
+ context.pass = pass;
12991
+ context.outputTransform = this.gradeCode();
12992
+ context.outputExposure = this.gradeExposure();
12993
+ context.jitter = this.latePassJitter;
12994
+ context.format = RECON_HISTORY_FORMAT;
12995
+ context.depthFormat = DEPTH_FORMAT;
12996
+ context.samples = 1;
12997
+ definition.draw(context as PassContext);
12998
+ context.jitter = this.passJitter;
12999
+ pass.end();
13000
+ }
11968
13001
  }
11969
13002
 
11970
13003
  /** Whether this draw lands in the reconstructed picture. See `lateRouting.ts`. */
@@ -12391,7 +13424,10 @@ export class WebGPURenderer implements RendererApi {
12391
13424
  * other is the disagreement the parity rule exists to prevent. Said once rather than dropped.
12392
13425
  */
12393
13426
  private runReflections(encoder: GPUCommandEncoder): void {
12394
- if (this.reflectionQueue.length === 0) return;
13427
+ const frameReflections = this.frameReflections;
13428
+ const surfaceHalf = this.reflectionSurface;
13429
+ const materials = frameReflections !== null && surfaceHalf !== null;
13430
+ if (this.reflectionQueue.length === 0 && !materials) return;
12395
13431
  const scene = this.sceneColorView;
12396
13432
  const reflection = this.ssrReflectionView;
12397
13433
  const group = this.ssrBindGroup;
@@ -12436,9 +13472,81 @@ export class WebGPURenderer implements RendererApi {
12436
13472
  f[base + at('uSsrThickness')] = surface.thicknessM;
12437
13473
  f[base + at('uSsrSteps')] = surface.steps;
12438
13474
  f[base + at('uSsrEdgeFade')] = REFLECTION_EDGE_FADE;
13475
+ f[base + at('uSsrMaterial')] = 0;
12439
13476
  slot++;
12440
13477
  });
13478
+ const regions = slot;
13479
+ /* The frame's materials' trace takes the slot after the surfaces': the box, its axis and its
13480
+ strength unread, the march and the ceiling its own. See `frameReflections.ts`. */
13481
+ if (materials) {
13482
+ const base = (slot * SSR_SLOT) / 4;
13483
+ f.set(this.ssrDepthToWorld, base + at('uSsrDepthToWorld'));
13484
+ f.set(this.ssrViewProj, base + at('uSsrViewProj'));
13485
+ f.set(this.frameEye, base + at('uSsrEye'));
13486
+ f.fill(0, base + at('uSsrAxis'), base + at('uSsrAxis') + 3);
13487
+ f.fill(1, base + at('uSsrTint'), base + at('uSsrTint') + 3);
13488
+ f[base + at('uSsrStrength')] = 1;
13489
+ f[base + at('uSsrFresnel')] = 0;
13490
+ f[base + at('uSsrFacingCos')] = -1;
13491
+ f[base + at('uSsrReach')] = frameReflections.reachM;
13492
+ f[base + at('uSsrThickness')] = frameReflections.thicknessM;
13493
+ f[base + at('uSsrSteps')] = frameReflections.steps;
13494
+ f[base + at('uSsrEdgeFade')] = REFLECTION_EDGE_FADE;
13495
+ f[base + at('uSsrMaterial')] = 1;
13496
+ f[base + at('uSsrMaxRoughness')] = frameReflections.maxRoughness;
13497
+ slot++;
13498
+ }
12441
13499
  device.queue.writeBuffer(this.ssrUniforms, 0, this.ssrStaging, 0, slot * SSR_SLOT);
13500
+ const tracePipeline = postPipeline(
13501
+ this.pipelines,
13502
+ device,
13503
+ this.ssrLayout,
13504
+ 'post.ssrTrace',
13505
+ SSR_TRACE_FRAG_WGSL,
13506
+ this.pipelines.format,
13507
+ PREMULTIPLIED_OVER_BLEND,
13508
+ );
13509
+
13510
+ if (materials && this.depthView !== null && this.ssrMaterialGroup !== null) {
13511
+ /* The frame's opaque lit draws again, into what the trace and the resolve read. */
13512
+ surfaceHalf.draw(encoder, this.depthView, this.issueBound);
13513
+ const trace = encoder.beginRenderPass({
13514
+ label: 'post.ssr.materialTrace',
13515
+ colorAttachments: [
13516
+ { view: reflection, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
13517
+ ],
13518
+ });
13519
+ trace.setPipeline(tracePipeline);
13520
+ trace.setBindGroup(0, group, [regions * SSR_SLOT]);
13521
+ trace.draw(3);
13522
+ trace.end();
13523
+ /* The blur's texel and its radius at roughness 1, as a share of the frame's height. */
13524
+ const m = this.ssrMaterialFloats;
13525
+ const blurAt = this.postField(SSR_MATERIAL_FIELDS, 'uSsrBlur');
13526
+ m[blurAt] = 1 / Math.max(width, 1);
13527
+ m[blurAt + 1] = 1 / Math.max(height, 1);
13528
+ m[blurAt + 2] = frameReflections.blur * height;
13529
+ device.queue.writeBuffer(this.ssrMaterialUniforms, 0, m);
13530
+ const resolve = encoder.beginRenderPass({
13531
+ label: 'post.ssr.materialResolve',
13532
+ colorAttachments: [{ view: scene, loadOp: 'load', storeOp: 'store' }],
13533
+ });
13534
+ resolve.setPipeline(
13535
+ postPipeline(
13536
+ this.pipelines,
13537
+ device,
13538
+ this.ssrMaterialLayout,
13539
+ 'post.ssrMaterialResolve',
13540
+ SSR_MATERIAL_RESOLVE_FRAG_WGSL,
13541
+ this.pipelines.format,
13542
+ FRAME_REFLECTION_BLEND,
13543
+ ),
13544
+ );
13545
+ resolve.setBindGroup(0, this.ssrMaterialGroup, [0]);
13546
+ resolve.draw(3);
13547
+ resolve.end();
13548
+ }
13549
+ if (regions === 0) return;
12442
13550
 
12443
13551
  /* Cleared whole and then scissored per surface: the resolve reads every pixel, and one left
12444
13552
  from the last frame would composite that reflection over this frame's scene. */
@@ -12448,17 +13556,7 @@ export class WebGPURenderer implements RendererApi {
12448
13556
  { view: reflection, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
12449
13557
  ],
12450
13558
  });
12451
- trace.setPipeline(
12452
- postPipeline(
12453
- this.pipelines,
12454
- device,
12455
- this.ssrLayout,
12456
- 'post.ssrTrace',
12457
- SSR_TRACE_FRAG_WGSL,
12458
- this.pipelines.format,
12459
- PREMULTIPLIED_OVER_BLEND,
12460
- ),
12461
- );
13559
+ trace.setPipeline(tracePipeline);
12462
13560
  slot = 0;
12463
13561
  this.reflectionQueue.replay((surface) => {
12464
13562
  const index = slot++;
@@ -13222,6 +14320,101 @@ export class WebGPURenderer implements RendererApi {
13222
14320
  );
13223
14321
  }
13224
14322
 
14323
+ /** Whether a draw now is the frame's own, and so has a surface half to keep. */
14324
+ private reflectionSurfaceKeeps(): boolean {
14325
+ return (
14326
+ this.reflectionSurface !== null &&
14327
+ !this.reflectionPassActive &&
14328
+ !this.probePassActive &&
14329
+ !this.capturePassActive &&
14330
+ !this.framePresented &&
14331
+ !this.overlayActive &&
14332
+ this.oitMode === 'none'
14333
+ );
14334
+ }
14335
+
14336
+ /**
14337
+ * An opaque lit draw's surface half, kept for the frame's materials' reflections: the draw again,
14338
+ * with the same slots, buffers and bind group, through the pipeline with `REFLECTION_SURFACE` on —
14339
+ * which writes what the pixel reflects of its environment into the two targets
14340
+ * `reflectionSurfacePass.ts` holds and tests for the depth its frame draw wrote. Replayed when the
14341
+ * frame's pass has ended, before the trace reads them. See `shaders/flat/reflectionSurface.ts`.
14342
+ */
14343
+ private keepReflectionSurface(
14344
+ mesh: GpuMesh,
14345
+ frameKey: string,
14346
+ base: string,
14347
+ skinned: boolean,
14348
+ morphed: boolean,
14349
+ deltas: GPUTextureView | null,
14350
+ layer: number,
14351
+ eight: boolean,
14352
+ clothed: boolean,
14353
+ modelKind: SurfaceModelKind | null,
14354
+ slot: number,
14355
+ material: number,
14356
+ instanceBuffer: GPUBuffer | null,
14357
+ instances: number,
14358
+ indirect: GPUBuffer | null,
14359
+ /** A batch playing a bone animation, whose surface half moves as its draw does. */
14360
+ animated: GpuInstancedBatch | null = null,
14361
+ ): void {
14362
+ const surface = this.reflectionSurface;
14363
+ const present = this.meshPresent.get(base);
14364
+ if (surface === null || present === undefined) return;
14365
+ const animation = animated === null ? null : this.animationViews(animated);
14366
+ const key = `${frameKey}|rs`;
14367
+ const pipeline =
14368
+ this.pipelines.peek(key) ??
14369
+ flatPipeline(
14370
+ this.pipelines,
14371
+ this.surface.device,
14372
+ animation === null ? this.flatLayoutFor(skinned, morphed) : this.animatedBindGroupLayout,
14373
+ this.variant,
14374
+ key,
14375
+ present,
14376
+ false,
14377
+ skinned,
14378
+ morphed,
14379
+ true,
14380
+ layer,
14381
+ instanceBuffer !== null,
14382
+ 'none',
14383
+ this.materialDoubleSided,
14384
+ false,
14385
+ eight,
14386
+ clothed,
14387
+ modelKind,
14388
+ 'whole',
14389
+ true,
14390
+ animation !== null,
14391
+ );
14392
+ /* A static list's surface halves go into its own pool, beside its draws. */
14393
+ const command =
14394
+ this.staticRecording === null ? surface.take() : takeRecorded(this.staticRecording.surface);
14395
+ command.pipeline = pipeline;
14396
+ command.bindGroup =
14397
+ animated === null || animation === null
14398
+ ? this.flatBindGroupFor(skinned, morphed, deltas)
14399
+ : this.animatedGroup(animated, animation);
14400
+ command.offsetA = slot;
14401
+ command.offsetB = this.passOffset;
14402
+ command.offsetC = material;
14403
+ command.offsetD = this.drawViewOffset;
14404
+ command.offsetCount = 4;
14405
+ let index = 0;
14406
+ for (; index < mesh.vertexBuffers.length; index++) {
14407
+ command.vertexBuffers[index] = mesh.vertexBuffers[index] as GPUBuffer;
14408
+ }
14409
+ if (instanceBuffer !== null) command.vertexBuffers[index++] = instanceBuffer;
14410
+ command.vertexCount = index;
14411
+ command.indexBuffer = mesh.indexBuffer;
14412
+ command.indexed = true;
14413
+ command.count = mesh.indexCount;
14414
+ command.instances = instances;
14415
+ command.indirect = indirect;
14416
+ }
14417
+
13225
14418
  /**
13226
14419
  * A skin draw's diffuse and colour halves, kept for the spread: the frame's draw again, twice,
13227
14420
  * with the same slots, buffers and bind group, through the pipelines that write the diffuse's
@@ -13241,6 +14434,11 @@ export class WebGPURenderer implements RendererApi {
13241
14434
  slot: number,
13242
14435
  material: number,
13243
14436
  ): void {
14437
+ /* A skin's halves are not bundled: a static list holding one replays draw by draw. */
14438
+ if (this.staticRecording !== null) {
14439
+ this.staticRecording.immediate = true;
14440
+ return;
14441
+ }
13244
14442
  const skin = this.skinScatter;
13245
14443
  const pipelines = this.skinPipelines;
13246
14444
  const present = this.meshPresent.get(base);
@@ -13276,8 +14474,10 @@ export class WebGPURenderer implements RendererApi {
13276
14474
  command.pipeline = pipeline;
13277
14475
  command.bindGroup = bindGroup;
13278
14476
  command.offsetA = slot;
13279
- command.offsetB = material;
13280
- command.offsetCount = 2;
14477
+ command.offsetB = this.passOffset;
14478
+ command.offsetC = material;
14479
+ command.offsetD = this.drawViewOffset;
14480
+ command.offsetCount = 4;
13281
14481
  for (let index = 0; index < mesh.vertexBuffers.length; index++) {
13282
14482
  command.vertexBuffers[index] = mesh.vertexBuffers[index] as GPUBuffer;
13283
14483
  }
@@ -13301,11 +14501,17 @@ export class WebGPURenderer implements RendererApi {
13301
14501
  private runSkinScatter(): void {
13302
14502
  const skin = this.skinScatter;
13303
14503
  if (skin === null || this.skinScattered) return;
14504
+ /*
14505
+ * **Not the frame's picture, so not the frame's moment**: a blended draw in a mirror, a probe
14506
+ * bake or a scene capture lands over none of the frame's skin. Latching there switched the
14507
+ * spread off for the frame, so a capture with a glowing sign in it left every face drawn after
14508
+ * it flat. `scripts/capture-check.mjs`.
14509
+ */
14510
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive) return;
13304
14511
  /* Latched whether or not skin was kept: a skin drawn after this point is the whole surface. */
13305
14512
  this.skinScattered = true;
13306
14513
  if (!skin.pending) return;
13307
- if (this.reflectionPassActive || this.probePassActive || this.oitReplaying) return;
13308
- if (this.capturePassActive) return;
14514
+ if (this.oitReplaying) return;
13309
14515
  const encoder = this.encoder;
13310
14516
  const depth = this.depthView;
13311
14517
  const resolved = this.resolvedDepthView;
@@ -13430,6 +14636,65 @@ export class WebGPURenderer implements RendererApi {
13430
14636
  return true;
13431
14637
  }
13432
14638
 
14639
+ /** The colour copy a registered pass reads, at the scene's size and format. See `passColour`. */
14640
+ private allocatePassColour(): void {
14641
+ const source = this.sceneColor;
14642
+ if (source === null || this.passColour !== null) return;
14643
+ this.passColour = this.surface.device.createTexture({
14644
+ label: 'pass.sceneColor',
14645
+ size: [source.width, source.height],
14646
+ format: this.pipelines.format,
14647
+ usage: 0x4 | 0x2, // TEXTURE_BINDING | COPY_DST
14648
+ });
14649
+ this.passColourView = this.passColour.createView();
14650
+ }
14651
+
14652
+ /**
14653
+ * The frame's colour so far, into `passColour`, for a registered pass that reads it: the
14654
+ * boundary `takeRefractSnapshot` describes — flush, end the pass, copy what its end resolved,
14655
+ * reopen with `load` — and refused where that one is, in a mirror, a probe bake or a capture,
14656
+ * whose pictures are not the frame's. **Not latched**, unlike refraction's: each reading pass
14657
+ * sees the passes drawn before it, at a full-frame copy each.
14658
+ */
14659
+ private takePassColour(): void {
14660
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive) return;
14661
+ const copy = this.passColour;
14662
+ const source = this.sceneColor;
14663
+ const encoder = this.encoder;
14664
+ if (copy === null || source === null || encoder === null || this.pass === null) return;
14665
+ if (this.quality.frameGraph) this.flushGraph();
14666
+ this.pass.end();
14667
+ encoder.copyTextureToTexture({ texture: source }, { texture: copy }, [copy.width, copy.height]);
14668
+ this.pass = this.reopenFramePass('pass.colourTaken');
14669
+ }
14670
+
14671
+ /**
14672
+ * The frame's pass again after a copy between passes, loading everything already drawn: the same
14673
+ * attachments the copy's caller ended, with nothing cleared.
14674
+ */
14675
+ private reopenFramePass(label: string): GPURenderPassEncoder | null {
14676
+ const encoder = this.encoder;
14677
+ const target = this.compositeTarget();
14678
+ if (encoder === null || target === null) return null;
14679
+ const multisampled = this.compositeMsaa() !== null;
14680
+ return encoder.beginRenderPass({
14681
+ label,
14682
+ timestampWrites: this.gpuTimer.writesFor(),
14683
+ colorAttachments: [
14684
+ {
14685
+ view: this.compositeMsaa() ?? target,
14686
+ resolveTarget: multisampled ? target : undefined,
14687
+ loadOp: 'load',
14688
+ storeOp: resolvedStoreOp(multisampled, false, this.quality.discardResolvedAttachments),
14689
+ },
14690
+ ],
14691
+ depthStencilAttachment:
14692
+ this.depthView === null
14693
+ ? undefined
14694
+ : { view: this.depthView, depthLoadOp: 'load', depthStoreOp: 'store' },
14695
+ });
14696
+ }
14697
+
13433
14698
  /**
13434
14699
  * The pass to record into, reopening one on the canvas if the frame has already gone.
13435
14700
  *
@@ -13621,6 +14886,8 @@ export class WebGPURenderer implements RendererApi {
13621
14886
  /* And a march reads the same copy, for the same reason and with the same failure if it is
13622
14887
  discarded: a depth of zero is the far plane reversed, so every ray finds nothing. */
13623
14888
  this.reflectionQueue.length > 0 ||
14889
+ /* And the frame's materials' reflections, whose surface half tests against the depth too. */
14890
+ this.reflectionSurface !== null ||
13624
14891
  /* And the medium's own march, which stops where the frame's depth says a surface is. Left
13625
14892
  out of this, every ray runs to `maxDistance` through the walls and the fog is drawn on
13626
14893
  top of the room rather than in it. */
@@ -13656,6 +14923,8 @@ export class WebGPURenderer implements RendererApi {
13656
14923
  * site reads as one `if` either way.
13657
14924
  */
13658
14925
  private recordDraw(reads: number, writes: number): DrawCommand | null {
14926
+ /* A static list recording takes its draws into its own pool, never the frame graph's. */
14927
+ if (this.staticRecording !== null) return takeRecorded(this.staticRecording.frame);
13659
14928
  /* The order-independent replay owns its pass and issues directly into it; a recorded command
13660
14929
  would be replayed into the frame's passes instead, which is not where these belong. */
13661
14930
  if (this.oitEncoder !== null) return null;
@@ -13698,9 +14967,23 @@ export class WebGPURenderer implements RendererApi {
13698
14967
 
13699
14968
  /** One recorded draw, issued into an open pass. Shared by the graph's replay and `runLate`. */
13700
14969
  private issueCommand(pass: GPURenderPassEncoder, command: DrawCommand): void {
14970
+ /* A static list, recorded once: one call, and the pass's bindings are its to set again. */
14971
+ if (command.bundles !== null) {
14972
+ pass.executeBundles(command.bundles);
14973
+ return;
14974
+ }
13701
14975
  if (command.pipeline === null || command.bindGroup === null) return;
13702
14976
  pass.setPipeline(command.pipeline);
13703
- if (command.offsetCount === 2) {
14977
+ if (command.offsetCount === 4) {
14978
+ pass.setBindGroup(0, command.bindGroup, [
14979
+ command.offsetA,
14980
+ command.offsetB,
14981
+ command.offsetC,
14982
+ command.offsetD,
14983
+ ]);
14984
+ } else if (command.offsetCount === 3) {
14985
+ pass.setBindGroup(0, command.bindGroup, [command.offsetA, command.offsetB, command.offsetC]);
14986
+ } else if (command.offsetCount === 2) {
13704
14987
  pass.setBindGroup(0, command.bindGroup, [command.offsetA, command.offsetB]);
13705
14988
  } else if (command.offsetCount === 1) {
13706
14989
  pass.setBindGroup(0, command.bindGroup, [command.offsetA]);
@@ -13962,7 +15245,7 @@ export class WebGPURenderer implements RendererApi {
13962
15245
  this.flushRings();
13963
15246
  this.pass?.end();
13964
15247
  this.pass = null;
13965
- if (!this.surface.lost) this.surface.device.queue.submit([this.encoder.finish()]);
15248
+ if (!this.surface.lost) this.submit([this.encoder.finish()]);
13966
15249
  this.encoder = null;
13967
15250
  }
13968
15251
 
@@ -14022,7 +15305,7 @@ export class WebGPURenderer implements RendererApi {
14022
15305
  * there would have nothing to ride on. See `GpuTimestamps.resolve`.
14023
15306
  */
14024
15307
  this.gpuTimer.resolve(this.encoder);
14025
- this.surface.device.queue.submit([this.encoder.finish()]);
15308
+ this.submit([this.encoder.finish()]);
14026
15309
  this.encoder = null;
14027
15310
  this.framePresented = true;
14028
15311
  /* Latched rather than assigned: `framePresented` goes back to false every `beginFrame`. */
@@ -14485,6 +15768,8 @@ export class WebGPURenderer implements RendererApi {
14485
15768
  const reads = maskOf(...(definition.reads ?? []));
14486
15769
  this.passReads.set(handle, reads);
14487
15770
  this.passReadsUnion |= reads;
15771
+ /* The colour copy a reader needs, now rather than at its first draw: see `passColour`. */
15772
+ if ((reads & COLOR_SNAPSHOT) !== 0 && this.passColour === null) this.allocatePassColour();
14488
15773
  if (definition.prepare !== undefined) this.preparingPasses++;
14489
15774
  definition.init?.(this.passDevice());
14490
15775
  return handle;
@@ -14504,12 +15789,16 @@ export class WebGPURenderer implements RendererApi {
14504
15789
  environment: PassEnvironment | null;
14505
15790
  distanceField: ComposedField | null;
14506
15791
  jitter: Float32Array;
15792
+ sceneDepth: GPUTextureView | null;
15793
+ sceneColor: GPUTextureView | null;
14507
15794
  } = {
14508
15795
  backend: 'webgpu',
14509
15796
  encoder: null,
14510
15797
  environment: null,
14511
15798
  distanceField: null,
14512
15799
  jitter: this.passJitter,
15800
+ sceneDepth: null,
15801
+ sceneColor: null,
14513
15802
  };
14514
15803
  /**
14515
15804
  * The object handed to a pass as `PrepareContext.environment`, rebuilt only when it changes.
@@ -14607,6 +15896,15 @@ export class WebGPURenderer implements RendererApi {
14607
15896
  * than merely what happens.
14608
15897
  */
14609
15898
  this.prepareContext.distanceField = this.fieldComposer?.field ?? null;
15899
+ /* Settled for the frame: `ensureComposite` has run, so the view stays until the pass draws. */
15900
+ this.prepareContext.sceneDepth = this.resolvedDepthView;
15901
+ /*
15902
+ * Under a reconstruction a colour reader draws after the upscale (see `drawPass`), so it reads
15903
+ * the output-size copy the late refraction keeps rather than the render's.
15904
+ */
15905
+ this.prepareContext.sceneColor = this.reconstructionWanted
15906
+ ? this.refractSnapshotView
15907
+ : this.passColourView;
14610
15908
  for (const definition of this.passes.definitions) {
14611
15909
  definition?.prepare?.(this.prepareContext as PrepareContext);
14612
15910
  }
@@ -14623,20 +15921,42 @@ export class WebGPURenderer implements RendererApi {
14623
15921
  if (!this.canDraw()) return;
14624
15922
  const definition = passAt(this.passes, handle);
14625
15923
  if (definition === undefined) return;
15924
+ const reads = this.passReads.get(handle) ?? 0;
15925
+ /*
15926
+ * **Under a reconstruction, a pass that reads the frame's colour draws after the blended draws
15927
+ * do.** Those land after the upscale, at the output's size, so a pass drawn among them at the
15928
+ * render's would read a frame without them and then be covered by them — a full-screen effect
15929
+ * with the hit sparks drawn sharp over it. Queued instead, and drawn by `runLate` over the
15930
+ * reconstructed picture, after every late draw, whatever order it was asked in.
15931
+ */
15932
+ if ((reads & COLOR_SNAPSHOT) !== 0 && this.drawsLate()) {
15933
+ this.latePasses.push(handle);
15934
+ return;
15935
+ }
15936
+ /*
15937
+ * A pass that reads the depth copy gets it with everything drawn so far: the copy is taken
15938
+ * here, once a frame, as a soft-edged sprite takes it. Before `openPass` below, because taking
15939
+ * it ends the frame's pass and opens another.
15940
+ */
15941
+ if ((reads & DEPTH_SNAPSHOT) !== 0 && !this.framePresented && this.openPass() !== null) {
15942
+ this.takeVolumeDepth();
15943
+ }
15944
+ /*
15945
+ * And one that reads the colour gets it afresh, with everything drawn before it — a full-screen
15946
+ * pass drawn last reads the whole frame and an effect drawn mid-frame reads the frame so far.
15947
+ * A copy a reading pass, where the depth above is one a frame: what a colour reader draws is
15948
+ * itself part of what the next one should see.
15949
+ */
15950
+ if ((reads & COLOR_SNAPSHOT) !== 0 && !this.framePresented && this.openPass() !== null) {
15951
+ this.takePassColour();
15952
+ }
14626
15953
  if (!this.quality.frameGraph) {
14627
15954
  const pass = this.openPass();
14628
15955
  if (pass === null) return;
14629
15956
  definition.draw(this.fillPassContext(pass));
14630
15957
  return;
14631
15958
  }
14632
- recordNode(
14633
- this.arena,
14634
- VERB_PASS,
14635
- this.passReads.get(handle) ?? 0,
14636
- this.currentTarget(),
14637
- handle,
14638
- 0,
14639
- );
15959
+ recordNode(this.arena, VERB_PASS, reads, this.currentTarget(), handle, 0);
14640
15960
  if (this.framePresented) this.queueOverlayFlush();
14641
15961
  }
14642
15962
 
@@ -14727,7 +16047,7 @@ export class WebGPURenderer implements RendererApi {
14727
16047
  definition.dispatch(this.computeContext as ComputeContext);
14728
16048
  pass.end();
14729
16049
  this.flushRings();
14730
- device.queue.submit([encoder.finish()]);
16050
+ this.submit([encoder.finish()]);
14731
16051
  }
14732
16052
 
14733
16053
  /** Let go of a definition. A handle kept past this dispatches nothing; see `ComputeHandle`. */
@@ -14757,8 +16077,9 @@ export class WebGPURenderer implements RendererApi {
14757
16077
  ): void {
14758
16078
  const binner = this.clusterBinner;
14759
16079
  i[at('uClustered')] = binner === null ? 0 : 1;
14760
- if (binner === null) return;
14761
16080
 
16081
+ /* Written whether or not the lights are clustered: a surface overlay's rim finds its place on
16082
+ the screen through the same two (\`overlayScreen\`). */
14762
16083
  const p = camera.projection;
14763
16084
  const tanHalfFovY = 1 / (p[5] ?? 1);
14764
16085
  const aspect = (p[5] ?? 1) / (p[0] ?? 1);
@@ -14774,6 +16095,7 @@ export class WebGPURenderer implements RendererApi {
14774
16095
  f[frustumAt + 2] = tanHalfFovY;
14775
16096
  f[frustumAt + 3] = aspect;
14776
16097
  f.set(camera.view as Float32Array, at('uView'));
16098
+ if (binner === null) return;
14777
16099
 
14778
16100
  /* The rectangles past the fixed arm's go into the table. See `ClusterLightSet.areas`. */
14779
16101
  const set = fillClusterLightSet(env, this.clusterLights, this.shadedAreaLights);
@@ -15098,6 +16420,49 @@ export class WebGPURenderer implements RendererApi {
15098
16420
  /** This frame's exposure into the tone curve. Starts at the profile's own. */
15099
16421
  private exposure: number;
15100
16422
 
16423
+ /**
16424
+ * The `filmic` curve every later frame is graded through, until it is set again. See
16425
+ * `RendererApi.setFilmicCurve`; the constants are solved here once, not per frame or per pixel.
16426
+ */
16427
+ setFilmicCurve(curve: FilmicCurve): void {
16428
+ this.filmCurve = curve;
16429
+ if (resolveFilmicCurve(curve, this.filmConstants, this.headroom) && !this.warnedFilmClamped) {
16430
+ this.warnedFilmClamped = true;
16431
+ console.warn(FILMIC_CLAMPED);
16432
+ }
16433
+ }
16434
+
16435
+ private warnedFilmClamped = false;
16436
+ /** The curve last set, so a new headroom can solve it again. */
16437
+ private filmCurve: FilmicCurve = DEFAULT_FILMIC_CURVE;
16438
+ /** The display's peak over paper white; 1 on a standard range. See `setDisplayLuminance`. */
16439
+ private headroom = 1;
16440
+
16441
+ /** The range the frame goes out in. See `RendererApi.displayRange`. */
16442
+ get displayRange(): DisplayRange {
16443
+ return this.surface.displayRange;
16444
+ }
16445
+
16446
+ /** Why it is that range, in words. */
16447
+ get displayRangeReason(): string {
16448
+ return this.surface.displayRangeReason;
16449
+ }
16450
+
16451
+ /**
16452
+ * How bright paper white and the display's peak are, in any one unit (nits, usually), held until
16453
+ * changed: their ratio is how far above white the highlights may run on a high range display,
16454
+ * and nothing changes on a standard one. See `RendererApi.setDisplayLuminance`.
16455
+ */
16456
+ setDisplayLuminance(paperWhite: number, peak: number): void {
16457
+ this.headroom = displayHeadroom(this.surface.displayRange, paperWhite, peak);
16458
+ resolveFilmicCurve(this.filmCurve, this.filmConstants, this.headroom);
16459
+ }
16460
+
16461
+ /** The curve's eight constants, and the two views the composite uploads them through. */
16462
+ private readonly filmConstants = new Float32Array(FILMIC_CONSTANTS);
16463
+ private readonly filmA = this.filmConstants.subarray(0, 4);
16464
+ private readonly filmB = this.filmConstants.subarray(4, 8);
16465
+
15101
16466
  /*
15102
16467
  * -----------------------------------------------------------------------------------------
15103
16468
  * The three screen effects, which this backend cannot yet honour — and says so.
@@ -15142,6 +16507,8 @@ export class WebGPURenderer implements RendererApi {
15142
16507
  private readonly veilColor: Vec3 = [0, 0, 0];
15143
16508
  private veilAlpha = 0;
15144
16509
  private vignette = 0;
16510
+ /** The lens's colour fringe: red's and green's pull and where it begins. See `fringe.ts`. */
16511
+ private readonly fringe = new Float32Array(FRINGE_FLOATS);
15145
16512
  private grain = 0;
15146
16513
  private grainSeed = 0;
15147
16514
  private warnedLookWithoutComposite = false;
@@ -15165,6 +16532,21 @@ export class WebGPURenderer implements RendererApi {
15165
16532
  this.vignette = clampVignette(strength);
15166
16533
  }
15167
16534
 
16535
+ /**
16536
+ * The lens's colour fringe, held until changed: `intensity` is a percentage, 0 is none, and red
16537
+ * and green are pulled toward the centre by their wavelength's distance from blue, growing from
16538
+ * `start` — a share of the half-frame, 0 the centre — to the whole of it at the edge. Applied to
16539
+ * scene light at the composite's first read. Needs `screenEffects`, and says so once rather than
16540
+ * doing nothing. See `shaders/fringe.ts`.
16541
+ */
16542
+ setChromaticAberration(intensity: number, start = 0): void {
16543
+ if (!this.hasComposite) {
16544
+ this.warnLookWithoutComposite(intensity);
16545
+ return;
16546
+ }
16547
+ resolveFringe(intensity, start, this.fringe);
16548
+ }
16549
+
15168
16550
  /**
15169
16551
  * Film grain, held until changed: an amplitude in display values (0 is none, 0.03 is a fine
15170
16552
  * grain) and this frame's seed. **The seed is the caller's**, because the engine takes time from
@@ -15316,9 +16698,27 @@ export class WebGPURenderer implements RendererApi {
15316
16698
  this.rushStrength = Math.min(Math.max(strength, 0), 1);
15317
16699
  }
15318
16700
 
15319
- /** How much of the frame's camera motion blur to apply, 0 to 1. */
15320
- setCameraMotionBlur(scale: number): void {
16701
+ /**
16702
+ * How much of the frame's motion blur to apply, 0 to 1, and the longest smear as a share of the
16703
+ * frame, both held until changed. See `RendererApi.setCameraMotionBlur`.
16704
+ */
16705
+ setCameraMotionBlur(scale: number, maxShare = MOTION_BLUR_MAX_UV): void {
15321
16706
  this.motionBlurScale = Math.min(Math.max(scale, 0), 1);
16707
+ this.motionBlurMax = clampMotionShare(maxShare);
16708
+ }
16709
+
16710
+ /** The longest smear, as a share of the frame. See `setCameraMotionBlur`. */
16711
+ private motionBlurMax = MOTION_BLUR_MAX_UV;
16712
+ private warnedObjectBlur = false;
16713
+
16714
+ /**
16715
+ * Said once: a draw named where it was last frame while the frame blurs, and this frame keeps no
16716
+ * motion target to blur it by — the camera's motion alone smears it. See `OBJECT_BLUR_UNKEPT`.
16717
+ */
16718
+ private warnObjectBlur(): void {
16719
+ if (this.warnedObjectBlur) return;
16720
+ this.warnedObjectBlur = true;
16721
+ console.warn(OBJECT_BLUR_UNKEPT);
15322
16722
  }
15323
16723
  /**
15324
16724
  * The camera has cut: this frame is a new shot, not the next moment of the last one.
@@ -15806,7 +17206,7 @@ export class WebGPURenderer implements RendererApi {
15806
17206
  .end();
15807
17207
  }
15808
17208
  }
15809
- device.queue.submit([clear.finish()]);
17209
+ this.submit([clear.finish()]);
15810
17210
  }
15811
17211
 
15812
17212
  /**
@@ -15927,17 +17327,18 @@ export class WebGPURenderer implements RendererApi {
15927
17327
  * `bindMeshPass` and `drawMesh`, which record into whatever `this.pass` is. That is the same
15928
17328
  * arrangement `bakeFace` uses for a point light's cube, and it is why a bake must not be
15929
17329
  * opened inside a frame's pass by a caller who then expects the frame to continue.
15930
- */
15931
- const savedPass = this.pass;
15932
- /*
17330
+ *
15933
17331
  * **A bake is a boundary, and boundaries flush.** The mirror says why in its own words: what
15934
17332
  * has been recorded and not yet replayed belongs to the pass that is about to end, and leaving
15935
17333
  * it replays it into whichever pass opens next — a draw in the wrong target rather than a
15936
17334
  * missing one. This is the fourth such boundary in the backend and the only one that was never
15937
17335
  * given the treatment, which is what let a bake carry the frame's pending draws into a cube
15938
- * face.
17336
+ * face. **The pass is saved after the flush**, which opens it where none was: saved before,
17337
+ * the null given back left the flushed pass open and the frame's encoder locked. See
17338
+ * `captureScene`.
15939
17339
  */
15940
17340
  if (this.quality.frameGraph) this.flushGraph();
17341
+ const savedPass = this.pass;
15941
17342
  const savedEncoder = this.encoder;
15942
17343
  /*
15943
17344
  * **The rings are marked here and given back after the submit below**, because this bake is its
@@ -16036,7 +17437,7 @@ export class WebGPURenderer implements RendererApi {
16036
17437
  const whole = endFace === PROBE_FACES.length;
16037
17438
  if (whole) this.buildProbeChain(encoder, probe, this.probeSweeps?.writeLayer(layer) ?? layer);
16038
17439
  this.flushRings();
16039
- device.queue.submit([encoder.finish()]);
17440
+ this.submit([encoder.finish()]);
16040
17441
  this.rewindRings(skinMark);
16041
17442
  this.encoderEpoch = frameEpoch;
16042
17443
 
@@ -16046,6 +17447,7 @@ export class WebGPURenderer implements RendererApi {
16046
17447
  this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
16047
17448
  this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
16048
17449
  this.materials.dirty();
17450
+ this.reopenPass();
16049
17451
  this.probePassActive = false;
16050
17452
  this.probeBounce = false;
16051
17453
  this.rebindEnvironment();
@@ -16103,7 +17505,7 @@ export class WebGPURenderer implements RendererApi {
16103
17505
  this.uploadProbeFaces(probe, image);
16104
17506
  const encoder = device.createCommandEncoder({ label: 'probe.environmentImage' });
16105
17507
  this.buildProbeChain(encoder, probe, 0);
16106
- device.queue.submit([encoder.finish()]);
17508
+ this.submit([encoder.finish()]);
16107
17509
 
16108
17510
  this.markProbeFilled(0);
16109
17511
  this.probeAmbient = options?.irradiance ?? true;
@@ -16141,7 +17543,7 @@ export class WebGPURenderer implements RendererApi {
16141
17543
  const { device } = this.surface;
16142
17544
  const encoder = device.createCommandEncoder({ label: 'probe.layerImage' });
16143
17545
  this.buildProbeChain(encoder, probe, this.probeSweeps?.writeLayer(layer) ?? layer);
16144
- device.queue.submit([encoder.finish()]);
17546
+ this.submit([encoder.finish()]);
16145
17547
  this.markProbeFilled(layer);
16146
17548
  this.probeAmbient = options?.irradiance ?? true;
16147
17549
  this.rebuildFlatBindGroup();
@@ -16629,6 +18031,7 @@ export class WebGPURenderer implements RendererApi {
16629
18031
  this.sceneColor,
16630
18032
  this.sceneColorMsaa,
16631
18033
  this.refractSnapshot,
18034
+ this.passColour,
16632
18035
  this.resolvedDepth,
16633
18036
  this.aoTarget,
16634
18037
  this.aoScratch,
@@ -16698,6 +18101,9 @@ export class WebGPURenderer implements RendererApi {
16698
18101
  usage: 0x4 | 0x2 | 0x10, // TEXTURE_BINDING | COPY_DST | RENDER_ATTACHMENT
16699
18102
  });
16700
18103
  this.refractSnapshotView = this.refractSnapshot.createView();
18104
+ this.passColour = null;
18105
+ this.passColourView = null;
18106
+ if ((this.passReadsUnion & COLOR_SNAPSHOT) !== 0) this.allocatePassColour();
16701
18107
  this.refractMips = new SnapshotMips(
16702
18108
  device,
16703
18109
  this.pipelines,
@@ -16926,6 +18332,10 @@ export class WebGPURenderer implements RendererApi {
16926
18332
  usage: USAGE,
16927
18333
  });
16928
18334
  this.ssrReflectionView = this.ssrReflection.createView();
18335
+ /* The frame's materials, at the scene's size; a stand-in where nothing traces them. */
18336
+ this.reflectionSurface?.size(width, height);
18337
+ const probeMap = this.reflectionSurface?.probeView ?? this.blankView;
18338
+ const tintMap = this.reflectionSurface?.tintView ?? this.blankView;
16929
18339
  this.ssrBindGroup =
16930
18340
  this.resolvedDepthView === null || this.sceneColorView === null
16931
18341
  ? null
@@ -16939,8 +18349,23 @@ export class WebGPURenderer implements RendererApi {
16939
18349
  },
16940
18350
  ...entry(SSR_TEXTURES.uSsrDepth, this.resolvedDepthView, true),
16941
18351
  ...entry(SSR_TEXTURES.uSsrScene, this.sceneColorView),
18352
+ ...entry(SSR_TEXTURES.uSsrProbeMap, probeMap),
18353
+ ...entry(SSR_TEXTURES.uSsrTintMap, tintMap),
16942
18354
  ],
16943
18355
  });
18356
+ this.ssrMaterialGroup = device.createBindGroup({
18357
+ label: 'post.ssrMaterialGroup',
18358
+ layout: this.ssrMaterialLayout,
18359
+ entries: [
18360
+ {
18361
+ binding: SSR_MATERIAL_UNIFORMS,
18362
+ resource: { buffer: this.ssrMaterialUniforms, size: SSR_MATERIAL_SIZE },
18363
+ },
18364
+ ...entry(SSR_MATERIAL_TEXTURES.uSsrReflection, this.ssrReflectionView),
18365
+ ...entry(SSR_MATERIAL_TEXTURES.uSsrProbeMap, probeMap),
18366
+ ...entry(SSR_MATERIAL_TEXTURES.uSsrTintMap, tintMap),
18367
+ ],
18368
+ });
16944
18369
  this.ssrResolveGroup = device.createBindGroup({
16945
18370
  label: 'post.ssrResolveGroup',
16946
18371
  layout: this.ssrResolveLayout,