@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
@@ -255,7 +255,8 @@ export function createScatterDepthBindGroupLayout(device, fragBinding, fragSize,
255
255
  {
256
256
  binding: previous.texture,
257
257
  visibility: VISIBILITY_FRAGMENT,
258
- texture: { sampleType: 'unfilterable-float' },
258
+ /* The depth program's peel read, an array since the sun's maps became one. */
259
+ texture: { sampleType: 'unfilterable-float', viewDimension: '2d-array' },
259
260
  },
260
261
  {
261
262
  binding: previous.sampler,
@@ -125,7 +125,12 @@ export function createSdfTextBindGroupLayout(device) {
125
125
  visibility: VISIBILITY_FRAGMENT,
126
126
  buffer: { type: 'uniform', hasDynamicOffset: true, minBindingSize: SDF_TEXT_FRAG_SIZE },
127
127
  },
128
- { binding: ATLAS.texture, visibility: VISIBILITY_FRAGMENT, texture: {} },
128
+ /* The atlas is a surface texture, and every surface texture is a `2d-array` view. */
129
+ {
130
+ binding: ATLAS.texture,
131
+ visibility: VISIBILITY_FRAGMENT,
132
+ texture: { viewDimension: '2d-array' },
133
+ },
129
134
  { binding: ATLAS.sampler, visibility: VISIBILITY_FRAGMENT, sampler: {} },
130
135
  ],
131
136
  });
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The instance cull, in WGSL, hand-authored under the compute exception `AGENTS.md` records
3
+ * (2026-08-24): a compute shader has no WebGL2 twin to generate from, and `npm run wgsl` neither
4
+ * writes nor polices this file.
5
+ *
6
+ * **It keeps exactly what `cullInstances` keeps**, and `gpu-parity.mjs` runs both on a device: an
7
+ * instance's world sphere is the mesh's sphere with its centre through the model matrix and its
8
+ * radius scaled by the largest column, and it is kept unless wholly beyond one of the six planes.
9
+ * The one difference is order — survivors are compacted by `atomicAdd`, so they arrive in whatever
10
+ * order the invocations reached it. That is invisible to an opaque draw and is why a blended draw
11
+ * is never culled here.
12
+ *
13
+ * `args` is a `drawIndexedIndirect` record, `(indexCount, instanceCount, firstIndex, baseVertex,
14
+ * firstInstance)`; the renderer writes it with a zero instance count before each dispatch and this
15
+ * counts the survivors into it.
16
+ */
17
+ export declare const INSTANCE_CULL_WORKGROUP = 64;
18
+ export declare const INSTANCE_CULL_WGSL = "\nstruct Params {\n planes: array<vec4<f32>, 6>,\n sphere: vec4<f32>,\n count: u32,\n pad0: u32,\n pad1: u32,\n pad2: u32,\n}\n\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> source: array<f32>;\n@group(0) @binding(2) var<storage, read_write> kept: array<f32>;\n@group(0) @binding(3) var<storage, read_write> args: array<atomic<u32>, 5>;\n\nconst STRIDE: u32 = 20u;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) id: vec3<u32>) {\n let i = id.x;\n if (i >= params.count) { return; }\n let at = i * STRIDE;\n let c0 = vec3<f32>(source[at], source[at + 1u], source[at + 2u]);\n let c1 = vec3<f32>(source[at + 4u], source[at + 5u], source[at + 6u]);\n let c2 = vec3<f32>(source[at + 8u], source[at + 9u], source[at + 10u]);\n let t = vec3<f32>(source[at + 12u], source[at + 13u], source[at + 14u]);\n let local = params.sphere;\n let centre = c0 * local.x + c1 * local.y + c2 * local.z + t;\n let scale = sqrt(max(dot(c0, c0), max(dot(c1, c1), dot(c2, c2))));\n let radius = local.w * scale;\n for (var p = 0u; p < 6u; p = p + 1u) {\n let plane = params.planes[p];\n if (dot(plane.xyz, centre) + plane.w < -radius) { return; }\n }\n let slot = atomicAdd(&args[1], 1u);\n let to = slot * STRIDE;\n for (var k = 0u; k < STRIDE; k = k + 1u) {\n kept[to + k] = source[at + k];\n }\n}\n";
@@ -0,0 +1,59 @@
1
+ import { INSTANCE_FLOATS } from '../../../instances.js';
2
+ /**
3
+ * The instance cull, in WGSL, hand-authored under the compute exception `AGENTS.md` records
4
+ * (2026-08-24): a compute shader has no WebGL2 twin to generate from, and `npm run wgsl` neither
5
+ * writes nor polices this file.
6
+ *
7
+ * **It keeps exactly what `cullInstances` keeps**, and `gpu-parity.mjs` runs both on a device: an
8
+ * instance's world sphere is the mesh's sphere with its centre through the model matrix and its
9
+ * radius scaled by the largest column, and it is kept unless wholly beyond one of the six planes.
10
+ * The one difference is order — survivors are compacted by `atomicAdd`, so they arrive in whatever
11
+ * order the invocations reached it. That is invisible to an opaque draw and is why a blended draw
12
+ * is never culled here.
13
+ *
14
+ * `args` is a `drawIndexedIndirect` record, `(indexCount, instanceCount, firstIndex, baseVertex,
15
+ * firstInstance)`; the renderer writes it with a zero instance count before each dispatch and this
16
+ * counts the survivors into it.
17
+ */
18
+ export const INSTANCE_CULL_WORKGROUP = 64;
19
+ export const INSTANCE_CULL_WGSL = /* wgsl */ `
20
+ struct Params {
21
+ planes: array<vec4<f32>, 6>,
22
+ sphere: vec4<f32>,
23
+ count: u32,
24
+ pad0: u32,
25
+ pad1: u32,
26
+ pad2: u32,
27
+ }
28
+
29
+ @group(0) @binding(0) var<uniform> params: Params;
30
+ @group(0) @binding(1) var<storage, read> source: array<f32>;
31
+ @group(0) @binding(2) var<storage, read_write> kept: array<f32>;
32
+ @group(0) @binding(3) var<storage, read_write> args: array<atomic<u32>, 5>;
33
+
34
+ const STRIDE: u32 = ${INSTANCE_FLOATS}u;
35
+
36
+ @compute @workgroup_size(${INSTANCE_CULL_WORKGROUP})
37
+ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
38
+ let i = id.x;
39
+ if (i >= params.count) { return; }
40
+ let at = i * STRIDE;
41
+ let c0 = vec3<f32>(source[at], source[at + 1u], source[at + 2u]);
42
+ let c1 = vec3<f32>(source[at + 4u], source[at + 5u], source[at + 6u]);
43
+ let c2 = vec3<f32>(source[at + 8u], source[at + 9u], source[at + 10u]);
44
+ let t = vec3<f32>(source[at + 12u], source[at + 13u], source[at + 14u]);
45
+ let local = params.sphere;
46
+ let centre = c0 * local.x + c1 * local.y + c2 * local.z + t;
47
+ let scale = sqrt(max(dot(c0, c0), max(dot(c1, c1), dot(c2, c2))));
48
+ let radius = local.w * scale;
49
+ for (var p = 0u; p < 6u; p = p + 1u) {
50
+ let plane = params.planes[p];
51
+ if (dot(plane.xyz, centre) + plane.w < -radius) { return; }
52
+ }
53
+ let slot = atomicAdd(&args[1], 1u);
54
+ let to = slot * STRIDE;
55
+ for (var k = 0u; k < STRIDE; k = k + 1u) {
56
+ kept[to + k] = source[at + k];
57
+ }
58
+ }
59
+ `;
@@ -13,6 +13,10 @@
13
13
  * **What would make it wrong** is a genuine crowd — a stadium, a swarm of rigged birds — and the
