Add mesh buffer pooling

This commit is contained in:
Elias Stepanik 2025-06-13 02:53:19 +02:00
parent fb3d60cb2d
commit 0cf98496ed
4 changed files with 137 additions and 83 deletions

View File

@ -1,28 +1,34 @@
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::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::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,
(
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,
),
);
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 +38,7 @@ impl Plugin for EnvironmentPlugin {
// ------------------------------------------------------------------------
.init_resource::<ChunkQueue>()
.init_resource::<SpawnedChunks>()
.init_resource::<MeshBufferPool>()
// ------------------------------------------------------------------------
// frame update
// ------------------------------------------------------------------------
@ -42,8 +49,7 @@ 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
/* ---------- optional debug drawing ------- */
visualize_octree_system
.run_if(should_visualize_octree)
@ -52,9 +58,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 +70,15 @@ 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
}
}

View File

@ -1,7 +1,7 @@
use crate::plugins::environment::systems::voxels::structure::*;
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 +96,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 +298,12 @@ 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,
) -> Option<Mesh> {
// ────────────────────────────────────────────────────────────────────────────
// Helpers
@ -328,12 +328,18 @@ pub(crate) fn mesh_chunk(
// 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 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 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
// 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),
@ -386,15 +392,17 @@ 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]) {
if filled(cell[0], cell[1], cell[2])
&& !filled(neighbor[0], neighbor[1], neighbor[2])
{
mask[idx(u, v)] = true;
}
}
@ -409,7 +417,10 @@ pub(crate) fn mesh_chunk(
// 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)]
&& !visited[idx(u0 + width, v0)]
{
width += 1;
}
@ -417,7 +428,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)]
|| visited[idx(u0 + du, v0 + height)]
{
break 'h;
}
}
@ -466,19 +479,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

@ -1,27 +1,30 @@
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 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>,
) {
// map ChunkKey → (entity, mesh-handle, material-handle)
let existing: HashMap<ChunkKey, (Entity, Handle<Mesh>, Handle<StandardMaterial>, u32)> =
@ -39,8 +42,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 +87,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) {
Some(new_mesh) => {
if let Some(mesh) = meshes.get_mut(&mesh_h) {
*mesh = new_mesh;
@ -98,10 +100,10 @@ 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) {
// 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::default());
commands.entity(root.0).with_children(|p| {
let e = p
@ -110,7 +112,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 +128,4 @@ pub fn rebuild_dirty_chunks(
tree.clear_dirty_flags();
}
}
}

View File

@ -1,7 +1,7 @@
use std::collections::{HashMap, HashSet, VecDeque};
use bevy::color::Color;
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>
where
@ -19,11 +19,13 @@ where
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)]
pub struct Voxel {
#[serde(serialize_with = "serialize_color", deserialize_with = "deserialize_color")]
#[serde(
serialize_with = "serialize_color",
deserialize_with = "deserialize_color"
)]
pub color: Color,
}
@ -44,7 +46,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,
@ -77,13 +78,10 @@ impl OctreeNode {
impl Voxel {
/// Creates a new empty octree node.
pub fn new(color: Color) -> Self {
Self {
color,
}
Self { color }
}
}
pub const NEIGHBOR_OFFSETS: [(f32, f32, f32); 6] = [
(-1.0, 0.0, 0.0), // Left
(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
];
#[derive(Debug)]
pub struct Ray {
pub origin: Vec3,
@ -106,25 +103,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 +126,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 +134,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 +143,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 +174,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();
}
}