@woosh/meep-engine 3.60.4 → 3.60.5

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 (379) hide show
  1. package/CHANGELOG.md +2602 -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.map +1 -1
  63. package/src/shade/renderer/Renderer.js +7 -5
  64. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +40 -80
  65. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
  66. package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +197 -147
  67. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  68. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  69. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  70. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  71. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  72. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +2 -2
  73. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +2 -2
  74. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +2 -1
  75. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
  76. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +18 -1
  77. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts +31 -0
  78. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts.map +1 -0
  79. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.js +343 -0
  80. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts +17 -0
  81. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts.map +1 -0
  82. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.js +29 -0
  83. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -1
  84. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +7 -13
  85. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
  86. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +1 -8
  87. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +5 -5
  88. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
  89. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +10 -10
  90. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +1 -4
  91. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
  92. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +1 -10
  93. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts +5 -15
  94. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts.map +1 -1
  95. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.js +6 -34
  96. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
  97. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +1 -7
  98. package/src/shade/renderer/hiz/HZBFootprint.d.ts +14 -0
  99. package/src/shade/renderer/hiz/HZBFootprint.d.ts.map +1 -0
  100. package/src/shade/renderer/hiz/HZBFootprint.js +16 -0
  101. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts +10 -0
  102. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts.map +1 -0
  103. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.js +11 -0
  104. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts +10 -0
  105. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts.map +1 -0
  106. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.js +11 -0
  107. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +0 -7
  108. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  109. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +405 -350
  110. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts +12 -0
  111. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts.map +1 -0
  112. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.js +27 -0
  113. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts +12 -0
  114. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts.map +1 -0
  115. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.js +27 -0
  116. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts +14 -0
  117. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts.map +1 -0
  118. package/src/shade/renderer/hiz/hzb_footprint_texels.js +34 -0
  119. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +0 -5
  120. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
  121. package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +37 -57
  122. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts +11 -0
  123. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts.map +1 -0
  124. package/src/shade/renderer/hiz/{shader_hzb_reduce_mip0_clip.js → shader_hzb_reduce_mip0.js} +120 -111
  125. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  126. package/src/shade/renderer/material/GPUMaterialManager.d.ts +7 -0
  127. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  128. package/src/shade/renderer/material/GPUMaterialManager.js +11 -0
  129. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts +12 -12
  130. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
  131. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +17 -118
  132. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  133. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +7 -2
  134. package/src/shade/renderer/water/GPUWaterRenderer.d.ts +0 -6
  135. package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
  136. package/src/shade/renderer/water/GPUWaterRenderer.js +12 -17
  137. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts +35 -0
  138. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts.map +1 -0
  139. package/src/shade/renderer/water/medium/graph_water_meshlets.js +89 -0
  140. package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
  141. package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
  142. package/src/shade/renderer/water/shader_water.js +22 -13
  143. package/src/shade/renderer/water/shader_water_avboit.d.ts.map +1 -1
  144. package/src/shade/renderer/water/shader_water_avboit.js +7 -7
  145. package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts +1 -1
  146. package/src/shade/renderer/water/surface/graph_water_tessellation.js +1 -1
  147. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +9 -1
  148. package/editor/REWORK_PLAN_2026_08.md +0 -692
  149. package/src/CODEC_LAYOUT_VERIFICATION.md +0 -419
  150. package/src/CORRECTNESS_HUNT_2026_07_24.md +0 -142
  151. package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +0 -738
  152. package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +0 -79
  153. package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +0 -93
  154. package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +0 -80
  155. package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +0 -164
  156. package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +0 -69
  157. package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +0 -110
  158. package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +0 -129
  159. package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +0 -72
  160. package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +0 -74
  161. package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +0 -169
  162. package/src/DUPLICATION_AUDIT_2026_08_07.md +0 -254
  163. package/src/DUPLICATION_AUDIT_REMEDIATION.md +0 -250
  164. package/src/DUPLICATION_REVIEW_2026_10_24.md +0 -1072
  165. package/src/REVIEW_2026_08_06/adv-core.md +0 -511
  166. package/src/REVIEW_2026_08_06/adv-gfx.md +0 -480
  167. package/src/REVIEW_2026_08_06/adv-view.md +0 -446
  168. package/src/REVIEW_2026_08_06/api-core.md +0 -296
  169. package/src/REVIEW_2026_08_06/api-engine.md +0 -195
  170. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +0 -507
  171. package/src/REVIEW_2026_08_06/core-binary.md +0 -348
  172. package/src/REVIEW_2026_08_06/core-collection.md +0 -376
  173. package/src/REVIEW_2026_08_06/core-math.md +0 -221
  174. package/src/REVIEW_2026_08_06/core-process.md +0 -266
  175. package/src/REVIEW_2026_08_06/core-science.md +0 -223
  176. package/src/REVIEW_2026_08_06/engine-ai-nav.md +0 -279
  177. package/src/REVIEW_2026_08_06/engine-animation.md +0 -167
  178. package/src/REVIEW_2026_08_06/engine-asset.md +0 -166
  179. package/src/REVIEW_2026_08_06/engine-ecs.md +0 -920
  180. package/src/REVIEW_2026_08_06/engine-input.md +0 -197
  181. package/src/REVIEW_2026_08_06/engine-misc.md +0 -248
  182. package/src/REVIEW_2026_08_06/engine-terrain.md +0 -52
  183. package/src/REVIEW_2026_08_06/generation.md +0 -176
  184. package/src/REVIEW_2026_08_06/geom-2d.md +0 -434
  185. package/src/REVIEW_2026_08_06/geom-3d.md +0 -117
  186. package/src/REVIEW_2026_08_06/gfx-gi.md +0 -143
  187. package/src/REVIEW_2026_08_06/gfx-render.md +0 -740
  188. package/src/REVIEW_2026_08_06/gfx-texture.md +0 -690
  189. package/src/REVIEW_2026_08_06/prior-criticals.md +0 -49
  190. package/src/REVIEW_2026_08_06/prior-highs.md +0 -138
  191. package/src/REVIEW_2026_08_06/test-gaps.md +0 -364
  192. package/src/REVIEW_2026_08_06/view.md +0 -667
  193. package/src/REVIEW_2026_08_06.md +0 -610
  194. package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +0 -542
  195. package/src/core/bvh2/bvh3/NOTES.md +0 -2
  196. package/src/core/bvh8/NOTES.md +0 -63
  197. package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +0 -243
  198. package/src/core/color/COLOR_FIX_PLAN_2026_08_28.md +0 -417
  199. package/src/core/color/COLOR_REVIEW_2026_08_28.md +0 -779
  200. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +0 -433
  201. package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +0 -486
  202. package/src/core/geom/3d/atlas/BFF_PLAN.md +0 -174
  203. package/src/core/geom/3d/atlas/REVIEW_02.md +0 -392
  204. package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +0 -146
  205. package/src/core/geom/3d/gjk/NOTES.md +0 -8
  206. package/src/core/geom/3d/topology/TOPO_MESH_RETIREMENT_PLAN.md +0 -135
  207. package/src/core/geom/GEOMETRY_REVIEW_002_API_ISSUES.md +0 -381
  208. package/src/core/geom/GEOMETRY_REVIEW_002_NAMING.md +0 -684
  209. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +0 -554
  210. package/src/core/model/reactive/evaluation/PREDICATE_APPLICABILITY_PROPOSAL.md +0 -446
  211. package/src/engine/REVIEW_2026_07_03.md +0 -211
  212. package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +0 -815
  213. package/src/engine/control/first-person/DESIGN.md +0 -658
  214. package/src/engine/control/first-person/DESIGN_COLLISION.md +0 -382
  215. package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +0 -563
  216. package/src/engine/control/first-person/TODO.md +0 -160
  217. package/src/engine/ecs/ASYNC_SYSTEMS_PROPOSAL.md +0 -531
  218. package/src/engine/ecs/HIERARCHY_REVIEW.md +0 -717
  219. package/src/engine/ecs/async/WORKER_SYSTEM_PLAN.md +0 -262
  220. package/src/engine/ecs/ik/PLAN.md +0 -179
  221. package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +0 -272
  222. package/src/engine/ecs/storage/binary/object-v2/DESIGN.md +0 -199
  223. package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +0 -521
  224. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +0 -463
  225. package/src/engine/graphics/ecs/camera/pp/NOTES.md +0 -48
  226. package/src/engine/graphics/render/frame_graph/CORRECTNESS_FIXES_2026_09_04.md +0 -659
  227. package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +0 -235
  228. package/src/engine/graphics/sh3/path_tracer/REVIEW_001_ACTION_PLAN.md +0 -490
  229. package/src/engine/graphics/texture/sampler/compare/FLIP_IMPLEMENTATION_PLAN.md +0 -466
  230. package/src/engine/input/GAMEPAD_API_PROPOSAL.md +0 -717
  231. package/src/engine/input/ecs/ism/ISM_DESIGN.md +0 -1523
  232. package/src/engine/input/ecs/ism/ISM_IMPLEMENTATION_PLAN.md +0 -627
  233. package/src/engine/navigation/mesh/OVERLAPPING_SURFACES_2026_08_25.md +0 -661
  234. package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +0 -229
  235. package/src/engine/network/CONGESTION_CONTROL.md +0 -87
  236. package/src/engine/network/MESSAGE_TRANSPORT.md +0 -364
  237. package/src/engine/network/SMOOTHNESS_DESIGN.md +0 -400
  238. package/src/engine/physics/cloth/MEASUREMENTS.md +0 -5304
  239. package/src/engine/physics/cloth/PLAN.md +0 -3056
  240. package/src/engine/physics/fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md +0 -1382
  241. package/src/engine/physics/fluid/REVIEW_02_PLAN.md +0 -243
  242. package/src/engine/physics/fluid/solver/NOTES.md +0 -46
  243. package/src/engine/sound/SOUND_EMITTER_RETIREMENT_PLAN.md +0 -536
  244. package/src/engine/sound/simulation/IMPLEMENTATION_PLAN.md +0 -432
  245. package/src/engine/sound/simulation/PROBE_PLACEMENT.md +0 -250
  246. package/src/engine/sound/simulation/PROBE_TRANSFER_RESEARCH.md +0 -322
  247. package/src/engine/sound/simulation/REFLECTION_IR_PLAN.md +0 -261
  248. package/src/engine/sound/simulation/VISIBILITY_GRAPH_PLAN.md +0 -121
  249. package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +0 -999
  250. package/src/format/image/avif/DECISIONS.md +0 -545
  251. package/src/format/image/jpeg/NOTES.md +0 -3
  252. package/src/format/scene/gltf/MESHOPT_COMPRESSION_PLAN.md +0 -814
  253. package/src/generation/COORDINATES.md +0 -54
  254. package/src/generation/REVIEW_01_ACTION_PLAN.md +0 -628
  255. package/src/generation/foliage/ANNUAL_SHOOTS_BRIEF_2026_10_25.md +0 -220
  256. package/src/generation/foliage/CROWN_FILLING_BRIEF_2026_10_26.md +0 -172
  257. package/src/generation/foliage/CROWN_FULLNESS_BRIEF_2026_10_26.md +0 -86
  258. package/src/generation/foliage/FOLIAGE_DESIGN.md +0 -547
  259. package/src/generation/foliage/FOLIAGE_PLAN.md +0 -198
  260. package/src/generation/foliage/LEAF_ARCHITECTURE_BRIEF_2026_10_26.md +0 -158
  261. package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +0 -521
  262. package/src/generation/foliage/REAL_TIME_TREES_BRIEF_2026_10_27.md +0 -86
  263. package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +0 -347
  264. package/src/generation/foliage/SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md +0 -248
  265. package/src/generation/foliage/SPEEDTREE_COMPARISON_2026_10_24.md +0 -694
  266. package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +0 -552
  267. package/src/shade/RENDERER_CONTRACT.md +0 -697
  268. package/src/shade/SHADER_F16_PROPOSAL_2026_10_24.md +0 -263
  269. package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +0 -569
  270. package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +0 -755
  271. package/src/shade/device/mock/EXTENSION_PROPOSAL_2026_08_22.md +0 -458
  272. package/src/shade/device/mock/IMPLEMENTATION_PLAN.md +0 -255
  273. package/src/shade/device/mock/REVIEW_2026_08_22.md +0 -527
  274. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +0 -894
  275. package/src/shade/device/timing/profile/SGPT_FORMAT.md +0 -345
  276. package/src/shade/renderer/NOTES.md +0 -78
  277. package/src/shade/renderer/PLAN.md +0 -8
  278. package/src/shade/renderer/animation/ADDITIVE_BLENDING_PLAN.md +0 -145
  279. package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +0 -357
  280. package/src/shade/renderer/atmosphere/NOTES.md +0 -6
  281. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts +0 -76
  282. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts.map +0 -1
  283. package/src/shade/renderer/camera/orbital/MomentumAxis.js +0 -164
  284. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts +0 -45
  285. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts.map +0 -1
  286. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.js +0 -66
  287. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts +0 -98
  288. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts.map +0 -1
  289. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.js +0 -184
  290. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +0 -175
  291. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +0 -1
  292. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +0 -411
  293. package/src/shade/renderer/camera/orbital/README.md +0 -81
  294. package/src/shade/renderer/camera/orbital/index.d.ts +0 -6
  295. package/src/shade/renderer/camera/orbital/index.d.ts.map +0 -1
  296. package/src/shade/renderer/camera/orbital/index.js +0 -9
  297. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +0 -964
  298. package/src/shade/renderer/fluid/GPU_FLUID_OCCUPANCY_AND_SOTA_REVIEW_2026_10_26.md +0 -361
  299. package/src/shade/renderer/geometry/bvh/BLAS_REFIT_BATCH_PLAN.md +0 -619
  300. package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +0 -674
  301. package/src/shade/renderer/geometry/sdf/NOTES.md +0 -6
  302. package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +0 -540
  303. package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +0 -305
  304. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +0 -913
  305. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +0 -787
  306. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +0 -481
  307. package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +0 -301
  308. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +0 -448
  309. package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +0 -385
  310. package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +0 -656
  311. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +0 -308
  312. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +0 -657
  313. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +0 -551
  314. package/src/shade/renderer/geometry/virtual/runtime/SHADOW_PLAN_2026_09_28.md +0 -529
  315. package/src/shade/renderer/global_illumination/lpv/NOTES.md +0 -3
  316. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +0 -4306
  317. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md +0 -410
  318. package/src/shade/renderer/global_illumination/sharc/NOTES.md +0 -8
  319. package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +0 -1369
  320. package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +0 -202
  321. package/src/shade/renderer/gpu_primitive/indirect/NOTES.md +0 -94
  322. package/src/shade/renderer/hiz/NOTES.md +0 -3
  323. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +0 -8
  324. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +0 -1
  325. package/src/shade/renderer/light/cluster/NOTES.md +0 -11
  326. package/src/shade/renderer/light/cluster/PROPOSAL_ALGORITHM_2026_10_16.md +0 -524
  327. package/src/shade/renderer/light/cluster/PROPOSAL_HIERARCHICAL_2026_10_16.md +0 -1101
  328. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts +0 -24
  329. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts.map +0 -1
  330. package/src/shade/renderer/material/resident/shader_resident_texture_means.js +0 -120
  331. package/src/shade/renderer/particles/DESIGN.md +0 -1011
  332. package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +0 -280
  333. package/src/shade/renderer/particles/ORIENTATION_PLAN_2026_10_15.md +0 -453
  334. package/src/shade/renderer/path_tracer/PATH_TRACED_MEDIA_PLAN_2026_10_02.md +0 -520
  335. package/src/shade/renderer/postprocess/bloom/NOTES.md +0 -6
  336. package/src/shade/renderer/postprocess/fsr/NOTES.md +0 -4
  337. package/src/shade/renderer/postprocess/gtao/NOTES.md +0 -6
  338. package/src/shade/renderer/postprocess/nss/NOTES.md +0 -332
  339. package/src/shade/renderer/postprocess/perspective/NOTES.md +0 -3
  340. package/src/shade/renderer/postprocess/sscs/NOTES.md +0 -5
  341. package/src/shade/renderer/postprocess/ssr/NOTES.md +0 -14
  342. package/src/shade/renderer/postprocess/ssr/REPROJECTION_PROPOSAL.md +0 -286
  343. package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +0 -39
  344. package/src/shade/renderer/postprocess/taa/NOTES.md +0 -11
  345. package/src/shade/renderer/postprocess/upscale-dither/NOTES.md +0 -78
  346. package/src/shade/renderer/postprocess/vrs/NOTES.md +0 -4
  347. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +0 -1218
  348. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +0 -600
  349. package/src/shade/renderer/restir/reservoir/NOTES.md +0 -6
  350. package/src/shade/renderer/shader/chunk/atmosphere/NOTES.md +0 -6
  351. package/src/shade/renderer/shader/chunk/brdf/FRESNEL_BRIEF_2026_10_16.md +0 -391
  352. package/src/shade/renderer/shader/chunk/color/tonemap/ACES_TONE_MAPPING_BRIEF_2026_10_17.md +0 -153
  353. package/src/shade/renderer/shader/chunk/color/tonemap/HDR_TONE_MAPPING_FORMS_BRIEF_2026_10_16.md +0 -322
  354. package/src/shade/renderer/shader/chunk/color/tonemap/SDR_TONE_MAPPING_BRIEF_2026_10_16.md +0 -247
  355. package/src/shade/renderer/shader/chunk/fsr/NOTES.md +0 -3
  356. package/src/shade/renderer/shader/chunk/fsr/spd/NOTES.md +0 -4
  357. package/src/shade/renderer/shader/chunk/geometry/ray/NOTES.md +0 -4
  358. package/src/shade/renderer/shader/chunk/material/MULTISCATTER.md +0 -159
  359. package/src/shade/renderer/shader/chunk/mboit/NOTES.md +0 -38
  360. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/NOTES.md +0 -3
  361. package/src/shade/renderer/shader/chunk/spherical_harmonics/sampling/NOTES.md +0 -8
  362. package/src/shade/renderer/shader/compiler/SHADER_SWITCH_PROPOSAL_2026_10_16.md +0 -927
  363. package/src/shade/renderer/shadow/contact/FINDINGS.md +0 -413
  364. package/src/shade/renderer/shadow/map/csm/NOTES.md +0 -5
  365. package/src/shade/renderer/shadow/ray/NOTES.md +0 -31
  366. package/src/shade/renderer/shadow/sdf/NOTES.md +0 -9
  367. package/src/shade/renderer/shadow/sdf/brixelizer/NOTES.md +0 -5
  368. package/src/shade/renderer/shadow/sdf/cascade/NOTES.md +0 -9
  369. package/src/shade/renderer/shadow/vsm/NOTES.md +0 -12
  370. package/src/shade/renderer/shadow/vsm/PLAN.md +0 -442
  371. package/src/shade/renderer/shadow/vsm/REVIEW_LEDGER.md +0 -43
  372. package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +0 -2444
  373. package/src/shade/renderer/texture/mipmaps/NOTES.md +0 -3
  374. package/src/shade/renderer/volumetrics/NOTES.md +0 -1329
  375. package/src/shade/renderer/volumetrics/VOLUMETRIC_NOISE_PLAN_2026_10_15.md +0 -336
  376. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts +0 -23
  377. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +0 -1
  378. package/src/shade/renderer/water/medium/GPUWaterBodies.js +0 -75
  379. 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
+ }