#include "TopingSystem.h" #include "VoxelWorld.h" #include #include namespace voxel { static constexpr float PI = 3.14159265f; // ── 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 }; // ── Corner definitions ─────────────────────────────────────────── // Each corner of the +Y face is shared by two edges. When BOTH edges // are open (bits unset), a corner fill triangle closes the gap between // the two bevel strips. When only one edge is open, a cap triangle // closes the strip end. struct CornerDef { float cx, cz; // corner position in [0,1]^2 int bitA, bitB; // edge bits that share this corner }; static const CornerDef kCorners[4] = { { 1.0f, 0.0f, 0, 3 }, // +X start / -Z end { 1.0f, 1.0f, 0, 2 }, // +X end / +Z end { 0.0f, 1.0f, 1, 2 }, // -X end / +Z start { 0.0f, 0.0f, 1, 3 }, // -X start / -Z start }; // For each edge: which other edge shares its start/end corner static const int kStartNeighbor[4] = { 3, 3, 1, 1 }; static const int kEndNeighbor[4] = { 2, 2, 0, 0 }; // ── 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 (CW 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 }); } } // ── Deterministic hash for pseudo-random blade variety ─────────── // Returns a value in [0,1) from integer inputs. Golden ratio based. static float hashF(int a, int b, int c) { float x = (float)(a * 127 + b * 311 + c * 523) * 0.6180339887f; return x - floorf(x); } // ── Emit a single grass blade (2 segments, double-sided) ──────── // The blade is a tapered ribbon curving outward from the voxel face. // 2 segments give curvature; double-sided for backface-culled PSO. // // midLeanRatio controls the curvature profile: // low (0.05-0.15): mostly straight bottom, sharp curve at top // high (0.30-0.50): curves early from base, droopy look // static void emitBlade(std::vector& verts, float px, float pz, // base position on voxel face (y=1) float outX, float outZ, // outward direction from edge (normalized) float height, float baseW, float lean, float angleDeg, float midLeanRatio) // 0.0-0.5, curvature shape { // Rotate outward direction by angle to get lean direction float angleRad = angleDeg * PI / 180.0f; float ca = cosf(angleRad), sa = sinf(angleRad); float lx = outX * ca - outZ * sa; float lz = outX * sa + outZ * ca; // Width direction (perpendicular to lean in XZ plane) float wx = -lz, wz = lx; // 3 cross-sections with variable curvature struct Section { float x, y, z, hw; }; Section secs[3] = { { px, 1.0f, pz, baseW * 0.50f }, { px + lean * midLeanRatio * lx, 1.0f + height * 0.5f, pz + lean * midLeanRatio * lz, baseW * 0.32f }, { px + lean * lx, 1.0f + height, pz + lean * lz, baseW * 0.08f }, }; for (int s = 0; s < 2; s++) { const auto& s0 = secs[s]; const auto& s1 = secs[s + 1]; float l0x = s0.x - s0.hw * wx, l0z = s0.z - s0.hw * wz; float r0x = s0.x + s0.hw * wx, r0z = s0.z + s0.hw * wz; float l1x = s1.x - s1.hw * wx, l1z = s1.z - s1.hw * wz; float r1x = s1.x + s1.hw * wx, r1z = s1.z + s1.hw * wz; float tx = s1.x - s0.x, ty = s1.y - s0.y, tz = s1.z - s0.z; float nx = ty * wz; float ny = tz * wx - tx * wz; float nz = -ty * wx; float nlen = sqrtf(nx * nx + ny * ny + nz * nz); if (nlen > 1e-6f) { nx /= nlen; ny /= nlen; nz /= nlen; } else { nx = lx; ny = 0; nz = lz; } // Front face emitTri(verts, l0x, s0.y, l0z, r0x, s0.y, r0z, l1x, s1.y, l1z, nx, ny, nz); emitTri(verts, r0x, s0.y, r0z, r1x, s1.y, r1z, l1x, s1.y, l1z, nx, ny, nz); // Back face (flipped normal — emitTri auto-corrects winding) emitTri(verts, l0x, s0.y, l0z, r0x, s0.y, r0z, l1x, s1.y, l1z, -nx, -ny, -nz); emitTri(verts, r0x, s0.y, r0z, r1x, s1.y, r1z, l1x, s1.y, l1z, -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 { TopingDef def{}; def.materialID = 3; def.face = FACE_POS_Y; def.priority = 0; def.height = 0.06f; def.width = 0.12f; def.segments = 1; defs_.push_back(def); } // Type 1: Grass blades — individual grass blades along open edges { TopingDef def{}; def.materialID = 1; def.face = FACE_POS_Y; def.priority = 1; def.height = 0.20f; // reference height (blade heights vary per preset) def.width = 0.10f; // reference inset def.segments = 2; // blade segments (curvature) 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); } } } // ── Dispatch to material-specific generation ───────────────────── void TopingSystem::generateVariant(TopingDef& def, uint8_t bitmask) { const uint32_t startOffset = (uint32_t)vertices_.size(); if (def.materialID == 3) generateStoneVariant(def, bitmask); else generateGrassVariant(def, bitmask); const uint32_t count = (uint32_t)vertices_.size() - startOffset; def.variants[bitmask] = { startOffset, count }; } // ═════════════════════════════════════════════════════════════════ // Stone bevel generation // ═════════════════════════════════════════════════════════════════ // Features: // 1. Bevel strips along each open edge (outer wall + slope) // 2. Corner fill triangles where two adjacent open edges meet // 3. Cap triangles where a strip end meets a connected edge // void TopingSystem::generateStoneVariant(TopingDef& def, uint8_t bitmask) { const float h = def.height; const float w = def.width; // ── 1. Bevel strips for open edges ────────────────────────── for (int edge = 0; edge < 4; edge++) { if (bitmask & (1 << edge)) continue; const EdgeDef& e = kEdges[edge]; const float dx = e.ex - e.sx; const float dz = e.ez - e.sz; // Outer wall (vertical, facing outward) emitTri(vertices_, e.sx, 1.0f + h, e.sz, e.ex, 1.0f + h, e.ez, e.sx, 1.0f, e.sz, e.nx, 0.0f, e.nz); emitTri(vertices_, e.ex, 1.0f + h, e.ez, e.ex, 1.0f, e.ez, e.sx, 1.0f, e.sz, e.nx, 0.0f, e.nz); // Slope (facing inward + up) float slopeLen = sqrtf(h * h + w * w); if (slopeLen < 1e-4f) slopeLen = 1.0f; float cnx = e.ix * (h / slopeLen); float cny = w / slopeLen; float cnz = e.iz * (h / slopeLen); float inSx = e.sx + w * e.ix, inSz = e.sz + w * e.iz; float inEx = e.ex + w * e.ix, inEz = e.ez + w * e.iz; emitTri(vertices_, e.sx, 1.0f + h, e.sz, inSx, 1.0f, inSz, e.ex, 1.0f + h, e.ez, cnx, cny, cnz); emitTri(vertices_, e.ex, 1.0f + h, e.ez, inSx, 1.0f, inSz, inEx, 1.0f, inEz, cnx, cny, cnz); // ── 3. Caps at strip endpoints ────────────────────────── // At start: if the other edge sharing this corner is CONNECTED int startNeighbor = kStartNeighbor[edge]; if (bitmask & (1 << startNeighbor)) { // Cap triangle at strip start, facing -stripDir float capNx = -dx, capNz = -dz; float innerX = e.sx + w * e.ix; float innerZ = e.sz + w * e.iz; emitTri(vertices_, e.sx, 1.0f, e.sz, e.sx, 1.0f + h, e.sz, innerX, 1.0f, innerZ, capNx, 0.0f, capNz); } // At end: if the other edge sharing this corner is CONNECTED int endNeighbor = kEndNeighbor[edge]; if (bitmask & (1 << endNeighbor)) { // Cap triangle at strip end, facing +stripDir float capNx = dx, capNz = dz; float innerX = e.ex + w * e.ix; float innerZ = e.ez + w * e.iz; emitTri(vertices_, e.ex, 1.0f, e.ez, e.ex, 1.0f + h, e.ez, innerX, 1.0f, innerZ, capNx, 0.0f, capNz); } } // ── 2. Corner fills where two adjacent open edges meet ────── for (int c = 0; c < 4; c++) { const auto& corner = kCorners[c]; // Both edges open → fill the slope gap at the corner if ((bitmask & (1 << corner.bitA)) == 0 && (bitmask & (1 << corner.bitB)) == 0) { const EdgeDef& eA = kEdges[corner.bitA]; const EdgeDef& eB = kEdges[corner.bitB]; // Peak at corner (shared by both edges) float peakX = corner.cx, peakZ = corner.cz; // Inner points from each edge's inward direction float innerAx = corner.cx + w * eA.ix; float innerAz = corner.cz + w * eA.iz; float innerBx = corner.cx + w * eB.ix; float innerBz = corner.cz + w * eB.iz; // Slope normal: compute from cross product, ensure upward float e1x = innerAx - peakX, e1y = -h, e1z = innerAz - peakZ; float e2x = innerBx - peakX, e2y = -h, e2z = innerBz - peakZ; float fnx = e1y * e2z - e1z * e2y; float fny = e1z * e2x - e1x * e2z; float fnz = e1x * e2y - e1y * e2x; if (fny < 0) { fnx = -fnx; fny = -fny; fnz = -fnz; } float fnlen = sqrtf(fnx * fnx + fny * fny + fnz * fnz); if (fnlen > 1e-6f) { fnx /= fnlen; fny /= fnlen; fnz /= fnlen; } else { fnx = 0; fny = 1; fnz = 0; } emitTri(vertices_, peakX, 1.0f + h, peakZ, innerAx, 1.0f, innerAz, innerBx, 1.0f, innerBz, fnx, fny, fnz); } } } // ═════════════════════════════════════════════════════════════════ // Grass blade generation — tuft-based // ═════════════════════════════════════════════════════════════════ // Grass is generated as clusters ("tufts") of blades that fan out // from a shared base point. Each blade within a tuft has unique // height, width, orientation, curvature, and lean — all derived // deterministically from hash functions for reproducibility. // // Tuft placement along open edges; extra tufts at open corners. // Density: fewer open edges → denser tufts per edge (hedge effect). // void TopingSystem::generateGrassVariant(TopingDef& def, uint8_t bitmask) { // Count open edges int openEdges = 0; for (int b = 0; b < 4; b++) if (!(bitmask & (1 << b))) openEdges++; if (openEdges == 0) return; // Tuft count per edge (more open edges → fewer tufts each) int tuftsPerEdge; switch (openEdges) { case 1: tuftsPerEdge = 7; break; case 2: tuftsPerEdge = 5; break; case 3: tuftsPerEdge = 4; break; default: tuftsPerEdge = 4; break; } // ── 1. Tufts along open edges ─────────────────────────────── for (int edge = 0; edge < 4; edge++) { if (bitmask & (1 << edge)) continue; const EdgeDef& e = kEdges[edge]; const float dx = e.ex - e.sx; const float dz = e.ez - e.sz; for (int ti = 0; ti < tuftsPerEdge; ti++) { // ── Tuft CENTER position ───────────────────────────── // Along edge: fully random float t = hashF(edge * 7 + 1, ti * 13 + 3, 99); t = 0.03f + t * 0.94f; // Distance from edge: 0.0 (flush) to 0.45 (near center) // Use squared hash to bias toward edge while allowing far tufts float edgeDistHash = hashF(edge, ti, 44); float tuftInset = edgeDistHash * edgeDistHash * 0.30f; // quadratic bias float tuftCenterX = e.sx + t * dx + tuftInset * e.ix; float tuftCenterZ = e.sz + t * dz + tuftInset * e.iz; // ── Per-tuft personality ───────────────────────────── float tuftHeightScale = 0.20f + hashF(edge, ti, 77) * 0.80f; float tuftLeanScale = 0.3f + hashF(edge, ti, 55) * 1.5f; int bladesInTuft = 3 + (int)(hashF(edge, ti, 33) * 6.99f); // Emit blades tightly clustered around tuft center for (int bi = 0; bi < bladesInTuft; bi++) { float h1 = hashF(edge, ti, bi * 3 + 0); float h2 = hashF(edge, ti, bi * 3 + 1); float h3 = hashF(edge, ti, bi * 3 + 2); float h4 = hashF(edge + 4, ti, bi * 3 + 0); float h5 = hashF(edge + 4, ti, bi * 3 + 1); // Fan angle float spreadAngle = 55.0f; float fanT = (bladesInTuft > 1) ? (float)bi / (float)(bladesInTuft - 1) : 0.5f; float angle = (fanT - 0.5f) * 2.0f * spreadAngle; angle += (h1 - 0.5f) * 20.0f; // Height: per-blade × per-tuft float bladeHeight = (0.06f + h2 * 0.28f) * tuftHeightScale; float baseWidth = 0.030f + h3 * 0.030f; float lean = (0.03f + h4 * 0.12f) * tuftLeanScale; float midLean = 0.08f + h5 * 0.35f; // Tight scatter around tuft center (±0.03 — clustered) float h6 = hashF(edge + 8, ti, bi * 3 + 2); float offX = (h1 - 0.5f) * 0.06f; float offZ = (h6 - 0.5f) * 0.06f; float bx = tuftCenterX + offX; float bz = tuftCenterZ + offZ; emitBlade(vertices_, bx, bz, e.nx, e.nz, bladeHeight, baseWidth, lean, angle, midLean); } } } // ── 2. Corner tufts where two adjacent open edges meet ────── for (int c = 0; c < 4; c++) { const auto& corner = kCorners[c]; if ((bitmask & (1 << corner.bitA)) != 0 || (bitmask & (1 << corner.bitB)) != 0) continue; const EdgeDef& eA = kEdges[corner.bitA]; const EdgeDef& eB = kEdges[corner.bitB]; // Diagonal outward direction (bisector of two edge normals) float diagX = eA.nx + eB.nx; float diagZ = eA.nz + eB.nz; float diagLen = sqrtf(diagX * diagX + diagZ * diagZ); if (diagLen > 1e-6f) { diagX /= diagLen; diagZ /= diagLen; } // Corner tuft: cluster filling the diagonal gap int cornerBlades = 4 + (int)(hashF(c + 10, 0, 33) * 5.99f); // Corner tuft inset: 0.05 to 0.35 diagonally inward float cDistHash = hashF(c + 10, 0, 44); float cornerInset = 0.05f + cDistHash * 0.30f; float cbx = corner.cx + cornerInset * (eA.ix + eB.ix); float cbz = corner.cz + cornerInset * (eA.iz + eB.iz); float cornerHeightScale = 0.25f + hashF(c + 10, 0, 77) * 0.75f; float cornerLeanScale = 0.3f + hashF(c + 10, 0, 55) * 1.5f; for (int bi = 0; bi < cornerBlades; bi++) { float h1 = hashF(c + 10, bi, 0); float h2 = hashF(c + 10, bi, 1); float h3 = hashF(c + 10, bi, 2); float h4 = hashF(c + 10, bi, 3); float h5 = hashF(c + 10, bi, 4); // Wide fan across the diagonal float fanT = (float)bi / (float)(cornerBlades - 1); float angle = (fanT - 0.5f) * 2.0f * 70.0f; angle += (h1 - 0.5f) * 15.0f; float height = (0.10f + h2 * 0.22f) * cornerHeightScale; float baseWidth = 0.030f + h3 * 0.025f; float lean = (0.04f + h4 * 0.10f) * cornerLeanScale; float midLean = 0.10f + h5 * 0.30f; // Tight scatter around corner tuft center (clustered) float h6 = hashF(c + 10, bi, 5); float offX = (h1 - 0.5f) * 0.06f; float offZ = (h6 - 0.5f) * 0.06f; emitBlade(vertices_, cbx + offX, cbz + offZ, diagX, diagZ, height, baseWidth, lean, angle, midLean); } } } // ── 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 if (def.face == FACE_POS_Y) { if (!world.getVoxel(wx, wy + 1, wz).isEmpty()) continue; // Compute 4-bit adjacency bitmask 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; if (!world.getVoxel(nx, wy + 1, nz).isEmpty()) return false; if (nMat == mat) return true; 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 }); } } } } }); } } // namespace voxel