@driftengine/core 4.8.6 → 4.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/CHANGELOG.json +132 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +7 -2
  4. package/dist/index.js +8 -1
  5. package/dist/render/anisotropicLobe.d.ts +10 -0
  6. package/dist/render/anisotropicLobe.js +19 -0
  7. package/dist/render/areaLights.d.ts +35 -0
  8. package/dist/render/areaLights.js +35 -0
  9. package/dist/render/atmosphere.d.ts +13 -0
  10. package/dist/render/atmosphere.js +9 -1
  11. package/dist/render/backend/api.d.ts +41 -3
  12. package/dist/render/backend/createRenderer.d.ts +6 -0
  13. package/dist/render/backend/createRenderer.js +4 -1
  14. package/dist/render/backend/webgl2/renderer.d.ts +140 -16
  15. package/dist/render/backend/webgl2/renderer.js +436 -44
  16. package/dist/render/backend/webgl2/sceneCaptureTarget.d.ts +29 -0
  17. package/dist/render/backend/webgl2/sceneCaptureTarget.js +96 -0
  18. package/dist/render/backend/webgpu/device.d.ts +7 -1
  19. package/dist/render/backend/webgpu/device.js +31 -3
  20. package/dist/render/backend/webgpu/flatPass.d.ts +16 -1
  21. package/dist/render/backend/webgpu/flatPass.js +58 -26
  22. package/dist/render/backend/webgpu/particlePass.js +2 -1
  23. package/dist/render/backend/webgpu/postPass.d.ts +51 -9
  24. package/dist/render/backend/webgpu/postPass.js +16 -2
  25. package/dist/render/backend/webgpu/reflectionSurfacePass.d.ts +30 -0
  26. package/dist/render/backend/webgpu/reflectionSurfacePass.js +100 -0
  27. package/dist/render/backend/webgpu/renderer.d.ts +166 -3
  28. package/dist/render/backend/webgpu/renderer.js +738 -83
  29. package/dist/render/backend/webgpu/sceneCaptureTarget.d.ts +24 -0
  30. package/dist/render/backend/webgpu/sceneCaptureTarget.js +59 -0
  31. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  32. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
  33. package/dist/render/backend/webgpu/surfaceTexturePass.js +34 -8
  34. package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
  35. package/dist/render/clusteredLights.d.ts +23 -5
  36. package/dist/render/clusteredLights.js +35 -8
  37. package/dist/render/displayRange.d.ts +31 -0
  38. package/dist/render/displayRange.js +47 -0
  39. package/dist/render/driftLight/bake.d.ts +2 -0
  40. package/dist/render/driftLight/bake.js +1 -1
  41. package/dist/render/driftLight/falloff.d.ts +4 -2
  42. package/dist/render/driftLight/falloff.js +8 -2
  43. package/dist/render/filmicCurve.d.ts +74 -0
  44. package/dist/render/filmicCurve.js +203 -0
  45. package/dist/render/fog.d.ts +4 -1
  46. package/dist/render/fog.js +12 -2
  47. package/dist/render/frame/resources.d.ts +2 -2
  48. package/dist/render/frame/resources.js +6 -0
  49. package/dist/render/frameReflections.d.ts +40 -0
  50. package/dist/render/frameReflections.js +48 -0
  51. package/dist/render/gi/bounceLights.d.ts +3 -2
  52. package/dist/render/gi/bounceLights.js +8 -3
  53. package/dist/render/instanceCull.js +5 -0
  54. package/dist/render/instances.d.ts +19 -5
  55. package/dist/render/instances.js +9 -3
  56. package/dist/render/lightBudget.d.ts +12 -1
  57. package/dist/render/lightBudget.js +23 -3
  58. package/dist/render/lightmap.d.ts +23 -9
  59. package/dist/render/lightmap.js +40 -18
  60. package/dist/render/particlePool.d.ts +21 -0
  61. package/dist/render/particlePool.js +7 -0
  62. package/dist/render/pass.d.ts +49 -0
  63. package/dist/render/pointLightSelection.d.ts +13 -0
  64. package/dist/render/pointLightSelection.js +3 -1
  65. package/dist/render/reflectionSurfaceTargets.d.ts +28 -0
  66. package/dist/render/reflectionSurfaceTargets.js +99 -0
  67. package/dist/render/renderQuality.d.ts +47 -15
  68. package/dist/render/renderQuality.js +4 -0
  69. package/dist/render/rendererHarness.js +3 -0
  70. package/dist/render/rgb9e5.d.ts +22 -0
  71. package/dist/render/rgb9e5.js +47 -0
  72. package/dist/render/sceneCapture.d.ts +29 -0
  73. package/dist/render/sceneCapture.js +36 -0
  74. package/dist/render/sceneTarget.d.ts +5 -0
  75. package/dist/render/sceneTarget.js +24 -1
  76. package/dist/render/screenSpaceReflection.d.ts +32 -1
  77. package/dist/render/screenSpaceReflection.js +14 -0
  78. package/dist/render/shaders/arcane.d.ts +1 -1
  79. package/dist/render/shaders/bolt.d.ts +1 -1
  80. package/dist/render/shaders/bolt.js +7 -1
  81. package/dist/render/shaders/caustics.d.ts +1 -1
  82. package/dist/render/shaders/depth.js +11 -0
  83. package/dist/render/shaders/film.d.ts +1 -1
  84. package/dist/render/shaders/filmLook.js +2 -1
  85. package/dist/render/shaders/fire.d.ts +1 -1
  86. package/dist/render/shaders/flat/anisotropic.d.ts +1 -1
  87. package/dist/render/shaders/flat/anisotropic.js +37 -3
  88. package/dist/render/shaders/flat/areaLight.d.ts +1 -1
  89. package/dist/render/shaders/flat/areaLight.js +46 -0
  90. package/dist/render/shaders/flat/glassTint.d.ts +3 -2
  91. package/dist/render/shaders/flat/glassTint.js +5 -1
  92. package/dist/render/shaders/flat/index.d.ts +15 -0
  93. package/dist/render/shaders/flat/index.js +22 -2
  94. package/dist/render/shaders/flat/lobes.d.ts +6 -0
  95. package/dist/render/shaders/flat/lobes.js +49 -43
  96. package/dist/render/shaders/flat/main.d.ts +1 -1
  97. package/dist/render/shaders/flat/main.js +85 -22
  98. package/dist/render/shaders/flat/models.d.ts +5 -2
  99. package/dist/render/shaders/flat/models.js +100 -16
  100. package/dist/render/shaders/flat/overlay.d.ts +16 -0
  101. package/dist/render/shaders/flat/overlay.js +117 -0
  102. package/dist/render/shaders/flat/pointShadow.js +1 -1
  103. package/dist/render/shaders/flat/preamble.js +10 -3
  104. package/dist/render/shaders/flat/reflectionSurface.d.ts +21 -0
  105. package/dist/render/shaders/flat/reflectionSurface.js +61 -0
  106. package/dist/render/shaders/fog.d.ts +1 -1
  107. package/dist/render/shaders/fog.js +9 -2
  108. package/dist/render/shaders/fringe.d.ts +29 -0
  109. package/dist/render/shaders/fringe.js +55 -0
  110. package/dist/render/shaders/generated/arcane.wgsl.d.ts +1 -1
  111. package/dist/render/shaders/generated/arcane.wgsl.js +1 -1
  112. package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
  113. package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
  114. package/dist/render/shaders/generated/caustics.wgsl.d.ts +1 -1
  115. package/dist/render/shaders/generated/caustics.wgsl.js +1 -1
  116. package/dist/render/shaders/generated/depth.wgsl.d.ts +4 -4
  117. package/dist/render/shaders/generated/depth.wgsl.js +4 -4
  118. package/dist/render/shaders/generated/film.wgsl.d.ts +1 -1
  119. package/dist/render/shaders/generated/film.wgsl.js +1 -1
  120. package/dist/render/shaders/generated/fire.wgsl.d.ts +1 -1
  121. package/dist/render/shaders/generated/fire.wgsl.js +1 -1
  122. package/dist/render/shaders/generated/flat.wgsl.d.ts +192 -16
  123. package/dist/render/shaders/generated/flat.wgsl.js +230 -54
  124. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  125. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  126. package/dist/render/shaders/generated/particle.wgsl.d.ts +4 -4
  127. package/dist/render/shaders/generated/particle.wgsl.js +4 -4
  128. package/dist/render/shaders/generated/rush.wgsl.d.ts +56 -26
  129. package/dist/render/shaders/generated/rush.wgsl.js +56 -26
  130. package/dist/render/shaders/generated/scatter.wgsl.d.ts +1 -1
  131. package/dist/render/shaders/generated/scatter.wgsl.js +1 -1
  132. package/dist/render/shaders/generated/smoke.wgsl.d.ts +1 -1
  133. package/dist/render/shaders/generated/smoke.wgsl.js +1 -1
  134. package/dist/render/shaders/generated/ssrResolve.wgsl.d.ts +29 -0
  135. package/dist/render/shaders/generated/ssrResolve.wgsl.js +29 -0
  136. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +32 -7
  137. package/dist/render/shaders/generated/ssrTrace.wgsl.js +32 -7
  138. package/dist/render/shaders/generated/water.wgsl.d.ts +23 -16
  139. package/dist/render/shaders/generated/water.wgsl.js +23 -16
  140. package/dist/render/shaders/gi/probeBake.wgsl.js +9 -5
  141. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +2 -2
  142. package/dist/render/shaders/gpudriven/fog.wgsl.js +11 -3
  143. package/dist/render/shaders/line.d.ts +1 -1
  144. package/dist/render/shaders/particle.d.ts +3 -3
  145. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  146. package/dist/render/shaders/recon/motion.wgsl.js +13 -2
  147. package/dist/render/shaders/rush.js +54 -11
  148. package/dist/render/shaders/scatter.d.ts +1 -1
  149. package/dist/render/shaders/smoke.d.ts +1 -1
  150. package/dist/render/shaders/ssrResolve.d.ts +17 -0
  151. package/dist/render/shaders/ssrResolve.js +51 -0
  152. package/dist/render/shaders/ssrTrace.js +46 -1
  153. package/dist/render/shaders/water.d.ts +1 -1
  154. package/dist/render/shaders/water.js +12 -3
  155. package/dist/render/skinScatter.d.ts +3 -1
  156. package/dist/render/skinScatter.js +3 -1
  157. package/dist/render/ssrPass.d.ts +16 -0
  158. package/dist/render/ssrPass.js +107 -1
  159. package/dist/render/surfaceModel.d.ts +29 -8
  160. package/dist/render/surfaceModel.js +33 -9
  161. package/dist/render/surfaceOverlay.d.ts +132 -0
  162. package/dist/render/surfaceOverlay.js +85 -0
  163. package/dist/render/surfaceTexture.d.ts +44 -1
  164. package/dist/render/surfaceTexture.js +96 -5
  165. package/dist/render/temporalAa.d.ts +14 -0
  166. package/dist/render/temporalAa.js +20 -0
  167. package/dist/render/vertexDefaults.d.ts +17 -0
  168. package/dist/render/vertexDefaults.js +24 -0
  169. package/package.json +6 -6
  170. package/src/index.ts +22 -2
  171. package/src/render/anisotropicLobe.ts +29 -0
  172. package/src/render/areaLights.ts +58 -0
  173. package/src/render/atmosphere.ts +22 -1
  174. package/src/render/backend/api.ts +58 -2
  175. package/src/render/backend/createRenderer.ts +18 -1
  176. package/src/render/backend/webgl2/renderer.ts +548 -50
  177. package/src/render/backend/webgl2/sceneCaptureTarget.ts +130 -0
  178. package/src/render/backend/webgpu/device.ts +42 -3
  179. package/src/render/backend/webgpu/flatPass.ts +63 -26
  180. package/src/render/backend/webgpu/particlePass.ts +2 -1
  181. package/src/render/backend/webgpu/postPass.ts +18 -0
  182. package/src/render/backend/webgpu/reflectionSurfacePass.ts +114 -0
  183. package/src/render/backend/webgpu/renderer.ts +927 -100
  184. package/src/render/backend/webgpu/sceneCaptureTarget.ts +63 -0
  185. package/src/render/backend/webgpu/surfaceTexturePass.ts +44 -14
  186. package/src/render/clusteredLights.ts +41 -8
  187. package/src/render/displayRange.ts +61 -0
  188. package/src/render/driftLight/bake.ts +3 -1
  189. package/src/render/driftLight/falloff.ts +13 -2
  190. package/src/render/filmicCurve.ts +229 -0
  191. package/src/render/fog.ts +19 -3
  192. package/src/render/frame/resources.ts +6 -0
  193. package/src/render/frameReflections.ts +72 -0
  194. package/src/render/gi/bounceLights.ts +12 -3
  195. package/src/render/instanceCull.ts +4 -0
  196. package/src/render/instances.ts +26 -6
  197. package/src/render/lightBudget.ts +40 -2
  198. package/src/render/lightmap.ts +64 -23
  199. package/src/render/particlePool.ts +23 -0
  200. package/src/render/pass.ts +53 -1
  201. package/src/render/pointLightSelection.ts +20 -1
  202. package/src/render/reflectionSurfaceTargets.ts +105 -0
  203. package/src/render/renderQuality.ts +52 -15
  204. package/src/render/rendererHarness.ts +3 -0
  205. package/src/render/rgb9e5.ts +52 -0
  206. package/src/render/sceneCapture.ts +56 -0
  207. package/src/render/sceneTarget.ts +30 -2
  208. package/src/render/screenSpaceReflection.ts +38 -1
  209. package/src/render/shaders/bolt.ts +7 -1
  210. package/src/render/shaders/depth.ts +11 -0
  211. package/src/render/shaders/filmLook.ts +2 -1
  212. package/src/render/shaders/flat/anisotropic.ts +37 -3
  213. package/src/render/shaders/flat/areaLight.ts +46 -0
  214. package/src/render/shaders/flat/glassTint.ts +13 -2
  215. package/src/render/shaders/flat/index.ts +37 -2
  216. package/src/render/shaders/flat/lobes.ts +50 -43
  217. package/src/render/shaders/flat/main.ts +85 -22
  218. package/src/render/shaders/flat/models.ts +100 -16
  219. package/src/render/shaders/flat/overlay.ts +117 -0
  220. package/src/render/shaders/flat/pointShadow.ts +1 -1
  221. package/src/render/shaders/flat/preamble.ts +10 -3
  222. package/src/render/shaders/flat/reflectionSurface.ts +61 -0
  223. package/src/render/shaders/fog.ts +9 -2
  224. package/src/render/shaders/fringe.ts +59 -0
  225. package/src/render/shaders/generated/arcane.wgsl.ts +1 -1
  226. package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
  227. package/src/render/shaders/generated/caustics.wgsl.ts +1 -1
  228. package/src/render/shaders/generated/depth.wgsl.ts +4 -4
  229. package/src/render/shaders/generated/film.wgsl.ts +1 -1
  230. package/src/render/shaders/generated/fire.wgsl.ts +1 -1
  231. package/src/render/shaders/generated/flat.wgsl.ts +230 -54
  232. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  233. package/src/render/shaders/generated/particle.wgsl.ts +4 -4
  234. package/src/render/shaders/generated/rush.wgsl.ts +56 -26
  235. package/src/render/shaders/generated/scatter.wgsl.ts +1 -1
  236. package/src/render/shaders/generated/smoke.wgsl.ts +1 -1
  237. package/src/render/shaders/generated/ssrResolve.wgsl.ts +30 -0
  238. package/src/render/shaders/generated/ssrTrace.wgsl.ts +32 -7
  239. package/src/render/shaders/generated/water.wgsl.ts +23 -16
  240. package/src/render/shaders/gi/probeBake.wgsl.ts +9 -5
  241. package/src/render/shaders/gpudriven/fog.wgsl.ts +11 -3
  242. package/src/render/shaders/recon/motion.wgsl.ts +13 -2
  243. package/src/render/shaders/rush.ts +54 -11
  244. package/src/render/shaders/ssrResolve.ts +52 -0
  245. package/src/render/shaders/ssrTrace.ts +46 -1
  246. package/src/render/shaders/water.ts +12 -3
  247. package/src/render/skinScatter.ts +3 -1
  248. package/src/render/ssrPass.ts +139 -1
  249. package/src/render/surfaceModel.ts +47 -10
  250. package/src/render/surfaceOverlay.ts +195 -0
  251. package/src/render/surfaceTexture.ts +136 -9
  252. package/src/render/temporalAa.ts +20 -0
  253. package/src/render/vertexDefaults.ts +28 -0
