use bevy::math::Vec3;
use super::MeshData;
use super::rings::{RingBasis, connect_rings, emit_cap, emit_ring};
use crate::params::{TrunkParams, TrunkTermination};
use crate::skeleton::{Node, trunk_radius_at, trunk_wobble};
const TRUNK_BASIS: RingBasis = RingBasis {
right: Vec3::X,
forward: Vec3::NEG_Z,
};
const POINTED_TIP_ASPECT: f32 = 4.0;
pub fn trunk_mesh(trunk: &TrunkParams, node: &Node, seed: i32) -> MeshData {
let mut mesh = MeshData::default();
if node.progress <= 0.0 {
return mesh;
}
let segments = trunk.radial_segments.max(3);
let rings = trunk.height_segments.max(1) + 1;
let base_radius = trunk.radius * trunk.flare;
let root_height = trunk.root_flare_height * trunk.height;
let has_root_flares = trunk.root_flare_count > 0 && trunk.root_flare_spread > 0.0;
let rp = node.radius_progress;
let bulge = |angle: f32, root_blend: f32| -> f32 {
if root_blend <= 0.0 {
return 0.0;
}
let raw = ((angle * trunk.root_flare_count as f32).cos() * 0.5 + 0.5).powi(2);
raw * trunk.root_flare_spread * base_radius * root_blend
};
let root_blend_at = |y: f32| -> f32 {
if has_root_flares && y < root_height {
1.0 - y / root_height
} else {
0.0
}
};
let center_at = |t: f32| -> Vec3 {
let (wx, wz) = trunk_wobble(trunk, seed, t);
Vec3::new(wx, t * trunk.height, wz)
};
let mut prev_ring = None;
for k in 0..rings {
let t = (k as f32 / (rings - 1) as f32) * node.progress;
let radius = trunk_radius_at(trunk, t);
let root_blend = root_blend_at(t * trunk.height);
let twist = (t * trunk.twist).to_radians();
let ring = emit_ring(&mut mesh, center_at(t), TRUNK_BASIS, segments, twist, t, |_, angle| {
(radius + bulge(angle - twist, root_blend)) * rp
});
if let Some(prev) = prev_ring {
connect_rings(&mut mesh, prev, ring, segments);
}
prev_ring = Some(ring);
}
if let (TrunkTermination::PointedTip, Some(top_ring)) = (trunk.termination, prev_ring)
&& node.progress >= 1.0
{
let tip_length = trunk_radius_at(trunk, 1.0) * rp * POINTED_TIP_ASPECT;
let apex = center_at(1.0) + Vec3::Y * tip_length;
let twist = trunk.twist.to_radians();
let tip = emit_ring(&mut mesh, apex, TRUNK_BASIS, segments, twist, 1.0, |_, _| 0.0);
connect_rings(&mut mesh, top_ring, tip, segments);
}
let bottom_blend = root_blend_at(0.0);
emit_cap(&mut mesh, Vec3::ZERO, TRUNK_BASIS, segments, 0.0, Vec3::NEG_Y, |_, angle| {
(base_radius + bulge(angle, bottom_blend)) * rp
});
let top_cap = match trunk.termination {
TrunkTermination::FlatCap => true,
TrunkTermination::PointedTip | TrunkTermination::LeaderBranch => node.progress < 1.0,
};
if top_cap {
let t_top = node.progress;
let radius = trunk_radius_at(trunk, t_top);
let root_blend = root_blend_at(t_top * trunk.height);
let twist = (t_top * trunk.twist).to_radians();
emit_cap(&mut mesh, center_at(t_top), TRUNK_BASIS, segments, twist, Vec3::Y, |_, angle| {
(radius + bulge(angle - twist, root_blend)) * rp
});
}
mesh
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mesh::rings::tests::{assert_faces_outward, assert_indices_valid};
use crate::presets::TreePreset;
use crate::skeleton::generate_skeleton;
#[test]
fn all_triangles_face_outward() {
for preset in TreePreset::ALL {
let params = preset.params();
let skeleton = generate_skeleton(¶ms);
let mesh = trunk_mesh(¶ms.trunk, &skeleton.nodes[0], params.seed);
assert!(!mesh.is_empty());
assert_indices_valid(&mesh, preset.name());
assert_faces_outward(&mesh, preset.name());
}
}
#[test]
fn growing_trunk_is_capped_and_outward() {
let params = TreePreset::Pine.params(); let skeleton = generate_skeleton(¶ms);
let aged = skeleton.aged(1.0, ¶ms.growth);
let node = &aged.nodes[0];
assert!(node.progress > 0.0 && node.progress < 1.0);
let mesh = trunk_mesh(¶ms.trunk, node, params.seed);
assert_indices_valid(&mesh, "growing pine");
assert_faces_outward(&mesh, "growing pine");
let top = mesh.positions.iter().map(|p| p.y).fold(0.0, f32::max);
assert!((top - params.trunk.height * node.progress).abs() < 1e-4);
}
#[test]
fn trunk_progress_zero_is_empty() {
let params = TreePreset::Oak.params();
let skeleton = generate_skeleton(¶ms);
let aged = skeleton.aged(0.0, ¶ms.growth);
let mesh = trunk_mesh(¶ms.trunk, &aged.nodes[0], params.seed);
assert!(mesh.is_empty());
assert!(mesh.positions.is_empty());
}
#[test]
fn indices_in_range() {
let params = TreePreset::Baobab.params(); let skeleton = generate_skeleton(¶ms);
let mesh = trunk_mesh(¶ms.trunk, &skeleton.nodes[0], params.seed);
assert_indices_valid(&mesh, "baobab");
let max = *mesh.indices.iter().max().unwrap() as usize;
assert!(max < mesh.positions.len());
}
#[test]
fn flat_and_pointed_trunks_are_closed() {
for termination in [TrunkTermination::FlatCap, TrunkTermination::PointedTip] {
for taper in [0.3, 0.05] {
let mut params = TreePreset::Oak.params();
params.trunk.termination = termination;
params.trunk.taper = taper;
let skeleton = generate_skeleton(¶ms);
let mesh = trunk_mesh(¶ms.trunk, &skeleton.nodes[0], params.seed);
let key = |i: u32| (mesh.positions[i as usize] * 1.0e4).round().as_ivec3().to_array();
let mut uses = std::collections::HashMap::new();
for &[a, b, c] in mesh.indices.as_chunks::<3>().0 {
for (u, v) in [(a, b), (b, c), (c, a)] {
let (ku, kv) = (key(u), key(v));
if ku != kv {
*uses.entry((ku.min(kv), ku.max(kv))).or_insert(0) += 1;
}
}
}
let open = uses.values().filter(|&&n| n == 1).count();
assert_eq!(open, 0, "{termination:?} taper {taper}: {open} open edges");
assert_faces_outward(&mesh, "closed trunk");
}
}
}
}