@woosh/meep-engine 3.60.4 → 3.60.6

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 (419) hide show
  1. package/CHANGELOG.md +2621 -0
  2. package/CONTRIBUTING.md +7 -0
  3. package/MIGRATION_2_TO_3.md +136 -0
  4. package/SECURITY.md +4 -0
  5. package/editor/EditorCameraNavigation.d.ts +2 -1
  6. package/editor/EditorCameraNavigation.d.ts.map +1 -1
  7. package/editor/EditorCameraNavigation.js +51 -57
  8. package/package.json +6 -2
  9. package/src/engine/EngineHarness.d.ts +3 -9
  10. package/src/engine/EngineHarness.d.ts.map +1 -1
  11. package/src/engine/EngineHarness.js +6 -8
  12. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts +9 -6
  13. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts.map +1 -1
  14. package/src/engine/graphics/camera/makeOrbitalCameraController.js +44 -71
  15. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts +64 -0
  16. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts.map +1 -0
  17. package/src/engine/graphics/camera/orbital/MomentumAxis.js +187 -0
  18. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts +86 -0
  19. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts.map +1 -0
  20. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.js +319 -0
  21. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts +34 -10
  22. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts.map +1 -1
  23. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js +116 -30
  24. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts +1 -7
  25. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
  26. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +1 -37
  27. package/src/engine/graphics/ecs/water/Water.d.ts +8 -0
  28. package/src/engine/graphics/ecs/water/Water.d.ts.map +1 -1
  29. package/src/engine/graphics/ecs/water/Water.js +8 -0
  30. package/src/engine/graphics3/WaterSystem.d.ts.map +1 -1
  31. package/src/engine/graphics3/WaterSystem.js +731 -734
  32. package/src/engine/graphics3/decal/DecalTileShader.d.ts +61 -0
  33. package/src/engine/graphics3/decal/DecalTileShader.d.ts.map +1 -0
  34. package/src/engine/graphics3/decal/DecalTileShader.js +138 -0
  35. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts +10 -4
  36. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts.map +1 -1
  37. package/src/engine/graphics3/decal/GPUDecalRenderer.js +149 -44
  38. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts +25 -0
  39. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts.map +1 -0
  40. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.js +55 -0
  41. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts +22 -0
  42. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts.map +1 -0
  43. package/src/engine/graphics3/decal/graph_gather_decal_tiles.js +70 -0
  44. package/src/engine/graphics3/decal/shader_decal_composite.d.ts +7 -5
  45. package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
  46. package/src/engine/graphics3/decal/shader_decal_composite.js +48 -44
  47. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts +12 -0
  48. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts.map +1 -0
  49. package/src/engine/graphics3/decal/shader_decal_tile_copy.js +78 -0
  50. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts +11 -0
  51. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts.map +1 -0
  52. package/src/engine/graphics3/decal/shader_gather_decal_tiles.js +80 -0
  53. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts +13 -0
  54. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts.map +1 -0
  55. package/src/engine/input/devices/mouse/MouseEventButtons.js +23 -0
  56. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts +4 -0
  57. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts.map +1 -1
  58. package/src/engine/input/ecs/controllers/KeyboardCameraController.js +9 -45
  59. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  60. package/src/shade/playground/water_refraction/README.md +5 -6
  61. package/src/shade/playground/water_refraction/index.html +1 -1
  62. package/src/shade/renderer/Renderer.d.ts +11 -0
  63. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  64. package/src/shade/renderer/Renderer.js +50 -6
  65. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +40 -80
  66. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
  67. package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +197 -147
  68. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  69. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  70. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  71. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  72. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  73. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +28 -45
  74. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts.map +1 -1
  75. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +111 -119
  76. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +22 -3
  77. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
  78. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +84 -3
  79. package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.d.ts +19 -0
  80. package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.d.ts.map +1 -0
  81. package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.js +25 -0
  82. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts +31 -0
  83. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts.map +1 -0
  84. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.js +343 -0
  85. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts +15 -0
  86. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts.map +1 -1
  87. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.js +38 -0
  88. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts +17 -0
  89. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts.map +1 -0
  90. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.js +29 -0
  91. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -1
  92. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +7 -13
  93. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts +28 -59
  94. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts.map +1 -1
  95. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.js +84 -789
  96. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
  97. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +1 -8
  98. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +7 -6
  99. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
  100. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +12 -11
  101. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.d.ts +2 -2
  102. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.js +2 -2
  103. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +1 -4
  104. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
  105. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +1 -10
  106. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts +5 -15
  107. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts.map +1 -1
  108. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.js +6 -34
  109. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
  110. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +1 -7
  111. package/src/shade/renderer/hiz/HZBFootprint.d.ts +14 -0
  112. package/src/shade/renderer/hiz/HZBFootprint.d.ts.map +1 -0
  113. package/src/shade/renderer/hiz/HZBFootprint.js +16 -0
  114. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts +10 -0
  115. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts.map +1 -0
  116. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.js +11 -0
  117. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts +10 -0
  118. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts.map +1 -0
  119. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.js +11 -0
  120. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +0 -7
  121. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  122. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +405 -350
  123. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts +12 -0
  124. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts.map +1 -0
  125. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.js +27 -0
  126. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts +12 -0
  127. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts.map +1 -0
  128. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.js +27 -0
  129. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts +14 -0
  130. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts.map +1 -0
  131. package/src/shade/renderer/hiz/hzb_footprint_texels.js +34 -0
  132. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +0 -5
  133. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
  134. package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +37 -57
  135. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts +11 -0
  136. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts.map +1 -0
  137. package/src/shade/renderer/hiz/{shader_hzb_reduce_mip0_clip.js → shader_hzb_reduce_mip0.js} +120 -111
  138. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  139. package/src/shade/renderer/material/GPUMaterialManager.d.ts +11 -1
  140. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  141. package/src/shade/renderer/material/GPUMaterialManager.js +16 -8
  142. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts +12 -12
  143. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
  144. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +17 -118
  145. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts +7 -3
  146. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts.map +1 -1
  147. package/src/shade/renderer/postprocess/gtao/GTAO.js +26 -13
  148. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts +14 -0
  149. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts.map +1 -1
  150. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.js +196 -68
  151. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.d.ts.map +1 -1
  152. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.js +42 -28
  153. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.d.ts.map +1 -1
  154. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.js +429 -414
  155. package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.d.ts +16 -0
  156. package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.d.ts.map +1 -0
  157. package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.js +156 -0
  158. package/src/shade/renderer/scene/GPUSceneContext.d.ts +2 -2
  159. package/src/shade/renderer/scene/GPUSceneContext.js +5 -5
  160. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  161. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +7 -2
  162. package/src/shade/renderer/water/GPUWaterRenderer.d.ts +0 -6
  163. package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
  164. package/src/shade/renderer/water/GPUWaterRenderer.js +12 -17
  165. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts +35 -0
  166. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts.map +1 -0
  167. package/src/shade/renderer/water/medium/graph_water_meshlets.js +89 -0
  168. package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
  169. package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
  170. package/src/shade/renderer/water/shader_water.js +22 -13
  171. package/src/shade/renderer/water/shader_water_avboit.d.ts.map +1 -1
  172. package/src/shade/renderer/water/shader_water_avboit.js +7 -7
  173. package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts +1 -1
  174. package/src/shade/renderer/water/surface/graph_water_tessellation.js +1 -1
  175. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +9 -1
  176. package/editor/REWORK_PLAN_2026_08.md +0 -692
  177. package/src/CODEC_LAYOUT_VERIFICATION.md +0 -419
  178. package/src/CORRECTNESS_HUNT_2026_07_24.md +0 -142
  179. package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +0 -738
  180. package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +0 -79
  181. package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +0 -93
  182. package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +0 -80
  183. package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +0 -164
  184. package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +0 -69
  185. package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +0 -110
  186. package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +0 -129
  187. package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +0 -72
  188. package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +0 -74
  189. package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +0 -169
  190. package/src/DUPLICATION_AUDIT_2026_08_07.md +0 -254
  191. package/src/DUPLICATION_AUDIT_REMEDIATION.md +0 -250
  192. package/src/DUPLICATION_REVIEW_2026_10_24.md +0 -1072
  193. package/src/REVIEW_2026_08_06/adv-core.md +0 -511
  194. package/src/REVIEW_2026_08_06/adv-gfx.md +0 -480
  195. package/src/REVIEW_2026_08_06/adv-view.md +0 -446
  196. package/src/REVIEW_2026_08_06/api-core.md +0 -296
  197. package/src/REVIEW_2026_08_06/api-engine.md +0 -195
  198. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +0 -507
  199. package/src/REVIEW_2026_08_06/core-binary.md +0 -348
  200. package/src/REVIEW_2026_08_06/core-collection.md +0 -376
  201. package/src/REVIEW_2026_08_06/core-math.md +0 -221
  202. package/src/REVIEW_2026_08_06/core-process.md +0 -266
  203. package/src/REVIEW_2026_08_06/core-science.md +0 -223
  204. package/src/REVIEW_2026_08_06/engine-ai-nav.md +0 -279
  205. package/src/REVIEW_2026_08_06/engine-animation.md +0 -167
  206. package/src/REVIEW_2026_08_06/engine-asset.md +0 -166
  207. package/src/REVIEW_2026_08_06/engine-ecs.md +0 -920
  208. package/src/REVIEW_2026_08_06/engine-input.md +0 -197
  209. package/src/REVIEW_2026_08_06/engine-misc.md +0 -248
  210. package/src/REVIEW_2026_08_06/engine-terrain.md +0 -52
  211. package/src/REVIEW_2026_08_06/generation.md +0 -176
  212. package/src/REVIEW_2026_08_06/geom-2d.md +0 -434
  213. package/src/REVIEW_2026_08_06/geom-3d.md +0 -117
  214. package/src/REVIEW_2026_08_06/gfx-gi.md +0 -143
  215. package/src/REVIEW_2026_08_06/gfx-render.md +0 -740
  216. package/src/REVIEW_2026_08_06/gfx-texture.md +0 -690
  217. package/src/REVIEW_2026_08_06/prior-criticals.md +0 -49
  218. package/src/REVIEW_2026_08_06/prior-highs.md +0 -138
  219. package/src/REVIEW_2026_08_06/test-gaps.md +0 -364
  220. package/src/REVIEW_2026_08_06/view.md +0 -667
  221. package/src/REVIEW_2026_08_06.md +0 -610
  222. package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +0 -542
  223. package/src/core/bvh2/bvh3/NOTES.md +0 -2
  224. package/src/core/bvh8/NOTES.md +0 -63
  225. package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +0 -243
  226. package/src/core/color/COLOR_FIX_PLAN_2026_08_28.md +0 -417
  227. package/src/core/color/COLOR_REVIEW_2026_08_28.md +0 -779
  228. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +0 -433
  229. package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +0 -486
  230. package/src/core/geom/3d/atlas/BFF_PLAN.md +0 -174
  231. package/src/core/geom/3d/atlas/REVIEW_02.md +0 -392
  232. package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +0 -146
  233. package/src/core/geom/3d/gjk/NOTES.md +0 -8
  234. package/src/core/geom/3d/topology/TOPO_MESH_RETIREMENT_PLAN.md +0 -135
  235. package/src/core/geom/GEOMETRY_REVIEW_002_API_ISSUES.md +0 -381
  236. package/src/core/geom/GEOMETRY_REVIEW_002_NAMING.md +0 -684
  237. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +0 -554
  238. package/src/core/model/reactive/evaluation/PREDICATE_APPLICABILITY_PROPOSAL.md +0 -446
  239. package/src/engine/REVIEW_2026_07_03.md +0 -211
  240. package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +0 -815
  241. package/src/engine/control/first-person/DESIGN.md +0 -658
  242. package/src/engine/control/first-person/DESIGN_COLLISION.md +0 -382
  243. package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +0 -563
  244. package/src/engine/control/first-person/TODO.md +0 -160
  245. package/src/engine/ecs/ASYNC_SYSTEMS_PROPOSAL.md +0 -531
  246. package/src/engine/ecs/HIERARCHY_REVIEW.md +0 -717
  247. package/src/engine/ecs/async/WORKER_SYSTEM_PLAN.md +0 -262
  248. package/src/engine/ecs/ik/PLAN.md +0 -179
  249. package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +0 -272
  250. package/src/engine/ecs/storage/binary/object-v2/DESIGN.md +0 -199
  251. package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +0 -521
  252. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +0 -463
  253. package/src/engine/graphics/ecs/camera/pp/NOTES.md +0 -48
  254. package/src/engine/graphics/render/frame_graph/CORRECTNESS_FIXES_2026_09_04.md +0 -659
  255. package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +0 -235
  256. package/src/engine/graphics/sh3/path_tracer/REVIEW_001_ACTION_PLAN.md +0 -490
  257. package/src/engine/graphics/texture/sampler/compare/FLIP_IMPLEMENTATION_PLAN.md +0 -466
  258. package/src/engine/input/GAMEPAD_API_PROPOSAL.md +0 -717
  259. package/src/engine/input/ecs/ism/ISM_DESIGN.md +0 -1523
  260. package/src/engine/input/ecs/ism/ISM_IMPLEMENTATION_PLAN.md +0 -627
  261. package/src/engine/navigation/mesh/OVERLAPPING_SURFACES_2026_08_25.md +0 -661
  262. package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +0 -229
  263. package/src/engine/network/CONGESTION_CONTROL.md +0 -87
  264. package/src/engine/network/MESSAGE_TRANSPORT.md +0 -364
  265. package/src/engine/network/SMOOTHNESS_DESIGN.md +0 -400
  266. package/src/engine/physics/cloth/MEASUREMENTS.md +0 -5304
  267. package/src/engine/physics/cloth/PLAN.md +0 -3056
  268. package/src/engine/physics/fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md +0 -1382
  269. package/src/engine/physics/fluid/REVIEW_02_PLAN.md +0 -243
  270. package/src/engine/physics/fluid/solver/NOTES.md +0 -46
  271. package/src/engine/sound/SOUND_EMITTER_RETIREMENT_PLAN.md +0 -536
  272. package/src/engine/sound/simulation/IMPLEMENTATION_PLAN.md +0 -432
  273. package/src/engine/sound/simulation/PROBE_PLACEMENT.md +0 -250
  274. package/src/engine/sound/simulation/PROBE_TRANSFER_RESEARCH.md +0 -322
  275. package/src/engine/sound/simulation/REFLECTION_IR_PLAN.md +0 -261
  276. package/src/engine/sound/simulation/VISIBILITY_GRAPH_PLAN.md +0 -121
  277. package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +0 -999
  278. package/src/format/image/avif/DECISIONS.md +0 -545
  279. package/src/format/image/jpeg/NOTES.md +0 -3
  280. package/src/format/scene/gltf/MESHOPT_COMPRESSION_PLAN.md +0 -814
  281. package/src/generation/COORDINATES.md +0 -54
  282. package/src/generation/REVIEW_01_ACTION_PLAN.md +0 -628
  283. package/src/generation/foliage/ANNUAL_SHOOTS_BRIEF_2026_10_25.md +0 -220
  284. package/src/generation/foliage/CROWN_FILLING_BRIEF_2026_10_26.md +0 -172
  285. package/src/generation/foliage/CROWN_FULLNESS_BRIEF_2026_10_26.md +0 -86
  286. package/src/generation/foliage/FOLIAGE_DESIGN.md +0 -547
  287. package/src/generation/foliage/FOLIAGE_PLAN.md +0 -198
  288. package/src/generation/foliage/LEAF_ARCHITECTURE_BRIEF_2026_10_26.md +0 -158
  289. package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +0 -521
  290. package/src/generation/foliage/REAL_TIME_TREES_BRIEF_2026_10_27.md +0 -86
  291. package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +0 -347
  292. package/src/generation/foliage/SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md +0 -248
  293. package/src/generation/foliage/SPEEDTREE_COMPARISON_2026_10_24.md +0 -694
  294. package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +0 -552
  295. package/src/shade/RENDERER_CONTRACT.md +0 -697
  296. package/src/shade/SHADER_F16_PROPOSAL_2026_10_24.md +0 -263
  297. package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +0 -569
  298. package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +0 -755
  299. package/src/shade/device/mock/EXTENSION_PROPOSAL_2026_08_22.md +0 -458
  300. package/src/shade/device/mock/IMPLEMENTATION_PLAN.md +0 -255
  301. package/src/shade/device/mock/REVIEW_2026_08_22.md +0 -527
  302. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +0 -894
  303. package/src/shade/device/timing/profile/SGPT_FORMAT.md +0 -345
  304. package/src/shade/renderer/NOTES.md +0 -78
  305. package/src/shade/renderer/PLAN.md +0 -8
  306. package/src/shade/renderer/animation/ADDITIVE_BLENDING_PLAN.md +0 -145
  307. package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +0 -357
  308. package/src/shade/renderer/atmosphere/NOTES.md +0 -6
  309. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts +0 -76
  310. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts.map +0 -1
  311. package/src/shade/renderer/camera/orbital/MomentumAxis.js +0 -164
  312. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts +0 -45
  313. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts.map +0 -1
  314. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.js +0 -66
  315. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts +0 -98
  316. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts.map +0 -1
  317. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.js +0 -184
  318. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +0 -175
  319. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +0 -1
  320. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +0 -411
  321. package/src/shade/renderer/camera/orbital/README.md +0 -81
  322. package/src/shade/renderer/camera/orbital/index.d.ts +0 -6
  323. package/src/shade/renderer/camera/orbital/index.d.ts.map +0 -1
  324. package/src/shade/renderer/camera/orbital/index.js +0 -9
  325. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +0 -964
  326. package/src/shade/renderer/fluid/GPU_FLUID_OCCUPANCY_AND_SOTA_REVIEW_2026_10_26.md +0 -361
  327. package/src/shade/renderer/geometry/bvh/BLAS_REFIT_BATCH_PLAN.md +0 -619
  328. package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +0 -674
  329. package/src/shade/renderer/geometry/sdf/NOTES.md +0 -6
  330. package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +0 -540
  331. package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +0 -305
  332. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +0 -913
  333. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +0 -787
  334. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +0 -481
  335. package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +0 -301
  336. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +0 -448
  337. package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +0 -385
  338. package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +0 -656
  339. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +0 -308
  340. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +0 -657
  341. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +0 -551
  342. package/src/shade/renderer/geometry/virtual/runtime/SHADOW_PLAN_2026_09_28.md +0 -529
  343. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts +0 -47
  344. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts.map +0 -1
  345. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.js +0 -80
  346. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts +0 -16
  347. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts.map +0 -1
  348. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.js +0 -67
  349. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts +0 -17
  350. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts.map +0 -1
  351. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.js +0 -145
  352. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts +0 -12
  353. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts.map +0 -1
  354. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.js +0 -105
  355. package/src/shade/renderer/global_illumination/lpv/NOTES.md +0 -3
  356. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +0 -4306
  357. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md +0 -410
  358. package/src/shade/renderer/global_illumination/sharc/NOTES.md +0 -8
  359. package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +0 -1369
  360. package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +0 -202
  361. package/src/shade/renderer/gpu_primitive/indirect/NOTES.md +0 -94
  362. package/src/shade/renderer/hiz/NOTES.md +0 -3
  363. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +0 -8
  364. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +0 -1
  365. package/src/shade/renderer/light/cluster/NOTES.md +0 -11
  366. package/src/shade/renderer/light/cluster/PROPOSAL_ALGORITHM_2026_10_16.md +0 -524
  367. package/src/shade/renderer/light/cluster/PROPOSAL_HIERARCHICAL_2026_10_16.md +0 -1101
  368. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts +0 -24
  369. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts.map +0 -1
  370. package/src/shade/renderer/material/resident/shader_resident_texture_means.js +0 -120
  371. package/src/shade/renderer/particles/DESIGN.md +0 -1011
  372. package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +0 -280
  373. package/src/shade/renderer/particles/ORIENTATION_PLAN_2026_10_15.md +0 -453
  374. package/src/shade/renderer/path_tracer/PATH_TRACED_MEDIA_PLAN_2026_10_02.md +0 -520
  375. package/src/shade/renderer/postprocess/bloom/NOTES.md +0 -6
  376. package/src/shade/renderer/postprocess/fsr/NOTES.md +0 -4
  377. package/src/shade/renderer/postprocess/gtao/NOTES.md +0 -6
  378. package/src/shade/renderer/postprocess/nss/NOTES.md +0 -332
  379. package/src/shade/renderer/postprocess/perspective/NOTES.md +0 -3
  380. package/src/shade/renderer/postprocess/sscs/NOTES.md +0 -5
  381. package/src/shade/renderer/postprocess/ssr/NOTES.md +0 -14
  382. package/src/shade/renderer/postprocess/ssr/REPROJECTION_PROPOSAL.md +0 -286
  383. package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +0 -39
  384. package/src/shade/renderer/postprocess/taa/NOTES.md +0 -11
  385. package/src/shade/renderer/postprocess/upscale-dither/NOTES.md +0 -78
  386. package/src/shade/renderer/postprocess/vrs/NOTES.md +0 -4
  387. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +0 -1218
  388. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +0 -600
  389. package/src/shade/renderer/restir/reservoir/NOTES.md +0 -6
  390. package/src/shade/renderer/shader/chunk/atmosphere/NOTES.md +0 -6
  391. package/src/shade/renderer/shader/chunk/brdf/FRESNEL_BRIEF_2026_10_16.md +0 -391
  392. package/src/shade/renderer/shader/chunk/color/tonemap/ACES_TONE_MAPPING_BRIEF_2026_10_17.md +0 -153
  393. package/src/shade/renderer/shader/chunk/color/tonemap/HDR_TONE_MAPPING_FORMS_BRIEF_2026_10_16.md +0 -322
  394. package/src/shade/renderer/shader/chunk/color/tonemap/SDR_TONE_MAPPING_BRIEF_2026_10_16.md +0 -247
  395. package/src/shade/renderer/shader/chunk/fsr/NOTES.md +0 -3
  396. package/src/shade/renderer/shader/chunk/fsr/spd/NOTES.md +0 -4
  397. package/src/shade/renderer/shader/chunk/geometry/ray/NOTES.md +0 -4
  398. package/src/shade/renderer/shader/chunk/material/MULTISCATTER.md +0 -159
  399. package/src/shade/renderer/shader/chunk/mboit/NOTES.md +0 -38
  400. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/NOTES.md +0 -3
  401. package/src/shade/renderer/shader/chunk/spherical_harmonics/sampling/NOTES.md +0 -8
  402. package/src/shade/renderer/shader/compiler/SHADER_SWITCH_PROPOSAL_2026_10_16.md +0 -927
  403. package/src/shade/renderer/shadow/contact/FINDINGS.md +0 -413
  404. package/src/shade/renderer/shadow/map/csm/NOTES.md +0 -5
  405. package/src/shade/renderer/shadow/ray/NOTES.md +0 -31
  406. package/src/shade/renderer/shadow/sdf/NOTES.md +0 -9
  407. package/src/shade/renderer/shadow/sdf/brixelizer/NOTES.md +0 -5
  408. package/src/shade/renderer/shadow/sdf/cascade/NOTES.md +0 -9
  409. package/src/shade/renderer/shadow/vsm/NOTES.md +0 -12
  410. package/src/shade/renderer/shadow/vsm/PLAN.md +0 -442
  411. package/src/shade/renderer/shadow/vsm/REVIEW_LEDGER.md +0 -43
  412. package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +0 -2444
  413. package/src/shade/renderer/texture/mipmaps/NOTES.md +0 -3
  414. package/src/shade/renderer/volumetrics/NOTES.md +0 -1329
  415. package/src/shade/renderer/volumetrics/VOLUMETRIC_NOISE_PLAN_2026_10_15.md +0 -336
  416. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts +0 -23
  417. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +0 -1
  418. package/src/shade/renderer/water/medium/GPUWaterBodies.js +0 -75
  419. package/src/shade/wgsl/NOTES.md +0 -4
