@driftengine/core 4.4.2 → 4.6.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 (339) hide show
  1. package/CHANGELOG.json +97 -0
  2. package/README.md +1 -1
  3. package/dist/geometry/boxCsg.d.ts +28 -0
  4. package/dist/geometry/boxCsg.js +139 -0
  5. package/dist/geometry/csg.d.ts +36 -0
  6. package/dist/geometry/csg.js +329 -0
  7. package/dist/geometry/meshBuilder.js +13 -0
  8. package/dist/geometry/meshConcat.js +1 -0
  9. package/dist/geometry/pixelFont.js +2 -0
  10. package/dist/geometry/solid.d.ts +61 -0
  11. package/dist/geometry/solid.js +152 -0
  12. package/dist/geometry/solidFlat.d.ts +35 -0
  13. package/dist/geometry/solidFlat.js +428 -0
  14. package/dist/geometry/solidHarness.d.ts +19 -0
  15. package/dist/geometry/solidHarness.js +66 -0
  16. package/dist/geometry/solidNormals.d.ts +17 -0
  17. package/dist/geometry/solidNormals.js +86 -0
  18. package/dist/geometry/solidRound.d.ts +41 -0
  19. package/dist/geometry/solidRound.js +323 -0
  20. package/dist/geometry/solidWriter.d.ts +16 -0
  21. package/dist/geometry/solidWriter.js +42 -0
  22. package/dist/index.d.ts +28 -3
  23. package/dist/index.js +20 -1
  24. package/dist/render/backend/api.d.ts +34 -14
  25. package/dist/render/backend/drawFog.d.ts +26 -0
  26. package/dist/render/backend/drawFog.js +30 -0
  27. package/dist/render/backend/materialChanges.d.ts +2 -1
  28. package/dist/render/backend/materialChanges.js +6 -1
  29. package/dist/render/backend/webgl2/glassTintPrograms.d.ts +13 -0
  30. package/dist/render/backend/webgl2/glassTintPrograms.js +38 -0
  31. package/dist/render/backend/webgl2/instanced.d.ts +24 -5
  32. package/dist/render/backend/webgl2/instanced.js +72 -15
  33. package/dist/render/backend/webgl2/renderer.d.ts +201 -16
  34. package/dist/render/backend/webgl2/renderer.js +596 -224
  35. package/dist/render/backend/webgpu/buffers.d.ts +29 -1
  36. package/dist/render/backend/webgpu/buffers.js +61 -4
  37. package/dist/render/backend/webgpu/depthPass.d.ts +1 -1
  38. package/dist/render/backend/webgpu/depthPass.js +7 -2
  39. package/dist/render/backend/webgpu/drawCommand.d.ts +5 -0
  40. package/dist/render/backend/webgpu/drawCommand.js +1 -0
  41. package/dist/render/backend/webgpu/flatPass.d.ts +6 -1
  42. package/dist/render/backend/webgpu/flatPass.js +57 -9
  43. package/dist/render/backend/webgpu/glassTintPass.d.ts +28 -0
  44. package/dist/render/backend/webgpu/glassTintPass.js +206 -0
  45. package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +60 -1
  46. package/dist/render/backend/webgpu/gpuDrivenPass.js +305 -5
  47. package/dist/render/backend/webgpu/instanceCullPass.d.ts +34 -0
  48. package/dist/render/backend/webgpu/instanceCullPass.js +144 -0
  49. package/dist/render/backend/webgpu/instanced.d.ts +23 -2
  50. package/dist/render/backend/webgpu/instanced.js +41 -4
  51. package/dist/render/backend/webgpu/latePass.d.ts +30 -0
  52. package/dist/render/backend/webgpu/latePass.js +140 -0
  53. package/dist/render/backend/webgpu/lightVolumePass.js +5 -1
  54. package/dist/render/backend/webgpu/motionPass.d.ts +95 -0
  55. package/dist/render/backend/webgpu/motionPass.js +491 -0
  56. package/dist/render/backend/webgpu/pipelineCache.d.ts +10 -1
  57. package/dist/render/backend/webgpu/pipelineCache.js +11 -1
  58. package/dist/render/backend/webgpu/pointGlassTintPass.d.ts +32 -0
  59. package/dist/render/backend/webgpu/pointGlassTintPass.js +193 -0
  60. package/dist/render/backend/webgpu/pointShadowPass.d.ts +35 -6
  61. package/dist/render/backend/webgpu/pointShadowPass.js +105 -20
  62. package/dist/render/backend/webgpu/postPass.d.ts +3 -13
  63. package/dist/render/backend/webgpu/renderer.d.ts +271 -64
  64. package/dist/render/backend/webgpu/renderer.js +1395 -580
  65. package/dist/render/backend/webgpu/scatterPass.js +2 -1
  66. package/dist/render/backend/webgpu/sdfTextPass.js +6 -1
  67. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.d.ts +18 -0
  68. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.js +59 -0
  69. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
  70. package/dist/render/backend/webgpu/skinPaletteRing.js +4 -0
  71. package/dist/render/backend/webgpu/snapshotMips.d.ts +29 -0
  72. package/dist/render/backend/webgpu/snapshotMips.js +52 -0
  73. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +22 -3
  74. package/dist/render/backend/webgpu/surfaceTexturePass.js +78 -39
  75. package/dist/render/driftLight/bake.d.ts +7 -0
  76. package/dist/render/driftLight/bake.js +61 -56
  77. package/dist/render/driftLight/denseField.d.ts +46 -0
  78. package/dist/render/driftLight/denseField.js +109 -0
  79. package/dist/render/driftLight/lightField.d.ts +10 -5
  80. package/dist/render/driftLight/lightField.js +25 -19
  81. package/dist/render/driftLight/presence.d.ts +50 -0
  82. package/dist/render/driftLight/presence.js +43 -0
  83. package/dist/render/driftLight/uniforms.d.ts +3 -3
  84. package/dist/render/driftLight/uniforms.js +5 -6
  85. package/dist/render/driftLight/worldLightField.d.ts +41 -0
  86. package/dist/render/driftLight/worldLightField.js +66 -0
  87. package/dist/render/fixtureAtlas.d.ts +20 -0
  88. package/dist/render/fixtureAtlas.js +49 -0
  89. package/dist/render/glass.d.ts +45 -0
  90. package/dist/render/glass.js +39 -0
  91. package/dist/render/glassCasters.d.ts +40 -0
  92. package/dist/render/glassCasters.js +84 -0
  93. package/dist/render/glassShadow.d.ts +78 -0
  94. package/dist/render/glassShadow.js +125 -0
  95. package/dist/render/globalMediumPass.d.ts +2 -3
  96. package/dist/render/globalMediumPass.js +2 -8
  97. package/dist/render/gpudriven/lit.d.ts +12 -0
  98. package/dist/render/gpudriven/lit.js +23 -0
  99. package/dist/render/gpudriven/materialTable.d.ts +15 -5
  100. package/dist/render/gpudriven/materialTable.js +26 -6
  101. package/dist/render/gpudriven/pipeline.d.ts +1 -1
  102. package/dist/render/gpudriven/pipeline.js +4 -3
  103. package/dist/render/gpudriven/sunGlass.d.ts +35 -0
  104. package/dist/render/gpudriven/sunGlass.js +89 -0
  105. package/dist/render/instanceCull.d.ts +63 -0
  106. package/dist/render/instanceCull.js +160 -0
  107. package/dist/render/instances.d.ts +15 -0
  108. package/dist/render/lightBudget.d.ts +36 -35
  109. package/dist/render/lightBudget.js +38 -37
  110. package/dist/render/lightGrid.d.ts +42 -0
  111. package/dist/render/lightGrid.js +87 -0
  112. package/dist/render/mesh.d.ts +18 -24
  113. package/dist/render/mesh.js +51 -68
  114. package/dist/render/occlusion.d.ts +10 -0
  115. package/dist/render/occlusion.js +36 -7
  116. package/dist/render/pointGlassTint.d.ts +59 -0
  117. package/dist/render/pointGlassTint.js +198 -0
  118. package/dist/render/pointLightSelection.d.ts +7 -0
  119. package/dist/render/pointLightSelection.js +29 -4
  120. package/dist/render/pointShadowArray.d.ts +56 -3
  121. package/dist/render/pointShadowArray.js +127 -16
  122. package/dist/render/pointShadowMap.d.ts +23 -1
  123. package/dist/render/pointShadowMap.js +27 -1
  124. package/dist/render/rainField.d.ts +13 -0
  125. package/dist/render/rainField.js +27 -5
  126. package/dist/render/recon/changeFrames.d.ts +15 -0
  127. package/dist/render/recon/changeFrames.js +21 -0
  128. package/dist/render/recon/lateDepth.d.ts +11 -0
  129. package/dist/render/recon/lateDepth.js +70 -0
  130. package/dist/render/recon/lateRouting.d.ts +14 -0
  131. package/dist/render/recon/lateRouting.js +18 -0
  132. package/dist/render/recon/mover.d.ts +57 -0
  133. package/dist/render/recon/mover.js +86 -0
  134. package/dist/render/recon/resolve.d.ts +34 -4
  135. package/dist/render/recon/resolve.js +161 -20
  136. package/dist/render/renderQuality.d.ts +35 -0
  137. package/dist/render/renderQuality.js +7 -0
  138. package/dist/render/rendererHarness.js +16 -0
  139. package/dist/render/sceneTarget.d.ts +9 -1
  140. package/dist/render/sceneTarget.js +60 -12
  141. package/dist/render/shaders/depth.d.ts +16 -1
  142. package/dist/render/shaders/depth.js +94 -15
  143. package/dist/render/shaders/flat/directionalShadow.js +91 -6
  144. package/dist/render/shaders/flat/driftLight.d.ts +1 -1
  145. package/dist/render/shaders/flat/driftLight.js +32 -7
  146. package/dist/render/shaders/flat/glassTint.d.ts +6 -0
  147. package/dist/render/shaders/flat/glassTint.js +25 -0
  148. package/dist/render/shaders/flat/index.d.ts +13 -0
  149. package/dist/render/shaders/flat/index.js +9 -3
  150. package/dist/render/shaders/flat/lampGlass.d.ts +1 -0
  151. package/dist/render/shaders/flat/lampGlass.js +183 -0
  152. package/dist/render/shaders/flat/main.d.ts +1 -1
  153. package/dist/render/shaders/flat/main.js +190 -38
  154. package/dist/render/shaders/flat/pointShadow.d.ts +1 -1
  155. package/dist/render/shaders/flat/pointShadow.js +2 -0
  156. package/dist/render/shaders/flat/preamble.js +96 -26
  157. package/dist/render/shaders/flat/surfaceEffects.d.ts +29 -0
  158. package/dist/render/shaders/flat/surfaceEffects.js +152 -0
  159. package/dist/render/shaders/generated/depth.wgsl.d.ts +230 -7
  160. package/dist/render/shaders/generated/depth.wgsl.js +230 -7
  161. package/dist/render/shaders/generated/flat.wgsl.d.ts +1644 -1596
  162. package/dist/render/shaders/generated/flat.wgsl.js +1683 -1621
  163. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +3 -13
  164. package/dist/render/shaders/generated/globalMedium.wgsl.js +3 -13
  165. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +3 -13
  166. package/dist/render/shaders/generated/lightVolume.wgsl.js +21 -17
  167. package/dist/render/shaders/generated/octahedralResolve.wgsl.d.ts +29 -0
  168. package/dist/render/shaders/generated/octahedralResolve.wgsl.js +29 -0
  169. package/dist/render/shaders/generated/rush.wgsl.d.ts +21 -16
  170. package/dist/render/shaders/generated/rush.wgsl.js +21 -16
  171. package/dist/render/shaders/generated/sdfText.wgsl.d.ts +2 -2
  172. package/dist/render/shaders/generated/sdfText.wgsl.js +2 -2
  173. package/dist/render/shaders/generated/text.wgsl.d.ts +1 -1
  174. package/dist/render/shaders/generated/text.wgsl.js +1 -1
  175. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +1 -1
  176. package/dist/render/shaders/gi/driftLight.wgsl.js +27 -4
  177. package/dist/render/shaders/globalMedium.js +6 -6
  178. package/dist/render/shaders/gpudriven/blendRaster.wgsl.d.ts +7 -0
  179. package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +86 -1
  180. package/dist/render/shaders/gpudriven/cull.wgsl.d.ts +1 -1
  181. package/dist/render/shaders/gpudriven/cull.wgsl.js +11 -2
  182. package/dist/render/shaders/gpudriven/materialTable.wgsl.d.ts +1 -1
  183. package/dist/render/shaders/gpudriven/materialTable.wgsl.js +3 -1
  184. package/dist/render/shaders/gpudriven/shade.wgsl.js +50 -1
  185. package/dist/render/shaders/gpudriven/sunGlass.wgsl.d.ts +9 -0
  186. package/dist/render/shaders/gpudriven/sunGlass.wgsl.js +133 -0
  187. package/dist/render/shaders/lightVolume.js +6 -6
  188. package/dist/render/shaders/octahedralResolve.d.ts +8 -0
  189. package/dist/render/shaders/octahedralResolve.js +48 -17
  190. package/dist/render/shaders/recon/lateDepth.wgsl.d.ts +1 -0
  191. package/dist/render/shaders/recon/lateDepth.wgsl.js +69 -0
  192. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  193. package/dist/render/shaders/recon/motion.wgsl.js +84 -0
  194. package/dist/render/shaders/recon/resolve.wgsl.d.ts +8 -7
  195. package/dist/render/shaders/recon/resolve.wgsl.js +161 -27
  196. package/dist/render/shaders/rush.js +11 -1
  197. package/dist/render/shaders/sdfText.d.ts +1 -1
  198. package/dist/render/shaders/sdfText.js +3 -2
  199. package/dist/render/shaders/text.d.ts +1 -1
  200. package/dist/render/shaders/text.js +18 -9
  201. package/dist/render/shadowCasters.d.ts +20 -1
  202. package/dist/render/shadowCasters.js +15 -1
  203. package/dist/render/shadowMap.d.ts +54 -5
  204. package/dist/render/shadowMap.js +190 -36
  205. package/dist/render/sunGlassTint.d.ts +25 -0
  206. package/dist/render/sunGlassTint.js +79 -0
  207. package/dist/render/surfaceEffects.d.ts +83 -0
  208. package/dist/render/surfaceEffects.js +75 -0
  209. package/dist/render/surfaceScene.d.ts +8 -0
  210. package/dist/render/surfaceScene.js +8 -0
  211. package/dist/render/surfaceTexture.d.ts +18 -2
  212. package/dist/render/surfaceTexture.js +68 -15
  213. package/dist/render/temporalAa.d.ts +25 -6
  214. package/dist/render/temporalAa.js +45 -15
  215. package/dist/render/textureSource.d.ts +30 -0
  216. package/dist/render/textureSource.js +53 -0
  217. package/dist/render/translucentQueue.d.ts +6 -0
  218. package/dist/render/translucentQueue.js +15 -1
  219. package/dist/render/vertexDefaults.js +2 -0
  220. package/dist/world/hlod.d.ts +104 -0
  221. package/dist/world/hlod.js +214 -0
  222. package/package.json +6 -6
  223. package/src/geometry/boxCsg.ts +186 -0
  224. package/src/geometry/csg.ts +384 -0
  225. package/src/geometry/meshBuilder.ts +15 -0
  226. package/src/geometry/meshConcat.ts +1 -0
  227. package/src/geometry/pixelFont.ts +2 -0
  228. package/src/geometry/solid.ts +189 -0
  229. package/src/geometry/solidFlat.ts +522 -0
  230. package/src/geometry/solidHarness.ts +83 -0
  231. package/src/geometry/solidNormals.ts +103 -0
  232. package/src/geometry/solidRound.ts +443 -0
  233. package/src/geometry/solidWriter.ts +58 -0
  234. package/src/index.ts +61 -1
  235. package/src/render/backend/api.ts +36 -11
  236. package/src/render/backend/drawFog.ts +31 -0
  237. package/src/render/backend/materialChanges.ts +9 -2
  238. package/src/render/backend/webgl2/glassTintPrograms.ts +58 -0
  239. package/src/render/backend/webgl2/instanced.ts +80 -14
  240. package/src/render/backend/webgl2/renderer.ts +700 -250
  241. package/src/render/backend/webgpu/buffers.ts +92 -5
  242. package/src/render/backend/webgpu/depthPass.ts +7 -2
  243. package/src/render/backend/webgpu/drawCommand.ts +6 -0
  244. package/src/render/backend/webgpu/flatPass.ts +62 -11
  245. package/src/render/backend/webgpu/glassTintPass.ts +245 -0
  246. package/src/render/backend/webgpu/gpuDrivenPass.ts +349 -5
  247. package/src/render/backend/webgpu/instanceCullPass.ts +165 -0
  248. package/src/render/backend/webgpu/instanced.ts +42 -4
  249. package/src/render/backend/webgpu/latePass.ts +161 -0
  250. package/src/render/backend/webgpu/lightVolumePass.ts +5 -1
  251. package/src/render/backend/webgpu/motionPass.ts +535 -0
  252. package/src/render/backend/webgpu/pipelineCache.ts +12 -1
  253. package/src/render/backend/webgpu/pointGlassTintPass.ts +233 -0
  254. package/src/render/backend/webgpu/pointShadowPass.ts +158 -26
  255. package/src/render/backend/webgpu/renderer.ts +1671 -670
  256. package/src/render/backend/webgpu/scatterPass.ts +2 -1
  257. package/src/render/backend/webgpu/sdfTextPass.ts +6 -1
  258. package/src/render/backend/webgpu/shaders/instanceCull.wgsl.ts +61 -0
  259. package/src/render/backend/webgpu/skinPaletteRing.ts +4 -0
  260. package/src/render/backend/webgpu/snapshotMips.ts +80 -0
  261. package/src/render/backend/webgpu/surfaceTexturePass.ts +91 -51
  262. package/src/render/driftLight/bake.ts +85 -62
  263. package/src/render/driftLight/denseField.ts +149 -0
  264. package/src/render/driftLight/lightField.ts +33 -18
  265. package/src/render/driftLight/presence.ts +69 -0
  266. package/src/render/driftLight/uniforms.ts +8 -9
  267. package/src/render/driftLight/worldLightField.ts +83 -0
  268. package/src/render/fixtureAtlas.ts +63 -0
  269. package/src/render/glass.ts +74 -0
  270. package/src/render/glassCasters.ts +164 -0
  271. package/src/render/glassShadow.ts +153 -0
  272. package/src/render/globalMediumPass.ts +4 -11
  273. package/src/render/gpudriven/lit.ts +31 -0
  274. package/src/render/gpudriven/materialTable.ts +28 -6
  275. package/src/render/gpudriven/pipeline.ts +4 -3
  276. package/src/render/gpudriven/sunGlass.ts +117 -0
  277. package/src/render/instanceCull.ts +205 -0
  278. package/src/render/instances.ts +16 -0
  279. package/src/render/lightBudget.ts +41 -38
  280. package/src/render/lightGrid.ts +130 -0
  281. package/src/render/mesh.ts +59 -72
  282. package/src/render/occlusion.ts +51 -7
  283. package/src/render/pointGlassTint.ts +250 -0
  284. package/src/render/pointLightSelection.ts +72 -4
  285. package/src/render/pointShadowArray.ts +188 -22
  286. package/src/render/pointShadowMap.ts +41 -0
  287. package/src/render/rainField.ts +39 -5
  288. package/src/render/recon/changeFrames.ts +24 -0
  289. package/src/render/recon/lateDepth.ts +83 -0
  290. package/src/render/recon/lateRouting.ts +29 -0
  291. package/src/render/recon/mover.ts +118 -0
  292. package/src/render/recon/resolve.ts +206 -20
  293. package/src/render/renderQuality.ts +47 -0
  294. package/src/render/rendererHarness.ts +16 -0
  295. package/src/render/sceneTarget.ts +62 -11
  296. package/src/render/shaders/depth.ts +118 -15
  297. package/src/render/shaders/flat/directionalShadow.ts +97 -6
  298. package/src/render/shaders/flat/driftLight.ts +32 -7
  299. package/src/render/shaders/flat/glassTint.ts +25 -0
  300. package/src/render/shaders/flat/index.ts +22 -3
  301. package/src/render/shaders/flat/lampGlass.ts +185 -0
  302. package/src/render/shaders/flat/main.ts +190 -38
  303. package/src/render/shaders/flat/pointShadow.ts +2 -0
  304. package/src/render/shaders/flat/preamble.ts +96 -26
  305. package/src/render/shaders/flat/surfaceEffects.ts +152 -0
  306. package/src/render/shaders/generated/depth.wgsl.ts +237 -7
  307. package/src/render/shaders/generated/flat.wgsl.ts +1685 -1621
  308. package/src/render/shaders/generated/globalMedium.wgsl.ts +3 -13
  309. package/src/render/shaders/generated/lightVolume.wgsl.ts +23 -17
  310. package/src/render/shaders/generated/octahedralResolve.wgsl.ts +30 -0
  311. package/src/render/shaders/generated/rush.wgsl.ts +21 -16
  312. package/src/render/shaders/generated/sdfText.wgsl.ts +2 -2
  313. package/src/render/shaders/generated/text.wgsl.ts +1 -1
  314. package/src/render/shaders/gi/driftLight.wgsl.ts +27 -4
  315. package/src/render/shaders/globalMedium.ts +6 -6
  316. package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +88 -1
  317. package/src/render/shaders/gpudriven/cull.wgsl.ts +11 -2
  318. package/src/render/shaders/gpudriven/materialTable.wgsl.ts +3 -1
  319. package/src/render/shaders/gpudriven/shade.wgsl.ts +50 -1
  320. package/src/render/shaders/gpudriven/sunGlass.wgsl.ts +139 -0
  321. package/src/render/shaders/lightVolume.ts +6 -6
  322. package/src/render/shaders/octahedralResolve.ts +50 -17
  323. package/src/render/shaders/recon/lateDepth.wgsl.ts +72 -0
  324. package/src/render/shaders/recon/motion.wgsl.ts +84 -0
  325. package/src/render/shaders/recon/resolve.wgsl.ts +161 -27
  326. package/src/render/shaders/rush.ts +11 -1
  327. package/src/render/shaders/sdfText.ts +3 -2
  328. package/src/render/shaders/text.ts +18 -9
  329. package/src/render/shadowCasters.ts +28 -1
  330. package/src/render/shadowMap.ts +231 -37
  331. package/src/render/sunGlassTint.ts +86 -0
  332. package/src/render/surfaceEffects.ts +132 -0
  333. package/src/render/surfaceScene.ts +16 -0
  334. package/src/render/surfaceTexture.ts +110 -19
  335. package/src/render/temporalAa.ts +78 -14
  336. package/src/render/textureSource.ts +72 -0
  337. package/src/render/translucentQueue.ts +20 -1
  338. package/src/render/vertexDefaults.ts +2 -0
  339. package/src/world/hlod.ts +290 -0
@@ -2,19 +2,17 @@ import { mat4 } from 'gl-matrix';
2
2
  import type { ProbeBakeOptions } from '../../reflectionProbe.ts';
3
3
  import { probeFaceRange } from '../../reflectionProbe.ts';
4
4
  import {
5
+ TEMPORAL_FLICKER,
5
6
  TEMPORAL_HISTORY_BLEND,
6
7
  TemporalHistory,
8
+ flickerRule,
7
9
  jitterOffset,
8
10
  jitterProjection,
9
11
  } from '../../temporalAa.ts';
12
+ import type { FlickerRule } from '../../temporalAa.ts';
10
13
  import { OIT_MULTISAMPLE_REFUSAL } from '../../orderIndependent.ts';
11
14
  import { TranslucentQueue } from '../../translucentQueue.ts';
12
- import {
13
- DEPTH_CLEAR,
14
- DEPTH_COMPARE_EQUAL,
15
- MAX_DEPTH_LAYER,
16
- REVERSED_DEPTH,
17
- } from '../../depthConvention.ts';
15
+ import { DEPTH_CLEAR, MAX_DEPTH_LAYER, REVERSED_DEPTH } from '../../depthConvention.ts';
18
16
  import { RECONSTRUCTION_MULTISAMPLE_REFUSAL, reconRenderSize } from '../../recon/frameSizes.ts';
19
17
  import { jitterOffset as jitterOffsetFor, reconJitterPhases } from '../../recon/jitter.ts';
