Added Greedy Mesher + tracy profile

This commit is contained in:
Elias Stepanik 2025-06-08 06:50:47 +02:00
parent 922e99f937
commit 3440093284
8 changed files with 317 additions and 59 deletions

View File

@ -1,7 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run horror-game" type="CargoCommandRunConfiguration" factoryName="Cargo Command" singleton="false">
<option name="buildProfileId" value="dev" />
<option name="command" value="run --package horror-game --bin horror-game" />
<option name="buildProfileId" value="release" />
<option name="command" value="run --package horror-game --bin horror-game --features bevy/trace_tracy_memory" />
<option name="workingDirectory" value="file://$PROJECT_DIR$/client" />
<envs />
<option name="emulateTerminal" value="true" />

View File

@ -17,4 +17,5 @@ big_space = "0.9.1"
noise = "0.9.0"
itertools = "0.13.0"
bitvec = "1.0.1"
smallvec = "1.14.0"
smallvec = "1.14.0"
once_cell = "1.21.3"

View File

@ -23,7 +23,7 @@ impl Plugin for EnvironmentPlugin {
app.insert_resource(ChunkCullingCfg { view_distance_chunks: 10 });
app.insert_resource(ChunkBudget { per_frame: 20 });
app.add_systems(Update, log_mesh_count);
app
// ------------------------------------------------------------------------
// resources
@ -56,6 +56,11 @@ impl Plugin for EnvironmentPlugin {
}
}
fn log_mesh_count(meshes: Res<Assets<Mesh>>, time: Res<Time>) {
if time.delta_secs_f64() as i32 % 5 == 0 {
info!("meshes: {}", meshes.len());
}
}
fn should_visualize_octree(octree_query: Query<&SparseVoxelOctree>,) -> bool {
octree_query.single().show_wireframe

View File

@ -8,25 +8,34 @@ use crate::plugins::environment::systems::voxels::structure::*;
/// configured radius
pub fn despawn_distant_chunks(
mut commands : Commands,
cam_q : Query<&GlobalTransform, With<Camera>>,
tree_q : Query<&SparseVoxelOctree>,
mut spawned : ResMut<SpawnedChunks>,
chunk_q : Query<&Chunk>,
cfg : Res<ChunkCullingCfg>,
mut commands : Commands,
cam_q : Query<&GlobalTransform, With<Camera>>,
tree_q : Query<&SparseVoxelOctree>,
mut spawned : ResMut<SpawnedChunks>,
chunk_q : Query<(Entity,
&Chunk,
&Mesh3d,
&MeshMaterial3d<StandardMaterial>)>,
mut meshes : ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
cfg : Res<ChunkCullingCfg>,
) {
let tree = tree_q.single();
let cam = cam_q.single().translation();
let center = world_to_chunk(tree, cam);
let centre = world_to_chunk(tree, cam);
for chunk in chunk_q.iter() {
for (ent, chunk, mesh3d, mat3d) in chunk_q.iter() {
let ChunkKey(x, y, z) = chunk.key;
if (x - center.0).abs() > cfg.view_distance_chunks ||
(y - center.1).abs() > cfg.view_distance_chunks ||
(z - center.2).abs() > cfg.view_distance_chunks {
if let Some(ent) = spawned.0.remove(&chunk.key) {
commands.entity(ent).despawn_recursive();
}
if (x - centre.0).abs() > cfg.view_distance_chunks ||
(y - centre.1).abs() > cfg.view_distance_chunks ||
(z - centre.2).abs() > cfg.view_distance_chunks {
// free assets borrow, don't move
meshes.remove(&mesh3d.0);
materials.remove(&mat3d.0);
commands.entity(ent).despawn_recursive();
spawned.0.remove(&chunk.key);
}
}
}

View File

@ -269,3 +269,62 @@ pub fn world_to_chunk(tree: &SparseVoxelOctree, p: Vec3) -> ChunkKey {
)
}
impl AABB {
pub fn intersects_aabb(&self, other: &AABB) -> bool {
self.min.x <= other.max.x &&
self.max.x >= other.min.x &&
self.min.y <= other.max.y &&
self.max.y >= other.min.y &&
self.min.z <= other.max.z &&
self.max.z >= other.min.z
}
pub fn center(&self) -> Vec3 {
(self.min + self.max) * 0.5
}
}
impl SparseVoxelOctree {
pub fn collect_voxels_in_region(&self, min: Vec3, max: Vec3) -> Vec<(Vec3, Voxel)> {
let half_size = self.size * 0.5;
let root_bounds = AABB {
min: Vec3::new(-half_size, -half_size, -half_size),
max: Vec3::new(half_size, half_size, half_size),
};
let mut voxels = Vec::new();
self.collect_voxels_in_region_recursive(&self.root, root_bounds, min, max, &mut voxels);
voxels
}
fn collect_voxels_in_region_recursive(
&self,
node: &OctreeNode,
node_bounds: AABB,
min: Vec3,
max: Vec3,
out: &mut Vec<(Vec3, Voxel)>,
) {
if !node_bounds.intersects_aabb(&AABB { min, max }) {
return;
}
if node.is_leaf {
if let Some(voxel) = &node.voxel {
let center = node_bounds.center();
if center.x >= min.x && center.x <= max.x &&
center.y >= min.y && center.y <= max.y &&
center.z >= min.z && center.z <= max.z
{
out.push((center, *voxel));
}
}
}
if let Some(children) = &node.children {
for (i, child) in children.iter().enumerate() {
let child_bounds = self.compute_child_bounds(&node_bounds, i);
self.collect_voxels_in_region_recursive(child, child_bounds, min, max, out);
}
}
}
}

View File

@ -3,7 +3,7 @@ use bevy::prelude::*;
use bevy::render::mesh::{Indices, PrimitiveTopology, VertexAttributeValues, Mesh};
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],
origin: Vec3,
step: f32,
@ -296,4 +296,181 @@ pub(crate) fn mesh_chunk(
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, VertexAttributeValues::Float32x2(uvs));
mesh.insert_indices(Indices::U32(indices));
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,
) -> Mesh {
// ────────────────────────────────────────────────────────────────────────────
// Helpers
// ────────────────────────────────────────────────────────────────────────────
const N: usize = CHUNK_SIZE as usize;
// Safe voxel query that falls back to the octree for outofchunk requests.
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 (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.
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();
let mut push_quad = |base: Vec3, size: Vec2, n: Vec3, u: Vec3, v: Vec3| {
let i0 = positions.len() as u32;
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]]);
if n.x + n.y + n.z >= 0.0 {
indices.extend_from_slice(&[i0, i0 + 1, i0 + 2, i0 + 2, i0 + 3, i0]);
} else {
// Flip winding for faces with a negative normal component sum so the
// result is still counterclockwise.
indices.extend_from_slice(&[i0, i0 + 3, i0 + 2, i0 + 2, i0 + 1, i0]);
}
};
// ────────────────────────────────────────────────────────────────────────────
// Greedy meshing
// ────────────────────────────────────────────────────────────────────────────
// 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) ] {
// 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),
(1, d) => (2, 0, Vec3::new(0.0, d as f32, 0.0), Vec3::Z, Vec3::X),
(2, d) => (0, 1, Vec3::new(0.0, 0.0, d as f32), Vec3::X, Vec3::Y),
_ => unreachable!(),
};
// Iterate over every slice perpendicular to `axis`. Faces can lie on
// the 0…N grid lines (inclusive) because the positiveside faces of the
// last voxel sit at slice N.
for slice in 0..=N {
// Build the face mask for this slice.
let mut mask = vec![false; N * N];
let idx = |u: usize, v: usize| -> usize { u * N + v };
for u in 0..N {
for v in 0..N {
// Translate (u,v,slice) to (x,y,z) voxel coordinates.
let mut cell = [0i32; 3];
let mut neighbor = [0i32; 3];
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;
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;
}
}
}
// Greedy merge the mask into maximal rectangles.
let mut visited = vec![false; N * N];
for u0 in 0..N {
for v0 in 0..N {
if !mask[idx(u0, v0)] || visited[idx(u0, v0)] {
continue;
}
// Determine the rectangle width.
let mut width = 1;
while u0 + width < N && mask[idx(u0 + width, v0)] && !visited[idx(u0 + width, v0)] {
width += 1;
}
// Determine the rectangle height.
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)] {
break 'h;
}
}
height += 1;
}
// Mark the rectangle area as visited.
for du in 0..width {
for dv in 0..height {
visited[idx(u0 + du, v0 + dv)] = true;
}
}
// Compute worldspace base corner.
let mut base = origin;
match axis {
0 => {
base.x += step * slice as f32;
base.y += step * u0 as f32;
base.z += step * v0 as f32;
}
1 => {
base.x += step * v0 as f32;
base.y += step * slice as f32;
base.z += step * u0 as f32;
}
2 => {
base.x += step * u0 as f32;
base.y += step * v0 as f32;
base.z += step * slice as f32;
}
_ => unreachable!(),
}
let size = Vec2::new(width as f32 * step, height as f32 * step);
push_quad(base, size, face_normal, u_vec, v_vec);
}
}
}
}
// ────────────────────────────────────────────────────────────────────────────
// Final mesh assembly
// ────────────────────────────────────────────────────────────────────────────
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