14
14
  * fix is then the one `skinPalette.ts` names: one texture holding every palette with a per-draw
15
15
  * row offset, which spends a uniform and a shader change to stop spending textures.
16
+ *
17
+ * **A reconstructing frame spends one more per skinned mover**: last frame's pose is uploaded into
18
+ * a slot of its own for the motion pass. So the same budget is about twelve such characters with a
19
+ * reflection and three shadow layers each.
16
20
  */
17
21
  export declare const MAX_SKIN_PALETTES = 128;
18
22
  /**
@@ -14,6 +14,10 @@ import { SkinPaletteTexture } from './skinPaletteTexture.js';
14
14
  * **What would make it wrong** is a genuine crowd — a stadium, a swarm of rigged birds — and the
15
15
  * fix is then the one `skinPalette.ts` names: one texture holding every palette with a per-draw
16
16
  * row offset, which spends a uniform and a shader change to stop spending textures.
17
+ *
18
+ * **A reconstructing frame spends one more per skinned mover**: last frame's pose is uploaded into
19
+ * a slot of its own for the motion pass. So the same budget is about twelve such characters with a
20
+ * reflection and three shadow layers each.
17
21
  */
18
22
  export const MAX_SKIN_PALETTES = 128;
19
23
  /**
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The mip chain of the frame's refraction copy, recorded into the frame's own encoder.
3
+ *
4
+ * **Frosted glass reads a blurrier level of what is behind it** (`glass.ts`), so on a frame that draws
5
+ * glass the copy needs every level filled from the one above. `generateMipChain` does that for an
6
+ * upload, on an encoder of its own that it submits at once — which here would run before the frame's
7
+ * encoder had even made the copy, and would build views and bind groups on every call. So this
8
+ * makes them once per copy texture and records one pass per level wherever the copy was taken.
9
+ *
10
+ * What it costs is a render pass per level on every frame that takes the copy — about a dozen for a
11
+ * 1080p copy, each a quarter of the one before, together about a third of the copy itself — and
12
+ * nothing on a frame that shows nothing through anything. **A frame that refracts and draws no glass
13
+ * pays it too**, and that is the trade: the copy is taken at the first draw that asks and shared by
14
+ * every later one, so a glass pane drawn after a refracting one would otherwise read levels nothing
15
+ * filled. WebGL2 fills its chain on the first glass draw instead, because a `generateMipmap` there
16
+ * ends no pass. What would change it is knowing at the copy whether glass follows in the frame.
17
+ */
18
+ import type { MipPipelines } from './surfaceTexturePass.ts';
19
+ export declare class SnapshotMips {
20
+ private readonly label;
21
+ private readonly pipeline;
22
+ private readonly targets;
23
+ private readonly groups;
24
+ constructor(device: GPUDevice, pipelines: MipPipelines, texture: GPUTexture, format: GPUTextureFormat,
25
+ /** One layer of an array texture, where the chain is a layer's rather than the texture's. */
26
+ layer?: number | null, label?: string);
27
+ /** Fill every level from level 0, in `encoder`, after whatever wrote level 0 there. */
28
+ record(encoder: GPUCommandEncoder): void;
29
+ }
@@ -0,0 +1,52 @@
1
+ import { mipBlitPipeline } from './surfaceTexturePass.js';
2
+ export class SnapshotMips {
3
+ label;
4
+ pipeline;
5
+ targets = [];
6
+ groups = [];
7
+ constructor(device, pipelines, texture, format,
8
+ /** One layer of an array texture, where the chain is a layer's rather than the texture's. */
9
+ layer = null, label = 'refract.mips') {
10
+ this.label = label;
11
+ this.pipeline = mipBlitPipeline(device, pipelines, format);
12
+ const sampler = device.createSampler({
13
+ label: 'refract.mips.sampler',
14
+ magFilter: 'linear',
15
+ minFilter: 'linear',
16
+ });
17
+ const layout = this.pipeline.getBindGroupLayout(0);
18
+ const at = layer === null ? {} : { dimension: '2d', baseArrayLayer: layer, arrayLayerCount: 1 };
19
+ for (let level = 1; level < texture.mipLevelCount; level++) {
20
+ const source = texture.createView({ ...at, baseMipLevel: level - 1, mipLevelCount: 1 });
21
+ this.targets.push(texture.createView({ ...at, baseMipLevel: level, mipLevelCount: 1 }));
22
+ this.groups.push(device.createBindGroup({
23
+ label: `refract.mips.${String(level)}`,
24
+ layout,
25
+ entries: [
26
+ { binding: 0, resource: source },
27
+ { binding: 1, resource: sampler },
28
+ ],
29
+ }));
30
+ }
31
+ }
32
+ /** Fill every level from level 0, in `encoder`, after whatever wrote level 0 there. */
33
+ record(encoder) {
34
+ for (let i = 0; i < this.targets.length; i++) {
35
+ const pass = encoder.beginRenderPass({
36
+ label: this.label,
37
+ colorAttachments: [
38
+ {
39
+ view: this.targets[i],
40
+ loadOp: 'clear',
41
+ storeOp: 'store',
42
+ clearValue: [0, 0, 0, 0],
43
+ },
44
+ ],
45
+ });
46
+ pass.setPipeline(this.pipeline);
47
+ pass.setBindGroup(0, this.groups[i]);
48
+ pass.draw(3);
49
+ pass.end();
50
+ }
51
+ }
52
+ }
@@ -29,8 +29,19 @@ export interface MipPipelines {
29
29
  * drew it at a seventh of its authored size. Writing the blit twice would be two statements of one
30
30
  * thing, and the second copy is the one that would quietly disagree about the format.
31
31
  */
32
- export declare function generateMipChain(device: GPUDevice, pipelines: MipPipelines, texture: GPUTexture, format: GPUTextureFormat, levels: number): void;
33
- /** An image uploaded once and bound per material. */
32
+ export declare function generateMipChain(device: GPUDevice, pipelines: MipPipelines, texture: GPUTexture, format: GPUTextureFormat, levels: number,
33
+ /** How many array layers to fill, each its own chain. One for a plain texture. */
34
+ layers?: number): void;
35
+ /**
36
+ * Cached per format, because the two differ: an `-srgb` target encodes on write, and blitting a
37
+ * linear chain through a pipeline declared for the other one is a gamma error per level.
38
+ */
39
+ export declare function mipBlitPipeline(device: GPUDevice, pipelines: MipPipelines, format: GPUTextureFormat): GPURenderPipeline;
40
+ /**
41
+ * An image uploaded once and bound per material — or several of one size, as an array's layers.
42
+ * Every surface texture is a `2d-array` view, a plain image an array of one; `surfaceTexture.ts`
43
+ * says why.
44
+ */
34
45
  export declare class GpuSurfaceTexture {
35
46
  private readonly device;
36
47
  private readonly pipelines;
@@ -42,7 +53,15 @@ export declare class GpuSurfaceTexture {
42
53
  private levels;
43
54
  private width;
44
55
  private height;
45
- constructor(device: GPUDevice, pipelines: PipelineCache, source: TexImageSource, options?: SurfaceTextureOptions);
56
+ /** How many images the array holds. */
57
+ readonly layers: number;
58
+ /** The per-layer effects table, or null for a texture given none. See `surfaceEffects.ts`. */
59
+ private effectsTable;
60
+ /** Its view, which the flat group binds beside this image when it is a material's albedo. */
61
+ effectsView: GPUTextureView | null;
62
+ constructor(device: GPUDevice, pipelines: PipelineCache, source: TexImageSource | readonly TexImageSource[], options?: SurfaceTextureOptions);
63
+ /** A view of every layer, as the shaders' `texture_2d_array` bindings read it. */
64
+ private arrayView;
46
65
  /** The view every binding of this image holds. A new one when `update` changes the size. */
47
66
  get view(): GPUTextureView;
48
67
  /**
@@ -1,3 +1,6 @@
1
+ import { SURFACE_EFFECT_TEXELS, packSurfaceEffects } from '../../surfaceEffects.js';
2
+ import { toHalfFloats } from '../../halfFloat.js';
3
+ import { isSourceList, layerSize, refuseArrayUpdate, sourceSize } from '../../textureSource.js';
1
4
  import { shaderModule } from './shaderModules.js';
2
5
  /**
3
6
  * A caller's image on the device, and the mip chain WebGPU will not build for it.
@@ -65,7 +68,9 @@ export function mipLevelCount(width, height) {
65
68
  * drew it at a seventh of its authored size. Writing the blit twice would be two statements of one
66
69
  * thing, and the second copy is the one that would quietly disagree about the format.
67
70
  */
68
- export function generateMipChain(device, pipelines, texture, format, levels) {
71
+ export function generateMipChain(device, pipelines, texture, format, levels,
72
+ /** How many array layers to fill, each its own chain. One for a plain texture. */
73
+ layers = 1) {
69
74
  if (levels < 2)
70
75
  return;
71
76
  const pipeline = mipBlitPipeline(device, pipelines, format);
@@ -75,33 +80,37 @@ export function generateMipChain(device, pipelines, texture, format, levels) {
75
80
  minFilter: 'linear',
76
81
  });
77
82
  const encoder = device.createCommandEncoder({ label: 'mip.chain' });
78
- for (let level = 1; level < levels; level++) {
79
- const source = texture.createView({ baseMipLevel: level - 1, mipLevelCount: 1 });
80
- const target = texture.createView({ baseMipLevel: level, mipLevelCount: 1 });
81
- const pass = encoder.beginRenderPass({
82
- label: `mip.level${level}`,
83
- colorAttachments: [
84
- { view: target, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
85
- ],
86
- });
87
- pass.setPipeline(pipeline);
88
- pass.setBindGroup(0, device.createBindGroup({
89
- layout: pipeline.getBindGroupLayout(0),
90
- entries: [
91
- { binding: 0, resource: source },
92
- { binding: 1, resource: sampler },
93
- ],
94
- }));
95
- pass.draw(3);
96
- pass.end();
97
- }
83
+ /* One level of one layer at a time: a render target is a single 2D subresource, and the source
84
+ view is one level of the same layer, so no pass reads what it writes. */
85
+ for (let layer = 0; layer < layers; layer++)
86
+ for (let level = 1; level < levels; level++) {
87
+ const one = { dimension: '2d', baseArrayLayer: layer, arrayLayerCount: 1 };
88
+ const source = texture.createView({ ...one, baseMipLevel: level - 1, mipLevelCount: 1 });
89
+ const target = texture.createView({ ...one, baseMipLevel: level, mipLevelCount: 1 });
90
+ const pass = encoder.beginRenderPass({
91
+ label: `mip.level${level}`,
92
+ colorAttachments: [
93
+ { view: target, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
94
+ ],
95
+ });
96
+ pass.setPipeline(pipeline);
97
+ pass.setBindGroup(0, device.createBindGroup({
98
+ layout: pipeline.getBindGroupLayout(0),
99
+ entries: [
100
+ { binding: 0, resource: source },
101
+ { binding: 1, resource: sampler },
102
+ ],
103
+ }));
104
+ pass.draw(3);
105
+ pass.end();
106
+ }
98
107
  device.queue.submit([encoder.finish()]);
99
108
  }
100
109
  /**
101
110
  * Cached per format, because the two differ: an `-srgb` target encodes on write, and blitting a
102
111
  * linear chain through a pipeline declared for the other one is a gamma error per level.
103
112
  */
104
- function mipBlitPipeline(device, pipelines, format) {
113
+ export function mipBlitPipeline(device, pipelines, format) {
105
114
  return pipelines.get(`surface.mip:${format}`, () => ({
106
115
  label: `surface.mip:${format}`,
107
116
  layout: 'auto',
@@ -117,13 +126,11 @@ function mipBlitPipeline(device, pipelines, format) {
117
126
  primitive: { topology: 'triangle-list' },
118
127
  }));
119
128
  }
120
- function sourceSize(source) {
121
- const candidate = source;
122
- const width = candidate.videoWidth ?? candidate.width ?? 0;
123
- const height = candidate.videoHeight ?? candidate.height ?? 0;
124
- return { width: Math.max(1, width), height: Math.max(1, height) };
125
- }
126
- /** An image uploaded once and bound per material. */
129
+ /**
130
+ * An image uploaded once and bound per material — or several of one size, as an array's layers.
131
+ * Every surface texture is a `2d-array` view, a plain image an array of one; `surfaceTexture.ts`
132
+ * says why.
133
+ */
127
134
  export class GpuSurfaceTexture {
128
135
  device;
129
136
  pipelines;
@@ -135,10 +142,18 @@ export class GpuSurfaceTexture {
135
142
  levels;
136
143
  width;
137
144
  height;
145
+ /** How many images the array holds. */
146
+ layers;
147
+ /** The per-layer effects table, or null for a texture given none. See `surfaceEffects.ts`. */
148
+ effectsTable = null;
149
+ /** Its view, which the flat group binds beside this image when it is a material's albedo. */
150
+ effectsView = null;
138
151
  constructor(device, pipelines, source, options = {}) {
139
152
  this.device = device;
140
153
  this.pipelines = pipelines;
141
- const { width, height } = sourceSize(source);
154
+ const sources = isSourceList(source) ? source : [source];
155
+ const { width, height } = layerSize(sources);
156
+ this.layers = sources.length;
142
157
  this.mipmapped = options.mipmap ?? true;
143
158
  /* `SRGB8_ALPHA8`'s equivalent. Decoded in the sampler, before filtering, which is the only
144
159
  place it is correct — `surfaceTexture.ts` makes the argument in full. */
@@ -147,7 +162,7 @@ export class GpuSurfaceTexture {
147
162
  this.height = height;
148
163
  this.levels = this.mipmapped ? mipLevelCount(width, height) : 1;
149
164
  this.texture = this.allocate();
150
- this.current = this.texture.createView();
165
+ this.current = this.arrayView(this.texture);
151
166
  const wrap = (options.wrap ?? 'repeat') === 'repeat' ? 'repeat' : 'clamp-to-edge';
152
167
  /*
153
168
  * Nearest where the caller said their pixels are the subject. `mipmapFilter` stays as it was:
@@ -178,7 +193,25 @@ export class GpuSurfaceTexture {
178
193
  ? Math.max(1, Math.floor(options.anisotropy ?? DEFAULT_ANISOTROPY))
179
194
  : 1,
180
195
  });
181
- this.upload(source);
196
+ this.upload(sources);
197
+ /* Half floats, as `renderer.ts` uploads it: filterable, so the ordinary float layout takes it,
198
+ and read by `textureLoad` alone, so the filter never runs. */
199
+ const effects = options.effects ?? [];
200
+ if (effects.length > 0) {
201
+ const table = device.createTexture({
202
+ label: 'surface.effects',
203
+ size: [SURFACE_EFFECT_TEXELS, effects.length],
204
+ format: 'rgba16float',
205
+ usage: 0x4 | 0x2, // TEXTURE_BINDING | COPY_DST
206
+ });
207
+ device.queue.writeTexture({ texture: table }, toHalfFloats(packSurfaceEffects(effects)), { bytesPerRow: SURFACE_EFFECT_TEXELS * 8, rowsPerImage: effects.length }, { width: SURFACE_EFFECT_TEXELS, height: effects.length, depthOrArrayLayers: 1 });
208
+ this.effectsTable = table;
209
+ this.effectsView = table.createView();
210
+ }
211
+ }
212
+ /** A view of every layer, as the shaders' `texture_2d_array` bindings read it. */
213
+ arrayView(texture) {
214
+ return texture.createView({ dimension: '2d-array', arrayLayerCount: this.layers });
182
215
  }
183
216
  /** The view every binding of this image holds. A new one when `update` changes the size. */
184
217
  get view() {
@@ -202,23 +235,24 @@ export class GpuSurfaceTexture {
202
235
  const replaced = this.texture;
203
236
  if (replaced === null)
204
237
  return null;
238
+ refuseArrayUpdate(this.layers);
205
239
  const { width, height } = sourceSize(source);
206
240
  if (width === this.width && height === this.height) {
207
- this.upload(source);
241
+ this.upload([source]);
208
242
  return null;
209
243
  }
210
244
  this.width = width;
211
245
  this.height = height;
212
246
  this.levels = this.mipmapped ? mipLevelCount(width, height) : 1;
213
247
  this.texture = this.allocate();
214
- this.current = this.texture.createView();
215
- this.upload(source);
248
+ this.current = this.arrayView(this.texture);
249
+ this.upload([source]);
216
250
  return replaced;
217
251
  }
218
252
  allocate() {
219
253
  return this.device.createTexture({
220
254
  label: 'surface.texture',
221
- size: [this.width, this.height],
255
+ size: [this.width, this.height, this.layers],
222
256
  format: this.format,
223
257
  mipLevelCount: this.levels,
224
258
  /*
@@ -243,8 +277,11 @@ export class GpuSurfaceTexture {
243
277
  return;
244
278
  this.texture.destroy();
245
279
  this.texture = null;
280
+ this.effectsTable?.destroy();
281
+ this.effectsTable = null;
282
+ this.effectsView = null;
246
283
  }
247
- upload(source) {
284
+ upload(sources) {
248
285
  const texture = this.texture;
249
286
  if (texture === null)
250
287
  return;
@@ -259,7 +296,9 @@ export class GpuSurfaceTexture {
259
296
  * The orientation check that passed this backend was run on a canvas alone, where the two
260
297
  * agreed. Measure both source types, or a texture pipeline is half tested.
261
298
  */
262
- this.device.queue.copyExternalImageToTexture({ source: source, flipY: false }, { texture }, [this.width, this.height]);
299
+ for (let layer = 0; layer < sources.length; layer++) {
300
+ this.device.queue.copyExternalImageToTexture({ source: sources[layer], flipY: false }, { texture, origin: [0, 0, layer] }, [this.width, this.height]);
301
+ }
263
302
  if (this.levels > 1)
264
303
  this.generateMips(texture);
265
304
  }
@@ -271,6 +310,6 @@ export class GpuSurfaceTexture {
271
310
  * an attachment and a resource at once — rejected, and rejected silently enough to matter.
272
311
  */
273
312
  generateMips(texture) {
274
- generateMipChain(this.device, this.pipelines, texture, this.format, this.levels);
313
+ generateMipChain(this.device, this.pipelines, texture, this.format, this.levels, this.layers);
275
314
  }
276
315
  }
@@ -32,3 +32,10 @@ export interface FieldLight extends LightReach {
32
32
  * `distance` is the scene's distance field, or null for no occlusion.
33
33
  */
34
34
  export declare function bakeBrick(layout: LightFieldLayout, brick: number, lights: readonly FieldLight[], falloff: PointLightFalloff, distance: DistanceAt | null, light: Float32Array, direction: Float32Array): void;
35
+ /**
36
+ * One sample: the light arriving at (px, py, pz) from `lights[candidates[first..last)]`, written at
37
+ * `texel` in `light` (colour, then 1 for a sample and 0 for one inside solid geometry) and in
38
+ * `direction` (the brightness-weighted way it arrives from). Shared by every bake, so a brick and a
39
+ * dense volume cannot come to disagree about what a sample is.
40
+ */
41
+ export declare function sampleFieldLight(px: number, py: number, pz: number, lights: readonly FieldLight[], candidates: ArrayLike<number>, first: number, last: number, falloff: PointLightFalloff, distance: DistanceAt | null, light: Float32Array, direction: Float32Array, texel: number): void;
@@ -33,63 +33,68 @@ export function bakeBrick(layout, brick, lights, falloff, distance, light, direc
33
33
  for (let k = 0; k < BRICK_SAMPLES; k++) {
34
34
  for (let j = 0; j < BRICK_SAMPLES; j++) {
35
35
  for (let i = 0; i < BRICK_SAMPLES; i++) {
36
- const texel = (i + BRICK_SAMPLES * (j + BRICK_SAMPLES * k)) * 4;
37
- const px = bx + i * spacing;
38
- const py = by + j * spacing;
39
- const pz = bz + k * spacing;
40
- light.fill(0, texel, texel + 4);
41
- direction.fill(0, texel, texel + 4);
42
- if (distance !== null && distance(px, py, pz) < 0)
43
- continue;
44
- let r = 0;
45
- let g = 0;
46
- let b = 0;
47
- let towardX = 0;
48
- let towardY = 0;
49
- let towardZ = 0;
50
- let weight = 0;
51
- for (let listed = first; listed < last; listed++) {
52
- const source = lights[layout.lights[listed]];
53
- if (source === undefined)
54
- continue;
55
- const dx = source.x - px;
56
- const dy = source.y - py;
57
- const dz = source.z - pz;
58
- const dist = Math.hypot(dx, dy, dz);
59
- if (dist >= source.radius)
60
- continue;
61
- const shape = pointLightShape(dist, source.radius, falloff);
62
- if (shape <= 0)
63
- continue;
64
- const seen = distance === null
65
- ? 1
66
- : softVisibility(distance, px, py, pz, source.x, source.y, source.z, source.sourceRadius);
67
- if (seen <= 0)
68
- continue;
69
- const lr = source.r * shape * seen;
70
- const lg = source.g * shape * seen;
71
- const lb = source.b * shape * seen;
72
- r += lr;
73
- g += lg;
74
- b += lb;
75
- const bright = 0.2126 * lr + 0.7152 * lg + 0.0722 * lb;
76
- if (dist > 1e-6 && bright > 0) {
77
- towardX += (bright * dx) / dist;
78
- towardY += (bright * dy) / dist;
79
- towardZ += (bright * dz) / dist;
80
- weight += bright;
81
- }
82
- }
83
- light[texel] = r;
84
- light[texel + 1] = g;
85
- light[texel + 2] = b;
86
- light[texel + 3] = 1;
87
- if (weight > 0) {
88
- direction[texel] = towardX / weight;
89
- direction[texel + 1] = towardY / weight;
90
- direction[texel + 2] = towardZ / weight;
91
- }
36
+ sampleFieldLight(bx + i * spacing, by + j * spacing, bz + k * spacing, lights, layout.lights, first, last, falloff, distance, light, direction, (i + BRICK_SAMPLES * (j + BRICK_SAMPLES * k)) * 4);
92
37
  }
93
38
  }
94
39
  }
95
40
  }
41
+ /**
42
+ * One sample: the light arriving at (px, py, pz) from `lights[candidates[first..last)]`, written at
43
+ * `texel` in `light` (colour, then 1 for a sample and 0 for one inside solid geometry) and in
44
+ * `direction` (the brightness-weighted way it arrives from). Shared by every bake, so a brick and a
45
+ * dense volume cannot come to disagree about what a sample is.
46
+ */
47
+ export function sampleFieldLight(px, py, pz, lights, candidates, first, last, falloff, distance, light, direction, texel) {
48
+ light.fill(0, texel, texel + 4);
49
+ direction.fill(0, texel, texel + 4);
50
+ if (distance !== null && distance(px, py, pz) < 0)
51
+ return;
52
+ let r = 0;
53
+ let g = 0;
54
+ let b = 0;
55
+ let towardX = 0;
56
+ let towardY = 0;
57
+ let towardZ = 0;
58
+ let weight = 0;
59
+ for (let listed = first; listed < last; listed++) {
60
+ const source = lights[candidates[listed]];
61
+ if (source === undefined)
62
+ continue;
63
+ const dx = source.x - px;
64
+ const dy = source.y - py;
65
+ const dz = source.z - pz;
66
+ const dist = Math.hypot(dx, dy, dz);
67
+ if (dist >= source.radius)
68
+ continue;
69
+ const shape = pointLightShape(dist, source.radius, falloff);
70
+ if (shape <= 0)
71
+ continue;
72
+ const seen = distance === null
73
+ ? 1
74
+ : softVisibility(distance, px, py, pz, source.x, source.y, source.z, source.sourceRadius);
75
+ if (seen <= 0)
76
+ continue;
77
+ const lr = source.r * shape * seen;
78
+ const lg = source.g * shape * seen;
79
+ const lb = source.b * shape * seen;
80
+ r += lr;
81
+ g += lg;
82
+ b += lb;
83
+ const bright = 0.2126 * lr + 0.7152 * lg + 0.0722 * lb;
84
+ if (dist > 1e-6 && bright > 0) {
85
+ towardX += (bright * dx) / dist;
86
+ towardY += (bright * dy) / dist;
87
+ towardZ += (bright * dz) / dist;
88
+ weight += bright;
89
+ }
90
+ }
91
+ light[texel] = r;
92
+ light[texel + 1] = g;
93
+ light[texel + 2] = b;
94
+ light[texel + 3] = 1;
95
+ if (weight > 0) {
96
+ direction[texel] = towardX / weight;
97
+ direction[texel + 1] = towardY / weight;
98
+ direction[texel + 2] = towardZ / weight;
99
+ }
100
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * DriftLight for a whole world: every fixed light summed into one dense grid, baked offline.
3
+ *
4
+ * **Dense, where the courtyard field is sparse, because a city is lit nearly everywhere.** A brick
5
+ * earns its index and its shared faces where light is rare; street lamps reaching thirty metres
6
+ * forty metres apart leave almost no cell dark, so the sparseness buys nothing and the shared
7
+ * faces cost 2.4× the samples. At 8 m a sample, a two-kilometre city 128 m tall is 256 × 16 × 256
8
+ * samples, two half-float texels each: 16 MB on the GPU, and the renderer's lookup is one filtered
9
+ * fetch for the light and one for its direction.
10
+ *
11
+ * **The same sample as every other bake.** `sampleFieldLight` is shared with `bakeBrick`, so a dense
12
+ * volume and a brick agree bit for bit wherever both have a sample. The order a sample's lights are
13
+ * summed in does not have to match for that: the sum runs in double precision and is stored in
14
+ * single, which the last bits of a few terms in another order do not reach.
15
+ *
16
+ * **Lights are bucketed first**, four samples a bucket on every axis, so a sample sums only the
17
+ * lights whose reach touches its bucket rather than all of them: a city of ten thousand lamps is a
18
+ * few dozen candidates a sample. Offline and allocation-free per sample; it may take seconds.
19
+ *
20
+ * What it gives up is resolution: at 8 m a lamp's pool is a few samples across, which is why this
21
+ * stands in only past the exact lights, where a pool is a few dozen pixels.
22
+ */
23
+ import type { FieldLight } from './bake.ts';
24
+ import type { PointLightFalloff } from './falloff.ts';
25
+ import type { DistanceAt } from './visibility.ts';
26
+ /** The most samples along x or z: WebGL2 promises 256 on every axis of a 3D texture. */
27
+ export declare const DENSE_MAX_AXIS = 256;
28
+ /** The most along y, which holds the light and then the direction: two layers in 256. */
29
+ export declare const DENSE_MAX_HEIGHT = 128;
30
+ export interface DenseLightVolume {
31
+ /** The world position of sample (0, 0, 0). */
32
+ readonly origin: readonly [number, number, number];
33
+ /** Metres between two samples. */
34
+ readonly spacing: number;
35
+ /** Samples on each axis. */
36
+ readonly dims: readonly [number, number, number];
37
+ /** Four floats a sample, x fastest then y then z: the colour, then 1 for a sample and 0 inside. */
38
+ readonly light: Float32Array;
39
+ /** Four floats a sample: the direction light arrives from, weighted by brightness, then 0. */
40
+ readonly direction: Float32Array;
41
+ }
42
+ /**
43
+ * Sum `lights` over the box `bounds` (min xyz, max xyz) at `spacing` metres. `distance` occludes, or
44
+ * null for none. The first sample stands on the box's min corner.
45
+ */
46
+ export declare function bakeDenseField(lights: readonly FieldLight[], falloff: PointLightFalloff, distance: DistanceAt | null, bounds: ArrayLike<number>, spacing: number): DenseLightVolume;