Working Chuncked Voxel system.

This commit is contained in:
Elias Stepanik 2025-06-07 17:21:05 +02:00
parent 39b7c7cf41
commit 8641b57ca4
9 changed files with 453 additions and 211 deletions

View File

@ -16,8 +16,18 @@ impl Plugin for EnvironmentPlugin {
),
);
app.add_systems(Update, (crate::plugins::environment::systems::voxels::rendering::render,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.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(),
);

View File

@ -0,0 +1,10 @@
use bevy::prelude::*;
use crate::plugins::environment::systems::voxels::structure::{ChunkKey, Voxel};
/// Component attached to the entity that owns the mesh of one chunk.
#[derive(Component)]
pub struct Chunk {
pub key: ChunkKey,
pub voxels: Vec<(IVec3, Voxel)>, // local coords 0‥15
pub dirty: bool,
}

View File

@ -245,3 +245,15 @@ pub fn face_orientation(dx: f32, dy: f32, dz: f32, voxel_size_f: f32) -> (Vec3,
}
}
}
pub(crate) fn chunk_key_from_world(tree: &SparseVoxelOctree, pos: Vec3) -> ChunkKey {
let half = tree.size * 0.5;
let step = tree.get_spacing_at_depth(tree.max_depth);
let scale = CHUNK_SIZE as f32 * step; // metres per chunk
ChunkKey(
((pos.x + half) / scale).floor() as i32,
((pos.y + half) / scale).floor() as i32,
((pos.z + half) / scale).floor() as i32,
)
}

View File

