@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,502 +1,569 @@
1
- import { GOLDEN_RATIO } from "../../../core/math/GOLDEN_RATIO.js";
2
- import { PI2 } from "../../../core/math/PI2.js";
3
- import { clamp } from "../../../core/math/clamp.js";
4
- import { lerp } from "../../../core/math/lerp.js";
5
- import { seededRandom } from "../../../core/math/random/seededRandom.js";
6
- import { t64_look_rotation } from "../../../engine/ecs/transform/t64_look_rotation.js";
7
- import { Transform64 } from "../../../engine/ecs/transform/Transform64.js";
8
- import { Attribute } from "../../../shade/renderer/geometry/Attribute.js";
9
- import { PhyllotaxisMode } from "../config/PhyllotaxisMode.js";
10
- import { TreeSpeciesConfig } from "../config/TreeSpeciesConfig.js";
11
- import { spine_frame_double_reflection } from "../skeleton/spine_frame_double_reflection.js";
12
- import { tree_mesh_compute_branch_rings } from "./tree_mesh_build_bark.js";
13
-
14
- const GOLDEN_CONJUGATE = 1.0 / GOLDEN_RATIO; // ~0.6180339887498949
15
- const GOLDEN_ANGLE = PI2 / (GOLDEN_RATIO * GOLDEN_RATIO);
16
-
17
- const SCRATCH_POS = new Float32Array(3);
18
- const SCRATCH_TAN = new Float32Array(3);
19
- const SCRATCH_P0 = new Float32Array(3);
20
- const SCRATCH_T0 = new Float32Array(3);
21
- const SCRATCH_R0 = new Float32Array(3);
22
- const SCRATCH_RMF = new Float64Array(6);
23
-
24
- /**
25
- * Flat typed-array structure holding all phyllotactic foliage attachment points and oriented card frames.
26
- *
27
- * @typedef {Object} FoliageAttachmentSet
28
- * @property {number} count number of foliage attachments
29
- * @property {Float32Array} positions `count * 3` attachment world/local positions
30
- * @property {Float32Array} forward `count * 3` unit card length axis F
31
- * @property {Float32Array} up `count * 3` unit card outward normal U
32
- * @property {Float32Array} right `count * 3` unit card width axis W = F x U (so W x F = +U)
33
- * @property {Float32Array} scales `count` per-card size multiplier
34
- * @property {Float32Array} ranks `count` golden-ratio low-discrepancy decimation rank in [0, 1)
35
- * @property {Float32Array} phases `count` per-card wind phase in [0, 1)
36
- * @property {Float32Array} branch_sway `count` parent branch sway weight in [0, 1]
37
- * @property {Float32Array} color_jitter `count` per-card luminance/tint jitter in [-1, 1]
38
- * @property {Uint16Array} joints `count * 2` `[j_primary, j_parent]`
39
- * @property {Float32Array} weights `count * 2` `[w_primary, w_parent]`
40
- */
41
-
42
- /**
43
- * Generates phyllotactic foliage card attachment transforms along all eligible branches of a `TreeSkeleton`.
44
- *
45
- * @param {import("../skeleton/TreeSkeleton.js").TreeSkeleton} skeleton
46
- * @param {TreeSpeciesConfig} [config]
47
- * @returns {FoliageAttachmentSet}
48
- */
49
- export function foliage_collect_attachments(skeleton, config = new TreeSpeciesConfig()) {
50
- const species = config && config.isTreeSpeciesConfig ? config : TreeSpeciesConfig.from(config);
51
- const fol = species.foliage;
52
- if (!fol.enabled || skeleton.branch_count === 0) {
53
- return {
54
- count: 0,
55
- transforms: [],
56
- positions: new Float32Array(0),
57
- forward: new Float32Array(0),
58
- up: new Float32Array(0),
59
- right: new Float32Array(0),
60
- scales: new Float32Array(0),
61
- ranks: new Float32Array(0),
62
- phases: new Float32Array(0),
63
- branch_sway: new Float32Array(0),
64
- color_jitter: new Float32Array(0),
65
- joints: new Uint16Array(0),
66
- weights: new Float32Array(0),
67
- joints4: new Uint16Array(0),
68
- weights4: new Float32Array(0),
69
- attr_ranks: Attribute.from(new Float32Array(0), 1, "foliage_ranks"),
70
- attr_phases: Attribute.from(new Float32Array(0), 1, "foliage_phases"),
71
- attr_branch_sway: Attribute.from(new Float32Array(0), 1, "foliage_branch_sway"),
72
- attr_color_jitter: Attribute.from(new Float32Array(0), 1, "foliage_color_jitter"),
73
- attr_joints: Attribute.from(new Uint16Array(0), 2, "foliage_joints"),
74
- attr_weights: Attribute.from(new Float32Array(0), 2, "foliage_weights")
75
- };
76
- }
77
-
78
- const rng = seededRandom((species.seed ^ 0x5A3C9E17) | 0);
79
- const max_existing_level = Math.max(0, species.levels.length - 1);
80
- const min_level = Math.min(fol.attach_min_level, max_existing_level);
81
- const branch_rings = tree_mesh_compute_branch_rings(skeleton, species, species.lods?.[0]);
82
-
83
- const cx = skeleton.crown_center[0];
84
- const cy = skeleton.crown_center[1];
85
- const cz = skeleton.crown_center[2];
86
- const ry = Math.max(0.5, skeleton.crown_radii[1]);
87
-
88
- // First collect eligible branch edge chains via `skeleton.graph`
89
- const eligible_branches = [];
90
- for (let b = 0; b < skeleton.branch_count; b++) {
91
- const level = skeleton.branch_level[b];
92
- if (level < min_level) continue;
93
-
94
- const nodes = [];
95
- const edges = [];
96
- skeleton.collect_branch_chain(b, nodes, edges);
97
- if (edges.length === 0) continue;
98
-
99
- const t_norms = new Array(nodes.length);
100
- for (let i = 0; i < nodes.length; i++) {
101
- t_norms[i] = skeleton.node_t_norm(nodes[i]);
102
- }
103
-
104
- eligible_branches.push({ branch_id: b, level, nodes, edges, t_norms });
105
- }
106
-
107
- const lateral_count = Math.max(0, fol.cards_per_branch | 0);
108
- const rosette_count = Math.max(0, fol.terminal_rosette_count | 0);
109
- const per_branch = lateral_count + rosette_count;
110
- const total_count = eligible_branches.length * per_branch;
111
-
112
- /** @type {Transform64[]} */
113
- const transforms = new Array(total_count);
114
- const positions = new Float32Array(total_count * 3);
115
- const forward = new Float32Array(total_count * 3);
116
- const up = new Float32Array(total_count * 3);
117
- const right = new Float32Array(total_count * 3);
118
- const scales = new Float32Array(total_count);
119
- const ranks = new Float32Array(total_count);
120
- const phases = new Float32Array(total_count);
121
- const branch_sway = new Float32Array(total_count);
122
- const color_jitter = new Float32Array(total_count);
123
- const joints = new Uint16Array(total_count * 2);
124
- const weights = new Float32Array(total_count * 2);
125
- const joints4 = new Uint16Array(total_count * 4);
126
- const weights4 = new Float32Array(total_count * 4);
127
-
128
- let cursor = 0;
129
- const bare = clamp(fol.bare_ratio, 0.0, 0.92);
130
-
131
- for (let eb = 0; eb < eligible_branches.length; eb++) {
132
- const item = eligible_branches[eb];
133
- const level = item.level;
134
- const nodes = item.nodes;
135
- const edges = item.edges;
136
- const t_norms = item.t_norms;
137
- const b_rings = branch_rings.get(item.branch_id);
138
- const t_min = Math.max(bare, t_norms[0]);
139
- const t_max = t_norms[t_norms.length - 1];
140
- const base_phi = rng() * PI2;
141
- const b_phase = skeleton.branch_phase_hash[item.branch_id];
142
-
143
- for (let k = 0; k < per_branch; k++) {
144
- const is_rosette = k >= lateral_count;
145
- let t_attach = 1.0;
146
-
147
- if (!is_rosette && lateral_count > 0) {
148
- let u_k = (k + 0.5) / lateral_count;
149
- if (fol.phyllotaxis_mode === PhyllotaxisMode.Opposite
150
- || fol.phyllotaxis_mode === PhyllotaxisMode.OppositeDecussate) {
151
- const pair_idx = k >> 1;
152
- const pair_total = Math.max(1, Math.ceil(lateral_count / 2));
153
- u_k = (pair_idx + 0.5) / pair_total;
154
- } else if (fol.phyllotaxis_mode === PhyllotaxisMode.Whorled) {
155
- const w_size = Math.max(1, fol.whorl_count | 0);
156
- const w_idx = Math.floor(k / w_size);
157
- const w_total = Math.max(1, Math.ceil(lateral_count / w_size));
158
- u_k = (w_idx + 0.5) / w_total;
159
- }
160
- t_attach = clamp(lerp(t_min, t_max * 0.92, u_k), t_min, t_max * 0.95);
161
- } else {
162
- const ros_idx = k - lateral_count;
163
- const u_ros = (ros_idx + 0.5) / Math.max(1, rosette_count);
164
- t_attach = clamp(lerp(Math.max(t_min, t_max * 0.88), t_max * 0.96, u_ros), t_min, t_max * 0.96);
165
- }
166
-
167
- // Locate segment containing t_attach
168
- let seg_idx = edges.length - 1;
169
- for (let s = 0; s < edges.length; s++) {
170
- if (t_attach >= t_norms[s] && t_attach <= t_norms[s + 1]) {
171
- seg_idx = s;
172
- break;
173
- }
174
- }
175
-
176
- const edge_id = edges[seg_idx];
177
- const v0 = nodes[seg_idx];
178
- const v1 = nodes[seg_idx + 1];
179
- const t0 = t_norms[seg_idx];
180
- const t1 = t_norms[seg_idx + 1];
181
- const local_u = clamp((t_attach - t0) / Math.max(1e-5, t1 - t0), 0.05, 0.95);
182
-
183
- skeleton.evaluate_segment_position(SCRATCH_POS, 0, edge_id, local_u);
184
- skeleton.evaluate_segment_tangent(SCRATCH_TAN, 0, edge_id, local_u);
185
- skeleton.node_read_position(SCRATCH_P0, 0, v0);
186
- skeleton.node_read_tangent(SCRATCH_T0, 0, v0);
187
- skeleton.node_read_frame_r(SCRATCH_R0, 0, v0);
188
-
189
- spine_frame_double_reflection(
190
- SCRATCH_RMF, 0,
191
- SCRATCH_P0[0], SCRATCH_P0[1], SCRATCH_P0[2],
192
- SCRATCH_T0[0], SCRATCH_T0[1], SCRATCH_T0[2],
193
- SCRATCH_R0[0], SCRATCH_R0[1], SCRATCH_R0[2],
194
- SCRATCH_POS[0], SCRATCH_POS[1], SCRATCH_POS[2],
195
- SCRATCH_TAN[0], SCRATCH_TAN[1], SCRATCH_TAN[2]
196
- );
197
- const rx = SCRATCH_RMF[0], ry_f = SCRATCH_RMF[1], rz = SCRATCH_RMF[2];
198
- const sx = SCRATCH_RMF[3], sy_f = SCRATCH_RMF[4], sz = SCRATCH_RMF[5];
199
-
200
- // Phyllotactic angle phi
201
- let phi = base_phi;
202
- if (is_rosette) {
203
- const r_idx = k - lateral_count;
204
- phi += (r_idx * PI2) / Math.max(1, rosette_count);
205
- } else {
206
- switch (fol.phyllotaxis_mode) {
207
- case PhyllotaxisMode.AlternateDistichous:
208
- phi += k * Math.PI;
209
- break;
210
- case PhyllotaxisMode.Tristichous:
211
- phi += k * (PI2 / 3.0);
212
- break;
213
- case PhyllotaxisMode.Opposite:
214
- phi += (k & 1) * Math.PI;
215
- break;
216
- case PhyllotaxisMode.OppositeDecussate:
217
- phi += (k & 1) * Math.PI + (k >> 1) * (Math.PI * 0.5);
218
- break;
219
- case PhyllotaxisMode.Whorled: {
220
- const w_size = Math.max(1, fol.whorl_count | 0);
221
- const w_idx = Math.floor(k / w_size);
222
- const sub_idx = k % w_size;
223
- phi += (sub_idx * PI2) / w_size + (w_idx * Math.PI) / w_size;
224
- break;
225
- }
226
- case PhyllotaxisMode.SpiralGolden:
227
- default:
228
- phi += k * GOLDEN_ANGLE;
229
- break;
230
- }
231
- }
232
- phi += (rng() * 2.0 - 1.0) * fol.angular_jitter;
233
-
234
- const cos_p = Math.cos(phi);
235
- const sin_p = Math.sin(phi);
236
- const drx = cos_p * rx + sin_p * ry_f * 0 + sin_p * sx;
237
- const dry = cos_p * ry_f + sin_p * sy_f;
238
- const drz = cos_p * rz + sin_p * sz;
239
-
240
- // Anchor card stem origin on the actual meshed bark frustum chord [ra, rb]
241
- // and interpolate the carrier frustum's exact 4-slot skinning weights
242
- let px = SCRATCH_POS[0];
243
- let py = SCRATCH_POS[1];
244
- let pz = SCRATCH_POS[2];
245
- let seg_tx = SCRATCH_TAN[0];
246
- let seg_ty = SCRATCH_TAN[1];
247
- let seg_tz = SCRATCH_TAN[2];
248
-
249
- const c4 = cursor * 4;
250
- const c2 = cursor * 2;
251
-
252
- if (b_rings && b_rings.length >= 2) {
253
- const ring_idx = Math.min(seg_idx, b_rings.length - 2);
254
- const ra = b_rings[ring_idx];
255
- const rb = b_rings[ring_idx + 1];
256
- const xi = clamp((t_attach - ra.t_norm) / Math.max(1e-5, rb.t_norm - ra.t_norm), 0.05, 0.95);
257
-
258
- px = lerp(ra.cx, rb.cx, xi);
259
- py = lerp(ra.cy, rb.cy, xi);
260
- pz = lerp(ra.cz, rb.cz, xi);
261
-
262
- const dx_seg = rb.cx - ra.cx;
263
- const dy_seg = rb.cy - ra.cy;
264
- const dz_seg = rb.cz - ra.cz;
265
- const seg_len = Math.hypot(dx_seg, dy_seg, dz_seg);
266
- if (seg_len > 1e-6) {
267
- seg_tx = dx_seg / seg_len;
268
- seg_ty = dy_seg / seg_len;
269
- seg_tz = dz_seg / seg_len;
270
- }
271
-
272
- // Merge 4-slot skinning weights from ra (weight 1 - xi) and rb (weight xi)
273
- const merged_j = [-1, -1, -1, -1, -1, -1, -1, -1];
274
- const merged_w = [0, 0, 0, 0, 0, 0, 0, 0];
275
- let m_count = 0;
276
-
277
- const accumulate_joint = (j_id, w_val) => {
278
- if (w_val <= 1e-7) return;
279
- for (let m = 0; m < m_count; m++) {
280
- if (merged_j[m] === j_id) {
281
- merged_w[m] += w_val;
282
- return;
283
- }
284
- }
285
- merged_j[m_count] = j_id;
286
- merged_w[m_count] = w_val;
287
- m_count++;
288
- };
289
-
290
- const w_a = 1.0 - xi;
291
- const w_b = xi;
292
- accumulate_joint(ra.j0, ra.w0 * w_a);
293
- accumulate_joint(ra.j1, ra.w1 * w_a);
294
- accumulate_joint(ra.j2, ra.w2 * w_a);
295
- accumulate_joint(ra.j3, ra.w3 * w_a);
296
- accumulate_joint(rb.j0, rb.w0 * w_b);
297
- accumulate_joint(rb.j1, rb.w1 * w_b);
298
- accumulate_joint(rb.j2, rb.w2 * w_b);
299
- accumulate_joint(rb.j3, rb.w3 * w_b);
300
-
301
- // Sort descending by weight and normalize top 4
302
- for (let a = 0; a < m_count - 1; a++) {
303
- for (let b_sort = a + 1; b_sort < m_count; b_sort++) {
304
- if (merged_w[b_sort] > merged_w[a]) {
305
- const tw = merged_w[a];
306
- merged_w[a] = merged_w[b_sort];
307
- merged_w[b_sort] = tw;
308
- const tj = merged_j[a];
309
- merged_j[a] = merged_j[b_sort];
310
- merged_j[b_sort] = tj;
311
- }
312
- }
313
- }
314
-
315
- const top_k = Math.min(4, m_count);
316
- let w4_sum = 0.0;
317
- for (let s = 0; s < top_k; s++) w4_sum += merged_w[s];
318
- const inv_w4 = w4_sum > 1e-7 ? 1.0 / w4_sum : 1.0;
319
-
320
- for (let s = 0; s < 4; s++) {
321
- if (s < top_k) {
322
- joints4[c4 + s] = merged_j[s];
323
- weights4[c4 + s] = merged_w[s] * inv_w4;
324
- } else {
325
- joints4[c4 + s] = 0;
326
- weights4[c4 + s] = 0.0;
327
- }
328
- }
329
-
330
- // Populate 2-slot legacy joints/weights normalized to sum to 1.0
331
- const j_prim = top_k > 0 ? merged_j[0] : 0;
332
- const j_par = top_k > 1 ? merged_j[1] : j_prim;
333
- const w0_raw = top_k > 0 ? merged_w[0] : 1.0;
334
- const w1_raw = top_k > 1 ? merged_w[1] : 0.0;
335
- const w2_sum = w0_raw + w1_raw;
336
- joints[c2] = j_prim;
337
- joints[c2 + 1] = j_par;
338
- weights[c2] = w2_sum > 1e-7 ? w0_raw / w2_sum : 1.0;
339
- weights[c2 + 1] = w2_sum > 1e-7 ? w1_raw / w2_sum : 0.0;
340
- } else {
341
- const j_prim = skeleton.node_read_joint(v1, 0);
342
- const j_par = skeleton.node_read_joint(v1, 1);
343
- const w_prim = j_prim === j_par ? 1.0 : clamp(0.35 + 0.65 * t_attach, 0.0, 1.0);
344
- joints[c2] = j_prim;
345
- joints[c2 + 1] = j_par;
346
- weights[c2] = w_prim;
347
- weights[c2 + 1] = 1.0 - w_prim;
348
- joints4[c4] = j_prim;
349
- joints4[c4 + 1] = j_par;
350
- weights4[c4] = w_prim;
351
- weights4[c4 + 1] = 1.0 - w_prim;
352
- }
353
-
354
- // Outward canopy normal n_canopy from crown center
355
- let ncx = px - cx;
356
- let ncy = (py - cy) + 0.35 * ry;
357
- let ncz = pz - cz;
358
- const nc_len = Math.sqrt(ncx * ncx + ncy * ncy + ncz * ncz);
359
- if (nc_len > 1e-6) {
360
- ncx /= nc_len;
361
- ncy /= nc_len;
362
- ncz /= nc_len;
363
- } else {
364
- ncx = 0.0;
365
- ncy = 1.0;
366
- ncz = 0.0;
367
- }
368
-
369
- // Initial card forward axis F along insertion angle from branch frustum tangent toward D_rad
370
- const ins_angle = is_rosette ? fol.insertion_angle * 0.55 : fol.insertion_angle;
371
- const cos_a = Math.cos(ins_angle);
372
- const sin_a = Math.sin(ins_angle);
373
- let fx = cos_a * seg_tx + sin_a * drx;
374
- let fy = cos_a * seg_ty + sin_a * dry;
375
- let fz = cos_a * seg_tz + sin_a * drz;
376
-
377
- // Blend phototropism (outward/sky) and gravitropism (downward droop along -Y)
378
- fx += fol.phototropism * 0.45 * ncx + (rng() * 2.0 - 1.0) * fol.angular_jitter * 0.35;
379
- fy += fol.phototropism * 0.25 * Math.max(0.0, ncy) - fol.gravitropism * 0.85;
380
- fz += fol.phototropism * 0.45 * ncz + (rng() * 2.0 - 1.0) * fol.angular_jitter * 0.35;
381
-
382
- const f_len = Math.sqrt(fx * fx + fy * fy + fz * fz);
383
- if (f_len > 1e-8) {
384
- fx /= f_len;
385
- fy /= f_len;
386
- fz /= f_len;
387
- } else {
388
- fx = drx;
389
- fy = dry;
390
- fz = drz;
391
- }
392
-
393
- // Desired card upward normal U: blend of world +Y and outward canopy normal n_canopy
394
- let ux = ncx * 0.45;
395
- let uy = 0.65 + ncy * 0.35;
396
- let uz = ncz * 0.45;
397
- // Gram-Schmidt orthogonalize U against F
398
- const dot_uf = ux * fx + uy * fy + uz * fz;
399
- ux -= dot_uf * fx;
400
- uy -= dot_uf * fy;
401
- uz -= dot_uf * fz;
402
- const u_len = Math.sqrt(ux * ux + uy * uy + uz * uz);
403
- if (u_len > 1e-6) {
404
- ux /= u_len;
405
- uy /= u_len;
406
- uz /= u_len;
407
- } else {
408
- // Fallback perpendicular to F
409
- if (Math.abs(fy) < 0.9) {
410
- ux = -fz;
411
- uy = 0.0;
412
- uz = fx;
413
- } else {
414
- ux = 1.0;
415
- uy = 0.0;
416
- uz = 0.0;
417
- }
418
- const d2 = ux * fx + uy * fy + uz * fz;
419
- ux -= d2 * fx;
420
- uy -= d2 * fy;
421
- uz -= d2 * fz;
422
- const l2 = Math.sqrt(ux * ux + uy * uy + uz * uz);
423
- ux /= l2;
424
- uy /= l2;
425
- uz /= l2;
426
- }
427
-
428
- // Enforce outward/sky hemisphere alignment: dot(U, n_canopy + (0, 0.5, 0)) >= 0
429
- if (ux * ncx + uy * (ncy + 0.5) + uz * ncz < 0.0) {
430
- ux = -ux;
431
- uy = -uy;
432
- uz = -uz;
433
- }
434
-
435
- // W = F x U so that (W, F) card triangles have face normal W x F = +U!
436
- const wx = fy * uz - fz * uy;
437
- const wy = fz * ux - fx * uz;
438
- const wz = fx * uy - fy * ux;
439
-
440
- const c3 = cursor * 3;
441
-
442
- positions[c3] = px;
443
- positions[c3 + 1] = py;
444
- positions[c3 + 2] = pz;
445
-
446
- forward[c3] = fx;
447
- forward[c3 + 1] = fy;
448
- forward[c3 + 2] = fz;
449
-
450
- up[c3] = ux;
451
- up[c3 + 1] = uy;
452
- up[c3 + 2] = uz;
453
-
454
- right[c3] = wx;
455
- right[c3 + 1] = wy;
456
- right[c3 + 2] = wz;
457
-
458
- const scale_var = 1.0 + (rng() * 2.0 - 1.0) * fol.size_variance;
459
- const card_scale = Math.max(0.25, scale_var * (is_rosette ? 0.92 : 1.0));
460
- scales[cursor] = card_scale;
461
-
462
- const t64 = new Transform64();
463
- t64.setTranslation(px, py, pz);
464
- t64_look_rotation(t64, fx, fy, fz, ux, uy, uz);
465
- t64.setScaleScalar(card_scale);
466
- t64.updateMatrix();
467
- transforms[cursor] = t64;
468
-
469
- // Golden-ratio low-discrepancy rank in [0, 1)
470
- ranks[cursor] = ((cursor * GOLDEN_CONJUGATE) + b_phase * 0.13) % 1.0;
471
- phases[cursor] = (b_phase + k * 0.17) % 1.0;
472
- branch_sway[cursor] = clamp((level + t_attach) * 0.28, 0.1, 1.0);
473
- color_jitter[cursor] = rng() * 2.0 - 1.0;
474
-
475
- cursor++;
476
- }
477
- }
478
-
479
- return {
480
- count: cursor,
481
- transforms,
482
- positions,
483
- forward,
484
- up,
485
- right,
486
- scales,
487
- ranks,
488
- phases,
489
- branch_sway,
490
- color_jitter,
491
- joints,
492
- weights,
493
- joints4,
494
- weights4,
495
- attr_ranks: Attribute.from(ranks, 1, "foliage_ranks"),
496
- attr_phases: Attribute.from(phases, 1, "foliage_phases"),
497
- attr_branch_sway: Attribute.from(branch_sway, 1, "foliage_branch_sway"),
498
- attr_color_jitter: Attribute.from(color_jitter, 1, "foliage_color_jitter"),
499
- attr_joints: Attribute.from(joints, 2, "foliage_joints"),
500
- attr_weights: Attribute.from(weights, 2, "foliage_weights")
501
- };
502
- }
1
+ import { GOLDEN_RATIO } from "../../../core/math/GOLDEN_RATIO.js";
2
+ import { PI2 } from "../../../core/math/PI2.js";
3
+ import { clamp } from "../../../core/math/clamp.js";
4
+ import { lerp } from "../../../core/math/lerp.js";
5
+ import { seededRandom } from "../../../core/math/random/seededRandom.js";
6
+ import { t64_look_rotation } from "../../../engine/ecs/transform/t64_look_rotation.js";
7
+ import { Transform64 } from "../../../engine/ecs/transform/Transform64.js";
8
+ import { Attribute } from "../../../shade/renderer/geometry/Attribute.js";
9
+ import { CrownEnvelopeType } from "../config/CrownEnvelopeType.js";
10
+ import { PhyllotaxisMode } from "../config/PhyllotaxisMode.js";
11
+ import { TreeSpeciesConfig } from "../config/TreeSpeciesConfig.js";
12
+ import { spine_frame_double_reflection } from "../skeleton/spine_frame_double_reflection.js";
13
+ import { tree_mesh_compute_branch_rings } from "./tree_mesh_build_bark.js";
14
+
15
+ const GOLDEN_CONJUGATE = 1.0 / GOLDEN_RATIO; // ~0.6180339887498949
16
+ const GOLDEN_ANGLE = PI2 / (GOLDEN_RATIO * GOLDEN_RATIO);
17
+
18
+ const SCRATCH_POS = new Float32Array(3);
19
+ const SCRATCH_TAN = new Float32Array(3);
20
+ const SCRATCH_P0 = new Float32Array(3);
21
+ const SCRATCH_T0 = new Float32Array(3);
22
+ const SCRATCH_R0 = new Float32Array(3);
23
+ const SCRATCH_RMF = new Float64Array(6);
24
+
25
+ /**
26
+ * Flat typed-array structure holding all phyllotactic foliage attachment points and oriented card frames.
27
+ *
28
+ * @typedef {Object} FoliageAttachmentSet
29
+ * @property {number} count number of foliage attachments
30
+ * @property {Float32Array} positions `count * 3` attachment world/local positions
31
+ * @property {Float32Array} forward `count * 3` unit card length axis F
32
+ * @property {Float32Array} up `count * 3` unit card outward normal U
33
+ * @property {Float32Array} right `count * 3` unit card width axis W = F x U (so W x F = +U)
34
+ * @property {Float32Array} scales `count` per-card size multiplier
35
+ * @property {Float32Array} ranks `count` golden-ratio low-discrepancy decimation rank in [0, 1)
36
+ * @property {Float32Array} phases `count` per-card wind phase in [0, 1)
37
+ * @property {Float32Array} branch_sway `count` parent branch sway weight in [0, 1]
38
+ * @property {Float32Array} color_jitter `count` per-card luminance/tint jitter in [-1, 1]
39
+ * @property {Float32Array} maturities `count` per-card ontogenetic leaf maturity in [0, 1] (0 = juvenile tip, 1 = mature base)
40
+ * @property {Float32Array} sun_exposure `count` per-card canopy solar irradiance exposure in [0, 1]
41
+ * @property {Uint8Array} variant_indices `count` assigned foliage atlas variant index in `[0, variant_count - 1]`
42
+ * @property {Uint16Array} joints `count * 2` `[j_primary, j_parent]`
43
+ * @property {Float32Array} weights `count * 2` `[w_primary, w_parent]`
44
+ */
45
+
46
+ /**
47
+ * Generates phyllotactic foliage card attachment transforms along all eligible branches of a `TreeSkeleton`.
48
+ *
49
+ * @param {import("../skeleton/TreeSkeleton.js").TreeSkeleton} skeleton
50
+ * @param {TreeSpeciesConfig} [config]
51
+ * @returns {FoliageAttachmentSet}
52
+ */
53
+ export function foliage_collect_attachments(skeleton, config = new TreeSpeciesConfig()) {
54
+ const species = config && config.isTreeSpeciesConfig ? config : TreeSpeciesConfig.from(config);
55
+ const fol = species.foliage;
56
+ if (!fol.enabled || skeleton.branch_count === 0) {
57
+ return {
58
+ count: 0,
59
+ transforms: [],
60
+ positions: new Float32Array(0),
61
+ forward: new Float32Array(0),
62
+ up: new Float32Array(0),
63
+ right: new Float32Array(0),
64
+ scales: new Float32Array(0),
65
+ ranks: new Float32Array(0),
66
+ phases: new Float32Array(0),
67
+ branch_sway: new Float32Array(0),
68
+ color_jitter: new Float32Array(0),
69
+ maturities: new Float32Array(0),
70
+ sun_exposure: new Float32Array(0),
71
+ variant_indices: new Uint8Array(0),
72
+ joints: new Uint16Array(0),
73
+ weights: new Float32Array(0),
74
+ joints4: new Uint16Array(0),
75
+ weights4: new Float32Array(0),
76
+ attr_ranks: Attribute.from(new Float32Array(0), 1, "foliage_ranks"),
77
+ attr_phases: Attribute.from(new Float32Array(0), 1, "foliage_phases"),
78
+ attr_branch_sway: Attribute.from(new Float32Array(0), 1, "foliage_branch_sway"),
79
+ attr_color_jitter: Attribute.from(new Float32Array(0), 1, "foliage_color_jitter"),
80
+ attr_maturities: Attribute.from(new Float32Array(0), 1, "foliage_maturities"),
81
+ attr_variant_indices: Attribute.from(new Uint8Array(0), 1, "foliage_variant_indices"),
82
+ attr_joints: Attribute.from(new Uint16Array(0), 2, "foliage_joints"),
83
+ attr_weights: Attribute.from(new Float32Array(0), 2, "foliage_weights")
84
+ };
85
+ }
86
+
87
+ const rng = seededRandom((species.seed ^ 0x5A3C9E17) | 0);
88
+ const max_existing_level = Math.max(0, species.levels.length - 1);
89
+ const min_level = Math.min(fol.attach_min_level, max_existing_level);
90
+ const branch_rings = tree_mesh_compute_branch_rings(skeleton, species, species.lods?.[0]);
91
+ const variant_count = clamp((species.foliage_material?.variant_count ?? 4) | 0, 1, 16);
92
+
93
+ const crown = species.crown;
94
+ const is_conical_crown = crown.envelope_type === CrownEnvelopeType.Conical;
95
+ const cx = skeleton.crown_center[0];
96
+ const cy = skeleton.crown_center[1];
97
+ const cz = skeleton.crown_center[2];
98
+ const rx_crown = Math.max(0.5, skeleton.crown_radii[0]);
99
+ const ry = Math.max(0.5, skeleton.crown_radii[1]);
100
+ const rz_crown = Math.max(0.5, skeleton.crown_radii[2]);
101
+ const ref_card_len = Math.max(0.25, fol.card_length);
102
+
103
+ // First collect eligible branch edge chains via `skeleton.graph`
104
+ const eligible_branches = [];
105
+ for (let b = 0; b < skeleton.branch_count; b++) {
106
+ const level = skeleton.branch_level[b];
107
+ if (level < min_level) continue;
108
+
109
+ const nodes = [];
110
+ const edges = [];
111
+ skeleton.collect_branch_chain(b, nodes, edges);
112
+ if (edges.length === 0) continue;
113
+
114
+ const t_norms = new Array(nodes.length);
115
+ for (let i = 0; i < nodes.length; i++) {
116
+ t_norms[i] = skeleton.node_t_norm(nodes[i]);
117
+ }
118
+
119
+ eligible_branches.push({ branch_id: b, level, nodes, edges, t_norms });
120
+ }
121
+
122
+ const lateral_count = Math.max(0, fol.cards_per_branch | 0);
123
+ const rosette_count = Math.max(0, fol.terminal_rosette_count | 0);
124
+ const per_branch = lateral_count + rosette_count;
125
+ const total_count = eligible_branches.length * per_branch;
126
+
127
+ /** @type {Transform64[]} */
128
+ const transforms = new Array(total_count);
129
+ const positions = new Float32Array(total_count * 3);
130
+ const forward = new Float32Array(total_count * 3);
131
+ const up = new Float32Array(total_count * 3);
132
+ const right = new Float32Array(total_count * 3);
133
+ const scales = new Float32Array(total_count);
134
+ const ranks = new Float32Array(total_count);
135
+ const phases = new Float32Array(total_count);
136
+ const branch_sway = new Float32Array(total_count);
137
+ const color_jitter = new Float32Array(total_count);
138
+ const maturities = new Float32Array(total_count);
139
+ const sun_exposure = new Float32Array(total_count);
140
+ const variant_indices = new Uint8Array(total_count);
141
+ const joints = new Uint16Array(total_count * 2);
142
+ const weights = new Float32Array(total_count * 2);
143
+ const joints4 = new Uint16Array(total_count * 4);
144
+ const weights4 = new Float32Array(total_count * 4);
145
+
146
+ let cursor = 0;
147
+ const bare = clamp(fol.bare_ratio, 0.0, 0.92);
148
+
149
+ for (let eb = 0; eb < eligible_branches.length; eb++) {
150
+ const item = eligible_branches[eb];
151
+ const level = item.level;
152
+ const nodes = item.nodes;
153
+ const edges = item.edges;
154
+ const t_norms = item.t_norms;
155
+ const b_rings = branch_rings.get(item.branch_id);
156
+ const b_len = Math.max(0.12, skeleton.branch_length[item.branch_id]);
157
+ const shoot_scale = clamp(b_len / (ref_card_len * 0.95), 0.22, 1.0);
158
+ const t_min = Math.max(bare, t_norms[0]);
159
+ const t_max = t_norms[t_norms.length - 1];
160
+ const base_phi = rng() * PI2;
161
+ const b_phase = skeleton.branch_phase_hash[item.branch_id];
162
+
163
+ for (let k = 0; k < per_branch; k++) {
164
+ const is_rosette = k >= lateral_count;
165
+ let t_attach = 1.0;
166
+
167
+ if (!is_rosette && lateral_count > 0) {
168
+ let u_k = (k + 0.5) / lateral_count;
169
+ if (fol.phyllotaxis_mode === PhyllotaxisMode.Opposite
170
+ || fol.phyllotaxis_mode === PhyllotaxisMode.OppositeDecussate) {
171
+ const pair_idx = k >> 1;
172
+ const pair_total = Math.max(1, Math.ceil(lateral_count / 2));
173
+ u_k = (pair_idx + 0.5) / pair_total;
174
+ } else if (fol.phyllotaxis_mode === PhyllotaxisMode.Whorled) {
175
+ const w_size = Math.max(1, fol.whorl_count | 0);
176
+ const w_idx = Math.floor(k / w_size);
177
+ const w_total = Math.max(1, Math.ceil(lateral_count / w_size));
178
+ u_k = (w_idx + 0.5) / w_total;
179
+ }
180
+ t_attach = clamp(lerp(t_min, t_max * 0.92, u_k), t_min, t_max * 0.95);
181
+ } else {
182
+ const ros_idx = k - lateral_count;
183
+ const u_ros = (ros_idx + 0.5) / Math.max(1, rosette_count);
184
+ t_attach = clamp(lerp(Math.max(t_min, t_max * 0.88), t_max * 0.96, u_ros), t_min, t_max * 0.96);
185
+ }
186
+
187
+ // Locate segment containing t_attach
188
+ let seg_idx = edges.length - 1;
189
+ for (let s = 0; s < edges.length; s++) {
190
+ if (t_attach >= t_norms[s] && t_attach <= t_norms[s + 1]) {
191
+ seg_idx = s;
192
+ break;
193
+ }
194
+ }
195
+
196
+ const edge_id = edges[seg_idx];
197
+ const v0 = nodes[seg_idx];
198
+ const v1 = nodes[seg_idx + 1];
199
+ const t0 = t_norms[seg_idx];
200
+ const t1 = t_norms[seg_idx + 1];
201
+ const local_u = clamp((t_attach - t0) / Math.max(1e-5, t1 - t0), 0.05, 0.95);
202
+
203
+ skeleton.evaluate_segment_position(SCRATCH_POS, 0, edge_id, local_u);
204
+ skeleton.evaluate_segment_tangent(SCRATCH_TAN, 0, edge_id, local_u);
205
+ skeleton.node_read_position(SCRATCH_P0, 0, v0);
206
+ skeleton.node_read_tangent(SCRATCH_T0, 0, v0);
207
+ skeleton.node_read_frame_r(SCRATCH_R0, 0, v0);
208
+
209
+ spine_frame_double_reflection(
210
+ SCRATCH_RMF, 0,
211
+ SCRATCH_P0[0], SCRATCH_P0[1], SCRATCH_P0[2],
212
+ SCRATCH_T0[0], SCRATCH_T0[1], SCRATCH_T0[2],
213
+ SCRATCH_R0[0], SCRATCH_R0[1], SCRATCH_R0[2],
214
+ SCRATCH_POS[0], SCRATCH_POS[1], SCRATCH_POS[2],
215
+ SCRATCH_TAN[0], SCRATCH_TAN[1], SCRATCH_TAN[2]
216
+ );
217
+ const rx = SCRATCH_RMF[0], ry_f = SCRATCH_RMF[1], rz = SCRATCH_RMF[2];
218
+ const sx = SCRATCH_RMF[3], sy_f = SCRATCH_RMF[4], sz = SCRATCH_RMF[5];
219
+
220
+ // Phyllotactic angle phi
221
+ let phi = base_phi;
222
+ if (is_rosette) {
223
+ const r_idx = k - lateral_count;
224
+ phi += (r_idx * PI2) / Math.max(1, rosette_count);
225
+ } else {
226
+ switch (fol.phyllotaxis_mode) {
227
+ case PhyllotaxisMode.AlternateDistichous:
228
+ phi += k * Math.PI;
229
+ break;
230
+ case PhyllotaxisMode.Tristichous:
231
+ phi += k * (PI2 / 3.0);
232
+ break;
233
+ case PhyllotaxisMode.Opposite:
234
+ phi += (k & 1) * Math.PI;
235
+ break;
236
+ case PhyllotaxisMode.OppositeDecussate:
237
+ phi += (k & 1) * Math.PI + (k >> 1) * (Math.PI * 0.5);
238
+ break;
239
+ case PhyllotaxisMode.Whorled: {
240
+ const w_size = Math.max(1, fol.whorl_count | 0);
241
+ const w_idx = Math.floor(k / w_size);
242
+ const sub_idx = k % w_size;
243
+ phi += (sub_idx * PI2) / w_size + (w_idx * Math.PI) / w_size;
244
+ break;
245
+ }
246
+ case PhyllotaxisMode.SpiralGolden:
247
+ default:
248
+ phi += k * GOLDEN_ANGLE;
249
+ break;
250
+ }
251
+ }
252
+ phi += (rng() * 2.0 - 1.0) * fol.angular_jitter;
253
+
254
+ const cos_p = Math.cos(phi);
255
+ const sin_p = Math.sin(phi);
256
+ const drx = cos_p * rx + sin_p * sx;
257
+ const dry = cos_p * ry_f + sin_p * sy_f;
258
+ const drz = cos_p * rz + sin_p * sz;
259
+
260
+ // Anchor card stem origin on the actual meshed bark frustum chord [ra, rb]
261
+ // and interpolate the carrier frustum's exact 4-slot skinning weights
262
+ let px = SCRATCH_POS[0];
263
+ let py = SCRATCH_POS[1];
264
+ let pz = SCRATCH_POS[2];
265
+ let seg_tx = SCRATCH_TAN[0];
266
+ let seg_ty = SCRATCH_TAN[1];
267
+ let seg_tz = SCRATCH_TAN[2];
268
+
269
+ const c4 = cursor * 4;
270
+ const c2 = cursor * 2;
271
+
272
+ if (b_rings && b_rings.length >= 2) {
273
+ const ring_idx = Math.min(seg_idx, b_rings.length - 2);
274
+ const ra = b_rings[ring_idx];
275
+ const rb = b_rings[ring_idx + 1];
276
+ const xi = clamp((t_attach - ra.t_norm) / Math.max(1e-5, rb.t_norm - ra.t_norm), 0.05, 0.95);
277
+
278
+ px = lerp(ra.cx, rb.cx, xi);
279
+ py = lerp(ra.cy, rb.cy, xi);
280
+ pz = lerp(ra.cz, rb.cz, xi);
281
+
282
+ const dx_seg = rb.cx - ra.cx;
283
+ const dy_seg = rb.cy - ra.cy;
284
+ const dz_seg = rb.cz - ra.cz;
285
+ const seg_len = Math.hypot(dx_seg, dy_seg, dz_seg);
286
+ if (seg_len > 1e-6) {
287
+ seg_tx = dx_seg / seg_len;
288
+ seg_ty = dy_seg / seg_len;
289
+ seg_tz = dz_seg / seg_len;
290
+ }
291
+
292
+ // Merge 4-slot skinning weights from ra (weight 1 - xi) and rb (weight xi)
293
+ const merged_j = [-1, -1, -1, -1, -1, -1, -1, -1];
294
+ const merged_w = [0, 0, 0, 0, 0, 0, 0, 0];
295
+ let m_count = 0;
296
+
297
+ const accumulate_joint = (j_id, w_val) => {
298
+ if (w_val <= 1e-7) return;
299
+ for (let m = 0; m < m_count; m++) {
300
+ if (merged_j[m] === j_id) {
301
+ merged_w[m] += w_val;
302
+ return;
303
+ }
304
+ }
305
+ merged_j[m_count] = j_id;
306
+ merged_w[m_count] = w_val;
307
+ m_count++;
308
+ };
309
+
310
+ const w_a = 1.0 - xi;
311
+ const w_b = xi;
312
+ accumulate_joint(ra.j0, ra.w0 * w_a);
313
+ accumulate_joint(ra.j1, ra.w1 * w_a);
314
+ accumulate_joint(ra.j2, ra.w2 * w_a);
315
+ accumulate_joint(ra.j3, ra.w3 * w_a);
316
+ accumulate_joint(rb.j0, rb.w0 * w_b);
317
+ accumulate_joint(rb.j1, rb.w1 * w_b);
318
+ accumulate_joint(rb.j2, rb.w2 * w_b);
319
+ accumulate_joint(rb.j3, rb.w3 * w_b);
320
+
321
+ // Sort descending by weight and normalize top 4
322
+ for (let a = 0; a < m_count - 1; a++) {
323
+ for (let b_sort = a + 1; b_sort < m_count; b_sort++) {
324
+ if (merged_w[b_sort] > merged_w[a]) {
325
+ const tw = merged_w[a];
326
+ merged_w[a] = merged_w[b_sort];
327
+ merged_w[b_sort] = tw;
328
+ const tj = merged_j[a];
329
+ merged_j[a] = merged_j[b_sort];
330
+ merged_j[b_sort] = tj;
331
+ }
332
+ }
333
+ }
334
+
335
+ const top_k = Math.min(4, m_count);
336
+ let w4_sum = 0.0;
337
+ for (let s = 0; s < top_k; s++) w4_sum += merged_w[s];
338
+ const inv_w4 = w4_sum > 1e-7 ? 1.0 / w4_sum : 1.0;
339
+
340
+ for (let s = 0; s < 4; s++) {
341
+ if (s < top_k) {
342
+ joints4[c4 + s] = merged_j[s];
343
+ weights4[c4 + s] = merged_w[s] * inv_w4;
344
+ } else {
345
+ joints4[c4 + s] = 0;
346
+ weights4[c4 + s] = 0.0;
347
+ }
348
+ }
349
+
350
+ // Populate 2-slot legacy joints/weights normalized to sum to 1.0
351
+ const j_prim = top_k > 0 ? merged_j[0] : 0;
352
+ const j_par = top_k > 1 ? merged_j[1] : j_prim;
353
+ const w0_raw = top_k > 0 ? merged_w[0] : 1.0;
354
+ const w1_raw = top_k > 1 ? merged_w[1] : 0.0;
355
+ const w2_sum = w0_raw + w1_raw;
356
+ joints[c2] = j_prim;
357
+ joints[c2 + 1] = j_par;
358
+ weights[c2] = w2_sum > 1e-7 ? w0_raw / w2_sum : 1.0;
359
+ weights[c2 + 1] = w2_sum > 1e-7 ? w1_raw / w2_sum : 0.0;
360
+ } else {
361
+ const j_prim = skeleton.node_read_joint(v1, 0);
362
+ const j_par = skeleton.node_read_joint(v1, 1);
363
+ const w_prim = j_prim === j_par ? 1.0 : clamp(0.35 + 0.65 * t_attach, 0.0, 1.0);
364
+ joints[c2] = j_prim;
365
+ joints[c2 + 1] = j_par;
366
+ weights[c2] = w_prim;
367
+ weights[c2 + 1] = 1.0 - w_prim;
368
+ joints4[c4] = j_prim;
369
+ joints4[c4 + 1] = j_par;
370
+ weights4[c4] = w_prim;
371
+ weights4[c4 + 1] = 1.0 - w_prim;
372
+ }
373
+
374
+ // Outward canopy normal n_canopy from crown center
375
+ let ncx = px - cx;
376
+ let ncy = (py - cy) + 0.35 * ry;
377
+ let ncz = pz - cz;
378
+ const nc_len = Math.sqrt(ncx * ncx + ncy * ncy + ncz * ncz);
379
+ if (nc_len > 1e-6) {
380
+ ncx /= nc_len;
381
+ ncy /= nc_len;
382
+ ncz /= nc_len;
383
+ } else {
384
+ ncx = 0.0;
385
+ ncy = 1.0;
386
+ ncz = 0.0;
387
+ }
388
+
389
+ const h_norm_card = clamp((py - crown.base_height) / Math.max(0.1, crown.height), 0.0, 1.0);
390
+ const w_apex_spire = is_conical_crown && h_norm_card > 0.72
391
+ ? Math.pow(clamp((h_norm_card - 0.72) / 0.25, 0.0, 1.0), 1.35)
392
+ : 0.0;
393
+
394
+ // Initial card forward axis F along insertion angle from branch frustum tangent toward D_rad
395
+ const base_ins = is_rosette ? fol.insertion_angle * 0.55 : fol.insertion_angle;
396
+ const ins_angle = base_ins * (1.0 - 0.65 * w_apex_spire);
397
+ const cos_a = Math.cos(ins_angle);
398
+ const sin_a = Math.sin(ins_angle);
399
+ let fx = cos_a * seg_tx + sin_a * drx;
400
+ let fy = cos_a * seg_ty + sin_a * dry;
401
+ let fz = cos_a * seg_tz + sin_a * drz;
402
+
403
+ // Blend phototropism (outward/sky), gravitropism (downward droop along -Y), and orthotropic apical candle spire
404
+ const grav_atten = 1.0 - 0.90 * w_apex_spire;
405
+ const out_atten = 1.0 - 0.75 * w_apex_spire;
406
+ fx += fol.phototropism * 0.45 * out_atten * ncx + (rng() * 2.0 - 1.0) * fol.angular_jitter * 0.35 * out_atten;
407
+ fy += fol.phototropism * 0.25 * Math.max(0.0, ncy) - fol.gravitropism * 0.85 * grav_atten;
408
+ fz += fol.phototropism * 0.45 * out_atten * ncz + (rng() * 2.0 - 1.0) * fol.angular_jitter * 0.35 * out_atten;
409
+
410
+ if (w_apex_spire > 1e-4) {
411
+ const w_spire = clamp(w_apex_spire * (is_rosette ? 0.92 : 0.78), 0.0, 0.94);
412
+ fx = lerp(fx, -0.15 * ncx, w_spire);
413
+ fy = lerp(fy, 1.0, w_spire);
414
+ fz = lerp(fz, -0.15 * ncz, w_spire);
415
+ }
416
+
417
+ const f_len = Math.sqrt(fx * fx + fy * fy + fz * fz);
418
+ if (f_len > 1e-8) {
419
+ fx /= f_len;
420
+ fy /= f_len;
421
+ fz /= f_len;
422
+ } else {
423
+ fx = drx;
424
+ fy = dry;
425
+ fz = drz;
426
+ }
427
+
428
+ // Desired card upward normal U: blend of world +Y and outward canopy normal n_canopy
429
+ let ux = ncx * 0.45;
430
+ let uy = 0.65 + ncy * 0.35;
431
+ let uz = ncz * 0.45;
432
+ // Gram-Schmidt orthogonalize U against F
433
+ const dot_uf = ux * fx + uy * fy + uz * fz;
434
+ ux -= dot_uf * fx;
435
+ uy -= dot_uf * fy;
436
+ uz -= dot_uf * fz;
437
+ const u_len = Math.sqrt(ux * ux + uy * uy + uz * uz);
438
+ if (u_len > 1e-6) {
439
+ ux /= u_len;
440
+ uy /= u_len;
441
+ uz /= u_len;
442
+ } else {
443
+ // Fallback perpendicular to F
444
+ if (Math.abs(fy) < 0.9) {
445
+ ux = -fz;
446
+ uy = 0.0;
447
+ uz = fx;
448
+ } else {
449
+ ux = 1.0;
450
+ uy = 0.0;
451
+ uz = 0.0;
452
+ }
453
+ const d2 = ux * fx + uy * fy + uz * fz;
454
+ ux -= d2 * fx;
455
+ uy -= d2 * fy;
456
+ uz -= d2 * fz;
457
+ const l2 = Math.sqrt(ux * ux + uy * uy + uz * uz);
458
+ ux /= l2;
459
+ uy /= l2;
460
+ uz /= l2;
461
+ }
462
+
463
+ // Enforce outward/sky hemisphere alignment: dot(U, n_canopy + (0, 0.5, 0)) >= 0
464
+ if (ux * ncx + uy * (ncy + 0.5) + uz * ncz < 0.0) {
465
+ ux = -ux;
466
+ uy = -uy;
467
+ uz = -uz;
468
+ }
469
+
470
+ // W = F x U so that (W, F) card triangles have face normal W x F = +U!
471
+ const wx = fy * uz - fz * uy;
472
+ const wy = fz * ux - fx * uz;
473
+ const wz = fx * uy - fy * ux;
474
+
475
+ const c3 = cursor * 3;
476
+
477
+ positions[c3] = px;
478
+ positions[c3 + 1] = py;
479
+ positions[c3 + 2] = pz;
480
+
481
+ forward[c3] = fx;
482
+ forward[c3 + 1] = fy;
483
+ forward[c3 + 2] = fz;
484
+
485
+ up[c3] = ux;
486
+ up[c3 + 1] = uy;
487
+ up[c3 + 2] = uz;
488
+
489
+ right[c3] = wx;
490
+ right[c3 + 1] = wy;
491
+ right[c3 + 2] = wz;
492
+
493
+ const apex_env_scale = is_conical_crown && h_norm_card > 0.70
494
+ ? lerp(1.0, 0.24, Math.pow(clamp((h_norm_card - 0.70) / 0.28, 0.0, 1.0), 1.15))
495
+ : 1.0;
496
+ const scale_var = 1.0 + (rng() * 2.0 - 1.0) * fol.size_variance;
497
+ const card_scale = Math.max(
498
+ 0.18,
499
+ scale_var * (is_rosette ? 0.92 : 1.0) * Math.min(shoot_scale, apex_env_scale)
500
+ );
501
+ scales[cursor] = card_scale;
502
+
503
+ const t64 = new Transform64();
504
+ t64.setTranslation(px, py, pz);
505
+ t64_look_rotation(t64, fx, fy, fz, ux, uy, uz);
506
+ t64.setScaleScalar(card_scale);
507
+ t64.updateMatrix();
508
+ transforms[cursor] = t64;
509
+
510
+ // Golden-ratio low-discrepancy rank in [0, 1)
511
+ ranks[cursor] = ((cursor * GOLDEN_CONJUGATE) + b_phase * 0.13) % 1.0;
512
+ phases[cursor] = (b_phase + k * 0.17) % 1.0;
513
+ branch_sway[cursor] = clamp((level + t_attach) * 0.28, 0.1, 1.0);
514
+ const c_jit = rng() * 2.0 - 1.0;
515
+ color_jitter[cursor] = c_jit;
516
+
517
+ // Ontogenetic leaf maturity mu_i in [0, 1]:
518
+ // Acropetal shoot progression (terminal rosette / distal tip -> 0 juvenile, basal nodes -> 1 mature)
519
+ // combined with crown shell periphery (outer shell -> younger shoots, inner core -> older shade leaves)
520
+ const u_shoot = is_rosette
521
+ ? clamp(0.88 + 0.12 * ((k - lateral_count + 0.5) / Math.max(1, rosette_count)), 0.88, 1.0)
522
+ : clamp((t_attach - t_min) / Math.max(1e-4, t_max * 0.92 - t_min), 0.0, 1.0);
523
+ const ndx_c = (px - cx) / rx_crown;
524
+ const ndy_c = (py - cy) / ry;
525
+ const ndz_c = (pz - cz) / rz_crown;
526
+ const rho_shell = clamp(Math.sqrt(ndx_c * ndx_c + ndy_c * ndy_c + ndz_c * ndz_c), 0.0, 1.0);
527
+ const maturity = clamp(
528
+ 0.66 * (1.0 - u_shoot) + 0.24 * (1.0 - rho_shell) + 0.05 + 0.07 * c_jit,
529
+ 0.0,
530
+ 1.0
531
+ );
532
+ const sun_exp = clamp(0.62 * rho_shell + 0.38 * h_norm_card, 0.0, 1.0);
533
+ maturities[cursor] = maturity;
534
+ sun_exposure[cursor] = sun_exp;
535
+ variant_indices[cursor] = clamp(Math.floor(maturity * variant_count), 0, variant_count - 1);
536
+
537
+ cursor++;
538
+ }
539
+ }
540
+
541
+ return {
542
+ count: cursor,
543
+ transforms,
544
+ positions,
545
+ forward,
546
+ up,
547
+ right,
548
+ scales,
549
+ ranks,
550
+ phases,
551
+ branch_sway,
552
+ color_jitter,
553
+ maturities,
554
+ sun_exposure,
555
+ variant_indices,
556
+ joints,
557
+ weights,
558
+ joints4,
559
+ weights4,
560
+ attr_ranks: Attribute.from(ranks, 1, "foliage_ranks"),
561
+ attr_phases: Attribute.from(phases, 1, "foliage_phases"),
562
+ attr_branch_sway: Attribute.from(branch_sway, 1, "foliage_branch_sway"),
563
+ attr_color_jitter: Attribute.from(color_jitter, 1, "foliage_color_jitter"),
564
+ attr_maturities: Attribute.from(maturities, 1, "foliage_maturities"),
565
+ attr_variant_indices: Attribute.from(variant_indices, 1, "foliage_variant_indices"),
566
+ attr_joints: Attribute.from(joints, 2, "foliage_joints"),
567
+ attr_weights: Attribute.from(weights, 2, "foliage_weights")
568
+ };
569
+ }