@woosh/meep-engine 3.44.0 → 3.46.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 (447) hide show
  1. package/build/bundle-worker-terrain.js +1 -1
  2. package/package.json +1 -1
  3. package/src/core/assert.d.ts.map +1 -1
  4. package/src/core/assert.js +5 -1
  5. package/src/core/binary/hash/XXH128Hash.d.ts +1 -37
  6. package/src/core/binary/hash/XXH128Hash.d.ts.map +1 -1
  7. package/src/core/binary/hash/XXH128Hash.js +4 -55
  8. package/src/core/binary/hash/XXH3_SECRET.d.ts +1 -12
  9. package/src/core/binary/hash/XXH3_SECRET.d.ts.map +1 -1
  10. package/src/core/binary/hash/XXH3_SECRET.js +4 -25
  11. package/src/core/binary/hash/crc32.d.ts +1 -15
  12. package/src/core/binary/hash/crc32.d.ts.map +1 -1
  13. package/src/core/binary/hash/crc32.js +4 -96
  14. package/src/core/binary/hash/xxh3_128.d.ts +1 -9
  15. package/src/core/binary/hash/xxh3_128.d.ts.map +1 -1
  16. package/src/core/binary/hash/xxh3_128.js +4 -341
  17. package/src/core/binary/hash/xxh3_64.d.ts +1 -8
  18. package/src/core/binary/hash/xxh3_64.d.ts.map +1 -1
  19. package/src/core/binary/hash/xxh3_64.js +4 -241
  20. package/src/core/binary/hash/xxh3_common.d.ts +1 -223
  21. package/src/core/binary/hash/xxh3_common.d.ts.map +1 -1
  22. package/src/core/binary/hash/xxh3_common.js +90 -476
  23. package/src/core/collection/array/array_sort_insertion_ascending.d.ts +19 -0
  24. package/src/core/collection/array/array_sort_insertion_ascending.d.ts.map +1 -0
  25. package/src/core/collection/array/array_sort_insertion_ascending.js +31 -0
  26. package/src/core/collection/array/combine_hash.d.ts +6 -0
  27. package/src/core/collection/array/combine_hash.d.ts.map +1 -1
  28. package/src/core/collection/array/combine_hash.js +6 -0
  29. package/src/core/function/FunctionCompiler.js +2 -2
  30. package/src/core/geom/3d/aabb/aabb3_array_round_outward_f32.d.ts +27 -0
  31. package/src/core/geom/3d/aabb/aabb3_array_round_outward_f32.d.ts.map +1 -0
  32. package/src/core/geom/3d/aabb/aabb3_array_round_outward_f32.js +39 -0
  33. package/src/core/geom/3d/aabb/aabb3_from_v3_array.d.ts.map +1 -1
  34. package/src/core/geom/3d/aabb/aabb3_from_v3_array.js +4 -1
  35. package/src/core/geom/3d/aabb/aabb3_transform_oriented.d.ts +2 -2
  36. package/src/core/geom/3d/aabb/aabb3_transform_oriented.d.ts.map +1 -1
  37. package/src/core/geom/3d/aabb/aabb3_transform_oriented.js +1 -1
  38. package/src/core/geom/3d/mat4/m4_multiply.d.ts +3 -0
  39. package/src/core/geom/3d/mat4/m4_multiply.d.ts.map +1 -1
  40. package/src/core/geom/3d/mat4/m4_multiply.js +3 -0
  41. package/src/core/geom/3d/mat4/m4_multiply_f64.d.ts +30 -0
  42. package/src/core/geom/3d/mat4/m4_multiply_f64.d.ts.map +1 -0
  43. package/src/core/geom/3d/mat4/m4_multiply_f64.js +106 -0
  44. package/src/core/geom/3d/pose/pose3_apply_inverse_point_array.d.ts +21 -0
  45. package/src/core/geom/3d/pose/pose3_apply_inverse_point_array.d.ts.map +1 -0
  46. package/src/core/geom/3d/pose/pose3_apply_inverse_point_array.js +36 -0
  47. package/src/core/geom/3d/pose/pose3_apply_point_array.d.ts +25 -0
  48. package/src/core/geom/3d/pose/pose3_apply_point_array.d.ts.map +1 -0
  49. package/src/core/geom/3d/pose/pose3_apply_point_array.js +40 -0
  50. package/src/core/geom/3d/pose/pose3_compose_array.d.ts +28 -0
  51. package/src/core/geom/3d/pose/pose3_compose_array.d.ts.map +1 -0
  52. package/src/core/geom/3d/pose/pose3_compose_array.js +41 -0
  53. package/src/core/geom/3d/pose/pose3_to_local_array.d.ts +29 -0
  54. package/src/core/geom/3d/pose/pose3_to_local_array.d.ts.map +1 -0
  55. package/src/core/geom/3d/pose/pose3_to_local_array.js +42 -0
  56. package/src/core/geom/3d/quaternion/quat3_clamp_angle_array.d.ts +28 -0
  57. package/src/core/geom/3d/quaternion/quat3_clamp_angle_array.d.ts.map +1 -0
  58. package/src/core/geom/3d/quaternion/quat3_clamp_angle_array.js +66 -0
  59. package/src/core/geom/3d/quaternion/quat3_multiply_conjugate_left_array.d.ts +22 -0
  60. package/src/core/geom/3d/quaternion/quat3_multiply_conjugate_left_array.d.ts.map +1 -0
  61. package/src/core/geom/3d/quaternion/quat3_multiply_conjugate_left_array.js +30 -0
  62. package/src/core/geom/3d/quaternion/quat3_multiply_conjugate_right_array.d.ts +23 -0
  63. package/src/core/geom/3d/quaternion/quat3_multiply_conjugate_right_array.d.ts.map +1 -0
  64. package/src/core/geom/3d/quaternion/quat3_multiply_conjugate_right_array.js +31 -0
  65. package/src/core/geom/3d/triangle/tri3_compute_normal_area_weighted_array.d.ts +26 -0
  66. package/src/core/geom/3d/triangle/tri3_compute_normal_area_weighted_array.d.ts.map +1 -0
  67. package/src/core/geom/3d/triangle/tri3_compute_normal_area_weighted_array.js +46 -0
  68. package/src/core/geom/ConicRay.js +2 -2
  69. package/src/core/geom/Vector4.d.ts.map +1 -1
  70. package/src/core/geom/Vector4.js +8 -5
  71. package/src/core/geom/vec3/v3_distance_array.d.ts +11 -0
  72. package/src/core/geom/vec3/v3_distance_array.d.ts.map +1 -0
  73. package/src/core/geom/vec3/v3_distance_array.js +19 -0
  74. package/src/core/geom/vec3/v3_normalize_array_checked.d.ts +23 -0
  75. package/src/core/geom/vec3/v3_normalize_array_checked.d.ts.map +1 -0
  76. package/src/core/geom/vec3/v3_normalize_array_checked.js +49 -0
  77. package/src/core/geom/vec3/v3_reject_array.d.ts +15 -0
  78. package/src/core/geom/vec3/v3_reject_array.d.ts.map +1 -0
  79. package/src/core/geom/vec3/v3_reject_array.js +32 -0
  80. package/src/core/graph/csr/csr_build_incidence.d.ts +26 -0
  81. package/src/core/graph/csr/csr_build_incidence.d.ts.map +1 -0
  82. package/src/core/graph/csr/csr_build_incidence.js +71 -0
  83. package/src/core/graph/csr/csr_graph_color_greedy.d.ts +25 -0
  84. package/src/core/graph/csr/csr_graph_color_greedy.d.ts.map +1 -0
  85. package/src/core/graph/csr/csr_graph_color_greedy.js +83 -0
  86. package/src/core/graph/csr/csr_graph_validate_coloring.d.ts +15 -0
  87. package/src/core/graph/csr/csr_graph_validate_coloring.d.ts.map +1 -0
  88. package/src/core/graph/csr/csr_graph_validate_coloring.js +33 -0
  89. package/src/core/math/hash/XXH128Hash.d.ts +38 -0
  90. package/src/core/math/hash/XXH128Hash.d.ts.map +1 -0
  91. package/src/core/math/hash/XXH128Hash.js +55 -0
  92. package/src/core/math/hash/XXH3_SECRET.d.ts +13 -0
  93. package/src/core/math/hash/XXH3_SECRET.d.ts.map +1 -0
  94. package/src/core/math/hash/XXH3_SECRET.js +25 -0
  95. package/src/core/math/hash/crc32.d.ts +16 -0
  96. package/src/core/math/hash/crc32.d.ts.map +1 -0
  97. package/src/core/math/hash/crc32.js +96 -0
  98. package/src/core/math/hash/xxh3_128.d.ts +10 -0
  99. package/src/core/math/hash/xxh3_128.d.ts.map +1 -0
  100. package/src/core/math/hash/xxh3_128.js +341 -0
  101. package/src/core/math/hash/xxh3_64.d.ts +9 -0
  102. package/src/core/math/hash/xxh3_64.d.ts.map +1 -0
  103. package/src/core/math/hash/xxh3_64.js +241 -0
  104. package/src/core/math/hash/xxh3_common.d.ts +224 -0
  105. package/src/core/math/hash/xxh3_common.d.ts.map +1 -0
  106. package/src/core/math/hash/xxh3_common.js +476 -0
  107. package/src/core/math/linalg/matrix_symmetric_3x3_solve_ldlt.d.ts +26 -0
  108. package/src/core/math/linalg/matrix_symmetric_3x3_solve_ldlt.d.ts.map +1 -0
  109. package/src/core/math/linalg/matrix_symmetric_3x3_solve_ldlt.js +71 -0
  110. package/src/core/math/lookup/ParameterLookupTable.js +2 -2
  111. package/src/core/math/max2_exact.d.ts +15 -0
  112. package/src/core/math/max2_exact.d.ts.map +1 -0
  113. package/src/core/math/max2_exact.js +30 -0
  114. package/src/core/math/min2_exact.d.ts +20 -0
  115. package/src/core/math/min2_exact.d.ts.map +1 -0
  116. package/src/core/math/min2_exact.js +35 -0
  117. package/src/core/model/node-graph/node/NodeInstancePortReference.js +2 -2
  118. package/src/core/model/reactive/model/ReactiveBinaryExpression.js +2 -2
  119. package/src/core/model/reactive/model/terminal/ReactiveReference.js +2 -2
  120. package/src/engine/asset/AssetDescription.js +2 -2
  121. package/src/engine/ecs/animation/Animation.js +2 -2
  122. package/src/engine/ecs/animation/AnimationClip.d.ts.map +1 -1
  123. package/src/engine/ecs/animation/AnimationClip.js +7 -5
  124. package/src/engine/ecs/gui/GUIElement.d.ts.map +1 -1
  125. package/src/engine/ecs/gui/GUIElement.js +7 -5
  126. package/src/engine/ecs/gui/hud/HeadsUpDisplay.js +2 -2
  127. package/src/engine/ecs/gui/position/ViewportPosition.d.ts.map +1 -1
  128. package/src/engine/ecs/gui/position/ViewportPosition.js +11 -7
  129. package/src/engine/ecs/ik/IKGoalSystem.d.ts.map +1 -1
  130. package/src/engine/ecs/ik/IKGoalSystem.js +3 -19
  131. package/src/engine/ecs/parent/parent_entity_model_of.d.ts +17 -0
  132. package/src/engine/ecs/parent/parent_entity_model_of.d.ts.map +1 -0
  133. package/src/engine/ecs/parent/parent_entity_model_of.js +26 -0
  134. package/src/engine/ecs/transform/Transform64.d.ts +1 -1
  135. package/src/engine/ecs/transform/Transform64.d.ts.map +1 -1
  136. package/src/engine/ecs/transform/Transform64.js +5 -4
  137. package/src/engine/ecs/transform/t64_read_pose_array.d.ts +15 -0
  138. package/src/engine/ecs/transform/t64_read_pose_array.d.ts.map +1 -0
  139. package/src/engine/ecs/transform/t64_read_pose_array.js +24 -0
  140. package/src/engine/graphics/ecs/animation/animator/AnimationClip.d.ts.map +1 -1
  141. package/src/engine/graphics/ecs/animation/animator/AnimationClip.js +8 -5
  142. package/src/engine/graphics/ecs/animation/animator/AnimationClipDefinition.js +2 -2
  143. package/src/engine/graphics/ecs/animation/animator/AnimationNotification.js +2 -2
  144. package/src/engine/graphics/ecs/animation/animator/AnimationNotificationDefinition.js +2 -2
  145. package/src/engine/graphics/ecs/animation/animator/graph/definition/AnimationGraphDefinition.js +2 -2
  146. package/src/engine/graphics/ecs/animation/animator/graph/definition/AnimationStateDefinition.js +2 -2
  147. package/src/engine/graphics/ecs/animation/animator/graph/definition/AnimationTransitionDefinition.d.ts.map +1 -1
  148. package/src/engine/graphics/ecs/animation/animator/graph/definition/AnimationTransitionDefinition.js +8 -5
  149. package/src/engine/graphics/ecs/decal/v2/Decal.d.ts.map +1 -1
  150. package/src/engine/graphics/ecs/decal/v2/Decal.js +9 -8
  151. package/src/engine/graphics/geometry/geometry_construct_triangle_normal_area_weighted.d.ts +2 -0
  152. package/src/engine/graphics/geometry/geometry_construct_triangle_normal_area_weighted.d.ts.map +1 -1
  153. package/src/engine/graphics/geometry/geometry_construct_triangle_normal_area_weighted.js +10 -21
  154. package/src/engine/graphics/render/frame_graph/resource/TextureResourceDescriptor.d.ts.map +1 -1
  155. package/src/engine/graphics/render/frame_graph/resource/TextureResourceDescriptor.js +9 -6
  156. package/src/engine/graphics3/AnimationGraphSystem.d.ts +5 -0
  157. package/src/engine/graphics3/AnimationGraphSystem.d.ts.map +1 -1
  158. package/src/engine/graphics3/AnimationGraphSystem.js +48 -2
  159. package/src/engine/graphics3/AnimationSystem.d.ts +5 -0
  160. package/src/engine/graphics3/AnimationSystem.d.ts.map +1 -1
  161. package/src/engine/graphics3/AnimationSystem.js +47 -2
  162. package/src/engine/graphics3/ParticipatingMedia.js +204 -204
  163. package/src/engine/graphics3/animation/AnimationClips.d.ts +5 -0
  164. package/src/engine/graphics3/animation/AnimationClips.d.ts.map +1 -1
  165. package/src/engine/graphics3/animation/AnimationClips.js +5 -0
  166. package/src/engine/graphics3/register_clips.d.ts +29 -0
  167. package/src/engine/graphics3/register_clips.d.ts.map +1 -1
  168. package/src/engine/graphics3/register_clips.js +52 -0
  169. package/src/engine/grid/obstacle/GridObstacle.js +2 -2
  170. package/src/engine/intelligence/behavior/selector/WeightedElement.js +2 -2
  171. package/src/engine/interpolation/Interpolated.js +2 -2
  172. package/src/engine/physics/broadphase/compute_fat_world_aabb.d.ts +9 -0
  173. package/src/engine/physics/broadphase/compute_fat_world_aabb.d.ts.map +1 -1
  174. package/src/engine/physics/broadphase/compute_fat_world_aabb.js +3 -1
  175. package/src/engine/physics/cloth/PLAN.md +10 -8
  176. package/src/engine/physics/cloth/build/cloth_bind_in_frame.d.ts +22 -0
  177. package/src/engine/physics/cloth/build/cloth_bind_in_frame.d.ts.map +1 -1
  178. package/src/engine/physics/cloth/build/cloth_bind_in_frame.js +51 -19
  179. package/src/engine/physics/cloth/build/cloth_proxy_from_joints.d.ts.map +1 -1
  180. package/src/engine/physics/cloth/build/cloth_proxy_from_joints.js +16 -2
  181. package/src/engine/physics/cloth/build/cloth_proxy_from_mesh.d.ts.map +1 -1
  182. package/src/engine/physics/cloth/build/cloth_proxy_from_mesh.js +19 -24
  183. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts +7 -0
  184. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts.map +1 -1
  185. package/src/engine/physics/cloth/collider/ClothColliderIndex.js +85 -67
  186. package/src/engine/physics/cloth/collider/cloth_collider_bounds.d.ts.map +1 -1
  187. package/src/engine/physics/cloth/collider/cloth_collider_bounds.js +19 -101
  188. package/src/engine/physics/cloth/collider/cloth_collider_pack.d.ts +2 -5
  189. package/src/engine/physics/cloth/collider/cloth_collider_pack.d.ts.map +1 -1
  190. package/src/engine/physics/cloth/collider/cloth_collider_pack.js +7 -15
  191. package/src/engine/physics/cloth/collider/cloth_collider_sdf.d.ts.map +1 -1
  192. package/src/engine/physics/cloth/collider/cloth_collider_sdf.js +16 -61
  193. package/src/engine/physics/cloth/ecs/Cloth.d.ts.map +1 -1
  194. package/src/engine/physics/cloth/ecs/Cloth.js +7 -5
  195. package/src/engine/physics/cloth/ecs/ClothCollider.d.ts.map +1 -1
  196. package/src/engine/physics/cloth/ecs/ClothCollider.js +7 -5
  197. package/src/engine/physics/cloth/ecs/ClothColliderFlags.d.ts +0 -1
  198. package/src/engine/physics/cloth/ecs/ClothColliderFlags.js +6 -19
  199. package/src/engine/physics/cloth/ecs/ClothColliderSystem.d.ts +1 -2
  200. package/src/engine/physics/cloth/ecs/ClothColliderSystem.d.ts.map +1 -1
  201. package/src/engine/physics/cloth/ecs/ClothColliderSystem.js +3 -12
  202. package/src/engine/physics/cloth/ecs/ClothDynamics.d.ts.map +1 -1
  203. package/src/engine/physics/cloth/ecs/ClothDynamics.js +20 -11
  204. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -1
  205. package/src/engine/physics/cloth/ecs/ClothWorld.js +40 -130
  206. package/src/engine/physics/cloth/ecs/cloth_build_rows.d.ts.map +1 -1
  207. package/src/engine/physics/cloth/ecs/cloth_build_rows.js +17 -58
  208. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts.map +1 -1
  209. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.js +3 -3
  210. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts.map +1 -1
  211. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.js +44 -104
  212. package/src/engine/physics/cloth/ecs/cloth_geodesic.d.ts.map +1 -1
  213. package/src/engine/physics/cloth/ecs/cloth_geodesic.js +6 -13
  214. package/src/engine/physics/cloth/ecs/cloth_holds.d.ts.map +1 -1
  215. package/src/engine/physics/cloth/ecs/cloth_holds.js +15 -40
  216. package/src/engine/physics/cloth/ecs/cloth_reconstruct_pose.d.ts.map +1 -1
  217. package/src/engine/physics/cloth/ecs/cloth_reconstruct_pose.js +45 -79
  218. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts.map +1 -1
  219. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.js +7 -27
  220. package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts.map +1 -1
  221. package/src/engine/physics/cloth/ecs/cloth_write_back.js +30 -62
  222. package/src/engine/physics/cloth/playground/cloth_playground_build.d.ts.map +1 -1
  223. package/src/engine/physics/cloth/playground/cloth_playground_build.js +2 -3
  224. package/src/engine/physics/cloth/playground/collide_main.js +2 -28
  225. package/src/engine/physics/cloth/playground/garment_main.js +4 -19
  226. package/src/engine/physics/cloth/playground/main.js +6 -48
  227. package/src/engine/physics/cloth/playground/rig_main.js +947 -955
  228. package/src/engine/physics/cloth/playground/wind_main.js +8 -7
  229. package/src/engine/physics/cloth/solver/ClothState.d.ts.map +1 -1
  230. package/src/engine/physics/cloth/solver/ClothState.js +16 -76
  231. package/src/engine/physics/cloth/solver/cloth_color_particles.d.ts +10 -8
  232. package/src/engine/physics/cloth/solver/cloth_color_particles.d.ts.map +1 -1
  233. package/src/engine/physics/cloth/solver/cloth_color_particles.js +30 -102
  234. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts.map +1 -1
  235. package/src/engine/physics/cloth/solver/cloth_contact_find.js +3 -2
  236. package/src/engine/physics/cloth/solver/cloth_hold_line.d.ts.map +1 -1
  237. package/src/engine/physics/cloth/solver/cloth_hold_line.js +2 -14
  238. package/src/engine/physics/cloth/solver/cloth_kinetic_energy.d.ts +21 -0
  239. package/src/engine/physics/cloth/solver/cloth_kinetic_energy.d.ts.map +1 -0
  240. package/src/engine/physics/cloth/solver/cloth_kinetic_energy.js +45 -0
  241. package/src/engine/physics/cloth/solver/cloth_particle_coupling.d.ts +32 -0
  242. package/src/engine/physics/cloth/solver/cloth_particle_coupling.d.ts.map +1 -0
  243. package/src/engine/physics/cloth/solver/cloth_particle_coupling.js +133 -0
  244. package/src/engine/physics/cloth/solver/cloth_particle_of_source.d.ts +16 -0
  245. package/src/engine/physics/cloth/solver/cloth_particle_of_source.d.ts.map +1 -0
  246. package/src/engine/physics/cloth/solver/cloth_particle_of_source.js +19 -0
  247. package/src/engine/physics/cloth/solver/cloth_self_find.d.ts.map +1 -1
  248. package/src/engine/physics/cloth/solver/cloth_self_find.js +6 -55
  249. package/src/engine/physics/cloth/solver/cloth_solve_vertex.d.ts.map +1 -1
  250. package/src/engine/physics/cloth/solver/cloth_solve_vertex.js +30 -65
  251. package/src/engine/physics/cloth/solver/cloth_step.d.ts.map +1 -1
  252. package/src/engine/physics/cloth/solver/cloth_step.js +6 -28
  253. package/src/engine/physics/cloth/solver/cloth_step_instance.d.ts.map +1 -1
  254. package/src/engine/physics/cloth/solver/cloth_step_instance.js +2 -3
  255. package/src/engine/physics/cloth/solver/cloth_stretch_residual.d.ts +21 -0
  256. package/src/engine/physics/cloth/solver/cloth_stretch_residual.d.ts.map +1 -0
  257. package/src/engine/physics/cloth/solver/cloth_stretch_residual.js +54 -0
  258. package/src/engine/physics/cloth/solver/cloth_vertex_accumulator.d.ts +15 -0
  259. package/src/engine/physics/cloth/solver/cloth_vertex_accumulator.d.ts.map +1 -1
  260. package/src/engine/physics/cloth/solver/cloth_vertex_accumulator.js +28 -0
  261. package/src/engine/physics/cloth/solver/cloth_worst_tether_ratio.d.ts +30 -0
  262. package/src/engine/physics/cloth/solver/cloth_worst_tether_ratio.d.ts.map +1 -0
  263. package/src/engine/physics/cloth/solver/cloth_worst_tether_ratio.js +56 -0
  264. package/src/engine/physics/cloth/solver/constraint/cloth_contact_row.d.ts.map +1 -1
  265. package/src/engine/physics/cloth/solver/constraint/cloth_contact_row.js +3 -17
  266. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts +2 -1
  267. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts.map +1 -1
  268. package/src/engine/physics/cloth/wind/ClothFluidWind.js +7 -25
  269. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts +3 -1
  270. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts.map +1 -1
  271. package/src/engine/physics/cloth/wind/cloth_sample_air.js +40 -62
  272. package/src/engine/physics/ecs/ContactProbeSystem.d.ts.map +1 -1
  273. package/src/engine/physics/ecs/ContactProbeSystem.js +2 -18
  274. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  275. package/src/engine/physics/ecs/PhysicsSystem.js +9 -50
  276. package/src/engine/physics/fluid/FluidField.d.ts.map +1 -1
  277. package/src/engine/physics/fluid/FluidField.js +7 -5
  278. package/src/engine/physics/fluid/ecs/FluidComponent.d.ts.map +1 -1
  279. package/src/engine/physics/fluid/ecs/FluidComponent.js +19 -11
  280. package/src/engine/physics/fluid/ecs/fluid_sample_domain_bounds.d.ts +24 -0
  281. package/src/engine/physics/fluid/ecs/fluid_sample_domain_bounds.d.ts.map +1 -0
  282. package/src/engine/physics/fluid/ecs/fluid_sample_domain_bounds.js +42 -0
  283. package/src/engine/physics/fluid/ecs/fluid_sample_velocity_world_mps.d.ts +25 -0
  284. package/src/engine/physics/fluid/ecs/fluid_sample_velocity_world_mps.d.ts.map +1 -0
  285. package/src/engine/physics/fluid/ecs/fluid_sample_velocity_world_mps.js +40 -0
  286. package/src/engine/physics/fluid/effector/AbstractFluidEffector.d.ts +2 -2
  287. package/src/engine/physics/fluid/effector/AbstractFluidEffector.js +2 -2
  288. package/src/engine/physics/fluid/effector/GlobalFluidEffector.d.ts.map +1 -1
  289. package/src/engine/physics/fluid/effector/GlobalFluidEffector.js +11 -7
  290. package/src/engine/physics/fluid/effector/ImpulseFluidEffector.d.ts.map +1 -1
  291. package/src/engine/physics/fluid/effector/ImpulseFluidEffector.js +11 -7
  292. package/src/engine/physics/fluid/effector/WakeFluidEffector.d.ts.map +1 -1
  293. package/src/engine/physics/fluid/effector/WakeFluidEffector.js +16 -9
  294. package/src/engine/physics/inverse_kinematics/frame_fabrik/frame_fabrik_solve.d.ts.map +1 -1
  295. package/src/engine/physics/inverse_kinematics/frame_fabrik/frame_fabrik_solve.js +40 -90
  296. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
  297. package/src/engine/physics/narrowphase/capsule_contacts.js +3 -5
  298. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts.map +1 -1
  299. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.js +2 -14
  300. package/src/engine/physics/narrowphase/hull_sphere_contact.d.ts.map +1 -1
  301. package/src/engine/physics/narrowphase/hull_sphere_contact.js +2 -5
  302. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  303. package/src/engine/physics/narrowphase/narrowphase_step.js +13 -32
  304. package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
  305. package/src/engine/physics/narrowphase/sphere_box_contact.js +2 -5
  306. package/src/engine/sound/ecs/audio/AudioEmitter.d.ts.map +1 -1
  307. package/src/engine/sound/ecs/audio/AudioEmitter.js +12 -7
  308. package/src/engine/sound/ecs/trigger/AudioEventTrigger.d.ts.map +1 -1
  309. package/src/engine/sound/ecs/trigger/AudioEventTrigger.js +192 -191
  310. package/src/engine/sound/simulation/core/AcousticMedium.js +2 -2
  311. package/src/engine/sound/simulation/definition/AcousticDirectivity.js +2 -2
  312. package/src/engine/sound/simulation/definition/AcousticMaterial.d.ts.map +1 -1
  313. package/src/engine/sound/simulation/definition/AcousticMaterial.js +11 -7
  314. package/src/engine/sound/sopra/definition/BusDefinition.d.ts.map +1 -1
  315. package/src/engine/sound/sopra/definition/BusDefinition.js +5 -4
  316. package/src/engine/sound/sopra/definition/DuckingRule.d.ts.map +1 -1
  317. package/src/engine/sound/sopra/definition/DuckingRule.js +7 -5
  318. package/src/engine/sound/sopra/definition/EventDescription.d.ts.map +1 -1
  319. package/src/engine/sound/sopra/definition/EventDescription.js +23 -13
  320. package/src/engine/sound/sopra/definition/MixerSnapshot.js +2 -2
  321. package/src/engine/sound/sopra/definition/ParameterDefinition.d.ts.map +1 -1
  322. package/src/engine/sound/sopra/definition/ParameterDefinition.js +7 -5
  323. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.js +2 -2
  324. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts.map +1 -1
  325. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.js +4 -3
  326. package/src/engine/sound/sopra/definition/clip/ContainerAudioClip.js +2 -2
  327. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.js +3 -3
  328. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts.map +1 -1
  329. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.js +16 -9
  330. package/src/engine/sound/sopra/definition/clip/SilenceAudioClip.js +2 -2
  331. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts +1 -1
  332. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts.map +1 -1
  333. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.js +2 -2
  334. package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts.map +1 -1
  335. package/src/engine/sound/sopra/definition/effect/CompressorEffect.js +7 -5
  336. package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts.map +1 -1
  337. package/src/engine/sound/sopra/definition/effect/EqEffect.js +8 -5
  338. package/src/engine/sound/sopra/definition/effect/ReverbEffect.js +2 -2
  339. package/src/engine/sound/sopra/legacy/SoundEmitter.d.ts.map +1 -1
  340. package/src/engine/sound/sopra/legacy/SoundEmitter.js +357 -352
  341. package/src/engine/sound/sopra/legacy/SoundTrack.d.ts.map +1 -1
  342. package/src/engine/sound/sopra/legacy/SoundTrack.js +281 -278
  343. package/src/format/image/png/PNGReader.js +644 -644
  344. package/src/format/image/png/crc32.d.ts +1 -1
  345. package/src/format/image/png/crc32.js +7 -8
  346. package/src/format/texture/basis/transcode/etc1s_to_astc.d.ts.map +1 -1
  347. package/src/format/texture/basis/transcode/etc1s_to_astc.js +2 -1
  348. package/src/shade/descriptor/binding/BindGroupLayoutDescriptor.js +2 -2
  349. package/src/shade/descriptor/binding/BindGroupLayoutEntry.d.ts.map +1 -1
  350. package/src/shade/descriptor/binding/BindGroupLayoutEntry.js +12 -8
  351. package/src/shade/descriptor/binding/StorageTextureBindingLayout.js +2 -2
  352. package/src/shade/descriptor/binding/TextureBindingLayout.js +2 -2
  353. package/src/shade/descriptor/blend/BlendComponent.js +2 -2
  354. package/src/shade/descriptor/pipeline/PipelineDescriptorBase.js +2 -2
  355. package/src/shade/descriptor/pipeline/PipelineLayoutDescriptor.js +2 -2
  356. package/src/shade/descriptor/pipeline/ProgrammableStage.js +2 -2
  357. package/src/shade/descriptor/pipeline/compute/ComputePipelineDescriptor.js +2 -2
  358. package/src/shade/descriptor/pipeline/render/ColorTargetState.js +2 -2
  359. package/src/shade/descriptor/pipeline/render/FragmentState.js +2 -2
  360. package/src/shade/descriptor/pipeline/render/PrimitiveState.js +2 -2
  361. package/src/shade/descriptor/pipeline/render/RenderPipelineDescriptor.d.ts.map +1 -1
  362. package/src/shade/descriptor/pipeline/render/RenderPipelineDescriptor.js +8 -5
  363. package/src/shade/descriptor/pipeline/render/VertexAttribute.js +2 -2
  364. package/src/shade/descriptor/texture/SamplerDescriptor.d.ts.map +1 -1
  365. package/src/shade/descriptor/texture/SamplerDescriptor.js +20 -12
  366. package/src/shade/descriptor/texture/TextureDescriptor.d.ts.map +1 -1
  367. package/src/shade/descriptor/texture/TextureDescriptor.js +11 -7
  368. package/src/shade/device/graph/BufferResourceDescriptor.d.ts.map +1 -1
  369. package/src/shade/device/graph/BufferResourceDescriptor.js +196 -193
  370. package/src/shade/device/graph/TextureResourceDescriptor.d.ts.map +1 -1
  371. package/src/shade/device/graph/TextureResourceDescriptor.js +178 -173
  372. package/src/shade/device/timing/profile/sgpt_read_header.js +1 -1
  373. package/src/shade/device/timing/profile/sgpt_read_records.js +1 -1
  374. package/src/shade/device/timing/profile/sgpt_write_header.js +1 -1
  375. package/src/shade/device/timing/profile/sgpt_write_record.js +1 -1
  376. package/src/shade/playground/image_decode_bench/main.js +2 -1
  377. package/src/shade/playground/skinned_blas_refit/diff_traced_against_rasterized.d.ts.map +1 -1
  378. package/src/shade/playground/skinned_blas_refit/diff_traced_against_rasterized.js +2 -1
  379. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts.map +1 -1
  380. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.js +2 -1
  381. package/src/shade/playground/ssr_variance/statistics.d.ts.map +1 -1
  382. package/src/shade/playground/ssr_variance/statistics.js +2 -1
  383. package/src/shade/playground/texture_encode/main.d.ts.map +1 -1
  384. package/src/shade/playground/texture_encode/main.js +2 -1
  385. package/src/shade/playground/volumetrics_froxel/measure.d.ts.map +1 -1
  386. package/src/shade/playground/volumetrics_froxel/measure.js +2 -1
  387. package/src/shade/renderer/binding/BufferBinding.js +2 -2
  388. package/src/shade/renderer/buffer/table/gpu_database_check_links.d.ts.map +1 -1
  389. package/src/shade/renderer/buffer/table/gpu_database_check_links.js +2 -1
  390. package/src/shade/renderer/camera/Camera.js +2 -2
  391. package/src/shade/renderer/geometry/Attribute.js +2 -2
  392. package/src/shade/renderer/geometry/Geometry.js +2 -2
  393. package/src/shade/renderer/geometry/geometry_deduplicate_vertices.js +2 -2
  394. package/src/shade/renderer/geometry/meshlet/MeshletBatch.js +2 -2
  395. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.d.ts.map +1 -1
  396. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.js +3 -2
  397. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_leaf_frontier.d.ts.map +1 -1
  398. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_leaf_frontier.js +2 -1
  399. package/src/shade/renderer/geometry/virtual/build/level/vgeo_partition_into_groups.d.ts.map +1 -1
  400. package/src/shade/renderer/geometry/virtual/build/level/vgeo_partition_into_groups.js +2 -1
  401. package/src/shade/renderer/geometry/virtual/build/level/vgeo_run_round.d.ts.map +1 -1
  402. package/src/shade/renderer/geometry/virtual/build/level/vgeo_run_round.js +2 -1
  403. package/src/shade/renderer/geometry/virtual/build/level/vgeo_try_repack.d.ts.map +1 -1
  404. package/src/shade/renderer/geometry/virtual/build/level/vgeo_try_repack.js +3 -2
  405. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_graph_components.d.ts.map +1 -1
  406. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_graph_components.js +2 -1
  407. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.d.ts.map +1 -1
  408. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.js +2 -1
  409. package/src/shade/renderer/geometry/virtual/format/checksum/vgeo_compute_asset_id.js +13 -13
  410. package/src/shade/renderer/geometry/virtual/format/checksum/vgeo_compute_directory_block_checksum.js +41 -41
  411. package/src/shade/renderer/geometry/virtual/format/checksum/vgeo_compute_frame_checksum.js +41 -41
  412. package/src/shade/renderer/geometry/virtual/format/checksum/vgeo_compute_header_checksum.js +25 -25
  413. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts.map +1 -1
  414. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.js +3 -2
  415. package/src/shade/renderer/material/StandardShadeMaterial.d.ts.map +1 -1
  416. package/src/shade/renderer/material/StandardShadeMaterial.js +13 -8
  417. package/src/shade/renderer/particles/optimizer/dag/particle_vector_extract.d.ts.map +1 -1
  418. package/src/shade/renderer/particles/optimizer/dag/particle_vector_extract.js +3 -2
  419. package/src/shade/renderer/scene/optimization/geometry/internal/triangle_fingerprint_stream.d.ts.map +1 -1
  420. package/src/shade/renderer/scene/optimization/geometry/internal/triangle_fingerprint_stream.js +14 -5
  421. package/src/shade/renderer/scene/optimization/sampler2d_from_image_bitmap.js +1 -1
  422. package/src/shade/renderer/shader/type/WGSLStructField.js +2 -2
  423. package/src/shade/renderer/shader/type/WebGPUArray.js +2 -2
  424. package/src/shade/renderer/shader/type/WebGPUStruct.d.ts.map +1 -1
  425. package/src/shade/renderer/shader/type/WebGPUStruct.js +9 -6
  426. package/src/shade/renderer/texture/ShadeTexture.d.ts.map +1 -1
  427. package/src/shade/renderer/texture/ShadeTexture.js +17 -10
  428. package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.js +2 -2
  429. package/src/shade/renderer/water/surface/chunk_water_surface_sample.d.ts +4 -4
  430. package/src/shade/renderer/water/surface/chunk_water_surface_sample.js +6 -6
  431. package/src/shade/renderer/water/surface/particles/GPUWaterParticles.d.ts.map +1 -1
  432. package/src/shade/renderer/water/surface/particles/GPUWaterParticles.js +46 -55
  433. package/src/shade/renderer/water/surface/particles/WATER_PARTICLE_STRUCT.d.ts +3 -3
  434. package/src/shade/renderer/water/surface/particles/WATER_PARTICLE_STRUCT.d.ts.map +1 -1
  435. package/src/shade/renderer/water/surface/particles/WATER_PARTICLE_STRUCT.js +14 -4
  436. package/src/shade/renderer/water/surface/particles/chunk_water_particle.d.ts +10 -1
  437. package/src/shade/renderer/water/surface/particles/chunk_water_particle.d.ts.map +1 -1
  438. package/src/shade/renderer/water/surface/particles/chunk_water_particle.js +82 -40
  439. package/src/shade/renderer/water/surface/particles/chunk_water_particle_step.d.ts +4 -1
  440. package/src/shade/renderer/water/surface/particles/chunk_water_particle_step.d.ts.map +1 -1
  441. package/src/shade/renderer/water/surface/particles/chunk_water_particle_step.js +8 -4
  442. package/src/shade/renderer/water/surface/particles/shader_water_particles.d.ts +98 -86
  443. package/src/shade/renderer/water/surface/particles/shader_water_particles.d.ts.map +1 -1
  444. package/src/shade/renderer/water/surface/particles/shader_water_particles.js +440 -298
  445. package/src/view/common/HTMLElementCacheKey.d.ts.map +1 -1
  446. package/src/view/common/HTMLElementCacheKey.js +4 -3
  447. package/src/view/minimap/MinimapMarker.js +2 -2
