From 566f3b6d5afb7e2c45e510a0b2960ce1ebfdd680 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 24 Aug 2026 16:29:19 +0200 Subject: [PATCH] feat(core): add chunk mesh vertex and index buffer generation --- core/include/vf/Mesh.h | 48 +++++++++++ core/src/Mesh.cpp | 184 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 core/include/vf/Mesh.h create mode 100644 core/src/Mesh.cpp diff --git a/core/include/vf/Mesh.h b/core/include/vf/Mesh.h new file mode 100644 index 0000000..d645823 --- /dev/null +++ b/core/include/vf/Mesh.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include "World.h" +#include "Math.h" + +namespace vf { + +struct Vertex { + float px, py, pz; + float nx, ny, nz; + float u, v; + float tile; + float light = 1.0f; // baked sun+block light, 0..1 + float ao = 1.0f; // ambient occlusion, 0..1 (1 = unoccluded) +}; + +struct Mesh { + std::vector vertices; + std::vector indices; + + std::size_t quadCount() const { return vertices.size() / 4; } + bool empty() const { return vertices.empty(); } +}; + +// Opaque and transparent geometry for one chunk, kept apart so the renderer can +// draw the transparent pass last (depth write off, sorted back-to-front). +struct ChunkMesh { + Mesh opaque; + Mesh transparent; + bool empty() const { return opaque.empty() && transparent.empty(); } +}; + +// Greedy meshing (Lysenko sweep) for one chunk. Faces are culled against +// neighbouring blocks in `world` (crossing chunk borders correctly) and +// coplanar faces of the same block type are merged into rectangles. +Mesh greedyMeshChunk(const World& world, const Vec3i& chunkCoord); + +// Naive count of visible (culled but unmerged) faces for the same chunk. +// Used to prove greedy meshing actually reduces geometry. +int culledFaceCount(const World& world, const Vec3i& chunkCoord); + +// Classic per-vertex ambient occlusion level (0 darkest .. 3 lightest) from the +// two edge-adjacent voxels and the diagonal corner voxel around one face vertex. +int vertexAo(bool side1, bool side2, bool corner); + +} // namespace vf diff --git a/core/src/Mesh.cpp b/core/src/Mesh.cpp new file mode 100644 index 0000000..03619d6 --- /dev/null +++ b/core/src/Mesh.cpp @@ -0,0 +1,184 @@ +#include "vf/Mesh.h" + +namespace vf { + +namespace { + +struct MaskCell { + bool set = false; + bool backface = false; + Block block = Block::Air; + int tile = 0; + + bool operator==(const MaskCell& o) const { + return set == o.set && backface == o.backface && block == o.block && tile == o.tile; + } + bool operator!=(const MaskCell& o) const { return !(*this == o); } +}; + +Face faceFor(int axis, bool backface) { + switch (axis) { + case 0: return backface ? Face::NegX : Face::PosX; + case 1: return backface ? Face::NegY : Face::PosY; + default: return backface ? Face::NegZ : Face::PosZ; + } +} + +void emitQuad(Mesh& mesh, const Vec3& base, const Vec3& du, const Vec3& dv, const Vec3& normal, + int width, int height, float tile, bool backface) { + Vec3 p0 = base; + Vec3 p1 = base + du; + Vec3 p2 = base + du + dv; + Vec3 p3 = base + dv; + + uint32_t start = static_cast(mesh.vertices.size()); + float w = static_cast(width); + float h = static_cast(height); + + mesh.vertices.push_back({p0.x, p0.y, p0.z, normal.x, normal.y, normal.z, 0.0f, 0.0f, tile}); + mesh.vertices.push_back({p1.x, p1.y, p1.z, normal.x, normal.y, normal.z, w, 0.0f, tile}); + mesh.vertices.push_back({p2.x, p2.y, p2.z, normal.x, normal.y, normal.z, w, h, tile}); + mesh.vertices.push_back({p3.x, p3.y, p3.z, normal.x, normal.y, normal.z, 0.0f, h, tile}); + + if (backface) { + mesh.indices.insert(mesh.indices.end(), + {start, start + 2, start + 1, start, start + 3, start + 2}); + } else { + mesh.indices.insert(mesh.indices.end(), + {start, start + 1, start + 2, start, start + 2, start + 3}); + } +} + +} // namespace + +Mesh greedyMeshChunk(const World& world, const Vec3i& chunkCoord) { + Mesh mesh; + const int origin[3] = {chunkCoord.x * chunkSize, chunkCoord.y * chunkSize, + chunkCoord.z * chunkSize}; + const int dims[3] = {chunkSize, chunkSize, chunkSize}; + + auto voxel = [&](int lx, int ly, int lz) { + return world.blockAt(origin[0] + lx, origin[1] + ly, origin[2] + lz); + }; + + std::vector mask(chunkSize * chunkSize); + + for (int d = 0; d < 3; ++d) { + int u = (d + 1) % 3; + int v = (d + 2) % 3; + int x[3] = {0, 0, 0}; + int q[3] = {0, 0, 0}; + q[d] = 1; + + for (x[d] = -1; x[d] < dims[d];) { + int n = 0; + for (x[v] = 0; x[v] < dims[v]; ++x[v]) { + for (x[u] = 0; x[u] < dims[u]; ++x[u], ++n) { + Block a = voxel(x[0], x[1], x[2]); + Block b = voxel(x[0] + q[0], x[1] + q[1], x[2] + q[2]); + + bool aFace = (x[d] >= 0) && faceVisible(a, b); + bool bFace = (x[d] < dims[d] - 1) && faceVisible(b, a); + + MaskCell cell; + if (aFace && !bFace) { + cell.set = true; + cell.backface = false; + cell.block = a; + cell.tile = faceTile(a, faceFor(d, false)); + } else if (bFace && !aFace) { + cell.set = true; + cell.backface = true; + cell.block = b; + cell.tile = faceTile(b, faceFor(d, true)); + } + mask[n] = cell; + } + } + + ++x[d]; + + n = 0; + for (int j = 0; j < dims[v]; ++j) { + for (int i = 0; i < dims[u];) { + MaskCell current = mask[n]; + if (!current.set) { + ++i; + ++n; + continue; + } + + int width = 1; + while (i + width < dims[u] && mask[n + width] == current) ++width; + + int height = 1; + bool stop = false; + while (j + height < dims[v] && !stop) { + for (int k = 0; k < width; ++k) { + if (mask[n + k + height * dims[u]] != current) { + stop = true; + break; + } + } + if (!stop) ++height; + } + + x[u] = i; + x[v] = j; + + Vec3 base{static_cast(origin[0] + x[0]), + static_cast(origin[1] + x[1]), + static_cast(origin[2] + x[2])}; + Vec3 du{0, 0, 0}; + Vec3 dv{0, 0, 0}; + (&du.x)[u] = static_cast(width); + (&dv.x)[v] = static_cast(height); + + Vec3 normal{0, 0, 0}; + (&normal.x)[d] = current.backface ? -1.0f : 1.0f; + + emitQuad(mesh, base, du, dv, normal, width, height, + static_cast(current.tile), current.backface); + + for (int l = 0; l < height; ++l) + for (int k = 0; k < width; ++k) + mask[n + k + l * dims[u]] = MaskCell{}; + + i += width; + n += width; + } + } + } + } + + return mesh; +} + +int vertexAo(bool side1, bool side2, bool corner) { + if (side1 && side2) return 0; + return 3 - (static_cast(side1) + static_cast(side2) + static_cast(corner)); +} + +int culledFaceCount(const World& world, const Vec3i& chunkCoord) { + const int origin[3] = {chunkCoord.x * chunkSize, chunkCoord.y * chunkSize, + chunkCoord.z * chunkSize}; + static const int offsets[6][3] = {{1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, + {0, -1, 0}, {0, 0, 1}, {0, 0, -1}}; + int faces = 0; + for (int lx = 0; lx < chunkSize; ++lx) { + for (int ly = 0; ly < chunkSize; ++ly) { + for (int lz = 0; lz < chunkSize; ++lz) { + Block self = world.blockAt(origin[0] + lx, origin[1] + ly, origin[2] + lz); + if (isAir(self)) continue; + for (auto& o : offsets) { + Block other = world.blockAt(origin[0] + lx + o[0], origin[1] + ly + o[1], + origin[2] + lz + o[2]); + if (faceVisible(self, other)) ++faces; + } + } + } + } + return faces; +} + +} // namespace vf