@@ -44,7 +44,7 @@ import { COOKIE_TILE } from '../../lightBudget.js';
44
44
  import { FULL_LIGHT_BUDGET } from '../../uniformVectorBudget.js';
45
45
  import { toHalfFloats } from '../../halfFloat.js';
46
46
  import { DEFAULT_SOURCE_RADIUS, FACE_COUNT, createResolvedPointShadows, } from '../../pointShadowImage.js';
47
- import { MAX_AREA_LIGHTS } from '../../areaLights.js';
47
+ import { MAX_AREA_LIGHTS, packAreaSizesAndDoors } from '../../areaLights.js';
48
48
  import { areaShadowLayerCount, castingRange, createResolvedAreaShadows, firstAreaShadowLayer, AreaShadowSet, } from '../../areaShadowSet.js';
49
49
  import { createBakeScratch, runPointShadowBakes, selectCastingLights, } from '../../pointShadowBudget.js';
50
50
  import { POINT_SHADOW_SAMPLER, PointShadowSystem, } from '../../pointShadowSystem.js';
@@ -54,20 +54,28 @@ import { createPanelBindGroup, createPanelBindGroupLayout, createPanelCorners, p
54
54
  import { createGpuTextCube, createTextBindGroup, createTextBindGroupLayout, GpuText, textPipeline, TEXT_FRAG_FIELDS, TEXT_FRAG_SIZE, TEXT_VERT_FIELDS, TEXT_VERT_SIZE, } from './textPass.js';
55
55
  import { deviceSnappedCellSize, deviceSnappedOrigin } from '../../textLayout.js';
56
56
  import { GpuSurfaceTexture } from './surfaceTexturePass.js';
57
+ import { GpuSceneCaptureTarget } from './sceneCaptureTarget.js';
58
+ import { sceneCaptureTexels } from '../../sceneCapture.js';
57
59
  import { gpuCompressedFormats } from './compressedUpload.js';
58
60
  import { GpuClothBinding, GpuClothParticles, clothStandIn } from './clothTextures.js';
59
- import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel } from '../../surfaceModel.js';
61
+ import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel, skinProfileDistance, } from '../../surfaceModel.js';
62
+ import { OVERLAY_FLOATS, overlayLays, packSurfaceOverlay, } from '../../surfaceOverlay.js';
63
+ import { frameReflectionsRefused, resolveFrameReflections, } from '../../frameReflections.js';
64
+ import { ReflectionSurfacePass } from './reflectionSurfacePass.js';
60
65
  import { validateClothBinding } from '../../clothBindingData.js';
61
66
  import { createSdfTextBindGroup, createSdfTextBindGroupLayout, GpuSdfText, sdfTextPipeline, SDF_TEXT_FRAG_FIELDS, SDF_TEXT_FRAG_SIZE, SDF_TEXT_VERT_FIELDS, SDF_TEXT_VERT_SIZE, } from './sdfTextPass.js';
62
67
  import { createFilmBindGroup, createFilmBindGroupLayout, filmPipeline, FILM_FRAG_FIELDS, FILM_FRAG_SIZE, FILM_VERT_FIELDS, FILM_VERT_SIZE, } from './filmPass.js';
63
68
  import { reflectionTargetSize } from '../../planarReflectionDraw.js';
64
69
  import { drawingBufferSize } from '../../drawingBuffer.js';
65
- import { MOTION_BLUR_MAX_UV, OUTPUT_TRANSFORM_CODE, RUSH_REACH_UV } from '../../vertexDefaults.js';
70
+ import { MOTION_BLUR_MAX_UV, OBJECT_BLUR_UNKEPT, OUTPUT_TRANSFORM_CODE, RUSH_REACH_UV, clampMotionShare, forwardTransformCode, } from '../../vertexDefaults.js';
71
+ import { FRINGE_FLOATS, resolveFringe } from '../../shaders/fringe.js';
72
+ import { displayHeadroom } from '../../displayRange.js';
73
+ import { DEFAULT_FILMIC_CURVE, FILMIC_CLAMPED, FILMIC_CONSTANTS, FILMIC_WITHOUT_COMPOSITE, resolveFilmicCurve, } from '../../filmicCurve.js';
66
74
  import { ExposurePass } from './exposurePass.js';
67
75
  import { AO_STORE } from '../../shaders/ambientOcclusion.js';
68
76
  import { clampAutoExposure } from '../../shaders/exposure.js';
69
77
  import { clampLocalExposure } from '../../shaders/localExposure.js';
70
- import { createDepthResolveLayout, createPostStageLayout, depthResolvePipeline, postPipeline, postPipelineTargets, sceneColorFormat, AO_BLUR_FIELDS, AO_BLUR_FRAG_WGSL, AO_BLUR_SIZE, AO_BLUR_TEXTURES, AO_BLUR_UNIFORMS, AO_FRAG_FIELDS, AO_FRAG_SIZE, AO_FRAG_WGSL, AO_TEXTURES, AO_UNIFORMS, BLOOM_DOWNSAMPLE_FIELDS, BLOOM_DOWNSAMPLE_FRAG_WGSL, BLOOM_PREFILTER_FIELDS, BLOOM_PREFILTER_FRAG_WGSL, BLOOM_STAGE_SIZE, BLOOM_TEXTURES, BLOOM_UNIFORMS, BLOOM_UPSAMPLE_FIELDS, TINTED_ADD_BLEND, BLOOM_UPSAMPLE_FRAG_WGSL, RESOLVED_DEPTH_FORMAT, RUSH_FRAG_FIELDS, RUSH_FRAG_SIZE, RUSH_FRAG_WGSL, RUSH_TEXTURES, RUSH_UNIFORMS, TAA_FRAG_FIELDS, TAA_FRAG_SIZE, TAA_TEXTURES, TAA_UNIFORMS, TEMPORAL_RESOLVE_FRAG_WGSL, MEDIUM_FRAG_FIELDS, MEDIUM_FRAG_SIZE, MEDIUM_FRAG_WGSL, MEDIUM_TEXTURES, MEDIUM_TRANSMIT_BLEND, MEDIUM_UNIFORMS, MEDIUM_UPSAMPLE_FIELDS, MEDIUM_UPSAMPLE_FRAG_WGSL, MEDIUM_UPSAMPLE_SIZE, MEDIUM_UPSAMPLE_TEXTURES, MEDIUM_UPSAMPLE_UNIFORMS, OIT_COMPOSITE_BLEND, OIT_RESOLVE_FRAG_WGSL, OIT_TEXTURES, DECAL_FRAG_FIELDS, DECAL_FRAG_SIZE, DECAL_MULTIPLY_BLEND, DECAL_PROJECT_FRAG_WGSL, DECAL_TEXTURES, DECAL_UNIFORMS, PREMULTIPLIED_OVER_BLEND, REFLECTION_OVER_BLEND, SSR_FRAG_FIELDS, SSR_FRAG_SIZE, SSR_RESOLVE_FRAG_WGSL, SSR_RESOLVE_TEXTURES, SSR_TEXTURES, SSR_TRACE_FRAG_WGSL, SSR_UNIFORMS, createOitResolveLayout, createSsrResolveLayout, } from './postPass.js';
78
+ import { createDepthResolveLayout, createPostStageLayout, depthResolvePipeline, postPipeline, postPipelineTargets, sceneColorFormat, AO_BLUR_FIELDS, AO_BLUR_FRAG_WGSL, AO_BLUR_SIZE, AO_BLUR_TEXTURES, AO_BLUR_UNIFORMS, AO_FRAG_FIELDS, AO_FRAG_SIZE, AO_FRAG_WGSL, AO_TEXTURES, AO_UNIFORMS, BLOOM_DOWNSAMPLE_FIELDS, BLOOM_DOWNSAMPLE_FRAG_WGSL, BLOOM_PREFILTER_FIELDS, BLOOM_PREFILTER_FRAG_WGSL, BLOOM_STAGE_SIZE, BLOOM_TEXTURES, BLOOM_UNIFORMS, BLOOM_UPSAMPLE_FIELDS, TINTED_ADD_BLEND, BLOOM_UPSAMPLE_FRAG_WGSL, RESOLVED_DEPTH_FORMAT, RUSH_FRAG_FIELDS, RUSH_FRAG_SIZE, RUSH_FRAG_WGSL, RUSH_TEXTURES, RUSH_UNIFORMS, TAA_FRAG_FIELDS, TAA_FRAG_SIZE, TAA_TEXTURES, TAA_UNIFORMS, TEMPORAL_RESOLVE_FRAG_WGSL, MEDIUM_FRAG_FIELDS, MEDIUM_FRAG_SIZE, MEDIUM_FRAG_WGSL, MEDIUM_TEXTURES, MEDIUM_TRANSMIT_BLEND, MEDIUM_UNIFORMS, MEDIUM_UPSAMPLE_FIELDS, MEDIUM_UPSAMPLE_FRAG_WGSL, MEDIUM_UPSAMPLE_SIZE, MEDIUM_UPSAMPLE_TEXTURES, MEDIUM_UPSAMPLE_UNIFORMS, OIT_COMPOSITE_BLEND, OIT_RESOLVE_FRAG_WGSL, OIT_TEXTURES, DECAL_FRAG_FIELDS, DECAL_FRAG_SIZE, DECAL_MULTIPLY_BLEND, DECAL_PROJECT_FRAG_WGSL, DECAL_TEXTURES, DECAL_UNIFORMS, PREMULTIPLIED_OVER_BLEND, FRAME_REFLECTION_BLEND, REFLECTION_OVER_BLEND, SSR_FRAG_FIELDS, SSR_FRAG_SIZE, SSR_MATERIAL_FIELDS, SSR_MATERIAL_RESOLVE_FRAG_WGSL, SSR_MATERIAL_SIZE, SSR_MATERIAL_TEXTURES, SSR_MATERIAL_UNIFORMS, SSR_RESOLVE_FRAG_WGSL, SSR_RESOLVE_TEXTURES, SSR_TEXTURES, SSR_TRACE_FRAG_WGSL, SSR_UNIFORMS, createOitResolveLayout, createSsrResolveLayout, } from './postPass.js';
71
79
  import { GRADE_PLACEHOLDER_SIZE, identityGradeLut, validateGradeLut, } from '../../colourGrade.js';
72
80
  import { FILM_LOOK_WITHOUT_COMPOSITE, clampGrain, clampVignette, grainSeed, } from '../../shaders/filmLook.js';
73
81
  import { createProbeMipBindGroupLayout, createProbePrefilterBindGroupLayout, PREFILTER_UNIFORM_OFFSETS, probeLevels, probeMipPipeline, probePrefilterPipeline, PROBE_FACES, PROBE_FACE_BASIS, PROBE_FAR_M, PROBE_FOV_DEG, PROBE_MIP_UNIFORM_SIZE, PROBE_NEAR_M, } from './probePass.js';
@@ -91,7 +99,7 @@ import { GpuGlassTint } from './glassTintPass.js';
91
99
  import { SUN_DYNAMIC_LAYER, SUN_GLASS_MOVING_LAYER, SUN_GLASS_STATIC_LAYER, SUN_PEELED_LAYER, SUN_STATIC_LAYER, } from '../../shadowMap.js';
92
100
  import { VERTEX_LAYOUT, createGpuMesh, createGpuMeshIncremental } from './buffers.js';
93
101
  import { SkinPaletteRing } from './skinPaletteRing.js';