@ -1,5 +1,6 @@
use std::collections::HashMap;
use std::fmt::format;
use bevy::pbr::wireframe::Wireframe;
use bevy::prelude::*;
use bevy::render::mesh::Mesh;
use big_space::prelude::GridCell;
@ -7,41 +8,41 @@ 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::*;
/// 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)>,
mut spawned : ResMut<SpawnedChunks>,
root: Res<RootGrid>,
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>)>,
mut spawned : ResMut<SpawnedChunks>,
root : Res<RootGrid>,
) {
// map ChunkKey → entity
let existing: HashMap<ChunkKey, Entity> =
chunk_q.iter().map(|(e, c)| (c.key, e)).collect();
// map ChunkKey → (entity, mesh-handle, material-handle)
let existing: HashMap<ChunkKey, (Entity, Handle<Mesh>, Handle<StandardMaterial>)> =
chunk_q
.iter()
.map(|(e, c, m, mat)| (c.key, (e, m.0.clone(), mat.0.clone())))
.collect();
for (_tree_ent, mut tree) in &mut octrees {
for 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();
//------------------------------------------------ collect voxel data
let mut bufs = 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(
let origin = 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,
@ -50,7 +51,7 @@ pub fn rebuild_dirty_chunks(
for lx in 0..CHUNK_SIZE {
for ly in 0..CHUNK_SIZE {
for lz in 0..CHUNK_SIZE {
let world = start
let world = origin
+ Vec3::new(lx as f32 * step, ly as f32 * step, lz as f32 * step);
if let Some(v) = tree.get_voxel_at_world_coords(world) {
buf[lx as usize][ly as usize][lz as usize] = Some(*v);
@ -59,28 +60,33 @@ pub fn rebuild_dirty_chunks(
}
}
chunk_voxel_bufs.push((key, buf, start, step));
bufs.push((key, buf, origin, 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);
//------------------------------------------------ create / update
for (key, buf, origin, step) in bufs {
if let Some((ent, mesh_h, _mat_h)) = existing.get(&key).cloned() {
// update mesh in-place; keeps old asset id
if let Some(mesh) = meshes.get_mut(&mesh_h) {
*mesh = mesh_chunk(&buf, origin, step, &tree);
}
spawned.0.insert(key, ent);
} else {
// spawn brand-new chunk
let mesh_h = meshes.add(mesh_chunk(&buf, origin, step, &tree));
let mat_h = materials.add(StandardMaterial::default());
commands.entity(root.0).with_children(|p| {
let e = p.spawn((
mesh_3d,
material,
Transform::default(),
GridCell::<i64>::ZERO,
Chunk { key, voxels: Vec::new(), dirty: false },
)).id();
let e = p
.spawn((
Mesh3d::from(mesh_h.clone()),
MeshMaterial3d(mat_h.clone()),
Transform::default(),
GridCell::<i64>::ZERO,
Chunk { key, voxels: Vec::new(), dirty: false },
/*Wireframe,*/
))
.id();
spawned.0.insert(key, e);
});
}

View File

@ -93,6 +93,7 @@ pub struct Chunk {
pub key: ChunkKey,
pub voxels: Vec<(IVec3, Voxel)>, // local coords 0‥15
pub dirty: bool,
}