@ -0,0 +1,299 @@
use bevy::asset::RenderAssetUsages;
use bevy::prelude::*;
use bevy::render::mesh::{Indices, PrimitiveTopology, VertexAttributeValues, Mesh};
use crate::plugins::environment::systems::voxels::structure::*;
pub(crate) fn mesh_chunk(
buffer: &[[[Option<Voxel>; CHUNK_SIZE as usize]; CHUNK_SIZE as usize]; CHUNK_SIZE as usize],
origin: Vec3,
step: f32,
tree: &SparseVoxelOctree,
) -> Mesh {
let mut positions = Vec::<[f32; 3]>::new();
let mut normals = Vec::<[f32; 3]>::new();
let mut uvs = Vec::<[f32; 2]>::new();
let mut indices = Vec::<u32>::new();
// helper safe test for a filled voxel
let filled = |x: i32, y: i32, z: i32| -> bool {
if (0..CHUNK_SIZE).contains(&x)
&& (0..CHUNK_SIZE).contains(&y)
&& (0..CHUNK_SIZE).contains(&z)
{
buffer[x as usize][y as usize][z as usize].is_some()
} else {
let world = origin + Vec3::new(x as f32 * step,
y as f32 * step,
z as f32 * step);
tree.get_voxel_at_world_coords(world).is_some()
}
};
// push a single quad
let mut quad = |base: Vec3,
size: Vec2,
n: Vec3, // face normal (-1|+1 on one axis)
u: Vec3,
v: Vec3|
{
let i0 = positions.len() as u32;
// 4 vertices -----------------------------------------------------------
positions.extend_from_slice(&[
(base).into(),
(base + u * size.x).into(),
(base + u * size.x + v * size.y).into(),
(base + v * size.y).into(),
]);
normals.extend_from_slice(&[[n.x, n.y, n.z]; 4]);
uvs .extend_from_slice(&[[0.0,1.0],[1.0,1.0],[1.0,0.0],[0.0,0.0]]);
// indices -- flip for the negative-side faces -------------------------
if n.x + n.y + n.z >= 0.0 {
// CCW (front-face)
indices.extend_from_slice(&[i0, i0 + 1, i0 + 2, i0 + 2, i0 + 3, i0]);
} else {
// CW → reverse two vertices so that the winding becomes CCW again
indices.extend_from_slice(&[i0, i0 + 3, i0 + 2, i0 + 2, i0 + 1, i0]);
}
};
//-----------------------------------------------------------------------
// Zfaces
//-----------------------------------------------------------------------
for z in 0..CHUNK_SIZE { // -Z faces (normal Z)
let nz = -1;
let voxel_z = z;
let neighbour_z = voxel_z as i32 + nz;
for y in 0..CHUNK_SIZE {
let mut x = 0;
while x < CHUNK_SIZE {
if filled(x, y, voxel_z) && !filled(x, y, neighbour_z) {
// greedy run along +X
let run_start = x;
let mut run = 1;
while x + run < CHUNK_SIZE
&& filled(x + run, y, voxel_z)
&& !filled(x + run, y, neighbour_z)
{
run += 1;
}
let face_z = voxel_z as f32 * step + if nz == 1 { step } else { 0.0 };
let world_base = origin + Vec3::new(run_start as f32 * step, y as f32 * step, face_z);
quad(world_base,
Vec2::new(run as f32 * step, step),
Vec3::new(0.0, 0.0, nz as f32),
Vec3::X,
Vec3::Y);
x += run;
} else {
x += 1;
}
}
}
}
// ------ 2nd pass : +Z faces ---------------------------------------------
for z in 0..CHUNK_SIZE { // +Z faces (normal +Z)
let nz = 1;
let voxel_z = z; // this voxel
let neighbour_z = voxel_z as i32 + nz; // cell “in front of it”
for y in 0..CHUNK_SIZE {
let mut x = 0;
while x < CHUNK_SIZE {
if filled(x, y, voxel_z) && !filled(x, y, neighbour_z) {
let run_start = x;
let mut run = 1;
while x + run < CHUNK_SIZE
&& filled(x + run, y, voxel_z)
&& !filled(x + run, y, neighbour_z)
{ run += 1; }
let world_base = origin
+ Vec3::new(run_start as f32 * step,
y as f32 * step,
(voxel_z + 1) as f32 * step); // +1 !
quad(world_base,
Vec2::new(run as f32 * step, step),
Vec3::new(0.0, 0.0, 1.0), // +Z
Vec3::X,
Vec3::Y);
x += run;
} else {
x += 1;
}
}
}
}
// ────────────────────────────────────────────────────────────────────────────
// X faces (-X pass … original code)
// ────────────────────────────────────────────────────────────────────────────
for x in 0..CHUNK_SIZE { // -X faces (normal X)
let nx = -1;
let voxel_x = x;
let neighbour_x = voxel_x as i32 + nx;
for z in 0..CHUNK_SIZE {
let mut y = 0;
while y < CHUNK_SIZE {
if filled(voxel_x, y, z) && !filled(neighbour_x, y, z) {
let run_start = y;
let mut run = 1;
while y + run < CHUNK_SIZE
&& filled(voxel_x, y + run, z)
&& !filled(neighbour_x, y + run, z)
{ run += 1; }
// **fixed x-coordinate: add step when nx == +1**
let face_x = voxel_x as f32 * step + if nx == 1 { step } else { 0.0 };
let world_base = origin
+ Vec3::new(face_x,
run_start as f32 * step,
z as f32 * step);
quad(world_base,
Vec2::new(run as f32 * step, step),
Vec3::new(nx as f32, 0.0, 0.0),
Vec3::Y,
Vec3::Z);
y += run;
} else {
y += 1;
}
}
}
}
// ------ 2nd pass : +X faces ---------------------------------------------
for x in 0..CHUNK_SIZE { // +X faces (normal +X)
let nx = 1;
let voxel_x = x;
let neighbour_x = voxel_x as i32 + nx;
for z in 0..CHUNK_SIZE {
let mut y = 0;
while y < CHUNK_SIZE {
if filled(voxel_x, y, z) && !filled(neighbour_x, y, z) {
let run_start = y;
let mut run = 1;
while y + run < CHUNK_SIZE
&& filled(voxel_x, y + run, z)
&& !filled(neighbour_x, y + run, z)
{ run += 1; }
let world_base = origin
+ Vec3::new((voxel_x + 1) as f32 * step, // +1 !
run_start as f32 * step,
z as f32 * step);
quad(world_base,
Vec2::new(run as f32 * step, step),
Vec3::new(1.0, 0.0, 0.0), // +X
Vec3::Y,
Vec3::Z);
y += run;
} else {
y += 1;
}
}
}
}
// ────────────────────────────────────────────────────────────────────────────
// Y faces (-Y pass … original code)
// ────────────────────────────────────────────────────────────────────────────
for y in 0..CHUNK_SIZE { // -Y faces (normal Y)
let ny = -1;
let voxel_y = y;
let neighbour_y = voxel_y as i32 + ny;
for x in 0..CHUNK_SIZE {
let mut z = 0;
while z < CHUNK_SIZE {
if filled(x, voxel_y, z) && !filled(x, neighbour_y, z) {
let run_start = z;
let mut run = 1;
while z + run < CHUNK_SIZE
&& filled(x, voxel_y, z + run)
&& !filled(x, neighbour_y, z + run)
{ run += 1; }
// **fixed y-coordinate: add step when ny == +1**
let face_y = voxel_y as f32 * step + if ny == 1 { step } else { 0.0 };
let world_base = origin
+ Vec3::new(x as f32 * step,
face_y,
run_start as f32 * step);
quad(world_base,
Vec2::new(run as f32 * step, step),
Vec3::new(0.0, ny as f32, 0.0),
Vec3::Z,
Vec3::X);
z += run;
} else {
z += 1;
}
}
}
}
// ------ 2nd pass : +Y faces ---------------------------------------------
for y in 0..CHUNK_SIZE { // +Y faces (normal +Y)
let ny = 1;
let voxel_y = y;
let neighbour_y = voxel_y as i32 + ny;
for x in 0..CHUNK_SIZE {
let mut z = 0;
while z < CHUNK_SIZE {
if filled(x, voxel_y, z) && !filled(x, neighbour_y, z) {
let run_start = z;
let mut run = 1;
while z + run < CHUNK_SIZE
&& filled(x, voxel_y, z + run)
&& !filled(x, neighbour_y, z + run)
{ run += 1; }
let world_base = origin
+ Vec3::new(x as f32 * step,
(voxel_y + 1) as f32 * step, // +1 !
run_start as f32 * step);
quad(world_base,
Vec2::new(run as f32 * step, step),
Vec3::new(0.0, 1.0, 0.0), // +Y
Vec3::Z,
Vec3::X);
z += run;
} else {
z += 1;
}
}
}
}
//-----------------------------------------------------------------------
// build final mesh
//-----------------------------------------------------------------------
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, VertexAttributeValues::Float32x3(positions));
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, VertexAttributeValues::Float32x3(normals));
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, VertexAttributeValues::Float32x2(uvs));
mesh.insert_indices(Indices::U32(indices));
mesh
}