20
18
  import {
@@ -23,16 +21,25 @@ import {
23
21
  RECON_WORKGROUP,
24
22
  reconResolveWgsl,
25
23
  } from '../../shaders/recon/resolve.wgsl.ts';
26
- import {
27
- MOTION_DRAW_FLOATS,
28
- MOTION_DRAW_STRIDE,
29
- MOTION_FRAME_FLOATS,
30
- MOTION_TARGET_FORMAT,
31
- RECON_MOTION_WGSL,
32
- } from '../../shaders/recon/motion.wgsl.ts';
24
+ import { MotionPass } from './motionPass.ts';
25
+ import { LatePass } from './latePass.ts';
26
+ import { SnapshotMips } from './snapshotMips.ts';
27
+ import { resolveGlass } from '../../glass.ts';
28
+ import type { GlassOptions, ResolvedGlass } from '../../glass.ts';
29
+ import { routesLate, tracksMotion } from '../../recon/lateRouting.ts';
30
+ import { frameOfChange, keepsPrevious, pairedInstances } from '../../recon/changeFrames.ts';
31
+ import { INSTANCE_STRIDE } from '../../instances.ts';
33
32
  import { DEFAULT_DISOCCLUSION } from '../../recon/disocclusion.ts';
34
33
  import { DEFAULT_RECON_QUALITY } from '../../recon/resolve.ts';
35
34
  import type { ReadonlyMat4 } from 'gl-matrix';
35
+ import {
36
+ MOVER_MOTION,
37
+ MOVER_TWICE,
38
+ advanceMover,
39
+ claimRendererId,
40
+ isMover,
41
+ } from '../../recon/mover.ts';
42
+ import type { Mover } from '../../recon/mover.ts';
36
43
 
37
44
  import type { Vec3 } from '../../../math/color.ts';
38
45
  import { clamp } from '../../../math/scalar.ts';
@@ -99,6 +106,8 @@ import { ProbeSweeps } from '../../probeSweeps.ts';
99
106
  import { ggxMaxLevelFor, irradianceLevelFor, octahedralEdgeFor } from '../../prefilterEnvMap.ts';
100
107
  import { environmentTexels } from './environmentTexels.ts';
101
108
  import { packIesAtlas } from '../../iesProfile.ts';
109
+ import { fixtureAtlasLayout, fixtureAtlasTexels } from '../../fixtureAtlas.ts';
110
+ import { resolveSurfaceScene } from '../../surfaceScene.ts';
102
111
  import { COOKIE_TILE } from '../../lightBudget.ts';
103
112
  import { FULL_LIGHT_BUDGET } from '../../uniformVectorBudget.ts';
104
113
  import { toHalfFloats } from '../../halfFloat.ts';
@@ -128,6 +137,7 @@ import {
128
137
  type ShadowLight,
129
138
  } from '../../pointShadowSystem.ts';
130
139
  import { GpuPointShadowArray, GpuPointShadowMap } from './pointShadowPass.ts';
140
+ import { OCTAHEDRAL_EDGE, strideLayers } from '../../pointShadowArray.ts';
131
141
  import {
132
142
  createPanelBindGroup,
133
143
  createPanelBindGroupLayout,
@@ -326,6 +336,16 @@ import {
326
336
  } from '../../globalMedium.ts';
327
337
  import type { GlobalMediumOptions } from '../../globalMedium.ts';
328
338
  import type { MeshData } from '../../mesh.ts';
339
+ import { castsDepth, glassOf } from '../../shadowCasters.ts';
340
+ import { GlassCasterList, type GlassReplaySink } from '../../glassCasters.ts';
341
+ import { GpuGlassTint } from './glassTintPass.ts';
342
+ import {
343
+ SUN_DYNAMIC_LAYER,
344
+ SUN_GLASS_MOVING_LAYER,
345
+ SUN_GLASS_STATIC_LAYER,
346
+ SUN_PEELED_LAYER,
347
+ SUN_STATIC_LAYER,
348
+ } from '../../shadowMap.ts';
329
349
  import type { SceneCasterMaterial, ShadowCasterSink, ShadowCasters } from '../../shadowCasters.ts';
330
350
  import { VERTEX_LAYOUT, createGpuMesh, createGpuMeshIncremental, type GpuMesh } from './buffers.ts';
331
351
  import type { GpuSurface } from './device.ts';
@@ -470,6 +490,7 @@ import {
470
490
  import { aimReflection, createGpuReflection, type GpuReflection } from './reflectionPass.ts';
471
491
  import { FrameBudget } from '../budget.ts';
472
492
  import { MaterialChanges, ownsMaterial } from '../materialChanges.ts';
493
+ import { FOG_RECEDE, fogModeOf } from '../drawFog.ts';
473
494
  import { DYNAMIC_ALIGNMENT as DYNAMIC_UNIFORM_ALIGNMENT, UniformRing } from './uniformRing.ts';
474
495
  import { DecalQueue, MAX_DRAWN_DECALS } from '../../decalQueue.ts';
475
496
  import { DistanceFieldScene } from '../../gi/fieldScene.ts';
@@ -525,6 +546,9 @@ import {
525
546
  import { createFrustum, frustumFromViewProjection } from '../../../math/frustum.ts';
526
547
  import type { Frustum } from '../../../math/frustum.ts';
527
548
  import type { Bounds } from '../../../math/bounds.ts';
549
+ import { batchBoxVisible, cullsInstances } from '../../instanceCull.ts';
550
+ import { InstanceCullPass, MAX_CULL_SLOTS } from './instanceCullPass.ts';
551
+ import type { CullSlot } from './instanceCullPass.ts';
528
552
  import { OcclusionBuffer } from '../../occlusion.ts';
529
553
  import { boundsVisible } from '../../visibility.ts';
530
554
  import { createPassRegistry, drainRegistry, passAt, registerIn, unregisterIn } from '../../pass.ts';
@@ -550,11 +574,15 @@ import type {
550
574
  } from '../../pass.ts';
551
575
  import type { Arena, FlushSchedule, ScheduledPass } from '../../frame/index.ts';
552
576
  import { GpuInstancedBatch } from './instanced.ts';
553
- import type { MeshInstances } from '../../instances.ts';
577
+ import type { InstancedOptions, MeshInstances } from '../../instances.ts';
554
578
  import { cutoutOf } from '../../cutoutCaster.ts';
555
579
  import type { CutoutCaster } from '../../cutoutCaster.ts';
556
580
  import { LightField } from '../../driftLight/lightField.ts';
557
581
  import type { LightFieldOptions, LightFieldSource } from '../../driftLight/lightField.ts';
582
+ import type { DriftLightVolumes } from '../../driftLight/presence.ts';
583
+ import type { DenseLightVolume } from '../../driftLight/denseField.ts';
584
+ import { WorldLightField } from '../../driftLight/worldLightField.ts';
585
+ import type { WorldLightFieldOptions } from '../../driftLight/worldLightField.ts';
558
586
  import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/uniforms.ts';
559
587
 
560
588
  /**
@@ -1308,6 +1336,49 @@ function padded(direction: ArrayLike<number>): Float32Array {
1308
1336
  return SCRATCH_VEC4;
1309
1337
  }
1310
1338
 
1339
+ /** The sun texture's usage: attached, sampled, and copied both ways when glass grows it. */
1340
+ const SUN_USAGE = 0x10 | 0x4 | 0x1 | 0x2; // RENDER_ATTACHMENT | TEXTURE_BINDING | COPY_SRC | COPY_DST
1341
+
1342
+ /** Every view of the sun's texture a pass reads or attaches, for `maps` of its layers. */
1343
+ function sunViewsOf(
1344
+ texture: GPUTexture,
1345
+ maps: number,
1346
+ layers: number,
1347
+ ): {
1348
+ array: GPUTextureView;
1349
+ staticArray: GPUTextureView;
1350
+ static: GPUTextureView;
1351
+ dynamic: GPUTextureView | null;
1352
+ peel: GPUTextureView | null;
1353
+ glass: readonly [GPUTextureView, GPUTextureView] | null;
1354
+ } {
1355
+ const layer = (index: number): GPUTextureView =>
1356
+ texture.createView({ dimension: '2d', baseArrayLayer: index, arrayLayerCount: 1 });
1357
+ return {
1358
+ array: texture.createView({ dimension: '2d-array' }),
1359
+ staticArray: texture.createView({
1360
+ dimension: '2d-array',
1361
+ baseArrayLayer: SUN_STATIC_LAYER,
1362
+ arrayLayerCount: 1,
1363
+ }),
1364
+ static: layer(SUN_STATIC_LAYER),
1365
+ dynamic: maps > SUN_DYNAMIC_LAYER ? layer(SUN_DYNAMIC_LAYER) : null,
1366
+ peel: maps > SUN_PEELED_LAYER ? layer(SUN_PEELED_LAYER) : null,
1367
+ glass: layers > maps ? [layer(SUN_GLASS_STATIC_LAYER), layer(SUN_GLASS_MOVING_LAYER)] : null,
1368
+ };
1369
+ }
1370
+
1371
+ /**
1372
+ * What the sun keeps through glass: two `rgba8unorm` layers, attached one at a time at level 0,
1373
+ * sampled whole with their mip chains, which are rebuilt after each layer is drawn.
1374
+ */
1375
+ interface SunTintTarget {
1376
+ readonly texture: GPUTexture;
1377
+ readonly layers: readonly [GPUTextureView, GPUTextureView];
1378
+ readonly array: GPUTextureView;
1379
+ readonly mips: readonly [SnapshotMips, SnapshotMips];
1380
+ }
1381
+
1311
1382
  export class WebGPURenderer implements RendererApi {
1312
1383
  /**
1313
1384
  * Real timestamp queries, where the adapter offers them.
@@ -1417,6 +1488,8 @@ export class WebGPURenderer implements RendererApi {
1417
1488
  * caller who passed null gets — rather than reading a slot this frame has already re-let.
1418
1489
  */
1419
1490
  private skinPaletteSlot = -1;
1491
+ /** The palette `skinPaletteSlot` was uploaded from, for a skinned mover to copy. See `Mover`. */
1492
+ private skinPaletteData: Float32Array | null = null;
1420
1493
 
1421
1494
  private readonly surface: GpuSurface;
1422
1495
  /** The encoder for the frame in flight, or null between frames. */
@@ -1426,6 +1499,27 @@ export class WebGPURenderer implements RendererApi {
1426
1499
  private pass: GPURenderPassEncoder | null = null;
1427
1500
 
1428
1501
  private readonly pipelines: PipelineCache;
1502
+ /**
1503
+ * The pipelines a reconstructing frame's blended draws are built for: the reconstructed picture's
1504
+ * format at one sample, which is where `LatePass` draws them. Aliased to `pipelines` when the
1505
+ * world already lands on exactly that, as `overlayPipelines` is.
1506
+ */
1507
+ private readonly latePipelines: PipelineCache;
1508
+ /** The blended draws of a reconstructing frame, drawn after the upscale. See `LatePass`. */
1509
+ private latePass: LatePass | null = null;
1510
+ /** A late draw asked to refract: `runLate` copies the reconstructed picture before replaying. */
1511
+ private lateRefractWanted = false;
1512
+ /** The refraction copy's mip chain, recorded after each copy: what frosted glass reads. */
1513
+ private refractMips: SnapshotMips | null = null;
1514
+ /**
1515
+ * The refraction copy's sampler: filtering, and **between levels as well**, so a frosted pane's
1516
+ * level falls smoothly between two blurs rather than stepping from one to the next.
1517
+ */
1518
+ private refractSampler: GPUSampler | null = null;
1519
+ /** One glass resolution reused by every draw, so a pane allocates nothing. See `glass.ts`. */
1520
+ private readonly glassScratch: ResolvedGlass = { transmission: 0, frost: 0, tint: [1, 1, 1] };
1521
+ /** `issueCommand` bound once, so the late replay allocates no closure per frame. */
1522
+ private readonly issueBound: (pass: GPURenderPassEncoder, command: DrawCommand) => void;
1429
1523
  /**
1430
1524
  * The same pipelines again, built for the canvas, for anything drawn after `endFrame`.
1431
1525
  *
@@ -1818,6 +1912,12 @@ export class WebGPURenderer implements RendererApi {
1818
1912
  * what the layout already declares.
1819
1913
  */
1820
1914
  private bloomGroups: GPUBindGroup[] = [];
1915
+ /**
1916
+ * The picture the bloom reads, and its size: the scene target, or while reconstructing the
1917
+ * resolved picture at the drawing buffer's size. See `rebuildBloomGroups`.
1918
+ */
1919
+ private bloomSourceWidth = 1;
1920
+ private bloomSourceHeight = 1;
1821
1921
  private frameWidth = 1;
1822
1922
  private frameHeight = 1;
1823
1923
  /**
@@ -1971,13 +2071,25 @@ export class WebGPURenderer implements RendererApi {
1971
2071
  * the *unsharpened* result — sharpening into the history would sharpen a sharpened picture every
1972
2072
  * frame, which is the halo the reference's own clamp exists to prevent, compounded.
1973
2073
  *
1974
- * The motion target is render size and carries a drawn object's own motion where something wrote
1975
- * one. **Nothing writes it yet**, so it is cleared every frame and every pixel takes the camera's
1976
- * motion, which the resolve derives from the depth and the reprojection. Static geometry is
1977
- * exact that way and a moving object ghosts until the motion pass lands.
2074
+ * The motion target is render size and carries a drawn object's own motion where the motion pass
2075
+ * wrote one — a draw that named its mover, or passed last frame's matrix. Every other texel is
2076
+ * cleared, and the resolve takes the camera's motion there, derived from the depth and the
2077
+ * reprojection: exact for anything that did not move. See `MotionPass`.
1978
2078
  */
1979
2079
  private reconHistories: (GPUTexture | null)[] = [null, null];
1980
2080
  private reconHistoryViews: (GPUTextureView | null)[] = [null, null];
2081
+ /**
2082
+ * Each output pixel's flicker record, a pair swapped with the histories: the temporal resolve's
2083
+ * anti-flicker, over the reconstruction's own period (`recon/resolve.ts`).
2084
+ */
2085
+ private reconFlickers: (GPUTexture | null)[] = [null, null];
2086
+ private reconFlickerViews: (GPUTextureView | null)[] = [null, null];
2087
+ /** Each output pixel's held depth swing, a pair of its own: see `recon/resolve.ts`. */
2088
+ private reconSwings: (GPUTexture | null)[] = [null, null];
2089
+ private reconSwingViews: (GPUTextureView | null)[] = [null, null];
2090
+ /** The anti-flicker's rule for the period last asked for, kept so a frame makes none. */
2091
+ private reconFlickerPhases = 0;
2092
+ private reconFlickerRule: FlickerRule = TEMPORAL_FLICKER;
1981
2093
  private reconWrite = 0;
1982
2094
  private reconHasHistory = false;
1983
2095
  private reconShown: GPUTexture | null = null;
@@ -1996,40 +2108,24 @@ export class WebGPURenderer implements RendererApi {
1996
2108
  private reconGroups: (GPUBindGroup | null)[] = [null, null];
1997
2109
  private reconSharpenGroups: (GPUBindGroup | null)[] = [null, null];
1998
2110
  private reconRushGroup: GPUBindGroup | null = null;
1999
- /**
2000
- * What the motion pass will draw, recorded by `drawMesh` and spent once a frame.
2001
- *
2002
- * A parallel array of meshes and one flat matrix buffer rather than an array of objects, because
2003
- * this is written on the frame's hot path and the house rule about allocating there binds it: the
2004
- * matrices are copied into a buffer that only ever grows, and the count is what resets.
2005
- */
2006
- private readonly motionMeshes: GpuMesh[] = [];
2007
- private motionMatrices = new Float32Array(0);
2008
- private motionCount = 0;
2009
- private motionLayout: GPUBindGroupLayout | null = null;
2010
- /**
2011
- * One pipeline per vertex stride, because a stride is a property of the *mesh*.
2012
- *
2013
- * Which optional attributes a mesh supplied decides how far apart its positions are, and this
2014
- * pass reads nothing but positions — so two meshes drawn by the same shader still need two
2015
- * vertex layouts. A map rather than a cache key on the pipeline cache: there is one shader here
2016
- * and a handful of strides in any scene.
2017
- */
2018
- private readonly motionPipelines = new Map<number, GPURenderPipeline>();
2019
- private motionModule: GPUShaderModule | null = null;
2020
- private motionPipelineLayout: GPUPipelineLayout | null = null;
2021
- private motionFrameBuffer: GPUBuffer | null = null;
2022
- private motionDrawBuffer: GPUBuffer | null = null;
2023
- private motionDrawCapacity = 0;
2024
- private motionGroup: GPUBindGroup | null = null;
2025
- private readonly motionFrameStaging = new Float32Array(MOTION_FRAME_FLOATS);
2026
- private motionDrawStaging = new Uint8Array(0);
2111
+ /** The motion pass, made with the reconstruction's targets. See `MotionPass`. */
2112
+ private motionPass: MotionPass | null = null;
2113
+ /** This renderer's identity to a `Mover`. See `claimRendererId`. */
2114
+ private readonly moverRenderer = claimRendererId();
2115
+ /** Frames begun, so a mover can tell last frame from any other. Advanced in `beginFrame`. */
2116
+ private moverFrame = 0;
2117
+ /** Between `beginFrame` and `endFrame`. A change outside it is the next frame's. */
2118
+ private inFrame = false;
2119
+ private warnedMoverTwice = false;
2027
2120
  /** The scene's own clip transform with the generated shaders' y negation folded in. */
2028
2121
  private readonly motionRaster = mat4.create();
2029
2122
  private reconSizeKey = '';
2030
2123
  /** Last frame's raw view-projection and its inverse, and the two eyes. */
2031
2124
  private readonly reconPreviousRawViewProj = mat4.create();
2032
2125
  private readonly reconInverseViewProj = mat4.create();
2126
+ /** The inverse and the reprojection, multiplied in double precision. See `runReconstruction`. */
2127
+ private readonly reconInverse64 = new Float64Array(16);
2128
+ private readonly reconReprojection64 = new Float64Array(16);
2033
2129
  private readonly reconPreviousInverseViewProj = mat4.create();
2034
2130
  private readonly reconEye = new Float32Array(3);
2035
2131
  private readonly reconPreviousEye = new Float32Array(3);
@@ -2181,6 +2277,8 @@ export class WebGPURenderer implements RendererApi {
2181
2277
  /** Said once for the whole renderer. See `refuseOitMultisampled`. */
2182
2278
  private oitMultisampleRefused = false;
2183
2279
  private oitReplaying = false;
2280
+ /** `uSurfaceScene`, resolved once a pass. See `surfaceScene.ts`. */
2281
+ private readonly surfaceScene = new Float32Array(4);
2184
2282
  /** Which buffer the replay is filling, which the pipeline key must carry. */
2185
2283
  private oitMode: OitTarget = 'none';
2186
2284
  /**
@@ -2276,6 +2374,7 @@ export class WebGPURenderer implements RendererApi {
2276
2374
  refraction: number;
2277
2375
  refractTint: Vec3 | undefined;
2278
2376
  thicknessM: number;
2377
+ glass: GlassOptions | undefined;
2279
2378
  } = {
2280
2379
  lit: true,
2281
2380
  fog: true,
@@ -2286,6 +2385,7 @@ export class WebGPURenderer implements RendererApi {
2286
2385
  refraction: 0,
2287
2386
  refractTint: undefined,
2288
2387
  thicknessM: 0,
2388
+ glass: undefined,
2289
2389
  };
2290
2390
 
2291
2391
  private reflectionPassActive = false;
@@ -2470,6 +2570,8 @@ export class WebGPURenderer implements RendererApi {
2470
2570
  }
2471
2571
 
2472
2572
  private flushRings(): void {
2573
+ /* Culls first: a submit that follows may carry a draw reading what they write. */
2574
+ this.instanceCulls?.flush();
2473
2575
  for (const ring of this.rings()) ring.flush();
2474
2576
  }
2475
2577
 
@@ -2591,7 +2693,7 @@ export class WebGPURenderer implements RendererApi {
2591
2693
  this.filmFrags.writeBlock(fragmentSlot, this.filmFragInts);
2592
2694
 
2593
2695
  const filmPipe = filmPipeline(
2594
- this.pipelines,
2696
+ this.blendedPipelines(),
2595
2697
  this.surface.device,
2596
2698
  this.filmLayout,
2597
2699
  `film|${geometry.key ?? ''}`,
@@ -2605,10 +2707,7 @@ export class WebGPURenderer implements RendererApi {
2605
2707
  * holds the mirror's colour whatever `uReflectionEnabled` says, and WebGPU validates the
2606
2708
  * binding rather than the read — the argument `filmBindGroupBlank` exists for.
2607
2709
  */
2608
- const filmCommand = this.recordDraw(
2609
- filmGroup === this.filmBindGroup ? MIRROR_COLOR : 0,
2610
- this.currentTarget(),
2611
- );
2710
+ const filmCommand = this.recordBlended(filmGroup === this.filmBindGroup ? MIRROR_COLOR : 0);
2612
2711
  if (filmCommand !== null) {
2613
2712
  filmCommand.pipeline = filmPipe;
2614
2713
  filmCommand.bindGroup = filmGroup;
@@ -2710,6 +2809,14 @@ export class WebGPURenderer implements RendererApi {
2710
2809
  return new GpuSurfaceTexture(this.surface.device, this.pipelines, source, options);
2711
2810
  }
2712
2811
 
2812
+ /** Several images of one size as an array's layers. See `RendererApi.createSurfaceTextureArray`. */
2813
+ createSurfaceTextureArray(
2814
+ sources: readonly TexImageSource[],
2815
+ options: SurfaceTextureOptions = {},
2816
+ ): GpuSurfaceTexture {
2817
+ return new GpuSurfaceTexture(this.surface.device, this.pipelines, sources, options);
2818
+ }
2819
+
2713
2820
  /**
2714
2821
  * GPU textures an `updateSurfaceTexture` replaced, destroyed when the next frame begins.
2715
2822
  *
@@ -2799,6 +2906,7 @@ export class WebGPURenderer implements RendererApi {
2799
2906
  */
2800
2907
  setSkinPalette(palette: Float32Array | null): void {
2801
2908
  if (this.surface.lost) return;
2909
+ this.skinPaletteData = palette;
2802
2910
  if (palette === null) {
2803
2911
  this.skinPaletteSlot = -1;
2804
2912
  return;
@@ -2987,6 +3095,19 @@ export class WebGPURenderer implements RendererApi {
2987
3095
  });
2988
3096
  }
2989
3097
 
3098
+ /**
3099
+ * How much of the surfaces drawn next survives a screen-door dither. See `renderer.ts`.
3100
+ *
3101
+ * Written straight into the scratch block rather than through `material()`'s callback, because
3102
+ * this one is called per crossfading region per frame and a closure per call is an allocation.
3103
+ */
3104
+ setDitherFade(amount: number): void {
3105
+ if (this.surface.lost) return;
3106
+ /* The second half of `uWriteMode`, which the order-independent weight shares. */
3107
+ this.perFrameFloats[this.materialField('uWriteMode') + 1] = Math.min(1, Math.max(-1, amount));
3108
+ this.materials.dirty();
3109
+ }
3110
+
2990
3111
  /**
2991
3112
  * How strong the microscopic relief on the surfaces drawn next is, and how coarse.
2992
3113
  *
@@ -3167,7 +3288,8 @@ export class WebGPURenderer implements RendererApi {
3167
3288
  * no-op before the first `bindMeshPass` of a session, same as every other pass that reads it.
3168
3289
  */
3169
3290
  drawSdfText(text: GpuSdfText, model: Float32Array, color: Vec3, opacity: number): void {
3170
- const viewProj = this.viewProj;
3291
+ /* Unjittered when it lands after the upscale, like every late draw. See `drawsLate`. */
3292
+ const viewProj = this.drawsLate() ? this.correctedViewProj : this.viewProj;
3171
3293
  if (!this.canDraw() || viewProj === null) return;
3172
3294
  const font = text.font;
3173
3295
  const atlas = text.atlas;
@@ -3201,12 +3323,12 @@ export class WebGPURenderer implements RendererApi {
3201
3323
  this.sdfTextFrags.writeFloat(fragmentSlot, fragAt('uOutputExposure'), this.gradeExposure());
3202
3324
 
3203
3325
  const sdfPipe = sdfTextPipeline(
3204
- this.targetPipelines(),
3326
+ this.blendedPipelines(),
3205
3327
  this.surface.device,
3206
3328
  this.sdfTextLayout,
3207
3329
  );
3208
3330
  const sdfGroup = this.sdfTextBindGroupFor(atlas);
3209
- const sdfCommand = this.recordDraw(0, this.currentTarget());
3331
+ const sdfCommand = this.recordBlended(0);
3210
3332
  if (sdfCommand !== null) {
3211
3333
  sdfCommand.pipeline = sdfPipe;
3212
3334
  sdfCommand.bindGroup = sdfGroup;
@@ -3322,8 +3444,8 @@ export class WebGPURenderer implements RendererApi {
3322
3444
 
3323
3445
  const pass = this.openTextPass();
3324
3446
  if (pass === null) return;
3325
- const textPipe = textPipeline(this.targetPipelines(), this.surface.device, this.textLayout);
3326
- const textCommand = this.recordDraw(0, this.currentTarget());
3447
+ const textPipe = textPipeline(this.blendedPipelines(), this.surface.device, this.textLayout);
3448
+ const textCommand = this.recordBlended(0);
3327
3449
  if (textCommand !== null) {
3328
3450
  textCommand.pipeline = textPipe;
3329
3451
  textCommand.bindGroup = this.textBindGroup;
@@ -3773,13 +3895,13 @@ export class WebGPURenderer implements RendererApi {
3773
3895
 
3774
3896
  const layout = this.particleLayoutFor(batch.material).layout;
3775
3897
  const particlePipe = particlePipeline(
3776
- this.pipelines,
3898
+ this.blendedPipelines(),
3777
3899
  device,
3778
3900
  layout,
3779
3901
  batch.material,
3780
3902
  batch.blend,
3781
3903
  );
3782
- const particleCommand = this.recordDraw(0, this.currentTarget());
3904
+ const particleCommand = this.recordBlended(0);
3783
3905
  if (particleCommand !== null) {
3784
3906
  particleCommand.pipeline = particlePipe;
3785
3907
  particleCommand.bindGroup = batch.bindGroup;
@@ -3924,13 +4046,13 @@ export class WebGPURenderer implements RendererApi {
3924
4046
  frags.writeFloats(fragmentSlot, atF('uTint'), plumes.tint);
3925
4047
 
3926
4048
  const plumePipe = plumePipeline(
3927
- this.pipelines,
4049
+ this.blendedPipelines(),
3928
4050
  this.surface.device,
3929
4051
  this.plumeLayout,
3930
4052
  plumes.material,
3931
4053
  plumes.blend,
3932
4054
  );
3933
- const plumeCommand = this.recordDraw(0, this.currentTarget());
4055
+ const plumeCommand = this.recordBlended(0);
3934
4056
  if (plumeCommand !== null) {
3935
4057
  plumeCommand.pipeline = plumePipe;
3936
4058
  plumeCommand.bindGroup = this.plumeBindGroup;
@@ -4485,9 +4607,13 @@ export class WebGPURenderer implements RendererApi {
4485
4607
  this.windStreakVerts.writeBlock(vertexSlot, this.windStreakVertBlock);
4486
4608
  this.windStreakFrags.writeBlock(fragmentSlot, this.windStreakFragBlock);
4487
4609
 
4488
- const windPipe = windStreakPipeline(this.pipelines, this.surface.device, this.windStreakLayout);
4610
+ const windPipe = windStreakPipeline(
4611
+ this.blendedPipelines(),
4612
+ this.surface.device,
4613
+ this.windStreakLayout,
4614
+ );
4489
4615
  /* Two uniform buffers and two vertex buffers. Nothing is sampled, so nothing is read. */
4490
- const windCommand = this.recordDraw(0, this.currentTarget());
4616
+ const windCommand = this.recordBlended(0);
4491
4617
  if (windCommand !== null) {
4492
4618
  windCommand.pipeline = windPipe;
4493
4619
  windCommand.bindGroup = this.windStreakBindGroup;
@@ -4715,8 +4841,8 @@ export class WebGPURenderer implements RendererApi {
4715
4841
  this.boltVerts.writeBlock(vertexSlot, this.boltVertBlock);
4716
4842
  this.boltFrags.writeBlock(fragmentSlot, this.boltFragInts);
4717
4843
 
4718
- const boltPipe = boltPipeline(this.pipelines, this.surface.device, this.boltLayout);
4719
- const boltCommand = this.recordDraw(0, this.currentTarget());
4844
+ const boltPipe = boltPipeline(this.blendedPipelines(), this.surface.device, this.boltLayout);
4845
+ const boltCommand = this.recordBlended(0);
4720
4846
  if (boltCommand !== null) {
4721
4847
  boltCommand.pipeline = boltPipe;
4722
4848
  boltCommand.bindGroup = this.boltBindGroup;
@@ -4853,9 +4979,14 @@ export class WebGPURenderer implements RendererApi {
4853
4979
  this.lineFrags.writeFloat(fragmentSlot, atF('uFogNear'), medium.fogNear);
4854
4980
  this.lineFrags.writeFloat(fragmentSlot, atF('uFogFar'), medium.fogFar);
4855
4981
 
4856
- const linePipe = linePipeline(this.pipelines, this.surface.device, this.lineLayout, additive);
4982
+ const linePipe = linePipeline(
4983
+ this.blendedPipelines(),
4984
+ this.surface.device,
4985
+ this.lineLayout,
4986
+ additive,
4987
+ );
4857
4988
  /* Two uniform blocks and three vertex buffers, and no attachment among them. */
4858
- const lineCommand = this.recordDraw(0, this.currentTarget());
4989
+ const lineCommand = this.recordBlended(0);
4859
4990
  if (lineCommand !== null) {
4860
4991
  lineCommand.pipeline = linePipe;
4861
4992
  lineCommand.bindGroup = this.lineBindGroup;
@@ -4900,6 +5031,8 @@ export class WebGPURenderer implements RendererApi {
4900
5031
  private readonly waterLayout: GPUBindGroupLayout;
4901
5032
  /** The one-pixel stand-in, kept so the water bind group can be rebuilt without one. */
4902
5033
  private readonly blankView: GPUTextureView;
5034
+ /** The blank texel as a one-layer array: the stand-in every surface-texture binding takes. */
5035
+ private readonly blankSurfaceView: GPUTextureView;
4903
5036
  private readonly blankSampler: GPUSampler;
4904
5037
  /**
4905
5038
  * A slot per body, not one buffer rewritten before each draw.
@@ -5078,8 +5211,39 @@ export class WebGPURenderer implements RendererApi {
5078
5211
 
5079
5212
  /* -- The shadow pass -------------------------------------------------------------- */
5080
5213
 
5081
- private readonly shadowMap: GPUTexture;
5082
- private readonly shadowView: GPUTextureView;
5214
+ /**
5215
+ * The sun's static, moving and peeled maps, as layers of one texture (`shadowMap.ts`). The three
5216
+ * views below are single layers, what each pass attaches; the two here are what is sampled.
5217
+ */
5218
+ private sunShadowTexture: GPUTexture;
5219
+ /** Every layer, for the lit pass, the light volumes and the medium. */
5220
+ private sunArrayView: GPUTextureView;
5221
+ /**
5222
+ * The static layer alone, as an array, for the peel: it draws into another layer of the same
5223
+ * texture, and a view covering that layer too would put it in two usages in one pass.
5224
+ */
5225
+ private sunStaticArrayView: GPUTextureView;
5226
+ /** How many layers the sun's texture holds: its maps, and the two glass layers once glass casts. */
5227
+ private sunLayerCount: number;
5228
+ /** The glass depth layers, static then moving, once glass has cast. See `growSunForGlass`. */
5229
+ private sunGlassViews: readonly [GPUTextureView, GPUTextureView] | null = null;
5230
+ /** The texture a growth replaced, destroyed once the encoder that copied out of it is submitted. */
5231
+ private retiredSunTexture: GPUTexture | null = null;
5232
+ /** What the sun's light keeps through glass: two `rgba8unorm` layers, made when glass first casts. */
5233
+ private sunTint: SunTintTarget | null = null;
5234
+ /** The tint pass's layouts, per-draw block, groups and pipelines, made when glass first casts. */
5235
+ private glassTint: GpuGlassTint | null = null;
5236
+ /** The tint pass open while the glass list is replayed into it. */
5237
+ private tintPass: GPURenderPassEncoder | null = null;
5238
+ /** True while the glass list is replayed into a glass depth layer through `casterSink`. */
5239
+ private drawingGlassDepth = false;
5240
+ /** Whether the open sun pass is the movers' layer, whose glass goes to the moving glass layer. */
5241
+ private shadowLayerIsDynamic = false;
5242
+ /** The glass tints' sampler: filtering, and between levels, for frost's coarser reads. */
5243
+ private readonly tintSampler: GPUSampler;
5244
+ /** The light a tint pass is drawn for: xyz a direction (w 0) or a position (w 1). */
5245
+ private readonly glassLight = new Float32Array(4);
5246
+ private shadowView: GPUTextureView;
5083
5247
  private readonly shadowLayout: GPUBindGroupLayout;
5084
5248
  /** The same layout plus the joint palette. See `createDepthBindGroupLayout`. */
5085
5249
  private readonly shadowSkinnedLayout: GPUBindGroupLayout;
@@ -5105,8 +5269,7 @@ export class WebGPURenderer implements RendererApi {
5105
5269
  * read twice — so a profile that switches peeling off gets no map and no pass on either
5106
5270
  * backend rather than a map nothing writes.
5107
5271
  */
5108
- private readonly peelMap: GPUTexture | null;
5109
- private readonly peelView: GPUTextureView | null;
5272
+ private peelView: GPUTextureView | null;
5110
5273
  /**
5111
5274
  * The movers' own layer: the character, the rope it swings on, and anything else that walks.
5112
5275
  *
@@ -5127,10 +5290,9 @@ export class WebGPURenderer implements RendererApi {
5127
5290
  * and therefore as lit — so nothing looked broken, only empty, which is why it survived the
5128
5291
  * port and every gate in it.
5129
5292
  */
5130
- private readonly dynamicMap: GPUTexture | null;
5131
- private readonly dynamicView: GPUTextureView | null;
5293
+ private dynamicView: GPUTextureView | null;
5132
5294
  /** The peel's own bind groups: `uPeelShadowLayer` at 1, and the static map to peel against. */
5133
- private readonly shadowPeelBindGroup: GPUBindGroup | null;
5295
+ private shadowPeelBindGroup: GPUBindGroup | null;
5134
5296
  /**
5135
5297
  * The cutout casters' layout, and a bind group per cutout map for each layer, built the first
5136
5298
  * time a map casts and kept: a `createBindGroup` per draw is the allocation the house rules forbid.
@@ -5142,7 +5304,7 @@ export class WebGPURenderer implements RendererApi {
5142
5304
  private readonly cutoutPeelGroups = new Map<GpuSurfaceTexture, GPUBindGroup>();
5143
5305
  private shadowPeelPassUniforms: GPUBuffer | null = null;
5144
5306
  private shadowPeelSampler: GPUSampler | null = null;
5145
- private readonly scatterDepthPeelBindGroup: GPUBindGroup | null;
5307
+ private scatterDepthPeelBindGroup: GPUBindGroup | null;
5146
5308
  /** Which layer the open pass is writing, so the sink picks the bindings that match it. */
5147
5309
  private shadowLayerIsPeel = false;
5148
5310
  /**
@@ -5201,7 +5363,7 @@ export class WebGPURenderer implements RendererApi {
5201
5363
  const held = groups.get(texture);
5202
5364
  if (held !== undefined) return held;
5203
5365
  const passUniforms = peel ? this.shadowPeelPassUniforms : this.shadowPassUniforms;
5204
- const previousView = peel ? this.shadowView : this.shadowBlankView;
5366
+ const previousView = peel ? this.sunStaticArrayView : this.shadowBlankView;
5205
5367
  const previousSampler = peel ? this.shadowPeelSampler : this.shadowBlankSampler;
5206
5368
  if (passUniforms === null || previousView === null || previousSampler === null) return null;
5207
5369
  const group = createDepthBindGroup(
@@ -5279,8 +5441,23 @@ export class WebGPURenderer implements RendererApi {
5279
5441
  * Built once and held, rather than made per call: it closes over `this` and a fresh one
5280
5442
  * every frame would be an allocation in a per-frame path.
5281
5443
  */
5444
+ /** The glass the open shadow pass was offered, kept for its own passes: `glassCasters.ts`. */
5445
+ private readonly glassCasters = new GlassCasterList();
5446
+ /** One resolution every glass caster is read through, so recording one allocates nothing. */
5447
+ private readonly glassCasterScratch: ResolvedGlass = {
5448
+ transmission: 0,
5449
+ frost: 0,
5450
+ tint: [1, 1, 1],
5451
+ };
5282
5452
  private readonly casterSink: ShadowCasterSink = {
5283
5453
  mesh: (mesh, model, material) => {
5454
+ if (!this.drawingGlassDepth && !castsDepth(material)) {
5455
+ /* Glass: kept aside for its own two passes rather than dropped. See `glassCasters.ts`. */
5456
+ if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
5457
+ this.glassCasters.recordMesh(mesh, model, material, this.glassCasterScratch);
5458
+ }
5459
+ return;
5460
+ }
5284
5461
  const pass = this.shadowPass;
5285
5462
  if (pass === null) return;
5286
5463
  /* A cutout caster's group is looked up before a slot is spent, so a miss costs nothing. */
@@ -5330,6 +5507,12 @@ export class WebGPURenderer implements RendererApi {
5330
5507
  * uses — one draw, however many instances, exactly as the colour pass does it.
5331
5508
  */
5332
5509
  instanced: (batch, data, material) => {
5510
+ if (!this.drawingGlassDepth && !castsDepth(material)) {
5511
+ if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
5512
+ this.glassCasters.recordInstanced(batch, data, material, this.glassCasterScratch);
5513
+ }
5514
+ return;
5515
+ }
5333
5516
  const pass = this.shadowPass;
5334
5517
  if (pass === null) return;
5335
5518
  const gpuBatch = batch as GpuInstancedBatch;
@@ -5378,7 +5561,13 @@ export class WebGPURenderer implements RendererApi {
5378
5561
  pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
5379
5562
  pass.drawIndexed(geometry.indexCount, count);
5380
5563
  },
5381
- skinnedMesh: (mesh, model, palette) => {
5564
+ skinnedMesh: (mesh, model, palette, material) => {
5565
+ if (!this.drawingGlassDepth && !castsDepth(material)) {
5566
+ if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
5567
+ this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
5568
+ }
5569
+ return;
5570
+ }
5382
5571
  const pass = this.shadowPass;
5383
5572
  if (pass === null) return;
5384
5573
  /* Its own slot, so two characters casting into one bake do not both take the second one's
@@ -5762,10 +5951,13 @@ export class WebGPURenderer implements RendererApi {
5762
5951
  * nothing at all under the default profile, which is on. The cache carries it for the same
5763
5952
  * reason it carries the sample count: no pass can then disagree with the attachment.
5764
5953
  */
5954
+ /* Off compiles none of the glass lookup into a lit pipeline: see `PipelineCache.glassShadows`. */
5955
+ const glassShadows = quality.glassShadows !== 'off';
5765
5956
  this.pipelines = new PipelineCache(
5766
5957
  device,
5767
5958
  quality.screenEffects ? sceneColorFormat(quality.hdrScene) : surface.format,
5768
5959
  this.samples,
5960
+ glassShadows,
5769
5961
  );
5770
5962
  /*
5771
5963
  * The canvas' own state, for the pass `ensurePass` reopens after `endFrame`.
@@ -5778,7 +5970,12 @@ export class WebGPURenderer implements RendererApi {
5778
5970
  this.overlayPipelines =
5779
5971
  this.pipelines.format === surface.format && this.samples === 1
5780
5972
  ? this.pipelines
5781
- : new PipelineCache(device, surface.format, 1);
5973
+ : new PipelineCache(device, surface.format, 1, glassShadows);
5974
+ this.latePipelines =
5975
+ this.pipelines.format === RECON_HISTORY_FORMAT && this.samples === 1
5976
+ ? this.pipelines
5977
+ : new PipelineCache(device, RECON_HISTORY_FORMAT, 1, glassShadows);
5978
+ this.issueBound = this.issueCommand.bind(this);
5782
5979
  this.pipelineTargets =
5783
5980
  this.overlayPipelines === this.pipelines
5784
5981
  ? [this.pipelines]
@@ -5866,6 +6063,9 @@ export class WebGPURenderer implements RendererApi {
5866
6063
  * `uStaticShadowMap` rather than by anything here changing shape.
5867
6064
  */
5868
6065
  const blankView = blank.createView();
6066
+ /* The same texel as a one-layer array, for the four material maps, which are arrays now. */
6067
+ const blankSurfaceView = blank.createView({ dimension: '2d-array' });
6068
+ this.blankSurfaceView = blankSurfaceView;
5869
6069
  const blankSampler = device.createSampler({ label: 'flat.sampler' });
5870
6070
 
5871
6071
  /*
@@ -5929,42 +6129,35 @@ export class WebGPURenderer implements RendererApi {
5929
6129
  const blankDriftIndexView = blankVolume('flat.emptyDriftLightIndex', 'r32uint');
5930
6130
  const blankDriftAtlasView = blankVolume('flat.emptyDriftLightAtlas', 'rgba16float');
5931
6131
 
5932
- this.shadowMap = device.createTexture({
5933
- label: 'shadow.static',
5934
- size: [this.quality.directionalShadowMapSize, this.quality.directionalShadowMapSize],
6132
+ /*
6133
+ * The static world, its movers and — where more than one static depth layer is asked for —
6134
+ * the peel, as layers of one texture, on the conditions `renderer.ts` builds its array on: one
6135
+ * decision read twice. Same size and format throughout, because the shader samples every
6136
+ * layer with one \`uShadowMapSize\` and one set of filter offsets.
6137
+ */
6138
+ const sunLayers = this.quality.directionalShadows
6139
+ ? this.quality.directionalShadowDepthLayers > 1
6140
+ ? 3
6141
+ : 2
6142
+ : 1;
6143
+ this.sunShadowTexture = device.createTexture({
6144
+ label: 'shadow.sun',
6145
+ size: [
6146
+ this.quality.directionalShadowMapSize,
6147
+ this.quality.directionalShadowMapSize,
6148
+ sunLayers,
6149
+ ],
5935
6150
  format: SHADOW_FORMAT,
5936
- usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
6151
+ /* Copyable both ways, because the first glass grows it by copying every layer across. */
6152
+ usage: SUN_USAGE,
5937
6153
  });
5938
- this.shadowView = this.shadowMap.createView();
5939
- /*
5940
- * The peel layer, on the condition `renderer.ts` builds its own on: more than one depth
5941
- * layer asked for. `depth.ts` fills it by discarding every fragment at or in front of what
5942
- * the static pass stored, so what lands here is the second independently fading occluder.
5943
- */
5944
- this.peelMap =
5945
- this.quality.directionalShadowDepthLayers > 1
5946
- ? device.createTexture({
5947
- label: 'shadow.peel',
5948
- size: [this.quality.directionalShadowMapSize, this.quality.directionalShadowMapSize],
5949
- format: SHADOW_FORMAT,
5950
- usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
5951
- })
5952
- : null;
5953
- this.peelView = this.peelMap === null ? null : this.peelMap.createView();
5954
- /*
5955
- * The movers' layer, on the condition `renderer.ts` builds its own on: directional shadows
5956
- * at all. Same size and same format as the static map, because the shader samples all three
5957
- * with one `uShadowMapSize` and one set of PCF offsets.
5958
- */
5959
- this.dynamicMap = this.quality.directionalShadows
5960
- ? device.createTexture({
5961
- label: 'shadow.dynamic',
5962
- size: [this.quality.directionalShadowMapSize, this.quality.directionalShadowMapSize],
5963
- format: SHADOW_FORMAT,
5964
- usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
5965
- })
5966
- : null;
5967
- this.dynamicView = this.dynamicMap === null ? null : this.dynamicMap.createView();
6154
+ this.sunLayerCount = sunLayers;
6155
+ const views = sunViewsOf(this.sunShadowTexture, sunLayers, sunLayers);
6156
+ this.sunArrayView = views.array;
6157
+ this.sunStaticArrayView = views.staticArray;
6158
+ this.shadowView = views.static;
6159
+ this.dynamicView = views.dynamic;
6160
+ this.peelView = views.peel;
5968
6161
 
5969
6162
  /*
5970
6163
  * **Cleared once, here, because a texture no pass has written reads as zero**, which the
@@ -6005,6 +6198,14 @@ export class WebGPURenderer implements RendererApi {
6005
6198
  * rather than the hardware's.
6006
6199
  */
6007
6200
  const shadowSampler = device.createSampler({ label: 'shadow.sampler' });
6201
+ this.tintSampler = device.createSampler({
6202
+ label: 'shadow.tintSampler',
6203
+ magFilter: 'linear',
6204
+ minFilter: 'linear',
6205
+ mipmapFilter: 'linear',
6206
+ addressModeU: 'clamp-to-edge',
6207
+ addressModeV: 'clamp-to-edge',
6208
+ });
6008
6209
  /*
6009
6210
  * The photometric atlas's own sampler, **created here rather than reusing `postSampler`.**
6010
6211
  *
@@ -6026,14 +6227,17 @@ export class WebGPURenderer implements RendererApi {
6026
6227
  });
6027
6228
 
6028
6229
  this.flatTextures = (name) => {
6029
- if (name === 'uStaticShadowMap') return { view: this.shadowView, sampler: shadowSampler };
6030
- /*
6031
- * The movers' layer. Cleared at construction like the others, so a profile with
6032
- * directional shadows on that never opens the layer reads the far plane and therefore
6033
- * "nothing moving is in the way" — not whatever the allocation happened to contain.
6034
- */
6035
- if (name === 'uDynamicShadowMap' && this.dynamicView !== null) {
6036
- return { view: this.dynamicView, sampler: shadowSampler };
6230
+ if (name === 'uSunShadows') return { view: this.sunArrayView, sampler: shadowSampler };
6231
+ /* What the sun keeps through glass, or the one-texel stand-in the lookup reads as none. */
6232
+ if (name === 'uSunGlassTints') {
6233
+ return { view: this.sunTint?.array ?? blankArrayView, sampler: this.tintSampler };
6234
+ }
6235
+ /* What the lamps keep through glass, one layer a light, or the same stand-in. */
6236
+ if (name === 'uPointGlassTints') {
6237
+ return {
6238
+ view: this.pointShadowArray?.glassTint?.view ?? blankArrayView,
6239
+ sampler: this.tintSampler,
6240
+ };
6037
6241
  }
6038
6242
  /*
6039
6243
  * The caller's image, or the stand-in. Read from the field rather than passed in, because
@@ -6048,7 +6252,7 @@ export class WebGPURenderer implements RendererApi {
6048
6252
  if (name === 'uNormalMap') {
6049
6253
  const map = this.normalMap;
6050
6254
  return map === null
6051
- ? { view: blankView, sampler: blankSampler }
6255
+ ? { view: blankSurfaceView, sampler: blankSampler }
6052
6256
  : { view: map.view, sampler: map.sampler };
6053
6257
  }
6054
6258
  /* The caller's ORM map, or the stand-in, for the reason `uNormalMap` gives just above. */
@@ -6056,24 +6260,25 @@ export class WebGPURenderer implements RendererApi {
6056
6260
  if (name === 'uEmissiveMap') {
6057
6261
  const map = this.emissiveMap;
6058
6262
  return map === null
6059
- ? { view: blankView, sampler: blankSampler }
6263
+ ? { view: blankSurfaceView, sampler: blankSampler }
6060
6264
  : { view: map.view, sampler: map.sampler };
6061
6265
  }
6062
6266
  if (name === 'uOrmMap') {
6063
6267
  const map = this.ormMap;
6064
6268
  return map === null
6065
- ? { view: blankView, sampler: blankSampler }
6269
+ ? { view: blankSurfaceView, sampler: blankSampler }
6066
6270
  : { view: map.view, sampler: map.sampler };
6067
6271
  }
6068
6272
  if (name === 'uAlbedo') {
6069
6273
  const albedo = this.albedo;
6070
6274
  return albedo === null
6071
- ? { view: blankView, sampler: blankSampler }
6275
+ ? { view: blankSurfaceView, sampler: blankSampler }
6072
6276
  : { view: albedo.view, sampler: albedo.sampler };
6073
6277
  }
6074
- /* The peel, where the profile asked for one. `uPeeledShadowEnabled` gates the sample. */
6075
- if (name === 'uPeeledShadowMap' && this.peelView !== null) {
6076
- return { view: this.peelView, sampler: shadowSampler };
6278
+ /* The albedo's effects table, or a one-texel stand-in the shader's size test reads as none.
6279
+ The albedo is the group's key, so its table needs no key of its own. */
6280
+ if (name === 'uSurfaceEffects') {
6281
+ return { view: this.albedo?.effectsView ?? blankView, sampler: blankSampler };
6077
6282
  }
6078
6283
  /*
6079
6284
  * The room, once a bake has filled it. Unbaked reads as the blank cube: nothing to mirror.
@@ -6106,18 +6311,8 @@ export class WebGPURenderer implements RendererApi {
6106
6311
  * this one is a curve sampled between two angles rather than data addressed by index, so
6107
6312
  * interpolation across a row is the point. `postSampler` is linear and clamped.
6108
6313
  */
6109
- if (name === 'uIesAtlas') {
6110
- return { view: this.iesView ?? blankView, sampler: iesSampler };
6111
- }
6112
- /*
6113
- * The cookie atlas, or the single white texel a scene with no cookie falls back to.
6114
- *
6115
- * A filtering sampler, like the photometric atlas and unlike the froxel table: a cookie is
6116
- * an image and its whole point is that it is soft at the edges. The shader's own half-texel
6117
- * inset is what keeps one tile out of the next; a clamp cannot, the seam being interior.
6118
- */
6119
- if (name === 'uCookieAtlas') {
6120
- return { view: this.cookieView ?? blankView, sampler: iesSampler };
6314
+ if (name === 'uFixtureAtlas') {
6315
+ return { view: this.fixtureAtlasView(), sampler: iesSampler };
6121
6316
  }
6122
6317
  /*
6123
6318
  * DriftLight's brick index, read by `texelFetch` alone, so it shares the shadow group's
@@ -6143,7 +6338,15 @@ export class WebGPURenderer implements RendererApi {
6143
6338
  * texels by construction, an offset that snapped to one being no offset at all.
6144
6339
  */
6145
6340
  if (name === 'uRefractScene') {
6146
- return { view: this.refractSnapshotView ?? blankView, sampler: iesSampler };
6341
+ this.refractSampler ??= device.createSampler({
6342
+ label: 'refract.sampler',
6343
+ magFilter: 'linear',
6344
+ minFilter: 'linear',
6345
+ mipmapFilter: 'linear',
6346
+ addressModeU: 'clamp-to-edge',
6347
+ addressModeV: 'clamp-to-edge',
6348
+ });
6349
+ return { view: this.refractSnapshotView ?? blankView, sampler: this.refractSampler };
6147
6350
  }
6148
6351
  /*
6149
6352
  * Every point light's shadow, in one binding.
@@ -6369,9 +6572,7 @@ export class WebGPURenderer implements RendererApi {
6369
6572
  */
6370
6573
  this.mediumLayout = createPostStageLayout(device, MEDIUM_UNIFORMS, MEDIUM_FRAG_SIZE, [
6371
6574
  { binding: MEDIUM_TEXTURES.uDepth, filterable: false },
6372
- { binding: MEDIUM_TEXTURES.uStaticShadowMap, filterable: false },
6373
- { binding: MEDIUM_TEXTURES.uPeeledShadowMap, filterable: false },
6374
- { binding: MEDIUM_TEXTURES.uDynamicShadowMap, filterable: false },
6575
+ { binding: MEDIUM_TEXTURES.uSunShadows, filterable: false, dimension: '2d-array' },
6375
6576
  ]);
6376
6577
  this.mediumUniforms = device.createBuffer({
6377
6578
  label: 'post.mediumUniforms',
@@ -6603,7 +6804,7 @@ export class WebGPURenderer implements RendererApi {
6603
6804
  this.scatterDepthDraws.buffer,
6604
6805
  { binding: DEPTH_FRAG_BINDING, buffer: depthPassUniforms, size: DEPTH_FRAG_SIZE },
6605
6806
  DEPTH_PREVIOUS_BINDING,
6606
- blankView,
6807
+ blankArrayView,
6607
6808
  blankSampler,
6608
6809
  );
6609
6810
  this.shadowBindGroup = createDepthBindGroup(
@@ -6611,10 +6812,10 @@ export class WebGPURenderer implements RendererApi {
6611
6812
  this.shadowLayout,
6612
6813
  this.shadowDraws.buffer,
6613
6814
  depthPassUniforms,
6614
- blankView,
6815
+ blankArrayView,
6615
6816
  blankSampler,
6616
6817
  );
6617
- this.shadowBlankView = blankView;
6818
+ this.shadowBlankView = blankArrayView;
6618
6819
  this.shadowBlankSampler = blankSampler;
6619
6820
  this.shadowPassUniforms = depthPassUniforms;
6620
6821
 
@@ -6652,7 +6853,7 @@ export class WebGPURenderer implements RendererApi {
6652
6853
  this.shadowLayout,
6653
6854
  this.shadowDraws.buffer,
6654
6855
  peelPassUniforms,
6655
- this.shadowView,
6856
+ this.sunStaticArrayView,
6656
6857
  shadowSampler,
6657
6858
  );
6658
6859
  this.scatterDepthPeelBindGroup =
@@ -6664,7 +6865,7 @@ export class WebGPURenderer implements RendererApi {
6664
6865
  this.scatterDepthDraws.buffer,
6665
6866
  { binding: DEPTH_FRAG_BINDING, buffer: peelPassUniforms, size: DEPTH_FRAG_SIZE },
6666
6867
  DEPTH_PREVIOUS_BINDING,
6667
- this.shadowView,
6868
+ this.sunStaticArrayView,
6668
6869
  shadowSampler,
6669
6870
  );
6670
6871
 
@@ -6964,6 +7165,8 @@ export class WebGPURenderer implements RendererApi {
6964
7165
  if (!attribute.optional) continue;
6965
7166
  present[String(attribute.name)] = key.includes(`:${String(attribute.name)}`);
6966
7167
  }
7168
+ /* Not a layout entry of its own — it widens `uvs` — so read from the key beside them. */
7169
+ present['layers'] = key.includes(':layers');
6967
7170
  return present;
6968
7171
  }
6969
7172
 
@@ -6976,6 +7179,7 @@ export class WebGPURenderer implements RendererApi {
6976
7179
  * uncaught exceptions a second in somebody's error reporting.
6977
7180
  */
6978
7181
  beginFrame(clearColor: Vec3): void {
7182
+ this.cullFrame += 1;
6979
7183
  /* Whatever an update replaced during the last frame; nothing recorded can read it now. */
6980
7184
  for (const texture of this.retiredTextures) texture.destroy();
6981
7185
  this.retiredTextures.length = 0;
@@ -7070,7 +7274,10 @@ export class WebGPURenderer implements RendererApi {
7070
7274
  this.perFrame.reset();
7071
7275
  this.materials.dirty();
7072
7276
  /* Spent by `runMotion` at the end of the last frame; a frame states its movers afresh. */
7073
- this.motionCount = 0;
7277
+ this.motionPass?.reset();
7278
+ this.latePass?.reset();
7279
+ this.moverFrame += 1;
7280
+ this.inFrame = true;
7074
7281
  /*
7075
7282
  * The canvas rather than the swap chain's texture, and the difference is that reading this
7076
7283
  * does not *acquire* anything. See `swapView`.
@@ -7235,6 +7442,15 @@ export class WebGPURenderer implements RendererApi {
7235
7442
  present[String(attribute.name)] = supplied;
7236
7443
  key += supplied ? `:${String(attribute.name)}` : '';
7237
7444
  }
7445
+ /*
7446
+ * Texture-array layers widen the UV field to three floats (`LAYERED_UVS`), which is a different
7447
+ * vertex layout, so the key has to say so or a layered and a plain mesh would share a pipeline
7448
+ * and one of them would read every field after the UVs from the wrong offset.
7449
+ */
7450
+ if (data.layers !== undefined) {
7451
+ present['layers'] = true;
7452
+ key += ':layers';
7453
+ }
7238
7454
  const fullKey = `${this.variant}|${key}`;
7239
7455
  /*
7240
7456
  * **For every target this mesh can land on, which is two when the overlay is its own.**
@@ -7391,7 +7607,27 @@ export class WebGPURenderer implements RendererApi {
7391
7607
  'what is kept and what a buffer is hinted as.',
7392
7608
  );
7393
7609
  }
7394
- mesh.update(this.surface.device, positions, normals);
7610
+ /*
7611
+ * **The rows before this rewrite are last frame's**, kept only for a reconstruction and only
7612
+ * on the first rewrite of a frame — a second must not replace where the vertices were last
7613
+ * frame with where they were a moment ago. A rewrite between frames is the next frame's. See
7614
+ * `changeFrames.ts`.
7615
+ */
7616
+ const motion = mesh.motion;
7617
+ let previous: GPUBuffer | null = null;
7618
+ if (motion !== null && this.reconstructionWanted) {
7619
+ const frame = frameOfChange(this.moverFrame, this.inFrame);
7620
+ if (keepsPrevious(motion.changed, frame)) {
7621
+ motion.previous ??= this.surface.device.createBuffer({
7622
+ label: 'recon.dynamicPrevious',
7623
+ size: mesh.vertexBuffers[0]?.size ?? 0,
7624
+ usage: 0x0020 | 0x0008, // VERTEX | COPY_DST
7625
+ });
7626
+ previous = motion.previous;
7627
+ }
7628
+ motion.changed = frame;
7629
+ }
7630
+ mesh.update(this.surface.device, positions, normals, previous);
7395
7631
  }
7396
7632
 
7397
7633
  /**
@@ -7440,62 +7676,15 @@ export class WebGPURenderer implements RendererApi {
7440
7676
  * Its sequence is its own: `reconJitterPhases` takes eight positions for every output pixel a
7441
7677
  * render pixel covers, where the temporal resolve's eight are enough at one to one.
7442
7678
  */
7443
- this.reconstructing = this.reconstructionWanted && !this.reflectionPassActive;
7444
- if (this.quality.reconstruction > 0 && this.samples > 1 && !this.reconMultisampleSaid) {
7445
- this.reconMultisampleSaid = true;
7446
- console.warn(RECONSTRUCTION_MULTISAMPLE_REFUSAL);
7447
- }
7448
- if (this.reconstructing) {
7449
- /*
7450
- * The offset `settleReconJitter` chose for the frame at `beginFrame`, applied here and not
7451
- * advanced here: a frame binding its mesh pass twice is one frame of the sequence.
7452
- *
7453
- * **The y offset is negated on the way in, and that is not a preference.** This shifts the
7454
- * *corrected* matrix, whose y `CLIP_CORRECTION` has already negated so that the negation
7455
- * every generated vertex stage ends with cancels out — §3 row 56. So an offset added here
7456
- * arrives on screen with its sign reversed, while x, which nothing negates, arrives as
7457
- * given. The temporal resolve cannot see this because its shader never reads the offset; the
7458
- * reconstruction reads it at every texel it unprojects, and a sign error there moves every
7459
- * sample two jitters away from where the depth says it is.
7460
- */
7461
- this.viewProj = jitterProjection(
7462
- this.jitteredViewProj,
7463
- this.correctedViewProj,
7464
- this.reconJitter[0] as number,
7465
- -(this.reconJitter[1] as number),
7466
- this.renderWidth,
7467
- this.renderHeight,
7468
- ) as Float32Array;
7469
- this.temporalJittering = false;
7470
- this.temporalHistoryUsable = false;
7471
- }
7472
- this.temporalJittering =
7473
- !this.reconstructing &&
7474
- this.quality.temporalAa &&
7475
- this.quality.screenEffects &&
7476
- !this.reflectionPassActive;
7477
- if (this.temporalJittering) {
7478
- /* The scene's size, not the drawing buffer's: the history is a picture of the render. */
7479
- this.temporalHistoryUsable = this.temporalHistory.openFrame(
7480
- this.renderWidth,
7481
- this.renderHeight,
7482
- );
7483
- const [jx, jy] = jitterOffset(this.temporalHistory.frameIndex);
7484
- this.temporalJitterX = jx;
7485
- this.temporalJitterY = jy;
7486
- /* A jitter is half a *render* texel, which is what the sequence is spread over. */
7487
- this.viewProj = jitterProjection(
7488
- this.jitteredViewProj,
7489
- this.correctedViewProj,
7490
- jx,
7491
- jy,
7492
- this.renderWidth,
7493
- this.renderHeight,
7494
- ) as Float32Array;
7495
- } else if (!this.reconstructing) {
7496
- this.temporalHistoryUsable = false;
7497
- this.viewProj = this.correctedViewProj;
7498
- }
7679
+ /*
7680
+ * **After the present there is no jitter to add and no temporal frame to open.** A mesh pass
7681
+ * bound then is the interface's, drawn over a picture already resolved: jittered, a map and every
7682
+ * edge of it moved by a fraction of a pixel each frame with nothing to resolve it; and opening a
7683
+ * second temporal frame stepped the sequence twice a frame, so the world saw every other
7684
+ * position and the anti-flicker's period never started.
7685
+ */
7686
+ if (this.framePresented) this.viewProj = this.correctedViewProj;
7687
+ else this.jitterFrame();
7499
7688
  /*
7500
7689
  * The frustum, once a frame, and from the **uncorrected** matrix.
7501
7690
  *
@@ -7595,8 +7784,14 @@ export class WebGPURenderer implements RendererApi {
7595
7784
  * frame either way, because an unwritten one is four zeroes and `dot(p, 0) < 0` is false,
7596
7785
  * so the term would silently pass everything the first time somebody enabled it.
7597
7786
  */
7598
- i[at('uClipEnabled')] = this.reflectionPassActive ? 1 : 0;
7599
- f.set(this.reflection?.clipPlane ?? NO_CLIP_PLANE, at('uClipPlane'));
7787
+ /* The mirror's plane in a reflection pass and zero, which clips nothing, in every other: the lit
7788
+ stage has no flag beside the plane. See `uClipPlane` in `flat/preamble.ts`. */
7789
+ f.set(
7790
+ this.reflectionPassActive ? (this.reflection?.clipPlane ?? NO_CLIP_PLANE) : NO_CLIP_PLANE,
7791
+ at('uClipPlane'),
7792
+ );
7793
+ resolveSurfaceScene(env, this.surfaceScene);
7794
+ f.set(this.surfaceScene, at('uSurfaceScene'));
7600
7795
 
7601
7796
  /*
7602
7797
  * The material and grading terms, matching `renderer.ts` value for value.
@@ -7617,6 +7812,9 @@ export class WebGPURenderer implements RendererApi {
7617
7812
  */
7618
7813
  f[at('uGrain')] = 1;
7619
7814
  f[at('uReflectivity')] = 0;
7815
+ /* No weighting and no dither until a pass or a crossfade asks. See `setDitherFade`. */
7816
+ f[at('uWriteMode')] = 0;
7817
+ f[at('uWriteMode') + 1] = 0;
7620
7818
  /* 1, not 0: a multiplier's identity is one. See `setEnvironmentGain`. */
7621
7819
  f[at('uEnvironmentGain')] = 1;
7622
7820
  /*
@@ -8050,7 +8248,7 @@ export class WebGPURenderer implements RendererApi {
8050
8248
  depthLayer = 0,
8051
8249
  tint: Vec3 | null = null,
8052
8250
  /** Where it was last frame. See `RendererApi.drawMesh`, and `runMotion` for what reads it. */
8053
- previousModel: ArrayLike<number> | null = null,
8251
+ previousModel: ArrayLike<number> | Mover | null = null,
8054
8252
  ): void {
8055
8253
  /*
8056
8254
  * Skipped where the flag asks and the bounds say so.
@@ -8075,7 +8273,7 @@ export class WebGPURenderer implements RendererApi {
8075
8273
  * depth, so a motion pass drawing it would either be depth-rejected everywhere — wasted — or,
8076
8274
  * where it happened to pass, write motion for a surface no pixel of the frame belongs to.
8077
8275
  */
8078
- if (previousModel !== null) this.recordMotion(mesh, model, previousModel);
8276
+ if (this.tracksMotionNow()) this.recordDrawMotion(mesh, model, previousModel);
8079
8277
  this.submitMesh(mesh, model, tint, 1, false, { depthLayer });
8080
8278
  }
8081
8279
 
@@ -8176,7 +8374,7 @@ export class WebGPURenderer implements RendererApi {
8176
8374
  * **Recorded rather than drawn**, when the effect is on and this is not already the replay.
8177
8375
  * Everything is copied — see `TranslucentQueue`, and the scratch matrix that made it necessary.
8178
8376
  */
8179
- if (this.oitActive && !this.oitReplaying) {
8377
+ if (this.oitActive && !this.oitReplaying && options.additive !== true) {
8180
8378
  this.translucentQueue.record(mesh, model, opacity, options);
8181
8379
  return;
8182
8380
  }
@@ -8238,7 +8436,16 @@ export class WebGPURenderer implements RendererApi {
8238
8436
  return;
8239
8437
  }
8240
8438
 
8241
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, this.viewProj);
8439
+ /* A blended draw landing after the upscale is drawn into an unjittered picture. */
8440
+ /* Unless it moves with what it lies on, which the resolve reconstructs it with. See
8441
+ `TranslucentMeshOptions.reconstructed`. */
8442
+ const lands = blend && options.reconstructed !== true && this.drawsLate();
8443
+ const late = lands || (blend && this.oitReplaying && this.reconstructing);
8444
+ this.perDraw.writeFloats(
8445
+ slot,
8446
+ FLAT_VERT_FIELDS.uViewProj.offset,
8447
+ late ? this.correctedViewProj : this.viewProj,
8448
+ );
8242
8449
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uModel.offset, model);
8243
8450
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uTint.offset, tint ?? WHITE);
8244
8451
  /* The frame's wind, for a mesh carrying a sway lane. Written on this path and not the
@@ -8272,17 +8479,18 @@ export class WebGPURenderer implements RendererApi {
8272
8479
  this.perFrameFloats[this.materialField('uOpacity')] = opacity;
8273
8480
  /* Material state exactly like `uOpacity`, dirtied for the accumulation pass and restored
8274
8481
  below — the revealage pass draws with it at zero and its colour multiplied away. */
8275
- this.perFrameFloats[this.materialField('uOitWeighted')] = this.oitMode === 'accum' ? 1 : 0;
8482
+ this.perFrameFloats[this.materialField('uWriteMode')] = this.oitMode === 'accum' ? 1 : 0;
8276
8483
  }
8277
8484
  /* Same shape as `dimmed`: a slot is only worth taking when either differs from the default
8278
8485
  `bindMeshPass` already wrote, and both are put back together once the draw is submitted. */
8279
8486
  const lit = options.lit ?? true;
8280
- const fog = options.fog ?? true;
8487
+ /* Added light fades in the medium rather than receding into it. See `drawFog.ts`. */
8488
+ const fog = fogModeOf(options.fog ?? true, blend && options.additive === true);
8281
8489
  const toneMapped = options.toneMapped ?? true;
8282
- const unlitOrUnfogged = !lit || !fog;
8490
+ const unlitOrUnfogged = !lit || fog !== FOG_RECEDE;
8283
8491
  if (unlitOrUnfogged) {
8284
8492
  if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
8285
- if (!fog) this.perFrameInts[this.materialField('uFogEnabled')] = 0;
8493
+ this.perFrameInts[this.materialField('uFogEnabled')] = fog;
8286
8494
  }
8287
8495
  /* `1` is sRGB alone: the conversion without the curve. `Math.min` rather than a literal, so
8288
8496
  a renderer asked for `none` stays at none. See `TranslucentMeshOptions.toneMapped`. */
@@ -8298,17 +8506,7 @@ export class WebGPURenderer implements RendererApi {
8298
8506
  * shades as an ordinary translucent one rather than sampling a texture holding the frame
8299
8507
  * before last. That is the defined state the two-backends rule asks for.
8300
8508
  */
8301
- const refraction = options.refraction ?? 0;
8302
- const refracting = refraction > 0 && this.takeRefractSnapshot();
8303
- if (refracting) {
8304
- this.perFrameFloats[this.materialField('uRefractStrength')] = refraction;
8305
- const refractTint = options.refractTint;
8306
- const tintAt = this.materialField('uRefractTint');
8307
- this.perFrameFloats[tintAt] = refractTint?.[0] ?? 1;
8308
- this.perFrameFloats[tintAt + 1] = refractTint?.[1] ?? 1;
8309
- this.perFrameFloats[tintAt + 2] = refractTint?.[2] ?? 1;
8310
- this.perFrameFloats[this.materialField('uRefractThickness')] = options.thicknessM ?? 0;
8311
- }
8509
+ const refracting = this.bindSeeThrough(options);
8312
8510
  /* Taken for itself when any of those differs from the pass, and put back below: the rule both
8313
8511
  backends count material changes by. See `materialChanges.ts`. */
8314
8512
  const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
@@ -8337,8 +8535,10 @@ export class WebGPURenderer implements RendererApi {
8337
8535
  */
8338
8536
  const depthWrite = options.depthWrite ?? true;
8339
8537
  const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
8538
+ /* Added rather than blended over: its own blend state, so its own pipeline. */
8539
+ const adds = blend && options.additive === true;
8340
8540
  const key =
8341
- `${keyed}${blend ? '|blend' : ''}` +
8541
+ `${keyed}${blend ? (adds ? '|add' : '|blend') : ''}` +
8342
8542
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
8343
8543
  /* The two order-independent buffers are two formats and two blend states, so a draw into
8344
8544
  one must never share a cached pipeline with the same mesh drawn into the other. */
@@ -8368,7 +8568,7 @@ export class WebGPURenderer implements RendererApi {
8368
8568
  * pays the compile it was trying to avoid. That is a slow frame; the alternative is a
8369
8569
  * thrown error in the middle of one.
8370
8570
  */
8371
- const pipelines = this.targetPipelines();
8571
+ const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
8372
8572
  let pipeline = pipelines.peek(key);
8373
8573
  if (pipeline === undefined) {
8374
8574
  const present = this.meshPresent.get(base);
@@ -8382,7 +8582,7 @@ export class WebGPURenderer implements RendererApi {
8382
8582
  this.variant,
8383
8583
  key,
8384
8584
  present,
8385
- blend,
8585
+ adds ? 'additive' : blend,
8386
8586
  skinned,
8387
8587
  morphed,
8388
8588
  depthWrite,
@@ -8416,7 +8616,9 @@ export class WebGPURenderer implements RendererApi {
8416
8616
  * the uniform rings are the state arena this design would otherwise have had to build —
8417
8617
  * so the command carries them as its two dynamic offsets.
8418
8618
  */
8419
- const command = this.recordDraw(0, this.currentTarget());
8619
+ const command = blend
8620
+ ? this.recordBlended(0, lands)
8621
+ : this.recordDraw(0, this.currentTarget());
8420
8622
  if (command !== null) {
8421
8623
  command.pipeline = pipeline;
8422
8624
  command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
@@ -8447,17 +8649,15 @@ export class WebGPURenderer implements RendererApi {
8447
8649
  whether or not this one found a slot to draw with. */
8448
8650
  if (dimmed) {
8449
8651
  this.perFrameFloats[this.materialField('uOpacity')] = 1;
8450
- this.perFrameFloats[this.materialField('uOitWeighted')] = 0;
8652
+ this.perFrameFloats[this.materialField('uWriteMode')] = 0;
8451
8653
  }
8452
8654
  if (unlitOrUnfogged) {
8453
8655
  if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
8454
- if (!fog) this.perFrameInts[this.materialField('uFogEnabled')] = 1;
8455
- }
8456
- if (refracting) {
8457
- /* Back to off for every other draw in the frame, exactly as `uOpacity` is: this scratch is
8458
- shared, so a strength left set is worn by everything drawn after it. */
8459
- this.perFrameFloats[this.materialField('uRefractStrength')] = 0;
8656
+ this.perFrameInts[this.materialField('uFogEnabled')] = FOG_RECEDE;
8460
8657
  }
8658
+ /* Back to off for every other draw in the frame, exactly as `uOpacity` is: this scratch is
8659
+ shared, so a strength left set is worn by everything drawn after it. */
8660
+ if (refracting) this.releaseSeeThrough();
8461
8661
  if (!toneMapped) {
8462
8662
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
8463
8663
  }
@@ -8479,7 +8679,11 @@ export class WebGPURenderer implements RendererApi {
8479
8679
  * anyway, because WebGL2 *cannot* and a capability one backend has and the other does not is
8480
8680
  * the shape this repository spent a week removing.
8481
8681
  */
8482
- createInstanced(mesh: GpuMesh, capacity: number): GpuInstancedBatch {
8682
+ createInstanced(
8683
+ mesh: GpuMesh,
8684
+ capacity: number,
8685
+ options: InstancedOptions = {},
8686
+ ): GpuInstancedBatch {
8483
8687
  /*
8484
8688
  * **A skinned mesh cannot be instanced, and the reason is the vertex layout rather than the
8485
8689
  * shader.** An instanced pipeline reclaims locations 11 and 12 from the joint indices and
@@ -8509,34 +8713,53 @@ export class WebGPURenderer implements RendererApi {
8509
8713
  'instanced pipeline.',
8510
8714
  );
8511
8715
  }
8512
- if (this.instanced.has(mesh)) {
8513
- throw new Error(
8514
- 'WebGPU: this mesh already has an instanced batch — a mesh may have one, because the ' +
8515
- "other backend binds the attributes to the mesh's own vertex array.",
8516
- );
8517
- }
8518
8716
  const batch = new GpuInstancedBatch(
8519
8717
  this.surface.device,
8520
8718
  mesh,
8521
8719
  capacity,
8522
8720
  `flat.instances:${(mesh as GpuMesh & { key?: string }).key ?? 'mesh'}`,
8721
+ options.cull ?? false,
8523
8722
  );
8524
- this.instanced.set(mesh, batch);
8525
8723
  return batch;
8526
8724
  }
8527
8725
 
8528
- /** Which meshes already carry a batch, so a second is refused rather than silently shared. */
8529
- private readonly instanced = new Map<GpuMesh, GpuInstancedBatch>();
8726
+ /** The device instance culls, built on the first culling draw. See `InstanceCullPass`. */
8727
+ private instanceCulls: InstanceCullPass | null = null;
8728
+ /** Counts frames, so a batch knows when its cull slots were last taken. */
8729
+ private cullFrame = 0;
8530
8730
 
8531
8731
  /** Push placement and colour. Only the live prefix; see `GpuInstancedBatch.upload`. */
8532
8732
  uploadInstanced(batch: GpuInstancedBatch, data: MeshInstances): void {
8533
8733
  if (this.surface.lost) return;
8534
- batch.upload(this.surface.device.queue, data);
8734
+ /*
8735
+ * **The placement before this upload is last frame's**, kept only for a reconstruction and only
8736
+ * on a frame's first upload, for the reason `updateMesh` gives. See `changeFrames.ts`.
8737
+ */
8738
+ let previous: GPUBuffer | null = null;
8739
+ if (this.reconstructionWanted) {
8740
+ const frame = frameOfChange(this.moverFrame, this.inFrame);
8741
+ if (keepsPrevious(batch.changed, frame)) {
8742
+ batch.previousBuffer ??= this.surface.device.createBuffer({
8743
+ label: 'recon.instancesPrevious',
8744
+ size: batch.capacity * INSTANCE_STRIDE,
8745
+ usage: 0x0020 | 0x0008, // VERTEX | COPY_DST
8746
+ });
8747
+ previous = batch.previousBuffer;
8748
+ }
8749
+ batch.changed = frame;
8750
+ }
8751
+ batch.upload(this.surface.device.queue, data, previous);
8535
8752
  }
8536
8753
 
8537
8754
  /** Draw every live instance, opaque. */
8538
8755
  drawInstanced(batch: GpuInstancedBatch, data: MeshInstances): void {
8539
8756
  this.submitInstanced(batch, data, 1, false, {});
8757
+ /* A batch that moved this frame pairs each slot with last frame's; the batch is the identity. */
8758
+ const previous = batch.previousBuffer;
8759
+ if (previous !== null && batch.changed === this.moverFrame && this.tracksMotionNow()) {
8760
+ const paired = pairedInstances(batch.previousCount, Math.min(data.count, batch.capacity));
8761
+ if (paired > 0) this.motionPass?.recordInstanced(batch.mesh, batch.buffer, previous, paired);
8762
+ }
8540
8763
  }
8541
8764
 
8542
8765
  /** Draw every live instance, blended. See `drawTranslucentMesh` for what the options mean. */
@@ -8551,10 +8774,48 @@ export class WebGPURenderer implements RendererApi {
8551
8774
 
8552
8775
  /** Release the placement. The mesh is the caller's and is not released. */
8553
8776
  disposeInstanced(batch: GpuInstancedBatch): void {
8554
- this.instanced.delete(batch.mesh);
8555
8777
  batch.dispose();
8556
8778
  }
8557
8779
 
8780
+ /**
8781
+ * Write what a translucent draw shows through it into the material scratch: refraction's bend and
8782
+ * absorption, and glass. Returns whether the frame's copy is there to read, and so whether
8783
+ * `releaseSeeThrough` owes the scratch back.
8784
+ *
8785
+ * **A glass draw takes the copy even when it does not bend**, since a pane shows what is behind it
8786
+ * either way. A copy that cannot be taken — a mirror, a probe bake — leaves both off, so the pane
8787
+ * shades as ordinary translucency rather than sampling a stand-in. See `glass.ts`.
8788
+ */
8789
+ private bindSeeThrough(options: TranslucentMeshOptions): boolean {
8790
+ const refraction = options.refraction ?? 0;
8791
+ const glassy = resolveGlass(options.glass, this.glassScratch);
8792
+ if ((refraction <= 0 && !glassy) || !this.takeRefractSnapshot()) return false;
8793
+ const glass = this.glassScratch;
8794
+ /* Strength, path thickness, transmission, frost: one vector, for the budget the shader states. */
8795
+ const seeAt = this.materialField('uSeeThrough');
8796
+ this.perFrameFloats[seeAt] = Math.max(refraction, 0);
8797
+ this.perFrameFloats[seeAt + 1] = options.thicknessM ?? 0;
8798
+ this.perFrameFloats[seeAt + 2] = glass.transmission;
8799
+ this.perFrameFloats[seeAt + 3] = glass.frost;
8800
+ const refractTint = options.refractTint;
8801
+ const tintAt = this.materialField('uRefractTint');
8802
+ this.perFrameFloats[tintAt] = refractTint?.[0] ?? 1;
8803
+ this.perFrameFloats[tintAt + 1] = refractTint?.[1] ?? 1;
8804
+ this.perFrameFloats[tintAt + 2] = refractTint?.[2] ?? 1;
8805
+ const glassTintAt = this.materialField('uGlassTint');
8806
+ this.perFrameFloats[glassTintAt] = glass.tint[0];
8807
+ this.perFrameFloats[glassTintAt + 1] = glass.tint[1];
8808
+ this.perFrameFloats[glassTintAt + 2] = glass.tint[2];
8809
+ return true;
8810
+ }
8811
+
8812
+ /** Put the scratch back after `bindSeeThrough`: refraction and glass off. */
8813
+ private releaseSeeThrough(): void {
8814
+ const seeAt = this.materialField('uSeeThrough');
8815
+ this.perFrameFloats[seeAt] = 0;
8816
+ this.perFrameFloats[seeAt + 2] = 0;
8817
+ }
8818
+
8558
8819
  /**
8559
8820
  * One draw, one material slot, one uniform slot, however many instances.
8560
8821
  *
@@ -8567,6 +8828,37 @@ export class WebGPURenderer implements RendererApi {
8567
8828
  * **What it gives up** against `drawMesh` is skinning and morphing, refused where the variant
8568
8829
  * is built, and a per-instance opacity: `opacity` here dims the whole batch.
8569
8830
  */
8831
+ /**
8832
+ * The next of a batch's cull slots this frame, with its cull queued against the current view; null
8833
+ * when the batch has drawn from `MAX_CULL_SLOTS` views already or the frame's job ring is full,
8834
+ * and the draw then takes every instance.
8835
+ */
8836
+ private takeCullSlot(batch: GpuInstancedBatch, count: number): CullSlot | null {
8837
+ if (batch.slotFrame !== this.cullFrame) {
8838
+ batch.slotFrame = this.cullFrame;
8839
+ batch.slotsTaken = 0;
8840
+ }
8841
+ if (batch.slotsTaken >= MAX_CULL_SLOTS) return null;
8842
+ this.instanceCulls ??= new InstanceCullPass(this.surface.device);
8843
+ let slot = batch.slots[batch.slotsTaken];
8844
+ if (slot === undefined) {
8845
+ slot = this.instanceCulls.createSlot(batch.buffer, batch.capacity, 'flat.instances');
8846
+ batch.slots.push(slot);
8847
+ }
8848
+ const bounds = batch.mesh.bounds;
8849
+ const queued = this.instanceCulls.queue(
8850
+ slot,
8851
+ this.frustum,
8852
+ bounds.centre,
8853
+ bounds.radius,
8854
+ count,
8855
+ batch.mesh.indexCount,
8856
+ );
8857
+ if (!queued) return null;
8858
+ batch.slotsTaken += 1;
8859
+ return slot;
8860
+ }
8861
+
8570
8862
  private submitInstanced(
8571
8863
  batch: GpuInstancedBatch,
8572
8864
  data: MeshInstances,
@@ -8579,6 +8871,14 @@ export class WebGPURenderer implements RendererApi {
8579
8871
  if (!mesh.complete) return;
8580
8872
  const count = Math.min(data.count, batch.capacity);
8581
8873
  if (count === 0) return;
8874
+ /* A culling batch's camera draw: the whole batch against the view and the occluders on the CPU,
8875
+ then instance by instance on the device into an indirect draw. Blended draws keep every
8876
+ instance and their order. See `InstancedOptions.cull`. */
8877
+ const culling = !blend && batch.cull;
8878
+ if (culling && !batchBoxVisible(batch.box, this.frustum, this.occlusion)) return;
8879
+ const cut =
8880
+ culling && cullsInstances(count, mesh.indexCount) ? this.takeCullSlot(batch, count) : null;
8881
+ const instanceBuffer = cut === null ? batch.buffer : cut.instances;
8582
8882
 
8583
8883
  const slot = this.perDraw.allocate();
8584
8884
  if (slot === null) {
@@ -8591,7 +8891,13 @@ export class WebGPURenderer implements RendererApi {
8591
8891
  return;
8592
8892
  }
8593
8893
 
8594
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, this.viewProj);
8894
+ /* Late unless it moves with what it lies on. See `TranslucentMeshOptions.reconstructed`. */
8895
+ const lands = blend && options.reconstructed !== true && this.drawsLate();
8896
+ this.perDraw.writeFloats(
8897
+ slot,
8898
+ FLAT_VERT_FIELDS.uViewProj.offset,
8899
+ lands ? this.correctedViewProj : this.viewProj,
8900
+ );
8595
8901
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
8596
8902
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
8597
8903
  this.perDraw.writeInt(slot, FLAT_VERT_FIELDS.uHasTangents.offset, mesh.hasTangents ? 1 : 0);
@@ -8601,12 +8907,12 @@ export class WebGPURenderer implements RendererApi {
8601
8907
  this.perFrameFloats[this.materialField('uOpacity')] = opacity;
8602
8908
  }
8603
8909
  const lit = options.lit ?? true;
8604
- const fog = options.fog ?? true;
8910
+ const fog = fogModeOf(options.fog ?? true, blend && options.additive === true);
8605
8911
  const toneMapped = options.toneMapped ?? true;
8606
- const unlitOrUnfogged = !lit || !fog;
8912
+ const unlitOrUnfogged = !lit || fog !== FOG_RECEDE;
8607
8913
  if (unlitOrUnfogged) {
8608
8914
  if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
8609
- if (!fog) this.perFrameInts[this.materialField('uFogEnabled')] = 0;
8915
+ this.perFrameInts[this.materialField('uFogEnabled')] = fog;
8610
8916
  }
8611
8917
  if (!toneMapped) {
8612
8918
  this.perFrameInts[this.materialField('uOutputTransform')] = Math.min(this.gradeCode(), 1);
@@ -8614,19 +8920,21 @@ export class WebGPURenderer implements RendererApi {
8614
8920
 
8615
8921
  const depthWrite = options.depthWrite ?? true;
8616
8922
  const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
8617
- /* An instanced draw does not refract. See `materialChanges.ts`. */
8618
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting: false });
8923
+ /* A blended batch shows what is behind it — refraction or glass — as a single draw does. */
8924
+ const refracting = blend && this.bindSeeThrough(options);
8925
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
8619
8926
  if (own) this.materials.dirty();
8620
8927
  const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
8621
8928
  /* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
8622
8929
  a third vertex buffer and a different vertex module, and sharing a cache entry with the
8623
8930
  plain one would hand a draw the wrong layout. */
8931
+ const adds = blend && options.additive === true;
8624
8932
  const key =
8625
- `${base}|inst${blend ? '|blend' : ''}` +
8933
+ `${base}|inst${blend ? (adds ? '|add' : '|blend') : ''}` +
8626
8934
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
8627
8935
  `${this.materialDoubleSided ? '|2s' : ''}`;
8628
8936
 
8629
- const pipelines = this.targetPipelines();
8937
+ const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
8630
8938
  let pipeline = pipelines.peek(key);
8631
8939
  if (pipeline === undefined) {
8632
8940
  const present = this.meshPresent.get(base);
@@ -8640,7 +8948,7 @@ export class WebGPURenderer implements RendererApi {
8640
8948
  this.variant,
8641
8949
  key,
8642
8950
  present,
8643
- blend,
8951
+ adds ? 'additive' : blend,
8644
8952
  false,
8645
8953
  false,
8646
8954
  depthWrite,
@@ -8653,7 +8961,9 @@ export class WebGPURenderer implements RendererApi {
8653
8961
 
8654
8962
  const material = this.materialSlotForDraw();
8655
8963
  if (material !== null) {
8656
- const command = this.recordDraw(0, this.currentTarget());
8964
+ const command = blend
8965
+ ? this.recordBlended(0, lands)
8966
+ : this.recordDraw(0, this.currentTarget());
8657
8967
  if (command !== null) {
8658
8968
  command.pipeline = pipeline;
8659
8969
  command.bindGroup = this.flatBindGroupFor(false, false, null);
@@ -8664,12 +8974,13 @@ export class WebGPURenderer implements RendererApi {
8664
8974
  for (; index < mesh.vertexBuffers.length; index++) {
8665
8975
  command.vertexBuffers[index] = mesh.vertexBuffers[index] as GPUBuffer;
8666
8976
  }
8667
- command.vertexBuffers[index] = batch.buffer;
8977
+ command.vertexBuffers[index] = instanceBuffer;
8668
8978
  command.vertexCount = mesh.vertexBuffers.length + 1;
8669
8979
  command.indexBuffer = mesh.indexBuffer;
8670
8980
  command.indexed = true;
8671
8981
  command.count = mesh.indexCount;
8672
8982
  command.instances = count;
8983
+ command.indirect = cut === null ? null : cut.args;
8673
8984
  } else {
8674
8985
  const pass = this.openPass();
8675
8986
  if (pass === null) return;
@@ -8679,9 +8990,10 @@ export class WebGPURenderer implements RendererApi {
8679
8990
  for (; index < mesh.vertexBuffers.length; index++) {
8680
8991
  pass.setVertexBuffer(index, mesh.vertexBuffers[index] as GPUBuffer);
8681
8992
  }
8682
- pass.setVertexBuffer(index, batch.buffer);
8993
+ pass.setVertexBuffer(index, instanceBuffer);
8683
8994
  pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
8684
- pass.drawIndexed(mesh.indexCount, count);
8995
+ if (cut === null) pass.drawIndexed(mesh.indexCount, count);
8996
+ else pass.drawIndexedIndirect(cut.args, 0);
8685
8997
  }
8686
8998
  }
8687
8999
  /* Back to the defaults, unconditionally and for the reason `drawMesh` gives at length: this
@@ -8692,11 +9004,12 @@ export class WebGPURenderer implements RendererApi {
8692
9004
  }
8693
9005
  if (unlitOrUnfogged) {
8694
9006
  this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
8695
- this.perFrameInts[this.materialField('uFogEnabled')] = 1;
9007
+ this.perFrameInts[this.materialField('uFogEnabled')] = FOG_RECEDE;
8696
9008
  }
8697
9009
  if (!toneMapped) {
8698
9010
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
8699
9011
  }
9012
+ if (refracting) this.releaseSeeThrough();
8700
9013
  if (own) this.materials.dirty();
8701
9014
  }
8702
9015
 
@@ -8890,6 +9203,8 @@ export class WebGPURenderer implements RendererApi {
8890
9203
  this.lightViewProj.set(lightViewProj as Float32Array);
8891
9204
  mat4.multiply(this.correctedLightViewProj, SHADOW_CLIP_CORRECTION, lightViewProj);
8892
9205
  this.shadowLayerIsPeel = peel;
9206
+ this.shadowLayerIsDynamic = dynamic;
9207
+ this.glassCasters.clear();
8893
9208
  /*
8894
9209
  * A frame's peel is only as good as the pass that fills it, and the static layer is the one
8895
9210
  * that opens first.
@@ -8997,48 +9312,387 @@ export class WebGPURenderer implements RendererApi {
8997
9312
  }
8998
9313
 
8999
9314
  /** Close the shadow pass and submit it, so the frame can sample what it wrote. */
9000
- endShadowPass(): void {
9001
- if (this.surface.lost || this.shadowPass === null || this.shadowEncoder === null) return;
9002
- this.shadowDraws.flush();
9003
- this.scatterDepthDraws.flush();
9004
- this.shadowPass.end();
9005
- this.surface.device.queue.submit([this.shadowEncoder.finish()]);
9006
- /* The map now holds a rendered peel rather than whatever it was created with. */
9007
- if (this.shadowLayerIsPeel) this.peelFilled = true;
9008
- this.shadowLayerIsPeel = false;
9009
- this.shadowPass = null;
9010
- this.shadowEncoder = null;
9011
- this.gpuTimer.end();
9315
+ /**
9316
+ * Add the sun's two glass layers, once, carrying every layer it holds across on `encoder`.
9317
+ *
9318
+ * **Copied rather than re-baked**, for the reason `SunShadowArray.growForGlass` gives: the static
9319
+ * map is the consumer's to bake and may be baked once, and the glass that grows the texture
9320
+ * arrives in the very pass that drew it. Every view of the old texture is stale afterwards, so
9321
+ * every group holding one is rebuilt here — the lit pass's, the volumes', the medium's and the
9322
+ * peel's — and the old texture is destroyed only once this encoder is submitted.
9323
+ */
9324
+ private growSunForGlass(encoder: GPUCommandEncoder): void {
9325
+ if (this.sunGlassViews !== null) return;
9326
+ const device = this.surface.device;
9327
+ const size = this.quality.directionalShadowMapSize;
9328
+ const maps = this.sunLayerCount;
9329
+ /* The glass layers at their fixed places after the three maps: see `SUN_GLASS_STATIC_LAYER`. */
9330
+ const layers = SUN_GLASS_MOVING_LAYER + 1;
9331
+ const old = this.sunShadowTexture;
9332
+ const texture = device.createTexture({
9333
+ label: 'shadow.sun',
9334
+ size: [size, size, layers],
9335
+ format: SHADOW_FORMAT,
9336
+ usage: SUN_USAGE,
9337
+ });
9338
+ for (let layer = 0; layer < maps; layer++) {
9339
+ encoder.copyTextureToTexture(
9340
+ { texture: old, origin: { x: 0, y: 0, z: layer } },
9341
+ { texture, origin: { x: 0, y: 0, z: layer } },
9342
+ [size, size, 1],
9343
+ );
9344
+ }
9345
+ this.retiredSunTexture = old;
9346
+ this.sunShadowTexture = texture;
9347
+ this.sunLayerCount = layers;
9348
+ const views = sunViewsOf(texture, maps, layers);
9349
+ this.sunArrayView = views.array;
9350
+ this.sunStaticArrayView = views.staticArray;
9351
+ this.shadowView = views.static;
9352
+ this.dynamicView = views.dynamic;
9353
+ this.peelView = views.peel;
9354
+ this.sunGlassViews = views.glass;
9355
+ /* Both glass layers born at the far plane: nothing is nearer the sun than a pane not drawn. */
9356
+ for (const view of views.glass ?? []) {
9357
+ encoder
9358
+ .beginRenderPass({
9359
+ label: 'shadow.glass.clear',
9360
+ colorAttachments: [],
9361
+ depthStencilAttachment: {
9362
+ view,
9363
+ depthClearValue: 1,
9364
+ depthLoadOp: 'clear',
9365
+ depthStoreOp: 'store',
9366
+ },
9367
+ })
9368
+ .end();
9369
+ }
9370
+ this.rebuildSunGroups();
9012
9371
  }
9013
9372
 
9014
- /* -- Point shadows ------------------------------------------------------------------ */
9373
+ /** Every group that holds a view of the sun's texture, rebuilt after it has been replaced. */
9374
+ private rebuildSunGroups(): void {
9375
+ const device = this.surface.device;
9376
+ const peelUniforms = this.shadowPeelPassUniforms;
9377
+ const peelSampler = this.shadowPeelSampler;
9378
+ if (peelUniforms !== null && peelSampler !== null) {
9379
+ this.shadowPeelBindGroup = createDepthBindGroup(
9380
+ device,
9381
+ this.shadowLayout,
9382
+ this.shadowDraws.buffer,
9383
+ peelUniforms,
9384
+ this.sunStaticArrayView,
9385
+ peelSampler,
9386
+ );
9387
+ this.scatterDepthPeelBindGroup = createScatterDepthBindGroup(
9388
+ device,
9389
+ this.scatterDepthLayout,
9390
+ this.scatterDepthDraws.buffer,
9391
+ { binding: DEPTH_FRAG_BINDING, buffer: peelUniforms, size: DEPTH_FRAG_SIZE },
9392
+ DEPTH_PREVIOUS_BINDING,
9393
+ this.sunStaticArrayView,
9394
+ peelSampler,
9395
+ );
9396
+ }
9397
+ this.cutoutPeelGroups.clear();
9398
+ this.rebuildFlatGroupsForNewRings();
9399
+ this.lightVolumeBindGroup = this.buildLightVolumeBindGroup();
9400
+ this.mediumBindGroup = this.buildMediumBindGroup();
9401
+ }
9402
+
9403
+ /** The sun's tint target, made the first time glass casts and born clear on `encoder`. */
9404
+ private ensureSunTint(encoder: GPUCommandEncoder): SunTintTarget {
9405
+ if (this.sunTint !== null) return this.sunTint;
9406
+ const size = this.quality.directionalShadowMapSize;
9407
+ const edge = this.quality.glassShadows === 'half' ? Math.max(1, size >> 1) : size;
9408
+ const device = this.surface.device;
9409
+ const texture = device.createTexture({
9410
+ label: 'shadow.sunTint',
9411
+ size: [edge, edge, 2],
9412
+ format: 'rgba8unorm',
9413
+ mipLevelCount: Math.floor(Math.log2(edge)) + 1,
9414
+ usage: 0x10 | 0x4, // RENDER_ATTACHMENT | TEXTURE_BINDING
9415
+ });
9416
+ const layer = (index: number): GPUTextureView =>
9417
+ texture.createView({
9418
+ dimension: '2d',
9419
+ baseArrayLayer: index,
9420
+ arrayLayerCount: 1,
9421
+ baseMipLevel: 0,
9422
+ mipLevelCount: 1,
9423
+ });
9424
+ const mips = (index: number): SnapshotMips =>
9425
+ new SnapshotMips(device, this.pipelines, texture, 'rgba8unorm', index, 'shadow.tint.mips');
9426
+ const target: SunTintTarget = {
9427
+ texture,
9428
+ layers: [layer(0), layer(1)],
9429
+ array: texture.createView({ dimension: '2d-array' }),
9430
+ mips: [mips(0), mips(1)],
9431
+ };
9432
+ for (const view of target.layers) {
9433
+ encoder
9434
+ .beginRenderPass({
9435
+ label: 'shadow.tint.clear',
9436
+ colorAttachments: [{ view, loadOp: 'clear', clearValue: [1, 1, 1, 1], storeOp: 'store' }],
9437
+ })
9438
+ .end();
9439
+ }
9440
+ this.sunTint = target;
9441
+ this.rebuildFlatGroupsForNewRings();
9442
+ return target;
9443
+ }
9015
9444
 
9016
9445
  /**
9017
- * The pool, or null where the profile switched cube shadows off.
9446
+ * Draw the pass's glass twice: where the nearest pane is, and what the panes let through.
9018
9447
  *
9019
- * `PointShadowSystem` over `GpuPointShadowMap`: the pooling, the staleness and the crossfade
9020
- * are the same code the WebGL2 path runs, and only the cubemap is this backend's. See
9021
- * `PointShadowSource`.
9448
+ * After the opaque casters, on the pass's own encoder. A layer that has held glass is redrawn
9449
+ * even when this pass offered none, so glass that has gone takes its colour with it.
9022
9450
  */
9023
- private readonly pointShadows: PointShadowSystem<GpuPointShadowMap> | null;
9024
- /** A world's casting rectangles, on layers above the pool in the same array. See `renderer.ts`. */
9025
- private readonly areaShadows: AreaShadowSet<GpuPointShadowMap> | null;
9026
- private readonly resolvedAreaShadows = createResolvedAreaShadows(MAX_AREA_LIGHTS);
9027
- private readonly areaBakeScratch = createBakeScratch(MAX_AREA_LIGHTS);
9028
- private warnedAreaShadowRange = false;
9029
- private warnedAreaShadowsOff = false;
9030
- private warnedAreaShadowLayers = false;
9031
- /** How many point lights the array was last sized for; see the other backend's copy. */
9032
- private preparedPointLightCount = 0;
9033
- /** See `renderer.ts`: built when a world reports its lights, and rebuilt only to grow. */
9034
- private pointShadowArray: GpuPointShadowArray | null = null;
9035
- private readonly bakeScratch = createBakeScratch(POINT_SHADOW_POOL);
9036
- /** Said once per renderer; see `updatePointShadows`. */
9037
- private warnedNoPointShadowArray = false;
9038
- /** Said once, for the retired ambient dial. */
9039
- private warnedEnvironmentAmbient = false;
9040
- private readonly castingWorldIndices = new Int32Array(POINT_SHADOW_POOL).fill(-1);
9041
- private readonly resolvedPointShadows = createResolvedPointShadows(MAX_POINT_LIGHTS);
9451
+ private flushSunGlass(encoder: GPUCommandEncoder, which: 'static' | 'dynamic'): void {
9452
+ const list = this.glassCasters;
9453
+ if (this.quality.glassShadows === 'off') return;
9454
+ if (list.count === 0 && this.sunGlassViews === null) return;
9455
+ if (list.count > 0) this.growSunForGlass(encoder);
9456
+ const glassViews = this.sunGlassViews;
9457
+ if (glassViews === null) return;
9458
+
9459
+ /* Where the nearest pane is: the depth pipelines, through the sink every opaque caster took. */
9460
+ this.shadowPass = encoder.beginRenderPass({
9461
+ label: 'shadow.glass',
9462
+ colorAttachments: [],
9463
+ depthStencilAttachment: {
9464
+ view: glassViews[which === 'static' ? 0 : 1],
9465
+ depthClearValue: 1,
9466
+ depthLoadOp: 'clear',
9467
+ depthStoreOp: 'store',
9468
+ },
9469
+ });
9470
+ this.drawingGlassDepth = true;
9471
+ list.replay(this.glassDepthSink);
9472
+ this.drawingGlassDepth = false;
9473
+ this.shadowPass.end();
9474
+ this.shadowPass = null;
9475
+
9476
+ /* What the panes let through, every one on a ray, multiplied. */
9477
+ const target = this.ensureSunTint(encoder);
9478
+ const tint = (this.glassTint ??= new GpuGlassTint(this.surface.device));
9479
+ tint.frag.reset();
9480
+ /* The direction the light travels, off the light matrix's depth row; the sign is immaterial,
9481
+ since the tint takes the cosine absolutely, and w 0 says it is a direction. */
9482
+ const m = this.correctedLightViewProj;
9483
+ const length = Math.hypot(m[2] as number, m[6] as number, m[10] as number) || 1;
9484
+ this.glassLight[0] = (m[2] as number) / length;
9485
+ this.glassLight[1] = (m[6] as number) / length;
9486
+ this.glassLight[2] = (m[10] as number) / length;
9487
+ this.glassLight[3] = 0;
9488
+ this.tintPass = encoder.beginRenderPass({
9489
+ label: 'shadow.tint',
9490
+ colorAttachments: [
9491
+ {
9492
+ view: target.layers[which === 'static' ? 0 : 1],
9493
+ loadOp: 'clear',
9494
+ clearValue: [1, 1, 1, 1],
9495
+ storeOp: 'store',
9496
+ },
9497
+ ],
9498
+ });
9499
+ list.replay(this.glassTintSink);
9500
+ this.tintPass.end();
9501
+ this.tintPass = null;
9502
+ tint.frag.flush();
9503
+ /* The chain frost reads, for the layer just drawn. */
9504
+ target.mips[which === 'static' ? 0 : 1].record(encoder);
9505
+ }
9506
+
9507
+ /** The glass list into a glass depth layer: `casterSink` itself, with glass let through. */
9508
+ private readonly glassDepthSink: GlassReplaySink = {
9509
+ mesh: (mesh, model, material) => this.casterSink.mesh(mesh, model, material),
9510
+ instanced: (batch, data, material) => this.casterSink.instanced?.(batch, data, material),
9511
+ skinnedMesh: (mesh, model, palette, material) =>
9512
+ this.casterSink.skinnedMesh(mesh, model, palette, material),
9513
+ };
9514
+
9515
+ /**
9516
+ * The glass list into a tint layer, through the tint pipelines, **culling nothing**: every pane on
9517
+ * a ray counts, whichever way it faces. See the other backend's `glassTintSink`.
9518
+ */
9519
+ private readonly glassTintSink: GlassReplaySink = {
9520
+ mesh: (mesh, model, material, glass) => {
9521
+ const pass = this.tintPass;
9522
+ const tint = this.glassTint;
9523
+ if (pass === null || tint === null) return;
9524
+ const geometry = mesh as GpuMesh & { key?: string };
9525
+ if (geometry.vertexBuffers === undefined) return;
9526
+ const cutout = cutoutOf(material);
9527
+ const albedo = cutout === null ? null : (cutout.albedo as unknown as GpuSurfaceTexture);
9528
+ const slot = this.shadowDraws.allocate();
9529
+ const pane = tint.frag.allocate();
9530
+ if (slot === null || pane === null) return;
9531
+ this.shadowDraws.writeFloats(
9532
+ slot,
9533
+ DEPTH_VERT_FIELDS.uLightViewProj.offset,
9534
+ this.correctedLightViewProj,
9535
+ );
9536
+ this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uModel.offset, model as Float32Array);
9537
+ this.writeShadowWind(slot);
9538
+ if (cutout !== null) this.writeCutout(slot, cutout, DEPTH_CUTOUT_VERT_FIELDS);
9539
+ tint.writePane(pane, glass, this.glassLight);
9540
+ pass.setPipeline(
9541
+ tint.pipeline(
9542
+ this.pipelines,
9543
+ albedo === null ? 'rigid' : 'cutout',
9544
+ geometry.key ?? '',
9545
+ this.presentOf(geometry),
9546
+ 'none',
9547
+ ),
9548
+ );
9549
+ pass.setBindGroup(
9550
+ 0,
9551
+ albedo === null
9552
+ ? tint.group(this.shadowDraws.buffer)
9553
+ : tint.cutoutGroup(this.shadowDraws.buffer, albedo.view, albedo.sampler),
9554
+ [slot, pane],
9555
+ );
9556
+ for (let index = 0; index < geometry.vertexBuffers.length; index++) {
9557
+ pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
9558
+ }
9559
+ pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
9560
+ pass.drawIndexed(geometry.indexCount);
9561
+ },
9562
+ instanced: (batch, data, material, glass) => {
9563
+ const pass = this.tintPass;
9564
+ const tint = this.glassTint;
9565
+ if (pass === null || tint === null) return;
9566
+ const gpuBatch = batch as GpuInstancedBatch;
9567
+ const geometry = gpuBatch.mesh as GpuMesh & { key?: string };
9568
+ if (geometry.vertexBuffers === undefined) return;
9569
+ const count = Math.min(data.count, gpuBatch.capacity);
9570
+ if (count === 0) return;
9571
+ const cutout = cutoutOf(material);
9572
+ const albedo = cutout === null ? null : (cutout.albedo as unknown as GpuSurfaceTexture);
9573
+ const slot = this.shadowDraws.allocate();
9574
+ const pane = tint.frag.allocate();
9575
+ if (slot === null || pane === null) return;
9576
+ this.shadowDraws.writeFloats(
9577
+ slot,
9578
+ DEPTH_VERT_FIELDS.uLightViewProj.offset,
9579
+ this.correctedLightViewProj,
9580
+ );
9581
+ if (cutout !== null) this.writeCutout(slot, cutout, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS);
9582
+ tint.writePane(pane, glass, this.glassLight);
9583
+ pass.setPipeline(
9584
+ tint.pipeline(
9585
+ this.pipelines,
9586
+ albedo === null ? 'instanced' : 'instancedCutout',
9587
+ geometry.key ?? '',
9588
+ this.presentOf(geometry),
9589
+ 'none',
9590
+ ),
9591
+ );
9592
+ pass.setBindGroup(
9593
+ 0,
9594
+ albedo === null
9595
+ ? tint.group(this.shadowDraws.buffer)
9596
+ : tint.cutoutGroup(this.shadowDraws.buffer, albedo.view, albedo.sampler),
9597
+ [slot, pane],
9598
+ );
9599
+ let index = 0;
9600
+ for (; index < geometry.vertexBuffers.length; index++) {
9601
+ pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
9602
+ }
9603
+ pass.setVertexBuffer(index, gpuBatch.buffer);
9604
+ pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
9605
+ pass.drawIndexed(geometry.indexCount, count);
9606
+ },
9607
+ skinnedMesh: (mesh, model, palette, _material, glass) => {
9608
+ const pass = this.tintPass;
9609
+ const tint = this.glassTint;
9610
+ if (pass === null || tint === null) return;
9611
+ const geometry = mesh as GpuMesh & { key?: string };
9612
+ if (geometry.vertexBuffers === undefined) return;
9613
+ const paletteSlot = this.skinPalettes.take(this.surface.device, palette);
9614
+ const view = paletteSlot === null ? null : this.skinPalettes.view(paletteSlot);
9615
+ if (view === null) return;
9616
+ const slot = this.shadowDraws.allocate();
9617
+ const pane = tint.frag.allocate();
9618
+ if (slot === null || pane === null) return;
9619
+ this.shadowDraws.writeFloats(
9620
+ slot,
9621
+ DEPTH_VERT_FIELDS.uLightViewProj.offset,
9622
+ this.correctedLightViewProj,
9623
+ );
9624
+ this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uModel.offset, model as Float32Array);
9625
+ this.writeShadowWind(slot);
9626
+ tint.writePane(pane, glass, this.glassLight);
9627
+ pass.setPipeline(
9628
+ tint.pipeline(
9629
+ this.pipelines,
9630
+ 'skinned',
9631
+ geometry.key ?? '',
9632
+ this.presentOf(geometry),
9633
+ 'none',
9634
+ ),
9635
+ );
9636
+ pass.setBindGroup(0, tint.skinnedGroup(this.shadowDraws.buffer, view), [slot, pane]);
9637
+ for (let index = 0; index < geometry.vertexBuffers.length; index++) {
9638
+ pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
9639
+ }
9640
+ pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
9641
+ pass.drawIndexed(geometry.indexCount);
9642
+ },
9643
+ };
9644
+
9645
+ endShadowPass(): void {
9646
+ if (this.surface.lost || this.shadowPass === null || this.shadowEncoder === null) return;
9647
+ this.shadowPass.end();
9648
+ /* The peel keeps no glass: its layer is the second opaque occluder and nothing else. */
9649
+ if (!this.shadowLayerIsPeel) {
9650
+ this.flushSunGlass(this.shadowEncoder, this.shadowLayerIsDynamic ? 'dynamic' : 'static');
9651
+ }
9652
+ this.shadowDraws.flush();
9653
+ this.scatterDepthDraws.flush();
9654
+ this.surface.device.queue.submit([this.shadowEncoder.finish()]);
9655
+ /* A growth's old texture, copied out of by what was just submitted and needed no longer. */
9656
+ this.retiredSunTexture?.destroy();
9657
+ this.retiredSunTexture = null;
9658
+ /* The map now holds a rendered peel rather than whatever it was created with. */
9659
+ if (this.shadowLayerIsPeel) this.peelFilled = true;
9660
+ this.shadowLayerIsPeel = false;
9661
+ this.shadowPass = null;
9662
+ this.shadowEncoder = null;
9663
+ this.gpuTimer.end();
9664
+ }
9665
+
9666
+ /* -- Point shadows ------------------------------------------------------------------ */
9667
+
9668
+ /**
9669
+ * The pool, or null where the profile switched cube shadows off.
9670
+ *
9671
+ * `PointShadowSystem` over `GpuPointShadowMap`: the pooling, the staleness and the crossfade
9672
+ * are the same code the WebGL2 path runs, and only the cubemap is this backend's. See
9673
+ * `PointShadowSource`.
9674
+ */
9675
+ private readonly pointShadows: PointShadowSystem<GpuPointShadowMap> | null;
9676
+ /** A world's casting rectangles, on layers above the pool in the same array. See `renderer.ts`. */
9677
+ private readonly areaShadows: AreaShadowSet<GpuPointShadowMap> | null;
9678
+ private readonly resolvedAreaShadows = createResolvedAreaShadows(MAX_AREA_LIGHTS);
9679
+ private readonly areaBakeScratch = createBakeScratch(MAX_AREA_LIGHTS);
9680
+ private warnedAreaShadowRange = false;
9681
+ private warnedAreaShadowsOff = false;
9682
+ private warnedAreaShadowLayers = false;
9683
+ /** How many point lights the array was last sized for; see the other backend's copy. */
9684
+ private preparedPointLightCount = 0;
9685
+ /** See `renderer.ts`: built when a world reports its lights, and rebuilt only to grow. */
9686
+ private pointShadowArray: GpuPointShadowArray | null = null;
9687
+ /** The array glass replaced, destroyed once the round that copied out of it is submitted. */
9688
+ private retiredPointTexture: GPUTexture | null = null;
9689
+ private readonly bakeScratch = createBakeScratch(POINT_SHADOW_POOL);
9690
+ /** Said once per renderer; see `updatePointShadows`. */
9691
+ private warnedNoPointShadowArray = false;
9692
+ /** Said once, for the retired ambient dial. */
9693
+ private warnedEnvironmentAmbient = false;
9694
+ private readonly castingWorldIndices = new Int32Array(POINT_SHADOW_POOL).fill(-1);
9695
+ private readonly resolvedPointShadows = createResolvedPointShadows(MAX_POINT_LIGHTS);
9042
9696
  /** One face's view-projection, rebuilt per face rather than allocated. */
9043
9697
  private readonly faceViewProj = mat4.create();
9044
9698
  /** The corrected copy the vertex stage is handed. See `SHADOW_CLIP_CORRECTION`. */
@@ -9217,6 +9871,7 @@ export class WebGPURenderer implements RendererApi {
9217
9871
  */
9218
9872
  this.shadowDraws.reset();
9219
9873
  this.scatterDepthDraws.reset();
9874
+ this.glassTint?.frag.reset();
9220
9875
  this.shadowEncoder = this.surface.device.createCommandEncoder({ label: 'pointShadow' });
9221
9876
  /* A cube face is a shadow pass like any other, and the courtyard is where it matters: six of
9222
9877
  them a frame is the difference this slot exists to make visible. */
@@ -9244,7 +9899,7 @@ export class WebGPURenderer implements RendererApi {
9244
9899
  light.z,
9245
9900
  light.radius,
9246
9901
  (view, viewProj) => this.bakeFace(view, viewProj, casters as ShadowCasters),
9247
- (layer, face, far, near) => this.resolveFace(layer, face, far, near),
9902
+ (layer, face, far, near, x, y, z) => this.resolveFace(layer, face, far, near, x, y, z),
9248
9903
  this.faceViewProj,
9249
9904
  light.shadowNear,
9250
9905
  light.sourceRadius,
@@ -9258,10 +9913,13 @@ export class WebGPURenderer implements RendererApi {
9258
9913
  * encoder, a second submit and a second `shadows` timer slot for work that is the same work.
9259
9914
  */
9260
9915
  this.updateAreaShadows(areaLights, frameDt, staticCasters, dynamicCasters);
9916
+ /* The chain frost reads, once for the round, for the lights whose glass it drew or cleared. */
9917
+ this.pointShadowArray?.glassTint?.recordMips(this.shadowEncoder);
9261
9918
  } finally {
9262
9919
  this.depthCullMode = 'back';
9263
9920
  this.shadowDraws.flush();
9264
9921
  this.scatterDepthDraws.flush();
9922
+ this.glassTint?.frag.flush();
9265
9923
  /*
9266
9924
  * **Before the submit, and that ordering is the whole point of the ring.** A write to the
9267
9925
  * queue lands ahead of the encoder's commands whenever it is issued, so uploading per
@@ -9273,6 +9931,9 @@ export class WebGPURenderer implements RendererApi {
9273
9931
  this.shadowEncoder = null;
9274
9932
  this.shadowPass = null;
9275
9933
  this.gpuTimer.end();
9934
+ /* An interleaving's old array, copied out of by what was just submitted. */
9935
+ this.retiredPointTexture?.destroy();
9936
+ this.retiredPointTexture = null;
9276
9937
  }
9277
9938
 
9278
9939
  /*
@@ -9283,6 +9944,10 @@ export class WebGPURenderer implements RendererApi {
9283
9944
  * written every frame regardless.
9284
9945
  */
9285
9946
  pool.resolve(this.resolvedPointShadows);
9947
+ /* As the array stores them: 2L once glass has interleaved it. See `strideLayers`. */
9948
+ const stride = this.pointShadowArray.stride;
9949
+ strideLayers(this.resolvedPointShadows.layers, stride);
9950
+ strideLayers(this.resolvedPointShadows.liveLayers, stride);
9286
9951
  }
9287
9952
 
9288
9953
  /**
@@ -9327,7 +9992,8 @@ export class WebGPURenderer implements RendererApi {
9327
9992
  light.z,
9328
9993
  range,
9329
9994
  (view, viewProj) => this.bakeFace(view, viewProj, casters as ShadowCasters),
9330
- (layer, face, far, mapNear) => this.resolveFace(layer, face, far, mapNear),
9995
+ (layer, face, far, mapNear, x, y, z) =>
9996
+ this.resolveFace(layer, face, far, mapNear, x, y, z),
9331
9997
  this.faceViewProj,
9332
9998
  near,
9333
9999
  /* The filter reads the emitter's size, never the bake. See `renderer.ts`'s copy. */
@@ -9336,6 +10002,9 @@ export class WebGPURenderer implements RendererApi {
9336
10002
  ),
9337
10003
  );
9338
10004
  set.resolve(areaLights, this.resolvedAreaShadows);
10005
+ const stride = this.pointShadowArray?.stride ?? 1;
10006
+ strideLayers(this.resolvedAreaShadows.layer, stride);
10007
+ strideLayers(this.resolvedAreaShadows.liveLayer, stride);
9339
10008
  }
9340
10009
 
9341
10010
  /**
@@ -9363,9 +10032,27 @@ export class WebGPURenderer implements RendererApi {
9363
10032
  depthStoreOp: 'store',
9364
10033
  },
9365
10034
  });
10035
+ /* A face is an enumeration of its own: its glass is its own. */
10036
+ this.glassCasters.clear();
9366
10037
  casters(this.casterSink);
9367
10038
  this.shadowPass.end();
9368
10039
  this.shadowPass = null;
10040
+ /*
10041
+ * The first face to offer glass interleaves the array, before its opaque depth is resolved:
10042
+ * `PointShadowArray.growForGlass`. The lit pass's groups hold the old array's view, so they
10043
+ * are rebuilt, and the old texture goes once this round's encoder — which copies out of it —
10044
+ * is submitted.
10045
+ */
10046
+ const array = this.pointShadowArray;
10047
+ if (array !== null && this.glassCasters.count > 0 && this.quality.glassShadows !== 'off') {
10048
+ const tintEdge =
10049
+ this.quality.glassShadows === 'half' ? OCTAHEDRAL_EDGE >> 1 : OCTAHEDRAL_EDGE;
10050
+ const old = array.growForGlass(encoder, tintEdge, this.pipelines);
10051
+ if (old !== null) {
10052
+ this.retiredPointTexture = old;
10053
+ this.rebuildFlatBindGroup();
10054
+ }
10055
+ }
9369
10056
  }
9370
10057
 
9371
10058
  /**
@@ -9375,11 +10062,70 @@ export class WebGPURenderer implements RendererApi {
9375
10062
  * serve every light: the face is consumed before the next one is drawn. See
9376
10063
  * `pointShadowArray.ts` for why the regions are disjoint.
9377
10064
  */
9378
- private resolveFace(layer: number, face: number, far: number, near: number): void {
10065
+ private resolveFace(
10066
+ layer: number,
10067
+ face: number,
10068
+ far: number,
10069
+ near: number,
10070
+ x: number,
10071
+ y: number,
10072
+ z: number,
10073
+ ): void {
9379
10074
  const encoder = this.shadowEncoder;
9380
10075
  const array = this.pointShadowArray;
9381
10076
  if (encoder === null || array === null) return;
9382
10077
  array.resolve(encoder, layer, face, far, near);
10078
+ const tintTarget = array.glassTint;
10079
+ if (tintTarget === null || this.quality.glassShadows === 'off') return;
10080
+ /*
10081
+ * The face's glass, twice, as `PointShadowMap.bakeGlassFace` draws it on the other backend. A
10082
+ * face that offers none still clears a region a previous owner's glass is in, from scratches
10083
+ * cleared and not drawn into, because a pool slot changes hands with its layer.
10084
+ */
10085
+ const offered = this.glassCasters.count > 0;
10086
+ if (!offered && !tintTarget.held(layer, face)) return;
10087
+ this.shadowPass = encoder.beginRenderPass({
10088
+ label: 'pointShadow.glass',
10089
+ colorAttachments: [],
10090
+ depthStencilAttachment: {
10091
+ view: array.scratchView,
10092
+ depthClearValue: 1,
10093
+ depthLoadOp: 'clear',
10094
+ depthStoreOp: 'store',
10095
+ },
10096
+ });
10097
+ if (offered) {
10098
+ this.drawingGlassDepth = true;
10099
+ this.glassCasters.replay(this.glassDepthSink);
10100
+ this.drawingGlassDepth = false;
10101
+ }
10102
+ this.shadowPass.end();
10103
+ this.shadowPass = null;
10104
+ array.resolveGlass(encoder, layer, face, far, near);
10105
+
10106
+ this.tintPass = encoder.beginRenderPass({
10107
+ label: 'pointShadow.tint',
10108
+ colorAttachments: [
10109
+ {
10110
+ view: tintTarget.scratchView,
10111
+ loadOp: 'clear',
10112
+ clearValue: [1, 1, 1, 1],
10113
+ storeOp: 'store',
10114
+ },
10115
+ ],
10116
+ });
10117
+ if (offered) {
10118
+ this.glassTint ??= new GpuGlassTint(this.surface.device);
10119
+ /* A place, w 1: each pane's angle to the lamp comes from its own position. */
10120
+ this.glassLight[0] = x;
10121
+ this.glassLight[1] = y;
10122
+ this.glassLight[2] = z;
10123
+ this.glassLight[3] = 1;
10124
+ this.glassCasters.replay(this.glassTintSink);
10125
+ }
10126
+ this.tintPass.end();
10127
+ this.tintPass = null;
10128
+ tintTarget.resolve(encoder, layer, face, offered);
9383
10129
  }
9384
10130
 
9385
10131
  private buildFlatBindGroup(): GPUBindGroup {
@@ -9739,8 +10485,9 @@ export class WebGPURenderer implements RendererApi {
9739
10485
  * drawn over a reflective floor rather than under it.
9740
10486
  */
9741
10487
  this.runReflections(encoder);
9742
- /* The translucent set, into its two buffers and back over the scene. */
9743
- this.runOit(encoder);
10488
+ /* The translucent set, into its two buffers and back over the scene — after the upscale instead
10489
+ while reconstructing, where `runLate` draws the rest of the blended frame. */
10490
+ if (!this.reconstructing) this.runOit(encoder);
9744
10491
  /*
9745
10492
  * **The medium, after everything that draws and before anything that composites.**
9746
10493
  *
@@ -9761,6 +10508,8 @@ export class WebGPURenderer implements RendererApi {
9761
10508
  */
9762
10509
  this.runMotion(encoder);
9763
10510
  this.runReconstruction(encoder);
10511
+ this.runLate(encoder);
10512
+ if (this.reconstructing) this.runOit(encoder);
9764
10513
  const aoStrength = this.quality.ambientOcclusion;
9765
10514
  if (aoStrength > 0 && this.frameProjection !== null) this.runOcclusion(encoder);
9766
10515
  const bloomStrength = this.quality.bloom * this.bloomScale;
@@ -9793,6 +10542,23 @@ export class WebGPURenderer implements RendererApi {
9793
10542
  f[at('uMotionStrength')] = motionStrength;
9794
10543
  f[at('uMotionMax')] = MOTION_BLUR_MAX_UV;
9795
10544
  f[at('uAoStrength')] = this.frameProjection === null ? 0 : aoStrength;
10545
+ /*
10546
+ * The occlusion read where this frame's jitter put it (`uAoOffset`): measured from the jittered
10547
+ * depth and applied to a picture the jitter has been taken out of. A texel stands at its index
10548
+ * plus a half less the jitter, so the occlusion of a point is a jitter further on. The
10549
+ * reconstruction's y is given upward and negated into the corrected matrix, which this pass's
10550
+ * rows run down from; the temporal resolve's goes in as it is, and arrives as it is.
10551
+ */
10552
+ f[at('uAoOffset')] = this.reconstructing
10553
+ ? (this.reconJitter[0] as number) / this.renderWidth
10554
+ : this.temporalJittering
10555
+ ? this.temporalJitterX / this.renderWidth
10556
+ : 0;
10557
+ f[at('uAoOffset') + 1] = this.reconstructing
10558
+ ? -(this.reconJitter[1] as number) / this.renderHeight
10559
+ : this.temporalJittering
10560
+ ? this.temporalJitterY / this.renderHeight
10561
+ : 0;
9796
10562
  f[at('uDofStrength')] = dofStrength;
9797
10563
  if (dofStrength > 0 && this.frameProjection !== null) {
9798
10564
  mat4.invert(this.dofInvProjection, this.frameProjection);
@@ -9952,6 +10718,8 @@ export class WebGPURenderer implements RendererApi {
9952
10718
 
9953
10719
  for (const dead of [
9954
10720
  ...this.reconHistories,
10721
+ ...this.reconFlickers,
10722
+ ...this.reconSwings,
9955
10723
  this.reconShown,
9956
10724
  this.reconMotion,
9957
10725
  this.reconPreviousDepth,
@@ -9960,6 +10728,10 @@ export class WebGPURenderer implements RendererApi {
9960
10728
  }
9961
10729
  this.reconHistories = [null, null];
9962
10730
  this.reconHistoryViews = [null, null];
10731
+ this.reconFlickers = [null, null];
10732
+ this.reconFlickerViews = [null, null];
10733
+ this.reconSwings = [null, null];
10734
+ this.reconSwingViews = [null, null];
9963
10735
  this.reconShown = null;
9964
10736
  this.reconShownView = null;
9965
10737
  this.reconMotion = null;
@@ -9984,24 +10756,44 @@ export class WebGPURenderer implements RendererApi {
9984
10756
  });
9985
10757
  this.reconHistories[i] = texture;
9986
10758
  this.reconHistoryViews[i] = texture.createView();
10759
+ const record = device.createTexture({
10760
+ label: `recon.flicker${String(i)}`,
10761
+ size: [outputWidth, outputHeight],
10762
+ format: RECON_HISTORY_FORMAT,
10763
+ usage: HISTORY_USAGE,
10764
+ });
10765
+ this.reconFlickers[i] = record;
10766
+ this.reconFlickerViews[i] = record.createView();
10767
+ const swing = device.createTexture({
10768
+ label: `recon.swing${String(i)}`,
10769
+ size: [outputWidth, outputHeight],
10770
+ format: 'r32float',
10771
+ usage: HISTORY_USAGE,
10772
+ });
10773
+ this.reconSwings[i] = swing;
10774
+ this.reconSwingViews[i] = swing.createView();
9987
10775
  }
9988
10776
  this.reconShown = device.createTexture({
9989
10777
  label: 'recon.shown',
9990
10778
  size: [outputWidth, outputHeight],
9991
10779
  format: RECON_HISTORY_FORMAT,
9992
- usage: HISTORY_USAGE,
10780
+ /* An attachment too, because the blended draws land on it after the upscale, and a copy
10781
+ source for the panes that refract it. See `runLate`. */
10782
+ usage: HISTORY_USAGE | 0x10 | 0x1, // RENDER_ATTACHMENT | COPY_SRC
9993
10783
  });
9994
10784
  this.reconShownView = this.reconShown.createView();
9995
10785
  this.reconMotion = device.createTexture({
9996
10786
  label: 'recon.motion',
9997
10787
  size: [renderWidth, renderHeight],
9998
10788
  format: RECON_HISTORY_FORMAT,
9999
- /* An attachment as well, because the pass that will write it is a draw. Nothing writes it
10000
- today and nothing needs to clear it either: WebGPU zero-initialises a new texture, and
10001
- a zero in the fourth channel is what tells the resolve to derive the camera's motion. */
10789
+ /* An attachment as well, because the pass that writes it is a draw. It is cleared every
10790
+ frame by that pass, and a zero in the fourth channel is what tells the resolve to derive
10791
+ the camera's motion. */
10002
10792
  usage: 0x4 | 0x10,
10003
10793
  });
10004
10794
  this.reconMotionView = this.reconMotion.createView();
10795
+ this.motionPass ??= new MotionPass(device);
10796
+ this.latePass ??= new LatePass(device);
10005
10797
  this.reconPreviousDepth = device.createTexture({
10006
10798
  label: 'recon.previousDepth',
10007
10799
  size: [renderWidth, renderHeight],
@@ -10056,6 +10848,26 @@ export class WebGPURenderer implements RendererApi {
10056
10848
  viewDimension: '2d' as const,
10057
10849
  },
10058
10850
  },
10851
+ sampled(8, 'float'),
10852
+ {
10853
+ binding: 9,
10854
+ visibility: COMPUTE,
10855
+ storageTexture: {
10856
+ access: 'write-only' as const,
10857
+ format: RECON_HISTORY_FORMAT,
10858
+ viewDimension: '2d' as const,
10859
+ },
10860
+ },
10861
+ sampled(10, 'unfilterable-float'),
10862
+ {
10863
+ binding: 11,
10864
+ visibility: COMPUTE,
10865
+ storageTexture: {
10866
+ access: 'write-only' as const,
10867
+ format: 'r32float' as const,
10868
+ viewDimension: '2d' as const,
10869
+ },
10870
+ },
10059
10871
  ],
10060
10872
  });
10061
10873
  const module = device.createShaderModule({ label: 'recon.resolve', code: reconResolveWgsl() });
@@ -10087,7 +10899,15 @@ export class WebGPURenderer implements RendererApi {
10087
10899
  if (this.reconMotionView === null || this.reconPreviousDepthView === null) return;
10088
10900
  if (this.reconShownView === null) return;
10089
10901
  const { device } = this.surface;
10090
- const build = (label: string, history: GPUTextureView, target: GPUTextureView) =>
10902
+ const build = (
10903
+ label: string,
10904
+ history: GPUTextureView,
10905
+ target: GPUTextureView,
10906
+ records: GPUTextureView,
10907
+ nextRecords: GPUTextureView,
10908
+ swings: GPUTextureView,
10909
+ nextSwings: GPUTextureView,
10910
+ ) =>
10091
10911
  device.createBindGroup({
10092
10912
  label,
10093
10913
  layout,
@@ -10100,242 +10920,298 @@ export class WebGPURenderer implements RendererApi {
10100
10920
  { binding: 5, resource: this.postSampler },
10101
10921
  { binding: 6, resource: { buffer: params } },
10102
10922
  { binding: 7, resource: target },
10923
+ { binding: 8, resource: records },
10924
+ { binding: 9, resource: nextRecords },
10925
+ { binding: 10, resource: swings },
10926
+ { binding: 11, resource: nextSwings },
10103
10927
  ],
10104
10928
  });
10105
10929
  for (let write = 0; write < 2; write += 1) {
10106
10930
  const read = this.reconHistoryViews[1 - write];
10107
10931
  const target = this.reconHistoryViews[write];
10108
- if (read === null || target === null) continue;
10109
- this.reconGroups[write] = build(`recon.resolve${String(write)}`, read, target);
10932
+ const records = this.reconFlickerViews[1 - write];
10933
+ const nextRecords = this.reconFlickerViews[write];
10934
+ const swings = this.reconSwingViews[1 - write];
10935
+ const nextSwings = this.reconSwingViews[write];
10936
+ if (read === null || target === null || records === null || nextRecords === null) continue;
10937
+ if (swings === null || nextSwings === null) continue;
10938
+ this.reconGroups[write] = build(
10939
+ `recon.resolve${String(write)}`,
10940
+ read,
10941
+ target,
10942
+ records,
10943
+ nextRecords,
10944
+ swings,
10945
+ nextSwings,
10946
+ );
10947
+ /* The sharpen reads no record, but one layout serves both dispatches: the same pair, so no
10948
+ texture is both read and written. */
10110
10949
  this.reconSharpenGroups[write] = build(
10111
10950
  `recon.sharpen${String(write)}`,
10112
10951
  target,
10113
10952
  this.reconShownView,
10953
+ records,
10954
+ nextRecords,
10955
+ swings,
10956
+ nextSwings,
10114
10957
  );
10115
10958
  }
10116
10959
  /* The composite's own group, over the resolved picture instead of the scene target. */
10117
10960
  this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
10118
10961
  /* And the scene's, because `ensureComposite` built it before this one existed. */
10119
10962
  this.rebuildRushBindGroup();
10963
+ /* And the bloom's, which blooms the resolved picture: see `rebuildBloomGroups`. */
10964
+ this.rebuildBloomGroups();
10965
+ }
10966
+
10967
+ /** This frame's jitter and temporal frame, decided where its scene camera is bound. See `bindMeshPass`. */
10968
+ private jitterFrame(): void {
10969
+ this.reconstructing = this.reconstructionWanted && !this.reflectionPassActive;
10970
+ if (this.quality.reconstruction > 0 && this.samples > 1 && !this.reconMultisampleSaid) {
10971
+ this.reconMultisampleSaid = true;
10972
+ console.warn(RECONSTRUCTION_MULTISAMPLE_REFUSAL);
10973
+ }
10974
+ if (this.reconstructing) {
10975
+ /*
10976
+ * The offset `settleReconJitter` chose for the frame at `beginFrame`, applied here and not
10977
+ * advanced here: a frame binding its mesh pass twice is one frame of the sequence.
10978
+ *
10979
+ * **The y offset is negated on the way in, and that is not a preference.** This shifts the
10980
+ * *corrected* matrix, whose y `CLIP_CORRECTION` has already negated so that the negation
10981
+ * every generated vertex stage ends with cancels out — §3 row 56. So an offset added here
10982
+ * arrives on screen with its sign reversed, while x, which nothing negates, arrives as
10983
+ * given. The temporal resolve cannot see this because its shader never reads the offset; the
10984
+ * reconstruction reads it at every texel it unprojects, and a sign error there moves every
10985
+ * sample two jitters away from where the depth says it is.
10986
+ */
10987
+ this.viewProj = jitterProjection(
10988
+ this.jitteredViewProj,
10989
+ this.correctedViewProj,
10990
+ this.reconJitter[0] as number,
10991
+ -(this.reconJitter[1] as number),
10992
+ this.renderWidth,
10993
+ this.renderHeight,
10994
+ ) as Float32Array;
10995
+ this.temporalJittering = false;
10996
+ this.temporalHistoryUsable = false;
10997
+ }
10998
+ this.temporalJittering =
10999
+ !this.reconstructing &&
11000
+ this.quality.temporalAa &&
11001
+ this.quality.screenEffects &&
11002
+ !this.reflectionPassActive;
11003
+ if (this.temporalJittering) {
11004
+ /* The scene's size, not the drawing buffer's: the history is a picture of the render. */
11005
+ this.temporalHistoryUsable = this.temporalHistory.openFrame(
11006
+ this.renderWidth,
11007
+ this.renderHeight,
11008
+ );
11009
+ const [jx, jy] = jitterOffset(this.temporalHistory.frameIndex);
11010
+ this.temporalJitterX = jx;
11011
+ this.temporalJitterY = jy;
11012
+ /* A jitter is half a *render* texel, which is what the sequence is spread over. */
11013
+ this.viewProj = jitterProjection(
11014
+ this.jitteredViewProj,
11015
+ this.correctedViewProj,
11016
+ jx,
11017
+ jy,
11018
+ this.renderWidth,
11019
+ this.renderHeight,
11020
+ ) as Float32Array;
11021
+ } else if (!this.reconstructing) {
11022
+ this.temporalHistoryUsable = false;
11023
+ this.viewProj = this.correctedViewProj;
11024
+ }
10120
11025
  }
10121
11026
 
10122
11027
  /**
10123
- * Remember that this mesh moved, so the motion pass can draw where it was.
11028
+ * Whether a blended draw recorded now lands after the upscale. See `lateRouting.ts`.
10124
11029
  *
10125
- * Silent unless a reconstruction is running: a scene that always states its previous transforms
10126
- * — which is the right thing for a scene to do, the alternative being to know what the renderer
10127
- * is configured as — must cost nothing at all when nobody is going to read them.
11030
+ * Not during the order-independent replay, which owns its pass and issues into it directly.
10128
11031
  */
10129
- private recordMotion(
10130
- mesh: GpuMesh,
10131
- model: ArrayLike<number>,
10132
- previousModel: ArrayLike<number>,
10133
- ): void {
10134
- if (!this.reconstructing) return;
10135
- const at = this.motionCount;
10136
- const wanted = (at + 1) * MOTION_DRAW_FLOATS;
10137
- if (this.motionMatrices.length < wanted) {
10138
- /* Doubling, so a scene settles on one allocation rather than one a draw. */
10139
- const grown = new Float32Array(Math.max(wanted, this.motionMatrices.length * 2, 64));
10140
- grown.set(this.motionMatrices);
10141
- this.motionMatrices = grown;
10142
- }
10143
- this.motionMatrices.set(model as ArrayLike<number> & Iterable<number>, at * MOTION_DRAW_FLOATS);
10144
- this.motionMatrices.set(
10145
- previousModel as ArrayLike<number> & Iterable<number>,
10146
- at * MOTION_DRAW_FLOATS + 16,
11032
+ private drawsLate(): boolean {
11033
+ return (
11034
+ this.latePass !== null &&
11035
+ this.oitEncoder === null &&
11036
+ routesLate(
11037
+ this.reconstructing,
11038
+ this.reflectionPassActive,
11039
+ this.probePassActive,
11040
+ this.framePresented,
11041
+ )
10147
11042
  );
10148
- this.motionMeshes[at] = mesh;
10149
- this.motionCount = at + 1;
10150
11043
  }
10151
11044
 
10152
11045
  /**
10153
- * The pipeline the motion pass draws with, built once.
10154
- *
10155
- * **The position buffer alone**, because that is all the shader reads and a mesh keeps one buffer
10156
- * an attribute — so this binds buffer zero and nothing else, whatever else the mesh carries.
11046
+ * The command a blended draw records into: the late pool while reconstructing, the frame graph
11047
+ * otherwise. **Late draws record whatever `quality.frameGraph` says**, since the late replay is
11048
+ * the only moment they can land; everything else keeps the path it has.
10157
11049
  */
10158
- private buildMotionPipeline(): void {
10159
- if (this.motionLayout !== null) return;
10160
- const { device } = this.surface;
10161
- this.motionLayout = device.createBindGroupLayout({
10162
- label: 'recon.motion.layout',
10163
- entries: [
10164
- { binding: 0, visibility: 0x1, buffer: { type: 'uniform' as const } },
10165
- {
10166
- binding: 1,
10167
- visibility: 0x1,
10168
- buffer: { type: 'uniform' as const, hasDynamicOffset: true },
10169
- },
10170
- ],
10171
- });
10172
- this.motionModule = device.createShaderModule({
10173
- label: 'recon.motion',
10174
- code: RECON_MOTION_WGSL,
10175
- });
10176
- this.motionPipelineLayout = device.createPipelineLayout({
10177
- bindGroupLayouts: [this.motionLayout],
10178
- });
11050
+ private recordBlended(reads: number, lands = this.drawsLate()): DrawCommand | null {
11051
+ const late = this.latePass;
11052
+ if (late !== null && lands) return late.take();
11053
+ return this.recordDraw(reads, this.currentTarget());
10179
11054
  }
10180
11055
 
10181
- /** The pipeline for one vertex stride, built on first sight of it. */
10182
- private motionPipelineFor(stride: number): GPURenderPipeline | null {
10183
- const held = this.motionPipelines.get(stride);
10184
- if (held !== undefined) return held;
10185
- const module = this.motionModule;
10186
- const layout = this.motionPipelineLayout;
10187
- if (module === null || layout === null) return null;
10188
- const { device } = this.surface;
10189
- const pipeline = device.createRenderPipeline({
10190
- label: `recon.motion.${String(stride)}`,
10191
- layout,
10192
- vertex: {
10193
- module,
10194
- entryPoint: 'motionVert',
10195
- /* Positions are the layout's first attribute and are never optional, so they are at offset
10196
- zero of every stride this can be handed. */
10197
- buffers: [
10198
- {
10199
- arrayStride: stride,
10200
- attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }],
10201
- },
10202
- ],
10203
- },
10204
- fragment: { module, entryPoint: 'motionFrag', targets: [{ format: MOTION_TARGET_FORMAT }] },
10205
- /*
10206
- * **The scene's own winding**, because this draws the scene's geometry through the scene's
10207
- * clip transform — the product the renderer hands over already carries the negation that
10208
- * `naga` writes into every generated vertex stage, so the faces come out the way they did
10209
- * and a `cw` front face here would draw the inside of every mesh. §3 row 57.
10210
- */
10211
- primitive: { topology: 'triangle-list', cullMode: 'back' },
10212
- depthStencil: {
10213
- format: DEPTH_FORMAT,
10214
- /*
10215
- * **Tested and not written.** The frame's depth is finished; this pass only wants the
10216
- * pixels the scene actually kept, and `DEPTH_COMPARE_EQUAL` is the comparison that admits
10217
- * exactly them — the same transform on the same positions gives the same depth, which is
10218
- * why the rasterisation matrix is handed over whole rather than rebuilt here.
10219
- *
10220
- * **And the equality is not fragile here**, which was worth measuring rather than
10221
- * assuming: this driver contracts multiply-adds (§3 row 107) and two shader modules can
10222
- * contract a matrix product differently, so an equality across them could have admitted
10223
- * only some of a surface. Run with `greater-equal` instead, the ghost measured the same
10224
- * to the pixel — 5,942 either way — so nothing is being dropped.
10225
- */
10226
- depthWriteEnabled: false,
10227
- depthCompare: DEPTH_COMPARE_EQUAL,
10228
- },
10229
- });
10230
- this.motionPipelines.set(stride, pipeline);
10231
- return pipeline;
11056
+ /** The pipelines a blended draw is built for: the reconstructed picture's while late. */
11057
+ private blendedPipelines(lands = this.drawsLate()): PipelineCache {
11058
+ return lands ? this.latePipelines : this.targetPipelines();
10232
11059
  }
10233
11060
 
10234
11061
  /**
10235
- * Where every mover was last frame, into the motion target.
10236
- *
10237
- * **After everything that draws and before the resolve reads it.** The frame's depth has to be
10238
- * finished for the equality test to mean anything, and the resolve is what consumes the result.
11062
+ * The blended draws of a reconstructing frame, over the reconstructed picture at the output size
11063
+ * and unjittered, against the render's depth taken up to that size. Before occlusion, bloom and
11064
+ * the grade, so they are graded exactly as they were when they were drawn into the scene.
10239
11065
  */
10240
- private runMotion(encoder: GPUCommandEncoder): void {
10241
- const target = this.reconMotionView;
11066
+ private runLate(encoder: GPUCommandEncoder): void {
11067
+ const late = this.latePass;
11068
+ const shown = this.reconShownView;
10242
11069
  const depth = this.depthView;
10243
- if (!this.reconstructing || target === null || depth === null) return;
10244
- const { device } = this.surface;
10245
- this.buildMotionPipeline();
10246
- const layout = this.motionLayout;
10247
- if (layout === null) return;
10248
-
10249
- /*
10250
- * **Cleared every frame, even when nothing moved.** A texel left from last frame is a flag
10251
- * saying "a draw wrote this", and the resolve would take a motion belonging to a surface that
10252
- * is no longer there. The clear is the pass's own load operation, so an empty queue still runs
10253
- * it — which is why this is not returned from before the pass is opened.
10254
- */
11070
+ if (!this.reconstructing || late === null || shown === null || depth === null) return;
11071
+ const oitSet = this.oitActive && this.translucentQueue.length > 0;
11072
+ if (late.pending === 0 && !oitSet) return;
11073
+ const width = Math.max(1, this.surface.canvas.width);
11074
+ const height = Math.max(1, this.surface.canvas.height);
11075
+ /* The order-independent set tests against this depth too, so it is made for either. */
11076
+ late.upscale(
11077
+ encoder,
11078
+ depth,
11079
+ this.renderWidth,
11080
+ this.renderHeight,
11081
+ width,
11082
+ height,
11083
+ this.reconJitter[0] as number,
11084
+ this.reconJitter[1] as number,
11085
+ );
11086
+ if (this.lateRefractWanted && this.refractSnapshot !== null && this.reconShown !== null) {
11087
+ encoder.copyTextureToTexture(
11088
+ { texture: this.reconShown },
11089
+ { texture: this.refractSnapshot },
11090
+ [this.refractSnapshot.width, this.refractSnapshot.height],
11091
+ );
11092
+ this.refractMips?.record(encoder);
11093
+ this.refractSnapshotTaken = true;
11094
+ }
11095
+ this.lateRefractWanted = false;
11096
+ if (late.pending === 0) return;
10255
11097
  const pass = encoder.beginRenderPass({
10256
- label: 'recon.motion',
11098
+ label: 'recon.late',
10257
11099
  timestampWrites: this.gpuTimer.writesFor(),
10258
- colorAttachments: [
10259
- { view: target, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
10260
- ],
11100
+ colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
10261
11101
  depthStencilAttachment: {
10262
- view: depth,
11102
+ view: late.depthView(width, height),
10263
11103
  depthLoadOp: 'load',
11104
+ /* Kept: the order-independent set replays against it after this pass. */
10264
11105
  depthStoreOp: 'store',
10265
- depthReadOnly: false,
10266
11106
  },
10267
11107
  });
11108
+ late.replay(pass, this.issueBound);
11109
+ pass.end();
11110
+ }
10268
11111
 
10269
- const count = this.motionCount;
10270
- if (count > 0) {
10271
- const bytes = count * MOTION_DRAW_STRIDE;
10272
- if (this.motionDrawCapacity < count) {
10273
- this.motionDrawBuffer?.destroy();
10274
- this.motionDrawCapacity = Math.max(count, this.motionDrawCapacity * 2, 16);
10275
- this.motionDrawBuffer = device.createBuffer({
10276
- label: 'recon.motion.draws',
10277
- size: this.motionDrawCapacity * MOTION_DRAW_STRIDE,
10278
- usage: 0x40 | 0x8, // UNIFORM | COPY_DST
10279
- });
10280
- this.motionDrawStaging = new Uint8Array(this.motionDrawCapacity * MOTION_DRAW_STRIDE);
10281
- this.motionGroup = null;
10282
- }
10283
- this.motionFrameBuffer ??= device.createBuffer({
10284
- label: 'recon.motion.frame',
10285
- size: MOTION_FRAME_FLOATS * 4,
10286
- usage: 0x40 | 0x8,
10287
- });
10288
- this.motionGroup ??= device.createBindGroup({
10289
- label: 'recon.motion.group',
10290
- layout,
10291
- entries: [
10292
- { binding: 0, resource: { buffer: this.motionFrameBuffer } },
10293
- {
10294
- binding: 1,
10295
- resource: { buffer: this.motionDrawBuffer as GPUBuffer, size: MOTION_DRAW_STRIDE },
10296
- },
10297
- ],
10298
- });
10299
-
10300
- /* The scene's clip transform, with the generated stages' y negation folded in. */
10301
- mat4.multiply(this.motionRaster, MOTION_CLIP_FLIP, this.viewProj ?? this.correctedViewProj);
10302
- this.motionFrameStaging.set(this.motionRaster as Float32Array, 0);
10303
- this.motionFrameStaging.set(this.frameRawViewProj as Float32Array, 16);
10304
- this.motionFrameStaging.set(this.reconPreviousRawViewProj as Float32Array, 32);
10305
- device.queue.writeBuffer(this.motionFrameBuffer, 0, this.motionFrameStaging);
10306
-
10307
- const draws = new Float32Array(this.motionDrawStaging.buffer);
10308
- for (let i = 0; i < count; i += 1) {
10309
- draws.set(
10310
- this.motionMatrices.subarray(i * MOTION_DRAW_FLOATS, (i + 1) * MOTION_DRAW_FLOATS),
10311
- (i * MOTION_DRAW_STRIDE) / 4,
10312
- );
10313
- }
10314
- device.queue.writeBuffer(
10315
- this.motionDrawBuffer as GPUBuffer,
10316
- 0,
10317
- this.motionDrawStaging,
10318
- 0,
10319
- bytes,
10320
- );
11112
+ /** Whether this draw lands in the reconstructed picture. See `lateRouting.ts`. */
11113
+ private tracksMotionNow(): boolean {
11114
+ return tracksMotion(
11115
+ this.reconstructing,
11116
+ this.reflectionPassActive,
11117
+ this.probePassActive,
11118
+ this.framePresented,
11119
+ );
11120
+ }
10321
11121
 
10322
- let bound: GPURenderPipeline | null = null;
10323
- for (let i = 0; i < count; i += 1) {
10324
- const mesh = this.motionMeshes[i];
10325
- if (mesh === undefined) continue;
10326
- const pipeline = this.motionPipelineFor(mesh.vertexStride);
10327
- if (pipeline === null) continue;
10328
- if (pipeline !== bound) {
10329
- pass.setPipeline(pipeline);
10330
- bound = pipeline;
10331
- }
10332
- pass.setBindGroup(0, this.motionGroup, [i * MOTION_DRAW_STRIDE]);
10333
- pass.setVertexBuffer(0, mesh.vertexBuffers[0] as GPUBuffer);
10334
- pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
10335
- pass.drawIndexed(mesh.indexCount);
10336
- }
11122
+ /**
11123
+ * What this draw's motion is, recorded for the motion pass.
11124
+ *
11125
+ * A mesh rewritten this frame carries its vertex motion whatever else the draw says — under last
11126
+ * frame's model where a mover or a matrix knows it, under this frame's where nothing does. A
11127
+ * mover otherwise carries a rigid draw's last model or a skinned draw's last pose; a matrix
11128
+ * carries a rigid draw's. A draw that says nothing and moved no vertex takes the camera's motion.
11129
+ */
11130
+ private recordDrawMotion(
11131
+ mesh: GpuMesh,
11132
+ model: ArrayLike<number>,
11133
+ previous: ArrayLike<number> | Mover | null,
11134
+ ): void {
11135
+ const dynamic = mesh.motion;
11136
+ const rows = dynamic?.previous ?? null;
11137
+ const rewritten = rows !== null && dynamic !== null && dynamic.changed === this.moverFrame;
11138
+ if (previous === null) {
11139
+ if (rewritten) this.motionPass?.recordDynamic(mesh, model, model, rows);
11140
+ return;
10337
11141
  }
10338
- pass.end();
11142
+ if (!isMover(previous)) {
11143
+ if (rewritten) this.motionPass?.recordDynamic(mesh, model, previous, rows);
11144
+ else this.motionPass?.recordRigid(mesh, model, previous);
11145
+ return;
11146
+ }
11147
+ /* A rig is only followed where the draw really skins: a palette set, a mesh with joints. */
11148
+ const skinned =
11149
+ !rewritten && this.skinPaletteSlot >= 0 && mesh.isSkinned && mesh.skinOffsets !== null;
11150
+ const verdict = advanceMover(
11151
+ previous,
11152
+ this.moverRenderer,
11153
+ this.moverFrame,
11154
+ model,
11155
+ skinned ? this.skinPaletteData : null,
11156
+ );
11157
+ if (verdict === MOVER_TWICE) {
11158
+ this.warnMoverTwice();
11159
+ return;
11160
+ }
11161
+ if (rewritten) {
11162
+ const was = verdict === MOVER_MOTION ? previous.previousModel : model;
11163
+ this.motionPass?.recordDynamic(mesh, model, was, rows);
11164
+ return;
11165
+ }
11166
+ if (verdict !== MOVER_MOTION) return;
11167
+ if (skinned) this.recordSkinnedMotion(mesh, model, previous);
11168
+ else this.motionPass?.recordRigid(mesh, model, previous.previousModel);
11169
+ }
11170
+
11171
+ /**
11172
+ * A skinned mover's motion: this frame's palette is the draw's own slot, and last frame's takes a
11173
+ * slot of its own, like any palette a draw binds — so it is uploaded once and nothing overwrites
11174
+ * it before the motion pass reads it. See `SkinPaletteRing`.
11175
+ */
11176
+ private recordSkinnedMotion(mesh: GpuMesh, model: ArrayLike<number>, mover: Mover): void {
11177
+ const now = this.skinPalettes.view(this.skinPaletteSlot);
11178
+ const slot = this.skinPalettes.take(this.surface.device, mover.previousPalette);
11179
+ const was = slot === null ? null : this.skinPalettes.view(slot);
11180
+ if (now === null || was === null) return;
11181
+ this.motionPass?.recordSkinned(mesh, model, mover.previousModel, now, was);
11182
+ }
11183
+
11184
+ private warnMoverTwice(): void {
11185
+ if (this.warnedMoverTwice) return;
11186
+ this.warnedMoverTwice = true;
11187
+ console.warn(
11188
+ 'WebGPU: a Mover was drawn twice in one frame. A mover is one object; the second draw ' +
11189
+ 'records no motion. Give each moving object its own `createMover()`.',
11190
+ );
11191
+ }
11192
+
11193
+ /**
11194
+ * Where every mover was last frame, into the motion target. See `MotionPass`.
11195
+ *
11196
+ * **After everything that draws and before the resolve reads it.** The frame's depth has to be
11197
+ * finished for the pass's equality test to mean anything, and the resolve is what consumes it.
11198
+ */
11199
+ private runMotion(encoder: GPUCommandEncoder): void {
11200
+ const target = this.reconMotionView;
11201
+ const depth = this.depthView;
11202
+ const motion = this.motionPass;
11203
+ if (!this.reconstructing || target === null || depth === null || motion === null) return;
11204
+ /* The scene's clip transform, with the generated stages' y negation folded in. */
11205
+ mat4.multiply(this.motionRaster, MOTION_CLIP_FLIP, this.viewProj ?? this.correctedViewProj);
11206
+ motion.run(
11207
+ encoder,
11208
+ target,
11209
+ depth,
11210
+ this.motionRaster as Float32Array,
11211
+ this.frameRawViewProj as Float32Array,
11212
+ this.reconPreviousRawViewProj as Float32Array,
11213
+ this.gpuTimer.writesFor(),
11214
+ );
10339
11215
  }
10340
11216
 
10341
11217
  /**
@@ -10365,7 +11241,16 @@ export class WebGPURenderer implements RendererApi {
10365
11241
  * OpenGL's depth convention — which is the convention the shader's accessor converts the
10366
11242
  * renderer's reversed buffer into.
10367
11243
  */
10368
- mat4.invert(this.reconInverseViewProj, this.frameRawViewProj);
11244
+ /*
11245
+ * The inverse and the reprojection in double precision, rounded once on the way into the
11246
+ * uniform: the product of two single-precision matrices this far apart in depth is not the
11247
+ * identity for a still camera, and the resolve read the difference as motion.
11248
+ */
11249
+ mat4.invert(this.reconInverse64, this.frameRawViewProj);
11250
+ mat4.multiply(this.reconReprojection64, this.reconPreviousRawViewProj, this.reconInverse64);
11251
+ for (let i = 0; i < 16; i += 1) {
11252
+ this.reconInverseViewProj[i] = this.reconInverse64[i] as number;
11253
+ }
10369
11254
  const f = this.reconFloats;
10370
11255
  const u = this.reconInts;
10371
11256
  u[0] = this.renderWidth;
@@ -10389,6 +11274,17 @@ export class WebGPURenderer implements RendererApi {
10389
11274
  f[69] = DEFAULT_DISOCCLUSION.motionScale;
10390
11275
  f[70] = DEFAULT_DISOCCLUSION.normalFloor;
10391
11276
  f[71] = DEFAULT_DISOCCLUSION.normalCeiling;
11277
+ /* The anti-flicker over the reconstruction's own period, its rule made once a period. */
11278
+ const phases = reconJitterPhases(this.renderWidth, outputWidth);
11279
+ if (phases !== this.reconFlickerPhases) {
11280
+ this.reconFlickerPhases = phases;
11281
+ this.reconFlickerRule = flickerRule(phases);
11282
+ }
11283
+ u[72] = this.reconFrameIndex % phases === 0 ? 1 : 0;
11284
+ u[73] = 1;
11285
+ f[74] = this.reconFlickerRule.memory;
11286
+ f[75] = this.reconFlickerRule.width;
11287
+ f.set(this.reconReprojection64, 76);
10392
11288
  device.queue.writeBuffer(params, 0, this.reconStaging);
10393
11289
 
10394
11290
  const groupsX = Math.ceil(outputWidth / RECON_WORKGROUP);
@@ -10781,9 +11677,15 @@ export class WebGPURenderer implements RendererApi {
10781
11677
  if (!this.oitActive || this.translucentQueue.length === 0) return;
10782
11678
  const accum = this.oitAccumView;
10783
11679
  const reveal = this.oitRevealView;
10784
- const scene = this.sceneColorView;
11680
+ /* After the upscale while reconstructing: over the reconstructed picture, against its depth. */
11681
+ const scene = this.reconstructing ? this.reconShownView : this.sceneColorView;
10785
11682
  const group = this.oitResolveGroup;
10786
- const depth = this.depthView;
11683
+ const depth = this.reconstructing
11684
+ ? (this.latePass?.depthView(
11685
+ Math.max(1, this.surface.canvas.width),
11686
+ Math.max(1, this.surface.canvas.height),
11687
+ ) ?? null)
11688
+ : this.depthView;
10787
11689
  if (accum === null || reveal === null || scene === null || group === null || depth === null) {
10788
11690
  return;
10789
11691
  }
@@ -10797,12 +11699,13 @@ export class WebGPURenderer implements RendererApi {
10797
11699
  */
10798
11700
  if (this.translucentQueue.refracts && !this.refractSnapshotTaken) {
10799
11701
  const snapshot = this.refractSnapshot;
10800
- const source = this.sceneColor;
11702
+ const source = this.reconstructing ? this.reconShown : this.sceneColor;
10801
11703
  if (snapshot !== null && source !== null) {
10802
11704
  encoder.copyTextureToTexture({ texture: source }, { texture: snapshot }, [
10803
11705
  snapshot.width,
10804
11706
  snapshot.height,
10805
11707
  ]);
11708
+ this.refractMips?.record(encoder);
10806
11709
  this.refractSnapshotTaken = true;
10807
11710
  }
10808
11711
  }
@@ -10843,6 +11746,7 @@ export class WebGPURenderer implements RendererApi {
10843
11746
  options.refraction = draw.refraction;
10844
11747
  options.refractTint = draw.refractTint as unknown as Vec3 | undefined;
10845
11748
  options.thicknessM = draw.thicknessM;
11749
+ options.glass = draw.glass ?? undefined;
10846
11750
  this.drawTranslucentMesh(draw.mesh as GpuMesh, draw.model, draw.opacity, options);
10847
11751
  });
10848
11752
  this.oitReplaying = false;
@@ -10873,9 +11777,9 @@ export class WebGPURenderer implements RendererApi {
10873
11777
  this.pipelines,
10874
11778
  device,
10875
11779
  this.oitResolveLayout,
10876
- 'post.oitResolve',
11780
+ this.reconstructing ? 'post.oitResolve.recon' : 'post.oitResolve',
10877
11781
  OIT_RESOLVE_FRAG_WGSL,
10878
- sceneColorFormat(this.quality.hdrScene),
11782
+ this.reconstructing ? RECON_HISTORY_FORMAT : sceneColorFormat(this.quality.hdrScene),
10879
11783
  OIT_COMPOSITE_BLEND,
10880
11784
  ),
10881
11785
  );
@@ -11265,18 +12169,14 @@ export class WebGPURenderer implements RendererApi {
11265
12169
  this.lightVolumeVertices.buffer,
11266
12170
  this.lightVolumeFragments.buffer,
11267
12171
  (name) =>
11268
- name === 'uStaticShadowMap'
11269
- ? { view: this.shadowView, sampler: this.volumeShadowSampler }
11270
- : name === 'uPeeledShadowMap' && this.peelView !== null
11271
- ? { view: this.peelView, sampler: this.volumeShadowSampler }
11272
- : name === 'uDynamicShadowMap' && this.dynamicView !== null
11273
- ? { view: this.dynamicView, sampler: this.volumeShadowSampler }
11274
- : /* The frame's own depth, snapshotted before the first volume of the frame draws.
12172
+ name === 'uSunShadows'
12173
+ ? { view: this.sunArrayView, sampler: this.volumeShadowSampler }
12174
+ : /* The frame's own depth, snapshotted before the first volume of the frame draws.
11275
12175
  Null under a profile with no composite, and then `uSceneDepthEnabled` is 0 and
11276
12176
  the stand-in is never sampled. */
11277
- name === 'uSceneDepth' && this.resolvedDepthView !== null
11278
- ? { view: this.resolvedDepthView, sampler: this.postDepthSampler }
11279
- : this.volumeBlank,
12177
+ name === 'uSceneDepth' && this.resolvedDepthView !== null
12178
+ ? { view: this.resolvedDepthView, sampler: this.postDepthSampler }
12179
+ : this.volumeBlank,
11280
12180
  );
11281
12181
  }
11282
12182
 
@@ -11302,6 +12202,12 @@ export class WebGPURenderer implements RendererApi {
11302
12202
  private takeRefractSnapshot(): boolean {
11303
12203
  if (this.reflectionPassActive || this.probePassActive) return false;
11304
12204
  if (this.refractSnapshotTaken) return true;
12205
+ /* A late draw refracts the reconstructed picture, which does not exist until the resolve has
12206
+ run; `runLate` takes the copy then, once, before any late draw is replayed. */
12207
+ if (this.drawsLate()) {
12208
+ this.lateRefractWanted = true;
12209
+ return true;
12210
+ }
11305
12211
  /* The replay of an order-independent set runs after the scene's pass has ended, and `runOit`
11306
12212
  took the copy for it before it began; there is no pass here to end and reopen. */
11307
12213
  if (this.oitReplaying) return false;
@@ -11340,6 +12246,8 @@ export class WebGPURenderer implements RendererApi {
11340
12246
  snapshot.width,
11341
12247
  snapshot.height,
11342
12248
  ]);
12249
+ /* The chain, between the passes, where nothing is attached: what frosted glass reads. */
12250
+ this.refractMips?.record(encoder);
11343
12251
  const multisampled = this.compositeMsaa() !== null;
11344
12252
  this.pass = encoder.beginRenderPass({
11345
12253
  label: 'refract.snapshotTaken',
@@ -11676,6 +12584,7 @@ export class WebGPURenderer implements RendererApi {
11676
12584
  command.indexBuffer = null;
11677
12585
  command.indexed = false;
11678
12586
  command.instances = 1;
12587
+ command.indirect = null;
11679
12588
  recordNode(this.arena, VERB_DRAW, reads, writes, at, 0);
11680
12589
  return command;
11681
12590
  }
@@ -11701,8 +12610,13 @@ export class WebGPURenderer implements RendererApi {
11701
12610
  }
11702
12611
  if (verb !== VERB_DRAW) return;
11703
12612
  const command = this.commands.commands[nodeState(this.arena, at)];
11704
- if (command === undefined || command.pipeline === null || command.bindGroup === null) return;
12613
+ if (command === undefined) return;
12614
+ this.issueCommand(pass, command);
12615
+ }
11705
12616
 
12617
+ /** One recorded draw, issued into an open pass. Shared by the graph's replay and `runLate`. */
12618
+ private issueCommand(pass: GPURenderPassEncoder, command: DrawCommand): void {
12619
+ if (command.pipeline === null || command.bindGroup === null) return;
11706
12620
  pass.setPipeline(command.pipeline);
11707
12621
  if (command.offsetCount === 2) {
11708
12622
  pass.setBindGroup(0, command.bindGroup, [command.offsetA, command.offsetB]);
@@ -11717,7 +12631,8 @@ export class WebGPURenderer implements RendererApi {
11717
12631
  }
11718
12632
  if (command.indexed && command.indexBuffer !== null) {
11719
12633
  pass.setIndexBuffer(command.indexBuffer, 'uint32');
11720
- pass.drawIndexed(command.count, command.instances);
12634
+ if (command.indirect !== null) pass.drawIndexedIndirect(command.indirect, 0);
12635
+ else pass.drawIndexed(command.count, command.instances);
11721
12636
  } else {
11722
12637
  pass.draw(command.count, command.instances);
11723
12638
  }
@@ -11946,6 +12861,7 @@ export class WebGPURenderer implements RendererApi {
11946
12861
  }
11947
12862
 
11948
12863
  endFrame(): void {
12864
+ this.inFrame = false;
11949
12865
  if (this.surface.lost || this.encoder === null) return;
11950
12866
  /*
11951
12867
  * Anything still recorded goes now.
@@ -12373,6 +13289,11 @@ export class WebGPURenderer implements RendererApi {
12373
13289
  * declares nothing behaves exactly as it did before. Read it beside `visible`, which answers the
12374
13290
  * other half of the same question.
12375
13291
  */
13292
+ /** Whether a world-space box is wholly behind the declared occluders. See `Renderer.occludedBox`. */
13293
+ occludedBox(min: ArrayLike<number>, max: ArrayLike<number>): boolean {
13294
+ return this.occlusion?.occludedBox(min, max) ?? false;
13295
+ }
13296
+
12376
13297
  occluded(bounds: Bounds, model: ReadonlyMat4): boolean {
12377
13298
  return this.occlusion?.occluded(bounds, model) ?? false;
12378
13299
  }
@@ -12828,12 +13749,14 @@ export class WebGPURenderer implements RendererApi {
12828
13749
  const cssHeight = Math.max(canvas.clientHeight, 1);
12829
13750
  const origin = canvas.getBoundingClientRect();
12830
13751
  /*
12831
- * **CSS pixels to the scene's pixels, not the viewer's.** A viewport is state on the frame's
12832
- * own pass, which draws into the render-size targets, so a box measured in the drawing buffer
12833
- * would be scaled by the reconstruction ratio and land off the side of a smaller attachment.
13752
+ * **CSS pixels to the pixels of the attachment this pass draws into.** Mid-frame that is the
13753
+ * scene's render size, which a reconstruction makes smaller than the drawing buffer; after the
13754
+ * present it is the swap image, the drawing buffer itself. Measured against the scene's size
13755
+ * there, an interface's inset landed two thirds of the way to where it was asked for.
12834
13756
  */
12835
- const scaleX = this.renderWidth / cssWidth;
12836
- const scaleY = this.renderHeight / cssHeight;
13757
+ const target = this.passSize();
13758
+ const scaleX = target[0] / cssWidth;
13759
+ const scaleY = target[1] / cssHeight;
12837
13760
  const left = rect.left - origin.left;
12838
13761
  const top = rect.top - origin.top;
12839
13762
  const w = Math.max(1, Math.round(rect.width * scaleX));
@@ -12861,9 +13784,8 @@ export class WebGPURenderer implements RendererApi {
12861
13784
  * edge for the frames it is half-off. That asymmetry is the cost of the clamp, it is
12862
13785
  * recorded in the parity ledger, and the alternative was an exception.
12863
13786
  */
12864
- /* The attachment this pass draws into, which is the scene's size — see `renderWidth`. */
12865
- const targetWidth = this.renderWidth;
12866
- const targetHeight = this.renderHeight;
13787
+ const targetWidth = target[0];
13788
+ const targetHeight = target[1];
12867
13789
  const clampedX = Math.max(0, Math.min(x, targetWidth));
12868
13790
  const clampedY = Math.max(0, Math.min(y, targetHeight));
12869
13791
  const clampedW = Math.max(0, Math.min(x + w, targetWidth) - clampedX);
@@ -12940,11 +13862,31 @@ export class WebGPURenderer implements RendererApi {
12940
13862
  if (this.quality.frameGraph) this.flushGraph();
12941
13863
  const pass = this.pass;
12942
13864
  if (this.surface.lost || pass === null) return;
12943
- /* The whole of what this pass draws into, which is the scene's size and not the viewer's. */
12944
- pass.setViewport(0, 0, this.renderWidth, this.renderHeight, 0, 1);
12945
- pass.setScissorRect(0, 0, this.renderWidth, this.renderHeight);
13865
+ /* The whole of what this pass draws into: the scene's size mid-frame, the canvas after. */
13866
+ const [width, height] = this.passSize();
13867
+ pass.setViewport(0, 0, width, height, 0, 1);
13868
+ pass.setScissorRect(0, 0, width, height);
12946
13869
  }
12947
13870
 
13871
+ /**
13872
+ * The size of the attachment the open pass draws into: the swap image — the drawing buffer —
13873
+ * in the overlay after the present, the scene's render size in the frame's own pass. A tuple
13874
+ * held once rather than a new array a call.
13875
+ */
13876
+ private passSize(): readonly [number, number] {
13877
+ const size = this.passSizeOut;
13878
+ if (this.overlayActive) {
13879
+ size[0] = Math.max(1, this.surface.canvas.width);
13880
+ size[1] = Math.max(1, this.surface.canvas.height);
13881
+ } else {
13882
+ size[0] = this.renderWidth;
13883
+ size[1] = this.renderHeight;
13884
+ }
13885
+ return size;
13886
+ }
13887
+
13888
+ private readonly passSizeOut: [number, number] = [0, 0];
13889
+
12948
13890
  /**
12949
13891
  * Copy a rectangle of the frame into a 2D canvas, pixel for pixel.
12950
13892
  *
@@ -13212,6 +14154,10 @@ export class WebGPURenderer implements RendererApi {
13212
14154
  *
13213
14155
  * What it gives up is one frame of history: the frame after a cut has no blur and no temporal
13214
14156
  * blend, as the first frame of a session has none. Nothing else is reset.
14157
+ *
14158
+ * **A reconstructing renderer depends on it**: its history is the last shot's, and without this
14159
+ * call a cut blends that shot into the first frames of the next. Nothing detects a cut on the
14160
+ * caller's behalf, deliberately — a fast pan and a cut look alike from here.
13215
14161
  */
13216
14162
  cameraCut(): void {
13217
14163
  this.hasPreviousView = false;
@@ -13421,9 +14367,13 @@ export class WebGPURenderer implements RendererApi {
13421
14367
  this.causticsVerts.writeBlock(vertexSlot, this.causticsVertBlock);
13422
14368
  this.causticsFrags.writeBlock(fragmentSlot, this.causticsFragInts);
13423
14369
 
13424
- const causticsPipe = causticsPipeline(this.pipelines, this.surface.device, this.causticsLayout);
14370
+ const causticsPipe = causticsPipeline(
14371
+ this.blendedPipelines(),
14372
+ this.surface.device,
14373
+ this.causticsLayout,
14374
+ );
13425
14375
  /* The pattern is generated in the shader from the two uniform blocks; nothing is sampled. */
13426
- const causticsCommand = this.recordDraw(0, this.currentTarget());
14376
+ const causticsCommand = this.recordBlended(0);
13427
14377
  if (causticsCommand !== null) {
13428
14378
  causticsCommand.pipeline = causticsPipe;
13429
14379
  causticsCommand.bindGroup = this.causticsBindGroup;
@@ -13527,10 +14477,14 @@ export class WebGPURenderer implements RendererApi {
13527
14477
  private probeView: GPUTextureView | null = null;
13528
14478
  private readonly probeSize: number;
13529
14479
  private readonly probeMipLayout: GPUBindGroupLayout;
13530
- /** The photometric atlas, allocated on first use. See `setIesProfiles`. */
13531
- private iesTexture: GPUTexture | null = null;
14480
+ /** The fixture atlas — cookies over profiles — built on first use. See `fixtureAtlas.ts`. */
14481
+ private fixtureTexture: GPUTexture | null = null;
14482
+ private fixtureView: GPUTextureView | null = null;
14483
+ /** The profiles as packed and the cookie images, kept so either can rebuild the atlas. */
14484
+ private iesAtlas = packIesAtlas([]);
14485
+ private cookieImages: readonly TexImageSource[] = [];
13532
14486
  /** The scene's DriftLight field, and its two volumes once it is whole. See `createLightField`. */
13533
- private lightField: LightField | null = null;
14487
+ private lightField: DriftLightVolumes | null = null;
13534
14488
  private driftIndex: GPUTexture | null = null;
13535
14489
  private driftAtlas: GPUTexture | null = null;
13536
14490
  private driftIndexView: GPUTextureView | null = null;
@@ -13542,10 +14496,7 @@ export class WebGPURenderer implements RendererApi {
13542
14496
  /** The frame's exact lights packed for the traced bake. See `gi/bounceLights.ts`. */
13543
14497
  private readonly bounceLights = createBounceLights();
13544
14498
  private readonly bakeLamps = { data: this.bounceLights, count: 0, falloff: 0 };
13545
- private iesView: GPUTextureView | null = null;
13546
- private cookieTexture: GPUTexture | null = null;
13547
- private cookieView: GPUTextureView | null = null;
13548
- /** Tiles in the cookie atlas. 0 is the white placeholder and the whole off path. */
14499
+ /** Cookie tiles in the atlas. 0 is none, and the whole off path. */
13549
14500
  private cookieTiles = 0;
13550
14501
  /** Rows in that atlas. One until a consumer loads a profile, which is the row of ones. */
13551
14502
  private iesRows = 1;
@@ -14033,42 +14984,58 @@ export class WebGPURenderer implements RendererApi {
14033
14984
  */
14034
14985
  setSpotCookies(images: readonly TexImageSource[]): void {
14035
14986
  if (this.surface.lost) return;
14987
+ this.cookieImages = images.slice();
14988
+ this.forgetFixtureAtlas();
14989
+ this.rebuildFlatBindGroup();
14990
+ }
14991
+
14992
+ /** Let the atlas go, to be built again from what is in hand the next time it is bound. */
14993
+ private forgetFixtureAtlas(): void {
14994
+ this.fixtureTexture?.destroy();
14995
+ this.fixtureTexture = null;
14996
+ this.fixtureView = null;
14997
+ }
14998
+
14999
+ /**
15000
+ * The fixture atlas's view, built from the profiles and cookies in hand if it is not already: the
15001
+ * profiles' rows at the bottom, the cookies' tiles on top. See `renderer.ts` and `fixtureAtlas.ts`.
15002
+ *
15003
+ * **`rgba16float`, not `rgba32float`**: a 32-bit float texture is not filterable in core WebGPU,
15004
+ * and binding one to a filtering sampler takes the whole bind group with it. Both tables want
15005
+ * linear filtering. `RENDER_ATTACHMENT` because `copyExternalImageToTexture` writes through one.
15006
+ */
15007
+ private fixtureAtlasView(): GPUTextureView {
15008
+ if (this.fixtureView !== null) return this.fixtureView;
14036
15009
  const { device } = this.surface;
14037
- const tiles = images.length;
14038
- const width = tiles === 0 ? 1 : COOKIE_TILE * tiles;
14039
- const height = tiles === 0 ? 1 : COOKIE_TILE;
14040
- if (this.cookieTexture === null || this.cookieTexture.width !== width) {
14041
- this.cookieTexture?.destroy();
14042
- this.cookieTexture = device.createTexture({
14043
- label: 'cookie.atlas',
14044
- size: [width, height],
14045
- format: 'rgba8unorm',
14046
- /* `RENDER_ATTACHMENT` because `copyExternalImageToTexture` writes through one. */
14047
- usage: 0x4 | 0x2 | 0x10,
14048
- });
14049
- this.cookieView = this.cookieTexture.createView();
14050
- }
14051
- if (tiles === 0) {
14052
- /* White, not black: this multiplies a light's colour. */
14053
- device.queue.writeTexture(
14054
- { texture: this.cookieTexture },
14055
- new Uint8Array([255, 255, 255, 255]),
14056
- { bytesPerRow: 4, rowsPerImage: 1 },
14057
- { width: 1, height: 1, depthOrArrayLayers: 1 },
15010
+ const layout = fixtureAtlasLayout(this.iesAtlas.height, this.cookieImages.length);
15011
+ const texture = device.createTexture({
15012
+ label: 'fixture.atlas',
15013
+ size: [layout.width, layout.height],
15014
+ format: 'rgba16float',
15015
+ usage: 0x4 | 0x2 | 0x10,
15016
+ });
15017
+ device.queue.writeTexture(
15018
+ { texture },
15019
+ /* Converted, because `writeTexture` does not — see `environmentTexels`. */
15020
+ toHalfFloats(fixtureAtlasTexels(layout, this.iesAtlas)),
15021
+ { bytesPerRow: layout.width * 8, rowsPerImage: layout.height },
15022
+ { width: layout.width, height: layout.height, depthOrArrayLayers: 1 },
15023
+ );
15024
+ for (let tile = 0; tile < this.cookieImages.length; tile++) {
15025
+ const source = this.cookieImages[tile];
15026
+ if (source === undefined) continue;
15027
+ device.queue.copyExternalImageToTexture(
15028
+ { source: source as GPUCopyExternalImageSource },
15029
+ { texture, origin: { x: tile * COOKIE_TILE, y: 0 } },
15030
+ { width: COOKIE_TILE, height: COOKIE_TILE },
14058
15031
  );
14059
- } else {
14060
- for (let tile = 0; tile < tiles; tile++) {
14061
- const source = images[tile];
14062
- if (source === undefined) continue;
14063
- device.queue.copyExternalImageToTexture(
14064
- { source: source as GPUCopyExternalImageSource },
14065
- { texture: this.cookieTexture, origin: { x: tile * COOKIE_TILE, y: 0 } },
14066
- { width: COOKIE_TILE, height: COOKIE_TILE },
14067
- );
14068
- }
14069
15032
  }
14070
- this.cookieTiles = tiles;
14071
- this.rebuildFlatBindGroup();
15033
+ this.fixtureTexture = texture;
15034
+ this.fixtureView = texture.createView();
15035
+ this.iesRows = this.iesAtlas.height;
15036
+ this.iesPlanes = this.iesAtlas.planes;
15037
+ this.cookieTiles = this.cookieImages.length;
15038
+ return this.fixtureView;
14072
15039
  }
14073
15040
 
14074
15041
  /**
@@ -14085,6 +15052,23 @@ export class WebGPURenderer implements RendererApi {
14085
15052
  return field;
14086
15053
  }
14087
15054
 
15055
+ /**
15056
+ * Hand a world's DriftLight volume to the renderer: every fixed light of a city, summed offline by
15057
+ * `bakeDenseField`, standing in past the frame's exact choice. Replaces any field made before, of
15058
+ * either kind; the scene calls `field.follow(buffer.complete, ...)` each frame as it would for
15059
+ * `createLightField`, and marks the lights the volume summed `inLightField` so none is counted
15060
+ * twice. See `driftLight/worldLightField.ts`.
15061
+ */
15062
+ createWorldLightField(
15063
+ volume: DenseLightVolume,
15064
+ options: WorldLightFieldOptions = {},
15065
+ ): WorldLightField {
15066
+ this.forgetLightFieldVolumes();
15067
+ const field = new WorldLightField(volume, options);
15068
+ this.lightField = field;
15069
+ return field;
15070
+ }
15071
+
14088
15072
  /** Let go of the field and its volumes; the scene's lights are all exact again. */
14089
15073
  disposeLightField(): void {
14090
15074
  this.forgetLightFieldVolumes();
@@ -14111,12 +15095,12 @@ export class WebGPURenderer implements RendererApi {
14111
15095
  */
14112
15096
  private uploadLightField(): void {
14113
15097
  const field = this.lightField;
14114
- if (field === null || this.driftAtlas !== null || !field.ready || field.layout.count === 0) {
15098
+ if (field === null || this.driftAtlas !== null || !field.ready || field.empty) {
14115
15099
  return;
14116
15100
  }
14117
15101
  if (this.surface.lost) return;
14118
15102
  const { device } = this.surface;
14119
- const [ix, iy, iz] = field.layout.dims;
15103
+ const [ix, iy, iz] = field.indexDims;
14120
15104
  const index = device.createTexture({
14121
15105
  label: 'driftLight.index',
14122
15106
  size: [ix, iy, iz],
@@ -14126,7 +15110,7 @@ export class WebGPURenderer implements RendererApi {
14126
15110
  });
14127
15111
  device.queue.writeTexture(
14128
15112
  { texture: index },
14129
- field.layout.index,
15113
+ field.index,
14130
15114
  { bytesPerRow: ix * 4, rowsPerImage: iy },
14131
15115
  [ix, iy, iz],
14132
15116
  );
@@ -14159,45 +15143,8 @@ export class WebGPURenderer implements RendererApi {
14159
15143
 
14160
15144
  setIesProfiles(profiles: readonly PhotometricProfile[]): void {
14161
15145
  if (this.surface.lost) return;
14162
- const { device } = this.surface;
14163
- const atlas = packIesAtlas(profiles);
14164
- /*
14165
- * Reallocated when the row count changes, because a texture's size is immutable. A set of the
14166
- * same size overwrites in place, which is the common case of a consumer swapping one fixture.
14167
- */
14168
- if (this.iesTexture === null || this.iesTexture.height !== atlas.height) {
14169
- this.iesTexture?.destroy();
14170
- this.iesTexture = device.createTexture({
14171
- label: 'ies.atlas',
14172
- size: [atlas.width, atlas.height],
14173
- /*
14174
- * **`r16float`, not `r32float`, and that is a filtering constraint rather than a size
14175
- * choice.** A 32-bit float texture is not filterable in core WebGPU — it needs the
14176
- * optional `float32-filterable` feature — and binding one to a filtering sampler is a
14177
- * validation failure that takes the whole bind group with it. This row *wants* linear
14178
- * filtering: it is a curve sampled between two angles, which is the entire reason it is
14179
- * not `NEAREST` like the froxel table beside it.
14180
- *
14181
- * Half precision holds a normalised intensity to about three decimal digits, which is far
14182
- * finer than the eight-bit frame it ends up in. **What would make it wrong** is a profile
14183
- * carrying absolute candela rather than a normalised curve, which would run out of range —
14184
- * and `packIesAtlas` normalises for its own reasons.
14185
- */
14186
- format: 'r16float',
14187
- usage: 0x4 | 0x2, // TEXTURE_BINDING | COPY_DST
14188
- });
14189
- this.iesView = this.iesTexture.createView();
14190
- }
14191
- device.queue.writeTexture(
14192
- { texture: this.iesTexture },
14193
- /* Converted, because `writeTexture` does not — see `environmentTexels` for what handing it
14194
- the wrong width silently produces. */
14195
- toHalfFloats(atlas.data),
14196
- { bytesPerRow: atlas.width * 2, rowsPerImage: atlas.height },
14197
- { width: atlas.width, height: atlas.height, depthOrArrayLayers: 1 },
14198
- );
14199
- this.iesRows = atlas.height;
14200
- this.iesPlanes = atlas.planes;
15146
+ this.iesAtlas = packIesAtlas(profiles);
15147
+ this.forgetFixtureAtlas();
14201
15148
  this.rebuildFlatBindGroup();
14202
15149
  }
14203
15150
 
@@ -14418,6 +15365,33 @@ export class WebGPURenderer implements RendererApi {
14418
15365
  * The occlusion and bloom targets are half size. Both are blurs read back at full size, so a
14419
15366
  * full-resolution intermediate buys nothing a bilinear fetch does not already give.
14420
15367
  */
15368
+ /**
15369
+ * The medium's march group, holding the shadow views it reads — built with the composite, and
15370
+ * again when the sun's texture grows its glass layers and every view of the old one is stale.
15371
+ *
15372
+ * **The stand-in has to read as unoccluded and not as absent**, which is `emptyTexture.ts`'s
15373
+ * point and `createLightVolumeBindGroup`'s: `occlusion()` calls any stored depth below the
15374
+ * receiver's a blocker — so a black stand-in would put the whole medium in shadow, which is a
15375
+ * picture rather than an error. `volumeBlank` is the white one-texel texture built for this.
15376
+ */
15377
+ private buildMediumBindGroup(): GPUBindGroup | null {
15378
+ if (this.resolvedDepthView === null) return null;
15379
+ return this.surface.device.createBindGroup({
15380
+ label: 'post.mediumBindGroup',
15381
+ layout: this.mediumLayout,
15382
+ entries: [
15383
+ {
15384
+ binding: MEDIUM_UNIFORMS,
15385
+ resource: { buffer: this.mediumUniforms, size: MEDIUM_FRAG_SIZE },
15386
+ },
15387
+ { binding: MEDIUM_TEXTURES.uDepth.texture, resource: this.resolvedDepthView },
15388
+ { binding: MEDIUM_TEXTURES.uDepth.sampler, resource: this.postDepthSampler },
15389
+ { binding: MEDIUM_TEXTURES.uSunShadows.texture, resource: this.sunArrayView },
15390
+ { binding: MEDIUM_TEXTURES.uSunShadows.sampler, resource: this.volumeShadowSampler },
15391
+ ],
15392
+ });
15393
+ }
15394
+
14421
15395
  private ensureComposite(width: number, height: number): void {
14422
15396
  if (!this.hasComposite) return;
14423
15397
  if (this.sceneColor !== null && this.frameWidth === width && this.frameHeight === height)
@@ -14471,13 +15445,39 @@ export class WebGPURenderer implements RendererApi {
14471
15445
  * the allocation the house rules are about. A frame that never refracts pays the memory and
14472
15446
  * takes no copy.
14473
15447
  */
15448
+ /*
15449
+ * **At the output size and the reconstructed picture's format while reconstructing**, because
15450
+ * every refracting draw is then blended, drawn after the upscale, and reads the reconstructed
15451
+ * picture rather than the render. See `runLate`.
15452
+ */
15453
+ const reconstructed = this.reconstructionWanted;
14474
15454
  this.refractSnapshot = device.createTexture({
14475
15455
  label: 'refract.snapshot',
14476
- size: [width, height],
14477
- format,
14478
- usage: 0x4 | 0x2,
15456
+ size: reconstructed
15457
+ ? [Math.max(1, this.surface.canvas.width), Math.max(1, this.surface.canvas.height)]
15458
+ : [width, height],
15459
+ format: reconstructed ? RECON_HISTORY_FORMAT : format,
15460
+ /*
15461
+ * **Every level, so glass can read a blurrier one**, and a render attachment so `SnapshotMips`
15462
+ * can fill them. Plain refraction reads level 0, as it always has. See `glass.ts`.
15463
+ */
15464
+ mipLevelCount:
15465
+ Math.floor(
15466
+ Math.log2(
15467
+ reconstructed
15468
+ ? Math.max(1, this.surface.canvas.width, this.surface.canvas.height)
15469
+ : Math.max(1, width, height),
15470
+ ),
15471
+ ) + 1,
15472
+ usage: 0x4 | 0x2 | 0x10, // TEXTURE_BINDING | COPY_DST | RENDER_ATTACHMENT
14479
15473
  });
14480
15474
  this.refractSnapshotView = this.refractSnapshot.createView();
15475
+ this.refractMips = new SnapshotMips(
15476
+ device,
15477
+ this.pipelines,
15478
+ this.refractSnapshot,
15479
+ reconstructed ? RECON_HISTORY_FORMAT : format,
15480
+ );
14481
15481
  /*
14482
15482
  * **Every cached flat bind group is stale the moment this view changes**, and the failure is
14483
15483
  * silent: `flatTextures` resolves `uRefractScene` to the one-texel white stand-in when there is
@@ -14619,10 +15619,14 @@ export class WebGPURenderer implements RendererApi {
14619
15619
  * that range — a step of 1/255 in how much scene shows through, below what a viewer can see
14620
15620
  * against a blended surface.
14621
15621
  */
15622
+ /* At the output size while reconstructing, where the set is replayed after the upscale. */
15623
+ const oitSize: [number, number] = this.reconstructionWanted
15624
+ ? [Math.max(1, this.surface.canvas.width), Math.max(1, this.surface.canvas.height)]
15625
+ : [width, height];
14622
15626
  this.oitAccum?.destroy();
14623
15627
  this.oitAccum = device.createTexture({
14624
15628
  label: 'post.oitAccum',
14625
- size: [width, height],
15629
+ size: oitSize,
14626
15630
  format: 'rgba16float',
14627
15631
  usage: USAGE,
14628
15632
  });
@@ -14630,7 +15634,7 @@ export class WebGPURenderer implements RendererApi {
14630
15634
  this.oitReveal?.destroy();
14631
15635
  this.oitReveal = device.createTexture({
14632
15636
  label: 'post.oitReveal',
14633
- size: [width, height],
15637
+ size: oitSize,
14634
15638
  format: 'r8unorm',
14635
15639
  usage: USAGE,
14636
15640
  });
@@ -14644,7 +15648,10 @@ export class WebGPURenderer implements RendererApi {
14644
15648
  * An empty list is a real answer for a frame too small to hold one, and `runBloom` reads it
14645
15649
  * as no bloom rather than as a target it should have had.
14646
15650
  */
14647
- this.bloomLevels = bloomLevelSizes(width, height).map((size, index) => {
15651
+ /* The resolved picture's size while reconstructing, as the order-independent pair above is. */
15652
+ this.bloomSourceWidth = oitSize[0];
15653
+ this.bloomSourceHeight = oitSize[1];
15654
+ this.bloomLevels = bloomLevelSizes(oitSize[0], oitSize[1]).map((size, index) => {
14648
15655
  const texture = device.createTexture({
14649
15656
  label: `post.bloom${index}`,
14650
15657
  size: [size.width, size.height],
@@ -14735,42 +15742,7 @@ export class WebGPURenderer implements RendererApi {
14735
15742
  ...entry(AO_TEXTURES.uDepth, this.resolvedDepthView, true),
14736
15743
  ],
14737
15744
  });
14738
- /*
14739
- * The medium's two groups, holding the shadow views the march reads.
14740
- *
14741
- * **The stand-in has to read as unoccluded and not as absent**, which is `emptyTexture.ts`'s
14742
- * point and `createLightVolumeBindGroup`'s: the dynamic layer is sampled unconditionally, and
14743
- * `occlusion()` calls any stored depth below the receiver's a blocker — so a black stand-in
14744
- * would put the whole medium in shadow, which is a picture rather than an error. `volumeBlank`
14745
- * is the white one-texel texture already built for exactly this.
14746
- */
14747
- const shadowPair = (
14748
- b: { texture: number; sampler: number },
14749
- view: GPUTextureView | null,
14750
- ): GPUBindGroupEntry[] => [
14751
- { binding: b.texture, resource: view ?? this.volumeBlank.view },
14752
- {
14753
- binding: b.sampler,
14754
- resource: view === null ? this.volumeBlank.sampler : this.volumeShadowSampler,
14755
- },
14756
- ];
14757
- this.mediumBindGroup =
14758
- this.resolvedDepthView === null
14759
- ? null
14760
- : device.createBindGroup({
14761
- label: 'post.mediumBindGroup',
14762
- layout: this.mediumLayout,
14763
- entries: [
14764
- {
14765
- binding: MEDIUM_UNIFORMS,
14766
- resource: { buffer: this.mediumUniforms, size: MEDIUM_FRAG_SIZE },
14767
- },
14768
- ...entry(MEDIUM_TEXTURES.uDepth, this.resolvedDepthView, true),
14769
- ...shadowPair(MEDIUM_TEXTURES.uStaticShadowMap, this.shadowView),
14770
- ...shadowPair(MEDIUM_TEXTURES.uPeeledShadowMap, this.peelView),
14771
- ...shadowPair(MEDIUM_TEXTURES.uDynamicShadowMap, this.dynamicView),
14772
- ],
14773
- });
15745
+ this.mediumBindGroup = this.buildMediumBindGroup();
14774
15746
  this.mediumUpsampleGroup =
14775
15747
  this.resolvedDepthView === null || this.mediumTargetView === null
14776
15748
  ? null
@@ -14852,13 +15824,31 @@ export class WebGPURenderer implements RendererApi {
14852
15824
  /* The volume's group holds the depth snapshot, which is one of the targets just replaced.
14853
15825
  Keeping the old group would clamp every beam against a destroyed texture. */
14854
15826
  this.lightVolumeBindGroup = this.buildLightVolumeBindGroup();
14855
- /*
14856
- * One group per source the chain reads, in the order `runBloom` indexes them: the scene
14857
- * first, then every level. The prefilter reads the scene; the downsample into level `i`
14858
- * reads level `i - 1`, which is group `i`; the upsample into level `i - 1` reads level
14859
- * `i`, which is group `i + 1`.
14860
- */
14861
- this.bloomGroups = [this.sceneColorView, ...this.bloomLevels.map((level) => level.view)].map(
15827
+ this.rebuildBloomGroups();
15828
+ this.writeBloomBlocks();
15829
+ }
15830
+
15831
+ /**
15832
+ * One group per source the bloom chain reads, in the order `runBloom` indexes them: the picture
15833
+ * first, then every level. The prefilter reads the picture; the downsample into level `i` reads
15834
+ * level `i - 1`, which is group `i`; the upsample into level `i - 1` reads level `i`, which is
15835
+ * group `i + 1`.
15836
+ *
15837
+ * **While reconstructing the picture is the resolved one**, which `ensureReconstruction` makes
15838
+ * after this runs and rebuilds these over. The scene target is the jittered render then: a bright
15839
+ * thing smaller than a render texel is caught in some phases and missed in others, and its halo,
15840
+ * wide and over everything round it, flashed with it — a night street's panels breathing a few
15841
+ * levels of luma frame to frame. Built at the drawing buffer's size, which `ensureComposite`
15842
+ * already sized the pyramid for.
15843
+ */
15844
+ private rebuildBloomGroups(): void {
15845
+ const picture =
15846
+ this.reconstructionWanted && this.reconShownView !== null
15847
+ ? this.reconShownView
15848
+ : this.sceneColorView;
15849
+ if (picture === null) return;
15850
+ const { device } = this.surface;
15851
+ this.bloomGroups = [picture, ...this.bloomLevels.map((level) => level.view)].map(
14862
15852
  (view, index) =>
14863
15853
  device.createBindGroup({
14864
15854
  label: `post.bloomGroup${index}`,
@@ -14868,11 +15858,11 @@ export class WebGPURenderer implements RendererApi {
14868
15858
  binding: BLOOM_UNIFORMS,
14869
15859
  resource: { buffer: this.bloomUniforms, size: BLOOM_STAGE_SIZE },
14870
15860
  },
14871
- ...entry(BLOOM_TEXTURES.uSource, view),
15861
+ { binding: BLOOM_TEXTURES.uSource.texture, resource: view },
15862
+ { binding: BLOOM_TEXTURES.uSource.sampler, resource: this.postSampler },
14872
15863
  ],
14873
15864
  }),
14874
15865
  );
14875
- this.writeBloomBlocks();
14876
15866
  }
14877
15867
 
14878
15868
  /**
@@ -14890,12 +15880,12 @@ export class WebGPURenderer implements RendererApi {
14890
15880
  if (levels.length === 0) return;
14891
15881
  const slot = (index: number): number => (index * BLOOM_SLOT) / 4;
14892
15882
 
14893
- /* The prefilter reads the *scene*, so its texel is the frame's rather than a level's.
15883
+ /* The prefilter reads the *picture*, so its texel is the picture's rather than a level's.
14894
15884
  `uTexel`, not `uTexelSize`, and the block carries no strength at all: the composite
14895
15885
  applies that when it adds the result back. Both were written wrong here first. */
14896
15886
  const prefilter = (name: string): number => this.postField(BLOOM_PREFILTER_FIELDS, name);
14897
- f[slot(0) + prefilter('uTexel')] = 1 / Math.max(1, this.frameWidth);
14898
- f[slot(0) + prefilter('uTexel') + 1] = 1 / Math.max(1, this.frameHeight);
15887
+ f[slot(0) + prefilter('uTexel')] = 1 / Math.max(1, this.bloomSourceWidth);
15888
+ f[slot(0) + prefilter('uTexel') + 1] = 1 / Math.max(1, this.bloomSourceHeight);
14899
15889
  /* In scene units, which is why an HDR target is what makes a threshold above 1 mean
14900
15890
  anything: against a clamped buffer nothing is ever brighter than white. */
14901
15891
  f[slot(0) + prefilter('uThreshold')] = this.bloomThresholdSet ?? this.quality.bloomThreshold;
@@ -15002,6 +15992,10 @@ export class WebGPURenderer implements RendererApi {
15002
15992
  * the 2026-08-13 rule asks for — one decision, bound twice.
15003
15993
  */
15004
15994
  dispose(): void {
15995
+ this.motionPass?.dispose();
15996
+ this.motionPass = null;
15997
+ this.latePass?.dispose();
15998
+ this.latePass = null;
15005
15999
  for (const texture of this.retiredTextures) texture.destroy();
15006
16000
  this.retiredTextures.length = 0;
15007
16001
  const passDevice = this.passDevice();
@@ -15018,6 +16012,13 @@ export class WebGPURenderer implements RendererApi {
15018
16012
  this.fieldComposer = null;
15019
16013
  this.driftIndex?.destroy();
15020
16014
  this.driftAtlas?.destroy();
16015
+ this.forgetFixtureAtlas();
16016
+ this.sunTint?.texture.destroy();
16017
+ this.sunTint = null;
16018
+ this.glassTint?.dispose();
16019
+ this.glassTint = null;
16020
+ this.retiredSunTexture?.destroy();
16021
+ this.retiredSunTexture = null;
15021
16022
 
15022
16023
  this.pass = null;
15023
16024
  this.encoder = null;