@zephyr3d/scene 0.9.26 → 0.10.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 (506) hide show
  1. package/README.md +60 -27
  2. package/dist/animation/animation_bank.js +634 -0
  3. package/dist/animation/animation_bank.js.map +1 -0
  4. package/dist/animation/animation_bank_gpu.js +256 -0
  5. package/dist/animation/animation_bank_gpu.js.map +1 -0
  6. package/dist/animation/animationset.js +146 -30
  7. package/dist/animation/animationset.js.map +1 -1
  8. package/dist/animation/cloth/gpu_cloth_component.js +406 -0
  9. package/dist/animation/cloth/gpu_cloth_component.js.map +1 -0
  10. package/dist/animation/cloth/gpu_cloth_system.js +332 -202
  11. package/dist/animation/cloth/gpu_cloth_system.js.map +1 -1
  12. package/dist/animation/gpu_animation_driver.js +289 -0
  13. package/dist/animation/gpu_animation_driver.js.map +1 -0
  14. package/dist/animation/joint_dynamics/collision.js +112 -1
  15. package/dist/animation/joint_dynamics/collision.js.map +1 -1
  16. package/dist/animation/joint_dynamics/controller.js +6 -0
  17. package/dist/animation/joint_dynamics/controller.js.map +1 -1
  18. package/dist/animation/joint_dynamics/joint_dynamics_system.js +11 -0
  19. package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -1
  20. package/dist/animation/joint_dynamics/solver.js +50 -12
  21. package/dist/animation/joint_dynamics/solver.js.map +1 -1
  22. package/dist/animation/joint_dynamics/types.js.map +1 -1
  23. package/dist/animation/skeleton.js +300 -56
  24. package/dist/animation/skeleton.js.map +1 -1
  25. package/dist/animation/skin_palette_atlas.js +252 -0
  26. package/dist/animation/skin_palette_atlas.js.map +1 -0
  27. package/dist/animation/spring/multi_chain_spring_system.js +298 -38
  28. package/dist/animation/spring/multi_chain_spring_system.js.map +1 -1
  29. package/dist/animation/spring/spring_chain.js +2 -0
  30. package/dist/animation/spring/spring_chain.js.map +1 -1
  31. package/dist/animation/spring/spring_collider.js +159 -21
  32. package/dist/animation/spring/spring_collider.js.map +1 -1
  33. package/dist/animation/spring/spring_particle.js +1 -0
  34. package/dist/animation/spring/spring_particle.js.map +1 -1
  35. package/dist/animation/spring/spring_solver.js +173 -0
  36. package/dist/animation/spring/spring_solver.js.map +1 -0
  37. package/dist/animation/spring/spring_system.js +249 -24
  38. package/dist/animation/spring/spring_system.js.map +1 -1
  39. package/dist/animation/spring_modifier.js +32 -1
  40. package/dist/animation/spring_modifier.js.map +1 -1
  41. package/dist/app/app.js +26 -0
  42. package/dist/app/app.js.map +1 -1
  43. package/dist/app/engine.js +86 -15
  44. package/dist/app/engine.js.map +1 -1
  45. package/dist/app/gamepad.js +191 -0
  46. package/dist/app/gamepad.js.map +1 -0
  47. package/dist/app/inputactions.js +271 -0
  48. package/dist/app/inputactions.js.map +1 -0
  49. package/dist/app/inputmgr.js +331 -22
  50. package/dist/app/inputmgr.js.map +1 -1
  51. package/dist/app/loading.js +138 -0
  52. package/dist/app/loading.js.map +1 -0
  53. package/dist/app/pipelinecache.js +209 -0
  54. package/dist/app/pipelinecache.js.map +1 -0
  55. package/dist/app/runtimescript.js +25 -0
  56. package/dist/app/runtimescript.js.map +1 -1
  57. package/dist/app/scriptingsystem.js +54 -0
  58. package/dist/app/scriptingsystem.js.map +1 -1
  59. package/dist/app/scriptregistry.js +7 -2
  60. package/dist/app/scriptregistry.js.map +1 -1
  61. package/dist/asset/asset_manifest.js +59 -0
  62. package/dist/asset/asset_manifest.js.map +1 -0
  63. package/dist/asset/asset_preload.js +258 -0
  64. package/dist/asset/asset_preload.js.map +1 -0
  65. package/dist/asset/assetmanager.js +117 -65
  66. package/dist/asset/assetmanager.js.map +1 -1
  67. package/dist/asset/loaders/ktx2/basis/LICENSE +201 -0
  68. package/dist/asset/loaders/ktx2/basis/basis_transcoder.js +2 -0
  69. package/dist/asset/loaders/ktx2/basis/basis_transcoder.wasm +0 -0
  70. package/dist/asset/loaders/ktx2/basis_location.js +17 -0
  71. package/dist/asset/loaders/ktx2/basis_location.js.map +1 -0
  72. package/dist/asset/loaders/ktx2/ktx2_loader.js +88 -0
  73. package/dist/asset/loaders/ktx2/ktx2_loader.js.map +1 -0
  74. package/dist/asset/loaders/ktx2/ktx2_targets.js +118 -0
  75. package/dist/asset/loaders/ktx2/ktx2_targets.js.map +1 -0
  76. package/dist/asset/loaders/ktx2/transcoder_pool.js +259 -0
  77. package/dist/asset/loaders/ktx2/transcoder_pool.js.map +1 -0
  78. package/dist/asset/mesh_lod_builder.js +104 -0
  79. package/dist/asset/mesh_lod_builder.js.map +1 -0
  80. package/dist/asset/mesh_settings.js +111 -0
  81. package/dist/asset/mesh_settings.js.map +1 -0
  82. package/dist/asset/meshopt_decoder.js +18 -0
  83. package/dist/asset/meshopt_decoder.js.map +1 -0
  84. package/dist/asset/meshopt_simplifier.js +26 -0
  85. package/dist/asset/meshopt_simplifier.js.map +1 -0
  86. package/dist/asset/model.js +177 -3
  87. package/dist/asset/model.js.map +1 -1
  88. package/dist/asset/texture_settings.js +231 -0
  89. package/dist/asset/texture_settings.js.map +1 -0
  90. package/dist/asset/zmsh_binary.js +131 -0
  91. package/dist/asset/zmsh_binary.js.map +1 -0
  92. package/dist/asset/zsdf_binary.js +111 -0
  93. package/dist/asset/zsdf_binary.js.map +1 -0
  94. package/dist/blitter/bilateralblur.js +1 -2
  95. package/dist/blitter/bilateralblur.js.map +1 -1
  96. package/dist/blitter/gaussianblur.js +1 -6
  97. package/dist/blitter/gaussianblur.js.map +1 -1
  98. package/dist/camera/base.js +31 -10
  99. package/dist/camera/base.js.map +1 -1
  100. package/dist/camera/camera.js +178 -200
  101. package/dist/camera/camera.js.map +1 -1
  102. package/dist/camera/follow.js +342 -0
  103. package/dist/camera/follow.js.map +1 -0
  104. package/dist/camera/fps.js +9 -7
  105. package/dist/camera/fps.js.map +1 -1
  106. package/dist/camera/orbit.js +46 -19
  107. package/dist/camera/orbit.js.map +1 -1
  108. package/dist/index.d.ts +30475 -19865
  109. package/dist/index.js +67 -12
  110. package/dist/index.js.map +1 -1
  111. package/dist/material/blinn.js +3 -0
  112. package/dist/material/blinn.js.map +1 -1
  113. package/dist/material/eye.js +1 -1
  114. package/dist/material/eye.js.map +1 -1
  115. package/dist/material/hair.js +2 -3
  116. package/dist/material/hair.js.map +1 -1
  117. package/dist/material/hairstrand.js +235 -235
  118. package/dist/material/hairstrand.js.map +1 -1
  119. package/dist/material/lambert.js +4 -1
  120. package/dist/material/lambert.js.map +1 -1
  121. package/dist/material/material.js +362 -300
  122. package/dist/material/material.js.map +1 -1
  123. package/dist/material/meshmaterial.js +118 -45
  124. package/dist/material/meshmaterial.js.map +1 -1
  125. package/dist/material/mixins/lightmodel/blinnphong.js +1 -1
  126. package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
  127. package/dist/material/mixins/lightmodel/lambert.js +1 -1
  128. package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
  129. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +34 -61
  130. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
  131. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +16 -2
  132. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
  133. package/dist/material/mixins/lightmodel/pbrspecularglossness.js +2 -1
  134. package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
  135. package/dist/material/mixins/lit.js +223 -83
  136. package/dist/material/mixins/lit.js.map +1 -1
  137. package/dist/material/mixins/pbr/common.js +31 -206
  138. package/dist/material/mixins/pbr/common.js.map +1 -1
  139. package/dist/material/mtoon.js +1 -1
  140. package/dist/material/mtoon.js.map +1 -1
  141. package/dist/material/pbrmr.js +3 -0
  142. package/dist/material/pbrmr.js.map +1 -1
  143. package/dist/material/pbrsg.js +3 -0
  144. package/dist/material/pbrsg.js.map +1 -1
  145. package/dist/material/sdf_debug.js +78 -0
  146. package/dist/material/sdf_debug.js.map +1 -0
  147. package/dist/material/shader/helper.js +733 -190
  148. package/dist/material/shader/helper.js.map +1 -1
  149. package/dist/material/shader/terrain_blend.js +204 -43
  150. package/dist/material/shader/terrain_blend.js.map +1 -1
  151. package/dist/material/skin.js +331 -297
  152. package/dist/material/skin.js.map +1 -1
  153. package/dist/material/skinprofile.js +1058 -0
  154. package/dist/material/skinprofile.js.map +1 -0
  155. package/dist/material/sss.js +353 -0
  156. package/dist/material/sss.js.map +1 -0
  157. package/dist/material/sss_kernel.js +265 -0
  158. package/dist/material/sss_kernel.js.map +1 -0
  159. package/dist/material/sssprofile.js +879 -0
  160. package/dist/material/sssprofile.js.map +1 -0
  161. package/dist/material/subsurfaceprofile.js +4 -8
  162. package/dist/material/subsurfaceprofile.js.map +1 -1
  163. package/dist/material/terrain-cm.js +464 -188
  164. package/dist/material/terrain-cm.js.map +1 -1
  165. package/dist/material/unlit.js +3 -0
  166. package/dist/material/unlit.js.map +1 -1
  167. package/dist/material/water.js +2009 -201
  168. package/dist/material/water.js.map +1 -1
  169. package/dist/node_modules/meshoptimizer/LICENSE +21 -0
  170. package/dist/node_modules/meshoptimizer/meshopt_decoder.js +197 -0
  171. package/dist/node_modules/meshoptimizer/meshopt_decoder.js.map +1 -0
  172. package/dist/node_modules/meshoptimizer/meshopt_simplifier.js +677 -0
  173. package/dist/node_modules/meshoptimizer/meshopt_simplifier.js.map +1 -0
  174. package/dist/physics/character.js +226 -0
  175. package/dist/physics/character.js.map +1 -0
  176. package/dist/physics/collider.js +209 -0
  177. package/dist/physics/collider.js.map +1 -0
  178. package/dist/physics/events.js +79 -0
  179. package/dist/physics/events.js.map +1 -0
  180. package/dist/physics/joint.js +272 -0
  181. package/dist/physics/joint.js.map +1 -0
  182. package/dist/physics/node_physics.js +234 -0
  183. package/dist/physics/node_physics.js.map +1 -0
  184. package/dist/physics/part.js +91 -0
  185. package/dist/physics/part.js.map +1 -0
  186. package/dist/physics/rigid_body.js +379 -0
  187. package/dist/physics/rigid_body.js.map +1 -0
  188. package/dist/physics/vehicle.js +335 -0
  189. package/dist/physics/vehicle.js.map +1 -0
  190. package/dist/physics/world.js +25 -0
  191. package/dist/physics/world.js.map +1 -0
  192. package/dist/posteffect/bloom.js +49 -14
  193. package/dist/posteffect/bloom.js.map +1 -1
  194. package/dist/posteffect/motionblur.js +668 -63
  195. package/dist/posteffect/motionblur.js.map +1 -1
  196. package/dist/posteffect/posteffect.js +12 -1
  197. package/dist/posteffect/posteffect.js.map +1 -1
  198. package/dist/posteffect/postsss.js +572 -0
  199. package/dist/posteffect/postsss.js.map +1 -0
  200. package/dist/posteffect/sao.js +48 -50
  201. package/dist/posteffect/sao.js.map +1 -1
  202. package/dist/posteffect/skinsss.js +605 -355
  203. package/dist/posteffect/skinsss.js.map +1 -1
  204. package/dist/posteffect/ssgi.js +91 -162
  205. package/dist/posteffect/ssgi.js.map +1 -1
  206. package/dist/posteffect/ssr.js +3 -11
  207. package/dist/posteffect/ssr.js.map +1 -1
  208. package/dist/posteffect/sss.js +34 -15
  209. package/dist/posteffect/sss.js.map +1 -1
  210. package/dist/posteffect/taa.js +1 -0
  211. package/dist/posteffect/taa.js.map +1 -1
  212. package/dist/render/abuffer_oit.js +40 -39
  213. package/dist/render/abuffer_oit.js.map +1 -1
  214. package/dist/render/clipmap.js +328 -62
  215. package/dist/render/clipmap.js.map +1 -1
  216. package/dist/render/cluster_light.js +958 -224
  217. package/dist/render/cluster_light.js.map +1 -1
  218. package/dist/render/cull_visitor.js +26 -5
  219. package/dist/render/cull_visitor.js.map +1 -1
  220. package/dist/render/debug_views.js +37 -0
  221. package/dist/render/debug_views.js.map +1 -0
  222. package/dist/render/depthpass.js +5 -1
  223. package/dist/render/depthpass.js.map +1 -1
  224. package/dist/render/drawable_mixin.js +53 -6
  225. package/dist/render/drawable_mixin.js.map +1 -1
  226. package/dist/render/dualdepthpeeling_oit.js +53 -53
  227. package/dist/render/dualdepthpeeling_oit.js.map +1 -1
  228. package/dist/render/envlight.js +196 -113
  229. package/dist/render/envlight.js.map +1 -1
  230. package/dist/render/fbm_wavegenerator.js +8 -13
  231. package/dist/render/fbm_wavegenerator.js.map +1 -1
  232. package/dist/render/fft_wavegenerator.js +268 -136
  233. package/dist/render/fft_wavegenerator.js.map +1 -1
  234. package/dist/render/gerstner_wavegenerator.js +113 -28
  235. package/dist/render/gerstner_wavegenerator.js.map +1 -1
  236. package/dist/render/gi/probe_volume.js +1334 -0
  237. package/dist/render/gi/probe_volume.js.map +1 -0
  238. package/dist/render/gi/scene_gi.js +282 -0
  239. package/dist/render/gi/scene_gi.js.map +1 -0
  240. package/dist/render/gi/voxel_atlas.js +198 -0
  241. package/dist/render/gi/voxel_atlas.js.map +1 -0
  242. package/dist/render/gi/voxel_lighting.js +652 -0
  243. package/dist/render/gi/voxel_lighting.js.map +1 -0
  244. package/dist/render/gi/voxel_scene.js +642 -0
  245. package/dist/render/gi/voxel_scene.js.map +1 -0
  246. package/dist/render/gi/voxelize_pass.js +223 -0
  247. package/dist/render/gi/voxelize_pass.js.map +1 -0
  248. package/dist/render/globalbindgroup_allocator.js +3 -2
  249. package/dist/render/globalbindgroup_allocator.js.map +1 -1
  250. package/dist/render/gpu_picking.js +9 -36
  251. package/dist/render/gpu_picking.js.map +1 -1
  252. package/dist/render/hzb.js +80 -22
  253. package/dist/render/hzb.js.map +1 -1
  254. package/dist/render/instance_culling.js +499 -0
  255. package/dist/render/instance_culling.js.map +1 -0
  256. package/dist/render/lightpass.js +23 -9
  257. package/dist/render/lightpass.js.map +1 -1
  258. package/dist/render/lod.js +46 -0
  259. package/dist/render/lod.js.map +1 -0
  260. package/dist/render/oit.js +11 -11
  261. package/dist/render/oit.js.map +1 -1
  262. package/dist/render/primitive.js +301 -9
  263. package/dist/render/primitive.js.map +1 -1
  264. package/dist/render/primitive_readback.js +209 -0
  265. package/dist/render/primitive_readback.js.map +1 -0
  266. package/dist/render/render_queue.js +198 -11
  267. package/dist/render/render_queue.js.map +1 -1
  268. package/dist/render/renderer.js +14 -7
  269. package/dist/render/renderer.js.map +1 -1
  270. package/dist/render/rendergraph/blackboard.js +15 -1
  271. package/dist/render/rendergraph/blackboard.js.map +1 -1
  272. package/dist/render/rendergraph/executor.js +136 -0
  273. package/dist/render/rendergraph/executor.js.map +1 -1
  274. package/dist/render/rendergraph/forward_plus_builder.js +585 -59
  275. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  276. package/dist/render/rendergraph/frame_resource_requirements.js +1 -0
  277. package/dist/render/rendergraph/frame_resource_requirements.js.map +1 -1
  278. package/dist/render/rendergraph/history_resources.js +8 -1
  279. package/dist/render/rendergraph/history_resources.js.map +1 -1
  280. package/dist/render/rendergraph/scene_render_context.js +31 -2
  281. package/dist/render/rendergraph/scene_render_context.js.map +1 -1
  282. package/dist/render/rendergraph/types.js.map +1 -1
  283. package/dist/render/renderpass.js +28 -5
  284. package/dist/render/renderpass.js.map +1 -1
  285. package/dist/render/sdf/box_allocator.js +98 -0
  286. package/dist/render/sdf/box_allocator.js.map +1 -0
  287. package/dist/render/sdf/global_sdf.js +865 -0
  288. package/dist/render/sdf/global_sdf.js.map +1 -0
  289. package/dist/render/sdf/scene_sdf.js +562 -0
  290. package/dist/render/sdf/scene_sdf.js.map +1 -0
  291. package/dist/render/sdf/sdf_atlas.js +227 -0
  292. package/dist/render/sdf/sdf_atlas.js.map +1 -0
  293. package/dist/render/sdf/sdf_programs.js +42 -0
  294. package/dist/render/sdf/sdf_programs.js.map +1 -0
  295. package/dist/render/sdf/sdf_view.js +199 -0
  296. package/dist/render/sdf/sdf_view.js.map +1 -0
  297. package/dist/render/sdf/waterline.js +610 -0
  298. package/dist/render/sdf/waterline.js.map +1 -0
  299. package/dist/render/shadow_mask_pass.js +142 -25
  300. package/dist/render/shadow_mask_pass.js.map +1 -1
  301. package/dist/render/shore_wave_generator.js +1193 -0
  302. package/dist/render/shore_wave_generator.js.map +1 -0
  303. package/dist/render/sky.js +516 -86
  304. package/dist/render/sky.js.map +1 -1
  305. package/dist/render/transmission_thickness_pass.js +477 -0
  306. package/dist/render/transmission_thickness_pass.js.map +1 -0
  307. package/dist/render/underwater.js +524 -0
  308. package/dist/render/underwater.js.map +1 -0
  309. package/dist/render/virtualtexture/virtual_texture.js +1214 -0
  310. package/dist/render/virtualtexture/virtual_texture.js.map +1 -0
  311. package/dist/render/volumetric_cloud/cloud_common.js +472 -0
  312. package/dist/render/volumetric_cloud/cloud_common.js.map +1 -0
  313. package/dist/render/volumetric_cloud/cloud_noise.js +189 -0
  314. package/dist/render/volumetric_cloud/cloud_noise.js.map +1 -0
  315. package/dist/render/volumetric_cloud/debug_view.js +131 -0
  316. package/dist/render/volumetric_cloud/debug_view.js.map +1 -0
  317. package/dist/render/volumetric_cloud/resources.js +380 -0
  318. package/dist/render/volumetric_cloud/resources.js.map +1 -0
  319. package/dist/render/volumetric_cloud/sky_cache.js +380 -0
  320. package/dist/render/volumetric_cloud/sky_cache.js.map +1 -0
  321. package/dist/render/volumetric_cloud/volumetric_cloud.js +318 -0
  322. package/dist/render/volumetric_cloud/volumetric_cloud.js.map +1 -0
  323. package/dist/render/water_caustics.js +1195 -0
  324. package/dist/render/water_caustics.js.map +1 -0
  325. package/dist/render/water_foam.js +627 -0
  326. package/dist/render/water_foam.js.map +1 -0
  327. package/dist/render/water_interaction.js +1795 -0
  328. package/dist/render/water_interaction.js.map +1 -0
  329. package/dist/render/weightedblended_oit.js +36 -36
  330. package/dist/render/weightedblended_oit.js.map +1 -1
  331. package/dist/scene/basesprite.js +9 -4
  332. package/dist/scene/basesprite.js.map +1 -1
  333. package/dist/scene/batchgroup.js +108 -12
  334. package/dist/scene/batchgroup.js.map +1 -1
  335. package/dist/scene/environment.js +28 -7
  336. package/dist/scene/environment.js.map +1 -1
  337. package/dist/scene/foliage.js +1020 -0
  338. package/dist/scene/foliage.js.map +1 -0
  339. package/dist/scene/foliage_buckets.js +147 -0
  340. package/dist/scene/foliage_buckets.js.map +1 -0
  341. package/dist/scene/foliage_mask.js +358 -0
  342. package/dist/scene/foliage_mask.js.map +1 -0
  343. package/dist/scene/hair.js +15 -4
  344. package/dist/scene/hair.js.map +1 -1
  345. package/dist/scene/light.js +179 -13
  346. package/dist/scene/light.js.map +1 -1
  347. package/dist/scene/mesh.js +327 -45
  348. package/dist/scene/mesh.js.map +1 -1
  349. package/dist/scene/meshdrawable.js +6 -0
  350. package/dist/scene/meshdrawable.js.map +1 -1
  351. package/dist/scene/msdftext.js +9 -3
  352. package/dist/scene/msdftext.js.map +1 -1
  353. package/dist/scene/msdftextsprite.js +9 -3
  354. package/dist/scene/msdftextsprite.js.map +1 -1
  355. package/dist/scene/particlesys.js +8 -2
  356. package/dist/scene/particlesys.js.map +1 -1
  357. package/dist/scene/physics_settings.js +157 -0
  358. package/dist/scene/physics_settings.js.map +1 -0
  359. package/dist/scene/scene.js +190 -44
  360. package/dist/scene/scene.js.map +1 -1
  361. package/dist/scene/scene_node.js +147 -6
  362. package/dist/scene/scene_node.js.map +1 -1
  363. package/dist/scene/terrain-cm/bladegrassmaterial.js +735 -0
  364. package/dist/scene/terrain-cm/bladegrassmaterial.js.map +1 -0
  365. package/dist/scene/terrain-cm/grass.js +881 -42
  366. package/dist/scene/terrain-cm/grass.js.map +1 -1
  367. package/dist/scene/terrain-cm/grass_gpu.js +929 -0
  368. package/dist/scene/terrain-cm/grass_gpu.js.map +1 -0
  369. package/dist/scene/terrain-cm/grassmaterial.js +180 -9
  370. package/dist/scene/terrain-cm/grassmaterial.js.map +1 -1
  371. package/dist/scene/terrain-cm/terrain-cm.js +465 -36
  372. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  373. package/dist/scene/terrain-cm/terrain_gi_albedo.js +257 -0
  374. package/dist/scene/terrain-cm/terrain_gi_albedo.js.map +1 -0
  375. package/dist/scene/terrain-cm/terrain_rvt.js +395 -0
  376. package/dist/scene/terrain-cm/terrain_rvt.js.map +1 -0
  377. package/dist/scene/update_queue.js +54 -0
  378. package/dist/scene/update_queue.js.map +1 -0
  379. package/dist/scene/water.js +997 -96
  380. package/dist/scene/water.js.map +1 -1
  381. package/dist/scene/water_surface_sampler.js +324 -0
  382. package/dist/scene/water_surface_sampler.js.map +1 -0
  383. package/dist/scene/wind.js +317 -0
  384. package/dist/scene/wind.js.map +1 -0
  385. package/dist/shaders/atmosphere.js +388 -166
  386. package/dist/shaders/atmosphere.js.map +1 -1
  387. package/dist/shaders/fog.js +100 -32
  388. package/dist/shaders/fog.js.map +1 -1
  389. package/dist/shaders/gi.js +365 -0
  390. package/dist/shaders/gi.js.map +1 -0
  391. package/dist/shaders/ltc_rect.js +202 -0
  392. package/dist/shaders/ltc_rect.js.map +1 -0
  393. package/dist/shaders/misc.js +1 -10
  394. package/dist/shaders/misc.js.map +1 -1
  395. package/dist/shaders/noise.js +5 -1
  396. package/dist/shaders/noise.js.map +1 -1
  397. package/dist/shaders/probes.js +309 -0
  398. package/dist/shaders/probes.js.map +1 -0
  399. package/dist/shaders/sdf.js +537 -0
  400. package/dist/shaders/sdf.js.map +1 -0
  401. package/dist/shaders/shadow.js +104 -70
  402. package/dist/shaders/shadow.js.map +1 -1
  403. package/dist/shaders/skin_brdf.js +243 -0
  404. package/dist/shaders/skin_brdf.js.map +1 -0
  405. package/dist/shaders/ssr.js +6 -159
  406. package/dist/shaders/ssr.js.map +1 -1
  407. package/dist/shaders/temporal.js +38 -27
  408. package/dist/shaders/temporal.js.map +1 -1
  409. package/dist/shaders/water.js +40 -87
  410. package/dist/shaders/water.js.map +1 -1
  411. package/dist/shaders/water_medium.js +42 -0
  412. package/dist/shaders/water_medium.js.map +1 -0
  413. package/dist/shadow/dom.js +110 -19
  414. package/dist/shadow/dom.js.map +1 -1
  415. package/dist/shadow/esm.js +3 -3
  416. package/dist/shadow/esm.js.map +1 -1
  417. package/dist/shadow/pcf_opt.js +15 -48
  418. package/dist/shadow/pcf_opt.js.map +1 -1
  419. package/dist/shadow/pcf_pd.js +15 -53
  420. package/dist/shadow/pcf_pd.js.map +1 -1
  421. package/dist/shadow/pcss.js +34 -9
  422. package/dist/shadow/pcss.js.map +1 -1
  423. package/dist/shadow/shadow_impl.js +3 -8
  424. package/dist/shadow/shadow_impl.js.map +1 -1
  425. package/dist/shadow/shadow_region.js +87 -49
  426. package/dist/shadow/shadow_region.js.map +1 -1
  427. package/dist/shadow/shadowmapper.js +93 -53
  428. package/dist/shadow/shadowmapper.js.map +1 -1
  429. package/dist/shadow/ssm.js +18 -72
  430. package/dist/shadow/ssm.js.map +1 -1
  431. package/dist/shadow/vsm.js +3 -3
  432. package/dist/shadow/vsm.js.map +1 -1
  433. package/dist/shapes/shape.js +44 -1
  434. package/dist/shapes/shape.js.map +1 -1
  435. package/dist/utility/buoyancy.js +287 -0
  436. package/dist/utility/buoyancy.js.map +1 -0
  437. package/dist/utility/physical.js +10 -1
  438. package/dist/utility/physical.js.map +1 -1
  439. package/dist/utility/pmrem.js +17 -43
  440. package/dist/utility/pmrem.js.map +1 -1
  441. package/dist/utility/sdf/bake.js +435 -0
  442. package/dist/utility/sdf/bake.js.map +1 -0
  443. package/dist/utility/sdf/bvh.js +576 -0
  444. package/dist/utility/sdf/bvh.js.map +1 -0
  445. package/dist/utility/serialization/manager.js +36 -6
  446. package/dist/utility/serialization/manager.js.map +1 -1
  447. package/dist/utility/serialization/scene/animation.js +77 -7
  448. package/dist/utility/serialization/scene/animation.js.map +1 -1
  449. package/dist/utility/serialization/scene/batch.js +11 -4
  450. package/dist/utility/serialization/scene/batch.js.map +1 -1
  451. package/dist/utility/serialization/scene/camera.js +140 -279
  452. package/dist/utility/serialization/scene/camera.js.map +1 -1
  453. package/dist/utility/serialization/scene/cloth.js +25 -0
  454. package/dist/utility/serialization/scene/cloth.js.map +1 -0
  455. package/dist/utility/serialization/scene/common.js +14 -6
  456. package/dist/utility/serialization/scene/common.js.map +1 -1
  457. package/dist/utility/serialization/scene/foliage.js +821 -0
  458. package/dist/utility/serialization/scene/foliage.js.map +1 -0
  459. package/dist/utility/serialization/scene/hair.js +11 -4
  460. package/dist/utility/serialization/scene/hair.js.map +1 -1
  461. package/dist/utility/serialization/scene/light.js +115 -7
  462. package/dist/utility/serialization/scene/light.js.map +1 -1
  463. package/dist/utility/serialization/scene/material.js +388 -220
  464. package/dist/utility/serialization/scene/material.js.map +1 -1
  465. package/dist/utility/serialization/scene/mesh.js +69 -4
  466. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  467. package/dist/utility/serialization/scene/node.js +140 -5
  468. package/dist/utility/serialization/scene/node.js.map +1 -1
  469. package/dist/utility/serialization/scene/node_physics.js +1318 -0
  470. package/dist/utility/serialization/scene/node_physics.js.map +1 -0
  471. package/dist/utility/serialization/scene/particle.js +11 -4
  472. package/dist/utility/serialization/scene/particle.js.map +1 -1
  473. package/dist/utility/serialization/scene/physics.js +120 -0
  474. package/dist/utility/serialization/scene/physics.js.map +1 -0
  475. package/dist/utility/serialization/scene/primitive.js +124 -10
  476. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  477. package/dist/utility/serialization/scene/scene.js +1687 -51
  478. package/dist/utility/serialization/scene/scene.js.map +1 -1
  479. package/dist/utility/serialization/scene/sprite.js +11 -4
  480. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  481. package/dist/utility/serialization/scene/terrain.js +163 -33
  482. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  483. package/dist/utility/serialization/scene/text.js +11 -4
  484. package/dist/utility/serialization/scene/text.js.map +1 -1
  485. package/dist/utility/serialization/scene/water.js +2473 -114
  486. package/dist/utility/serialization/scene/water.js.map +1 -1
  487. package/dist/utility/serialization/types.js.map +1 -1
  488. package/dist/utility/shprojector.js +97 -109
  489. package/dist/utility/shprojector.js.map +1 -1
  490. package/dist/utility/textures/foam.js +170 -0
  491. package/dist/utility/textures/foam.js.map +1 -0
  492. package/dist/utility/textures/ggxlut.js +17 -43
  493. package/dist/utility/textures/ggxlut.js.map +1 -1
  494. package/dist/utility/textures/ltcdata.js +4 -4
  495. package/dist/utility/textures/ltcdata.js.map +1 -1
  496. package/dist/utility/textures/ltclut.js +16 -6
  497. package/dist/utility/textures/ltclut.js.map +1 -1
  498. package/dist/utility/unsupported.js +24 -0
  499. package/dist/utility/unsupported.js.map +1 -0
  500. package/dist/values.js +13 -1
  501. package/dist/values.js.map +1 -1
  502. package/package.json +25 -12
  503. package/src/asset/loaders/ktx2/basis/LICENSE +201 -0
  504. package/src/asset/loaders/ktx2/basis/basis_transcoder.js +2 -0
  505. package/src/asset/loaders/ktx2/basis/basis_transcoder.wasm +0 -0
  506. package/src/asset/meshoptimizer/LICENSE +21 -0
