From 78ee3483d7dc67022ec2be9b3b85522fa2a7b916 Mon Sep 17 00:00:00 2001 From: Elias Stepanik <40958815+eliasstepanik@users.noreply.github.com> Date: Mon, 9 Jun 2025 19:17:15 +0200 Subject: [PATCH 1/3] Add octree serialization and sorted chunk loading --- client/Cargo.toml | 3 +- .../environment/systems/voxels/octree.rs | 32 +++++++++++++++++-- .../systems/voxels/queue_systems.rs | 30 +++++++++++------ .../environment/systems/voxels/structure.rs | 29 ++++++++++++++--- 4 files changed, 78 insertions(+), 16 deletions(-) diff --git a/client/Cargo.toml b/client/Cargo.toml index 456e265..2ec4468 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -19,4 +19,5 @@ itertools = "0.13.0" bitvec = "1.0.1" smallvec = "1.14.0" once_cell = "1.21.3" -rayon = "1.10.0" \ No newline at end of file +rayon = "1.10.0" +bincode = "1.3" diff --git a/client/src/plugins/environment/systems/voxels/octree.rs b/client/src/plugins/environment/systems/voxels/octree.rs index da16ba0..1794ca3 100644 --- a/client/src/plugins/environment/systems/voxels/octree.rs +++ b/client/src/plugins/environment/systems/voxels/octree.rs @@ -1,4 +1,7 @@ use std::collections::{HashMap, HashSet}; +use std::path::Path; +use std::io; +use bincode; use bevy::asset::Assets; use bevy::color::Color; use bevy::math::{DQuat, DVec3}; @@ -457,8 +460,33 @@ impl SparseVoxelOctree { None } - - + /// Save the octree to a file using bincode serialization. + pub fn save_to_file>(&self, path: P) -> io::Result<()> { + let data = bincode::serialize(self) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + std::fs::write(path, data) + } + + /// Load an octree from a file and rebuild runtime caches. + pub fn load_from_file>(path: P) -> io::Result { + let bytes = std::fs::read(path)?; + let mut tree: Self = bincode::deserialize(&bytes) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + tree.rebuild_cache(); + Ok(tree) + } + + /// Rebuild runtime caches like occupied_chunks after loading. + pub fn rebuild_cache(&mut self) { + self.dirty.clear(); + self.dirty_chunks.clear(); + self.occupied_chunks.clear(); + let voxels = Self::collect_voxels_from_node(&self.root, self.size); + 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/queue_systems.rs b/client/src/plugins/environment/systems/voxels/queue_systems.rs index eb26aee..e08d08a 100644 --- a/client/src/plugins/environment/systems/voxels/queue_systems.rs +++ b/client/src/plugins/environment/systems/voxels/queue_systems.rs @@ -24,15 +24,27 @@ pub fn enqueue_visible_chunks( prev_cam.0 = Some(centre); let r = cfg.view_distance_chunks; - for key in &tree.occupied_chunks { - let dx = key.0 - centre.0; - let dy = key.1 - centre.1; - let dz = key.2 - centre.2; - if dx.abs() > r || dy.abs() > r || dz.abs() > r { continue; } - if spawned.0.contains_key(key) { continue; } - if queue.set.contains(key) { continue; } - queue.keys.push_back(*key); - queue.set.insert(*key); + + let mut keys: Vec<(ChunkKey, i32)> = tree + .occupied_chunks + .iter() + .filter_map(|key| { + let dx = key.0 - centre.0; + let dy = key.1 - centre.1; + let dz = key.2 - centre.2; + if dx.abs() > r || dy.abs() > r || dz.abs() > r { return None; } + if spawned.0.contains_key(key) { return None; } + Some((*key, dx*dx + dy*dy + dz*dz)) + }) + .collect(); + + keys.sort_by_key(|(_, d)| *d); + + queue.keys.clear(); + queue.set.clear(); + for (key, _) in keys { + queue.keys.push_back(key); + queue.set.insert(key); } } diff --git a/client/src/plugins/environment/systems/voxels/structure.rs b/client/src/plugins/environment/systems/voxels/structure.rs index d5e43eb..ab5057c 100644 --- a/client/src/plugins/environment/systems/voxels/structure.rs +++ b/client/src/plugins/environment/systems/voxels/structure.rs @@ -1,11 +1,29 @@ use std::collections::{HashMap, HashSet, VecDeque}; use bevy::color::Color; use bevy::prelude::*; +use serde::{Serialize, Deserialize, Serializer, Deserializer}; + +fn serialize_color(color: &Color, serializer: S) -> Result +where + S: Serializer, +{ + let [r, g, b, a] = color.as_linear_rgba_f32(); + [r, g, b, a].serialize(serializer) +} + +fn deserialize_color<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let arr: [f32; 4] = Deserialize::deserialize(deserializer)?; + Ok(Color::rgba_linear(arr[0], arr[1], arr[2], arr[3])) +} /// Represents a single voxel with a color. -#[derive(Debug, Clone, Copy, Component, PartialEq, Default)] +#[derive(Debug, Clone, Copy, Component, PartialEq, Default, Serialize, Deserialize)] pub struct Voxel { + #[serde(serialize_with = "serialize_color", deserialize_with = "deserialize_color")] pub color: Color, } @@ -16,7 +34,7 @@ pub struct DirtyVoxel { /// Represents a node in the sparse voxel octree. -#[derive(Debug, Component, Clone)] +#[derive(Debug, Component, Clone, Serialize, Deserialize)] pub struct OctreeNode { pub children: Option>, pub voxel: Option, @@ -24,7 +42,7 @@ pub struct OctreeNode { } /// Represents the root of the sparse voxel octree. /// Represents the root of the sparse voxel octree. -#[derive(Debug, Component)] +#[derive(Debug, Component, Serialize, Deserialize)] pub struct SparseVoxelOctree { pub root: OctreeNode, @@ -34,8 +52,11 @@ pub struct SparseVoxelOctree { pub show_world_grid: bool, pub show_chunks: bool, + #[serde(skip)] pub dirty: Vec, + #[serde(skip)] pub dirty_chunks: HashSet, + #[serde(skip)] pub occupied_chunks: HashSet, } @@ -101,7 +122,7 @@ pub struct Chunk { pub struct ChunkLod(pub u32); -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct ChunkKey(pub i32, pub i32, pub i32); From e406ac15cbe88f599aa70ca58946d99c2b0654a3 Mon Sep 17 00:00:00 2001 From: Elias Stepanik <40958815+eliasstepanik@users.noreply.github.com> Date: Mon, 9 Jun 2025 19:29:18 +0200 Subject: [PATCH 2/3] Fix color serialization and enable serde feature --- client/Cargo.toml | 3 ++- client/src/plugins/environment/systems/voxels/structure.rs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client/Cargo.toml b/client/Cargo.toml index 2ec4468..0de68ce 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -9,7 +9,7 @@ build = "build.rs" [dependencies] -bevy = { version = "0.15.1", features = ["jpeg", "trace_tracy", "trace_tracy_memory"] } +bevy = { version = "0.15.1", features = ["jpeg", "trace_tracy", "trace_tracy_memory", "serialize"] } rand = "0.8.5" serde = { version = "1.0", features = ["derive"] } toml = "0.8" @@ -21,3 +21,4 @@ smallvec = "1.14.0" once_cell = "1.21.3" rayon = "1.10.0" bincode = "1.3" + diff --git a/client/src/plugins/environment/systems/voxels/structure.rs b/client/src/plugins/environment/systems/voxels/structure.rs index ab5057c..06ab422 100644 --- a/client/src/plugins/environment/systems/voxels/structure.rs +++ b/client/src/plugins/environment/systems/voxels/structure.rs @@ -7,7 +7,7 @@ fn serialize_color(color: &Color, serializer: S) -> Result where S: Serializer, { - let [r, g, b, a] = color.as_linear_rgba_f32(); + let [r, g, b, a] = color.to_linear().to_f32_array(); [r, g, b, a].serialize(serializer) } @@ -16,7 +16,7 @@ where D: Deserializer<'de>, { let arr: [f32; 4] = Deserialize::deserialize(deserializer)?; - Ok(Color::rgba_linear(arr[0], arr[1], arr[2], arr[3])) + Ok(Color::linear_rgba(arr[0], arr[1], arr[2], arr[3])) } From 85699338a96630571fec99479c2c749c11edbf84 Mon Sep 17 00:00:00 2001 From: Elias Stepanik Date: Mon, 9 Jun 2025 21:44:47 +0200 Subject: [PATCH 3/3] Load and Unload System working --- .../plugins/environment/environment_plugin.rs | 4 - .../environment/systems/voxel_system.rs | 92 +++++++++++++++++-- .../plugins/environment/systems/voxels/lod.rs | 4 +- .../environment/systems/voxels/octree.rs | 2 +- .../environment/systems/voxels/structure.rs | 3 +- client/src/plugins/input/systems/voxels.rs | 33 ++++++- 6 files changed, 115 insertions(+), 23 deletions(-) diff --git a/client/src/plugins/environment/environment_plugin.rs b/client/src/plugins/environment/environment_plugin.rs index ffd533b..f6ae4e7 100644 --- a/client/src/plugins/environment/environment_plugin.rs +++ b/client/src/plugins/environment/environment_plugin.rs @@ -70,8 +70,4 @@ fn should_visualize_octree(octree_query: Query<&SparseVoxelOctree>,) -> bool { fn should_draw_grid(octree_query: Query<&SparseVoxelOctree>,) -> bool { octree_query.single().show_world_grid -} - -fn should_visualize_chunks(octree_query: Query<&SparseVoxelOctree>,) -> bool { - octree_query.single().show_chunks } \ No newline at end of file diff --git a/client/src/plugins/environment/systems/voxel_system.rs b/client/src/plugins/environment/systems/voxel_system.rs index 66948e5..72ed8da 100644 --- a/client/src/plugins/environment/systems/voxel_system.rs +++ b/client/src/plugins/environment/systems/voxel_system.rs @@ -1,32 +1,106 @@ +use std::path::Path; +use rayon::prelude::*; use crate::plugins::big_space::big_space_plugin::RootGrid; use crate::plugins::environment::systems::voxels::structure::*; use bevy::prelude::*; use bevy::render::mesh::*; use noise::{NoiseFn, Perlin}; +use rand::{thread_rng, Rng}; pub fn setup( mut commands: Commands, root: Res, ) { - let unit_size = 1.0_f32; + // Octree parameters + let unit_size = 1.0_f32; let octree_base_size = 64.0 * unit_size; - let octree_depth = 10; + let octree_depth = 10; - // 1. Create octree and wrap in Arc> for thread-safe generation - let mut octree = SparseVoxelOctree::new(octree_depth, octree_base_size, false, false, false); + let path = Path::new("octree.bin"); - // 2. Generate sphere in parallel, dropping the cloned Arc inside the function - let color = Color::rgb(0.2, 0.8, 0.2); - - generate_voxel_sphere(&mut octree, 110, color); - // 4. Spawn entity with both Transform and the real octree component + let mut octree = if path.exists() { + match SparseVoxelOctree::load_from_file(path) { + Ok(tree) => tree, + Err(err) => { + error!("failed to load octree: {err}"); + SparseVoxelOctree::new(octree_depth, octree_base_size, false, false, false) + } + } + } else { + let mut tree = SparseVoxelOctree::new(octree_depth, octree_base_size, false, false, false); + let color = Color::rgb(0.2, 0.8, 0.2); + // How many random spheres? + /*const NUM_SPHERES: usize = 5; + let mut rng = thread_rng(); + + for _ in 0..NUM_SPHERES { + let center = Vec3::new( + rng.gen_range(-1000.0..1000.0), + rng.gen_range(-1000.0..1000.0), + rng.gen_range(-1000.0..1000.0), + ); + + let radius = rng.gen_range(20..=150); // voxels + + generate_voxel_sphere_parallel(&mut tree, center, radius, color); + }*/ + + generate_voxel_sphere(&mut tree, 200, color); + tree + }; + + + + + // Attach octree to the scene graph commands.entity(root.0).with_children(|parent| { parent.spawn((Transform::default(), octree)); }); } +pub fn generate_voxel_sphere_parallel( + octree: &mut SparseVoxelOctree, + center: Vec3, + radius: i32, + color: Color, +) { + let step = octree.get_spacing_at_depth(octree.max_depth); + let radius_sq = radius * radius; + + // 1. Collect voxel positions in parallel + let voxels: Vec<(Vec3, Voxel)> = (-radius..=radius) + .into_par_iter() + .flat_map_iter(|ix| { + let dx2 = ix * ix; + (-radius..=radius).flat_map(move |iy| { + let dy2 = iy * iy; + let r2_xy = dx2 + dy2; + + if r2_xy > radius_sq { + return Vec::new(); // this (x,y) column is outside + } + + let max_z = ((radius_sq - r2_xy) as f32).sqrt() as i32; + (-max_z..=max_z).map(move |iz| { + let pos = Vec3::new( + center.x + ix as f32 * step, + center.y + iy as f32 * step, + center.z + iz as f32 * step, + ); + (pos, Voxel { color }) + }).collect::>() + }) + }) + .collect(); + + // 2. Single-threaded insert (keeps `SparseVoxelOctree` API unchanged) + for (pos, voxel) in voxels { + octree.insert(pos, voxel); + } +} + fn generate_voxel_sphere( octree: &mut SparseVoxelOctree, diff --git a/client/src/plugins/environment/systems/voxels/lod.rs b/client/src/plugins/environment/systems/voxels/lod.rs index f8360c5..df3577d 100644 --- a/client/src/plugins/environment/systems/voxels/lod.rs +++ b/client/src/plugins/environment/systems/voxels/lod.rs @@ -14,8 +14,8 @@ pub fn update_chunk_lods( // Borrow the octree only once to avoid repeated query lookups let mut tree = tree_q.single_mut(); - let max_depth = tree.max_depth; - let range_step = cfg.view_distance_chunks as f32 / max_depth as f32; + let max_depth = tree.max_depth - 1; + let range_step = cfg.view_distance_chunks as f32 / (max_depth as f32 - 1.0); let chunk_size = CHUNK_SIZE as f32 * tree.get_spacing_at_depth(max_depth); let mut changed = Vec::new(); diff --git a/client/src/plugins/environment/systems/voxels/octree.rs b/client/src/plugins/environment/systems/voxels/octree.rs index 1794ca3..be22069 100644 --- a/client/src/plugins/environment/systems/voxels/octree.rs +++ b/client/src/plugins/environment/systems/voxels/octree.rs @@ -20,7 +20,6 @@ impl SparseVoxelOctree { size, show_wireframe, show_world_grid, - show_chunks, dirty: Vec::new(), dirty_chunks: Default::default(), occupied_chunks: Default::default(), @@ -482,6 +481,7 @@ impl SparseVoxelOctree { self.dirty.clear(); self.dirty_chunks.clear(); self.occupied_chunks.clear(); + let voxels = Self::collect_voxels_from_node(&self.root, self.size); for (pos, _voxel, _depth) in voxels { let key = chunk_key_from_world(self, pos); diff --git a/client/src/plugins/environment/systems/voxels/structure.rs b/client/src/plugins/environment/systems/voxels/structure.rs index 06ab422..21457e1 100644 --- a/client/src/plugins/environment/systems/voxels/structure.rs +++ b/client/src/plugins/environment/systems/voxels/structure.rs @@ -42,7 +42,7 @@ pub struct OctreeNode { } /// Represents the root of the sparse voxel octree. /// Represents the root of the sparse voxel octree. -#[derive(Debug, Component, Serialize, Deserialize)] +#[derive(Debug, Component, Serialize, Deserialize, Clone)] pub struct SparseVoxelOctree { pub root: OctreeNode, @@ -50,7 +50,6 @@ pub struct SparseVoxelOctree { pub size: f32, pub show_wireframe: bool, pub show_world_grid: bool, - pub show_chunks: bool, #[serde(skip)] pub dirty: Vec, diff --git a/client/src/plugins/input/systems/voxels.rs b/client/src/plugins/input/systems/voxels.rs index cbc33ed..617f416 100644 --- a/client/src/plugins/input/systems/voxels.rs +++ b/client/src/plugins/input/systems/voxels.rs @@ -1,5 +1,7 @@ +use std::path::Path; use bevy::prelude::*; use crate::plugins::environment::systems::camera_system::CameraController; +use crate::plugins::environment::systems::voxels::octree; use crate::plugins::environment::systems::voxels::structure::*; ///TODO @@ -28,16 +30,37 @@ pub fn voxel_system( octree.show_world_grid = !octree.show_world_grid; } } - if keyboard_input.just_pressed(KeyCode::F4){ - for mut octree in octree_query.iter_mut() { - octree.show_chunks = !octree.show_chunks; - } - } + if keyboard_input.just_pressed(KeyCode::KeyQ) && window.cursor_options.visible == false{ for mut octree in octree_query.iter_mut() { octree.insert(transform.translation, Voxel::new(Color::srgb(1.0, 0.0, 0.0))); } } + if keyboard_input.just_pressed(KeyCode::F4){ + let path = Path::new("octree.bin"); + for octree in octree_query.iter() { + if let Err(e) = octree.save_to_file(path) { + error!("failed to save octree: {e}"); + } + } + } +/* if keyboard_input.just_pressed(KeyCode::F5){ + let path = Path::new("octree.bin"); + if path.exists() { + let path = Path::new("octree.bin"); + + let mut octree = if path.exists() { + match SparseVoxelOctree::load_from_file(path) { + Ok(tree) => tree, + Err(err) => { + error!("failed to load octree: {err}"); + } + } + } + + } + }*/ + // ======================= // 6) Building