@@ -10,6 +10,7 @@ import { WGSLStructField } from "../../../shader/type/WGSLStructField.js";
10
10
  import { atomic_u32, f32, u32 } from "../../../shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
11
11
  import { COLLECTION_U32_STRUCT_READ } from "../../../gpu_primitive/COLLECTION_U32_STRUCT_READ.js";
12
12
  import { STRUCT_DISPATCH_COMPUTE_COMMAND } from "../../../gpu_primitive/indirect/STRUCT_DISPATCH_COMPUTE_COMMAND.js";
13
+ import { WEBGPU_GUARANTEED_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION } from "../../../gpu_primitive/indirect/WEBGPU_GUARANTEED_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION.js";
13
14
  import { GEOMETRY_METADATA_STRUCT } from "../../../geometry/GEOMETRY_METADATA_STRUCT.js";
14
15
  import { MESHLET_METADATA_STRUCT } from "../../../geometry/meshlet/encoding/MESHLET_METADATA_STRUCT.js";
15
16
  import { BVH2_NODE_STRUCT } from "../../../shader/chunk/bvh/BVH2_NODE_STRUCT.js";
@@ -52,52 +53,56 @@ export const WATER_PARTICLE_CORNERS = 4;
52
53
  export const WATER_PARTICLE_RANK_LIMIT = 64;
