207 lines
9.3 KiB
C++
207 lines
9.3 KiB
C++
#include "hexmap/HexMapMesh.h"
|
|
|
|
#include "hexmap/HexCell.h"
|
|
#include "hexmap/HexMeshData.h"
|
|
#include "hexmap/HexMetrics.h"
|
|
#include "hexmap/HexNoise.h"
|
|
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
|
|
namespace eve::hexmap {
|
|
namespace {
|
|
|
|
/** @brief One pending surface vertex: a world position plus its texture coordinates. */
|
|
struct SurfaceVertex {
|
|
HexVec3 position;
|
|
float u = 0.f;
|
|
float v = 0.f;
|
|
};
|
|
|
|
/**
|
|
* @brief Whether three positions span no area, so the face would be invisible.
|
|
*
|
|
* The test reuses the right-handed cross product `HexMeshData::finalize()` derives
|
|
* its face normals from.
|
|
*/
|
|
[[nodiscard]] bool isDegenerate(HexVec3 a, HexVec3 b, HexVec3 c) noexcept {
|
|
const HexVec3 ab = b - a;
|
|
const HexVec3 ac = c - a;
|
|
const HexVec3 normal{ab.y * ac.z - ab.z * ac.y, ab.z * ac.x - ab.x * ac.z, ab.x * ac.y - ab.y * ac.x};
|
|
return (normal.x * normal.x + normal.y * normal.y + normal.z * normal.z) <= 1e-8f;
|
|
}
|
|
|
|
/**
|
|
* @brief Appends one triangle, perturbing every position exactly once.
|
|
*
|
|
* `a -> b -> c` must resolve to a `+Y` normal (counter-clockwise seen from above,
|
|
* using up = `+Z`), otherwise the face is culled.
|
|
*
|
|
* @param out Destination mesh.
|
|
* @param noise Map noise, applied here so no call site can displace a vertex twice.
|
|
*/
|
|
void emitTriangle(HexMeshData& out, const SurfaceVertex& a, const SurfaceVertex& b, const SurfaceVertex& c,
|
|
const HexNoise& noise) {
|
|
const HexVec3 pa = noise.perturb(a.position);
|
|
const HexVec3 pb = noise.perturb(b.position);
|
|
const HexVec3 pc = noise.perturb(c.position);
|
|
if (isDegenerate(pa, pb, pc)) return;
|
|
out.addTriangle(out.addVertex(pa, a.u, a.v), out.addVertex(pb, b.u, b.v), out.addVertex(pc, c.u, c.v));
|
|
}
|
|
|
|
/**
|
|
* @brief Appends the quad whose four corners run `r0 -> r1 -> r2 -> r3` around its outline.
|
|
*
|
|
* `HexMeshData::addQuad(a, b, c, d)` splits the ring `a -> b -> d -> c` along `b -> c`,
|
|
* so the corners are re-ordered here and every call site keeps readable outline order.
|
|
*/
|
|
void emitQuad(HexMeshData& out, const SurfaceVertex& r0, const SurfaceVertex& r1, const SurfaceVertex& r2,
|
|
const SurfaceVertex& r3, const HexNoise& noise) {
|
|
emitTriangle(out, r0, r3, r1, noise);
|
|
emitTriangle(out, r1, r3, r2, noise);
|
|
}
|
|
|
|
/** @brief Unit horizontal normal of `forward`, turned a quarter circle around `+Y`. */
|
|
[[nodiscard]] HexVec3 acrossDirection(HexVec3 forward) noexcept {
|
|
const float length = std::sqrt(forward.x * forward.x + forward.z * forward.z);
|
|
if (length <= 1e-6f) return HexVec3{0.f, 0.f, 0.f};
|
|
return HexVec3{forward.z / length, 0.f, -forward.x / length};
|
|
}
|
|
|
|
/** @brief One cross-section of a strip: a centreline point, its across direction and its width. */
|
|
struct StripStation {
|
|
HexVec3 position;
|
|
HexVec3 across;
|
|
float halfWidth = 0.f;
|
|
float u = 0.f;
|
|
};
|
|
|
|
/**
|
|
* @brief Appends the ribbon segment between two cross-sections.
|
|
*
|
|
* The outline runs `minus0 -> plus0 -> plus1 -> minus1`, the ring order these helpers
|
|
* resolve to an upward normal. Both across directions must belong to the same
|
|
* direction of travel; a zero half-width folds one pair of corners together and
|
|
* leaves only the tapering triangle.
|
|
*
|
|
* @param out Destination mesh.
|
|
* @param from Upstream cross-section.
|
|
* @param to Downstream cross-section.
|
|
* @param noise Map noise, applied once per emitted position.
|
|
*/
|
|
void emitSegment(HexMeshData& out, const StripStation& from, const StripStation& to, const HexNoise& noise) {
|
|
const SurfaceVertex minus0{from.position - from.across * from.halfWidth, from.u, 0.f};
|
|
const SurfaceVertex plus0{from.position + from.across * from.halfWidth, from.u, 1.f};
|
|
const SurfaceVertex plus1{to.position + to.across * to.halfWidth, to.u, 1.f};
|
|
const SurfaceVertex minus1{to.position - to.across * to.halfWidth, to.u, 0.f};
|
|
emitQuad(out, minus0, plus0, plus1, minus1, noise);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void buildRiverMesh(const HexMap& map, std::int32_t chunkIndex, HexMeshData& out) {
|
|
out.clear();
|
|
if (chunkIndex < 0 || chunkIndex >= map.chunkCount()) {
|
|
out.finalize();
|
|
return;
|
|
}
|
|
|
|
const HexNoise& noise = map.noise();
|
|
const float halfWidth = HexMetrics::innerRadius() * HexMetrics::kRiverSurfaceScale * 0.5f;
|
|
|
|
for (std::int32_t row = 0; row < HexMetrics::kChunkSizeZ; ++row) {
|
|
for (std::int32_t column = 0; column < HexMetrics::kChunkSizeX; ++column) {
|
|
const HexCoordinates coordinates = map.chunkCell(chunkIndex, column, row);
|
|
const HexCellData* data = map.cell(coordinates);
|
|
if (data == nullptr || !map.hasRiver(coordinates)) continue;
|
|
|
|
const HexVec3 ground = map.cellGroundPosition(coordinates);
|
|
const HexVec3 centre{ground.x, HexMetrics::streamBedY(map.elevation(coordinates)), ground.z};
|
|
|
|
// Per-edge nodes of the channel, keyed by HexDirection index.
|
|
std::array<bool, 6> connected{};
|
|
std::array<bool, 6> incoming{};
|
|
std::array<bool, 6> outgoing{};
|
|
std::array<HexVec3, 6> edgePosition{};
|
|
std::int32_t connectionCount = 0;
|
|
for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
|
|
const HexDirection direction = static_cast<HexDirection>(i);
|
|
if (!map.hasRiverThrough(coordinates, direction)) continue;
|
|
connected[i] = true;
|
|
incoming[i] = data->flags.hasRiverIn(direction);
|
|
outgoing[i] = data->flags.hasRiverOut(direction);
|
|
++connectionCount;
|
|
// The edge crossing sits on the solid edge midpoint, the same point the
|
|
// terrain mesh owns on a shared border.
|
|
edgePosition[i] = centre + HexMetrics::solidEdgeMiddle(direction);
|
|
}
|
|
if (connectionCount == 0) continue;
|
|
|
|
std::int32_t emitted = 0;
|
|
|
|
// Every incoming/outgoing pair bends through the cell centre.
|
|
for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
|
|
if (!incoming[i]) continue;
|
|
for (std::int32_t j = 0; j < kHexDirectionCount; ++j) {
|
|
if (!outgoing[j] || j == i) continue;
|
|
const StripStation start{edgePosition[i], acrossDirection(centre - edgePosition[i]), halfWidth,
|
|
0.f};
|
|
const StripStation middle{centre, acrossDirection(edgePosition[j] - edgePosition[i]), halfWidth,
|
|
0.5f};
|
|
const StripStation finish{edgePosition[j], acrossDirection(edgePosition[j] - centre), halfWidth,
|
|
1.f};
|
|
emitSegment(out, start, middle, noise);
|
|
emitSegment(out, middle, finish, noise);
|
|
++emitted;
|
|
}
|
|
}
|
|
|
|
// A begin/end cell (HexFlags::hasRiverBeginOrEnd) has exactly one connection, so
|
|
// there is no pair: the channel tapers from the edge to the cell centre instead.
|
|
if (emitted == 0 && connectionCount == 1) {
|
|
for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
|
|
if (!connected[i]) continue;
|
|
if (outgoing[i]) {
|
|
const StripStation start{centre, acrossDirection(edgePosition[i] - centre), 0.f, 0.5f};
|
|
const StripStation finish{edgePosition[i], acrossDirection(edgePosition[i] - centre), halfWidth,
|
|
1.f};
|
|
emitSegment(out, start, finish, noise);
|
|
} else {
|
|
const StripStation start{edgePosition[i], acrossDirection(centre - edgePosition[i]), halfWidth,
|
|
0.f};
|
|
const StripStation finish{centre, acrossDirection(centre - edgePosition[i]), 0.f, 0.5f};
|
|
emitSegment(out, start, finish, noise);
|
|
}
|
|
++emitted;
|
|
}
|
|
}
|
|
|
|
// Degenerate topologies (for example a sink fed by two rivers) carry no direction
|
|
// pair at all; join the collected edges in direction order so the cell is not blank.
|
|
if (emitted == 0) {
|
|
std::int32_t previous = -1;
|
|
for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
|
|
if (!connected[i]) continue;
|
|
if (previous >= 0) {
|
|
const StripStation start{edgePosition[previous],
|
|
acrossDirection(centre - edgePosition[previous]), halfWidth, 0.f};
|
|
const StripStation middle{centre, acrossDirection(edgePosition[i] - edgePosition[previous]),
|
|
halfWidth, 0.5f};
|
|
const StripStation finish{edgePosition[i], acrossDirection(edgePosition[i] - centre), halfWidth,
|
|
1.f};
|
|
emitSegment(out, start, middle, noise);
|
|
emitSegment(out, middle, finish, noise);
|
|
++emitted;
|
|
}
|
|
previous = i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
out.finalize();
|
|
}
|
|
|
|
} // namespace eve::hexmap
|