@woosh/meep-engine 3.53.0 → 3.55.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 (249) 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/{graphics3/decal/DecalAtlasFill.d.ts → graphics/texture/atlas/SpriteAtlasFill.d.ts} +4 -4
  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/{graphics3/decal/decal_atlas_border.d.ts → graphics/texture/atlas/sprite_atlas_border.d.ts} +12 -12
  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/RENDERER_CONTRACT.md +5 -2
  68. package/src/shade/device/SHADE_DEVICE_FLOOR.d.ts.map +1 -1
  69. package/src/shade/device/SHADE_DEVICE_FLOOR.js +2 -0
  70. package/src/shade/playground/foliage_wind/foliage_world.d.ts +6 -0
  71. package/src/shade/playground/foliage_wind/foliage_world.d.ts.map +1 -1
  72. package/src/shade/playground/foliage_wind/foliage_world.js +602 -593
  73. package/src/shade/playground/foliage_wind/index.html +260 -254
  74. package/src/shade/playground/foliage_wind/main.js +275 -263
  75. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  76. package/src/shade/renderer/Renderer.js +1 -1
  77. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  78. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  79. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  80. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  81. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  82. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +1 -1
  83. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +3 -3
  84. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  85. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +350 -294
  86. package/src/shade/renderer/hiz/generate_downsampler_code.d.ts +0 -2
  87. package/src/shade/renderer/hiz/generate_downsampler_code.d.ts.map +1 -1
  88. package/src/shade/renderer/hiz/generate_downsampler_code.js +99 -100
  89. package/src/shade/renderer/hiz/hzb_pick_alignment.d.ts +15 -0
  90. package/src/shade/renderer/hiz/hzb_pick_alignment.d.ts.map +1 -0
  91. package/src/shade/renderer/hiz/hzb_pick_alignment.js +148 -0
  92. package/src/shade/renderer/hiz/hzb_plan_reduction_steps.d.ts +28 -0
  93. package/src/shade/renderer/hiz/hzb_plan_reduction_steps.d.ts.map +1 -0
  94. package/src/shade/renderer/hiz/hzb_plan_reduction_steps.js +58 -0
  95. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +5 -5
  96. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
  97. package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +57 -67
  98. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +3 -3
  99. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +1 -1
  100. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.js +111 -112
  101. package/src/shade/renderer/hiz/shader_hzb_spd.d.ts +5 -0
  102. package/src/shade/renderer/hiz/shader_hzb_spd.d.ts.map +1 -0
  103. package/src/shade/renderer/hiz/shader_hzb_spd.js +160 -0
  104. package/src/shade/renderer/light/GPULightCollection.d.ts +8 -0
  105. package/src/shade/renderer/light/GPULightCollection.d.ts.map +1 -1
  106. package/src/shade/renderer/light/GPULightCollection.js +67 -0
  107. package/src/shade/renderer/light/environment/FAR_FIELD_IRRADIANCE_SH_RESOURCE.d.ts +7 -0
  108. package/src/shade/renderer/light/environment/FAR_FIELD_IRRADIANCE_SH_RESOURCE.d.ts.map +1 -0
  109. package/src/shade/renderer/light/environment/FAR_FIELD_IRRADIANCE_SH_RESOURCE.js +13 -0
  110. package/src/shade/renderer/light/environment/shader_project_far_field_irradiance_sh.d.ts +10 -0
  111. package/src/shade/renderer/light/environment/shader_project_far_field_irradiance_sh.d.ts.map +1 -0
  112. package/src/shade/renderer/light/environment/shader_project_far_field_irradiance_sh.js +72 -0
  113. package/src/shade/renderer/light/world_cluster/WORLD_LIGHT_CLUSTER.d.ts +39 -0
  114. package/src/shade/renderer/light/world_cluster/WORLD_LIGHT_CLUSTER.d.ts.map +1 -0
  115. package/src/shade/renderer/light/world_cluster/WORLD_LIGHT_CLUSTER.js +90 -0
  116. package/src/shade/renderer/light/world_cluster/chunk_world_light_cluster_lookup.d.ts +12 -0
  117. package/src/shade/renderer/light/world_cluster/chunk_world_light_cluster_lookup.d.ts.map +1 -0
  118. package/src/shade/renderer/light/world_cluster/chunk_world_light_cluster_lookup.js +56 -0
  119. package/src/shade/renderer/light/world_cluster/graph_build_world_light_clusters.d.ts +31 -0
  120. package/src/shade/renderer/light/world_cluster/graph_build_world_light_clusters.d.ts.map +1 -0
  121. package/src/shade/renderer/light/world_cluster/graph_build_world_light_clusters.js +196 -0
  122. package/src/shade/renderer/light/world_cluster/shader_world_cluster_assign_lights.d.ts +10 -0
  123. package/src/shade/renderer/light/world_cluster/shader_world_cluster_assign_lights.d.ts.map +1 -0
  124. package/src/shade/renderer/light/world_cluster/shader_world_cluster_assign_lights.js +163 -0
  125. package/src/shade/renderer/light/world_cluster/shader_world_cluster_cull_lights.d.ts +4 -0
  126. package/src/shade/renderer/light/world_cluster/shader_world_cluster_cull_lights.d.ts.map +1 -0
  127. package/src/shade/renderer/light/world_cluster/shader_world_cluster_cull_lights.js +137 -0
  128. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  129. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  130. package/src/shade/renderer/particles/GPUParticleSystem.js +4 -3
  131. package/src/shade/renderer/particles/graph_particles_avboit.d.ts +4 -3
  132. package/src/shade/renderer/particles/graph_particles_avboit.d.ts.map +1 -1
  133. package/src/shade/renderer/particles/graph_particles_avboit.js +28 -11
  134. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts +9 -13
  135. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts.map +1 -1
  136. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.js +25 -20
  137. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.d.ts.map +1 -1
  138. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.js +21 -38
  139. package/src/shade/renderer/postprocess/denoise/fragment_shader_denoise_ao.d.ts.map +1 -1
  140. package/src/shade/renderer/postprocess/denoise/fragment_shader_denoise_ao.js +154 -147
  141. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts +2 -1
  142. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts.map +1 -1
  143. package/src/shade/renderer/postprocess/gtao/GTAO.js +5 -1
  144. package/src/shade/renderer/postprocess/gtao/GTAO_CONSTANTS.d.ts +8 -0
  145. package/src/shade/renderer/postprocess/gtao/GTAO_CONSTANTS.d.ts.map +1 -1
  146. package/src/shade/renderer/postprocess/gtao/GTAO_CONSTANTS.js +10 -1
  147. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts.map +1 -1
  148. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.js +5 -0
  149. package/src/shade/renderer/postprocess/gtao/chunk_gtao_compute_mip_level.d.ts +22 -0
  150. package/src/shade/renderer/postprocess/gtao/chunk_gtao_compute_mip_level.d.ts.map +1 -0
  151. package/src/shade/renderer/postprocess/gtao/chunk_gtao_compute_mip_level.js +33 -0
  152. package/src/shade/renderer/postprocess/gtao/chunk_gtao_resolve_hzb_depth.d.ts +22 -0
  153. package/src/shade/renderer/postprocess/gtao/chunk_gtao_resolve_hzb_depth.d.ts.map +1 -0
  154. package/src/shade/renderer/postprocess/gtao/chunk_gtao_resolve_hzb_depth.js +56 -0
  155. package/src/shade/renderer/postprocess/gtao/chunk_gtao_sample_depth.d.ts +13 -0
  156. package/src/shade/renderer/postprocess/gtao/chunk_gtao_sample_depth.d.ts.map +1 -0
  157. package/src/shade/renderer/postprocess/gtao/chunk_gtao_sample_depth.js +42 -0
  158. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.d.ts +2 -1
  159. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.d.ts.map +1 -1
  160. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.js +382 -357
  161. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.d.ts.map +1 -1
  162. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.js +414 -410
  163. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  164. package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +0 -7
  165. package/src/shade/renderer/shader/chunk/depth/chunk_query_depth_from_screen_space_bb.d.ts.map +1 -1
  166. package/src/shade/renderer/shader/chunk/depth/chunk_query_depth_from_screen_space_bb.js +62 -62
  167. package/src/shade/renderer/shader/chunk/random/lowbias/chunk_hash_lowbias32_vec2u.d.ts +6 -0
  168. package/src/shade/renderer/shader/chunk/random/lowbias/chunk_hash_lowbias32_vec2u.d.ts.map +1 -0
  169. package/src/shade/renderer/shader/chunk/random/lowbias/chunk_hash_lowbias32_vec2u.js +19 -0
  170. package/src/shade/renderer/shader/chunk/screen_space/chunk_ffx_sssr_get_mip_resolution.d.ts.map +1 -1
  171. package/src/shade/renderer/shader/chunk/screen_space/chunk_ffx_sssr_get_mip_resolution.js +16 -10
  172. package/src/shade/renderer/shader/chunk/screen_space/chunk_ffx_sssr_hierarchical_raymarch.d.ts.map +1 -1
  173. package/src/shade/renderer/shader/chunk/screen_space/chunk_ffx_sssr_hierarchical_raymarch.js +118 -117
  174. package/src/shade/renderer/texture/atlas/GPUTileAtlas3D.d.ts +41 -7
  175. package/src/shade/renderer/texture/atlas/GPUTileAtlas3D.d.ts.map +1 -1
  176. package/src/shade/renderer/texture/atlas/GPUTileAtlas3D.js +162 -58
  177. package/src/shade/renderer/volumetrics/GPUSceneVolumetrics.js +1 -1
  178. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts +10 -12
  179. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  180. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +583 -606
  181. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +1 -1
  182. package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +640 -640
  183. package/src/shade/renderer/volumetrics/irradiance_clipmap/GPUIrradianceClipmap.d.ts +113 -0
  184. package/src/shade/renderer/volumetrics/irradiance_clipmap/GPUIrradianceClipmap.d.ts.map +1 -0
  185. package/src/shade/renderer/volumetrics/irradiance_clipmap/GPUIrradianceClipmap.js +484 -0
  186. package/src/shade/renderer/volumetrics/irradiance_clipmap/IRRADIANCE_CLIPMAP.d.ts +50 -0
  187. package/src/shade/renderer/volumetrics/irradiance_clipmap/IRRADIANCE_CLIPMAP.d.ts.map +1 -0
  188. package/src/shade/renderer/volumetrics/irradiance_clipmap/IRRADIANCE_CLIPMAP.js +60 -0
  189. package/src/shade/renderer/volumetrics/irradiance_clipmap/IRRADIANCE_CLIPMAP_STRUCT.d.ts +25 -0
  190. package/src/shade/renderer/volumetrics/irradiance_clipmap/IRRADIANCE_CLIPMAP_STRUCT.d.ts.map +1 -0
  191. package/src/shade/renderer/volumetrics/irradiance_clipmap/IRRADIANCE_CLIPMAP_STRUCT.js +47 -0
  192. package/src/shade/renderer/volumetrics/irradiance_clipmap/IrradianceCascadePlacement.d.ts +74 -0
  193. package/src/shade/renderer/volumetrics/irradiance_clipmap/IrradianceCascadePlacement.d.ts.map +1 -0
  194. package/src/shade/renderer/volumetrics/irradiance_clipmap/IrradianceCascadePlacement.js +173 -0
  195. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_gi_sample.d.ts +26 -0
  196. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_gi_sample.d.ts.map +1 -0
  197. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_gi_sample.js +83 -0
  198. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_local_light.d.ts +36 -0
  199. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_local_light.d.ts.map +1 -0
  200. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_local_light.js +129 -0
  201. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_sample.d.ts +121 -0
  202. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_sample.d.ts.map +1 -0
  203. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_sample.js +370 -0
  204. package/src/shade/renderer/volumetrics/irradiance_clipmap/shader_irradiance_clipmap_build.d.ts +16 -0
  205. package/src/shade/renderer/volumetrics/irradiance_clipmap/shader_irradiance_clipmap_build.d.ts.map +1 -0
  206. package/src/shade/renderer/volumetrics/irradiance_clipmap/shader_irradiance_clipmap_build.js +191 -0
  207. package/src/shade/renderer/wind/GPUSceneWind.d.ts +1 -1
  208. package/src/shade/renderer/wind/GPUSceneWind.js +1 -1
  209. package/src/engine/graphics3/decal/DecalAtlas.d.ts +0 -43
  210. package/src/engine/graphics3/decal/DecalAtlas.d.ts.map +0 -1
  211. package/src/engine/graphics3/decal/DecalAtlasFill.d.ts.map +0 -1
  212. package/src/engine/graphics3/decal/DecalAtlasFill.js +0 -32
  213. package/src/engine/graphics3/decal/decal_atlas_border.d.ts.map +0 -1
  214. package/src/shade/renderer/postprocess/gtao/graph_build_depth_mipmap.d.ts +0 -26
  215. package/src/shade/renderer/postprocess/gtao/graph_build_depth_mipmap.d.ts.map +0 -1
  216. package/src/shade/renderer/postprocess/gtao/graph_build_depth_mipmap.js +0 -268
  217. package/src/shade/renderer/postprocess/ssr/copy_shader_f32.d.ts +0 -3
  218. package/src/shade/renderer/postprocess/ssr/copy_shader_f32.d.ts.map +0 -1
  219. package/src/shade/renderer/postprocess/ssr/copy_shader_f32.js +0 -32
  220. package/src/shade/renderer/postprocess/ssr/depth_max_shader.d.ts +0 -17
  221. package/src/shade/renderer/postprocess/ssr/depth_max_shader.d.ts.map +0 -1
  222. package/src/shade/renderer/postprocess/ssr/depth_max_shader.js +0 -98
  223. package/src/shade/renderer/postprocess/ssr/graph_build_depth_max_pyramid.d.ts +0 -23
  224. package/src/shade/renderer/postprocess/ssr/graph_build_depth_max_pyramid.d.ts.map +0 -1
  225. package/src/shade/renderer/postprocess/ssr/graph_build_depth_max_pyramid.js +0 -118
  226. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_froxel_centre_ws.d.ts +0 -15
  227. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_froxel_centre_ws.d.ts.map +0 -1
  228. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_froxel_centre_ws.js +0 -49
  229. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.d.ts +0 -17
  230. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.d.ts.map +0 -1
  231. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.js +0 -50
  232. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi_sample.d.ts +0 -35
  233. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi_sample.d.ts.map +0 -1
  234. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi_sample.js +0 -105
  235. package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.d.ts +0 -35
  236. package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.d.ts.map +0 -1
  237. package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.js +0 -80
  238. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi.d.ts +0 -10
  239. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi.d.ts.map +0 -1
  240. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi.js +0 -51
  241. package/src/shade/renderer/volumetrics/gi/volumetrics_gi_resources.d.ts +0 -28
  242. package/src/shade/renderer/volumetrics/gi/volumetrics_gi_resources.d.ts.map +0 -1
  243. package/src/shade/renderer/volumetrics/gi/volumetrics_gi_resources.js +0 -39
  244. package/src/shade/renderer/volumetrics/graph_build_froxel_irradiance.d.ts +0 -48
  245. package/src/shade/renderer/volumetrics/graph_build_froxel_irradiance.d.ts.map +0 -1
  246. package/src/shade/renderer/volumetrics/graph_build_froxel_irradiance.js +0 -121
  247. package/src/shade/renderer/volumetrics/shader_volumetrics_build_irradiance.d.ts +0 -28
  248. package/src/shade/renderer/volumetrics/shader_volumetrics_build_irradiance.d.ts.map +0 -1
  249. package/src/shade/renderer/volumetrics/shader_volumetrics_build_irradiance.js +0 -237