53
54
 
54
55
  /**
55
- * Packets a crowded point keeps. A point reads a cell of every level its body has, and a cell is crowded where its most
56
- * crowded point reads more than this: a point of it at its level and the coarser ones, or one under it where finer
57
- * levels hold packets too. Then each run of a front in it is drawn by its packets' least share of their cells: this
58
- * many times the packet's steepness over the summed steepness that point reads, whatever the levels it is read at. So
59
- * about this many stay at a point, the steepest whole wherever they sit, and a front stays whole or goes. What is
60
- * elsewhere on the body does not count, so a splash far off, or a front moving to a coarser level, changes nothing here.
61
- *
62
- * A front's lot is its own for life and the same in every cell it reaches, so the fronts of the lowest lots are kept in
63
- * every crowd they are in, whatever their size: the count at a point varies by fronts' worth around this, and a front
64
- * the outline broke is drawn particle by particle, ~20 entries in one cell. Where a point would read more than the
65
- * gather budget, its cells keep no more of it than their steepness's part, the lowest keys first
66
- * ({@link WATER_PARTICLE_CROWD_BINS}), so no point reads past the budget. A whole front is drawn as the seven it was
67
- * emitted as (water_particle_group), so three fresh splashes cross whole, whatever their steepness and levels; a fourth
68
- * over them thins one.
69
- *
70
- * A cell only one front reaches is not crowded, however many of its packets it holds: one front puts at most ~40 in
71
- * a cell, within the gather budget.
56
+ * How many places along the array either side a particle looks for its front's neighbours. A front keeps its
57
+ * particles in a row and in order — a ring is emitted whole, a split puts its three in its place, the dead leave no
58
+ * gap — and a mirror image sits right after the packet it left, so a neighbour is a place or two away, a few where
59
+ * images split beside their packets. A pair further apart, as a ring's last and first are, is drawn as two runs whose
60
+ * windows meet, the same surface.
72
61
  */
73
- export const WATER_PARTICLE_CELL_SHARE = 24;
62
+ export const WATER_PARTICLE_LINK_REACH = 8;
74
63
 