@@ -1,734 +1,731 @@
1
- import { assert } from "../../core/assert.js";
2
- import Water from "../graphics/ecs/water/Water.js";
3
- import WaterWaves, { WATER_WAVES_SHORE_REFLECTION_DEFAULT } from "../graphics/ecs/water/WaterWaves.js";
4
- import { System } from "../ecs/System.js";
5
- import { EventType } from "../ecs/EventType.js";
6
- import { Transform64 } from "../ecs/transform/Transform64.js";
7
- import { TransformAttachment } from "../ecs/transform-attachment/TransformAttachment.js";
8
- import { TRANSFORM64_EVENT_CHANGE } from "../ecs/transform/TRANSFORM64_EVENT_CHANGE.js";
9
- import { TRANSFORM_ATTACHMENT_EVENT_CHANGE } from "../ecs/transform-attachment/TRANSFORM_ATTACHMENT_EVENT_CHANGE.js";
10
- import { MeshletGeometry } from "../../shade/renderer/geometry/MeshletGeometry.js";
11
- import { ResourceAccessKind } from "../../core/model/ResourceAccessKind.js";
12
- import { ResourceAccessSpecification } from "../../core/model/ResourceAccessSpecification.js";
13
- import { GPUWaterRenderer } from "../../shade/renderer/water/GPUWaterRenderer.js";
14
- import { GPUWaterBodies } from "../../shade/renderer/water/medium/GPUWaterBodies.js";
15
- import { GPUWaterQueries, WATER_QUERY_SAMPLE_FLOATS } from "../../shade/renderer/water/query/GPUWaterQueries.js";
16
- import { GPUWaterSurface } from "../../shade/renderer/water/surface/GPUWaterSurface.js";
17
- import { FramePhase } from "../../shade/renderer/extension/FramePhase.js";
18
- import { RenderExtension } from "../../shade/renderer/extension/RenderExtension.js";
19
- import { SceneColor } from "../../shade/renderer/extension/SceneColor.js";
20
- import { m4_invert } from "../../core/geom/3d/mat4/m4_invert.js";
21
- import { GPUReadbackStatus } from "../../shade/renderer/buffer/table/GPUReadbackStatus.js";
22
- import { generate_record_writer } from "../../shade/renderer/buffer/table/serde/generate_record_writer.js";
23
- import { WATER_WAVE_IMPULSE_LIMIT, WATER_WAVE_RECORD_STRUCT } from "../../shade/renderer/water/surface/WATER_WAVE_RECORD_STRUCT.js";
24
- import { WATER_SPECTRUM_BAND_COUNT } from "../../shade/renderer/water/surface/spectrum/WATER_SPECTRUM.js";
25
- import { GPUFluidComponent } from "../physics/fluid/ecs/GPUFluidComponent.js";
26
- import { GPUFluidSystem } from "./GPUFluidSystem.js";
27
-
28
- // Only the authored fields: a whole-record write would undo what the GPU derives into the row.
29
- const AUTHORED_WRITER = generate_record_writer(WATER_WAVE_RECORD_STRUCT, ['upwind_fetch', 'shore_reflection', 'node', 'geometry', 'simulate_hidden']);
30
- const IMPULSE_WRITER = generate_record_writer(WATER_WAVE_RECORD_STRUCT, ['impulses', 'impulse_count']);
31
- /** The WaterWaves fields a body's row follows. */
32
- const WAVE_FIELDS = ['upwindFetch', 'shoreReflection'];
33
- /** A splash's or query's world-to-local matrix: rain can splash hundreds of times a second. */
34
- const SPLASH_INVERSE = new Float64Array(16);
35
-
36
- const REQUEST_INITIAL_CAPACITY = 16;
37
-
38
- /**
39
- * @param {Int32Array|Uint32Array|Float32Array|Float64Array|Uint8Array} array
40
- * @param {number} length
41
- * @returns {any} `array`, or a copy of it at least `length` long
42
- */
43
- function fit(array, length) {
44
- if (array.length >= length) {
45
- return array;
46
- }
47
-
48
- const grown = new array.constructor(Math.max(length, 2 * array.length));
49
-
50
- grown.set(array);
51
-
52
- return grown;
53
- }
54
-
55
- /**
56
- * Registers [Water, MeshletGeometry, Transform64] in the scene's existing GPU database. Authored
57
- * edits queue writes; idle frames never walk bodies. Transform rows follow the same change events
58
- * and GPU hierarchy as shaded geometry. Water does not enter the opaque mesh table or its TLAS.
59
- *
60
- * Every body has a surface row (WATER_SURFACE_SIMULATION_PLAN_2026_10_06.md §6.1): its fetch and shore from an
61
- * optional {@link WaterWaves}, its queued splashes, and what the GPU derives from them each frame. The GPU culls
62
- * every body against the frame's view before simulating; a hidden body is paused unless
63
- * {@link Water#simulateWhenHidden} (WATER_SURFACE_DISTANCE_LOD_PROPOSAL_2026_10_06.md §3).
64
- *
65
- * **The wind is the scene's** (GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md §6, the Water row): the culling pass
66
- * samples the published wind field 10 m above each body's origin and low-passes it per body, on the GPU. What the
67
- * system decides on the CPU is only whether that field can hold wind at all ({@link GPUWaterSurface#windy}, which
68
- * animates the shared bands): while any enabled {@link GPUFluidComponent} steps a fluid, or the far field
69
- * ({@link GPUFluidSystem#far_field}) is not still. Asked every update, allocating nothing.
70
- */
71
- export class WaterSystem extends System {
72
-
73
- /**
74
- * Offer the compute pipelines the water surface query API dispatches.
75
- *
76
- * @param {import("../../shade/device/pipeline/PipelineEnquiry.js").PipelineEnquiry} enquiry
77
- */
78
- static enquirePipelines(enquiry) {
79
- GPUWaterQueries.enquirePipelines(enquiry);
80
- }
81
-
82
- dependencies = [Water, MeshletGeometry, Transform64];
83
- components_used = [ResourceAccessSpecification.from(WaterWaves, ResourceAccessKind.Read),
84
- // whether the scene's wind field has a fluid behind it
85
- ResourceAccessSpecification.from(GPUFluidComponent, ResourceAccessKind.Read)];
86
- #graphics;
87
- #scene;
88
- #context = null;
89
- #renderer = new GPUWaterRenderer();
90
- #waves = new GPUWaterSurface();
91
- #bodies = new GPUWaterBodies();
92
- #entries = new Map();
93
- #dirty = new Set();
94
- #impulses = new Set();
95
- /** @type {GPUFluidSystem|null} the far field's source, once found */
96
- #fluid = null;
97
- #far = new Float64Array(3);
98
- /** Whether {@link #visit_domain} met an enabled component. */
99
- #enabled_found = false;
100
- #visit_domain = component => {
101
- if (component.enabled) { this.#enabled_found = true; return false; }
102
- return true;
103
- };
104
- #extensions = [];
105
- #time = 0;
106
-
107
- /*
108
- The surface read API: per handle, the request's entity, local xz points, count, footprint,
109
- target, and status.
110
- */
111
- #request_entities = new Int32Array(REQUEST_INITIAL_CAPACITY);
112
- #request_point_offsets = new Uint32Array(REQUEST_INITIAL_CAPACITY);
113
- #request_counts = new Uint32Array(REQUEST_INITIAL_CAPACITY);
114
- #request_footprints = new Float32Array(REQUEST_INITIAL_CAPACITY);
115
- #request_offsets = new Uint32Array(REQUEST_INITIAL_CAPACITY);
116
- /** @type {(Float32Array|null)[]} */
117
- #request_targets = new Array(REQUEST_INITIAL_CAPACITY).fill(null);
118
- #request_points = new Float64Array(2 * REQUEST_INITIAL_CAPACITY);
119
- #request_point_count = 0;
120
- #world_to_local_xz = new Float64Array(2 * REQUEST_INITIAL_CAPACITY);
121
- #status = new Uint8Array(REQUEST_INITIAL_CAPACITY);
122
-
123
- /** Recycled handles. */
124
- #free_handles = new Uint32Array(REQUEST_INITIAL_CAPACITY);
125
- #free_count = 0;
126
-
127
- /** Handles below this have been handed out at least once. */
128
- #fresh = 0;
129
-
130
- /** Handles asked for and not yet answered headless or handed to the device. */
131
- #asked = new Uint32Array(REQUEST_INITIAL_CAPACITY);
132
- #asked_count = 0;
133
-
134
- /** Handles that settled at the last apply point, recycled at the next. */
135
- #settled = new Uint32Array(REQUEST_INITIAL_CAPACITY);
136
- #settled_count = 0;
137
-
138
- /**
139
- * The read API on the device: its database, gather and readback. Made by the first frame with a
140
- * request, dropped with the device.
141
- *
142
- * @type {GPUWaterQueries|null}
143
- */
144
- #queries = null;
145
-
146
- get settings() { return this.#renderer.settings; }
147
- get waves() { return this.#waves; }
148
- get transparency_channel() { return this.#renderer; }
149
- get scene() { return this.#scene; }
150
- request_probe(uv) { return this.#renderer.request_probe(uv); }
151
- get time() { return this.#time; }
152
- set time(seconds) {
153
- assert.ok(Number.isFinite(seconds) && seconds >= 0, 'water time must be finite and non-negative');
154
- this.#time = seconds;
155
- }
156
-
157
- /**
158
- * What became of each surface query request, by handle: a `GPUReadbackStatus`. `Pending` from
159
- * {@link request_local} / {@link request_world} until the apply point ({@link update}) that
160
- * answers it, `Delivered` (the samples are unpacked in `target`) or `Dropped` (the target is as
161
- * it was) from there until the next apply point, then `None`, the handle recycled.
162
- *
163
- * Read it fresh each time rather than keeping a reference: the array is replaced when the
164
- * handle space grows.
165
- *
166
- * @returns {Uint8Array}
167
- */
168
- get status() {
169
- return this.#status;
170
- }
171
-
172
- /**
173
- * The surface read API's device half, or null before the first frame with a request on a
174
- * device. For inspection.
175
- *
176
- * @returns {GPUWaterQueries|null}
177
- */
178
- get queries() {
179
- return this.#queries;
180
- }
181
-
182
- constructor(graphics, scene) {
183
- super(); assert.defined(graphics, 'graphics'); assert.defined(scene, 'scene');
184
- this.#graphics = graphics; this.#scene = scene;
185
- this.#renderer.scene = scene;
186
- this.#waves.onInputsConsumed.add(context => {
187
- if (context !== this.#context) { return; }
188
- for (const entry of this.#impulses) {
189
- entry.staged.impulse_count = 0;
190
- if (entry.queue.length === 0) { this.#impulses.delete(entry); }
191
- }
192
- });
193
- this.#renderer.bodies = this.#bodies;
194
- this.#renderer.waves = this.#waves;
195
- }
196
-
197
- async startup(entityManager) {
198
- this.entityManager = entityManager;
199
- assert.ok(this.#scene.dataset == null || this.#scene.dataset === entityManager.dataset, 'scene belongs to another dataset');
200
- this.#scene.dataset = entityManager.dataset;
201
- this.#graphics.add_transparency_channel(this.#renderer);
202
- this.#extensions = [this.#graphics.add_extension(new WaterExtension(this)),
203
- this.#graphics.add_extension(new WaterSurfaceExtension(this))];
204
- }
205
-
206
- async shutdown() {
207
- for (const entry of Array.from(this.#entries.values())) { this.unlink(entry.water, entry.geometry, entry.transform, entry.entity); }
208
- this.#drop_queries();
209
- this.#graphics.remove_transparency_channel(this.#renderer);
210
- for (const extension of this.#extensions) { this.#graphics.remove_extension(extension); }
211
- this.#extensions.length = 0;
212
- this.#renderer.dispose(); this.#waves.dispose(); this.#context = null;
213
- }
214
-
215
- link(water, geometry, transform, entity) {
216
- // queue: splashes not yet staged, four numbers each (local x, z, radius, kick); staged: the batch on the
217
- // GPU row until a submitted frame spends it
218
- const entry = { water, geometry, transform, entity, context: null, row: -1, wave: -1, waves: null, queue: [],
219
- staged: { impulses: Array.from({ length: WATER_WAVE_IMPULSE_LIMIT }, () => new Float64Array(4)), impulse_count: 0 } };
220
- const dataset = this.entityManager.dataset;
221
- entry.changed = () => this.#dirty.add(entry);
222
- entry.world = () => entry.context?.rows.mark_world(entity);
223
- entry.local = () => entry.context?.rows.mark_local(entity);
224
- entry.components = ({ klass }) => {
225
- if (klass === WaterWaves) { this.#watch_waves(entry); entry.changed(); }
226
- if (klass === TransformAttachment) { entry.local(); }
227
- };
228
- water.onChanged.add(entry.changed);
229
- dataset.addEntityEventListener(entity, TRANSFORM64_EVENT_CHANGE, entry.world);
230
- dataset.addEntityEventListener(entity, TRANSFORM_ATTACHMENT_EVENT_CHANGE, entry.local);
231
- dataset.addEntityEventListener(entity, EventType.ComponentAdded, entry.components);
232
- dataset.addEntityEventListener(entity, EventType.ComponentRemoved, entry.components);
233
- this.#entries.set(entity, entry);
234
- this.#watch_waves(entry);
235
- this.#dirty.add(entry);
236
- this.#renderer.body_count++;
237
- }
238
-
239
- #watch_waves(entry) {
240
- if (entry.waves != null) { for (const field of WAVE_FIELDS) { entry.waves[field].onChanged.remove(entry.changed); } }
241
- entry.waves = this.entityManager.dataset.getComponent(entry.entity, WaterWaves);
242
- if (entry.waves != null) { for (const field of WAVE_FIELDS) { entry.waves[field].onChanged.add(entry.changed); } }
243
- }
244
-
245
- unlink(water, geometry, transform, entity) {
246
- const entry = this.#entries.get(entity);
247
- if (entry === undefined) { return; }
248
- const dataset = this.entityManager.dataset;
249
- water.onChanged.remove(entry.changed);
250
- if (entry.waves != null) { for (const field of WAVE_FIELDS) { entry.waves[field].onChanged.remove(entry.changed); } }
251
- dataset.removeEntityEventListener(entity, TRANSFORM64_EVENT_CHANGE, entry.world);
252
- dataset.removeEntityEventListener(entity, TRANSFORM_ATTACHMENT_EVENT_CHANGE, entry.local);
253
- dataset.removeEntityEventListener(entity, EventType.ComponentAdded, entry.components);
254
- dataset.removeEntityEventListener(entity, EventType.ComponentRemoved, entry.components);
255
- if (entry.context !== null && entry.context === this.#graphics.scene_context(this.#scene)) {
256
- entry.context.scene_database.get('water_bodies').remove(entry.row);
257
- entry.context.rows.release(entity);
258
- }
259
- this.#entries.delete(entity); this.#dirty.delete(entry); this.#impulses.delete(entry);
260
- this.#renderer.body_count--;
261
- if (entry.context !== null) { this.#renderer.meshlet_capacity -= entry.geometry.meshlets.count; }
262
- }
263
-
264
- update(dt) {
265
- this.#recycle_requests();
266
- this.#time += dt; this.#renderer.time = this.#time; this.#waves.advance(dt);
267
- this.#waves.windy = this.#wind_can_blow();
268
- const context = this.#graphics.scene_context(this.#scene);
269
- if (context === null) {
270
- if (this.#context !== null || this.#queries !== null) {
271
- this.#drop_queries();
272
- this.#context = null;
273
- }
274
- this.#answer_requests(null);
275
- return;
276
- }
277
- if (context !== this.#context) {
278
- this.#drop_queries();
279
- this.#context = context;
280
- // Device replacement is the only whole-scene rebuild; ordinary frames drain edits.
281
- for (const entry of this.#entries.values()) {
282
- entry.context = null; entry.row = -1; entry.wave = -1;
283
- // a batch the old context never spent goes back to the front of the queue
284
- const staged = [];
285
- for (let i = 0; i < entry.staged.impulse_count; i++) { staged.push(...entry.staged.impulses[i]); }
286
- entry.queue.unshift(...staged); entry.staged.impulse_count = 0;
287
- this.#dirty.add(entry);
288
- }
289
- this.#renderer.meshlet_capacity = 0;
290
- }
291
- for (const entry of this.#dirty) { this.#write(entry, context); }
292
- this.#dirty.clear();
293
- for (const entry of this.#impulses) { this.#stage(entry, context); }
294
- this.#answer_requests(context);
295
- }
296
-
297
- /**
298
- * Whether the scene's wind field can hold wind: an enabled {@link GPUFluidComponent} on any entity, or a far
299
- * field that is not still. The lowest holder of the component answers without a walk; only when it is disabled
300
- * are the others visited.
301
- * @returns {boolean}
302
- */
303
- #wind_can_blow() {
304
- const em = this.entityManager, dataset = em.dataset;
305
- if (dataset !== null) {
306
- const lowest = dataset.getAnyComponentInstance(GPUFluidComponent);
307
- if (lowest !== null) {
308
- if (lowest.enabled) { return true; }
309
- this.#enabled_found = false;
310
- dataset.traverseComponents(GPUFluidComponent, this.#visit_domain);
311
- if (this.#enabled_found) { return true; }
312
- }
313
- }
314
- // registered later than this system, or never
315
- const fluid = this.#fluid ??= em.getSystem(GPUFluidSystem);
316
- if (fluid === null) { return false; }
317
- const far = this.#far;
318
- fluid.far_field(far);
319
- return far[0] !== 0 || far[1] !== 0 || far[2] !== 0;
320
- }
321
-
322
- /** Top up a body's batch on its surface row from its queue, up to the row's limit. */
323
- #stage(entry, context) {
324
- const staged = entry.staged, queue = entry.queue;
325
- if (entry.wave === -1 || queue.length === 0 || staged.impulse_count === WATER_WAVE_IMPULSE_LIMIT) { return; }
326
- const taken = Math.min(queue.length / 4, WATER_WAVE_IMPULSE_LIMIT - staged.impulse_count);
327
- for (let i = 0; i < taken; i++) {
328
- const impulse = staged.impulses[staged.impulse_count++];
329
- for (let c = 0; c < 4; c++) { impulse[c] = queue[4 * i + c]; }
330
- }
331
- queue.copyWithin(0, 4 * taken);
332
- queue.length -= 4 * taken;
333
- this.#waves.splashed = true;
334
- for (let i = staged.impulse_count; i < WATER_WAVE_IMPULSE_LIMIT; i++) { staged.impulses[i].fill(0); }
335
- context.scene_database.get('water_waves').set_fields(entry.wave, staged, IMPULSE_WRITER);
336
- }
337
-
338
- #write(entry, context) {
339
- const bodies = context.scene_database.get('water_bodies');
340
- const waves = context.scene_database.get('water_waves');
341
- if (entry.context === null) {
342
- context.rows.acquire(entry.entity);
343
- context.geometries.add(entry.geometry);
344
- entry.context = context;
345
- this.#renderer.meshlet_capacity += entry.geometry.meshlets.count;
346
- }
347
- const geometry = context.geometries.getAddress(entry.geometry);
348
- if (entry.wave === -1) {
349
- // the GPU seeds the wind from its first sample, and derives the weights for it
350
- entry.wave = waves.add({ upwind_fetch: 0, shore_reflection: WATER_WAVES_SHORE_REFLECTION_DEFAULT,
351
- impulses: Array.from({ length: WATER_WAVE_IMPULSE_LIMIT }, () => [0, 0, 0, 0]), impulse_count: 0,
352
- node: entry.entity + 1, geometry, simulate_hidden: 0, wind: [0, 0, 0], wind_seeded: 0, derived_for: [-1, -1, 0, 0],
353
- weights: new Array(WATER_SPECTRUM_BAND_COUNT).fill(0), slope_variance: 0,
354
- wind_from: [0, 0], wind_to: [0, 0], wind_blend: 1, displacement_bounds: [0, 0, 0, 0], particle_levels: 0 });
355
- }
356
- const waves_component = entry.waves;
357
- const reflection = Math.min(1, Math.max(0, waves_component?.shoreReflection.getValue() ?? WATER_WAVES_SHORE_REFLECTION_DEFAULT));
358
- waves.set_fields(entry.wave, { upwind_fetch: Math.max(0, waves_component?.upwindFetch.getValue() ?? 0), shore_reflection: reflection, node: entry.entity + 1, geometry,
359
- simulate_hidden: entry.water.simulateWhenHidden.getValue() ? 1 : 0 }, AUTHORED_WRITER);
360
- const water = entry.water;
361
- const record = { geometry, node: entry.entity + 1,
362
- dirty: 1,
363
- wave: entry.wave, ior: Math.max(1, water.ior.getValue()),
364
- roughness: Math.min(1, Math.max(0, water.roughness.getValue())), medium: water.medium.to_gpu_record(),
365
- backscattering: [0, 0, 0, 0], extinction: [0, 0, 0, 0], diffuse_transmittance: 0 };
366
- if (entry.row === -1) { entry.row = bodies.add(record); } else { bodies.set(entry.row, record); }
367
- }
368
-
369
- /**
370
- * A splash at a world position: rings of wave particles of the given world radius (half their wavelength)
371
- * whose height is the given vertical velocity kick, m/s, over the time a crest takes to cross them. The
372
- * position is interpreted in the CPU-owned pose; queued on the CPU and spent on the GPU, sixteen a body a frame.
373
- */
374
- splash(entity, position, radius, strength) {
375
- const entry = this.#entries.get(entity); assert.defined(entry, 'water entity');
376
- assert.greaterThan(radius, 0, 'radius'); assert.isFinite(radius, 'radius'); assert.isFinite(strength, 'strength');
377
- for (let i = 0; i < 3; i++) { assert.isFinite(position[i], `position[${i}]`); }
378
- const inverse = SPLASH_INVERSE;
379
- const inverted = m4_invert(inverse, entry.transform);
380
- assert.ok(inverted, 'water transform is singular');
381
- const x = inverse[0] * position[0] + inverse[4] * position[1] + inverse[8] * position[2] + inverse[12];
382
- const z = inverse[2] * position[0] + inverse[6] * position[1] + inverse[10] * position[2] + inverse[14];
383
- entry.queue.push(x, z, radius, strength); this.#impulses.add(entry);
384
- }
385
-
386
- /**
387
- * Ask for the water surface height and world-space unit normal at `count` points in the water
388
- * body's local `(x, z)` plane: `local_xz` holds `[x0, z0, x1, z1, ...]`, and each sample lands
389
- * in `target` from `target_offset` as {@link WATER_QUERY_SAMPLE_FLOATS} (4) floats,
390
- * `[world_height, nx, ny, nz]`, unpacked on the CPU when the request settles `Delivered` at a
391
- * later apply point ({@link update}); see {@link status}.
392
- *
393
- * Points outside the body's authored outline or inside island holes are clamped continuously to
394
- * the nearest point on the mesh's triangles so no `NaN`s are ever produced.
395
- *
396
- * @param {number} entity water entity
397
- * @param {ArrayLike<number>} local_xz interleaved `[x0, z0, x1, z1, ...]` in local body coordinates
398
- * @param {number} count number of `(x, z)` points, at least 1
399
- * @param {number} footprint spatial filter footprint in metres, at least 0 (0 resolves all wavelengths)
400
- * @param {Float32Array} target memory the caller owns and keeps until the request settles
401
- * @param {number} target_offset in floats
402
- * @returns {number} a handle into {@link status}
403
- */
404
- request_local(entity, local_xz, count, footprint, target, target_offset) {
405
- const entry = this.#entries.get(entity);
406
- assert.defined(entry, 'water entity');
407
- assert.isArrayLike(local_xz, 'local_xz');
408
- assert.isPositiveInteger(count, 'count');
409
- assert.greaterThanOrEqual(local_xz.length, 2 * count, 'local_xz');
410
- for (let i = 0; i < 2 * count; i++) {
411
- assert.isFinite(local_xz[i], `local_xz[${i}]`);
412
- }
413
- assert.isFinite(footprint, 'footprint');
414
- assert.greaterThanOrEqual(footprint, 0, 'footprint');
415
- assert.ok(target instanceof Float32Array, 'target must be a Float32Array');
416
- assert.isNonNegativeInteger(target_offset, 'target_offset');
417
- assert.lessThanOrEqual(target_offset + WATER_QUERY_SAMPLE_FLOATS * count, target.length, 'the query samples reach past the target');
418
-
419
- const h = this.#allocate_handle();
420
- const point_start = this.#request_point_count;
421
- const next_point_count = point_start + count;
422
-
423
- if (2 * next_point_count > this.#request_points.length) {
424
- this.#request_points = fit(this.#request_points, 2 * next_point_count);
425
- }
426
-
427
- const points = this.#request_points;
428
-
429
- for (let i = 0; i < 2 * count; i++) {
430
- points[2 * point_start + i] = local_xz[i];
431
- }
432
-
433
- this.#request_point_count = next_point_count;
434
- this.#request_entities[h] = entity;
435
- this.#request_point_offsets[h] = point_start;
436
- this.#request_counts[h] = count;
437
- this.#request_footprints[h] = footprint;
438
- this.#request_offsets[h] = target_offset;
439
- this.#request_targets[h] = target;
440
- this.#status[h] = GPUReadbackStatus.Pending;
441
-
442
- if (this.#asked_count === this.#asked.length) {
443
- this.#asked = fit(this.#asked, this.#asked_count + 1);
444
- }
445
-
446
- this.#asked[this.#asked_count++] = h;
447
-
448
- return h;
449
- }
450
-
451
- /**
452
- * Ask for the water surface height and world-space unit normal at `count` points in world
453
- * space: `world_xyz` holds `[x0, y0, z0, x1, y1, z1, ...]`. Internally transforms each world
454
- * point into the water body's local `(x, z)` plane via the inverse of its CPU-owned
455
- * `Transform64` and delegates to {@link request_local}.
456
- *
457
- * @param {number} entity water entity
458
- * @param {ArrayLike<number>} world_xyz interleaved `[x0, y0, z0, ...]` in world metres
459
- * @param {number} count number of `(x, y, z)` points, at least 1
460
- * @param {number} footprint spatial filter footprint in metres, at least 0 (0 resolves all wavelengths)
461
- * @param {Float32Array} target memory the caller owns and keeps until the request settles
462
- * @param {number} target_offset in floats
463
- * @returns {number} a handle into {@link status}
464
- */
465
- request_world(entity, world_xyz, count, footprint, target, target_offset) {
466
- const entry = this.#entries.get(entity);
467
- assert.defined(entry, 'water entity');
468
- assert.isArrayLike(world_xyz, 'world_xyz');
469
- assert.isPositiveInteger(count, 'count');
470
- assert.greaterThanOrEqual(world_xyz.length, 3 * count, 'world_xyz');
471
- for (let i = 0; i < 3 * count; i++) {
472
- assert.isFinite(world_xyz[i], `world_xyz[${i}]`);
473
- }
474
-
475
- const inverse = SPLASH_INVERSE;
476
- const inverted = m4_invert(inverse, entry.transform);
477
- assert.ok(inverted, 'water transform is singular');
478
-
479
- if (2 * count > this.#world_to_local_xz.length) {
480
- this.#world_to_local_xz = fit(this.#world_to_local_xz, 2 * count);
481
- }
482
-
483
- const local_xz = this.#world_to_local_xz;
484
-
485
- for (let i = 0; i < count; i++) {
486
- const wx = world_xyz[3 * i];
487
- const wy = world_xyz[3 * i + 1];
488
- const wz = world_xyz[3 * i + 2];
489
-
490
- local_xz[2 * i] = inverse[0] * wx + inverse[4] * wy + inverse[8] * wz + inverse[12];
491
- local_xz[2 * i + 1] = inverse[2] * wx + inverse[6] * wy + inverse[10] * wz + inverse[14];
492
- }
493
-
494
- return this.request_local(entity, local_xz, count, footprint, target, target_offset);
495
- }
496
-
497
- /**
498
- * @returns {number}
499
- */
500
- #allocate_handle() {
501
- if (this.#free_count > 0) {
502
- return this.#free_handles[--this.#free_count];
503
- }
504
-
505
- const h = this.#fresh++;
506
-
507
- if (h === this.#status.length) {
508
- const capacity = 2 * h;
509
-
510
- this.#request_entities = fit(this.#request_entities, capacity);
511
- this.#request_point_offsets = fit(this.#request_point_offsets, capacity);
512
- this.#request_counts = fit(this.#request_counts, capacity);
513
- this.#request_footprints = fit(this.#request_footprints, capacity);
514
- this.#request_offsets = fit(this.#request_offsets, capacity);
515
- this.#status = fit(this.#status, capacity);
516
- this.#free_handles = fit(this.#free_handles, capacity);
517
- this.#settled = fit(this.#settled, capacity);
518
-
519
- for (let i = this.#request_targets.length; i < capacity; i++) {
520
- this.#request_targets.push(null);
521
- }
522
- }
523
-
524
- return h;
525
- }
526
-
527
- /**
528
- * A request answered: its status set, readable until the next apply point.
529
- *
530
- * @param {number} h
531
- * @param {number} status
532
- */
533
- #settle(h, status) {
534
- this.#status[h] = status;
535
- this.#request_targets[h] = null;
536
- this.#settled[this.#settled_count++] = h;
537
- }
538
-
539
- /**
540
- * The apply point's first half: the handles answered at the last one go back to the free list.
541
- */
542
- #recycle_requests() {
543
- const settled = this.#settled;
544
- const status = this.#status;
545
- const free = this.#free_handles;
546
-
547
- for (let i = 0; i < this.#settled_count; i++) {
548
- const h = settled[i];
549
-
550
- status[h] = GPUReadbackStatus.None;
551
- free[this.#free_count++] = h;
552
- }
553
-
554
- this.#settled_count = 0;
555
- }
556
-
557
- /**
558
- * The apply point's second half: what the device delivered or dropped, and, headless, every
559
- * request asked since from the body's rest surface.
560
- *
561
- * @param {object|null} context
562
- */
563
- #answer_requests(context) {
564
- const queries = this.#queries;
565
-
566
- if (queries !== null) {
567
- queries.apply();
568
- this.#take_settled(queries);
569
- }
570
-
571
- if (context !== null || this.#asked_count === 0) {
572
- return;
573
- }
574
-
575
- const asked = this.#asked;
576
- const points = this.#request_points;
577
-
578
- for (let i = 0; i < this.#asked_count; i++) {
579
- const h = asked[i];
580
- const entry = this.#entries.get(this.#request_entities[h]);
581
-
582
- if (entry === undefined) {
583
- this.#settle(h, GPUReadbackStatus.Dropped);
584
- continue;
585
- }
586
-
587
- const m = entry.transform;
588
- const inverse = SPLASH_INVERSE;
589
- const inverted = m4_invert(inverse, m);
590
- let nx = 0, ny = 1, nz = 0;
591
- if (inverted) {
592
- const rx = inverse[1], ry = inverse[5], rz = inverse[9];
593
- const len = Math.hypot(rx, ry, rz);
594
- if (len > 1e-20) {
595
- nx = rx / len;
596
- ny = ry / len;
597
- nz = rz / len;
598
- }
599
- }
600
-
601
- const box = entry.geometry.bounding_box;
602
- const mid_y = 0.5 * (box[1] + box[4]);
603
- const count = this.#request_counts[h];
604
- const point_base = 2 * this.#request_point_offsets[h];
605
- const target = this.#request_targets[h];
606
- let at = this.#request_offsets[h];
607
-
608
- for (let k = 0; k < count; k++, at += WATER_QUERY_SAMPLE_FLOATS) {
609
- const lx = Math.min(Math.max(points[point_base + 2 * k], box[0]), box[3]);
610
- const lz = Math.min(Math.max(points[point_base + 2 * k + 1], box[2]), box[5]);
611
- const world_y = m[1] * lx + m[5] * mid_y + m[9] * lz + m[13];
612
-
613
- target[at] = world_y;
614
- target[at + 1] = nx;
615
- target[at + 2] = ny;
616
- target[at + 3] = nz;
617
- }
618
-
619
- this.#settle(h, GPUReadbackStatus.Delivered);
620
- }
621
-
622
- this.#request_point_count = 0;
623
- this.#asked_count = 0;
624
- }
625
-
626
- /**
627
- * @param {GPUWaterQueries} queries after an apply or a destroy
628
- */
629
- #take_settled(queries) {
630
- for (let i = 0; i < queries.settled_count; i++) {
631
- this.#settle(queries.settled_tag(i), queries.settled_status(i));
632
- }
633
- }
634
-
635
- /**
636
- * Let go of the read API's device half: whatever it had in flight is dropped, its status set now.
637
- */
638
- #drop_queries() {
639
- const queries = this.#queries;
640
-
641
- if (queries === null) {
642
- return;
643
- }
644
-
645
- queries.destroy();
646
-
647
- this.#take_settled(queries);
648
-
649
- this.#queries = null;
650
- }
651
-
652
- /**
653
- * Hand the frame's requests to the device, after the frame's surface simulation passes.
654
- *
655
- * @param {import("../graphics/render/frame_graph/FrameGraph.js").FrameGraph} graph
656
- * @param {object} scene
657
- * @param {GPUDevice} device
658
- */
659
- #record_requests(graph, scene, device) {
660
- const count = this.#asked_count;
661
-
662
- if (count === 0) {
663
- return;
664
- }
665
-
666
- let queries = this.#queries;
667
-
668
- if (queries === null) {
669
- const graphics = this.#graphics.renderer?.graphics;
670
- if (graphics?.compute_pipelines !== undefined) {
671
- const enquiry = { device, render: [], compute: [] };
672
- WaterSystem.enquirePipelines(enquiry);
673
- for (let i = 0; i < enquiry.compute.length; i++) {
674
- graphics.compute_pipelines.prepare(enquiry.compute[i]);
675
- }
676
- }
677
- queries = this.#queries = new GPUWaterQueries({ device });
678
- }
679
-
680
- const asked = this.#asked;
681
- const points = this.#request_points;
682
-
683
- for (let i = 0; i < count; i++) {
684
- const h = asked[i];
685
- const entry = this.#entries.get(this.#request_entities[h]);
686
-
687
- if (entry === undefined || entry.row === -1) {
688
- this.#settle(h, GPUReadbackStatus.Dropped);
689
- continue;
690
- }
691
-
692
- queries.request(
693
- entry.row,
694
- points,
695
- 2 * this.#request_point_offsets[h],
696
- this.#request_counts[h],
697
- this.#request_footprints[h],
698
- this.#request_targets[h],
699
- this.#request_offsets[h],
700
- h
701
- );
702
- }
703
-
704
- this.#request_point_count = 0;
705
- this.#asked_count = 0;
706
-
707
- queries.record({ graph, scene, surface: this.#waves });
708
- }
709
-
710
- record_surface(frame) {
711
- const view = frame.view, scene = view.scene;
712
- if (scene.scene !== this.#scene) { return; }
713
- const device = this.#graphics.renderer.graphics.device;
714
- this.#waves.record({ graph: frame.graph, device, scene, time: this.#time,
715
- view: { camera: view.camera.gpu_buffer, projection: view.camera.camera.projection_matrix, height: view.resolution[1] },
716
- pixels: this.#renderer.settings.tessellation_pixels });
717
- this.#record_requests(frame.graph, scene, device);
718
- }
719
- record(frame) {
720
- const inspection = this.#renderer.inspect(frame.graph, frame.view, frame.resolution);
721
- if (inspection !== undefined) { frame.get(SceneColor).color = inspection; }
722
- }
723
- }
724
-
725
- export class WaterExtension extends RenderExtension {
726
- name = 'water'; phase = FramePhase.BeforePresent;
727
- constructor(system) { super(); this.system = system; }
728
- record(frame) { this.system.record(frame); }
729
- }
730
- export class WaterSurfaceExtension extends RenderExtension {
731
- name = 'water surface'; phase = FramePhase.FrameStart;
732
- constructor(system) { super(); this.system = system; }
733
- record(frame) { this.system.record_surface(frame); }
734
- }
1
+ import { assert } from "../../core/assert.js";
2
+ import Water from "../graphics/ecs/water/Water.js";
3
+ import WaterWaves, { WATER_WAVES_SHORE_REFLECTION_DEFAULT } from "../graphics/ecs/water/WaterWaves.js";
4
+ import { System } from "../ecs/System.js";
5
+ import { EventType } from "../ecs/EventType.js";
6
+ import { Transform64 } from "../ecs/transform/Transform64.js";
7
+ import { TransformAttachment } from "../ecs/transform-attachment/TransformAttachment.js";
8
+ import { TRANSFORM64_EVENT_CHANGE } from "../ecs/transform/TRANSFORM64_EVENT_CHANGE.js";
9
+ import { TRANSFORM_ATTACHMENT_EVENT_CHANGE } from "../ecs/transform-attachment/TRANSFORM_ATTACHMENT_EVENT_CHANGE.js";
10
+ import { MeshletGeometry } from "../../shade/renderer/geometry/MeshletGeometry.js";
11
+ import { ResourceAccessKind } from "../../core/model/ResourceAccessKind.js";
12
+ import { ResourceAccessSpecification } from "../../core/model/ResourceAccessSpecification.js";
13
+ import { GPUWaterRenderer } from "../../shade/renderer/water/GPUWaterRenderer.js";
14
+ import { GPUWaterQueries, WATER_QUERY_SAMPLE_FLOATS } from "../../shade/renderer/water/query/GPUWaterQueries.js";
15
+ import { GPUWaterSurface } from "../../shade/renderer/water/surface/GPUWaterSurface.js";
16
+ import { FramePhase } from "../../shade/renderer/extension/FramePhase.js";
17
+ import { RenderExtension } from "../../shade/renderer/extension/RenderExtension.js";
18
+ import { SceneColor } from "../../shade/renderer/extension/SceneColor.js";
19
+ import { m4_invert } from "../../core/geom/3d/mat4/m4_invert.js";
20
+ import { GPUReadbackStatus } from "../../shade/renderer/buffer/table/GPUReadbackStatus.js";
21
+ import { generate_record_writer } from "../../shade/renderer/buffer/table/serde/generate_record_writer.js";
22
+ import { WATER_WAVE_IMPULSE_LIMIT, WATER_WAVE_RECORD_STRUCT } from "../../shade/renderer/water/surface/WATER_WAVE_RECORD_STRUCT.js";
23
+ import { WATER_SPECTRUM_BAND_COUNT } from "../../shade/renderer/water/surface/spectrum/WATER_SPECTRUM.js";
24
+ import { GPUFluidComponent } from "../physics/fluid/ecs/GPUFluidComponent.js";
25
+ import { GPUFluidSystem } from "./GPUFluidSystem.js";
26
+
27
+ // Only the authored fields: a whole-record write would undo what the GPU derives into the row.
28
+ const AUTHORED_WRITER = generate_record_writer(WATER_WAVE_RECORD_STRUCT, ['upwind_fetch', 'shore_reflection', 'node', 'geometry', 'simulate_hidden']);
29
+ const IMPULSE_WRITER = generate_record_writer(WATER_WAVE_RECORD_STRUCT, ['impulses', 'impulse_count']);
30
+ /** The WaterWaves fields a body's row follows. */
31
+ const WAVE_FIELDS = ['upwindFetch', 'shoreReflection'];
32
+ /** A splash's or query's world-to-local matrix: rain can splash hundreds of times a second. */
33
+ const SPLASH_INVERSE = new Float64Array(16);
34
+
35
+ const REQUEST_INITIAL_CAPACITY = 16;
36
+
37
+ /**
38
+ * @param {Int32Array|Uint32Array|Float32Array|Float64Array|Uint8Array} array
39
+ * @param {number} length
40
+ * @returns {any} `array`, or a copy of it at least `length` long
41
+ */
42
+ function fit(array, length) {
43
+ if (array.length >= length) {
44
+ return array;
45
+ }
46
+
47
+ const grown = new array.constructor(Math.max(length, 2 * array.length));
48
+
49
+ grown.set(array);
50
+
51
+ return grown;
52
+ }
53
+
54
+ /**
55
+ * Registers [Water, MeshletGeometry, Transform64] in the scene's existing GPU database. Authored
56
+ * edits queue writes; idle frames never walk bodies. Transform rows follow the same change events
57
+ * and GPU hierarchy as shaded geometry. Water does not enter the opaque mesh table or its TLAS.
58
+ *
59
+ * Every body has a surface row (WATER_SURFACE_SIMULATION_PLAN_2026_10_06.md §6.1): its fetch and shore from an
60
+ * optional {@link WaterWaves}, its queued splashes, and what the GPU derives from them each frame. The GPU culls
61
+ * every body against the frame's view before simulating; a hidden body is paused unless
62
+ * {@link Water#simulateWhenHidden} (WATER_SURFACE_DISTANCE_LOD_PROPOSAL_2026_10_06.md §3).
63
+ *
64
+ * **The wind is the scene's** (GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md §6, the Water row): the culling pass
65
+ * samples the published wind field 10 m above each body's origin and low-passes it per body, on the GPU. What the
66
+ * system decides on the CPU is only whether that field can hold wind at all ({@link GPUWaterSurface#windy}, which
67
+ * animates the shared bands): while any enabled {@link GPUFluidComponent} steps a fluid, or the far field
68
+ * ({@link GPUFluidSystem#far_field}) is not still. Asked every update, allocating nothing.
69
+ */
70
+ export class WaterSystem extends System {
71
+
72
+ /**
73
+ * Offer the compute pipelines the water surface query API dispatches.
74
+ *
75
+ * @param {import("../../shade/device/pipeline/PipelineEnquiry.js").PipelineEnquiry} enquiry
76
+ */
77
+ static enquirePipelines(enquiry) {
78
+ GPUWaterQueries.enquirePipelines(enquiry);
79
+ }
80
+
81
+ dependencies = [Water, MeshletGeometry, Transform64];
82
+ components_used = [ResourceAccessSpecification.from(WaterWaves, ResourceAccessKind.Read),
83
+ // whether the scene's wind field has a fluid behind it
84
+ ResourceAccessSpecification.from(GPUFluidComponent, ResourceAccessKind.Read)];
85
+ #graphics;
86
+ #scene;
87
+ #context = null;
88
+ #renderer = new GPUWaterRenderer();
89
+ #waves = new GPUWaterSurface();
90
+ #entries = new Map();
91
+ #dirty = new Set();
92
+ #impulses = new Set();
93
+ /** @type {GPUFluidSystem|null} the far field's source, once found */
94
+ #fluid = null;
95
+ #far = new Float64Array(3);
96
+ /** Whether {@link #visit_domain} met an enabled component. */
97
+ #enabled_found = false;
98
+ #visit_domain = component => {
99
+ if (component.enabled) { this.#enabled_found = true; return false; }
100
+ return true;
101
+ };
102
+ #extensions = [];
103
+ #time = 0;
104
+
105
+ /*
106
+ The surface read API: per handle, the request's entity, local xz points, count, footprint,
107
+ target, and status.
108
+ */
109
+ #request_entities = new Int32Array(REQUEST_INITIAL_CAPACITY);
110
+ #request_point_offsets = new Uint32Array(REQUEST_INITIAL_CAPACITY);
111
+ #request_counts = new Uint32Array(REQUEST_INITIAL_CAPACITY);
112
+ #request_footprints = new Float32Array(REQUEST_INITIAL_CAPACITY);
113
+ #request_offsets = new Uint32Array(REQUEST_INITIAL_CAPACITY);
114
+ /** @type {(Float32Array|null)[]} */
115
+ #request_targets = new Array(REQUEST_INITIAL_CAPACITY).fill(null);
116
+ #request_points = new Float64Array(2 * REQUEST_INITIAL_CAPACITY);
117
+ #request_point_count = 0;
118
+ #world_to_local_xz = new Float64Array(2 * REQUEST_INITIAL_CAPACITY);
119
+ #status = new Uint8Array(REQUEST_INITIAL_CAPACITY);
120
+
121
+ /** Recycled handles. */
122
+ #free_handles = new Uint32Array(REQUEST_INITIAL_CAPACITY);
123
+ #free_count = 0;
124
+
125
+ /** Handles below this have been handed out at least once. */
126
+ #fresh = 0;
127
+
128
+ /** Handles asked for and not yet answered headless or handed to the device. */
129
+ #asked = new Uint32Array(REQUEST_INITIAL_CAPACITY);
130
+ #asked_count = 0;
131
+
132
+ /** Handles that settled at the last apply point, recycled at the next. */
133
+ #settled = new Uint32Array(REQUEST_INITIAL_CAPACITY);
134
+ #settled_count = 0;
135
+
136
+ /**
137
+ * The read API on the device: its database, gather and readback. Made by the first frame with a
138
+ * request, dropped with the device.
139
+ *
140
+ * @type {GPUWaterQueries|null}
141
+ */
142
+ #queries = null;
143
+
144
+ get settings() { return this.#renderer.settings; }
145
+ get waves() { return this.#waves; }
146
+ get transparency_channel() { return this.#renderer; }
147
+ get scene() { return this.#scene; }
148
+ request_probe(uv) { return this.#renderer.request_probe(uv); }
149
+ get time() { return this.#time; }
150
+ set time(seconds) {
151
+ assert.ok(Number.isFinite(seconds) && seconds >= 0, 'water time must be finite and non-negative');
152
+ this.#time = seconds;
153
+ }
154
+
155
+ /**
156
+ * What became of each surface query request, by handle: a `GPUReadbackStatus`. `Pending` from
157
+ * {@link request_local} / {@link request_world} until the apply point ({@link update}) that
158
+ * answers it, `Delivered` (the samples are unpacked in `target`) or `Dropped` (the target is as
159
+ * it was) from there until the next apply point, then `None`, the handle recycled.
160
+ *
161
+ * Read it fresh each time rather than keeping a reference: the array is replaced when the
162
+ * handle space grows.
163
+ *
164
+ * @returns {Uint8Array}
165
+ */
166
+ get status() {
167
+ return this.#status;
168
+ }
169
+
170
+ /**
171
+ * The surface read API's device half, or null before the first frame with a request on a
172
+ * device. For inspection.
173
+ *
174
+ * @returns {GPUWaterQueries|null}
175
+ */
176
+ get queries() {
177
+ return this.#queries;
178
+ }
179
+
180
+ constructor(graphics, scene) {
181
+ super(); assert.defined(graphics, 'graphics'); assert.defined(scene, 'scene');
182
+ this.#graphics = graphics; this.#scene = scene;
183
+ this.#renderer.scene = scene;
184
+ this.#waves.onInputsConsumed.add(context => {
185
+ if (context !== this.#context) { return; }
186
+ for (const entry of this.#impulses) {
187
+ entry.staged.impulse_count = 0;
188
+ if (entry.queue.length === 0) { this.#impulses.delete(entry); }
189
+ }
190
+ });
191
+ this.#renderer.waves = this.#waves;
192
+ }
193
+
194
+ async startup(entityManager) {
195
+ this.entityManager = entityManager;
196
+ assert.ok(this.#scene.dataset == null || this.#scene.dataset === entityManager.dataset, 'scene belongs to another dataset');
197
+ this.#scene.dataset = entityManager.dataset;
198
+ this.#graphics.add_transparency_channel(this.#renderer);
199
+ this.#extensions = [this.#graphics.add_extension(new WaterExtension(this)),
200
+ this.#graphics.add_extension(new WaterSurfaceExtension(this))];
201
+ }
202
+
203
+ async shutdown() {
204
+ for (const entry of Array.from(this.#entries.values())) { this.unlink(entry.water, entry.geometry, entry.transform, entry.entity); }
205
+ this.#drop_queries();
206
+ this.#graphics.remove_transparency_channel(this.#renderer);
207
+ for (const extension of this.#extensions) { this.#graphics.remove_extension(extension); }
208
+ this.#extensions.length = 0;
209
+ this.#renderer.dispose(); this.#waves.dispose(); this.#context = null;
210
+ }
211
+
212
+ link(water, geometry, transform, entity) {
213
+ // queue: splashes not yet staged, four numbers each (local x, z, radius, kick); staged: the batch on the
214
+ // GPU row until a submitted frame spends it
215
+ const entry = { water, geometry, transform, entity, context: null, row: -1, wave: -1, waves: null, queue: [],
216
+ staged: { impulses: Array.from({ length: WATER_WAVE_IMPULSE_LIMIT }, () => new Float64Array(4)), impulse_count: 0 } };
217
+ const dataset = this.entityManager.dataset;
218
+ entry.changed = () => this.#dirty.add(entry);
219
+ entry.world = () => entry.context?.rows.mark_world(entity);
220
+ entry.local = () => entry.context?.rows.mark_local(entity);
221
+ entry.components = ({ klass }) => {
222
+ if (klass === WaterWaves) { this.#watch_waves(entry); entry.changed(); }
223
+ if (klass === TransformAttachment) { entry.local(); }
224
+ };
225
+ water.onChanged.add(entry.changed);
226
+ dataset.addEntityEventListener(entity, TRANSFORM64_EVENT_CHANGE, entry.world);
227
+ dataset.addEntityEventListener(entity, TRANSFORM_ATTACHMENT_EVENT_CHANGE, entry.local);
228
+ dataset.addEntityEventListener(entity, EventType.ComponentAdded, entry.components);
229
+ dataset.addEntityEventListener(entity, EventType.ComponentRemoved, entry.components);
230
+ this.#entries.set(entity, entry);
231
+ this.#watch_waves(entry);
232
+ this.#dirty.add(entry);
233
+ this.#renderer.body_count++;
234
+ }
235
+
236
+ #watch_waves(entry) {
237
+ if (entry.waves != null) { for (const field of WAVE_FIELDS) { entry.waves[field].onChanged.remove(entry.changed); } }
238
+ entry.waves = this.entityManager.dataset.getComponent(entry.entity, WaterWaves);
239
+ if (entry.waves != null) { for (const field of WAVE_FIELDS) { entry.waves[field].onChanged.add(entry.changed); } }
240
+ }
241
+
242
+ unlink(water, geometry, transform, entity) {
243
+ const entry = this.#entries.get(entity);
244
+ if (entry === undefined) { return; }
245
+ const dataset = this.entityManager.dataset;
246
+ water.onChanged.remove(entry.changed);
247
+ if (entry.waves != null) { for (const field of WAVE_FIELDS) { entry.waves[field].onChanged.remove(entry.changed); } }
248
+ dataset.removeEntityEventListener(entity, TRANSFORM64_EVENT_CHANGE, entry.world);
249
+ dataset.removeEntityEventListener(entity, TRANSFORM_ATTACHMENT_EVENT_CHANGE, entry.local);
250
+ dataset.removeEntityEventListener(entity, EventType.ComponentAdded, entry.components);
251
+ dataset.removeEntityEventListener(entity, EventType.ComponentRemoved, entry.components);
252
+ if (entry.context !== null && entry.context === this.#graphics.scene_context(this.#scene)) {
253
+ entry.context.scene_database.get('water_bodies').remove(entry.row);
254
+ entry.context.rows.release(entity);
255
+ }
256
+ this.#entries.delete(entity); this.#dirty.delete(entry); this.#impulses.delete(entry);
257
+ this.#renderer.body_count--;
258
+ if (entry.context !== null) { this.#renderer.meshlet_capacity -= entry.geometry.meshlets.count; }
259
+ }
260
+
261
+ update(dt) {
262
+ this.#recycle_requests();
263
+ this.#time += dt; this.#renderer.time = this.#time; this.#waves.advance(dt);
264
+ this.#waves.windy = this.#wind_can_blow();
265
+ const context = this.#graphics.scene_context(this.#scene);
266
+ if (context === null) {
267
+ if (this.#context !== null || this.#queries !== null) {
268
+ this.#drop_queries();
269
+ this.#context = null;
270
+ }
271
+ this.#answer_requests(null);
272
+ return;
273
+ }
274
+ if (context !== this.#context) {
275
+ this.#drop_queries();
276
+ this.#context = context;
277
+ // Device replacement is the only whole-scene rebuild; ordinary frames drain edits.
278
+ for (const entry of this.#entries.values()) {
279
+ entry.context = null; entry.row = -1; entry.wave = -1;
280
+ // a batch the old context never spent goes back to the front of the queue
281
+ const staged = [];
282
+ for (let i = 0; i < entry.staged.impulse_count; i++) { staged.push(...entry.staged.impulses[i]); }
283
+ entry.queue.unshift(...staged); entry.staged.impulse_count = 0;
284
+ this.#dirty.add(entry);
285
+ }
286
+ this.#renderer.meshlet_capacity = 0;
287
+ }
288
+ for (const entry of this.#dirty) { this.#write(entry, context); }
289
+ this.#dirty.clear();
290
+ for (const entry of this.#impulses) { this.#stage(entry, context); }
291
+ this.#answer_requests(context);
292
+ }
293
+
294
+ /**
295
+ * Whether the scene's wind field can hold wind: an enabled {@link GPUFluidComponent} on any entity, or a far
296
+ * field that is not still. The lowest holder of the component answers without a walk; only when it is disabled
297
+ * are the others visited.
298
+ * @returns {boolean}
299
+ */
300
+ #wind_can_blow() {
301
+ const em = this.entityManager, dataset = em.dataset;
302
+ if (dataset !== null) {
303
+ const lowest = dataset.getAnyComponentInstance(GPUFluidComponent);
304
+ if (lowest !== null) {
305
+ if (lowest.enabled) { return true; }
306
+ this.#enabled_found = false;
307
+ dataset.traverseComponents(GPUFluidComponent, this.#visit_domain);
308
+ if (this.#enabled_found) { return true; }
309
+ }
310
+ }
311
+ // registered later than this system, or never
312
+ const fluid = this.#fluid ??= em.getSystem(GPUFluidSystem);
313
+ if (fluid === null) { return false; }
314
+ const far = this.#far;
315
+ fluid.far_field(far);
316
+ return far[0] !== 0 || far[1] !== 0 || far[2] !== 0;
317
+ }
318
+
319
+ /** Top up a body's batch on its surface row from its queue, up to the row's limit. */
320
+ #stage(entry, context) {
321
+ const staged = entry.staged, queue = entry.queue;
322
+ if (entry.wave === -1 || queue.length === 0 || staged.impulse_count === WATER_WAVE_IMPULSE_LIMIT) { return; }
323
+ const taken = Math.min(queue.length / 4, WATER_WAVE_IMPULSE_LIMIT - staged.impulse_count);
324
+ for (let i = 0; i < taken; i++) {
325
+ const impulse = staged.impulses[staged.impulse_count++];
326
+ for (let c = 0; c < 4; c++) { impulse[c] = queue[4 * i + c]; }
327
+ }
328
+ queue.copyWithin(0, 4 * taken);
329
+ queue.length -= 4 * taken;
330
+ this.#waves.splashed = true;
331
+ for (let i = staged.impulse_count; i < WATER_WAVE_IMPULSE_LIMIT; i++) { staged.impulses[i].fill(0); }
332
+ context.scene_database.get('water_waves').set_fields(entry.wave, staged, IMPULSE_WRITER);
333
+ }
334
+
335
+ #write(entry, context) {
336
+ const bodies = context.scene_database.get('water_bodies');
337
+ const waves = context.scene_database.get('water_waves');
338
+ if (entry.context === null) {
339
+ context.rows.acquire(entry.entity);
340
+ context.geometries.add(entry.geometry);
341
+ entry.context = context;
342
+ this.#renderer.meshlet_capacity += entry.geometry.meshlets.count;
343
+ }
344
+ const geometry = context.geometries.getAddress(entry.geometry);
345
+ if (entry.wave === -1) {
346
+ // the GPU seeds the wind from its first sample, and derives the weights for it
347
+ entry.wave = waves.add({ upwind_fetch: 0, shore_reflection: WATER_WAVES_SHORE_REFLECTION_DEFAULT,
348
+ impulses: Array.from({ length: WATER_WAVE_IMPULSE_LIMIT }, () => [0, 0, 0, 0]), impulse_count: 0,
349
+ node: entry.entity + 1, geometry, simulate_hidden: 0, wind: [0, 0, 0], wind_seeded: 0, derived_for: [-1, -1, 0, 0],
350
+ weights: new Array(WATER_SPECTRUM_BAND_COUNT).fill(0), slope_variance: 0,
351
+ wind_from: [0, 0], wind_to: [0, 0], wind_blend: 1, displacement_bounds: [0, 0, 0, 0], particle_levels: 0 });
352
+ }
353
+ const waves_component = entry.waves;
354
+ const reflection = Math.min(1, Math.max(0, waves_component?.shoreReflection.getValue() ?? WATER_WAVES_SHORE_REFLECTION_DEFAULT));
355
+ waves.set_fields(entry.wave, { upwind_fetch: Math.max(0, waves_component?.upwindFetch.getValue() ?? 0), shore_reflection: reflection, node: entry.entity + 1, geometry,
356
+ simulate_hidden: entry.water.simulateWhenHidden.getValue() ? 1 : 0 }, AUTHORED_WRITER);
357
+ const water = entry.water;
358
+ const record = { geometry, node: entry.entity + 1,
359
+ dirty: 1,
360
+ wave: entry.wave, ior: Math.max(1, water.ior.getValue()),
361
+ roughness: Math.min(1, Math.max(0, water.roughness.getValue())), medium: water.medium.to_gpu_record(),
362
+ backscattering: [0, 0, 0, 0], extinction: [0, 0, 0, 0], diffuse_transmittance: 0 };
363
+ if (entry.row === -1) { entry.row = bodies.add(record); } else { bodies.set(entry.row, record); }
364
+ }
365
+
366
+ /**
367
+ * A splash at a world position: rings of wave particles of the given world radius (half their wavelength)
368
+ * whose height is the given vertical velocity kick, m/s, over the time a crest takes to cross them. The
369
+ * position is interpreted in the CPU-owned pose; queued on the CPU and spent on the GPU, sixteen a body a frame.
370
+ */
371
+ splash(entity, position, radius, strength) {
372
+ const entry = this.#entries.get(entity); assert.defined(entry, 'water entity');
373
+ assert.greaterThan(radius, 0, 'radius'); assert.isFinite(radius, 'radius'); assert.isFinite(strength, 'strength');
374
+ for (let i = 0; i < 3; i++) { assert.isFinite(position[i], `position[${i}]`); }
375
+ const inverse = SPLASH_INVERSE;
376
+ const inverted = m4_invert(inverse, entry.transform);
377
+ assert.ok(inverted, 'water transform is singular');
378
+ const x = inverse[0] * position[0] + inverse[4] * position[1] + inverse[8] * position[2] + inverse[12];
379
+ const z = inverse[2] * position[0] + inverse[6] * position[1] + inverse[10] * position[2] + inverse[14];
380
+ entry.queue.push(x, z, radius, strength); this.#impulses.add(entry);
381
+ }
382
+
383
+ /**
384
+ * Ask for the water surface height and world-space unit normal at `count` points in the water
385
+ * body's local `(x, z)` plane: `local_xz` holds `[x0, z0, x1, z1, ...]`, and each sample lands
386
+ * in `target` from `target_offset` as {@link WATER_QUERY_SAMPLE_FLOATS} (4) floats,
387
+ * `[world_height, nx, ny, nz]`, unpacked on the CPU when the request settles `Delivered` at a
388
+ * later apply point ({@link update}); see {@link status}.
389
+ *
390
+ * Points outside the body's authored outline or inside island holes are clamped continuously to
391
+ * the nearest point on the mesh's triangles so no `NaN`s are ever produced.
392
+ *
393
+ * @param {number} entity water entity
394
+ * @param {ArrayLike<number>} local_xz interleaved `[x0, z0, x1, z1, ...]` in local body coordinates
395
+ * @param {number} count number of `(x, z)` points, at least 1
396
+ * @param {number} footprint spatial filter footprint in metres, at least 0 (0 resolves all wavelengths)
397
+ * @param {Float32Array} target memory the caller owns and keeps until the request settles
398
+ * @param {number} target_offset in floats
399
+ * @returns {number} a handle into {@link status}
400
+ */
401
+ request_local(entity, local_xz, count, footprint, target, target_offset) {
402
+ const entry = this.#entries.get(entity);
403
+ assert.defined(entry, 'water entity');
404
+ assert.isArrayLike(local_xz, 'local_xz');
405
+ assert.isPositiveInteger(count, 'count');
406
+ assert.greaterThanOrEqual(local_xz.length, 2 * count, 'local_xz');
407
+ for (let i = 0; i < 2 * count; i++) {
408
+ assert.isFinite(local_xz[i], `local_xz[${i}]`);
409
+ }
410
+ assert.isFinite(footprint, 'footprint');
411
+ assert.greaterThanOrEqual(footprint, 0, 'footprint');
412
+ assert.ok(target instanceof Float32Array, 'target must be a Float32Array');
413
+ assert.isNonNegativeInteger(target_offset, 'target_offset');
414
+ assert.lessThanOrEqual(target_offset + WATER_QUERY_SAMPLE_FLOATS * count, target.length, 'the query samples reach past the target');
415
+
416
+ const h = this.#allocate_handle();
417
+ const point_start = this.#request_point_count;
418
+ const next_point_count = point_start + count;
419
+
420
+ if (2 * next_point_count > this.#request_points.length) {
421
+ this.#request_points = fit(this.#request_points, 2 * next_point_count);
422
+ }
423
+
424
+ const points = this.#request_points;
425
+
426
+ for (let i = 0; i < 2 * count; i++) {
427
+ points[2 * point_start + i] = local_xz[i];
428
+ }
429
+
430
+ this.#request_point_count = next_point_count;
431
+ this.#request_entities[h] = entity;
432
+ this.#request_point_offsets[h] = point_start;
433
+ this.#request_counts[h] = count;
434
+ this.#request_footprints[h] = footprint;
435
+ this.#request_offsets[h] = target_offset;
436
+ this.#request_targets[h] = target;
437
+ this.#status[h] = GPUReadbackStatus.Pending;
438
+
439
+ if (this.#asked_count === this.#asked.length) {
440
+ this.#asked = fit(this.#asked, this.#asked_count + 1);
441
+ }
442
+
443
+ this.#asked[this.#asked_count++] = h;
444
+
445
+ return h;
446
+ }
447
+
448
+ /**
449
+ * Ask for the water surface height and world-space unit normal at `count` points in world
450
+ * space: `world_xyz` holds `[x0, y0, z0, x1, y1, z1, ...]`. Internally transforms each world
451
+ * point into the water body's local `(x, z)` plane via the inverse of its CPU-owned
452
+ * `Transform64` and delegates to {@link request_local}.
453
+ *
454
+ * @param {number} entity water entity
455
+ * @param {ArrayLike<number>} world_xyz interleaved `[x0, y0, z0, ...]` in world metres
456
+ * @param {number} count number of `(x, y, z)` points, at least 1
457
+ * @param {number} footprint spatial filter footprint in metres, at least 0 (0 resolves all wavelengths)
458
+ * @param {Float32Array} target memory the caller owns and keeps until the request settles
459
+ * @param {number} target_offset in floats
460
+ * @returns {number} a handle into {@link status}
461
+ */
462
+ request_world(entity, world_xyz, count, footprint, target, target_offset) {
463
+ const entry = this.#entries.get(entity);
464
+ assert.defined(entry, 'water entity');
465
+ assert.isArrayLike(world_xyz, 'world_xyz');
466
+ assert.isPositiveInteger(count, 'count');
467
+ assert.greaterThanOrEqual(world_xyz.length, 3 * count, 'world_xyz');
468
+ for (let i = 0; i < 3 * count; i++) {
469
+ assert.isFinite(world_xyz[i], `world_xyz[${i}]`);
470
+ }
471
+
472
+ const inverse = SPLASH_INVERSE;
473
+ const inverted = m4_invert(inverse, entry.transform);
474
+ assert.ok(inverted, 'water transform is singular');
475
+
476
+ if (2 * count > this.#world_to_local_xz.length) {
477
+ this.#world_to_local_xz = fit(this.#world_to_local_xz, 2 * count);
478
+ }
479
+
480
+ const local_xz = this.#world_to_local_xz;
481
+
482
+ for (let i = 0; i < count; i++) {
483
+ const wx = world_xyz[3 * i];
484
+ const wy = world_xyz[3 * i + 1];
485
+ const wz = world_xyz[3 * i + 2];
486
+
487
+ local_xz[2 * i] = inverse[0] * wx + inverse[4] * wy + inverse[8] * wz + inverse[12];
488
+ local_xz[2 * i + 1] = inverse[2] * wx + inverse[6] * wy + inverse[10] * wz + inverse[14];
489
+ }
490
+
491
+ return this.request_local(entity, local_xz, count, footprint, target, target_offset);
492
+ }
493
+
494
+ /**
495
+ * @returns {number}
496
+ */
497
+ #allocate_handle() {
498
+ if (this.#free_count > 0) {
499
+ return this.#free_handles[--this.#free_count];
500
+ }
501
+
502
+ const h = this.#fresh++;
503
+
504
+ if (h === this.#status.length) {
505
+ const capacity = 2 * h;
506
+
507
+ this.#request_entities = fit(this.#request_entities, capacity);
508
+ this.#request_point_offsets = fit(this.#request_point_offsets, capacity);
509
+ this.#request_counts = fit(this.#request_counts, capacity);
510
+ this.#request_footprints = fit(this.#request_footprints, capacity);
511
+ this.#request_offsets = fit(this.#request_offsets, capacity);
512
+ this.#status = fit(this.#status, capacity);
513
+ this.#free_handles = fit(this.#free_handles, capacity);
514
+ this.#settled = fit(this.#settled, capacity);
515
+
516
+ for (let i = this.#request_targets.length; i < capacity; i++) {
517
+ this.#request_targets.push(null);
518
+ }
519
+ }
520
+
521
+ return h;
522
+ }
523
+
524
+ /**
525
+ * A request answered: its status set, readable until the next apply point.
526
+ *
527
+ * @param {number} h
528
+ * @param {number} status
529
+ */
530
+ #settle(h, status) {
531
+ this.#status[h] = status;
532
+ this.#request_targets[h] = null;
533
+ this.#settled[this.#settled_count++] = h;
534
+ }
535
+
536
+ /**
537
+ * The apply point's first half: the handles answered at the last one go back to the free list.
538
+ */
539
+ #recycle_requests() {
540
+ const settled = this.#settled;
541
+ const status = this.#status;
542
+ const free = this.#free_handles;
543
+
544
+ for (let i = 0; i < this.#settled_count; i++) {
545
+ const h = settled[i];
546
+
547
+ status[h] = GPUReadbackStatus.None;
548
+ free[this.#free_count++] = h;
549
+ }
550
+
551
+ this.#settled_count = 0;
552
+ }
553
+
554
+ /**
555
+ * The apply point's second half: what the device delivered or dropped, and, headless, every
556
+ * request asked since from the body's rest surface.
557
+ *
558
+ * @param {object|null} context
559
+ */
560
+ #answer_requests(context) {
561
+ const queries = this.#queries;
562
+
563
+ if (queries !== null) {
564
+ queries.apply();
565
+ this.#take_settled(queries);
566
+ }
567
+
568
+ if (context !== null || this.#asked_count === 0) {
569
+ return;
570
+ }
571
+
572
+ const asked = this.#asked;
573
+ const points = this.#request_points;
574
+
575
+ for (let i = 0; i < this.#asked_count; i++) {
576
+ const h = asked[i];
577
+ const entry = this.#entries.get(this.#request_entities[h]);
578
+
579
+ if (entry === undefined) {
580
+ this.#settle(h, GPUReadbackStatus.Dropped);
581
+ continue;
582
+ }
583
+
584
+ const m = entry.transform;
585
+ const inverse = SPLASH_INVERSE;
586
+ const inverted = m4_invert(inverse, m);
587
+ let nx = 0, ny = 1, nz = 0;
588
+ if (inverted) {
589
+ const rx = inverse[1], ry = inverse[5], rz = inverse[9];
590
+ const len = Math.hypot(rx, ry, rz);
591
+ if (len > 1e-20) {
592
+ nx = rx / len;
593
+ ny = ry / len;
594
+ nz = rz / len;
595
+ }
596
+ }
597
+
598
+ const box = entry.geometry.bounding_box;
599
+ const mid_y = 0.5 * (box[1] + box[4]);
600
+ const count = this.#request_counts[h];
601
+ const point_base = 2 * this.#request_point_offsets[h];
602
+ const target = this.#request_targets[h];
603
+ let at = this.#request_offsets[h];
604
+
605
+ for (let k = 0; k < count; k++, at += WATER_QUERY_SAMPLE_FLOATS) {
606
+ const lx = Math.min(Math.max(points[point_base + 2 * k], box[0]), box[3]);
607
+ const lz = Math.min(Math.max(points[point_base + 2 * k + 1], box[2]), box[5]);
608
+ const world_y = m[1] * lx + m[5] * mid_y + m[9] * lz + m[13];
609
+
610
+ target[at] = world_y;
611
+ target[at + 1] = nx;
612
+ target[at + 2] = ny;
613
+ target[at + 3] = nz;
614
+ }
615
+
616
+ this.#settle(h, GPUReadbackStatus.Delivered);
617
+ }
618
+
619
+ this.#request_point_count = 0;
620
+ this.#asked_count = 0;
621
+ }
622
+
623
+ /**
624
+ * @param {GPUWaterQueries} queries after an apply or a destroy
625
+ */
626
+ #take_settled(queries) {
627
+ for (let i = 0; i < queries.settled_count; i++) {
628
+ this.#settle(queries.settled_tag(i), queries.settled_status(i));
629
+ }
630
+ }
631
+
632
+ /**
633
+ * Let go of the read API's device half: whatever it had in flight is dropped, its status set now.
634
+ */
635
+ #drop_queries() {
636
+ const queries = this.#queries;
637
+
638
+ if (queries === null) {
639
+ return;
640
+ }
641
+
642
+ queries.destroy();
643
+
644
+ this.#take_settled(queries);
645
+
646
+ this.#queries = null;
647
+ }
648
+
649
+ /**
650
+ * Hand the frame's requests to the device, after the frame's surface simulation passes.
651
+ *
652
+ * @param {import("../graphics/render/frame_graph/FrameGraph.js").FrameGraph} graph
653
+ * @param {object} scene
654
+ * @param {GPUDevice} device
655
+ */
656
+ #record_requests(graph, scene, device) {
657
+ const count = this.#asked_count;
658
+
659
+ if (count === 0) {
660
+ return;
661
+ }
662
+
663
+ let queries = this.#queries;
664
+
665
+ if (queries === null) {
666
+ const graphics = this.#graphics.renderer?.graphics;
667
+ if (graphics?.compute_pipelines !== undefined) {
668
+ const enquiry = { device, render: [], compute: [] };
669
+ WaterSystem.enquirePipelines(enquiry);
670
+ for (let i = 0; i < enquiry.compute.length; i++) {
671
+ graphics.compute_pipelines.prepare(enquiry.compute[i]);
672
+ }
673
+ }
674
+ queries = this.#queries = new GPUWaterQueries({ device });
675
+ }
676
+
677
+ const asked = this.#asked;
678
+ const points = this.#request_points;
679
+
680
+ for (let i = 0; i < count; i++) {
681
+ const h = asked[i];
682
+ const entry = this.#entries.get(this.#request_entities[h]);
683
+
684
+ if (entry === undefined || entry.row === -1) {
685
+ this.#settle(h, GPUReadbackStatus.Dropped);
686
+ continue;
687
+ }
688
+
689
+ queries.request(
690
+ entry.row,
691
+ points,
692
+ 2 * this.#request_point_offsets[h],
693
+ this.#request_counts[h],
694
+ this.#request_footprints[h],
695
+ this.#request_targets[h],
696
+ this.#request_offsets[h],
697
+ h
698
+ );
699
+ }
700
+
701
+ this.#request_point_count = 0;
702
+ this.#asked_count = 0;
703
+
704
+ queries.record({ graph, scene, surface: this.#waves });
705
+ }
706
+
707
+ record_surface(frame) {
708
+ const view = frame.view, scene = view.scene;
709
+ if (scene.scene !== this.#scene) { return; }
710
+ const device = this.#graphics.renderer.graphics.device;
711
+ this.#waves.record({ graph: frame.graph, device, scene, time: this.#time,
712
+ view: { camera: view.camera.gpu_buffer, projection: view.camera.camera.projection_matrix, height: view.resolution[1] },
713
+ pixels: this.#renderer.settings.tessellation_pixels });
714
+ this.#record_requests(frame.graph, scene, device);
715
+ }
716
+ record(frame) {
717
+ const inspection = this.#renderer.inspect(frame.graph, frame.view, frame.resolution);
718
+ if (inspection !== undefined) { frame.get(SceneColor).color = inspection; }
719
+ }
720
+ }
721
+
722
+ export class WaterExtension extends RenderExtension {
723
+ name = 'water'; phase = FramePhase.BeforePresent;
724
+ constructor(system) { super(); this.system = system; }
725
+ record(frame) { this.system.record(frame); }
726
+ }
727
+ export class WaterSurfaceExtension extends RenderExtension {
728
+ name = 'water surface'; phase = FramePhase.FrameStart;
729
+ constructor(system) { super(); this.system = system; }
730
+ record(frame) { this.system.record_surface(frame); }
731
+ }