Compare commits

...

14 Commits

Author SHA1 Message Date
Elias Stepanik
f5714ff105
Merge pull request #26 from eliasstepanik/codex/build-texture-atlas-for-voxel-world
Add texture atlas for voxels
2025-06-14 01:45:41 +02:00
Elias Stepanik
028a966856 Randomize voxel side textures 2025-06-14 01:32:37 +02:00
Elias Stepanik
496c5bf673 Remove voxel color field 2025-06-14 01:23:37 +02:00
Elias Stepanik
eecd786ccb Fix voxel creation calls 2025-06-14 01:09:31 +02:00
Elias Stepanik
430a933e8b fix texture atlas image creation 2025-06-14 00:54:01 +02:00
Elias Stepanik
440fd4a717 Fix mesh_chunk braces 2025-06-14 00:48:01 +02:00
Elias Stepanik
1b4f070015 Add basic voxel texture atlas support 2025-06-14 00:34:37 +02:00
Elias Stepanik
0b07669345
Merge pull request #25 from eliasstepanik/gpu-meshing
Gpu meshing
2025-06-14 00:11:13 +02:00
Elias Stepanik
6c4b125ea9 Implement greedy meshing shader 2025-06-13 12:24:23 +02:00
Elias Stepanik
eb34a16a0a Fix GPU mesher structs 2025-06-13 03:37:27 +02:00
Elias Stepanik
b355ac13a9 Fix GPU worker storage types 2025-06-13 03:29:56 +02:00
Elias Stepanik
5a7269a446 Integrate bevy_app_compute for GPU meshing 2025-06-13 03:19:44 +02:00
Elias Stepanik
1802595f7e Add GPU meshing pipeline skeleton 2025-06-13 03:14:02 +02:00
Elias Stepanik
0cf98496ed Add mesh buffer pooling 2025-06-13 02:53:19 +02:00
13 changed files with 638 additions and 275 deletions

View File

@ -21,4 +21,6 @@ smallvec = "1.14.0"
once_cell = "1.21.3"
rayon = "1.10.0"
bincode = "1.3"
bevy_app_compute = "0.16"
bytemuck = { version = "1.14", features = ["derive"] }

View File

@ -0,0 +1,170 @@
// Generates mesh quads for a voxel chunk using a simple greedy algorithm.
// Each invocation processes a slice of the chunk along one axis.
// Results are stored in a vertex/index buffer.
struct Params {
origin: vec3<f32>,
step: f32,
axis: u32,
dir: i32,
slice: u32,
_pad: u32,
};
struct Vertex {
pos: vec3<f32>,
normal: vec3<f32>,
uv: vec2<f32>,
};
@group(0) @binding(0) var<storage, read> voxels: array<u32>;
@group(0) @binding(1) var<uniform> params: Params;
@group(0) @binding(2) var<storage, read_write> vertices: array<Vertex>;
@group(0) @binding(3) var<storage, read_write> indices: array<u32>;
@group(0) @binding(4) var<storage, read_write> counts: atomic<u32>;
const N: u32 = 16u;
const MASK_LEN: u32 = N * N;
fn voxel_index(p: vec3<i32>) -> u32 {
return u32(p.x) * N * N + u32(p.y) * N + u32(p.z);
}
fn voxel_filled(p: vec3<i32>) -> bool {
return p.x >= 0 && p.x < i32(N) && p.y >= 0 && p.y < i32(N) && p.z >= 0 && p.z < i32(N) && voxels[voxel_index(p)] != 0u;
}
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
var mask: array<bool, MASK_LEN>;
var visited: array<bool, MASK_LEN>;
// Iterate over all axes and both face directions.
for (var axis: u32 = 0u; axis < 3u; axis = axis + 1u) {
for (var dir_idx: u32 = 0u; dir_idx < 2u; dir_idx = dir_idx + 1u) {
let dir: i32 = select(-1, 1, dir_idx == 1u);
for (var slice: u32 = 0u; slice < N; slice = slice + 1u) {
// Build mask for this slice.
for (var u: u32 = 0u; u < N; u = u + 1u) {
for (var v: u32 = 0u; v < N; v = v + 1u) {
var cell = vec3<i32>(0, 0, 0);
var neighbor = vec3<i32>(0, 0, 0);
if axis == 0u {
cell = vec3<i32>(i32(slice), i32(u), i32(v));
neighbor = cell + vec3<i32>(dir, 0, 0);
} else if axis == 1u {
cell = vec3<i32>(i32(v), i32(slice), i32(u));
neighbor = cell + vec3<i32>(0, dir, 0);
} else {
cell = vec3<i32>(i32(u), i32(v), i32(slice));
neighbor = cell + vec3<i32>(0, 0, dir);
}
let i = u * N + v;
mask[i] = voxel_filled(cell) && !voxel_filled(neighbor);
visited[i] = false;
}
}
// Greedy merge.
for (var u0: u32 = 0u; u0 < N; u0 = u0 + 1u) {
for (var v0: u32 = 0u; v0 < N; v0 = v0 + 1u) {
let i0 = u0 * N + v0;
if !mask[i0] || visited[i0] {
continue;
}
var width: u32 = 1u;
loop {
if u0 + width >= N || !mask[u0 + width * N + v0] || visited[u0 + width * N + v0] {
break;
}
width = width + 1u;
}
var height: u32 = 1u;
outer: loop {
if v0 + height >= N {
break;
}
for (var du: u32 = 0u; du < width; du = du + 1u) {
let idx = (u0 + du) * N + v0 + height;
if !mask[idx] || visited[idx] {
break outer;
}
}
height = height + 1u;
}
for (var du: u32 = 0u; du < width; du = du + 1u) {
for (var dv: u32 = 0u; dv < height; dv = dv + 1u) {
visited[(u0 + du) * N + v0 + dv] = true;
}
}
// Compute base world-space position.
var base = params.origin;
if axis == 0u {
base = base + vec3<f32>(f32(slice) + (dir > 0 ? 1.0 : 0.0), f32(u0), f32(v0)) * params.step;
} else if axis == 1u {
base = base + vec3<f32>(f32(v0), f32(slice) + (dir > 0 ? 1.0 : 0.0), f32(u0)) * params.step;
} else {
base = base + vec3<f32>(f32(u0), f32(v0), f32(slice) + (dir > 0 ? 1.0 : 0.0)) * params.step;
}
let size = vec2<f32>(f32(width) * params.step, f32(height) * params.step);
var normal = vec3<f32>(0.0, 0.0, 0.0);
var u_unit = vec3<f32>(0.0, 0.0, 0.0);
var v_unit = vec3<f32>(0.0, 0.0, 0.0);
if axis == 0u {
normal = vec3<f32>(f32(dir), 0.0, 0.0);
u_unit = vec3<f32>(0.0, 1.0, 0.0);
v_unit = vec3<f32>(0.0, 0.0, 1.0);
} else if axis == 1u {
normal = vec3<f32>(0.0, f32(dir), 0.0);
u_unit = vec3<f32>(0.0, 0.0, 1.0);
v_unit = vec3<f32>(1.0, 0.0, 0.0);
} else {
normal = vec3<f32>(0.0, 0.0, f32(dir));
u_unit = vec3<f32>(1.0, 0.0, 0.0);
v_unit = vec3<f32>(0.0, 1.0, 0.0);
}
let p0 = base;
let p1 = base + u_unit * size.x;
let p2 = base + u_unit * size.x + v_unit * size.y;
let p3 = base + v_unit * size.y;
let vi = atomicAdd(&counts[0], 4u);
vertices[vi] = Vertex(pos: p0, normal: normal, uv: vec2<f32>(0.0, 1.0));
vertices[vi + 1u] = Vertex(pos: p1, normal: normal, uv: vec2<f32>(1.0, 1.0));
vertices[vi + 2u] = Vertex(pos: p2, normal: normal, uv: vec2<f32>(1.0, 0.0));
vertices[vi + 3u] = Vertex(pos: p3, normal: normal, uv: vec2<f32>(0.0, 0.0));
let ii = atomicAdd(&counts[1], 6u);
if dir > 0 {
indices[ii] = vi;
indices[ii + 1u] = vi + 1u;
indices[ii + 2u] = vi + 2u;
indices[ii + 3u] = vi + 2u;
indices[ii + 4u] = vi + 3u;
indices[ii + 5u] = vi;
} else {
indices[ii] = vi;
indices[ii + 1u] = vi + 3u;
indices[ii + 2u] = vi + 2u;
indices[ii + 3u] = vi + 2u;
indices[ii + 4u] = vi + 1u;
indices[ii + 5u] = vi;
}
}
}
}
}
}
}

