@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
@@ -30,6 +30,29 @@
30
30
  * the render read bicubically fills the gap. The history keeps colour and weight; the picture shown
31
31
  * is it sharpened.
32
32
  *
33
+ * **And the temporal resolve's anti-flicker, where the caller carries its records** (`temporalAa.ts`
34
+ * says why it works and what it gives up). A thread finer than a render texel is caught in some
35
+ * phases of the jitter and missed in others, and in a phase that misses it the nine texels can all
36
+ * miss it: the box collapses onto what is behind, the history is clipped out, and a still picture
37
+ * flashes at every fine edge each period. A still camera over a still scene gathers every pixel the
38
+ * same way each period, so each pixel keeps a record of this frame's gathered luma over the period,
39
+ * and once two periods running have repeated its box widens by its own spread and it keeps more of
40
+ * its history. **The record stays at the pixel rather than following the reprojection**, because the
41
+ * proof is only ever for a pixel that has not moved; it starts over wherever the history did not
42
+ * survive, and is forgotten in proportion to motion.
43
+ *
44
+ * **And a still pixel whose depth has been seen to swing is not a disocclusion.** A rail finer than
45
+ * a texel in front of a wall is caught in some phases and missed in others, so last frame can hold
46
+ * the wall where this frame holds the rail with nothing moved — and the depth test threw the history
47
+ * away, leaving the record nothing to keep. So each pixel also keeps the largest relative miss of
48
+ * last frame's depth it has seen lately, kept whatever the test says, and while still its tolerance
49
+ * widens to `DEPTH_SWING_WIDTH` of it — the temporal resolve's answer to a crack of light round a
50
+ * door, as a held peak rather than its running mean: a rail caught in one phase of eighteen misses
51
+ * once a period, and a mean has worn that miss down to a thirtieth by the next. **What it gives up**
52
+ * is the depth test's word at such a pixel while still: a surface arriving there inside the swing
53
+ * keeps the history, and the neighbourhood clip, which the unproven sample of an arrival leaves
54
+ * unwidened, is what takes it out.
55
+ *
33
56
  * **Conventions.** Images are rows from `v = 0` upward. Depths are clip depths in the matrices'
34
57
  * convention, OpenGL's here — the nearest is the smallest — and the renderer converts its reversed
35
58
  * buffer before a sample reaches this arithmetic, as the temporal resolve already does.
@@ -44,6 +67,16 @@ import {
44
67
  } from './disocclusion.ts';
45
68
  import type { DisocclusionParams } from './disocclusion.ts';
46
69
  import { sampleBilinear, sampleHistoryBicubic5 } from './reproject.ts';
70
+ import {
71
+ TEMPORAL_FLICKER,
72
+ flickerBlend,
73
+ flickerRule,
74
+ flickerWidening,
75
+ freshFlicker,
76
+ nextFlicker,
77
+ stillness,
78
+ } from '../temporalAa.ts';
79
+ import type { Flicker, FlickerRule } from '../temporalAa.ts';
47
80
 
48
81
  /** What a reconstruction is asked for. */
