// 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, } impl FaceDir { /// Returns the index identifying this direction's outward normal to the shader. /// /// The six values are a contract with the `FACE_NORMALS` table in `assets/shaders/cube.vert`, which is indexed by them directly. const fn to_index(self) -> u32 { match self { Self::PosX => 0, Self::NegX => 1, Self::PosY => 2, Self::NegY => 3, Self::PosZ => 4, Self::NegZ => 5, } } } /// 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 outward normal. dir: FaceDir, } /// The flat RGB albedo emitted for every face. const MATERIAL_COLOR: [f32; 3] = [0.2, 0.8, 0.2]; /// 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, Vec) { 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], vertices: &mut Vec, indices: &mut Vec, mut sample: impl FnMut(usize, usize, usize) -> Option, 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), key.dir.to_index(), ); }); } } /// 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], 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 packed face normal. /// /// A quad is planar, so all four vertices share `normal`. 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, indices: &mut Vec, corners: [[f32; 3]; 4], normal: u32, ) { #[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: MATERIAL_COLOR, normal, }); } indices.extend_from_slice(&[base, base + 1, base + 2, base + 2, base + 3, base]); } #[cfg(test)] #[path = "tests/meshing.rs"] mod tests;