View File

@ -2,4 +2,7 @@ pub mod debug;
pub mod helper;
pub mod octree;
pub mod structure;
pub mod rendering;
mod chunk;
mod meshing;
pub mod render_chunks;

View File

@ -5,6 +5,7 @@ use bevy::math::{DQuat, DVec3};
use bevy::prelude::*;
use bevy::render::mesh::{Indices, PrimitiveTopology, VertexAttributeValues};
use bevy::render::render_asset::RenderAssetUsages;
use crate::plugins::environment::systems::voxels::helper::chunk_key_from_world;
use crate::plugins::environment::systems::voxels::structure::{DirtyVoxel, OctreeNode, Ray, SparseVoxelOctree, Voxel, AABB, NEIGHBOR_OFFSETS};
impl SparseVoxelOctree {
@ -18,6 +19,7 @@ impl SparseVoxelOctree {
show_world_grid,
show_chunks,
dirty: Vec::new(),
dirty_chunks: Default::default(),
}
}
pub fn insert(&mut self, position: Vec3, voxel: Voxel) {
@ -36,7 +38,9 @@ impl SparseVoxelOctree {
let dirty_voxel = DirtyVoxel{
position: aligned,
};
self.dirty.push(dirty_voxel);
self.dirty_chunks.insert(chunk_key_from_world(self, position));
Self::insert_recursive(&mut self.root, aligned, voxel, self.max_depth);
@ -80,12 +84,23 @@ impl SparseVoxelOctree {
pub fn remove(&mut self, position: Vec3) {
let aligned = self.normalize_to_voxel_at_depth(position, self.max_depth);
let dirty_voxel = DirtyVoxel{
position: aligned,
};
self.dirty.push(dirty_voxel);
self.dirty.push(DirtyVoxel { position: aligned });
Self::remove_recursive(&mut self.root, aligned.x, aligned.y, aligned.z, self.max_depth);
// mark the chunk
self.dirty_chunks.insert(chunk_key_from_world(self, position));
Self::remove_recursive(
&mut self.root,
aligned.x,
aligned.y,
aligned.z,
self.max_depth,
);
}
pub fn clear_dirty_flags(&mut self) {
self.dirty.clear();
self.dirty_chunks.clear();
}
fn remove_recursive(

View File

@ -0,0 +1,89 @@
use std::collections::HashMap;
use std::fmt::format;
use bevy::prelude::*;
use bevy::render::mesh::Mesh;
use big_space::prelude::GridCell;
use itertools::Itertools;
use crate::plugins::big_space::big_space_plugin::RootGrid;
use crate::plugins::environment::systems::voxels::chunk::Chunk;
use crate::plugins::environment::systems::voxels::meshing::mesh_chunk;
use crate::plugins::environment::systems::voxels::structure::*;
/// rebuilds meshes only for chunks flagged dirty by the octree
pub fn rebuild_dirty_chunks(
mut commands: Commands,
mut octrees: Query<(Entity, &mut SparseVoxelOctree)>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
chunk_q: Query<(Entity, &Chunk)>,
root: Res<RootGrid>,
) {
// map ChunkKey → entity
let existing: HashMap<ChunkKey, Entity> =
chunk_q.iter().map(|(e, c)| (c.key, e)).collect();
for (_tree_ent, mut tree) in &mut octrees {
if tree.dirty_chunks.is_empty() {
continue;
}
// gather voxel data for every dirty chunk
let mut chunk_voxel_bufs: Vec<(
ChunkKey,
[[[Option<Voxel>; CHUNK_SIZE as usize]; CHUNK_SIZE as usize]; CHUNK_SIZE as usize],
Vec3, // chunk origin
f32, // voxel step
)> = Vec::new();
for key in tree.dirty_chunks.iter().copied() {
let mut buf =
[[[None; CHUNK_SIZE as usize]; CHUNK_SIZE as usize]; CHUNK_SIZE as usize];
let half = tree.size * 0.5;
let step = tree.get_spacing_at_depth(tree.max_depth);
let start = Vec3::new(
key.0 as f32 * CHUNK_SIZE as f32 * step - half,
key.1 as f32 * CHUNK_SIZE as f32 * step - half,
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 = start
+ 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);
}
}
}
}
chunk_voxel_bufs.push((key, buf, start, step));
}
// build / replace meshes
for (key, buf, origin, step) in chunk_voxel_bufs {
let mesh_handle =
meshes.add(mesh_chunk(&buf, origin, step, &tree));
let mesh_3d = Mesh3d::from(mesh_handle);
let material = MeshMaterial3d::<StandardMaterial>::default();
if let Some(&ent) = existing.get(&key) {
commands.entity(ent).insert(mesh_3d);
} else {
commands.entity(root.0).with_children(|p| {
p.spawn((
mesh_3d,
material,
Transform::default(),
GridCell::<i64>::ZERO,
Chunk { key, voxels: Vec::new(), dirty: false },
));
});
}
}
tree.clear_dirty_flags();
}
}