View File

@ -1,28 +1,42 @@
use crate::plugins::environment::systems::voxels::debug::{draw_grid, visualize_octree_system};
use crate::plugins::environment::systems::voxels::lod::update_chunk_lods;
use crate::plugins::environment::systems::voxels::meshing_gpu::{
GpuMeshingWorker, queue_gpu_meshing,
};
use bevy_app_compute::prelude::{AppComputePlugin, AppComputeWorkerPlugin};
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::atlas::{VoxelTextureAtlas};
use crate::plugins::environment::systems::voxels::structure::{
ChunkBudget, ChunkCullingCfg, ChunkQueue, MeshBufferPool, PrevCameraChunk, SparseVoxelOctree,
SpawnedChunks,
};
use bevy::app::{App, Plugin, PreStartup, PreUpdate, Startup};
use bevy::prelude::*;
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::lod::update_chunk_lods;
use crate::plugins::environment::systems::voxels::structure::{ChunkBudget, ChunkCullingCfg, ChunkQueue, SparseVoxelOctree, SpawnedChunks, PrevCameraChunk};
pub struct EnvironmentPlugin;
impl Plugin for EnvironmentPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Startup,
(
setup_texture_atlas,
crate::plugins::environment::systems::camera_system::setup,
crate::plugins::environment::systems::environment_system::setup.after(crate::plugins::environment::systems::camera_system::setup),
crate::plugins::environment::systems::voxel_system::setup
crate::plugins::environment::systems::environment_system::setup
.after(crate::plugins::environment::systems::camera_system::setup),
crate::plugins::environment::systems::voxel_system::setup,
),
);
app.add_plugins(AppComputePlugin);
app.add_plugins(AppComputeWorkerPlugin::<GpuMeshingWorker>::default());
let view_distance_chunks = 100;
app.insert_resource(ChunkCullingCfg { view_distance_chunks });
app.insert_resource(ChunkCullingCfg {
view_distance_chunks,
});
app.insert_resource(ChunkBudget { per_frame: 20 });
app.init_resource::<PrevCameraChunk>();
app.add_systems(Update, log_mesh_count);
@ -32,6 +46,7 @@ impl Plugin for EnvironmentPlugin {
// ------------------------------------------------------------------------
.init_resource::<ChunkQueue>()
.init_resource::<SpawnedChunks>()
.init_resource::<MeshBufferPool>()
// ------------------------------------------------------------------------
// frame update
// ------------------------------------------------------------------------
@ -42,8 +57,8 @@ impl Plugin for EnvironmentPlugin {
enqueue_visible_chunks,
process_chunk_queue.after(enqueue_visible_chunks),
update_chunk_lods.after(process_chunk_queue),
rebuild_dirty_chunks .after(process_chunk_queue), // 4. (re)mesh dirty chunks
rebuild_dirty_chunks.after(process_chunk_queue), // 4. (re)mesh dirty chunks
queue_gpu_meshing.after(rebuild_dirty_chunks),
/* ---------- optional debug drawing ------- */
visualize_octree_system
.run_if(should_visualize_octree)
@ -52,9 +67,8 @@ impl Plugin for EnvironmentPlugin {
.run_if(should_draw_grid)
.after(visualize_octree_system),
)
.chain(), // make the whole tuple execute in this exact order
.chain(), // make the whole tuple execute in this exact order
);
}
}
@ -65,11 +79,20 @@ fn log_mesh_count(meshes: Res<Assets<Mesh>>, time: Res<Time>) {
}
fn should_visualize_octree(octree_query: Query<&SparseVoxelOctree>) -> bool {
let Ok(octree) = octree_query.get_single() else { return false };
let Ok(octree) = octree_query.get_single() else {
return false;
};
octree.show_wireframe
}
fn should_draw_grid(octree_query: Query<&SparseVoxelOctree>) -> bool {
let Ok(octree) = octree_query.get_single() else { return false };
let Ok(octree) = octree_query.get_single() else {
return false;
};
octree.show_world_grid
}
}
fn setup_texture_atlas(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
let atlas = VoxelTextureAtlas::generate(&mut images);
commands.insert_resource(atlas);
}

View File