75
64
  /**
76
- * Octaves a crowded cell counts its entries in, by the key its lottery keeps them by (a front's lot over its
77
- * steepness; the lowest kept first), below the most it can keep: bin j holds keys from 2^-(j+1) of that reach to
78
- * 2^-j of it, the last everything below. A cell whose point could read past the gather budget drops its octaves from
79
- * the top until the rest fit its part of the budget, so it may keep up to an octave's worth fewer than would fit.
65
+ * Most particles one entry draws. A run ends where its neighbours' supports leave its cell. A cell is under four
66
+ * support radii s across, so the particles reaching it stand within a square of under 6s, and an arc of a circle inside a
67
+ * square of side L is at most about πL long; the step keeps a front's neighbours at least a third of their envelope's
68
+ * half-length h apart (they split at h), so s is at most ~3.2 of their spacing, and a cell holds at most ~62 of one
69
+ * front. A front closer set than the step leaves is drawn with a hole past this many.
80
70
  */
81
- export const WATER_PARTICLE_CROWD_BINS = 4;
71
+ export const WATER_PARTICLE_RUN_LIMIT = 96;
82
72
 
83
73
  /**
84
- * Words a bucket's crowd takes, as the weigh pass sums it: its summed steepness, its largest front and complement of a
85
- * front entered, and its entries.
74
+ * Steepness classes a cell counts its runs in: half octaves of |a|/r below 0.1, the steepest a splash makes, the last
75
+ * everything flatter. A run's class is its front's, the same in every cell it reaches. The cut pass reads a cell's
76
+ * classes and total as vectors, written out for ten (WaterParticleCrowd).
86
77
  */
87
- export const WATER_PARTICLE_CROWD_WORDS = 4;
78
+ export const WATER_PARTICLE_CROWD_BINS = 10;
79
+
80
+ /** Words a bucket's crowd takes, as the weigh pass counts it: its runs by steepness class, then all of them. */
81
+ export const WATER_PARTICLE_CROWD_WORDS = WATER_PARTICLE_CROWD_BINS + 1;
88
82
 
89
83
  /**
90
- * Words the nest pass adds to a bucket's crowd, in a buffer of their own so that it reads weigh's sums as plain loads:
91
- * the most entries and the most summed steepness a point under it reads where finer levels hold packets, and its
92
- * entries by key, {@link WATER_PARTICLE_CROWD_BINS} octaves.
84
+ * Words the nest, cut and mark passes keep per bucket: the most runs a point under it reads at finer levels and its own
85
+ * and the coarser ones; how many steepness classes its points keep, the steepest first, within
86
+ * {@link WATER_PARTICLE_CROWD_KEEP}, and within the gather budget; and a slot of the frame's table of cut fronts.
93
87
  */
94
- export const WATER_PARTICLE_NEST_WORDS = 2 + WATER_PARTICLE_CROWD_BINS;
88
+ export const WATER_PARTICLE_NEST_WORDS = 4;
95
89
 
96
- /** Width of the ramp a thinned packet fades over, a fraction of its share. */
97
- export const WATER_PARTICLE_THINNING_RAMP = 0.25;
90
+ /** Slots of the cut fronts' table a front's tag is looked for in, from its own: at most an eighth of them hold one. */
91
+ export const WATER_PARTICLE_FRONT_PROBES = 8;
92
+
93
+ /**
94
+ * Runs a crowded point keeps: the steepest, class by class, as many as fit. A run a crowd cannot keep at any of its
95
+ * cells dooms its particles (WATER_PARTICLE_STRUCT's `fading`): they fade and die as packets below the drawing threshold
96
+ * do, so the weakest go first and nothing doomed comes back. The surface sampler's gather budget above this holds the
97
+ * doomed while they fade; a run past the budget itself, as where fresh fronts arrive faster than doomed ones fade, is
98
+ * cut: it is not drawn and dies at its next step, and so does all of its front, which a cut would otherwise leave in
99
+ * pieces no cell counted. The steepest class is always kept: a fresh splash is drawn, though points where more of those
100
+ * cross than the budget holds then lose some.
101
+ */
102
+ export const WATER_PARTICLE_CROWD_KEEP = 48;
98
103
 
99
- /** Fixed-point scale of a packet's steepness in its cells' sums. */
100
- const WATER_PARTICLE_WEIGHT_SCALE = 1 << 20;
104
+ /** A doomed particle's `fading` that kills it at its next step: no steepness reaches it. */
105
+ const WATER_PARTICLE_CUT = 1e30;
101
106
 
102
107
  const STRIDE = WATER_PARTICLE_BODY_BOUNDS_STRIDE;
103
108
  const WORDS = WATER_PARTICLE_CROWD_WORDS;
@@ -303,7 +308,7 @@ fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) la
303
308
  let ridge = select(0.0, height, base + count <= capacity);
304
309
  for (var k = 0u; k < count; k++) {
305
310
  if (base + k >= capacity) { break; }
306
- next.elements[base + k] = ${WATER_PARTICLE_STRUCT.wgsl_ref}(impulse.xy * scale, f32(k) * dispersion, age, ridge, radius, dispersion, row, 0u);
311
+ next.elements[base + k] = ${WATER_PARTICLE_STRUCT.wgsl_ref}(impulse.xy * scale, f32(k) * dispersion, age, ridge, radius, dispersion, row, 0u, 0.0);
307
312
  }
308
313
  held += f32(count) * abs(ridge);
309
314
  }
@@ -324,10 +329,16 @@ finalize_resources.createGroup()
324
329
  .addStorageBuffer('offsets', COLLECTION_U32_STRUCT_READ, true)
325
330
  .addStorageBuffer('table_command', STRUCT_DISPATCH_COMPUTE_COMMAND, true);
326
331
 
332
+ /**
333
+ * Words the frame's clear writes per bucket of the table: its count, its largest |a|, its crowd and its nest, four to a
334
+ * thread (the table is a power of two of at least {@link WATER_PARTICLE_TABLE_MIN} buckets, so each array is whole vec4s).
335
+ */
336
+ export const WATER_PARTICLE_CLEAR_WORDS = 2 + WATER_PARTICLE_CROWD_WORDS + WATER_PARTICLE_NEST_WORDS;
337
+
327
338
  /**
328
339
  * The live count, clamped to capacity (emitters append past it), and the frame's hash table size: twice the
329
340
  * entries the particles can take, a power of two, at least ${WATER_PARTICLE_TABLE_MIN}; and the command that clears
330
- * that much of the table, a thread a bucket.
341
+ * that much of the table, a thread every four words, folded.
331
342
  */
332
343
  export const shader_water_particles_finalize = ComputeShader.from({ label: 'Water / particles finalize', resources: finalize_resources,
333
344
  body: CodeChunk.from(/* language=WGSL */ `
@@ -338,8 +349,9 @@ fn main() {
338
349
  let wanted = max(2u * ${WATER_PARTICLE_CORNERS}u * live, ${WATER_PARTICLE_TABLE_MIN}u);
339
350
  let table = min(1u << (32u - countLeadingZeros(wanted - 1u)), limits.table_limit);
340
351
  offsets.count = table;
341
- table_command.workgroupCountX = (table + 63u) / 64u;
342
- table_command.workgroupCountY = 1u;
352
+ let groups = (table / 4u * ${WATER_PARTICLE_CLEAR_WORDS}u + 63u) / 64u;
353
+ table_command.workgroupCountX = min(groups, ${WEBGPU_GUARANTEED_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION}u);
354
+ table_command.workgroupCountY = (groups + ${WEBGPU_GUARANTEED_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION - 1}u) / ${WEBGPU_GUARANTEED_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION}u;
343
355
  table_command.workgroupCountZ = 1u;
344
356
  }
345
357
  `, [WATER_PARTICLE_STRUCT.declaration_chunk]) });
