bvle-voxels/src/voxel/TopingSystem.cpp
Samuel Bouchet ef89bd8c49 Phase 4.2: grass blade tufts, stone corner fills/caps, vegetation shading
Stone: add corner fill triangles at adjacent open edges and cap
triangles at strip terminaisons. Grass: replace bevel strips with
tuft-based grass blades — clusters of 3-9 curved double-sided
blades with per-tuft height/lean personality and hash-driven
placement (quadratic inset 0-0.30 from edge). Vegetation PS uses
half-Lambert wrap lighting + translucency for soft stylized shading
(inspired by Airborn Trees). Stone keeps classic Lambert.
2026-03-26 18:48:35 +01:00

544 lines
23 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "TopingSystem.h"
#include "VoxelWorld.h"
#include <cmath>
#include <cstring>
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<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)
{
// 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<TopingVertex>& 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