use bevy::reflect::Reflect;
use crate::skeleton::{Node, NodeKind, Skeleton};
#[derive(Clone, Debug, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default, deny_unknown_fields))]
pub struct GrowthParams {
pub maturity_age: f32,
pub trunk_fraction: f32,
pub branch_fraction: f32,
pub child_duration_scale: f32,
pub thickening_lag: f32,
pub leaf_threshold: f32,
}
impl Default for GrowthParams {
fn default() -> Self {
Self {
maturity_age: 10.0,
trunk_fraction: 0.4,
branch_fraction: 0.35,
child_duration_scale: 0.8,
thickening_lag: 2.0,
leaf_threshold: 0.5,
}
}
}
pub fn assign_birth_times(skeleton: &mut Skeleton, growth: &GrowthParams) {
let branch_duration = growth.maturity_age * growth.branch_fraction;
for i in 0..skeleton.nodes.len() {
let Some(parent_idx) = skeleton.nodes[i].parent else {
let trunk = &mut skeleton.nodes[i];
trunk.birth = 0.0;
trunk.duration = growth.maturity_age * growth.trunk_fraction;
continue;
};
let parent = &skeleton.nodes[parent_idx as usize];
let (birth, duration) = match skeleton.nodes[i].kind {
NodeKind::Trunk => unreachable!("trunk has no parent"),
NodeKind::Primary | NodeKind::Leader => {
(parent.duration * skeleton.nodes[i].attach_t, branch_duration)
}
NodeKind::Split => (parent.birth + parent.duration, branch_duration),
NodeKind::Sub | NodeKind::Strand => (
parent.birth + parent.duration * skeleton.nodes[i].attach_t,
parent.duration * growth.child_duration_scale,
),
};
let node = &mut skeleton.nodes[i];
node.birth = birth;
node.duration = duration;
}
}
pub(crate) fn fraction(age: f32, birth: f32, duration: f32) -> f32 {
if duration <= 0.0 {
if age >= birth { 1.0 } else { 0.0 }
} else {
((age - birth) / duration).clamp(0.0, 1.0)
}
}
impl Skeleton {
pub fn aged(&self, age: f32, growth: &GrowthParams) -> Skeleton {
let mut nodes: Vec<Node> = Vec::with_capacity(self.nodes.len());
let mut remap: Vec<Option<u32>> = vec![None; self.nodes.len()];
let mut shift: Vec<bevy::math::Vec3> = Vec::with_capacity(self.nodes.len());
for (i, node) in self.nodes.iter().enumerate() {
let parent_new = match node.parent {
None => None,
Some(p) => match remap[p as usize] {
Some(new_p) if age > node.birth => Some(new_p),
_ => continue,
},
};
let progress = fraction(age, node.birth, node.duration);
let radius_progress = fraction(age, node.birth, node.duration * growth.thickening_lag);
let (start, anchor) = match parent_new {
None => (node.start, node.anchor),
Some(p) => {
let parent = &nodes[p as usize];
if parent.kind == NodeKind::Trunk {
let anchor = node.anchor + shift[p as usize];
let radial = (node.start - node.anchor) * parent.radius_progress;
(anchor + radial, anchor)
} else {
let along = if parent.progress > 0.0 {
node.attach_t / parent.progress
} else {
0.0
};
let start = parent.point_at(along);
(start, start)
}
}
};
remap[i] = Some(nodes.len() as u32);
shift.push(start - node.start);
nodes.push(Node {
parent: parent_new,
anchor,
start,
length: node.length * progress,
base_radius: node.base_radius * radius_progress,
tip_radius: node.tip_radius * radius_progress,
progress,
radius_progress,
..node.clone()
});
}
Skeleton { nodes }
}
pub fn mature_at(&self, growth: &GrowthParams) -> f32 {
self.nodes
.iter()
.map(|n| n.birth + n.duration * growth.thickening_lag.max(1.0))
.fold(0.0, f32::max)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::presets::TreePreset;
use crate::skeleton::generate_skeleton;
#[test]
fn age_zero_has_only_trunk() {
let params = TreePreset::Oak.params();
let full = generate_skeleton(¶ms);
let aged = full.aged(0.0, ¶ms.growth);
assert_eq!(aged.nodes.len(), 1);
assert_eq!(aged.nodes[0].kind, NodeKind::Trunk);
assert_eq!(aged.nodes[0].progress, 0.0);
assert_eq!(aged.nodes[0].length, 0.0);
}
#[test]
fn mature_equals_unaged() {
let params = TreePreset::Oak.params();
let full = generate_skeleton(¶ms);
let aged = full.aged(f32::INFINITY, ¶ms.growth);
assert_eq!(aged.nodes.len(), full.nodes.len());
for (a, b) in aged.nodes.iter().zip(&full.nodes) {
assert_eq!(a.length, b.length);
assert_eq!(a.start, b.start);
assert_eq!(a.base_radius, b.base_radius);
assert_eq!(a.progress, 1.0);
assert_eq!(a.child_count, b.child_count);
}
}
#[test]
fn child_born_when_parent_reaches_attach() {
let params = TreePreset::Oak.params();
let full = generate_skeleton(¶ms);
let (child_idx, child) = full
.nodes
.iter()
.enumerate()
.find(|(_, n)| n.kind == NodeKind::Sub)
.expect("oak has sub-branches");
let parent_idx = child.parent.unwrap() as usize;
let parent_full_length = full.nodes[parent_idx].length;
let age = child.birth + 1e-3;
let aged = full.aged(age, ¶ms.growth);
let aged_child = aged
.nodes
.iter()
.find(|n| n.kind == NodeKind::Sub && n.birth == child.birth && n.attach_t == child.attach_t)
.unwrap_or_else(|| panic!("child {child_idx} should exist at age {age}"));
let aged_parent = &aged.nodes[aged_child.parent.unwrap() as usize];
let expected = parent_full_length * child.attach_t;
assert!(
(aged_parent.length - expected).abs() < 1e-3,
"parent length {} vs expected {expected}",
aged_parent.length
);
let parent_end = aged_parent.start + aged_parent.direction * aged_parent.length;
assert!(aged_child.start.distance(parent_end) < 1e-3);
}
#[test]
fn monotone_node_count() {
let params = TreePreset::Oak.params();
let full = generate_skeleton(¶ms);
let mut last = 0;
let mut age = 0.0;
while age <= params.growth.maturity_age {
let n = full.aged(age, ¶ms.growth).nodes.len();
assert!(n >= last, "node count dropped from {last} to {n} at age {age}");
last = n;
age += 1.0;
}
}
#[test]
fn primaries_sit_on_thin_trunk() {
let mut params = TreePreset::Oak.params();
params.trunk.randomness = 0.0;
let full = generate_skeleton(¶ms);
let g = ¶ms.growth;
let trunk_duration = g.maturity_age * g.trunk_fraction;
let aged = full.aged(trunk_duration, g);
let trunk = &aged.nodes[0];
assert!(trunk.radius_progress < 1.0);
for n in aged.nodes.iter().filter(|n| n.kind == NodeKind::Primary) {
let t = n.start.y / params.trunk.height;
let r = crate::skeleton::trunk_radius_at(¶ms.trunk, t) * trunk.radius_progress;
let horizontal = (n.start.x * n.start.x + n.start.z * n.start.z).sqrt();
assert!((horizontal - r).abs() < 1e-4, "{horizontal} vs {r}");
}
}
#[test]
fn trunk_attached_anchors_stay_on_the_trunk_axis() {
let params = TreePreset::Oak.params(); let full = generate_skeleton(¶ms);
for age in [2.0, 4.0, 6.0, f32::INFINITY] {
let aged = full.aged(age, ¶ms.growth);
for n in aged.nodes.iter().filter(|n| n.kind == NodeKind::Primary) {
let t = n.anchor.y / params.trunk.height;
let (wx, wz) = crate::skeleton::trunk_wobble(¶ms.trunk, params.seed, t);
let off_axis = (n.anchor.x - wx).hypot(n.anchor.z - wz);
assert!(off_axis < 1e-4, "age {age}: primary {} anchor {off_axis} m off the trunk axis", n.id);
}
}
}
#[test]
fn a_strand_is_born_when_its_branch_reaches_the_point_it_hangs_from() {
let params = TreePreset::Willow.params();
let full = generate_skeleton(¶ms);
let strands = |s: &Skeleton| s.nodes.iter().filter(|n| n.kind == NodeKind::Strand).count();
let mut last = 0;
for age in [1.0, 2.0, 3.0, 4.0, 6.0, f32::INFINITY] {
let aged = full.aged(age, ¶ms.growth);
for n in aged.nodes.iter().filter(|n| n.kind == NodeKind::Strand) {
let parent = &aged.nodes[n.parent.unwrap() as usize];
assert!(parent.progress + 1e-4 >= n.attach_t, "age {age}: a strand hangs from {} of a branch grown to {}", n.attach_t, parent.progress);
let along = if parent.progress > 0.0 { n.attach_t / parent.progress } else { 0.0 };
assert!(n.start.distance(parent.point_at(along)) < 1e-3, "age {age}: a strand starts off its branch");
}
let now = strands(&aged);
assert!(now >= last, "age {age}: strands dropped from {last} to {now}");
last = now;
}
assert_eq!(last, strands(&full));
assert!(strands(&full.aged(2.0, ¶ms.growth)) < last, "every strand is there at age 2");
}
}