@@ -347,77 +359,83 @@ fn main() {
347
359
  const clear_resources = new ShaderResourceSetDescriptor();
348
360
  clear_resources.createGroup()
349
361
  .addStorageBuffer('buckets', WATER_PARTICLE_BUCKETS_STRUCT, true)
350
- .addStorageBuffer('bucket_amplitude', WebGPUArray.u32, true)
351
- .addStorageBuffer('bucket_weight', WebGPUArray.u32, true)
352
- .addStorageBuffer('bucket_nest', WebGPUArray.u32, true);
362
+ .addStorageBuffer('bucket_amplitude', WebGPUArray.vec4u, true)
363
+ .addStorageBuffer('bucket_crowd', WebGPUArray.vec4u, true)
364
+ .addStorageBuffer('bucket_nest', WebGPUArray.vec4u, true);
353
365
 
354
366
  /**
355
- * Zero the frame's table, as far as finalize sized it: each bucket's count, largest |a| and crowd. A frame pays for the
356
- * table its particles take, not for the pool's capacity; the buckets past it are never read.
367
+ * Clear the frame's table, as far as finalize sized it: each bucket's count, largest |a| and crowd, its points let keep
368
+ * and draw every steepness class until the cut pass says otherwise, and its slot of the cut fronts' table. A thread
369
+ * every four words, the words of each array in a row, so neighbouring threads write neighbouring words; a bucket's nest
370
+ * is its four. A frame pays for the table its particles take, not for the pool's capacity; the buckets past it are
371
+ * never read.
357
372
  */
358
373
  export const shader_water_particles_clear = ComputeShader.from({ label: 'Water / particles clear', resources: clear_resources,
359
374
  body: CodeChunk.from(/* language=WGSL */ `
360
375
  @compute @workgroup_size(64)
361
376
  fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
362
- let b = folded_workgroup_index(group) * 64u + lane;
363
- if (b >= buckets.count) { return; }
364
- atomicStore(&buckets.elements[b], 0u);
365
- bucket_amplitude[b] = 0u;
366
- for (var word = 0u; word < ${WORDS}u; word++) { bucket_weight[${WORDS}u * b + word] = 0u; }
367
- for (var word = 0u; word < ${NEST}u; word++) { bucket_nest[${NEST}u * b + word] = 0u; }
377
+ let table = buckets.count;
378
+ let quarter = table / 4u;
379
+ var at = folded_workgroup_index(group) * 64u + lane;
380
+ if (at < quarter) {
381
+ for (var word = 4u * at; word < 4u * at + 4u; word++) { atomicStore(&buckets.elements[word], 0u); }
382
+ return;
383
+ }
384
+ at -= quarter;
385
+ if (at < quarter) { bucket_amplitude[at] = vec4<u32>(0u); return; }
386
+ at -= quarter;
387
+ if (at < ${WORDS}u * quarter) { bucket_crowd[at] = vec4<u32>(0u); return; }
388
+ at -= ${WORDS}u * quarter;
389
+ if (at < table) { bucket_nest[at] = vec4<u32>(0u, ${BINS}u, ${BINS}u, 0u); }
368
390
  }
369
391
  `, [chunk_folded_workgroup_index]) });
370
392
 
371
393
  /**
372
- * A live particle's place in the hash this frame: its kernel at this time, its level and the cells its
373
- * support overlaps (one or two a side, the level's cell being at least the support's diameter).
394
+ * A live particle's place in the hash this frame: the level and the cells its support overlaps (one or two a side, the
395
+ * level's cell being at least the support's diameter), and the entry it makes.
374
396
  */
375
397
  const chunk_water_particle_cells = CodeChunk.from(/* language=WGSL */ `
376
- struct WaterParticleCells {
377
- entry: ${WATER_PARTICLE_ENTRY_STRUCT.wgsl_ref},
398
+ struct WaterParticleReach {
378
399
  level: u32,
379
400
  first: vec2<i32>,
380
401
  span: vec2<u32>,
381
402
  }
382
403
 
383
- fn water_particle_cells(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}, source: u32) -> WaterParticleCells {
384
- var cells: WaterParticleCells;
404
+ // The cells a particle's support overlaps at its level.
405
+ fn water_particle_reach(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}) -> WaterParticleReach {
406
+ var reach: WaterParticleReach;
385
407
  let position = water_particle_position(particle);
386
- let width = water_particle_width(particle);
387
408
  let distance = water_particle_distance(particle);
388
- let half_length = water_particle_half_length_at(particle.radius, distance);
389
- let support = water_particle_support(half_length, width, particle.dispersion);
390
- cells.level = water_particle_level(support);
391
- let size = water_particle_cell_size(cells.level);
392
- cells.first = vec2<i32>(floor((position - vec2<f32>(support)) / size));
409
+ let support = water_particle_support(water_particle_half_length_at(particle.radius, distance), water_particle_width(particle),
410
+ particle.dispersion);
411
+ reach.level = water_particle_level(support);
412
+ let size = water_particle_cell_size(reach.level);
413
+ reach.first = vec2<i32>(floor((position - vec2<f32>(support)) / size));
393
414
  let last = vec2<i32>(floor((position + vec2<f32>(support)) / size));
394
- cells.span = vec2<u32>(clamp(last - cells.first, vec2<i32>(0), vec2<i32>(1))) + vec2<u32>(1u);
395
- cells.entry = ${WATER_PARTICLE_ENTRY_STRUCT.wgsl_ref}(position, cells.first, particle.angle, particle.amplitude, particle.radius,
396
- particle.dispersion, water_particle_phase(particle), water_particle_key(particle.body, cells.level), source, distance, half_length);
397
- return cells;
398
- }
399
-
400
- fn water_particle_corner_cell(cells: WaterParticleCells, corner: u32) -> vec2<i32> {
401
- return cells.first + vec2<i32>(i32(corner & 1u), i32(corner >> 1u));
415
+ reach.span = vec2<u32>(clamp(last - reach.first, vec2<i32>(0), vec2<i32>(1))) + vec2<u32>(1u);
416
+ return reach;
402
417
  }
403
418
 
404
- fn water_particle_has_corner(cells: WaterParticleCells, corner: u32) -> bool {
405
- return all(vec2<u32>(corner & 1u, corner >> 1u) < cells.span);
419
+ // The entry a particle makes as a lone packet, for the first cell it reaches.
420
+ fn water_particle_entry(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}, source: u32, reach: WaterParticleReach) -> ${WATER_PARTICLE_ENTRY_STRUCT.wgsl_ref} {
421
+ let distance = water_particle_distance(particle);
422
+ return ${WATER_PARTICLE_ENTRY_STRUCT.wgsl_ref}(water_particle_position(particle), reach.first, particle.angle, particle.amplitude,
423
+ particle.radius, particle.dispersion, water_particle_phase(particle), water_particle_key(particle.body, reach.level), source,
424
+ distance, water_particle_half_length_at(particle.radius, distance), WATER_PARTICLE_LONE);
406
425
  }
407
426
 
408
- // A packet's claim on its cells: its steepness, fixed point.
409
- fn water_particle_weight(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}) -> u32 {
410
- return u32(min(abs(particle.amplitude) / max(particle.radius, 1e-6), 1.0) * ${WATER_PARTICLE_WEIGHT_SCALE}.0);
427
+ fn water_particle_corner_cell(reach: WaterParticleReach, corner: u32) -> vec2<i32> {
428
+ return reach.first + vec2<i32>(i32(corner & 1u), i32(corner >> 1u));
411
429
  }
412
430
 
413
- // The front a packet belongs to: from its centre, the same for every packet of the front.
414
- fn water_particle_front(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}) -> u32 {
415
- return pcg3d(vec3<u32>(bitcast<u32>(particle.origin.x), bitcast<u32>(particle.origin.y), particle.body)).x;
431
+ fn water_particle_has_corner(reach: WaterParticleReach, corner: u32) -> bool {
432
+ return all(vec2<u32>(corner & 1u, corner >> 1u) < reach.span);
416
433
  }
417
434
 
418
- // A number in [0, 1) that stays with a front while it travels, the same for all its packets.
419
- fn water_particle_lot(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}) -> f32 {
420
- return f32(water_particle_front(particle) >> 8u) * (1.0 / 16777216.0);
435
+ // Whether a particle's support overlaps a cell of its level.
436
+ fn water_particle_reaches(reach: WaterParticleReach, cell: vec2<i32>) -> bool {
437
+ let offset = cell - reach.first;
438
+ return all(offset >= vec2<i32>(0)) && all(vec2<u32>(offset) < reach.span);
421
439
  }
422
440
 
423
441
  // The bucket of the cell holding a cell's points at a level as coarse as its own or coarser, as the sampler finds it.
@@ -425,120 +443,111 @@ fn water_particle_bucket_over(body: u32, level: u32, cell: vec2<i32>, over: u32,
425
443
  return water_particle_bucket(body, over, cell >> vec2<u32>(over - level), table_size);
426
444
  }
427
445
 
428
- // The key a crowd keeps a packet by: its front's lot over its steepness. A packet is drawn while its lot is under its
429
- // share, which is its steepness times its cell's, so a crowd keeps the packets of its lowest keys, the steepest first.
430
- fn water_particle_precedence(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}) -> f32 {
431
- return water_particle_lot(particle) / f32(water_particle_weight(particle));
432
- }
433
-
434
- // The largest key a crowded cell's lottery can keep, from the summed steepness a point of it reads at its level and
435
- // the coarser ones: its most crowded point reads at least that, and the ramp draws a lot up to its share above it.
436
- fn water_particle_crowd_reach(steepness: u32) -> f32 {
437
- return ${1 + WATER_PARTICLE_THINNING_RAMP} * ${WATER_PARTICLE_CELL_SHARE}.0 / f32(steepness);
446
+ // What makes a front's particles alike (water_particle_alike) but their angle, hashed: a tag, never 0, and where in the
447
+ // cut fronts' table it is looked for.
448
+ fn water_particle_front_hash(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}) -> vec2<u32> {
449
+ let a = pcg3d(vec3<u32>(bitcast<u32>(particle.origin.x), bitcast<u32>(particle.origin.y), particle.body));
450
+ let b = pcg3d(vec3<u32>(bitcast<u32>(particle.age) ^ a.x, bitcast<u32>(particle.amplitude) ^ a.y, bitcast<u32>(particle.radius) ^ a.z));
451
+ let c = pcg3d(vec3<u32>(bitcast<u32>(particle.dispersion) ^ b.x, b.y, b.z));
452
+ return vec2<u32>(c.x | 1u, c.y);
438
453
  }
439
454
 
440
- // The octave of a cell's reach a key falls in, from the exponent of their ratio: [2^-(j+1), 2^-j) is bin j, the last
441
- // takes everything below, and one past the reach is not counted (${BINS}u).
442
- fn water_particle_crowd_bin(precedence: f32, reach: f32) -> u32 {
443
- let ratio = precedence / reach;
444
- if (!(ratio < 1.0)) { return ${BINS}u; }
445
- return min(126u - ((bitcast<u32>(ratio) >> 23u) & 0xffu), ${BINS - 1}u);
446
- }
447
-
448
- // How much of a packet its crowd leaves drawn, from its share of the most crowded of its cells: all of it from a share
449
- // of one, and below that, all of it while its lot is under its share, fading to none a ramp of that share above, and
450
- // none from a share of none. So a cell keeps about its share whatever its crowd.
451
- fn water_particle_visibility(share: f32, lot: f32) -> f32 {
452
- if (!(share > 0.0)) { return 0.0; }
453
- return clamp((share - lot) / (${WATER_PARTICLE_THINNING_RAMP} * share) + 1.0, 0.0, 1.0);
455
+ // A run's steepness class (${BINS} half octaves of |a|/r below 0.1, the steepest first): its front's, alike at every
456
+ // particle of the run and in every cell it reaches.
457
+ fn water_particle_crowd_class(particle: ${WATER_PARTICLE_STRUCT.wgsl_ref}) -> u32 {
458
+ let steepness = abs(particle.amplitude) / max(particle.radius, 1e-30);
459
+ if (!(steepness > 0.0)) { return ${BINS - 1}u; }
460
+ return u32(clamp(floor(2.0 * log2(0.1 / steepness)), 0.0, ${BINS - 1}.0));
454
461
  }
455
462
  `, [chunk_water_particle]);
456
463
 
457
- const fronts_resources = new ShaderResourceSetDescriptor();
458
- fronts_resources.createGroup()
459
- .addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
460
- .addStorageBuffer('fronts', COLLECTION_U32_STRUCT_READ, true);
461
-
462
464
  /**
463
- * Where each front's run of particles begins: 1 at a particle not of the front before it, else 0. A front keeps its
464
- * particles in a row and in order — a ring is emitted whole, a split puts its three in its place, the dead leave no
465
- * gap — so the inclusive scan of these numbers each run from 1. A mirror image follows its packet in the row: it
466
- * breaks its front's run there, as a death does, and is a front of its own.
465
+ * A particle's cells as the links pass wrote them for the frame: three words, its first cell and its level with its
466
+ * spans. Bindings: `reaches`.
467
467
  */
468
- export const shader_water_particles_fronts = ComputeShader.from({ label: 'Water / particles fronts', resources: fronts_resources,
469
- body: CodeChunk.from(/* language=WGSL */ `
470
- @compute @workgroup_size(64)
471
- fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
472
- let i = folded_workgroup_index(group) * 64u + lane;
473
- if (i >= next.count) { return; }
474
- fronts.elements[i] = select(1u, 0u, i > 0u && water_particle_same_front(next.elements[i - 1u], next.elements[i]));
468
+ const chunk_water_particle_reach_read = CodeChunk.from(/* language=WGSL */ `
469
+ fn water_particle_reach_at(i: u32) -> WaterParticleReach {
470
+ let packed = reaches[3u * i + 2u];
471
+ return WaterParticleReach(packed & 0xffu, vec2<i32>(bitcast<i32>(reaches[3u * i]), bitcast<i32>(reaches[3u * i + 1u])),
472
+ vec2<u32>(((packed >> 8u) & 1u) + 1u, ((packed >> 9u) & 1u) + 1u));
475
473
  }
476
- `, [chunk_water_particle, chunk_folded_workgroup_index]) });
477
474
 
478
- const runs_resources = new ShaderResourceSetDescriptor();
479
- runs_resources.createGroup()
480
- .addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
481
- .addStorageBuffer('fronts', COLLECTION_U32_STRUCT_READ)
482
- .addStorageBuffer('runs', WebGPUArray.u32, true);
475
+ // No cells: what a missing neighbour reaches.
476
+ fn water_particle_reach_none() -> WaterParticleReach {
477
+ return WaterParticleReach(0u, vec2<i32>(0), vec2<u32>(0u));
478
+ }
479
+ `, [chunk_water_particle_cells]);
483
480
 
484
481
  /**
485
- * Two words a run: its first particle, and the share its crowd leaves it, none yet (the share pass lowers it).
482
+ * Whether a particle begins a run of its front in a cell its support overlaps: it has no neighbour before it round the
483
+ * front, or that neighbour's support does not overlap the cell. Each run is drawn in the cell by its first particle,
484
+ * once. Bindings: `links`, `reaches`.
486
485
  */
487
- export const shader_water_particles_runs = ComputeShader.from({ label: 'Water / particles runs', resources: runs_resources,
488
- body: CodeChunk.from(/* language=WGSL */ `
489
- @compute @workgroup_size(64)
490
- fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
491
- let i = folded_workgroup_index(group) * 64u + lane;
492
- if (i >= next.count) { return; }
493
- let run = fronts.elements[i];
494
- if (i == 0u || fronts.elements[i - 1u] != run) {
495
- runs[2u * (run - 1u)] = i;
496
- runs[2u * (run - 1u) + 1u] = bitcast<u32>(1e30);
497
- }
486
+ const chunk_water_particle_run_start = CodeChunk.from(/* language=WGSL */ `
487
+ // The cells the support of a particle's neighbour before it overlaps; none without one.
488
+ fn water_particle_before(i: u32) -> WaterParticleReach {
489
+ let before = links[2u * i];
490
+ if (before == ~0u) { return water_particle_reach_none(); }
491
+ return water_particle_reach_at(before);
498
492
  }
499
- `, [WATER_PARTICLE_STRUCT.declaration_chunk, chunk_folded_workgroup_index]) });
500
493
 
501
- const groups_resources = new ShaderResourceSetDescriptor();
502
- groups_resources.createGroup()
494
+ fn water_particle_starts_run(before: WaterParticleReach, cell: vec2<i32>) -> bool {
495
+ return !water_particle_reaches(before, cell);
496
+ }
497
+ `, [chunk_water_particle_reach_read]);
498
+
499
+ const links_resources = new ShaderResourceSetDescriptor();
500
+ links_resources.createGroup()
503
501
  .addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
504
- .addStorageBuffer('fronts', COLLECTION_U32_STRUCT_READ)
505
- .addStorageBuffer('runs', WebGPUArray.u32)
506
- .addStorageBuffer('draw', WebGPUArray.u32, true);
502
+ .addStorageBuffer('links', WebGPUArray.u32, true)
503
+ .addStorageBuffer('reaches', WebGPUArray.u32, true);
507
504
 
508
505
  /**
509
- * Two words a particle: its run, and how the frame draws it ({@link chunk_water_particle}'s water_particle_group): 1 as
510
- * itself, 0 not at all, 3^g for the middle of a whole front's group of 3^g, drawn as one packet.
506
+ * Two words a particle: its front's neighbours β before and after it round the front (water_particle_linked), or none,
507
+ * from the particles {@link WATER_PARTICLE_LINK_REACH} places either side of it. A pair is linked from both ends or
508
+ * neither, so each draws the other's side of their meeting or neither does, and their windows sum to one. And three
509
+ * more: the cells its support overlaps, which every pass after reads rather than works out again.
511
510
  */
512
- export const shader_water_particles_groups = ComputeShader.from({ label: 'Water / particles groups', resources: groups_resources,
511
+ export const shader_water_particles_links = ComputeShader.from({ label: 'Water / particles links', resources: links_resources,
513
512
  body: CodeChunk.from(/* language=WGSL */ `
514
513
  @compute @workgroup_size(64)
515
514
  fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
516
515
  let i = folded_workgroup_index(group) * 64u + lane;
517
516
  let count = next.count;
518
517
  if (i >= count) { return; }
519
- let run = fronts.elements[i] - 1u;
520
- let start = runs[2u * run];
521
- var end = count;
522
- if (run + 1u < fronts.elements[count - 1u]) { end = runs[2u * (run + 1u)]; }
523
- draw[2u * i] = run;
524
- draw[2u * i + 1u] = water_particle_group(next.elements[i], end - start, i - start);
518
+ let particle = next.elements[i];
519
+ var before = ~0u;
520
+ var after = ~0u;
521
+ let last = min(i + ${WATER_PARTICLE_LINK_REACH}u, count - 1u);
522
+ for (var j = max(i, ${WATER_PARTICLE_LINK_REACH}u) - ${WATER_PARTICLE_LINK_REACH}u; j <= last; j++) {
523
+ if (j == i) { continue; }
524
+ let other = next.elements[j];
525
+ if (water_particle_linked(other, particle)) { before = j; }
526
+ if (water_particle_linked(particle, other)) { after = j; }
527
+ }
528
+ links[2u * i] = before;
529
+ links[2u * i + 1u] = after;
530
+ let reach = water_particle_reach(particle);
531
+ reaches[3u * i] = bitcast<u32>(reach.first.x);
532
+ reaches[3u * i + 1u] = bitcast<u32>(reach.first.y);
533
+ reaches[3u * i + 2u] = reach.level | ((reach.span.x - 1u) << 8u) | ((reach.span.y - 1u) << 9u);
525
534
  }
526
- `, [chunk_water_particle, chunk_folded_workgroup_index]) });
535
+ `, [chunk_water_particle_cells, chunk_folded_workgroup_index]) });
527
536
 
528
537
  const weigh_resources = new ShaderResourceSetDescriptor();
529
538
  weigh_resources.createGroup()
530
539
  .addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
531
540
  .addStorageBuffer('activity', WebGPUArray.u32)
532
541
  .addStorageBuffer('offsets', COLLECTION_U32_STRUCT_READ)
533
- .addStorageBuffer('draw', WebGPUArray.u32)
534
- .addStorageBuffer('bucket_weight', WebGPUArray.from(atomic_u32), true)
542
+ .addStorageBuffer('links', WebGPUArray.u32)
543
+ .addStorageBuffer('reaches', WebGPUArray.u32)
544
+ .addStorageBuffer('bucket_crowd', WebGPUArray.from(atomic_u32), true)
535
545
  .addStorageBuffer('body_bounds', WebGPUArray.from(atomic_u32), true);
536
546
 
537
547
  /**
538
- * Each packet drawn of a body simulated this frame adds its steepness to every cell its support overlaps: how crowded
539
- * a cell is, for the share pass to thin it by. Per bucket ({@link WATER_PARTICLE_CROWD_WORDS}) the sum, the largest
540
- * front and complement of a front entered, which are each other's complements only where one front alone was, and the
541
- * entries. And its level to its body's: a point reads a cell of every level its body has.
548
+ * Each run of a body simulated this frame counted into every cell it is drawn in, by its steepness class
549
+ * ({@link WATER_PARTICLE_CROWD_WORDS}): how crowded the cell is, for the cut pass to keep its steepest by. And its level
550
+ * to its body's: a point reads a cell of every level its body has.
542
551
  */
543
552
  export const shader_water_particles_weigh = ComputeShader.from({ label: 'Water / particles weigh', resources: weigh_resources,
544
553
  body: CodeChunk.from(/* language=WGSL */ `
@@ -546,40 +555,38 @@ export const shader_water_particles_weigh = ComputeShader.from({ label: 'Water /
546
555
  fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
547
556
  let i = folded_workgroup_index(group) * 64u + lane;
548
557
  if (i >= next.count) { return; }
549
- let drawn = draw[2u * i + 1u];
550
- if (drawn == 0u) { return; }
551
- let particle = water_particle_drawn(next.elements[i], drawn);
558
+ let particle = next.elements[i];
552
559
  if ((activity[particle.body] & ${WaterActivity.Active}u) == 0u) { return; }
553
- let cells = water_particle_cells(particle, i);
554
- let weight = water_particle_weight(particle);
555
- let front = water_particle_front(particle);
560
+ let cells = water_particle_reach_at(i);
561
+ let grade = water_particle_crowd_class(particle);
562
+ let before = water_particle_before(i);
556
563
  atomicOr(&body_bounds[particle.body * ${STRIDE}u], 1u << cells.level);
557
564
  for (var corner = 0u; corner < ${WATER_PARTICLE_CORNERS}u; corner++) {
558
565
  if (!water_particle_has_corner(cells, corner)) { continue; }
559
- let at = ${WATER_PARTICLE_CROWD_WORDS}u * water_particle_bucket(particle.body, cells.level, water_particle_corner_cell(cells, corner), offsets.count);
560
- atomicAdd(&bucket_weight[at], weight);
561
- atomicMax(&bucket_weight[at + 1u], front);
562
- atomicMax(&bucket_weight[at + 2u], ~front);
563
- atomicAdd(&bucket_weight[at + 3u], 1u);
566
+ let cell = water_particle_corner_cell(cells, corner);
567
+ if (!water_particle_starts_run(before, cell)) { continue; }
568
+ let at = ${WORDS}u * water_particle_bucket(particle.body, cells.level, cell, offsets.count);
569
+ atomicAdd(&bucket_crowd[at + grade], 1u);
570
+ atomicAdd(&bucket_crowd[at + ${BINS}u], 1u);
564
571
  }
565
572
  }
566
- `, [chunk_water_particle_cells, chunk_folded_workgroup_index]) });
573
+ `, [chunk_water_particle_run_start, chunk_folded_workgroup_index]) });
567
574
 
568
575
  const nest_resources = new ShaderResourceSetDescriptor();
569
576
  nest_resources.createGroup()
570
577
  .addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
571
578
  .addStorageBuffer('activity', WebGPUArray.u32)
572
579
  .addStorageBuffer('offsets', COLLECTION_U32_STRUCT_READ)
573
- .addStorageBuffer('draw', WebGPUArray.u32)
580
+ .addStorageBuffer('links', WebGPUArray.u32)
581
+ .addStorageBuffer('reaches', WebGPUArray.u32)
574
582
  .addStorageBuffer('body_bounds', WebGPUArray.u32)
575
- .addStorageBuffer('bucket_weight', WebGPUArray.u32)
583
+ .addStorageBuffer('bucket_crowd', WebGPUArray.u32)
576
584
  .addStorageBuffer('bucket_nest', WebGPUArray.from(atomic_u32), true);
577
585
 
578
586
  /**
579
- * Each packet drawn of a body simulated this frame, its cells weighed: what a point of each of its cells reads at its
580
- * level and the coarser ones, entries and summed steepness, raised onto the cell over it at every coarser level its
581
- * body has, so a cell knows its most crowded point where finer levels hold packets under it; and its entry counted
582
- * among its cell's by its key (water_particle_crowd_bin), for the share pass to cap a crowd by.
587
+ * Each run of a body simulated this frame, its cells weighed: the runs a point of the cell it is drawn in reads at that
588
+ * level and the coarser ones, raised onto the cell over it at every coarser level its body has, so a cell knows the most
589
+ * its points read where finer levels hold runs under it.
583
590
  */
584
591
  export const shader_water_particles_nest = ComputeShader.from({ label: 'Water / particles nest', resources: nest_resources,
585
592
  body: CodeChunk.from(/* language=WGSL */ `
@@ -587,115 +594,238 @@ export const shader_water_particles_nest = ComputeShader.from({ label: 'Water /
587
594
  fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
588
595
  let i = folded_workgroup_index(group) * 64u + lane;
589
596
  if (i >= next.count) { return; }
590
- let drawn = draw[2u * i + 1u];
591
- if (drawn == 0u) { return; }
592
- let particle = water_particle_drawn(next.elements[i], drawn);
597
+ let particle = next.elements[i];
593
598
  if ((activity[particle.body] & ${WaterActivity.Active}u) == 0u) { return; }
594
- let cells = water_particle_cells(particle, i);
595
- let precedence = water_particle_precedence(particle);
599
+ let cells = water_particle_reach_at(i);
596
600
  // its body's levels coarser than its own
597
601
  let coarser = body_bounds[particle.body * ${STRIDE}u] & ~((2u << cells.level) - 1u);
602
+ let before = water_particle_before(i);
598
603
  for (var corner = 0u; corner < ${WATER_PARTICLE_CORNERS}u; corner++) {
599
604
  if (!water_particle_has_corner(cells, corner)) { continue; }
600
605
  let cell = water_particle_corner_cell(cells, corner);
601
- let bucket = water_particle_bucket(particle.body, cells.level, cell, offsets.count);
606
+ if (!water_particle_starts_run(before, cell)) { continue; }
602
607
  // what a point of the cell reads at its level and the coarser ones
603
- var entries = bucket_weight[${WORDS}u * bucket + 3u];
604
- var steepness = bucket_weight[${WORDS}u * bucket];
608
+ var runs = bucket_crowd[${WORDS}u * water_particle_bucket(particle.body, cells.level, cell, offsets.count) + ${BINS}u];
605
609
  var above = coarser;
606
610
  for (var n = 0u; n < WATER_PARTICLE_LEVELS && above != 0u; n++) {
607
- let over = ${WORDS}u * water_particle_bucket_over(particle.body, cells.level, cell, firstTrailingBit(above), offsets.count);
611
+ let over = water_particle_bucket_over(particle.body, cells.level, cell, firstTrailingBit(above), offsets.count);
608
612
  above &= above - 1u;
609
- entries += bucket_weight[over + 3u];
610
- steepness += bucket_weight[over];
613
+ runs += bucket_crowd[${WORDS}u * over + ${BINS}u];
611
614
  }
612
615
  // onto every cell over it
613
616
  above = coarser;
614
617
  for (var n = 0u; n < WATER_PARTICLE_LEVELS && above != 0u; n++) {
615
- let over = ${NEST}u * water_particle_bucket_over(particle.body, cells.level, cell, firstTrailingBit(above), offsets.count);
618
+ let over = water_particle_bucket_over(particle.body, cells.level, cell, firstTrailingBit(above), offsets.count);
616
619
  above &= above - 1u;
617
- atomicMax(&bucket_nest[over], entries);
618
- atomicMax(&bucket_nest[over + 1u], steepness);
620
+ atomicMax(&bucket_nest[${NEST}u * over], runs);
619
621
  }
620
- let octave = water_particle_crowd_bin(precedence, water_particle_crowd_reach(steepness));
621
- if (octave < ${BINS}u) { atomicAdd(&bucket_nest[${NEST}u * bucket + 2u + octave], 1u); }
622
622
  }
623
623
  }
624
- `, [chunk_water_particle_cells, chunk_folded_workgroup_index]) });
624
+ `, [chunk_water_particle_run_start, chunk_folded_workgroup_index]) });
625
625
 
626
- const share_resources = new ShaderResourceSetDescriptor();
627
- share_resources.createGroup()
626
+ const cut_resources = new ShaderResourceSetDescriptor();
627
+ cut_resources.createGroup()
628
628
  .addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
629
629
  .addStorageBuffer('activity', WebGPUArray.u32)
630
630
  .addStorageBuffer('offsets', COLLECTION_U32_STRUCT_READ)
631
- .addStorageBuffer('bucket_weight', WebGPUArray.u32)
632
- .addStorageBuffer('bucket_nest', WebGPUArray.u32)
631
+ .addStorageBuffer('links', WebGPUArray.u32)
632
+ .addStorageBuffer('reaches', WebGPUArray.u32)
633
633
  .addStorageBuffer('body_bounds', WebGPUArray.u32)
634
- .addStorageBuffer('draw', WebGPUArray.u32)
635
- .addStorageBuffer('runs', WebGPUArray.from(atomic_u32), true);
634
+ .addStorageBuffer('bucket_crowd', WebGPUArray.u32)
635
+ .addStorageBuffer('bucket_nest', WebGPUArray.from(atomic_u32), true);
636
636
 
637
637
  /**
638
- * Each packet drawn its share of the most crowded of its cells, lowered onto its run: a run is thinned whole, by the most
639
- * crowded cell any of its packets is in, so a front is never holed. A cell is crowded where its most crowded point (one
640
- * of it at its level and the coarser ones, or one under it the nest pass raised) reads more than
641
- * {@link WATER_PARTICLE_CELL_SHARE} entries, and a packet's share there is that many times its steepness over the summed
642
- * steepness that point reads. Where that point would read more than the gather budget, the cell keeps no more of it
643
- * than its own steepness's part: its keys' octaves dropped from the top until the rest fit
644
- * ({@link WATER_PARTICLE_CROWD_BINS}). A cell only one front reaches does not count.
638
+ * How many steepness classes each cell's points keep, the steepest first: as many as keep its most crowded point within
639
+ * {@link WATER_PARTICLE_CROWD_KEEP} runs, over its own cell and the coarser ones over it, class by class, and what finer
640
+ * levels add under it (the nest pass's most, less what its own and coarser cells hold), counted as the steepest, since a
641
+ * run sits finer the younger it is; and as many as keep it within the gather budget. Never fewer than one. Each run
642
+ * lowers its cell's bucket to what it finds.
645
643
  */
646
- export const shader_water_particles_share = ComputeShader.from({ label: 'Water / particles share', resources: share_resources,
644
+ export const shader_water_particles_cut = ComputeShader.from({ label: 'Water / particles cut', resources: cut_resources,
647
645
  body: CodeChunk.from(/* language=WGSL */ `
646
+ // A cell's runs by steepness class, and in all: ${BINS} classes and the total, written out as vectors, so that a cell's
647
+ // words are read at once and a class is never a vector's component by a variable index (local memory).
648
+ struct WaterParticleCrowd {
649
+ low: vec4<u32>,
650
+ mid: vec4<u32>,
651
+ high: vec3<u32>,
652
+ }
653
+
654
+ fn water_particle_crowd_at(bucket: u32) -> WaterParticleCrowd {
655
+ let at = ${WORDS}u * bucket;
656
+ return WaterParticleCrowd(vec4<u32>(bucket_crowd[at], bucket_crowd[at + 1u], bucket_crowd[at + 2u], bucket_crowd[at + 3u]),
657
+ vec4<u32>(bucket_crowd[at + 4u], bucket_crowd[at + 5u], bucket_crowd[at + 6u], bucket_crowd[at + 7u]),
658
+ vec3<u32>(bucket_crowd[at + 8u], bucket_crowd[at + 9u], bucket_crowd[at + 10u]));
659
+ }
660
+
661
+ fn water_particle_crowd_add(a: WaterParticleCrowd, b: WaterParticleCrowd) -> WaterParticleCrowd {
662
+ return WaterParticleCrowd(a.low + b.low, a.mid + b.mid, a.high + b.high);
663
+ }
664
+
665
+ // The classes kept and drawn, the steepest first, once a point's reads take in a class: they only grow, so a class past
666
+ // the keep or the budget ends its count.
667
+ fn water_particle_crowd_step(reads: u32, grade: u32, classes: vec2<u32>) -> vec2<u32> {
668
+ return vec2<u32>(select(classes.x, grade + 1u, reads <= ${WATER_PARTICLE_CROWD_KEEP}u),
669
+ select(classes.y, grade + 1u, reads <= ${WATER_PARTICLE_GATHER_BUDGET}u));
670
+ }
671
+
672
+ fn water_particle_crowd_classes(finer: u32, crowd: WaterParticleCrowd) -> vec2<u32> {
673
+ var reads = finer;
674
+ var classes = vec2<u32>(1u);
675
+ reads += crowd.low.x; classes = water_particle_crowd_step(reads, 0u, classes);
676
+ reads += crowd.low.y; classes = water_particle_crowd_step(reads, 1u, classes);
677
+ reads += crowd.low.z; classes = water_particle_crowd_step(reads, 2u, classes);
678
+ reads += crowd.low.w; classes = water_particle_crowd_step(reads, 3u, classes);
679
+ reads += crowd.mid.x; classes = water_particle_crowd_step(reads, 4u, classes);
680
+ reads += crowd.mid.y; classes = water_particle_crowd_step(reads, 5u, classes);
681
+ reads += crowd.mid.z; classes = water_particle_crowd_step(reads, 6u, classes);
682
+ reads += crowd.mid.w; classes = water_particle_crowd_step(reads, 7u, classes);
683
+ reads += crowd.high.x; classes = water_particle_crowd_step(reads, 8u, classes);
684
+ reads += crowd.high.y; classes = water_particle_crowd_step(reads, 9u, classes);
685
+ return classes;
686
+ }
687
+
648
688
  @compute @workgroup_size(64)
649
689
  fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
650
690
  let i = folded_workgroup_index(group) * 64u + lane;
651
691
  if (i >= next.count) { return; }
652
- let drawn = draw[2u * i + 1u];
653
- if (drawn == 0u) { return; }
654
- let particle = water_particle_drawn(next.elements[i], drawn);
692
+ let particle = next.elements[i];
655
693
  if ((activity[particle.body] & ${WaterActivity.Active}u) == 0u) { return; }
656
- let cells = water_particle_cells(particle, i);
657
- let weight = f32(water_particle_weight(particle));
658
- let coarser = body_bounds[particle.body * ${STRIDE}u] & ~((2u << cells.level) - 1u);
659
- var share = 1e30;
694
+ let cells = water_particle_reach_at(i);
695
+ let levels = body_bounds[particle.body * ${STRIDE}u];
696
+ let coarser = levels & ~((2u << cells.level) - 1u);
697
+ let before = water_particle_before(i);
660
698
  for (var corner = 0u; corner < ${WATER_PARTICLE_CORNERS}u; corner++) {
661
699
  if (!water_particle_has_corner(cells, corner)) { continue; }
662
700
  let cell = water_particle_corner_cell(cells, corner);
701
+ if (!water_particle_starts_run(before, cell)) { continue; }
663
702
  let bucket = water_particle_bucket(particle.body, cells.level, cell, offsets.count);
664
- let at = ${WORDS}u * bucket;
665
- let nested = ${NEST}u * bucket;
666
- if (bucket_weight[at + 1u] == ~bucket_weight[at + 2u]) { continue; }
667
- // what a point of the cell reads at its level and the coarser ones
668
- var entries = bucket_weight[at + 3u];
669
- var steepness = bucket_weight[at];
703
+ // what its points read at its level and the coarser ones, by class and in all
704
+ var crowd = water_particle_crowd_at(bucket);
670
705
  var above = coarser;
671
706
  for (var n = 0u; n < WATER_PARTICLE_LEVELS && above != 0u; n++) {
672
- let over = ${WORDS}u * water_particle_bucket_over(particle.body, cells.level, cell, firstTrailingBit(above), offsets.count);
707
+ let over = water_particle_bucket_over(particle.body, cells.level, cell, firstTrailingBit(above), offsets.count);
673
708
  above &= above - 1u;
674
- entries += bucket_weight[over + 3u];
675
- steepness += bucket_weight[over];
676
- }
677
- // and what its most crowded point reads
678
- let crowd = max(entries, bucket_nest[nested]);
679
- let total = max(steepness, bucket_nest[nested + 1u]);
680
- if (crowd <= ${WATER_PARTICLE_CELL_SHARE}u || total == 0u) { continue; }
681
- share = min(share, ${WATER_PARTICLE_CELL_SHARE}.0 * weight / f32(total));
682
- if (crowd <= ${WATER_PARTICLE_GATHER_BUDGET}u) { continue; }
683
- // no more kept than its part of the budget: the octaves of its keys dropped from the top until the rest fit
684
- let quota = ${WATER_PARTICLE_GATHER_BUDGET}.0 * f32(bucket_weight[at]) / f32(total);
685
- var below = 0u;
686
- for (var octave = 0u; octave < ${BINS}u; octave++) { below += bucket_nest[nested + 2u + octave]; }
687
- var reach = water_particle_crowd_reach(steepness);
688
- for (var octave = 0u; octave < ${BINS}u && f32(below) > quota; octave++) {
689
- below -= bucket_nest[nested + 2u + octave];
690
- reach = select(0.0, 0.5 * reach, octave + 1u < ${BINS}u);
709
+ crowd = water_particle_crowd_add(crowd, water_particle_crowd_at(over));
691
710
  }
692
- // drawn while its lot is under the ramp above its share: while its key is under the reach left
693
- share = min(share, reach * weight / ${1 + WATER_PARTICLE_THINNING_RAMP});
711
+ // and, under the most crowded, what the finer ones add
712
+ let runs = crowd.high.z;
713
+ let most = max(atomicLoad(&bucket_nest[${NEST}u * bucket]), runs);
714
+ // within the crowd's keep, every class is kept and drawn: as the clear left it
715
+ if (most <= ${WATER_PARTICLE_CROWD_KEEP}u) { continue; }
716
+ let classes = water_particle_crowd_classes(most - runs, crowd);
717
+ atomicMin(&bucket_nest[${NEST}u * bucket + 1u], classes.x);
718
+ atomicMin(&bucket_nest[${NEST}u * bucket + 2u], classes.y);
694
719
  }
695
- // non-negative floats order as their bits do
696
- atomicMin(&runs[2u * draw[2u * i] + 1u], bitcast<u32>(share));
697
720
  }
698
- `, [chunk_water_particle_cells, chunk_folded_workgroup_index]) });
721
+ `, [chunk_water_particle_run_start, chunk_folded_workgroup_index]) });
722
+
723
+ const mark_resources = new ShaderResourceSetDescriptor();
724
+ mark_resources.createGroup()
725
+ .addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
726
+ .addStorageBuffer('activity', WebGPUArray.u32)
727
+ .addStorageBuffer('offsets', COLLECTION_U32_STRUCT_READ)
728
+ .addStorageBuffer('reaches', WebGPUArray.u32)
729
+ .addStorageBuffer('bucket_nest', WebGPUArray.from(atomic_u32), true);
730
+
731
+ /**
732
+ * Each particle of a body simulated this frame whose steepness class some cell it reaches cannot draw within the gather
733
+ * budget, or that an earlier frame cut, puts its front's tag in the frame's table of cut fronts: the first free slot of
734
+ * {@link WATER_PARTICLE_FRONT_PROBES} from its front's own, or the one already holding it. Its front's particles are
735
+ * alike and β apart, so every one of them then finds the tag, and the front is cut whole.
736
+ */
737
+ export const shader_water_particles_mark = ComputeShader.from({ label: 'Water / particles mark', resources: mark_resources,
738
+ body: CodeChunk.from(/* language=WGSL */ `
739
+ @compute @workgroup_size(64)
740
+ fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
741
+ let i = folded_workgroup_index(group) * 64u + lane;
742
+ if (i >= next.count) { return; }
743
+ let particle = next.elements[i];
744
+ if ((activity[particle.body] & ${WaterActivity.Active}u) == 0u) { return; }
745
+ let cells = water_particle_reach_at(i);
746
+ let grade = water_particle_crowd_class(particle);
747
+ var cut = particle.fading >= ${WATER_PARTICLE_CUT};
748
+ for (var corner = 0u; corner < ${WATER_PARTICLE_CORNERS}u; corner++) {
749
+ if (!water_particle_has_corner(cells, corner)) { continue; }
750
+ let at = ${NEST}u * water_particle_bucket(particle.body, cells.level, water_particle_corner_cell(cells, corner), offsets.count);
751
+ cut = cut || grade >= atomicLoad(&bucket_nest[at + 2u]);
752
+ }
753
+ if (!cut) { return; }
754
+ let hash = water_particle_front_hash(particle);
755
+ var probe = 0u;
756
+ // a weak exchange may fail with the slot still free: that slot again, twice the probes at most
757
+ for (var attempt = 0u; attempt < ${2 * WATER_PARTICLE_FRONT_PROBES}u && probe < ${WATER_PARTICLE_FRONT_PROBES}u; attempt++) {
758
+ let at = ${NEST}u * ((hash.y + probe) & (offsets.count - 1u)) + 3u;
759
+ let exchange = atomicCompareExchangeWeak(&bucket_nest[at], 0u, hash.x);
760
+ if (exchange.exchanged || exchange.old_value == hash.x) { break; }
761
+ if (exchange.old_value != 0u) { probe++; }
762
+ }
763
+ }
764
+ `, [chunk_water_particle_reach_read, chunk_folded_workgroup_index]) });
765
+
766
+ const keep_resources = new ShaderResourceSetDescriptor();
767
+ keep_resources.createGroup()
768
+ .addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT, true)
769
+ .addStorageBuffer('activity', WebGPUArray.u32)
770
+ .addStorageBuffer('offsets', COLLECTION_U32_STRUCT_READ)
771
+ .addStorageBuffer('reaches', WebGPUArray.u32)
772
+ .addStorageBuffer('bucket_nest', WebGPUArray.u32)
773
+ .addStorageBuffer('shown', WebGPUArray.u32, true);
774
+
775
+ /**
776
+ * What each crowd leaves of a body simulated this frame, particle by particle: a particle whose steepness class some
777
+ * cell it reaches does not keep is doomed, to fade from its next step and die (chunk_water_particle_step), and is drawn
778
+ * as it is until then; one whose front is in the frame's table of cut fronts (the mark pass) is not drawn, here or
779
+ * anywhere, and dies at its next step. So a crowd loses its flattest runs, and never by a lot or an order the particles
780
+ * arrive in. A frame that does not step decides the same again: nothing it reads has changed. One word a particle:
781
+ * whether it is drawn.
782
+ */
783
+ export const shader_water_particles_keep = ComputeShader.from({ label: 'Water / particles keep', resources: keep_resources,
784
+ body: CodeChunk.from(/* language=WGSL */ `
785
+ @compute @workgroup_size(64)
786
+ fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
787
+ let i = folded_workgroup_index(group) * 64u + lane;
788
+ if (i >= next.count) { return; }
789
+ let particle = next.elements[i];
790
+ if ((activity[particle.body] & ${WaterActivity.Active}u) == 0u) { shown[i] = 1u; return; }
791
+ let cells = water_particle_reach_at(i);
792
+ let grade = water_particle_crowd_class(particle);
793
+ var doomed = false;
794
+ var cut = particle.fading >= ${WATER_PARTICLE_CUT};
795
+ for (var corner = 0u; corner < ${WATER_PARTICLE_CORNERS}u; corner++) {
796
+ if (!water_particle_has_corner(cells, corner)) { continue; }
797
+ let at = ${NEST}u * water_particle_bucket(particle.body, cells.level, water_particle_corner_cell(cells, corner), offsets.count);
798
+ doomed = doomed || grade >= bucket_nest[at + 1u];
799
+ cut = cut || grade >= bucket_nest[at + 2u];
800
+ }
801
+ // its front cut where another of its particles is
802
+ let hash = water_particle_front_hash(particle);
803
+ for (var probe = 0u; probe < ${WATER_PARTICLE_FRONT_PROBES}u && !cut; probe++) {
804
+ let tag = bucket_nest[${NEST}u * ((hash.y + probe) & (offsets.count - 1u)) + 3u];
805
+ if (tag == 0u) { break; }
806
+ cut = tag == hash.x;
807
+ }
808
+ shown[i] = select(1u, 0u, cut);
809
+ if (cut) {
810
+ next.elements[i].fading = ${WATER_PARTICLE_CUT};
811
+ } else if (doomed && particle.fading == 0.0) {
812
+ // a tenth of its steepness now, where a fading packet dies
813
+ next.elements[i].fading = 0.1 * abs(particle.amplitude) / max(particle.radius, 1e-30);
814
+ }
815
+ }
816
+ `, [chunk_water_particle_reach_read, chunk_folded_workgroup_index]) });
817
+
818
+ /**
819
+ * The cells the support of a drawn particle's drawn neighbour before it overlaps, or none: a drawn particle begins a
820
+ * run of its front in a cell its support overlaps where these do not. Bindings: `links`, `reaches`, `shown`.
821
+ */
822
+ const chunk_water_particle_drawn_run_start = CodeChunk.from(/* language=WGSL */ `
823
+ fn water_particle_drawn_before(i: u32) -> WaterParticleReach {
824
+ let before = links[2u * i];
825
+ if (before == ~0u || shown[before] == 0u) { return water_particle_reach_none(); }
826
+ return water_particle_reach_at(before);
827
+ }
828
+ `, [chunk_water_particle_reach_read]);
699
829
 