94
- import { DEPTH_FORMAT, FLAT_VERT_FIELDS, FLAT_VERT_SIZE, createFlatBindGroup, createFlatBindGroupLayout, flatFragmentBindings, flatVariant, flatVertexBindings, flatPipeline, flatPipelineAsync, } from './flatPass.js';
102
+ import { DEPTH_FORMAT, FLAT_VERT_FIELDS, FLAT_VERT_SIZE, createFlatBindGroup, createFlatBindGroupLayout, flatFragmentBindings, flatVariant, flatVertexBindings, flatPipeline, flatPipelineAsync, probeStageCeilings, } from './flatPass.js';
95
103
  import { DEPTH_FRAG_BINDING, DEPTH_FRAG_SIZE, DEPTH_FRAG_WGSL, DEPTH_PREVIOUS_BINDING, DEPTH_CUTOUT_VERT_FIELDS, DEPTH_CUTOUT_VERT_SIZE, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS, DEPTH_VERT_FIELDS, DEPTH_VERT_SIZE, SHADOW_FORMAT, createDepthBindGroup, createDepthBindGroupLayout, depthPipeline, } from './depthPass.js';
96
104
  import { PipelineCache } from './pipelineCache.js';
97
105
  import { SKIN_TARGET_FORMAT, SkinScatterPass } from './skinScatterPass.js';
@@ -213,34 +221,20 @@ const MAX_SKY_DRAWS = 64;
213
221
  /**
214
222
  * Whether the environment probe's permutation fits this device's per-stage binding limits.
215
223
  *
216
- * The widest flat variant declares one albedo, three directional shadow maps, twelve
217
- * point-shadow cubes and the probe: seventeen textures and seventeen samplers. WebGPU's default
218
- * is sixteen of each and an adapter is free to offer no more — this one offers 48 textures and
219
- * exactly 16 samplers, so the samplers are what decide it.
224
+ * **Two ceilings, counted separately**: sampled textures and samplers, since textures that share a
225
+ * sampler (`// wgsl:share` in `flat/preamble.ts`) count once against the second. The widest variant
226
+ * binds sixteen textures and twelve samplers, which is exactly WebGPU's default texture ceiling, and
227
+ * `select.ts` asks for the adapter's own — 48 here — so the probe does not depend on that request.
220
228
  */
221
229
  function probeFits(device) {
222
230
  /*
223
- * **Two ceilings, counted separately, and they are no longer the same number.**
224
- *
225
- * The widest permutation declares seventeen sampled textures — one albedo, three directional
226
- * shadow maps, the point-shadow array, a normal map and the probe's own cube — **seven**, where
227
- * it was seventeen while every point light's shadow was a `samplerCube` of its own. `select.ts` asks
228
- * for the adapter's ceiling, which is 48 on this machine against a device default of 16, so
229
- * six fits everywhere and the probe no longer depends on the request being granted.
230
- *
231
- * It used to declare seventeen *samplers* too, and that was the binding constraint: this
232
- * adapter offers exactly sixteen and no request can raise it, so the probe stood down and
233
- * reflective surfaces kept a gradient. The shadow bindings share one declaration — see
234
- * `// wgsl:share shadow` in `flat/preamble.ts`, which costs nothing because they were already
235
- * one object — so the count is four, and the fourth is the normal map's: it is a colour image
236
- * and wants filtering, which a non-filtering shadow sampler cannot give it.
237
- *
238
- * **Counted from the generated bindings rather than typed**: `FLAT_BINDINGS.flatFrag`'s
239
- * widest variant is `directionalShadows+environmentProbe+pointShadows`, seven textures and
240
- * four samplers. A stale number here reads as a device declining a feature it can run.
241
- */
242
- const TEXTURES = 7;
243
- const SAMPLERS = 4;
231
+ * **Counted from the generated bindings rather than typed** (`probeStageCeilings`). It was typed
232
+ * here as seven textures and four samplers while the widest variant grew to sixteen and twelve
233
+ * with the material maps, glass, the fixture atlas and DriftLight: a stale number reads as a
234
+ * device declining a feature it can run, or, the way this one had drifted, as one admitted to a
235
+ * permutation it cannot bind.
236
+ */
237
+ const { textures: TEXTURES, samplers: SAMPLERS } = probeStageCeilings();
244
238
  const fits = device.limits.maxSampledTexturesPerShaderStage >= TEXTURES &&
245
239
  device.limits.maxSamplersPerShaderStage >= SAMPLERS;
246
240
  if (!fits) {
@@ -277,6 +271,7 @@ const VERB_PASS = 3;
277
271
  /** What the depth snapshot copies from, and what it copies into. */
278
272
  const DEPTH_SOURCE = maskOf('sceneDepth');
279
273
  const DEPTH_SNAPSHOT = maskOf('depthSnapshot');
274
+ const COLOR_SNAPSHOT = maskOf('colorSnapshot');
280
275
  /** The mirror's own colour and depth. */
281
276
  const MIRROR_TARGET = maskOf('mirrorColor', 'mirrorDepth');
282
277
  /**
@@ -1278,6 +1273,17 @@ export class WebGPURenderer {
1278
1273
  */
1279
1274
  refractSnapshot = null;
1280
1275
  refractSnapshotView = null;
1276
+ /**
1277
+ * The frame's colour as a registered pass reads it (`reads: ['colorSnapshot']`): the scene's own
1278
+ * size and format, refilled at each such pass's draw. Allocated only once a pass declares the
1279
+ * read, beside the frame's targets, and never in the frame loop. See `takePassColour`.
1280
+ */
1281
+ passColour = null;
1282
+ passColourView = null;
1283
+ /** Registered passes that read the colour, queued this frame for after the upscale. */
1284
+ latePasses = [];
1285
+ /** A late pass draws unjittered, as every late draw does. */
1286
+ latePassJitter = new Float32Array(2);
1281
1287
  /** Whether this frame has taken it. One snapshot serves every refracting draw. */
1282
1288
  refractSnapshotTaken = false;
1283
1289
  /**
@@ -1780,8 +1786,18 @@ export class WebGPURenderer {
1780
1786
  reflectionQueue = new ReflectionQueue();
1781
1787
  ssrLayout;
1782
1788
  ssrResolveLayout;
1789
+ /** The frame's materials' reflections, resolved; null where off. See `frameReflections.ts`. */
1790
+ frameReflections;
1791
+ /** Their surface half, null where they are off or cannot be drawn: `reflectionSurfacePass.ts`. */
1792
+ reflectionSurface;
1793
+ /** The material resolve: its layout, its one block (the blur) and its group over the three maps. */
1794
+ ssrMaterialLayout;
1795
+ ssrMaterialUniforms;
1796
+ ssrMaterialFloats = new Float32Array(SSR_MATERIAL_SIZE / 4);
1797
+ ssrMaterialGroup = null;
1783
1798
  ssrUniforms;
1784
- ssrStaging = new ArrayBuffer(SSR_SLOT * MAX_REFLECTIVE_SURFACES);
1799
+ /* One slot a surface, and one more for the frame's materials' trace. */
1800
+ ssrStaging = new ArrayBuffer(SSR_SLOT * (MAX_REFLECTIVE_SURFACES + 1));
1785
1801
  ssrFloats = new Float32Array(this.ssrStaging);
1786
1802
  ssrReflection = null;
1787
1803
  ssrReflectionView = null;
@@ -2227,6 +2243,104 @@ export class WebGPURenderer {
2227
2243
  mipmap: false,
2228
2244
  });
2229
2245
  }
2246
+ /**
2247
+ * A texture the scene can be drawn into, for a material to show. See `RendererApi` and
2248
+ * `sceneCapture.ts`. At the world's own format, which under `hdrScene` is half floats.
2249
+ */
2250
+ createSceneCapture(width, height) {
2251
+ const texture = new GpuSurfaceTexture(this.surface.device, this.pipelines, sceneCaptureTexels(width, height, this.quality.hdrScene), { wrap: 'clamp', mipmap: false });
2252
+ this.captureTargets.set(texture, new GpuSceneCaptureTarget(this.surface.device, width, height));
2253
+ return texture;
2254
+ }
2255
+ /** Each capture's attachments, beside the texture a material binds. */
2256
+ captureTargets = new WeakMap();
2257
+ /**
2258
+ * Whether a scene capture is being drawn: an off-screen pass as a probe bake is, so the frame's
2259
+ * camera, jitter, motion, refraction copy, skin's spread and late routing are left alone, but
2260
+ * lit by the probes and DriftLight as the frame is. See `captureScene`.
2261
+ */
2262
+ capturePassActive = false;
2263
+ /**
2264
+ * Draw the scene into a capture from `camera`. See `RendererApi.captureScene`.
2265
+ *
2266
+ * **Its own encoder, as a probe bake's**: the frame's pass is set aside and restored, the rings
2267
+ * are marked and rewound around the submit, and the frame graph is flushed at both boundaries so
2268
+ * no recorded draw crosses one. **Radiance**: the output transform and exposure are held off.
2269
+ */
2270
+ captureScene(capture, camera, clearColor, draw) {
2271
+ if (this.surface.lost)
2272
+ return false;
2273
+ const target = this.captureTargets.get(capture);
2274
+ const resolve = capture.layerView();
2275
+ if (target === undefined || resolve === null)
2276
+ return false;
2277
+ camera.updateMatrices(target.width / target.height);
2278
+ const { device } = this.surface;
2279
+ const { depth, colorMsaa } = target.attachments(this.samples, this.pipelines.format);
2280
+ /*
2281
+ * **Flushed, and only then set aside.** The flush replays what the frame has recorded, and
2282
+ * replaying opens the frame's pass where none is open yet; saved before it, the pass given back
2283
+ * afterwards was the null that stood there, while the one the flush opened was left open on the
2284
+ * frame's encoder and every later pass on it was refused — a black frame for any capture taken
2285
+ * after the frame had drawn something. `scripts/capture-check.mjs` takes one at three points.
2286
+ */
2287
+ if (this.quality.frameGraph)
2288
+ this.flushGraph();
2289
+ const savedPass = this.pass;
2290
+ const savedEncoder = this.encoder;
2291
+ this.markRings();
2292
+ const skinMark = this.skinPalettes.mark();
2293
+ const frameEpoch = this.encoderEpoch;
2294
+ this.encoderEpoch = ++this.encoderEpochs;
2295
+ const encoder = device.createCommandEncoder({ label: 'capture' });
2296
+ this.encoder = encoder;
2297
+ this.capturePassActive = true;
2298
+ const heldGrade = this.perFrameInts[this.materialField('uOutputTransform')] ?? 0;
2299
+ const heldExposure = this.perFrameFloats[this.materialField('uOutputExposure')] ?? 1;
2300
+ this.perFrameInts[this.materialField('uOutputTransform')] = 0;
2301
+ this.perFrameFloats[this.materialField('uOutputExposure')] = 1;
2302
+ this.materials.dirty();
2303
+ const pass = encoder.beginRenderPass({
2304
+ label: 'capture',
2305
+ colorAttachments: [
2306
+ {
2307
+ view: colorMsaa ?? resolve,
2308
+ resolveTarget: colorMsaa === null ? undefined : resolve,
2309
+ loadOp: 'clear',
2310
+ storeOp: resolvedStoreOp(colorMsaa !== null, true, this.quality.discardResolvedAttachments),
2311
+ clearValue: { r: clearColor[0], g: clearColor[1], b: clearColor[2], a: 1 },
2312
+ },
2313
+ ],
2314
+ depthStencilAttachment: {
2315
+ view: depth,
2316
+ depthClearValue: DEPTH_CLEAR,
2317
+ depthLoadOp: 'clear',
2318
+ depthStoreOp: 'discard',
2319
+ },
2320
+ });
2321
+ this.pass = pass;
2322
+ try {
2323
+ draw(camera);
2324
+ /* The callback's draws replayed into this pass before it ends: see `bakeProbe`. */
2325
+ if (this.quality.frameGraph)
2326
+ this.flushGraph();
2327
+ }
2328
+ finally {
2329
+ pass.end();
2330
+ this.pass = null;
2331
+ this.flushRings();
2332
+ device.queue.submit([encoder.finish()]);
2333
+ this.rewindRings(skinMark);
2334
+ this.encoderEpoch = frameEpoch;
2335
+ this.encoder = savedEncoder;
2336
+ this.pass = savedPass;
2337
+ this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
2338
+ this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
2339
+ this.materials.dirty();
2340
+ this.capturePassActive = false;
2341
+ }
2342
+ return true;
2343
+ }
2230
2344
  /**
2231
2345
  * GPU textures an `updateSurfaceTexture` replaced, destroyed when the next frame begins.
2232
2346
  *
@@ -2278,8 +2392,12 @@ export class WebGPURenderer {
2278
2392
  this.emissiveMap = null;
2279
2393
  if (this.modelMap === texture)
2280
2394
  this.modelMap = null;
2395
+ if (this.overlayMaps === texture)
2396
+ this.overlayMaps = null;
2281
2397
  this.forgetBindingsOf(texture);
2282
2398
  texture.dispose();
2399
+ this.captureTargets.get(texture)?.dispose();
2400
+ this.captureTargets.delete(texture);
2283
2401
  }
2284
2402
  /**
2285
2403
  * Drop every bind group that may hold `texture`'s view, and rebuild the ones bound now.
@@ -2293,6 +2411,7 @@ export class WebGPURenderer {
2293
2411
  /* Same reasoning, for whichever SDF labels were bound against this atlas. */
2294
2412
  this.sdfTextBindGroups.delete(texture);
2295
2413
  this.flatBindGroups.clear();
2414
+ this.overlayGroups.clear();
2296
2415
  /*
2297
2416
  * The blank one is built with the albedo held aside, because `buildFlatBindGroup` reads
2298
2417
  * `this.albedo` for that slot — leaving it in place would put a real colour map into the
@@ -2458,7 +2577,7 @@ export class WebGPURenderer {
2458
2577
  const model = material?.model;
2459
2578
  warnUnpagedLightmap(model, modelMap);
2460
2579
  if (model?.kind === 'skin' && this.skinScatter !== null) {
2461
- this.skinScatter.setProfile(model.profile, model.scatterColor[0] * model.radius, model.scatterColor[1] * model.radius, model.scatterColor[2] * model.radius);
2580
+ this.skinScatter.setProfile(model.profile, skinProfileDistance(model, 0), skinProfileDistance(model, 1), skinProfileDistance(model, 2));
2462
2581
  }
2463
2582
  this.material((f, i) => {
2464
2583
  /* The material's four switches, one row: albedo, ORM map, emissive map, two-sided. */
