@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
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The second pipeline's sunlight through glass, as a reference: `glassTintAt` over the glass depth
3
+ * and tint maps the GPU-driven pass draws from the sun, for `shade.wgsl.ts`'s lookup to be read
4
+ * against.
5
+ *
6
+ * **The forward path's rule, not a second one** (`glassShadow.ts`, and `sunGlassLookup` in
7
+ * `flat/directionalShadow.ts`): the outline from taps at the base filter radius that count a pane
8
+ * only where the receiver is behind it, and the colour from spread taps read coarse, unmixed from
9
+ * the ground around the pane by its clarity and weighed by its share. Frost widens only the
10
+ * colour's taps, so the light a frosted pane passes is the light a clear one does.
11
+ *
12
+ * **What differs is where two numbers come from.** A compute invocation has no derivatives, so the
13
+ * map's scale in metres is the caller's (`uvPerMetre`, which an orthographic light matrix states
14
+ * outright); and a coarse level here is a box average over the spread's footprint, which is what a
15
+ * mip level approximates on the device. `scripts/gpu-parity.mjs` holds the device's per-tap
16
+ * arithmetic to `glassShadow.ts`; the sampling is what the glass page measures.
17
+ */
18
+ import { frostRadius, spreadTint, unmixTint } from '../glassShadow.js';
19
+ import { SHADOW_DEPTH_BIAS, SHADOW_PCF_OFFSETS, SHADOW_TEXEL_SCALE } from './shadow.js';
20
+ const centre = { r: 1, g: 1, b: 1, clarity: 1 };
21
+ const coarse = { r: 1, g: 1, b: 1, clarity: 1 };
22
+ const pane = { r: 1, g: 1, b: 1, clarity: 1 };
23
+ const found = { r: 0, g: 0, b: 0, clarity: 0 };
24
+ function index(maps, u, v) {
25
+ const x = Math.min(maps.size - 1, Math.max(0, Math.floor(u * maps.size)));
26
+ const y = Math.min(maps.size - 1, Math.max(0, Math.floor(v * maps.size)));
27
+ return y * maps.size + x;
28
+ }
29
+ /** The average of every texel within `radius` of (u, v): a mip level's footprint, in the large. */
30
+ function coarseAt(maps, u, v, radius, out) {
31
+ const reach = radius * maps.size;
32
+ const x0 = Math.max(0, Math.floor(u * maps.size - reach));
33
+ const x1 = Math.min(maps.size - 1, Math.floor(u * maps.size + reach));
34
+ const y0 = Math.max(0, Math.floor(v * maps.size - reach));
35
+ const y1 = Math.min(maps.size - 1, Math.floor(v * maps.size + reach));
36
+ out.r = 0;
37
+ out.g = 0;
38
+ out.b = 0;
39
+ out.clarity = 0;
40
+ let n = 0;
41
+ for (let y = y0; y <= y1; y++) {
42
+ for (let x = x0; x <= x1; x++) {
43
+ const at = (y * maps.size + x) * 4;
44
+ out.r += maps.tint[at];
45
+ out.g += maps.tint[at + 1];
46
+ out.b += maps.tint[at + 2];
47
+ out.clarity += maps.tint[at + 3];
48
+ n++;
49
+ }
50
+ }
51
+ out.r /= n;
52
+ out.g /= n;
53
+ out.b /= n;
54
+ out.clarity /= n;
55
+ }
56
+ /** What the sun keeps through glass at a receiver at map position (u, v) and light depth `depth`. */
57
+ export function glassTintAt(u, v, depth, maps, settings, out) {
58
+ out.r = 1;
59
+ out.g = 1;
60
+ out.b = 1;
61
+ out.clarity = 1;
62
+ if (u < 0 || u > 1 || v < 0 || v > 1)
63
+ return out;
64
+ const texel = SHADOW_TEXEL_SCALE / maps.size;
65
+ const bias = SHADOW_DEPTH_BIAS / settings.depthSpan;
66
+ const taps = Math.min(settings.taps, SHADOW_PCF_OFFSETS.length);
67
+ const at = index(maps, u, v);
68
+ centre.clarity = maps.tint[at * 4 + 3];
69
+ const behind = Math.max(depth - maps.glassDepth[at], 0) * settings.depthSpan;
70
+ const radius = frostRadius(centre.clarity, behind * settings.uvPerMetre, texel);
71
+ let cover = 0;
72
+ found.r = 0;
73
+ found.g = 0;
74
+ found.b = 0;
75
+ let shares = 0;
76
+ for (let i = 0; i < taps; i++) {
77
+ const offset = SHADOW_PCF_OFFSETS[i];
78
+ const nearest = maps.glassDepth[index(maps, u + offset[0] * texel, v + offset[1] * texel)];
79
+ if (depth > nearest + bias)
80
+ cover++;
81
+ coarseAt(maps, u + offset[0] * radius, v + offset[1] * radius, radius, coarse);
82
+ const share = unmixTint(coarse, centre.clarity, pane);
83
+ found.r += pane.r * share;
84
+ found.g += pane.g * share;
85
+ found.b += pane.b * share;
86
+ shares += share;
87
+ }
88
+ return spreadTint(cover / Math.max(taps, 1), found, shares, out);
89
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Which instances of a batch can be seen: the decision both backends make, and the reference the
3
+ * WebGPU compute cull is held to.
4
+ *
5
+ * **An instance's bounds are the mesh's, placed by its matrix** — the mesh's bounding sphere with its
6
+ * centre through the model matrix and its radius scaled by the largest axis, exactly as
7
+ * `boundsVisible` treats one mesh — so a batch needs no bounds of its own and a caller supplies none.
8
+ * The test is `sphereInFrustum`'s: kept unless wholly beyond one plane.
9
+ *
10
+ * WebGL2 runs `cullInstances` on the CPU; WebGPU runs `CULL_INSTANCES_WGSL`, the same arithmetic, on
11
+ * the device. Both keep what this keeps: `gpu-parity.mjs` compares them on a device. What this gives
12
+ * up is shape: a long thin mesh is culled by the sphere around it, which keeps it in more views than
13
+ * its box would. What would make that wrong is a batch of long thin things seen end on, where a box
14
+ * test pays; nothing in the engine draws those instanced today.
15
+ *
16
+ * Per frame and allocation-free: everything writes into arrays the caller owns.
17
+ */
18
+ import type { MeshInstances } from './instances.ts';
19
+ import type { Bounds } from '../math/bounds.ts';
20
+ import type { FrustumPlanes } from '../math/frustum.ts';
21
+ /**
22
+ * Indices a culling batch must draw, all its instances together, before culling it instance by
23
+ * instance pays; below it the batch is drawn whole once its box is seen (`batchBoxVisible`).
24
+ *
25
+ * **What a per-instance cull costs is fixed a batch, and what it saves grows with the batch.** On
26
+ * WebGPU it is a dispatch and an indirect draw, and Chrome validates every indirect draw in its GPU
27
+ * process — measured at 8 µs apiece in a city frame whose six hundred small prop batches took one
28
+ * each, 52 fps against 70 with those draws gone. On WebGL2 it is a loop over the instances and an
29
+ * upload of the survivors. A few thousand indices cost a device less than either.
30
+ *
31
+ * What it gives up is the instances of a small batch that stand out of view once its box is in it,
32
+ * which the device transforms and clips for nothing. What would make it wrong is a device whose
33
+ * vertex stage limits the frame while the CPU idles — where this wants to come down.
34
+ */
35
+ export declare const INSTANCE_CULL_MIN_INDICES: number;
36
+ /**
37
+ * Whether a culling batch of `count` instances of a mesh of `indexCount` indices is culled instance
38
+ * by instance, or drawn whole once its box is seen. See `INSTANCE_CULL_MIN_INDICES`.
39
+ */
40
+ export declare function cullsInstances(count: number, indexCount: number): boolean;
41
+ /**
42
+ * Keep the instances of `data` whose spheres meet `frustum`, compacted in order into `out`, and
43
+ * return how many. `out` must hold as many as `data` does; its count is set.
44
+ */
45
+ export declare function cullInstances(data: MeshInstances, local: Bounds, frustum: FrustumPlanes, out: MeshInstances): number;
46
+ /**
47
+ * The world box around every instance's sphere, for testing a whole batch at once: (minX, minY,
48
+ * minZ, maxX, maxY, maxZ) into `out`. A batch holding nothing writes a min above its max.
49
+ *
50
+ * **A box, not a sphere, and the difference is the whole point for a flat batch.** A region of props
51
+ * a hundred metres square has a sphere seventy metres deep underground, where nothing occludes it,
52
+ * so a sphere test never finds a region hidden. The box stays as flat as the props are.
53
+ */
54
+ export declare function instancesBox(data: MeshInstances, local: Bounds, out: Float32Array): void;
55
+ /** What the batch test asks of an occlusion buffer. `OcclusionBuffer` and both renderers answer it. */
56
+ export interface BoxOccluder {
57
+ occludedBox(min: ArrayLike<number>, max: ArrayLike<number>): boolean;
58
+ }
59
+ /**
60
+ * Whether a batch's box (`instancesBox`) can be seen at all: inside the frustum, and not behind the
61
+ * declared occluders. One test for a whole region's props, run before any instance is looked at.
62
+ */
63
+ export declare function batchBoxVisible(box: Float32Array, frustum: FrustumPlanes, occluder: BoxOccluder | null): boolean;
@@ -0,0 +1,160 @@
1
+ import { boxInFrustum } from '../math/frustum.js';
2
+ /** How many planes a frustum holds, four floats each. */
3
+ const PLANES = 6;
4
+ /**
5
+ * Indices a culling batch must draw, all its instances together, before culling it instance by
6
+ * instance pays; below it the batch is drawn whole once its box is seen (`batchBoxVisible`).
7
+ *
8
+ * **What a per-instance cull costs is fixed a batch, and what it saves grows with the batch.** On
9
+ * WebGPU it is a dispatch and an indirect draw, and Chrome validates every indirect draw in its GPU
10
+ * process — measured at 8 µs apiece in a city frame whose six hundred small prop batches took one
11
+ * each, 52 fps against 70 with those draws gone. On WebGL2 it is a loop over the instances and an
12
+ * upload of the survivors. A few thousand indices cost a device less than either.
13
+ *
14
+ * What it gives up is the instances of a small batch that stand out of view once its box is in it,
15
+ * which the device transforms and clips for nothing. What would make it wrong is a device whose
16
+ * vertex stage limits the frame while the CPU idles — where this wants to come down.
17
+ */
18
+ export const INSTANCE_CULL_MIN_INDICES = 1 << 14;
19
+ /**
20
+ * Whether a culling batch of `count` instances of a mesh of `indexCount` indices is culled instance
21
+ * by instance, or drawn whole once its box is seen. See `INSTANCE_CULL_MIN_INDICES`.
22
+ */
23
+ export function cullsInstances(count, indexCount) {
24
+ return count * indexCount >= INSTANCE_CULL_MIN_INDICES;
25
+ }
26
+ /**
27
+ * Keep the instances of `data` whose spheres meet `frustum`, compacted in order into `out`, and
28
+ * return how many. `out` must hold as many as `data` does; its count is set.
29
+ */
30
+ export function cullInstances(data, local, frustum, out) {
31
+ const count = Math.min(data.count, data.capacity, out.capacity);
32
+ const models = data.models;
33
+ const tints = data.tints;
34
+ let kept = 0;
35
+ for (let i = 0; i < count; i++) {
36
+ sphereOf(models, i, local, scratch);
37
+ if (!sphereMeets(frustum, scratch))
38
+ continue;
39
+ /* Copied float by float: a subarray would be a view allocated per instance, per frame. */
40
+ const from = i * 16;
41
+ const to = kept * 16;
42
+ for (let k = 0; k < 16; k++)
43
+ out.models[to + k] = models[from + k];
44
+ out.tints[kept * 3] = tints[i * 3];
45
+ out.tints[kept * 3 + 1] = tints[i * 3 + 1];
46
+ out.tints[kept * 3 + 2] = tints[i * 3 + 2];
47
+ kept++;
48
+ }
49
+ out.count = kept;
50
+ return kept;
51
+ }
52
+ /** `sphereInFrustum` over a packed (x, y, z, radius): kept unless wholly beyond one plane. */
53
+ function sphereMeets(frustum, sphere) {
54
+ const x = sphere[0];
55
+ const y = sphere[1];
56
+ const z = sphere[2];
57
+ const radius = sphere[3];
58
+ for (let plane = 0; plane < PLANES; plane++) {
59
+ const at = plane * 4;
60
+ const distance = frustum[at] * x +
61
+ frustum[at + 1] * y +
62
+ frustum[at + 2] * z +
63
+ frustum[at + 3];
64
+ if (distance < -radius)
65
+ return false;
66
+ }
67
+ return true;
68
+ }
69
+ /**
70
+ * The world box around every instance's sphere, for testing a whole batch at once: (minX, minY,
71
+ * minZ, maxX, maxY, maxZ) into `out`. A batch holding nothing writes a min above its max.
72
+ *
73
+ * **A box, not a sphere, and the difference is the whole point for a flat batch.** A region of props
74
+ * a hundred metres square has a sphere seventy metres deep underground, where nothing occludes it,
75
+ * so a sphere test never finds a region hidden. The box stays as flat as the props are.
76
+ */
77
+ export function instancesBox(data, local, out) {
78
+ const count = Math.min(data.count, data.capacity);
79
+ if (count === 0) {
80
+ out[0] = 1;
81
+ out[1] = 1;
82
+ out[2] = 1;
83
+ out[3] = -1;
84
+ out[4] = -1;
85
+ out[5] = -1;
86
+ return;
87
+ }
88
+ let minX = Infinity;
89
+ let minY = Infinity;
90
+ let minZ = Infinity;
91
+ let maxX = -Infinity;
92
+ let maxY = -Infinity;
93
+ let maxZ = -Infinity;
94
+ for (let i = 0; i < count; i++) {
95
+ sphereOf(data.models, i, local, scratch);
96
+ const r = scratch[3];
97
+ minX = Math.min(minX, scratch[0] - r);
98
+ minY = Math.min(minY, scratch[1] - r);
99
+ minZ = Math.min(minZ, scratch[2] - r);
100
+ maxX = Math.max(maxX, scratch[0] + r);
101
+ maxY = Math.max(maxY, scratch[1] + r);
102
+ maxZ = Math.max(maxZ, scratch[2] + r);
103
+ }
104
+ out[0] = minX;
105
+ out[1] = minY;
106
+ out[2] = minZ;
107
+ out[3] = maxX;
108
+ out[4] = maxY;
109
+ out[5] = maxZ;
110
+ }
111
+ const scratch = new Float32Array(4);
112
+ /** Instance `i`'s world sphere into `out` as (x, y, z, radius). */
113
+ function sphereOf(models, i, local, out) {
114
+ const m = i * 16;
115
+ const cx = local.centre[0] ?? 0;
116
+ const cy = local.centre[1] ?? 0;
117
+ const cz = local.centre[2] ?? 0;
118
+ const m0 = models[m];
119
+ const m1 = models[m + 1];
120
+ const m2 = models[m + 2];
121
+ const m4 = models[m + 4];
122
+ const m5 = models[m + 5];
123
+ const m6 = models[m + 6];
124
+ const m8 = models[m + 8];
125
+ const m9 = models[m + 9];
126
+ const m10 = models[m + 10];
127
+ out[0] = m0 * cx + m4 * cy + m8 * cz + models[m + 12];
128
+ out[1] = m1 * cx + m5 * cy + m9 * cz + models[m + 13];
129
+ out[2] = m2 * cx + m6 * cy + m10 * cz + models[m + 14];
130
+ out[3] =
131
+ local.radius *
132
+ Math.sqrt(Math.max(m0 * m0 + m1 * m1 + m2 * m2, m4 * m4 + m5 * m5 + m6 * m6, m8 * m8 + m9 * m9 + m10 * m10));
133
+ }
134
+ /**
135
+ * Whether a batch's box (`instancesBox`) can be seen at all: inside the frustum, and not behind the
136
+ * declared occluders. One test for a whole region's props, run before any instance is looked at.
137
+ */
138
+ export function batchBoxVisible(box, frustum, occluder) {
139
+ const minX = box[0];
140
+ const minY = box[1];
141
+ const minZ = box[2];
142
+ const maxX = box[3];
143
+ const maxY = box[4];
144
+ const maxZ = box[5];
145
+ if (minX > maxX)
146
+ return false;
147
+ if (!boxInFrustum(frustum, minX, minY, minZ, maxX, maxY, maxZ))
148
+ return false;
149
+ if (occluder === null)
150
+ return true;
151
+ boxMin[0] = minX;
152
+ boxMin[1] = minY;
153
+ boxMin[2] = minZ;
154
+ boxMax[0] = maxX;
155
+ boxMax[1] = maxY;
156
+ boxMax[2] = maxZ;
157
+ return !occluder.occludedBox(boxMin, boxMax);
158
+ }
159
+ const boxMin = new Float32Array(3);
160
+ const boxMax = new Float32Array(3);
@@ -67,3 +67,18 @@ export declare const INSTANCE_STRIDE: number;
67
67
  * Writes nothing beyond `count`, and allocates nothing: `out` is the caller's staging array.
68
68
  */
69
69
  export declare function packInstances(instances: MeshInstances, out: Float32Array): void;
70
+ /** How a batch made by `createInstanced` behaves. */
71
+ export interface InstancedOptions {
72
+ /**
73
+ * Cull the camera draw: the whole batch skipped when its instances' sphere is out of view or behind
74
+ * the declared occluders, and otherwise each instance kept only where its sphere meets the view —
75
+ * on the CPU on WebGL2, as compute into an indirect draw on WebGPU — once the batch is large enough
76
+ * to repay that (`cullsInstances`); a smaller one is drawn whole. Shadow, mirror-free motion and
77
+ * blended draws still see every instance. Off by default, because a batch small enough to draw whole
78
+ * pays for nothing it would save.
79
+ *
80
+ * **For a batch spread over space** — a prop across a region, a crowd, traffic. A batch in one
81
+ * place gains only the whole-batch test, which it could as well do itself.
82
+ */
83
+ readonly cull?: boolean;
84
+ }
@@ -34,13 +34,14 @@ export declare const MAX_POINT_LIGHTS = 16;
34
34
  /** Two live maps make changing the caster's owning light a crossfade, not a pop. */
35
35
  export declare const LIVE_POINT_SHADOW_MAPS = 2;
36
36
  /**
37
- * Texture units the directional cascade binds: static, its depth peel, and dynamic.
37
+ * Texture units the sun's shadow binds: one, for its static, moving and peeled layers.
38
38
  *
39
- * Named because the shading pass's total unit count has to be compared against what a
40
- * device offers, and a budget assembled from three literals scattered across two files
41
- * is a budget nobody can check. See the guard in `Renderer`'s constructor.
39
+ * **It was three**, a `sampler2D` per map, until 4.5.0 folded them into one `sampler2DArray`
40
+ * (`shadowMap.ts`) the way the lamps' twelve cubemaps became one — which is where the two tints
41
+ * glass casts with found their units. Named because the shading pass's total has to be compared
42
+ * against what a device offers; see the guard in `Renderer`'s constructor.
42
43
  */
43
- export declare const DIRECTIONAL_SHADOW_UNITS = 3;
44
+ export declare const DIRECTIONAL_SHADOW_UNITS = 1;
44
45
  /**
45
46
  * Texture units every point light's shadow binds: one, for all of them.
46
47
  *
@@ -130,22 +131,20 @@ export declare const ENVIRONMENT_TEXTURE_UNIT: number;
130
131
  */
131
132
  export declare const CLUSTER_TABLE_TEXTURE_UNIT: number;
132
133
  /**
133
- * Where the photometric atlas binds: one row per IES profile, any number of them.
134
- *
135
- * **One unit for every profile a world holds, which is the whole reason it is an atlas.** A
136
- * fixture's distribution is a curve rather than an image, so a row of 128 floats holds one and a
137
- * texture holds as many as a consumer loads. Per-profile textures would have spent the remaining
138
- * units on the third fixture.
139
- *
140
- * **Spent unconditionally, for the same reason the froxel table is**, and the argument is at that
141
- * declaration: the lookup is a branch on a value rather than a permutation, so the sampler is
142
- * declared in every profile and a declared sampler needs a complete texture bound whether or not
143
- * the branch reads it. With no profile loaded that texture is a single row of ones, which is the
144
- * multiplicative identity — a row of zeros would switch off every light that reached it.
145
- *
146
- * The tenth of sixteen. `CAPABILITIES.md` §3 keeps the running count.
134
+ * Where the fixture atlas binds: a fixture's spot cookies and its photometric profiles, one texture.
135
+ *
136
+ * **Two tables in one binding since 2026-09-30**, which is the fold `textureUnitBudget.test.ts` asks
137
+ * the next sampler for: the widest lit shader bound fifteen of the sixteen fragment samplers WebGL2
138
+ * guarantees, and the cookie atlas and the photometric atlas were two small float tables read with
139
+ * `textureLod`, both 128 texels across. See `fixtureAtlas.ts` for the layout and why cookies sit on
140
+ * top.
141
+ *
142
+ * **Spent unconditionally, for the reason the froxel table is**: both lookups are branches on values
143
+ * rather than permutations, so the sampler is declared in every profile and a declared sampler needs
144
+ * a complete texture. With nothing loaded that is a single row of ones — the multiplicative
145
+ * identity, since a row of zeros would switch off every light that reached it.
147
146
  */
148
- export declare const IES_ATLAS_TEXTURE_UNIT: number;
147
+ export declare const FIXTURE_ATLAS_TEXTURE_UNIT: number;
149
148
  /**
150
149
  * The joint palette, read by the **vertex** stage.
151
150
  *
@@ -172,21 +171,6 @@ export declare const SKIN_PALETTE_TEXTURE_UNIT: number;
172
171
  * of sixteen spoken for, three free.
173
172
  */
174
173
  export declare const MORPH_DELTA_TEXTURE_UNIT: number;
175
- /**
176
- * Where the spot-light cookie atlas binds: one row of square tiles, one tile a cookie.
177
- *
178
- * **The fourteenth of sixteen, and two are left.** `CAPABILITIES.md` §3 keeps the running count —
179
- * and had been three units stale for as long as the IES atlas, the skin palette and the morph
180
- * deltas have existed, which is why the number is derived in `textureUnitBudget.test.ts` and only
181
- * quoted in prose.
182
- *
183
- * **Spent unconditionally, for the reason the froxel table and the IES atlas are**: a declared
184
- * sampler needs a complete texture bound whether or not the branch reads it, so a one-texel white
185
- * placeholder stands in it when no consumer has loaded a cookie. White rather than black, because
186
- * it multiplies a light's colour and a black stand-in would switch off every light that reached
187
- * it — the identity `packIesAtlas` chooses for the same reason.
188
- */
189
- export declare const COOKIE_ATLAS_TEXTURE_UNIT: number;
190
174
  /**
191
175
  * The colour the frame had already drawn, for a refracting surface to sample.
192
176
  *
@@ -212,6 +196,23 @@ export declare const REFRACT_SCENE_TEXTURE_UNIT: number;
212
196
  */
213
197
  export declare const DRIFT_LIGHT_INDEX_TEXTURE_UNIT: number;
214
198
  export declare const DRIFT_LIGHT_ATLAS_TEXTURE_UNIT: number;
199
+ /**
200
+ * What the sun keeps through glass (`sunGlassTint.ts`), the lit program's fourteenth fragment
201
+ * sampler: the unit the sun's three maps gave back when they became one array.
202
+ */
203
+ export declare const SUN_GLASS_TINT_TEXTURE_UNIT: number;
204
+ /**
205
+ * What each lamp keeps through glass (`pointGlassTint.ts`), a fragment sampler of the lit program.
206
+ * A unit of the combined pool, which WebGL2 guarantees thirty-two of; what a stage may declare is
207
+ * sixteen, which `textureUnitBudget.test.ts` counts from the shader source.
208
+ */
209
+ export declare const POINT_GLASS_TINT_TEXTURE_UNIT: number;
210
+ /**
211
+ * Each surface layer's effects table (`surfaceEffects.ts`), bound beside the albedo array it
212
+ * describes. **Paid for by folding**: the cookie and photometric atlases became one fixture atlas
213
+ * to free the sampler this takes, so the lit stage still keeps one of its sixteen spare.
214
+ */
215
+ export declare const SURFACE_EFFECTS_TEXTURE_UNIT: number;
215
216
  /**
216
217
  * A cookie's tile, in texels a side.
217
218
  *
@@ -32,13 +32,14 @@ export const MAX_POINT_LIGHTS = 16;
32
32
  /** Two live maps make changing the caster's owning light a crossfade, not a pop. */
33
33
  export const LIVE_POINT_SHADOW_MAPS = 2;
34
34
  /**
35
- * Texture units the directional cascade binds: static, its depth peel, and dynamic.
35
+ * Texture units the sun's shadow binds: one, for its static, moving and peeled layers.
36
36
  *
37
- * Named because the shading pass's total unit count has to be compared against what a
38
- * device offers, and a budget assembled from three literals scattered across two files
39
- * is a budget nobody can check. See the guard in `Renderer`'s constructor.
37
+ * **It was three**, a `sampler2D` per map, until 4.5.0 folded them into one `sampler2DArray`
38
+ * (`shadowMap.ts`) the way the lamps' twelve cubemaps became one — which is where the two tints
39
+ * glass casts with found their units. Named because the shading pass's total has to be compared
40
+ * against what a device offers; see the guard in `Renderer`'s constructor.
40
41
  */
41
- export const DIRECTIONAL_SHADOW_UNITS = 3;
42
+ export const DIRECTIONAL_SHADOW_UNITS = 1;
42
43
  /**
43
44
  * Texture units every point light's shadow binds: one, for all of them.
44
45
  *
@@ -128,22 +129,20 @@ export const ENVIRONMENT_TEXTURE_UNIT = EMISSIVE_TEXTURE_UNIT + 1;
128
129
  */
129
130
  export const CLUSTER_TABLE_TEXTURE_UNIT = ENVIRONMENT_TEXTURE_UNIT + 1;
130
131
  /**
131
- * Where the photometric atlas binds: one row per IES profile, any number of them.
132
- *
133
- * **One unit for every profile a world holds, which is the whole reason it is an atlas.** A
134
- * fixture's distribution is a curve rather than an image, so a row of 128 floats holds one and a
135
- * texture holds as many as a consumer loads. Per-profile textures would have spent the remaining
136
- * units on the third fixture.
137
- *
138
- * **Spent unconditionally, for the same reason the froxel table is**, and the argument is at that
139
- * declaration: the lookup is a branch on a value rather than a permutation, so the sampler is
140
- * declared in every profile and a declared sampler needs a complete texture bound whether or not
141
- * the branch reads it. With no profile loaded that texture is a single row of ones, which is the
142
- * multiplicative identity — a row of zeros would switch off every light that reached it.
143
- *
144
- * The tenth of sixteen. `CAPABILITIES.md` §3 keeps the running count.
132
+ * Where the fixture atlas binds: a fixture's spot cookies and its photometric profiles, one texture.
133
+ *
134
+ * **Two tables in one binding since 2026-09-30**, which is the fold `textureUnitBudget.test.ts` asks
135
+ * the next sampler for: the widest lit shader bound fifteen of the sixteen fragment samplers WebGL2
136
+ * guarantees, and the cookie atlas and the photometric atlas were two small float tables read with
137
+ * `textureLod`, both 128 texels across. See `fixtureAtlas.ts` for the layout and why cookies sit on
138
+ * top.
139
+ *
140
+ * **Spent unconditionally, for the reason the froxel table is**: both lookups are branches on values
141
+ * rather than permutations, so the sampler is declared in every profile and a declared sampler needs
142
+ * a complete texture. With nothing loaded that is a single row of ones — the multiplicative
143
+ * identity, since a row of zeros would switch off every light that reached it.
145
144
  */
146
- export const IES_ATLAS_TEXTURE_UNIT = CLUSTER_TABLE_TEXTURE_UNIT + 1;
145
+ export const FIXTURE_ATLAS_TEXTURE_UNIT = CLUSTER_TABLE_TEXTURE_UNIT + 1;
147
146
  /**
148
147
  * The joint palette, read by the **vertex** stage.
149
148
  *
@@ -161,7 +160,7 @@ export const IES_ATLAS_TEXTURE_UNIT = CLUSTER_TABLE_TEXTURE_UNIT + 1;
161
160
  * What it costs is one of the five units left after the IES atlas. What would make it wrong is
162
161
  * the lit pass growing past sixteen, which `textureUnitBudget.test.ts` asserts against.
163
162
  */
164
- export const SKIN_PALETTE_TEXTURE_UNIT = IES_ATLAS_TEXTURE_UNIT + 1;
163
+ export const SKIN_PALETTE_TEXTURE_UNIT = FIXTURE_ATLAS_TEXTURE_UNIT + 1;
165
164
  /**
166
165
  * A mesh's morph deltas, also read by the vertex stage, and also out of this same sixteen.
167
166
  *
@@ -170,21 +169,6 @@ export const SKIN_PALETTE_TEXTURE_UNIT = IES_ATLAS_TEXTURE_UNIT + 1;
170
169
  * of sixteen spoken for, three free.
171
170
  */
172
171
  export const MORPH_DELTA_TEXTURE_UNIT = SKIN_PALETTE_TEXTURE_UNIT + 1;
173
- /**
174
- * Where the spot-light cookie atlas binds: one row of square tiles, one tile a cookie.
175
- *
176
- * **The fourteenth of sixteen, and two are left.** `CAPABILITIES.md` §3 keeps the running count —
177
- * and had been three units stale for as long as the IES atlas, the skin palette and the morph
178
- * deltas have existed, which is why the number is derived in `textureUnitBudget.test.ts` and only
179
- * quoted in prose.
180
- *
181
- * **Spent unconditionally, for the reason the froxel table and the IES atlas are**: a declared
182
- * sampler needs a complete texture bound whether or not the branch reads it, so a one-texel white
183
- * placeholder stands in it when no consumer has loaded a cookie. White rather than black, because
184
- * it multiplies a light's colour and a black stand-in would switch off every light that reached
185
- * it — the identity `packIesAtlas` chooses for the same reason.
186
- */
187
- export const COOKIE_ATLAS_TEXTURE_UNIT = MORPH_DELTA_TEXTURE_UNIT + 1;
188
172
  /**
189
173
  * The colour the frame had already drawn, for a refracting surface to sample.
190
174
  *
@@ -198,7 +182,7 @@ export const COOKIE_ATLAS_TEXTURE_UNIT = MORPH_DELTA_TEXTURE_UNIT + 1;
198
182
  * cubemaps became one array, which is what freed the units this one is spending.
199
183
  * `textureUnitBudget.test.ts` carries the phone that budget was written for.
200
184
  */
201
- export const REFRACT_SCENE_TEXTURE_UNIT = COOKIE_ATLAS_TEXTURE_UNIT + 1;
185
+ export const REFRACT_SCENE_TEXTURE_UNIT = MORPH_DELTA_TEXTURE_UNIT + 1;
202
186
  /**
203
187
  * DriftLight's brick index and atlas, the lit program's fourteenth and fifteenth fragment samplers.
204
188
  *
@@ -210,6 +194,23 @@ export const REFRACT_SCENE_TEXTURE_UNIT = COOKIE_ATLAS_TEXTURE_UNIT + 1;
210
194
  */
211
195
  export const DRIFT_LIGHT_INDEX_TEXTURE_UNIT = REFRACT_SCENE_TEXTURE_UNIT + 1;
212
196
  export const DRIFT_LIGHT_ATLAS_TEXTURE_UNIT = DRIFT_LIGHT_INDEX_TEXTURE_UNIT + 1;
197
+ /**
198
+ * What the sun keeps through glass (`sunGlassTint.ts`), the lit program's fourteenth fragment
199
+ * sampler: the unit the sun's three maps gave back when they became one array.
200
+ */
201
+ export const SUN_GLASS_TINT_TEXTURE_UNIT = DRIFT_LIGHT_ATLAS_TEXTURE_UNIT + 1;
202
+ /**
203
+ * What each lamp keeps through glass (`pointGlassTint.ts`), a fragment sampler of the lit program.
204
+ * A unit of the combined pool, which WebGL2 guarantees thirty-two of; what a stage may declare is
205
+ * sixteen, which `textureUnitBudget.test.ts` counts from the shader source.
206
+ */
207
+ export const POINT_GLASS_TINT_TEXTURE_UNIT = SUN_GLASS_TINT_TEXTURE_UNIT + 1;
208
+ /**
209
+ * Each surface layer's effects table (`surfaceEffects.ts`), bound beside the albedo array it
210
+ * describes. **Paid for by folding**: the cookie and photometric atlases became one fixture atlas
211
+ * to free the sampler this takes, so the lit stage still keeps one of its sixteen spare.
212
+ */
213
+ export const SURFACE_EFFECTS_TEXTURE_UNIT = POINT_GLASS_TINT_TEXTURE_UNIT + 1;
213
214
  /**
214
215
  * A cookie's tile, in texels a side.
215
216
  *
@@ -0,0 +1,42 @@
1
+ /**
2
+ * A world's fixed lights bucketed once, so the frame's exact choice looks only at the lights near
3
+ * the eye rather than at every one of them.
4
+ *
5
+ * **The choice must be the full scan's, exactly, and needs no sort to be.** Both of the choice's
6
+ * lists break a tie in distance by source index, so the order the lights are met in cannot reach the
7
+ * result: what reaches it is the full scan's candidates minus only lights no query could keep —
8
+ * every light whose cell lies within `viewRange + maxRadius` of the eye is gathered, and the
9
+ * selection's own `viewRange + radius` test does the rest.
10
+ *
11
+ * **Cost stated**: built once, from where the lights stood. A light that moves or changes its reach
12
+ * must be re-bucketed by the caller — this is for the city's fixed thousands, and a scene's handful
13
+ * of movers stays with the plain scan.
14
+ *
15
+ * Nothing here allocates after construction; the gather writes into the grid's own scratch.
16
+ */
17
+ import type { PointLightSource } from './pointLightSelection.ts';
18
+ /** The most cells a grid may span, which keeps a stray light far away from costing a gigabyte. */
19
+ export declare const LIGHT_GRID_MAX_CELLS: number;
20
+ export interface LightGrid {
21
+ readonly sources: readonly PointLightSource[];
22
+ /** Metres a cell spans on each axis. */
23
+ readonly cell: number;
24
+ /** The world position of cell (0, 0, 0)'s corner. */
25
+ readonly origin: readonly [number, number, number];
26
+ readonly dims: readonly [number, number, number];
27
+ /** Where cell `c`'s lights start in `lights`; `start[c + 1]` is where they end. */
28
+ readonly start: Uint32Array;
29
+ /** Source indices by cell, ascending within each. */
30
+ readonly lights: Uint32Array;
31
+ /** The largest reach of any source, which widens every query. */
32
+ readonly maxRadius: number;
33
+ /** The last gather, cell by cell: `gathered[0..count)`. */
34
+ readonly gathered: Int32Array;
35
+ }
36
+ /** Bucket `sources` into cells of `cell` metres. Built once; not a frame path. */
37
+ export declare function createLightGrid(sources: readonly PointLightSource[], cell: number): LightGrid;
38
+ /**
39
+ * Gather into `grid.gathered`, cell by cell, every light whose cell lies within `reach` metres of
40
+ * (x, y, z) on each axis, and answer how many.
41
+ */
42
+ export declare function gatherLights(grid: LightGrid, x: number, y: number, z: number, reach: number): number;