49
82
  export interface ReconQuality {
@@ -140,6 +173,12 @@ export interface ResolveFrame {
140
173
  readonly inverseViewProj: ArrayLike<number>;
141
174
  /** Last frame's view-projection and its inverse, unjittered. */
142
175
  readonly previousViewProj: ArrayLike<number>;
176
+ /**
177
+ * Last frame's view-projection times this frame's inverse: where a texel's clip position was
178
+ * last frame. Derived from the two when absent; a caller holding its matrices at a lower precision
179
+ * than it multiplies at passes its own product, as the renderer does.
180
+ */
181
+ readonly reprojection?: ArrayLike<number>;
143
182
  readonly previousInverseViewProj: ArrayLike<number>;
144
183
  readonly eye: ArrayLike<number>;
145
184
  readonly previousEye: ArrayLike<number>;
@@ -148,6 +187,22 @@ export interface ResolveFrame {
148
187
  readonly previousJitter: ArrayLike<number>;
149
188
  readonly quality: ReconQuality;
150
189
  readonly disocclusion: DisocclusionParams;
190
+ /**
191
+ * Last frame's flicker records, output size, four floats a pixel — the spread, this period's
192
+ * running sum, the last period's mean, and how many periods running repeated, as `Flicker` holds
193
+ * them — or absent for the resolve without its anti-flicker.
194
+ */
195
+ readonly flicker?: ArrayLike<number> | null;
196
+ /**
197
+ * Last frame's depth swings, output size, one float a pixel: the largest miss, relative to the
198
+ * depth, of last frame's depth against the one this pixel's surface should have had, seen lately
199
+ * — held, and halving over a period. Carried with `flicker` and read only with it.
200
+ */
201
+ readonly depthSwing?: ArrayLike<number> | null;
202
+ /** Whether this frame samples the first phase of the jitter's period. */
203
+ readonly periodStart?: boolean;
204
+ /** The jitter's period: the reconstruction's own phase count, not the temporal resolve's. */
205
+ readonly flickerPhases?: number;
151
206
  }
152
207
 
153
208
  /**
@@ -170,6 +225,13 @@ export function reconstructionWeight(dx: number, dy: number): number {
170
225
  */
171
226
  export const CONFIDENT_WEIGHT = 1;
172
227
 
228
+ /**
229
+ * How many of its held depth swings a still pixel's depth test tolerates. The peak halves over a
230
+ * period, so the frame before the next once-a-period miss holds just over half of it: tolerating
231
+ * twice the peak passes that miss outright, and three leaves the same room again.
232
+ */
233
+ export const DEPTH_SWING_WIDTH = 3;
234
+
173
235
  const SAMPLE = new Float32Array(3);
174
236
  const GATHERED = new Float64Array(3);
175
237
  const HISTORY = new Float32Array(4);
@@ -185,6 +247,36 @@ const UP = new Float64Array(3);
185
247
  const NORMAL = new Float64Array(3);
186
248
  const PREVIOUS_NORMAL = new Float64Array(3);
187
249
  const CLIP = new Float64Array(4);
250
+ /** The last frame's reprojection, derived once a frame rather than once a pixel. */
251
+ const REPROJECTION = new Float64Array(16);
252
+ let reprojected: ResolveFrame | null = null;
253
+
254
+ /** `frame.reprojection`, or the product of its two matrices, multiplied once per frame. */
255
+ function reprojectionOf(frame: ResolveFrame): ArrayLike<number> {
256
+ if (frame.reprojection !== undefined) return frame.reprojection;
257
+ if (reprojected !== frame) {
258
+ reprojected = frame;
259
+ const a = frame.previousViewProj;
260
+ const b = frame.inverseViewProj;
261
+ for (let c = 0; c < 4; c += 1) {
262
+ for (let r = 0; r < 4; r += 1) {
263
+ let sum = 0;
264
+ for (let k = 0; k < 4; k += 1) sum += (a[k * 4 + r] as number) * (b[c * 4 + k] as number);
265
+ REPROJECTION[c * 4 + r] = sum;
266
+ }
267
+ }
268
+ }
269
+ return REPROJECTION;
270
+ }
271
+ const HELD: Flicker = { spread: 0, sum: 0, mean: 0, repeated: 0 };
272
+ const CARRIED: Flicker = { spread: 0, sum: 0, mean: 0, repeated: 0 };
273
+ /** How far, in output pixels, the last `historyWeight` found its surface had moved. */
274
+ let moved = 0;
275
+ /** How far, relative, last frame's depth missed the one expected there; -1 where nothing was held. */
276
+ let missed = -1;
277
+ /** The rule for the last period asked for, kept: one is made per period, not per pixel. */
278
+ let rulePhases = 0;
279
+ let rule: FlickerRule = TEMPORAL_FLICKER;
188
280
 
189
281
  function clampTexel(value: number, size: number): number {
190
282
  return value < 0 ? 0 : value >= size ? size - 1 : value;
@@ -296,7 +388,15 @@ function normalAt(
296
388
  * was behind last frame's eye or off its picture, and otherwise what it gathered, scaled by whether it
297
389
  * is the same surface. Reads `HISTORY`; writes the history's colour there.
298
390
  */
299
- function historyWeight(frame: ResolveFrame, u: number, v: number, dx: number, dy: number): number {
391
+ function historyWeight(
392
+ frame: ResolveFrame,
393
+ u: number,
394
+ v: number,
395
+ dx: number,
396
+ dy: number,
397
+ swing: number,
398
+ ): number {
399
+ missed = -1;
300
400
  if (!frame.hasHistory) return 0;
301
401
  const rw = frame.renderWidth;
302
402
  const rh = frame.renderHeight;
@@ -309,28 +409,46 @@ function historyWeight(frame: ResolveFrame, u: number, v: number, dx: number, dy
309
409
  mv = frame.motion[flagAt + 1] as number;
310
410
  expected = frame.motion[flagAt + 2] as number;
311
411
  } else {
312
- if (worldAt(frame.depth, frame.inverseViewProj, frame.jitter, rw, rh, dx, dy, WORLD) <= 0) {
313
- return 0;
314
- }
315
- const m = frame.previousViewProj;
412
+ /*
413
+ * **Through one matrix, last frame's view-projection times this frame's inverse**, rather than
414
+ * out to a world position and back. The two are the same arithmetic in exact numbers; in single
415
+ * precision the round trip is not the identity — a near plane a quarter of a metre from a far one
416
+ * five kilometres off — and a still camera measured a few hundredths of a pixel of motion growing
417
+ * out from the middle of the frame, which crept every still history and kept every pixel from
418
+ * the anti-flicker's stillness. The product is taken where the precision is (`reprojection`),
419
+ * and for a still camera it is the identity.
420
+ */
421
+ const du = (dx + 0.5 - (frame.jitter[0] as number)) / rw;
422
+ const dv = (dy + 0.5 - (frame.jitter[1] as number)) / rh;
423
+ const x = du * 2 - 1;
424
+ const y = dv * 2 - 1;
425
+ const z = frame.depth[dy * rw + dx] as number;
426
+ const inverse = frame.inverseViewProj;
427
+ /* The homogeneous w of the world position: none, or behind the eye, is no surface. */
428
+ const hw =
429
+ (inverse[3] as number) * x +
430
+ (inverse[7] as number) * y +
431
+ (inverse[11] as number) * z +
432
+ (inverse[15] as number);
433
+ if (!(hw > 1e-12)) return 0;
434
+ const m = reprojectionOf(frame);
316
435
  for (let r = 0; r < 4; r += 1) {
317
436
  CLIP[r] =
318
- (m[r] as number) * (WORLD[0] as number) +
319
- (m[4 + r] as number) * (WORLD[1] as number) +
320
- (m[8 + r] as number) * (WORLD[2] as number) +
437
+ (m[r] as number) * x +
438
+ (m[4 + r] as number) * y +
439
+ (m[8 + r] as number) * z +
321
440
  (m[12 + r] as number);
322
441
  }
323
442
  /*
324
443
  * A surface behind last frame's eye has a w that is not positive: the disocclusion trusts no
325
444
  * such depth, so it needs no refusal of its own here.
326
445
  */
327
- expected = CLIP[3] as number;
328
- const du = (dx + 0.5 - (frame.jitter[0] as number)) / rw;
329
- const dv = (dy + 0.5 - (frame.jitter[1] as number)) / rh;
330
- mu = ((CLIP[0] as number) / expected) * 0.5 + 0.5 - du;
331
- mv = ((CLIP[1] as number) / expected) * 0.5 + 0.5 - dv;
446
+ expected = (CLIP[3] as number) / hw;
447
+ mu = ((CLIP[0] as number) / (CLIP[3] as number)) * 0.5 + 0.5 - du;
448
+ mv = ((CLIP[1] as number) / (CLIP[3] as number)) * 0.5 + 0.5 - dv;
332
449
  }
333
450
 
451
+ moved = Math.hypot(mu * frame.outputWidth, mv * frame.outputHeight);
334
452
  const pu = u + mu;
335
453
  const pv = v + mv;
336
454
  if (!(pu >= 0 && pu <= 1 && pv >= 0 && pv <= 1)) return 0;
@@ -440,22 +558,32 @@ function historyWeight(frame: ResolveFrame, u: number, v: number, dx: number, dy
440
558
  (NORMAL[2] as number) * (PREVIOUS_NORMAL[2] as number);
441
559
  normalDot = 1 - planar * (1 - cosine);
442
560
  }
561
+ if (held > 0 && expected > 0) missed = Math.abs(held - expected) / expected;
562
+ /* A still pixel's own depth swing, as wide as the anti-flicker's box: see the header. */
563
+ const swung = swing > 0 ? DEPTH_SWING_WIDTH * swing * stillness(moved) : 0;
443
564
  const trust = disocclusionWeight(
444
565
  expected,
445
566
  held,
446
567
  Math.hypot(mu, mv),
447
568
  normalDot,
448
569
  frame.disocclusion,
449
- slope,
570
+ slope + swung,
450
571
  );
451
572
  return (HISTORY[3] as number) * trust;
452
573
  }
453
574
 
454
575
  /**
455
576
  * One output pixel: the history's next colour, before sharpening, and the weight it has gathered —
456
- * four values into `out`.
577
+ * four values into `out` — and, where the frame carries records, the pixel's next one into `record`.
457
578
  */
458
- export function resolvePixel(frame: ResolveFrame, ox: number, oy: number, out: Float32Array): void {
579
+ export function resolvePixel(
580
+ frame: ResolveFrame,
581
+ ox: number,
582
+ oy: number,
583
+ out: Float32Array,
584
+ record?: Float32Array,
585
+ swingOut?: Float32Array,
586
+ ): void {
459
587
  const rw = frame.renderWidth;
460
588
  const rh = frame.renderHeight;
461
589
  const ow = frame.outputWidth;
@@ -508,8 +636,58 @@ export function resolvePixel(frame: ResolveFrame, ox: number, oy: number, out: F
508
636
  for (let c = 0; c < 3; c += 1) GATHERED[c] = (GATHERED[c] as number) / gathered;
509
637
 
510
638
  /* 3 and 4. The history, and the weight it still deserves. */
511
- const cap = (1 - frame.quality.alpha) / frame.quality.alpha;
512
- const kept = Math.min(cap, historyWeight(frame, u, v, dx, dy));
639
+ const alpha = frame.quality.alpha;
640
+ const records = frame.flicker;
641
+ const anti = records !== undefined && records !== null;
642
+ if (anti) {
643
+ const phases = frame.flickerPhases ?? 0;
644
+ if (phases !== rulePhases) {
645
+ rulePhases = phases;
646
+ rule = phases > 0 ? flickerRule(phases) : TEMPORAL_FLICKER;
647
+ }
648
+ }
649
+ const swings = frame.depthSwing;
650
+ const swing =
651
+ anti && swings !== undefined && swings !== null ? (swings[oy * ow + ox] as number) : 0;
652
+ const weight = historyWeight(frame, u, v, dx, dy, swing);
653
+ let cap = (1 - alpha) / alpha;
654
+ /* The anti-flicker: a proven pixel's box widened by its spread, and more of its history kept. */
655
+ if (anti) {
656
+ const sample =
657
+ (GATHERED[0] as number) * 0.2126 +
658
+ (GATHERED[1] as number) * 0.7152 +
659
+ (GATHERED[2] as number) * 0.0722;
660
+ const at = (oy * ow + ox) * 4;
661
+ if (weight > 0) {
662
+ HELD.spread = records[at] as number;
663
+ HELD.sum = records[at + 1] as number;
664
+ HELD.mean = records[at + 2] as number;
665
+ HELD.repeated = records[at + 3] as number;
666
+ } else {
667
+ freshFlicker(HELD, sample, rule);
668
+ }
669
+ const still = weight > 0 ? stillness(moved) : 0;
670
+ const widen = flickerWidening(HELD, sample, still, rule);
671
+ for (let c = 0; c < 3; c += 1) {
672
+ LOW[c] = (LOW[c] as number) - widen;
673
+ HIGH[c] = (HIGH[c] as number) + widen;
674
+ }
675
+ const keep = flickerBlend(1 - alpha, HELD, sample, still, rule);
676
+ cap = keep / (1 - keep);
677
+ nextFlicker(CARRIED, HELD, sample, still, frame.periodStart === true, rule);
678
+ if (record !== undefined) {
679
+ record[0] = CARRIED.spread;
680
+ record[1] = CARRIED.sum;
681
+ record[2] = CARRIED.mean;
682
+ record[3] = CARRIED.repeated;
683
+ }
684
+ /* The peak carried on, halving over a period, whatever the test said; none where nothing was
685
+ held to miss. */
686
+ if (swingOut !== undefined) {
687
+ swingOut[0] = missed >= 0 ? Math.max(missed, swing * 0.5 ** rule.memory) : 0;
688
+ }
689
+ }
690
+ const kept = Math.min(cap, weight);
513
691
 
514
692
  /* 5. Clipped, mixed by weight, and the gaps filled from the render. */
515
693
  const total = kept + gathered;
@@ -529,6 +707,8 @@ export function resolvePixel(frame: ResolveFrame, ox: number, oy: number, out: F
529
707
  }
530
708
 
531
709
  const TAP = new Float32Array(4);
710
+ const RECORD = new Float32Array(4);
711
+ const SWING = new Float32Array(1);
532
712
  const CENTRE = new Float32Array(4);
533
713
  const AROUND = new Float32Array(12);
534
714
  const SHOWN = new Float32Array(3);
@@ -541,19 +721,25 @@ const OFFSETS: readonly (readonly [number, number])[] = [
541
721
 
542
722
  /**
543
723
  * The whole frame: the next history into `history`, four floats a pixel, and the picture shown into
544
- * `shown`, three — the history sharpened against the four direct neighbours it has just been given.
724
+ * `shown`, three — the history sharpened against the four direct neighbours it has just been given —
725
+ * and, where the frame carries records, the next ones into `records`, four floats a pixel, and the
726
+ * next depth swings into `swings`, one.
545
727
  */
546
728
  export function resolveFrame(
547
729
  frame: ResolveFrame,
548
730
  history: Float32Array,
549
731
  shown: Float32Array,
732
+ records?: Float32Array,
733
+ swings?: Float32Array,
550
734
  ): void {
551
735
  const ow = frame.outputWidth;
552
736
  const oh = frame.outputHeight;
553
737
  for (let y = 0; y < oh; y += 1) {
554
738
  for (let x = 0; x < ow; x += 1) {
555
- resolvePixel(frame, x, y, TAP);
739
+ resolvePixel(frame, x, y, TAP, RECORD, SWING);
556
740
  history.set(TAP, (y * ow + x) * 4);
741
+ if (records !== undefined) records.set(RECORD, (y * ow + x) * 4);
742
+ if (swings !== undefined) swings[y * ow + x] = SWING[0] as number;
557
743
  }
558
744
  }
559
745
  for (let y = 0; y < oh; y += 1) {
@@ -37,6 +37,9 @@ export type ShadowFilterTaps = 4 | 8 | 12;
37
37
  export type DirectionalShadowDepthLayers = 1 | 2;
38
38
  export type WaterReflectionFilterTaps = 1 | 5 | 9;
39
39
 
40
+ /** How light through glass is coloured: see `RenderQuality.glassShadows`. */
41
+ export type GlassShadows = 'full' | 'half' | 'off';
42
+
40
43
  export interface RenderQuality {
41
44
  /**
42
45
  * The transfer curve applied at the end of every shaded pass.
@@ -149,6 +152,25 @@ export interface RenderQuality {
149
152
  readonly lightVolumeSamples: number;
150
153
  /** Independently composited static sun depths; two preserves common overlaps. */
151
154
  readonly directionalShadowDepthLayers: DirectionalShadowDepthLayers;
155
+ /**
156
+ * Whether light through glass takes the glass's colour, and at what size.
157
+ *
158
+ * **`'full'`, the default**: every shadow a glass caster reaches gains a layer holding the depth of
159
+ * the nearest pane and a tint layer holding what the panes let through, at the size of the depth
160
+ * map beside it — 8 bytes a texel, so 32 MB for a 2048 sun map and 8 MB for a 1024 lamp layer.
161
+ * **`'half'`** halves the tint layers only (the glass depth must match its array): 20 MB and 5 MB,
162
+ * and a coloured patch softer than an opaque shadow's edge. **`'off'`** is glass before this
163
+ * existed: a pane casts nothing at all.
164
+ *
165
+ * Nothing is allocated until a glass caster first reaches a shadow, so a scene with no glass pays
166
+ * no memory and no pass whatever this says. **The lit shader is the exception**: `'full'` and
167
+ * `'half'` compile the glass lookup into it, and a lookup that finds no glass and returns at its
168
+ * first line still costs the lit pass its registers — measured on the courtyard at night, 0.27 ms
169
+ * at 720p on WebGPU, 0.42 ms on WebGL2 and 1.2 ms at 4K. `'off'` compiles none of it, so a world
170
+ * that never casts through glass is that much faster for saying so. What would make `'full'` wrong
171
+ * is a phone budget with a lot of glass; that is what `'half'` is for. See `glassShadow.ts`.
172
+ */
173
+ readonly glassShadows: GlassShadows;
152
174
  /** Horizontal world-space reach over which a directional shadow dissolves. */
153
175
  readonly directionalShadowMaxDistance: number;
154
176
  /** Maximum horizontal projection per vertical metre before low-angle shadows vanish. */
@@ -556,6 +578,20 @@ export interface RenderQuality {
556
578
  * the render is a quarter of the output and no reconstruction holds an edge through that. A
557
579
  * negative number is off, not a clamp to the bottom: it cannot mean "a little".
558
580
  *
581
+ * **What it needs from you.** A `createMover()` per moving object, passed with that object's
582
+ * draws where `drawMesh` takes last frame's matrix — the renderer keeps last frame's model and,
583
+ * for a skinned draw, last frame's pose. Instanced batches and meshes rewritten with
584
+ * `updateMesh` need nothing: the batch and the mesh are their own identity. And `cameraCut()` at
585
+ * every cut, because nothing detects one: without it a cut blends the previous shot into the
586
+ * next. Blended draws — translucent meshes, particles, plumes, film, bolts, lines, world text —
587
+ * are drawn after the upscale at the output size and need nothing either; light volumes stay in
588
+ * the reconstructed picture, being a march that keeps its history.
589
+ *
590
+ * **What it cannot give back** is the resolution it draws at: an edge that never moves is softer
591
+ * than a native frame's at any ratio, because two thirds of the pixels at 1.5 are a guess made
592
+ * sharp. Measured on `demo/dev/ghost.html` against a native frame at four samples, still edges
593
+ * err by about 8 levels where a native frame at one sample errs by 3.
594
+ *
559
595
  * `?recon=` is how it gets A/B'd in one reload.
560
596
  */
561
597
  readonly reconstruction: number;
@@ -838,6 +874,7 @@ export const DEFAULT_RENDER_QUALITY: Readonly<RenderQuality> = Object.freeze({
838
874
  shadowFilterTaps: MAX_SHADOW_FILTER_TAPS,
839
875
  lightVolumeSamples: 32,
840
876
  directionalShadowDepthLayers: 2,
877
+ glassShadows: 'full',
841
878
  directionalShadowMaxDistance: 6,
842
879
  directionalShadowMaxSlope: 3,
843
880
  pointShadowRebakeDistance: 0.001,
@@ -998,6 +1035,7 @@ export function resolveRenderQuality(options: RenderQualityOptions = {}): Readon
998
1035
  lightVolumeSamples: options.lightVolumeSamples ?? DEFAULT_RENDER_QUALITY.lightVolumeSamples,
999
1036
  directionalShadowDepthLayers:
1000
1037
  options.directionalShadowDepthLayers ?? DEFAULT_RENDER_QUALITY.directionalShadowDepthLayers,
1038
+ glassShadows: options.glassShadows ?? DEFAULT_RENDER_QUALITY.glassShadows,
1001
1039
  directionalShadowMaxDistance:
1002
1040
  options.directionalShadowMaxDistance ?? DEFAULT_RENDER_QUALITY.directionalShadowMaxDistance,
1003
1041
  directionalShadowMaxSlope:
@@ -1128,6 +1166,15 @@ export function resolveRenderQuality(options: RenderQualityOptions = {}): Readon
1128
1166
  `RenderQuality.directionalShadowDepthLayers must be 1 or 2, got ${quality.directionalShadowDepthLayers}`,
1129
1167
  );
1130
1168
  }
1169
+ if (
1170
+ quality.glassShadows !== 'full' &&
1171
+ quality.glassShadows !== 'half' &&
1172
+ quality.glassShadows !== 'off'
1173
+ ) {
1174
+ throw new Error(
1175
+ `RenderQuality.glassShadows must be 'full', 'half' or 'off', got ${String(quality.glassShadows)}`,
1176
+ );
1177
+ }
1131
1178
  if (quality.plumeNoiseOctaves > 3) {
1132
1179
  throw new Error(
1133
1180
  `RenderQuality.plumeNoiseOctaves must be <= 3, got ${quality.plumeNoiseOctaves}`,
@@ -72,6 +72,10 @@ export function recordingGl(
72
72
  const programFragments = new WeakMap<object, string>();
73
73
  const calls: { name: string; args: unknown[] }[] = [];
74
74
  const constants: Record<string, number> = {
75
+ /* The canvas's size, as a real context reports its drawing buffer; a function here made every
76
+ jitter and every per-pixel offset divided by it a NaN. */
77
+ drawingBufferWidth: 640,
78
+ drawingBufferHeight: 480,
75
79
  ACTIVE_UNIFORMS: 0x8b86,
76
80
  ACTIVE_ATTRIBUTES: 0x8b89,
77
81
  COMPILE_STATUS: 0x8b81,
@@ -129,6 +133,18 @@ export function recordingGl(
129
133
  COLOR_BUFFER_BIT: 0x00004000,
130
134
  /* Named so a test can tell a mesh that declared itself deforming from one that did not. */
131
135
  ARRAY_BUFFER: 0x8892,
136
+ /* Named so a blend a pass sets can be read back as numbers: glass multiplies what it lets
137
+ through, and a test that cannot tell DST_COLOR from a recorder cannot tell it multiplies. */
138
+ ZERO: 0,
139
+ DST_ALPHA: 0x0304,
140
+ DST_COLOR: 0x0306,
141
+ FUNC_ADD: 0x8006,
142
+ COLOR: 0x1800,
143
+ DEPTH: 0x1801,
144
+ /* Named so a copy can be read as from-and-to: a blit's two framebuffers are the whole claim. */
145
+ READ_FRAMEBUFFER: 0x8ca8,
146
+ DRAW_FRAMEBUFFER: 0x8ca9,
147
+ BLEND: 0x0be2,
132
148
  STATIC_DRAW: 0x88e4,
133
149
  DYNAMIC_DRAW: 0x88e8,
134
150
  };
@@ -82,6 +82,8 @@ export class SceneTarget {
82
82
  private colorCopyFramebuffer: WebGLFramebuffer | null = null;
83
83
  /** Whether this frame has filled it. One snapshot serves every refracting draw in a frame. */
84
84
  private colorCopyFilled = false;
85
+ /** Whether this frame's copy has its mip chain, which frosted glass reads. See `snapshotColor`. */
86
+ private colorCopyMips = false;
85
87
  /**
86
88
  * The colour grade, as a 3D texture on unit 4, and the table it was built from.
87
89
  *
@@ -275,12 +277,24 @@ export class SceneTarget {
275
277
  */
276
278
  if (this.colorCopy === null) this.colorCopy = gl.createTexture();
277
279
  gl.bindTexture(gl.TEXTURE_2D, this.colorCopy);
278
- if (this.floatColor) {
279
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, width, height, 0, gl.RGBA, gl.HALF_FLOAT, null);
280
- } else {
281
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
280
+ /*
281
+ * **Every level, so glass can read a blurrier one.** Frosted glass reads the copy at a level its
282
+ * frost picks, and a texture sampled through a mipmapping filter with a level missing is
283
+ * incomplete and reads black — so the chain is allocated here, once per size, and filled only on
284
+ * a frame that draws glass (`snapshotColor`). Plain refraction reads level 0, as it always has.
285
+ */
286
+ const levels = Math.floor(Math.log2(Math.max(width, height))) + 1;
287
+ for (let level = 0; level < levels; level += 1) {
288
+ const w = Math.max(1, width >> level);
289
+ const h = Math.max(1, height >> level);
290
+ if (this.floatColor) {
291
+ gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA16F, w, h, 0, gl.RGBA, gl.HALF_FLOAT, null);
292
+ } else {
293
+ gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
294
+ }
282
295
  }
283
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
296
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, levels - 1);
297
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
284
298
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
285
299
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
286
300
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
@@ -419,6 +433,7 @@ export class SceneTarget {
419
433
  /* Last frame's snapshot is last frame's scene. See `snapshotDepth`. */
420
434
  this.depthCopyFilled = false;
421
435
  this.colorCopyFilled = false;
436
+ this.colorCopyMips = false;
422
437
  this.bind();
423
438
  }
424
439
 
@@ -516,12 +531,21 @@ export class SceneTarget {
516
531
  * Null where the copy could not be made, and a caller must read that as "do not refract" rather
517
532
  * than as a black texture — refracting against black paints every pane the colour of a hole.
518
533
  */
519
- snapshotColor(afresh = false): WebGLTexture | null {
534
+ snapshotColor(
535
+ afresh = false,
536
+ /** Fill the copy's mip chain as well, once a frame: what frosted glass reads. */
537
+ mips = false,
538
+ ): WebGLTexture | null {
520
539
  if (this.colorCopy === null || this.colorCopyFramebuffer === null) return null;
521
- if (this.colorCopyFilled && !afresh) return this.colorCopy;
540
+ if (this.colorCopyFilled && !afresh) {
541
+ if (mips) this.fillColorCopyMips();
542
+ return this.colorCopy;
543
+ }
522
544
  const { gl } = this;
523
545
  const source = this.msaaFramebuffer ?? this.framebuffer;
524
546
  if (source === null) return null;
547
+ const drawing = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
548
+ const reading = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
525
549
 
526
550
  gl.bindFramebuffer(gl.READ_FRAMEBUFFER, source);
527
551
  gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.colorCopyFramebuffer);
@@ -546,13 +570,33 @@ export class SceneTarget {
546
570
  gl.NEAREST,
547
571
  );
548
572
  const refused = gl.getError() !== gl.NO_ERROR;
549
- /* Back to what the caller was drawing into, whichever that was. */
550
- gl.bindFramebuffer(gl.FRAMEBUFFER, source);
573
+ /*
574
+ * **Back to what the caller was drawing into, whichever that was** — read before the copy and
575
+ * handed back, never assumed. It was the scene's framebuffer, assumed, and a pane drawn into a
576
+ * reflection probe's face then left the scene's bound: the probe attached its next face to it,
577
+ * a face-sized colour beside a screen-sized depth, and every draw after refused as incomplete.
578
+ */
579
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, drawing);
580
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, reading);
551
581
  if (refused) return null;
552
582
  this.colorCopyFilled = true;
583
+ this.colorCopyMips = false;
584
+ if (mips) this.fillColorCopyMips();
553
585
  return this.colorCopy;
554
586
  }
555
587
 
588
+ /** The copy's mip chain from its level 0, once per copy. */
589
+ private fillColorCopyMips(): void {
590
+ if (this.colorCopyMips || this.colorCopy === null) return;
591
+ const { gl } = this;
592
+ /* Bound on whichever unit is active and handed back, so the caller's texture there survives. */
593
+ const previous = gl.getParameter(gl.TEXTURE_BINDING_2D) as WebGLTexture | null;
594
+ gl.bindTexture(gl.TEXTURE_2D, this.colorCopy);
595
+ gl.generateMipmap(gl.TEXTURE_2D);
596
+ gl.bindTexture(gl.TEXTURE_2D, previous);
597
+ this.colorCopyMips = true;
598
+ }
599
+
556
600
  snapshotDepth(
557
601
  /**
558
602
  * Take the copy again even though one has already been taken this frame.
@@ -573,6 +617,8 @@ export class SceneTarget {
573
617
  const { gl } = this;
574
618
  const source = this.msaaFramebuffer ?? this.framebuffer;
575
619
  if (source === null) return null;
620
+ const drawing = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
621
+ const reading = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
576
622
 
577
623
  gl.bindFramebuffer(gl.READ_FRAMEBUFFER, source);
578
624
  gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.depthCopyFramebuffer);
@@ -595,8 +641,9 @@ export class SceneTarget {
595
641
  gl.NEAREST,
596
642
  );
597
643
  const refused = gl.getError() !== gl.NO_ERROR;
598
- /* Back to what the caller was drawing into, whichever that was. */
599
- gl.bindFramebuffer(gl.FRAMEBUFFER, source);
644
+ /* Back to what the caller was drawing into, read rather than assumed: see `snapshotColor`. */
645
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, drawing);
646
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, reading);
600
647
  if (refused) return null;
601
648
  this.depthCopyFilled = true;
602
649
  return this.depthCopy;
@@ -708,6 +755,8 @@ export class SceneTarget {
708
755
  /** The projection's x and y scales, which turn a world radius into a screen one. */
709
756
  readonly projScale: Float32Array;
710
757
  readonly invProjection: Float32Array;
758
+ /** Where the frame's jitter put the occlusion, in uv: see the shader's `uAoOffset`. */
759
+ readonly offset?: Float32Array;
711
760
  },
712
761
  bloom?: {
713
762
  readonly strength: number;
@@ -976,6 +1025,8 @@ export class SceneTarget {
976
1025
  this.uniforms['uAoStrength'] ?? null,
977
1026
  aoTexture === null ? 0 : (ao?.strength ?? 0),
978
1027
  );
1028
+ const offset = ao?.offset;
1029
+ gl.uniform2f(this.uniforms['uAoOffset'] ?? null, offset?.[0] ?? 0, offset?.[1] ?? 0);
979
1030
 
980
1031
  /* Bloom on its own unit, and the placeholder for the same reason as the one above: a
981
1032
  driver may fetch a texture's descriptor before it evaluates the branch that skips the