@ -1,25 +1,21 @@
use std::path::Path;
use rayon::prelude::*;
use crate::plugins::big_space::big_space_plugin::RootGrid;
use crate::plugins::environment::systems::voxels::structure::*;
use rayon::prelude::*;
use std::path::Path;
use bevy::prelude::*;
use bevy::render::mesh::*;
use noise::{NoiseFn, Perlin};
use rand::{thread_rng, Rng};
use rand::{Rng, thread_rng};
pub fn setup(
mut commands: Commands,
root: Res<RootGrid>,
) {
pub fn setup(mut commands: Commands, root: Res<RootGrid>) {
// Octree parameters
let unit_size = 1.0_f32;
let unit_size = 1.0_f32;
let octree_base_size = 64.0 * unit_size;
let octree_depth = 10;
let octree_depth = 10;
let path = Path::new("octree.bin");
let mut octree = if path.exists() {
match SparseVoxelOctree::load_from_file(path) {
Ok(tree) => tree,
@ -30,7 +26,6 @@ pub fn setup(
}
} else {
let mut tree = SparseVoxelOctree::new(octree_depth, octree_base_size, false, false, false);
let color = Color::srgb(0.2, 0.8, 0.2);
// How many random spheres?
/*const NUM_SPHERES: usize = 5;
let mut rng = threald_rng();
@ -44,30 +39,22 @@ pub fn setup(
let radius = rng.gen_range(20..=150); // voxels
generate_voxel_sphere_parallel(&mut tree, center, radius, color);
generate_voxel_sphere_parallel(&mut tree, center, radius);
}*/
generate_voxel_sphere(&mut tree, 200, color);
generate_voxel_sphere(&mut tree, 200);
tree
};
// Attach octree to the scene graph
commands.entity(root.0).with_children(|parent| {
parent.spawn((Transform::default(), octree));
});
}
pub fn generate_voxel_sphere_parallel(
octree: &mut SparseVoxelOctree,
center: Vec3,
radius: i32,
color: Color,
) {
let step = octree.get_spacing_at_depth(octree.max_depth);
let radius_sq = radius * radius;
pub fn generate_voxel_sphere_parallel(octree: &mut SparseVoxelOctree, center: Vec3, radius: i32) {
let step = octree.get_spacing_at_depth(octree.max_depth);
let radius_sq = radius * radius;
// 1. Collect voxel positions in parallel
let voxels: Vec<(Vec3, Voxel)> = (-radius..=radius)
@ -75,7 +62,7 @@ pub fn generate_voxel_sphere_parallel(
.flat_map_iter(|ix| {
let dx2 = ix * ix;
(-radius..=radius).flat_map(move |iy| {
let dy2 = iy * iy;
let dy2 = iy * iy;
let r2_xy = dx2 + dy2;
if r2_xy > radius_sq {
@ -83,14 +70,16 @@ pub fn generate_voxel_sphere_parallel(
}
let max_z = ((radius_sq - r2_xy) as f32).sqrt() as i32;
(-max_z..=max_z).map(move |iz| {
let pos = Vec3::new(
center.x + ix as f32 * step,
center.y + iy as f32 * step,
center.z + iz as f32 * step,
);
(pos, Voxel { color })
}).collect::<Vec<_>>()
(-max_z..=max_z)
.map(move |iz| {
let pos = Vec3::new(
center.x + ix as f32 * step,
center.y + iy as f32 * step,
center.z + iz as f32 * step,
);
(pos, Voxel::random_sides())
})
.collect::<Vec<_>>()
})
})
.collect();
@ -101,12 +90,7 @@ pub fn generate_voxel_sphere_parallel(
}
}
fn generate_voxel_sphere(
octree: &mut SparseVoxelOctree,
planet_radius: i32,
voxel_color: Color,
) {
fn generate_voxel_sphere(octree: &mut SparseVoxelOctree, planet_radius: i32) {
// For simplicity, we center the sphere around (0,0,0).
// We'll loop over a cubic region [-planet_radius, +planet_radius] in x, y, z
let min = -planet_radius;
@ -131,9 +115,7 @@ fn generate_voxel_sphere(
let position = Vec3::new(wx, wy, wz);
// Insert the voxel
let voxel = Voxel {
color: voxel_color,
};
let voxel = Voxel::random_sides();
octree.insert(position, voxel);
}
}
@ -141,13 +123,9 @@ fn generate_voxel_sphere(
}
}
/// Inserts a 16x256x16 "column" of voxels into the octree at (0,0,0) corner.
/// If you want it offset or centered differently, just adjust the for-loop ranges or offsets.
fn generate_voxel_rect(
octree: &mut SparseVoxelOctree,
voxel_color: Color,
) {
fn generate_voxel_rect(octree: &mut SparseVoxelOctree) {
// The dimensions of our rectangle: 16 x 256 x 16
let size_x = 16;
let size_y = 256;
@ -172,21 +150,14 @@ fn generate_voxel_rect(
let position = Vec3::new(wx, wy, wz);
// Insert the voxel
let voxel = Voxel {
color: voxel_color,
};
let voxel = Voxel::random_sides();
octree.insert(position, voxel);
}
}
}
}
fn generate_large_plane(
octree: &mut SparseVoxelOctree,
width: usize,
depth: usize,
color: Color,
) {
fn generate_large_plane(octree: &mut SparseVoxelOctree, width: usize, depth: usize) {
// We'll get the voxel spacing (size at the deepest level).
let step = octree.get_spacing_at_depth(octree.max_depth);
@ -207,20 +178,16 @@ fn generate_large_plane(
let position = Vec3::new(wx, wy, wz);
// Insert the voxel
let voxel = Voxel {
color,
};
let voxel = Voxel::random_sides();
octree.insert(position, voxel);
}
}
}
pub fn generate_solid_plane_with_noise(
octree: &mut SparseVoxelOctree,
width: usize,
depth: usize,
color: Color,
noise: &Perlin,
frequency: f32,
amplitude: f32,
@ -245,13 +212,9 @@ pub fn generate_solid_plane_with_noise(
// Fill from layer 0 up to max_layer
for iy in 0..=max_layer {
let position = Vec3::new(
x * step,
iy as f32 * step,
z * step,
);
let position = Vec3::new(x * step, iy as f32 * step, z * step);
let voxel = Voxel { color };
let voxel = Voxel::random_sides();
octree.insert(position, voxel);
}
}

View File

@ -0,0 +1,71 @@
use bevy::asset::RenderAssetUsages;
use bevy::prelude::*;
use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat};
/// Configuration and handle for the voxel texture atlas.
#[derive(Resource, Clone)]
pub struct VoxelTextureAtlas {
pub handle: Handle<Image>,
pub columns: usize,
pub rows: usize,
}
impl VoxelTextureAtlas {
/// Create a simple procedural atlas with solid colors.
pub fn generate(images: &mut Assets<Image>) -> Self {
let tile_size = 16u32;
let columns = 2;
let rows = 3;
let width = tile_size * columns as u32;
let height = tile_size * rows as u32;
let mut data = vec![0u8; (width * height * 4) as usize];
let colors = [
[255, 0, 0, 255], // 0: red
[0, 0, 0, 255], // 1: black
[0, 255, 0, 255], // 2: green
[0, 0, 255, 255], // 3: blue
[255, 255, 0, 255], // 4: yellow
[255, 0, 255, 255], // 5: magenta
];
for (i, col) in colors.iter().enumerate() {
let cx = (i % columns) as u32 * tile_size;
let cy = (i / columns) as u32 * tile_size;
for y in 0..tile_size {
for x in 0..tile_size {
let idx = (((cy + y) * width + (cx + x)) * 4) as usize;
data[idx..idx + 4].copy_from_slice(col);
}
}
}
let image = Image::new_fill(
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
TextureDimension::D2,
&data,
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::default(),
);
let handle = images.add(image);
Self {
handle,
columns,
rows,
}
}
/// Compute UV coordinates for the given atlas index.
pub fn uv_rect(&self, index: usize) -> [[f32; 2]; 4] {
let col = index % self.columns;
let row = index / self.columns;
let cols = self.columns as f32;
let rows = self.rows as f32;
let u0 = col as f32 / cols;
let v0 = row as f32 / rows;
let u1 = (col + 1) as f32 / cols;
let v1 = (row + 1) as f32 / rows;
[[u0, v1], [u1, v1], [u1, v0], [u0, v0]]
}
}

View File

@ -1,5 +1,5 @@
use bevy::prelude::*;
use crate::plugins::environment::systems::voxels::structure::*;
use bevy::prelude::*;
/// Visualize each node of the octree as a scaled cuboid, **center-based**.
/// `octree_tf.translation` is the world-space center of the root bounding box.
@ -13,8 +13,7 @@ pub fn visualize_octree_system(
// Draw a translucent cuboid for the root
gizmos.cuboid(
Transform::from_translation(octree_tf.translation)
.with_scale(Vec3::splat(octree.size)),
Transform::from_translation(octree_tf.translation).with_scale(Vec3::splat(octree.size)),
Color::srgba(1.0, 1.0, 0.0, 0.15),
);
@ -85,9 +84,8 @@ fn visualize_recursive_center(
// Draw a small cuboid at the same center as the parent node.
gizmos.cuboid(
Transform::from_translation(parent_center)
.with_scale(Vec3::splat(leaf_size)),
voxel.color,
Transform::from_translation(parent_center).with_scale(Vec3::splat(leaf_size)),
Color::WHITE,
);
}
}
@ -100,7 +98,9 @@ pub fn draw_grid(
camera_query: Query<&Transform, With<Camera>>,
octree_query: Query<(&SparseVoxelOctree, &Transform)>,
) {
let Ok(camera_tf) = camera_query.get_single() else { return };
let Ok(camera_tf) = camera_query.get_single() else {
return;
};
let camera_pos = camera_tf.translation;
for (octree, octree_tf) in octree_query.iter() {
@ -142,4 +142,4 @@ pub fn draw_grid(
gizmos.line(p3, p4, Color::WHITE);
}
}
}
}

View File

@ -1,7 +1,8 @@
use crate::plugins::environment::systems::voxels::structure::*;
use crate::plugins::environment::systems::voxels::atlas::VoxelTextureAtlas;
use bevy::asset::RenderAssetUsages;
use bevy::prelude::*;
use bevy::render::mesh::{Indices, PrimitiveTopology, VertexAttributeValues, Mesh};
use crate::plugins::environment::systems::voxels::structure::*;
use bevy::render::mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues};
/*pub(crate) fn mesh_chunk(
buffer: &[[[Option<Voxel>; CHUNK_SIZE as usize]; CHUNK_SIZE as usize]; CHUNK_SIZE as usize],
@ -96,7 +97,7 @@ use crate::plugins::environment::systems::voxels::structure::*;
}
}
}
// ------ 2nd pass : +Z faces ---------------------------------------------
for z in 0..CHUNK_SIZE { // +Z faces (normal +Z)
let nz = 1;
@ -298,12 +299,13 @@ use crate::plugins::environment::systems::voxels::structure::*;
mesh
}*/
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,
step: f32,
tree: &SparseVoxelOctree,
pool: &mut MeshBufferPool,
atlas: &VoxelTextureAtlas,
) -> Option<Mesh> {
// ────────────────────────────────────────────────────────────────────────────
// Helpers
@ -313,29 +315,35 @@ pub(crate) fn mesh_chunk(
const MASK_LEN: usize = N * N;
// Safe voxel query that falls back to the octree for outofchunk requests.
let filled = |x: i32, y: i32, z: i32| -> bool {
let get_voxel = |x: i32, y: i32, z: i32| -> Option<Voxel> {
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()
buffer[x as usize][y as usize][z as usize]
} 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()
tree.get_voxel_at_world_coords(world).copied()
}
};
// Push a single quad (4 vertices, 6 indices). `base` is the lowerleft
// corner in world space; `u`/`v` are the tangent vectors (length 1); `size`
// is expressed in world units along those axes; `n` is the face normal.
// Preallocate vertex buffers for better performance
// Preallocate vertex buffers for better performance, reusing the pool.
pool.clear();
let voxel_count = N * N * N;
let mut positions = Vec::<[f32; 3]>::with_capacity(voxel_count * 4);
let mut normals = Vec::<[f32; 3]>::with_capacity(voxel_count * 4);
let mut uvs = Vec::<[f32; 2]>::with_capacity(voxel_count * 4);
let mut indices = Vec::<u32>::with_capacity(voxel_count * 6);
pool.positions.reserve(voxel_count * 4);
pool.normals.reserve(voxel_count * 4);
pool.uvs.reserve(voxel_count * 4);
pool.indices.reserve(voxel_count * 6);
let mut push_quad = |base: Vec3, size: Vec2, n: Vec3, u: Vec3, v: Vec3| {
let positions = &mut pool.positions;
let normals = &mut pool.normals;
let uvs = &mut pool.uvs;
let indices = &mut pool.indices;
let mut push_quad = |base: Vec3, size: Vec2, n: Vec3, u: Vec3, v: Vec3, tex_id: usize| {
let i0 = positions.len() as u32;
positions.extend_from_slice(&[
(base).into(),
@ -344,7 +352,8 @@ pub(crate) fn mesh_chunk(
(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]]);
let uv_rect = atlas.uv_rect(tex_id);
uvs.extend_from_slice(&uv_rect);
if n.x + n.y + n.z >= 0.0 {
indices.extend_from_slice(&[i0, i0 + 1, i0 + 2, i0 + 2, i0 + 3, i0]);
@ -361,7 +370,7 @@ pub(crate) fn mesh_chunk(
// Axes: 0→X, 1→Y, 2→Z. For each axis we process the negative and positive
// faces (dir = 1 / +1).
for (axis, dir) in [ (0, -1), (0, 1), (1, -1), (1, 1), (2, -1), (2, 1) ] {
for (axis, dir) in [(0, -1), (0, 1), (1, -1), (1, 1), (2, -1), (2, 1)] {
// Mapping of (u,v) axes and their unit vectors in world space.
let (u_axis, v_axis, face_normal, u_vec, v_vec) = match (axis, dir) {
(0, d) => (1, 2, Vec3::new(d as f32, 0.0, 0.0), Vec3::Y, Vec3::Z),
@ -376,7 +385,7 @@ pub(crate) fn mesh_chunk(
for slice in 0..=N {
// Build the face mask for this slice using a fixed-size array to
// avoid heap allocations.
let mut mask = [false; MASK_LEN];
let mut mask = [None::<usize>; MASK_LEN];
let mut visited = [false; MASK_LEN];
let idx = |u: usize, v: usize| -> usize { u * N + v };
@ -386,16 +395,27 @@ pub(crate) fn mesh_chunk(
let mut cell = [0i32; 3];
let mut neighbor = [0i32; 3];
cell [axis] = slice as i32 + if dir == 1 { -1 } else { 0 };
cell[axis] = slice as i32 + if dir == 1 { -1 } else { 0 };
neighbor[axis] = cell[axis] + dir;
cell [u_axis] = u as i32;
cell [v_axis] = v as i32;
cell[u_axis] = u as i32;
cell[v_axis] = v as i32;
neighbor[u_axis] = u as i32;
neighbor[v_axis] = v as i32;
if filled(cell[0], cell[1], cell[2]) && !filled(neighbor[0], neighbor[1], neighbor[2]) {
mask[idx(u, v)] = true;
if let Some(vox) = get_voxel(cell[0], cell[1], cell[2]) {
if get_voxel(neighbor[0], neighbor[1], neighbor[2]).is_none() {
let face_idx = match (axis, dir) {
(0, -1) => 0,
(0, 1) => 1,
(1, -1) => 2,
(1, 1) => 3,
(2, -1) => 4,
(2, 1) => 5,
_ => unreachable!(),
};
mask[idx(u, v)] = Some(vox.textures[face_idx]);
}
}
}
}
@ -403,13 +423,17 @@ pub(crate) fn mesh_chunk(
// Greedy merge the mask into maximal rectangles.
for u0 in 0..N {
for v0 in 0..N {
if !mask[idx(u0, v0)] || visited[idx(u0, v0)] {
if visited[idx(u0, v0)] {
continue;
}
let Some(tex_id) = mask[idx(u0, v0)] else { continue };
// Determine the rectangle width.
let mut width = 1;
while u0 + width < N && mask[idx(u0 + width, v0)] && !visited[idx(u0 + width, v0)] {
while u0 + width < N
&& mask[idx(u0 + width, v0)] == Some(tex_id)
&& !visited[idx(u0 + width, v0)]
{
width += 1;
}
@ -417,7 +441,9 @@ pub(crate) fn mesh_chunk(
let mut height = 1;
'h: while v0 + height < N {
for du in 0..width {
if !mask[idx(u0 + du, v0 + height)] || visited[idx(u0 + du, v0 + height)] {
if mask[idx(u0 + du, v0 + height)] != Some(tex_id)
|| visited[idx(u0 + du, v0 + height)]
{
break 'h;
}
}
@ -453,7 +479,7 @@ pub(crate) fn mesh_chunk(
}
let size = Vec2::new(width as f32 * step, height as f32 * step);
push_quad(base, size, face_normal, u_vec, v_vec);
push_quad(base, size, face_normal, u_vec, v_vec, tex_id);
}
}
}
@ -466,19 +492,23 @@ pub(crate) fn mesh_chunk(
return None;
}
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
let mut mesh = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
);
mesh.insert_attribute(
Mesh::ATTRIBUTE_POSITION,
VertexAttributeValues::Float32x3(positions),
VertexAttributeValues::Float32x3(positions.clone()),
);
mesh.insert_attribute(
Mesh::ATTRIBUTE_NORMAL,
VertexAttributeValues::Float32x3(normals),
VertexAttributeValues::Float32x3(normals.clone()),
);
mesh.insert_attribute(
Mesh::ATTRIBUTE_UV_0,
VertexAttributeValues::Float32x2(uvs),
VertexAttributeValues::Float32x2(uvs.clone()),
);
mesh.insert_indices(Indices::U32(indices));
mesh.insert_indices(Indices::U32(indices.clone()));
pool.clear();
Some(mesh)
}

View File

@ -0,0 +1,66 @@
use bevy::prelude::*;
use bevy_app_compute::prelude::*;
use super::structure::{MeshBufferPool, SparseVoxelOctree};
#[repr(C)]
#[derive(ShaderType, Copy, Clone, Default)]
pub struct Params {
pub origin: Vec3,
pub step: f32,
pub axis: u32,
pub dir: i32,
pub slice: u32,
pub _pad: u32,
}
#[repr(C)]
#[derive(ShaderType, Copy, Clone, Default)]
pub struct VertexGpu {
pub pos: Vec3,
pub normal: Vec3,
pub uv: Vec2,
}
#[derive(TypePath)]
struct GreedyMeshingShader;
impl ComputeShader for GreedyMeshingShader {
fn shader() -> ShaderRef {
"shaders/greedy_meshing.wgsl".into()
}
}
/// GPU worker executing greedy meshing for chunks.
#[derive(Resource)]
pub struct GpuMeshingWorker;
impl ComputeWorker for GpuMeshingWorker {
fn build(world: &mut World) -> AppComputeWorker<Self> {
AppComputeWorkerBuilder::new(world)
.add_storage("voxels", &[0u32; 1])
.add_uniform("params", &Params::default())
.add_storage("vertices", &[VertexGpu::default(); 1])
.add_storage("indices", &[0u32; 1])
.add_storage("counts", &[0u32; 2])
.add_pass::<GreedyMeshingShader>(
[1, 1, 1],
&["voxels", "params", "vertices", "indices", "counts"],
)
.one_shot()
.build()
}
}
/// Placeholder system that will dispatch the compute worker for dirty chunks.
pub fn queue_gpu_meshing(
mut worker: ResMut<AppComputeWorker<GpuMeshingWorker>>,
_octrees: Query<&SparseVoxelOctree>,
_pool: ResMut<MeshBufferPool>,
) {
if !worker.ready() {
return;
}
// TODO: populate the worker buffers with chunk data before dispatching.
worker.execute();
}

View File

@ -4,8 +4,10 @@ pub mod octree;
pub mod structure;
mod chunk;
mod meshing;
pub mod render_chunks;
pub mod culling;
pub mod queue_systems;
pub mod lod;
mod meshing;
pub mod meshing_gpu;
pub mod queue_systems;
pub mod render_chunks;
pub mod atlas;

View File

@ -1,19 +1,27 @@
use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::io;
use bincode;
use crate::plugins::environment::systems::voxels::helper::chunk_key_from_world;
use crate::plugins::environment::systems::voxels::structure::{
AABB, CHUNK_SIZE, ChunkKey, DirtyVoxel, NEIGHBOR_OFFSETS, OctreeNode, Ray, SparseVoxelOctree,
Voxel,
};
use bevy::asset::Assets;
use bevy::color::Color;
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, CHUNK_SIZE, ChunkKey};
use bincode;
use std::collections::{HashMap, HashSet};
use std::io;
use std::path::Path;
impl SparseVoxelOctree {
/// Creates a new octree with the specified max depth, size, and wireframe visibility.
pub fn new(max_depth: u32, size: f32, show_wireframe: bool, show_world_grid: bool, show_chunks: bool) -> Self {
pub fn new(
max_depth: u32,
size: f32,
show_wireframe: bool,
show_world_grid: bool,
show_chunks: bool,
) -> Self {
Self {
root: OctreeNode::new(),
max_depth,
@ -38,9 +46,7 @@ impl SparseVoxelOctree {
world_center = self.denormalize_voxel_center(aligned);
}
let dirty_voxel = DirtyVoxel{
position: aligned,
};
let dirty_voxel = DirtyVoxel { position: aligned };
self.dirty.push(dirty_voxel);
let key = chunk_key_from_world(self, position);
@ -48,7 +54,6 @@ impl SparseVoxelOctree {
self.mark_neighbor_chunks_dirty(position);
self.occupied_chunks.insert(key);
Self::insert_recursive(&mut self.root, aligned, voxel, self.max_depth);
}
@ -147,9 +152,12 @@ impl SparseVoxelOctree {
/// Mark all six neighbor chunks of the given key as dirty if they exist.
pub fn mark_neighbors_dirty_from_key(&mut self, key: ChunkKey) {
let offsets = [
(-1, 0, 0), (1, 0, 0),
(0, -1, 0), (0, 1, 0),
(0, 0, -1), (0, 0, 1),
(-1, 0, 0),
(1, 0, 0),
(0, -1, 0),
(0, 1, 0),
(0, 0, -1),
(0, 0, 1),
];
for (dx, dy, dz) in offsets {
let neighbor = ChunkKey(key.0 + dx, key.1 + dy, key.2 + dz);
@ -159,13 +167,7 @@ impl SparseVoxelOctree {
}
}
fn remove_recursive(
node: &mut OctreeNode,
x: f32,
y: f32,
z: f32,
depth: u32,
) -> bool {
fn remove_recursive(node: &mut OctreeNode, x: f32, y: f32, z: f32, depth: u32) -> bool {
if depth == 0 {
if node.voxel.is_some() {
node.voxel = None;
@ -222,7 +224,6 @@ impl SparseVoxelOctree {
false
}
fn expand_root(&mut self, _x: f32, _y: f32, _z: f32) {
info!("Root expanding ...");
// Save the old root and its size.
@ -244,7 +245,15 @@ impl SparseVoxelOctree {
/// The coordinate system here assumes the node covers [old_size/2, +old_size/2] in each axis.
fn collect_voxels_from_node(node: &OctreeNode, old_size: f32) -> Vec<(Vec3, Voxel, u32)> {
let mut voxels = Vec::new();
Self::collect_voxels_recursive(node, -old_size / 2.0, -old_size / 2.0, -old_size / 2.0, old_size, 0, &mut voxels);
Self::collect_voxels_recursive(
node,
-old_size / 2.0,
-old_size / 2.0,
-old_size / 2.0,
old_size,
0,
&mut voxels,
);
voxels
}
@ -270,14 +279,20 @@ impl SparseVoxelOctree {
let offset_x = if (i & 1) != 0 { half } else { 0.0 };
let offset_y = if (i & 2) != 0 { half } else { 0.0 };
let offset_z = if (i & 4) != 0 { half } else { 0.0 };
Self::collect_voxels_recursive(child, x + offset_x, y + offset_y, z + offset_z, half, depth + 1, out);
Self::collect_voxels_recursive(
child,
x + offset_x,
y + offset_y,
z + offset_z,
half,
depth + 1,
out,
);
}
}
}
pub fn traverse(&self) -> Vec<(Vec3, Color, u32)> {
pub fn traverse(&self) -> Vec<(Vec3, u32)> {
let mut voxels = Vec::new();
// Start at the normalized center (0.5, 0.5, 0.5) rather than (0,0,0)
Self::traverse_recursive(
@ -296,20 +311,20 @@ impl SparseVoxelOctree {
local_center: Vec3,
size: f32,
depth: u32,
out: &mut Vec<(Vec3, Color, u32)>,
out: &mut Vec<(Vec3, u32)>,
octree: &SparseVoxelOctree,
) {
// If a leaf contains a voxel, record its world-space center
if node.is_leaf {
if let Some(voxel) = node.voxel {
out.push((octree.denormalize_voxel_center(local_center), voxel.color, depth));
out.push((octree.denormalize_voxel_center(local_center), depth));
}
}
// If the node has children, subdivide the cell into 8 subcells.
if let Some(ref children) = node.children {
let offset = size / 4.0; // child center offset from parent center
let new_size = size / 2.0; // each child cell's size in normalized space
let offset = size / 4.0; // child center offset from parent center
let new_size = size / 2.0; // each child cell's size in normalized space
for (i, child) in children.iter().enumerate() {
// Compute each axis' offset: use +offset if the bit is set, else -offset.
let dx = if (i & 1) != 0 { offset } else { -offset };
@ -322,8 +337,6 @@ impl SparseVoxelOctree {
}
}
/// Retrieve a voxel from the octree if it exists (x,y,z in [-0.5..+0.5] range).
pub fn get_voxel_at(&self, x: f32, y: f32, z: f32) -> Option<&Voxel> {
Self::get_voxel_recursive(&self.root, x, y, z)
@ -388,7 +401,6 @@ impl SparseVoxelOctree {
self.get_voxel_at_world_coords(neighbor_world).is_some()
}
/// Performs a raycast against the octree and returns the first intersected voxel.
pub fn raycast(&self, ray: &Ray) -> Option<(f32, f32, f32, u32, Vec3)> {
// Start from the root node
@ -397,12 +409,7 @@ impl SparseVoxelOctree {
min: Vec3::new(-half_size as f32, -half_size as f32, -half_size as f32),
max: Vec3::new(half_size as f32, half_size as f32, half_size as f32),
};
self.raycast_recursive(
&self.root,
ray,
&root_bounds,
0,
)
self.raycast_recursive(&self.root, ray, &root_bounds, 0)
}
fn raycast_recursive(
@ -435,7 +442,8 @@ impl SparseVoxelOctree {
let mut hits = Vec::new();
for (i, child) in children.iter().enumerate() {
let child_bounds = self.compute_child_bounds(bounds, i);
if let Some(hit) = self.raycast_recursive(child, ray, &child_bounds, depth + 1) {
if let Some(hit) = self.raycast_recursive(child, ray, &child_bounds, depth + 1)
{
hits.push(hit);
}
}
@ -445,11 +453,11 @@ impl SparseVoxelOctree {
let dist_a = ((a.0 as f32 - ray.origin.x).powi(2)
+ (a.1 as f32 - ray.origin.y).powi(2)
+ (a.2 as f32 - ray.origin.z).powi(2))
.sqrt();
.sqrt();
let dist_b = ((b.0 as f32 - ray.origin.x).powi(2)
+ (b.1 as f32 - ray.origin.y).powi(2)
+ (b.2 as f32 - ray.origin.z).powi(2))
.sqrt();
.sqrt();
dist_a.partial_cmp(&dist_b).unwrap()
});
return Some(hits[0]);
@ -462,16 +470,15 @@ impl SparseVoxelOctree {
/// Save the octree to a file using bincode serialization.
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
let data = bincode::serialize(self)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let data = bincode::serialize(self).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
std::fs::write(path, data)
}
/// Load an octree from a file and rebuild runtime caches.
pub fn load_from_file<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let bytes = std::fs::read(path)?;
let mut tree: Self = bincode::deserialize(&bytes)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let mut tree: Self =
bincode::deserialize(&bytes).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
tree.rebuild_cache();
Ok(tree)
}
@ -481,7 +488,7 @@ impl SparseVoxelOctree {
self.dirty.clear();
self.dirty_chunks.clear();
self.occupied_chunks.clear();
let voxels = Self::collect_voxels_from_node(&self.root, self.size);
for (pos, _voxel, _depth) in voxels {
let key = chunk_key_from_world(self, pos);
@ -489,4 +496,3 @@ impl SparseVoxelOctree {
}
}
}

View File

@ -1,27 +1,32 @@
use std::collections::HashMap;
use std::fmt::format;
use crate::plugins::big_space::big_space_plugin::RootGrid;
use crate::plugins::environment::systems::voxels::meshing::mesh_chunk;
use crate::plugins::environment::systems::voxels::structure::*;
use crate::plugins::environment::systems::voxels::atlas::VoxelTextureAtlas;
use bevy::pbr::wireframe::Wireframe;
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::meshing::mesh_chunk;
use crate::plugins::environment::systems::voxels::structure::*;
use std::collections::HashMap;
use std::fmt::format;
/// rebuilds meshes only for chunks flagged dirty by the octree
pub fn rebuild_dirty_chunks(
mut commands : Commands,
mut octrees : Query<&mut SparseVoxelOctree>,
mut meshes : ResMut<Assets<Mesh>>,
mut commands: Commands,
mut octrees: Query<&mut SparseVoxelOctree>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
chunk_q : Query<(Entity,
&Chunk,
&Mesh3d,
&MeshMaterial3d<StandardMaterial>,
&ChunkLod)>,
mut spawned : ResMut<SpawnedChunks>,
root : Res<RootGrid>,
chunk_q: Query<(
Entity,
&Chunk,
&Mesh3d,
&MeshMaterial3d<StandardMaterial>,
&ChunkLod,
)>,
mut spawned: ResMut<SpawnedChunks>,
mut pool: ResMut<MeshBufferPool>,
root: Res<RootGrid>,
atlas: Res<VoxelTextureAtlas>,
) {
// map ChunkKey → (entity, mesh-handle, material-handle)
let existing: HashMap<ChunkKey, (Entity, Handle<Mesh>, Handle<StandardMaterial>, u32)> =
@ -39,8 +44,7 @@ pub fn rebuild_dirty_chunks(
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];
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);
@ -85,7 +89,7 @@ pub fn rebuild_dirty_chunks(
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
match mesh_chunk(&buf, origin, step, &tree) {
match mesh_chunk(&buf, origin, step, &tree, &mut pool, &atlas) {
Some(new_mesh) => {
if let Some(mesh) = meshes.get_mut(&mesh_h) {
*mesh = new_mesh;
@ -98,10 +102,13 @@ pub fn rebuild_dirty_chunks(
spawned.0.remove(&key);
}
}
} else if let Some(mesh) = mesh_chunk(&buf, origin, step, &tree) {
} else if let Some(mesh) = mesh_chunk(&buf, origin, step, &tree, &mut pool, &atlas) {
// spawn brand-new chunk only if mesh has faces
let mesh_h = meshes.add(mesh);
let mat_h = materials.add(StandardMaterial::default());
let mat_h = materials.add(StandardMaterial {
base_color_texture: Some(atlas.handle.clone()),
..Default::default()
});
commands.entity(root.0).with_children(|p| {
let e = p
@ -110,7 +117,11 @@ pub fn rebuild_dirty_chunks(
MeshMaterial3d(mat_h.clone()),
Transform::default(),
GridCell::ZERO,
Chunk { key, voxels: Vec::new(), dirty: false },
Chunk {
key,
voxels: Vec::new(),
dirty: false,
},
ChunkLod(lod),
/*Wireframe,*/
))
@ -122,4 +133,4 @@ pub fn rebuild_dirty_chunks(
tree.clear_dirty_flags();
}
}
}

View File

@ -1,30 +1,21 @@
use std::collections::{HashMap, HashSet, VecDeque};
use bevy::color::Color;
use bevy::prelude::*;
use serde::{Serialize, Deserialize, Serializer, Deserializer};
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet, VecDeque};
fn serialize_color<S>(color: &Color, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let [r, g, b, a] = color.to_linear().to_f32_array();
[r, g, b, a].serialize(serializer)
}
fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error>
where
D: Deserializer<'de>,
{
let arr: [f32; 4] = Deserialize::deserialize(deserializer)?;
Ok(Color::linear_rgba(arr[0], arr[1], arr[2], arr[3]))
}
/// Represents a single voxel with a color.
#[derive(Debug, Clone, Copy, Component, PartialEq, Default, Serialize, Deserialize)]
/// Represents a single voxel with texture indices for each face.
#[derive(Debug, Clone, Copy, Component, PartialEq, Serialize, Deserialize)]
pub struct Voxel {
#[serde(serialize_with = "serialize_color", deserialize_with = "deserialize_color")]
pub color: Color,
/// Indexes into the texture atlas for the six faces in the order
/// left, right, bottom, top, back, front.
#[serde(default)]
pub textures: [usize; 6],
}
impl Default for Voxel {
fn default() -> Self {
Self { textures: [0; 6] }
}
}
#[derive(Debug, Clone, Copy)]
@ -44,7 +35,6 @@ pub struct OctreeNode {
/// Represents the root of the sparse voxel octree.
#[derive(Debug, Component, Serialize, Deserialize, Clone)]
pub struct SparseVoxelOctree {
pub root: OctreeNode,
pub max_depth: u32,
pub size: f32,
@ -76,14 +66,26 @@ impl OctreeNode {
impl Voxel {
/// Creates a new empty octree node.
pub fn new(color: Color) -> Self {
Self {
color,
pub fn new(textures: [usize; 6]) -> Self {
Self { textures }
}
/// Generate a voxel with a red top, black bottom and random colors on
/// all remaining faces. Assumes the atlas uses index 0 for red, index 1
/// for black and indices >=2 for random colors.
pub fn random_sides() -> Self {
let mut rng = rand::thread_rng();
let mut textures = [0usize; 6];
// Face order: left, right, bottom, top, back, front
textures[3] = 0; // top is red
textures[2] = 1; // bottom is black
for &i in &[0usize, 1usize, 4usize, 5usize] {
textures[i] = rng.gen_range(2..6);
}
Self { textures }
}
}
pub const NEIGHBOR_OFFSETS: [(f32, f32, f32); 6] = [
(-1.0, 0.0, 0.0), // Left
(1.0, 0.0, 0.0), // Right
@ -93,7 +95,6 @@ pub const NEIGHBOR_OFFSETS: [(f32, f32, f32); 6] = [
(0.0, 0.0, 1.0), // Front
];
#[derive(Debug)]
pub struct Ray {
pub origin: Vec3,
@ -106,25 +107,22 @@ pub struct AABB {
pub max: Vec3,
}
pub const CHUNK_SIZE: i32 = 16; // 16×16×16 voxels
pub const CHUNK_POW : u32 = 4;
pub const CHUNK_SIZE: i32 = 16; // 16×16×16 voxels
pub const CHUNK_POW: u32 = 4;
#[derive(Component)]
pub struct Chunk {
pub key: ChunkKey,
pub voxels: Vec<(IVec3, Voxel)>, // local coords 0‥15
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, Serialize, Deserialize)]
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 {
@ -132,7 +130,7 @@ pub struct ChunkBudget {
}
impl Default for ChunkBudget {
fn default() -> Self {
Self { per_frame: 4 } // tweak to taste
Self { per_frame: 4 } // tweak to taste
}
}
@ -140,7 +138,7 @@ impl Default for ChunkBudget {
#[derive(Resource, Default)]
pub struct ChunkQueue {
pub keys: VecDeque<ChunkKey>,
pub set: HashSet<ChunkKey>,
pub set: HashSet<ChunkKey>,
}
/// map “which chunk key already has an entity in the world?”
@ -149,8 +147,16 @@ 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 } } }
pub struct ChunkCullingCfg {
pub view_distance_chunks: i32,
}
impl Default for ChunkCullingCfg {
fn default() -> Self {
Self {
view_distance_chunks: 6,
}
}
}
#[derive(Resource, Default)]
pub struct PrevCameraChunk(pub Option<ChunkKey>);
@ -172,3 +178,23 @@ impl ChunkOffsets {
Self(offsets)
}
}
/// Pool reused when constructing chunk meshes. Reusing the backing
/// storage avoids frequent allocations when rebuilding many chunks.
#[derive(Resource, Default)]
pub struct MeshBufferPool {
pub positions: Vec<[f32; 3]>,
pub normals: Vec<[f32; 3]>,
pub uvs: Vec<[f32; 2]>,
pub indices: Vec<u32>,
}
impl MeshBufferPool {
/// Clears all buffers while keeping the allocated capacity.
pub fn clear(&mut self) {
self.positions.clear();
self.normals.clear();
self.uvs.clear();
self.indices.clear();
}
}

View File

@ -1,12 +1,11 @@
use std::path::Path;
use bevy::prelude::*;
use crate::plugins::environment::systems::camera_system::CameraController;
use crate::plugins::environment::systems::voxels::octree;
use crate::plugins::environment::systems::voxels::structure::*;
use bevy::prelude::*;
use std::path::Path;
///TODO
pub fn voxel_system(
keyboard_input: Res<ButtonInput<KeyCode>>,
mouse_button_input: Res<ButtonInput<MouseButton>>,
mut octree_query: Query<&mut SparseVoxelOctree>,
@ -14,29 +13,33 @@ pub fn voxel_system(
mut query: Query<(&mut Transform, &mut CameraController)>,
mut windows: Query<&mut Window>,
) {
let Ok(mut window) = windows.get_single_mut() else { return };
let Ok((mut transform, _)) = query.get_single_mut() else { return };
let Ok(mut window) = windows.get_single_mut() else {
return;
};
let Ok((mut transform, _)) = query.get_single_mut() else {
return;
};
// =======================
// 5) Octree Keys
// =======================
if keyboard_input.just_pressed(KeyCode::F2){
if keyboard_input.just_pressed(KeyCode::F2) {
for mut octree in octree_query.iter_mut() {
octree.show_wireframe = !octree.show_wireframe;
}
}
if keyboard_input.just_pressed(KeyCode::F3){
if keyboard_input.just_pressed(KeyCode::F3) {
for mut octree in octree_query.iter_mut() {
octree.show_world_grid = !octree.show_world_grid;
}
}
if keyboard_input.just_pressed(KeyCode::KeyQ) && window.cursor_options.visible == false{
if keyboard_input.just_pressed(KeyCode::KeyQ) && window.cursor_options.visible == false {
for mut octree in octree_query.iter_mut() {
octree.insert(transform.translation, Voxel::new(Color::srgb(1.0, 0.0, 0.0)));
octree.insert(transform.translation, Voxel::random_sides());
}
}
if keyboard_input.just_pressed(KeyCode::F4){
if keyboard_input.just_pressed(KeyCode::F4) {
let path = Path::new("octree.bin");
for octree in octree_query.iter() {
if let Err(e) = octree.save_to_file(path) {
@ -44,7 +47,7 @@ pub fn voxel_system(
}
}
}
/* if keyboard_input.just_pressed(KeyCode::F5){
/* if keyboard_input.just_pressed(KeyCode::F5){
let path = Path::new("octree.bin");
if path.exists() {
let path = Path::new("octree.bin");
@ -57,17 +60,18 @@ pub fn voxel_system(
}
}
}
}
}*/
// =======================
// 6) Building
// =======================
if (mouse_button_input.just_pressed(MouseButton::Left) || mouse_button_input.just_pressed(MouseButton::Right)) && !window.cursor_options.visible {
if (mouse_button_input.just_pressed(MouseButton::Left)
|| mouse_button_input.just_pressed(MouseButton::Right))
&& !window.cursor_options.visible
{
// Get the mouse position in normalized device coordinates (-1 to 1)
if let Some(_) = window.cursor_position() {
// Set the ray direction to the camera's forward vector
@ -79,44 +83,33 @@ pub fn voxel_system(
direction: ray_direction,
};
for mut octree in octree_query.iter_mut() {
if let Some((hit_x, hit_y, hit_z, depth,normal)) = octree.raycast(&ray) {
if let Some((hit_x, hit_y, hit_z, depth, normal)) = octree.raycast(&ray) {
if mouse_button_input.just_pressed(MouseButton::Right) {
let voxel_size = octree.get_spacing_at_depth(depth);
let hit_position = Vec3::new(hit_x as f32, hit_y as f32, hit_z as f32);
let epsilon = voxel_size * 0.1; // Adjust this value as needed (e.g., 0.1 times the voxel size)
// Offset position by epsilon in the direction of the normal
let offset_position = hit_position - (normal * Vec3::new(epsilon as f32, epsilon as f32, epsilon as f32));
let offset_position = hit_position
- (normal * Vec3::new(epsilon as f32, epsilon as f32, epsilon as f32));
// Remove the voxel
octree.remove(offset_position);
}
else if mouse_button_input.just_pressed(MouseButton::Left) {
} else if mouse_button_input.just_pressed(MouseButton::Left) {
let voxel_size = octree.get_spacing_at_depth(depth);
let hit_position = Vec3::new(hit_x as f32, hit_y as f32, hit_z as f32);
let epsilon = voxel_size * 0.1; // Adjust this value as needed (e.g., 0.1 times the voxel size)
// Offset position by epsilon in the direction of the normal
let offset_position = hit_position + (normal * Vec3::new(epsilon as f32, epsilon as f32, epsilon as f32));
let offset_position = hit_position
+ (normal * Vec3::new(epsilon as f32, epsilon as f32, epsilon as f32));
// Insert the new voxel
octree.insert(
offset_position,
Voxel::new(Color::srgb(1.0, 0.0, 0.0)),
);
octree.insert(offset_position, Voxel::random_sides());
}
}
}
}
}
}
}