Skip to main content

kvas_engine/
tree.rs

1//! Procedural trees. A `Tree` grows a trunk with branches (tapered tubes) and clusters of leaf
2//! cards at the twigs. Bark and leaves are two `Transient` child meshes drawn with the built-in
3//! `builtin:bark` and `builtin:foliage` shaders: leaves are cut out in the shader and sway in the
4//! wind on the GPU, so the meshes are built only when a parameter changes.
5//!
6//! ```
7//! use kvas_engine::tree::{Tree, build};
8//! let (bark, leaves) = build(&Tree::default());
9//! assert!(!bark.indices.is_empty() && !leaves.indices.is_empty());
10//! ```
11
12use crate::components::*;
13use crate::hierarchy;
14use crate::mesh::{MeshData, Vertex};
15use crate::procedural::{signature, Rng};
16use crate::reflect::Field;
17use glam::{Quat, Vec3};
18use kvas_ecs::{Entity, World};
19use serde::{Deserialize, Serialize};
20use std::sync::Arc;
21
22crate::choice_enum! {
23    /// Overall shape of a tree: Broadleaf (oak), Conifer (pine), Palm, or Bush.
24    TreeStyle { Broadleaf, Conifer, Palm, Bush } default Broadleaf
25}
26
27/// A seeded procedural tree. Change any field in the Inspector and the tree regrows.
28#[derive(Clone, Serialize, Deserialize)]
29#[serde(default)]
30pub struct Tree {
31    /// Overall shape.
32    pub style: TreeStyle,
33    /// Height of the trunk in metres.
34    pub height: f32,
35    /// Radius of the trunk at the ground in metres.
36    pub trunk_radius: f32,
37    /// How much the trunk and branches wander, 0..1.
38    pub crookedness: f32,
39    /// Branching levels below the twigs, 1..4.
40    pub levels: i32,
41    /// Branches growing from each branch.
42    pub branches: i32,
43    /// Branch length relative to its parent.
44    pub branch_length: f32,
45    /// Angle between a branch and its parent in degrees.
46    pub branch_angle: f32,
47    /// Leaf cards per twig cluster.
48    pub leaf_density: i32,
49    /// Size of one leaf card in metres.
50    pub leaf_size: f32,
51    /// Bark colour.
52    pub bark_color: [f32; 4],
53    /// Main leaf colour.
54    pub leaf_color: [f32; 4],
55    /// Second leaf colour mixed in per card (autumn tints, sun-bleached leaves...).
56    pub leaf_color_2: [f32; 4],
57    /// Wind sway strength.
58    pub wind: f32,
59    /// Deterministic seed.
60    pub seed: i32,
61    #[serde(skip)]
62    #[doc(hidden)]
63    pub sig: u64,
64}
65
66impl Default for Tree {
67    fn default() -> Self {
68        Self {
69            style: TreeStyle::Broadleaf,
70            height: 5.5,
71            trunk_radius: 0.22,
72            crookedness: 0.35,
73            levels: 3,
74            branches: 4,
75            branch_length: 0.62,
76            branch_angle: 42.0,
77            leaf_density: 14,
78            leaf_size: 0.75,
79            bark_color: [0.29, 0.22, 0.16, 1.0],
80            leaf_color: [0.05, 0.14, 0.03, 1.0],
81            leaf_color_2: [0.16, 0.24, 0.04, 1.0],
82            wind: 1.0,
83            seed: 1,
84            sig: 0,
85        }
86    }
87}
88
89impl Component for Tree {
90    fn fields(&mut self) -> Vec<Field<'_>> {
91        vec![
92            Field::choice("style", &mut self.style),
93            Field::f32("height", &mut self.height),
94            Field::f32("trunk_radius", &mut self.trunk_radius),
95            Field::f32("crookedness", &mut self.crookedness),
96            Field::i32("levels", &mut self.levels),
97            Field::i32("branches", &mut self.branches),
98            Field::f32("branch_length", &mut self.branch_length),
99            Field::f32("branch_angle", &mut self.branch_angle),
100            Field::i32("leaf_density", &mut self.leaf_density),
101            Field::f32("leaf_size", &mut self.leaf_size),
102            Field::color("bark_color", &mut self.bark_color),
103            Field::color("leaf_color", &mut self.leaf_color),
104            Field::color("leaf_color_2", &mut self.leaf_color_2),
105            Field::f32("wind", &mut self.wind),
106            Field::i32("seed", &mut self.seed),
107        ]
108    }
109}
110
111/// Marks a child mesh created for a tree (bark or leaves).
112#[derive(Clone, Copy, Default)]
113pub struct TreePart;
114
115const SIDES: usize = 9;
116
117struct Grow<'a> {
118    rng: Rng,
119    t: &'a Tree,
120    bark: MeshData,
121    /// Twig tips: position, direction and level size, where leaf clusters go.
122    tips: Vec<(Vec3, Vec3, f32)>,
123}
124
125fn any_perp(d: Vec3) -> Vec3 {
126    d.any_orthonormal_vector()
127}
128
129impl Grow<'_> {
130    fn r(&mut self) -> f32 {
131        self.rng.f32()
132    }
133
134    /// A tapered tube along `points` with radii `radii`; `v0` continues the bark texture.
135    fn tube(&mut self, points: &[Vec3], radii: &[f32], v0: f32) {
136        let base = self.bark.vertices.len() as u32;
137        let mut v = v0;
138        let mut frame = any_perp((points[1] - points[0]).normalize_or_zero());
139        for (i, p) in points.iter().enumerate() {
140            let dir = if i + 1 < points.len() { points[i + 1] - *p } else { *p - points[i - 1] }.normalize_or_zero();
141            // Keep the ring orientation continuous (parallel transport).
142            frame = (frame - dir * frame.dot(dir)).normalize_or(any_perp(dir));
143            let side = dir.cross(frame);
144            if i > 0 {
145                v += (points[i] - points[i - 1]).length() / (radii[i].max(0.01) * 6.0);
146            }
147            for k in 0..=SIDES {
148                let a = k as f32 / SIDES as f32 * std::f32::consts::TAU;
149                let n = frame * a.cos() + side * a.sin();
150                self.bark.vertices.push(Vertex { pos: (*p + n * radii[i]).into(), normal: n.into(), uv: [k as f32 / SIDES as f32, v] });
151            }
152        }
153        let ring = (SIDES + 1) as u32;
154        for i in 0..points.len() as u32 - 1 {
155            for k in 0..SIDES as u32 {
156                let (a, b, c, d) = (base + i * ring + k, base + i * ring + k + 1, base + (i + 1) * ring + k, base + (i + 1) * ring + k + 1);
157                self.bark.indices.extend([a, c, b, b, c, d]);
158            }
159        }
160    }
161
162    /// Grows a branch from `start` along `dir`; recurses into children until `level` reaches 0.
163    fn branch(&mut self, start: Vec3, dir: Vec3, length: f32, radius: f32, level: i32, v0: f32) {
164        let t = self.t;
165        let segments = if level == t.levels.max(1) { 7 } else { 4 };
166        let mut points = vec![start];
167        let mut radii = vec![radius];
168        let mut d = dir;
169        let mut p = start;
170        let seg = length / segments as f32;
171        let bend = t.crookedness * 0.45;
172        for i in 1..=segments {
173            let wander = Vec3::new(self.r() - 0.5, (self.r() - 0.5) * 0.4, self.r() - 0.5) * bend;
174            // Broadleaf branches reach up and out; conifer branches droop.
175            let tropism = match t.style {
176                TreeStyle::Conifer if level < t.levels => Vec3::NEG_Y * 0.08,
177                TreeStyle::Palm => Vec3::Y * 0.04 + Vec3::new(0.06, 0.0, 0.0) * (i as f32 / segments as f32),
178                _ => Vec3::Y * 0.05,
179            };
180            d = (d + wander + tropism).normalize();
181            p += d * seg;
182            points.push(p);
183            let k = i as f32 / segments as f32;
184            radii.push((radius * (1.0 - 0.72 * k)).max(0.008));
185        }
186        self.tube(&points, &radii, v0);
187        if level <= 0 {
188            self.tips.push((p, d, length));
189            // Leaves also along the last stretch of the twig.
190            let mid = points[points.len() / 2];
191            self.tips.push((mid, d, length * 0.8));
192            return;
193        }
194        let children = t.branches.clamp(1, 8);
195        let angle = t.branch_angle.to_radians();
196        let mut twist = self.r() * std::f32::consts::TAU;
197        for c in 0..children {
198            let along = match t.style {
199                TreeStyle::Conifer if level == t.levels => 0.25 + 0.7 * (c as f32 + self.r() * 0.5) / children as f32,
200                _ => 0.35 + 0.6 * (c as f32 + self.r()) / children as f32,
201            };
202            let idx = ((along * segments as f32) as usize).min(segments);
203            let at = points[idx];
204            let r_at = radii[idx];
205            twist += 2.399_963; // golden angle
206            let axis = (Quat::from_axis_angle(d, twist) * any_perp(d)).normalize();
207            let child_dir = (Quat::from_axis_angle(axis, angle * (0.8 + 0.4 * self.r())) * d).normalize();
208            let mut child_len = length * t.branch_length * (0.85 + 0.3 * self.r());
209            if t.style == TreeStyle::Conifer && level == t.levels {
210                // Whorls get shorter towards the top: a cone.
211                child_len = t.height * 0.45 * (1.0 - along) * (0.85 + 0.3 * self.r());
212            }
213            self.branch(at, child_dir, child_len, r_at * 0.62, level - 1, v0);
214        }
215    }
216}
217
218/// Builds the bark and leaf meshes of a tree (in the tree's local space, root at the origin).
219pub fn build(t: &Tree) -> (MeshData, MeshData) {
220    let mut g = Grow { rng: Rng::new(t.seed as u64 ^ 0x7ee5), t, bark: MeshData::default(), tips: Vec::new() };
221    let levels = t.levels.clamp(1, 4);
222    let height = t.height.max(0.3);
223    match t.style {
224        TreeStyle::Bush => {
225            for i in 0..5 {
226                let a = i as f32 / 5.0 * std::f32::consts::TAU + g.r();
227                let dir = Vec3::new(a.cos() * 0.6, 1.0, a.sin() * 0.6).normalize();
228                g.branch(Vec3::ZERO, dir, height * 0.6, t.trunk_radius * 0.5, (levels - 1).max(1), 0.0);
229            }
230        }
231        TreeStyle::Palm => {
232            // One bending trunk, then a crown of long fronds (leaf cards along drooping stems).
233            let tree = Tree { crookedness: t.crookedness * 0.5, ..t.clone() };
234            let mut p = Grow { rng: Rng::new(t.seed as u64 ^ 0x9a1), t: &tree, bark: MeshData::default(), tips: Vec::new() };
235            p.branch(Vec3::ZERO, Vec3::Y, height, t.trunk_radius, 0, 0.0);
236            let (top, _, _) = p.tips[0];
237            g.bark = p.bark;
238            for i in 0..9 {
239                let a = i as f32 / 9.0 * std::f32::consts::TAU + g.r() * 0.3;
240                let out = Vec3::new(a.cos(), 0.0, a.sin());
241                for s in 1..=6 {
242                    let k = s as f32 / 6.0;
243                    let pos = top + out * (k * height * 0.38) + Vec3::Y * (0.35 * k - 0.9 * k * k) * height * 0.3;
244                    g.tips.push((pos, out, height * 0.1));
245                }
246            }
247        }
248        _ => {
249            let trunk_levels = levels;
250            g.branch(Vec3::ZERO, Vec3::Y, height, t.trunk_radius, trunk_levels, 0.0);
251        }
252    }
253
254    // Leaves: cards around every twig tip, turned to random directions; normals point away from
255    // the crown centre so the foliage is lit like one soft volume.
256    let mut leaves = MeshData::default();
257    let (mut lo, mut hi) = (Vec3::splat(f32::MAX), Vec3::splat(f32::MIN));
258    for (p, _, _) in &g.tips {
259        lo = lo.min(*p);
260        hi = hi.max(*p);
261    }
262    let centre = (lo + hi) * 0.5;
263    let conifer = t.style == TreeStyle::Conifer;
264    let per = t.leaf_density.clamp(1, 64) as usize;
265    let tips = g.tips.clone();
266    for (p, dir, len) in tips {
267        let spread = (len * 0.35).clamp(0.15, 1.2) * if conifer { 0.6 } else { 1.0 };
268        for _ in 0..per {
269            let offset = Vec3::new(g.r() - 0.5, g.r() - 0.5, g.r() - 0.5) * 2.0 * spread + dir * g.r() * spread * 0.5;
270            let at = p + offset;
271            let size = t.leaf_size * (0.7 + 0.6 * g.r()) * if conifer { 0.8 } else { 1.0 };
272            let facing = Vec3::new(g.r() - 0.5, g.r() - 0.5, g.r() - 0.5).normalize_or(Vec3::Y);
273            let u = any_perp(facing);
274            let v = facing.cross(u);
275            let (u, v) = if conifer { (dir * size * 0.9, v * size * 0.55) } else { (u * size * 0.5, v * size * 0.5) };
276            let mut n = (at - centre).normalize_or(Vec3::Y);
277            n = (n + Vec3::Y * 0.35).normalize();
278            let tint = (g.r() * 8.0).floor();
279            let base = leaves.vertices.len() as u32;
280            for (cx, cy) in [(-1.0f32, -1.0f32), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)] {
281                let pos = at + u * cx + v * cy;
282                leaves.vertices.push(Vertex { pos: pos.into(), normal: n.into(), uv: [(cx * 0.5 + 0.5) + tint, cy * 0.5 + 0.5] });
283            }
284            leaves.indices.extend([base, base + 1, base + 2, base, base + 2, base + 3]);
285            leaves.indices.extend([base + 2, base + 1, base, base + 3, base + 2, base]);
286        }
287    }
288    (g.bark, leaves)
289}
290
291/// Rebuilds changed trees and their transient bark and leaf meshes.
292pub fn update(world: &mut World) {
293    let orphans: Vec<Entity> = world
294        .entities_with::<TreePart>()
295        .into_iter()
296        .filter(|&c| hierarchy::parent_of(world, c).is_none_or(|p| !world.is_alive(p) || !world.has::<Tree>(p)))
297        .collect();
298    for c in orphans {
299        world.despawn(c);
300    }
301    for e in world.entities_with::<Tree>() {
302        let t = world.get::<Tree>(e).unwrap().clone();
303        let sig = signature(&(t.style, t.height.to_bits(), t.trunk_radius.to_bits(), t.crookedness.to_bits(), t.levels, t.branches, t.branch_length.to_bits(), t.branch_angle.to_bits(), t.leaf_density, t.leaf_size.to_bits(), t.seed));
304        let parts: Vec<Entity> = hierarchy::children_all(world, e).into_iter().filter(|&c| world.has::<TreePart>(c)).collect();
305        if t.sig != sig || parts.len() != 2 {
306            for c in &parts {
307                world.despawn(*c);
308            }
309            let (bark, leaves) = build(&t);
310            for (name, mesh) in [("Bark", bark), ("Leaves", leaves)] {
311                let c = world.spawn();
312                world.insert(c, Name(name.into()));
313                world.insert(c, Transform::default());
314                world.insert(c, Parent(e));
315                world.insert(c, Transient);
316                world.insert(c, TreePart);
317                world.insert(c, DynamicMesh { data: Arc::new(mesh), version: sig });
318                world.insert(c, MeshRenderer::default());
319            }
320            world.get_mut::<Tree>(e).unwrap().sig = sig;
321        }
322        // Colours and wind apply live.
323        for c in hierarchy::children_all(world, e) {
324            let is_leaves = world.get::<Name>(c).is_some_and(|n| n.0 == "Leaves");
325            if let Some(m) = world.get_mut::<MeshRenderer>(c) {
326                if is_leaves {
327                    m.shader = "builtin:foliage".into();
328                    m.color = t.leaf_color;
329                    m.user = [t.wind, if t.style == TreeStyle::Conifer { 1.0 } else if t.style == TreeStyle::Palm { 2.0 } else { 0.0 }, 0.0, 0.0, t.leaf_color_2[0], t.leaf_color_2[1], t.leaf_color_2[2], 0.0];
330                    m.cast_shadows = true;
331                    m.roughness = 0.8;
332                } else {
333                    m.shader = "builtin:bark".into();
334                    m.color = t.bark_color;
335                    m.user = [t.wind, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
336                    m.cast_shadows = true;
337                    m.roughness = 0.95;
338                }
339            }
340        }
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347    use crate::scene::Scene;
348
349    #[test]
350    fn every_style_grows_bark_and_leaves_deterministically() {
351        for style in [TreeStyle::Broadleaf, TreeStyle::Conifer, TreeStyle::Palm, TreeStyle::Bush] {
352            let t = Tree { style, ..Default::default() };
353            let (bark, leaves) = build(&t);
354            assert!(bark.indices.len() > 100 && leaves.indices.len() > 100, "{style:?}");
355            assert!(bark.validate().is_ok() && leaves.validate().is_ok());
356            assert_eq!(build(&t).1.vertices[5].pos, leaves.vertices[5].pos);
357            let (_, max) = bark.bounds();
358            assert!(max.y > t.height * 0.5, "{style:?} reaches up");
359        }
360    }
361
362    #[test]
363    fn parts_are_transient_and_rebuilt_on_change() {
364        let mut scene = Scene::default();
365        let e = hierarchy::spawn_named(&mut scene.world, "Oak");
366        scene.world.insert(e, Tree::default());
367        update(&mut scene.world);
368        let parts = hierarchy::children_all(&scene.world, e);
369        assert_eq!(parts.len(), 2);
370        assert!(hierarchy::children(&scene.world, e).is_empty(), "hidden from the hierarchy");
371        scene.world.get_mut::<Tree>(e).unwrap().wind = 3.0;
372        update(&mut scene.world);
373        assert_eq!(hierarchy::children_all(&scene.world, e), parts, "appearance changes keep the meshes");
374        scene.world.get_mut::<Tree>(e).unwrap().seed = 9;
375        update(&mut scene.world);
376        assert_ne!(hierarchy::children_all(&scene.world, e), parts);
377    }
378}