@woosh/meep-engine 3.53.0 → 3.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/package.json +1 -1
  2. package/src/core/math/noise/worley_noise_2d.d.ts +14 -0
  3. package/src/core/math/noise/worley_noise_2d.d.ts.map +1 -0
  4. package/src/core/math/noise/worley_noise_2d.js +74 -0
  5. package/src/engine/graphics/texture/atlas/CachingTextureAtlas.d.ts +6 -1
  6. package/src/engine/graphics/texture/atlas/CachingTextureAtlas.d.ts.map +1 -1
  7. package/src/engine/graphics/texture/atlas/CachingTextureAtlas.js +363 -325
  8. package/src/engine/graphics/texture/atlas/ManagedTextureAtlas.d.ts +93 -14
  9. package/src/engine/graphics/texture/atlas/ManagedTextureAtlas.d.ts.map +1 -1
  10. package/src/engine/graphics/texture/atlas/ManagedTextureAtlas.js +322 -183
  11. package/src/engine/graphics/texture/atlas/ReferencedTextureAtlas.d.ts +9 -0
  12. package/src/engine/graphics/texture/atlas/ReferencedTextureAtlas.d.ts.map +1 -1
  13. package/src/engine/graphics/texture/atlas/ReferencedTextureAtlas.js +110 -83
  14. package/src/engine/graphics/texture/atlas/SpriteAtlas.d.ts +42 -0
  15. package/src/engine/graphics/texture/atlas/SpriteAtlas.d.ts.map +1 -0
  16. package/src/engine/{graphics3/decal/DecalAtlas.js → graphics/texture/atlas/SpriteAtlas.js} +202 -194
  17. package/src/engine/graphics/texture/atlas/SpriteAtlasFill.d.ts +12 -0
  18. package/src/engine/graphics/texture/atlas/SpriteAtlasFill.d.ts.map +1 -0
  19. package/src/engine/graphics/texture/atlas/SpriteAtlasFill.js +30 -0
  20. package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts +7 -0
  21. package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts.map +1 -1
  22. package/src/engine/graphics/texture/atlas/TextureAtlas.js +11 -1
  23. package/src/engine/graphics/texture/atlas/sprite_atlas_border.d.ts +52 -0
  24. package/src/engine/graphics/texture/atlas/sprite_atlas_border.d.ts.map +1 -0
  25. package/src/engine/{graphics3/decal/decal_atlas_border.js → graphics/texture/atlas/sprite_atlas_border.js} +69 -69
  26. package/src/engine/graphics3/DecalSystem.d.ts.map +1 -1
  27. package/src/engine/graphics3/DecalSystem.js +447 -450
  28. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts +9 -22
  29. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts.map +1 -1
  30. package/src/engine/graphics3/GPUParticleEmitterSystem.js +892 -951
  31. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  32. package/src/generation/foliage/FOLIAGE_DESIGN.md +2 -1
  33. package/src/generation/foliage/config/BarkMaterialConfig.d.ts +4 -3
  34. package/src/generation/foliage/config/BarkMaterialConfig.d.ts.map +1 -1
  35. package/src/generation/foliage/config/BarkMaterialConfig.js +6 -6
  36. package/src/generation/foliage/config/FoliageBarkType.d.ts +11 -0
  37. package/src/generation/foliage/config/FoliageBarkType.d.ts.map +1 -0
  38. package/src/generation/foliage/config/FoliageBarkType.js +16 -0
  39. package/src/generation/foliage/config/FoliageMaterialConfig.d.ts +58 -5
  40. package/src/generation/foliage/config/FoliageMaterialConfig.d.ts.map +1 -1
  41. package/src/generation/foliage/config/FoliageMaterialConfig.js +279 -181
  42. package/src/generation/foliage/index.d.ts +2 -1
  43. package/src/generation/foliage/index.js +44 -43
  44. package/src/generation/foliage/material/make_bark_material.d.ts.map +1 -1
  45. package/src/generation/foliage/material/make_bark_material.js +23 -95
  46. package/src/generation/foliage/material/make_foliage_material.d.ts +29 -3
  47. package/src/generation/foliage/material/make_foliage_material.d.ts.map +1 -1
  48. package/src/generation/foliage/material/make_foliage_material.js +605 -359
  49. package/src/generation/foliage/mesh/branch_collar_project.d.ts.map +1 -1
  50. package/src/generation/foliage/mesh/branch_collar_project.js +205 -203
  51. package/src/generation/foliage/mesh/foliage_collect_attachments.d.ts +15 -0
  52. package/src/generation/foliage/mesh/foliage_collect_attachments.d.ts.map +1 -1
  53. package/src/generation/foliage/mesh/foliage_collect_attachments.js +569 -502
  54. package/src/generation/foliage/mesh/tree_mesh_build_bark.d.ts.map +1 -1
  55. package/src/generation/foliage/mesh/tree_mesh_build_bark.js +1049 -1047
  56. package/src/generation/foliage/mesh/tree_mesh_build_foliage.d.ts +2 -1
  57. package/src/generation/foliage/mesh/tree_mesh_build_foliage.d.ts.map +1 -1
  58. package/src/generation/foliage/mesh/tree_mesh_build_foliage.js +454 -415
  59. package/src/generation/foliage/presets/make_tree_species_preset.d.ts.map +1 -1
  60. package/src/generation/foliage/presets/make_tree_species_preset.js +870 -837
  61. package/src/generation/foliage/skeleton/crown_envelope_evaluate.js +121 -121
  62. package/src/generation/foliage/skeleton/tree_skeleton_compute_pipe_radii.d.ts +10 -7
  63. package/src/generation/foliage/skeleton/tree_skeleton_compute_pipe_radii.d.ts.map +1 -1
  64. package/src/generation/foliage/skeleton/tree_skeleton_compute_pipe_radii.js +206 -179
  65. package/src/generation/foliage/skeleton/tree_skeleton_grow.d.ts.map +1 -1
  66. package/src/generation/foliage/skeleton/tree_skeleton_grow.js +1871 -1104
  67. package/src/shade/playground/foliage_wind/foliage_world.d.ts +6 -0
  68. package/src/shade/playground/foliage_wind/foliage_world.d.ts.map +1 -1
  69. package/src/shade/playground/foliage_wind/foliage_world.js +602 -593
  70. package/src/shade/playground/foliage_wind/index.html +260 -254
  71. package/src/shade/playground/foliage_wind/main.js +275 -263
  72. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  73. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  74. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  75. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  76. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  77. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +1 -1
  78. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +3 -3
  79. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  80. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +350 -294
  81. package/src/shade/renderer/hiz/generate_downsampler_code.d.ts +0 -2
  82. package/src/shade/renderer/hiz/generate_downsampler_code.d.ts.map +1 -1
  83. package/src/shade/renderer/hiz/generate_downsampler_code.js +99 -100
  84. package/src/shade/renderer/hiz/hzb_pick_alignment.d.ts +15 -0
  85. package/src/shade/renderer/hiz/hzb_pick_alignment.d.ts.map +1 -0
  86. package/src/shade/renderer/hiz/hzb_pick_alignment.js +148 -0
  87. package/src/shade/renderer/hiz/hzb_plan_reduction_steps.d.ts +28 -0
  88. package/src/shade/renderer/hiz/hzb_plan_reduction_steps.d.ts.map +1 -0
  89. package/src/shade/renderer/hiz/hzb_plan_reduction_steps.js +58 -0
  90. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +5 -5
  91. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
  92. package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +57 -67
  93. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +3 -3
  94. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +1 -1
  95. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.js +111 -112
  96. package/src/shade/renderer/hiz/shader_hzb_spd.d.ts +5 -0
  97. package/src/shade/renderer/hiz/shader_hzb_spd.d.ts.map +1 -0
  98. package/src/shade/renderer/hiz/shader_hzb_spd.js +160 -0
  99. package/src/shade/renderer/light/GPULightCollection.d.ts +8 -0
  100. package/src/shade/renderer/light/GPULightCollection.d.ts.map +1 -1
  101. package/src/shade/renderer/light/GPULightCollection.js +67 -0
  102. package/src/shade/renderer/light/environment/FAR_FIELD_IRRADIANCE_SH_RESOURCE.d.ts +7 -0
  103. package/src/shade/renderer/light/environment/FAR_FIELD_IRRADIANCE_SH_RESOURCE.d.ts.map +1 -0
  104. package/src/shade/renderer/light/environment/FAR_FIELD_IRRADIANCE_SH_RESOURCE.js +13 -0
  105. package/src/shade/renderer/light/environment/shader_project_far_field_irradiance_sh.d.ts +10 -0
  106. package/src/shade/renderer/light/environment/shader_project_far_field_irradiance_sh.d.ts.map +1 -0
  107. package/src/shade/renderer/light/environment/shader_project_far_field_irradiance_sh.js +72 -0
  108. package/src/shade/renderer/light/world_cluster/WORLD_LIGHT_CLUSTER.d.ts +39 -0
  109. package/src/shade/renderer/light/world_cluster/WORLD_LIGHT_CLUSTER.d.ts.map +1 -0
  110. package/src/shade/renderer/light/world_cluster/WORLD_LIGHT_CLUSTER.js +90 -0
  111. package/src/shade/renderer/light/world_cluster/chunk_world_light_cluster_lookup.d.ts +12 -0
  112. package/src/shade/renderer/light/world_cluster/chunk_world_light_cluster_lookup.d.ts.map +1 -0
  113. package/src/shade/renderer/light/world_cluster/chunk_world_light_cluster_lookup.js +56 -0
  114. package/src/shade/renderer/light/world_cluster/graph_build_world_light_clusters.d.ts +31 -0
  115. package/src/shade/renderer/light/world_cluster/graph_build_world_light_clusters.d.ts.map +1 -0
  116. package/src/shade/renderer/light/world_cluster/graph_build_world_light_clusters.js +196 -0
  117. package/src/shade/renderer/light/world_cluster/shader_world_cluster_assign_lights.d.ts +10 -0
  118. package/src/shade/renderer/light/world_cluster/shader_world_cluster_assign_lights.d.ts.map +1 -0
  119. package/src/shade/renderer/light/world_cluster/shader_world_cluster_assign_lights.js +163 -0
  120. package/src/shade/renderer/light/world_cluster/shader_world_cluster_cull_lights.d.ts +4 -0
  121. package/src/shade/renderer/light/world_cluster/shader_world_cluster_cull_lights.d.ts.map +1 -0
  122. package/src/shade/renderer/light/world_cluster/shader_world_cluster_cull_lights.js +137 -0
  123. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  124. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  125. package/src/shade/renderer/particles/GPUParticleSystem.js +4 -3
  126. package/src/shade/renderer/particles/graph_particles_avboit.d.ts +4 -3
  127. package/src/shade/renderer/particles/graph_particles_avboit.d.ts.map +1 -1
  128. package/src/shade/renderer/particles/graph_particles_avboit.js +28 -11
  129. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts +9 -13
  130. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts.map +1 -1
  131. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.js +25 -20
  132. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.d.ts.map +1 -1
  133. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.js +21 -38
  134. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  135. package/src/shade/renderer/shader/chunk/depth/chunk_query_depth_from_screen_space_bb.d.ts.map +1 -1
  136. package/src/shade/renderer/shader/chunk/depth/chunk_query_depth_from_screen_space_bb.js +62 -62
  137. package/src/shade/renderer/shader/chunk/random/lowbias/chunk_hash_lowbias32_vec2u.d.ts +6 -0
  138. package/src/shade/renderer/shader/chunk/random/lowbias/chunk_hash_lowbias32_vec2u.d.ts.map +1 -0
  139. package/src/shade/renderer/shader/chunk/random/lowbias/chunk_hash_lowbias32_vec2u.js +19 -0
  140. package/src/shade/renderer/shader/chunk/screen_space/chunk_ffx_sssr_get_mip_resolution.d.ts.map +1 -1
  141. package/src/shade/renderer/shader/chunk/screen_space/chunk_ffx_sssr_get_mip_resolution.js +16 -10
  142. package/src/shade/renderer/shader/chunk/screen_space/chunk_ffx_sssr_hierarchical_raymarch.d.ts.map +1 -1
  143. package/src/shade/renderer/shader/chunk/screen_space/chunk_ffx_sssr_hierarchical_raymarch.js +118 -117
  144. package/src/shade/renderer/texture/atlas/GPUTileAtlas3D.d.ts +41 -7
  145. package/src/shade/renderer/texture/atlas/GPUTileAtlas3D.d.ts.map +1 -1
  146. package/src/shade/renderer/texture/atlas/GPUTileAtlas3D.js +162 -58
  147. package/src/shade/renderer/volumetrics/GPUSceneVolumetrics.js +1 -1
  148. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts +10 -12
  149. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  150. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +583 -606
  151. package/src/shade/renderer/volumetrics/chunk_volumetrics_froxel_sample.d.ts +24 -0
  152. package/src/shade/renderer/volumetrics/chunk_volumetrics_froxel_sample.d.ts.map +1 -0
  153. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi_sample.d.ts.map +1 -1
  154. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +1 -1
  155. package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +640 -640
  156. package/src/shade/renderer/volumetrics/irradiance_clipmap/GPUIrradianceClipmap.d.ts +113 -0
  157. package/src/shade/renderer/volumetrics/irradiance_clipmap/GPUIrradianceClipmap.d.ts.map +1 -0
  158. package/src/shade/renderer/volumetrics/irradiance_clipmap/GPUIrradianceClipmap.js +484 -0
  159. package/src/shade/renderer/volumetrics/irradiance_clipmap/IRRADIANCE_CLIPMAP.d.ts +50 -0
  160. package/src/shade/renderer/volumetrics/irradiance_clipmap/IRRADIANCE_CLIPMAP.d.ts.map +1 -0
  161. package/src/shade/renderer/volumetrics/irradiance_clipmap/IRRADIANCE_CLIPMAP.js +60 -0
  162. package/src/shade/renderer/volumetrics/irradiance_clipmap/IRRADIANCE_CLIPMAP_STRUCT.d.ts +25 -0
  163. package/src/shade/renderer/volumetrics/irradiance_clipmap/IRRADIANCE_CLIPMAP_STRUCT.d.ts.map +1 -0
  164. package/src/shade/renderer/volumetrics/irradiance_clipmap/IRRADIANCE_CLIPMAP_STRUCT.js +47 -0
  165. package/src/shade/renderer/volumetrics/irradiance_clipmap/IrradianceCascadePlacement.d.ts +74 -0
  166. package/src/shade/renderer/volumetrics/irradiance_clipmap/IrradianceCascadePlacement.d.ts.map +1 -0
  167. package/src/shade/renderer/volumetrics/irradiance_clipmap/IrradianceCascadePlacement.js +173 -0
  168. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_gi_sample.d.ts +26 -0
  169. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_gi_sample.d.ts.map +1 -0
  170. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_gi_sample.js +83 -0
  171. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_local_light.d.ts +31 -0
  172. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_local_light.d.ts.map +1 -0
  173. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_local_light.js +113 -0
  174. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_sample.d.ts +121 -0
  175. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_sample.d.ts.map +1 -0
  176. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_sample.js +356 -0
  177. package/src/shade/renderer/volumetrics/irradiance_clipmap/shader_irradiance_clipmap_build.d.ts +16 -0
  178. package/src/shade/renderer/volumetrics/irradiance_clipmap/shader_irradiance_clipmap_build.d.ts.map +1 -0
  179. package/src/shade/renderer/volumetrics/irradiance_clipmap/shader_irradiance_clipmap_build.js +191 -0
  180. package/src/shade/renderer/volumetrics/shader_volumetrics_classify_froxels.d.ts +16 -0
  181. package/src/shade/renderer/volumetrics/shader_volumetrics_classify_froxels.d.ts.map +1 -0
  182. package/src/shade/renderer/volumetrics/shader_volumetrics_lighting_regimes.d.ts +23 -0
  183. package/src/shade/renderer/volumetrics/shader_volumetrics_lighting_regimes.d.ts.map +1 -0
  184. package/src/shade/renderer/wind/GPUSceneWind.d.ts +1 -1
  185. package/src/shade/renderer/wind/GPUSceneWind.js +1 -1
  186. package/src/engine/graphics3/decal/DecalAtlasFill.js +0 -32
  187. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_froxel_centre_ws.js +0 -49
  188. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.js +0 -50
  189. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi_sample.js +0 -105
  190. package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.js +0 -80
  191. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi.js +0 -51
  192. package/src/shade/renderer/volumetrics/gi/volumetrics_gi_resources.js +0 -39
  193. package/src/shade/renderer/volumetrics/graph_build_froxel_irradiance.js +0 -121
  194. package/src/shade/renderer/volumetrics/shader_volumetrics_build_irradiance.js +0 -237