700
830
  const bin_resources = new ShaderResourceSetDescriptor();
701
831
  bin_resources.createGroup()
@@ -705,19 +835,15 @@ bin_resources.createGroup()
705
835
  .addStorageBuffer('slots', WebGPUArray.u32, true)
706
836
  .addStorageBuffer('bucket_amplitude', WebGPUArray.from(atomic_u32), true)
707
837
  .addStorageBuffer('body_bounds', WebGPUArray.from(atomic_u32), true)
708
- .addStorageBuffer('draw', WebGPUArray.u32)
709
- .addStorageBuffer('runs', WebGPUArray.u32)
710
- .addStorageBuffer('visible', WebGPUArray.from(f32), true);
838
+ .addStorageBuffer('links', WebGPUArray.u32)
839
+ .addStorageBuffer('reaches', WebGPUArray.u32)
840
+ .addStorageBuffer('shown', WebGPUArray.u32);
711
841
 
712
842
  /**
713
- * Enter each packet drawn of a body simulated this frame in every hash cell its support overlaps: count it into the
714
- * cell's bucket, keep its slot there, raise the bucket's largest |a| to its own, and mark its level on its body. One
715
- * atomic each: no compare-exchange loop a crowded bucket would serialize. A paused body's particles take no slot:
716
- * nothing draws them this frame.
717
- *
718
- * Where cells are crowded (the share pass), a run is drawn by the share its most crowded cell leaves it and its
719
- * front's lot: one decision for all its packets and all their cells, so neither a front nor the surface across cells
720
- * is holed, and it does not change with the order particles arrive in. One left out takes no slot.
843
+ * Enter each drawn run of a body simulated this frame in every cell it is drawn in, by its first particle: count it into
844
+ * the cell's bucket, keep its slot there, raise the bucket's largest |a| to its own, and mark its level on its body. One
845
+ * atomic each: no compare-exchange loop a crowded bucket would serialize. A paused body's particles take no slot, nor
846
+ * does one its crowd does not draw: nothing draws them this frame.
721
847
  */
