mirror of
https://github.com/eliasstepanik/voxel-simulation.git
synced 2026-01-11 05:48:29 +00:00
commit
0b07669345
@ -21,4 +21,6 @@ smallvec = "1.14.0"
|
|||||||
once_cell = "1.21.3"
|
once_cell = "1.21.3"
|
||||||
rayon = "1.10.0"
|
rayon = "1.10.0"
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
|
bevy_app_compute = "0.16"
|
||||||
|
bytemuck = { version = "1.14", features = ["derive"] }
|
||||||
|
|
||||||
|
|||||||
170
client/assets/shaders/greedy_meshing.wgsl
Normal file
170
client/assets/shaders/greedy_meshing.wgsl
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,28 +1,40 @@
|
|||||||
|
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::structure::{
|
||||||
|
ChunkBudget, ChunkCullingCfg, ChunkQueue, MeshBufferPool, PrevCameraChunk, SparseVoxelOctree,
|
||||||
|
SpawnedChunks,
|
||||||
|
};
|
||||||
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::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;
|
pub struct EnvironmentPlugin;
|
||||||
impl Plugin for EnvironmentPlugin {
|
impl Plugin for EnvironmentPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
|
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
Startup,
|
Startup,
|
||||||
(
|
(
|
||||||
crate::plugins::environment::systems::camera_system::setup,
|
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::environment_system::setup
|
||||||
crate::plugins::environment::systems::voxel_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;
|
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.insert_resource(ChunkBudget { per_frame: 20 });
|
||||||
app.init_resource::<PrevCameraChunk>();
|
app.init_resource::<PrevCameraChunk>();
|
||||||
app.add_systems(Update, log_mesh_count);
|
app.add_systems(Update, log_mesh_count);
|
||||||
@ -32,6 +44,7 @@ impl Plugin for EnvironmentPlugin {
|
|||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
.init_resource::<ChunkQueue>()
|
.init_resource::<ChunkQueue>()
|
||||||
.init_resource::<SpawnedChunks>()
|
.init_resource::<SpawnedChunks>()
|
||||||
|
.init_resource::<MeshBufferPool>()
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
// frame update
|
// frame update
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
@ -42,8 +55,8 @@ impl Plugin for EnvironmentPlugin {
|
|||||||
enqueue_visible_chunks,
|
enqueue_visible_chunks,
|
||||||
process_chunk_queue.after(enqueue_visible_chunks),
|
process_chunk_queue.after(enqueue_visible_chunks),
|
||||||
update_chunk_lods.after(process_chunk_queue),
|
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 ------- */
|
/* ---------- optional debug drawing ------- */
|
||||||
visualize_octree_system
|
visualize_octree_system
|
||||||
.run_if(should_visualize_octree)
|
.run_if(should_visualize_octree)
|
||||||
@ -54,7 +67,6 @@ impl Plugin for EnvironmentPlugin {
|
|||||||
)
|
)
|
||||||
.chain(), // make the whole tuple execute in this exact order
|
.chain(), // make the whole tuple execute in this exact order
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,11 +77,15 @@ fn log_mesh_count(meshes: Res<Assets<Mesh>>, time: Res<Time>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn should_visualize_octree(octree_query: Query<&SparseVoxelOctree>) -> bool {
|
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
|
octree.show_wireframe
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_draw_grid(octree_query: Query<&SparseVoxelOctree>) -> bool {
|
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
|
octree.show_world_grid
|
||||||
}
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
|
use crate::plugins::environment::systems::voxels::structure::*;
|
||||||
use bevy::asset::RenderAssetUsages;
|
use bevy::asset::RenderAssetUsages;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::render::mesh::{Indices, PrimitiveTopology, VertexAttributeValues, Mesh};
|
use bevy::render::mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues};
|
||||||
use crate::plugins::environment::systems::voxels::structure::*;
|
|
||||||
|
|
||||||
/*pub(crate) fn mesh_chunk(
|
/*pub(crate) fn mesh_chunk(
|
||||||
buffer: &[[[Option<Voxel>; CHUNK_SIZE as usize]; CHUNK_SIZE as usize]; CHUNK_SIZE as usize],
|
buffer: &[[[Option<Voxel>; CHUNK_SIZE as usize]; CHUNK_SIZE as usize]; CHUNK_SIZE as usize],
|
||||||
@ -298,12 +298,12 @@ use crate::plugins::environment::systems::voxels::structure::*;
|
|||||||
mesh
|
mesh
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
|
||||||
pub(crate) fn mesh_chunk(
|
pub(crate) fn mesh_chunk(
|
||||||
buffer: &[[[Option<Voxel>; CHUNK_SIZE as usize]; CHUNK_SIZE as usize]; CHUNK_SIZE as usize],
|
buffer: &[[[Option<Voxel>; CHUNK_SIZE as usize]; CHUNK_SIZE as usize]; CHUNK_SIZE as usize],
|
||||||
origin: Vec3,
|
origin: Vec3,
|
||||||
step: f32,
|
step: f32,
|
||||||
tree: &SparseVoxelOctree,
|
tree: &SparseVoxelOctree,
|
||||||
|
pool: &mut MeshBufferPool,
|
||||||
) -> Option<Mesh> {
|
) -> Option<Mesh> {
|
||||||
// ────────────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
// Helpers
|
// Helpers
|
||||||
@ -328,12 +328,18 @@ pub(crate) fn mesh_chunk(
|
|||||||
// Push a single quad (4 vertices, 6 indices). `base` is the lower‑left
|
// Push a single quad (4 vertices, 6 indices). `base` is the lower‑left
|
||||||
// corner in world space; `u`/`v` are the tangent vectors (length 1); `size`
|
// 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.
|
// 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 voxel_count = N * N * N;
|
||||||
let mut positions = Vec::<[f32; 3]>::with_capacity(voxel_count * 4);
|
pool.positions.reserve(voxel_count * 4);
|
||||||
let mut normals = Vec::<[f32; 3]>::with_capacity(voxel_count * 4);
|
pool.normals.reserve(voxel_count * 4);
|
||||||
let mut uvs = Vec::<[f32; 2]>::with_capacity(voxel_count * 4);
|
pool.uvs.reserve(voxel_count * 4);
|
||||||
let mut indices = Vec::<u32>::with_capacity(voxel_count * 6);
|
pool.indices.reserve(voxel_count * 6);
|
||||||
|
|
||||||
|
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| {
|
let mut push_quad = |base: Vec3, size: Vec2, n: Vec3, u: Vec3, v: Vec3| {
|
||||||
let i0 = positions.len() as u32;
|
let i0 = positions.len() as u32;
|
||||||
@ -361,7 +367,7 @@ pub(crate) fn mesh_chunk(
|
|||||||
|
|
||||||
// Axes: 0→X, 1→Y, 2→Z. For each axis we process the negative and positive
|
// Axes: 0→X, 1→Y, 2→Z. For each axis we process the negative and positive
|
||||||
// faces (dir = −1 / +1).
|
// 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.
|
// 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) {
|
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),
|
(0, d) => (1, 2, Vec3::new(d as f32, 0.0, 0.0), Vec3::Y, Vec3::Z),
|
||||||
@ -386,15 +392,17 @@ pub(crate) fn mesh_chunk(
|
|||||||
let mut cell = [0i32; 3];
|
let mut cell = [0i32; 3];
|
||||||
let mut neighbor = [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;
|
neighbor[axis] = cell[axis] + dir;
|
||||||
|
|
||||||
cell [u_axis] = u as i32;
|
cell[u_axis] = u as i32;
|
||||||
cell [v_axis] = v as i32;
|
cell[v_axis] = v as i32;
|
||||||
neighbor[u_axis] = u as i32;
|
neighbor[u_axis] = u as i32;
|
||||||
neighbor[v_axis] = v as i32;
|
neighbor[v_axis] = v as i32;
|
||||||
|
|
||||||
if filled(cell[0], cell[1], cell[2]) && !filled(neighbor[0], neighbor[1], neighbor[2]) {
|
if filled(cell[0], cell[1], cell[2])
|
||||||
|
&& !filled(neighbor[0], neighbor[1], neighbor[2])
|
||||||
|
{
|
||||||
mask[idx(u, v)] = true;
|
mask[idx(u, v)] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -409,7 +417,10 @@ pub(crate) fn mesh_chunk(
|
|||||||
|
|
||||||
// Determine the rectangle width.
|
// Determine the rectangle width.
|
||||||
let mut width = 1;
|
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)]
|
||||||
|
&& !visited[idx(u0 + width, v0)]
|
||||||
|
{
|
||||||
width += 1;
|
width += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -417,7 +428,9 @@ pub(crate) fn mesh_chunk(
|
|||||||
let mut height = 1;
|
let mut height = 1;
|
||||||
'h: while v0 + height < N {
|
'h: while v0 + height < N {
|
||||||
for du in 0..width {
|
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)]
|
||||||
|
|| visited[idx(u0 + du, v0 + height)]
|
||||||
|
{
|
||||||
break 'h;
|
break 'h;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -466,19 +479,23 @@ pub(crate) fn mesh_chunk(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
|
let mut mesh = Mesh::new(
|
||||||
|
PrimitiveTopology::TriangleList,
|
||||||
|
RenderAssetUsages::default(),
|
||||||
|
);
|
||||||
mesh.insert_attribute(
|
mesh.insert_attribute(
|
||||||
Mesh::ATTRIBUTE_POSITION,
|
Mesh::ATTRIBUTE_POSITION,
|
||||||
VertexAttributeValues::Float32x3(positions),
|
VertexAttributeValues::Float32x3(positions.clone()),
|
||||||
);
|
);
|
||||||
mesh.insert_attribute(
|
mesh.insert_attribute(
|
||||||
Mesh::ATTRIBUTE_NORMAL,
|
Mesh::ATTRIBUTE_NORMAL,
|
||||||
VertexAttributeValues::Float32x3(normals),
|
VertexAttributeValues::Float32x3(normals.clone()),
|
||||||
);
|
);
|
||||||
mesh.insert_attribute(
|
mesh.insert_attribute(
|
||||||
Mesh::ATTRIBUTE_UV_0,
|
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)
|
Some(mesh)
|
||||||
}
|
}
|
||||||
|
|||||||
66
client/src/plugins/environment/systems/voxels/meshing_gpu.rs
Normal file
66
client/src/plugins/environment/systems/voxels/meshing_gpu.rs
Normal 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();
|
||||||
|
}
|
||||||
@ -4,8 +4,9 @@ pub mod octree;
|
|||||||
pub mod structure;
|
pub mod structure;
|
||||||
|
|
||||||
mod chunk;
|
mod chunk;
|
||||||
mod meshing;
|
|
||||||
pub mod render_chunks;
|
|
||||||
pub mod culling;
|
pub mod culling;
|
||||||
pub mod queue_systems;
|
|
||||||
pub mod lod;
|
pub mod lod;
|
||||||
|
mod meshing;
|
||||||
|
pub mod meshing_gpu;
|
||||||
|
pub mod queue_systems;
|
||||||
|
pub mod render_chunks;
|
||||||
|
|||||||
@ -1,27 +1,30 @@
|
|||||||
use std::collections::HashMap;
|
use crate::plugins::big_space::big_space_plugin::RootGrid;
|
||||||
use std::fmt::format;
|
use crate::plugins::environment::systems::voxels::meshing::mesh_chunk;
|
||||||
|
use crate::plugins::environment::systems::voxels::structure::*;
|
||||||
use bevy::pbr::wireframe::Wireframe;
|
use bevy::pbr::wireframe::Wireframe;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::render::mesh::Mesh;
|
use bevy::render::mesh::Mesh;
|
||||||
use big_space::prelude::GridCell;
|
use big_space::prelude::GridCell;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use crate::plugins::big_space::big_space_plugin::RootGrid;
|
use std::collections::HashMap;
|
||||||
use crate::plugins::environment::systems::voxels::meshing::mesh_chunk;
|
use std::fmt::format;
|
||||||
use crate::plugins::environment::systems::voxels::structure::*;
|
|
||||||
|
|
||||||
/// rebuilds meshes only for chunks flagged dirty by the octree
|
/// rebuilds meshes only for chunks flagged dirty by the octree
|
||||||
pub fn rebuild_dirty_chunks(
|
pub fn rebuild_dirty_chunks(
|
||||||
mut commands : Commands,
|
mut commands: Commands,
|
||||||
mut octrees : Query<&mut SparseVoxelOctree>,
|
mut octrees: Query<&mut SparseVoxelOctree>,
|
||||||
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_q: Query<(
|
||||||
|
Entity,
|
||||||
&Chunk,
|
&Chunk,
|
||||||
&Mesh3d,
|
&Mesh3d,
|
||||||
&MeshMaterial3d<StandardMaterial>,
|
&MeshMaterial3d<StandardMaterial>,
|
||||||
&ChunkLod)>,
|
&ChunkLod,
|
||||||
mut spawned : ResMut<SpawnedChunks>,
|
)>,
|
||||||
root : Res<RootGrid>,
|
mut spawned: ResMut<SpawnedChunks>,
|
||||||
|
mut pool: ResMut<MeshBufferPool>,
|
||||||
|
root: Res<RootGrid>,
|
||||||
) {
|
) {
|
||||||
// map ChunkKey → (entity, mesh-handle, material-handle)
|
// map ChunkKey → (entity, mesh-handle, material-handle)
|
||||||
let existing: HashMap<ChunkKey, (Entity, Handle<Mesh>, Handle<StandardMaterial>, u32)> =
|
let existing: HashMap<ChunkKey, (Entity, Handle<Mesh>, Handle<StandardMaterial>, u32)> =
|
||||||
@ -39,8 +42,7 @@ pub fn rebuild_dirty_chunks(
|
|||||||
let mut bufs = Vec::new();
|
let mut bufs = Vec::new();
|
||||||
for key in tree.dirty_chunks.iter().copied() {
|
for key in tree.dirty_chunks.iter().copied() {
|
||||||
let lod = existing.get(&key).map(|v| v.3).unwrap_or(0);
|
let lod = existing.get(&key).map(|v| v.3).unwrap_or(0);
|
||||||
let mut buf =
|
let mut buf = [[[None; CHUNK_SIZE as usize]; CHUNK_SIZE as usize]; CHUNK_SIZE as usize];
|
||||||
[[[None; CHUNK_SIZE as usize]; CHUNK_SIZE as usize]; CHUNK_SIZE as usize];
|
|
||||||
|
|
||||||
let half = tree.size * 0.5;
|
let half = tree.size * 0.5;
|
||||||
let step = tree.get_spacing_at_depth(tree.max_depth);
|
let step = tree.get_spacing_at_depth(tree.max_depth);
|
||||||
@ -85,7 +87,7 @@ pub fn rebuild_dirty_chunks(
|
|||||||
for (key, buf, origin, step, lod) in bufs {
|
for (key, buf, origin, step, lod) in bufs {
|
||||||
if let Some((ent, mesh_h, _mat_h, _)) = existing.get(&key).cloned() {
|
if let Some((ent, mesh_h, _mat_h, _)) = existing.get(&key).cloned() {
|
||||||
// update mesh in-place; keeps old asset id
|
// update mesh in-place; keeps old asset id
|
||||||
match mesh_chunk(&buf, origin, step, &tree) {
|
match mesh_chunk(&buf, origin, step, &tree, &mut pool) {
|
||||||
Some(new_mesh) => {
|
Some(new_mesh) => {
|
||||||
if let Some(mesh) = meshes.get_mut(&mesh_h) {
|
if let Some(mesh) = meshes.get_mut(&mesh_h) {
|
||||||
*mesh = new_mesh;
|
*mesh = new_mesh;
|
||||||
@ -98,7 +100,7 @@ pub fn rebuild_dirty_chunks(
|
|||||||
spawned.0.remove(&key);
|
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) {
|
||||||
// spawn brand-new chunk only if mesh has faces
|
// spawn brand-new chunk only if mesh has faces
|
||||||
let mesh_h = meshes.add(mesh);
|
let mesh_h = meshes.add(mesh);
|
||||||
let mat_h = materials.add(StandardMaterial::default());
|
let mat_h = materials.add(StandardMaterial::default());
|
||||||
@ -110,7 +112,11 @@ pub fn rebuild_dirty_chunks(
|
|||||||
MeshMaterial3d(mat_h.clone()),
|
MeshMaterial3d(mat_h.clone()),
|
||||||
Transform::default(),
|
Transform::default(),
|
||||||
GridCell::ZERO,
|
GridCell::ZERO,
|
||||||
Chunk { key, voxels: Vec::new(), dirty: false },
|
Chunk {
|
||||||
|
key,
|
||||||
|
voxels: Vec::new(),
|
||||||
|
dirty: false,
|
||||||
|
},
|
||||||
ChunkLod(lod),
|
ChunkLod(lod),
|
||||||
/*Wireframe,*/
|
/*Wireframe,*/
|
||||||
))
|
))
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use std::collections::{HashMap, HashSet, VecDeque};
|
|
||||||
use bevy::color::Color;
|
use bevy::color::Color;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use serde::{Serialize, Deserialize, Serializer, Deserializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
|
|
||||||
fn serialize_color<S>(color: &Color, serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize_color<S>(color: &Color, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where
|
where
|
||||||
@ -19,11 +19,13 @@ where
|
|||||||
Ok(Color::linear_rgba(arr[0], arr[1], arr[2], arr[3]))
|
Ok(Color::linear_rgba(arr[0], arr[1], arr[2], arr[3]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Represents a single voxel with a color.
|
/// Represents a single voxel with a color.
|
||||||
#[derive(Debug, Clone, Copy, Component, PartialEq, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, Component, PartialEq, Default, Serialize, Deserialize)]
|
||||||
pub struct Voxel {
|
pub struct Voxel {
|
||||||
#[serde(serialize_with = "serialize_color", deserialize_with = "deserialize_color")]
|
#[serde(
|
||||||
|
serialize_with = "serialize_color",
|
||||||
|
deserialize_with = "deserialize_color"
|
||||||
|
)]
|
||||||
pub color: Color,
|
pub color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,7 +46,6 @@ pub struct OctreeNode {
|
|||||||
/// Represents the root of the sparse voxel octree.
|
/// Represents the root of the sparse voxel octree.
|
||||||
#[derive(Debug, Component, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Component, Serialize, Deserialize, Clone)]
|
||||||
pub struct SparseVoxelOctree {
|
pub struct SparseVoxelOctree {
|
||||||
|
|
||||||
pub root: OctreeNode,
|
pub root: OctreeNode,
|
||||||
pub max_depth: u32,
|
pub max_depth: u32,
|
||||||
pub size: f32,
|
pub size: f32,
|
||||||
@ -77,13 +78,10 @@ impl OctreeNode {
|
|||||||
impl Voxel {
|
impl Voxel {
|
||||||
/// Creates a new empty octree node.
|
/// Creates a new empty octree node.
|
||||||
pub fn new(color: Color) -> Self {
|
pub fn new(color: Color) -> Self {
|
||||||
Self {
|
Self { color }
|
||||||
color,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub const NEIGHBOR_OFFSETS: [(f32, f32, f32); 6] = [
|
pub const NEIGHBOR_OFFSETS: [(f32, f32, f32); 6] = [
|
||||||
(-1.0, 0.0, 0.0), // Left
|
(-1.0, 0.0, 0.0), // Left
|
||||||
(1.0, 0.0, 0.0), // Right
|
(1.0, 0.0, 0.0), // Right
|
||||||
@ -93,7 +91,6 @@ pub const NEIGHBOR_OFFSETS: [(f32, f32, f32); 6] = [
|
|||||||
(0.0, 0.0, 1.0), // Front
|
(0.0, 0.0, 1.0), // Front
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Ray {
|
pub struct Ray {
|
||||||
pub origin: Vec3,
|
pub origin: Vec3,
|
||||||
@ -107,24 +104,21 @@ 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;
|
pub const CHUNK_POW: u32 = 4;
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct Chunk {
|
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,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component, Debug, Clone, Copy)]
|
#[derive(Component, Debug, Clone, Copy)]
|
||||||
pub struct ChunkLod(pub u32);
|
pub struct ChunkLod(pub u32);
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
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
|
/// maximum amount of *new* chunk meshes we are willing to create each frame
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
pub struct ChunkBudget {
|
pub struct ChunkBudget {
|
||||||
@ -149,8 +143,16 @@ pub struct SpawnedChunks(pub HashMap<ChunkKey, Entity>);
|
|||||||
|
|
||||||
/// how big the cube around the player is, measured in chunks
|
/// how big the cube around the player is, measured in chunks
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
pub struct ChunkCullingCfg { pub view_distance_chunks: i32 }
|
pub struct ChunkCullingCfg {
|
||||||
impl Default for ChunkCullingCfg { fn default() -> Self { Self { view_distance_chunks: 6 } } }
|
pub view_distance_chunks: i32,
|
||||||
|
}
|
||||||
|
impl Default for ChunkCullingCfg {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
view_distance_chunks: 6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Resource, Default)]
|
#[derive(Resource, Default)]
|
||||||
pub struct PrevCameraChunk(pub Option<ChunkKey>);
|
pub struct PrevCameraChunk(pub Option<ChunkKey>);
|
||||||
@ -172,3 +174,23 @@ impl ChunkOffsets {
|
|||||||
Self(offsets)
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user