@@ -1,1104 +1,1871 @@
1
- import { PointHashGrid3 } from "../../../core/geom/3d/hash-grid/PointHashGrid3.js";
2
- import { GOLDEN_RATIO } from "../../../core/math/GOLDEN_RATIO.js";
3
- import { PI2 } from "../../../core/math/PI2.js";
4
- import { clamp } from "../../../core/math/clamp.js";
5
- import { lerp } from "../../../core/math/lerp.js";
6
- import { curl_noise_3d } from "../../../core/math/noise/curl_noise_3d.js";
7
- import { seededRandom } from "../../../core/math/random/seededRandom.js";
8
- import { CrownEnvelopeType } from "../config/CrownEnvelopeType.js";
9
- import { PhyllotaxisMode } from "../config/PhyllotaxisMode.js";
10
- import { TreeSpeciesConfig } from "../config/TreeSpeciesConfig.js";
11
- import { TreeSkeleton } from "./TreeSkeleton.js";
12
- import { crown_envelope_evaluate, crown_envelope_sdf } from "./crown_envelope_evaluate.js";
13
- import { spine_frame_double_reflection } from "./spine_frame_double_reflection.js";
14
- import { tree_skeleton_compute_pipe_radii } from "./tree_skeleton_compute_pipe_radii.js";
15
-
16
- const GOLDEN_ANGLE = PI2 / (GOLDEN_RATIO * GOLDEN_RATIO); // ~2.399963229728653 rad (137.50776 deg)
17
-
18
- const SCRATCH_CURL = [0, 0, 0];
19
- const SCRATCH_RMF = new Float64Array(6);
20
- const SCRATCH_POS = new Float32Array(3);
21
- const SCRATCH_TAN = new Float32Array(3);
22
- const SCRATCH_P0 = new Float32Array(3);
23
- const SCRATCH_T0 = new Float32Array(3);
24
- const SCRATCH_R0 = new Float32Array(3);
25
- const SCRATCH_M_PREV = new Float32Array(3);
26
- const SCRATCH_M_NEXT = new Float32Array(3);
27
- const SCRATCH_GUIDE_P0 = new Float32Array(3);
28
- const SCRATCH_GUIDE_P1 = new Float32Array(3);
29
- const SCRATCH_GUIDE_T = new Float32Array(3);
30
-
31
- /**
32
- * Evaluates the trunk guide curve position `out_pos` and unit tangent `out_tan` at normalized
33
- * parameter `t_norm` in [0, 1]. Defaults to the vertical upward plumb line `(0, t_norm * trunk_len, 0)`
34
- * when `trunk_guide` is null or omitted.
35
- *
36
- * @param {Float32Array|number[]} out_pos
37
- * @param {Float32Array|number[]} out_tan
38
- * @param {Array<ArrayLike<number>>|Function|null|undefined} trunk_guide
39
- * @param {number} t_norm
40
- * @param {number} trunk_len
41
- */
42
- function evaluate_trunk_guide(out_pos, out_tan, trunk_guide, t_norm, trunk_len) {
43
- const t = clamp(t_norm, 0.0, 1.0);
44
- if (!trunk_guide) {
45
- out_pos[0] = 0.0;
46
- out_pos[1] = t * trunk_len;
47
- out_pos[2] = 0.0;
48
- out_tan[0] = 0.0;
49
- out_tan[1] = 1.0;
50
- out_tan[2] = 0.0;
51
- return;
52
- }
53
-
54
- const sample_pt = (u, dst) => {
55
- if (typeof trunk_guide === "function") {
56
- trunk_guide(u, dst);
57
- return;
58
- }
59
- if (Array.isArray(trunk_guide) && trunk_guide.length >= 2) {
60
- const n = trunk_guide.length - 1;
61
- const su = clamp(u, 0.0, 1.0) * n;
62
- const idx = Math.min(n - 1, Math.floor(su));
63
- const frac = su - idx;
64
- const a = trunk_guide[idx];
65
- const b = trunk_guide[idx + 1];
66
- dst[0] = lerp(a[0] ?? 0.0, b[0] ?? 0.0, frac);
67
- dst[1] = lerp(a[1] ?? 0.0, b[1] ?? 0.0, frac);
68
- dst[2] = lerp(a[2] ?? 0.0, b[2] ?? 0.0, frac);
69
- return;
70
- }
71
- dst[0] = 0.0;
72
- dst[1] = u * trunk_len;
73
- dst[2] = 0.0;
74
- };
75
-
76
- sample_pt(t, out_pos);
77
- const eps = 0.01;
78
- const t_a = Math.max(0.0, t - eps);
79
- const t_b = Math.min(1.0, t + eps);
80
- const pa = [0, 0, 0];
81
- const pb = [0, 0, 0];
82
- sample_pt(t_a, pa);
83
- sample_pt(t_b, pb);
84
- let tx = pb[0] - pa[0];
85
- let ty = pb[1] - pa[1];
86
- let tz = pb[2] - pa[2];
87
- const tl = Math.hypot(tx, ty, tz);
88
- if (tl > 1e-8) {
89
- out_tan[0] = tx / tl;
90
- out_tan[1] = ty / tl;
91
- out_tan[2] = tz / tl;
92
- } else {
93
- out_tan[0] = 0.0;
94
- out_tan[1] = 1.0;
95
- out_tan[2] = 0.0;
96
- }
97
- }
98
-
99
- /**
100
- * Rotates unit vector `(vx, vy, vz)` around unit axis `(ax, ay, az)` by `angle` radians (Rodrigues' formula).
101
- * @param {number[]|Float64Array} out
102
- * @param {number} vx
103
- * @param {number} vy
104
- * @param {number} vz
105
- * @param {number} ax
106
- * @param {number} ay
107
- * @param {number} az
108
- * @param {number} angle
109
- */
110
- function rotate_around_axis(out, vx, vy, vz, ax, ay, az, angle) {
111
- const c = Math.cos(angle);
112
- const s = Math.sin(angle);
113
- const dot = vx * ax + vy * ay + vz * az;
114
- const omc = 1.0 - c;
115
-
116
- const cx = ay * vz - az * vy;
117
- const cy = az * vx - ax * vz;
118
- const cz = ax * vy - ay * vx;
119
-
120
- let rx = vx * c + cx * s + ax * dot * omc;
121
- let ry = vy * c + cy * s + ay * dot * omc;
122
- let rz = vz * c + cz * s + az * dot * omc;
123
-
124
- const len = Math.sqrt(rx * rx + ry * ry + rz * rz);
125
- if (len > 1e-12) {
126
- const inv = 1.0 / len;
127
- rx *= inv;
128
- ry *= inv;
129
- rz *= inv;
130
- }
131
- out[0] = rx;
132
- out[1] = ry;
133
- out[2] = rz;
134
- }
135
-
136
- /**
137
- * Generates a directed botanical `TreeSkeleton` from a `TreeSpeciesConfig`, combining:
138
- * - Multi-level parametric Cubic Hermite spine generation (`BranchLevelConfig`, `AnimationCurve` profiles)
139
- * - Botanical phyllotaxis (`SpiralGolden`, `AlternateDistichous`, `Tristichous`, `Opposite`, `OppositeDecussate`, `Whorled`)
140
- * - Gravitropism (toward `-Y`), phototropism (toward `+Y` and outward crown exterior), and 3D `curl_noise_3d` gnarl
141
- * - Dichotomous / sympodial trunk & bough splitting
142
- * - Space colonization crown competition via `PointHashGrid3`
143
- * - Rotation-Minimizing Frames (`spine_frame_double_reflection`)
144
- * - Pipe Model cross-section radius accumulation (`tree_skeleton_compute_pipe_radii`)
145
- *
146
- * @param {TreeSpeciesConfig|Object} [config]
147
- * @param {TreeSkeleton} [out]
148
- * @returns {TreeSkeleton}
149
- */
150
- export function tree_skeleton_grow(config = new TreeSpeciesConfig(), out = new TreeSkeleton()) {
151
- const species = config && config.isTreeSpeciesConfig ? config : TreeSpeciesConfig.from(config);
152
- out.clear();
153
-
154
- const rng = seededRandom(species.seed | 0);
155
- // Seed-derived 3D noise offset so unseeded Simplex/Curl fields are unique and deterministic per seed
156
- const noise_ox = (rng() - 0.5) * 1000.0;
157
- const noise_oy = (rng() - 0.5) * 1000.0;
158
- const noise_oz = (rng() - 0.5) * 1000.0;
159
-
160
- const crown = species.crown;
161
- const crown_cy = crown.base_height + crown.height * 0.5;
162
- out.crown_center[0] = 0.0;
163
- out.crown_center[1] = crown_cy;
164
- out.crown_center[2] = 0.0;
165
-
166
- out.crown_radii[0] = Math.max(0.25, crown.radius);
167
- out.crown_radii[1] = Math.max(0.25, crown.height * 0.5);
168
- out.crown_radii[2] = Math.max(0.25, crown.radius);
169
-
170
- // Build Space Colonization attraction point cloud + PointHashGrid3 once
171
- const use_space_col = crown.space_colonization_weight > 0.0 && crown.attraction_point_count > 0;
172
- const attr_count = use_space_col ? (crown.attraction_point_count | 0) : 0;
173
- const attr_positions = new Float32Array(attr_count * 3);
174
- const attr_alive = new Uint8Array(attr_count);
175
- let attr_grid = null;
176
-
177
- if (use_space_col) {
178
- let placed = 0;
179
- let attempts = 0;
180
- const max_attempts = attr_count * 12;
181
- while (placed < attr_count && attempts < max_attempts) {
182
- attempts++;
183
- const u_h = rng();
184
- const env_r = crown.radius * crown_envelope_evaluate(crown.envelope_type, u_h);
185
- const rx = (rng() * 2.0 - 1.0) * env_r;
186
- const rz = (rng() * 2.0 - 1.0) * env_r;
187
- if (rx * rx + rz * rz <= env_r * env_r) {
188
- const ry = crown.base_height + u_h * crown.height;
189
- attr_positions[placed * 3] = rx;
190
- attr_positions[placed * 3 + 1] = ry;
191
- attr_positions[placed * 3 + 2] = rz;
192
- attr_alive[placed] = 1;
193
- placed++;
194
- }
195
- }
196
- while (placed < attr_count) {
197
- const theta = rng() * PI2;
198
- const u_h = rng();
199
- const r = Math.sqrt(rng()) * crown.radius * crown_envelope_evaluate(crown.envelope_type, u_h);
200
- attr_positions[placed * 3] = Math.cos(theta) * r;
201
- attr_positions[placed * 3 + 1] = crown.base_height + u_h * crown.height;
202
- attr_positions[placed * 3 + 2] = Math.sin(theta) * r;
203
- attr_alive[placed] = 1;
204
- placed++;
205
- }
206
-
207
- attr_grid = new PointHashGrid3();
208
- attr_grid.build(attr_positions, attr_count, Math.max(0.25, crown.attraction_radius));
209
- }
210
-
211
- const r_influence_sq = crown.attraction_radius * crown.attraction_radius;
212
- const r_kill_sq = crown.kill_radius * crown.kill_radius;
213
-
214
- // Queue of branches to grow in level-order (guarantees parent branches & vertices precede children)
215
- const queue = [];
216
-
217
- const lvl0 = species.levels[0];
218
- if (!lvl0) {
219
- return out;
220
- }
221
-
222
- const trunk_len = Math.max(0.2, lvl0.length * (1.0 + (rng() * 2.0 - 1.0) * lvl0.length_variance));
223
- const trunk_base_r = Math.max(0.01, trunk_len * lvl0.radius_ratio);
224
-
225
- evaluate_trunk_guide(SCRATCH_GUIDE_P0, SCRATCH_GUIDE_T, species.trunk_guide, 0.0, trunk_len);
226
-
227
- // Apply subtle initial basal trunk lean around the guide tangent
228
- const max_lean = lvl0.phyllotaxis_mode === PhyllotaxisMode.Whorled ? 0.025 : 0.06;
229
- const lean_ang = clamp((lvl0.basal_lean ?? 0.02) * (0.75 + 0.5 * rng()), 0.0, max_lean);
230
- const lean_az = rng() * PI2;
231
- const init_tx = clamp(SCRATCH_GUIDE_T[0] + Math.sin(lean_ang) * Math.cos(lean_az), -0.95, 0.95);
232
- const init_ty = Math.max(0.2, SCRATCH_GUIDE_T[1] * Math.cos(lean_ang));
233
- const init_tz = clamp(SCRATCH_GUIDE_T[2] + Math.sin(lean_ang) * Math.sin(lean_az), -0.95, 0.95);
234
- const init_t_len = Math.hypot(init_tx, init_ty, init_tz) || 1.0;
235
- const norm_tx = init_tx / init_t_len;
236
- const norm_ty = init_ty / init_t_len;
237
- const norm_tz = init_tz / init_t_len;
238
-
239
- // Orthonormal initial R perpendicular to norm_T
240
- let init_rx = 1.0 - norm_tx * norm_tx;
241
- let init_ry = -norm_ty * norm_tx;
242
- let init_rz = -norm_tz * norm_tx;
243
- const init_r_len = Math.hypot(init_rx, init_ry, init_rz) || 1.0;
244
- init_rx /= init_r_len;
245
- init_ry /= init_r_len;
246
- init_rz /= init_r_len;
247
-
248
- queue.push({
249
- level: 0,
250
- parent_branch_id: -1,
251
- parent_edge_id: -1,
252
- parent_attach_t: 0.0,
253
- start_vertex: -1,
254
- px: SCRATCH_GUIDE_P0[0],
255
- py: SCRATCH_GUIDE_P0[1],
256
- pz: SCRATCH_GUIDE_P0[2],
257
- tx: norm_tx,
258
- ty: norm_ty,
259
- tz: norm_tz,
260
- rx: init_rx,
261
- ry: init_ry,
262
- rz: init_rz,
263
- length: trunk_len,
264
- base_radius: trunk_base_r,
265
- start_seg: 0,
266
- total_segs: Math.max(1, lvl0.segments),
267
- initial_arc_s: 0.0,
268
- has_split: false
269
- });
270
-
271
- const rot_scratch = [0, 0, 0];
272
-
273
- for (let q_idx = 0; q_idx < queue.length; q_idx++) {
274
- const job = queue[q_idx];
275
- const level = job.level;
276
- const lvl_cfg = species.levels[level];
277
- if (!lvl_cfg) continue;
278
-
279
- const phase_hash = rng();
280
- const N_seg = job.total_segs;
281
- const remaining_segs = N_seg - job.start_seg;
282
- if (remaining_segs <= 0) continue;
283
-
284
- const step_len = job.length / N_seg;
285
-
286
- let cur_px = job.px;
287
- let cur_py = job.py;
288
- let cur_pz = job.pz;
289
- let cur_tx = job.tx;
290
- let cur_ty = job.ty;
291
- let cur_tz = job.tz;
292
- let cur_rx = job.rx;
293
- let cur_ry = job.ry;
294
- let cur_rz = job.rz;
295
- let cur_arc_s = job.initial_arc_s;
296
-
297
- // Compute binormal S = T x R
298
- let cur_sx = cur_ty * cur_rz - cur_tz * cur_ry;
299
- let cur_sy = cur_tz * cur_rx - cur_tx * cur_rz;
300
- let cur_sz = cur_tx * cur_ry - cur_ty * cur_rx;
301
-
302
- let prev_v = job.start_vertex;
303
- let branch_id = -1;
304
-
305
- if (prev_v < 0) {
306
- const t0 = job.start_seg / N_seg;
307
- const r0_profile = Math.max(0.05, lvl_cfg.radius_profile.evaluate(t0));
308
- const r0 = Math.max(0.002, job.base_radius * Math.max(1.0 - lvl_cfg.taper * t0, 0.05) * r0_profile);
309
-
310
- branch_id = out.add_branch(
311
- 0,
312
- level,
313
- job.parent_branch_id,
314
- job.parent_edge_id,
315
- job.parent_attach_t,
316
- job.length,
317
- r0,
318
- phase_hash
319
- );
320
- prev_v = out.add_node(
321
- cur_px, cur_py, cur_pz,
322
- cur_tx, cur_ty, cur_tz,
323
- cur_rx, cur_ry, cur_rz,
324
- r0,
325
- branch_id,
326
- level,
327
- job.parent_branch_id,
328
- t0,
329
- cur_arc_s,
330
- phase_hash
331
- );
332
- if (job.attach_parent_vertex !== undefined && job.attach_parent_vertex >= 0) {
333
- out.connect_nodes(job.attach_parent_vertex, prev_v);
334
- }
335
- out.branch_root_vertex[branch_id] = prev_v;
336
- out.branch_tip_vertex[branch_id] = prev_v;
337
- } else {
338
- const prev_r = out.node_read_radius(prev_v);
339
- branch_id = out.add_branch(
340
- prev_v,
341
- level,
342
- job.parent_branch_id,
343
- job.parent_edge_id,
344
- job.parent_attach_t,
345
- job.length,
346
- prev_r,
347
- phase_hash
348
- );
349
- }
350
-
351
- const branch_edges = [];
352
- const branch_edge_t0 = [];
353
- const branch_edge_t1 = [];
354
- const branch_vertices = [prev_v];
355
-
356
- // Pre-schedule phyllotactic child attachment parameters so the parent spine kinks sympodially at lateral emissions
357
- const next_level_preview = level + 1;
358
- const has_children = next_level_preview < species.levels.length && lvl_cfg.child_count > 0;
359
- const span_fraction_est = job.child_span_fraction ?? Math.max(0.25, (N_seg - job.start_seg) / Math.max(1, N_seg));
360
- const planned_child_count = has_children
361
- ? (span_fraction_est < 0.99 ? Math.max(1, Math.round(lvl_cfg.child_count * span_fraction_est)) : lvl_cfg.child_count)
362
- : 0;
363
- const bare_est = clamp(lvl_cfg.bare_length_ratio, 0.0, 0.92);
364
- const t_min_est = Math.max(bare_est, job.start_seg / N_seg);
365
- const base_phi = rng() * PI2;
366
- const scheduled_children = [];
367
-
368
- if (planned_child_count > 0 && 1.0 > t_min_est + 1e-3) {
369
- for (let c = 0; c < planned_child_count; c++) {
370
- let u_c = (c + 0.5) / planned_child_count;
371
- if (lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.Opposite
372
- || lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.OppositeDecussate) {
373
- const pair_idx = c >> 1;
374
- const pair_count = Math.max(1, Math.ceil(planned_child_count / 2));
375
- u_c = (pair_idx + 0.5) / pair_count;
376
- } else if (lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.Whorled) {
377
- const w_size = Math.max(1, lvl_cfg.whorl_count | 0);
378
- const whorl_idx = Math.floor(c / w_size);
379
- const whorl_total = Math.max(1, Math.ceil(planned_child_count / w_size));
380
- u_c = (whorl_idx + 0.5) / whorl_total;
381
- }
382
-
383
- const t_att = clamp(
384
- lerp(t_min_est, 0.96, u_c) + (rng() - 0.5) * (0.3 / planned_child_count),
385
- t_min_est,
386
- 0.98
387
- );
388
-
389
- let phi_c = base_phi;
390
- switch (lvl_cfg.phyllotaxis_mode) {
391
- case PhyllotaxisMode.AlternateDistichous:
392
- phi_c += c * Math.PI;
393
- break;
394
- case PhyllotaxisMode.Tristichous:
395
- phi_c += c * (PI2 / 3.0);
396
- break;
397
- case PhyllotaxisMode.Opposite:
398
- phi_c += (c & 1) * Math.PI;
399
- break;
400
- case PhyllotaxisMode.OppositeDecussate:
401
- phi_c += (c & 1) * Math.PI + (c >> 1) * (Math.PI * 0.5);
402
- break;
403
- case PhyllotaxisMode.Whorled: {
404
- const w_size = Math.max(1, lvl_cfg.whorl_count | 0);
405
- const w_idx = Math.floor(c / w_size);
406
- const sub_idx = c % w_size;
407
- phi_c += (sub_idx * PI2) / w_size + (w_idx * Math.PI) / w_size;
408
- break;
409
- }
410
- case PhyllotaxisMode.SpiralGolden:
411
- default:
412
- phi_c += c * GOLDEN_ANGLE;
413
- break;
414
- }
415
- phi_c += (rng() * 2.0 - 1.0) * lvl_cfg.azimuth_jitter;
416
- scheduled_children.push({ t_attach: t_att, phi: phi_c });
417
- }
418
- }
419
-
420
- const k_symp = lvl_cfg.nodal_kink_strength ?? 0.45;
421
- const zig_base = lvl_cfg.internode_zigzag ?? 0.06;
422
- const tip_up = lvl_cfg.tip_upcurl ?? 0.18;
423
-
424
- for (let s = job.start_seg + 1; s <= N_seg; s++) {
425
- const t_prev = (s - 1) / N_seg;
426
- const t_curr = s / N_seg;
427
-
428
- // 1. Intrinsic S-curve pitch curvature (zero-mean higher harmonic on trunk so trunk weaves rather than leaning)
429
- const s_wave = level === 0
430
- ? (lvl_cfg.curvature * Math.sin(PI2 * 1.5 * t_curr + phase_hash * PI2))
431
- : (lvl_cfg.curve_back !== 0.0
432
- ? (lvl_cfg.curvature * Math.cos(Math.PI * t_curr) + lvl_cfg.curve_back * Math.sin(PI2 * t_curr))
433
- : lvl_cfg.curvature);
434
- const d_pitch = s_wave / Math.max(2, N_seg * 0.65);
435
-
436
- if (Math.abs(d_pitch) > 1e-6) {
437
- rotate_around_axis(rot_scratch, cur_tx, cur_ty, cur_tz, cur_sx, cur_sy, cur_sz, d_pitch);
438
- cur_tx = rot_scratch[0];
439
- cur_ty = rot_scratch[1];
440
- cur_tz = rot_scratch[2];
441
- rotate_around_axis(rot_scratch, cur_rx, cur_ry, cur_rz, cur_sx, cur_sy, cur_sz, d_pitch);
442
- cur_rx = rot_scratch[0];
443
- cur_ry = rot_scratch[1];
444
- cur_rz = rot_scratch[2];
445
- }
446
-
447
- // 2. Helical spiral twist around T
448
- if (Math.abs(lvl_cfg.spiral_twist) > 1e-6) {
449
- const d_twist = lvl_cfg.spiral_twist / N_seg;
450
- rotate_around_axis(rot_scratch, cur_rx, cur_ry, cur_rz, cur_tx, cur_ty, cur_tz, d_twist);
451
- cur_rx = rot_scratch[0];
452
- cur_ry = rot_scratch[1];
453
- cur_rz = rot_scratch[2];
454
- cur_sx = cur_ty * cur_rz - cur_tz * cur_ry;
455
- cur_sy = cur_tz * cur_rx - cur_tx * cur_rz;
456
- cur_sz = cur_tx * cur_ry - cur_ty * cur_rx;
457
- }
458
-
459
- // 2b. Sympodial Nodal Kinking at scheduled lateral child emissions + Internodal Zig-Zag
460
- let kink_rx = 0.0;
461
- let kink_ry = 0.0;
462
- let kink_rz = 0.0;
463
-
464
- if (k_symp > 0.0 && scheduled_children.length > 0 && lvl_cfg.phyllotaxis_mode !== PhyllotaxisMode.Whorled) {
465
- for (let c = 0; c < scheduled_children.length; c++) {
466
- const sc = scheduled_children[c];
467
- if (sc.t_attach >= t_prev && sc.t_attach <= t_curr) {
468
- // Recoil away from child branch emission azimuth phi
469
- const recoil_phi = sc.phi + Math.PI;
470
- const c_recoil = Math.cos(recoil_phi);
471
- const s_recoil = Math.sin(recoil_phi);
472
- const amp = clamp(0.18 * k_symp * Math.sin(lvl_cfg.insertion_angle), 0.0, 0.32);
473
- kink_rx += (c_recoil * cur_rx + s_recoil * cur_sx) * amp;
474
- kink_ry += (c_recoil * cur_ry + s_recoil * cur_sy) * amp;
475
- kink_rz += (c_recoil * cur_rz + s_recoil * cur_sz) * amp;
476
- }
477
- }
478
- }
479
-
480
- if (zig_base > 0.0) {
481
- // Alternate by PI on trunk so consecutive internode kinks balance around the plumb line
482
- const zig_step = (level === 0 || lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.AlternateDistichous)
483
- ? Math.PI
484
- : GOLDEN_ANGLE;
485
- const zig_phi = base_phi + s * zig_step + (rng() - 0.5) * 0.55;
486
- const zig_amp = zig_base * (0.65 + 0.70 * rng()) * (0.65 + 0.35 * t_curr);
487
- const cz = Math.cos(zig_phi);
488
- const sz = Math.sin(zig_phi);
489
- kink_rx += (cz * cur_rx + sz * cur_sx) * zig_amp;
490
- kink_ry += (cz * cur_ry + sz * cur_sy) * zig_amp;
491
- kink_rz += (cz * cur_rz + sz * cur_sz) * zig_amp;
492
- }
493
-
494
- const kink_mag = Math.hypot(kink_rx, kink_ry, kink_rz);
495
- if (kink_mag > 1e-6) {
496
- // Rotation axis B = T x (kink_dir) in the (R, S) plane
497
- const kx = kink_rx / kink_mag;
498
- const ky = kink_ry / kink_mag;
499
- const kz = kink_rz / kink_mag;
500
- let bx = cur_ty * kz - cur_tz * ky;
501
- let by = cur_tz * kx - cur_tx * kz;
502
- let bz = cur_tx * ky - cur_ty * kx;
503
- const b_len = Math.hypot(bx, by, bz);
504
- if (b_len > 1e-6) {
505
- bx /= b_len;
506
- by /= b_len;
507
- bz /= b_len;
508
- const clamped_kink = Math.min(0.42, kink_mag);
509
- rotate_around_axis(rot_scratch, cur_tx, cur_ty, cur_tz, bx, by, bz, clamped_kink);
510
- cur_tx = rot_scratch[0];
511
- cur_ty = rot_scratch[1];
512
- cur_tz = rot_scratch[2];
513
- rotate_around_axis(rot_scratch, cur_rx, cur_ry, cur_rz, bx, by, bz, clamped_kink);
514
- cur_rx = rot_scratch[0];
515
- cur_ry = rot_scratch[1];
516
- cur_rz = rot_scratch[2];
517
- cur_sx = cur_ty * cur_rz - cur_tz * cur_ry;
518
- cur_sy = cur_tz * cur_rx - cur_tx * cur_rz;
519
- cur_sz = cur_tx * cur_ry - cur_ty * cur_rx;
520
- }
521
- }
522
-
523
- // 3. Biomechanical Tropism steering (Gravitropic sag + Terminal Phototropic Up-Curl + Outward spread)
524
- let steer_x = 0.0;
525
- let steer_y = 0.0;
526
- let steer_z = 0.0;
527
-
528
- if (level === 0) {
529
- // Orthotropic upward striving & plumb-line righting reflex along guide curve (default vertical +Y)
530
- evaluate_trunk_guide(SCRATCH_GUIDE_P0, SCRATCH_GUIDE_T, species.trunk_guide, t_prev, job.length);
531
- evaluate_trunk_guide(SCRATCH_GUIDE_P1, SCRATCH_GUIDE_T, species.trunk_guide, t_curr, job.length);
532
- const gtx = SCRATCH_GUIDE_T[0];
533
- const gty = SCRATCH_GUIDE_T[1];
534
- const gtz = SCRATCH_GUIDE_T[2];
535
-
536
- const righting = 1.65 + Math.max(0.0, -lvl_cfg.gravitropism) * 2.5 + Math.max(0.0, lvl_cfg.phototropism);
537
- steer_x += gtx * righting;
538
- steer_y += gty * righting;
539
- steer_z += gtz * righting;
540
-
541
- // Plumb-line centering force pulling lateral drift back toward the guide curve
542
- const dx_g = cur_px - SCRATCH_GUIDE_P0[0];
543
- const dy_g = cur_py - SCRATCH_GUIDE_P0[1];
544
- const dz_g = cur_pz - SCRATCH_GUIDE_P0[2];
545
- const dot_g = dx_g * gtx + dy_g * gty + dz_g * gtz;
546
- const lat_x = dx_g - dot_g * gtx;
547
- const lat_y = dy_g - dot_g * gty;
548
- const lat_z = dz_g - dot_g * gtz;
549
- const plumb_k = (lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.Whorled ? 5.2 : 3.4) / Math.max(1.0, job.length);
550
- steer_x -= lat_x * plumb_k * N_seg * 0.5;
551
- steer_y -= lat_y * plumb_k * N_seg * 0.5;
552
- steer_z -= lat_z * plumb_k * N_seg * 0.5;
553
- } else {
554
- if (lvl_cfg.gravitropism !== 0.0 || tip_up !== 0.0) {
555
- if (crown.envelope_type === CrownEnvelopeType.Weeping && level >= 2) {
556
- steer_y -= lvl_cfg.gravitropism * (0.4 + 1.2 * t_curr);
557
- } else {
558
- // Cantilever sag in mid-span followed by phototropic up-curl near branch tips
559
- const sag_term = lvl_cfg.gravitropism * 3.0 * t_curr * Math.pow(Math.max(0.0, 1.0 - t_curr), 0.75);
560
- const up_term = tip_up * Math.pow(t_curr, 1.5);
561
- steer_y += up_term - sag_term;
562
- }
563
- }
564
-
565
- if (lvl_cfg.phototropism !== 0.0) {
566
- const raw_sky_mix = clamp(crown.sky_phototropism_mix ?? 0.6, 0.0, 1.0);
567
- const sky_mix = level === 1 ? raw_sky_mix * 0.45 : raw_sky_mix;
568
- const horiz_r = Math.sqrt(cur_px * cur_px + cur_pz * cur_pz);
569
- const out_x = horiz_r > 1e-4 ? cur_px / horiz_r : cur_rx;
570
- const out_z = horiz_r > 1e-4 ? cur_pz / horiz_r : cur_rz;
571
-
572
- steer_x += lvl_cfg.phototropism * (1.0 - sky_mix) * out_x;
573
- steer_y += lvl_cfg.phototropism * sky_mix;
574
- steer_z += lvl_cfg.phototropism * (1.0 - sky_mix) * out_z;
575
- }
576
- }
577
-
578
- // 4. 2-Octave 3D Curl Noise gnarl scaled properly by sqrt(6 / N_seg)
579
- if (lvl_cfg.gnarl_strength > 0.0) {
580
- const freq = lvl_cfg.gnarl_frequency;
581
- curl_noise_3d(
582
- SCRATCH_CURL,
583
- cur_px * freq + noise_ox + level * 17.3,
584
- cur_py * freq + noise_oy + level * 31.7,
585
- cur_pz * freq + noise_oz + level * 47.1
586
- );
587
- const c1x = SCRATCH_CURL[0], c1y = SCRATCH_CURL[1], c1z = SCRATCH_CURL[2];
588
- curl_noise_3d(
589
- SCRATCH_CURL,
590
- cur_px * freq * 2.15 + noise_ox + 53.1 + phase_hash * 29.3,
591
- cur_py * freq * 2.15 + noise_oy + 79.3,
592
- cur_pz * freq * 2.15 + noise_oz + 113.7
593
- );
594
- const cx_tot = 0.68 * c1x + 0.32 * SCRATCH_CURL[0];
595
- const cy_tot = 0.68 * c1y + 0.32 * SCRATCH_CURL[1];
596
- const cz_tot = 0.68 * c1z + 0.32 * SCRATCH_CURL[2];
597
-
598
- const gnarl_scale = lvl_cfg.gnarl_strength * Math.sqrt(6.0 / Math.max(2, N_seg));
599
- // Project curl vector perpendicular to T so it steers direction cleanly
600
- const dot_ct = cx_tot * cur_tx + cy_tot * cur_ty + cz_tot * cur_tz;
601
- steer_x += (cx_tot - dot_ct * cur_tx) * gnarl_scale * 2.2;
602
- steer_y += (cy_tot - dot_ct * cur_ty) * gnarl_scale * 1.6;
603
- steer_z += (cz_tot - dot_ct * cur_tz) * gnarl_scale * 2.2;
604
- }
605
-
606
- // 5. Space colonization attraction steering (for branches in crown, level >= 1)
607
- if (use_space_col && level >= 1 && attr_grid !== null) {
608
- let attr_vx = 0.0;
609
- let attr_vy = 0.0;
610
- let attr_vz = 0.0;
611
- let attr_hits = 0;
612
-
613
- attr_grid.forEachNear(cur_px, cur_py, cur_pz, (idx) => {
614
- if (attr_alive[idx] === 0) return;
615
- const ax = attr_positions[idx * 3] - cur_px;
616
- const ay = attr_positions[idx * 3 + 1] - cur_py;
617
- const az = attr_positions[idx * 3 + 2] - cur_pz;
618
- const d2 = ax * ax + ay * ay + az * az;
619
- if (d2 <= r_kill_sq) {
620
- attr_alive[idx] = 0;
621
- } else if (d2 <= r_influence_sq && d2 > 1e-8) {
622
- const inv_d = 1.0 / Math.sqrt(d2);
623
- attr_vx += ax * inv_d;
624
- attr_vy += ay * inv_d;
625
- attr_vz += az * inv_d;
626
- attr_hits++;
627
- }
628
- });
629
-
630
- if (attr_hits > 0) {
631
- const attr_len = Math.sqrt(attr_vx * attr_vx + attr_vy * attr_vy + attr_vz * attr_vz);
632
- if (attr_len > 1e-6) {
633
- const w_sc = crown.space_colonization_weight / attr_len;
634
- steer_x += attr_vx * w_sc;
635
- steer_y += attr_vy * w_sc;
636
- steer_z += attr_vz * w_sc;
637
- }
638
- }
639
- }
640
-
641
- // 6. Crown envelope soft containment for lateral branches (level >= 1)
642
- let seg_len = step_len;
643
- if (level >= 1 && crown.prune_ratio > 0.0) {
644
- const probe_x = cur_px + cur_tx * step_len;
645
- const probe_y = cur_py + cur_ty * step_len;
646
- const probe_z = cur_pz + cur_tz * step_len;
647
- const sdf = crown_envelope_sdf(probe_x, probe_y, probe_z, crown);
648
- if (sdf > 0.0) {
649
- // Gently steer back toward crown center and shorten segment near boundary
650
- const to_cx = -probe_x;
651
- const to_cy = crown_cy - probe_y;
652
- const to_cz = -probe_z;
653
- const to_c_len = Math.sqrt(to_cx * to_cx + to_cy * to_cy + to_cz * to_cz);
654
- if (to_c_len > 1e-5) {
655
- const pull = crown.prune_ratio * 0.45 / to_c_len;
656
- steer_x += to_cx * pull;
657
- steer_y += to_cy * pull;
658
- steer_z += to_cz * pull;
659
- }
660
- seg_len *= Math.max(0.35, 1.0 - crown.prune_ratio * 0.5);
661
- }
662
- }
663
-
664
- // Apply steering to tangent and normalize
665
- const steer_div = Math.max(2.0, Math.sqrt(N_seg) * 1.35);
666
- let next_tx = cur_tx + steer_x / steer_div;
667
- let next_ty = cur_ty + steer_y / steer_div;
668
- let next_tz = cur_tz + steer_z / steer_div;
669
- const t_len = Math.sqrt(next_tx * next_tx + next_ty * next_ty + next_tz * next_tz);
670
- if (t_len > 1e-12) {
671
- next_tx /= t_len;
672
- next_ty /= t_len;
673
- next_tz /= t_len;
674
- } else {
675
- next_tx = cur_tx;
676
- next_ty = cur_ty;
677
- next_tz = cur_tz;
678
- }
679
-
680
- let next_px = cur_px + 0.5 * (cur_tx + next_tx) * seg_len;
681
- let next_py = cur_py + 0.5 * (cur_ty + next_ty) * seg_len;
682
- let next_pz = cur_pz + 0.5 * (cur_tz + next_tz) * seg_len;
683
-
684
- // Orthotropic plumb-tube containment on trunk (level === 0):
685
- // Preserves local 3D nodal kinks and serpentine sweep while guaranteeing the trunk never drifts sideways
686
- if (level === 0 && branch_id === 0) {
687
- const gx = SCRATCH_GUIDE_P1[0];
688
- const gy = SCRATCH_GUIDE_P1[1];
689
- const gz = SCRATCH_GUIDE_P1[2];
690
- const gtx = SCRATCH_GUIDE_T[0];
691
- const gty = SCRATCH_GUIDE_T[1];
692
- const gtz = SCRATCH_GUIDE_T[2];
693
-
694
- const dx = next_px - gx;
695
- const dy = next_py - gy;
696
- const dz = next_pz - gz;
697
- const dot_g = dx * gtx + dy * gty + dz * gtz;
698
- const off_x = dx - dot_g * gtx;
699
- const off_y = dy - dot_g * gty;
700
- const off_z = dz - dot_g * gtz;
701
- const off_r = Math.hypot(off_x, off_y, off_z);
702
-
703
- const is_conifer = lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.Whorled;
704
- const max_drift_frac = is_conifer ? 0.032 : 0.058;
705
- // Envelope rises from 0 at root, peaks in mid-trunk (allowing 3D sweep/crookedness), and tapers near tip
706
- const env = Math.sin(Math.PI * Math.min(0.92, t_curr * 0.88)) * (1.0 - 0.38 * t_curr * t_curr);
707
- const max_r = job.length * max_drift_frac * Math.max(0.18, env);
708
-
709
- if (off_r > max_r && off_r > 1e-6) {
710
- const scale_r = max_r / off_r;
711
- next_px = gx + dot_g * gtx + off_x * scale_r;
712
- next_py = gy + dot_g * gty + off_y * scale_r;
713
- next_pz = gz + dot_g * gtz + off_z * scale_r;
714
-
715
- let ch_x = next_px - cur_px;
716
- let ch_y = next_py - cur_py;
717
- let ch_z = next_pz - cur_pz;
718
- const ch_l = Math.hypot(ch_x, ch_y, ch_z);
719
- if (ch_l > 1e-6) {
720
- next_tx = ch_x / ch_l;
721
- next_ty = ch_y / ch_l;
722
- next_tz = ch_z / ch_l;
723
- }
724
- }
725
- }
726
-
727
- // Transport RMF via Wang et al. Double Reflection
728
- spine_frame_double_reflection(
729
- SCRATCH_RMF, 0,
730
- cur_px, cur_py, cur_pz,
731
- cur_tx, cur_ty, cur_tz,
732
- cur_rx, cur_ry, cur_rz,
733
- next_px, next_py, next_pz,
734
- next_tx, next_ty, next_tz
735
- );
736
- let next_rx = SCRATCH_RMF[0];
737
- let next_ry = SCRATCH_RMF[1];
738
- let next_rz = SCRATCH_RMF[2];
739
- cur_sx = SCRATCH_RMF[3];
740
- cur_sy = SCRATCH_RMF[4];
741
- cur_sz = SCRATCH_RMF[5];
742
-
743
- cur_arc_s += seg_len;
744
-
745
- const r_prof = Math.max(0.04, lvl_cfg.radius_profile.evaluate(t_curr));
746
- const r_curr = Math.max(
747
- 0.0015,
748
- job.base_radius * Math.max(1.0 - lvl_cfg.taper * t_curr, 0.05) * r_prof
749
- );
750
-
751
- const curr_v = out.add_node(
752
- next_px, next_py, next_pz,
753
- next_tx, next_ty, next_tz,
754
- next_rx, next_ry, next_rz,
755
- r_curr,
756
- branch_id,
757
- level,
758
- job.parent_branch_id,
759
- t_curr,
760
- cur_arc_s,
761
- phase_hash
762
- );
763
-
764
- const edge_id = out.add_segment(
765
- prev_v,
766
- curr_v,
767
- cur_tx * seg_len,
768
- cur_ty * seg_len,
769
- cur_tz * seg_len,
770
- next_tx * seg_len,
771
- next_ty * seg_len,
772
- next_tz * seg_len,
773
- seg_len,
774
- t_prev === 0.0 ? job.parent_attach_t : t_prev
775
- );
776
-
777
- branch_edges.push(edge_id);
778
- branch_edge_t0.push(t_prev);
779
- branch_edge_t1.push(t_curr);
780
- branch_vertices.push(curr_v);
781
-
782
- out.branch_tip_vertex[branch_id] = curr_v;
783
- out.branch_length[branch_id] = cur_arc_s - job.initial_arc_s;
784
-
785
- // Check for dichotomous / sympodial splitting at interior segments
786
- const min_split_seg = level === 0
787
- ? Math.max(2, Math.floor(N_seg * 0.5))
788
- : Math.max(1, Math.floor(N_seg * 0.25));
789
- if (
790
- !job.has_split
791
- && lvl_cfg.split_probability > 0.0
792
- && s >= min_split_seg
793
- && s <= Math.floor(N_seg * 0.75)
794
- && rng() < lvl_cfg.split_probability
795
- ) {
796
- job.has_split = true;
797
- const fork_count = Math.max(2, lvl_cfg.split_count | 0);
798
- const split_ang = lvl_cfg.split_angle;
799
- const base_az = rng() * PI2;
800
-
801
- // Preserve pre-split orthonormal triad (T, R, S) before deflecting fork 0
802
- const pre_tx = next_tx, pre_ty = next_ty, pre_tz = next_tz;
803
- const pre_rx = next_rx, pre_ry = next_ry, pre_rz = next_rz;
804
- const pre_sx = cur_sx, pre_sy = cur_sy, pre_sz = cur_sz;
805
-
806
- // Current branch deflects as fork 0: rotate both T and R around bend_axis so R remains orthogonal to T
807
- const fork0_deflect = level === 0 ? split_ang * 0.22 : split_ang * 0.5;
808
- const bend_axis_x = Math.cos(base_az) * pre_rx + Math.sin(base_az) * pre_sx;
809
- const bend_axis_y = Math.cos(base_az) * pre_ry + Math.sin(base_az) * pre_sy;
810
- const bend_axis_z = Math.cos(base_az) * pre_rz + Math.sin(base_az) * pre_sz;
811
- rotate_around_axis(
812
- rot_scratch,
813
- pre_tx, pre_ty, pre_tz,
814
- bend_axis_x, bend_axis_y, bend_axis_z,
815
- fork0_deflect
816
- );
817
- next_tx = rot_scratch[0];
818
- next_ty = rot_scratch[1];
819
- next_tz = rot_scratch[2];
820
-
821
- rotate_around_axis(
822
- rot_scratch,
823
- pre_rx, pre_ry, pre_rz,
824
- bend_axis_x, bend_axis_y, bend_axis_z,
825
- fork0_deflect
826
- );
827
- next_rx = rot_scratch[0];
828
- next_ry = rot_scratch[1];
829
- next_rz = rot_scratch[2];
830
-
831
- cur_sx = next_ty * next_rz - next_tz * next_ry;
832
- cur_sy = next_tz * next_rx - next_tx * next_rz;
833
- cur_sz = next_tx * next_ry - next_ty * next_rx;
834
-
835
- // Spawn fork 1..fork_count-1 as continuation branches with their own root vertex connected to curr_v
836
- const rem_segs = Math.max(1, N_seg - s);
837
- const rem_fraction = Math.max(0.15, rem_segs / Math.max(1, N_seg));
838
- for (let f = 1; f < fork_count; f++) {
839
- const f_az = base_az + (f * PI2) / fork_count;
840
- const fax = Math.cos(f_az) * pre_rx + Math.sin(f_az) * pre_sx;
841
- const fay = Math.cos(f_az) * pre_ry + Math.sin(f_az) * pre_sy;
842
- const faz = Math.cos(f_az) * pre_rz + Math.sin(f_az) * pre_sz;
843
-
844
- const f_tan = [0, 0, 0];
845
- rotate_around_axis(f_tan, pre_tx, pre_ty, pre_tz, fax, fay, faz, split_ang * 0.65);
846
-
847
- const f_r = [0, 0, 0];
848
- rotate_around_axis(f_r, pre_rx, pre_ry, pre_rz, fax, fay, faz, split_ang * 0.65);
849
-
850
- queue.push({
851
- level,
852
- parent_branch_id: branch_id,
853
- parent_edge_id: edge_id,
854
- parent_attach_t: t_curr,
855
- attach_parent_vertex: curr_v,
856
- start_vertex: -1,
857
- px: next_px,
858
- py: next_py,
859
- pz: next_pz,
860
- tx: f_tan[0],
861
- ty: f_tan[1],
862
- tz: f_tan[2],
863
- rx: f_r[0],
864
- ry: f_r[1],
865
- rz: f_r[2],
866
- length: Math.max(0.15, job.length * rem_fraction),
867
- base_radius: r_curr * 0.78,
868
- start_seg: 0,
869
- total_segs: rem_segs,
870
- initial_arc_s: cur_arc_s,
871
- has_split: true,
872
- child_span_fraction: rem_fraction
873
- });
874
- }
875
- }
876
-
877
- cur_px = next_px;
878
- cur_py = next_py;
879
- cur_pz = next_pz;
880
- cur_tx = next_tx;
881
- cur_ty = next_ty;
882
- cur_tz = next_tz;
883
- cur_rx = next_rx;
884
- cur_ry = next_ry;
885
- cur_rz = next_rz;
886
- prev_v = curr_v;
887
- }
888
-
889
- // Smooth or preserve crisp interior Hermite tangents across consecutive edges of this branch
890
- // (preserving branch_edges[0]'s m0 so continuation forks keep their exact fork departure tangent)
891
- if (branch_edges.length >= 2) {
892
- const kink_sharp = clamp(lvl_cfg.kink_sharpness ?? 0.5, 0.0, 0.92);
893
- for (let k = 1; k < branch_edges.length; k++) {
894
- const e_prev = branch_edges[k - 1];
895
- const e_next = branch_edges[k];
896
- const v_mid = branch_vertices[k];
897
- const l_prev = Math.max(1e-5, out.segment_length(e_prev));
898
- const l_next = Math.max(1e-5, out.segment_length(e_next));
899
-
900
- out.segment_read_m1(SCRATCH_M_PREV, 0, e_prev);
901
- out.segment_read_m0(SCRATCH_M_NEXT, 0, e_next);
902
-
903
- const t_in_x = SCRATCH_M_PREV[0] / l_prev;
904
- const t_in_y = SCRATCH_M_PREV[1] / l_prev;
905
- const t_in_z = SCRATCH_M_PREV[2] / l_prev;
906
- const t_out_x = SCRATCH_M_NEXT[0] / l_next;
907
- const t_out_y = SCRATCH_M_NEXT[1] / l_next;
908
- const t_out_z = SCRATCH_M_NEXT[2] / l_next;
909
-
910
- let tx = t_in_x + t_out_x;
911
- let ty = t_in_y + t_out_y;
912
- let tz = t_in_z + t_out_z;
913
- const t_len = Math.sqrt(tx * tx + ty * ty + tz * tz);
914
- if (t_len > 1e-12) {
915
- tx /= t_len;
916
- ty /= t_len;
917
- tz /= t_len;
918
-
919
- out.node_write_tangent(v_mid, tx, ty, tz);
920
-
921
- // Blend between smooth C1 average tangent and crisp sympodial kink tangents
922
- let m1x = lerp(tx, t_in_x, kink_sharp);
923
- let m1y = lerp(ty, t_in_y, kink_sharp);
924
- let m1z = lerp(tz, t_in_z, kink_sharp);
925
- const m1_l = Math.sqrt(m1x * m1x + m1y * m1y + m1z * m1z);
926
- if (m1_l > 1e-12) { m1x /= m1_l; m1y /= m1_l; m1z /= m1_l; }
927
-
928
- let m0x = lerp(tx, t_out_x, kink_sharp);
929
- let m0y = lerp(ty, t_out_y, kink_sharp);
930
- let m0z = lerp(tz, t_out_z, kink_sharp);
931
- const m0_l = Math.sqrt(m0x * m0x + m0y * m0y + m0z * m0z);
932
- if (m0_l > 1e-12) { m0x /= m0_l; m0y /= m0_l; m0z /= m0_l; }
933
-
934
- out.segment_write_m1(e_prev, m1x * l_prev, m1y * l_prev, m1z * l_prev);
935
- out.segment_write_m0(e_next, m0x * l_next, m0y * l_next, m0z * l_next);
936
- }
937
- }
938
- }
939
-
940
- // Spawn lateral child branches at level + 1 using pre-scheduled attachments
941
- const next_level = level + 1;
942
- if (scheduled_children.length > 0 && branch_edges.length > 0) {
943
- const next_cfg = species.levels[next_level];
944
- const t_max = branch_edge_t1[branch_edge_t1.length - 1];
945
-
946
- for (let c = 0; c < scheduled_children.length; c++) {
947
- const sc = scheduled_children[c];
948
- const t_attach = clamp(sc.t_attach, branch_edge_t0[0], t_max * 0.985);
949
- const phi = sc.phi;
950
-
951
- // Find parent edge containing t_attach
952
- let seg_idx = 0;
953
- for (let i = 0; i < branch_edges.length; i++) {
954
- if (t_attach >= branch_edge_t0[i] && t_attach <= branch_edge_t1[i]) {
955
- seg_idx = i;
956
- break;
957
- }
958
- }
959
-
960
- const parent_e = branch_edges[seg_idx];
961
- const t0_e = branch_edge_t0[seg_idx];
962
- const t1_e = branch_edge_t1[seg_idx];
963
- const local_u = clamp((t_attach - t0_e) / Math.max(1e-5, t1_e - t0_e), 0.0, 1.0);
964
-
965
- out.evaluate_segment_position(SCRATCH_POS, 0, parent_e, local_u);
966
- out.evaluate_segment_tangent(SCRATCH_TAN, 0, parent_e, local_u);
967
-
968
- const p_v0 = branch_vertices[seg_idx];
969
- const p_v1 = branch_vertices[seg_idx + 1];
970
- const r_p0 = out.node_read_radius(p_v0);
971
- const r_p1 = out.node_read_radius(p_v1);
972
- const parent_r_at_attach = lerp(r_p0, r_p1, local_u);
973
-
974
- // Transport parent RMF R vector from p_v0 along parent_e to SCRATCH_POS at local_u
975
- out.node_read_position(SCRATCH_P0, 0, p_v0);
976
- out.node_read_tangent(SCRATCH_T0, 0, p_v0);
977
- out.node_read_frame_r(SCRATCH_R0, 0, p_v0);
978
- const p_rx = SCRATCH_R0[0];
979
- const p_ry = SCRATCH_R0[1];
980
- const p_rz = SCRATCH_R0[2];
981
-
982
- spine_frame_double_reflection(
983
- SCRATCH_RMF, 0,
984
- SCRATCH_P0[0], SCRATCH_P0[1], SCRATCH_P0[2],
985
- SCRATCH_T0[0], SCRATCH_T0[1], SCRATCH_T0[2],
986
- p_rx, p_ry, p_rz,
987
- SCRATCH_POS[0], SCRATCH_POS[1], SCRATCH_POS[2],
988
- SCRATCH_TAN[0], SCRATCH_TAN[1], SCRATCH_TAN[2]
989
- );
990
- const prx = SCRATCH_RMF[0], pry = SCRATCH_RMF[1], prz = SCRATCH_RMF[2];
991
- const psx = SCRATCH_RMF[3], psy = SCRATCH_RMF[4], psz = SCRATCH_RMF[5];
992
-
993
- // Radial direction D_rad in parent cross-section plane
994
- const cos_p = Math.cos(phi);
995
- const sin_p = Math.sin(phi);
996
- let drx = cos_p * prx + sin_p * psx;
997
- let dry = cos_p * pry + sin_p * psy;
998
- let drz = cos_p * prz + sin_p * psz;
999
-
1000
- // Plagiotropic flattening on lateral branches (non-weeping): prevent child shoots
1001
- // from pointing straight downward from the underside of boughs into the lower trunk
1002
- if (level >= 1 && crown.envelope_type !== CrownEnvelopeType.Weeping) {
1003
- dry = dry < 0.0 ? dry * 0.15 + 0.08 : dry * 0.75 + 0.12;
1004
- const dot_tp = drx * SCRATCH_TAN[0] + dry * SCRATCH_TAN[1] + drz * SCRATCH_TAN[2];
1005
- drx -= dot_tp * SCRATCH_TAN[0];
1006
- dry -= dot_tp * SCRATCH_TAN[1];
1007
- drz -= dot_tp * SCRATCH_TAN[2];
1008
- const d_len = Math.sqrt(drx * drx + dry * dry + drz * drz);
1009
- if (d_len > 1e-6) {
1010
- drx /= d_len;
1011
- dry /= d_len;
1012
- drz /= d_len;
1013
- } else {
1014
- drx = cos_p * prx + sin_p * psx;
1015
- dry = cos_p * pry + sin_p * psy;
1016
- drz = cos_p * prz + sin_p * psz;
1017
- }
1018
- }
1019
-
1020
- // Insertion polar angle alpha from parent tangent T_p toward D_rad
1021
- const alpha_scale = lvl_cfg.insertion_angle_profile.evaluate(t_attach);
1022
- const alpha = clamp(
1023
- lvl_cfg.insertion_angle * alpha_scale + (rng() * 2.0 - 1.0) * lvl_cfg.insertion_angle_variation,
1024
- 0.1,
1025
- Math.PI * 0.85
1026
- );
1027
-
1028
- const cos_a = Math.cos(alpha);
1029
- const sin_a = Math.sin(alpha);
1030
- let ctx = cos_a * SCRATCH_TAN[0] + sin_a * drx;
1031
- let cty = cos_a * SCRATCH_TAN[1] + sin_a * dry;
1032
- let ctz = cos_a * SCRATCH_TAN[2] + sin_a * drz;
1033
- const ct_len = Math.sqrt(ctx * ctx + cty * cty + ctz * ctz);
1034
- if (ct_len > 1e-12) {
1035
- ctx /= ct_len;
1036
- cty /= ct_len;
1037
- ctz /= ct_len;
1038
- }
1039
-
1040
- // Initial RMF radial vector for child branch:
1041
- // R_c = cos(alpha) * D_rad - sin(alpha) * T_p is exact unit length and orthogonal
1042
- // to T_c = cos(alpha) * T_p + sin(alpha) * D_rad for all alpha in [0, pi],
1043
- // avoiding any singularity at alpha = pi/2.
1044
- let crx = cos_a * drx - sin_a * SCRATCH_TAN[0];
1045
- let cry = cos_a * dry - sin_a * SCRATCH_TAN[1];
1046
- let crz = cos_a * drz - sin_a * SCRATCH_TAN[2];
1047
- const cr_dot = crx * ctx + cry * cty + crz * ctz;
1048
- crx -= cr_dot * ctx;
1049
- cry -= cr_dot * cty;
1050
- crz -= cr_dot * ctz;
1051
- const cr_len = Math.sqrt(crx * crx + cry * cry + crz * crz);
1052
- if (cr_len > 1e-12) {
1053
- crx /= cr_len;
1054
- cry /= cr_len;
1055
- crz /= cr_len;
1056
- }
1057
-
1058
- // Compute child branch length modulated by length_profile and crown envelope at mid-span height
1059
- const est_mid_y = SCRATCH_POS[1] + Math.max(0.0, cty) * next_cfg.length * 0.5;
1060
- const h_norm = clamp((est_mid_y - crown.base_height) / Math.max(0.1, crown.height), 0.0, 1.0);
1061
- const env_scale = level === 0
1062
- ? crown_envelope_evaluate(crown.envelope_type, h_norm)
1063
- : lerp(1.0, crown_envelope_evaluate(crown.envelope_type, h_norm), 0.5);
1064
- const prof_scale = Math.max(0.1, next_cfg.length_profile.evaluate(t_attach));
1065
- const var_scale = 1.0 + (rng() * 2.0 - 1.0) * next_cfg.length_variance;
1066
- const child_len = Math.max(0.15, next_cfg.length * prof_scale * env_scale * var_scale);
1067
-
1068
- const child_base_r = Math.min(
1069
- parent_r_at_attach * 0.85,
1070
- Math.max(0.003, child_len * next_cfg.radius_ratio)
1071
- );
1072
-
1073
- queue.push({
1074
- level: next_level,
1075
- parent_branch_id: branch_id,
1076
- parent_edge_id: parent_e,
1077
- parent_attach_t: t_attach,
1078
- attach_parent_vertex: p_v0,
1079
- start_vertex: -1,
1080
- px: SCRATCH_POS[0],
1081
- py: SCRATCH_POS[1],
1082
- pz: SCRATCH_POS[2],
1083
- tx: ctx,
1084
- ty: cty,
1085
- tz: ctz,
1086
- rx: crx,
1087
- ry: cry,
1088
- rz: crz,
1089
- length: child_len,
1090
- base_radius: child_base_r,
1091
- start_seg: 0,
1092
- total_segs: Math.max(1, next_cfg.segments),
1093
- initial_arc_s: 0.0,
1094
- has_split: false
1095
- });
1096
- }
1097
- }
1098
- }
1099
-
1100
- // Bottom-up Pipe Model radius calculation & blending
1101
- tree_skeleton_compute_pipe_radii(out, species);
1102
-
1103
- return out;
1104
- }
1
+ import { BVH } from "../../../core/bvh2/bvh3/BVH.js";
2
+ import { bvh_query_user_data_intersects_capsule } from "../../../core/bvh2/bvh3/query/bvh_query_user_data_intersects_capsule.js";
3
+ import { PointHashGrid3 } from "../../../core/geom/3d/hash-grid/PointHashGrid3.js";
4
+ import { line3_compute_segment_segment_closest_points } from "../../../core/geom/3d/line/line3_compute_segment_segment_closest_points.js";
5
+ import { GOLDEN_RATIO } from "../../../core/math/GOLDEN_RATIO.js";
6
+ import { PI2 } from "../../../core/math/PI2.js";
7
+ import { clamp } from "../../../core/math/clamp.js";
8
+ import { lerp } from "../../../core/math/lerp.js";
9
+ import { curl_noise_3d } from "../../../core/math/noise/curl_noise_3d.js";
10
+ import { seededRandom } from "../../../core/math/random/seededRandom.js";
11
+ import { PhyllotaxisMode } from "../config/PhyllotaxisMode.js";
12
+ import { TreeSpeciesConfig } from "../config/TreeSpeciesConfig.js";
13
+ import { TreeSkeleton } from "./TreeSkeleton.js";
14
+ import { crown_envelope_evaluate, crown_envelope_sdf } from "./crown_envelope_evaluate.js";
15
+ import { spine_frame_double_reflection } from "./spine_frame_double_reflection.js";
16
+ import { tree_skeleton_compute_pipe_radii } from "./tree_skeleton_compute_pipe_radii.js";
17
+
18
+ const GOLDEN_ANGLE = PI2 / (GOLDEN_RATIO * GOLDEN_RATIO); // ~2.399963229728653 rad (137.50776 deg)
19
+
20
+ const SCRATCH_CURL = [0, 0, 0];
21
+ const SCRATCH_RMF = new Float64Array(6);
22
+ const SCRATCH_POS = new Float32Array(3);
23
+ const SCRATCH_TAN = new Float32Array(3);
24
+ const SCRATCH_P0 = new Float32Array(3);
25
+ const SCRATCH_T0 = new Float32Array(3);
26
+ const SCRATCH_R0 = new Float32Array(3);
27
+ const SCRATCH_M_PREV = new Float32Array(3);
28
+ const SCRATCH_M_NEXT = new Float32Array(3);
29
+ const SCRATCH_GUIDE_P0 = new Float32Array(3);
30
+ const SCRATCH_GUIDE_P1 = new Float32Array(3);
31
+ const SCRATCH_GUIDE_T = new Float32Array(3);
32
+
33
+ /**
34
+ * Evaluates the trunk guide curve position `out_pos` and unit tangent `out_tan` at normalized
35
+ * parameter `t_norm` in [0, 1]. Defaults to the vertical upward plumb line `(0, t_norm * trunk_len, 0)`
36
+ * when `trunk_guide` is null or omitted.
37
+ *
38
+ * @param {Float32Array|number[]} out_pos
39
+ * @param {Float32Array|number[]} out_tan
40
+ * @param {Array<ArrayLike<number>>|Function|null|undefined} trunk_guide
41
+ * @param {number} t_norm
42
+ * @param {number} trunk_len
43
+ */
44
+ function evaluate_trunk_guide(out_pos, out_tan, trunk_guide, t_norm, trunk_len) {
45
+ const t = clamp(t_norm, 0.0, 1.0);
46
+ if (!trunk_guide) {
47
+ out_pos[0] = 0.0;
48
+ out_pos[1] = t * trunk_len;
49
+ out_pos[2] = 0.0;
50
+ out_tan[0] = 0.0;
51
+ out_tan[1] = 1.0;
52
+ out_tan[2] = 0.0;
53
+ return;
54
+ }
55
+
56
+ const sample_pt = (u, dst) => {
57
+ if (typeof trunk_guide === "function") {
58
+ trunk_guide(u, dst);
59
+ return;
60
+ }
61
+ if (Array.isArray(trunk_guide) && trunk_guide.length >= 2) {
62
+ const n = trunk_guide.length - 1;
63
+ const su = clamp(u, 0.0, 1.0) * n;
64
+ const idx = Math.min(n - 1, Math.floor(su));
65
+ const frac = su - idx;
66
+ const a = trunk_guide[idx];
67
+ const b = trunk_guide[idx + 1];
68
+ dst[0] = lerp(a[0] ?? 0.0, b[0] ?? 0.0, frac);
69
+ dst[1] = lerp(a[1] ?? 0.0, b[1] ?? 0.0, frac);
70
+ dst[2] = lerp(a[2] ?? 0.0, b[2] ?? 0.0, frac);
71
+ return;
72
+ }
73
+ dst[0] = 0.0;
74
+ dst[1] = u * trunk_len;
75
+ dst[2] = 0.0;
76
+ };
77
+
78
+ sample_pt(t, out_pos);
79
+ const eps = 0.01;
80
+ const t_a = Math.max(0.0, t - eps);
81
+ const t_b = Math.min(1.0, t + eps);
82
+ const pa = [0, 0, 0];
83
+ const pb = [0, 0, 0];
84
+ sample_pt(t_a, pa);
85
+ sample_pt(t_b, pb);
86
+ let tx = pb[0] - pa[0];
87
+ let ty = pb[1] - pa[1];
88
+ let tz = pb[2] - pa[2];
89
+ const tl = Math.hypot(tx, ty, tz);
90
+ if (tl > 1e-8) {
91
+ out_tan[0] = tx / tl;
92
+ out_tan[1] = ty / tl;
93
+ out_tan[2] = tz / tl;
94
+ } else {
95
+ out_tan[0] = 0.0;
96
+ out_tan[1] = 1.0;
97
+ out_tan[2] = 0.0;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Rotates unit vector `(vx, vy, vz)` around unit axis `(ax, ay, az)` by `angle` radians (Rodrigues' formula).
103
+ * @param {number[]|Float64Array} out
104
+ * @param {number} vx
105
+ * @param {number} vy
106
+ * @param {number} vz
107
+ * @param {number} ax
108
+ * @param {number} ay
109
+ * @param {number} az
110
+ * @param {number} angle
111
+ */
112
+ function rotate_around_axis(out, vx, vy, vz, ax, ay, az, angle) {
113
+ const c = Math.cos(angle);
114
+ const s = Math.sin(angle);
115
+ const dot = vx * ax + vy * ay + vz * az;
116
+ const omc = 1.0 - c;
117
+
118
+ const cx = ay * vz - az * vy;
119
+ const cy = az * vx - ax * vz;
120
+ const cz = ax * vy - ay * vx;
121
+
122
+ let rx = vx * c + cx * s + ax * dot * omc;
123
+ let ry = vy * c + cy * s + ay * dot * omc;
124
+ let rz = vz * c + cz * s + az * dot * omc;
125
+
126
+ const len = Math.sqrt(rx * rx + ry * ry + rz * rz);
127
+ if (len > 1e-12) {
128
+ const inv = 1.0 / len;
129
+ rx *= inv;
130
+ ry *= inv;
131
+ rz *= inv;
132
+ }
133
+ out[0] = rx;
134
+ out[1] = ry;
135
+ out[2] = rz;
136
+ }
137
+
138
+ /**
139
+ * Determines the non-exempt sub-segment parameter intervals `[s_min, 1]` on candidate segment A
140
+ * and `[t_min, 1]` on existing segment B for branch-branch capsule collision testing.
141
+ * Returns `false` if the segment pair is topologically exempt (e.g. direct parent-child base collar).
142
+ *
143
+ * @param {number} bA
144
+ * @param {number} parent_bA
145
+ * @param {number} attach_seg_A
146
+ * @param {boolean} is_split_A
147
+ * @param {number} local_idx_A
148
+ * @param {number} bB
149
+ * @param {number} parent_bB
150
+ * @param {number} attach_seg_B
151
+ * @param {boolean} is_split_B
152
+ * @param {number} local_idx_B
153
+ * @param {Float64Array|number[]} out_bounds `[s_min, t_min]`
154
+ * @returns {boolean}
155
+ */
156
+ function resolve_subsegment_bounds(
157
+ bA, parent_bA, attach_seg_A, is_split_A, local_idx_A,
158
+ bB, parent_bB, attach_seg_B, is_split_B, local_idx_B,
159
+ out_bounds
160
+ ) {
161
+ if (bA === bB) return false;
162
+
163
+ // 1. A is a direct child of B
164
+ if (parent_bA === bB) {
165
+ if (is_split_A) {
166
+ if (local_idx_A === 0 && local_idx_B === attach_seg_A) return false;
167
+ if (local_idx_A === 0 && local_idx_B === attach_seg_A + 1) {
168
+ out_bounds[0] = 0.55;
169
+ out_bounds[1] = 0.55;
170
+ return true;
171
+ }
172
+ if (local_idx_A === 1 && (local_idx_B === attach_seg_A || local_idx_B === attach_seg_A + 1)) {
173
+ out_bounds[0] = 0.45;
174
+ out_bounds[1] = 0.45;
175
+ return true;
176
+ }
177
+ } else {
178
+ if (local_idx_A === 0 && Math.abs(local_idx_B - attach_seg_A) <= 1) return false;
179
+ if (local_idx_A === 1 && Math.abs(local_idx_B - attach_seg_A) <= 1) {
180
+ if (bA < 0 || local_idx_B === attach_seg_A) return false;
181
+ out_bounds[0] = 0.35;
182
+ out_bounds[1] = 0.0;
183
+ return true;
184
+ }
185
+ }
186
+ }
187
+
188
+ // 2. B is a direct child of A
189
+ if (parent_bB === bA) {
190
+ if (is_split_B) {
191
+ if (local_idx_B === 0 && local_idx_A === attach_seg_B) return false;
192
+ if (local_idx_B === 0 && local_idx_A === attach_seg_B + 1) {
193
+ out_bounds[0] = 0.55;
194
+ out_bounds[1] = 0.55;
195
+ return true;
196
+ }
197
+ if (local_idx_B === 1 && (local_idx_A === attach_seg_B || local_idx_A === attach_seg_B + 1)) {
198
+ out_bounds[0] = 0.45;
199
+ out_bounds[1] = 0.45;
200
+ return true;
201
+ }
202
+ } else {
203
+ if (local_idx_B <= 1 && Math.abs(local_idx_A - attach_seg_B) <= 1) return false;
204
+ }
205
+ }
206
+
207
+ // 3. A and B are siblings sharing the same parent branch
208
+ if (parent_bA >= 0 && parent_bA === parent_bB) {
209
+ if (local_idx_A === 0 && local_idx_B === 0) {
210
+ out_bounds[0] = 0.50;
211
+ out_bounds[1] = 0.50;
212
+ return true;
213
+ }
214
+ if (local_idx_A === 0 && local_idx_B === 1 && Math.abs(attach_seg_A - attach_seg_B) <= 1) {
215
+ out_bounds[0] = 0.25;
216
+ out_bounds[1] = 0.0;
217
+ return true;
218
+ }
219
+ if (local_idx_A === 1 && local_idx_B === 0 && Math.abs(attach_seg_A - attach_seg_B) <= 1) {
220
+ out_bounds[0] = 0.0;
221
+ out_bounds[1] = 0.25;
222
+ return true;
223
+ }
224
+ }
225
+
226
+ out_bounds[0] = 0.0;
227
+ out_bounds[1] = 0.0;
228
+ return true;
229
+ }
230
+
231
+ /**
232
+ * Generates a directed botanical `TreeSkeleton` from a `TreeSpeciesConfig`, combining:
233
+ * - Multi-level parametric Cubic Hermite spine generation (`BranchLevelConfig`, `AnimationCurve` profiles)
234
+ * - Botanical phyllotaxis (`SpiralGolden`, `AlternateDistichous`, `Tristichous`, `Opposite`, `OppositeDecussate`, `Whorled`)
235
+ * - Gravitropism (toward `-Y`), phototropism (toward `+Y` and outward crown exterior), and 3D `curl_noise_3d` gnarl
236
+ * - Dichotomous / sympodial trunk & bough splitting
237
+ * - Space colonization crown competition via `PointHashGrid3`
238
+ * - Rotation-Minimizing Frames (`spine_frame_double_reflection`)
239
+ * - Pipe Model cross-section radius accumulation (`tree_skeleton_compute_pipe_radii`)
240
+ *
241
+ * @param {TreeSpeciesConfig|Object} [config]
242
+ * @param {TreeSkeleton} [out]
243
+ * @returns {TreeSkeleton}
244
+ */
245
+ export function tree_skeleton_grow(config = new TreeSpeciesConfig(), out = new TreeSkeleton()) {
246
+ const species = config && config.isTreeSpeciesConfig ? config : TreeSpeciesConfig.from(config);
247
+ out.clear();
248
+
249
+ const rng = seededRandom(species.seed | 0);
250
+ // Seed-derived 3D noise offset so unseeded Simplex/Curl fields are unique and deterministic per seed
251
+ const noise_ox = (rng() - 0.5) * 1000.0;
252
+ const noise_oy = (rng() - 0.5) * 1000.0;
253
+ const noise_oz = (rng() - 0.5) * 1000.0;
254
+
255
+ const crown = species.crown;
256
+ const crown_cy = crown.base_height + crown.height * 0.5;
257
+ out.crown_center[0] = 0.0;
258
+ out.crown_center[1] = crown_cy;
259
+ out.crown_center[2] = 0.0;
260
+
261
+ out.crown_radii[0] = Math.max(0.25, crown.radius);
262
+ out.crown_radii[1] = Math.max(0.25, crown.height * 0.5);
263
+ out.crown_radii[2] = Math.max(0.25, crown.radius);
264
+
265
+ // Build Space Colonization attraction point cloud + PointHashGrid3 once
266
+ const use_space_col = crown.space_colonization_weight > 0.0 && crown.attraction_point_count > 0;
267
+ const attr_count = use_space_col ? (crown.attraction_point_count | 0) : 0;
268
+ const attr_positions = new Float32Array(attr_count * 3);
269
+ const attr_alive = new Uint8Array(attr_count);
270
+ let attr_grid = null;
271
+
272
+ if (use_space_col) {
273
+ let placed = 0;
274
+ let attempts = 0;
275
+ const max_attempts = attr_count * 12;
276
+ while (placed < attr_count && attempts < max_attempts) {
277
+ attempts++;
278
+ const u_h = rng();
279
+ const env_r = crown.radius * crown_envelope_evaluate(crown.envelope_type, u_h);
280
+ const rx = (rng() * 2.0 - 1.0) * env_r;
281
+ const rz = (rng() * 2.0 - 1.0) * env_r;
282
+ if (rx * rx + rz * rz <= env_r * env_r) {
283
+ const ry = crown.base_height + u_h * crown.height;
284
+ attr_positions[placed * 3] = rx;
285
+ attr_positions[placed * 3 + 1] = ry;
286
+ attr_positions[placed * 3 + 2] = rz;
287
+ attr_alive[placed] = 1;
288
+ placed++;
289
+ }
290
+ }
291
+ while (placed < attr_count) {
292
+ const theta = rng() * PI2;
293
+ const u_h = rng();
294
+ const r = Math.sqrt(rng()) * crown.radius * crown_envelope_evaluate(crown.envelope_type, u_h);
295
+ attr_positions[placed * 3] = Math.cos(theta) * r;
296
+ attr_positions[placed * 3 + 1] = crown.base_height + u_h * crown.height;
297
+ attr_positions[placed * 3 + 2] = Math.sin(theta) * r;
298
+ attr_alive[placed] = 1;
299
+ placed++;
300
+ }
301
+
302
+ attr_grid = new PointHashGrid3();
303
+ attr_grid.build(attr_positions, attr_count, Math.max(0.25, crown.attraction_radius));
304
+ }
305
+
306
+ const r_influence_sq = crown.attraction_radius * crown.attraction_radius;
307
+ const r_kill_sq = crown.kill_radius * crown.kill_radius;
308
+
309
+ // Queue of branches to grow in level-order (guarantees parent branches & vertices precede children)
310
+ const queue = [];
311
+
312
+ const lvl0 = species.levels[0];
313
+ if (!lvl0) {
314
+ return out;
315
+ }
316
+
317
+ const trunk_len = Math.max(0.2, lvl0.length * (1.0 + (rng() * 2.0 - 1.0) * lvl0.length_variance));
318
+ const trunk_base_r = Math.max(0.01, trunk_len * lvl0.radius_ratio);
319
+
320
+ evaluate_trunk_guide(SCRATCH_GUIDE_P0, SCRATCH_GUIDE_T, species.trunk_guide, 0.0, trunk_len);
321
+
322
+ // Apply subtle initial basal trunk lean around the guide tangent
323
+ const max_lean = lvl0.phyllotaxis_mode === PhyllotaxisMode.Whorled ? 0.025 : 0.06;
324
+ const lean_ang = clamp((lvl0.basal_lean ?? 0.02) * (0.75 + 0.5 * rng()), 0.0, max_lean);
325
+ const lean_az = rng() * PI2;
326
+ const init_tx = clamp(SCRATCH_GUIDE_T[0] + Math.sin(lean_ang) * Math.cos(lean_az), -0.95, 0.95);
327
+ const init_ty = Math.max(0.2, SCRATCH_GUIDE_T[1] * Math.cos(lean_ang));
328
+ const init_tz = clamp(SCRATCH_GUIDE_T[2] + Math.sin(lean_ang) * Math.sin(lean_az), -0.95, 0.95);
329
+ const init_t_len = Math.hypot(init_tx, init_ty, init_tz) || 1.0;
330
+ const norm_tx = init_tx / init_t_len;
331
+ const norm_ty = init_ty / init_t_len;
332
+ const norm_tz = init_tz / init_t_len;
333
+
334
+ // Orthonormal initial R perpendicular to norm_T
335
+ let init_rx = 1.0 - norm_tx * norm_tx;
336
+ let init_ry = -norm_ty * norm_tx;
337
+ let init_rz = -norm_tz * norm_tx;
338
+ const init_r_len = Math.hypot(init_rx, init_ry, init_rz) || 1.0;
339
+ init_rx /= init_r_len;
340
+ init_ry /= init_r_len;
341
+ init_rz /= init_r_len;
342
+
343
+ queue.push({
344
+ level: 0,
345
+ parent_branch_id: -1,
346
+ parent_edge_id: -1,
347
+ parent_attach_seg_idx: -1,
348
+ parent_attach_t: 0.0,
349
+ start_vertex: -1,
350
+ px: SCRATCH_GUIDE_P0[0],
351
+ py: SCRATCH_GUIDE_P0[1],
352
+ pz: SCRATCH_GUIDE_P0[2],
353
+ tx: norm_tx,
354
+ ty: norm_ty,
355
+ tz: norm_tz,
356
+ rx: init_rx,
357
+ ry: init_ry,
358
+ rz: init_rz,
359
+ emerge_drx: init_rx,
360
+ emerge_dry: init_ry,
361
+ emerge_drz: init_rz,
362
+ length: trunk_len,
363
+ full_branch_length: trunk_len,
364
+ base_radius: trunk_base_r,
365
+ start_seg: 0,
366
+ total_segs: Math.max(1, lvl0.segments),
367
+ initial_arc_s: 0.0,
368
+ has_split: false,
369
+ is_split_fork: false,
370
+ cum_t0: 0.0,
371
+ cum_t1: 1.0
372
+ });
373
+
374
+ const rot_scratch = [0, 0, 0];
375
+
376
+ // Scope-local BVH and per-segment metadata for branch-branch spatial avoidance & collision prevention
377
+ const bvh = new BVH();
378
+ const bvh_hits = new Uint32Array(256);
379
+ const query_capsule = new Float32Array(7);
380
+ const sub_bounds = new Float64Array(2);
381
+ const closest_st = new Float64Array(2);
382
+
383
+ const seg_branch_id = [];
384
+ const seg_local_idx = [];
385
+ const seg_p0x = [], seg_p0y = [], seg_p0z = [], seg_r0 = [];
386
+ const seg_p1x = [], seg_p1y = [], seg_p1z = [], seg_r1 = [];
387
+ const seg_bvh_leaf = [];
388
+
389
+ const branch_parent_seg_idx = [];
390
+ const branch_is_split_fork = [];
391
+
392
+ /**
393
+ * Evaluates the minimum signed surface-to-surface clearance between a candidate segment A
394
+ * and all non-exempt existing segments in `bvh`.
395
+ */
396
+ const evaluate_candidate_clearance = (
397
+ bA, parent_bA, attach_seg_A, is_split_A, local_idx_A,
398
+ a0x, a0y, a0z, rA0,
399
+ a1x, a1y, a1z, rA1,
400
+ extra_margin
401
+ ) => {
402
+ if (bvh.size === 0) return 10.0;
403
+ const max_rA = Math.max(rA0, rA1);
404
+ query_capsule[0] = a0x;
405
+ query_capsule[1] = a0y;
406
+ query_capsule[2] = a0z;
407
+ query_capsule[3] = a1x;
408
+ query_capsule[4] = a1y;
409
+ query_capsule[5] = a1z;
410
+ query_capsule[6] = max_rA * 1.15 + extra_margin + 0.04;
411
+
412
+ const hit_count = bvh_query_user_data_intersects_capsule(bvh_hits, 0, bvh, query_capsule);
413
+ let min_clear = 10.0;
414
+
415
+ for (let h = 0; h < hit_count; h++) {
416
+ const eB = bvh_hits[h];
417
+ const bB = seg_branch_id[eB];
418
+ const local_idx_B = seg_local_idx[eB];
419
+ const parent_bB = out.branch_parent[bB];
420
+ const attach_seg_B = branch_parent_seg_idx[bB];
421
+ const is_split_B = branch_is_split_fork[bB];
422
+
423
+ if (!resolve_subsegment_bounds(
424
+ bA, parent_bA, attach_seg_A, is_split_A, local_idx_A,
425
+ bB, parent_bB, attach_seg_B, is_split_B, local_idx_B,
426
+ sub_bounds
427
+ )) {
428
+ continue;
429
+ }
430
+
431
+ const s_min = sub_bounds[0];
432
+ const t_min = sub_bounds[1];
433
+ const sa0x = a0x + (a1x - a0x) * s_min;
434
+ const sa0y = a0y + (a1y - a0y) * s_min;
435
+ const sa0z = a0z + (a1z - a0z) * s_min;
436
+
437
+ const b0x = seg_p0x[eB], b0y = seg_p0y[eB], b0z = seg_p0z[eB];
438
+ const b1x = seg_p1x[eB], b1y = seg_p1y[eB], b1z = seg_p1z[eB];
439
+ const sb0x = b0x + (b1x - b0x) * t_min;
440
+ const sb0y = b0y + (b1y - b0y) * t_min;
441
+ const sb0z = b0z + (b1z - b0z) * t_min;
442
+
443
+ line3_compute_segment_segment_closest_points(
444
+ closest_st,
445
+ sa0x, sa0y, sa0z, a1x, a1y, a1z,
446
+ sb0x, sb0y, sb0z, b1x, b1y, b1z
447
+ );
448
+
449
+ const s_f = s_min + (1.0 - s_min) * closest_st[0];
450
+ const t_f = t_min + (1.0 - t_min) * closest_st[1];
451
+
452
+ const qAx = a0x + (a1x - a0x) * s_f;
453
+ const qAy = a0y + (a1y - a0y) * s_f;
454
+ const qAz = a0z + (a1z - a0z) * s_f;
455
+ const qBx = b0x + (b1x - b0x) * t_f;
456
+ const qBy = b0y + (b1y - b0y) * t_f;
457
+ const qBz = b0z + (b1z - b0z) * t_f;
458
+
459
+ const dist = Math.hypot(qAx - qBx, qAy - qBy, qAz - qBz);
460
+ const rA = rA0 + (rA1 - rA0) * s_f;
461
+ const rB = seg_r0[eB] + (seg_r1[eB] - seg_r0[eB]) * t_f;
462
+ const r_req = (rA + rB) * 1.08 + extra_margin;
463
+ const clear = dist - r_req;
464
+ if (clear < min_clear) {
465
+ min_clear = clear;
466
+ }
467
+ }
468
+ return min_clear;
469
+ };
470
+
471
+ for (let q_idx = 0; q_idx < queue.length; q_idx++) {
472
+ const job = queue[q_idx];
473
+ const level = job.level;
474
+ const lvl_cfg = species.levels[level];
475
+ if (!lvl_cfg) continue;
476
+
477
+ const phase_hash = rng();
478
+ const N_seg = job.total_segs;
479
+ const remaining_segs = N_seg - job.start_seg;
480
+ if (remaining_segs <= 0) continue;
481
+
482
+ const step_len = job.length / N_seg;
483
+ const prof_p0 = lvl_cfg.radius_profile.evaluate(0.0);
484
+ const prof_p1 = lvl_cfg.radius_profile.evaluate(1.0);
485
+ const prof_span = prof_p0 - prof_p1;
486
+
487
+ let cur_px = job.px;
488
+ let cur_py = job.py;
489
+ let cur_pz = job.pz;
490
+ let cur_tx = job.tx;
491
+ let cur_ty = job.ty;
492
+ let cur_tz = job.tz;
493
+ let cur_rx = job.rx;
494
+ let cur_ry = job.ry;
495
+ let cur_rz = job.rz;
496
+ let cur_arc_s = job.initial_arc_s;
497
+
498
+ // Compute binormal S = T x R
499
+ let cur_sx = cur_ty * cur_rz - cur_tz * cur_ry;
500
+ let cur_sy = cur_tz * cur_rx - cur_tx * cur_rz;
501
+ let cur_sz = cur_tx * cur_ry - cur_ty * cur_rx;
502
+
503
+ let prev_v = job.start_vertex;
504
+ let branch_id = -1;
505
+
506
+ if (prev_v < 0) {
507
+ const t0 = job.start_seg / N_seg;
508
+ const u_prof0 = prof_span > 1e-4
509
+ ? clamp((prof_p0 - lvl_cfg.radius_profile.evaluate(t0)) / prof_span, 0.0, 1.0)
510
+ : t0;
511
+ const min_r0_frac = level === 0 ? 0.08 : 0.15;
512
+ const r0 = Math.max(0.002, job.base_radius * Math.max(1.0 - lvl_cfg.taper * u_prof0, min_r0_frac));
513
+
514
+ branch_id = out.add_branch(
515
+ 0,
516
+ level,
517
+ job.parent_branch_id,
518
+ job.parent_edge_id,
519
+ job.parent_attach_t,
520
+ job.length,
521
+ r0,
522
+ phase_hash
523
+ );
524
+ prev_v = out.add_node(
525
+ cur_px, cur_py, cur_pz,
526
+ cur_tx, cur_ty, cur_tz,
527
+ cur_rx, cur_ry, cur_rz,
528
+ r0,
529
+ branch_id,
530
+ level,
531
+ job.parent_branch_id,
532
+ t0,
533
+ cur_arc_s,
534
+ phase_hash
535
+ );
536
+ if (job.attach_parent_vertex !== undefined && job.attach_parent_vertex >= 0) {
537
+ out.connect_nodes(job.attach_parent_vertex, prev_v);
538
+ }
539
+ out.branch_root_vertex[branch_id] = prev_v;
540
+ out.branch_tip_vertex[branch_id] = prev_v;
541
+ } else {
542
+ const prev_r = out.node_read_radius(prev_v);
543
+ branch_id = out.add_branch(
544
+ prev_v,
545
+ level,
546
+ job.parent_branch_id,
547
+ job.parent_edge_id,
548
+ job.parent_attach_t,
549
+ job.length,
550
+ prev_r,
551
+ phase_hash
552
+ );
553
+ }
554
+
555
+ branch_parent_seg_idx[branch_id] = job.parent_attach_seg_idx ?? -1;
556
+ branch_is_split_fork[branch_id] = Boolean(job.is_split_fork);
557
+
558
+ const branch_edges = [];
559
+ const branch_edge_t0 = [];
560
+ const branch_edge_t1 = [];
561
+ const branch_vertices = [prev_v];
562
+
563
+ // Pre-schedule phyllotactic child attachment parameters so the parent spine kinks sympodially at lateral emissions
564
+ const next_level_preview = level + 1;
565
+ const len_fraction_est = level >= 1
566
+ ? clamp(Math.pow(job.length / Math.max(0.2, lvl_cfg.length), 0.85), 0.0, 1.0)
567
+ : 1.0;
568
+ const has_children = next_level_preview < species.levels.length
569
+ && lvl_cfg.child_count > 0
570
+ && job.base_radius >= 0.006
571
+ && (level === 0 || job.length >= 0.55);
572
+ const span_fraction_est = job.child_span_fraction ?? Math.max(0.25, (N_seg - job.start_seg) / Math.max(1, N_seg));
573
+ const effective_span = span_fraction_est * len_fraction_est;
574
+ const planned_child_count = has_children
575
+ ? (effective_span < 0.99 ? Math.max(1, Math.round(lvl_cfg.child_count * effective_span)) : lvl_cfg.child_count)
576
+ : 0;
577
+ const bare_est = clamp(lvl_cfg.bare_length_ratio, 0.0, 0.92);
578
+ const t_min_est = Math.max(bare_est, job.start_seg / N_seg);
579
+ const raw_base_phi = rng() * PI2;
580
+ let base_phi = raw_base_phi;
581
+ if (
582
+ level >= 1
583
+ && (lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.AlternateDistichous
584
+ || lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.Opposite)
585
+ ) {
586
+ // Align distichous / opposite lateral branching plane with the horizontal plane perpendicular to T
587
+ const horiz_phi = Math.atan2(-cur_ry, cur_sy);
588
+ base_phi = horiz_phi + (raw_base_phi < Math.PI ? 0.0 : Math.PI) + ((raw_base_phi % Math.PI) / Math.PI - 0.5) * 0.28;
589
+ }
590
+ const scheduled_children = [];
591
+
592
+ if (planned_child_count > 0 && 1.0 > t_min_est + 1e-3) {
593
+ for (let c = 0; c < planned_child_count; c++) {
594
+ let u_c = planned_child_count > 1
595
+ ? (c + 0.35) / (planned_child_count - 0.55)
596
+ : 0.5;
597
+ if (lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.Opposite
598
+ || lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.OppositeDecussate) {
599
+ const pair_idx = c >> 1;
600
+ const pair_count = Math.max(1, Math.ceil(planned_child_count / 2));
601
+ u_c = pair_count > 1 ? (pair_idx + 0.35) / (pair_count - 0.55) : 0.5;
602
+ } else if (lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.Whorled) {
603
+ const w_size = Math.max(1, lvl_cfg.whorl_count | 0);
604
+ const whorl_idx = Math.floor(c / w_size);
605
+ const whorl_total = Math.max(1, Math.ceil(planned_child_count / w_size));
606
+ const u_lin = whorl_total > 1 ? (whorl_idx + 0.35) / (whorl_total - 0.55) : 0.5;
607
+ // Self-similar conical internode compression toward the apex (p > 1 => du/dt decreases as u -> 1)
608
+ u_c = level === 0 ? 1.0 - Math.pow(1.0 - clamp(u_lin, 0.0, 1.0), 1.42) : u_lin;
609
+ }
610
+
611
+ const t_att = clamp(
612
+ lerp(t_min_est, 0.968, clamp(u_c, 0.0, 1.0)) + (rng() - 0.5) * (0.24 / planned_child_count),
613
+ t_min_est,
614
+ 0.98
615
+ );
616
+
617
+ let phi_c = base_phi;
618
+ switch (lvl_cfg.phyllotaxis_mode) {
619
+ case PhyllotaxisMode.AlternateDistichous:
620
+ phi_c += c * Math.PI;
621
+ break;
622
+ case PhyllotaxisMode.Tristichous:
623
+ phi_c += c * (PI2 / 3.0);
624
+ break;
625
+ case PhyllotaxisMode.Opposite:
626
+ phi_c += (c & 1) * Math.PI;
627
+ break;
628
+ case PhyllotaxisMode.OppositeDecussate:
629
+ phi_c += (c & 1) * Math.PI + (c >> 1) * (Math.PI * 0.5);
630
+ break;
631
+ case PhyllotaxisMode.Whorled: {
632
+ const w_size = Math.max(1, lvl_cfg.whorl_count | 0);
633
+ const w_idx = Math.floor(c / w_size);
634
+ const sub_idx = c % w_size;
635
+ phi_c += (sub_idx * PI2) / w_size + (w_idx * Math.PI) / w_size;
636
+ break;
637
+ }
638
+ case PhyllotaxisMode.SpiralGolden:
639
+ default:
640
+ phi_c += c * GOLDEN_ANGLE;
641
+ break;
642
+ }
643
+ phi_c += (rng() * 2.0 - 1.0) * lvl_cfg.azimuth_jitter;
644
+ scheduled_children.push({ t_attach: t_att, phi: phi_c });
645
+ }
646
+ }
647
+
648
+ const k_symp = lvl_cfg.nodal_kink_strength ?? 0.45;
649
+ const zig_base = lvl_cfg.internode_zigzag ?? 0.06;
650
+ const tip_up = lvl_cfg.tip_upcurl ?? 0.18;
651
+
652
+ // Horizontal emergence direction of this branch for outward radial consistency near the trunk centerline
653
+ const emerge_horiz_len = Math.hypot(job.tx, job.tz);
654
+ const emerge_hx = emerge_horiz_len > 1e-4 ? job.tx / emerge_horiz_len : cur_rx;
655
+ const emerge_hz = emerge_horiz_len > 1e-4 ? job.tz / emerge_horiz_len : cur_rz;
656
+
657
+ for (let s = job.start_seg + 1; s <= N_seg; s++) {
658
+ const t_prev = (s - 1) / N_seg;
659
+ const t_curr = s / N_seg;
660
+
661
+ // 1. Intrinsic S-curve pitch curvature (zero-mean higher harmonic on trunk so trunk weaves rather than leaning)
662
+ let s_wave = level === 0
663
+ ? (lvl_cfg.curvature * Math.sin(PI2 * 1.5 * t_curr + phase_hash * PI2))
664
+ : (lvl_cfg.curve_back !== 0.0
665
+ ? (lvl_cfg.curvature * Math.cos(Math.PI * t_curr) + lvl_cfg.curve_back * Math.sin(PI2 * t_curr))
666
+ : lvl_cfg.curvature);
667
+ // Attenuate negative (inward-toward-parent) pitch curvature on steep upper-crown insertions
668
+ // so upper lateral branches never curve backward across the parent stem axis.
669
+ if (level >= 1 && s_wave < 0.0 && job.parent_attach_t > 0.60) {
670
+ s_wave *= clamp(1.0 - (job.parent_attach_t - 0.60) * 2.2, 0.15, 1.0);
671
+ }
672
+ const d_pitch = s_wave / Math.max(2, N_seg * 0.65);
673
+
674
+ if (Math.abs(d_pitch) > 1e-6) {
675
+ rotate_around_axis(rot_scratch, cur_tx, cur_ty, cur_tz, cur_sx, cur_sy, cur_sz, d_pitch);
676
+ cur_tx = rot_scratch[0];
677
+ cur_ty = rot_scratch[1];
678
+ cur_tz = rot_scratch[2];
679
+ rotate_around_axis(rot_scratch, cur_rx, cur_ry, cur_rz, cur_sx, cur_sy, cur_sz, d_pitch);
680
+ cur_rx = rot_scratch[0];
681
+ cur_ry = rot_scratch[1];
682
+ cur_rz = rot_scratch[2];
683
+ }
684
+
685
+ // 2. Helical spiral twist around T
686
+ if (Math.abs(lvl_cfg.spiral_twist) > 1e-6) {
687
+ const d_twist = lvl_cfg.spiral_twist / N_seg;
688
+ rotate_around_axis(rot_scratch, cur_rx, cur_ry, cur_rz, cur_tx, cur_ty, cur_tz, d_twist);
689
+ cur_rx = rot_scratch[0];
690
+ cur_ry = rot_scratch[1];
691
+ cur_rz = rot_scratch[2];
692
+ cur_sx = cur_ty * cur_rz - cur_tz * cur_ry;
693
+ cur_sy = cur_tz * cur_rx - cur_tx * cur_rz;
694
+ cur_sz = cur_tx * cur_ry - cur_ty * cur_rx;
695
+ }
696
+
697
+ // 2b. Sympodial Nodal Kinking at scheduled lateral child emissions + Internodal Zig-Zag
698
+ let kink_rx = 0.0;
699
+ let kink_ry = 0.0;
700
+ let kink_rz = 0.0;
701
+
702
+ if (k_symp > 0.0 && scheduled_children.length > 0 && lvl_cfg.phyllotaxis_mode !== PhyllotaxisMode.Whorled) {
703
+ for (let c = 0; c < scheduled_children.length; c++) {
704
+ const sc = scheduled_children[c];
705
+ if (sc.t_attach >= t_prev && sc.t_attach <= t_curr) {
706
+ // Recoil away from child branch emission azimuth phi
707
+ const c_phi = Math.cos(sc.phi);
708
+ const s_phi = Math.sin(sc.phi);
709
+ const em_x = c_phi * cur_rx + s_phi * cur_sx;
710
+ const em_y = c_phi * cur_ry + s_phi * cur_sy;
711
+ const em_z = c_phi * cur_rz + s_phi * cur_sz;
712
+ const apical_boost = sc.t_attach > 0.85 ? 1.35 : 1.0;
713
+ const recoil = k_symp * (level === 0 ? 0.34 : 0.48) * apical_boost * (0.75 + 0.5 * rng());
714
+ kink_rx -= em_x * recoil;
715
+ kink_ry -= em_y * recoil;
716
+ kink_rz -= em_z * recoil;
717
+ }
718
+ }
719
+ }
720
+
721
+ if (zig_base > 0.0) {
722
+ const z_sign = (s & 1) === 0 ? 1.0 : -1.0;
723
+ const z_phi = phase_hash * PI2 + s * 0.85;
724
+ const zx = Math.cos(z_phi) * cur_rx + Math.sin(z_phi) * cur_sx;
725
+ const zy = Math.cos(z_phi) * cur_ry + Math.sin(z_phi) * cur_sy;
726
+ const zz = Math.cos(z_phi) * cur_rz + Math.sin(z_phi) * cur_sz;
727
+ const z_amp = zig_base * z_sign * (0.7 + 0.6 * rng());
728
+ kink_rx += zx * z_amp;
729
+ kink_ry += zy * z_amp;
730
+ kink_rz += zz * z_amp;
731
+ }
732
+
733
+ const kink_mag = Math.hypot(kink_rx, kink_ry, kink_rz);
734
+ if (kink_mag > 1e-5) {
735
+ const kx = kink_rx / kink_mag;
736
+ const ky = kink_ry / kink_mag;
737
+ const kz = kink_rz / kink_mag;
738
+ // Bend axis = T x K
739
+ let bx = cur_ty * kz - cur_tz * ky;
740
+ let by = cur_tz * kx - cur_tx * kz;
741
+ let bz = cur_tx * ky - cur_ty * kx;
742
+ const b_len = Math.hypot(bx, by, bz);
743
+ if (b_len > 1e-6) {
744
+ bx /= b_len;
745
+ by /= b_len;
746
+ bz /= b_len;
747
+ const clamped_kink = Math.min(0.42, kink_mag);
748
+ rotate_around_axis(rot_scratch, cur_tx, cur_ty, cur_tz, bx, by, bz, clamped_kink);
749
+ cur_tx = rot_scratch[0];
750
+ cur_ty = rot_scratch[1];
751
+ cur_tz = rot_scratch[2];
752
+ rotate_around_axis(rot_scratch, cur_rx, cur_ry, cur_rz, bx, by, bz, clamped_kink);
753
+ cur_rx = rot_scratch[0];
754
+ cur_ry = rot_scratch[1];
755
+ cur_rz = rot_scratch[2];
756
+ cur_sx = cur_ty * cur_rz - cur_tz * cur_ry;
757
+ cur_sy = cur_tz * cur_rx - cur_tx * cur_rz;
758
+ cur_sz = cur_tx * cur_ry - cur_ty * cur_rx;
759
+ }
760
+ }
761
+
762
+ // 3. Biomechanical Tropism steering (Gravitropic sag + Terminal Phototropic Up-Curl + Outward spread)
763
+ let steer_x = 0.0;
764
+ let steer_y = 0.0;
765
+ let steer_z = 0.0;
766
+
767
+ if (level === 0 && branch_id === 0) {
768
+ // Orthotropic upward striving & plumb-line righting reflex along guide curve (default vertical +Y)
769
+ evaluate_trunk_guide(SCRATCH_GUIDE_P0, SCRATCH_GUIDE_T, species.trunk_guide, t_prev, job.length);
770
+ evaluate_trunk_guide(SCRATCH_GUIDE_P1, SCRATCH_GUIDE_T, species.trunk_guide, t_curr, job.length);
771
+ const gtx = SCRATCH_GUIDE_T[0];
772
+ const gty = SCRATCH_GUIDE_T[1];
773
+ const gtz = SCRATCH_GUIDE_T[2];
774
+
775
+ const is_conifer = lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.Whorled;
776
+ const apical_relax = (!is_conifer && (t_curr > 0.80 || job.has_split)) ? 0.55 : 1.0;
777
+ const righting = (1.65 + Math.max(0.0, -lvl_cfg.gravitropism) * 2.5 + Math.max(0.0, lvl_cfg.phototropism)) * apical_relax;
778
+ steer_x += gtx * righting;
779
+ steer_y += gty * righting;
780
+ steer_z += gtz * righting;
781
+
782
+ // Plumb-line centering force pulling lateral drift back toward the guide curve
783
+ const dx_g = cur_px - SCRATCH_GUIDE_P0[0];
784
+ const dy_g = cur_py - SCRATCH_GUIDE_P0[1];
785
+ const dz_g = cur_pz - SCRATCH_GUIDE_P0[2];
786
+ const dot_g = dx_g * gtx + dy_g * gty + dz_g * gtz;
787
+ const lat_x = dx_g - dot_g * gtx;
788
+ const lat_y = dy_g - dot_g * gty;
789
+ const lat_z = dz_g - dot_g * gtz;
790
+ const plumb_k = ((is_conifer ? 5.2 : 3.4) * apical_relax) / Math.max(1.0, job.length);
791
+ steer_x -= lat_x * plumb_k * N_seg * 0.5;
792
+ steer_y -= lat_y * plumb_k * N_seg * 0.5;
793
+ steer_z -= lat_z * plumb_k * N_seg * 0.5;
794
+ } else if (level === 0 && branch_id > 0) {
795
+ // Secondary bifurcated trunk leader (fork): ascend skyward while preserving fork divergence
796
+ steer_x += emerge_hx * 0.35;
797
+ steer_y += 1.25 + Math.max(0.0, lvl_cfg.phototropism);
798
+ steer_z += emerge_hz * 0.35;
799
+ } else {
800
+ if (lvl_cfg.gravitropism !== 0.0 || tip_up !== 0.0) {
801
+ // Continuous blend from cantilever mid-branch sag arch (sin(pi * t^0.75)) to
802
+ // pendulous weeping cascade (0.65 + 0.95 * t) based on gravitropism vs. tip_upcurl
803
+ const w_pendulous = level >= 2
804
+ ? clamp((lvl_cfg.gravitropism - 0.35 - 0.5 * Math.max(0.0, tip_up)) / 0.45, 0.0, 1.0)
805
+ : 0.0;
806
+ const arch_grav_env = Math.sin(Math.PI * Math.pow(t_curr, 0.75));
807
+ const weep_grav_env = 0.65 + 0.95 * t_curr;
808
+ const grav_env = lerp(arch_grav_env, weep_grav_env, w_pendulous);
809
+ const apical_grav_atten = level === 1 && job.parent_attach_t > 0.75 && w_pendulous < 0.1
810
+ ? clamp(1.0 - (job.parent_attach_t - 0.75) * 3.2, 0.15, 1.0)
811
+ : 1.0;
812
+ const up_env = t_curr * t_curr * (1.0 + 0.5 * Math.max(0.0, -cur_ty));
813
+ steer_y += -lvl_cfg.gravitropism * grav_env * apical_grav_atten + tip_up * up_env;
814
+ }
815
+
816
+ if (lvl_cfg.phototropism !== 0.0) {
817
+ const sky_mix = crown.sky_phototropism_mix;
818
+ const horiz_r = Math.sqrt(cur_px * cur_px + cur_pz * cur_pz);
819
+ let out_x = 0.0;
820
+ let out_z = 0.0;
821
+ if (horiz_r > 0.52) {
822
+ out_x = cur_px / horiz_r;
823
+ out_z = cur_pz / horiz_r;
824
+ } else if (horiz_r > 1e-4) {
825
+ const w_rad = horiz_r / 0.52;
826
+ const bx = lerp(emerge_hx, cur_px / horiz_r, w_rad);
827
+ const bz = lerp(emerge_hz, cur_pz / horiz_r, w_rad);
828
+ const bl = Math.hypot(bx, bz) || 1.0;
829
+ out_x = bx / bl;
830
+ out_z = bz / bl;
831
+ } else {
832
+ out_x = emerge_hx;
833
+ out_z = emerge_hz;
834
+ }
835
+
836
+ steer_x += lvl_cfg.phototropism * (1.0 - sky_mix) * out_x;
837
+ steer_y += lvl_cfg.phototropism * sky_mix;
838
+ steer_z += lvl_cfg.phototropism * (1.0 - sky_mix) * out_z;
839
+ }
840
+ }
841
+
842
+ // 4. 2-Octave 3D Curl Noise gnarl scaled properly by sqrt(6 / N_seg)
843
+ if (lvl_cfg.gnarl_strength > 0.0) {
844
+ const freq = lvl_cfg.gnarl_frequency;
845
+ curl_noise_3d(
846
+ SCRATCH_CURL,
847
+ cur_px * freq + noise_ox + level * 17.3,
848
+ cur_py * freq + noise_oy + level * 31.7,
849
+ cur_pz * freq + noise_oz + level * 47.1
850
+ );
851
+ const c1x = SCRATCH_CURL[0], c1y = SCRATCH_CURL[1], c1z = SCRATCH_CURL[2];
852
+ curl_noise_3d(
853
+ SCRATCH_CURL,
854
+ cur_px * freq * 2.15 + noise_ox + 53.1 + phase_hash * 29.3,
855
+ cur_py * freq * 2.15 + noise_oy + 79.3,
856
+ cur_pz * freq * 2.15 + noise_oz + 113.7
857
+ );
858
+ const cx_tot = 0.68 * c1x + 0.32 * SCRATCH_CURL[0];
859
+ const cy_tot = 0.68 * c1y + 0.32 * SCRATCH_CURL[1];
860
+ const cz_tot = 0.68 * c1z + 0.32 * SCRATCH_CURL[2];
861
+
862
+ const gnarl_scale = lvl_cfg.gnarl_strength * Math.sqrt(6.0 / Math.max(2, N_seg));
863
+ // Project curl vector perpendicular to T so it steers direction cleanly
864
+ const dot_ct = cx_tot * cur_tx + cy_tot * cur_ty + cz_tot * cur_tz;
865
+ steer_x += (cx_tot - dot_ct * cur_tx) * gnarl_scale * 2.2;
866
+ steer_y += (cy_tot - dot_ct * cur_ty) * gnarl_scale * 1.6;
867
+ steer_z += (cz_tot - dot_ct * cur_tz) * gnarl_scale * 2.2;
868
+ }
869
+
870
+ // 5. Space colonization attraction steering (for branches in crown, level >= 1)
871
+ if (use_space_col && level >= 1 && attr_grid !== null) {
872
+ let attr_vx = 0.0;
873
+ let attr_vy = 0.0;
874
+ let attr_vz = 0.0;
875
+ let attr_hits = 0;
876
+
877
+ attr_grid.forEachNear(cur_px, cur_py, cur_pz, (idx) => {
878
+ if (attr_alive[idx] === 0) return;
879
+ const ax = attr_positions[idx * 3] - cur_px;
880
+ const ay = attr_positions[idx * 3 + 1] - cur_py;
881
+ const az = attr_positions[idx * 3 + 2] - cur_pz;
882
+ const d2 = ax * ax + ay * ay + az * az;
883
+ if (d2 <= r_kill_sq) {
884
+ attr_alive[idx] = 0;
885
+ } else if (d2 <= r_influence_sq && d2 > 1e-8) {
886
+ const inv_d = 1.0 / Math.sqrt(d2);
887
+ attr_vx += ax * inv_d;
888
+ attr_vy += ay * inv_d;
889
+ attr_vz += az * inv_d;
890
+ attr_hits++;
891
+ }
892
+ });
893
+
894
+ if (attr_hits > 0) {
895
+ const attr_len = Math.sqrt(attr_vx * attr_vx + attr_vy * attr_vy + attr_vz * attr_vz);
896
+ if (attr_len > 1e-6) {
897
+ const w_sc = crown.space_colonization_weight / attr_len;
898
+ steer_x += attr_vx * w_sc;
899
+ steer_y += attr_vy * w_sc;
900
+ steer_z += attr_vz * w_sc;
901
+ }
902
+ }
903
+ }
904
+
905
+ // 6. Crown envelope soft containment for lateral branches (level >= 1)
906
+ let seg_len = step_len;
907
+ if (level >= 1 && crown.prune_ratio > 0.0) {
908
+ const probe_x = cur_px + cur_tx * step_len;
909
+ const probe_y = cur_py + cur_ty * step_len;
910
+ const probe_z = cur_pz + cur_tz * step_len;
911
+ const sdf = crown_envelope_sdf(probe_x, probe_y, probe_z, crown);
912
+ if (sdf > 0.0) {
913
+ // Steer radially inward toward trunk axis (only pulling vertically if outside vertical crown span)
914
+ const crown_top_y = crown.base_height + crown.height;
915
+ const to_cx = -probe_x;
916
+ const to_cy = probe_y > crown_top_y
917
+ ? crown_top_y - probe_y
918
+ : (probe_y < crown.base_height ? crown.base_height - probe_y : 0.0);
919
+ const to_cz = -probe_z;
920
+ const to_c_len = Math.sqrt(to_cx * to_cx + to_cy * to_cy + to_cz * to_cz);
921
+ if (to_c_len > 1e-5) {
922
+ const pull = crown.prune_ratio * 0.45 / to_c_len;
923
+ steer_x += to_cx * pull;
924
+ steer_y += to_cy * pull;
925
+ steer_z += to_cz * pull;
926
+ }
927
+ seg_len *= Math.max(0.35, 1.0 - crown.prune_ratio * 0.5);
928
+ }
929
+ }
930
+
931
+ // Compute target radius r_curr for this segment before BVH collision checks
932
+ const t_spine = level === 0 ? lerp(job.cum_t0 ?? 0.0, job.cum_t1 ?? 1.0, t_curr) : t_curr;
933
+ const u_prof = prof_span > 1e-4
934
+ ? clamp((prof_p0 - lvl_cfg.radius_profile.evaluate(t_spine)) / prof_span, 0.0, 1.0)
935
+ : t_spine;
936
+ const min_tip_frac = level === 0 ? 0.12 : (has_children ? 0.14 : 0.24);
937
+ const apical_shoot_taper = level === 0
938
+ ? (t_spine > 0.65 ? lerp(1.0, 0.11, Math.pow((t_spine - 0.65) / 0.35, 1.45)) : 1.0)
939
+ : (has_children && t_curr > 0.80 ? lerp(1.0, 0.62, Math.pow((t_curr - 0.80) / 0.20, 1.35)) : 1.0);
940
+ const r_curr = Math.max(
941
+ level === 0 ? 0.0015 : 0.002,
942
+ job.base_radius * Math.max(1.0 - lvl_cfg.taper * u_prof, min_tip_frac) * apical_shoot_taper
943
+ );
944
+ const r_prev = out.node_read_radius(prev_v);
945
+ const local_seg_idx = branch_edges.length;
946
+ const parent_bA = job.parent_branch_id;
947
+ const attach_seg_A = job.parent_attach_seg_idx ?? -1;
948
+ const is_split_A = Boolean(job.is_split_fork);
949
+
950
+ // 6b. Soft BVH Repulsion Field (steers growing shoot smoothly around nearby existing branches)
951
+ if ((level >= 1 || branch_id > 0) && bvh.size > 0) {
952
+ const look_len = seg_len * 1.55;
953
+ const look_x = cur_px + cur_tx * look_len;
954
+ const look_y = cur_py + cur_ty * look_len;
955
+ const look_z = cur_pz + cur_tz * look_len;
956
+ const soft_margin = Math.max(0.08, Math.min(0.24, seg_len * 0.65));
957
+
958
+ query_capsule[0] = cur_px;
959
+ query_capsule[1] = cur_py;
960
+ query_capsule[2] = cur_pz;
961
+ query_capsule[3] = look_x;
962
+ query_capsule[4] = look_y;
963
+ query_capsule[5] = look_z;
964
+ query_capsule[6] = Math.max(r_prev, r_curr) * 1.15 + soft_margin;
965
+
966
+ const hit_count = bvh_query_user_data_intersects_capsule(bvh_hits, 0, bvh, query_capsule);
967
+ for (let h = 0; h < hit_count; h++) {
968
+ const eB = bvh_hits[h];
969
+ const bB = seg_branch_id[eB];
970
+ const local_idx_B = seg_local_idx[eB];
971
+ if (!resolve_subsegment_bounds(
972
+ branch_id, parent_bA, attach_seg_A, is_split_A, local_seg_idx,
973
+ bB, out.branch_parent[bB], branch_parent_seg_idx[bB], branch_is_split_fork[bB], local_idx_B,
974
+ sub_bounds
975
+ )) {
976
+ continue;
977
+ }
978
+
979
+ const s_min = sub_bounds[0];
980
+ const t_min = sub_bounds[1];
981
+ const sa0x = cur_px + (look_x - cur_px) * s_min;
982
+ const sa0y = cur_py + (look_y - cur_py) * s_min;
983
+ const sa0z = cur_pz + (look_z - cur_pz) * s_min;
984
+
985
+ const b0x = seg_p0x[eB], b0y = seg_p0y[eB], b0z = seg_p0z[eB];
986
+ const b1x = seg_p1x[eB], b1y = seg_p1y[eB], b1z = seg_p1z[eB];
987
+ const sb0x = b0x + (b1x - b0x) * t_min;
988
+ const sb0y = b0y + (b1y - b0y) * t_min;
989
+ const sb0z = b0z + (b1z - b0z) * t_min;
990
+
991
+ line3_compute_segment_segment_closest_points(
992
+ closest_st,
993
+ sa0x, sa0y, sa0z, look_x, look_y, look_z,
994
+ sb0x, sb0y, sb0z, b1x, b1y, b1z
995
+ );
996
+
997
+ const s_f = s_min + (1.0 - s_min) * closest_st[0];
998
+ const t_f = t_min + (1.0 - t_min) * closest_st[1];
999
+
1000
+ const qAx = cur_px + (look_x - cur_px) * s_f;
1001
+ const qAy = cur_py + (look_y - cur_py) * s_f;
1002
+ const qAz = cur_pz + (look_z - cur_pz) * s_f;
1003
+ const qBx = b0x + (b1x - b0x) * t_f;
1004
+ const qBy = b0y + (b1y - b0y) * t_f;
1005
+ const qBz = b0z + (b1z - b0z) * t_f;
1006
+
1007
+ const dx = qAx - qBx;
1008
+ const dy = qAy - qBy;
1009
+ const dz = qAz - qBz;
1010
+ const dist = Math.hypot(dx, dy, dz);
1011
+ const rA = r_prev + (r_curr - r_prev) * Math.min(1.0, s_f);
1012
+ const rB = seg_r0[eB] + (seg_r1[eB] - seg_r0[eB]) * t_f;
1013
+ const r_req = (rA + rB) * 1.12 + 0.005;
1014
+
1015
+ if (dist < r_req + soft_margin) {
1016
+ let nx = dist > 1e-5 ? dx / dist : cur_rx;
1017
+ let ny = dist > 1e-5 ? dy / dist : cur_ry;
1018
+ let nz = dist > 1e-5 ? dz / dist : cur_rz;
1019
+ // Project repulsion normal perpendicular to cur_T so it steers smoothly around the obstacle
1020
+ const dot_nt = nx * cur_tx + ny * cur_ty + nz * cur_tz;
1021
+ nx -= dot_nt * cur_tx;
1022
+ ny -= dot_nt * cur_ty;
1023
+ nz -= dot_nt * cur_tz;
1024
+ const n_len = Math.hypot(nx, ny, nz);
1025
+ if (n_len > 1e-5) {
1026
+ nx /= n_len;
1027
+ ny /= n_len;
1028
+ nz /= n_len;
1029
+ const u_prox = clamp(1.0 - Math.max(0.0, dist - r_req) / soft_margin, 0.0, 1.0);
1030
+ const rep_w = u_prox * u_prox * 2.8;
1031
+ steer_x += nx * rep_w;
1032
+ steer_y += ny * rep_w;
1033
+ steer_z += nz * rep_w;
1034
+ }
1035
+ }
1036
+ }
1037
+ }
1038
+
1039
+ // Apply steering to tangent and normalize
1040
+ const steer_div = Math.max(2.0, Math.sqrt(N_seg) * 1.35);
1041
+ let next_tx = cur_tx + steer_x / steer_div;
1042
+ let next_ty = cur_ty + steer_y / steer_div;
1043
+ let next_tz = cur_tz + steer_z / steer_div;
1044
+ const t_len = Math.sqrt(next_tx * next_tx + next_ty * next_ty + next_tz * next_tz);
1045
+ if (t_len > 1e-12) {
1046
+ next_tx /= t_len;
1047
+ next_ty /= t_len;
1048
+ next_tz /= t_len;
1049
+ } else {
1050
+ next_tx = cur_tx;
1051
+ next_ty = cur_ty;
1052
+ next_tz = cur_tz;
1053
+ }
1054
+
1055
+ let next_px = cur_px + 0.5 * (cur_tx + next_tx) * seg_len;
1056
+ let next_py = cur_py + 0.5 * (cur_ty + next_ty) * seg_len;
1057
+ let next_pz = cur_pz + 0.5 * (cur_tz + next_tz) * seg_len;
1058
+
1059
+ // Orthotropic plumb-tube containment on trunk (level === 0):
1060
+ // Preserves local 3D nodal kinks and serpentine sweep while guaranteeing the trunk never drifts sideways
1061
+ if (level === 0 && branch_id === 0) {
1062
+ const gx = SCRATCH_GUIDE_P1[0];
1063
+ const gy = SCRATCH_GUIDE_P1[1];
1064
+ const gz = SCRATCH_GUIDE_P1[2];
1065
+ const gtx = SCRATCH_GUIDE_T[0];
1066
+ const gty = SCRATCH_GUIDE_T[1];
1067
+ const gtz = SCRATCH_GUIDE_T[2];
1068
+
1069
+ const dx = next_px - gx;
1070
+ const dy = next_py - gy;
1071
+ const dz = next_pz - gz;
1072
+ const dot_g = dx * gtx + dy * gty + dz * gtz;
1073
+ const off_x = dx - dot_g * gtx;
1074
+ const off_y = dy - dot_g * gty;
1075
+ const off_z = dz - dot_g * gtz;
1076
+ const off_r = Math.hypot(off_x, off_y, off_z);
1077
+
1078
+ const is_conifer = lvl_cfg.phyllotaxis_mode === PhyllotaxisMode.Whorled;
1079
+ const max_drift_frac = is_conifer ? 0.032 : (job.has_split ? 0.078 : 0.058);
1080
+ // Envelope rises from 0 at root, peaks in mid-trunk (allowing 3D sweep/crookedness), and tapers near tip
1081
+ const env = Math.sin(Math.PI * Math.min(0.92, t_curr * 0.88)) * (1.0 - 0.38 * t_curr * t_curr);
1082
+ const max_r = job.length * max_drift_frac * Math.max(job.has_split ? 0.42 : 0.18, env);
1083
+
1084
+ if (off_r > max_r && off_r > 1e-6) {
1085
+ const scale_r = max_r / off_r;
1086
+ next_px = gx + dot_g * gtx + off_x * scale_r;
1087
+ next_py = gy + dot_g * gty + off_y * scale_r;
1088
+ next_pz = gz + dot_g * gtz + off_z * scale_r;
1089
+
1090
+ let ch_x = next_px - cur_px;
1091
+ let ch_y = next_py - cur_py;
1092
+ let ch_z = next_pz - cur_pz;
1093
+ const ch_l = Math.hypot(ch_x, ch_y, ch_z);
1094
+ if (ch_l > 1e-6) {
1095
+ next_tx = ch_x / ch_l;
1096
+ next_ty = ch_y / ch_l;
1097
+ next_tz = ch_z / ch_l;
1098
+ }
1099
+ }
1100
+ }
1101
+
1102
+ // 6c. Monotonic Parent Collar Escape + Hard BVH Capsule Contact Projection & Self-Pruning
1103
+ if ((level >= 1 || branch_id > 0) && bvh.size > 0) {
1104
+ let was_projected = false;
1105
+
1106
+ // Ensure lateral child branch exits its parent attachment segment cleanly on local_seg_idx <= 1
1107
+ if (job.parent_edge_id >= 0 && !is_split_A && local_seg_idx <= 1) {
1108
+ const eP = job.parent_edge_id;
1109
+ const p0x = seg_p0x[eP], p0y = seg_p0y[eP], p0z = seg_p0z[eP];
1110
+ const p1x = seg_p1x[eP], p1y = seg_p1y[eP], p1z = seg_p1z[eP];
1111
+
1112
+ line3_compute_segment_segment_closest_points(
1113
+ closest_st,
1114
+ next_px, next_py, next_pz, next_px, next_py, next_pz,
1115
+ p0x, p0y, p0z, p1x, p1y, p1z
1116
+ );
1117
+ const t_p = closest_st[1];
1118
+ const qPx = p0x + (p1x - p0x) * t_p;
1119
+ const qPy = p0y + (p1y - p0y) * t_p;
1120
+ const qPz = p0z + (p1z - p0z) * t_p;
1121
+ const r_p = seg_r0[eP] + (seg_r1[eP] - seg_r0[eP]) * t_p;
1122
+
1123
+ let rx_p = next_px - qPx;
1124
+ let ry_p = next_py - qPy;
1125
+ let rz_p = next_pz - qPz;
1126
+ const d_p = Math.hypot(rx_p, ry_p, rz_p);
1127
+ if (local_seg_idx === 0 && (r_p + r_curr) * 1.18 > seg_len) {
1128
+ seg_len = (r_p + r_curr) * 1.18;
1129
+ }
1130
+ const min_d_p = local_seg_idx === 0
1131
+ ? Math.min(seg_len * 0.88, (r_p + r_curr) * 1.05)
1132
+ : (r_p + r_curr) * 1.10 + 0.004;
1133
+
1134
+ const em_drx = job.emerge_drx ?? cur_rx;
1135
+ const em_dry = job.emerge_dry ?? cur_ry;
1136
+ const em_drz = job.emerge_drz ?? cur_rz;
1137
+ const dot_em = rx_p * em_drx + ry_p * em_dry + rz_p * em_drz;
1138
+
1139
+ if (d_p < min_d_p || dot_em < 0.0) {
1140
+ const ux_p = (d_p > 1e-5 && dot_em > 0.0) ? rx_p / d_p : em_drx;
1141
+ const uy_p = (d_p > 1e-5 && dot_em > 0.0) ? ry_p / d_p : em_dry;
1142
+ const uz_p = (d_p > 1e-5 && dot_em > 0.0) ? rz_p / d_p : em_drz;
1143
+ const push_d = Math.max(0.0, min_d_p - (dot_em > 0.0 ? d_p : -d_p));
1144
+ next_px += ux_p * push_d;
1145
+ next_py += uy_p * push_d;
1146
+ next_pz += uz_p * push_d;
1147
+
1148
+ let ch_x = next_px - cur_px;
1149
+ let ch_y = next_py - cur_py;
1150
+ let ch_z = next_pz - cur_pz;
1151
+ const ch_l = Math.hypot(ch_x, ch_y, ch_z);
1152
+ if (ch_l > 1e-6) {
1153
+ next_tx = ch_x / ch_l;
1154
+ next_ty = ch_y / ch_l;
1155
+ next_tz = ch_z / ch_l;
1156
+ next_px = cur_px + next_tx * seg_len;
1157
+ next_py = cur_py + next_ty * seg_len;
1158
+ next_pz = cur_pz + next_tz * seg_len;
1159
+ was_projected = true;
1160
+ }
1161
+ }
1162
+ }
1163
+
1164
+ // Iterative BVH capsule contact projection
1165
+ for (let iter = 0; iter < 4; iter++) {
1166
+ query_capsule[0] = cur_px;
1167
+ query_capsule[1] = cur_py;
1168
+ query_capsule[2] = cur_pz;
1169
+ query_capsule[3] = next_px;
1170
+ query_capsule[4] = next_py;
1171
+ query_capsule[5] = next_pz;
1172
+ query_capsule[6] = Math.max(r_prev, r_curr) * 1.15 + 0.025;
1173
+
1174
+ const hit_count = bvh_query_user_data_intersects_capsule(bvh_hits, 0, bvh, query_capsule);
1175
+ let moved = false;
1176
+
1177
+ for (let h = 0; h < hit_count; h++) {
1178
+ const eB = bvh_hits[h];
1179
+ const bB = seg_branch_id[eB];
1180
+ const local_idx_B = seg_local_idx[eB];
1181
+ if (!resolve_subsegment_bounds(
1182
+ branch_id, parent_bA, attach_seg_A, is_split_A, local_seg_idx,
1183
+ bB, out.branch_parent[bB], branch_parent_seg_idx[bB], branch_is_split_fork[bB], local_idx_B,
1184
+ sub_bounds
1185
+ )) {
1186
+ continue;
1187
+ }
1188
+
1189
+ const s_min = sub_bounds[0];
1190
+ const t_min = sub_bounds[1];
1191
+ const sa0x = cur_px + (next_px - cur_px) * s_min;
1192
+ const sa0y = cur_py + (next_py - cur_py) * s_min;
1193
+ const sa0z = cur_pz + (next_pz - cur_pz) * s_min;
1194
+
1195
+ const b0x = seg_p0x[eB], b0y = seg_p0y[eB], b0z = seg_p0z[eB];
1196
+ const b1x = seg_p1x[eB], b1y = seg_p1y[eB], b1z = seg_p1z[eB];
1197
+ const sb0x = b0x + (b1x - b0x) * t_min;
1198
+ const sb0y = b0y + (b1y - b0y) * t_min;
1199
+ const sb0z = b0z + (b1z - b0z) * t_min;
1200
+
1201
+ line3_compute_segment_segment_closest_points(
1202
+ closest_st,
1203
+ sa0x, sa0y, sa0z, next_px, next_py, next_pz,
1204
+ sb0x, sb0y, sb0z, b1x, b1y, b1z
1205
+ );
1206
+
1207
+ const s_f = s_min + (1.0 - s_min) * closest_st[0];
1208
+ const t_f = t_min + (1.0 - t_min) * closest_st[1];
1209
+ if (local_seg_idx === 0 && s_f <= 0.05) continue;
1210
+
1211
+ const qAx = cur_px + (next_px - cur_px) * s_f;
1212
+ const qAy = cur_py + (next_py - cur_py) * s_f;
1213
+ const qAz = cur_pz + (next_pz - cur_pz) * s_f;
1214
+ const qBx = b0x + (b1x - b0x) * t_f;
1215
+ const qBy = b0y + (b1y - b0y) * t_f;
1216
+ const qBz = b0z + (b1z - b0z) * t_f;
1217
+
1218
+ const dx = qAx - qBx;
1219
+ const dy = qAy - qBy;
1220
+ const dz = qAz - qBz;
1221
+ const dist = Math.hypot(dx, dy, dz);
1222
+ const rA = r_prev + (r_curr - r_prev) * s_f;
1223
+ const rB = seg_r0[eB] + (seg_r1[eB] - seg_r0[eB]) * t_f;
1224
+ const r_req = (rA + rB) * 1.08 + 0.004;
1225
+
1226
+ if (dist < r_req) {
1227
+ let nx = dist > 1e-5 ? dx / dist : cur_rx;
1228
+ let ny = dist > 1e-5 ? dy / dist : cur_ry;
1229
+ let nz = dist > 1e-5 ? dz / dist : cur_rz;
1230
+ const push = (r_req - dist + 0.004) / Math.max(0.35, s_f);
1231
+ next_px += nx * push;
1232
+ next_py += ny * push;
1233
+ next_pz += nz * push;
1234
+
1235
+ let ch_x = next_px - cur_px;
1236
+ let ch_y = next_py - cur_py;
1237
+ let ch_z = next_pz - cur_pz;
1238
+ const ch_l = Math.hypot(ch_x, ch_y, ch_z);
1239
+ if (ch_l > 1e-6) {
1240
+ next_tx = ch_x / ch_l;
1241
+ next_ty = ch_y / ch_l;
1242
+ next_tz = ch_z / ch_l;
1243
+ next_px = cur_px + next_tx * seg_len;
1244
+ next_py = cur_py + next_ty * seg_len;
1245
+ next_pz = cur_pz + next_tz * seg_len;
1246
+ moved = true;
1247
+ was_projected = true;
1248
+ }
1249
+ }
1250
+ }
1251
+ if (!moved) break;
1252
+ }
1253
+
1254
+ // If segment >= 1 still penetrates an existing branch or was forced into an unnatural sharp reversal,
1255
+ // self-prune / arrest the branch at prev_v so it never spears through another bough.
1256
+ if (local_seg_idx >= 1) {
1257
+ const final_clear = evaluate_candidate_clearance(
1258
+ branch_id, parent_bA, attach_seg_A, is_split_A, local_seg_idx,
1259
+ cur_px, cur_py, cur_pz, r_prev,
1260
+ next_px, next_py, next_pz, r_curr,
1261
+ -0.004
1262
+ );
1263
+ const turn_cos = cur_tx * next_tx + cur_ty * next_ty + cur_tz * next_tz;
1264
+ if (final_clear < 0.0 || (was_projected && turn_cos < 0.25)) {
1265
+ break;
1266
+ }
1267
+ } else if (was_projected) {
1268
+ // Keep root node tangent aligned with projected first segment so base collar has zero S-hook
1269
+ cur_tx = next_tx;
1270
+ cur_ty = next_ty;
1271
+ cur_tz = next_tz;
1272
+ out.node_write_tangent(prev_v, cur_tx, cur_ty, cur_tz);
1273
+ }
1274
+ }
1275
+
1276
+ // Transport RMF via Wang et al. Double Reflection
1277
+ spine_frame_double_reflection(
1278
+ SCRATCH_RMF, 0,
1279
+ cur_px, cur_py, cur_pz,
1280
+ cur_tx, cur_ty, cur_tz,
1281
+ cur_rx, cur_ry, cur_rz,
1282
+ next_px, next_py, next_pz,
1283
+ next_tx, next_ty, next_tz
1284
+ );
1285
+ let next_rx = SCRATCH_RMF[0];
1286
+ let next_ry = SCRATCH_RMF[1];
1287
+ let next_rz = SCRATCH_RMF[2];
1288
+ cur_sx = SCRATCH_RMF[3];
1289
+ cur_sy = SCRATCH_RMF[4];
1290
+ cur_sz = SCRATCH_RMF[5];
1291
+
1292
+ cur_arc_s += seg_len;
1293
+
1294
+ const curr_v = out.add_node(
1295
+ next_px, next_py, next_pz,
1296
+ next_tx, next_ty, next_tz,
1297
+ next_rx, next_ry, next_rz,
1298
+ r_curr,
1299
+ branch_id,
1300
+ level,
1301
+ job.parent_branch_id,
1302
+ t_curr,
1303
+ cur_arc_s,
1304
+ phase_hash
1305
+ );
1306
+
1307
+ const edge_id = out.add_segment(
1308
+ prev_v,
1309
+ curr_v,
1310
+ cur_tx * seg_len,
1311
+ cur_ty * seg_len,
1312
+ cur_tz * seg_len,
1313
+ next_tx * seg_len,
1314
+ next_ty * seg_len,
1315
+ next_tz * seg_len,
1316
+ seg_len,
1317
+ t_prev === 0.0 ? job.parent_attach_t : t_prev
1318
+ );
1319
+
1320
+ // Register segment in scope-local BVH
1321
+ const bvh_pad = Math.max(r_prev, r_curr) * 1.25 + 0.008;
1322
+ const bvh_leaf = bvh.allocate_node();
1323
+ bvh.node_set_user_data(bvh_leaf, edge_id);
1324
+ bvh.node_set_aabb_primitive(
1325
+ bvh_leaf,
1326
+ Math.min(cur_px, next_px) - bvh_pad,
1327
+ Math.min(cur_py, next_py) - bvh_pad,
1328
+ Math.min(cur_pz, next_pz) - bvh_pad,
1329
+ Math.max(cur_px, next_px) + bvh_pad,
1330
+ Math.max(cur_py, next_py) + bvh_pad,
1331
+ Math.max(cur_pz, next_pz) + bvh_pad
1332
+ );
1333
+ bvh.insert_leaf(bvh_leaf);
1334
+
1335
+ seg_branch_id[edge_id] = branch_id;
1336
+ seg_local_idx[edge_id] = local_seg_idx;
1337
+ seg_p0x[edge_id] = cur_px;
1338
+ seg_p0y[edge_id] = cur_py;
1339
+ seg_p0z[edge_id] = cur_pz;
1340
+ seg_r0[edge_id] = r_prev;
1341
+ seg_p1x[edge_id] = next_px;
1342
+ seg_p1y[edge_id] = next_py;
1343
+ seg_p1z[edge_id] = next_pz;
1344
+ seg_r1[edge_id] = r_curr;
1345
+ seg_bvh_leaf[edge_id] = bvh_leaf;
1346
+
1347
+ branch_edges.push(edge_id);
1348
+ branch_edge_t0.push(t_prev);
1349
+ branch_edge_t1.push(t_curr);
1350
+ branch_vertices.push(curr_v);
1351
+
1352
+ out.branch_tip_vertex[branch_id] = curr_v;
1353
+ out.branch_length[branch_id] = cur_arc_s - job.initial_arc_s;
1354
+
1355
+ // Check for dichotomous / sympodial splitting at interior segments (only when stem is structurally thick enough)
1356
+ const min_split_seg = level === 0
1357
+ ? Math.max(2, Math.floor(N_seg * 0.5))
1358
+ : Math.max(1, Math.floor(N_seg * 0.25));
1359
+ if (
1360
+ !job.has_split
1361
+ && lvl_cfg.split_probability > 0.0
1362
+ && r_curr >= 0.012
1363
+ && s >= min_split_seg
1364
+ && s <= Math.floor(N_seg * 0.75)
1365
+ && rng() < lvl_cfg.split_probability
1366
+ ) {
1367
+ job.has_split = true;
1368
+ const fork_count = Math.max(2, lvl_cfg.split_count | 0);
1369
+ const split_ang = lvl_cfg.split_angle;
1370
+ let base_az = rng() * PI2;
1371
+
1372
+ // Preserve pre-split orthonormal triad (T, R, S) before deflecting fork 0
1373
+ const pre_tx = next_tx, pre_ty = next_ty, pre_tz = next_tz;
1374
+ const pre_rx = next_rx, pre_ry = next_ry, pre_rz = next_rz;
1375
+ const pre_sx = cur_sx, pre_sy = cur_sy, pre_sz = cur_sz;
1376
+
1377
+ // On lateral branches (level >= 1), align the split bend axis toward local vertical (+Y)
1378
+ // so the two daughter forks spread horizontally in the canopy rather than pitching into neighbor boughs.
1379
+ if (level >= 1) {
1380
+ const up_az = Math.atan2(pre_sy, pre_ry);
1381
+ const side_sign = base_az < Math.PI ? 1.0 : -1.0;
1382
+ base_az = up_az + (side_sign < 0.0 ? Math.PI : 0.0) + ((base_az % Math.PI) / Math.PI - 0.5) * 0.45;
1383
+ }
1384
+
1385
+ // Current branch deflects as fork 0: rotate both T and R around bend_axis so R remains orthogonal to T
1386
+ const fork0_deflect = level === 0 ? split_ang * 0.22 : split_ang * 0.5;
1387
+ const bend_axis_x = Math.cos(base_az) * pre_rx + Math.sin(base_az) * pre_sx;
1388
+ const bend_axis_y = Math.cos(base_az) * pre_ry + Math.sin(base_az) * pre_sy;
1389
+ const bend_axis_z = Math.cos(base_az) * pre_rz + Math.sin(base_az) * pre_sz;
1390
+ rotate_around_axis(
1391
+ rot_scratch,
1392
+ pre_tx, pre_ty, pre_tz,
1393
+ bend_axis_x, bend_axis_y, bend_axis_z,
1394
+ fork0_deflect
1395
+ );
1396
+ next_tx = rot_scratch[0];
1397
+ next_ty = rot_scratch[1];
1398
+ next_tz = rot_scratch[2];
1399
+
1400
+ rotate_around_axis(
1401
+ rot_scratch,
1402
+ pre_rx, pre_ry, pre_rz,
1403
+ bend_axis_x, bend_axis_y, bend_axis_z,
1404
+ fork0_deflect
1405
+ );
1406
+ next_rx = rot_scratch[0];
1407
+ next_ry = rot_scratch[1];
1408
+ next_rz = rot_scratch[2];
1409
+
1410
+ cur_sx = next_ty * next_rz - next_tz * next_ry;
1411
+ cur_sy = next_tz * next_rx - next_tx * next_rz;
1412
+ cur_sz = next_tx * next_ry - next_ty * next_rx;
1413
+
1414
+ // Spawn fork 1..fork_count-1 as continuation branches with their own root vertex connected to curr_v
1415
+ const rem_segs = Math.max(1, N_seg - s);
1416
+ const rem_fraction = Math.max(0.15, rem_segs / Math.max(1, N_seg));
1417
+ const fork_cum_t0 = lerp(job.cum_t0 ?? 0.0, job.cum_t1 ?? 1.0, t_curr);
1418
+ const fork_cum_t1 = job.cum_t1 ?? 1.0;
1419
+ const fork_len = Math.max(0.15, job.length * rem_fraction);
1420
+ const fork_r0 = r_curr * 0.78;
1421
+ const fork_step = fork_len / rem_segs;
1422
+
1423
+ for (let f = 1; f < fork_count; f++) {
1424
+ const f_az = base_az + (f * PI2) / fork_count;
1425
+ const fax = Math.cos(f_az) * pre_rx + Math.sin(f_az) * pre_sx;
1426
+ const fay = Math.cos(f_az) * pre_ry + Math.sin(f_az) * pre_sy;
1427
+ const faz = Math.cos(f_az) * pre_rz + Math.sin(f_az) * pre_sz;
1428
+
1429
+ const f_tan = [0, 0, 0];
1430
+ rotate_around_axis(f_tan, pre_tx, pre_ty, pre_tz, fax, fay, faz, split_ang * 0.65);
1431
+
1432
+ // Verify fork emergence clearance against BVH (and prevent inward-toward-trunk forks on lateral boughs)
1433
+ if (level >= 1) {
1434
+ const r_horiz_here = Math.hypot(next_px, next_pz);
1435
+ if (r_horiz_here > 0.35 && (next_px * f_tan[0] + next_pz * f_tan[2]) < -0.10 * r_horiz_here) {
1436
+ continue;
1437
+ }
1438
+ const f_clear = evaluate_candidate_clearance(
1439
+ -1, branch_id, local_seg_idx, true, 0,
1440
+ next_px, next_py, next_pz, fork_r0,
1441
+ next_px + f_tan[0] * fork_step,
1442
+ next_py + f_tan[1] * fork_step,
1443
+ next_pz + f_tan[2] * fork_step,
1444
+ fork_r0 * 0.85,
1445
+ 0.002
1446
+ );
1447
+ if (f_clear < -0.002) {
1448
+ continue;
1449
+ }
1450
+ }
1451
+
1452
+ const f_r = [0, 0, 0];
1453
+ rotate_around_axis(f_r, pre_rx, pre_ry, pre_rz, fax, fay, faz, split_ang * 0.65);
1454
+
1455
+ queue.push({
1456
+ level,
1457
+ parent_branch_id: branch_id,
1458
+ parent_edge_id: edge_id,
1459
+ parent_attach_seg_idx: local_seg_idx,
1460
+ parent_attach_t: t_curr,
1461
+ attach_parent_vertex: curr_v,
1462
+ start_vertex: -1,
1463
+ px: next_px,
1464
+ py: next_py,
1465
+ pz: next_pz,
1466
+ tx: f_tan[0],
1467
+ ty: f_tan[1],
1468
+ tz: f_tan[2],
1469
+ rx: f_r[0],
1470
+ ry: f_r[1],
1471
+ rz: f_r[2],
1472
+ emerge_drx: fax,
1473
+ emerge_dry: fay,
1474
+ emerge_drz: faz,
1475
+ length: fork_len,
1476
+ full_branch_length: job.full_branch_length ?? job.length,
1477
+ base_radius: fork_r0,
1478
+ start_seg: 0,
1479
+ total_segs: rem_segs,
1480
+ initial_arc_s: cur_arc_s,
1481
+ has_split: true,
1482
+ is_split_fork: true,
1483
+ child_span_fraction: rem_fraction,
1484
+ cum_t0: fork_cum_t0,
1485
+ cum_t1: fork_cum_t1
1486
+ });
1487
+ }
1488
+ }
1489
+
1490
+ cur_px = next_px;
1491
+ cur_py = next_py;
1492
+ cur_pz = next_pz;
1493
+ cur_tx = next_tx;
1494
+ cur_ty = next_ty;
1495
+ cur_tz = next_tz;
1496
+ cur_rx = next_rx;
1497
+ cur_ry = next_ry;
1498
+ cur_rz = next_rz;
1499
+ prev_v = curr_v;
1500
+ }
1501
+
1502
+ // If the branch was self-pruned early on contact (actual_segs < remaining_segs), renormalize
1503
+ // t_norm and taper radii across the surviving vertices [0..actual_segs] so its tip terminates cleanly at t_norm = 1.0
1504
+ const actual_segs = branch_edges.length;
1505
+ if (actual_segs > 0 && actual_segs < remaining_segs) {
1506
+ const s_param = /** @type {Float32Array} */ (out.attr_node_spine_param.data);
1507
+ for (let i = 0; i < actual_segs; i++) {
1508
+ const tn0 = i / actual_segs;
1509
+ const tn1 = (i + 1) / actual_segs;
1510
+ branch_edge_t0[i] = tn0;
1511
+ branch_edge_t1[i] = tn1;
1512
+
1513
+ const v_curr = branch_vertices[i + 1];
1514
+ s_param[v_curr * 3] = tn1;
1515
+
1516
+ const u_p = prof_span > 1e-4
1517
+ ? clamp((prof_p0 - lvl_cfg.radius_profile.evaluate(tn1)) / prof_span, 0.0, 1.0)
1518
+ : tn1;
1519
+ const min_tf = has_children ? 0.14 : 0.24;
1520
+ const ap_taper = has_children && tn1 > 0.80
1521
+ ? lerp(1.0, 0.62, Math.pow((tn1 - 0.80) / 0.20, 1.35))
1522
+ : 1.0;
1523
+ const r_new = Math.max(
1524
+ 0.002,
1525
+ job.base_radius * Math.max(1.0 - lvl_cfg.taper * u_p, min_tf) * ap_taper
1526
+ );
1527
+ out.node_write_radius(v_curr, r_new);
1528
+
1529
+ const e_curr = branch_edges[i];
1530
+ seg_r1[e_curr] = r_new;
1531
+ if (i + 1 < actual_segs) {
1532
+ seg_r0[branch_edges[i + 1]] = r_new;
1533
+ }
1534
+ }
1535
+ }
1536
+
1537
+ // Smooth or preserve crisp interior Hermite tangents across consecutive edges of this branch
1538
+ // (preserving branch_edges[0]'s m0 so continuation forks keep their exact fork departure tangent)
1539
+ if (branch_edges.length >= 2) {
1540
+ const kink_sharp = clamp(lvl_cfg.kink_sharpness ?? 0.5, 0.0, 0.92);
1541
+ for (let k = 1; k < branch_edges.length; k++) {
1542
+ const e_prev = branch_edges[k - 1];
1543
+ const e_next = branch_edges[k];
1544
+ const v_mid = branch_vertices[k];
1545
+ const l_prev = Math.max(1e-5, out.segment_length(e_prev));
1546
+ const l_next = Math.max(1e-5, out.segment_length(e_next));
1547
+
1548
+ out.segment_read_m1(SCRATCH_M_PREV, 0, e_prev);
1549
+ out.segment_read_m0(SCRATCH_M_NEXT, 0, e_next);
1550
+
1551
+ const t_in_x = SCRATCH_M_PREV[0] / l_prev;
1552
+ const t_in_y = SCRATCH_M_PREV[1] / l_prev;
1553
+ const t_in_z = SCRATCH_M_PREV[2] / l_prev;
1554
+ const t_out_x = SCRATCH_M_NEXT[0] / l_next;
1555
+ const t_out_y = SCRATCH_M_NEXT[1] / l_next;
1556
+ const t_out_z = SCRATCH_M_NEXT[2] / l_next;
1557
+
1558
+ let tx = t_in_x + t_out_x;
1559
+ let ty = t_in_y + t_out_y;
1560
+ let tz = t_in_z + t_out_z;
1561
+ const t_len = Math.sqrt(tx * tx + ty * ty + tz * tz);
1562
+ if (t_len > 1e-12) {
1563
+ tx /= t_len;
1564
+ ty /= t_len;
1565
+ tz /= t_len;
1566
+
1567
+ out.node_write_tangent(v_mid, tx, ty, tz);
1568
+
1569
+ // Blend between smooth C1 average tangent and crisp sympodial kink tangents
1570
+ let m1x = lerp(tx, t_in_x, kink_sharp);
1571
+ let m1y = lerp(ty, t_in_y, kink_sharp);
1572
+ let m1z = lerp(tz, t_in_z, kink_sharp);
1573
+ const m1_l = Math.sqrt(m1x * m1x + m1y * m1y + m1z * m1z);
1574
+ if (m1_l > 1e-12) { m1x /= m1_l; m1y /= m1_l; m1z /= m1_l; }
1575
+
1576
+ let m0x = lerp(tx, t_out_x, kink_sharp);
1577
+ let m0y = lerp(ty, t_out_y, kink_sharp);
1578
+ let m0z = lerp(tz, t_out_z, kink_sharp);
1579
+ const m0_l = Math.sqrt(m0x * m0x + m0y * m0y + m0z * m0z);
1580
+ if (m0_l > 1e-12) { m0x /= m0_l; m0y /= m0_l; m0z /= m0_l; }
1581
+
1582
+ out.segment_write_m1(e_prev, m1x * l_prev, m1y * l_prev, m1z * l_prev);
1583
+ out.segment_write_m0(e_next, m0x * l_next, m0y * l_next, m0z * l_next);
1584
+ }
1585
+ }
1586
+ }
1587
+
1588
+ // Spawn lateral child branches at level + 1 using pre-scheduled attachments
1589
+ const next_level = level + 1;
1590
+ if (scheduled_children.length > 0 && branch_edges.length > 0) {
1591
+ const next_cfg = species.levels[next_level];
1592
+ const t_max = branch_edge_t1[branch_edge_t1.length - 1];
1593
+ const PHI_CANDIDATES = [0.0, 0.36, -0.36, 0.72, -0.72, 1.08, -1.08, 1.48, -1.48];
1594
+
1595
+ for (let c = 0; c < scheduled_children.length; c++) {
1596
+ const sc = scheduled_children[c];
1597
+ if (sc.t_attach > t_max * 1.02) continue;
1598
+ const t_attach = clamp(sc.t_attach, branch_edge_t0[0], t_max * 0.985);
1599
+ const base_phi_c = sc.phi;
1600
+
1601
+ // Find parent edge containing t_attach
1602
+ let seg_idx = 0;
1603
+ for (let i = 0; i < branch_edges.length; i++) {
1604
+ if (t_attach >= branch_edge_t0[i] && t_attach <= branch_edge_t1[i]) {
1605
+ seg_idx = i;
1606
+ break;
1607
+ }
1608
+ }
1609
+
1610
+ const parent_e = branch_edges[seg_idx];
1611
+ const t0_e = branch_edge_t0[seg_idx];
1612
+ const t1_e = branch_edge_t1[seg_idx];
1613
+ const local_u = clamp((t_attach - t0_e) / Math.max(1e-5, t1_e - t0_e), 0.0, 1.0);
1614
+
1615
+ out.evaluate_segment_position(SCRATCH_POS, 0, parent_e, local_u);
1616
+ out.evaluate_segment_tangent(SCRATCH_TAN, 0, parent_e, local_u);
1617
+
1618
+ const p_v0 = branch_vertices[seg_idx];
1619
+ const p_v1 = branch_vertices[seg_idx + 1];
1620
+ const r_p0 = out.node_read_radius(p_v0);
1621
+ const r_p1 = out.node_read_radius(p_v1);
1622
+ const parent_r_at_attach = lerp(r_p0, r_p1, local_u);
1623
+ if (parent_r_at_attach < 0.004) continue;
1624
+
1625
+ // Transport parent RMF R vector from p_v0 along parent_e to SCRATCH_POS at local_u
1626
+ out.node_read_position(SCRATCH_P0, 0, p_v0);
1627
+ out.node_read_tangent(SCRATCH_T0, 0, p_v0);
1628
+ out.node_read_frame_r(SCRATCH_R0, 0, p_v0);
1629
+ const p_rx = SCRATCH_R0[0];
1630
+ const p_ry = SCRATCH_R0[1];
1631
+ const p_rz = SCRATCH_R0[2];
1632
+
1633
+ spine_frame_double_reflection(
1634
+ SCRATCH_RMF, 0,
1635
+ SCRATCH_P0[0], SCRATCH_P0[1], SCRATCH_P0[2],
1636
+ SCRATCH_T0[0], SCRATCH_T0[1], SCRATCH_T0[2],
1637
+ p_rx, p_ry, p_rz,
1638
+ SCRATCH_POS[0], SCRATCH_POS[1], SCRATCH_POS[2],
1639
+ SCRATCH_TAN[0], SCRATCH_TAN[1], SCRATCH_TAN[2]
1640
+ );
1641
+ const prx = SCRATCH_RMF[0], pry = SCRATCH_RMF[1], prz = SCRATCH_RMF[2];
1642
+ const psx = SCRATCH_RMF[3], psy = SCRATCH_RMF[4], psz = SCRATCH_RMF[5];
1643
+
1644
+ // Genealogical position along the full parent axis (accounts for continuation forks)
1645
+ const t_genealogical = lerp(job.cum_t0 ?? 0.0, job.cum_t1 ?? 1.0, t_attach);
1646
+
1647
+ // Insertion polar angle alpha from parent tangent T_p toward D_rad
1648
+ const alpha_scale = lvl_cfg.insertion_angle_profile.evaluate(t_genealogical);
1649
+ const alpha = clamp(
1650
+ lvl_cfg.insertion_angle * alpha_scale + (rng() * 2.0 - 1.0) * lvl_cfg.insertion_angle_variation,
1651
+ 0.1,
1652
+ Math.PI * 0.85
1653
+ );
1654
+ const cos_a = Math.cos(alpha);
1655
+ const sin_a = Math.sin(alpha);
1656
+ const var_scale = 1.0 + (rng() * 2.0 - 1.0) * next_cfg.length_variance;
1657
+ const w_plagio = level >= 1
1658
+ ? (1.0 - clamp(((next_cfg.gravitropism ?? 0.0) - 0.45) / 0.35, 0.0, 1.0))
1659
+ : 0.0;
1660
+
1661
+ // Helper to compute radial normal D_rad and child tangent T_c for a given azimuth phi_cand
1662
+ const compute_child_basis = (phi_cand, out_dr, out_ct) => {
1663
+ const cp = Math.cos(phi_cand);
1664
+ const sp = Math.sin(phi_cand);
1665
+ let dx_r = cp * prx + sp * psx;
1666
+ let dy_r = cp * pry + sp * psy;
1667
+ let dz_r = cp * prz + sp * psz;
1668
+
1669
+ if (w_plagio > 1e-3) {
1670
+ const flat_dry = dy_r < 0.0 ? dy_r * 0.15 + 0.08 : dy_r * 0.75 + 0.12;
1671
+ dy_r = lerp(dy_r, flat_dry, w_plagio);
1672
+ const dot_tp = dx_r * SCRATCH_TAN[0] + dy_r * SCRATCH_TAN[1] + dz_r * SCRATCH_TAN[2];
1673
+ dx_r -= dot_tp * SCRATCH_TAN[0];
1674
+ dy_r -= dot_tp * SCRATCH_TAN[1];
1675
+ dz_r -= dot_tp * SCRATCH_TAN[2];
1676
+ const d_len = Math.sqrt(dx_r * dx_r + dy_r * dy_r + dz_r * dz_r);
1677
+ if (d_len > 1e-6) {
1678
+ dx_r /= d_len;
1679
+ dy_r /= d_len;
1680
+ dz_r /= d_len;
1681
+ } else {
1682
+ dx_r = cp * prx + sp * psx;
1683
+ dy_r = cp * pry + sp * psy;
1684
+ dz_r = cp * prz + sp * psz;
1685
+ }
1686
+ }
1687
+ out_dr[0] = dx_r;
1688
+ out_dr[1] = dy_r;
1689
+ out_dr[2] = dz_r;
1690
+
1691
+ let tx_c = cos_a * SCRATCH_TAN[0] + sin_a * dx_r;
1692
+ let ty_c = cos_a * SCRATCH_TAN[1] + sin_a * dy_r;
1693
+ let tz_c = cos_a * SCRATCH_TAN[2] + sin_a * dz_r;
1694
+ const ct_l = Math.sqrt(tx_c * tx_c + ty_c * ty_c + tz_c * tz_c) || 1.0;
1695
+ out_ct[0] = tx_c / ct_l;
1696
+ out_ct[1] = ty_c / ct_l;
1697
+ out_ct[2] = tz_c / ct_l;
1698
+ };
1699
+
1700
+ const cand_dr = [0, 0, 0];
1701
+ const cand_ct = [0, 0, 0];
1702
+ compute_child_basis(base_phi_c, cand_dr, cand_ct);
1703
+
1704
+ // 1. Raw configured child branch length modulated by length_profile at t_genealogical and crown envelope
1705
+ const prof_scale = Math.max(0.12, next_cfg.length_profile.evaluate(t_genealogical));
1706
+ const est_mid_y = SCRATCH_POS[1] + Math.max(0.0, cand_ct[1]) * next_cfg.length * prof_scale * 0.35;
1707
+ const h_norm = clamp((est_mid_y - crown.base_height) / Math.max(0.1, crown.height), 0.0, 1.0);
1708
+ const env_scale = level === 0
1709
+ ? crown_envelope_evaluate(crown.envelope_type, h_norm)
1710
+ : lerp(1.0, crown_envelope_evaluate(crown.envelope_type, h_norm), 0.5);
1711
+ const raw_child_len = Math.max(0.12, next_cfg.length * prof_scale * env_scale * var_scale);
1712
+
1713
+ // 2. Continuous Gravity-Orientation & Allometric Slenderness Coupling (Cantilever vs. Pendulous Tension)
1714
+ const rho_design = Math.max(0.012, next_cfg.radius_ratio);
1715
+ const s_design = 1.0 / (2.0 * rho_design);
1716
+ const g_trop = next_cfg.gravitropism ?? 0.0;
1717
+ const u_curl = next_cfg.tip_upcurl ?? 0.0;
1718
+ const t_eff_y = clamp(cand_ct[1] - 0.85 * g_trop + 0.35 * u_curl, -1.0, 1.0);
1719
+
1720
+ // Cantilever load (horizontal bending / upward compression) vs. pendulous tensile compliance
1721
+ const k_cantilever = Math.sqrt(Math.max(0.0, 1.0 - t_eff_y * t_eff_y)) + 0.65 * Math.max(0.0, t_eff_y);
1722
+ const tau_tension = clamp(g_trop, 0.0, 1.0) * Math.max(0.0, -t_eff_y);
1723
+
1724
+ // 3. Parent-relative distal conical envelope (prevents distal shoots or split forks from outgrowing their parent)
1725
+ const actual_parent_len = Math.max(0.15, out.branch_length[branch_id]);
1726
+ const parent_full_len = Math.max(actual_parent_len, job.full_branch_length ?? actual_parent_len);
1727
+ const distal_tip_ratio = lerp(0.32, 0.55, tau_tension);
1728
+ const cone_max_len = level === 0
1729
+ ? parent_full_len * lerp(0.85, 0.32, Math.pow(t_genealogical, 1.6))
1730
+ : Math.min(
1731
+ actual_parent_len * lerp(0.90, 1.05, tau_tension),
1732
+ parent_full_len * lerp(0.85, distal_tip_ratio, Math.pow(t_genealogical, 1.25))
1733
+ );
1734
+ const desired_len = Math.max(0.12, Math.min(raw_child_len, cone_max_len));
1735
+
1736
+ const nom_len = Math.max(0.15, next_cfg.length);
1737
+ const r_design = (nom_len * rho_design * Math.pow(desired_len / nom_len, 0.75)) / (1.0 + 0.65 * tau_tension);
1738
+ const child_base_r = Math.min(
1739
+ parent_r_at_attach * 0.78,
1740
+ Math.max(0.0035, r_design)
1741
+ );
1742
+
1743
+ const gamma = lerp(0.82, 0.65, tau_tension);
1744
+ const allometric_len = child_base_r < r_design
1745
+ ? desired_len * Math.pow(child_base_r / Math.max(1e-5, r_design), gamma)
1746
+ : desired_len;
1747
+ const s_max = s_design * (1.35 + 0.65 * tau_tension) / (0.65 + 0.35 * k_cantilever);
1748
+ const child_len = clamp(Math.min(allometric_len, 2.0 * child_base_r * s_max), 0.12, desired_len);
1749
+ const child_segs = Math.max(1, next_cfg.segments);
1750
+ const child_step = child_len / child_segs;
1751
+
1752
+ // 4. Pre-Spawn BVH Bud Clearance Probing (selects open canopy azimuth or inhibits blocked bud)
1753
+ let best_drx = cand_dr[0], best_dry = cand_dr[1], best_drz = cand_dr[2];
1754
+ let best_ctx = cand_ct[0], best_cty = cand_ct[1], best_ctz = cand_ct[2];
1755
+ let best_clear = -Infinity;
1756
+ const r_horiz_attach = Math.hypot(SCRATCH_POS[0], SCRATCH_POS[2]);
1757
+
1758
+ for (let k = 0; k < PHI_CANDIDATES.length; k++) {
1759
+ if (k > 0) {
1760
+ compute_child_basis(base_phi_c + PHI_CANDIDATES[k], cand_dr, cand_ct);
1761
+ }
1762
+ // Prevent lateral shoots on boughs from pointing backward into the trunk core
1763
+ if (level >= 1 && r_horiz_attach > 0.30) {
1764
+ const radial_dot = (SCRATCH_POS[0] * cand_ct[0] + SCRATCH_POS[2] * cand_ct[2]) / r_horiz_attach;
1765
+ if (radial_dot < -0.15) continue;
1766
+ }
1767
+
1768
+ const p1_x = SCRATCH_POS[0] + cand_ct[0] * child_step;
1769
+ const p1_y = SCRATCH_POS[1] + cand_ct[1] * child_step;
1770
+ const p1_z = SCRATCH_POS[2] + cand_ct[2] * child_step;
1771
+
1772
+ const clear0 = evaluate_candidate_clearance(
1773
+ -1, branch_id, seg_idx, false, 0,
1774
+ SCRATCH_POS[0], SCRATCH_POS[1], SCRATCH_POS[2], child_base_r,
1775
+ p1_x, p1_y, p1_z, child_base_r * 0.88,
1776
+ 0.002
1777
+ );
1778
+
1779
+ const p2_x = p1_x + cand_ct[0] * child_step;
1780
+ const p2_y = p1_y + cand_ct[1] * child_step;
1781
+ const p2_z = p1_z + cand_ct[2] * child_step;
1782
+ const clear1 = child_segs >= 2
1783
+ ? evaluate_candidate_clearance(
1784
+ -1, branch_id, seg_idx, false, 1,
1785
+ p1_x, p1_y, p1_z, child_base_r * 0.88,
1786
+ p2_x, p2_y, p2_z, child_base_r * 0.75,
1787
+ 0.002
1788
+ )
1789
+ : clear0;
1790
+
1791
+ const min_c = Math.min(clear0, clear1);
1792
+ if (min_c > best_clear) {
1793
+ best_clear = min_c;
1794
+ best_drx = cand_dr[0];
1795
+ best_dry = cand_dr[1];
1796
+ best_drz = cand_dr[2];
1797
+ best_ctx = cand_ct[0];
1798
+ best_cty = cand_ct[1];
1799
+ best_ctz = cand_ct[2];
1800
+ }
1801
+ if (min_c >= 0.004) {
1802
+ break;
1803
+ }
1804
+ }
1805
+
1806
+ // Botanical bud inhibition: if all candidate directions are blocked by existing branches, suppress bud
1807
+ if (best_clear < -0.004) {
1808
+ continue;
1809
+ }
1810
+
1811
+ const drx = best_drx, dry = best_dry, drz = best_drz;
1812
+ const ctx = best_ctx, cty = best_cty, ctz = best_ctz;
1813
+
1814
+ // Initial RMF radial vector for child branch:
1815
+ // R_c = cos(alpha) * D_rad - sin(alpha) * T_p is exact unit length and orthogonal
1816
+ // to T_c = cos(alpha) * T_p + sin(alpha) * D_rad for all alpha in [0, pi],
1817
+ // avoiding any singularity at alpha = pi/2.
1818
+ let crx = cos_a * drx - sin_a * SCRATCH_TAN[0];
1819
+ let cry = cos_a * dry - sin_a * SCRATCH_TAN[1];
1820
+ let crz = cos_a * drz - sin_a * SCRATCH_TAN[2];
1821
+ const cr_dot = crx * ctx + cry * cty + crz * ctz;
1822
+ crx -= cr_dot * ctx;
1823
+ cry -= cr_dot * cty;
1824
+ crz -= cr_dot * ctz;
1825
+ const cr_len = Math.sqrt(crx * crx + cry * cry + crz * crz);
1826
+ if (cr_len > 1e-12) {
1827
+ crx /= cr_len;
1828
+ cry /= cr_len;
1829
+ crz /= cr_len;
1830
+ }
1831
+
1832
+ queue.push({
1833
+ level: next_level,
1834
+ parent_branch_id: branch_id,
1835
+ parent_edge_id: parent_e,
1836
+ parent_attach_seg_idx: seg_idx,
1837
+ parent_attach_t: t_attach,
1838
+ attach_parent_vertex: p_v0,
1839
+ start_vertex: -1,
1840
+ px: SCRATCH_POS[0],
1841
+ py: SCRATCH_POS[1],
1842
+ pz: SCRATCH_POS[2],
1843
+ tx: ctx,
1844
+ ty: cty,
1845
+ tz: ctz,
1846
+ rx: crx,
1847
+ ry: cry,
1848
+ rz: crz,
1849
+ emerge_drx: drx,
1850
+ emerge_dry: dry,
1851
+ emerge_drz: drz,
1852
+ length: child_len,
1853
+ full_branch_length: child_len,
1854
+ base_radius: child_base_r,
1855
+ start_seg: 0,
1856
+ total_segs: child_segs,
1857
+ initial_arc_s: 0.0,
1858
+ has_split: false,
1859
+ is_split_fork: false,
1860
+ cum_t0: 0.0,
1861
+ cum_t1: 1.0
1862
+ });
1863
+ }
1864
+ }
1865
+ }
1866
+
1867
+ // Bottom-up Pipe Model radius calculation & blending
1868
+ tree_skeleton_compute_pipe_radii(out, species);
1869
+
1870
+ return out;
1871
+ }