Added Chuck System to the Voxel System plus quead rendering + selective rerender

This commit is contained in:
Elias Stepanik 2025-06-07 18:27:32 +02:00
parent bf1220f4a9
commit 2d258b02ed
9 changed files with 213 additions and 20 deletions

View File

@ -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::<ChunkQueue>()
.init_resource::<SpawnedChunks>()
// ------------------------------------------------------------------------
// 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
);
}
}

View File

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

View File

@ -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<Camera>>,
tree_q : Query<&SparseVoxelOctree>,
mut spawned : ResMut<SpawnedChunks>,
chunk_q : Query<&Chunk>,
cfg : Res<ChunkCullingCfg>,
) {
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();
}
}
}
}

View File

@ -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,
)
}
}
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,
)
}

View File

@ -5,4 +5,6 @@ pub mod structure;
mod chunk;
mod meshing;
pub mod render_chunks;
pub mod render_chunks;
pub mod culling;
pub mod queue_systems;

View File

@ -403,6 +403,8 @@ impl SparseVoxelOctree {
None
}
}

View File

@ -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<ChunkQueue>,
spawned : Res<SpawnedChunks>,
cfg : Res<ChunkCullingCfg>,
cam_q : Query<&GlobalTransform, With<Camera>>,
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 octrees dirty set
pub fn process_chunk_queue(
mut queue : ResMut<ChunkQueue>,
budget : Res<ChunkBudget>,
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; }
}
}

View File

@ -15,6 +15,7 @@ pub fn rebuild_dirty_chunks(
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
chunk_q: Query<(Entity, &Chunk)>,
mut spawned : ResMut<SpawnedChunks>,
root: Res<RootGrid>,
) {
// 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::<i64>::ZERO,
Chunk { key, voxels: Vec::new(), dirty: false },
));
)).id();
spawned.0.insert(key, e);
});
}
}

View File

@ -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<ChunkKey>);
/// map “which chunk key already has an entity in the world?”
#[derive(Resource, Default)]
pub struct SpawnedChunks(pub HashMap<ChunkKey, Entity>);
/// 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 } } }