@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,415 +1,454 @@
1
- import { clamp } from "../../../core/math/clamp.js";
2
- import { Geometry } from "../../../shade/renderer/geometry/Geometry.js";
3
- import {
4
- geometry_obtain_attribute,
5
- geometry_obtain_index
6
- } from "../../../shade/renderer/geometry/geometry_obtain_attribute.js";
7
- import { StandardAttributes } from "../../../shade/renderer/geometry/StandardAttributes.js";
8
- import { FoliageCardType } from "../config/FoliageCardType.js";
9
- import { TreeLODConfig } from "../config/TreeLODConfig.js";
10
- import { TreeSpeciesConfig } from "../config/TreeSpeciesConfig.js";
11
- import { tree_mesh_apply_canopy_ao } from "./tree_mesh_apply_canopy_ao.js";
12
- import { geometry_obtain_joints_attribute } from "./tree_mesh_build_bark.js";
13
-
14
- /**
15
- * Synthesizes the foliage `Geometry` for a given LOD from a `FoliageAttachmentSet`,
16
- * applying golden-ratio low-discrepancy LOD decimation ($h_i < k_{\text{keep}}$),
17
- * optical area-compensation scaling ($s_{\text{comp}} = k_{\text{keep}}^{-\eta}$),
18
- * botanical 3D card geometry (`FoldedVCard`, `CrossCard`, `TriStarCluster`, `RibbedFrond`, `OctagonalCutoutHull`),
19
- * hemisphere-safe normal puffing, volumetric canopy AO, and 4-joint skeletal wind skinning.
20
- *
21
- * Returns `null` if foliage is disabled or `lod_config.foliage_keep_ratio <= 0` or `attachments.count === 0`.
22
- *
23
- * @param {import("../skeleton/TreeSkeleton.js").TreeSkeleton} skeleton
24
- * @param {import("./foliage_collect_attachments.js").FoliageAttachmentSet} attachments
25
- * @param {TreeSpeciesConfig} [config]
26
- * @param {TreeLODConfig} [lod_config]
27
- * @param {Geometry} [out]
28
- * @returns {Geometry|null}
29
- */
30
- export function tree_mesh_build_foliage(
31
- skeleton,
32
- attachments,
33
- config = new TreeSpeciesConfig(),
34
- lod_config = new TreeLODConfig(),
35
- out = new Geometry()
36
- ) {
37
- const species = config && config.isTreeSpeciesConfig ? config : TreeSpeciesConfig.from(config);
38
- const lod_cfg = lod_config && lod_config.isTreeLODConfig ? lod_config : TreeLODConfig.from(lod_config);
39
- const fol = species.foliage;
40
- const keep_ratio = clamp(lod_cfg.foliage_keep_ratio ?? 1.0, 0.0, 1.0);
41
- if (!fol.enabled || !attachments || attachments.count === 0 || keep_ratio <= 0.0) {
42
- return null;
43
- }
44
-
45
- // Filter active attachment indices by golden-ratio rank
46
- const active_indices = [];
47
- for (let i = 0; i < attachments.count; i++) {
48
- if (attachments.ranks[i] < keep_ratio) {
49
- active_indices.push(i);
50
- }
51
- }
52
- if (active_indices.length === 0 && attachments.count > 0) {
53
- active_indices.push(0);
54
- }
55
- const card_count = active_indices.length;
56
-
57
- // Area compensation scale: k_keep^(-eta)
58
- const eta = clamp(lod_cfg.foliage_area_compensation_exponent ?? 0.42, 0.0, 0.6);
59
- const area_comp = Math.pow(Math.max(0.08, keep_ratio), -eta);
60
-
61
- // Determine effective card type for this LOD
62
- let card_type = fol.card_type;
63
- if (lod_cfg.simplify_cards) {
64
- card_type = FoliageCardType.FoldedVCard;
65
- }
66
-
67
- const frond_segs = Math.max(2, fol.frond_segments | 0);
68
- let verts_per_card = 6;
69
- let tris_per_card = 4;
70
-
71
- switch (card_type) {
72
- case FoliageCardType.CrossCard:
73
- verts_per_card = 8;
74
- tris_per_card = 4;
75
- break;
76
- case FoliageCardType.TriStarCluster:
77
- verts_per_card = 12;
78
- tris_per_card = 6;
79
- break;
80
- case FoliageCardType.RibbedFrond:
81
- verts_per_card = 3 * (frond_segs + 1);
82
- tris_per_card = 4 * frond_segs;
83
- break;
84
- case FoliageCardType.OctagonalCutoutHull:
85
- verts_per_card = 7;
86
- tris_per_card = 6;
87
- break;
88
- case FoliageCardType.FoldedVCard:
89
- default:
90
- verts_per_card = 6;
91
- tris_per_card = 4;
92
- break;
93
- }
94
-
95
- const total_vertices = card_count * verts_per_card;
96
- const total_indices = card_count * tris_per_card * 3;
97
-
98
- const positions = geometry_obtain_attribute(out, StandardAttributes.Position, 3, total_vertices);
99
- const normals = geometry_obtain_attribute(out, StandardAttributes.Normal, 3, total_vertices);
100
- const uv0 = geometry_obtain_attribute(out, StandardAttributes.TextureCoordinates0, 2, total_vertices);
101
- const uv1 = geometry_obtain_attribute(out, StandardAttributes.TextureCoordinates1, 2, total_vertices);
102
- const colors = geometry_obtain_attribute(out, StandardAttributes.Color, 3, total_vertices);
103
- const joints = geometry_obtain_joints_attribute(out, total_vertices);
104
- const weights = geometry_obtain_attribute(out, StandardAttributes.SkinningWeights, 4, total_vertices);
105
- const indices = geometry_obtain_index(out, total_indices);
106
-
107
- const base_w = Math.max(0.05, fol.card_width) * area_comp;
108
- const base_l = Math.max(0.05, fol.card_length) * area_comp;
109
- const fold_angle = clamp(fol.fold_angle, 0.0, 0.65);
110
- const sin_fold = Math.sin(fold_angle);
111
- const cos_fold = Math.cos(fold_angle);
112
- const arch_h = fol.arch_height;
113
- const droop = fol.camber_droop;
114
- const color_var_amp = clamp(fol.color_variation ?? 0.08, 0.0, 0.35);
115
- const anchor_u = clamp(species.foliage_material?.stem_anchor_u ?? 0.5, 0.1, 0.9);
116
- const anchor_v = clamp(species.foliage_material?.stem_anchor_v ?? 0.0, 0.0, 0.25);
117
-
118
- let v_cursor = 0;
119
- let i_cursor = 0;
120
-
121
- /**
122
- * Helper to write a single foliage vertex in card-local coordinates `(x_w, y_u, z_f)`
123
- * and local normal `(nx_w, ny_u, nz_f)`.
124
- */
125
- const write_card_vertex = (
126
- px0, py0, pz0,
127
- wx, wy, wz,
128
- ux, uy, uz,
129
- fx, fy, fz,
130
- x_w, y_u, z_f,
131
- nx_w, ny_u, nz_f,
132
- u_tex, v_tex,
133
- flutter_w, phase_w,
134
- r_col, g_col, b_col,
135
- j0, j1, j2, j3,
136
- w0, w1, w2, w3
137
- ) => {
138
- const v3 = v_cursor * 3;
139
- const v2 = v_cursor * 2;
140
- const v4 = v_cursor * 4;
141
-
142
- positions[v3] = px0 + wx * x_w + ux * y_u + fx * z_f;
143
- positions[v3 + 1] = py0 + wy * x_w + uy * y_u + fy * z_f;
144
- positions[v3 + 2] = pz0 + wz * x_w + uz * y_u + fz * z_f;
145
-
146
- let gnx = wx * nx_w + ux * ny_u + fx * nz_f;
147
- let gny = wy * nx_w + uy * ny_u + fy * nz_f;
148
- let gnz = wz * nx_w + uz * ny_u + fz * nz_f;
149
- const nl = Math.sqrt(gnx * gnx + gny * gny + gnz * gnz);
150
- if (nl > 1e-8) {
151
- gnx /= nl;
152
- gny /= nl;
153
- gnz /= nl;
154
- }
155
- normals[v3] = gnx;
156
- normals[v3 + 1] = gny;
157
- normals[v3 + 2] = gnz;
158
-
159
- uv0[v2] = u_tex;
160
- uv0[v2 + 1] = v_tex;
161
-
162
- uv1[v2] = clamp(flutter_w, 0.0, 1.0);
163
- uv1[v2 + 1] = clamp(phase_w, 0.0, 1.0);
164
-
165
- colors[v3] = r_col;
166
- colors[v3 + 1] = g_col;
167
- colors[v3 + 2] = b_col;
168
-
169
- joints[v4] = j0;
170
- joints[v4 + 1] = j1;
171
- joints[v4 + 2] = j2;
172
- joints[v4 + 3] = j3;
173
-
174
- weights[v4] = w0;
175
- weights[v4 + 1] = w1;
176
- weights[v4 + 2] = w2;
177
- weights[v4 + 3] = w3;
178
-
179
- return v_cursor++;
180
- };
181
-
182
- const has_j4 = Boolean(attachments.joints4 && attachments.weights4 && attachments.joints4.length >= attachments.count * 4);
183
-
184
- for (let c = 0; c < card_count; c++) {
185
- const att_idx = active_indices[c];
186
- const a3 = att_idx * 3;
187
- const a2 = att_idx * 2;
188
- const a4 = att_idx * 4;
189
-
190
- const px0 = attachments.positions[a3];
191
- const py0 = attachments.positions[a3 + 1];
192
- const pz0 = attachments.positions[a3 + 2];
193
-
194
- const fx = attachments.forward[a3];
195
- const fy = attachments.forward[a3 + 1];
196
- const fz = attachments.forward[a3 + 2];
197
-
198
- const ux = attachments.up[a3];
199
- const uy = attachments.up[a3 + 1];
200
- const uz = attachments.up[a3 + 2];
201
-
202
- const wx = attachments.right[a3];
203
- const wy = attachments.right[a3 + 1];
204
- const wz = attachments.right[a3 + 2];
205
-
206
- const scale = attachments.scales[att_idx];
207
- const card_w = base_w * scale;
208
- const card_l = base_l * scale;
209
- const half_w = card_w * 0.5;
210
- const left_w = -card_w * anchor_u;
211
- const right_w = card_w * (1.0 - anchor_u);
212
-
213
- const phase = attachments.phases[att_idx];
214
- const b_sway = attachments.branch_sway[att_idx];
215
- const c_jit = attachments.color_jitter[att_idx] * color_var_amp;
216
-
217
- const r_col = clamp(1.0 + c_jit * 1.1, 0.7, 1.0);
218
- const g_col = clamp(1.0 + c_jit * 0.8, 0.7, 1.0);
219
- const b_col = clamp(1.0 - c_jit * 0.6, 0.7, 1.0);
220
-
221
- const j0 = has_j4 ? attachments.joints4[a4] : attachments.joints[a2];
222
- const j1 = has_j4 ? attachments.joints4[a4 + 1] : attachments.joints[a2 + 1];
223
- const j2 = has_j4 ? attachments.joints4[a4 + 2] : 0;
224
- const j3 = has_j4 ? attachments.joints4[a4 + 3] : 0;
225
- const w0 = has_j4 ? attachments.weights4[a4] : attachments.weights[a2];
226
- const w1 = has_j4 ? attachments.weights4[a4 + 1] : attachments.weights[a2 + 1];
227
- const w2 = has_j4 ? attachments.weights4[a4 + 2] : 0.0;
228
- const w3 = has_j4 ? attachments.weights4[a4 + 3] : 0.0;
229
-
230
- const base_v = v_cursor;
231
-
232
- if (card_type === FoliageCardType.CrossCard) {
233
- // Plane 0: horizontal (W, F) with normal +U (since W x F = +U)
234
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, left_w, 0, 0, 0, 1, 0, 0.0, anchor_v, b_sway * 0.4, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
235
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, right_w, 0, 0, 0, 1, 0, 1.0, anchor_v, b_sway * 0.4, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
236
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, left_w, 0, card_l, 0, 1, 0, 0.0, 1.0, 1.0, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
237
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, right_w, 0, card_l, 0, 1, 0, 1.0, 1.0, 1.0, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
238
-
239
- indices[i_cursor++] = base_v + 0;
240
- indices[i_cursor++] = base_v + 1;
241
- indices[i_cursor++] = base_v + 3;
242
- indices[i_cursor++] = base_v + 0;
243
- indices[i_cursor++] = base_v + 3;
244
- indices[i_cursor++] = base_v + 2;
245
-
246
- // Plane 1: vertical (-U to +U, F) with normal +W (since (-U) x F = +W)
247
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0, -left_w, 0, 1, 0, 0, 0.0, anchor_v, b_sway * 0.4, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
248
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0, -right_w, 0, 1, 0, 0, 1.0, anchor_v, b_sway * 0.4, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
249
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0, -left_w, card_l, 1, 0, 0, 0.0, 1.0, 1.0, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
250
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0, -right_w, card_l, 1, 0, 0, 1.0, 1.0, 1.0, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
251
-
252
- indices[i_cursor++] = base_v + 4;
253
- indices[i_cursor++] = base_v + 5;
254
- indices[i_cursor++] = base_v + 7;
255
- indices[i_cursor++] = base_v + 4;
256
- indices[i_cursor++] = base_v + 7;
257
- indices[i_cursor++] = base_v + 6;
258
- } else if (card_type === FoliageCardType.TriStarCluster) {
259
- for (let p = 0; p < 3; p++) {
260
- const ang = (p * Math.PI) / 3.0;
261
- const ca = Math.cos(ang);
262
- const sa = Math.sin(ang);
263
- // Width vector W_p = ca * W + sa * U; Normal vector N_p = W_p x F = -sa * W + ca * U
264
- const p_left_x = ca * left_w;
265
- const p_left_y = sa * left_w;
266
- const p_right_x = ca * right_w;
267
- const p_right_y = sa * right_w;
268
- const pn_x = -sa;
269
- const pn_y = ca;
270
- const p_base = base_v + p * 4;
271
-
272
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, p_left_x, p_left_y, 0, pn_x, pn_y, 0, 0.0, anchor_v, b_sway * 0.4, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
273
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, p_right_x, p_right_y, 0, pn_x, pn_y, 0, 1.0, anchor_v, b_sway * 0.4, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
274
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, p_left_x, p_left_y, card_l, pn_x, pn_y, 0, 0.0, 1.0, 1.0, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
275
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, p_right_x, p_right_y, card_l, pn_x, pn_y, 0, 1.0, 1.0, 1.0, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
276
-
277
- indices[i_cursor++] = p_base + 0;
278
- indices[i_cursor++] = p_base + 1;
279
- indices[i_cursor++] = p_base + 3;
280
- indices[i_cursor++] = p_base + 0;
281
- indices[i_cursor++] = p_base + 3;
282
- indices[i_cursor++] = p_base + 2;
283
- }
284
- } else if (card_type === FoliageCardType.RibbedFrond) {
285
- const lift_wing = half_w * sin_fold;
286
- const span_w = half_w * cos_fold;
287
-
288
- for (let s = 0; s <= frond_segs; s++) {
289
- const t = s / frond_segs;
290
- const v_tex = anchor_v + (1.0 - anchor_v) * t;
291
- const z_s = t * card_l;
292
- const y_spine = card_l * (arch_h * 4.0 * t * (1.0 - t) - droop * t * t);
293
- const env_w = Math.sin(Math.PI * (0.12 + 0.82 * t));
294
- const w_s = span_w * env_w;
295
- const lift_s = lift_wing * env_w;
296
- const flutter = clamp(b_sway * 0.3 + 0.7 * t, 0.0, 1.0);
297
-
298
- // Left wing, center midrib, right wing
299
- // Notice: for left wing (x from -w_s to 0, y from y_spine + lift_s to y_spine),
300
- // edge (+w_s, -lift_s, 0) x (0, 0, 1) has normal (+lift_s, +w_s, 0) -> (+sin_fold * 0.4, cos_fold, 0)
301
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, -w_s, y_spine + lift_s, z_s, -sin_fold * 0.35, cos_fold, 0, 0.0, v_tex, flutter, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
302
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0.0, y_spine, z_s, 0.0, 1.0, 0, anchor_u, v_tex, flutter * 0.75, phase, r_col * 0.94, g_col * 0.94, b_col * 0.94, j0, j1, j2, j3, w0, w1, w2, w3);
303
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, w_s, y_spine + lift_s, z_s, sin_fold * 0.35, cos_fold, 0, 1.0, v_tex, flutter, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
304
-
305
- if (s > 0) {
306
- const r0 = base_v + (s - 1) * 3;
307
- const r1 = base_v + s * 3;
308
- // Left strip
309
- indices[i_cursor++] = r0 + 0;
310
- indices[i_cursor++] = r0 + 1;
311
- indices[i_cursor++] = r1 + 1;
312
- indices[i_cursor++] = r0 + 0;
313
- indices[i_cursor++] = r1 + 1;
314
- indices[i_cursor++] = r1 + 0;
315
- // Right strip
316
- indices[i_cursor++] = r0 + 1;
317
- indices[i_cursor++] = r0 + 2;
318
- indices[i_cursor++] = r1 + 2;
319
- indices[i_cursor++] = r0 + 1;
320
- indices[i_cursor++] = r1 + 2;
321
- indices[i_cursor++] = r1 + 1;
322
- }
323
- }
324
- } else if (card_type === FoliageCardType.OctagonalCutoutHull) {
325
- const lift = half_w * sin_fold * 0.7;
326
- const span = half_w * cos_fold;
327
- const camber = card_l * arch_h;
328
-
329
- // v0: base petiole (0, 0, 0) at exact UV (anchor_u, anchor_v)
330
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0.0, 0.0, 0.0, 0, 1, 0, anchor_u, anchor_v, b_sway * 0.25, phase, r_col * 0.9, g_col * 0.9, b_col * 0.9, j0, j1, j2, j3, w0, w1, w2, w3);
331
- // v1: lower-left
332
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, -span * 0.85, lift * 0.7, card_l * 0.28, -sin_fold * 0.3, cos_fold, 0, 0.08, 0.28, b_sway * 0.55, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
333
- // v2: lower-right
334
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, span * 0.85, lift * 0.7, card_l * 0.28, sin_fold * 0.3, cos_fold, 0, 0.92, 0.28, b_sway * 0.55, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
335
- // v3: center midrib
336
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0.0, camber, card_l * 0.52, 0, 1, 0, anchor_u, 0.52, b_sway * 0.65, phase, r_col * 0.96, g_col * 0.96, b_col * 0.96, j0, j1, j2, j3, w0, w1, w2, w3);
337
- // v4: upper-left
338
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, -span, lift, card_l * 0.74, -sin_fold * 0.3, cos_fold, 0, 0.04, 0.74, 0.9, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
339
- // v5: upper-right
340
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, span, lift, card_l * 0.74, sin_fold * 0.3, cos_fold, 0, 0.96, 0.74, 0.9, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
341
- // v6: apex tip
342
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0.0, -card_l * droop * 0.5, card_l, 0, 1, 0, anchor_u, 0.98, 1.0, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
343
-
344
- // 6 outward (+U) triangles fanning around center midrib v3
345
- indices[i_cursor++] = base_v + 0;
346
- indices[i_cursor++] = base_v + 2;
347
- indices[i_cursor++] = base_v + 3;
348
-
349
- indices[i_cursor++] = base_v + 0;
350
- indices[i_cursor++] = base_v + 3;
351
- indices[i_cursor++] = base_v + 1;
352
-
353
- indices[i_cursor++] = base_v + 3;
354
- indices[i_cursor++] = base_v + 2;
355
- indices[i_cursor++] = base_v + 5;
356
-
357
- indices[i_cursor++] = base_v + 3;
358
- indices[i_cursor++] = base_v + 5;
359
- indices[i_cursor++] = base_v + 6;
360
-
361
- indices[i_cursor++] = base_v + 3;
362
- indices[i_cursor++] = base_v + 6;
363
- indices[i_cursor++] = base_v + 4;
364
-
365
- indices[i_cursor++] = base_v + 3;
366
- indices[i_cursor++] = base_v + 4;
367
- indices[i_cursor++] = base_v + 1;
368
- } else {
369
- // Default: FoldedVCard (6 vertices, 4 triangles)
370
- const lift = half_w * sin_fold;
371
- const span = half_w * cos_fold;
372
- const tip_droop = -card_l * droop * 0.5;
373
- const mid_arch = card_l * arch_h * 0.5;
374
-
375
- // Row 0 (base, z = 0)
376
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, -span, lift, 0.0, -sin_fold * 0.35, cos_fold, 0, 0.0, anchor_v, b_sway * 0.45, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
377
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0.0, 0.0, 0.0, 0.0, 1.0, 0, anchor_u, anchor_v, b_sway * 0.25, phase, r_col * 0.92, g_col * 0.92, b_col * 0.92, j0, j1, j2, j3, w0, w1, w2, w3);
378
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, span, lift, 0.0, sin_fold * 0.35, cos_fold, 0, 1.0, anchor_v, b_sway * 0.45, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
379
-
380
- // Row 1 (tip, z = card_l)
381
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, -span, lift + tip_droop, card_l, -sin_fold * 0.35, cos_fold, 0, 0.0, 1.0, 1.0, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
382
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0.0, mid_arch + tip_droop, card_l, 0.0, 1.0, 0, anchor_u, 1.0, 0.85, phase, r_col * 0.96, g_col * 0.96, b_col * 0.96, j0, j1, j2, j3, w0, w1, w2, w3);
383
- write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, span, lift + tip_droop, card_l, sin_fold * 0.35, cos_fold, 0, 1.0, 1.0, 1.0, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
384
-
385
- // Left wing quad
386
- indices[i_cursor++] = base_v + 0;
387
- indices[i_cursor++] = base_v + 1;
388
- indices[i_cursor++] = base_v + 4;
389
- indices[i_cursor++] = base_v + 0;
390
- indices[i_cursor++] = base_v + 4;
391
- indices[i_cursor++] = base_v + 3;
392
-
393
- // Right wing quad
394
- indices[i_cursor++] = base_v + 1;
395
- indices[i_cursor++] = base_v + 2;
396
- indices[i_cursor++] = base_v + 5;
397
- indices[i_cursor++] = base_v + 1;
398
- indices[i_cursor++] = base_v + 5;
399
- indices[i_cursor++] = base_v + 4;
400
- }
401
- }
402
-
403
- // Apply volumetric canopy AO and hemisphere-safe normal puffing BEFORE ensureTangents()
404
- tree_mesh_apply_canopy_ao(out, skeleton.crown_center, skeleton.crown_radii, {
405
- ao_core_darkening: fol.ao_core_darkening ?? 0.35,
406
- normal_puffiness: fol.normal_puffiness ?? 0.35,
407
- preserve_existing_color: true
408
- });
409
-
410
- out.name = `${species.name || "tree"}_foliage_lod${lod_cfg.level}`;
411
- out.ensureBounds();
412
- out.ensureTangents();
413
-
414
- return out;
415
- }
1
+ import { clamp } from "../../../core/math/clamp.js";
2
+ import { lerp } from "../../../core/math/lerp.js";
3
+ import { Geometry } from "../../../shade/renderer/geometry/Geometry.js";
4
+ import {
5
+ geometry_obtain_attribute,
6
+ geometry_obtain_index
7
+ } from "../../../shade/renderer/geometry/geometry_obtain_attribute.js";
8
+ import { StandardAttributes } from "../../../shade/renderer/geometry/StandardAttributes.js";
9
+ import { FoliageCardType } from "../config/FoliageCardType.js";
10
+ import { TreeLODConfig } from "../config/TreeLODConfig.js";
11
+ import { TreeSpeciesConfig } from "../config/TreeSpeciesConfig.js";
12
+ import { compute_foliage_variant_atlas_layout } from "../material/make_foliage_material.js";
13
+ import { tree_mesh_apply_canopy_ao } from "./tree_mesh_apply_canopy_ao.js";
14
+ import { geometry_obtain_joints_attribute } from "./tree_mesh_build_bark.js";
15
+
16
+ /**
17
+ * Synthesizes the foliage `Geometry` for a given LOD from a `FoliageAttachmentSet`,
18
+ * applying golden-ratio low-discrepancy LOD decimation ($h_i < k_{\text{keep}}$),
19
+ * optical area-compensation scaling ($s_{\text{comp}} = k_{\text{keep}}^{-\eta}$),
20
+ * botanical 3D card geometry (`FoldedVCard`, `CrossCard`, `TriStarCluster`, `RibbedFrond`, `OctagonalCutoutHull`),
21
+ * MaxRect-packed ontogenetic variant UV remapping, hemisphere-safe normal puffing,
22
+ * volumetric canopy AO, and 4-joint skeletal wind skinning.
23
+ *
24
+ * Returns `null` if foliage is disabled or `lod_config.foliage_keep_ratio <= 0` or `attachments.count === 0`.
25
+ *
26
+ * @param {import("../skeleton/TreeSkeleton.js").TreeSkeleton} skeleton
27
+ * @param {import("./foliage_collect_attachments.js").FoliageAttachmentSet} attachments
28
+ * @param {TreeSpeciesConfig} [config]
29
+ * @param {TreeLODConfig} [lod_config]
30
+ * @param {Geometry} [out]
31
+ * @returns {Geometry|null}
32
+ */
33
+ export function tree_mesh_build_foliage(
34
+ skeleton,
35
+ attachments,
36
+ config = new TreeSpeciesConfig(),
37
+ lod_config = new TreeLODConfig(),
38
+ out = new Geometry()
39
+ ) {
40
+ const species = config && config.isTreeSpeciesConfig ? config : TreeSpeciesConfig.from(config);
41
+ const lod_cfg = lod_config && lod_config.isTreeLODConfig ? lod_config : TreeLODConfig.from(lod_config);
42
+ const fol = species.foliage;
43
+ const keep_ratio = clamp(lod_cfg.foliage_keep_ratio ?? 1.0, 0.0, 1.0);
44
+ if (!fol.enabled || !attachments || attachments.count === 0 || keep_ratio <= 0.0) {
45
+ return null;
46
+ }
47
+
48
+ // Filter active attachment indices by golden-ratio rank
49
+ const active_indices = [];
50
+ for (let i = 0; i < attachments.count; i++) {
51
+ if (attachments.ranks[i] < keep_ratio) {
52
+ active_indices.push(i);
53
+ }
54
+ }
55
+ if (active_indices.length === 0 && attachments.count > 0) {
56
+ active_indices.push(0);
57
+ }
58
+ const card_count = active_indices.length;
59
+
60
+ // Area compensation scale: k_keep^(-eta)
61
+ const eta = clamp(lod_cfg.foliage_area_compensation_exponent ?? 0.42, 0.0, 0.6);
62
+ const area_comp = Math.pow(Math.max(0.08, keep_ratio), -eta);
63
+
64
+ // Determine effective card type for this LOD
65
+ let card_type = fol.card_type;
66
+ if (lod_cfg.simplify_cards) {
67
+ card_type = FoliageCardType.FoldedVCard;
68
+ }
69
+
70
+ const frond_segs = Math.max(2, fol.frond_segments | 0);
71
+ let verts_per_card = 6;
72
+ let tris_per_card = 4;
73
+
74
+ switch (card_type) {
75
+ case FoliageCardType.CrossCard:
76
+ verts_per_card = 8;
77
+ tris_per_card = 4;
78
+ break;
79
+ case FoliageCardType.TriStarCluster:
80
+ verts_per_card = 12;
81
+ tris_per_card = 6;
82
+ break;
83
+ case FoliageCardType.RibbedFrond:
84
+ verts_per_card = 3 * (frond_segs + 1);
85
+ tris_per_card = 4 * frond_segs;
86
+ break;
87
+ case FoliageCardType.OctagonalCutoutHull:
88
+ verts_per_card = 7;
89
+ tris_per_card = 6;
90
+ break;
91
+ case FoliageCardType.FoldedVCard:
92
+ default:
93
+ verts_per_card = 6;
94
+ tris_per_card = 4;
95
+ break;
96
+ }
97
+
98
+ const total_vertices = card_count * verts_per_card;
99
+ const total_indices = card_count * tris_per_card * 3;
100
+
101
+ const positions = geometry_obtain_attribute(out, StandardAttributes.Position, 3, total_vertices);
102
+ const normals = geometry_obtain_attribute(out, StandardAttributes.Normal, 3, total_vertices);
103
+ const uv0 = geometry_obtain_attribute(out, StandardAttributes.TextureCoordinates0, 2, total_vertices);
104
+ const uv1 = geometry_obtain_attribute(out, StandardAttributes.TextureCoordinates1, 2, total_vertices);
105
+ const colors = geometry_obtain_attribute(out, StandardAttributes.Color, 3, total_vertices);
106
+ const joints = geometry_obtain_joints_attribute(out, total_vertices);
107
+ const weights = geometry_obtain_attribute(out, StandardAttributes.SkinningWeights, 4, total_vertices);
108
+ const indices = geometry_obtain_index(out, total_indices);
109
+
110
+ const fol_mat = species.foliage_material;
111
+ const layout = compute_foliage_variant_atlas_layout(
112
+ fol_mat?.resolution ?? 128,
113
+ fol_mat?.variant_count ?? 4
114
+ );
115
+ const uv_rects = layout.uv_rects;
116
+ const variant_count = layout.variant_count;
117
+
118
+ const base_w = Math.max(0.05, fol.card_width) * area_comp;
119
+ const base_l = Math.max(0.05, fol.card_length) * area_comp;
120
+ const fold_angle = clamp(fol.fold_angle, 0.0, 0.65);
121
+ const sin_fold = Math.sin(fold_angle);
122
+ const cos_fold = Math.cos(fold_angle);
123
+ const arch_h = fol.arch_height;
124
+ const droop = fol.camber_droop;
125
+ const color_var_amp = clamp(fol.color_variation ?? 0.08, 0.0, 0.35);
126
+ const anchor_u = clamp(fol_mat?.stem_anchor_u ?? 0.5, 0.1, 0.9);
127
+ const anchor_v = clamp(fol_mat?.stem_anchor_v ?? 0.0, 0.0, 0.25);
128
+ const juv_shift = clamp(fol_mat?.juvenile_color_shift ?? 0.38, 0.0, 1.0);
129
+ const senescence = clamp(Math.max(fol_mat?.senescence ?? 0.0, fol_mat?.autumn_tint ?? 0.0), 0.0, 1.0);
130
+ const antho_pot = clamp(fol_mat?.anthocyanin_potential ?? 0.35, 0.0, 1.0);
131
+
132
+ let v_cursor = 0;
133
+ let i_cursor = 0;
134
+ let rect_u0 = 0.0;
135
+ let rect_v0 = 0.0;
136
+ let rect_u1 = 1.0;
137
+ let rect_v1 = 1.0;
138
+
139
+ /**
140
+ * Helper to write a single foliage vertex in card-local coordinates `(x_w, y_u, z_f)`
141
+ * and local normal `(nx_w, ny_u, nz_f)`, remapping local `[0, 1]^2` card UVs into the
142
+ * card's assigned MaxRect-packed atlas tile `[rect_u0, rect_v0, rect_u1, rect_v1]`.
143
+ */
144
+ const write_card_vertex = (
145
+ px0, py0, pz0,
146
+ wx, wy, wz,
147
+ ux, uy, uz,
148
+ fx, fy, fz,
149
+ x_w, y_u, z_f,
150
+ nx_w, ny_u, nz_f,
151
+ u_tex, v_tex,
152
+ flutter_w, phase_w,
153
+ r_col, g_col, b_col,
154
+ j0, j1, j2, j3,
155
+ w0, w1, w2, w3
156
+ ) => {
157
+ const v3 = v_cursor * 3;
158
+ const v2 = v_cursor * 2;
159
+ const v4 = v_cursor * 4;
160
+
161
+ positions[v3] = px0 + wx * x_w + ux * y_u + fx * z_f;
162
+ positions[v3 + 1] = py0 + wy * x_w + uy * y_u + fy * z_f;
163
+ positions[v3 + 2] = pz0 + wz * x_w + uz * y_u + fz * z_f;
164
+
165
+ let gnx = wx * nx_w + ux * ny_u + fx * nz_f;
166
+ let gny = wy * nx_w + uy * ny_u + fy * nz_f;
167
+ let gnz = wz * nx_w + uz * ny_u + fz * nz_f;
168
+ const nl = Math.sqrt(gnx * gnx + gny * gny + gnz * gnz);
169
+ if (nl > 1e-8) {
170
+ gnx /= nl;
171
+ gny /= nl;
172
+ gnz /= nl;
173
+ }
174
+ normals[v3] = gnx;
175
+ normals[v3 + 1] = gny;
176
+ normals[v3 + 2] = gnz;
177
+
178
+ uv0[v2] = lerp(rect_u0, rect_u1, u_tex);
179
+ uv0[v2 + 1] = lerp(rect_v0, rect_v1, v_tex);
180
+
181
+ uv1[v2] = clamp(flutter_w, 0.0, 1.0);
182
+ uv1[v2 + 1] = clamp(phase_w, 0.0, 1.0);
183
+
184
+ colors[v3] = r_col;
185
+ colors[v3 + 1] = g_col;
186
+ colors[v3 + 2] = b_col;
187
+
188
+ joints[v4] = j0;
189
+ joints[v4 + 1] = j1;
190
+ joints[v4 + 2] = j2;
191
+ joints[v4 + 3] = j3;
192
+
193
+ weights[v4] = w0;
194
+ weights[v4 + 1] = w1;
195
+ weights[v4 + 2] = w2;
196
+ weights[v4 + 3] = w3;
197
+
198
+ return v_cursor++;
199
+ };
200
+
201
+ const has_j4 = Boolean(attachments.joints4 && attachments.weights4 && attachments.joints4.length >= attachments.count * 4);
202
+ const has_variants = Boolean(attachments.variant_indices && attachments.variant_indices.length >= attachments.count);
203
+ const has_maturities = Boolean(attachments.maturities && attachments.maturities.length >= attachments.count);
204
+ const has_sun = Boolean(attachments.sun_exposure && attachments.sun_exposure.length >= attachments.count);
205
+
206
+ for (let c = 0; c < card_count; c++) {
207
+ const att_idx = active_indices[c];
208
+ const a3 = att_idx * 3;
209
+ const a2 = att_idx * 2;
210
+ const a4 = att_idx * 4;
211
+
212
+ const px0 = attachments.positions[a3];
213
+ const py0 = attachments.positions[a3 + 1];
214
+ const pz0 = attachments.positions[a3 + 2];
215
+
216
+ const fx = attachments.forward[a3];
217
+ const fy = attachments.forward[a3 + 1];
218
+ const fz = attachments.forward[a3 + 2];
219
+
220
+ const ux = attachments.up[a3];
221
+ const uy = attachments.up[a3 + 1];
222
+ const uz = attachments.up[a3 + 2];
223
+
224
+ const wx = attachments.right[a3];
225
+ const wy = attachments.right[a3 + 1];
226
+ const wz = attachments.right[a3 + 2];
227
+
228
+ const maturity = has_maturities ? attachments.maturities[att_idx] : 0.5;
229
+ const sun_exp = has_sun ? attachments.sun_exposure[att_idx] : 0.65;
230
+ const v_idx = has_variants
231
+ ? clamp(attachments.variant_indices[att_idx], 0, variant_count - 1)
232
+ : clamp(Math.floor(maturity * variant_count), 0, variant_count - 1);
233
+ const r4 = v_idx * 4;
234
+ rect_u0 = uv_rects[r4];
235
+ rect_v0 = uv_rects[r4 + 1];
236
+ rect_u1 = uv_rects[r4 + 2];
237
+ rect_v1 = uv_rects[r4 + 3];
238
+
239
+ const scale = attachments.scales[att_idx];
240
+ const card_w = base_w * scale;
241
+ const card_l = base_l * scale;
242
+ const half_w = card_w * 0.5;
243
+ const left_w = -card_w * anchor_u;
244
+ const right_w = card_w * (1.0 - anchor_u);
245
+
246
+ const phase = attachments.phases[att_idx];
247
+ const b_sway = attachments.branch_sway[att_idx];
248
+ const c_jit = attachments.color_jitter[att_idx] * color_var_amp;
249
+
250
+ // Continuous ontogenetic & crown-shell photoprotective modulation in vertex colors
251
+ const juv_lift = (0.5 - maturity) * juv_shift * 0.14;
252
+ let r_col = clamp(1.0 + c_jit * 1.1 + juv_lift * 0.9, 0.68, 1.0);
253
+ let g_col = clamp(1.0 + c_jit * 0.8 + juv_lift * 1.1, 0.68, 1.0);
254
+ let b_col = clamp(1.0 - c_jit * 0.6 - juv_lift * 0.5, 0.68, 1.0);
255
+
256
+ if (senescence > 1e-4) {
257
+ // Sun-exposed outer crown shell expresses stronger anthocyanin/carotenoid warmth than shaded interior
258
+ const shade_cool = (1.0 - sun_exp) * senescence * 0.22;
259
+ const sun_antho = sun_exp * senescence * antho_pot * 0.25;
260
+ r_col = clamp(r_col * (1.0 - shade_cool * 0.45 + sun_antho * 0.18), 0.65, 1.0);
261
+ g_col = clamp(g_col * (1.0 + shade_cool * 0.15 - sun_antho * 0.35), 0.55, 1.0);
262
+ b_col = clamp(b_col * (1.0 - sun_antho * 0.30), 0.55, 1.0);
263
+ }
264
+
265
+ const j0 = has_j4 ? attachments.joints4[a4] : attachments.joints[a2];
266
+ const j1 = has_j4 ? attachments.joints4[a4 + 1] : attachments.joints[a2 + 1];
267
+ const j2 = has_j4 ? attachments.joints4[a4 + 2] : 0;
268
+ const j3 = has_j4 ? attachments.joints4[a4 + 3] : 0;
269
+ const w0 = has_j4 ? attachments.weights4[a4] : attachments.weights[a2];
270
+ const w1 = has_j4 ? attachments.weights4[a4 + 1] : attachments.weights[a2 + 1];
271
+ const w2 = has_j4 ? attachments.weights4[a4 + 2] : 0.0;
272
+ const w3 = has_j4 ? attachments.weights4[a4 + 3] : 0.0;
273
+
274
+ const base_v = v_cursor;
275
+
276
+ if (card_type === FoliageCardType.CrossCard) {
277
+ // Plane 0: horizontal (W, F) with normal +U (since W x F = +U)
278
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, left_w, 0, 0, 0, 1, 0, 0.0, anchor_v, b_sway * 0.4, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
279
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, right_w, 0, 0, 0, 1, 0, 1.0, anchor_v, b_sway * 0.4, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
280
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, left_w, 0, card_l, 0, 1, 0, 0.0, 1.0, 1.0, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
281
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, right_w, 0, card_l, 0, 1, 0, 1.0, 1.0, 1.0, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
282
+
283
+ indices[i_cursor++] = base_v + 0;
284
+ indices[i_cursor++] = base_v + 1;
285
+ indices[i_cursor++] = base_v + 3;
286
+ indices[i_cursor++] = base_v + 0;
287
+ indices[i_cursor++] = base_v + 3;
288
+ indices[i_cursor++] = base_v + 2;
289
+
290
+ // Plane 1: vertical (-U to +U, F) with normal +W (since (-U) x F = +W)
291
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0, -left_w, 0, 1, 0, 0, 0.0, anchor_v, b_sway * 0.4, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
292
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0, -right_w, 0, 1, 0, 0, 1.0, anchor_v, b_sway * 0.4, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
293
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0, -left_w, card_l, 1, 0, 0, 0.0, 1.0, 1.0, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
294
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0, -right_w, card_l, 1, 0, 0, 1.0, 1.0, 1.0, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
295
+
296
+ indices[i_cursor++] = base_v + 4;
297
+ indices[i_cursor++] = base_v + 5;
298
+ indices[i_cursor++] = base_v + 7;
299
+ indices[i_cursor++] = base_v + 4;
300
+ indices[i_cursor++] = base_v + 7;
301
+ indices[i_cursor++] = base_v + 6;
302
+ } else if (card_type === FoliageCardType.TriStarCluster) {
303
+ for (let p = 0; p < 3; p++) {
304
+ const ang = (p * Math.PI) / 3.0;
305
+ const ca = Math.cos(ang);
306
+ const sa = Math.sin(ang);
307
+ const p_left_x = ca * left_w;
308
+ const p_left_y = sa * left_w;
309
+ const p_right_x = ca * right_w;
310
+ const p_right_y = sa * right_w;
311
+ const pn_x = -sa;
312
+ const pn_y = ca;
313
+ const p_base = base_v + p * 4;
314
+
315
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, p_left_x, p_left_y, 0, pn_x, pn_y, 0, 0.0, anchor_v, b_sway * 0.4, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
316
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, p_right_x, p_right_y, 0, pn_x, pn_y, 0, 1.0, anchor_v, b_sway * 0.4, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
317
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, p_left_x, p_left_y, card_l, pn_x, pn_y, 0, 0.0, 1.0, 1.0, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
318
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, p_right_x, p_right_y, card_l, pn_x, pn_y, 0, 1.0, 1.0, 1.0, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
319
+
320
+ indices[i_cursor++] = p_base + 0;
321
+ indices[i_cursor++] = p_base + 1;
322
+ indices[i_cursor++] = p_base + 3;
323
+ indices[i_cursor++] = p_base + 0;
324
+ indices[i_cursor++] = p_base + 3;
325
+ indices[i_cursor++] = p_base + 2;
326
+ }
327
+ } else if (card_type === FoliageCardType.RibbedFrond) {
328
+ const lift_wing = half_w * sin_fold;
329
+ const span_w = half_w * cos_fold;
330
+
331
+ for (let s = 0; s <= frond_segs; s++) {
332
+ const t = s / frond_segs;
333
+ const v_tex = anchor_v + (1.0 - anchor_v) * t;
334
+ const z_s = t * card_l;
335
+ const y_spine = card_l * (arch_h * 4.0 * t * (1.0 - t) - droop * t * t);
336
+ const env_w = Math.sin(Math.PI * (0.12 + 0.82 * t));
337
+ const w_s = span_w * env_w;
338
+ const lift_s = lift_wing * env_w;
339
+ const flutter = clamp(b_sway * 0.3 + 0.7 * t, 0.0, 1.0);
340
+
341
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, -w_s, y_spine + lift_s, z_s, -sin_fold * 0.35, cos_fold, 0, 0.0, v_tex, flutter, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
342
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0.0, y_spine, z_s, 0.0, 1.0, 0, anchor_u, v_tex, flutter * 0.75, phase, r_col * 0.94, g_col * 0.94, b_col * 0.94, j0, j1, j2, j3, w0, w1, w2, w3);
343
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, w_s, y_spine + lift_s, z_s, sin_fold * 0.35, cos_fold, 0, 1.0, v_tex, flutter, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
344
+
345
+ if (s > 0) {
346
+ const r0 = base_v + (s - 1) * 3;
347
+ const r1 = base_v + s * 3;
348
+ indices[i_cursor++] = r0 + 0;
349
+ indices[i_cursor++] = r0 + 1;
350
+ indices[i_cursor++] = r1 + 1;
351
+ indices[i_cursor++] = r0 + 0;
352
+ indices[i_cursor++] = r1 + 1;
353
+ indices[i_cursor++] = r1 + 0;
354
+
355
+ indices[i_cursor++] = r0 + 1;
356
+ indices[i_cursor++] = r0 + 2;
357
+ indices[i_cursor++] = r1 + 2;
358
+ indices[i_cursor++] = r0 + 1;
359
+ indices[i_cursor++] = r1 + 2;
360
+ indices[i_cursor++] = r1 + 1;
361
+ }
362
+ }
363
+ } else if (card_type === FoliageCardType.OctagonalCutoutHull) {
364
+ const lift = half_w * sin_fold * 0.7;
365
+ const span = half_w * cos_fold;
366
+ const camber = card_l * arch_h;
367
+
368
+ // v0: base petiole (0, 0, 0) at exact UV (anchor_u, anchor_v)
369
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0.0, 0.0, 0.0, 0, 1, 0, anchor_u, anchor_v, b_sway * 0.25, phase, r_col * 0.9, g_col * 0.9, b_col * 0.9, j0, j1, j2, j3, w0, w1, w2, w3);
370
+ // v1: lower-left
371
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, -span * 0.85, lift * 0.7, card_l * 0.28, -sin_fold * 0.3, cos_fold, 0, 0.08, 0.28, b_sway * 0.55, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
372
+ // v2: lower-right
373
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, span * 0.85, lift * 0.7, card_l * 0.28, sin_fold * 0.3, cos_fold, 0, 0.92, 0.28, b_sway * 0.55, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
374
+ // v3: center midrib
375
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0.0, camber, card_l * 0.52, 0, 1, 0, anchor_u, 0.52, b_sway * 0.65, phase, r_col * 0.96, g_col * 0.96, b_col * 0.96, j0, j1, j2, j3, w0, w1, w2, w3);
376
+ // v4: upper-left
377
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, -span, lift, card_l * 0.74, -sin_fold * 0.3, cos_fold, 0, 0.04, 0.74, 0.9, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
378
+ // v5: upper-right
379
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, span, lift, card_l * 0.74, sin_fold * 0.3, cos_fold, 0, 0.96, 0.74, 0.9, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
380
+ // v6: apex tip
381
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0.0, -card_l * droop * 0.5, card_l, 0, 1, 0, anchor_u, 0.98, 1.0, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
382
+
383
+ // 6 outward (+U) triangles fanning around center midrib v3
384
+ indices[i_cursor++] = base_v + 0;
385
+ indices[i_cursor++] = base_v + 2;
386
+ indices[i_cursor++] = base_v + 3;
387
+
388
+ indices[i_cursor++] = base_v + 0;
389
+ indices[i_cursor++] = base_v + 3;
390
+ indices[i_cursor++] = base_v + 1;
391
+
392
+ indices[i_cursor++] = base_v + 3;
393
+ indices[i_cursor++] = base_v + 2;
394
+ indices[i_cursor++] = base_v + 5;
395
+
396
+ indices[i_cursor++] = base_v + 3;
397
+ indices[i_cursor++] = base_v + 5;
398
+ indices[i_cursor++] = base_v + 6;
399
+
400
+ indices[i_cursor++] = base_v + 3;
401
+ indices[i_cursor++] = base_v + 6;
402
+ indices[i_cursor++] = base_v + 4;
403
+
404
+ indices[i_cursor++] = base_v + 3;
405
+ indices[i_cursor++] = base_v + 4;
406
+ indices[i_cursor++] = base_v + 1;
407
+ } else {
408
+ // Default: FoldedVCard (6 vertices, 4 triangles)
409
+ const lift = half_w * sin_fold;
410
+ const span = half_w * cos_fold;
411
+ const tip_droop = -card_l * droop * 0.5;
412
+ const mid_arch = card_l * arch_h * 0.5;
413
+
414
+ // Row 0 (base, z = 0)
415
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, -span, lift, 0.0, -sin_fold * 0.35, cos_fold, 0, 0.0, anchor_v, b_sway * 0.45, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
416
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0.0, 0.0, 0.0, 0.0, 1.0, 0, anchor_u, anchor_v, b_sway * 0.25, phase, r_col * 0.92, g_col * 0.92, b_col * 0.92, j0, j1, j2, j3, w0, w1, w2, w3);
417
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, span, lift, 0.0, sin_fold * 0.35, cos_fold, 0, 1.0, anchor_v, b_sway * 0.45, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
418
+
419
+ // Row 1 (tip, z = card_l)
420
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, -span, lift + tip_droop, card_l, -sin_fold * 0.35, cos_fold, 0, 0.0, 1.0, 1.0, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
421
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, 0.0, mid_arch + tip_droop, card_l, 0.0, 1.0, 0, anchor_u, 1.0, 0.85, phase, r_col * 0.96, g_col * 0.96, b_col * 0.96, j0, j1, j2, j3, w0, w1, w2, w3);
422
+ write_card_vertex(px0, py0, pz0, wx, wy, wz, ux, uy, uz, fx, fy, fz, span, lift + tip_droop, card_l, sin_fold * 0.35, cos_fold, 0, 1.0, 1.0, 1.0, phase, r_col, g_col, b_col, j0, j1, j2, j3, w0, w1, w2, w3);
423
+
424
+ // Left wing quad
425
+ indices[i_cursor++] = base_v + 0;
426
+ indices[i_cursor++] = base_v + 1;
427
+ indices[i_cursor++] = base_v + 4;
428
+ indices[i_cursor++] = base_v + 0;
429
+ indices[i_cursor++] = base_v + 4;
430
+ indices[i_cursor++] = base_v + 3;
431
+
432
+ // Right wing quad
433
+ indices[i_cursor++] = base_v + 1;
434
+ indices[i_cursor++] = base_v + 2;
435
+ indices[i_cursor++] = base_v + 5;
436
+ indices[i_cursor++] = base_v + 1;
437
+ indices[i_cursor++] = base_v + 5;
438
+ indices[i_cursor++] = base_v + 4;
439
+ }
440
+ }
441
+
442
+ // Apply volumetric canopy AO and hemisphere-safe normal puffing BEFORE ensureTangents()
443
+ tree_mesh_apply_canopy_ao(out, skeleton.crown_center, skeleton.crown_radii, {
444
+ ao_core_darkening: fol.ao_core_darkening ?? 0.35,
445
+ normal_puffiness: fol.normal_puffiness ?? 0.35,
446
+ preserve_existing_color: true
447
+ });
448
+
449
+ out.name = `${species.name || "tree"}_foliage_lod${lod_cfg.level}`;
450
+ out.ensureBounds();
451
+ out.ensureTangents();
452
+
453
+ return out;
454
+ }