mirror of
https://github.com/eliasstepanik/voxel-simulation.git
synced 2026-01-23 11:38:34 +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::app::{App, Plugin, PreStartup, PreUpdate, Startup};
|
||||||
use bevy::prelude::*;
|
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;
|
pub struct EnvironmentPlugin;
|
||||||
impl Plugin for EnvironmentPlugin {
|
impl Plugin for EnvironmentPlugin {
|
||||||
@ -16,20 +21,37 @@ impl Plugin for EnvironmentPlugin {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
app.add_systems(
|
app.insert_resource(ChunkCullingCfg { view_distance_chunks: 10 });
|
||||||
Update,
|
app.insert_resource(ChunkBudget { per_frame: 20 });
|
||||||
(
|
|
||||||
// old: voxels::rendering::render,
|
app
|
||||||
crate::plugins::environment::systems::voxels::render_chunks::rebuild_dirty_chunks,
|
// ------------------------------------------------------------------------
|
||||||
crate::plugins::environment::systems::voxels::debug::visualize_octree_system
|
// resources
|
||||||
.run_if(should_visualize_octree),
|
// ------------------------------------------------------------------------
|
||||||
crate::plugins::environment::systems::voxels::debug::draw_grid
|
.init_resource::<ChunkQueue>()
|
||||||
.run_if(should_draw_grid),
|
.init_resource::<SpawnedChunks>()
|
||||||
)
|
// ------------------------------------------------------------------------
|
||||||
.chain(),
|
// 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 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.
|
/// Component attached to the entity that owns the mesh of one chunk.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
@ -7,4 +7,33 @@ pub struct Chunk {
|
|||||||
pub key: ChunkKey,
|
pub key: ChunkKey,
|
||||||
pub voxels: Vec<(IVec3, Voxel)>, // local coords 0‥15
|
pub voxels: Vec<(IVec3, Voxel)>, // local coords 0‥15
|
||||||
pub dirty: bool,
|
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.
|
/// 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,
|
node: &OctreeNode,
|
||||||
x: f32,
|
x: f32,
|
||||||
y: 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.y + half) / scale).floor() as i32,
|
||||||
((pos.z + 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 chunk;
|
||||||
mod meshing;
|
mod meshing;
|
||||||
pub mod render_chunks;
|
pub mod render_chunks;
|
||||||
|
pub mod culling;
|
||||||
|
pub mod queue_systems;
|
||||||
@ -403,6 +403,8 @@ impl SparseVoxelOctree {
|
|||||||
|
|
||||||
None
|
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 meshes: ResMut<Assets<Mesh>>,
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
chunk_q: Query<(Entity, &Chunk)>,
|
chunk_q: Query<(Entity, &Chunk)>,
|
||||||
|
mut spawned : ResMut<SpawnedChunks>,
|
||||||
root: Res<RootGrid>,
|
root: Res<RootGrid>,
|
||||||
) {
|
) {
|
||||||
// map ChunkKey → entity
|
// map ChunkKey → entity
|
||||||
@ -71,15 +72,17 @@ pub fn rebuild_dirty_chunks(
|
|||||||
|
|
||||||
if let Some(&ent) = existing.get(&key) {
|
if let Some(&ent) = existing.get(&key) {
|
||||||
commands.entity(ent).insert(mesh_3d);
|
commands.entity(ent).insert(mesh_3d);
|
||||||
|
spawned.0.insert(key, ent);
|
||||||
} else {
|
} else {
|
||||||
commands.entity(root.0).with_children(|p| {
|
commands.entity(root.0).with_children(|p| {
|
||||||
p.spawn((
|
let e = p.spawn((
|
||||||
mesh_3d,
|
mesh_3d,
|
||||||
material,
|
material,
|
||||||
Transform::default(),
|
Transform::default(),
|
||||||
GridCell::<i64>::ZERO,
|
GridCell::<i64>::ZERO,
|
||||||
Chunk { key, voxels: Vec::new(), dirty: false },
|
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_SIZE: i32 = 16; // 16×16×16 voxels
|
||||||
|
pub const CHUNK_POW : u32 = 4;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
pub struct ChunkKey(pub i32, pub i32, pub i32);
|
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