#include "TopingSystem.h" #include "VoxelWorld.h" #include #include namespace voxel { // ── Edge definitions for +Y face ──────────────────────────────── // Each edge sits on one side of the unit square [0,1]² (the XZ plane at y=1). // The bevel strip runs along the edge, with a wedge cross-section // rising from the voxel face (y=1) to a peak (y=1+h) and sloping // inward by width w. // // Cross-section (looking along the strip): // // peak (edge, 1+h) // /| // / | // / | slope face (visible from above) // / | // / | outer wall (visible from the side) // / | // inner outer // (1-w,1) (edge,1) // struct EdgeDef { float sx, sz; // strip start point (on the voxel face) float ex, ez; // strip end point float ix, iz; // inward direction (unit, perpendicular to strip) float nx, nz; // outer wall normal (points outward) }; static const EdgeDef kEdges[4] = { // sx sz ex ez ix iz nx nz { 1.0f, 0.0f, 1.0f, 1.0f,-1.0f, 0.0f, 1.0f, 0.0f }, // bit 0: +X edge { 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,-1.0f, 0.0f }, // bit 1: -X edge { 0.0f, 1.0f, 1.0f, 1.0f, 0.0f,-1.0f, 0.0f, 1.0f }, // bit 2: +Z edge { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,-1.0f }, // bit 3: -Z edge }; // ── Helper: emit one triangle with auto-corrected winding ─────── // Compares the geometric normal (from vertex cross product) to the // desired shading normal. If they disagree, swaps B and C to flip // the winding so the triangle is front-facing (CCW in Wicked Engine). static void emitTri(std::vector& v, float ax, float ay, float az, float bx, float by, float bz, float cx, float cy, float cz, float nx, float ny, float nz) { // Geometric normal = cross(AB, AC) float abx = bx - ax, aby = by - ay, abz = bz - az; float acx = cx - ax, acy = cy - ay, acz = cz - az; float gnx = aby * acz - abz * acy; float gny = abz * acx - abx * acz; float gnz = abx * acy - aby * acx; // If geometric normal disagrees with desired normal, swap B↔C if (gnx * nx + gny * ny + gnz * nz > 0.0f) { v.push_back({ ax, ay, az, nx, ny, nz }); v.push_back({ cx, cy, cz, nx, ny, nz }); v.push_back({ bx, by, bz, nx, ny, nz }); } else { v.push_back({ ax, ay, az, nx, ny, nz }); v.push_back({ bx, by, bz, nx, ny, nz }); v.push_back({ cx, cy, cz, nx, ny, nz }); } } // ═════════════════════════════════════════════════════════════════ // TopingSystem implementation // ═════════════════════════════════════════════════════════════════ void TopingSystem::initialize() { registerDefs(); generateMeshes(); } // ── Register toping types ─────────────────────────────────────── void TopingSystem::registerDefs() { defs_.clear(); // Type 0: Stone bevel — clean angular ridge along open edges // Applied to stone (materialID=3), face +Y // Priority 0 (low): yields to grass at material boundaries { TopingDef def{}; def.materialID = 3; def.face = FACE_POS_Y; def.priority = 0; def.height = 0.06f; // subtle bevel def.width = 0.12f; def.segments = 1; // single smooth segment per edge defs_.push_back(def); } // Type 1: Grass edge — organic bumpy tufts along open edges // Applied to grass (materialID=1), face +Y // Priority 1 (high): generates bevels over stone at boundaries { TopingDef def{}; def.materialID = 1; def.face = FACE_POS_Y; def.priority = 1; def.height = 0.12f; // taller, more visible def.width = 0.18f; def.segments = 4; // subdivided for bumpy profile defs_.push_back(def); } } // ── Generate all 16 mesh variants per def ─────────────────────── void TopingSystem::generateMeshes() { vertices_.clear(); for (auto& def : defs_) { for (int bitmask = 0; bitmask < 16; bitmask++) { generateVariant(def, (uint8_t)bitmask); } } } // ── Generate mesh for one (def, bitmask) pair ─────────────────── // An UNSET bit means the edge is open → add bevel strip. // A SET bit means a neighbor is present → no bevel (toping connects). void TopingSystem::generateVariant(TopingDef& def, uint8_t bitmask) { const uint32_t startOffset = (uint32_t)vertices_.size(); for (int edge = 0; edge < 4; edge++) { if (bitmask & (1 << edge)) continue; // neighbor present → skip const EdgeDef& e = kEdges[edge]; const float w = def.width; const int segs = def.segments; // Build height profile along the strip std::vector heights(segs + 1); if (segs <= 1) { // Stone: constant height (smooth bevel) heights[0] = def.height; heights[1] = def.height; } else { // Grass: sinusoidal bumps, phase offset per edge for variety for (int j = 0; j <= segs; j++) { float t = (float)j / segs; float bump = sinf((t * 2.5f + edge * 0.31f) * 3.14159f); heights[j] = def.height * (0.5f + 0.5f * std::abs(bump)); if (heights[j] < 0.02f) heights[j] = 0.02f; } } // Strip direction const float dx = e.ex - e.sx; const float dz = e.ez - e.sz; for (int i = 0; i < segs; i++) { const float t0 = (float)i / segs; const float t1 = (float)(i + 1) / segs; const float h0 = heights[i]; const float h1 = heights[i + 1]; // Points at t0 along the strip (all at y=1, the voxel face) const float x0 = e.sx + t0 * dx; const float z0 = e.sz + t0 * dz; // Points at t1 const float x1 = e.sx + t1 * dx; const float z1 = e.sz + t1 * dz; // Cross-section at t0: // outerBot = (x0, 1, z0) — on the voxel face, at the edge // peak = (x0, 1+h0, z0) — raised at the edge // inner = (x0+w*ix, 1, z0+w*iz) — on the face, inward const float pk0y = 1.0f + h0; const float in0x = x0 + w * e.ix; const float in0z = z0 + w * e.iz; // Cross-section at t1: const float pk1y = 1.0f + h1; const float in1x = x1 + w * e.ix; const float in1z = z1 + w * e.iz; // ── Outer wall face (vertical, facing outward) ────── // Normal points outward: (nx, 0, nz) // Winding: emit both orderings — CW from outside view // Empirically CW = front-facing in our engine (see CLAUDE.md) emitTri(vertices_, x0, pk0y, z0, x1, pk1y, z1, x0, 1.0f, z0, e.nx, 0.0f, e.nz); emitTri(vertices_, x1, pk1y, z1, x1, 1.0f, z1, x0, 1.0f, z0, e.nx, 0.0f, e.nz); // ── Slope face (from peak down to inner edge) ─────── // Slope normal: the slope rises from inner (y=1) to peak (y=1+h) at the edge. // Since it tilts outward, the normal points INWARD and UP. // Normal = inward_dir * (h/L) + up * (w/L), where L = sqrt(h²+w²). const float avgH = (h0 + h1) * 0.5f; float slopeLen = sqrtf(avgH * avgH + w * w); if (slopeLen < 0.0001f) slopeLen = 1.0f; float cnx = e.ix * (avgH / slopeLen); float cny = w / slopeLen; float cnz = e.iz * (avgH / slopeLen); // Slope: peak → inner, strip direction // CW winding from normal direction emitTri(vertices_, x0, pk0y, z0, in0x, 1.0f, in0z, x1, pk1y, z1, cnx, cny, cnz); emitTri(vertices_, x1, pk1y, z1, in0x, 1.0f, in0z, in1x, 1.0f, in1z, cnx, cny, cnz); } } const uint32_t count = (uint32_t)vertices_.size() - startOffset; def.variants[bitmask] = { startOffset, count }; } // ── Collect toping instances from the world ───────────────────── // Scans every exposed voxel face that matches a registered TopingDef, // computes the 4-bit adjacency bitmask, and emits a TopingInstance. // // Currently only supports FACE_POS_Y (top face). For other faces, // the adjacency directions would need to be adapted to the face plane. void TopingSystem::collectInstances(const VoxelWorld& world) { instances_.clear(); // Quick lookup: material → toping def index (-1 if none) int8_t matToDef[256]; memset(matToDef, -1, sizeof(matToDef)); for (size_t i = 0; i < defs_.size(); i++) { matToDef[defs_[i].materialID] = (int8_t)i; } world.forEachChunk([&](const ChunkPos& cpos, const Chunk& chunk) { for (int z = 0; z < CHUNK_SIZE; z++) { for (int y = 0; y < CHUNK_SIZE; y++) { for (int x = 0; x < CHUNK_SIZE; x++) { const VoxelData& v = chunk.at(x, y, z); if (v.isEmpty()) continue; const uint8_t mat = v.getMaterialID(); const int8_t defIdx = matToDef[mat]; if (defIdx < 0) continue; const TopingDef& def = defs_[defIdx]; // World coordinates const int wx = cpos.x * CHUNK_SIZE + x; const int wy = cpos.y * CHUNK_SIZE + y; const int wz = cpos.z * CHUNK_SIZE + z; // Check if the target face is exposed (neighbor in face direction is empty) // Currently only FACE_POS_Y is supported if (def.face == FACE_POS_Y) { if (!world.getVoxel(wx, wy + 1, wz).isEmpty()) continue; // Face exposed. Compute 4-bit adjacency bitmask. // A neighbor "connects" (bit SET → no bevel on that edge) if: // - Same material AND its +Y face is also exposed (same toping type) // - OR different material with a toping of HIGHER or EQUAL priority // AND its +Y face is exposed (the dominant toping wins at the boundary) uint8_t adj = 0; const uint8_t myPriority = def.priority; auto checkNeighbor = [&](int nx, int nz) -> bool { uint8_t nMat = world.getVoxel(nx, wy, nz).getMaterialID(); if (nMat == 0) return false; // empty if (!world.getVoxel(nx, wy + 1, nz).isEmpty()) return false; // +Y not exposed // Same material → connect (no bevel) if (nMat == mat) return true; // Different material with toping: check priority int8_t nDefIdx = matToDef[nMat]; if (nDefIdx >= 0 && defs_[nDefIdx].priority >= myPriority) return true; return false; }; if (checkNeighbor(wx + 1, wz)) adj |= 1; // +X if (checkNeighbor(wx - 1, wz)) adj |= 2; // -X if (checkNeighbor(wx, wz + 1)) adj |= 4; // +Z if (checkNeighbor(wx, wz - 1)) adj |= 8; // -Z instances_.push_back({ (float)wx, (float)wy, (float)wz, (uint16_t)defIdx, adj }); } // TODO: support other face directions (FACE_NEG_Y, FACE_POS_X, etc.) // Each face direction needs different adjacency directions in its plane. } } } }); } } // namespace voxel