From c0e7dc34dfef9499cefd30319f640e22176e8153 Mon Sep 17 00:00:00 2001 From: Elias Stepanik <40958815+eliasstepanik@users.noreply.github.com> Date: Sun, 15 Jun 2025 14:58:50 +0200 Subject: [PATCH 1/3] optimize root expansion --- .../environment/systems/voxels/chunk.rs | 10 +-- .../environment/systems/voxels/debug.rs | 8 +- .../environment/systems/voxels/helper.rs | 49 ++++++------ .../environment/systems/voxels/octree.rs | 74 ++++++++++++++----- .../systems/voxels/render_chunks.rs | 6 +- .../environment/systems/voxels/structure.rs | 1 + 6 files changed, 89 insertions(+), 59 deletions(-) diff --git a/client/src/plugins/environment/systems/voxels/chunk.rs b/client/src/plugins/environment/systems/voxels/chunk.rs index 812809b..ab5d838 100644 --- a/client/src/plugins/environment/systems/voxels/chunk.rs +++ b/client/src/plugins/environment/systems/voxels/chunk.rs @@ -6,12 +6,12 @@ use crate::plugins::environment::systems::voxels::structure::{ChunkKey, SparseVo impl SparseVoxelOctree { pub fn chunk_has_any_voxel(&self, key: ChunkKey) -> bool { // world-space centre of the chunk - let step = self.get_spacing_at_depth(self.max_depth); - let half = self.size * 0.5; + let step = self.get_spacing_at_depth(self.max_depth); + let half = self.size * 0.5; let centre = Vec3::new( - (key.0 as f32 + 0.5) * CHUNK_SIZE as f32 * step - half, - (key.1 as f32 + 0.5) * CHUNK_SIZE as f32 * step - half, - (key.2 as f32 + 0.5) * CHUNK_SIZE as f32 * step - half, + self.center.x - half + (key.0 as f32 + 0.5) * CHUNK_SIZE as f32 * step, + self.center.y - half + (key.1 as f32 + 0.5) * CHUNK_SIZE as f32 * step, + self.center.z - half + (key.2 as f32 + 0.5) * CHUNK_SIZE as f32 * step, ); // depth of the octree node that exactly matches one chunk diff --git a/client/src/plugins/environment/systems/voxels/debug.rs b/client/src/plugins/environment/systems/voxels/debug.rs index 8ca6cd2..75d3f74 100644 --- a/client/src/plugins/environment/systems/voxels/debug.rs +++ b/client/src/plugins/environment/systems/voxels/debug.rs @@ -13,7 +13,7 @@ pub fn visualize_octree_system( // Draw a translucent cuboid for the root gizmos.cuboid( - Transform::from_translation(octree_tf.translation).with_scale(Vec3::splat(octree.size)), + Transform::from_translation(octree.center).with_scale(Vec3::splat(octree.size)), Color::srgba(1.0, 1.0, 0.0, 0.15), ); @@ -22,7 +22,7 @@ pub fn visualize_octree_system( visualize_recursive_center( &mut gizmos, &octree.root, - octree_tf.translation, // center of root in world + octree.center, // center of root in world octree.size, 0, octree.max_depth, @@ -103,9 +103,9 @@ pub fn draw_grid( }; let camera_pos = camera_tf.translation; - for (octree, octree_tf) in octree_query.iter() { + for (octree, _octree_tf) in octree_query.iter() { let half_size = octree.size * 0.5; - let root_center = octree_tf.translation; + let root_center = octree.center; // Voxel spacing at max depth let spacing = octree.get_spacing_at_depth(octree.max_depth); diff --git a/client/src/plugins/environment/systems/voxels/helper.rs b/client/src/plugins/environment/systems/voxels/helper.rs index bde4c9a..40170dc 100644 --- a/client/src/plugins/environment/systems/voxels/helper.rs +++ b/client/src/plugins/environment/systems/voxels/helper.rs @@ -28,8 +28,8 @@ impl SparseVoxelOctree { 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; + // Shift to [0, self.size] taking the octree centre into account + let shifted = (position - (self.center - 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. @@ -40,7 +40,7 @@ impl SparseVoxelOctree { 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) + voxel_center * self.size - Vec3::splat(half_size) + self.center } @@ -109,9 +109,9 @@ impl SparseVoxelOctree { 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) + (x >= self.center.x - half_size - eps && x < self.center.x + half_size + eps) + && (y >= self.center.y - half_size - eps && y < self.center.y + half_size + eps) + && (z >= self.center.z - half_size - eps && z < self.center.z + half_size + eps) } /// Retrieve a voxel at world coordinates by normalizing and looking up. @@ -122,12 +122,8 @@ impl SparseVoxelOctree { 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) + // Convert normalized coordinate to world space, accounting for the octree centre + (local_pos - Vec3::splat(0.5)) * self.size + self.center } @@ -248,24 +244,23 @@ pub fn face_orientation(dx: f32, dy: f32, dz: f32, voxel_size_f: f32) -> (Vec3, pub(crate) fn chunk_key_from_world(tree: &SparseVoxelOctree, pos: Vec3) -> ChunkKey { let half = tree.size * 0.5; - let step = tree.get_spacing_at_depth(tree.max_depth); - let scale = CHUNK_SIZE as f32 * step; // metres per chunk + let scale = CHUNK_SIZE as f32 * step; // metres per chunk ChunkKey( - ((pos.x + half) / scale).floor() as i32, - ((pos.y + half) / scale).floor() as i32, - ((pos.z + half) / scale).floor() as i32, + ((pos.x - tree.center.x + half) / scale).floor() as i32, + ((pos.y - tree.center.y + half) / scale).floor() as i32, + ((pos.z - tree.center.z + half) / scale).floor() as i32, ) } pub fn world_to_chunk(tree: &SparseVoxelOctree, p: Vec3) -> ChunkKey { - let step = tree.get_spacing_at_depth(tree.max_depth); - let half = tree.size * 0.5; + let step = tree.get_spacing_at_depth(tree.max_depth); + let half = tree.size * 0.5; let scale = CHUNK_SIZE as f32 * step; ChunkKey( - ((p.x + half) / scale).floor() as i32, - ((p.y + half) / scale).floor() as i32, - ((p.z + half) / scale).floor() as i32, + ((p.x - tree.center.x + half) / scale).floor() as i32, + ((p.y - tree.center.y + half) / scale).floor() as i32, + ((p.z - tree.center.z + half) / scale).floor() as i32, ) } @@ -273,9 +268,9 @@ pub fn chunk_center_world(tree: &SparseVoxelOctree, key: ChunkKey) -> Vec3 { let half = tree.size * 0.5; let step = tree.get_spacing_at_depth(tree.max_depth); Vec3::new( - (key.0 as f32 + 0.5) * CHUNK_SIZE as f32 * step - half, - (key.1 as f32 + 0.5) * CHUNK_SIZE as f32 * step - half, - (key.2 as f32 + 0.5) * CHUNK_SIZE as f32 * step - half, + tree.center.x - half + (key.0 as f32 + 0.5) * CHUNK_SIZE as f32 * step, + tree.center.y - half + (key.1 as f32 + 0.5) * CHUNK_SIZE as f32 * step, + tree.center.z - half + (key.2 as f32 + 0.5) * CHUNK_SIZE as f32 * step, ) } @@ -298,8 +293,8 @@ impl SparseVoxelOctree { pub fn collect_voxels_in_region(&self, min: Vec3, max: Vec3) -> Vec<(Vec3, Voxel)> { let half_size = self.size * 0.5; let root_bounds = AABB { - min: Vec3::new(-half_size, -half_size, -half_size), - max: Vec3::new(half_size, half_size, half_size), + min: self.center - Vec3::splat(half_size), + max: self.center + Vec3::splat(half_size), }; let mut voxels = Vec::new(); self.collect_voxels_in_region_recursive(&self.root, root_bounds, min, max, &mut voxels); diff --git a/client/src/plugins/environment/systems/voxels/octree.rs b/client/src/plugins/environment/systems/voxels/octree.rs index 481591e..c553ce5 100644 --- a/client/src/plugins/environment/systems/voxels/octree.rs +++ b/client/src/plugins/environment/systems/voxels/octree.rs @@ -26,6 +26,7 @@ impl SparseVoxelOctree { root: OctreeNode::new(), max_depth, size, + center: Vec3::ZERO, show_wireframe, show_world_grid, dirty: Vec::new(), @@ -125,9 +126,9 @@ impl SparseVoxelOctree { let step = self.get_spacing_at_depth(self.max_depth); let half = self.size * 0.5; - let gx = ((position.x + half) / step).floor() as i32; - let gy = ((position.y + half) / step).floor() as i32; - let gz = ((position.z + half) / step).floor() as i32; + let gx = ((position.x - self.center.x + half) / step).floor() as i32; + let gy = ((position.y - self.center.y + half) / step).floor() as i32; + let gz = ((position.z - self.center.z + half) / step).floor() as i32; let lx = gx - key.0 * CHUNK_SIZE; let ly = gy - key.1 * CHUNK_SIZE; @@ -272,32 +273,65 @@ impl SparseVoxelOctree { false } - fn expand_root(&mut self, _x: f32, _y: f32, _z: f32) { + 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. + // Save the old root + let old_root = std::mem::replace(&mut self.root, OctreeNode::new()); + let old_center = self.center; + let old_size = self.size; + let half = old_size * 0.5; + + // Determine in which direction to expand along each axis + let mut shift = Vec3::ZERO; + if x >= old_center.x { + shift.x = half; + } else { + shift.x = -half; + } + if y >= old_center.y { + shift.y = half; + } else { + shift.y = -half; + } + if z >= old_center.z { + shift.z = half; + } else { + shift.z = -half; + } + + // New center after expansion + self.center += shift; 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); + // Determine which child the old root should occupy + let mut index = 0usize; + if shift.x < 0.0 { + index |= 1; } + if shift.y < 0.0 { + index |= 2; + } + if shift.z < 0.0 { + index |= 4; + } + + let mut children = Box::new(core::array::from_fn(|_| OctreeNode::new())); + children[index] = old_root; + self.root.children = Some(children); + self.root.is_leaf = false; } /// 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)> { + fn collect_voxels_from_node(node: &OctreeNode, old_size: f32, center: Vec3) -> 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, + center.x - old_size / 2.0, + center.y - old_size / 2.0, + center.z - old_size / 2.0, old_size, 0, &mut voxels, @@ -440,7 +474,7 @@ impl SparseVoxelOctree { // 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); + let neighbor_world = neighbor * self.size - Vec3::splat(half_size) + self.center; if !self.contains(neighbor_world.x, neighbor_world.y, neighbor_world.z) { return false; @@ -454,8 +488,8 @@ impl SparseVoxelOctree { // 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), + min: self.center - Vec3::splat(half_size), + max: self.center + Vec3::splat(half_size), }; self.raycast_recursive(&self.root, ray, &root_bounds, 0) } @@ -537,7 +571,7 @@ impl SparseVoxelOctree { self.dirty_chunks.clear(); self.occupied_chunks.clear(); - let voxels = Self::collect_voxels_from_node(&self.root, self.size); + let voxels = Self::collect_voxels_from_node(&self.root, self.size, self.center); for (pos, _voxel, _depth) in voxels { let key = chunk_key_from_world(self, pos); self.occupied_chunks.insert(key); diff --git a/client/src/plugins/environment/systems/voxels/render_chunks.rs b/client/src/plugins/environment/systems/voxels/render_chunks.rs index cc2573d..d349e87 100644 --- a/client/src/plugins/environment/systems/voxels/render_chunks.rs +++ b/client/src/plugins/environment/systems/voxels/render_chunks.rs @@ -55,9 +55,9 @@ pub fn rebuild_dirty_chunks( let half = tree_ref.size * 0.5; let step = tree_ref.get_spacing_at_depth(tree_ref.max_depth); let origin = Vec3::new( - key.0 as f32 * CHUNK_SIZE as f32 * step - half, - key.1 as f32 * CHUNK_SIZE as f32 * step - half, - key.2 as f32 * CHUNK_SIZE as f32 * step - half, + tree_ref.center.x - half + key.0 as f32 * CHUNK_SIZE as f32 * step, + tree_ref.center.y - half + key.1 as f32 * CHUNK_SIZE as f32 * step, + tree_ref.center.z - half + key.2 as f32 * CHUNK_SIZE as f32 * step, ); let mult = 1 << lod; diff --git a/client/src/plugins/environment/systems/voxels/structure.rs b/client/src/plugins/environment/systems/voxels/structure.rs index 215ee09..46bb2d9 100644 --- a/client/src/plugins/environment/systems/voxels/structure.rs +++ b/client/src/plugins/environment/systems/voxels/structure.rs @@ -38,6 +38,7 @@ pub struct SparseVoxelOctree { pub root: OctreeNode, pub max_depth: u32, pub size: f32, + pub center: Vec3, pub show_wireframe: bool, pub show_world_grid: bool, From bd266e533d68e8680bb62b32f4ef59a8ec44695b Mon Sep 17 00:00:00 2001 From: Elias Stepanik <40958815+eliasstepanik@users.noreply.github.com> Date: Sun, 15 Jun 2025 15:11:33 +0200 Subject: [PATCH 2/3] Fix root expansion shifting --- .../environment/systems/voxels/octree.rs | 39 +++++-------------- 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/client/src/plugins/environment/systems/voxels/octree.rs b/client/src/plugins/environment/systems/voxels/octree.rs index c553ce5..7cfcc4c 100644 --- a/client/src/plugins/environment/systems/voxels/octree.rs +++ b/client/src/plugins/environment/systems/voxels/octree.rs @@ -278,45 +278,24 @@ impl SparseVoxelOctree { // Save the old root let old_root = std::mem::replace(&mut self.root, OctreeNode::new()); - let old_center = self.center; - let old_size = self.size; - let half = old_size * 0.5; - // Determine in which direction to expand along each axis - let mut shift = Vec3::ZERO; - if x >= old_center.x { - shift.x = half; - } else { - shift.x = -half; - } - if y >= old_center.y { - shift.y = half; - } else { - shift.y = -half; - } - if z >= old_center.z { - shift.z = half; - } else { - shift.z = -half; - } - - // New center after expansion - self.center += shift; - self.size *= 2.0; - self.max_depth += 1; - - // Determine which child the old root should occupy + // Determine which child of the new root the previous root should occupy. + // The root's center remains unchanged; only its size doubles so that all + // existing voxels stay in place. let mut index = 0usize; - if shift.x < 0.0 { + if x < self.center.x { index |= 1; } - if shift.y < 0.0 { + if y < self.center.y { index |= 2; } - if shift.z < 0.0 { + if z < self.center.z { index |= 4; } + self.size *= 2.0; + self.max_depth += 1; + let mut children = Box::new(core::array::from_fn(|_| OctreeNode::new())); children[index] = old_root; self.root.children = Some(children); From bca9ab261d8ff41bf17348c486175562fc129ee6 Mon Sep 17 00:00:00 2001 From: Elias Stepanik <40958815+eliasstepanik@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:36:35 +0200 Subject: [PATCH 3/3] Improve octree root expansion --- .../environment/systems/voxels/octree.rs | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/client/src/plugins/environment/systems/voxels/octree.rs b/client/src/plugins/environment/systems/voxels/octree.rs index 7cfcc4c..0ebdb77 100644 --- a/client/src/plugins/environment/systems/voxels/octree.rs +++ b/client/src/plugins/environment/systems/voxels/octree.rs @@ -273,33 +273,46 @@ impl SparseVoxelOctree { false } + /// Grow the octree so that the given world-space point fits within the root. + /// The previous root becomes a child of the new root without re-inserting every voxel. fn expand_root(&mut self, x: f32, y: f32, z: f32) { info!("Root expanding ..."); - // Save the old root let old_root = std::mem::replace(&mut self.root, OctreeNode::new()); + let old_center = self.center; + let half = self.size * 0.5; - // Determine which child of the new root the previous root should occupy. - // The root's center remains unchanged; only its size doubles so that all - // existing voxels stay in place. - let mut index = 0usize; - if x < self.center.x { - index |= 1; + // Determine the direction to shift the center. The old root occupies the opposite child. + let mut child_index = 0usize; + if x >= old_center.x { + self.center.x += half; + } else { + self.center.x -= half; + child_index |= 1; } - if y < self.center.y { - index |= 2; + if y >= old_center.y { + self.center.y += half; + } else { + self.center.y -= half; + child_index |= 2; } - if z < self.center.z { - index |= 4; + if z >= old_center.z { + self.center.z += half; + } else { + self.center.z -= half; + child_index |= 4; } self.size *= 2.0; self.max_depth += 1; let mut children = Box::new(core::array::from_fn(|_| OctreeNode::new())); - children[index] = old_root; + children[child_index] = old_root; self.root.children = Some(children); self.root.is_leaf = false; + + // Rebuild caches so chunk bookkeeping stays consistent with the new center. + self.rebuild_cache(); } /// Helper: Collect all voxels from a given octree node recursively.