@@ -1,593 +1,602 @@
1
- import { assert } from "../../../core/assert.js";
2
- import { Quaternion } from "../../../core/geom/Quaternion.js";
3
- import { Vector3 } from "../../../core/geom/Vector3.js";
4
- import { DEG_TO_RAD } from "../../../core/math/DEG_TO_RAD.js";
5
- import { PI2 } from "../../../core/math/PI2.js";
6
- import { seededRandom } from "../../../core/math/random/seededRandom.js";
7
- import { EntityComponentDataset } from "../../../engine/ecs/EntityComponentDataset.js";
8
- import { EntityManager } from "../../../engine/ecs/EntityManager.js";
9
- import { ParentEntitySystem } from "../../../engine/ecs/parent/ParentEntitySystem.js";
10
- import { TransformAttachmentSystem } from "../../../engine/ecs/transform-attachment/TransformAttachmentSystem.js";
11
- import { t64_announce_change } from "../../../engine/ecs/transform/t64_announce_change.js";
12
- import { Transform64 } from "../../../engine/ecs/transform/Transform64.js";
13
- import { Light } from "../../../engine/graphics/ecs/light/Light.js";
14
- import { DrawMode } from "../../../engine/graphics/ecs/mesh-v2/DrawMode.js";
15
- import { ShadedGeometry } from "../../../engine/graphics/ecs/mesh-v2/ShadedGeometry.js";
16
- import { FoliageWindRigs } from "../../../engine/graphics3/animation/FoliageWindRigs.js";
17
- import { FoliageWindSystem } from "../../../engine/graphics3/FoliageWindSystem.js";
18
- import { GPUFluidSystem } from "../../../engine/graphics3/GPUFluidSystem.js";
19
- import { LightSystem } from "../../../engine/graphics3/LightSystem.js";
20
- import { MeshSkinSystem } from "../../../engine/graphics3/MeshSkinSystem.js";
21
- import { prefab_instance_destroy, prefab_instantiate } from "../../../engine/graphics3/prefab/prefab_instantiate.js";
22
- import { ShadedGeometrySystem } from "../../../engine/graphics3/ShadedGeometrySystem.js";
23
- import { FluidEffectorsComponent } from "../../../engine/physics/fluid/ecs/FluidEffectorsComponent.js";
24
- import { FluidEffectorsSystem } from "../../../engine/physics/fluid/ecs/FluidEffectorsSystem.js";
25
- import { GPUFluidComponent } from "../../../engine/physics/fluid/ecs/GPUFluidComponent.js";
26
- import { GlobalFluidEffector } from "../../../engine/physics/fluid/effector/GlobalFluidEffector.js";
27
- import { ImpulseFluidEffector } from "../../../engine/physics/fluid/effector/ImpulseFluidEffector.js";
28
- import { VortexFluidEffector } from "../../../engine/physics/fluid/effector/VortexFluidEffector.js";
29
- import { TreePreset } from "../../../generation/foliage/config/TreePreset.js";
30
- import { generate_tree } from "../../../generation/foliage/asset/generate_tree.js";
31
- import { Attribute } from "../../renderer/geometry/Attribute.js";
32
- import { Geometry } from "../../renderer/geometry/Geometry.js";
33
- import { MeshoptClusterizer } from "../../renderer/geometry/meshlet/build/meshoptimizer/meshopt_clusterizer.js";
34
- import { meshlet_geometry_build_from_geometry } from "../../renderer/geometry/meshlet_geometry_build_from_geometry.js";
35
- import { StandardShadeMaterial } from "../../renderer/material/StandardShadeMaterial.js";
36
-
37
- /**
38
- * Ordered list of all species presets available in the playground.
39
- * @type {string[]}
40
- */
41
- export const FOLIAGE_PLAYGROUND_PRESETS = [
42
- TreePreset.Oak,
43
- TreePreset.Pine,
44
- TreePreset.Birch,
45
- TreePreset.Willow,
46
- TreePreset.Maple,
47
- TreePreset.Spruce,
48
- TreePreset.Palm,
49
- TreePreset.Shrub,
50
- TreePreset.Fern,
51
- ];
52
-
53
- /**
54
- * Builds a subdivided meadow ground grid `MeshletGeometry` with subtle organic height undulation.
55
- *
56
- * @param {number} [half_extent=32]
57
- * @param {number} [subdivisions=24]
58
- * @returns {import("../../renderer/geometry/MeshletGeometry.js").MeshletGeometry}
59
- */
60
- export function build_meadow_ground_geometry(half_extent = 32, subdivisions = 24) {
61
- const stride = subdivisions + 1;
62
- const vertex_count = stride * stride;
63
- const quad_count = subdivisions * subdivisions;
64
-
65
- const positions = new Float32Array(vertex_count * 3);
66
- const normals = new Float32Array(vertex_count * 3);
67
- const uvs = new Float32Array(vertex_count * 2);
68
- const indices = new Uint32Array(quad_count * 6);
69
-
70
- let v = 0;
71
- for (let iz = 0; iz <= subdivisions; iz++) {
72
- const tz = iz / subdivisions;
73
- const z = (tz * 2 - 1) * half_extent;
74
- for (let ix = 0; ix <= subdivisions; ix++) {
75
- const tx = ix / subdivisions;
76
- const x = (tx * 2 - 1) * half_extent;
77
-
78
- positions[v * 3] = x;
79
- positions[v * 3 + 1] = 0;
80
- positions[v * 3 + 2] = z;
81
-
82
- normals[v * 3] = 0;
83
- normals[v * 3 + 1] = 1;
84
- normals[v * 3 + 2] = 0;
85
-
86
- uvs[v * 2] = tx * 8;
87
- uvs[v * 2 + 1] = tz * 8;
88
- v++;
89
- }
90
- }
91
-
92
- let idx = 0;
93
- for (let iz = 0; iz < subdivisions; iz++) {
94
- for (let ix = 0; ix < subdivisions; ix++) {
95
- const i00 = iz * stride + ix;
96
- const i10 = i00 + 1;
97
- const i01 = (iz + 1) * stride + ix;
98
- const i11 = i01 + 1;
99
-
100
- indices[idx++] = i00;
101
- indices[idx++] = i01;
102
- indices[idx++] = i10;
103
-
104
- indices[idx++] = i10;
105
- indices[idx++] = i01;
106
- indices[idx++] = i11;
107
- }
108
- }
109
-
110
- const geometry = new Geometry();
111
- geometry.index = Attribute.from(indices, 1, "index");
112
- geometry.addAttribute(Attribute.from(positions, 3, "position"));
113
- geometry.addAttribute(Attribute.from(normals, 3, "normal"));
114
- geometry.addAttribute(Attribute.from(uvs, 2, "uv"));
115
- geometry.ensureTangents();
116
-
117
- return meshlet_geometry_build_from_geometry(geometry);
118
- }
119
-
120
- /**
121
- * Creates a full ECS world (`EntityManager` + `EntityComponentDataset`) for the Foliage & Wind
122
- * Playground, wiring:
123
- * - `ParentEntitySystem`
124
- * - `TransformAttachmentSystem`
125
- * - `ShadedGeometrySystem`
126
- * - `MeshSkinSystem`
127
- * - `FoliageWindSystem`
128
- * - `LightSystem`
129
- * - `FluidEffectorsSystem`
130
- * - `GPUFluidSystem`
131
- *
132
- * @param {import("../../../engine/graphics3/GraphicsEngine.js").GraphicsEngine} graphics
133
- * @param {import("../../renderer/scene/Scene.js").Scene} scene
134
- * @param {object} [initial_options]
135
- * @param {"specimen"|"grove"} [initial_options.mode="specimen"]
136
- * @param {string} [initial_options.preset="oak"]
137
- * @param {number} [initial_options.seed=1337]
138
- * @param {number} [initial_options.lod=0]
139
- * @param {number} [initial_options.windSpeed=6.5]
140
- * @param {number} [initial_options.headingDeg=25]
141
- * @param {number} [initial_options.gustStrength=4.5]
142
- * @param {number} [initial_options.gustPeriod=5.5]
143
- * @param {number} [initial_options.windWeight=1.0]
144
- * @param {boolean} [initial_options.enableGPUFluid=true]
145
- */
146
- export async function createFoliageWindWorld(graphics, scene, initial_options = {}) {
147
- assert.defined(graphics, 'graphics');
148
- assert.defined(scene, 'scene');
149
-
150
- await MeshoptClusterizer.ready;
151
-
152
- const enable_gpu_fluid = initial_options.enableGPUFluid !== false;
153
-
154
- const entities = new EntityManager();
155
- entities.fixedUpdateStepSize = 1 / 60;
156
-
157
- const dataset = new EntityComponentDataset();
158
- dataset.registerComponentType(GPUFluidComponent);
159
-
160
- entities.addSystem(new ParentEntitySystem());
161
- entities.addSystem(new TransformAttachmentSystem());
162
- entities.addSystem(new ShadedGeometrySystem(graphics, scene));
163
- entities.addSystem(new MeshSkinSystem(graphics, scene));
164
- entities.addSystem(new FoliageWindSystem(graphics, scene));
165
- entities.addSystem(new LightSystem(graphics, scene));
166
- entities.addSystem(new FluidEffectorsSystem());
167
-
168
- if (enable_gpu_fluid && typeof graphics.add_extension === "function") {
169
- entities.addSystem(new GPUFluidSystem(graphics, scene));
170
- }
171
-
172
- entities.attachDataset(dataset);
173
-
174
- await new Promise((resolve, reject) => entities.startup(resolve, reject));
175
-
176
- // --- 1. ECS Lighting Entities ---
177
- const sun_entity = dataset.createEntity();
178
- const sun_light = new Light();
179
- sun_light.type.set(Light.Type.DIRECTION);
180
- sun_light.color.setRGB(1.0, 0.96, 0.88);
181
- sun_light.intensity.set(2.2);
182
- sun_light.radius.set(0.006475);
183
- sun_light.castShadow.set(true);
184
- sun_light.maxShadowDistance.set(60);
185
-
186
- const sun_transform = new Transform64();
187
- const sun_dir = new Vector3(-0.48, -0.78, -0.40).normalize();
188
- const sun_rot = new Quaternion().fromUnitVectors(Vector3.forward, sun_dir);
189
- sun_transform.setRotation(sun_rot.x, sun_rot.y, sun_rot.z, sun_rot.w);
190
- sun_transform.updateMatrix();
191
- dataset.addComponentToEntity(sun_entity, sun_light);
192
- dataset.addComponentToEntity(sun_entity, sun_transform);
193
-
194
- const fill_entity = dataset.createEntity();
195
- const fill_light = new Light();
196
- fill_light.type.set(Light.Type.DIRECTION);
197
- fill_light.color.setRGB(0.55, 0.72, 0.95);
198
- fill_light.intensity.set(0.65);
199
- fill_light.castShadow.set(false);
200
-
201
- const fill_transform = new Transform64();
202
- const fill_dir = new Vector3(0.55, -0.55, 0.62).normalize();
203
- const fill_rot = new Quaternion().fromUnitVectors(Vector3.forward, fill_dir);
204
- fill_transform.setRotation(fill_rot.x, fill_rot.y, fill_rot.z, fill_rot.w);
205
- fill_transform.updateMatrix();
206
- dataset.addComponentToEntity(fill_entity, fill_light);
207
- dataset.addComponentToEntity(fill_entity, fill_transform);
208
-
209
- // --- 2. ECS Meadow Ground Entity ---
210
- const ground_entity = dataset.createEntity();
211
- const ground_material = new StandardShadeMaterial();
212
- ground_material.diffuse_color.setRGB(0.22, 0.32, 0.15);
213
- ground_material.roughness_factor = 0.92;
214
- ground_material.metallic_factor = 0.0;
215
- ground_material.ambient_factors.a = 1.0;
216
- ground_material.ambient_factors.b = 0.0;
217
- const ground_geometry = build_meadow_ground_geometry(36, 20);
218
- const ground_shaded = ShadedGeometry.from(ground_geometry, ground_material, DrawMode.Triangles);
219
- const ground_transform = new Transform64();
220
- ground_transform.updateMatrix();
221
- dataset.addComponentToEntity(ground_entity, ground_shaded);
222
- dataset.addComponentToEntity(ground_entity, ground_transform);
223
-
224
- // --- 3. ECS Fluid Wind & Cascade Simulation Entities ---
225
- const fluid_entity = dataset.createEntity();
226
- const fluid_transform = new Transform64();
227
- fluid_transform.setTranslation(0, 6, 0);
228
- fluid_transform.updateMatrix();
229
-
230
- const fluid_component = new GPUFluidComponent();
231
- fluid_component.resolution = [64, 32, 64];
232
- fluid_component.cascade_count = 3;
233
- fluid_component.cell_size = 0.5;
234
- fluid_component.vorticity_confinement = 1.5;
235
-
236
- const global_wind = new GlobalFluidEffector();
237
- global_wind.wind = [6.5, 0, 2.5];
238
- global_wind.drag = 1.5;
239
- global_wind.gust = 0.5;
240
- global_wind.gust_period = 5.5;
241
-
242
- const fluid_effectors = new FluidEffectorsComponent();
243
- fluid_effectors.addEffector(global_wind);
244
- dataset.addComponentToEntity(fluid_entity, fluid_component);
245
- dataset.addComponentToEntity(fluid_entity, fluid_effectors);
246
- dataset.addComponentToEntity(fluid_entity, fluid_transform);
247
-
248
- // Local travelling gust + vortex effector entity
249
- const gust_entity = dataset.createEntity();
250
- const gust_transform = new Transform64();
251
- gust_transform.setTranslation(-20, 4, 0);
252
- gust_transform.updateMatrix();
253
-
254
- const impulse_effector = new ImpulseFluidEffector();
255
- impulse_effector.radius = 0; // Inactive until triggered
256
- impulse_effector.spread = 0.35;
257
- impulse_effector.force = [0, 0, 0];
258
-
259
- const vortex_effector = new VortexFluidEffector();
260
- vortex_effector.radius = 0; // Inactive until triggered
261
- vortex_effector.length = 8;
262
- vortex_effector.strength = 0;
263
-
264
- const local_effectors = new FluidEffectorsComponent();
265
- local_effectors.addEffector(impulse_effector);
266
- local_effectors.addEffector(vortex_effector);
267
- dataset.addComponentToEntity(gust_entity, local_effectors);
268
- dataset.addComponentToEntity(gust_entity, gust_transform);
269
-
270
- // --- 4. Tree Management & Wind State ---
271
- const state = {
272
- mode: initial_options.mode ?? "specimen",
273
- preset: initial_options.preset ?? TreePreset.Oak,
274
- seed: initial_options.seed ?? 1337,
275
- lod: initial_options.lod ?? 0,
276
- windSpeed: initial_options.windSpeed ?? 6.5,
277
- headingDeg: initial_options.headingDeg ?? 25,
278
- gustStrength: initial_options.gustStrength ?? 4.5,
279
- gustPeriod: initial_options.gustPeriod ?? 5.5,
280
- windWeight: initial_options.windWeight ?? 1.0,
281
- };
282
-
283
- /**
284
- * Cache of generated `TreeAsset`s keyed by `${preset}:${seed}` so switching LODs or modes is instant.
285
- * @type {Map<string, import("../../../generation/foliage/asset/TreeAsset.js").TreeAsset>}
286
- */
287
- const asset_cache = new Map();
288
-
289
- /**
290
- * Active spawned tree instances in `dataset`.
291
- * @type {Array<{root: number, instance: import("../../../engine/graphics3/prefab/PrefabInstance.js").PrefabInstance, lod_asset: import("../../../generation/foliage/asset/TreeLODAsset.js").TreeLODAsset, preset: string}>}
292
- */
293
- const active_trees = [];
294
-
295
- /**
296
- * Active local gust wave state.
297
- */
298
- const active_gust = {
299
- remaining: 0,
300
- duration: 2.4,
301
- dir_x: 1,
302
- dir_z: 0,
303
- speed: 14,
304
- strength: 28,
305
- vortex_strength: 12,
306
- radius: 6.5,
307
- };
308
-
309
- function obtainTreeAsset(preset, seed) {
310
- const key = `${preset}:${seed | 0}`;
311
- let asset = asset_cache.get(key);
312
- if (!asset) {
313
- asset = generate_tree({ preset, seed: seed | 0 });
314
- asset_cache.set(key, asset);
315
- }
316
- return asset;
317
- }
318
-
319
- function clearTrees() {
320
- for (let i = active_trees.length - 1; i >= 0; i--) {
321
- const entry = active_trees[i];
322
- prefab_instance_destroy(dataset, entry.instance);
323
- if (dataset.entityExists(entry.root)) {
324
- dataset.removeEntity(entry.root);
325
- }
326
- }
327
- active_trees.length = 0;
328
- }
329
-
330
- function spawnTreeInstance(preset, seed, lod, x, y, z, yaw_rad, uniform_scale) {
331
- const tree_asset = obtainTreeAsset(preset, seed);
332
- const clamped_lod = Math.max(0, Math.min(tree_asset.lods.length - 1, lod | 0));
333
- const lod_asset = tree_asset.lods[clamped_lod];
334
-
335
- const root = dataset.createEntity();
336
- const placement = new Transform64();
337
- placement.setTranslation(x, y, z);
338
- const half_yaw = yaw_rad * 0.5;
339
- placement.setRotation(0, Math.sin(half_yaw), 0, Math.cos(half_yaw));
340
- placement.setScale(uniform_scale, uniform_scale, uniform_scale);
341
- placement.updateMatrix();
342
- dataset.addComponentToEntity(root, placement);
343
-
344
- const instance = prefab_instantiate(lod_asset.prefab, dataset, root);
345
-
346
- const rigs_comp = dataset.getComponent(root, FoliageWindRigs);
347
- if (rigs_comp !== undefined) {
348
- rigs_comp.weight = state.windWeight;
349
- }
350
-
351
- active_trees.push({
352
- root,
353
- instance,
354
- lod_asset,
355
- preset,
356
- });
357
- }
358
-
359
- /**
360
- * Rebuilds the spawned ECS tree entities for the current or updated configuration.
361
- *
362
- * @param {Partial<typeof state>} [overrides]
363
- */
364
- function rebuildTrees(overrides = {}) {
365
- if (overrides.mode !== undefined) state.mode = overrides.mode;
366
- if (overrides.preset !== undefined) state.preset = overrides.preset;
367
- if (overrides.seed !== undefined) state.seed = overrides.seed | 0;
368
- if (overrides.lod !== undefined) state.lod = overrides.lod | 0;
369
- if (overrides.windWeight !== undefined) state.windWeight = overrides.windWeight;
370
-
371
- clearTrees();
372
-
373
- if (state.mode === "grove") {
374
- // Hero specimen in the center
375
- spawnTreeInstance(state.preset, state.seed, state.lod, 0, 0, 0, 0, 1.0);
376
-
377
- // Ring of botanical companions around the meadow
378
- const grove_species = [
379
- TreePreset.Oak,
380
- TreePreset.Pine,
381
- TreePreset.Birch,
382
- TreePreset.Willow,
383
- TreePreset.Maple,
384
- TreePreset.Spruce,
385
- TreePreset.Palm,
386
- TreePreset.Shrub,
387
- ];
388
- const rng = seededRandom((state.seed ^ 0x5A17C9) | 0);
389
- const ring_count = grove_species.length;
390
-
391
- for (let i = 0; i < ring_count; i++) {
392
- const angle = (i / ring_count) * PI2 + (rng() - 0.5) * 0.25;
393
- const radius = 9.5 + rng() * 5.0;
394
- const x = Math.cos(angle) * radius;
395
- const z = Math.sin(angle) * radius;
396
- const yaw = rng() * PI2;
397
- const scale = 0.85 + rng() * 0.3;
398
- const species = grove_species[i];
399
- const tree_seed = (state.seed + (i + 1) * 97) | 0;
400
-
401
- spawnTreeInstance(species, tree_seed, state.lod, x, 0, z, yaw, scale);
402
- }
403
- } else {
404
- // Single hero specimen at the origin
405
- spawnTreeInstance(state.preset, state.seed, state.lod, 0, 0, 0, 0, 1.0);
406
- }
407
- }
408
-
409
- /**
410
- * Updates the ECS `GlobalFluidEffector` wind parameters and tree `FoliageWindRigs` blend weights.
411
- *
412
- * @param {Partial<typeof state>} [overrides]
413
- */
414
- function setWind(overrides = {}) {
415
- if (overrides.windSpeed !== undefined) state.windSpeed = overrides.windSpeed;
416
- if (overrides.headingDeg !== undefined) state.headingDeg = overrides.headingDeg;
417
- if (overrides.gustStrength !== undefined) state.gustStrength = overrides.gustStrength;
418
- if (overrides.gustPeriod !== undefined) state.gustPeriod = overrides.gustPeriod;
419
- if (overrides.windWeight !== undefined) state.windWeight = overrides.windWeight;
420
-
421
- const heading_rad = state.headingDeg * DEG_TO_RAD;
422
- const dir_x = Math.cos(heading_rad);
423
- const dir_z = Math.sin(heading_rad);
424
-
425
- const eff_speed = state.windSpeed > 0.05
426
- ? state.windSpeed
427
- : (state.gustStrength > 0 ? 0.05 : 0);
428
-
429
- global_wind.wind[0] = dir_x * eff_speed;
430
- global_wind.wind[1] = 0.0;
431
- global_wind.wind[2] = dir_z * eff_speed;
432
- global_wind.drag = 1.5;
433
- global_wind.gust = eff_speed > 0 ? state.gustStrength / eff_speed : 0;
434
- global_wind.gust_period = Math.max(0.5, state.gustPeriod);
435
-
436
- for (let i = 0; i < active_trees.length; i++) {
437
- const rigs_comp = dataset.getComponent(active_trees[i].root, FoliageWindRigs);
438
- if (rigs_comp !== undefined) {
439
- rigs_comp.weight = state.windWeight;
440
- }
441
- }
442
- }
443
-
444
- /**
445
- * Launches a localized 3D wind gust + vortex swirl across the grove through the ECS
446
- * `ImpulseFluidEffector` and `VortexFluidEffector` entities.
447
- *
448
- * @param {object} [options]
449
- * @param {number} [options.strength=32]
450
- * @param {number} [options.vortexStrength=14]
451
- * @param {number} [options.radius=7.5]
452
- * @param {number} [options.duration=2.2]
453
- */
454
- function triggerLocalGust({
455
- strength = 32,
456
- vortexStrength = 14,
457
- radius = 7.5,
458
- duration = 2.2,
459
- } = {}) {
460
- const heading_rad = state.headingDeg * DEG_TO_RAD;
461
- const dir_x = Math.cos(heading_rad);
462
- const dir_z = Math.sin(heading_rad);
463
-
464
- active_gust.remaining = duration;
465
- active_gust.duration = duration;
466
- active_gust.dir_x = dir_x;
467
- active_gust.dir_z = dir_z;
468
- active_gust.strength = strength;
469
- active_gust.vortex_strength = vortexStrength;
470
- active_gust.radius = radius;
471
-
472
- // Start upstream of the grove and sweep across the center
473
- const start_dist = -14;
474
- gust_transform.setTranslation(dir_x * start_dist, 3.5, dir_z * start_dist);
475
- gust_transform.updateMatrix();
476
- t64_announce_change(dataset, gust_entity);
477
-
478
- impulse_effector.radius = radius;
479
- impulse_effector.force[0] = dir_x * strength;
480
- impulse_effector.force[1] = 2.5;
481
- impulse_effector.force[2] = dir_z * strength;
482
-
483
- vortex_effector.radius = radius * 0.85;
484
- vortex_effector.strength = vortexStrength;
485
- }
486
-
487
- /**
488
- * Advances the ECS world by `dt` seconds.
489
- *
490
- * @param {number} dt
491
- */
492
- function step(dt) {
493
- const clamped_dt = Math.min(Math.max(dt, 0), 0.1);
494
-
495
- if (active_gust.remaining > 0) {
496
- active_gust.remaining = Math.max(0, active_gust.remaining - clamped_dt);
497
- const progress = 1.0 - active_gust.remaining / active_gust.duration;
498
- const envelope = Math.sin(progress * Math.PI);
499
- const dist = -14 + progress * 28;
500
-
501
- gust_transform.setTranslation(
502
- active_gust.dir_x * dist,
503
- 3.5,
504
- active_gust.dir_z * dist
505
- );
506
- gust_transform.updateMatrix();
507
- t64_announce_change(dataset, gust_entity);
508
-
509
- impulse_effector.radius = active_gust.radius;
510
- impulse_effector.force[0] = active_gust.dir_x * active_gust.strength * envelope;
511
- impulse_effector.force[1] = 2.0 * envelope;
512
- impulse_effector.force[2] = active_gust.dir_z * active_gust.strength * envelope;
513
-
514
- vortex_effector.radius = active_gust.radius * 0.85;
515
- vortex_effector.strength = active_gust.vortex_strength * envelope;
516
-
517
- if (active_gust.remaining === 0) {
518
- impulse_effector.radius = 0;
519
- impulse_effector.force[0] = 0;
520
- impulse_effector.force[1] = 0;
521
- impulse_effector.force[2] = 0;
522
- vortex_effector.radius = 0;
523
- vortex_effector.strength = 0;
524
- }
525
- }
526
-
527
- entities.update(clamped_dt);
528
- }
529
-
530
- /**
531
- * Returns live statistics for the HUD and tests.
532
- */
533
- function getStats() {
534
- let total_joints = 0;
535
- let total_vertices = 0;
536
- let total_triangles = 0;
537
-
538
- for (let i = 0; i < active_trees.length; i++) {
539
- const lod_asset = active_trees[i].lod_asset;
540
- total_joints += lod_asset.joint_count;
541
- total_vertices += lod_asset.vertex_count;
542
- total_triangles += lod_asset.triangle_count;
543
- }
544
-
545
- const context = graphics.scene_context(scene);
546
- const active_rig_count = context
547
- ? context.animation_manager.database.get('animation_foliage_rigs').occupancy.cardinality()
548
- : 0;
549
-
550
- return {
551
- mode: state.mode,
552
- preset: state.preset,
553
- seed: state.seed,
554
- lod: state.lod,
555
- treeCount: active_trees.length,
556
- totalJoints: total_joints,
557
- totalVertices: total_vertices,
558
- totalTriangles: total_triangles,
559
- activeRigCount: active_rig_count,
560
- windSpeed: state.windSpeed,
561
- gustStrength: state.gustStrength,
562
- windWeight: state.windWeight,
563
- gustActive: active_gust.remaining > 0,
564
- };
565
- }
566
-
567
- async function destroy() {
568
- clearTrees();
569
- await new Promise((resolve, reject) => entities.shutdown(resolve, reject));
570
- }
571
-
572
- // Initialize wind and spawn initial tree(s)
573
- setWind();
574
- rebuildTrees();
575
-
576
- return {
577
- entities,
578
- dataset,
579
- state,
580
- activeTrees: active_trees,
581
- fluidEntity: fluid_entity,
582
- gustEntity: gust_entity,
583
- globalWind: global_wind,
584
- impulseEffector: impulse_effector,
585
- vortexEffector: vortex_effector,
586
- rebuildTrees,
587
- setWind,
588
- triggerLocalGust,
589
- step,
590
- getStats,
591
- destroy,
592
- };
593
- }
1
+ import { assert } from "../../../core/assert.js";
2
+ import { Quaternion } from "../../../core/geom/Quaternion.js";
3
+ import { Vector3 } from "../../../core/geom/Vector3.js";
4
+ import { DEG_TO_RAD } from "../../../core/math/DEG_TO_RAD.js";
5
+ import { PI2 } from "../../../core/math/PI2.js";
6
+ import { seededRandom } from "../../../core/math/random/seededRandom.js";
7
+ import { EntityComponentDataset } from "../../../engine/ecs/EntityComponentDataset.js";
8
+ import { EntityManager } from "../../../engine/ecs/EntityManager.js";
9
+ import { ParentEntitySystem } from "../../../engine/ecs/parent/ParentEntitySystem.js";
10
+ import { TransformAttachmentSystem } from "../../../engine/ecs/transform-attachment/TransformAttachmentSystem.js";
11
+ import { t64_announce_change } from "../../../engine/ecs/transform/t64_announce_change.js";
12
+ import { Transform64 } from "../../../engine/ecs/transform/Transform64.js";
13
+ import { Light } from "../../../engine/graphics/ecs/light/Light.js";
14
+ import { DrawMode } from "../../../engine/graphics/ecs/mesh-v2/DrawMode.js";
15
+ import { ShadedGeometry } from "../../../engine/graphics/ecs/mesh-v2/ShadedGeometry.js";
16
+ import { FoliageWindRigs } from "../../../engine/graphics3/animation/FoliageWindRigs.js";
17
+ import { FoliageWindSystem } from "../../../engine/graphics3/FoliageWindSystem.js";
18
+ import { GPUFluidSystem } from "../../../engine/graphics3/GPUFluidSystem.js";
19
+ import { LightSystem } from "../../../engine/graphics3/LightSystem.js";
20
+ import { MeshSkinSystem } from "../../../engine/graphics3/MeshSkinSystem.js";
21
+ import { prefab_instance_destroy, prefab_instantiate } from "../../../engine/graphics3/prefab/prefab_instantiate.js";
22
+ import { ShadedGeometrySystem } from "../../../engine/graphics3/ShadedGeometrySystem.js";
23
+ import { FluidEffectorsComponent } from "../../../engine/physics/fluid/ecs/FluidEffectorsComponent.js";
24
+ import { FluidEffectorsSystem } from "../../../engine/physics/fluid/ecs/FluidEffectorsSystem.js";
25
+ import { GPUFluidComponent } from "../../../engine/physics/fluid/ecs/GPUFluidComponent.js";
26
+ import { GlobalFluidEffector } from "../../../engine/physics/fluid/effector/GlobalFluidEffector.js";
27
+ import { ImpulseFluidEffector } from "../../../engine/physics/fluid/effector/ImpulseFluidEffector.js";
28
+ import { VortexFluidEffector } from "../../../engine/physics/fluid/effector/VortexFluidEffector.js";
29
+ import { TreePreset } from "../../../generation/foliage/config/TreePreset.js";
30
+ import { generate_tree } from "../../../generation/foliage/asset/generate_tree.js";
31
+ import { Attribute } from "../../renderer/geometry/Attribute.js";
32
+ import { Geometry } from "../../renderer/geometry/Geometry.js";
33
+ import { MeshoptClusterizer } from "../../renderer/geometry/meshlet/build/meshoptimizer/meshopt_clusterizer.js";
34
+ import { meshlet_geometry_build_from_geometry } from "../../renderer/geometry/meshlet_geometry_build_from_geometry.js";
35
+ import { StandardShadeMaterial } from "../../renderer/material/StandardShadeMaterial.js";
36
+
37
+ /**
38
+ * Ordered list of all species presets available in the playground.
39
+ * @type {string[]}
40
+ */
41
+ export const FOLIAGE_PLAYGROUND_PRESETS = [
42
+ TreePreset.Oak,
43
+ TreePreset.Pine,
44
+ TreePreset.Birch,
45
+ TreePreset.Willow,
46
+ TreePreset.Maple,
47
+ TreePreset.Spruce,
48
+ TreePreset.Palm,
49
+ TreePreset.Shrub,
50
+ TreePreset.Fern,
51
+ ];
52
+
53
+ /**
54
+ * Builds a subdivided meadow ground grid `MeshletGeometry` with subtle organic height undulation.
55
+ *
56
+ * @param {number} [half_extent=32]
57
+ * @param {number} [subdivisions=24]
58
+ * @returns {import("../../renderer/geometry/MeshletGeometry.js").MeshletGeometry}
59
+ */
60
+ export function build_meadow_ground_geometry(half_extent = 32, subdivisions = 24) {
61
+ const stride = subdivisions + 1;
62
+ const vertex_count = stride * stride;
63
+ const quad_count = subdivisions * subdivisions;
64
+
65
+ const positions = new Float32Array(vertex_count * 3);
66
+ const normals = new Float32Array(vertex_count * 3);
67
+ const uvs = new Float32Array(vertex_count * 2);
68
+ const indices = new Uint32Array(quad_count * 6);
69
+
70
+ let v = 0;
71
+ for (let iz = 0; iz <= subdivisions; iz++) {
72
+ const tz = iz / subdivisions;
73
+ const z = (tz * 2 - 1) * half_extent;
74
+ for (let ix = 0; ix <= subdivisions; ix++) {
75
+ const tx = ix / subdivisions;
76
+ const x = (tx * 2 - 1) * half_extent;
77
+
78
+ positions[v * 3] = x;
79
+ positions[v * 3 + 1] = 0;
80
+ positions[v * 3 + 2] = z;
81
+
82
+ normals[v * 3] = 0;
83
+ normals[v * 3 + 1] = 1;
84
+ normals[v * 3 + 2] = 0;
85
+
86
+ uvs[v * 2] = tx * 8;
87
+ uvs[v * 2 + 1] = tz * 8;
88
+ v++;
89
+ }
90
+ }
91
+
92
+ let idx = 0;
93
+ for (let iz = 0; iz < subdivisions; iz++) {
94
+ for (let ix = 0; ix < subdivisions; ix++) {
95
+ const i00 = iz * stride + ix;
96
+ const i10 = i00 + 1;
97
+ const i01 = (iz + 1) * stride + ix;
98
+ const i11 = i01 + 1;
99
+
100
+ indices[idx++] = i00;
101
+ indices[idx++] = i01;
102
+ indices[idx++] = i10;
103
+
104
+ indices[idx++] = i10;
105
+ indices[idx++] = i01;
106
+ indices[idx++] = i11;
107
+ }
108
+ }
109
+
110
+ const geometry = new Geometry();
111
+ geometry.index = Attribute.from(indices, 1, "index");
112
+ geometry.addAttribute(Attribute.from(positions, 3, "position"));
113
+ geometry.addAttribute(Attribute.from(normals, 3, "normal"));
114
+ geometry.addAttribute(Attribute.from(uvs, 2, "uv"));
115
+ geometry.ensureTangents();
116
+
117
+ return meshlet_geometry_build_from_geometry(geometry);
118
+ }
119
+
120
+ /**
121
+ * Creates a full ECS world (`EntityManager` + `EntityComponentDataset`) for the Foliage & Wind
122
+ * Playground, wiring:
123
+ * - `ParentEntitySystem`
124
+ * - `TransformAttachmentSystem`
125
+ * - `ShadedGeometrySystem`
126
+ * - `MeshSkinSystem`
127
+ * - `FoliageWindSystem`
128
+ * - `LightSystem`
129
+ * - `FluidEffectorsSystem`
130
+ * - `GPUFluidSystem`
131
+ *
132
+ * @param {import("../../../engine/graphics3/GraphicsEngine.js").GraphicsEngine} graphics
133
+ * @param {import("../../renderer/scene/Scene.js").Scene} scene
134
+ * @param {object} [initial_options]
135
+ * @param {"specimen"|"grove"} [initial_options.mode="specimen"]
136
+ * @param {string} [initial_options.preset="oak"]
137
+ * @param {number} [initial_options.seed=1337]
138
+ * @param {number} [initial_options.lod=0]
139
+ * @param {boolean} [initial_options.showLeaves=true]
140
+ * @param {number} [initial_options.windSpeed=6.5]
141
+ * @param {number} [initial_options.headingDeg=25]
142
+ * @param {number} [initial_options.gustStrength=4.5]
143
+ * @param {number} [initial_options.gustPeriod=5.5]
144
+ * @param {number} [initial_options.windWeight=1.0]
145
+ * @param {boolean} [initial_options.enableGPUFluid=true]
146
+ */
147
+ export async function createFoliageWindWorld(graphics, scene, initial_options = {}) {
148
+ assert.defined(graphics, 'graphics');
149
+ assert.defined(scene, 'scene');
150
+
151
+ await MeshoptClusterizer.ready;
152
+
153
+ const enable_gpu_fluid = initial_options.enableGPUFluid !== false;
154
+
155
+ const entities = new EntityManager();
156
+ entities.fixedUpdateStepSize = 1 / 60;
157
+
158
+ const dataset = new EntityComponentDataset();
159
+ dataset.registerComponentType(GPUFluidComponent);
160
+
161
+ entities.addSystem(new ParentEntitySystem());
162
+ entities.addSystem(new TransformAttachmentSystem());
163
+ entities.addSystem(new ShadedGeometrySystem(graphics, scene));
164
+ entities.addSystem(new MeshSkinSystem(graphics, scene));
165
+ entities.addSystem(new FoliageWindSystem(graphics, scene));
166
+ entities.addSystem(new LightSystem(graphics, scene));
167
+ entities.addSystem(new FluidEffectorsSystem());
168
+
169
+ if (enable_gpu_fluid && typeof graphics.add_extension === "function") {
170
+ entities.addSystem(new GPUFluidSystem(graphics, scene));
171
+ }
172
+
173
+ entities.attachDataset(dataset);
174
+
175
+ await new Promise((resolve, reject) => entities.startup(resolve, reject));
176
+
177
+ // --- 1. ECS Lighting Entities ---
178
+ const sun_entity = dataset.createEntity();
179
+ const sun_light = new Light();
180
+ sun_light.type.set(Light.Type.DIRECTION);
181
+ sun_light.color.setRGB(1.0, 0.96, 0.88);
182
+ sun_light.intensity.set(2.2);
183
+ sun_light.radius.set(0.006475);
184
+ sun_light.castShadow.set(true);
185
+ sun_light.maxShadowDistance.set(60);
186
+
187
+ const sun_transform = new Transform64();
188
+ const sun_dir = new Vector3(-0.48, -0.78, -0.40).normalize();
189
+ const sun_rot = new Quaternion().fromUnitVectors(Vector3.forward, sun_dir);
190
+ sun_transform.setRotation(sun_rot.x, sun_rot.y, sun_rot.z, sun_rot.w);
191
+ sun_transform.updateMatrix();
192
+ dataset.addComponentToEntity(sun_entity, sun_light);
193
+ dataset.addComponentToEntity(sun_entity, sun_transform);
194
+
195
+ const fill_entity = dataset.createEntity();
196
+ const fill_light = new Light();
197
+ fill_light.type.set(Light.Type.DIRECTION);
198
+ fill_light.color.setRGB(0.55, 0.72, 0.95);
199
+ fill_light.intensity.set(0.65);
200
+ fill_light.castShadow.set(false);
201
+
202
+ const fill_transform = new Transform64();
203
+ const fill_dir = new Vector3(0.55, -0.55, 0.62).normalize();
204
+ const fill_rot = new Quaternion().fromUnitVectors(Vector3.forward, fill_dir);
205
+ fill_transform.setRotation(fill_rot.x, fill_rot.y, fill_rot.z, fill_rot.w);
206
+ fill_transform.updateMatrix();
207
+ dataset.addComponentToEntity(fill_entity, fill_light);
208
+ dataset.addComponentToEntity(fill_entity, fill_transform);
209
+
210
+ // --- 2. ECS Meadow Ground Entity ---
211
+ const ground_entity = dataset.createEntity();
212
+ const ground_material = new StandardShadeMaterial();
213
+ ground_material.diffuse_color.setRGB(0.22, 0.32, 0.15);
214
+ ground_material.roughness_factor = 0.92;
215
+ ground_material.metallic_factor = 0.0;
216
+ ground_material.ambient_factors.a = 1.0;
217
+ ground_material.ambient_factors.b = 0.0;
218
+ const ground_geometry = build_meadow_ground_geometry(36, 20);
219
+ const ground_shaded = ShadedGeometry.from(ground_geometry, ground_material, DrawMode.Triangles);
220
+ const ground_transform = new Transform64();
221
+ ground_transform.updateMatrix();
222
+ dataset.addComponentToEntity(ground_entity, ground_shaded);
223
+ dataset.addComponentToEntity(ground_entity, ground_transform);
224
+
225
+ // --- 3. ECS Fluid Wind & Cascade Simulation Entities ---
226
+ const fluid_entity = dataset.createEntity();
227
+ const fluid_transform = new Transform64();
228
+ fluid_transform.setTranslation(0, 6, 0);
229
+ fluid_transform.updateMatrix();
230
+
231
+ const fluid_component = new GPUFluidComponent();
232
+ fluid_component.resolution = [64, 32, 64];
233
+ fluid_component.cascade_count = 3;
234
+ fluid_component.cell_size = 0.5;
235
+ fluid_component.vorticity_confinement = 1.5;
236
+
237
+ const global_wind = new GlobalFluidEffector();
238
+ global_wind.wind = [6.5, 0, 2.5];
239
+ global_wind.drag = 1.5;
240
+ global_wind.gust = 0.5;
241
+ global_wind.gust_period = 5.5;
242
+
243
+ const fluid_effectors = new FluidEffectorsComponent();
244
+ fluid_effectors.addEffector(global_wind);
245
+ dataset.addComponentToEntity(fluid_entity, fluid_component);
246
+ dataset.addComponentToEntity(fluid_entity, fluid_effectors);
247
+ dataset.addComponentToEntity(fluid_entity, fluid_transform);
248
+
249
+ // Local travelling gust + vortex effector entity
250
+ const gust_entity = dataset.createEntity();
251
+ const gust_transform = new Transform64();
252
+ gust_transform.setTranslation(-20, 4, 0);
253
+ gust_transform.updateMatrix();
254
+
255
+ const impulse_effector = new ImpulseFluidEffector();
256
+ impulse_effector.radius = 0; // Inactive until triggered
257
+ impulse_effector.spread = 0.35;
258
+ impulse_effector.force = [0, 0, 0];
259
+
260
+ const vortex_effector = new VortexFluidEffector();
261
+ vortex_effector.radius = 0; // Inactive until triggered
262
+ vortex_effector.length = 8;
263
+ vortex_effector.strength = 0;
264
+
265
+ const local_effectors = new FluidEffectorsComponent();
266
+ local_effectors.addEffector(impulse_effector);
267
+ local_effectors.addEffector(vortex_effector);
268
+ dataset.addComponentToEntity(gust_entity, local_effectors);
269
+ dataset.addComponentToEntity(gust_entity, gust_transform);
270
+
271
+ // --- 4. Tree Management & Wind State ---
272
+ const state = {
273
+ mode: initial_options.mode ?? "specimen",
274
+ preset: initial_options.preset ?? TreePreset.Oak,
275
+ seed: initial_options.seed ?? 1337,
276
+ lod: initial_options.lod ?? 0,
277
+ showLeaves: initial_options.showLeaves ?? true,
278
+ windSpeed: initial_options.windSpeed ?? 6.5,
279
+ headingDeg: initial_options.headingDeg ?? 25,
280
+ gustStrength: initial_options.gustStrength ?? 4.5,
281
+ gustPeriod: initial_options.gustPeriod ?? 5.5,
282
+ windWeight: initial_options.windWeight ?? 1.0,
283
+ };
284
+
285
+ /**
286
+ * Cache of generated `TreeAsset`s keyed by `${preset}:${seed}:${showLeaves ? 1 : 0}` so switching LODs, modes, or leaves visibility is instant.
287
+ * @type {Map<string, import("../../../generation/foliage/asset/TreeAsset.js").TreeAsset>}
288
+ */
289
+ const asset_cache = new Map();
290
+
291
+ /**
292
+ * Active spawned tree instances in `dataset`.
293
+ * @type {Array<{root: number, instance: import("../../../engine/graphics3/prefab/PrefabInstance.js").PrefabInstance, lod_asset: import("../../../generation/foliage/asset/TreeLODAsset.js").TreeLODAsset, preset: string}>}
294
+ */
295
+ const active_trees = [];
296
+
297
+ /**
298
+ * Active local gust wave state.
299
+ */
300
+ const active_gust = {
301
+ remaining: 0,
302
+ duration: 2.4,
303
+ dir_x: 1,
304
+ dir_z: 0,
305
+ speed: 14,
306
+ strength: 28,
307
+ vortex_strength: 12,
308
+ radius: 6.5,
309
+ };
310
+
311
+ function obtainTreeAsset(preset, seed, show_leaves = state.showLeaves) {
312
+ const leaves_flag = Boolean(show_leaves);
313
+ const key = `${preset}:${seed | 0}:${leaves_flag ? 1 : 0}`;
314
+ let asset = asset_cache.get(key);
315
+ if (!asset) {
316
+ asset = generate_tree({
317
+ preset,
318
+ seed: seed | 0,
319
+ foliage: leaves_flag,
320
+ });
321
+ asset_cache.set(key, asset);
322
+ }
323
+ return asset;
324
+ }
325
+
326
+ function clearTrees() {
327
+ for (let i = active_trees.length - 1; i >= 0; i--) {
328
+ const entry = active_trees[i];
329
+ prefab_instance_destroy(dataset, entry.instance);
330
+ if (dataset.entityExists(entry.root)) {
331
+ dataset.removeEntity(entry.root);
332
+ }
333
+ }
334
+ active_trees.length = 0;
335
+ }
336
+
337
+ function spawnTreeInstance(preset, seed, lod, x, y, z, yaw_rad, uniform_scale) {
338
+ const tree_asset = obtainTreeAsset(preset, seed, state.showLeaves);
339
+ const clamped_lod = Math.max(0, Math.min(tree_asset.lods.length - 1, lod | 0));
340
+ const lod_asset = tree_asset.lods[clamped_lod];
341
+
342
+ const root = dataset.createEntity();
343
+ const placement = new Transform64();
344
+ placement.setTranslation(x, y, z);
345
+ const half_yaw = yaw_rad * 0.5;
346
+ placement.setRotation(0, Math.sin(half_yaw), 0, Math.cos(half_yaw));
347
+ placement.setScale(uniform_scale, uniform_scale, uniform_scale);
348
+ placement.updateMatrix();
349
+ dataset.addComponentToEntity(root, placement);
350
+
351
+ const instance = prefab_instantiate(lod_asset.prefab, dataset, root);
352
+
353
+ const rigs_comp = dataset.getComponent(root, FoliageWindRigs);
354
+ if (rigs_comp !== undefined) {
355
+ rigs_comp.weight = state.windWeight;
356
+ }
357
+
358
+ active_trees.push({
359
+ root,
360
+ instance,
361
+ lod_asset,
362
+ preset,
363
+ });
364
+ }
365
+
366
+ /**
367
+ * Rebuilds the spawned ECS tree entities for the current or updated configuration.
368
+ *
369
+ * @param {Partial<typeof state>} [overrides]
370
+ */
371
+ function rebuildTrees(overrides = {}) {
372
+ if (overrides.mode !== undefined) state.mode = overrides.mode;
373
+ if (overrides.preset !== undefined) state.preset = overrides.preset;
374
+ if (overrides.seed !== undefined) state.seed = overrides.seed | 0;
375
+ if (overrides.lod !== undefined) state.lod = overrides.lod | 0;
376
+ if (overrides.showLeaves !== undefined) state.showLeaves = Boolean(overrides.showLeaves);
377
+ if (overrides.windWeight !== undefined) state.windWeight = overrides.windWeight;
378
+
379
+ clearTrees();
380
+
381
+ if (state.mode === "grove") {
382
+ // Hero specimen in the center
383
+ spawnTreeInstance(state.preset, state.seed, state.lod, 0, 0, 0, 0, 1.0);
384
+
385
+ // Ring of botanical companions around the meadow
386
+ const grove_species = [
387
+ TreePreset.Oak,
388
+ TreePreset.Pine,
389
+ TreePreset.Birch,
390
+ TreePreset.Willow,
391
+ TreePreset.Maple,
392
+ TreePreset.Spruce,
393
+ TreePreset.Palm,
394
+ TreePreset.Shrub,
395
+ ];
396
+ const rng = seededRandom((state.seed ^ 0x5A17C9) | 0);
397
+ const ring_count = grove_species.length;
398
+
399
+ for (let i = 0; i < ring_count; i++) {
400
+ const angle = (i / ring_count) * PI2 + (rng() - 0.5) * 0.25;
401
+ const radius = 9.5 + rng() * 5.0;
402
+ const x = Math.cos(angle) * radius;
403
+ const z = Math.sin(angle) * radius;
404
+ const yaw = rng() * PI2;
405
+ const scale = 0.85 + rng() * 0.3;
406
+ const species = grove_species[i];
407
+ const tree_seed = (state.seed + (i + 1) * 97) | 0;
408
+
409
+ spawnTreeInstance(species, tree_seed, state.lod, x, 0, z, yaw, scale);
410
+ }
411
+ } else {
412
+ // Single hero specimen at the origin
413
+ spawnTreeInstance(state.preset, state.seed, state.lod, 0, 0, 0, 0, 1.0);
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Updates the ECS `GlobalFluidEffector` wind parameters and tree `FoliageWindRigs` blend weights.
419
+ *
420
+ * @param {Partial<typeof state>} [overrides]
421
+ */
422
+ function setWind(overrides = {}) {
423
+ if (overrides.windSpeed !== undefined) state.windSpeed = overrides.windSpeed;
424
+ if (overrides.headingDeg !== undefined) state.headingDeg = overrides.headingDeg;
425
+ if (overrides.gustStrength !== undefined) state.gustStrength = overrides.gustStrength;
426
+ if (overrides.gustPeriod !== undefined) state.gustPeriod = overrides.gustPeriod;
427
+ if (overrides.windWeight !== undefined) state.windWeight = overrides.windWeight;
428
+
429
+ const heading_rad = state.headingDeg * DEG_TO_RAD;
430
+ const dir_x = Math.cos(heading_rad);
431
+ const dir_z = Math.sin(heading_rad);
432
+
433
+ const eff_speed = state.windSpeed > 0.05
434
+ ? state.windSpeed
435
+ : (state.gustStrength > 0 ? 0.05 : 0);
436
+
437
+ global_wind.wind[0] = dir_x * eff_speed;
438
+ global_wind.wind[1] = 0.0;
439
+ global_wind.wind[2] = dir_z * eff_speed;
440
+ global_wind.drag = 1.5;
441
+ global_wind.gust = eff_speed > 0 ? state.gustStrength / eff_speed : 0;
442
+ global_wind.gust_period = Math.max(0.5, state.gustPeriod);
443
+
444
+ for (let i = 0; i < active_trees.length; i++) {
445
+ const rigs_comp = dataset.getComponent(active_trees[i].root, FoliageWindRigs);
446
+ if (rigs_comp !== undefined) {
447
+ rigs_comp.weight = state.windWeight;
448
+ }
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Launches a localized 3D wind gust + vortex swirl across the grove through the ECS
454
+ * `ImpulseFluidEffector` and `VortexFluidEffector` entities.
455
+ *
456
+ * @param {object} [options]
457
+ * @param {number} [options.strength=32]
458
+ * @param {number} [options.vortexStrength=14]
459
+ * @param {number} [options.radius=7.5]
460
+ * @param {number} [options.duration=2.2]
461
+ */
462
+ function triggerLocalGust({
463
+ strength = 32,
464
+ vortexStrength = 14,
465
+ radius = 7.5,
466
+ duration = 2.2,
467
+ } = {}) {
468
+ const heading_rad = state.headingDeg * DEG_TO_RAD;
469
+ const dir_x = Math.cos(heading_rad);
470
+ const dir_z = Math.sin(heading_rad);
471
+
472
+ active_gust.remaining = duration;
473
+ active_gust.duration = duration;
474
+ active_gust.dir_x = dir_x;
475
+ active_gust.dir_z = dir_z;
476
+ active_gust.strength = strength;
477
+ active_gust.vortex_strength = vortexStrength;
478
+ active_gust.radius = radius;
479
+
480
+ // Start upstream of the grove and sweep across the center
481
+ const start_dist = -14;
482
+ gust_transform.setTranslation(dir_x * start_dist, 3.5, dir_z * start_dist);
483
+ gust_transform.updateMatrix();
484
+ t64_announce_change(dataset, gust_entity);
485
+
486
+ impulse_effector.radius = radius;
487
+ impulse_effector.force[0] = dir_x * strength;
488
+ impulse_effector.force[1] = 2.5;
489
+ impulse_effector.force[2] = dir_z * strength;
490
+
491
+ vortex_effector.radius = radius * 0.85;
492
+ vortex_effector.strength = vortexStrength;
493
+ }
494
+
495
+ /**
496
+ * Advances the ECS world by `dt` seconds.
497
+ *
498
+ * @param {number} dt
499
+ */
500
+ function step(dt) {
501
+ const clamped_dt = Math.min(Math.max(dt, 0), 0.1);
502
+
503
+ if (active_gust.remaining > 0) {
504
+ active_gust.remaining = Math.max(0, active_gust.remaining - clamped_dt);
505
+ const progress = 1.0 - active_gust.remaining / active_gust.duration;
506
+ const envelope = Math.sin(progress * Math.PI);
507
+ const dist = -14 + progress * 28;
508
+
509
+ gust_transform.setTranslation(
510
+ active_gust.dir_x * dist,
511
+ 3.5,
512
+ active_gust.dir_z * dist
513
+ );
514
+ gust_transform.updateMatrix();
515
+ t64_announce_change(dataset, gust_entity);
516
+
517
+ impulse_effector.radius = active_gust.radius;
518
+ impulse_effector.force[0] = active_gust.dir_x * active_gust.strength * envelope;
519
+ impulse_effector.force[1] = 2.0 * envelope;
520
+ impulse_effector.force[2] = active_gust.dir_z * active_gust.strength * envelope;
521
+
522
+ vortex_effector.radius = active_gust.radius * 0.85;
523
+ vortex_effector.strength = active_gust.vortex_strength * envelope;
524
+
525
+ if (active_gust.remaining === 0) {
526
+ impulse_effector.radius = 0;
527
+ impulse_effector.force[0] = 0;
528
+ impulse_effector.force[1] = 0;
529
+ impulse_effector.force[2] = 0;
530
+ vortex_effector.radius = 0;
531
+ vortex_effector.strength = 0;
532
+ }
533
+ }
534
+
535
+ entities.update(clamped_dt);
536
+ }
537
+
538
+ /**
539
+ * Returns live statistics for the HUD and tests.
540
+ */
541
+ function getStats() {
542
+ let total_joints = 0;
543
+ let total_vertices = 0;
544
+ let total_triangles = 0;
545
+
546
+ for (let i = 0; i < active_trees.length; i++) {
547
+ const lod_asset = active_trees[i].lod_asset;
548
+ total_joints += lod_asset.joint_count;
549
+ total_vertices += lod_asset.vertex_count;
550
+ total_triangles += lod_asset.triangle_count;
551
+ }
552
+
553
+ const context = graphics.scene_context(scene);
554
+ const active_rig_count = context
555
+ ? context.animation_manager.database.get('animation_foliage_rigs').occupancy.cardinality()
556
+ : 0;
557
+
558
+ return {
559
+ mode: state.mode,
560
+ preset: state.preset,
561
+ seed: state.seed,
562
+ lod: state.lod,
563
+ showLeaves: state.showLeaves,
564
+ treeCount: active_trees.length,
565
+ totalJoints: total_joints,
566
+ totalVertices: total_vertices,
567
+ totalTriangles: total_triangles,
568
+ activeRigCount: active_rig_count,
569
+ windSpeed: state.windSpeed,
570
+ gustStrength: state.gustStrength,
571
+ windWeight: state.windWeight,
572
+ gustActive: active_gust.remaining > 0,
573
+ };
574
+ }
575
+
576
+ async function destroy() {
577
+ clearTrees();
578
+ await new Promise((resolve, reject) => entities.shutdown(resolve, reject));
579
+ }
580
+
581
+ // Initialize wind and spawn initial tree(s)
582
+ setWind();
583
+ rebuildTrees();
584
+
585
+ return {
586
+ entities,
587
+ dataset,
588
+ state,
589
+ activeTrees: active_trees,
590
+ fluidEntity: fluid_entity,
591
+ gustEntity: gust_entity,
592
+ globalWind: global_wind,
593
+ impulseEffector: impulse_effector,
594
+ vortexEffector: vortex_effector,
595
+ rebuildTrees,
596
+ setWind,
597
+ triggerLocalGust,
598
+ step,
599
+ getStats,
600
+ destroy,
601
+ };
602
+ }