2026-03-26 15:27:15 +01:00
|
|
|
#include "TopingSystem.h"
|
|
|
|
|
#include "VoxelWorld.h"
|
|
|
|
|
#include <cmath>
|
|
|
|
|
#include <cstring>
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-26 17:47:08 +01:00
|
|
|
// ── 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).
|
2026-03-26 15:27:15 +01:00
|
|
|
static void emitTri(std::vector<TopingVertex>& 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)
|
|
|
|
|
{
|
2026-03-26 17:47:08 +01:00
|
|
|
// 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 });
|
|
|
|
|
}
|
2026-03-26 15:27:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═════════════════════════════════════════════════════════════════
|
|
|
|
|
// 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
|
2026-03-26 17:47:08 +01:00
|
|
|
// Priority 0 (low): yields to grass at material boundaries
|
2026-03-26 15:27:15 +01:00
|
|
|
{
|
|
|
|
|
TopingDef def{};
|
|
|
|
|
def.materialID = 3;
|
|
|
|
|
def.face = FACE_POS_Y;
|
2026-03-26 17:47:08 +01:00
|
|
|
def.priority = 0;
|
2026-03-26 15:27:15 +01:00
|
|
|
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
|
2026-03-26 17:47:08 +01:00
|
|
|
// Priority 1 (high): generates bevels over stone at boundaries
|
2026-03-26 15:27:15 +01:00
|
|
|
{
|
|
|
|
|
TopingDef def{};
|
|
|
|
|
def.materialID = 1;
|
|
|
|
|
def.face = FACE_POS_Y;
|
2026-03-26 17:47:08 +01:00
|
|
|
def.priority = 1;
|
2026-03-26 15:27:15 +01:00
|
|
|
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<float> 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)
|
2026-03-26 17:47:08 +01:00
|
|
|
// Winding: emit both orderings — CW from outside view
|
|
|
|
|
// Empirically CW = front-facing in our engine (see CLAUDE.md)
|
2026-03-26 15:27:15 +01:00
|
|
|
emitTri(vertices_,
|
2026-03-26 17:47:08 +01:00
|
|
|
x0, pk0y, z0, x1, pk1y, z1, x0, 1.0f, z0,
|
2026-03-26 15:27:15 +01:00
|
|
|
e.nx, 0.0f, e.nz);
|
|
|
|
|
emitTri(vertices_,
|
2026-03-26 17:47:08 +01:00
|
|
|
x1, pk1y, z1, x1, 1.0f, z1, x0, 1.0f, z0,
|
2026-03-26 15:27:15 +01:00
|
|
|
e.nx, 0.0f, e.nz);
|
|
|
|
|
|
|
|
|
|
// ── Slope face (from peak down to inner edge) ───────
|
2026-03-26 17:47:08 +01:00
|
|
|
// 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²).
|
2026-03-26 15:27:15 +01:00
|
|
|
const float avgH = (h0 + h1) * 0.5f;
|
2026-03-26 17:47:08 +01:00
|
|
|
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
|
2026-03-26 15:27:15 +01:00
|
|
|
emitTri(vertices_,
|
2026-03-26 17:47:08 +01:00
|
|
|
x0, pk0y, z0, in0x, 1.0f, in0z, x1, pk1y, z1,
|
2026-03-26 15:27:15 +01:00
|
|
|
cnx, cny, cnz);
|
|
|
|
|
emitTri(vertices_,
|
2026-03-26 17:47:08 +01:00
|
|
|
x1, pk1y, z1, in0x, 1.0f, in0z, in1x, 1.0f, in1z,
|
2026-03-26 15:27:15 +01:00
|
|
|
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.
|
2026-03-26 17:47:08 +01:00
|
|
|
// 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)
|
2026-03-26 15:27:15 +01:00
|
|
|
uint8_t adj = 0;
|
2026-03-26 17:47:08 +01:00
|
|
|
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
|
2026-03-26 15:27:15 +01:00
|
|
|
|
|
|
|
|
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
|