diff --git a/client/src/plugins/environment/environment_plugin.rs b/client/src/plugins/environment/environment_plugin.rs index 1f6aa6d..5fbfb46 100644 --- a/client/src/plugins/environment/environment_plugin.rs +++ b/client/src/plugins/environment/environment_plugin.rs @@ -4,7 +4,9 @@ use crate::plugins::environment::systems::voxels::culling::{despawn_distant_chun 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}; + update_chunk_lods.after(process_chunk_queue), use crate::plugins::environment::systems::voxels::render_chunks::rebuild_dirty_chunks; +use crate::plugins::environment::systems::voxels::lod::update_chunk_lods; use crate::plugins::environment::systems::voxels::structure::{ChunkBudget, ChunkCullingCfg, ChunkQueue, SparseVoxelOctree, SpawnedChunks}; pub struct EnvironmentPlugin; @@ -40,6 +42,7 @@ impl Plugin for EnvironmentPlugin { 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 + update_chunk_lods.after(process_chunk_queue), rebuild_dirty_chunks .after(process_chunk_queue), // 4. (re)mesh dirty chunks /* ---------- optional debug drawing ------- */ diff --git a/client/src/plugins/environment/systems/voxels/helper.rs b/client/src/plugins/environment/systems/voxels/helper.rs index b865e17..bde4c9a 100644 --- a/client/src/plugins/environment/systems/voxels/helper.rs +++ b/client/src/plugins/environment/systems/voxels/helper.rs @@ -269,6 +269,16 @@ pub fn world_to_chunk(tree: &SparseVoxelOctree, p: Vec3) -> ChunkKey { ) } +pub fn chunk_center_world(tree: &SparseVoxelOctree, key: ChunkKey) -> Vec3 { + let half = tree.size * 0.5; + let step = tree.get_spacing_at_depth(tree.max_depth); + 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, + ) +} + impl AABB { pub fn intersects_aabb(&self, other: &AABB) -> bool { self.min.x <= other.max.x && diff --git a/client/src/plugins/environment/systems/voxels/lod.rs b/client/src/plugins/environment/systems/voxels/lod.rs new file mode 100644 index 0000000..9bc2819 --- /dev/null +++ b/client/src/plugins/environment/systems/voxels/lod.rs @@ -0,0 +1,42 @@ +use bevy::prelude::*; +use crate::plugins::environment::systems::voxels::helper::chunk_center_world; +use crate::plugins::environment::systems::voxels::structure::{Chunk, ChunkLod, ChunkCullingCfg, SparseVoxelOctree}; + +/// Update each chunk's LOD level based on its distance from the camera. +/// Chunks farther away get a higher LOD value (coarser mesh). +pub fn update_chunk_lods( + cam_q: Query<&GlobalTransform, With>, + mut chunks: Query<(&Chunk, &mut ChunkLod)>, + mut tree_q: Query<&mut SparseVoxelOctree>, + cfg: Res, +) { + let cam_pos = cam_q.single().translation(); + let max_depth; + let range_step; + { + let tree = tree_q.single(); + max_depth = tree.max_depth; + range_step = cfg.view_distance_chunks as f32 / max_depth as f32; + } + + let mut changed = Vec::new(); + for (chunk, mut lod) in chunks.iter_mut() { + let tree = tree_q.single(); + let center = chunk_center_world(&tree, chunk.key); + let dist_chunks = cam_pos.distance(center) + / (cfg.view_distance_chunks as f32); + let mut level = (dist_chunks / range_step).floor() as u32; + if level > max_depth { level = max_depth; } + if lod.0 != level { + lod.0 = level; + changed.push(chunk.key); + } + } + + if !changed.is_empty() { + let mut tree = tree_q.single_mut(); + for key in changed { + tree.dirty_chunks.insert(key); + } + } +} diff --git a/client/src/plugins/environment/systems/voxels/mod.rs b/client/src/plugins/environment/systems/voxels/mod.rs index b16df94..61b64e4 100644 --- a/client/src/plugins/environment/systems/voxels/mod.rs +++ b/client/src/plugins/environment/systems/voxels/mod.rs @@ -7,4 +7,5 @@ mod chunk; mod meshing; pub mod render_chunks; pub mod culling; -pub mod queue_systems; \ No newline at end of file +pub mod queue_systems; +pub mod lod; diff --git a/client/src/plugins/environment/systems/voxels/render_chunks.rs b/client/src/plugins/environment/systems/voxels/render_chunks.rs index bcdf2fd..a8edc65 100644 --- a/client/src/plugins/environment/systems/voxels/render_chunks.rs +++ b/client/src/plugins/environment/systems/voxels/render_chunks.rs @@ -18,15 +18,16 @@ pub fn rebuild_dirty_chunks( chunk_q : Query<(Entity, &Chunk, &Mesh3d, - &MeshMaterial3d)>, + &MeshMaterial3d, + &ChunkLod)>, mut spawned : ResMut, root : Res, ) { // map ChunkKey → (entity, mesh-handle, material-handle) - let existing: HashMap, Handle)> = + let existing: HashMap, Handle, u32)> = chunk_q .iter() - .map(|(e, c, m, mat)| (c.key, (e, m.0.clone(), mat.0.clone()))) + .map(|(e, c, m, mat, lod)| (c.key, (e, m.0.clone(), mat.0.clone(), lod.0))) .collect(); for mut tree in &mut octrees { @@ -37,6 +38,7 @@ pub fn rebuild_dirty_chunks( //------------------------------------------------ collect voxel data let mut bufs = Vec::new(); for key in tree.dirty_chunks.iter().copied() { + let lod = existing.get(&key).map(|v| v.3).unwrap_or(0); let mut buf = [[[None; CHUNK_SIZE as usize]; CHUNK_SIZE as usize]; CHUNK_SIZE as usize]; @@ -48,24 +50,40 @@ pub fn rebuild_dirty_chunks( key.2 as f32 * CHUNK_SIZE as f32 * step - half, ); - for lx in 0..CHUNK_SIZE { - for ly in 0..CHUNK_SIZE { - for lz in 0..CHUNK_SIZE { - let world = origin - + Vec3::new(lx as f32 * step, ly as f32 * step, lz as f32 * step); - if let Some(v) = tree.get_voxel_at_world_coords(world) { - buf[lx as usize][ly as usize][lz as usize] = Some(*v); + let mult = 1 << lod; + for gx in (0..CHUNK_SIZE).step_by(mult as usize) { + for gy in (0..CHUNK_SIZE).step_by(mult as usize) { + for gz in (0..CHUNK_SIZE).step_by(mult as usize) { + let center = origin + + Vec3::new( + (gx + mult / 2) as f32 * step, + (gy + mult / 2) as f32 * step, + (gz + mult / 2) as f32 * step, + ); + if let Some(v) = tree.get_voxel_at_world_coords(center) { + for lx in 0..mult { + for ly in 0..mult { + for lz in 0..mult { + let ix = gx + lx; + let iy = gy + ly; + let iz = gz + lz; + if ix < CHUNK_SIZE && iy < CHUNK_SIZE && iz < CHUNK_SIZE { + buf[ix as usize][iy as usize][iz as usize] = Some(*v); + } + } + } + } } } } } - bufs.push((key, buf, origin, step)); + bufs.push((key, buf, origin, step, lod)); } //------------------------------------------------ create / update - for (key, buf, origin, step) in bufs { - if let Some((ent, mesh_h, _mat_h)) = existing.get(&key).cloned() { + for (key, buf, origin, step, lod) in bufs { + if let Some((ent, mesh_h, _mat_h, _)) = existing.get(&key).cloned() { // update mesh in-place; keeps old asset id if let Some(mesh) = meshes.get_mut(&mesh_h) { *mesh = mesh_chunk(&buf, origin, step, &tree); @@ -84,6 +102,7 @@ pub fn rebuild_dirty_chunks( Transform::default(), GridCell::::ZERO, Chunk { key, voxels: Vec::new(), dirty: false }, + ChunkLod(lod), /*Wireframe,*/ )) .id(); diff --git a/client/src/plugins/environment/systems/voxels/structure.rs b/client/src/plugins/environment/systems/voxels/structure.rs index 9ebc72d..1c14a6c 100644 --- a/client/src/plugins/environment/systems/voxels/structure.rs +++ b/client/src/plugins/environment/systems/voxels/structure.rs @@ -93,9 +93,12 @@ pub struct Chunk { pub key: ChunkKey, pub voxels: Vec<(IVec3, Voxel)>, // local coords 0‥15 pub dirty: bool, - + } +#[derive(Component, Debug, Clone, Copy)] +pub struct ChunkLod(pub u32); + #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub struct ChunkKey(pub i32, pub i32, pub i32);