406 lines
14 KiB
Rust
406 lines
14 KiB
Rust
// SPDX-License-Identifier: AGPL-3.0-only
|
||
|
||
//! Cubic greedy mesher: converts a dense voxel [`Chunk`] into renderer geometry.
|
||
|
||
use crate::vertex::Vertex;
|
||
use shared::world::{BlockId, CHUNK_SIZE, Chunk};
|
||
|
||
/// The signed direction a face points along one of the three axes.
|
||
///
|
||
/// The sign is part of the merge key: two faces on the same plane but pointing in opposite directions (for example a top face and the bottom face directly above it) must never merge, so `PosY` and `NegY` are distinct variants.
|
||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||
enum FaceDir {
|
||
/// Points toward increasing X.
|
||
PosX,
|
||
/// Points toward decreasing X.
|
||
NegX,
|
||
/// Points toward increasing Y (upward).
|
||
PosY,
|
||
/// Points toward decreasing Y (downward).
|
||
NegY,
|
||
/// Points toward increasing Z.
|
||
PosZ,
|
||
/// Points toward decreasing Z.
|
||
NegZ,
|
||
}
|
||
|
||
/// Identifies whether two faces are mergeable.
|
||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||
struct FaceKey {
|
||
/// The material of the voxel owning the face.
|
||
block: BlockId,
|
||
/// The face's signed axis direction, which selects its colour.
|
||
dir: FaceDir,
|
||
}
|
||
|
||
/// Returns the flat RGB colour for a face pointing in `dir`.
|
||
///
|
||
/// The values reproduce the previous per-face emitter exactly so the rendered output is unchanged.
|
||
const fn color_of(dir: FaceDir) -> [f32; 3] {
|
||
match dir {
|
||
FaceDir::PosY => [0.2, 0.8, 0.2],
|
||
FaceDir::NegY => [0.1, 0.4, 0.1],
|
||
FaceDir::PosX | FaceDir::NegX => [0.15, 0.6, 0.15],
|
||
FaceDir::PosZ | FaceDir::NegZ => [0.18, 0.7, 0.18],
|
||
}
|
||
}
|
||
|
||
/// Converts a chunk-local integer coordinate to its floating-point value.
|
||
#[expect(
|
||
clippy::cast_precision_loss,
|
||
reason = "chunk-local coordinates never exceed CHUNK_SIZE (32) and are exact as f32"
|
||
)]
|
||
const fn coord(i: usize) -> f32 {
|
||
i as f32
|
||
}
|
||
|
||
/// The six face-adjacent neighbour chunks, if resident.
|
||
///
|
||
/// A `None` side means the neighbour is not loaded; that boundary is treated as exposed (its faces are emitted) so the load frontier shows no holes. The referenced chunks must outlive the [`Neighbors`] value, which is what the `'a` lifetime records.
|
||
#[derive(Default)]
|
||
pub struct Neighbors<'a> {
|
||
/// Neighbour toward decreasing X, sampled at its `x = CHUNK_SIZE - 1` face.
|
||
pub neg_x: Option<&'a Chunk>,
|
||
/// Neighbour toward increasing X, sampled at its `x = 0` face.
|
||
pub pos_x: Option<&'a Chunk>,
|
||
/// Neighbour toward decreasing Y, sampled at its `y = CHUNK_SIZE - 1` face.
|
||
pub neg_y: Option<&'a Chunk>,
|
||
/// Neighbour toward increasing Y, sampled at its `y = 0` face.
|
||
pub pos_y: Option<&'a Chunk>,
|
||
/// Neighbour toward decreasing Z, sampled at its `z = CHUNK_SIZE - 1` face.
|
||
pub neg_z: Option<&'a Chunk>,
|
||
/// Neighbour toward increasing Z, sampled at its `z = 0` face.
|
||
pub pos_z: Option<&'a Chunk>,
|
||
}
|
||
|
||
/// Returns the block occluding the `dir` face of the voxel at (`x`, `y`, `z`).
|
||
///
|
||
/// When the adjacent voxel lies inside the chunk it is read directly. When it lies across the chunk boundary it is read from the matching entry of `neighbors` at the opposite edge; a `None` neighbour is treated as [`BlockId::AIR`] so the boundary face is emitted (frontier safety).
|
||
fn occluder(
|
||
chunk: &Chunk,
|
||
neighbors: &Neighbors,
|
||
x: usize,
|
||
y: usize,
|
||
z: usize,
|
||
dir: FaceDir,
|
||
) -> BlockId {
|
||
const LAST: usize = CHUNK_SIZE - 1;
|
||
match dir {
|
||
FaceDir::PosX => {
|
||
if x < LAST {
|
||
chunk.get(x + 1, y, z)
|
||
} else {
|
||
neighbors.pos_x.map_or(BlockId::AIR, |c| c.get(0, y, z))
|
||
}
|
||
}
|
||
FaceDir::NegX => {
|
||
if x > 0 {
|
||
chunk.get(x - 1, y, z)
|
||
} else {
|
||
neighbors.neg_x.map_or(BlockId::AIR, |c| c.get(LAST, y, z))
|
||
}
|
||
}
|
||
FaceDir::PosY => {
|
||
if y < LAST {
|
||
chunk.get(x, y + 1, z)
|
||
} else {
|
||
neighbors.pos_y.map_or(BlockId::AIR, |c| c.get(x, 0, z))
|
||
}
|
||
}
|
||
FaceDir::NegY => {
|
||
if y > 0 {
|
||
chunk.get(x, y - 1, z)
|
||
} else {
|
||
neighbors.neg_y.map_or(BlockId::AIR, |c| c.get(x, LAST, z))
|
||
}
|
||
}
|
||
FaceDir::PosZ => {
|
||
if z < LAST {
|
||
chunk.get(x, y, z + 1)
|
||
} else {
|
||
neighbors.pos_z.map_or(BlockId::AIR, |c| c.get(x, y, 0))
|
||
}
|
||
}
|
||
FaceDir::NegZ => {
|
||
if z > 0 {
|
||
chunk.get(x, y, z - 1)
|
||
} else {
|
||
neighbors.neg_z.map_or(BlockId::AIR, |c| c.get(x, y, LAST))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Meshes `chunk` into GPU vertices and triangle indices via greedy merging.
|
||
///
|
||
/// Each axis is swept slice by slice; on every slice a 2D mask of exposed faces over the two perpendicular axes is built and merged into rectangles. Boundary voxels are tested against `neighbors`: a chunk-edge face is emitted only when the adjoining voxel in the matching neighbour is air, or when that neighbour is absent (see [`Neighbors`]).
|
||
#[must_use]
|
||
#[expect(
|
||
clippy::too_many_lines,
|
||
reason = "six directional passes, each an inline sample + corners closure pair"
|
||
)]
|
||
pub fn generate_mesh(chunk: &Chunk, neighbors: &Neighbors) -> (Vec<Vertex>, Vec<u32>) {
|
||
let mut vertices = Vec::new();
|
||
let mut indices = Vec::new();
|
||
// A single u×v mask, reused across every slice of every axis; each pass fully overwrites it per slice, so no explicit clearing is required.
|
||
let mut mask = vec![None; CHUNK_SIZE * CHUNK_SIZE];
|
||
|
||
// +Y (top): slice = y, mask u = x, mask v = z.
|
||
run_pass(
|
||
&mut mask,
|
||
&mut vertices,
|
||
&mut indices,
|
||
|y, x, z| {
|
||
let block = chunk.get(x, y, z);
|
||
(block != BlockId::AIR
|
||
&& occluder(chunk, neighbors, x, y, z, FaceDir::PosY) == BlockId::AIR)
|
||
.then_some(FaceKey {
|
||
block,
|
||
dir: FaceDir::PosY,
|
||
})
|
||
},
|
||
|y, x0, z0, w, h| {
|
||
let (xmin, xmax) = (coord(x0) - 0.5, coord(x0 + w) - 0.5);
|
||
let (zmin, zmax) = (coord(z0) - 0.5, coord(z0 + h) - 0.5);
|
||
let yp = coord(y) + 0.5;
|
||
[
|
||
[xmin, yp, zmax],
|
||
[xmax, yp, zmax],
|
||
[xmax, yp, zmin],
|
||
[xmin, yp, zmin],
|
||
]
|
||
},
|
||
);
|
||
|
||
// -Y (bottom): slice = y, mask u = x, mask v = z.
|
||
run_pass(
|
||
&mut mask,
|
||
&mut vertices,
|
||
&mut indices,
|
||
|y, x, z| {
|
||
let block = chunk.get(x, y, z);
|
||
(block != BlockId::AIR
|
||
&& occluder(chunk, neighbors, x, y, z, FaceDir::NegY) == BlockId::AIR)
|
||
.then_some(FaceKey {
|
||
block,
|
||
dir: FaceDir::NegY,
|
||
})
|
||
},
|
||
|y, x0, z0, w, h| {
|
||
let (xmin, xmax) = (coord(x0) - 0.5, coord(x0 + w) - 0.5);
|
||
let (zmin, zmax) = (coord(z0) - 0.5, coord(z0 + h) - 0.5);
|
||
let yp = coord(y) - 0.5;
|
||
[
|
||
[xmin, yp, zmin],
|
||
[xmax, yp, zmin],
|
||
[xmax, yp, zmax],
|
||
[xmin, yp, zmax],
|
||
]
|
||
},
|
||
);
|
||
|
||
// +X: slice = x, mask u = z, mask v = y.
|
||
run_pass(
|
||
&mut mask,
|
||
&mut vertices,
|
||
&mut indices,
|
||
|x, z, y| {
|
||
let block = chunk.get(x, y, z);
|
||
(block != BlockId::AIR
|
||
&& occluder(chunk, neighbors, x, y, z, FaceDir::PosX) == BlockId::AIR)
|
||
.then_some(FaceKey {
|
||
block,
|
||
dir: FaceDir::PosX,
|
||
})
|
||
},
|
||
|x, z0, y0, w, h| {
|
||
let (zmin, zmax) = (coord(z0) - 0.5, coord(z0 + w) - 0.5);
|
||
let (ymin, ymax) = (coord(y0) - 0.5, coord(y0 + h) - 0.5);
|
||
let xp = coord(x) + 0.5;
|
||
[
|
||
[xp, ymin, zmax],
|
||
[xp, ymin, zmin],
|
||
[xp, ymax, zmin],
|
||
[xp, ymax, zmax],
|
||
]
|
||
},
|
||
);
|
||
|
||
// -X: slice = x, mask u = z, mask v = y.
|
||
run_pass(
|
||
&mut mask,
|
||
&mut vertices,
|
||
&mut indices,
|
||
|x, z, y| {
|
||
let block = chunk.get(x, y, z);
|
||
(block != BlockId::AIR
|
||
&& occluder(chunk, neighbors, x, y, z, FaceDir::NegX) == BlockId::AIR)
|
||
.then_some(FaceKey {
|
||
block,
|
||
dir: FaceDir::NegX,
|
||
})
|
||
},
|
||
|x, z0, y0, w, h| {
|
||
let (zmin, zmax) = (coord(z0) - 0.5, coord(z0 + w) - 0.5);
|
||
let (ymin, ymax) = (coord(y0) - 0.5, coord(y0 + h) - 0.5);
|
||
let xp = coord(x) - 0.5;
|
||
[
|
||
[xp, ymin, zmin],
|
||
[xp, ymin, zmax],
|
||
[xp, ymax, zmax],
|
||
[xp, ymax, zmin],
|
||
]
|
||
},
|
||
);
|
||
|
||
// +Z: slice = z, mask u = x, mask v = y.
|
||
run_pass(
|
||
&mut mask,
|
||
&mut vertices,
|
||
&mut indices,
|
||
|z, x, y| {
|
||
let block = chunk.get(x, y, z);
|
||
(block != BlockId::AIR
|
||
&& occluder(chunk, neighbors, x, y, z, FaceDir::PosZ) == BlockId::AIR)
|
||
.then_some(FaceKey {
|
||
block,
|
||
dir: FaceDir::PosZ,
|
||
})
|
||
},
|
||
|z, x0, y0, w, h| {
|
||
let (xmin, xmax) = (coord(x0) - 0.5, coord(x0 + w) - 0.5);
|
||
let (ymin, ymax) = (coord(y0) - 0.5, coord(y0 + h) - 0.5);
|
||
let zp = coord(z) + 0.5;
|
||
[
|
||
[xmin, ymin, zp],
|
||
[xmax, ymin, zp],
|
||
[xmax, ymax, zp],
|
||
[xmin, ymax, zp],
|
||
]
|
||
},
|
||
);
|
||
|
||
// -Z: slice = z, mask u = x, mask v = y.
|
||
run_pass(
|
||
&mut mask,
|
||
&mut vertices,
|
||
&mut indices,
|
||
|z, x, y| {
|
||
let block = chunk.get(x, y, z);
|
||
(block != BlockId::AIR
|
||
&& occluder(chunk, neighbors, x, y, z, FaceDir::NegZ) == BlockId::AIR)
|
||
.then_some(FaceKey {
|
||
block,
|
||
dir: FaceDir::NegZ,
|
||
})
|
||
},
|
||
|z, x0, y0, w, h| {
|
||
let (xmin, xmax) = (coord(x0) - 0.5, coord(x0 + w) - 0.5);
|
||
let (ymin, ymax) = (coord(y0) - 0.5, coord(y0 + h) - 0.5);
|
||
let zp = coord(z) - 0.5;
|
||
[
|
||
[xmax, ymin, zp],
|
||
[xmin, ymin, zp],
|
||
[xmin, ymax, zp],
|
||
[xmax, ymax, zp],
|
||
]
|
||
},
|
||
);
|
||
|
||
(vertices, indices)
|
||
}
|
||
|
||
/// Runs one directional meshing pass over all `CHUNK_SIZE` slices.
|
||
///
|
||
/// `sample(slice, u, v)` returns the [`FaceKey`] for the face at mask cell `(u, v)` of `slice`, or `None` when no face is exposed there. `corners(slice, u0, v0, w, h)` yields the four world-space corners, ordered counter-clockwise as seen from outside the face, of a merged rectangle rooted at `(u0, v0)` with width `w` along `u` and height `h` along `v`.
|
||
fn run_pass(
|
||
mask: &mut [Option<FaceKey>],
|
||
vertices: &mut Vec<Vertex>,
|
||
indices: &mut Vec<u32>,
|
||
mut sample: impl FnMut(usize, usize, usize) -> Option<FaceKey>,
|
||
corners: impl Fn(usize, usize, usize, usize, usize) -> [[f32; 3]; 4],
|
||
) {
|
||
for slice in 0..CHUNK_SIZE {
|
||
for v in 0..CHUNK_SIZE {
|
||
for u in 0..CHUNK_SIZE {
|
||
mask[u + v * CHUNK_SIZE] = sample(slice, u, v);
|
||
}
|
||
}
|
||
|
||
merge_mask(mask, |key, u0, v0, w, h| {
|
||
push_quad(
|
||
vertices,
|
||
indices,
|
||
corners(slice, u0, v0, w, h),
|
||
color_of(key.dir),
|
||
);
|
||
});
|
||
}
|
||
}
|
||
|
||
/// Greedily covers the exposed cells of `mask` with maximal rectangles.
|
||
///
|
||
/// Cells are scanned row-major. At the first exposed, unconsumed cell the run is extended along `u` while the key matches, then along `v` while every cell of the next row over the current width matches. The covered cells are marked consumed (set to `None`) so they are not re-emitted, and `emit(key, u0, v0, w, h)` is called once for the rectangle.
|
||
fn merge_mask(
|
||
mask: &mut [Option<FaceKey>],
|
||
mut emit: impl FnMut(FaceKey, usize, usize, usize, usize),
|
||
) {
|
||
for v in 0..CHUNK_SIZE {
|
||
for u in 0..CHUNK_SIZE {
|
||
let Some(key) = mask[u + v * CHUNK_SIZE] else {
|
||
continue;
|
||
};
|
||
|
||
// Extend width along u while the key is unbroken.
|
||
let mut w = 1;
|
||
while u + w < CHUNK_SIZE && mask[(u + w) + v * CHUNK_SIZE] == Some(key) {
|
||
w += 1;
|
||
}
|
||
|
||
// Extend height along v while every cell of the next row matches over [0, w).
|
||
let mut h = 1;
|
||
'grow: while v + h < CHUNK_SIZE {
|
||
for du in 0..w {
|
||
if mask[(u + du) + (v + h) * CHUNK_SIZE] != Some(key) {
|
||
break 'grow;
|
||
}
|
||
}
|
||
h += 1;
|
||
}
|
||
|
||
// Consume the covered rectangle so its cells are not re-emitted.
|
||
for dv in 0..h {
|
||
for du in 0..w {
|
||
mask[(u + du) + (v + dv) * CHUNK_SIZE] = None;
|
||
}
|
||
}
|
||
|
||
emit(key, u, v, w, h);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Appends one quad (four vertices, six indices) with the given corners and colour.
|
||
///
|
||
/// Indices wind the two triangles as `[base, base+1, base+2, base+2, base+3, base]`, matching the corner ordering supplied by the caller.
|
||
fn push_quad(
|
||
vertices: &mut Vec<Vertex>,
|
||
indices: &mut Vec<u32>,
|
||
corners: [[f32; 3]; 4],
|
||
color: [f32; 3],
|
||
) {
|
||
#[expect(
|
||
clippy::cast_possible_truncation,
|
||
reason = "a chunk mesh holds far fewer than u32::MAX vertices"
|
||
)]
|
||
let base = vertices.len() as u32;
|
||
for position in corners {
|
||
vertices.push(Vertex { position, color });
|
||
}
|
||
indices.extend_from_slice(&[base, base + 1, base + 2, base + 2, base + 3, base]);
|
||
}
|
||
|
||
#[cfg(test)]
|
||
#[path = "tests/meshing.rs"]
|
||
mod tests;
|