722
848
  export const shader_water_particles_bin = ComputeShader.from({ label: 'Water / particles bin', resources: bin_resources,
723
849
  body: CodeChunk.from(/* language=WGSL */ `
@@ -729,73 +855,93 @@ fn water_particle_unslotted(i: u32) {
729
855
  fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
730
856
  let i = folded_workgroup_index(group) * 64u + lane;
731
857
  if (i >= next.count) { return; }
732
- let drawn = draw[2u * i + 1u];
733
- let particle = water_particle_drawn(next.elements[i], max(drawn, 1u));
734
- if (drawn == 0u || (activity[particle.body] & ${WaterActivity.Active}u) == 0u) { water_particle_unslotted(i); return; }
735
- let cells = water_particle_cells(particle, i);
736
- let visibility = water_particle_visibility(bitcast<f32>(runs[2u * draw[2u * i] + 1u]), water_particle_lot(particle));
737
- visible[i] = visibility;
738
- if (!(visibility > 0.0)) { water_particle_unslotted(i); return; }
858
+ let particle = next.elements[i];
859
+ if ((activity[particle.body] & ${WaterActivity.Active}u) == 0u || shown[i] == 0u) { water_particle_unslotted(i); return; }
860
+ let cells = water_particle_reach_at(i);
861
+ let before = water_particle_drawn_before(i);
739
862
  atomicOr(&body_bounds[particle.body * ${STRIDE}u], 1u << cells.level);
740
863
  for (var corner = 0u; corner < ${WATER_PARTICLE_CORNERS}u; corner++) {
741
864
  var slot = ~0u;
742
865
  if (water_particle_has_corner(cells, corner)) {
743
- let bucket = water_particle_bucket(particle.body, cells.level, water_particle_corner_cell(cells, corner), buckets.count);
744
- slot = atomicAdd(&buckets.elements[bucket], 1u);
745
- // non-negative floats order as their bits do
746
- atomicMax(&bucket_amplitude[bucket], bitcast<u32>(abs(particle.amplitude) * visibility));
866
+ let cell = water_particle_corner_cell(cells, corner);
867
+ if (!water_particle_reaches(before, cell)) {
868
+ let bucket = water_particle_bucket(particle.body, cells.level, cell, buckets.count);
869
+ slot = atomicAdd(&buckets.elements[bucket], 1u);
870
+ // non-negative floats order as their bits do
871
+ atomicMax(&bucket_amplitude[bucket], bitcast<u32>(abs(particle.amplitude)));
872
+ }
747
873
  }
748
874
  slots[i * ${WATER_PARTICLE_CORNERS}u + corner] = slot;
749
875
  }
750
876
  }
751
- `, [chunk_water_particle_cells, chunk_folded_workgroup_index]) });
877
+ `, [chunk_water_particle_drawn_run_start, chunk_folded_workgroup_index]) });
752
878
 
753
879
  const scatter_resources = new ShaderResourceSetDescriptor();
754
880
  scatter_resources.createGroup()
755
881
  .addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
756
882
  .addStorageBuffer('offsets', COLLECTION_U32_STRUCT_READ)
757
883
  .addStorageBuffer('slots', WebGPUArray.u32)
758
- .addStorageBuffer('visible', WebGPUArray.from(f32))
759
- .addStorageBuffer('draw', WebGPUArray.u32)
884
+ .addStorageBuffer('links', WebGPUArray.u32)
885
+ .addStorageBuffer('reaches', WebGPUArray.u32)
886
+ .addStorageBuffer('shown', WebGPUArray.u32)
760
887
  .addStorageBuffer('staging', WebGPUArray.from(WATER_PARTICLE_ENTRY_STRUCT), true);
761
888
 
762
- /** Each entry to its bucket's range, in arrival order, at the height its crowd leaves it; the rank pass orders it. */
889
+ /**
890
+ * Each run's entry to its bucket's range, in arrival order; the rank pass orders it. A run is its first particle and its
891
+ * front's drawn neighbours after it, one by one, while their supports overlap the cell, at most
892
+ * {@link WATER_PARTICLE_RUN_LIMIT}; an end is open where the front has no drawn neighbour beyond it.
893
+ */
763
894
  export const shader_water_particles_scatter = ComputeShader.from({ label: 'Water / particles scatter', resources: scatter_resources,
764
895
  body: CodeChunk.from(/* language=WGSL */ `