@@ -2503,7 +2622,10 @@ export class WebGPURenderer {
2503
2622
  f[emissive + 1] = asked?.[1] ?? 1;
2504
2623
  f[emissive + 2] = asked?.[2] ?? 1;
2505
2624
  /* The model's numbers and whether its map is bound. See `packModel`. */
2506
- packModel(material?.model ?? null, modelMap !== null, this.modelParams);
2625
+ packModel(material?.model ?? null, modelMap !== null, this.modelParams, material?.physicalSpecular === true);
2626
+ /* The first physical highlight asked for is when the lit pipelines start carrying one. */
2627
+ if (material?.physicalSpecular === true)
2628
+ this.enableLit('PHYSICAL_SPECULAR');
2507
2629
  f.set(this.modelParams, this.materialField('uModelParams'));
2508
2630
  });
2509
2631
  /*
@@ -2625,6 +2747,21 @@ export class WebGPURenderer {
2625
2747
  packAmbientSH(coefficients, this.ambientSH);
2626
2748
  this.material((f) => f.set(this.ambientSH, this.materialField('uAmbientSH')));
2627
2749
  }
2750
+ /**
2751
+ * The overlay the following draws wear, or null for none: pass state, put back to none by
2752
+ * `bindMeshPass`. See `renderer.ts` and `surfaceOverlay.ts`. The numbers go in the material block,
2753
+ * which every variant declares; the atlas goes in a twin of each flat group (`overlayGroup`).
2754
+ */
2755
+ setSurfaceOverlay(overlay) {
2756
+ if (this.surface.lost)
2757
+ return;
2758
+ packSurfaceOverlay(overlay, this.overlayFloats);
2759
+ /* The first overlay laid is when the lit pipelines start carrying one. */
2760
+ if (overlayLays(overlay))
2761
+ this.enableLit('SURFACE_OVERLAY');
2762
+ this.overlayMaps = (overlay?.maps ?? null);
2763
+ this.material((f) => f.set(this.overlayFloats, this.materialField('uOverlay')));
2764
+ }
2628
2765
  /** How reflective the surfaces drawn next are. Clamped, as `renderer.ts` clamps it. */
