├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples └── voxel.rs ├── shrubbery_logo.png ├── src ├── algorithm_settings.rs ├── attractor.rs ├── attractor_generator_settings.rs ├── branch.rs ├── leaf_classifier.rs ├── lib.rs ├── math_utils.rs ├── shape.rs ├── shrubbery.rs ├── vec.rs └── voxel.rs └── voxel_example.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | 13 | # Added by cargo 14 | 15 | /target 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shrubbery" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | glam = "0.22.0" 10 | rand = "*" 11 | 12 | [dev-dependencies] 13 | kiss3d = "*" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | shrubbery is dual-licensed under either 2 | 3 | * MIT License (docs/LICENSE-MIT or http://opensource.org/licenses/MIT) 4 | * Apache License, Version 2.0 (docs/LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) 5 | 6 | at your option. 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shrubbery 2 | 3 | rust library: Space colonization implementation, for generating trees / shrubbery, with built in voxelization utility. 4 | 5 | ### Screenshot example: voxel. 6 | left: Voxel tree 7 | 8 | right: debug representation of branches + attractors renders as yellow dots. 9 | ![voxel example](voxel_example.png) 10 | 11 | ## Example code 12 | ```rs 13 | let mut shrubbery = Shrubbery::new( 14 | vec3(0., 0., 0.), // rot position 15 | vec3(0., 1., 0.), // initial growth dir 16 | AlgorithmSettings { 17 | branch_len: 2.0, 18 | leaf_attraction_dist: 6.0, 19 | ..Default::default() 20 | }, 21 | AttractorGeneratorSettings::default(), 22 | ); 23 | 24 | // spawn particles for tree to grow into 25 | shrubbery.spawn_attractors_from_shape( 26 | vec3(0., 5. + 8.0, 0.), 27 | BoxShape { 28 | x: 15.0, 29 | y: 10.0, 30 | z: 15., 31 | }, 32 | ); 33 | 34 | // keep spawning root branches until attractors can be found 35 | shrubbery.build_trunk(); 36 | 37 | // grow tree 8 times 38 | (0..8).for_each(|_|shrubbery.grow()); 39 | 40 | // make data for the tree as a voxel 41 | let mut voxels = voxelize(shrubbery, VoxelizeSettings::default()); 42 | 43 | ``` 44 | 45 | ## License 46 | 47 | Shrubbery is free and open source! All code in this repository is dual-licensed under either: 48 | 49 | * MIT License ([LICENSE-MIT](docs/LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) 50 | * Apache License, Version 2.0 ([LICENSE-APACHE](docs/LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) 51 | 52 | at your option. 53 | 54 | Unless you explicitly state otherwise, any contribution intentionally submitted 55 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 56 | additional terms or conditions. 57 | -------------------------------------------------------------------------------- /examples/voxel.rs: -------------------------------------------------------------------------------- 1 | use shrubbery::shape::BoxShape; 2 | use shrubbery::voxel::{ 3 | drop_leaves, voxelize, BranchRootSizeIncreaser, BranchSizeSetting, LeafSetting, LeafShape, 4 | VoxelType, VoxelizeSettings, 5 | }; 6 | use shrubbery::{ 7 | algorithm_settings::AlgorithmSettings, 8 | attractor_generator_settings::AttractorGeneratorSettings, prelude::*, 9 | }; 10 | 11 | use shrubbery::math::*; 12 | 13 | use kiss3d::event::{Action, Key, WindowEvent}; 14 | use kiss3d::light::Light; 15 | use kiss3d::window::Window; 16 | 17 | fn make_shrubbery() -> Shrubbery { 18 | let mut shrubbery = Shrubbery::new( 19 | vec3(0., 0., 0.), 20 | vec3(0., 1., 0.), 21 | AlgorithmSettings { 22 | kill_distance: 2.0, 23 | branch_len: 2.0, 24 | leaf_attraction_dist: 6.0, 25 | min_trunk_height: 3.0, 26 | }, 27 | AttractorGeneratorSettings::default(), 28 | ); 29 | shrubbery.spawn_attractors_from_shape( 30 | vec3(0., 5. + 8.0, 0.), 31 | BoxShape { 32 | x: 15.0, 33 | y: 10.0, 34 | z: 15., 35 | }, 36 | ); 37 | shrubbery.build_trunk(); 38 | shrubbery 39 | } 40 | 41 | fn main() { 42 | let mut window = Window::new("that's a fine shrubbery"); 43 | window.set_light(Light::StickToCamera); 44 | let mut shrubbery = make_shrubbery(); 45 | 46 | // reference kiss3d box models so we can remove them 47 | let mut vis_nodes = vec![]; 48 | 49 | let settings = VoxelizeSettings { 50 | branch_size_setting: BranchSizeSetting::Generation { 51 | distances: vec![1.5, 1.0, 1.0, 1.0], 52 | }, 53 | branch_root_size_increaser: Some(BranchRootSizeIncreaser { 54 | height: 2.0, 55 | additional_size: 2.0, 56 | }), 57 | leaf_settings: LeafSetting::Shape(LeafShape::Sphere { r: 2.7 }), 58 | }; 59 | 60 | while window.render() { 61 | for event in window.events().iter() { 62 | match event.value { 63 | WindowEvent::Key(button, Action::Press, _) => { 64 | // flag to indicate rebuilding the voxels 65 | // process button input: it's dirty 66 | let mut dirty = true; 67 | match button { 68 | Key::R => shrubbery = make_shrubbery(), 69 | Key::G => shrubbery.post_process_gravity(1.0), 70 | Key::T => shrubbery.post_process_spin(3.14 * 0.5), 71 | Key::N => shrubbery.grow(), 72 | _ => dirty = false, 73 | } 74 | if dirty { 75 | build_voxels(&mut shrubbery, &settings, &mut vis_nodes, &mut window); 76 | } 77 | } 78 | _ => {} 79 | } 80 | } 81 | 82 | window.set_line_width(6.0); 83 | for branch in shrubbery.branches.iter() { 84 | let Some(parent_index) = branch.parent_index else { 85 | continue; 86 | }; 87 | let p_pos = shrubbery.branches[parent_index].pos; 88 | let from = kiss3d::nalgebra::Point3::new(branch.pos.x, branch.pos.y, branch.pos.z); 89 | let to = kiss3d::nalgebra::Point3::new(p_pos.x, p_pos.y, p_pos.z); 90 | 91 | let mut is_leaf = false; 92 | if let LeafSetting::BranchIsLeaf(classifier) = &settings.leaf_settings { 93 | is_leaf = branch.is_leaf(classifier); 94 | } 95 | let color = if is_leaf { 96 | kiss3d::nalgebra::Point3::new(0.0, 1.0, 0.0) 97 | } else { 98 | kiss3d::nalgebra::Point3::new(0.4, 0.2, 0.0) 99 | }; 100 | window.draw_line(&from, &to, &color); 101 | } 102 | 103 | for attractor in shrubbery.attractors.iter() { 104 | let pos = 105 | kiss3d::nalgebra::Point3::new(attractor.pos.x, attractor.pos.y, attractor.pos.z); 106 | window.set_point_size(6.0); 107 | window.draw_point(&pos, &kiss3d::nalgebra::Point3::new(1.0, 1.0, 0.0)); 108 | } 109 | } 110 | } 111 | 112 | fn build_voxels( 113 | shrubbery: &mut Shrubbery, 114 | settings: &VoxelizeSettings, 115 | vis_nodes: &mut Vec, 116 | window: &mut Window, 117 | ) { 118 | let mut gen_voxels = voxelize(shrubbery, settings); 119 | drop_leaves(&mut gen_voxels, 0.1); 120 | 121 | vis_nodes 122 | .iter_mut() 123 | .for_each(|mut n| window.remove_node(&mut n)); 124 | 125 | for (pos, voxel) in gen_voxels.iter() { 126 | let c_s = 1.0; 127 | let mut c = window.add_cube(c_s, c_s, c_s); 128 | c.append_translation(&kiss3d::nalgebra::Translation3::new( 129 | pos.x as f32 + 40.0, 130 | pos.y as f32, 131 | pos.z as f32, 132 | )); 133 | match voxel { 134 | VoxelType::Air => (), 135 | VoxelType::Branch => { 136 | c.set_color(0.4, 0.2, 0.0); 137 | } 138 | VoxelType::Greenery => { 139 | c.set_color(0.0, 1.0, 0.0); 140 | } 141 | }; 142 | vis_nodes.push(c); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /shrubbery_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TanTanDev/shrubbery/d034b98b190f0b2bb83696d71e4bfb866de3b267/shrubbery_logo.png -------------------------------------------------------------------------------- /src/algorithm_settings.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, Debug, PartialEq)] 2 | pub struct AlgorithmSettings { 3 | pub kill_distance: f32, 4 | pub branch_len: f32, 5 | pub leaf_attraction_dist: f32, 6 | pub min_trunk_height: f32, 7 | } 8 | 9 | impl Default for AlgorithmSettings { 10 | fn default() -> Self { 11 | Self { 12 | kill_distance: 0.3, 13 | branch_len: 0.3, 14 | leaf_attraction_dist: 5., 15 | min_trunk_height: 1., 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/attractor.rs: -------------------------------------------------------------------------------- 1 | use glam::Vec3; 2 | 3 | pub struct Attractor { 4 | pub pos: Vec3, 5 | pub reached: bool, 6 | } 7 | 8 | impl Attractor { 9 | pub fn new(pos: Vec3) -> Self { 10 | Self { 11 | pos, 12 | reached: false, 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/attractor_generator_settings.rs: -------------------------------------------------------------------------------- 1 | pub struct AttractorGeneratorSettings { 2 | pub max_leaves: Option, 3 | pub min_leaves: Option, 4 | // a value of 1: will spawn enough leaves to expand the whole area 5 | // 1.0 is minimum recommended value, higher values will yield potentially more branching 6 | // but higher values also generates more leaves, tolling performance 7 | pub density: f32, 8 | } 9 | 10 | impl Default for AttractorGeneratorSettings { 11 | fn default() -> Self { 12 | Self { 13 | max_leaves: Some(500), 14 | min_leaves: Some(30), 15 | density: 1.0, 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/branch.rs: -------------------------------------------------------------------------------- 1 | use glam::Vec3; 2 | 3 | use crate::leaf_classifier::LeafClassifier; 4 | 5 | pub struct Branch { 6 | pub pos: Vec3, 7 | pub parent_index: Option, 8 | pub dir: Vec3, 9 | pub original_dir: Vec3, 10 | // how many attractors are pulling this node 11 | pub attractors_count: i32, 12 | // how man branchings are from this node 13 | pub child_count: i32, 14 | // todo: explain 15 | pub generation: i32, 16 | } 17 | 18 | impl Branch { 19 | // make a new branch based on this branch calculated growth direciton 20 | pub fn next(&self, index: usize, branch_len: f32, is_new_generation: bool) -> Self { 21 | let mut generation = self.generation; 22 | if is_new_generation { 23 | generation += 1; 24 | } 25 | Self { 26 | pos: self.pos + self.dir * branch_len, 27 | parent_index: Some(index), 28 | dir: self.dir, 29 | attractors_count: 0, 30 | original_dir: self.dir, 31 | child_count: 0, 32 | generation, 33 | } 34 | } 35 | 36 | // no child branches: is leaf 37 | pub fn is_leaf(&self, classifier: &LeafClassifier) -> bool { 38 | match classifier { 39 | LeafClassifier::LastBranch => self.child_count == 0, 40 | LeafClassifier::NonRootBranch => self.generation != 0, 41 | } 42 | } 43 | 44 | pub fn reset(&mut self) { 45 | self.attractors_count = 0; 46 | self.dir = self.original_dir; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/leaf_classifier.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone, Debug, PartialEq)] 2 | pub enum LeafClassifier { 3 | // branch without children is leaf 4 | LastBranch, 5 | // new "generation" branches are leaves 6 | NonRootBranch, 7 | } 8 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod algorithm_settings; 2 | pub mod attractor; 3 | pub mod attractor_generator_settings; 4 | pub mod branch; 5 | pub mod leaf_classifier; 6 | pub mod shape; 7 | pub mod shrubbery; 8 | pub mod vec; 9 | pub mod voxel; 10 | pub use glam; 11 | pub mod math_utils; 12 | 13 | pub mod prelude { 14 | pub use crate::shrubbery::Shrubbery; 15 | } 16 | 17 | pub mod math { 18 | pub use glam::*; 19 | } 20 | -------------------------------------------------------------------------------- /src/math_utils.rs: -------------------------------------------------------------------------------- 1 | use glam::{vec2, Vec2, Vec3}; 2 | 3 | /// rotate a vec2 position around the origin of 0,0 4 | pub fn rotate_point(pos: Vec2, radians: f32) -> Vec2 { 5 | let (cos_theta, sin_theta) = (radians.cos(), radians.sin()); 6 | let out = vec2( 7 | cos_theta * pos.x - sin_theta * pos.y, 8 | sin_theta * pos.x + cos_theta * pos.y, 9 | ); 10 | out 11 | } 12 | 13 | /// return the shortest distance from a vec3 to a line with a star and end pos 14 | pub fn dist_to_line(pos: Vec3, line_start: Vec3, line_end: Vec3) -> f32 { 15 | let ab = line_end - line_start; 16 | let ac = pos - line_start; 17 | if ac.dot(ab) <= 0.0 { 18 | return ac.length(); 19 | } 20 | let bv = pos - line_end; 21 | if bv.dot(ab) >= 0.0 { 22 | return bv.length(); 23 | } 24 | ab.cross(ac).length() / ab.length() 25 | } 26 | -------------------------------------------------------------------------------- /src/shape.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | algorithm_settings::AlgorithmSettings, attractor::Attractor, 3 | attractor_generator_settings::AttractorGeneratorSettings, 4 | }; 5 | use glam::*; 6 | use rand::Rng; 7 | 8 | /// A shape to spawn attractors inside 9 | pub trait Shape { 10 | fn generate( 11 | &self, 12 | pos: Vec3, 13 | attractors: &mut Vec, 14 | algorithm_settings: &AlgorithmSettings, 15 | generator_settings: &AttractorGeneratorSettings, 16 | ); 17 | } 18 | 19 | /// x,y,z is total size 20 | pub struct BoxShape { 21 | pub x: f32, 22 | pub y: f32, 23 | pub z: f32, 24 | } 25 | 26 | impl Shape for BoxShape { 27 | fn generate( 28 | &self, 29 | pos: Vec3, 30 | attractors: &mut Vec, 31 | algorithm_settings: &AlgorithmSettings, 32 | generator_settings: &AttractorGeneratorSettings, 33 | ) { 34 | let mut ideal_spacing = 35 | algorithm_settings.leaf_attraction_dist * 0.5 - algorithm_settings.kill_distance * 0.5; 36 | ideal_spacing *= 1.0 / generator_settings.density; 37 | 38 | let x_l = (self.x / ideal_spacing) as i32; 39 | let x_y = (self.y / ideal_spacing) as i32; 40 | let x_z = (self.z / ideal_spacing) as i32; 41 | 42 | let center_shape_offset = -vec3(self.x * 0.5, self.y * 0.5, self.z * 0.5); 43 | let start_pos = vec3( 44 | ideal_spacing * 0.5, 45 | ideal_spacing * 0.5, 46 | ideal_spacing * 0.5, 47 | ); 48 | 49 | let scatter_distance = ideal_spacing * 0.5; 50 | let mut rng = rand::thread_rng(); 51 | 52 | for x in 0..x_l { 53 | for y in 0..x_y { 54 | for z in 0..x_z { 55 | let jitter = vec3( 56 | x as f32 * ideal_spacing 57 | + rng.gen_range(-scatter_distance..scatter_distance), 58 | y as f32 * ideal_spacing 59 | + rng.gen_range(-scatter_distance..scatter_distance), 60 | z as f32 * ideal_spacing 61 | + rng.gen_range(-scatter_distance..scatter_distance), 62 | ); 63 | attractors.push(Attractor::new( 64 | pos + start_pos + center_shape_offset + jitter, 65 | )); 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/shrubbery.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | algorithm_settings::AlgorithmSettings, 3 | attractor::Attractor, 4 | attractor_generator_settings::AttractorGeneratorSettings, 5 | branch::Branch, 6 | math_utils::{dist_to_line, rotate_point}, 7 | shape::Shape, 8 | }; 9 | 10 | use glam::{ivec3, vec2, IVec3, Vec2, Vec3}; 11 | 12 | /// a tree/bush, pressentation that you can grow and modify through post processing effect 13 | pub struct Shrubbery { 14 | pub branches: Vec, 15 | pub attractors: Vec, 16 | pub settings: AlgorithmSettings, 17 | pub generator_settings: AttractorGeneratorSettings, 18 | pub min_bounds: Vec3, 19 | pub max_bounds: Vec3, 20 | } 21 | 22 | impl Shrubbery { 23 | pub fn new( 24 | root_pos: Vec3, 25 | initial_dir: Vec3, 26 | settings: AlgorithmSettings, 27 | generator_settings: AttractorGeneratorSettings, 28 | ) -> Self { 29 | let mut branches = Vec::new(); 30 | let root = Branch { 31 | pos: root_pos, 32 | parent_index: None, 33 | dir: initial_dir, 34 | attractors_count: 0, 35 | original_dir: initial_dir, 36 | child_count: 0, 37 | generation: 0, 38 | }; 39 | branches.push(root); 40 | Self { 41 | branches, 42 | attractors: Vec::new(), 43 | settings, 44 | generator_settings, 45 | min_bounds: Vec3::splat(0f32), 46 | max_bounds: Vec3::splat(0f32), 47 | } 48 | } 49 | 50 | /// return the half size bounds, imagine a quad on the xz axis 51 | pub fn get_plane_half_size(&self) -> f32 { 52 | let size = self.get_bounding_size(); 53 | (size.x.max(size.z) as f32 * 0.5).ceil() 54 | } 55 | 56 | /// rturns size of the bounding box 57 | pub fn get_bounding_size(&self) -> IVec3 { 58 | let (min_bounds, max_bounds) = self.get_bounds(); 59 | max_bounds - min_bounds 60 | } 61 | 62 | /// returns the min x,y,z and max x,y,z position 63 | pub fn get_bounds(&self) -> (IVec3, IVec3) { 64 | ( 65 | ivec3( 66 | self.min_bounds.x.ceil() as i32, 67 | self.min_bounds.y.ceil() as i32, 68 | self.min_bounds.z.ceil() as i32, 69 | ), 70 | ivec3( 71 | self.max_bounds.x.ceil() as i32, 72 | self.max_bounds.y.ceil() as i32, 73 | self.max_bounds.z.ceil() as i32, 74 | ), 75 | ) 76 | } 77 | 78 | // expand bounding if branch_pos is outside 79 | pub fn update_bound(min_bounds: &mut Vec3, max_bounds: &mut Vec3, branch_pos: Vec3) { 80 | min_bounds.x = min_bounds.x.min(branch_pos.x); 81 | min_bounds.y = min_bounds.y.min(branch_pos.y); 82 | min_bounds.z = min_bounds.z.min(branch_pos.z); 83 | max_bounds.x = max_bounds.x.max(branch_pos.x); 84 | max_bounds.y = max_bounds.y.max(branch_pos.y); 85 | max_bounds.z = max_bounds.z.max(branch_pos.z); 86 | } 87 | 88 | /// spawn initial branches based on settings. 89 | pub fn build_trunk(&mut self) { 90 | let mut root_end_pos = self.branches[0].pos; 91 | let dir = self.branches[0].dir; 92 | let mut consumed_height = 0.; 93 | // the first root will be as long as it needs to be until it starts gaining attractions 94 | let max_iterations = 1000; 95 | 'a: for _ in 0..max_iterations { 96 | consumed_height += self.settings.branch_len; 97 | root_end_pos += self.settings.branch_len * dir; 98 | for leaf in self.attractors.iter() { 99 | let dist = root_end_pos.distance(leaf.pos); 100 | if dist < self.settings.leaf_attraction_dist { 101 | break 'a; 102 | } 103 | } 104 | } 105 | 106 | self.branches[0].child_count += 1; 107 | let new_branch = self.branches[0].next(0, consumed_height, false); 108 | Self::update_bound(&mut self.min_bounds, &mut self.max_bounds, new_branch.pos); 109 | self.branches.push(new_branch); 110 | 111 | // keep adding branches upwards until we reach the trunk_height 112 | while consumed_height < self.settings.min_trunk_height { 113 | consumed_height += self.settings.branch_len; 114 | let last_index = self.branches.len() - 1; 115 | let new_branch = 116 | self.branches[last_index].next(last_index, self.settings.branch_len, false); 117 | self.branches[last_index].child_count += 1; 118 | Self::update_bound(&mut self.min_bounds, &mut self.max_bounds, new_branch.pos); 119 | self.branches.push(new_branch) 120 | } 121 | } 122 | 123 | /// using space colonization algorithm, spawn new branches 124 | pub fn grow(&mut self) { 125 | for leaf in self.attractors.iter_mut() { 126 | let mut closest_branch: Option = None; 127 | let mut closest_dist = 999999.; 128 | // find shortest signed distance of all branches 129 | for (branch_index, branch) in self.branches.iter_mut().enumerate() { 130 | let dist = leaf.pos.distance(branch.pos); 131 | // is this branch to close to the leaf, discard it 132 | if dist < self.settings.kill_distance { 133 | leaf.reached = true; 134 | closest_branch = None; 135 | break; 136 | } 137 | // to far away to be attracted towards 138 | if dist > self.settings.leaf_attraction_dist { 139 | continue; 140 | } 141 | // record closest branch 142 | if dist < closest_dist { 143 | closest_branch = Some(branch_index); 144 | closest_dist = dist; 145 | } 146 | } 147 | // pull closest branch towards us 148 | if let Some(closest_branch_index) = closest_branch { 149 | let closest_branch_pos = self.branches[closest_branch_index].pos; 150 | let new_branch_dir = leaf.pos - closest_branch_pos; 151 | let new_branch_dir = new_branch_dir.normalize(); 152 | self.branches[closest_branch_index].dir += new_branch_dir; 153 | self.branches[closest_branch_index].attractors_count += 1; 154 | } 155 | } 156 | // remove reached leaves 157 | self.attractors.retain(|leaf| !leaf.reached); 158 | 159 | // spawn new branches using previous calculations 160 | let mut to_add = vec![]; 161 | for (branch_index, branch) in self 162 | .branches 163 | .iter_mut() 164 | .enumerate() 165 | .filter(|(_, branch)| branch.attractors_count > 0) 166 | { 167 | branch.dir = branch.dir.normalize(); 168 | let new_branch = branch.next(branch_index, self.settings.branch_len, true); 169 | branch.child_count += 1; 170 | Self::update_bound(&mut self.min_bounds, &mut self.max_bounds, new_branch.pos); 171 | to_add.push(new_branch); 172 | branch.reset(); 173 | } 174 | self.branches.extend(to_add); 175 | } 176 | 177 | /// spawn particles inside provided shape, based on settings 178 | pub fn spawn_attractors_from_shape(&mut self, pos: Vec3, shape: TShape) 179 | where 180 | TShape: Shape, 181 | { 182 | shape.generate( 183 | pos, 184 | &mut self.attractors, 185 | &self.settings, 186 | &self.generator_settings, 187 | ); 188 | } 189 | 190 | /// reduce y position of branches, weighted by dist to 0,0 xz. 191 | pub fn post_process_gravity(&mut self, gravity: f32) { 192 | let plane_half_size = self.get_plane_half_size(); 193 | for branch in self.branches.iter_mut() { 194 | // branch. 195 | let branch_plane = vec2(branch.pos.x, branch.pos.z); 196 | let root = Vec2::ZERO; 197 | let dist_to_root = branch_plane.distance(root); 198 | 199 | let weight = dist_to_root / plane_half_size; 200 | branch.pos.y -= weight * gravity; 201 | } 202 | } 203 | 204 | /// rotate branch x,z positions around origin: 0,0 205 | pub fn post_process_spin(&mut self, spin_amount: f32) { 206 | let plane_half_size = self.get_plane_half_size(); 207 | for branch in self.branches.iter_mut() { 208 | let branch_xz = vec2(branch.pos.x, branch.pos.z); 209 | let root = Vec2::ZERO; 210 | let dist_to_root = branch_xz.distance(root); 211 | let weight = dist_to_root / plane_half_size; 212 | 213 | let y_weight = (branch.pos.y * 0.3).cos() * 0.5 + 0.5; 214 | 215 | let new_xz = rotate_point(branch_xz, spin_amount * weight * y_weight); 216 | branch.pos.x = new_xz.x; 217 | branch.pos.z = new_xz.y; 218 | } 219 | } 220 | 221 | /// returns (distance, index of branch) 222 | pub fn distance_to_branch(&self, pos: Vec3) -> (f32, usize) { 223 | let mut closest = f32::MAX; 224 | let mut index = 0; 225 | for (i, branch) in self.branches.iter().enumerate() { 226 | let Some(parent_index) = branch.parent_index else { 227 | continue; 228 | }; 229 | 230 | let d = dist_to_line(pos, self.branches[parent_index].pos, branch.pos); 231 | if d < closest { 232 | closest = d; 233 | index = i; 234 | } 235 | } 236 | (closest, index) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/vec.rs: -------------------------------------------------------------------------------- 1 | use std::ops::*; 2 | 3 | pub type Vector2 = Vector<2>; 4 | pub type Vector3 = Vector<3>; 5 | 6 | #[derive(Copy, Clone, Debug)] 7 | pub struct Vector { 8 | data: [f32; DIMENSIONS], 9 | } 10 | 11 | impl Vector { 12 | pub fn zero() -> Self { 13 | Self { 14 | data: [0f32; DIMENSIONS], 15 | } 16 | } 17 | 18 | pub fn distance(self, other: Self) -> f32 { 19 | (0..DIMENSIONS) 20 | .map(|i| other.data[i] - self.data[i]) 21 | .sum::() 22 | .sqrt() 23 | } 24 | 25 | pub fn normalize(self) -> Self { 26 | let inv_len = 1.0 / Self::distance(self, Vector::zero()); 27 | self * inv_len 28 | // let d = Self::distance(self, Vec3::ZERO); 29 | // self * d 30 | } 31 | } 32 | 33 | impl Add for Vector { 34 | type Output = Vector; 35 | fn add(self, rhs: Self) -> Self::Output { 36 | let mut out = Vector::zero(); 37 | (0..DIMENSIONS).for_each(|i| out.data[i] = self.data[i] + rhs.data[i]); 38 | out 39 | } 40 | } 41 | 42 | impl AddAssign for Vector { 43 | fn add_assign(&mut self, rhs: Self) { 44 | (0..DIMENSIONS).for_each(|i| self.data[i] += rhs.data[i]); 45 | } 46 | } 47 | 48 | impl Mul for Vector { 49 | type Output = Vector; 50 | 51 | fn mul(self, rhs: f32) -> Self::Output { 52 | let mut out = Vector::zero(); 53 | (0..DIMENSIONS).for_each(|i| out.data[i] = self.data[i] * rhs); 54 | out 55 | } 56 | } 57 | 58 | impl Sub for Vector { 59 | type Output = Vector; 60 | 61 | fn sub(self, rhs: Self) -> Self::Output { 62 | let mut out = Vector::zero(); 63 | (0..DIMENSIONS).for_each(|i| out.data[i] = self.data[i] - rhs.data[i]); 64 | out 65 | } 66 | } 67 | 68 | // impl Vector for Vec3 { 69 | // fn distance(self, other: Self) -> f32 { 70 | // ((other.x - self.x).powi(2) + (other.y - self.y).powi(2) + (other.z - self.z).powi(2)) 71 | // .sqrt() 72 | // } 73 | 74 | // fn normalize(self) -> Self { 75 | // let d = Self::distance(self, Vec3::ZERO); 76 | // self * d 77 | // } 78 | // } 79 | 80 | // pub trait Vector: 81 | // Sized 82 | // + Copy 83 | // + Clone 84 | // + Sub 85 | // + Sized 86 | // + Mul 87 | // + Add 88 | // + AddAssign 89 | // { 90 | // fn distance(self, other: Self) -> f32; 91 | // fn normalize(self) -> Self; 92 | // } 93 | 94 | // #[derive(Copy, Clone, Debug, PartialEq)] 95 | // pub struct Vec3 { 96 | // pub x: f32, 97 | // pub y: f32, 98 | // pub z: f32, 99 | // } 100 | 101 | // pub fn vec3(x: f32, y: f32, z: f32) -> Vec3 { 102 | // Vec3 { x, y, z } 103 | // } 104 | 105 | // impl Vec3 { 106 | // const ZERO: Vec3 = Vec3 { 107 | // x: 0., 108 | // y: 0., 109 | // z: 0., 110 | // }; 111 | // } 112 | 113 | // impl Add for Vec3 { 114 | // type Output = Vec3; 115 | // fn add(self, rhs: Self) -> Self::Output { 116 | // vec3(self.x + rhs.x, self.y + rhs.y, self.z + rhs.z) 117 | // } 118 | // } 119 | 120 | // impl AddAssign for Vec3 { 121 | // fn add_assign(&mut self, rhs: Self) { 122 | // self.x += rhs.x; 123 | // self.y += rhs.y; 124 | // self.z += rhs.z; 125 | // } 126 | // } 127 | 128 | // // impl Mul for Vec3 { 129 | // // type Output = Vec3; 130 | 131 | // // fn mul(self, rhs: Self) -> Self::Output { 132 | // // vec3(self.x * rhs.x, self.y * rhs.y, self.z * rhs.z) 133 | // // } 134 | // // } 135 | 136 | // impl Mul for Vec3 { 137 | // type Output = Vec3; 138 | 139 | // fn mul(self, rhs: f32) -> Self::Output { 140 | // vec3(self.x * rhs, self.y * rhs, self.z * rhs) 141 | // } 142 | // } 143 | 144 | // impl Sub for Vec3 { 145 | // type Output = Vec3; 146 | 147 | // fn sub(self, rhs: Self) -> Self::Output { 148 | // vec3(self.x - rhs.x, self.y - rhs.y, self.z - rhs.z) 149 | // } 150 | // } 151 | 152 | // impl Vector for Vec3 { 153 | // fn distance(self, other: Self) -> f32 { 154 | // ((other.x - self.x).powi(2) + (other.y - self.y).powi(2) + (other.z - self.z).powi(2)) 155 | // .sqrt() 156 | // } 157 | 158 | // fn normalize(self) -> Self { 159 | // let d = Self::distance(self, Vec3::ZERO); 160 | // self * d 161 | // } 162 | // } 163 | 164 | // pub struct Vec2 { 165 | // pub x: f32, 166 | // pub y: f32, 167 | // } 168 | 169 | // impl Vector for T where T: Sub + Sized + Mul + Add {} 170 | -------------------------------------------------------------------------------- /src/voxel.rs: -------------------------------------------------------------------------------- 1 | use glam::{ivec3, vec3, IVec3, Vec3}; 2 | use rand::{thread_rng, Rng}; 3 | 4 | use crate::{leaf_classifier::LeafClassifier, shrubbery::Shrubbery}; 5 | 6 | #[derive(Copy, Clone, Debug, PartialEq)] 7 | pub enum VoxelType { 8 | Air, 9 | Branch, 10 | Greenery, 11 | } 12 | 13 | pub enum LeafShape { 14 | Sphere { r: f32 }, 15 | } 16 | 17 | /// what method to use to classify leaves 18 | pub enum LeafSetting { 19 | // generate no leaves 20 | None, 21 | // color branches as leaves 22 | BranchIsLeaf(LeafClassifier), 23 | // spawn leaf shapes 24 | Shape(LeafShape), 25 | } 26 | 27 | pub enum BranchSizeSetting { 28 | Value { distance: f32 }, 29 | Generation { distances: Vec }, 30 | } 31 | 32 | pub struct BranchRootSizeIncreaser { 33 | pub height: f32, 34 | // how much to maximally add to the root size 35 | pub additional_size: f32, 36 | } 37 | 38 | pub struct VoxelizeSettings { 39 | // pub leaf_shape: Option, 40 | pub leaf_settings: LeafSetting, 41 | // pub leaf_classifier: LeafClassifier, 42 | pub branch_size_setting: BranchSizeSetting, 43 | pub branch_root_size_increaser: Option, 44 | } 45 | 46 | pub fn drop_leaves(voxels: &mut Vec<(IVec3, VoxelType)>, procentage: f32) { 47 | let mut branch_indices = voxels 48 | .iter() 49 | .enumerate() 50 | .filter(|(_i, (_p, v))| v == &VoxelType::Greenery) 51 | .map(|(i, _)| i) 52 | .collect::>(); 53 | 54 | let to_drop = (branch_indices.len() as f32 * procentage) as usize; 55 | let mut to = Vec::with_capacity(to_drop); 56 | for _ in 0..to_drop { 57 | let branch_indicies_i = thread_rng().gen_range(0..branch_indices.len()); 58 | let index = branch_indices[branch_indicies_i]; 59 | branch_indices.remove(branch_indicies_i); 60 | to.push(index); 61 | } 62 | to.sort(); 63 | to.reverse(); 64 | for i in to.into_iter() { 65 | voxels.remove(i); 66 | } 67 | } 68 | 69 | /// 70 | pub fn voxelize(shrubbery: &Shrubbery, settings: &VoxelizeSettings) -> Vec<(IVec3, VoxelType)> { 71 | let (min_bounds, max_bounds) = shrubbery.get_bounds(); 72 | let mut size = max_bounds - min_bounds; 73 | // I use half the size to go -half_size -> half_size 74 | size.x = (size.x as f32 * 0.5).ceil() as i32; 75 | size.z = (size.z as f32 * 0.5).ceil() as i32; 76 | 77 | // apply extra padding in size from leaves 78 | if let LeafSetting::Shape(leaf_shape) = &settings.leaf_settings { 79 | let padding: i32 = match leaf_shape { 80 | LeafShape::Sphere { r } => r.ceil() as i32, 81 | }; 82 | // todo: include root size into padding 83 | size += IVec3::splat(padding); 84 | } 85 | 86 | let mut voxels = Vec::with_capacity(128); 87 | for x in -size.x..size.x { 88 | for y in 0..size.y { 89 | for z in -size.z..size.z { 90 | let pos = ivec3(x, y, z); 91 | process_voxel(pos, shrubbery, &settings, &mut voxels); 92 | } 93 | } 94 | } 95 | 96 | voxels 97 | } 98 | 99 | fn process_voxel( 100 | pos: IVec3, 101 | shrubbery: &Shrubbery, 102 | settings: &VoxelizeSettings, 103 | voxels: &mut Vec<(IVec3, VoxelType)>, 104 | ) { 105 | let sample_pos = vec3(pos.x as f32, pos.y as f32, pos.z as f32); 106 | 107 | // leaf shape 108 | if let LeafSetting::Shape(leaf_shape) = &settings.leaf_settings { 109 | if generate_leaf( 110 | sample_pos, 111 | &shrubbery, 112 | voxels, 113 | leaf_shape, 114 | &LeafClassifier::LastBranch, 115 | ) { 116 | // no need to check for branch 117 | return; 118 | } 119 | } 120 | 121 | let (dist_to_branch, closest_branch_index) = shrubbery.distance_to_branch(sample_pos); 122 | let mut size = match &settings.branch_size_setting { 123 | BranchSizeSetting::Value { distance } => *distance, 124 | BranchSizeSetting::Generation { distances } => { 125 | let closest_branch = &shrubbery.branches[closest_branch_index]; 126 | let index = closest_branch.generation.min(distances.len() as i32 - 1); 127 | *distances.get(index as usize).unwrap_or(&f32::MIN) 128 | } 129 | }; 130 | if let Some(increaser) = &settings.branch_root_size_increaser { 131 | let h_m = 1.0 - (sample_pos.y / increaser.height).min(1.0); 132 | size += h_m * increaser.additional_size; 133 | } 134 | if dist_to_branch < size { 135 | let closest_branch = &shrubbery.branches[closest_branch_index]; 136 | let is_leaf = if let LeafSetting::BranchIsLeaf(classifier) = &settings.leaf_settings { 137 | closest_branch.is_leaf(classifier) 138 | } else { 139 | false 140 | }; 141 | let voxel_type = if is_leaf { 142 | VoxelType::Greenery 143 | } else { 144 | VoxelType::Branch 145 | }; 146 | voxels.push(( 147 | ivec3( 148 | sample_pos.x as i32, 149 | sample_pos.y as i32, 150 | sample_pos.z as i32, 151 | ), 152 | voxel_type, 153 | )); 154 | } 155 | } 156 | 157 | fn generate_leaf( 158 | pos: Vec3, 159 | shrubbery: &Shrubbery, 160 | voxels: &mut Vec<(IVec3, VoxelType)>, 161 | leaf_shape: &LeafShape, 162 | leaf_classifier: &LeafClassifier, 163 | ) -> bool { 164 | for leaf_branch in shrubbery 165 | .branches 166 | .iter() 167 | .filter(|branch| branch.is_leaf(leaf_classifier)) 168 | { 169 | // make leaf 170 | let leaf_pos = leaf_branch.pos; 171 | match leaf_shape { 172 | LeafShape::Sphere { r } => { 173 | if is_inside_sphere(pos, leaf_pos, *r) { 174 | voxels.push(( 175 | ivec3( 176 | pos.x.ceil() as i32, 177 | pos.y.ceil() as i32, 178 | pos.z.ceil() as i32, 179 | ), 180 | VoxelType::Greenery, 181 | )); 182 | // quick exit, we found greenery at this position 183 | return true; 184 | } 185 | } 186 | } 187 | } 188 | false 189 | } 190 | 191 | fn is_inside_sphere(pos: Vec3, sphere_pos: Vec3, radius: f32) -> bool { 192 | pos.distance(sphere_pos) <= radius 193 | } 194 | -------------------------------------------------------------------------------- /voxel_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TanTanDev/shrubbery/d034b98b190f0b2bb83696d71e4bfb866de3b267/voxel_example.png --------------------------------------------------------------------------------