bvle-voxels/src/voxel/VoxelMesher.cpp

239 lines
9.9 KiB
C++
Raw Normal View History

#include "VoxelMesher.h"
#include <cstring>
#include <algorithm>
namespace voxel {
// ── Build binary masks per axis ─────────────────────────────────
// For each axis, solid[u][v] is a 32-bit mask where bit i=1 means
// voxel at position i along that axis is solid.
void VoxelMesher::buildAxisMasks(const Chunk& chunk, AxisMasks masks[3]) {
std::memset(masks, 0, sizeof(AxisMasks) * 3);
for (int z = 0; z < CHUNK_SIZE; z++) {
for (int y = 0; y < CHUNK_SIZE; y++) {
for (int x = 0; x < CHUNK_SIZE; x++) {
if (!chunk.at(x, y, z).isEmpty()) {
// X-axis: march along X, indexed by [Y][Z]
masks[0].solid[y][z] |= (1u << x);
// Y-axis: march along Y, indexed by [X][Z]
masks[1].solid[x][z] |= (1u << y);
// Z-axis: march along Z, indexed by [X][Y]
masks[2].solid[x][y] |= (1u << z);
}
}
}
}
}
// Helper: get voxel, considering neighbor chunks for boundary faces
static VoxelData getVoxelSafe(const Chunk& chunk, const VoxelWorld& world, int x, int y, int z) {
if (chunk.isInBounds(x, y, z)) {
return chunk.at(x, y, z);
}
// Cross-chunk lookup
int wx = chunk.pos.x * CHUNK_SIZE + x;
int wy = chunk.pos.y * CHUNK_SIZE + y;
int wz = chunk.pos.z * CHUNK_SIZE + z;
return world.getVoxel(wx, wy, wz);
}
// ── Greedy Merge ────────────────────────────────────────────────
// For a given face direction, merge visible faces of the same material
// into maximal rectangular quads.
void VoxelMesher::greedyMerge(
const Chunk& chunk,
const VoxelWorld& world,
uint8_t face,
const uint32_t faceMasks[CHUNK_SIZE][CHUNK_SIZE],
std::vector<PackedQuad>& outQuads
) {
// Determine axis mapping based on face
// face 0,1 = X axis -> iterate over Y,Z slices
// face 2,3 = Y axis -> iterate over X,Z slices
// face 4,5 = Z axis -> iterate over X,Y slices
// For each slice along the face normal axis
for (int depth = 0; depth < CHUNK_SIZE; depth++) {
// Build a 2D grid of material IDs for this slice
uint8_t matGrid[CHUNK_SIZE][CHUNK_SIZE];
bool visited[CHUNK_SIZE][CHUNK_SIZE];
std::memset(matGrid, 0, sizeof(matGrid));
std::memset(visited, 0, sizeof(visited));
int faceCount = 0;
for (int v = 0; v < CHUNK_SIZE; v++) {
for (int u = 0; u < CHUNK_SIZE; u++) {
// Check if this face is visible at this depth
bool faceVisible = false;
int x, y, z;
switch (face) {
case FACE_POS_X: x = depth; y = u; z = v; faceVisible = (faceMasks[u][v] >> depth) & 1; break;
case FACE_NEG_X: x = depth; y = u; z = v; faceVisible = (faceMasks[u][v] >> depth) & 1; break;
case FACE_POS_Y: y = depth; x = u; z = v; faceVisible = (faceMasks[u][v] >> depth) & 1; break;
case FACE_NEG_Y: y = depth; x = u; z = v; faceVisible = (faceMasks[u][v] >> depth) & 1; break;
case FACE_POS_Z: z = depth; x = u; y = v; faceVisible = (faceMasks[u][v] >> depth) & 1; break;
case FACE_NEG_Z: z = depth; x = u; y = v; faceVisible = (faceMasks[u][v] >> depth) & 1; break;
}
if (faceVisible) {
matGrid[v][u] = chunk.at(x, y, z).getMaterialID();
faceCount++;
}
}
}
if (faceCount == 0) continue;
// Greedy merge: scan row by row, merge same-material quads
for (int v = 0; v < CHUNK_SIZE; v++) {
for (int u = 0; u < CHUNK_SIZE; u++) {
if (visited[v][u] || matGrid[v][u] == 0) continue;
uint8_t mat = matGrid[v][u];
// Expand width (along u)
int w = 1;
while (u + w < CHUNK_SIZE && !visited[v][u + w] && matGrid[v][u + w] == mat) {
w++;
}
// Expand height (along v)
int h = 1;
bool canExpand = true;
while (v + h < CHUNK_SIZE && canExpand) {
for (int du = 0; du < w; du++) {
if (visited[v + h][u + du] || matGrid[v + h][u + du] != mat) {
canExpand = false;
break;
}
}
if (canExpand) h++;
}
// Mark as visited
for (int dv = 0; dv < h; dv++) {
for (int du = 0; du < w; du++) {
visited[v + dv][u + du] = true;
}
}
// Compute the actual position in chunk-local coords
uint8_t px, py, pz;
switch (face) {
case FACE_POS_X: px = (uint8_t)depth; py = (uint8_t)u; pz = (uint8_t)v; break;
case FACE_NEG_X: px = (uint8_t)depth; py = (uint8_t)u; pz = (uint8_t)v; break;
case FACE_POS_Y: px = (uint8_t)u; py = (uint8_t)depth; pz = (uint8_t)v; break;
case FACE_NEG_Y: px = (uint8_t)u; py = (uint8_t)depth; pz = (uint8_t)v; break;
case FACE_POS_Z: px = (uint8_t)u; py = (uint8_t)v; pz = (uint8_t)depth; break;
case FACE_NEG_Z: px = (uint8_t)u; py = (uint8_t)v; pz = (uint8_t)depth; break;
default: px = py = pz = 0; break;
}
// Width/height in the quad's local UV space
uint8_t qw, qh;
switch (face) {
case FACE_POS_X: case FACE_NEG_X: qw = (uint8_t)w; qh = (uint8_t)h; break;
case FACE_POS_Y: case FACE_NEG_Y: qw = (uint8_t)w; qh = (uint8_t)h; break;
case FACE_POS_Z: case FACE_NEG_Z: qw = (uint8_t)w; qh = (uint8_t)h; break;
default: qw = qh = 1; break;
}
outQuads.push_back(PackedQuad::create(
px, py, pz, qw, qh, face, mat, 0, 0
));
}
}
}
}
uint32_t VoxelMesher::meshChunk(Chunk& chunk, const VoxelWorld& world) {
chunk.quads.clear();
// Step 1: Build binary solid masks per axis
AxisMasks axisMasks[3];
buildAxisMasks(chunk, axisMasks);
// Step 2: For each face direction, compute visible face masks
// then do greedy merge
for (uint8_t face = 0; face < FACE_COUNT; face++) {
int axis = face / 2; // 0=X, 1=Y, 2=Z
bool positive = (face % 2 == 0);
// Compute visible face masks
// A face is visible if the voxel is solid and the neighbor in the face direction is air
uint32_t faceMasks[CHUNK_SIZE][CHUNK_SIZE];
std::memset(faceMasks, 0, sizeof(faceMasks));
for (int v = 0; v < CHUNK_SIZE; v++) {
for (int u = 0; u < CHUNK_SIZE; u++) {
uint32_t solid = axisMasks[axis].solid[u][v];
if (solid == 0) continue;
uint32_t visible;
if (positive) {
// +dir: face visible if solid here and NOT solid at pos+1
// Shift right: neighbor is at bit+1
uint32_t neighbor = (solid >> 1);
// The highest bit has no neighbor in this chunk - check boundary
visible = solid & ~neighbor;
// Bit 31 (chunk boundary): need to check neighbor chunk
// For now, always show boundary faces
if (solid & (1u << (CHUNK_SIZE - 1))) {
// Check if neighbor chunk's voxel is empty
int nx, ny, nz;
switch (axis) {
case 0: nx = CHUNK_SIZE; ny = u; nz = v; break; // X+
case 1: nx = u; ny = CHUNK_SIZE; nz = v; break; // Y+
case 2: nx = u; ny = v; nz = CHUNK_SIZE; break; // Z+
default: nx = ny = nz = 0;
}
if (!getVoxelSafe(chunk, world, nx, ny, nz).isEmpty()) {
visible &= ~(1u << (CHUNK_SIZE - 1)); // hide boundary face
}
}
} else {
// -dir: face visible if solid here and NOT solid at pos-1
uint32_t neighbor = (solid << 1);
visible = solid & ~neighbor;
// Bit 0 (chunk boundary)
if (solid & 1u) {
int nx, ny, nz;
switch (axis) {
case 0: nx = -1; ny = u; nz = v; break;
case 1: nx = u; ny = -1; nz = v; break;
case 2: nx = u; ny = v; nz = -1; break;
default: nx = ny = nz = 0;
}
if (!getVoxelSafe(chunk, world, nx, ny, nz).isEmpty()) {
visible &= ~1u; // hide boundary face
}
}
}
faceMasks[u][v] = visible;
}
}
uint32_t beforeCount = (uint32_t)chunk.quads.size();
greedyMerge(chunk, world, face, faceMasks, chunk.quads);
chunk.faceOffsets[face] = beforeCount;
chunk.faceCounts[face] = (uint32_t)chunk.quads.size() - beforeCount;
}
chunk.quadCount = (uint32_t)chunk.quads.size();
chunk.dirty = false;
return chunk.quadCount;
}
uint8_t VoxelMesher::calcAO(const VoxelWorld& world, const ChunkPos& cpos,
int x, int y, int z, uint8_t face) {
// Simplified AO: count occluding neighbors around the vertex
// Returns packed 4x2-bit AO for the 4 corners
// TODO: implement proper per-corner AO
return 0;
}
} // namespace voxel