diff --git a/client/src/plugins/environment/environment_plugin.rs b/client/src/plugins/environment/environment_plugin.rs index 310a041..1c93000 100644 --- a/client/src/plugins/environment/environment_plugin.rs +++ b/client/src/plugins/environment/environment_plugin.rs @@ -1,6 +1,11 @@ use bevy::app::{App, Plugin, PreStartup, PreUpdate, Startup}; use bevy::prelude::*; -use crate::plugins::environment::systems::voxels::structure::SparseVoxelOctree; +use crate::plugins::environment::systems::voxels::culling::{despawn_distant_chunks}; +use crate::plugins::environment::systems::voxels::debug::{draw_grid, visualize_octree_system}; +use crate::plugins::environment::systems::voxels::queue_systems; +use crate::plugins::environment::systems::voxels::queue_systems::{enqueue_visible_chunks, process_chunk_queue}; +use crate::plugins::environment::systems::voxels::render_chunks::rebuild_dirty_chunks; +use crate::plugins::environment::systems::voxels::structure::{ChunkBudget, ChunkCullingCfg, ChunkQueue, SparseVoxelOctree, SpawnedChunks}; pub struct EnvironmentPlugin; impl Plugin for EnvironmentPlugin { @@ -16,20 +21,37 @@ impl Plugin for EnvironmentPlugin { ), ); - app.add_systems( - Update, - ( - // old: voxels::rendering::render, - crate::plugins::environment::systems::voxels::render_chunks::rebuild_dirty_chunks, - crate::plugins::environment::systems::voxels::debug::visualize_octree_system - .run_if(should_visualize_octree), - crate::plugins::environment::systems::voxels::debug::draw_grid - .run_if(should_draw_grid), - ) - .chain(), - ); - + app.insert_resource(ChunkCullingCfg { view_distance_chunks: 10 }); + app.insert_resource(ChunkBudget { per_frame: 20 }); + + app + // ------------------------------------------------------------------------ + // resources + // ------------------------------------------------------------------------ + .init_resource::() + .init_resource::() + // ------------------------------------------------------------------------ + // frame update + // ------------------------------------------------------------------------ + .add_systems( + Update, + ( + /* ---------- culling & streaming ---------- */ + despawn_distant_chunks, // 1. remove too-far chunks + enqueue_visible_chunks.after(despawn_distant_chunks), // 2. find new visible ones + process_chunk_queue .after(enqueue_visible_chunks), // 3. spawn ≤ budget per frame + rebuild_dirty_chunks .after(process_chunk_queue), // 4. (re)mesh dirty chunks + /* ---------- optional debug drawing ------- */ + visualize_octree_system + .run_if(should_visualize_octree) + .after(rebuild_dirty_chunks), + draw_grid + .run_if(should_draw_grid) + .after(visualize_octree_system), + ) + .chain(), // make the whole tuple execute in this exact order + ); } } diff --git a/client/src/plugins/environment/systems/voxels/chunk.rs b/client/src/plugins/environment/systems/voxels/chunk.rs index ce5a426..23b7ef3 100644 --- a/client/src/plugins/environment/systems/voxels/chunk.rs +++ b/client/src/plugins/environment/systems/voxels/chunk.rs @@ -1,5 +1,5 @@ use bevy::prelude::*; -use crate::plugins::environment::systems::voxels::structure::{ChunkKey, Voxel}; +use crate::plugins::environment::systems::voxels::structure::{ChunkKey, SparseVoxelOctree, Voxel, CHUNK_POW, CHUNK_SIZE}; /// Component attached to the entity that owns the mesh of one chunk. #[derive(Component)] @@ -7,4 +7,33 @@ pub struct Chunk { pub key: ChunkKey, pub voxels: Vec<(IVec3, Voxel)>, // local coords 0‥15 pub dirty: bool, +} + + +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 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, + ); + + // depth of the octree node that exactly matches one chunk + let depth = self.max_depth.saturating_sub(CHUNK_POW); + + // normalised coordinates of that centre at the chosen depth + let norm = self.normalize_to_voxel_at_depth(centre, depth); + + // walk the tree down to that node … + if let Some(node) = + Self::get_node_at_depth(&self.root, norm.x, norm.y, norm.z, depth) + { + // … and ask whether that node or any child contains voxels + return self.has_volume(node); + } + false + } } \ No newline at end of file diff --git a/client/src/plugins/environment/systems/voxels/culling.rs b/client/src/plugins/environment/systems/voxels/culling.rs new file mode 100644 index 0000000..1c908b0 --- /dev/null +++ b/client/src/plugins/environment/systems/voxels/culling.rs @@ -0,0 +1,33 @@ +use std::collections::{HashMap, VecDeque}; +use bevy::prelude::*; +use crate::plugins::environment::systems::voxels::chunk::Chunk; +use crate::plugins::environment::systems::voxels::helper::world_to_chunk; +use crate::plugins::environment::systems::voxels::structure::{ChunkCullingCfg, ChunkKey, SparseVoxelOctree, SpawnedChunks, CHUNK_SIZE}; + + +/// despawn (or hide) every chunk entity whose centre is farther away than the +/// configured radius + +pub fn despawn_distant_chunks( + mut commands : Commands, + cam_q : Query<&GlobalTransform, With>, + tree_q : Query<&SparseVoxelOctree>, + mut spawned : ResMut, + chunk_q : Query<&Chunk>, + cfg : Res, +) { + let tree = tree_q.single(); + let cam = cam_q.single().translation(); + let center = world_to_chunk(tree, cam); + + for chunk in chunk_q.iter() { + let ChunkKey(x, y, z) = chunk.key; + if (x - center.0).abs() > cfg.view_distance_chunks || + (y - center.1).abs() > cfg.view_distance_chunks || + (z - center.2).abs() > cfg.view_distance_chunks { + if let Some(ent) = spawned.0.remove(&chunk.key) { + commands.entity(ent).despawn_recursive(); + } + } + } +} \ 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 index 0561557..6279d4f 100644 --- a/client/src/plugins/environment/systems/voxels/helper.rs +++ b/client/src/plugins/environment/systems/voxels/helper.rs @@ -133,7 +133,7 @@ impl SparseVoxelOctree { /// Helper function to recursively traverse the octree to a specific depth. - fn get_node_at_depth( + pub(crate) fn get_node_at_depth( node: &OctreeNode, x: f32, y: f32, @@ -256,4 +256,16 @@ pub(crate) fn chunk_key_from_world(tree: &SparseVoxelOctree, pos: Vec3) -> Chunk ((pos.y + half) / scale).floor() as i32, ((pos.z + half) / scale).floor() as i32, ) -} \ No newline at end of file +} + +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 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, + ) +} + diff --git a/client/src/plugins/environment/systems/voxels/mod.rs b/client/src/plugins/environment/systems/voxels/mod.rs index dace184..b16df94 100644 --- a/client/src/plugins/environment/systems/voxels/mod.rs +++ b/client/src/plugins/environment/systems/voxels/mod.rs @@ -5,4 +5,6 @@ pub mod structure; mod chunk; mod meshing; -pub mod render_chunks; \ No newline at end of file +pub mod render_chunks; +pub mod culling; +pub mod queue_systems; \ 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 index 1aea96b..f7ca585 100644 --- a/client/src/plugins/environment/systems/voxels/octree.rs +++ b/client/src/plugins/environment/systems/voxels/octree.rs @@ -403,6 +403,8 @@ impl SparseVoxelOctree { None } + + } diff --git a/client/src/plugins/environment/systems/voxels/queue_systems.rs b/client/src/plugins/environment/systems/voxels/queue_systems.rs new file mode 100644 index 0000000..14af3e5 --- /dev/null +++ b/client/src/plugins/environment/systems/voxels/queue_systems.rs @@ -0,0 +1,64 @@ +use bevy::prelude::*; +use crate::plugins::environment::systems::voxels::helper::world_to_chunk; +use crate::plugins::environment::systems::voxels::structure::*; + + + +/// enqueue chunks that *should* be visible but are not yet spawned +/// enqueue chunks that *should* be visible but are not yet spawned +pub fn enqueue_visible_chunks( + mut queue : ResMut, + spawned : Res, + cfg : Res, + cam_q : Query<&GlobalTransform, With>, + tree_q : Query<&SparseVoxelOctree>, +) { + let tree = tree_q.single(); + let cam_pos = cam_q.single().translation(); + let centre = world_to_chunk(tree, cam_pos); + let r = cfg.view_distance_chunks; + + // ------------------------------------------------------------------ + // 1. gather every *new* candidate chunk together with its distance² + // ------------------------------------------------------------------ + let mut candidates: Vec<(i32 /*dist²*/, ChunkKey)> = Vec::new(); + + for dx in -r..=r { + for dy in -r..=r { + for dz in -r..=r { + let key = ChunkKey(centre.0 + dx, centre.1 + dy, centre.2 + dz); + + if spawned.0.contains_key(&key) { continue; } // already spawned + if queue.0.contains(&key) { continue; } // already queued + if !tree.chunk_has_any_voxel(key) { continue; } // empty air + + let dist2 = dx*dx + dy*dy + dz*dz; // squared distance + candidates.push((dist2, key)); + } + } + } + + // ------------------------------------------------------------------ + // 2. sort by distance so nearest chunks enter the queue first + // ------------------------------------------------------------------ + candidates.sort_by_key(|&(d2, _)| d2); + + // push into FIFO queue in that order + for (_, key) in candidates { + queue.0.push_back(key); + } +} + +/// move a limited number of keys from the queue into the octree’s dirty set +pub fn process_chunk_queue( + mut queue : ResMut, + budget : Res, + mut tree_q : Query<&mut SparseVoxelOctree>, +) { + let mut tree = tree_q.single_mut(); + for _ in 0..budget.per_frame { + if let Some(key) = queue.0.pop_front() { + tree.dirty_chunks.insert(key); + } else { break; } + } +} \ No newline at end of file diff --git a/client/src/plugins/environment/systems/voxels/render_chunks.rs b/client/src/plugins/environment/systems/voxels/render_chunks.rs index 1068e09..8fe45ac 100644 --- a/client/src/plugins/environment/systems/voxels/render_chunks.rs +++ b/client/src/plugins/environment/systems/voxels/render_chunks.rs @@ -15,6 +15,7 @@ pub fn rebuild_dirty_chunks( mut meshes: ResMut>, mut materials: ResMut>, chunk_q: Query<(Entity, &Chunk)>, + mut spawned : ResMut, root: Res, ) { // map ChunkKey → entity @@ -71,15 +72,17 @@ pub fn rebuild_dirty_chunks( if let Some(&ent) = existing.get(&key) { commands.entity(ent).insert(mesh_3d); + spawned.0.insert(key, ent); } else { commands.entity(root.0).with_children(|p| { - p.spawn(( + let e = p.spawn(( mesh_3d, material, Transform::default(), GridCell::::ZERO, Chunk { key, voxels: Vec::new(), dirty: false }, - )); + )).id(); + spawned.0.insert(key, e); }); } } diff --git a/client/src/plugins/environment/systems/voxels/structure.rs b/client/src/plugins/environment/systems/voxels/structure.rs index 26b5d0b..618e902 100644 --- a/client/src/plugins/environment/systems/voxels/structure.rs +++ b/client/src/plugins/environment/systems/voxels/structure.rs @@ -86,6 +86,32 @@ pub struct AABB { } pub const CHUNK_SIZE: i32 = 16; // 16×16×16 voxels +pub const CHUNK_POW : u32 = 4; #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub struct ChunkKey(pub i32, pub i32, pub i32); + + +/// maximum amount of *new* chunk meshes we are willing to create each frame +#[derive(Resource)] +pub struct ChunkBudget { + pub per_frame: usize, +} +impl Default for ChunkBudget { + fn default() -> Self { + Self { per_frame: 4 } // tweak to taste + } +} + +/// FIFO queue with chunk keys that still need meshing +#[derive(Resource, Default)] +pub struct ChunkQueue(pub VecDeque); + +/// map “which chunk key already has an entity in the world?” +#[derive(Resource, Default)] +pub struct SpawnedChunks(pub HashMap); + +/// how big the cube around the player is, measured in chunks +#[derive(Resource)] +pub struct ChunkCullingCfg { pub view_distance_chunks: i32 } +impl Default for ChunkCullingCfg { fn default() -> Self { Self { view_distance_chunks: 6 } } } \ No newline at end of file