765
896
  @compute @workgroup_size(64)
766
897
  fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
767
898
  let i = folded_workgroup_index(group) * 64u + lane;
768
899
  if (i >= next.count) { return; }
769
- let drawn = draw[2u * i + 1u];
770
- if (drawn == 0u) { return; }
771
- let particle = water_particle_drawn(next.elements[i], drawn);
772
- let cells = water_particle_cells(particle, i);
773
- for (var corner = 0u; corner < ${WATER_PARTICLE_CORNERS}u; corner++) {
900
+ var first = 0u;
901
+ for (; first < ${WATER_PARTICLE_CORNERS}u && slots[i * ${WATER_PARTICLE_CORNERS}u + first] == ~0u; first++) {}
902
+ if (first == ${WATER_PARTICLE_CORNERS}u) { return; }
903
+ let particle = next.elements[i];
904
+ let cells = water_particle_reach_at(i);
905
+ let lone = water_particle_entry(particle, i, cells);
906
+ let before = links[2u * i];
907
+ let open_start = select(0u, WATER_PARTICLE_RUN_OPEN_START, before == ~0u || shown[before] == 0u);
908
+ for (var corner = first; corner < ${WATER_PARTICLE_CORNERS}u; corner++) {
774
909
  let slot = slots[i * ${WATER_PARTICLE_CORNERS}u + corner];
775
910
  if (slot == ~0u) { continue; }
776
911
  let cell = water_particle_corner_cell(cells, corner);
777
912
  let bucket = water_particle_bucket(particle.body, cells.level, cell, offsets.count);
778
913
  var start = 0u;
779
914
  if (bucket > 0u) { start = offsets.elements[bucket - 1u]; }
780
- var entry = cells.entry;
915
+ // the run, as far as its drawn neighbours reach the cell
916
+ var members = 1u;
917
+ var open_end = 0u;
918
+ var at = i;
919
+ for (; members < ${WATER_PARTICLE_RUN_LIMIT}u; members++) {
920
+ let after = links[2u * at + 1u];
921
+ if (after == ~0u) { open_end = WATER_PARTICLE_RUN_OPEN_END; break; }
922
+ if (shown[after] == 0u) { open_end = WATER_PARTICLE_RUN_OPEN_END; break; }
923
+ if (!water_particle_reaches(water_particle_reach_at(after), cell)) { break; }
924
+ at = after;
925
+ }
926
+ var entry = lone;
781
927
  entry.cell = cell;
782
- entry.amplitude *= visible[i];
928
+ entry.run = members | open_start | open_end;
783
929
  staging[start + slot] = entry;
784
930
  }
785
931
  }
786
- `, [chunk_water_particle_cells, chunk_folded_workgroup_index]) });
932
+ `, [chunk_water_particle_reach_read, chunk_folded_workgroup_index]) });
787
933
 
788
934
  const rank_resources = new ShaderResourceSetDescriptor();
789
935
  rank_resources.createGroup()
790
936
  .addStorageBuffer('next', WATER_PARTICLE_ARRAY_STRUCT)
791
937
  .addStorageBuffer('offsets', COLLECTION_U32_STRUCT_READ)
792
938
  .addStorageBuffer('slots', WebGPUArray.u32)
793
- .addStorageBuffer('draw', WebGPUArray.u32)
939
+ .addStorageBuffer('reaches', WebGPUArray.u32)
794
940
  .addStorageBuffer('staging', WebGPUArray.from(WATER_PARTICLE_ENTRY_STRUCT))
795
941
  .addStorageBuffer('entries', WebGPUArray.from(WATER_PARTICLE_ENTRY_STRUCT), true);
796
942
 
797
943
  /**
798
- * Order every bucket by particle index then cell, so a gather that runs out of budget drops the same
944
+ * Order every bucket by its runs' first particles then cells, so a gather that runs out of budget drops the same
799
945
  * entries every frame (the particle array keeps its order from frame to frame). A bucket of more than
800
946
  * ${WATER_PARTICLE_RANK_LIMIT} keeps its arrival order: a fixed budget of comparisons per entry.
801
947
  */
@@ -811,14 +957,12 @@ fn water_entry_before(a: ${WATER_PARTICLE_ENTRY_STRUCT.wgsl_ref}, b: ${WATER_PAR
811
957
  fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
812
958
  let i = folded_workgroup_index(group) * 64u + lane;
813
959
  if (i >= next.count) { return; }
814
- let drawn = draw[2u * i + 1u];
815
- if (drawn == 0u) { return; }
816
- let particle = water_particle_drawn(next.elements[i], drawn);
817
- let cells = water_particle_cells(particle, i);
960
+ let cells = water_particle_reach_at(i);
961
+ let body = next.elements[i].body;
818
962
  for (var corner = 0u; corner < ${WATER_PARTICLE_CORNERS}u; corner++) {
819
963
  let slot = slots[i * ${WATER_PARTICLE_CORNERS}u + corner];
820
964
  if (slot == ~0u) { continue; }
821
- let bucket = water_particle_bucket(particle.body, cells.level, water_particle_corner_cell(cells, corner), offsets.count);
965
+ let bucket = water_particle_bucket(body, cells.level, water_particle_corner_cell(cells, corner), offsets.count);
822
966
  var start = 0u;
823
967
  if (bucket > 0u) { start = offsets.elements[bucket - 1u]; }
824
968
  let end = offsets.elements[bucket];
@@ -831,7 +975,7 @@ fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) la
831
975
  entries[start + rank] = entry;
832
976
  }
833
977
  }
834
- `, [chunk_water_particle_cells, chunk_folded_workgroup_index]) });
978
+ `, [chunk_water_particle_reach_read, chunk_folded_workgroup_index]) });
835
979
 
836
980
  const bounds_resources = new ShaderResourceSetDescriptor();
837
981
  bounds_resources.createGroup()
@@ -839,14 +983,14 @@ bounds_resources.createGroup()
839
983
  .addStorageBuffer('activity', WebGPUArray.u32)
840
984
  .addStorageBuffer('offsets', COLLECTION_U32_STRUCT_READ)
841
985
  .addStorageBuffer('bucket_amplitude', WebGPUArray.u32)
842
- .addStorageBuffer('draw', WebGPUArray.u32)
986
+ .addStorageBuffer('slots', WebGPUArray.u32)
987
+ .addStorageBuffer('reaches', WebGPUArray.u32)
843
988
  .addStorageBuffer('body_bounds', WebGPUArray.from(atomic_u32), true);
844
989
 
845
990
  /**
846
- * Each body's bound per level: the largest of count × max |a| over the buckets its particles are in, which is
847
- * at least the bucket's Σ|a|. Every particle that reaches a point is in that point's cell's bucket, and neither
848
- * its height nor its horizontal displacement exceeds |a|, so the bound holds everywhere; a hash collision only
849
- * adds to it.
991
+ * Each body's bound per level: the largest of count × max |a| over the buckets its runs are in, which is at least the
992
+ * bucket's Σ|a|. Every run that reaches a point is in that point's cell's bucket, and neither its height nor its
993
+ * horizontal displacement exceeds |a|, so the bound holds everywhere; a hash collision only adds to it.
850
994
  */
851
995
  export const shader_water_particles_bounds = ComputeShader.from({ label: 'Water / particles bounds', resources: bounds_resources,
852
996
  body: CodeChunk.from(/* language=WGSL */ `
@@ -854,15 +998,13 @@ export const shader_water_particles_bounds = ComputeShader.from({ label: 'Water
854
998
  fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) lane: u32) {
855
999
  let i = folded_workgroup_index(group) * 64u + lane;
856
1000
  if (i >= next.count) { return; }
857
- let drawn = draw[2u * i + 1u];
858
- if (drawn == 0u) { return; }
859
- let particle = water_particle_drawn(next.elements[i], drawn);
1001
+ let particle = next.elements[i];
860
1002
  // a paused body keeps the bounds its particles last had
861
1003
  if ((activity[particle.body] & ${WaterActivity.Active}u) == 0u) { return; }
862
- let cells = water_particle_cells(particle, i);
1004
+ let cells = water_particle_reach_at(i);
863
1005
  var largest = 0.0;
864
1006
  for (var corner = 0u; corner < ${WATER_PARTICLE_CORNERS}u; corner++) {
865
- if (!water_particle_has_corner(cells, corner)) { continue; }
1007
+ if (slots[i * ${WATER_PARTICLE_CORNERS}u + corner] == ~0u) { continue; }
866
1008
  let bucket = water_particle_bucket(particle.body, cells.level, water_particle_corner_cell(cells, corner), offsets.count);
867
1009
  var start = 0u;
868
1010
  if (bucket > 0u) { start = offsets.elements[bucket - 1u]; }
@@ -871,4 +1013,4 @@ fn main(@builtin(workgroup_id) group: vec3u, @builtin(local_invocation_index) la
871
1013
  // non-negative floats order as their bits do
872
1014
  atomicMax(&body_bounds[particle.body * ${STRIDE}u + 1u + cells.level], bitcast<u32>(largest));
873
1015
  }
874
- `, [chunk_water_particle_cells, chunk_folded_workgroup_index]) });
1016
+ `, [chunk_water_particle_reach_read, chunk_folded_workgroup_index]) });