Merge pull request #18 from eliasstepanik/codex/add-octree-save/load-and-async-world-loading

Add octree save/load and sorted chunk queue
This commit is contained in:
Elias Stepanik 2025-06-09 21:45:05 +02:00 committed by GitHub
commit 124519c62a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 194 additions and 39 deletions

View File

@ -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"
@ -20,3 +20,5 @@ bitvec = "1.0.1"
smallvec = "1.14.0"
once_cell = "1.21.3"
rayon = "1.10.0"
bincode = "1.3"

View File

@ -71,7 +71,3 @@ 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
}

View File

@ -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<RootGrid>,
) {
// Octree parameters
let unit_size = 1.0_f32;
let octree_base_size = 64.0 * unit_size;
let octree_depth = 10;
// 1. Create octree and wrap in Arc<Mutex<>> 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 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();
generate_voxel_sphere(&mut octree, 110, color);
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),
);
// 4. Spawn entity with both Transform and the real octree component
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::<Vec<_>>()
})
})
.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,

View File

@ -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();

View File

@ -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};
@ -17,7 +20,6 @@ impl SparseVoxelOctree {
size,
show_wireframe,
show_world_grid,
show_chunks,
dirty: Vec::new(),
dirty_chunks: Default::default(),
occupied_chunks: Default::default(),
@ -458,7 +460,33 @@ impl SparseVoxelOctree {
None
}
/// Save the octree to a file using bincode serialization.
pub fn save_to_file<P: AsRef<Path>>(&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<P: AsRef<Path>>(path: P) -> io::Result<Self> {
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);
}
}
}

View File

@ -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 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 { continue; }
if spawned.0.contains_key(key) { continue; }
if queue.set.contains(key) { continue; }
queue.keys.push_back(*key);
queue.set.insert(*key);
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);
}
}

View File

@ -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<S>(color: &Color, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let [r, g, b, a] = color.to_linear().to_f32_array();
[r, g, b, a].serialize(serializer)
}
fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error>
where
D: Deserializer<'de>,
{
let arr: [f32; 4] = Deserialize::deserialize(deserializer)?;
Ok(Color::linear_rgba(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<Box<[OctreeNode; 8]>>,
pub voxel: Option<Voxel>,
@ -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, Clone)]
pub struct SparseVoxelOctree {
pub root: OctreeNode,
@ -32,10 +50,12 @@ pub struct SparseVoxelOctree {
pub size: f32,
pub show_wireframe: bool,
pub show_world_grid: bool,
pub show_chunks: bool,
#[serde(skip)]
pub dirty: Vec<DirtyVoxel>,
#[serde(skip)]
pub dirty_chunks: HashSet<ChunkKey>,
#[serde(skip)]
pub occupied_chunks: HashSet<ChunkKey>,
}
@ -101,7 +121,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);

View File

@ -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