mirror of
https://github.com/eliasstepanik/voxel-simulation.git
synced 2026-01-11 05:48:29 +00:00
Added Chuck System to the Voxel System plus quead rendering + selective rerender
This commit is contained in:
parent
bf1220f4a9
commit
2d258b02ed
@ -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
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
33
client/src/plugins/environment/systems/voxels/culling.rs
Normal file
33
client/src/plugins/environment/systems/voxels/culling.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -403,6 +403,8 @@ impl SparseVoxelOctree {
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -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 octree’s 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; }
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 } } }
|
||||
Loading…
x
Reference in New Issue
Block a user