@@ -1,41 +1,427 @@
1
1
  import { Vector4, DEPTH_CLEAR_VALUE } from '@zephyr3d/base';
2
- import { MAX_SHADOW_MASK_LIGHTS } from '../values.js';
2
+ import { MAX_GLOBAL_LIGHTS, MAX_SHADOW_MASK_LIGHTS } from '../values.js';
3
+ import { PBStructTypeInfo, PBArrayTypeInfo, PBPrimitiveTypeInfo, PBPrimitiveType } from '@zephyr3d/device';
3
4
  import { ShaderHelper } from '../material/shader/helper.js';
4
5
  import { getDevice } from '../app/api.js';
5
6
 
7
+ /** Name of the index pass's per-light view-space bounding spheres. */ const UNIFORM_NAME_LIGHT_SPHERES = 'lightSpheres';
8
+ /**
9
+ * vec4s per light in the index pass's sphere buffer: the bounding sphere, then the
10
+ * light's brightness in x.
11
+ */ const SPHERE_STRIDE = 2;
12
+ /** Target size in pixels of a cluster tile on screen. */ const TILE_SIZE_PX = 64;
13
+ /** Bound on the tile count along each screen axis, which caps the index pass cost at high resolution. */ const MAX_TILES_PER_AXIS = 64;
14
+ /** Number of depth slices. */ const TILE_COUNT_Z = 32;
15
+ /**
16
+ * View depth in meters where the exponential slices of a perspective camera begin.
17
+ * Everything nearer shares slice 0: exponential slicing would otherwise spend a quarter
18
+ * of the slices on the first meter in front of a 0.1 m near plane, where little is lit.
19
+ */ const NEAR_SLICE_METERS = 1;
20
+ /** Clusters per workgroup of the WebGPU index pass, and lights per shared batch. */ const INDEX_GROUP_SIZE = 64;
21
+ /**
22
+ * Average list length per cluster the WebGPU light list starts out sized for, once there
23
+ * are more lights than this (with fewer, every cluster fits all of them). It then grows
24
+ * to what the clusters asked for, read back from the GPU.
25
+ */ const LIST_LIGHTS_PER_CLUSTER = 64;
26
+ /** Bound on the entries of the WebGPU light list: 64 MB. */ const MAX_LIST_ENTRIES = 1 << 24;
27
+ /**
28
+ * Buckets of the per-cluster histogram of light scores that picks which lights a
29
+ * cluster keeps when its list is cut. Each spans a factor of 4 in score.
30
+ */ const SCORE_BUCKETS = 32;
31
+ /**
32
+ * Declares the uniforms and helper functions shared by the index pass programs.
33
+ */ function declareIndexPassScope(pb, scope, sphereStride, storage) {
34
+ (function() {
35
+ this.invProjMatrix = pb.mat4().uniform(0);
36
+ this.sizeParam = pb.vec4().uniform(0);
37
+ this.countParam = pb.ivec4().uniform(0);
38
+ this.orthoProj = pb.int().uniform(0);
39
+ this.sliceParam = pb.vec4().uniform(0);
40
+ // Already in view space: transformed once per light on the CPU rather than once
41
+ // per light in each of the clusters. See writeLight for the layout.
42
+ this[UNIFORM_NAME_LIGHT_SPHERES] = storage ? pb.vec4[0]().storageBufferReadonly(0) : pb.vec4[(ShaderHelper.getMaxClusterLights() + 1) * sphereStride]().uniformBuffer(0);
43
+ pb.func('lineIntersectionToZPlane', [
44
+ pb.vec3('a'),
45
+ pb.vec3('b'),
46
+ pb.float('zDistance')
47
+ ], function() {
48
+ this.$l.normal = pb.vec3(0, 0, 1);
49
+ this.$l.ab = pb.sub(this.b, this.a);
50
+ this.$l.t = pb.div(pb.sub(this.zDistance, pb.dot(this.normal, this.a)), pb.dot(this.normal, this.ab));
51
+ this.$return(pb.add(this.a, pb.mul(this.t, this.ab)));
52
+ });
53
+ // View depth where perspective slice k begins: slices before the first exponential
54
+ // one form the near slice, which begins at the near plane.
55
+ pb.func('sliceDepth', [
56
+ pb.float('k')
57
+ ], function() {
58
+ this.$if(pb.lessThan(this.k, this.sliceParam.y), function() {
59
+ this.$return(this.sizeParam.z);
60
+ });
61
+ this.$return(pb.mul(this.sliceParam.x, pb.pow(pb.div(this.sizeParam.w, this.sliceParam.x), pb.div(pb.sub(this.k, this.sliceParam.y), this.sliceParam.z))));
62
+ });
63
+ pb.func('clipToView', [
64
+ pb.vec4('clip')
65
+ ], function() {
66
+ this.$l.view = pb.mul(this.invProjMatrix, this.clip);
67
+ this.$return(pb.div(this.view, this.view.w));
68
+ });
69
+ pb.func('screenToView', [
70
+ pb.vec4('screen')
71
+ ], function() {
72
+ this.$l.texCoord = pb.div(this.screen.xy, this.sizeParam.xy);
73
+ this.$l.clip = pb.vec4(pb.sub(pb.mul(pb.vec2(this.texCoord.x, pb.sub(1, this.texCoord.y)), 2), pb.vec2(1)), this.screen.z, this.screen.w);
74
+ this.$return(this.clipToView(this.clip));
75
+ });
76
+ // Squared distance from a point to the box, zero inside it.
77
+ pb.func('aabbDistSq', [
78
+ pb.vec3('p'),
79
+ pb.vec3('aabbMin'),
80
+ pb.vec3('aabbMax')
81
+ ], function() {
82
+ this.$l.d = pb.max(pb.max(pb.sub(this.aabbMin, this.p), pb.sub(this.p, this.aabbMax)), pb.vec3(0));
83
+ this.$return(pb.dot(this.d, this.d));
84
+ });
85
+ // Radius 0 marks an unbounded light that no global slot took: it reaches every cluster.
86
+ pb.func('sphereReachesAABB', [
87
+ pb.vec4('sphere'),
88
+ pb.float('distSq')
89
+ ], function() {
90
+ this.$return(pb.or(pb.equal(this.sphere.w, 0), pb.lessThanEqual(this.distSq, pb.mul(this.sphere.w, this.sphere.w))));
91
+ });
92
+ // Whether the sphere lies entirely outside one of the tile frustum's side planes
93
+ // (inward unit normals through the eye). The box of a far slice is much wider than
94
+ // the frustum it bounds, and this rejects the lights reaching only its corners.
95
+ pb.func('sphereOutsideTile', [
96
+ pb.vec4('sphere'),
97
+ pb.vec3('left'),
98
+ pb.vec3('right'),
99
+ pb.vec3('bottom'),
100
+ pb.vec3('top')
101
+ ], function() {
102
+ this.$l.r = pb.neg(this.sphere.w);
103
+ this.$return(pb.and(pb.greaterThan(this.sphere.w, 0), pb.or(pb.or(pb.lessThan(pb.dot(this.left, this.sphere.xyz), this.r), pb.lessThan(pb.dot(this.right, this.sphere.xyz), this.r)), pb.or(pb.lessThan(pb.dot(this.bottom, this.sphere.xyz), this.r), pb.lessThan(pb.dot(this.top, this.sphere.xyz), this.r)))));
104
+ });
105
+ }).call(scope);
106
+ }
107
+ /**
108
+ * Emits the view-space bounds of cluster `tileIndex` (an int): locals `aabbMin`/`aabbMax`
109
+ * and the inward side-plane normals `planeLeft`/`planeRight`/`planeBottom`/`planeTop`.
110
+ */ function emitClusterBounds(pb, tileIndex) {
111
+ this.$l.tileIndex = tileIndex;
112
+ this.$l.tileSize = pb.div(this.sizeParam.xy, pb.vec2(this.countParam.xy));
113
+ this.$l.zIndex = pb.div(this.tileIndex, pb.mul(this.countParam.x, this.countParam.y));
114
+ this.$l.yIndex = pb.div(pb.sub(this.tileIndex, pb.mul(this.zIndex, this.countParam.x, this.countParam.y)), this.countParam.x);
115
+ this.$l.xIndex = pb.sub(this.tileIndex, pb.add(pb.mul(this.zIndex, this.countParam.x, this.countParam.y), pb.mul(this.yIndex, this.countParam.x)));
116
+ this.$l.maxPoint_sS = pb.vec4(pb.mul(pb.vec2(pb.float(pb.add(this.xIndex, 1)), pb.float(pb.add(this.yIndex, 1))), this.tileSize), 0.0, 1.0);
117
+ this.$l.minPoint_sS = pb.vec4(pb.mul(pb.vec2(pb.float(this.xIndex), pb.float(this.yIndex)), this.tileSize), 0.0, 1.0);
118
+ this.$l.maxPoint_vS = this.screenToView(this.maxPoint_sS).xyz;
119
+ this.$l.minPoint_vS = this.screenToView(this.minPoint_sS).xyz;
120
+ this.$l.sliceNear = pb.div(pb.float(this.zIndex), pb.float(this.countParam.z));
121
+ this.$l.sliceFar = pb.div(pb.add(pb.float(this.zIndex), 1), pb.float(this.countParam.z));
122
+ this.$l.aabbMin = pb.vec3();
123
+ this.$l.aabbMax = pb.vec3();
124
+ // Zero normals never reject: an orthographic tile is exactly its box.
125
+ this.$l.planeLeft = pb.vec3(0);
126
+ this.$l.planeRight = pb.vec3(0);
127
+ this.$l.planeBottom = pb.vec3(0);
128
+ this.$l.planeTop = pb.vec3(0);
129
+ this.$if(pb.notEqual(this.orthoProj, 0), function() {
130
+ // Orthographic: view rays are parallel, so a tile's view-space x/y extent does not
131
+ // depend on depth, and slices are linear because near may be zero or negative.
132
+ this.$l.tileNear = pb.neg(pb.mix(this.sizeParam.z, this.sizeParam.w, this.sliceNear));
133
+ this.$l.tileFar = pb.neg(pb.mix(this.sizeParam.z, this.sizeParam.w, this.sliceFar));
134
+ this.aabbMin = pb.vec3(pb.min(this.minPoint_vS.xy, this.maxPoint_vS.xy), this.tileFar);
135
+ this.aabbMax = pb.vec3(pb.max(this.minPoint_vS.xy, this.maxPoint_vS.xy), this.tileNear);
136
+ }).$else(function() {
137
+ this.$l.tileNear = pb.neg(this.sliceDepth(pb.float(this.zIndex)));
138
+ this.$l.tileFar = pb.neg(this.sliceDepth(pb.add(pb.float(this.zIndex), 1)));
139
+ this.$l.eyePos = pb.vec3(0);
140
+ this.$l.minPointNear = this.lineIntersectionToZPlane(this.eyePos, this.minPoint_vS, this.tileNear);
141
+ this.$l.minPointFar = this.lineIntersectionToZPlane(this.eyePos, this.minPoint_vS, this.tileFar);
142
+ this.$l.maxPointNear = this.lineIntersectionToZPlane(this.eyePos, this.maxPoint_vS, this.tileNear);
143
+ this.$l.maxPointFar = this.lineIntersectionToZPlane(this.eyePos, this.maxPoint_vS, this.tileFar);
144
+ this.aabbMin = pb.min(pb.min(this.minPointNear, this.minPointFar), pb.min(this.maxPointNear, this.maxPointFar));
145
+ this.aabbMax = pb.max(pb.max(this.minPointNear, this.minPointFar), pb.max(this.maxPointNear, this.maxPointFar));
146
+ // Side planes through the eye and the tile edges at z = tileNear (< 0), with
147
+ // normals facing into the tile.
148
+ this.$l.lo = pb.min(this.minPointNear.xy, this.maxPointNear.xy);
149
+ this.$l.hi = pb.max(this.minPointNear.xy, this.maxPointNear.xy);
150
+ this.planeLeft = pb.normalize(pb.vec3(pb.neg(this.tileNear), 0, this.lo.x));
151
+ this.planeRight = pb.normalize(pb.vec3(this.tileNear, 0, pb.neg(this.hi.x)));
152
+ this.planeBottom = pb.normalize(pb.vec3(0, pb.neg(this.tileNear), this.lo.y));
153
+ this.planeTop = pb.normalize(pb.vec3(0, this.tileNear, pb.neg(this.hi.y)));
154
+ });
155
+ }
156
+ /**
157
+ * Emits the state of a cluster keeping its 16 strongest lights. Needs the bounds from
158
+ * emitClusterBounds.
159
+ */ function emitSelectionInit(pb) {
160
+ this.$l.n = pb.int(0);
161
+ this.$l.lightIndex = [
162
+ pb.uint(0),
163
+ pb.uint(0),
164
+ pb.uint(0),
165
+ pb.uint(0),
166
+ pb.uint(0),
167
+ pb.uint(0),
168
+ pb.uint(0),
169
+ pb.uint(0),
170
+ pb.uint(0),
171
+ pb.uint(0),
172
+ pb.uint(0),
173
+ pb.uint(0),
174
+ pb.uint(0),
175
+ pb.uint(0),
176
+ pb.uint(0),
177
+ pb.uint(0)
178
+ ];
179
+ this.$l.lightScore = [
180
+ pb.float(0),
181
+ pb.float(0),
182
+ pb.float(0),
183
+ pb.float(0),
184
+ pb.float(0),
185
+ pb.float(0),
186
+ pb.float(0),
187
+ pb.float(0),
188
+ pb.float(0),
189
+ pb.float(0),
190
+ pb.float(0),
191
+ pb.float(0),
192
+ pb.float(0),
193
+ pb.float(0),
194
+ pb.float(0),
195
+ pb.float(0)
196
+ ];
197
+ // Once all slots are taken, the weakest one is replaced by any light scoring
198
+ // higher, so a crowded cluster keeps the lights that matter most to it rather
199
+ // than whichever happen to come first in the buffer.
200
+ this.$l.minSlot = pb.int(0);
201
+ this.$l.minScore = pb.float(0);
202
+ emitScoreSetup.call(this, pb);
203
+ }
204
+ /** Emits the locals emitLightScore needs. Needs the bounds from emitClusterBounds. */ function emitScoreSetup(pb) {
205
+ // Half the cluster diagonal: the irradiance estimate is taken at the cluster
206
+ // center but not closer than this, so a light inside the cluster stays finite.
207
+ this.$l.halfDiagSq = pb.mul(pb.dot(pb.sub(this.aabbMax, this.aabbMin), pb.sub(this.aabbMax, this.aabbMin)), 0.25);
208
+ this.$l.center = pb.mul(pb.add(this.aabbMin, this.aabbMax), 0.5);
209
+ }
210
+ /**
211
+ * Emits the local `score` of the light in `lightPos`, which emitLightReaches found to
212
+ * reach the cluster: its brightness times the lit shader's range window at the nearest
213
+ * point of the cluster, over the squared distance to the cluster center. Unbounded
214
+ * lights score highest.
215
+ */ function emitLightScore(pb, brightness) {
216
+ this.$l.score = pb.float(3.0e38);
217
+ this.$if(pb.greaterThan(this.lightPos.w, 0), function() {
218
+ this.$l.f = pb.clamp(pb.sub(1, pb.div(this.distSq, pb.mul(this.lightPos.w, this.lightPos.w))), 0, 1);
219
+ this.$l.toCenter = pb.sub(this.center, this.lightPos.xyz);
220
+ this.score = pb.div(pb.mul(brightness, this.f, this.f), pb.max(pb.dot(this.toCenter, this.toCenter), pb.max(this.halfDiagSq, 1e-6)));
221
+ });
222
+ }
223
+ /**
224
+ * Emits the test of the light at buffer index `index` (a uint) with view-space bounding
225
+ * sphere `sphere` and brightness `brightness` against the cluster, keeping it when it
226
+ * reaches the cluster and is among its 16 strongest. Must be emitted inside a loop over
227
+ * the lights: rejecting a light continues it.
228
+ */ /**
229
+ * Emits whether the light whose view-space bounding sphere is in the local `lightPos`
230
+ * reaches the cluster, declaring the local `distSq` (its squared distance to the box).
231
+ * Needs the bounds from emitClusterBounds.
232
+ */ function emitLightReaches(pb) {
233
+ this.$l.distSq = this.aabbDistSq(this.lightPos.xyz, this.aabbMin, this.aabbMax);
234
+ // Negative radius: shaded through a global slot, never through the clusters.
235
+ return pb.and(pb.greaterThanEqual(this.lightPos.w, 0), pb.and(this.sphereReachesAABB(this.lightPos, this.distSq), pb.not(this.sphereOutsideTile(this.lightPos, this.planeLeft, this.planeRight, this.planeBottom, this.planeTop))));
236
+ }
237
+ function emitConsiderLight(pb, index, sphere, brightness) {
238
+ this.$l.lightPos = sphere;
239
+ this.$if(emitLightReaches.call(this, pb), function() {
240
+ emitLightScore.call(this, pb, brightness);
241
+ this.$if(pb.lessThan(this.n, 16), function() {
242
+ this.lightIndex.setAt(this.n, index);
243
+ this.lightScore.setAt(this.n, this.score);
244
+ this.n = pb.add(this.n, 1);
245
+ }).$elseif(pb.greaterThan(this.score, this.minScore), function() {
246
+ this.lightIndex.setAt(this.minSlot, index);
247
+ this.lightScore.setAt(this.minSlot, this.score);
248
+ }).$else(function() {
249
+ this.$continue();
250
+ });
251
+ this.$if(pb.equal(this.n, 16), function() {
252
+ this.minSlot = 0;
253
+ this.minScore = this.lightScore[0];
254
+ this.$for(pb.int('k'), 1, 16, function() {
255
+ this.$if(pb.lessThan(this.lightScore.at(this.k), this.minScore), function() {
256
+ this.minSlot = this.k;
257
+ this.minScore = this.lightScore.at(this.k);
258
+ });
259
+ });
260
+ });
261
+ });
262
+ }
263
+ /** Emits the packing of the selected light indices, one byte each, front to back. */ function emitPackSelection(pb) {
264
+ this.$l.r = pb.add(pb.sal(this.lightIndex[0], 24), pb.sal(this.lightIndex[1], 16), pb.sal(this.lightIndex[2], 8), this.lightIndex[3]);
265
+ this.$l.g = pb.add(pb.sal(this.lightIndex[4], 24), pb.sal(this.lightIndex[5], 16), pb.sal(this.lightIndex[6], 8), this.lightIndex[7]);
266
+ this.$l.b = pb.add(pb.sal(this.lightIndex[8], 24), pb.sal(this.lightIndex[9], 16), pb.sal(this.lightIndex[10], 8), this.lightIndex[11]);
267
+ this.$l.a = pb.add(pb.sal(this.lightIndex[12], 24), pb.sal(this.lightIndex[13], 16), pb.sal(this.lightIndex[14], 8), this.lightIndex[15]);
268
+ return pb.uvec4(this.r, this.g, this.b, this.a);
269
+ }
270
+ /**
271
+ * The strength a light is ranked by: its peak color channel times its intensity, times
272
+ * the area for rect lights, whose intensity is per unit area.
273
+ */ function lightBrightness(light) {
274
+ const c = light.diffuseAndIntensity;
275
+ const brightness = Math.max(c.x, c.y, c.z) * c.w;
276
+ return light.isRectLight() ? brightness * light.width * light.height : brightness;
277
+ }
278
+ /**
279
+ * Writes the world-space sphere bounding the region `light` lights into `out` (center,
280
+ * radius). The range sphere, except for spot lights: the smallest sphere around the cone
281
+ * sector they light (the cone clipped by the range sphere), which for a half-angle up to
282
+ * 45 degrees passes through the apex and the rim, and beyond that is centered on the
283
+ * rim's plane. Radius <= 0 for unbounded lights.
284
+ */ function getLightBounds(light, out) {
285
+ const posRange = light.positionAndRange;
286
+ out[0] = posRange.x;
287
+ out[1] = posRange.y;
288
+ out[2] = posRange.z;
289
+ out[3] = posRange.w;
290
+ if (posRange.w > 0 && light.isSpotLight()) {
291
+ const dirCutoff = light.directionAndCutoff;
292
+ const cosAngle = Math.min(dirCutoff.w, 1);
293
+ if (cosAngle > 0) {
294
+ const len = Math.hypot(dirCutoff.x, dirCutoff.y, dirCutoff.z) || 1;
295
+ let dist;
296
+ if (cosAngle >= Math.SQRT1_2) {
297
+ dist = posRange.w / (2 * cosAngle);
298
+ out[3] = dist;
299
+ } else {
300
+ dist = posRange.w * cosAngle;
301
+ out[3] = posRange.w * Math.sqrt(1 - cosAngle * cosAngle);
302
+ }
303
+ out[0] += dirCutoff.x / len * dist;
304
+ out[1] += dirCutoff.y / len * dist;
305
+ out[2] += dirCutoff.z / len * dist;
306
+ }
307
+ }
308
+ }
309
+ const _bounds = new Float32Array(4);
6
310
  class ClusteredLight {
7
311
  /** Emit the shadow-mask overflow warning only once per session. */ static _warnedShadowMaskOverflow = false;
312
+ /** Instances returned by {@link ClusteredLight.release}, reused to keep their GPU resources. */ static _pool = [];
313
+ /**
314
+ * Fetches an instance from the pool, creating one if it is empty.
315
+ *
316
+ * @param width - Width in device pixels of the target it will be built for.
317
+ * @param height - Height in device pixels of that target.
318
+ *
319
+ * @remarks
320
+ * The GPU work reading an instance's buffers may still be pending when the render
321
+ * code is done with it, so release it only after the render graph has executed.
322
+ */ static acquire(width, height) {
323
+ // Prefer an instance already sized for this target, so alternating targets do not
324
+ // rebuild the index texture every frame.
325
+ const x = ClusteredLight.tileCount(width);
326
+ const y = ClusteredLight.tileCount(height);
327
+ const index = this._pool.findIndex((cl)=>cl._tileCountX === x && cl._tileCountY === y);
328
+ if (index >= 0) {
329
+ return this._pool.splice(index, 1)[0];
330
+ }
331
+ return this._pool.pop() ?? new ClusteredLight();
332
+ }
333
+ /** Tile count along a screen axis of `size` pixels. */ static tileCount(size) {
334
+ return Math.min(Math.max(Math.ceil(size / TILE_SIZE_PX), 1), MAX_TILES_PER_AXIS);
335
+ }
336
+ /** Returns an instance obtained from {@link ClusteredLight.acquire} to the pool. */ static release(cl) {
337
+ this._pool.push(cl);
338
+ }
8
339
  _tileCountX;
9
340
  _tileCountY;
10
341
  _tileCountZ;
11
342
  _lights;
343
+ /** View-space bounding sphere of each light in `_lights`, for culling. See writeLight. */ _lightSpheres;
344
+ /** Buffer indices of the lights shaded outside the clusters, zero-terminated. */ _globalLights;
12
345
  _lightIndexTexture;
13
346
  _lightIndexFramebuffer;
14
347
  _lightIndexProgram;
15
348
  _bindGroup;
349
+ /** WebGPU only: the pass counting each cluster's lights ahead of the index pass. */ _lightCountProgram;
350
+ _countBindGroup;
351
+ /** WebGPU only: list entries the clusters asked for, as last read back from the GPU. */ _requestedListEntries;
352
+ _readbackPending;
353
+ _readback;
354
+ _warnedListOverflow;
16
355
  _lightIndexVertexLayout;
356
+ /** Not released by disposing the layout, so kept to be disposed along with it. */ _lightIndexVertexBuffer;
17
357
  _lightIndexRenderStates;
18
358
  _lightBuffer;
359
+ _lightSphereBuffer;
360
+ /** Lights the WebGPU storage buffers above hold, slot 0 included. */ _storageLightCapacity;
361
+ /**
362
+ * WebGPU only: per cluster, the offset of its list in `_lightListBuffer` and its
363
+ * length, two uints each.
364
+ */ _clusterGridBuffer;
365
+ /** WebGPU only: light buffer indices of every cluster's lights, one list after another. */ _lightListBuffer;
366
+ _lightListCapacity;
367
+ /** WebGPU only: the atomic allocating list room, zeroed before each build. */ _listCounterBuffer;
368
+ /**
369
+ * WebGPU only: mappable copy of `_listCounterBuffer` for readBackListRequest, kept
370
+ * across reads instead of a staging buffer being made for each one.
371
+ */ _listCounterReadBuffer;
372
+ _listCounterZero;
373
+ _lightIndexTexSize;
19
374
  _sizeParam;
20
375
  _countParam;
21
376
  _clusterParam;
377
+ /** Perspective depth slicing: split depth, first exponential slice, exponential slice count. */ _sliceParam;
22
378
  _numShadowLights;
379
+ _orthographic;
380
+ /** Scratch list of the lights competing for the slots after the mask-backed ones. */ _rest;
381
+ /** Scratch for prioritize(). */ _sortScores;
382
+ _sortOrder;
383
+ _sortScratch;
23
384
  constructor(){
24
- this._tileCountX = 16;
25
- this._tileCountY = 16;
26
- this._tileCountZ = 32;
27
- this._lights = new Float32Array(16 * (ShaderHelper.getMaxClusterLights() + 1));
385
+ this._tileCountX = 0;
386
+ this._tileCountY = 0;
387
+ this._tileCountZ = TILE_COUNT_Z;
388
+ // Grown to fit the frame's lights, see ensureLightArrayCapacity.
389
+ this._lights = new Float32Array(16 * 64);
390
+ this._lightSpheres = new Float32Array(4 * SPHERE_STRIDE * 64);
391
+ this._globalLights = new Int32Array(MAX_GLOBAL_LIGHTS);
28
392
  this._lightIndexTexture = null;
29
393
  this._lightIndexFramebuffer = null;
30
394
  this._lightIndexProgram = null;
31
395
  this._lightBuffer = null;
396
+ this._lightSphereBuffer = null;
397
+ this._storageLightCapacity = 0;
398
+ this._clusterGridBuffer = null;
399
+ this._lightListBuffer = null;
400
+ this._lightListCapacity = 0;
401
+ this._listCounterBuffer = null;
402
+ this._listCounterReadBuffer = null;
403
+ this._listCounterZero = new Uint32Array(4);
404
+ this._lightIndexTexSize = new Int32Array(2);
32
405
  this._bindGroup = null;
406
+ this._lightCountProgram = null;
407
+ this._countBindGroup = null;
408
+ this._requestedListEntries = 0;
409
+ this._readbackPending = false;
410
+ this._readback = new Uint8Array(16);
411
+ this._warnedListOverflow = false;
33
412
  this._lightIndexVertexLayout = null;
413
+ this._lightIndexVertexBuffer = null;
34
414
  this._lightIndexRenderStates = null;
35
415
  this._sizeParam = new Vector4();
36
416
  this._countParam = new Int32Array(4);
37
417
  this._clusterParam = new Vector4();
418
+ this._sliceParam = new Vector4();
38
419
  this._numShadowLights = 0;
420
+ this._orthographic = false;
421
+ this._rest = [];
422
+ this._sortScores = new Float64Array(64);
423
+ this._sortOrder = new Uint32Array(64);
424
+ this._sortScratch = [];
39
425
  }
40
426
  get lightBuffer() {
41
427
  return this._lightBuffer;
@@ -47,37 +433,48 @@ class ClusteredLight {
47
433
  */ get numShadowLights() {
48
434
  return this._numShadowLights;
49
435
  }
436
+ /**
437
+ * Whether the clusters were built for an orthographic camera, which slices depth linearly
438
+ * rather than exponentially. The lit shader reads it to compute the matching slice.
439
+ */ get orthographic() {
440
+ return this._orthographic;
441
+ }
442
+ /**
443
+ * Buffer indices of the lights every fragment shades regardless of its cluster
444
+ * (directional lights, up to {@link MAX_GLOBAL_LIGHTS}), zero-terminated. Kept out of
445
+ * the clusters so they do not take one of the few slots in each.
446
+ */ get globalLights() {
447
+ return this._globalLights;
448
+ }
50
449
  get clusterParam() {
51
450
  return this._clusterParam;
52
451
  }
53
452
  get countParam() {
54
453
  return this._countParam;
55
454
  }
56
- get lightIndexTexture() {
455
+ /** The per-cluster light slots, except on WebGPU where the clusters hold lists. */ get lightIndexTexture() {
57
456
  return this._lightIndexTexture;
58
457
  }
458
+ get lightIndexTexSize() {
459
+ return this._lightIndexTexSize;
460
+ }
461
+ /** See {@link ShaderHelper.getClusterGrid}. WebGPU only. */ get clusterGridBuffer() {
462
+ return this._clusterGridBuffer;
463
+ }
464
+ /** See {@link ShaderHelper.getClusterLightList}. WebGPU only. */ get lightListBuffer() {
465
+ return this._lightListBuffer;
466
+ }
59
467
  createVertexLayout(device, textureWidth, textureHeight) {
60
- let vb;
61
- if (device.type === 'webgl') {
62
- const vertices = new Float32Array(this._tileCountX * this._tileCountY * this._tileCountZ * 3);
63
- for(let i = 0; i < vertices.length; i++){
64
- const ix = i % textureWidth;
65
- const iy = Math.floor(i / textureWidth);
66
- vertices[i * 3 + 0] = 2 * (ix + 0.5) / textureWidth - 1;
67
- vertices[i * 3 + 1] = 2 * (iy + 0.5) / textureHeight - 1;
68
- vertices[i * 3 + 2] = i;
69
- }
70
- vb = device.createVertexBuffer('position_f32x3', vertices);
71
- } else {
72
- const vertices = new Float32Array(this._tileCountX * this._tileCountY * this._tileCountZ * 2);
73
- for(let i = 0; i < vertices.length; i++){
74
- const ix = i % textureWidth;
75
- const iy = Math.floor(i / textureWidth);
76
- vertices[i * 2 + 0] = 2 * (ix + 0.5) / textureWidth - 1;
77
- vertices[i * 2 + 1] = 2 * (iy + 0.5) / textureHeight - 1;
78
- }
79
- vb = device.createVertexBuffer('position_f32x2', vertices);
468
+ const numClusters = this._tileCountX * this._tileCountY * this._tileCountZ;
469
+ const vertices = new Float32Array(numClusters * 2);
470
+ for(let i = 0; i < numClusters; i++){
471
+ const ix = i % textureWidth;
472
+ const iy = Math.floor(i / textureWidth);
473
+ vertices[i * 2 + 0] = 2 * (ix + 0.5) / textureWidth - 1;
474
+ vertices[i * 2 + 1] = 2 * (iy + 0.5) / textureHeight - 1;
80
475
  }
476
+ const vb = device.createVertexBuffer('position_f32x2', vertices);
477
+ this._lightIndexVertexBuffer = vb;
81
478
  this._lightIndexVertexLayout = device.createVertexLayout({
82
479
  vertexBuffers: [
83
480
  {
@@ -86,218 +483,416 @@ class ClusteredLight {
86
483
  ]
87
484
  });
88
485
  }
486
+ /** Sizes the grid for a target, dropping the resources built for another grid. */ resizeGrid(width, height) {
487
+ const x = ClusteredLight.tileCount(width);
488
+ const y = ClusteredLight.tileCount(height);
489
+ if (x === this._tileCountX && y === this._tileCountY) {
490
+ return;
491
+ }
492
+ this._tileCountX = x;
493
+ this._tileCountY = y;
494
+ this._lightIndexFramebuffer?.dispose();
495
+ this._lightIndexFramebuffer = null;
496
+ this._lightIndexTexture?.dispose();
497
+ this._lightIndexTexture = null;
498
+ this._lightIndexVertexLayout?.dispose();
499
+ this._lightIndexVertexLayout = null;
500
+ this._lightIndexVertexBuffer?.dispose();
501
+ this._lightIndexVertexBuffer = null;
502
+ this._clusterGridBuffer?.dispose();
503
+ this._clusterGridBuffer = null;
504
+ this._lightListBuffer?.dispose();
505
+ this._lightListBuffer = null;
506
+ this._lightListCapacity = 0;
507
+ }
89
508
  createRenderState(device) {
90
509
  this._lightIndexRenderStates = device.createRenderStateSet();
91
510
  this._lightIndexRenderStates.useDepthState().enableTest(false).enableWrite(false);
92
511
  this._lightIndexRenderStates.useRasterizerState().setCullMode('none');
93
512
  }
94
513
  createProgram(device) {
95
- const webgl1 = device.type === 'webgl';
96
- this._lightIndexProgram = device.buildRenderProgram({
97
- vertex (pb) {
98
- this.$inputs.pos = (webgl1 ? pb.vec3() : pb.vec2()).attrib('position');
99
- this.$outputs.value = webgl1 ? pb.vec4() : pb.uvec4();
100
- this.invProjMatrix = pb.mat4().uniform(0);
101
- this.viewMatrix = pb.mat4().uniform(0);
102
- this.sizeParam = pb.vec4().uniform(0);
103
- this.countParam = pb.ivec4().uniform(0);
104
- this[ShaderHelper.getLightBufferUniformName()] = pb.vec4[(ShaderHelper.getMaxClusterLights() + 1) * 4]().uniformBuffer(0);
105
- pb.func('lineIntersectionToZPlane', [
106
- pb.vec3('a'),
107
- pb.vec3('b'),
108
- pb.float('zDistance')
109
- ], function() {
110
- this.$l.normal = pb.vec3(0, 0, 1);
111
- this.$l.ab = pb.sub(this.b, this.a);
112
- this.$l.t = pb.div(pb.sub(this.zDistance, pb.dot(this.normal, this.a)), pb.dot(this.normal, this.ab));
113
- this.$return(pb.add(this.a, pb.mul(this.t, this.ab)));
114
- });
115
- pb.func('clipToView', [
116
- pb.vec4('clip')
117
- ], function() {
118
- this.$l.view = pb.mul(this.invProjMatrix, this.clip);
119
- this.$return(pb.div(this.view, this.view.w));
120
- });
121
- pb.func('screenToView', [
122
- pb.vec4('screen')
123
- ], function() {
124
- this.$l.texCoord = pb.div(this.screen.xy, this.sizeParam.xy);
125
- this.$l.clip = pb.vec4(pb.sub(pb.mul(pb.vec2(this.texCoord.x, pb.sub(1, this.texCoord.y)), 2), pb.vec2(1)), this.screen.z, this.screen.w);
126
- this.$return(this.clipToView(this.clip));
127
- });
128
- pb.func('sphereIntersectsAABB', [
129
- pb.vec4('sphere'),
130
- pb.vec3('aabbMin'),
131
- pb.vec3('aabbMax')
132
- ], function() {
133
- this.$l.dmin = pb.float(0);
134
- this.$if(pb.lessThanEqual(this.sphere.w, 0), function() {
135
- this.$return(true);
514
+ const sphereStride = SPHERE_STRIDE;
515
+ if (device.type === 'webgpu') {
516
+ // Declares what both passes use and returns the batched light loop: runs `body`
517
+ // for each light of each batch the workgroup loads, with the light's index in
518
+ // `lightId`, its sphere in `lightPos` and its brightness in `brightness`, for the
519
+ // threads where `test` holds. Also emits the cluster bounds and the locals below.
520
+ const setup = function() {
521
+ declareIndexPassScope(pb0, this, sphereStride, true);
522
+ this.clusterGrid = pb0.uint[0]().storageBuffer(0);
523
+ this.listCounter = pb0.atomic_uint[0]().storageBuffer(0);
524
+ // One batch of lights, loaded by the whole workgroup and then tested by each of
525
+ // its clusters, instead of every cluster reading every light on its own.
526
+ this.batchSpheres = pb0.vec4[INDEX_GROUP_SIZE]().workgroup();
527
+ this.batchBrightness = pb0.float[INDEX_GROUP_SIZE]().workgroup();
528
+ };
529
+ let pb0 = null;
530
+ const emitMainSetup = function(pb) {
531
+ this.$l.numClusters = pb.mul(this.countParam.x, this.countParam.y, this.countParam.z);
532
+ this.$l.cluster = pb.int(this.$builtins.globalInvocationId.x);
533
+ // Threads past the last cluster still take part in loading the batches.
534
+ this.$l.valid = pb.lessThan(this.cluster, this.numClusters);
535
+ emitClusterBounds.call(this, pb, pb.min(this.cluster, pb.sub(this.numClusters, 1)));
536
+ this.$l.cell = pb.mul(pb.uint(pb.min(this.cluster, pb.sub(this.numClusters, 1))), 2);
537
+ this.$l.local = pb.uint(this.$builtins.localInvocationId.x);
538
+ this.$l.numLights = pb.uint(pb.sub(this.countParam.w, 1));
539
+ this.$l.numBatches = pb.div(pb.add(this.numLights, INDEX_GROUP_SIZE - 1), INDEX_GROUP_SIZE);
540
+ };
541
+ const forEachBatchedLight = function(pb, test, body) {
542
+ this.$for(pb.uint('b'), 0, this.numBatches, function() {
543
+ this.$l.base = pb.add(pb.mul(this.b, INDEX_GROUP_SIZE), 1);
544
+ this.$l.li = pb.add(this.base, this.local);
545
+ this.$if(pb.lessThan(this.li, pb.uint(this.countParam.w)), function() {
546
+ this.batchSpheres.setAt(this.local, this[UNIFORM_NAME_LIGHT_SPHERES].at(pb.mul(this.li, 2)));
547
+ this.batchBrightness.setAt(this.local, this[UNIFORM_NAME_LIGHT_SPHERES].at(pb.add(pb.mul(this.li, 2), 1)).x);
136
548
  });
137
- this.$for(pb.int('i'), 0, 3, function() {
138
- this.$if(pb.lessThan(this.sphere.at(this.i), this.aabbMin.at(this.i)), function() {
139
- this.$l.delta = pb.sub(this.sphere.at(this.i), this.aabbMin.at(this.i));
140
- this.dmin = pb.add(this.dmin, pb.mul(this.delta, this.delta));
141
- }).$elseif(pb.greaterThan(this.sphere.at(this.i), this.aabbMax.at(this.i)), function() {
142
- this.$l.delta = pb.sub(this.sphere.at(this.i), this.aabbMax.at(this.i));
143
- this.dmin = pb.add(this.dmin, pb.mul(this.delta, this.delta));
549
+ pb.workgroupBarrier();
550
+ this.$if(test.call(this), function() {
551
+ this.$l.batchCount = pb.min(pb.sub(pb.uint(this.countParam.w), this.base), INDEX_GROUP_SIZE);
552
+ this.$for(pb.uint('j'), 0, this.batchCount, function() {
553
+ this.$l.lightId = pb.add(this.base, this.j);
554
+ this.$l.lightPos = this.batchSpheres.at(this.j);
555
+ this.$l.brightness = this.batchBrightness.at(this.j);
556
+ body.call(this);
144
557
  });
145
558
  });
146
- this.$if(pb.lessThanEqual(this.dmin, pb.mul(this.sphere.w, this.sphere.w)), function() {
147
- this.$return(true);
148
- });
149
- this.$return(false);
559
+ // The next batch overwrites the one still being read.
560
+ pb.workgroupBarrier();
150
561
  });
151
- pb.main(function() {
152
- if (pb.getDevice().type !== 'webgpu') {
153
- this.$builtins.pointSize = 1;
154
- }
155
- this.$builtins.position = pb.vec4(this.$inputs.pos.xy, 0, 1);
156
- if (pb.getDevice().type === 'webgpu') {
157
- this.$builtins.position = pb.mul(this.$builtins.position, pb.vec4(1, -1, 1, 1));
158
- }
159
- this.$l.tileIndex = webgl1 ? pb.int(this.$inputs.pos.z) : pb.int(this.$builtins.vertexIndex);
160
- this.$l.tileSize = pb.div(this.sizeParam.xy, pb.vec2(this.countParam.xy));
161
- this.$l.zIndex = pb.div(this.tileIndex, pb.mul(this.countParam.x, this.countParam.y));
162
- this.$l.yIndex = pb.div(pb.sub(this.tileIndex, pb.mul(this.zIndex, this.countParam.x, this.countParam.y)), this.countParam.x);
163
- this.$l.xIndex = pb.sub(this.tileIndex, pb.add(pb.mul(this.zIndex, this.countParam.x, this.countParam.y), pb.mul(this.yIndex, this.countParam.x)));
164
- this.$l.maxPoint_sS = pb.vec4(pb.mul(pb.vec2(pb.float(pb.add(this.xIndex, 1)), pb.float(pb.add(this.yIndex, 1))), this.tileSize), 0.0, 1.0);
165
- this.$l.minPoint_sS = pb.vec4(pb.mul(pb.vec2(pb.float(this.xIndex), pb.float(this.yIndex)), this.tileSize), 0.0, 1.0);
166
- this.$l.maxPoint_vS = this.screenToView(this.maxPoint_sS).xyz;
167
- this.$l.minPoint_vS = this.screenToView(this.minPoint_sS).xyz;
168
- this.$l.tileNear = pb.mul(pb.neg(this.sizeParam.z), pb.pow(pb.div(this.sizeParam.w, this.sizeParam.z), pb.div(pb.float(this.zIndex), pb.float(this.countParam.z))));
169
- this.$l.tileFar = pb.mul(pb.neg(this.sizeParam.z), pb.pow(pb.div(this.sizeParam.w, this.sizeParam.z), pb.div(pb.add(pb.float(this.zIndex), 1), pb.float(this.countParam.z))));
170
- this.$l.eyePos = pb.vec3(0);
171
- this.$l.minPointNear = this.lineIntersectionToZPlane(this.eyePos, this.minPoint_vS, this.tileNear);
172
- this.$l.minPointFar = this.lineIntersectionToZPlane(this.eyePos, this.minPoint_vS, this.tileFar);
173
- this.$l.maxPointNear = this.lineIntersectionToZPlane(this.eyePos, this.maxPoint_vS, this.tileNear);
174
- this.$l.maxPointFar = this.lineIntersectionToZPlane(this.eyePos, this.maxPoint_vS, this.tileFar);
175
- this.$l.aabbMin = pb.min(pb.min(this.minPointNear, this.minPointFar), pb.min(this.maxPointNear, this.maxPointFar));
176
- this.$l.aabbMax = pb.max(pb.max(this.minPointNear, this.minPointFar), pb.max(this.maxPointNear, this.maxPointFar));
177
- this.$l.n = pb.int(0);
178
- if (webgl1) {
179
- this.$l.lightIndices = pb.float[8]();
180
- this.$for(pb.int('i'), 0, 8, function() {
181
- this.lightIndices.setAt(this.i, 0);
562
+ };
563
+ // Count pass: how many lights reach each cluster, and in total. The total decides
564
+ // in the write pass whether the lists fit, which no single pass could know before
565
+ // every cluster had allocated its list.
566
+ this._lightCountProgram = device.buildComputeProgram({
567
+ label: 'ClusteredLightCount',
568
+ workgroupSize: [
569
+ INDEX_GROUP_SIZE,
570
+ 1,
571
+ 1
572
+ ],
573
+ compute (pb) {
574
+ pb0 = pb;
575
+ setup.call(this);
576
+ pb.main(function() {
577
+ emitMainSetup.call(this, pb);
578
+ this.$l.listCount = pb.uint(0);
579
+ forEachBatchedLight.call(this, pb, ()=>this.valid, function() {
580
+ this.$if(emitLightReaches.call(this, pb), function() {
581
+ this.listCount = pb.add(this.listCount, 1);
582
+ });
583
+ });
584
+ this.$if(this.valid, function() {
585
+ this.clusterGrid.setAt(pb.add(this.cell, 1), this.listCount);
586
+ pb.atomicAdd(this.listCounter.at(1), this.listCount);
587
+ });
588
+ });
589
+ }
590
+ });
591
+ this._lightCountProgram.name = '@ClusteredLight_Count';
592
+ this._countBindGroup = device.createBindGroup(this._lightCountProgram.bindGroupLayouts[0]);
593
+ // Write pass: allocates each cluster's list and fills it.
594
+ this._lightIndexProgram = device.buildComputeProgram({
595
+ label: 'ClusteredLightIndex',
596
+ workgroupSize: [
597
+ INDEX_GROUP_SIZE,
598
+ 1,
599
+ 1
600
+ ],
601
+ compute (pb) {
602
+ pb0 = pb;
603
+ setup.call(this);
604
+ this.listCapacity = pb.uint().uniform(0);
605
+ this.lightList = pb.uint[0]().storageBuffer(0);
606
+ // The bucket of a score, higher for stronger lights.
607
+ pb.func('scoreBucket', [
608
+ pb.float('score')
609
+ ], function() {
610
+ this.$return(pb.uint(pb.clamp(pb.add(pb.floor(pb.mul(pb.log2(pb.max(this.score, 1e-30)), 0.5)), SCORE_BUCKETS / 2), 0, SCORE_BUCKETS - 1)));
611
+ });
612
+ pb.main(function() {
613
+ emitMainSetup.call(this, pb);
614
+ emitScoreSetup.call(this, pb);
615
+ this.$l.need = pb.uint(0);
616
+ this.$if(this.valid, function() {
617
+ this.need = this.clusterGrid.at(pb.add(this.cell, 1));
618
+ });
619
+ // When the lists do not all fit, every cluster gives up the same share of its
620
+ // lights, so the sum fits and none is left empty. The margin absorbs the
621
+ // rounding of the float scale.
622
+ this.$l.total = pb.atomicLoad(this.listCounter.at(1));
623
+ this.$l.allowed = this.need;
624
+ this.$if(pb.greaterThan(this.total, this.listCapacity), function() {
625
+ this.allowed = pb.uint(pb.floor(pb.mul(pb.float(this.need), pb.div(pb.float(this.listCapacity), pb.float(this.total)), 0.999)));
626
+ });
627
+ this.$l.truncated = pb.lessThan(this.allowed, this.need);
628
+ // A cut list keeps the strongest lights for this cluster: the buckets above
629
+ // `cutBucket` whole, and of that bucket only the first `cutRoom`.
630
+ this.$l.hist = pb.uint[SCORE_BUCKETS]();
631
+ this.$for(pb.int('h'), 0, SCORE_BUCKETS, function() {
632
+ this.hist.setAt(this.h, 0);
633
+ });
634
+ forEachBatchedLight.call(this, pb, ()=>pb.and(this.valid, this.truncated), function() {
635
+ this.$if(emitLightReaches.call(this, pb), function() {
636
+ emitLightScore.call(this, pb, this.brightness);
637
+ this.$l.bucket = this.scoreBucket(this.score);
638
+ this.hist.setAt(this.bucket, pb.add(this.hist.at(this.bucket), 1));
639
+ });
640
+ });
641
+ this.$l.cutBucket = pb.int(-1);
642
+ this.$l.cutRoom = pb.uint(0);
643
+ this.$if(this.truncated, function() {
644
+ this.$l.kept = pb.uint(0);
645
+ this.$for(pb.int('h'), SCORE_BUCKETS - 1, 0, false, true, function() {
646
+ this.$if(pb.greaterThan(pb.add(this.kept, this.hist.at(this.h)), this.allowed), function() {
647
+ this.cutBucket = this.h;
648
+ this.cutRoom = pb.sub(this.allowed, this.kept);
649
+ this.$break();
650
+ });
651
+ this.kept = pb.add(this.kept, this.hist.at(this.h));
652
+ });
182
653
  });
183
- this.$for(pb.int('i'), 1, 256, function() {
184
- this.$if(pb.equal(this.i, this.countParam.w), function() {
654
+ this.$l.listOffset = pb.uint(0);
655
+ this.$if(pb.and(this.valid, pb.greaterThan(this.allowed, 0)), function() {
656
+ this.listOffset = pb.atomicAdd(this.listCounter.at(0), this.allowed);
657
+ // Only rounding can leave the sum above the capacity; trim the last lists.
658
+ this.allowed = pb.select(pb.uint(0), pb.min(this.allowed, pb.sub(this.listCapacity, this.listOffset)), pb.lessThan(this.listOffset, this.listCapacity));
659
+ });
660
+ this.$l.written = pb.uint(0);
661
+ forEachBatchedLight.call(this, pb, ()=>pb.and(this.valid, pb.lessThan(this.written, this.allowed)), function() {
662
+ this.$if(pb.greaterThanEqual(this.written, this.allowed), function() {
185
663
  this.$break();
186
664
  });
187
- this.$l.light = this[ShaderHelper.getLightBufferUniformName()].at(pb.mul(this.i, 4));
188
- this.$l.lightPos = pb.mul(this.viewMatrix, pb.vec4(this.light.xyz, 1));
189
- this.$l.lightPos.w = this.light.w;
190
- this.$if(this.sphereIntersectsAABB(this.lightPos, this.aabbMin, this.aabbMax), function() {
191
- this.$for(pb.int('j'), 0, 8, function() {
192
- this.$if(pb.equal(this.j, this.n), function() {
193
- this.lightIndices.setAt(this.j, pb.float(this.i));
194
- this.n = pb.add(this.n, 1);
195
- this.$break();
665
+ this.$if(emitLightReaches.call(this, pb), function() {
666
+ this.$l.keep = true;
667
+ this.$if(this.truncated, function() {
668
+ emitLightScore.call(this, pb, this.brightness);
669
+ this.$l.bucket = pb.int(this.scoreBucket(this.score));
670
+ this.keep = pb.greaterThan(this.bucket, this.cutBucket);
671
+ this.$if(pb.and(pb.equal(this.bucket, this.cutBucket), pb.greaterThan(this.cutRoom, 0)), function() {
672
+ this.keep = true;
673
+ this.cutRoom = pb.sub(this.cutRoom, 1);
196
674
  });
197
675
  });
198
- this.$if(pb.equal(this.n, 8), function() {
199
- this.$break();
676
+ this.$if(this.keep, function() {
677
+ this.lightList.setAt(pb.add(this.listOffset, this.written), this.lightId);
678
+ this.written = pb.add(this.written, 1);
200
679
  });
201
680
  });
202
681
  });
203
- this.$outputs.value.r = pb.add(pb.mul(this.lightIndices[0], 256), this.lightIndices[1]);
204
- this.$outputs.value.g = pb.add(pb.mul(this.lightIndices[2], 256), this.lightIndices[3]);
205
- this.$outputs.value.b = pb.add(pb.mul(this.lightIndices[4], 256), this.lightIndices[5]);
206
- this.$outputs.value.a = pb.add(pb.mul(this.lightIndices[6], 256), this.lightIndices[7]);
207
- } else {
208
- this.$l.lightIndex = [
209
- pb.uint(0),
210
- pb.uint(0),
211
- pb.uint(0),
212
- pb.uint(0),
213
- pb.uint(0),
214
- pb.uint(0),
215
- pb.uint(0),
216
- pb.uint(0),
217
- pb.uint(0),
218
- pb.uint(0),
219
- pb.uint(0),
220
- pb.uint(0),
221
- pb.uint(0),
222
- pb.uint(0),
223
- pb.uint(0),
224
- pb.uint(0)
225
- ];
682
+ this.$if(this.valid, function() {
683
+ this.clusterGrid.setAt(this.cell, this.listOffset);
684
+ this.clusterGrid.setAt(pb.add(this.cell, 1), this.written);
685
+ });
686
+ });
687
+ }
688
+ });
689
+ } else {
690
+ this._lightIndexProgram = device.buildRenderProgram({
691
+ vertex (pb) {
692
+ this.$inputs.pos = pb.vec2().attrib('position');
693
+ this.$outputs.value = pb.uvec4();
694
+ declareIndexPassScope(pb, this, sphereStride, false);
695
+ pb.main(function() {
696
+ this.$builtins.pointSize = 1;
697
+ this.$builtins.position = pb.vec4(this.$inputs.pos.xy, 0, 1);
698
+ emitClusterBounds.call(this, pb, pb.int(this.$builtins.vertexIndex));
699
+ emitSelectionInit.call(this, pb);
226
700
  this.$for(pb.uint('i'), 1, pb.uint(this.countParam.w), function() {
227
- this.$l.light = this[ShaderHelper.getLightBufferUniformName()].at(pb.mul(this.i, 4));
228
- this.$l.lightPos = pb.mul(this.viewMatrix, pb.vec4(this.light.xyz, 1));
229
- this.$l.lightPos.w = this.light.w;
230
- this.$if(this.sphereIntersectsAABB(this.lightPos, this.aabbMin, this.aabbMax), function() {
231
- this.lightIndex.setAt(this.n, this.i);
232
- this.n = pb.add(this.n, 1);
233
- this.$if(pb.equal(this.n, 16), function() {
234
- this.$break();
235
- });
236
- });
701
+ emitConsiderLight.call(this, pb, this.i, this[UNIFORM_NAME_LIGHT_SPHERES].at(pb.mul(this.i, sphereStride)), this[UNIFORM_NAME_LIGHT_SPHERES].at(pb.add(pb.mul(this.i, sphereStride), 1)).x);
237
702
  });
238
- this.$l.r = pb.add(pb.sal(this.lightIndex[0], 24), pb.sal(this.lightIndex[1], 16), pb.sal(this.lightIndex[2], 8), this.lightIndex[3]);
239
- this.$l.g = pb.add(pb.sal(this.lightIndex[4], 24), pb.sal(this.lightIndex[5], 16), pb.sal(this.lightIndex[6], 8), this.lightIndex[7]);
240
- this.$l.b = pb.add(pb.sal(this.lightIndex[8], 24), pb.sal(this.lightIndex[9], 16), pb.sal(this.lightIndex[10], 8), this.lightIndex[11]);
241
- this.$l.a = pb.add(pb.sal(this.lightIndex[12], 24), pb.sal(this.lightIndex[13], 16), pb.sal(this.lightIndex[14], 8), this.lightIndex[15]);
242
- this.$outputs.value = pb.uvec4(this.r, this.g, this.b, this.a);
243
- }
244
- });
245
- },
246
- fragment (pb) {
247
- this.$outputs.color = webgl1 ? pb.vec4() : pb.uvec4();
248
- pb.main(function() {
249
- this.$outputs.color = this.$inputs.value;
250
- });
251
- }
252
- });
703
+ this.$outputs.value = emitPackSelection.call(this, pb);
704
+ });
705
+ },
706
+ fragment (pb) {
707
+ this.$outputs.color = pb.uvec4();
708
+ pb.main(function() {
709
+ this.$outputs.color = this.$inputs.value;
710
+ });
711
+ }
712
+ });
713
+ }
253
714
  this._lightIndexProgram.name = '@ClusteredLight_Index';
254
715
  this._bindGroup = device.createBindGroup(this._lightIndexProgram.bindGroupLayouts[0]);
716
+ if (!ShaderHelper.usesClusterLightLists()) {
717
+ this._lightSphereBuffer?.dispose();
718
+ const sphereBufferType = this._lightIndexProgram.getBindingInfo(UNIFORM_NAME_LIGHT_SPHERES).type;
719
+ this._lightSphereBuffer = device.createStructuredBuffer(sphereBufferType, {
720
+ usage: 'uniform'
721
+ });
722
+ }
723
+ }
724
+ /**
725
+ * Grows the WebGPU light and sphere storage buffers to hold `count` entries (slot 0
726
+ * included). The contents are rewritten every frame, so nothing is copied.
727
+ */ ensureStorageLightCapacity(device, count) {
728
+ if (this._storageLightCapacity >= count && this._lightBuffer && this._lightSphereBuffer) {
729
+ return;
730
+ }
731
+ const capacity = Math.max(64, 1 << Math.ceil(Math.log2(count)));
255
732
  this._lightBuffer?.dispose();
256
- const lightBufferType = this._lightIndexProgram.getBindingInfo(ShaderHelper.getLightBufferUniformName()).type;
733
+ this._lightSphereBuffer?.dispose();
734
+ this._lightBuffer = device.createBuffer(capacity * 64, {
735
+ usage: 'uniform',
736
+ storage: true
737
+ });
738
+ this._lightSphereBuffer = device.createBuffer(capacity * 16 * SPHERE_STRIDE, {
739
+ usage: 'uniform',
740
+ storage: true
741
+ });
742
+ this._storageLightCapacity = capacity;
743
+ }
744
+ /**
745
+ * Makes the WebGPU cluster grid and light list buffers fit the current grid and
746
+ * `numLights` lights. The list grows but is not shrunk.
747
+ */ ensureListBuffers(device, numLights) {
748
+ const numClusters = this._tileCountX * this._tileCountY * this._tileCountZ;
749
+ if (!this._clusterGridBuffer) {
750
+ this._clusterGridBuffer = device.createBuffer(numClusters * 8, {
751
+ usage: 'uniform',
752
+ storage: true
753
+ });
754
+ }
755
+ const capacity = Math.min(Math.max(numClusters * Math.min(Math.max(numLights, 1), LIST_LIGHTS_PER_CLUSTER), // With some headroom, as moving lights change what the clusters ask for.
756
+ Math.ceil(this._requestedListEntries * 1.25)), MAX_LIST_ENTRIES);
757
+ if (!this._lightListBuffer || this._lightListCapacity < capacity) {
758
+ this._lightListBuffer?.dispose();
759
+ this._lightListBuffer = device.createBuffer(capacity * 4, {
760
+ usage: 'uniform',
761
+ storage: true
762
+ });
763
+ this._lightListCapacity = capacity;
764
+ }
765
+ if (!this._listCounterBuffer) {
766
+ this._listCounterBuffer = device.createBuffer(16, {
767
+ usage: 'uniform',
768
+ storage: true
769
+ });
770
+ }
771
+ if (!this._listCounterReadBuffer) {
772
+ this._listCounterReadBuffer = device.createBuffer(16, {
773
+ usage: 'read'
774
+ });
775
+ }
776
+ }
777
+ /**
778
+ * Reads back how many list entries the clusters asked for, so the next builds can
779
+ * size the list to fit. One read is in flight at a time.
780
+ */ readBackListRequest() {
781
+ if (this._readbackPending) {
782
+ return;
783
+ }
784
+ this._readbackPending = true;
785
+ // The read buffer is copied into only while no read is pending, so a copy never
786
+ // lands in it while it is being mapped.
787
+ getDevice().copyBuffer(this._listCounterBuffer, this._listCounterReadBuffer, 0, 0, 16);
788
+ this._listCounterReadBuffer.getBufferSubData(this._readback, 0, 16).then(()=>{
789
+ this._readbackPending = false;
790
+ const requested = new Uint32Array(this._readback.buffer, 0, 4)[1];
791
+ this._requestedListEntries = requested;
792
+ if (requested > MAX_LIST_ENTRIES && !this._warnedListOverflow) {
793
+ this._warnedListOverflow = true;
794
+ console.warn(`ClusteredLight: the light clusters ask for ${requested} list entries, above the ` + `${MAX_LIST_ENTRIES} budget; crowded clusters keep only their strongest lights. ` + `Giving lights a finite range usually cuts this down by orders of magnitude.`);
795
+ }
796
+ }, ()=>{
797
+ this._readbackPending = false;
798
+ });
799
+ }
800
+ /** Grows the CPU-side light arrays to hold `count` entries (slot 0 included). */ ensureLightArrayCapacity(count) {
801
+ const stride = SPHERE_STRIDE;
802
+ if (this._lights.length >= count * 16) {
803
+ return;
804
+ }
805
+ const capacity = 1 << Math.ceil(Math.log2(count));
806
+ this._lights = new Float32Array(capacity * 16);
807
+ this._lightSpheres = new Float32Array(capacity * 4 * stride);
808
+ }
809
+ /** Creates the uniform light buffer read by the lit shaders, laid out as ShaderHelper declares it. */ createLightBuffer(device) {
810
+ const lightBufferType = new PBStructTypeInfo('ClusteredLightBuffer', 'std140', [
811
+ {
812
+ name: ShaderHelper.getLightBufferUniformName(),
813
+ type: new PBArrayTypeInfo(new PBPrimitiveTypeInfo(PBPrimitiveType.F32VEC4), (ShaderHelper.getMaxClusterLights() + 1) * 4)
814
+ }
815
+ ]);
257
816
  this._lightBuffer = device.createStructuredBuffer(lightBufferType, {
258
817
  usage: 'uniform'
259
818
  });
260
819
  }
261
820
  createLightIndexTexture(device) {
262
- const exp = Math.log2(this._tileCountX * this._tileCountY * this._tileCountZ);
263
- const a = exp + 1 >>> 1;
264
- const b = exp - a;
265
- const textureWidth = 2 << a - 1;
266
- const textureHeight = 2 << b - 1;
267
- if (textureWidth * textureHeight !== this._tileCountX * this._tileCountY * this._tileCountZ) {
268
- throw new Error('Internal error');
269
- }
270
- this._lightIndexTexture = device.createTexture2D(device.type === 'webgl' ? 'rgba32f' : 'rgba32ui', textureWidth, textureHeight, {
821
+ // One texel per cluster in row-major order; the tail of the last row is unused.
822
+ const numClusters = this._tileCountX * this._tileCountY * this._tileCountZ;
823
+ const textureWidth = Math.min(1 << Math.ceil(Math.log2(Math.ceil(Math.sqrt(numClusters)))), device.getDeviceCaps().textureCaps.maxTextureSize);
824
+ const textureHeight = Math.ceil(numClusters / textureWidth);
825
+ this._lightIndexTexture = device.createTexture2D('rgba32ui', textureWidth, textureHeight, {
271
826
  mipmapping: false
272
827
  });
273
828
  this._lightIndexTexture.name = 'ClusterLightIndex';
829
+ this._lightIndexTexSize[0] = textureWidth;
830
+ this._lightIndexTexSize[1] = textureHeight;
274
831
  this._lightIndexFramebuffer?.dispose();
275
832
  this._lightIndexFramebuffer = device.createFrameBuffer([
276
833
  this._lightIndexTexture
277
834
  ], null);
278
835
  }
279
- calculateLightIndex(camera, renderQueue, screenSpaceShadowMask = camera.screenSpaceShadowMask, preExposure = 1) {
280
- const numLights = this.getVisibleLights(renderQueue, this._lights, screenSpaceShadowMask, preExposure);
836
+ /**
837
+ * Builds the per-cluster light lists for a camera.
838
+ *
839
+ * @param width - Width in device pixels of the target the lit pass will draw into.
840
+ * @param height - Height in device pixels of that target.
841
+ *
842
+ * @remarks
843
+ * The lit shader maps `fragCoord` to a tile with `width`/`height`, so they must be the
844
+ * size of the target it actually renders to, not whatever viewport is current here.
845
+ */ calculateLightIndex(camera, renderQueue, width, height, screenSpaceShadowMask = camera.screenSpaceShadowMask, preExposure = 1) {
846
+ const numLights = this.getVisibleLights(camera, renderQueue, screenSpaceShadowMask, preExposure);
281
847
  const device = getDevice();
282
- if (!this._lightIndexTexture) {
848
+ const compute = ShaderHelper.usesClusterLightLists();
849
+ this.resizeGrid(width, height);
850
+ if (compute) {
851
+ this.ensureStorageLightCapacity(device, numLights + 1);
852
+ this.ensureListBuffers(device, numLights);
853
+ } else if (!this._lightIndexTexture) {
283
854
  this.createLightIndexTexture(device);
284
855
  }
285
856
  if (!this._lightIndexProgram) {
286
857
  this.createProgram(device);
287
858
  }
288
- if (!this._lightIndexVertexLayout) {
859
+ if (!compute && !this._lightBuffer) {
860
+ this.createLightBuffer(device);
861
+ }
862
+ if (!compute && !this._lightIndexVertexLayout) {
289
863
  this.createVertexLayout(device, this._lightIndexTexture.width, this._lightIndexTexture.height);
290
864
  }
291
- if (!this._lightIndexRenderStates) {
865
+ if (!compute && !this._lightIndexRenderStates) {
292
866
  this.createRenderState(device);
293
867
  }
294
- const viewport = device.getViewport();
295
- const vw = device.screenXToDevice(viewport.width);
296
- const vh = device.screenYToDevice(viewport.height);
297
- const scale = this._tileCountZ / Math.log2(camera.getFarPlane() / camera.getNearPlane());
298
- const bias = -(this._tileCountZ * Math.log2(camera.getNearPlane()) / Math.log2(camera.getFarPlane() / camera.getNearPlane()));
868
+ const vw = width;
869
+ const vh = height;
870
+ const near = camera.getNearPlane();
871
+ const far = camera.getFarPlane();
872
+ // The lit shader computes slice = t * scale + bias, where t is log2(view depth) for
873
+ // perspective (exponential slices) and the view depth itself for orthographic (linear
874
+ // slices: an orthographic near plane may be zero or negative, where log2 is undefined).
875
+ this._orthographic = !camera.isPerspective();
876
+ let scale;
877
+ let bias;
878
+ if (this._orthographic) {
879
+ scale = this._tileCountZ / (far - near);
880
+ bias = -near * scale;
881
+ } else {
882
+ // A near slice pays off only when it spans more than one regular exponential slice.
883
+ const split = NEAR_SLICE_METERS / (camera.scene?.metersPerUnit ?? 1);
884
+ const nearSlice = split > near * Math.pow(far / near, 1 / this._tileCountZ) && split < far;
885
+ const expStart = nearSlice ? split : near;
886
+ const firstExpSlice = nearSlice ? 1 : 0;
887
+ const numExpSlices = this._tileCountZ - firstExpSlice;
888
+ scale = numExpSlices / Math.log2(far / expStart);
889
+ // Depths before the split land below firstExpSlice, which the lit shader's clamp
890
+ // and integer conversion take to slice 0.
891
+ bias = firstExpSlice - Math.log2(expStart) * scale;
892
+ this._sliceParam.setXYZW(expStart, firstExpSlice, numExpSlices, 0);
893
+ }
299
894
  this._clusterParam.setXYZW(vw, vh, scale, bias);
300
- this._sizeParam.setXYZW(vw, vh, camera.getNearPlane(), camera.getFarPlane());
895
+ this._sizeParam.setXYZW(vw, vh, near, far);
301
896
  this._countParam[0] = this._tileCountX;
302
897
  this._countParam[1] = this._tileCountY;
303
898
  this._countParam[2] = this._tileCountZ;
@@ -305,36 +900,170 @@ class ClusteredLight {
305
900
  // Keep it valid even when there are no unshadowed punctual lights so clustered
306
901
  // shading math stays well-defined for emissive and environment lighting paths.
307
902
  this._countParam[3] = numLights + 1;
903
+ const numClusters = this._tileCountX * this._tileCountY * this._tileCountZ;
308
904
  device.pushDeviceStates();
309
- device.setFramebuffer(this._lightIndexFramebuffer);
310
- if (numLights > 0) {
905
+ if (!compute) {
906
+ device.setFramebuffer(this._lightIndexFramebuffer);
907
+ // setFramebuffer may no-op, so reset viewport and scissor explicitly.
908
+ device.setViewport(null);
909
+ device.setScissor(null);
910
+ }
911
+ // The compute pass writes every cluster itself, empty ones included.
912
+ if (compute || numLights > 0) {
913
+ if (this._lightSphereBuffer.disposed) {
914
+ this._lightSphereBuffer.reload();
915
+ }
916
+ // Slot 0 is never read, so upload only up to the last light.
311
917
  if (this._lightBuffer.disposed) {
312
918
  this._lightBuffer.reload();
313
919
  }
314
- this._lightBuffer.bufferSubData(0, this._lights);
920
+ this._lightBuffer.bufferSubData(0, this._lights, 0, (numLights + 1) * 16);
921
+ this._lightSphereBuffer.bufferSubData(0, this._lightSpheres, 0, (numLights + 1) * 4 * SPHERE_STRIDE);
315
922
  this._bindGroup.setValue('invProjMatrix', camera.getInvProjectionMatrix());
316
- this._bindGroup.setValue('viewMatrix', camera.viewMatrix);
317
923
  this._bindGroup.setValue('sizeParam', this._sizeParam);
318
924
  this._bindGroup.setValue('countParam', this._countParam);
319
- this._bindGroup.setBuffer(ShaderHelper.getLightBufferUniformName(), this._lightBuffer);
925
+ this._bindGroup.setValue('orthoProj', this._orthographic ? 1 : 0);
926
+ this._bindGroup.setValue('sliceParam', this._sliceParam);
927
+ this._bindGroup.setBuffer(UNIFORM_NAME_LIGHT_SPHERES, this._lightSphereBuffer);
320
928
  device.setProgram(this._lightIndexProgram);
321
- device.setVertexLayout(this._lightIndexVertexLayout);
322
- device.setBindGroup(0, this._bindGroup);
323
- const savedRS = device.getRenderStates();
324
- device.setRenderStates(this._lightIndexRenderStates);
325
- device.draw('point-list', 0, this._tileCountX * this._tileCountY * this._tileCountZ);
326
- device.setRenderStates(savedRS);
929
+ if (compute) {
930
+ const numGroups = Math.ceil(numClusters / INDEX_GROUP_SIZE);
931
+ this._listCounterBuffer.bufferSubData(0, this._listCounterZero);
932
+ const countBindGroup = this._countBindGroup;
933
+ countBindGroup.setValue('invProjMatrix', camera.getInvProjectionMatrix());
934
+ countBindGroup.setValue('sizeParam', this._sizeParam);
935
+ countBindGroup.setValue('countParam', this._countParam);
936
+ countBindGroup.setValue('orthoProj', this._orthographic ? 1 : 0);
937
+ countBindGroup.setValue('sliceParam', this._sliceParam);
938
+ countBindGroup.setBuffer(UNIFORM_NAME_LIGHT_SPHERES, this._lightSphereBuffer);
939
+ countBindGroup.setBuffer('clusterGrid', this._clusterGridBuffer);
940
+ countBindGroup.setBuffer('listCounter', this._listCounterBuffer);
941
+ device.setProgram(this._lightCountProgram);
942
+ device.setBindGroup(0, countBindGroup);
943
+ device.compute(numGroups, 1, 1);
944
+ this._bindGroup.setValue('listCapacity', this._lightListCapacity);
945
+ this._bindGroup.setBuffer('clusterGrid', this._clusterGridBuffer);
946
+ this._bindGroup.setBuffer('lightList', this._lightListBuffer);
947
+ this._bindGroup.setBuffer('listCounter', this._listCounterBuffer);
948
+ device.setProgram(this._lightIndexProgram);
949
+ device.setBindGroup(0, this._bindGroup);
950
+ device.compute(numGroups, 1, 1);
951
+ this.readBackListRequest();
952
+ } else {
953
+ device.setVertexLayout(this._lightIndexVertexLayout);
954
+ device.setBindGroup(0, this._bindGroup);
955
+ const savedRS = device.getRenderStates();
956
+ device.setRenderStates(this._lightIndexRenderStates);
957
+ device.draw('point-list', 0, numClusters);
958
+ device.setRenderStates(savedRS);
959
+ }
327
960
  } else {
328
961
  device.clearFrameBuffer(new Vector4(0, 0, 0, 0), DEPTH_CLEAR_VALUE, 0);
329
962
  }
330
963
  device.popDeviceStates();
331
964
  }
332
- getVisibleLights(renderQueue, lights, useShadowMask, preExposure) {
965
+ /**
966
+ * Orders `lights` by estimated contribution at the camera, most important first.
967
+ *
968
+ * @remarks
969
+ * Both caps drop lights by buffer index - the global one truncates the tail, and
970
+ * a full cluster keeps its lowest 16 indices - so without this whichever lights the
971
+ * scene traversal happened to reach last are the ones that vanish. The score is
972
+ * `brightness * r^2 / (r^2 + d^2)`, with `r` the radius of the sphere bounding the
973
+ * lit region and `d` the camera's distance to it: 0 from anywhere inside it, falling
974
+ * off with distance beyond it, and larger for lights that reach further. The sphere is
975
+ * getLightBounds', so a spot light pointing away from the camera ranks by where it
976
+ * shines rather than where it stands, and the brightness is lightBrightness', so a
977
+ * rect light counts its whole area. Directional lights have no range and light
978
+ * everything, so they always rank first.
979
+ */ prioritize(lights, eye) {
980
+ if (lights.length < 2) {
981
+ return;
982
+ }
983
+ // Scores and a permutation in reused typed arrays, so a frame allocates nothing.
984
+ const n = lights.length;
985
+ if (this._sortScores.length < n) {
986
+ const capacity = 1 << Math.ceil(Math.log2(n));
987
+ this._sortScores = new Float64Array(capacity);
988
+ this._sortOrder = new Uint32Array(capacity);
989
+ }
990
+ const scores = this._sortScores;
991
+ const order = this._sortOrder.subarray(0, n);
992
+ const sorted = this._sortScratch;
993
+ sorted.length = n;
994
+ for(let i = 0; i < n; i++){
995
+ const light = lights[i];
996
+ getLightBounds(light, _bounds);
997
+ const range = _bounds[3];
998
+ let score;
999
+ if (light.isDirectionLight() || range <= 0) {
1000
+ score = Infinity;
1001
+ } else {
1002
+ const dx = _bounds[0] - eye.x;
1003
+ const dy = _bounds[1] - eye.y;
1004
+ const dz = _bounds[2] - eye.z;
1005
+ const d = Math.max(Math.sqrt(dx * dx + dy * dy + dz * dz) - range, 0);
1006
+ const range2 = range * range;
1007
+ score = lightBrightness(light) * range2 / (range2 + d * d);
1008
+ }
1009
+ scores[i] = score;
1010
+ order[i] = i;
1011
+ sorted[i] = light;
1012
+ }
1013
+ order.sort(this._compareScores);
1014
+ for(let i = 0; i < n; i++){
1015
+ lights[i] = sorted[order[i]];
1016
+ }
1017
+ sorted.length = 0;
1018
+ }
1019
+ /**
1020
+ * Orders buffer positions by descending score. Ties go to the lower position, which
1021
+ * keeps equally important lights in traversal order frame to frame. Compared rather
1022
+ * than subtracted: two directional lights would give Infinity - Infinity.
1023
+ */ _compareScores = (a, b)=>{
1024
+ const sa = this._sortScores[a];
1025
+ const sb = this._sortScores[b];
1026
+ return sa > sb ? -1 : sa < sb ? 1 : a - b;
1027
+ };
1028
+ getVisibleLights(camera, renderQueue, useShadowMask, preExposure) {
1029
+ this.ensureLightArrayCapacity(Math.min(renderQueue.shadowedLights.length + renderQueue.unshadowedLights.length, ShaderHelper.getMaxClusterLights()) + 1);
1030
+ const lights = this._lights;
1031
+ const view = camera.viewMatrix;
1032
+ const spheres = this._lightSpheres;
1033
+ const sphereStride = SPHERE_STRIDE;
1034
+ const globalLights = this._globalLights;
1035
+ globalLights.fill(0);
1036
+ let numGlobal = 0;
333
1037
  const writeLight = (light, slot)=>{
334
1038
  const offset = slot * 16;
335
1039
  const colorIntensity = light.diffuseAndIntensity;
336
- lights.set(light.positionAndRange, offset);
337
- lights.set(light.directionAndCutoff, offset + 4);
1040
+ const posRange = light.positionAndRange;
1041
+ const dirCutoff = light.directionAndCutoff;
1042
+ lights.set(posRange, offset);
1043
+ // Culling sphere: radius < 0 = shaded through a global slot, skipped by the index
1044
+ // pass; 0 = unbounded, in every cluster; otherwise it bounds the lit region.
1045
+ getLightBounds(light, _bounds);
1046
+ const x = _bounds[0];
1047
+ const y = _bounds[1];
1048
+ const z = _bounds[2];
1049
+ let radius = _bounds[3];
1050
+ if (radius <= 0) {
1051
+ if (numGlobal < MAX_GLOBAL_LIGHTS) {
1052
+ globalLights[numGlobal++] = slot;
1053
+ radius = -1;
1054
+ } else {
1055
+ radius = 0;
1056
+ }
1057
+ }
1058
+ // The view matrix is column-major.
1059
+ const s = slot * 4 * sphereStride;
1060
+ spheres[s + 0] = view[0] * x + view[4] * y + view[8] * z + view[12];
1061
+ spheres[s + 1] = view[1] * x + view[5] * y + view[9] * z + view[13];
1062
+ spheres[s + 2] = view[2] * x + view[6] * y + view[10] * z + view[14];
1063
+ spheres[s + 3] = radius;
1064
+ // Ranks the lights competing for a crowded cluster; same measure as prioritize().
1065
+ spheres[s + 4] = lightBrightness(light);
1066
+ lights.set(dirCutoff, offset + 4);
338
1067
  // Only the intensity carries the camera pre-exposure; the color stays as authored. The
339
1068
  // light's own cached vector must not be mutated because it is shared across cameras.
340
1069
  lights[offset + 8] = colorIntensity.x;
@@ -357,27 +1086,32 @@ class ClusteredLight {
357
1086
  }
358
1087
  slot = numShadow;
359
1088
  }
360
- // Unshadowed lights fill the region after the mask-backed shadow lights.
361
- const numUnshadowed = Math.min(renderQueue.unshadowedLights.length, ShaderHelper.getMaxClusterLights() - slot);
362
- for(let j = 0; j < numUnshadowed; j++){
363
- writeLight(renderQueue.unshadowedLights[j], slot + j + 1);
1089
+ // Everything without a mask slot fills the region after the mask-backed shadow
1090
+ // lights: the unshadowed lights, plus shadow-casting lights beyond the mask
1091
+ // capacity (MAX_SHADOW_MASK_LIGHTS). Rather than dropping the latter silently they
1092
+ // are still lit here - without a mask sample, i.e. degraded to no shadow. Both
1093
+ // compete for the remaining slots on equal terms, most important first.
1094
+ const rest = this._rest;
1095
+ rest.length = 0;
1096
+ for (const light of renderQueue.unshadowedLights){
1097
+ rest.push(light);
364
1098
  }
365
- slot += numUnshadowed;
366
- // Shadow-casting lights beyond the mask capacity (MAX_SHADOW_MASK_LIGHTS) have
367
- // no mask slot. Rather than dropping them silently, place them past index N so
368
- // they are still lit — without a mask sample, i.e. degraded to no shadow.
369
- if (useShadowMask && renderQueue.shadowedLights.length > numShadow) {
370
- const overflow = renderQueue.shadowedLights.length - numShadow;
371
- const numOverflow = Math.min(overflow, ShaderHelper.getMaxClusterLights() - slot);
372
- for(let k = 0; k < numOverflow; k++){
373
- writeLight(renderQueue.shadowedLights[numShadow + k], slot + k + 1);
374
- }
375
- slot += numOverflow;
376
- if (!ClusteredLight._warnedShadowMaskOverflow) {
377
- ClusteredLight._warnedShadowMaskOverflow = true;
378
- console.warn(`ClusteredLight: ${renderQueue.shadowedLights.length} shadow-casting lights exceed the ` + `screen-space shadow mask capacity (${MAX_SHADOW_MASK_LIGHTS}); ${overflow} light(s) are ` + `lit without shadows${numOverflow < overflow ? `, and ${overflow - numOverflow} dropped` : ''}.`);
379
- }
1099
+ const overflow = useShadowMask ? renderQueue.shadowedLights.length - numShadow : 0;
1100
+ for(let k = 0; k < overflow; k++){
1101
+ rest.push(renderQueue.shadowedLights[numShadow + k]);
1102
+ }
1103
+ this.prioritize(rest, camera.getWorldPosition());
1104
+ const numRest = Math.min(rest.length, ShaderHelper.getMaxClusterLights() - slot);
1105
+ for(let j = 0; j < numRest; j++){
1106
+ writeLight(rest[j], slot + j + 1);
1107
+ }
1108
+ slot += numRest;
1109
+ if (overflow > 0 && !ClusteredLight._warnedShadowMaskOverflow) {
1110
+ ClusteredLight._warnedShadowMaskOverflow = true;
1111
+ const dropped = rest.length - numRest;
1112
+ console.warn(`ClusteredLight: ${renderQueue.shadowedLights.length} shadow-casting lights exceed the ` + `screen-space shadow mask capacity (${MAX_SHADOW_MASK_LIGHTS}); ${overflow} light(s) are ` + `lit without shadows${dropped > 0 ? `, and the ${dropped} least important light(s) dropped` : ''}.`);
380
1113
  }
1114
+ rest.length = 0;
381
1115
  this._numShadowLights = numShadow;
382
1116
  return slot;
383
1117
  }