View File

@ -1,202 +0,0 @@
use bevy::asset::RenderAssetUsages;
use bevy::prelude::*;
use bevy::render::mesh::*;
use bevy::render::render_resource::*;
use big_space::prelude::GridCell;
use crate::plugins::big_space::big_space_plugin::RootGrid;
use crate::plugins::environment::systems::voxels::structure::*;
#[derive(Component)]
pub struct VoxelTerrainMarker {}
pub fn render(
mut commands: Commands,
mut query: Query<&mut SparseVoxelOctree>,
render_object_query: Query<Entity, With<VoxelTerrainMarker>>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
root: Res<RootGrid>,
) {
for mut octree in query.iter_mut() {
// Only update when marked dirty
if !octree.dirty.is_empty() {
// Remove old render objects
for entity in render_object_query.iter() {
info!("Despawning {}", entity);
commands.entity(entity).despawn_recursive();
}
// Get the voxel centers (world positions), color, and depth.
let voxels = octree.traverse();
// Debug: Log the number of voxels traversed.
info!("Voxel count: {}", voxels.len());
let mut voxel_meshes = Vec::new();
for (world_position, _color, depth) in voxels {
// Get the size of the voxel at the current depth.
let voxel_size = octree.get_spacing_at_depth(depth);
// The traverse method already returns the voxel center in world space.
// For each neighbor direction, check if this voxel face is exposed.
for &(dx, dy, dz) in NEIGHBOR_OFFSETS.iter() {
// Pass the world-space voxel center directly.
if !octree.has_neighbor(world_position, dx as i32, dy as i32, dz as i32, depth) {
// Determine face normal and the local offset for the face.
let (normal, offset) = match (dx, dy, dz) {
(-1.0, 0.0, 0.0) => (
Vec3::new(-1.0, 0.0, 0.0),
Vec3::new(-voxel_size / 2.0, 0.0, 0.0),
),
(1.0, 0.0, 0.0) => (
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(voxel_size / 2.0, 0.0, 0.0),
),
(0.0, -1.0, 0.0) => (
Vec3::new(0.0, -1.0, 0.0),
Vec3::new(0.0, -voxel_size / 2.0, 0.0),
),
(0.0, 1.0, 0.0) => (
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, voxel_size / 2.0, 0.0),
),
(0.0, 0.0, -1.0) => (
Vec3::new(0.0, 0.0, -1.0),
Vec3::new(0.0, 0.0, -voxel_size / 2.0),
),
(0.0, 0.0, 1.0) => (
Vec3::new(0.0, 0.0, 1.0),
Vec3::new(0.0, 0.0, voxel_size / 2.0),
),
_ => continue,
};
voxel_meshes.push(generate_face(
world_position + offset, // offset the face
voxel_size / 2.0,
normal
));
}
}
}
// Merge all the face meshes into a single mesh.
let mesh = merge_meshes(voxel_meshes);
let cube_handle = meshes.add(mesh);
// Create a material with cull_mode disabled to see both sides (for debugging)
let material = materials.add(StandardMaterial {
base_color: Color::srgba(0.8, 0.7, 0.6, 1.0),
cull_mode: Some(Face::Back), // disable culling for debugging
..Default::default()
});
commands.entity(root.0).with_children(|parent| {
parent.spawn((
PbrBundle {
mesh: Mesh3d::from(cube_handle),
material: MeshMaterial3d::from(material),
transform: Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)),
..Default::default()
},
GridCell::<i64>::ZERO,
VoxelTerrainMarker {},
));
});
// Reset the dirty flag after updating.
octree.dirty.clear();
}
}
}
fn generate_face(position: Vec3, face_size: f32, normal: Vec3) -> Mesh {
// Initialize an empty mesh with triangle topology
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
// Define a quad centered at the origin
let mut positions = vec![
[-face_size, -face_size, 0.0],
[ face_size, -face_size, 0.0],
[ face_size, face_size, 0.0],
[-face_size, face_size, 0.0],
];
// Normalize the provided normal to ensure correct rotation
let normal = normal.normalize();
// Compute a rotation that aligns the default +Z with the provided normal
let rotation = Quat::from_rotation_arc(Vec3::Z, normal);
// Rotate and translate the vertices based on the computed rotation and provided position
for p in positions.iter_mut() {
let vertex = rotation * Vec3::from(*p) + position;
*p = [vertex.x, vertex.y, vertex.z];
}
let uvs = vec![
[0.0, 1.0],
[1.0, 1.0],
[1.0, 0.0],
[0.0, 0.0],
];
let indices = Indices::U32(vec![0, 1, 2, 2, 3, 0]);
// Use the provided normal for all vertices
let normals = vec![[normal.x, normal.y, normal.z]; 4];
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
mesh.insert_indices(indices);
mesh
}
fn merge_meshes(meshes: Vec<Mesh>) -> Mesh {
let mut merged_positions = Vec::new();
let mut merged_uvs = Vec::new();
let mut merged_normals = Vec::new(); // To store merged normals
let mut merged_indices = Vec::new();
for mesh in meshes {
if let Some(VertexAttributeValues::Float32x3(positions)) = mesh.attribute(Mesh::ATTRIBUTE_POSITION) {
let start_index = merged_positions.len();
merged_positions.extend_from_slice(positions);
// Extract UVs
if let Some(VertexAttributeValues::Float32x2(uvs)) = mesh.attribute(Mesh::ATTRIBUTE_UV_0) {
merged_uvs.extend_from_slice(uvs);
}
// Extract normals
if let Some(VertexAttributeValues::Float32x3(normals)) = mesh.attribute(Mesh::ATTRIBUTE_NORMAL) {
merged_normals.extend_from_slice(normals);
}
// Extract indices and apply offset
if let Some(indices) = mesh.indices() {
if let Indices::U32(indices) = indices {
let offset_indices: Vec<u32> = indices.iter().map(|i| i + start_index as u32).collect();
merged_indices.extend(offset_indices);
}
}
}
}
// Create new merged mesh
let mut merged_mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
// Insert attributes into the merged mesh
merged_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, merged_positions);
merged_mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, merged_uvs);
merged_mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, merged_normals); // Insert merged normals
merged_mesh.insert_indices(Indices::U32(merged_indices));
merged_mesh
}

View File

@ -35,6 +35,7 @@ pub struct SparseVoxelOctree {
pub show_chunks: bool,
pub dirty: Vec<DirtyVoxel>,
pub dirty_chunks: HashSet<ChunkKey>,
}
impl OctreeNode {
@ -82,4 +83,9 @@ pub struct Ray {
pub struct AABB {
pub min: Vec3,
pub max: Vec3,
}
}
pub const CHUNK_SIZE: i32 = 16; // 16×16×16 voxels
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub struct ChunkKey(pub i32, pub i32, pub i32);