Mega-buffer architecture replacing per-chunk GPU buffers: - Single StructuredBuffer<PackedQuad> for all chunks (2M quads, 16 MB) - StructuredBuffer<GPUChunkInfo> with per-chunk metadata (position, quad offsets, face groups) - VS reads chunk info via push constants (b999) for driver-safe chunk indexing - CPU frustum culling with wi::primitive::Frustum + AABB per chunk - Quads sorted by face direction in greedy mesher (faceOffsets/faceCounts) - GPU frustum + backface cull compute shader (voxelCullCS.hlsl) - GPU binary mesher compute shader baseline (voxelMeshCS.hlsl) - Indirect draw buffers and timestamp query infrastructure - README with build instructions and project architecture
238 lines
9.9 KiB
C++
238 lines
9.9 KiB
C++
#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
|