2629
2766
  setSurfaceReflectivity(amount) {
2630
2767
  this.material((f) => {
@@ -3295,7 +3432,7 @@ export class WebGPURenderer {
3295
3432
  */
3296
3433
  gradeCode() {
3297
3434
  return !this.hasComposite || !this.quality.hdrScene
3298
- ? (OUTPUT_TRANSFORM_CODE[this.quality.outputTransform] ?? 0)
3435
+ ? forwardTransformCode(this.quality.outputTransform)
3299
3436
  : 0;
3300
3437
  }
3301
3438
  gradeExposure() {
@@ -3321,6 +3458,30 @@ export class WebGPURenderer {
3321
3458
  return;
3322
3459
  const device = this.surface.device;
3323
3460
  device.queue.writeBuffer(batch.instances, 0, batch.staging, 0, live * PARTICLE_INSTANCE_FLOATS);
3461
+ this.drawParticleInstances(batch, batch.instances, live, camera, env, timeSeconds);
3462
+ }
3463
+ /**
3464
+ * Draw particles a caller's own compute wrote, where they lie. See `DeviceParticles`.
3465
+ *
3466
+ * **The pool's draw with its instance stream swapped**: the same material, blend, flipbook, soft
3467
+ * edge and fog, reading the caller's buffer in place of the one the pool uploads into — so nothing
3468
+ * crosses back to the CPU. Not sorted, and a count past what the buffer holds is cut to it rather
3469
+ * than handed to a draw the device would refuse with the frame.
3470
+ */
3471
+ drawDeviceParticles(batch, particles, camera, env, timeSeconds) {
3472
+ const fits = Math.floor(particles.buffer.size / (PARTICLE_INSTANCE_FLOATS * 4));
3473
+ const live = Math.min(Math.max(0, Math.floor(particles.count)), fits);
3474
+ /* Not above zero covers a NaN as well: a count or a size that is no number draws nothing. */
3475
+ if (!this.canDraw() || !(live > 0))
3476
+ return;
3477
+ if (batch.drawnInEncoder === this.encoderEpoch)
3478
+ return;
3479
+ batch.drawnInEncoder = this.encoderEpoch;
3480
+ this.drawParticleInstances(batch, particles.buffer, live, camera, env, timeSeconds);
3481
+ }
3482
+ /** A pool's uniforms for this view, and its draw from `instances`: both entry points' tail. */
3483
+ drawParticleInstances(batch, instances, live, camera, env, timeSeconds) {
3484
+ const device = this.surface.device;
3324
3485
  const v = batch.vertFloats;
3325
3486
  const atV = (name) => (PARTICLE_VERT_FIELDS[name]?.offset ?? -4) / 4;
3326
3487
  mat4.multiply(this.correctedViewProj, CLIP_CORRECTION, camera.viewProjection);
@@ -3392,7 +3553,7 @@ export class WebGPURenderer {
3392
3553
  particleCommand.pipeline = particlePipe;
3393
3554
  particleCommand.bindGroup = group;
3394
3555
  particleCommand.vertexBuffers[0] = batch.corners;
3395
- particleCommand.vertexBuffers[1] = batch.instances;
3556
+ particleCommand.vertexBuffers[1] = instances;
3396
3557
  particleCommand.vertexCount = 2;
3397
3558
  particleCommand.indexBuffer = batch.indexBuffer;
3398
3559
  particleCommand.indexed = true;
@@ -3406,7 +3567,7 @@ export class WebGPURenderer {
3406
3567
  pass.setPipeline(particlePipe);
3407
3568
  pass.setBindGroup(0, group);
3408
3569
  pass.setVertexBuffer(0, batch.corners);
3409
- pass.setVertexBuffer(1, batch.instances);
3570
+ pass.setVertexBuffer(1, instances);
3410
3571
  pass.setIndexBuffer(batch.indexBuffer, 'uint32');
3411
3572
  pass.drawIndexed(PARTICLE_INDICES, live);
3412
3573
  }
@@ -3424,7 +3585,10 @@ export class WebGPURenderer {
3424
3585
  const fields = batch.fields;
3425
3586
  const atF = (name) => (fields[name]?.offset ?? -4) / 4;
3426
3587
  let depth = null;
3427
- if (batch.softDepth > 0 && !this.reflectionPassActive && !this.probePassActive) {
3588
+ if (batch.softDepth > 0 &&
3589
+ !this.reflectionPassActive &&
3590
+ !this.probePassActive &&
3591
+ !this.capturePassActive) {
3428
3592
  if (this.openPass() !== null && this.takeVolumeDepth())
3429
3593
  depth = this.resolvedDepthView;
3430
3594
  if (depth === null && !this.warnedSpriteDepth) {
@@ -3717,6 +3881,7 @@ export class WebGPURenderer {
3717
3881
  scatterInto(f, WATER_FRAG_FIELDS['uLightRadius'], lights.radii, 1);
3718
3882
  scatterInto(f, WATER_FRAG_FIELDS['uLightSourceRadius'], lights.sourceRadii, 1);
3719
3883
  scatterInto(f, WATER_FRAG_FIELDS['uLightWeight'], lights.weights, 1);
3884
+ scatterInto(f, WATER_FRAG_FIELDS['uLightCone'], lights.conesAndFalloffs, 4);
3720
3885
  const medium = resolveAtmosphere(env, camera.position[1] ?? 0, this.quality.underwaterAtmosphere, this.medium);
3721
3886
  f.set(medium.fogColor, atF('uFogColor'));
3722
3887
  f[atF('uFogDensity')] = medium.fogDensity;
@@ -4509,8 +4674,11 @@ export class WebGPURenderer {
4509
4674
  cookies: new Float32Array(0),
4510
4675
  heldSourceRadii: new Float32Array(MAX_POINT_LIGHTS),
4511
4676
  axesAndChannels: new Float32Array(MAX_POINT_LIGHTS * 4),
4677
+ conesAndFalloffs: new Float32Array(MAX_POINT_LIGHTS * 4),
4512
4678
  heldWeights: new Float32Array(MAX_POINT_LIGHTS),
4513
4679
  };
4680
+ /** The fixed arm's rectangles as `uAreaLightSize` holds them. See `packAreaSizesAndDoors`. */
4681
+ areaSizesAndDoors = new Float32Array(MAX_AREA_LIGHTS * 4);
4514
4682
  /** Refilled per frame rather than allocated; see `resolveAtmosphere`. */
4515
4683
  medium = {
4516
4684
  fogColor: new Float32Array(3),
@@ -5184,6 +5352,10 @@ export class WebGPURenderer {
5184
5352
  this.skinPalettes = new SkinPaletteRing(quality.drawsPerFrame);
5185
5353
  this.maxDrawingBufferPixels = quality.maxDrawingBufferPixels;
5186
5354
  this.exposure = quality.outputExposure;
5355
+ resolveFilmicCurve(DEFAULT_FILMIC_CURVE, this.filmConstants);
5356
+ if (quality.outputTransform === 'filmic' && !(quality.screenEffects && quality.hdrScene)) {
5357
+ console.warn(FILMIC_WITHOUT_COMPOSITE);
5358
+ }
5187
5359
  this.flushSchedule =
5188
5360
  quality.frameGraph && quality.identifierGraph
5189
5361
  ? createFlushSchedule(MAX_DRAWS_PER_FRAME)
@@ -5253,6 +5425,25 @@ export class WebGPURenderer {
5253
5425
  console.warn("WebGPU: skinScattering 'screen-space' needs screenEffects to add into; skin is pre-integrated");
5254
5426
  }
5255
5427
  this.skinScatter = skinScreen ? new SkinScatterPass(device) : null;
5428
+ /*
5429
+ * The frame's materials' reflections, where the frame can carry them: scene light to swap the
5430
+ * probe's share in, and one sample, which the trace's depth copy and both maps are. Said once
5431
+ * where it cannot. See `frameReflections.ts`.
5432
+ */
5433
+ this.frameReflections = resolveFrameReflections(quality.screenSpaceReflections);
5434
+ const reflectionsRefusal = this.frameReflections === null
5435
+ ? null
5436
+ : !quality.screenEffects || !quality.hdrScene
5437
+ ? 'the frame keeps no scene light to swap the probe in (`screenEffects` and `hdrScene`)'
5438
+ : this.samples > 1
5439
+ ? 'a multisampled frame has no single depth or colour to march through'
5440
+ : null;
5441
+ if (reflectionsRefusal !== null)
5442
+ console.warn(`WebGPU: ${frameReflectionsRefused(reflectionsRefusal)}`);
5443
+ this.reflectionSurface =
5444
+ this.frameReflections !== null && reflectionsRefusal === null
5445
+ ? new ReflectionSurfacePass(device)
5446
+ : null;
5256
5447
  this.skinPipelines = skinScreen
5257
5448
  ? new PipelineCache(device, SKIN_TARGET_FORMAT, this.samples, glassShadows, quality.clusteredLights)
5258
5449
  : null;
@@ -5608,6 +5799,11 @@ export class WebGPURenderer {
5608
5799
  addressModeU: 'clamp-to-edge',
5609
5800
  addressModeV: 'clamp-to-edge',
5610
5801
  });
5802
+ /* Or a surface overlay's atlas, in the twin a draw wearing one binds: `overlayGroup`. */
5803
+ const maps = this.overlayBinding ? this.overlayMaps : null;
5804
+ if (maps !== null) {
5805
+ return { view: maps.layerView() ?? blankView, sampler: this.refractSampler };
5806
+ }
5611
5807
  return { view: this.refractSnapshotView ?? blankView, sampler: this.refractSampler };
5612
5808
  }
5613
5809
  /*
@@ -5738,6 +5934,8 @@ export class WebGPURenderer {
5738
5934
  { binding: RUSH_TEXTURES.uExposureHeld },
5739
5935
  /* Local exposure's grid, read filtered between tiles. See `localExposure.ts`. */
5740
5936
  { binding: RUSH_TEXTURES.uExposureLocal },
5937
+ /* Each mover's own motion: the reconstruction's target, or the scene where there is none. */
5938
+ { binding: RUSH_TEXTURES.uMotion },
5741
5939
  ]);
5742
5940
  this.rushUniforms = device.createBuffer({
5743
5941
  label: 'post.rushUniforms',
@@ -5755,11 +5953,24 @@ export class WebGPURenderer {
5755
5953
  this.ssrLayout = createPostStageLayout(device, SSR_UNIFORMS, SSR_FRAG_SIZE, [
5756
5954
  { binding: SSR_TEXTURES.uSsrDepth, filterable: false },
5757
5955
  { binding: SSR_TEXTURES.uSsrScene },
5956
+ /* The frame's materials, for the trace that reads them: `frameReflections.ts`. */
5957
+ { binding: SSR_TEXTURES.uSsrProbeMap },
5958
+ { binding: SSR_TEXTURES.uSsrTintMap },
5758
5959
  ]);
5759
5960
  this.ssrResolveLayout = createSsrResolveLayout(device);
5961
+ this.ssrMaterialLayout = createPostStageLayout(device, SSR_MATERIAL_UNIFORMS, SSR_MATERIAL_SIZE, [
5962
+ { binding: SSR_MATERIAL_TEXTURES.uSsrReflection },
5963
+ { binding: SSR_MATERIAL_TEXTURES.uSsrProbeMap },
5964
+ { binding: SSR_MATERIAL_TEXTURES.uSsrTintMap },
5965
+ ]);
5966
+ this.ssrMaterialUniforms = device.createBuffer({
5967
+ label: 'post.ssrMaterialUniforms',
5968
+ size: SSR_MATERIAL_SIZE,
5969
+ usage: USAGE_UNIFORM_DST,
5970
+ });
5760
5971
  this.ssrUniforms = device.createBuffer({
5761
5972
  label: 'post.ssrUniforms',
5762
- size: SSR_SLOT * MAX_REFLECTIVE_SURFACES,
5973
+ size: SSR_SLOT * (MAX_REFLECTIVE_SURFACES + 1),
5763
5974
  usage: USAGE_UNIFORM_DST,
5764
5975
  });
5765
5976
  this.decalUniforms = device.createBuffer({
@@ -6175,7 +6386,11 @@ export class WebGPURenderer {
6175
6386
  /* Last frame's snapshot is last frame's scene. */
6176
6387
  this.refractSnapshotTaken = false;
6177
6388
  this.skinScattered = false;
6389
+ /* One frame of the jitter's sequence however many times the frame binds its camera. */
6390
+ this.temporalHistory.nextFrame();
6391
+ this.latePasses.length = 0;
6178
6392
  this.skinScatter?.reset();
6393
+ this.reflectionSurface?.reset();
6179
6394
  this.plumeVerts.reset();
6180
6395
  this.plumeFrags.reset();
6181
6396
  this.waterVerts.reset();
@@ -6566,7 +6781,7 @@ export class WebGPURenderer {
6566
6781
  * the viewer sees, and bounding it through a mirrored camera puts it in the wrong half of the
6567
6782
  * screen.
6568
6783
  */
6569
- if (!this.reflectionPassActive) {
6784
+ if (!this.reflectionPassActive && !this.capturePassActive) {
6570
6785
  mat4.copy(this.frameRawViewProj, camera.viewProjection);
6571
6786
  /* The eye in world space, which the resolve's normals are turned to face. */
6572
6787
  this.reconEye.set(camera.position);
@@ -6600,7 +6815,8 @@ export class WebGPURenderer {
6600
6815
  * second temporal frame stepped the sequence twice a frame, so the world saw every other
6601
6816
  * position and the anti-flicker's period never started.
6602
6817
  */
6603
- if (this.framePresented)
6818
+ /* A capture has no resolve to undo a jitter and is no frame of the sequence. */
6819
+ if (this.framePresented || this.capturePassActive)
6604
6820
  this.viewProj = this.correctedViewProj;
6605
6821
  else
6606
6822
  this.jitterFrame();
@@ -6731,6 +6947,9 @@ export class WebGPURenderer {
6731
6947
  f[at('uWriteMode') + 1] = 0;
6732
6948
  /* 1, not 0: a multiplier's identity is one. See `setEnvironmentGain`. */
6733
6949
  f[at('uEnvironmentDials') + 2] = 1;
6950
+ /* No overlay until a draw sets one. See `setSurfaceOverlay`. */
6951
+ this.overlayFloats.fill(0);
6952
+ f.fill(0, at('uOverlay'), at('uOverlay') + OVERLAY_FLOATS);
6734
6953
  /* The frame's own ambient until a draw asks for its own. See `setAmbientSH`. */
6735
6954
  f.fill(0, at('uAmbientSH'), at('uAmbientSH') + AMBIENT_SH_FLOATS);
6736
6955
  /*
@@ -6928,7 +7147,8 @@ export class WebGPURenderer {
6928
7147
  * uniform added on one backend is bound on both in the same change.
6929
7148
  */
6930
7149
  scatterInto(f, this.fragment.fields['uLightDir'], lights.directions, 3);
6931
- scatterInto(f, this.fragment.fields['uLightCone'], lights.coneCos, 2);
7150
+ /* The cone and, in z, the light's own falloff exponent. See `conesAndFalloffs`. */
7151
+ scatterInto(f, this.fragment.fields['uLightCone'], lights.conesAndFalloffs, 4);
6932
7152
  scatterInto(f, this.fragment.fields['uLightIesProfile'], lights.iesProfiles, 1);
6933
7153
  /* The azimuth and, in w, the light's channels. See `axesAndChannels`. */
6934
7154
  scatterInto(f, this.fragment.fields['uLightIesAxis'], lights.axesAndChannels, 4);
@@ -6949,7 +7169,9 @@ export class WebGPURenderer {
6949
7169
  scatterInto(f, this.fragment.fields['uAreaLightColor'], area.colors, 3);
6950
7170
  scatterInto(f, this.fragment.fields['uAreaLightRight'], area.right, 3);
6951
7171
  scatterInto(f, this.fragment.fields['uAreaLightUp'], area.up, 3);
6952
- scatterInto(f, this.fragment.fields['uAreaLightSize'], area.sizes, 2);
7172
+ /* The half extents and, in zw, the barn doors. See `packAreaSizesAndDoors`. */
7173
+ packAreaSizesAndDoors(area, this.areaSizesAndDoors);
7174
+ scatterInto(f, this.fragment.fields['uAreaLightSize'], this.areaSizesAndDoors, 4);
6953
7175
  scatterInto(f, this.fragment.fields['uAreaLightTwoSided'], area.twoSided, 1);
6954
7176
  }
6955
7177
  /*
@@ -7096,6 +7318,8 @@ export class WebGPURenderer {
7096
7318
  this.emissiveMap = null;
7097
7319
  this.modelMap = null;
7098
7320
  this.bindGroup = this.blankAlbedoBindGroup;
7321
+ /* Nor an overlay; its numbers are zeroed with the block's other defaults. */
7322
+ this.overlayMaps = null;
7099
7323
  this.uvScale[0] = 1;
7100
7324
  this.uvScale[1] = 1;
7101
7325
  this.uvOffset[0] = 0;
@@ -7177,6 +7401,9 @@ export class WebGPURenderer {
7177
7401
  */
7178
7402
  if (this.tracksMotionNow())
7179
7403
  this.recordDrawMotion(mesh, model, previousModel);
7404
+ else if (previousModel !== null && this.quality.cameraMotionBlur * this.motionBlurScale > 0) {
7405
+ this.warnObjectBlur();
7406
+ }
7180
7407
  noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
7181
7408
  /* An opaque draw meets the medium as the dial says: `submitMesh` reads an absent `fog` as
7182
7409
  true, which is a translucent draw's default and would put every opaque draw back in the fog
@@ -7532,6 +7759,10 @@ export class WebGPURenderer {
7532
7759
  if (splits) {
7533
7760
  this.keepSkinDiffuse(mesh, key, base, skinned, morphed, deltas, layer, eight, clothed, slot, material);
7534
7761
  }
7762
+ /* And its surface half, for the frame's materials' reflections. See `keepReflectionSurface`. */
7763
+ if (!blend && lit && depthWrite && this.reflectionSurfaceKeeps()) {
7764
+ this.keepReflectionSurface(mesh, key, base, skinned, morphed, deltas, layer, eight, clothed, modelKind, slot, material, null, 1, null);
7765
+ }
7535
7766
  }
7536
7767
  /* Back to the defaults for whatever is drawn next, exactly as `renderer.ts` restores them —
7537
7768
  unconditionally, because the scratch state above is shared by every draw after this one
@@ -7661,6 +7892,7 @@ export class WebGPURenderer {
7661
7892
  const glassy = resolveGlass(options.glass, this.glassScratch);
7662
7893
  if ((refraction <= 0 && !glassy) || !this.takeRefractSnapshot())
7663
7894
  return false;
7895
+ this.seeThroughBound = true;
7664
7896
  const glass = this.glassScratch;
7665
7897
  /* Strength, path thickness, transmission, frost: one vector, for the budget the shader states. */
7666
7898
  const seeAt = this.materialField('uSeeThrough');
@@ -7681,6 +7913,7 @@ export class WebGPURenderer {
7681
7913
  }
7682
7914
  /** Put the scratch back after `bindSeeThrough`: refraction and glass off. */
7683
7915
  releaseSeeThrough() {
7916
+ this.seeThroughBound = false;
7684
7917
  const seeAt = this.materialField('uSeeThrough');
7685
7918
  this.perFrameFloats[seeAt] = 0;
7686
7919
  this.perFrameFloats[seeAt + 2] = 0;
@@ -7845,6 +8078,10 @@ export class WebGPURenderer {
7845
8078
  else
7846
8079
  pass.drawIndexedIndirect(cut.args, 0);
7847
8080
  }
8081
+ /* And its surface half, for the frame's materials' reflections, with the same survivors. */
8082
+ if (!blend && lit && depthWrite && this.reflectionSurfaceKeeps()) {
8083
+ this.keepReflectionSurface(mesh, key, base, false, false, null, layer, false, false, modelKind, slot, material, instanceBuffer, count, cut === null ? null : cut.args);
8084
+ }
7848
8085
  }
7849
8086
  /* Back to the defaults, unconditionally and for the reason `drawMesh` gives at length: this
7850
8087
  scratch block is cumulative state shared by every draw after this one, and an early return
@@ -8473,6 +8710,20 @@ export class WebGPURenderer {
8473
8710
  warnedNoPointShadowArray = false;
8474
8711
  /** Said once, for the retired ambient dial. */
8475
8712
  warnedEnvironmentAmbient = false;
8713
+ /** The overlay the draws wear, `uOverlay`: see `setSurfaceOverlay`. */
8714
+ overlayFloats = new Float32Array(OVERLAY_FLOATS);
8715
+ /** Its atlas, bound where the refraction copy goes in a twin of each flat group. */
8716
+ overlayMaps = null;
8717
+ /**
8718
+ * Each flat group's twin with the atlas in the refraction slot, by the group and the atlas, built
8719
+ * on the first draw that wears it and dropped wherever `flatBindGroups` is.
8720
+ */
8721
+ overlayGroups = new Map();
8722
+ /** True while a twin is built: what `flatTextures` hands the refraction slot then. */
8723
+ overlayBinding = false;
8724
+ /** True between `bindSeeThrough` and `releaseSeeThrough`: that draw reads the copy, not the atlas. */
8725
+ seeThroughBound = false;
8726
+ warnedOverlayRefraction = false;
8476
8727
  castingWorldIndices = new Int32Array(POINT_SHADOW_POOL).fill(-1);
8477
8728
  resolvedPointShadows = createResolvedPointShadows(MAX_POINT_LIGHTS);
8478
8729
  /** One face's view-projection, rebuilt per face rather than allocated. */
@@ -8823,6 +9074,41 @@ export class WebGPURenderer {
8823
9074
  buildFlatBindGroup() {
8824
9075
  return createFlatBindGroup(this.surface.device, this.bindGroupLayout, this.variant, this.perDraw.buffer, this.perFrame.buffer, this.flatTextures);
8825
9076
  }
9077
+ /**
9078
+ * The open material's flat group, or its twin with the surface overlay's atlas in the refraction
9079
+ * slot where the draw wears one and does not refract. A twin is built from the live material,
9080
+ * which `bindGroup` always is, and kept by the group and the atlas.
9081
+ *
9082
+ * **A draw that refracts, or is glass, keeps the frame's copy** and its overlay reads no image —
9083
+ * the shader asks the same of `uSeeThrough` (`overlayImagesBound`) — said once.
9084
+ */
9085
+ overlayGroup() {
9086
+ const maps = this.overlayMaps;
9087
+ if (maps === null)
9088
+ return this.bindGroup;
9089
+ if (this.seeThroughBound) {
9090
+ if (!this.warnedOverlayRefraction) {
9091
+ this.warnedOverlayRefraction = true;
9092
+ console.warn('WebGPU: a draw that refracts or is glass wears a surface overlay with maps. The frame ' +
9093
+ "copy it reads takes the overlay's slot, so its rim, dissolve and wrinkles read no image.");
9094
+ }
9095
+ return this.bindGroup;
9096
+ }
9097
+ let byMaps = this.overlayGroups.get(this.bindGroup);
9098
+ if (byMaps === undefined) {
9099
+ byMaps = new Map();
9100
+ this.overlayGroups.set(this.bindGroup, byMaps);
9101
+ }
9102
+ let twin = byMaps.get(maps);
9103
+ if (twin === undefined) {
9104
+ this.overlayBinding = true;
9105
+ twin = this.buildFlatBindGroup();
9106
+ this.overlayBinding = false;
9107
+ this.bindGroupBudget.ask();
9108
+ byMaps.set(maps, twin);
9109
+ }
9110
+ return twin;
9111
+ }
8826
9112
  /**
8827
9113
  * The bind group a draw uses, skinned or not.
8828
9114
  *
@@ -8835,8 +9121,9 @@ export class WebGPURenderer {
8835
9121
  * rebuild this every draw.
8836
9122
  */
8837
9123
  flatBindGroupFor(skinned, morphed, deltas) {
9124
+ const base = this.overlayGroup();
8838
9125
  if (!skinned && !morphed)
8839
- return this.bindGroup;
9126
+ return base;
8840
9127
  const palette = skinned ? this.skinPalettes.view(this.skinPaletteSlot) : null;
8841
9128
  /* Slot `k` caches at `k + 1`; index 0 is the morphed draw that is not skinned and has no
8842
9129
  slot of its own. See `skinnedGroups`. */
@@ -8844,13 +9131,16 @@ export class WebGPURenderer {
8844
9131
  /* A skinned group carries the cloth's views, or the stand-in's: see `clothViews`. */
8845
9132
  const cloth = skinned ? this.clothViews() : null;
8846
9133
  if ((this.skinnedGroups[at] ?? null) === null ||
8847
- this.skinnedGroupsBeside[at] !== this.bindGroup ||
9134
+ this.skinnedGroupsBeside[at] !== base ||
8848
9135
  this.skinnedGroupsPalette[at] !== palette ||
8849
9136
  this.skinnedGroupsDeltas[at] !== deltas ||
8850
9137
  this.skinnedGroupsCloth[at] !== (cloth?.particles ?? null) ||
8851
9138
  this.skinnedGroupsClothBinding[at] !== (cloth?.binding ?? null)) {
9139
+ /* Built from the live material, with the overlay's atlas where `base` is its twin. */
9140
+ this.overlayBinding = base !== this.bindGroup;
8852
9141
  this.skinnedGroups[at] = createFlatBindGroup(this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, this.perDraw.buffer, this.perFrame.buffer, this.flatTextures, palette, deltas, cloth);
8853
- this.skinnedGroupsBeside[at] = this.bindGroup;
9142
+ this.overlayBinding = false;
9143
+ this.skinnedGroupsBeside[at] = base;
8854
9144
  this.skinnedGroupsPalette[at] = palette;
8855
9145
  this.skinnedGroupsDeltas[at] = deltas;
8856
9146
  this.skinnedGroupsCloth[at] = cloth?.particles ?? null;
@@ -8917,6 +9207,7 @@ export class WebGPURenderer {
8917
9207
  */
8918
9208
  rebuildFlatGroupsForNewRings() {
8919
9209
  this.flatBindGroups.clear();
9210
+ this.overlayGroups.clear();
8920
9211
  this.flatEnvironment = this.flatTextures('uEnvironment').view;
8921
9212
  this.skinnedGroups.length = 0;
8922
9213
  const albedo = this.albedo;
@@ -8969,6 +9260,7 @@ export class WebGPURenderer {
8969
9260
  */
8970
9261
  rebuildFlatBindGroup() {
8971
9262
  this.flatBindGroups.clear();
9263
+ this.overlayGroups.clear();
8972
9264
  this.flatEnvironment = this.flatTextures('uEnvironment').view;
8973
9265
  if (this.albedo === null &&
8974
9266
  this.normalMap === null &&
@@ -9194,9 +9486,14 @@ export class WebGPURenderer {
9194
9486
  ? (OUTPUT_TRANSFORM_CODE[this.quality.outputTransform] ?? 0)
9195
9487
  : 0;
9196
9488
  f[at('uOutputExposure')] = graded ? this.exposure : 1;
9489
+ f.set(this.filmA, at('uFilmA'));
9490
+ f.set(this.filmB, at('uFilmB'));
9491
+ f[at('uDisplayHeadroom')] = this.headroom;
9197
9492
  f.set(this.reprojection, at('uReprojection'));
9198
9493
  f[at('uMotionStrength')] = motionStrength;
9199
- f[at('uMotionMax')] = MOTION_BLUR_MAX_UV;
9494
+ f[at('uMotionMax')] = this.motionBlurMax;
9495
+ /* Each mover's own motion, where the reconstruction drew its target this frame. */
9496
+ f[at('uObjectMotion')] = this.reconstructing && this.reconMotionView !== null ? 1 : 0;
9200
9497
  f[at('uAoStrength')] = this.frameProjection === null ? 0 : aoStrength;
9201
9498
  /*
9202
9499
  * The occlusion read where this frame's jitter put it (`uAoOffset`): measured from the jittered
@@ -9240,6 +9537,7 @@ export class WebGPURenderer {
9240
9537
  f[at('uGradeSize')] = this.gradeSize;
9241
9538
  /* The lens and the print. See `setVignette` and `setFilmGrain`. */
9242
9539
  f[at('uVignette')] = this.vignette;
9540
+ f.set(this.fringe, at('uFringe'));
9243
9541
  f[at('uAutoExposure')] = adapting ? this.autoExposure : 0;
9244
9542
  f[at('uLocalExposure')] = measuring && localising ? this.localExposure : 0;
9245
9543
  f[at('uGrain')] = this.grain;
@@ -9277,7 +9575,7 @@ export class WebGPURenderer {
9277
9575
  return;
9278
9576
  if (this.aoTargetView === null)
9279
9577
  return;
9280
- this.rushBindGroup = this.buildRushBindGroup('post.rushBindGroup', this.sceneColorView);
9578
+ this.rushBindGroup = this.buildRushBindGroup('post.rushBindGroup', this.sceneColorView, this.sceneColorView);
9281
9579
  /*
9282
9580
  * **And the reconstruction's twin, from the same three call sites.** A colour grade replaces
9283
9581
  * the lookup table and a resize replaces the depth and the occlusion target, and both groups
@@ -9285,11 +9583,13 @@ export class WebGPURenderer {
9285
9583
  * come to read a stale view on exactly the frames a consumer changed something.
9286
9584
  */
9287
9585
  if (this.reconShownView !== null) {
9288
- this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
9586
+ this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView, this.reconMotionView ?? this.reconShownView);
9289
9587
  }
9290
9588
  }
9291
9589
  /** The composite's group over one scene source. See `rebuildRushBindGroup`. */
9292
- buildRushBindGroup(label, sceneView) {
9590
+ buildRushBindGroup(label, sceneView,
9591
+ /** Each mover's own motion, or a stand-in the shader is told not to read. */
9592
+ motionView) {
9293
9593
  const { device } = this.surface;
9294
9594
  if (this.resolvedDepthView === null || this.aoTargetView === null)
9295
9595
  return null;
@@ -9318,6 +9618,7 @@ export class WebGPURenderer {
9318
9618
  and a declared binding still needs a complete texture, as the bloom fallback says. */
9319
9619
  ...entry(RUSH_TEXTURES.uExposureHeld, this.exposurePass?.heldView ?? sceneView),
9320
9620
  ...entry(RUSH_TEXTURES.uExposureLocal, this.exposurePass?.localView ?? sceneView),
9621
+ ...entry(RUSH_TEXTURES.uMotion, motionView),
9321
9622
  ],
9322
9623
  layout: this.rushLayout,
9323
9624
  });
@@ -9574,7 +9875,7 @@ export class WebGPURenderer {
9574
9875
  this.reconSharpenGroups[write] = build(`recon.sharpen${String(write)}`, target, this.reconShownView, records, nextRecords, swings, nextSwings);
9575
9876
  }
9576
9877
  /* The composite's own group, over the resolved picture instead of the scene target. */
9577
- this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
9878
+ this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView, this.reconMotionView ?? this.reconShownView);
9578
9879
  /* And the scene's, because `ensureComposite` built it before this one existed. */
9579
9880
  this.rebuildRushBindGroup();
9580
9881
  /* And the bloom's, which blooms the resolved picture: see `rebuildBloomGroups`. */
@@ -9631,7 +9932,7 @@ export class WebGPURenderer {
9631
9932
  drawsLate() {
9632
9933
  return (this.latePass !== null &&
9633
9934
  this.oitEncoder === null &&
9634
- routesLate(this.reconstructing, this.reflectionPassActive, this.probePassActive, this.framePresented));
9935
+ routesLate(this.reconstructing, this.reflectionPassActive, this.probePassActive || this.capturePassActive, this.framePresented));
9635
9936
  }
9636
9937
  /**
9637
9938
  * The command a blended draw records into: the late pool while reconstructing, the frame graph
@@ -9663,7 +9964,7 @@ export class WebGPURenderer {
9663
9964
  if (!this.reconstructing || late === null || shown === null || depth === null)
9664
9965
  return;
9665
9966
  const oitSet = this.oitActive && this.translucentQueue.length > 0;
9666
- if (late.pending === 0 && !oitSet)
9967
+ if (late.pending === 0 && !oitSet && this.latePasses.length === 0)
9667
9968
  return;
9668
9969
  const width = Math.max(1, this.surface.canvas.width);
9669
9970
  const height = Math.max(1, this.surface.canvas.height);
@@ -9675,25 +9976,64 @@ export class WebGPURenderer {
9675
9976
  this.refractSnapshotTaken = true;
9676
9977
  }
9677
9978
  this.lateRefractWanted = false;
9678
- if (late.pending === 0)
9679
- return;
9680
- const pass = encoder.beginRenderPass({
9681
- label: 'recon.late',
9682
- timestampWrites: this.gpuTimer.writesFor(),
9683
- colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
9684
- depthStencilAttachment: {
9685
- view: late.depthView(width, height),
9686
- depthLoadOp: 'load',
9687
- /* Kept: the order-independent set replays against it after this pass. */
9688
- depthStoreOp: 'store',
9689
- },
9690
- });
9691
- late.replay(pass, this.issueBound);
9692
- pass.end();
9979
+ if (late.pending > 0) {
9980
+ const pass = encoder.beginRenderPass({
9981
+ label: 'recon.late',
9982
+ timestampWrites: this.gpuTimer.writesFor(),
9983
+ colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
9984
+ depthStencilAttachment: {
9985
+ view: late.depthView(width, height),
9986
+ depthLoadOp: 'load',
9987
+ /* Kept: the order-independent set replays against it after this pass. */
9988
+ depthStoreOp: 'store',
9989
+ },
9990
+ });
9991
+ late.replay(pass, this.issueBound);
9992
+ pass.end();
9993
+ }
9994
+ this.runLatePasses(encoder, shown, late.depthView(width, height));
9995
+ }
9996
+ /**
9997
+ * The registered passes `drawPass` queued for after the upscale: each handed a fresh copy of the
9998
+ * reconstructed picture with every late draw in it, in the output-size texture the late
9999
+ * refraction keeps (its own reads are done by now: they were recorded before this copy), then
10000
+ * drawn over the picture in a pass of its own.
10001
+ */
10002
+ runLatePasses(encoder, shown, depth) {
10003
+ const copy = this.refractSnapshot;
10004
+ const source = this.reconShown;
10005
+ for (const handle of this.latePasses) {
10006
+ const definition = passAt(this.passes, handle);
10007
+ if (definition === undefined)
10008
+ continue;
10009
+ if (copy !== null && source !== null) {
10010
+ encoder.copyTextureToTexture({ texture: source }, { texture: copy }, [
10011
+ copy.width,
10012
+ copy.height,
10013
+ ]);
10014
+ }
10015
+ const pass = encoder.beginRenderPass({
10016
+ label: 'recon.latePass',
10017
+ timestampWrites: this.gpuTimer.writesFor(),
10018
+ colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
10019
+ depthStencilAttachment: { view: depth, depthLoadOp: 'load', depthStoreOp: 'store' },
10020
+ });
10021
+ const context = this.passContext;
10022
+ context.pass = pass;
10023
+ context.outputTransform = this.gradeCode();
10024
+ context.outputExposure = this.gradeExposure();
10025
+ context.jitter = this.latePassJitter;
10026
+ context.format = RECON_HISTORY_FORMAT;
10027
+ context.depthFormat = DEPTH_FORMAT;
10028
+ context.samples = 1;
10029
+ definition.draw(context);
10030
+ context.jitter = this.passJitter;
10031
+ pass.end();
10032
+ }
9693
10033
  }
9694
10034
  /** Whether this draw lands in the reconstructed picture. See `lateRouting.ts`. */
9695
10035
  tracksMotionNow() {
9696
- return tracksMotion(this.reconstructing, this.reflectionPassActive, this.probePassActive, this.framePresented);
10036
+ return tracksMotion(this.reconstructing, this.reflectionPassActive, this.probePassActive || this.capturePassActive, this.framePresented);
9697
10037
  }
9698
10038
  /**
9699
10039
  * What this draw's motion is, recorded for the motion pass.
@@ -10056,7 +10396,10 @@ export class WebGPURenderer {
10056
10396
  * other is the disagreement the parity rule exists to prevent. Said once rather than dropped.
10057
10397
  */
10058
10398
  runReflections(encoder) {
10059
- if (this.reflectionQueue.length === 0)
10399
+ const frameReflections = this.frameReflections;
10400
+ const surfaceHalf = this.reflectionSurface;
10401
+ const materials = frameReflections !== null && surfaceHalf !== null;
10402
+ if (this.reflectionQueue.length === 0 && !materials)
10060
10403
  return;
10061
10404
  const scene = this.sceneColorView;
10062
10405
  const reflection = this.ssrReflectionView;
@@ -10094,14 +10437,70 @@ export class WebGPURenderer {
10094
10437
  f.set(surface.axis, base + at('uSsrAxis'));
10095
10438
  f.set(surface.tint, base + at('uSsrTint'));
10096
10439
  f[base + at('uSsrStrength')] = surface.strength;
10440
+ f[base + at('uSsrFresnel')] = surface.fresnel ? 1 : 0;
10441
+ f[base + at('uSsrFresnel') + 1] = surface.roughness;
10097
10442
  f[base + at('uSsrFacingCos')] = surface.facingCos;
10098
10443
  f[base + at('uSsrReach')] = surface.reachM;
10099
10444
  f[base + at('uSsrThickness')] = surface.thicknessM;
10100
10445
  f[base + at('uSsrSteps')] = surface.steps;
10101
10446
  f[base + at('uSsrEdgeFade')] = REFLECTION_EDGE_FADE;
10447
+ f[base + at('uSsrMaterial')] = 0;
10102
10448
  slot++;
10103
10449
  });
10450
+ const regions = slot;
10451
+ /* The frame's materials' trace takes the slot after the surfaces': the box, its axis and its
10452
+ strength unread, the march and the ceiling its own. See `frameReflections.ts`. */
10453
+ if (materials) {
10454
+ const base = (slot * SSR_SLOT) / 4;
10455
+ f.set(this.ssrDepthToWorld, base + at('uSsrDepthToWorld'));
10456
+ f.set(this.ssrViewProj, base + at('uSsrViewProj'));
10457
+ f.set(this.frameEye, base + at('uSsrEye'));
10458
+ f.fill(0, base + at('uSsrAxis'), base + at('uSsrAxis') + 3);
10459
+ f.fill(1, base + at('uSsrTint'), base + at('uSsrTint') + 3);
10460
+ f[base + at('uSsrStrength')] = 1;
10461
+ f[base + at('uSsrFresnel')] = 0;
10462
+ f[base + at('uSsrFacingCos')] = -1;
10463
+ f[base + at('uSsrReach')] = frameReflections.reachM;
10464
+ f[base + at('uSsrThickness')] = frameReflections.thicknessM;
10465
+ f[base + at('uSsrSteps')] = frameReflections.steps;
10466
+ f[base + at('uSsrEdgeFade')] = REFLECTION_EDGE_FADE;
10467
+ f[base + at('uSsrMaterial')] = 1;
10468
+ f[base + at('uSsrMaxRoughness')] = frameReflections.maxRoughness;
10469
+ slot++;
10470
+ }
10104
10471
  device.queue.writeBuffer(this.ssrUniforms, 0, this.ssrStaging, 0, slot * SSR_SLOT);
10472
+ const tracePipeline = postPipeline(this.pipelines, device, this.ssrLayout, 'post.ssrTrace', SSR_TRACE_FRAG_WGSL, this.pipelines.format, PREMULTIPLIED_OVER_BLEND);
10473
+ if (materials && this.depthView !== null && this.ssrMaterialGroup !== null) {
10474
+ /* The frame's opaque lit draws again, into what the trace and the resolve read. */
10475
+ surfaceHalf.draw(encoder, this.depthView, this.issueBound);
10476
+ const trace = encoder.beginRenderPass({
10477
+ label: 'post.ssr.materialTrace',
10478
+ colorAttachments: [
10479
+ { view: reflection, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
10480
+ ],
10481
+ });
10482
+ trace.setPipeline(tracePipeline);
10483
+ trace.setBindGroup(0, group, [regions * SSR_SLOT]);
10484
+ trace.draw(3);
10485
+ trace.end();
10486
+ /* The blur's texel and its radius at roughness 1, as a share of the frame's height. */
10487
+ const m = this.ssrMaterialFloats;
10488
+ const blurAt = this.postField(SSR_MATERIAL_FIELDS, 'uSsrBlur');
10489
+ m[blurAt] = 1 / Math.max(width, 1);
10490
+ m[blurAt + 1] = 1 / Math.max(height, 1);
10491
+ m[blurAt + 2] = frameReflections.blur * height;
10492
+ device.queue.writeBuffer(this.ssrMaterialUniforms, 0, m);
10493
+ const resolve = encoder.beginRenderPass({
10494
+ label: 'post.ssr.materialResolve',
10495
+ colorAttachments: [{ view: scene, loadOp: 'load', storeOp: 'store' }],
10496
+ });
10497
+ resolve.setPipeline(postPipeline(this.pipelines, device, this.ssrMaterialLayout, 'post.ssrMaterialResolve', SSR_MATERIAL_RESOLVE_FRAG_WGSL, this.pipelines.format, FRAME_REFLECTION_BLEND));
10498
+ resolve.setBindGroup(0, this.ssrMaterialGroup, [0]);
10499
+ resolve.draw(3);
10500
+ resolve.end();
10501
+ }
10502
+ if (regions === 0)
10503
+ return;
10105
10504
  /* Cleared whole and then scissored per surface: the resolve reads every pixel, and one left
10106
10505
  from the last frame would composite that reflection over this frame's scene. */
10107
10506
  const trace = encoder.beginRenderPass({
@@ -10110,7 +10509,7 @@ export class WebGPURenderer {
10110
10509
  { view: reflection, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
10111
10510
  ],
10112
10511
  });
10113
- trace.setPipeline(postPipeline(this.pipelines, device, this.ssrLayout, 'post.ssrTrace', SSR_TRACE_FRAG_WGSL, this.pipelines.format, PREMULTIPLIED_OVER_BLEND));
10512
+ trace.setPipeline(tracePipeline);
10114
10513
  slot = 0;
10115
10514
  this.reflectionQueue.replay((surface) => {
10116
10515
  const index = slot++;
@@ -10654,7 +11053,7 @@ export class WebGPURenderer {
10654
11053
  * glass, and a frame that refracts nothing breaks no pass at all.
10655
11054
  */
10656
11055
  takeRefractSnapshot() {
10657
- if (this.reflectionPassActive || this.probePassActive)
11056
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive)
10658
11057
  return false;
10659
11058
  if (this.refractSnapshotTaken)
10660
11059
  return true;
@@ -10737,10 +11136,55 @@ export class WebGPURenderer {
10737
11136
  !this.skinScattered &&
10738
11137
  !this.reflectionPassActive &&
10739
11138
  !this.probePassActive &&
11139
+ !this.capturePassActive &&
11140
+ !this.framePresented &&
11141
+ !this.overlayActive &&
11142
+ this.oitMode === 'none');
11143
+ }
11144
+ /** Whether a draw now is the frame's own, and so has a surface half to keep. */
11145
+ reflectionSurfaceKeeps() {
11146
+ return (this.reflectionSurface !== null &&
11147
+ !this.reflectionPassActive &&
11148
+ !this.probePassActive &&
11149
+ !this.capturePassActive &&
10740
11150
  !this.framePresented &&
10741
11151
  !this.overlayActive &&
10742
11152
  this.oitMode === 'none');
10743
11153
  }
11154
+ /**
11155
+ * An opaque lit draw's surface half, kept for the frame's materials' reflections: the draw again,
11156
+ * with the same slots, buffers and bind group, through the pipeline with `REFLECTION_SURFACE` on —
11157
+ * which writes what the pixel reflects of its environment into the two targets
11158
+ * `reflectionSurfacePass.ts` holds and tests for the depth its frame draw wrote. Replayed when the
11159
+ * frame's pass has ended, before the trace reads them. See `shaders/flat/reflectionSurface.ts`.
11160
+ */
11161
+ keepReflectionSurface(mesh, frameKey, base, skinned, morphed, deltas, layer, eight, clothed, modelKind, slot, material, instanceBuffer, instances, indirect) {
11162
+ const surface = this.reflectionSurface;
11163
+ const present = this.meshPresent.get(base);
11164
+ if (surface === null || present === undefined)
11165
+ return;
11166
+ const key = `${frameKey}|rs`;
11167
+ const pipeline = this.pipelines.peek(key) ??
11168
+ flatPipeline(this.pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, false, skinned, morphed, true, layer, instanceBuffer !== null, 'none', this.materialDoubleSided, false, eight, clothed, modelKind, 'whole', true);
11169
+ const command = surface.take();
11170
+ command.pipeline = pipeline;
11171
+ command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
11172
+ command.offsetA = slot;
11173
+ command.offsetB = material;
11174
+ command.offsetCount = 2;
11175
+ let index = 0;
11176
+ for (; index < mesh.vertexBuffers.length; index++) {
11177
+ command.vertexBuffers[index] = mesh.vertexBuffers[index];
11178
+ }
11179
+ if (instanceBuffer !== null)
11180
+ command.vertexBuffers[index++] = instanceBuffer;
11181
+ command.vertexCount = index;
11182
+ command.indexBuffer = mesh.indexBuffer;
11183
+ command.indexed = true;
11184
+ command.count = mesh.indexCount;
11185
+ command.instances = instances;
11186
+ command.indirect = indirect;
11187
+ }
10744
11188
  /**
10745
11189
  * A skin draw's diffuse and colour halves, kept for the spread: the frame's draw again, twice,
10746
11190
  * with the same slots, buffers and bind group, through the pipelines that write the diffuse's
@@ -10788,11 +11232,19 @@ export class WebGPURenderer {
10788
11232
  const skin = this.skinScatter;
10789
11233
  if (skin === null || this.skinScattered)
10790
11234
  return;
11235
+ /*
11236
+ * **Not the frame's picture, so not the frame's moment**: a blended draw in a mirror, a probe
11237
+ * bake or a scene capture lands over none of the frame's skin. Latching there switched the
11238
+ * spread off for the frame, so a capture with a glowing sign in it left every face drawn after
11239
+ * it flat. `scripts/capture-check.mjs`.
11240
+ */
11241
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive)
11242
+ return;
10791
11243
  /* Latched whether or not skin was kept: a skin drawn after this point is the whole surface. */
10792
11244
  this.skinScattered = true;
10793
11245
  if (!skin.pending)
10794
11246
  return;
10795
- if (this.reflectionPassActive || this.probePassActive || this.oitReplaying)
11247
+ if (this.oitReplaying)
10796
11248
  return;
10797
11249
  const encoder = this.encoder;
10798
11250
  const depth = this.depthView;
@@ -10851,7 +11303,7 @@ export class WebGPURenderer {
10851
11303
  * own terms: `resolvedDepthView` holds the *frame's* depth, and clamping a mirrored beam
10852
11304
  * against it would stop it at geometry that is not in front of it.
10853
11305
  */
10854
- if (this.reflectionPassActive)
11306
+ if (this.reflectionPassActive || this.capturePassActive)
10855
11307
  return false;
10856
11308
  if (this.resolvedDepthView === null)
10857
11309
  return false;
@@ -10907,6 +11359,66 @@ export class WebGPURenderer {
10907
11359
  this.volumeDepthTaken = true;
10908
11360
  return true;
10909
11361
  }
11362
+ /** The colour copy a registered pass reads, at the scene's size and format. See `passColour`. */
11363
+ allocatePassColour() {
11364
+ const source = this.sceneColor;
11365
+ if (source === null || this.passColour !== null)
11366
+ return;
11367
+ this.passColour = this.surface.device.createTexture({
11368
+ label: 'pass.sceneColor',
11369
+ size: [source.width, source.height],
11370
+ format: this.pipelines.format,
11371
+ usage: 0x4 | 0x2, // TEXTURE_BINDING | COPY_DST
11372
+ });
11373
+ this.passColourView = this.passColour.createView();
11374
+ }
11375
+ /**
11376
+ * The frame's colour so far, into `passColour`, for a registered pass that reads it: the
11377
+ * boundary `takeRefractSnapshot` describes — flush, end the pass, copy what its end resolved,
11378
+ * reopen with `load` — and refused where that one is, in a mirror, a probe bake or a capture,
11379
+ * whose pictures are not the frame's. **Not latched**, unlike refraction's: each reading pass
11380
+ * sees the passes drawn before it, at a full-frame copy each.
11381
+ */
11382
+ takePassColour() {
11383
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive)
11384
+ return;
11385
+ const copy = this.passColour;
11386
+ const source = this.sceneColor;
11387
+ const encoder = this.encoder;
11388
+ if (copy === null || source === null || encoder === null || this.pass === null)
11389
+ return;
11390
+ if (this.quality.frameGraph)
11391
+ this.flushGraph();
11392
+ this.pass.end();
11393
+ encoder.copyTextureToTexture({ texture: source }, { texture: copy }, [copy.width, copy.height]);
11394
+ this.pass = this.reopenFramePass('pass.colourTaken');
11395
+ }
11396
+ /**
11397
+ * The frame's pass again after a copy between passes, loading everything already drawn: the same
11398
+ * attachments the copy's caller ended, with nothing cleared.
11399
+ */
11400
+ reopenFramePass(label) {
11401
+ const encoder = this.encoder;
11402
+ const target = this.compositeTarget();
11403
+ if (encoder === null || target === null)
11404
+ return null;
11405
+ const multisampled = this.compositeMsaa() !== null;
11406
+ return encoder.beginRenderPass({
11407
+ label,
11408
+ timestampWrites: this.gpuTimer.writesFor(),
11409
+ colorAttachments: [
11410
+ {
11411
+ view: this.compositeMsaa() ?? target,
11412
+ resolveTarget: multisampled ? target : undefined,
11413
+ loadOp: 'load',
11414
+ storeOp: resolvedStoreOp(multisampled, false, this.quality.discardResolvedAttachments),
11415
+ },
11416
+ ],
11417
+ depthStencilAttachment: this.depthView === null
11418
+ ? undefined
11419
+ : { view: this.depthView, depthLoadOp: 'load', depthStoreOp: 'store' },
11420
+ });
11421
+ }
10910
11422
  /**
10911
11423
  * The pass to record into, reopening one on the canvas if the frame has already gone.
10912
11424
  *
@@ -11088,6 +11600,8 @@ export class WebGPURenderer {
11088
11600
  /* And a march reads the same copy, for the same reason and with the same failure if it is
11089
11601
  discarded: a depth of zero is the far plane reversed, so every ray finds nothing. */
11090
11602
  this.reflectionQueue.length > 0 ||
11603
+ /* And the frame's materials' reflections, whose surface half tests against the depth too. */
11604
+ this.reflectionSurface !== null ||
11091
11605
  /* And the medium's own march, which stops where the frame's depth says a surface is. Left
11092
11606
  out of this, every ray runs to `maxDistance` through the walls and the fog is drawn on
11093
11607
  top of the room rather than in it. */
@@ -11408,8 +11922,9 @@ export class WebGPURenderer {
11408
11922
  * any future branch added to one belongs in the other.
11409
11923
  */
11410
11924
  targetPipelines() {
11411
- if (this.probePassActive || this.reflectionPassActive)
11925
+ if (this.probePassActive || this.reflectionPassActive || this.capturePassActive) {
11412
11926
  return this.pipelines;
11927
+ }
11413
11928
  return this.overlayActive || this.framePresented ? this.overlayPipelines : this.pipelines;
11414
11929
  }
11415
11930
  /** End and submit a reopened pass. Safe to call when there is none. */
@@ -11903,6 +12418,9 @@ export class WebGPURenderer {
11903
12418
  const reads = maskOf(...(definition.reads ?? []));
11904
12419
  this.passReads.set(handle, reads);
11905
12420
  this.passReadsUnion |= reads;
12421
+ /* The colour copy a reader needs, now rather than at its first draw: see `passColour`. */
12422
+ if ((reads & COLOR_SNAPSHOT) !== 0 && this.passColour === null)
12423
+ this.allocatePassColour();
11906
12424
  if (definition.prepare !== undefined)
11907
12425
  this.preparingPasses++;
11908
12426
  definition.init?.(this.passDevice());
@@ -11922,6 +12440,8 @@ export class WebGPURenderer {
11922
12440
  environment: null,
11923
12441
  distanceField: null,
11924
12442
  jitter: this.passJitter,
12443
+ sceneDepth: null,
12444
+ sceneColor: null,
11925
12445
  };
11926
12446
  /**
11927
12447
  * The object handed to a pass as `PrepareContext.environment`, rebuilt only when it changes.
@@ -12007,6 +12527,15 @@ export class WebGPURenderer {
12007
12527
  * than merely what happens.
12008
12528
  */
12009
12529
  this.prepareContext.distanceField = this.fieldComposer?.field ?? null;
12530
+ /* Settled for the frame: `ensureComposite` has run, so the view stays until the pass draws. */
12531
+ this.prepareContext.sceneDepth = this.resolvedDepthView;
12532
+ /*
12533
+ * Under a reconstruction a colour reader draws after the upscale (see `drawPass`), so it reads
12534
+ * the output-size copy the late refraction keeps rather than the render's.
12535
+ */
12536
+ this.prepareContext.sceneColor = this.reconstructionWanted
12537
+ ? this.refractSnapshotView
12538
+ : this.passColourView;
12010
12539
  for (const definition of this.passes.definitions) {
12011
12540
  definition?.prepare?.(this.prepareContext);
12012
12541
  }
@@ -12024,6 +12553,35 @@ export class WebGPURenderer {
12024
12553
  const definition = passAt(this.passes, handle);
12025
12554
  if (definition === undefined)
12026
12555
  return;
12556
+ const reads = this.passReads.get(handle) ?? 0;
12557
+ /*
12558
+ * **Under a reconstruction, a pass that reads the frame's colour draws after the blended draws
12559
+ * do.** Those land after the upscale, at the output's size, so a pass drawn among them at the
12560
+ * render's would read a frame without them and then be covered by them — a full-screen effect
12561
+ * with the hit sparks drawn sharp over it. Queued instead, and drawn by `runLate` over the
12562
+ * reconstructed picture, after every late draw, whatever order it was asked in.
12563
+ */
12564
+ if ((reads & COLOR_SNAPSHOT) !== 0 && this.drawsLate()) {
12565
+ this.latePasses.push(handle);
12566
+ return;
12567
+ }
12568
+ /*
12569
+ * A pass that reads the depth copy gets it with everything drawn so far: the copy is taken
12570
+ * here, once a frame, as a soft-edged sprite takes it. Before `openPass` below, because taking
12571
+ * it ends the frame's pass and opens another.
12572
+ */
12573
+ if ((reads & DEPTH_SNAPSHOT) !== 0 && !this.framePresented && this.openPass() !== null) {
12574
+ this.takeVolumeDepth();
12575
+ }
12576
+ /*
12577
+ * And one that reads the colour gets it afresh, with everything drawn before it — a full-screen
12578
+ * pass drawn last reads the whole frame and an effect drawn mid-frame reads the frame so far.
12579
+ * A copy a reading pass, where the depth above is one a frame: what a colour reader draws is
12580
+ * itself part of what the next one should see.
12581
+ */
12582
+ if ((reads & COLOR_SNAPSHOT) !== 0 && !this.framePresented && this.openPass() !== null) {
12583
+ this.takePassColour();
12584
+ }
12027
12585
  if (!this.quality.frameGraph) {
12028
12586
  const pass = this.openPass();
12029
12587
  if (pass === null)
@@ -12031,7 +12589,7 @@ export class WebGPURenderer {
12031
12589
  definition.draw(this.fillPassContext(pass));
12032
12590
  return;
12033
12591
  }
12034
- recordNode(this.arena, VERB_PASS, this.passReads.get(handle) ?? 0, this.currentTarget(), handle, 0);
12592
+ recordNode(this.arena, VERB_PASS, reads, this.currentTarget(), handle, 0);
12035
12593
  if (this.framePresented)
12036
12594
  this.queueOverlayFlush();
12037
12595
  }
@@ -12140,8 +12698,8 @@ export class WebGPURenderer {
12140
12698
  bindClusters(camera, env, f, i, at) {
12141
12699
  const binner = this.clusterBinner;
12142
12700
  i[at('uClustered')] = binner === null ? 0 : 1;
12143
- if (binner === null)
12144
- return;
12701
+ /* Written whether or not the lights are clustered: a surface overlay's rim finds its place on
12702
+ the screen through the same two (\`overlayScreen\`). */
12145
12703
  const p = camera.projection;
12146
12704
  const tanHalfFovY = 1 / (p[5] ?? 1);
12147
12705
  const aspect = (p[5] ?? 1) / (p[0] ?? 1);
@@ -12157,6 +12715,8 @@ export class WebGPURenderer {
12157
12715
  f[frustumAt + 2] = tanHalfFovY;
12158
12716
  f[frustumAt + 3] = aspect;
12159
12717
  f.set(camera.view, at('uView'));
12718
+ if (binner === null)
12719
+ return;
12160
12720
  /* The rectangles past the fixed arm's go into the table. See `ClusterLightSet.areas`. */
12161
12721
  const set = fillClusterLightSet(env, this.clusterLights, this.shadedAreaLights);
12162
12722
  /* A slot is read only under `POINT_SHADOWS`; see `lightHasFixture`. */
@@ -12457,6 +13017,43 @@ export class WebGPURenderer {
12457
13017
  }
12458
13018
  /** This frame's exposure into the tone curve. Starts at the profile's own. */
12459
13019
  exposure;
13020
+ /**
13021
+ * The `filmic` curve every later frame is graded through, until it is set again. See
13022
+ * `RendererApi.setFilmicCurve`; the constants are solved here once, not per frame or per pixel.
13023
+ */
13024
+ setFilmicCurve(curve) {
13025
+ this.filmCurve = curve;
13026
+ if (resolveFilmicCurve(curve, this.filmConstants, this.headroom) && !this.warnedFilmClamped) {
13027
+ this.warnedFilmClamped = true;
13028
+ console.warn(FILMIC_CLAMPED);
13029
+ }
13030
+ }
13031
+ warnedFilmClamped = false;
13032
+ /** The curve last set, so a new headroom can solve it again. */
13033
+ filmCurve = DEFAULT_FILMIC_CURVE;
13034
+ /** The display's peak over paper white; 1 on a standard range. See `setDisplayLuminance`. */
13035
+ headroom = 1;
13036
+ /** The range the frame goes out in. See `RendererApi.displayRange`. */
13037
+ get displayRange() {
13038
+ return this.surface.displayRange;
13039
+ }
13040
+ /** Why it is that range, in words. */
13041
+ get displayRangeReason() {
13042
+ return this.surface.displayRangeReason;
13043
+ }
13044
+ /**
13045
+ * How bright paper white and the display's peak are, in any one unit (nits, usually), held until
13046
+ * changed: their ratio is how far above white the highlights may run on a high range display,
13047
+ * and nothing changes on a standard one. See `RendererApi.setDisplayLuminance`.
13048
+ */
13049
+ setDisplayLuminance(paperWhite, peak) {
13050
+ this.headroom = displayHeadroom(this.surface.displayRange, paperWhite, peak);
13051
+ resolveFilmicCurve(this.filmCurve, this.filmConstants, this.headroom);
13052
+ }
13053
+ /** The curve's eight constants, and the two views the composite uploads them through. */
13054
+ filmConstants = new Float32Array(FILMIC_CONSTANTS);
13055
+ filmA = this.filmConstants.subarray(0, 4);
13056
+ filmB = this.filmConstants.subarray(4, 8);
12460
13057
  /*
12461
13058
  * -----------------------------------------------------------------------------------------
12462
13059
  * The three screen effects, which this backend cannot yet honour — and says so.
@@ -12500,6 +13097,8 @@ export class WebGPURenderer {
12500
13097
  veilColor = [0, 0, 0];
12501
13098
  veilAlpha = 0;
12502
13099
  vignette = 0;
13100
+ /** The lens's colour fringe: red's and green's pull and where it begins. See `fringe.ts`. */
13101
+ fringe = new Float32Array(FRINGE_FLOATS);
12503
13102
  grain = 0;
12504
13103
  grainSeed = 0;
12505
13104
  warnedLookWithoutComposite = false;
@@ -12521,6 +13120,20 @@ export class WebGPURenderer {
12521
13120
  }
12522
13121
  this.vignette = clampVignette(strength);
12523
13122
  }
13123
+ /**
13124
+ * The lens's colour fringe, held until changed: `intensity` is a percentage, 0 is none, and red
13125
+ * and green are pulled toward the centre by their wavelength's distance from blue, growing from
13126
+ * `start` — a share of the half-frame, 0 the centre — to the whole of it at the edge. Applied to
13127
+ * scene light at the composite's first read. Needs `screenEffects`, and says so once rather than
13128
+ * doing nothing. See `shaders/fringe.ts`.
13129
+ */
13130
+ setChromaticAberration(intensity, start = 0) {
13131
+ if (!this.hasComposite) {
13132
+ this.warnLookWithoutComposite(intensity);
13133
+ return;
13134
+ }
13135
+ resolveFringe(intensity, start, this.fringe);
13136
+ }
12524
13137
  /**
12525
13138
  * Film grain, held until changed: an amplitude in display values (0 is none, 0.03 is a fine
12526
13139
  * grain) and this frame's seed. **The seed is the caller's**, because the engine takes time from
@@ -12654,9 +13267,26 @@ export class WebGPURenderer {
12654
13267
  setSpeedRush(strength) {
12655
13268
  this.rushStrength = Math.min(Math.max(strength, 0), 1);
12656
13269
  }
12657
- /** How much of the frame's camera motion blur to apply, 0 to 1. */
12658
- setCameraMotionBlur(scale) {
13270
+ /**
13271
+ * How much of the frame's motion blur to apply, 0 to 1, and the longest smear as a share of the
13272
+ * frame, both held until changed. See `RendererApi.setCameraMotionBlur`.
13273
+ */
13274
+ setCameraMotionBlur(scale, maxShare = MOTION_BLUR_MAX_UV) {
12659
13275
  this.motionBlurScale = Math.min(Math.max(scale, 0), 1);
13276
+ this.motionBlurMax = clampMotionShare(maxShare);
13277
+ }
13278
+ /** The longest smear, as a share of the frame. See `setCameraMotionBlur`. */
13279
+ motionBlurMax = MOTION_BLUR_MAX_UV;
13280
+ warnedObjectBlur = false;
13281
+ /**
13282
+ * Said once: a draw named where it was last frame while the frame blurs, and this frame keeps no
13283
+ * motion target to blur it by — the camera's motion alone smears it. See `OBJECT_BLUR_UNKEPT`.
13284
+ */
13285
+ warnObjectBlur() {
13286
+ if (this.warnedObjectBlur)
13287
+ return;
13288
+ this.warnedObjectBlur = true;
13289
+ console.warn(OBJECT_BLUR_UNKEPT);
12660
13290
  }
12661
13291
  /**
12662
13292
  * The camera has cut: this frame is a new shot, not the next moment of the last one.
@@ -13203,18 +13833,19 @@ export class WebGPURenderer {
13203
13833
  * `bindMeshPass` and `drawMesh`, which record into whatever `this.pass` is. That is the same
13204
13834
  * arrangement `bakeFace` uses for a point light's cube, and it is why a bake must not be
13205
13835
  * opened inside a frame's pass by a caller who then expects the frame to continue.
13206
- */
13207
- const savedPass = this.pass;
13208
- /*
13836
+ *
13209
13837
  * **A bake is a boundary, and boundaries flush.** The mirror says why in its own words: what
13210
13838
  * has been recorded and not yet replayed belongs to the pass that is about to end, and leaving
13211
13839
  * it replays it into whichever pass opens next — a draw in the wrong target rather than a
13212
13840
  * missing one. This is the fourth such boundary in the backend and the only one that was never
13213
13841
  * given the treatment, which is what let a bake carry the frame's pending draws into a cube
13214
- * face.
13842
+ * face. **The pass is saved after the flush**, which opens it where none was: saved before,
13843
+ * the null given back left the flushed pass open and the frame's encoder locked. See
13844
+ * `captureScene`.
13215
13845
  */
13216
13846
  if (this.quality.frameGraph)
13217
13847
  this.flushGraph();
13848
+ const savedPass = this.pass;
13218
13849
  const savedEncoder = this.encoder;
13219
13850
  /*
13220
13851
  * **The rings are marked here and given back after the submit below**, because this bake is its
@@ -13799,6 +14430,7 @@ export class WebGPURenderer {
13799
14430
  this.sceneColor,
13800
14431
  this.sceneColorMsaa,
13801
14432
  this.refractSnapshot,
14433
+ this.passColour,
13802
14434
  this.resolvedDepth,
13803
14435
  this.aoTarget,
13804
14436
  this.aoScratch,
@@ -13864,6 +14496,10 @@ export class WebGPURenderer {
13864
14496
  usage: 0x4 | 0x2 | 0x10, // TEXTURE_BINDING | COPY_DST | RENDER_ATTACHMENT
13865
14497
  });
13866
14498
  this.refractSnapshotView = this.refractSnapshot.createView();
14499
+ this.passColour = null;
14500
+ this.passColourView = null;
14501
+ if ((this.passReadsUnion & COLOR_SNAPSHOT) !== 0)
14502
+ this.allocatePassColour();
13867
14503
  this.refractMips = new SnapshotMips(device, this.pipelines, this.refractSnapshot, reconstructed ? RECON_HISTORY_FORMAT : format);
13868
14504
  /*
13869
14505
  * **Every cached flat bind group is stale the moment this view changes**, and the failure is
@@ -14079,6 +14715,10 @@ export class WebGPURenderer {
14079
14715
  usage: USAGE,
14080
14716
  });
14081
14717
  this.ssrReflectionView = this.ssrReflection.createView();
14718
+ /* The frame's materials, at the scene's size; a stand-in where nothing traces them. */
14719
+ this.reflectionSurface?.size(width, height);
14720
+ const probeMap = this.reflectionSurface?.probeView ?? this.blankView;
14721
+ const tintMap = this.reflectionSurface?.tintView ?? this.blankView;
14082
14722
  this.ssrBindGroup =
14083
14723
  this.resolvedDepthView === null || this.sceneColorView === null
14084
14724
  ? null
@@ -14092,8 +14732,23 @@ export class WebGPURenderer {
14092
14732
  },
14093
14733
  ...entry(SSR_TEXTURES.uSsrDepth, this.resolvedDepthView, true),
14094
14734
  ...entry(SSR_TEXTURES.uSsrScene, this.sceneColorView),
14735
+ ...entry(SSR_TEXTURES.uSsrProbeMap, probeMap),
14736
+ ...entry(SSR_TEXTURES.uSsrTintMap, tintMap),
14095
14737
  ],
14096
14738
  });
14739
+ this.ssrMaterialGroup = device.createBindGroup({
14740
+ label: 'post.ssrMaterialGroup',
14741
+ layout: this.ssrMaterialLayout,
14742
+ entries: [
14743
+ {
14744
+ binding: SSR_MATERIAL_UNIFORMS,
14745
+ resource: { buffer: this.ssrMaterialUniforms, size: SSR_MATERIAL_SIZE },
14746
+ },
14747
+ ...entry(SSR_MATERIAL_TEXTURES.uSsrReflection, this.ssrReflectionView),
14748
+ ...entry(SSR_MATERIAL_TEXTURES.uSsrProbeMap, probeMap),
14749
+ ...entry(SSR_MATERIAL_TEXTURES.uSsrTintMap, tintMap),
14750
+ ],
14751
+ });
14097
14752
  this.ssrResolveGroup = device.createBindGroup({
14098
14753
  label: 'post.ssrResolveGroup',
14099
14754
  layout: this.ssrResolveLayout,