diff --git a/client/Cargo.toml b/client/Cargo.toml index 82a5bbb..24a1aaf 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -14,4 +14,7 @@ rand = "0.8.5" serde = { version = "1.0", features = ["derive"] } toml = "0.8" big_space = "0.9.1" -noise = "0.9.0" \ No newline at end of file +noise = "0.9.0" +itertools = "0.13.0" +bitvec = "1.0.1" +smallvec = "1.14.0" \ No newline at end of file diff --git a/client/assets/textures/generate_skybox.bat b/client/assets/textures/generate_skybox.bat new file mode 100644 index 0000000..1956c18 --- /dev/null +++ b/client/assets/textures/generate_skybox.bat @@ -0,0 +1,3 @@ +#https://github.com/KhronosGroup/KTX-Software/releases + +toktx --t2 --cubemap --target_type RGBA --zcmp 18 --genmipmap sky.ktx2 right.jpg left.jpg top.jpg bottom.jpg front.jpg back.jpg \ No newline at end of file diff --git a/client/assets/textures/skybox_landscape/back.jpg b/client/assets/textures/skybox_landscape/back.jpg new file mode 100644 index 0000000..470a679 Binary files /dev/null and b/client/assets/textures/skybox_landscape/back.jpg differ diff --git a/client/assets/textures/skybox_landscape/bottom.jpg b/client/assets/textures/skybox_landscape/bottom.jpg new file mode 100644 index 0000000..893f394 Binary files /dev/null and b/client/assets/textures/skybox_landscape/bottom.jpg differ diff --git a/client/assets/textures/skybox_landscape/front.jpg b/client/assets/textures/skybox_landscape/front.jpg new file mode 100644 index 0000000..4e17b77 Binary files /dev/null and b/client/assets/textures/skybox_landscape/front.jpg differ diff --git a/client/assets/textures/skybox_landscape/left.jpg b/client/assets/textures/skybox_landscape/left.jpg new file mode 100644 index 0000000..5750b91 Binary files /dev/null and b/client/assets/textures/skybox_landscape/left.jpg differ diff --git a/client/assets/textures/skybox_landscape/right.jpg b/client/assets/textures/skybox_landscape/right.jpg new file mode 100644 index 0000000..8963037 Binary files /dev/null and b/client/assets/textures/skybox_landscape/right.jpg differ diff --git a/client/assets/textures/skybox_landscape/sky.ktx2 b/client/assets/textures/skybox_landscape/sky.ktx2 new file mode 100644 index 0000000..1db8c55 Binary files /dev/null and b/client/assets/textures/skybox_landscape/sky.ktx2 differ diff --git a/client/assets/textures/skybox_landscape/top.jpg b/client/assets/textures/skybox_landscape/top.jpg new file mode 100644 index 0000000..4db3c2a Binary files /dev/null and b/client/assets/textures/skybox_landscape/top.jpg differ diff --git a/client/assets/textures/skybox_space/back.png b/client/assets/textures/skybox_space/back.png new file mode 100644 index 0000000..2b5a000 Binary files /dev/null and b/client/assets/textures/skybox_space/back.png differ diff --git a/client/assets/textures/skybox_space/bottom.png b/client/assets/textures/skybox_space/bottom.png new file mode 100644 index 0000000..b2fb4af Binary files /dev/null and b/client/assets/textures/skybox_space/bottom.png differ diff --git a/client/assets/textures/skybox_space/front.png b/client/assets/textures/skybox_space/front.png new file mode 100644 index 0000000..8f8fd51 Binary files /dev/null and b/client/assets/textures/skybox_space/front.png differ diff --git a/client/assets/textures/skybox_space/left.png b/client/assets/textures/skybox_space/left.png new file mode 100644 index 0000000..3775937 Binary files /dev/null and b/client/assets/textures/skybox_space/left.png differ diff --git a/client/assets/textures/skybox_space/right.png b/client/assets/textures/skybox_space/right.png new file mode 100644 index 0000000..d3aa161 Binary files /dev/null and b/client/assets/textures/skybox_space/right.png differ diff --git a/client/assets/textures/skybox_space/sky.ktx2 b/client/assets/textures/skybox_space/sky.ktx2 new file mode 100644 index 0000000..2586262 Binary files /dev/null and b/client/assets/textures/skybox_space/sky.ktx2 differ diff --git a/client/assets/textures/skybox_space/top.png b/client/assets/textures/skybox_space/top.png new file mode 100644 index 0000000..0360a9a Binary files /dev/null and b/client/assets/textures/skybox_space/top.png differ diff --git a/client/assets/textures/skybox_space_1024/sky.ktx2 b/client/assets/textures/skybox_space_1024/sky.ktx2 new file mode 100644 index 0000000..92b2c85 Binary files /dev/null and b/client/assets/textures/skybox_space_1024/sky.ktx2 differ diff --git a/client/src/plugins/environment/systems/voxel_system.rs b/client/src/plugins/environment/systems/voxel_system.rs index 6a41451..79348f9 100644 --- a/client/src/plugins/environment/systems/voxel_system.rs +++ b/client/src/plugins/environment/systems/voxel_system.rs @@ -7,12 +7,11 @@ use big_space::prelude::GridCell; use noise::{Fbm, NoiseFn, Perlin}; use crate::plugins::big_space::big_space_plugin::RootGrid; use crate::plugins::environment::systems::camera_system::CameraController; +use crate::plugins::environment::systems::planet_system::PlanetMaker; use crate::plugins::environment::systems::voxels::structure::*; pub fn setup( mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, root: Res, ) { let unit_size = 1.0; @@ -27,13 +26,15 @@ pub fn setup( let color = Color::rgb(0.2, 0.8, 0.2); /*generate_voxel_rect(&mut octree,color);*/ generate_voxel_sphere(&mut octree, 10, color); - - commands.spawn( - ( - Transform::default(), - octree - ) - ); + + commands.entity(root.0).with_children(|parent| { + parent.spawn( + ( + Transform::default(), + octree + ) + ); + }); } fn generate_voxel_sphere( diff --git a/client/src/plugins/environment/systems/voxels/debug.rs b/client/src/plugins/environment/systems/voxels/debug.rs new file mode 100644 index 0000000..3529fec --- /dev/null +++ b/client/src/plugins/environment/systems/voxels/debug.rs @@ -0,0 +1,145 @@ +use bevy::prelude::*; +use crate::plugins::environment::systems::voxels::structure::*; + +/// Visualize each node of the octree as a scaled cuboid, **center-based**. +/// `octree_tf.translation` is the world-space center of the root bounding box. +pub fn visualize_octree_system( + mut gizmos: Gizmos, + octree_query: Query<(&SparseVoxelOctree, &Transform)>, +) { + for (octree, octree_tf) in octree_query.iter() { + // The root node covers [-size/2..+size/2], so half_size is: + let half_size = octree.size * 0.5; + + // Draw a translucent cuboid for the root + gizmos.cuboid( + Transform::from_translation(octree_tf.translation) + .with_scale(Vec3::splat(octree.size)), + Color::rgba(1.0, 1.0, 0.0, 0.15), + ); + + // Recursively draw children: + // Start from depth=0. The node at depth=0 has bounding side = octree.size. + visualize_recursive_center( + &mut gizmos, + &octree.root, + octree_tf.translation, // center of root in world + octree.size, + 0, + octree.max_depth, + ); + } +} + +/// Recursively draws cuboids for each node. +/// We follow the same indexing as insert_recursive, i.e. bit patterns: +/// i=0 => child in (-x,-y,-z) quadrant, +/// i=1 => (+x,-y,-z), i=2 => (-x,+y,-z), etc. +fn visualize_recursive_center( + gizmos: &mut Gizmos, + node: &OctreeNode, + parent_center: Vec3, + parent_size: f32, + depth: u32, + max_depth: u32, +) { + if depth >= max_depth { + return; + } + if let Some(children) = &node.children { + // Each child is half the parent’s size + let child_size = parent_size * 0.5; + let half = child_size * 0.5; + + for (i, child) in children.iter().enumerate() { + // For i in [0..8], bits: x=1, y=2, z=4 + let offset_x = if (i & 1) != 0 { half } else { -half }; + let offset_y = if (i & 2) != 0 { half } else { -half }; + let offset_z = if (i & 4) != 0 { half } else { -half }; + + let child_center = parent_center + Vec3::new(offset_x, offset_y, offset_z); + + // Draw the child bounding box + gizmos.cuboid( + Transform::from_translation(child_center).with_scale(Vec3::splat(child_size)), + Color::rgba(0.5, 1.0, 0.5, 0.15), // greenish + ); + + // Recurse + visualize_recursive_center( + gizmos, + child, + child_center, + child_size, + depth + 1, + max_depth, + ); + } + } else { + // If node.is_leaf && node.voxel.is_some(), draw a smaller marker + if node.is_leaf { + if let Some(voxel) = node.voxel { + // We'll choose a size that's a fraction of the parent's size. + // For example, 25% of the parent bounding box dimension. + let leaf_size = parent_size * 0.25; + + // Draw a small cuboid at the same center as the parent node. + gizmos.cuboid( + Transform::from_translation(parent_center) + .with_scale(Vec3::splat(leaf_size)), + voxel.color, + ); + } + } + } +} + +#[allow(dead_code)] +pub fn draw_grid( + mut gizmos: Gizmos, + camera_query: Query<&Transform, With>, + octree_query: Query<(&SparseVoxelOctree, &Transform)>, +) { + let camera_tf = camera_query.single(); + let camera_pos = camera_tf.translation; + + for (octree, octree_tf) in octree_query.iter() { + let half_size = octree.size * 0.5; + let root_center = octree_tf.translation; + + // Voxel spacing at max depth + let spacing = octree.get_spacing_at_depth(octree.max_depth); + let grid_count = (octree.size / spacing) as i32; + + // We'll define the bounding region as [center-half_size .. center+half_size]. + // So the min corner is (root_center - half_size). + let min_corner = root_center - Vec3::splat(half_size); + + // Draw lines in X & Z directions (like a ground plane). + for i in 0..=grid_count { + let offset = i as f32 * spacing; + + // 1) line along Z + let x = min_corner.x + offset; + let z1 = min_corner.z; + let z2 = min_corner.z + (grid_count as f32 * spacing); + + let p1 = Vec3::new(x, min_corner.y, z1); + let p2 = Vec3::new(x, min_corner.y, z2); + + // offset by -camera_pos for stable Gizmos in large coords + let p1_f32 = p1 - camera_pos; + let p2_f32 = p2 - camera_pos; + gizmos.line(p1_f32, p2_f32, Color::WHITE); + + // 2) line along X + let z = min_corner.z + offset; + let x1 = min_corner.x; + let x2 = min_corner.x + (grid_count as f32 * spacing); + + let p3 = Vec3::new(x1, min_corner.y, z) - camera_pos; + let p4 = Vec3::new(x2, min_corner.y, z) - camera_pos; + gizmos.line(p3, p4, Color::WHITE); + } + } +} \ No newline at end of file diff --git a/client/src/plugins/environment/systems/voxels/helper.rs b/client/src/plugins/environment/systems/voxels/helper.rs new file mode 100644 index 0000000..4a4df92 --- /dev/null +++ b/client/src/plugins/environment/systems/voxels/helper.rs @@ -0,0 +1,247 @@ +use bevy::prelude::*; +use crate::plugins::environment::systems::voxels::structure::*; + +impl SparseVoxelOctree { + pub fn ray_intersects_aabb(&self,ray: &Ray, aabb: &AABB) -> bool { + let inv_dir = 1.0 / ray.direction; + let t1 = (aabb.min - ray.origin) * inv_dir; + let t2 = (aabb.max - ray.origin) * inv_dir; + + let t_min = t1.min(t2); + let t_max = t1.max(t2); + + let t_enter = t_min.max_element(); + let t_exit = t_max.min_element(); + + t_enter <= t_exit && t_exit >= 0.0 + } + + + /// Returns the size of one voxel at the given depth. + pub fn get_spacing_at_depth(&self, depth: u32) -> f32 { + let effective = depth.min(self.max_depth); + self.size / (2_u32.pow(effective)) as f32 + } + + + /// Center-based: [-size/2..+size/2]. Shift +half_size => [0..size], floor, shift back. + pub fn normalize_to_voxel_at_depth(&self, position: Vec3, depth: u32) -> Vec3 { + // Convert world coordinate to normalized [0,1] space. + let half_size = self.size * 0.5; + // Shift to [0, self.size] + let shifted = (position + Vec3::splat(half_size)) / self.size; + // Determine the number of voxels along an edge at the given depth. + let voxel_count = 2_u32.pow(depth) as f32; + // Get the voxel index (as a float) and then compute the center in normalized space. + let voxel_index = (shifted * voxel_count).floor(); + let voxel_center = (voxel_index + Vec3::splat(0.5)) / voxel_count; + voxel_center + } + pub fn denormalize_voxel_center(&self, voxel_center: Vec3) -> Vec3 { + let half_size = self.size * 0.5; + // Convert the normalized voxel center back to world space. + voxel_center * self.size - Vec3::splat(half_size) + } + + + pub fn compute_child_bounds(&self, bounds: &AABB, index: usize) -> AABB { + let min = bounds.min; + let max = bounds.max; + let center = (min + max) / 2.0; + + let x_min = if (index & 1) == 0 { min.x } else { center.x }; + let x_max = if (index & 1) == 0 { center.x } else { max.x }; + + let y_min = if (index & 2) == 0 { min.y } else { center.y }; + let y_max = if (index & 2) == 0 { center.y } else { max.y }; + + let z_min = if (index & 4) == 0 { min.z } else { center.z }; + let z_max = if (index & 4) == 0 { center.z } else { max.z }; + + let child_bounds = AABB { + min: Vec3::new(x_min, y_min, z_min), + max: Vec3::new(x_max, y_max, z_max), + }; + + child_bounds + } + + pub fn ray_intersects_aabb_with_normal( + &self, + ray: &Ray, + aabb: &AABB, + ) -> Option<(f32, f32, Vec3)> { + // Define a safe inverse function to avoid division by zero. + let safe_inv = |d: f32| if d.abs() < 1e-6 { 1e6 } else { 1.0 / d }; + let inv_dir = Vec3::new( + safe_inv(ray.direction.x), + safe_inv(ray.direction.y), + safe_inv(ray.direction.z), + ); + + let t1 = (aabb.min - ray.origin) * inv_dir; + let t2 = (aabb.max - ray.origin) * inv_dir; + + let tmin = t1.min(t2); + let tmax = t1.max(t2); + + let t_enter = tmin.max_element(); + let t_exit = tmax.min_element(); + + if t_enter <= t_exit && t_exit >= 0.0 { + let epsilon = 1e-6; + let mut normal = Vec3::ZERO; + // Determine which face was hit by comparing t_enter to the computed values. + if (t_enter - t1.x).abs() < epsilon || (t_enter - t2.x).abs() < epsilon { + normal = Vec3::new(if ray.direction.x < 0.0 { 1.0 } else { -1.0 }, 0.0, 0.0); + } else if (t_enter - t1.y).abs() < epsilon || (t_enter - t2.y).abs() < epsilon { + normal = Vec3::new(0.0, if ray.direction.y < 0.0 { 1.0 } else { -1.0 }, 0.0); + } else if (t_enter - t1.z).abs() < epsilon || (t_enter - t2.z).abs() < epsilon { + normal = Vec3::new(0.0, 0.0, if ray.direction.z < 0.0 { 1.0 } else { -1.0 }); + } + Some((t_enter, t_exit, normal)) + } else { + None + } + } + + /// Checks if (x,y,z) is within [-size/2..+size/2]. + pub fn contains(&self, x: f32, y: f32, z: f32) -> bool { + let half_size = self.size / 2.0; + let eps = 1e-6; + (x >= -half_size - eps && x < half_size + eps) + && (y >= -half_size - eps && y < half_size + eps) + && (z >= -half_size - eps && z < half_size + eps) + } + + /// Retrieve a voxel at world coordinates by normalizing and looking up. + pub fn get_voxel_at_world_coords(&self, position: Vec3) -> Option<&Voxel> { + let aligned = self.normalize_to_voxel_at_depth(position, self.max_depth); + self.get_voxel_at(aligned.x, aligned.y, aligned.z) + } + + pub fn local_to_world(&self, local_pos: Vec3) -> Vec3 { + // Half the total octree size, used to shift the center to the origin. + let half_size = self.size * 0.5; + // Convert local coordinate to world space: + // 1. Subtract 0.5 to center the coordinate at zero (range becomes [-0.5, 0.5]) + // 2. Multiply by the total size to scale into world units. + // 3. Add half_size to shift from a center–based system to one starting at zero. + (local_pos - Vec3::splat(0.5)) * self.size + Vec3::splat(half_size) + } + + + + /// Helper function to recursively traverse the octree to a specific depth. + fn get_node_at_depth( + node: &OctreeNode, + x: f32, + y: f32, + z: f32, + depth: u32, + ) -> Option<&OctreeNode> { + if depth == 0 { + return Some(node); // We've reached the desired depth + } + + if let Some(ref children) = node.children { + // Determine which child to traverse into + let epsilon = 1e-6; + let index = ((x >= 0.5 - epsilon) as usize) + + ((y >= 0.5 - epsilon) as usize * 2) + + ((z >= 0.5 - epsilon) as usize * 4); + + let adjust_coord = |coord: f32| { + if coord >= 0.5 - epsilon { + (coord - 0.5) * 2.0 + } else { + coord * 2.0 + } + }; + + // Recurse into the correct child + Self::get_node_at_depth( + &children[index], + adjust_coord(x), + adjust_coord(y), + adjust_coord(z), + depth - 1, + ) + } else { + None // Node has no children at this depth + } + } + + pub fn has_volume(&self, node: &OctreeNode) -> bool { + // Check if this node is a leaf with a voxel + if node.is_leaf && node.voxel.is_some() { + return true; + } + + // If the node has children, recursively check them + if let Some(children) = &node.children { + for child in children.iter() { + if self.has_volume(child) { + return true; // If any child has a voxel, the chunk has volume + } + } + } + + // If no voxel found in this node or its children + false + } + + + +} + +/// Returns the (face_normal, local_offset) for the given neighbor direction. +/// - `dx, dy, dz`: The integer direction of the face (-1,0,0 / 1,0,0 / etc.) +/// - `voxel_size_f`: The world size of a single voxel (e.g. step as f32). +pub fn face_orientation(dx: f32, dy: f32, dz: f32, voxel_size_f: f32) -> (Vec3, Vec3) { + // We'll do a match on the direction + match (dx, dy, dz) { + // Negative X => face normal is (-1, 0, 0), local offset is -voxel_size/2 in X + (-1.0, 0.0, 0.0) => { + let normal = Vec3::new(-1.0, 0.0, 0.0); + let offset = Vec3::new(-voxel_size_f * 0.5, 0.0, 0.0); + (normal, offset) + } + // Positive X + (1.0, 0.0, 0.0) => { + let normal = Vec3::new(1.0, 0.0, 0.0); + let offset = Vec3::new(voxel_size_f * 0.5, 0.0, 0.0); + (normal, offset) + } + // Negative Y + (0.0, -1.0, 0.0) => { + let normal = Vec3::new(0.0, -1.0, 0.0); + let offset = Vec3::new(0.0, -voxel_size_f * 0.5, 0.0); + (normal, offset) + } + // Positive Y + (0.0, 1.0, 0.0) => { + let normal = Vec3::new(0.0, 1.0, 0.0); + let offset = Vec3::new(0.0, voxel_size_f * 0.5, 0.0); + (normal, offset) + } + // Negative Z + (0.0, 0.0, -1.0) => { + let normal = Vec3::new(0.0, 0.0, -1.0); + let offset = Vec3::new(0.0, 0.0, -voxel_size_f * 0.5); + (normal, offset) + } + // Positive Z + (0.0, 0.0, 1.0) => { + let normal = Vec3::new(0.0, 0.0, 1.0); + let offset = Vec3::new(0.0, 0.0, voxel_size_f * 0.5); + (normal, offset) + } + // If the direction is not one of the 6 axis directions, you might skip or handle differently + _ => { + // For safety, we can panic or return a default. + // But typically you won't call face_orientation with an invalid direction + panic!("Invalid face direction: ({}, {}, {})", dx, dy, dz); + } + } +} diff --git a/client/src/plugins/environment/systems/voxels/mod.rs b/client/src/plugins/environment/systems/voxels/mod.rs new file mode 100644 index 0000000..c2098a3 --- /dev/null +++ b/client/src/plugins/environment/systems/voxels/mod.rs @@ -0,0 +1,5 @@ +pub mod debug; +pub mod helper; +pub mod octree; +pub mod structure; +pub mod rendering; \ No newline at end of file diff --git a/client/src/plugins/environment/systems/voxels/octree.rs b/client/src/plugins/environment/systems/voxels/octree.rs new file mode 100644 index 0000000..c4802da --- /dev/null +++ b/client/src/plugins/environment/systems/voxels/octree.rs @@ -0,0 +1,393 @@ +use std::collections::{HashMap, HashSet}; +use bevy::asset::Assets; +use bevy::color::Color; +use bevy::math::{DQuat, DVec3}; +use bevy::prelude::*; +use bevy::render::mesh::{Indices, PrimitiveTopology, VertexAttributeValues}; +use bevy::render::render_asset::RenderAssetUsages; +use crate::plugins::environment::systems::voxels::structure::{DirtyVoxel, OctreeNode, Ray, SparseVoxelOctree, Voxel, AABB, NEIGHBOR_OFFSETS}; + +impl SparseVoxelOctree { + /// Creates a new octree with the specified max depth, size, and wireframe visibility. + pub fn new(max_depth: u32, size: f32, show_wireframe: bool, show_world_grid: bool, show_chunks: bool) -> Self { + Self { + root: OctreeNode::new(), + max_depth, + size, + show_wireframe, + show_world_grid, + show_chunks, + dirty: Vec::new(), + } + } + pub fn insert(&mut self, position: Vec3, voxel: Voxel) { + // Align to the center of the voxel at max_depth + let mut aligned = self.normalize_to_voxel_at_depth(position, self.max_depth); + let mut world_center = self.denormalize_voxel_center(aligned); + + // Expand as needed using the denormalized position. + while !self.contains(world_center.x, world_center.y, world_center.z) { + self.expand_root(world_center.x, world_center.y, world_center.z); + // Recompute aligned and world_center after expansion. + aligned = self.normalize_to_voxel_at_depth(position, self.max_depth); + world_center = self.denormalize_voxel_center(aligned); + } + + let dirty_voxel = DirtyVoxel{ + position: aligned, + }; + self.dirty.push(dirty_voxel); + + + Self::insert_recursive(&mut self.root, aligned, voxel, self.max_depth); + } + + fn insert_recursive(node: &mut OctreeNode, position: Vec3, voxel: Voxel, depth: u32) { + if depth == 0 { + node.voxel = Some(voxel); + node.is_leaf = true; + return; + } + let epsilon = 1e-6; + // Determine octant index by comparing with 0.5 + let index = ((position.x >= 0.5 - epsilon) as usize) + + ((position.y >= 0.5 - epsilon) as usize * 2) + + ((position.z >= 0.5 - epsilon) as usize * 4); + + // If there are no children, create them. + if node.children.is_none() { + node.children = Some(Box::new(core::array::from_fn(|_| OctreeNode::new()))); + node.is_leaf = false; + } + if let Some(ref mut children) = node.children { + // Adjust coordinate into the child’s [0, 1] range. + let adjust_coord = |coord: f32| { + if coord >= 0.5 - epsilon { + (coord - 0.5) * 2.0 + } else { + coord * 2.0 + } + }; + let child_pos = Vec3::new( + adjust_coord(position.x), + adjust_coord(position.y), + adjust_coord(position.z), + ); + Self::insert_recursive(&mut children[index], child_pos, voxel, depth - 1); + } + } + + pub fn remove(&mut self, position: Vec3) { + let aligned = self.normalize_to_voxel_at_depth(position, self.max_depth); + + let dirty_voxel = DirtyVoxel{ + position: aligned, + }; + self.dirty.push(dirty_voxel); + + Self::remove_recursive(&mut self.root, aligned.x, aligned.y, aligned.z, self.max_depth); + } + + fn remove_recursive( + node: &mut OctreeNode, + x: f32, + y: f32, + z: f32, + depth: u32, + ) -> bool { + if depth == 0 { + if node.voxel.is_some() { + node.voxel = None; + node.is_leaf = false; + return true; + } else { + return false; + } + } + + if node.children.is_none() { + return false; + } + let epsilon = 1e-6; + let index = ((x >= 0.5 - epsilon) as usize) + + ((y >= 0.5 - epsilon) as usize * 2) + + ((z >= 0.5 - epsilon) as usize * 4); + + let adjust_coord = |coord: f32| { + if coord >= 0.5 - epsilon { + (coord - 0.5) * 2.0 + } else { + coord * 2.0 + } + }; + + let child = &mut node.children.as_mut().unwrap()[index]; + let should_prune_child = Self::remove_recursive( + child, + adjust_coord(x), + adjust_coord(y), + adjust_coord(z), + depth - 1, + ); + + if should_prune_child { + // remove the child node + node.children.as_mut().unwrap()[index] = OctreeNode::new(); + } + + // Check if all children are empty + let all_children_empty = node + .children + .as_ref() + .unwrap() + .iter() + .all(|child| child.is_empty()); + + if all_children_empty { + node.children = None; + node.is_leaf = true; + return node.voxel.is_none(); + } + false + } + + + fn expand_root(&mut self, _x: f32, _y: f32, _z: f32) { + info!("Root expanding ..."); + // Save the old root and its size. + let old_root = std::mem::replace(&mut self.root, OctreeNode::new()); + let old_size = self.size; + + // Update the octree's size and depth. + self.size *= 2.0; + self.max_depth += 1; + + // Reinsert each voxel from the old tree. + let voxels = Self::collect_voxels_from_node(&old_root, old_size); + for (world_pos, voxel, _depth) in voxels { + self.insert(world_pos, voxel); + } + } + + /// Helper: Collect all voxels from a given octree node recursively. + /// The coordinate system here assumes the node covers [–old_size/2, +old_size/2] in each axis. + fn collect_voxels_from_node(node: &OctreeNode, old_size: f32) -> Vec<(Vec3, Voxel, u32)> { + let mut voxels = Vec::new(); + Self::collect_voxels_recursive(node, -old_size / 2.0, -old_size / 2.0, -old_size / 2.0, old_size, 0, &mut voxels); + voxels + } + + fn collect_voxels_recursive( + node: &OctreeNode, + x: f32, + y: f32, + z: f32, + size: f32, + depth: u32, + out: &mut Vec<(Vec3, Voxel, u32)>, + ) { + if node.is_leaf { + if let Some(voxel) = node.voxel { + // Compute the center of this node's region. + let center = Vec3::new(x + size / 2.0, y + size / 2.0, z + size / 2.0); + out.push((center, voxel, depth)); + } + } + if let Some(children) = &node.children { + let half = size / 2.0; + for (i, child) in children.iter().enumerate() { + let offset_x = if (i & 1) != 0 { half } else { 0.0 }; + let offset_y = if (i & 2) != 0 { half } else { 0.0 }; + let offset_z = if (i & 4) != 0 { half } else { 0.0 }; + Self::collect_voxels_recursive(child, x + offset_x, y + offset_y, z + offset_z, half, depth + 1, out); + } + } + } + + + + pub fn traverse(&self) -> Vec<(Vec3, Color, u32)> { + let mut voxels = Vec::new(); + // Start at the normalized center (0.5, 0.5, 0.5) rather than (0,0,0) + Self::traverse_recursive( + &self.root, + Vec3::splat(0.5), // normalized center of the root cell + 1.0, // full normalized cell size + 0, + &mut voxels, + self, + ); + voxels + } + + fn traverse_recursive( + node: &OctreeNode, + local_center: Vec3, + size: f32, + depth: u32, + out: &mut Vec<(Vec3, Color, u32)>, + octree: &SparseVoxelOctree, + ) { + // If a leaf contains a voxel, record its world-space center + if node.is_leaf { + if let Some(voxel) = node.voxel { + out.push((octree.denormalize_voxel_center(local_center), voxel.color, depth)); + } + } + + // If the node has children, subdivide the cell into 8 subcells. + if let Some(ref children) = node.children { + let offset = size / 4.0; // child center offset from parent center + let new_size = size / 2.0; // each child cell's size in normalized space + for (i, child) in children.iter().enumerate() { + // Compute each axis' offset: use +offset if the bit is set, else -offset. + let dx = if (i & 1) != 0 { offset } else { -offset }; + let dy = if (i & 2) != 0 { offset } else { -offset }; + let dz = if (i & 4) != 0 { offset } else { -offset }; + let child_center = local_center + Vec3::new(dx, dy, dz); + + Self::traverse_recursive(child, child_center, new_size, depth + 1, out, octree); + } + } + } + + + + /// Retrieve a voxel from the octree if it exists (x,y,z in [-0.5..+0.5] range). + pub fn get_voxel_at(&self, x: f32, y: f32, z: f32) -> Option<&Voxel> { + Self::get_voxel_recursive(&self.root, x, y, z) + } + + fn get_voxel_recursive(node: &OctreeNode, x: f32, y: f32, z: f32) -> Option<&Voxel> { + if node.is_leaf { + return node.voxel.as_ref(); + } + if let Some(children) = &node.children { + let epsilon = 1e-6; + let index = ((x >= 0.5 - epsilon) as usize) + + ((y >= 0.5 - epsilon) as usize * 2) + + ((z >= 0.5 - epsilon) as usize * 4); + let adjust_coord = |coord: f32| { + if coord >= 0.5 - epsilon { + (coord - 0.5) * 2.0 + } else { + coord * 2.0 + } + }; + Self::get_voxel_recursive( + &children[index], + adjust_coord(x), + adjust_coord(y), + adjust_coord(z), + ) + } else { + None + } + } + + /// Checks if there is a neighbor voxel at the specified direction from the given world coordinates at the specified depth. + /// The offsets are directions (-1, 0, 1) for x, y, z. + pub fn has_neighbor( + &self, + position: Vec3, + offset_x: i32, + offset_y: i32, + offset_z: i32, + depth: u32, + ) -> bool { + let aligned = self.normalize_to_voxel_at_depth(position, depth); + let voxel_count = 2_u32.pow(depth) as f32; + // Normalized voxel size is 1/voxel_count + let norm_voxel_size = 1.0 / voxel_count; + + let neighbor = Vec3::new( + aligned.x + (offset_x as f32) * norm_voxel_size, + aligned.y + (offset_y as f32) * norm_voxel_size, + aligned.z + (offset_z as f32) * norm_voxel_size, + ); + + // Convert the normalized neighbor coordinate back to world space + let half_size = self.size * 0.5; + let neighbor_world = neighbor * self.size - Vec3::splat(half_size); + + if !self.contains(neighbor_world.x, neighbor_world.y, neighbor_world.z) { + return false; + } + + self.get_voxel_at_world_coords(neighbor_world).is_some() + } + + + /// Performs a raycast against the octree and returns the first intersected voxel. + pub fn raycast(&self, ray: &Ray) -> Option<(f32, f32, f32, u32, Vec3)> { + // Start from the root node + let half_size = self.size / 2.0; + let root_bounds = AABB { + min: Vec3::new(-half_size as f32, -half_size as f32, -half_size as f32), + max: Vec3::new(half_size as f32, half_size as f32, half_size as f32), + }; + self.raycast_recursive( + &self.root, + ray, + &root_bounds, + 0, + ) + } + + fn raycast_recursive( + &self, + node: &OctreeNode, + ray: &Ray, + bounds: &AABB, + depth: u32, + ) -> Option<(f32, f32, f32, u32, Vec3)> { + // Check if the ray intersects this node's bounding box + if let Some((t_enter, _, normal)) = self.ray_intersects_aabb_with_normal(ray, bounds) { + // If this is a leaf node and contains a voxel, return it + if node.is_leaf && node.voxel.is_some() { + // Compute the exact hit position + let hit_position = ray.origin + ray.direction * t_enter; + + // Return the hit position along with depth and normal + return Some(( + hit_position.x as f32, + hit_position.y as f32, + hit_position.z as f32, + depth, + normal, + )); + } + + // If the node has children, traverse them + if let Some(ref children) = node.children { + // For each child, compute its bounding box and recurse + let mut hits = Vec::new(); + for (i, child) in children.iter().enumerate() { + let child_bounds = self.compute_child_bounds(bounds, i); + if let Some(hit) = self.raycast_recursive(child, ray, &child_bounds, depth + 1) { + hits.push(hit); + } + } + // Return the closest hit, if any + if !hits.is_empty() { + hits.sort_by(|a, b| { + let dist_a = ((a.0 as f32 - ray.origin.x).powi(2) + + (a.1 as f32 - ray.origin.y).powi(2) + + (a.2 as f32 - ray.origin.z).powi(2)) + .sqrt(); + let dist_b = ((b.0 as f32 - ray.origin.x).powi(2) + + (b.1 as f32 - ray.origin.y).powi(2) + + (b.2 as f32 - ray.origin.z).powi(2)) + .sqrt(); + dist_a.partial_cmp(&dist_b).unwrap() + }); + return Some(hits[0]); + } + } + } + + None + } + +} + diff --git a/client/src/plugins/environment/systems/voxels/rendering.rs b/client/src/plugins/environment/systems/voxels/rendering.rs new file mode 100644 index 0000000..afeb9ba --- /dev/null +++ b/client/src/plugins/environment/systems/voxels/rendering.rs @@ -0,0 +1,202 @@ +use bevy::asset::RenderAssetUsages; +use bevy::prelude::*; +use bevy::render::mesh::*; +use bevy::render::render_resource::*; +use big_space::prelude::GridCell; +use crate::plugins::big_space::big_space_plugin::RootGrid; +use crate::plugins::environment::systems::voxels::structure::*; +#[derive(Component)] +pub struct VoxelTerrainMarker {} + + +pub fn render( + mut commands: Commands, + mut query: Query<&mut SparseVoxelOctree>, + render_object_query: Query>, + mut meshes: ResMut>, + mut materials: ResMut>, + root: Res, +) { + + + for mut octree in query.iter_mut() { + // Only update when marked dirty + if !octree.dirty.is_empty() { + // Remove old render objects + for entity in render_object_query.iter() { + info!("Despawning {}", entity); + commands.entity(entity).despawn_recursive(); + } + + // Get the voxel centers (world positions), color, and depth. + let voxels = octree.traverse(); + + // Debug: Log the number of voxels traversed. + info!("Voxel count: {}", voxels.len()); + + let mut voxel_meshes = Vec::new(); + + for (world_position, _color, depth) in voxels { + // Get the size of the voxel at the current depth. + let voxel_size = octree.get_spacing_at_depth(depth); + + // The traverse method already returns the voxel center in world space. + + // For each neighbor direction, check if this voxel face is exposed. + for &(dx, dy, dz) in NEIGHBOR_OFFSETS.iter() { + // Pass the world-space voxel center directly. + if !octree.has_neighbor(world_position, dx as i32, dy as i32, dz as i32, depth) { + + // Determine face normal and the local offset for the face. + let (normal, offset) = match (dx, dy, dz) { + (-1.0, 0.0, 0.0) => ( + Vec3::new(-1.0, 0.0, 0.0), + Vec3::new(-voxel_size / 2.0, 0.0, 0.0), + ), + (1.0, 0.0, 0.0) => ( + Vec3::new(1.0, 0.0, 0.0), + Vec3::new(voxel_size / 2.0, 0.0, 0.0), + ), + (0.0, -1.0, 0.0) => ( + Vec3::new(0.0, -1.0, 0.0), + Vec3::new(0.0, -voxel_size / 2.0, 0.0), + ), + (0.0, 1.0, 0.0) => ( + Vec3::new(0.0, 1.0, 0.0), + Vec3::new(0.0, voxel_size / 2.0, 0.0), + ), + (0.0, 0.0, -1.0) => ( + Vec3::new(0.0, 0.0, -1.0), + Vec3::new(0.0, 0.0, -voxel_size / 2.0), + ), + (0.0, 0.0, 1.0) => ( + Vec3::new(0.0, 0.0, 1.0), + Vec3::new(0.0, 0.0, voxel_size / 2.0), + ), + _ => continue, + }; + + voxel_meshes.push(generate_face( + world_position + offset, // offset the face + voxel_size / 2.0, + normal + )); + } + } + } + + // Merge all the face meshes into a single mesh. + let mesh = merge_meshes(voxel_meshes); + let cube_handle = meshes.add(mesh); + + // Create a material with cull_mode disabled to see both sides (for debugging) + let material = materials.add(StandardMaterial { + base_color: Color::srgba(0.8, 0.7, 0.6, 1.0), + cull_mode: Some(Face::Back), // disable culling for debugging + ..Default::default() + }); + + commands.entity(root.0).with_children(|parent| { + parent.spawn(( + PbrBundle { + mesh: Mesh3d::from(cube_handle), + material: MeshMaterial3d::from(material), + transform: Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)), + ..Default::default() + }, + GridCell::::ZERO, + VoxelTerrainMarker {}, + )); + }); + + // Reset the dirty flag after updating. + octree.dirty.clear(); + } + } +} + +fn generate_face(position: Vec3, face_size: f32, normal: Vec3) -> Mesh { + // Initialize an empty mesh with triangle topology + let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default()); + + // Define a quad centered at the origin + let mut positions = vec![ + [-face_size, -face_size, 0.0], + [ face_size, -face_size, 0.0], + [ face_size, face_size, 0.0], + [-face_size, face_size, 0.0], + ]; + + // Normalize the provided normal to ensure correct rotation + let normal = normal.normalize(); + // Compute a rotation that aligns the default +Z with the provided normal + let rotation = Quat::from_rotation_arc(Vec3::Z, normal); + + // Rotate and translate the vertices based on the computed rotation and provided position + for p in positions.iter_mut() { + let vertex = rotation * Vec3::from(*p) + position; + *p = [vertex.x, vertex.y, vertex.z]; + } + + let uvs = vec![ + [0.0, 1.0], + [1.0, 1.0], + [1.0, 0.0], + [0.0, 0.0], + ]; + + let indices = Indices::U32(vec![0, 1, 2, 2, 3, 0]); + + // Use the provided normal for all vertices + let normals = vec![[normal.x, normal.y, normal.z]; 4]; + + mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); + mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); + mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs); + mesh.insert_indices(indices); + + mesh +} + +fn merge_meshes(meshes: Vec) -> Mesh { + let mut merged_positions = Vec::new(); + let mut merged_uvs = Vec::new(); + let mut merged_normals = Vec::new(); // To store merged normals + let mut merged_indices = Vec::new(); + + for mesh in meshes { + if let Some(VertexAttributeValues::Float32x3(positions)) = mesh.attribute(Mesh::ATTRIBUTE_POSITION) { + let start_index = merged_positions.len(); + merged_positions.extend_from_slice(positions); + + // Extract UVs + if let Some(VertexAttributeValues::Float32x2(uvs)) = mesh.attribute(Mesh::ATTRIBUTE_UV_0) { + merged_uvs.extend_from_slice(uvs); + } + + // Extract normals + if let Some(VertexAttributeValues::Float32x3(normals)) = mesh.attribute(Mesh::ATTRIBUTE_NORMAL) { + merged_normals.extend_from_slice(normals); + } + + // Extract indices and apply offset + if let Some(indices) = mesh.indices() { + if let Indices::U32(indices) = indices { + let offset_indices: Vec = indices.iter().map(|i| i + start_index as u32).collect(); + merged_indices.extend(offset_indices); + } + } + } + } + + // Create new merged mesh + let mut merged_mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default()); + + // Insert attributes into the merged mesh + merged_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, merged_positions); + merged_mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, merged_uvs); + merged_mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, merged_normals); // Insert merged normals + merged_mesh.insert_indices(Indices::U32(merged_indices)); + + merged_mesh +} \ No newline at end of file diff --git a/client/src/plugins/environment/systems/voxels/structure.rs b/client/src/plugins/environment/systems/voxels/structure.rs new file mode 100644 index 0000000..5f244de --- /dev/null +++ b/client/src/plugins/environment/systems/voxels/structure.rs @@ -0,0 +1,85 @@ +use std::collections::{HashMap, HashSet, VecDeque}; +use bevy::color::Color; +use bevy::prelude::*; + + +/// Represents a single voxel with a color. +#[derive(Debug, Clone, Copy, Component, PartialEq, Default)] +pub struct Voxel { + pub color: Color, +} + +#[derive(Debug, Clone, Copy)] +pub struct DirtyVoxel { + pub position: Vec3, +} + +/// Represents a node in the sparse voxel octree. + +#[derive(Debug, Component, Clone)] +pub struct OctreeNode { + pub children: Option>, + pub voxel: Option, + pub is_leaf: bool, +} +/// Represents the root of the sparse voxel octree. +/// Represents the root of the sparse voxel octree. +#[derive(Debug, Component)] +pub struct SparseVoxelOctree { + + pub root: OctreeNode, + pub max_depth: u32, + pub size: f32, + pub show_wireframe: bool, + pub show_world_grid: bool, + pub show_chunks: bool, + + pub dirty: Vec, +} + +impl OctreeNode { + /// Creates a new empty octree node. + pub fn new() -> Self { + Self { + children: None, + voxel: None, + is_leaf: true, + } + } + + pub fn is_empty(&self) -> bool { + self.voxel.is_none() && self.children.is_none() + } +} + +impl Voxel { + /// Creates a new empty octree node. + pub fn new(color: Color) -> Self { + Self { + color, + } + } +} + + +pub const NEIGHBOR_OFFSETS: [(f32, f32, f32); 6] = [ + (-1.0, 0.0, 0.0), // Left + (1.0, 0.0, 0.0), // Right + (0.0, -1.0, 0.0), // Down + (0.0, 1.0, 0.0), // Up + (0.0, 0.0, -1.0), // Back + (0.0, 0.0, 1.0), // Front +]; + + +#[derive(Debug)] +pub struct Ray { + pub origin: Vec3, + pub direction: Vec3, +} + +#[derive(Clone)] +pub struct AABB { + pub min: Vec3, + pub max: Vec3, +} \ No newline at end of file diff --git a/trim.bat b/trim.bat index 1ad781b..1656fc3 100644 --- a/trim.bat +++ b/trim.bat @@ -3,8 +3,8 @@ rem combine_all.bat – merge every *.rs and *.toml in this tree setlocal enabledelayedexpansion rem Output files -set "OUT_RS=target/combined.rs" -set "OUT_TOML=target/combined.toml" +set "OUT_RS=target/combined.rs.out" +set "OUT_TOML=target/combined.toml.out" if exist "%OUT_RS%" del "%OUT_RS%" if exist "%OUT_TOML%" del "%OUT_TOML%"