31[[nodiscard]] HexVec3 horizontalPerturb(
const HexNoise& noise, HexVec3
position)
noexcept {
39[[nodiscard]]
float dot(HexVec3
a, HexVec3
b)
noexcept {
return a.x *
b.x +
a.y *
b.y +
a.z *
b.z; }
42[[nodiscard]] HexVec3
cross(HexVec3
a, HexVec3
b)
noexcept {
43 return HexVec3{
a.y *
b.z -
a.z *
b.y,
a.z *
b.x -
a.x *
b.z,
a.x *
b.y -
a.y *
b.x};
47[[nodiscard]] HexVec3
normalize(HexVec3
v)
noexcept {
49 if (
length <= 1e-6f)
return HexVec3{};
54[[nodiscard]]
bool isZero(HexVec3
v)
noexcept {
return v.x == 0.f &&
v.y == 0.f &&
v.z == 0.f; }
63[[nodiscard]] HexVec3
yaw(
float orientation, HexVec3
v)
noexcept {
64 const float angle = orientation * 6.28318530718f;
65 const float cosine = std::cos(
angle);
66 const float sine = std::sin(
angle);
67 return HexVec3{
v.x * cosine -
v.z * sine,
v.y,
v.x * sine +
v.z * cosine};
71constexpr float kWallCode = 0.f;
73constexpr float kTowerCode = 1.f;
75constexpr float kBridgeCode = 2.f;
77constexpr float kUrbanCode = 3.f;
79constexpr float kFarmCode = 4.f;
81constexpr float kPlantCode = 5.f;
83constexpr float kSpecialCode = 6.f;
86constexpr float kBridgeHalfWidth = 0.25f;
88constexpr float kBridgeHalfThickness = 0.15f;
90constexpr float kTowerHalfExtent = 1.5f;
92constexpr float kTowerHeight = 2.f;
95void emitTriangle(HexMeshData& out,
const HexNoise& noise,
const HexVec3& p0,
const HexVec3& p1,
const HexVec3& p2,
97 const auto i0 =
static_cast<std::uint32_t
>(out.vertexCount());
98 out.addVertex(horizontalPerturb(noise, p0),
code, 0.f);
99 out.addVertex(horizontalPerturb(noise, p1),
code, 0.f);
100 out.addVertex(horizontalPerturb(noise, p2),
code, 0.f);
101 out.addTriangle(
i0,
i0 + 1u,
i0 + 2u);
113void emitEdgeQuad(HexMeshData& out,
const HexNoise& noise,
const HexVec3& corner,
const HexVec3& e1,
const HexVec3& e2,
115 const auto i0 =
static_cast<std::uint32_t
>(out.vertexCount());
116 out.addVertex(horizontalPerturb(noise, corner),
code, 0.f);
117 out.addVertex(horizontalPerturb(noise, corner + e1),
code, 0.f);
118 out.addVertex(horizontalPerturb(noise, corner + e1 + e2),
code, 0.f);
119 out.addVertex(horizontalPerturb(noise, corner + e2),
code, 0.f);
120 out.addTriangle(
i0 + 2u,
i0 + 1u,
i0);
121 out.addTriangle(
i0 + 3u,
i0 + 2u,
i0);
125void emitBoxFace(HexMeshData& out,
const HexNoise& noise, HexVec3 centre, HexVec3 e1, HexVec3 e2,
float code) {
126 const HexVec3 corner = centre - e1 * 0.5f - e2 * 0.5f;
127 emitEdgeQuad(out, noise, corner, e1, e2,
code);
139void emitBox(HexMeshData& out,
const HexNoise& noise, HexVec3
origin, HexVec3
right, HexVec3
up, HexVec3 fwd,
140 HexVec3 halfExtents,
float code) {
141 const HexVec3
r =
right * (2.f * halfExtents.x);
142 const HexVec3
u =
up * (2.f * halfExtents.y);
143 const HexVec3
f = fwd * (2.f * halfExtents.z);
147 emitBoxFace(out, noise,
origin +
r * 0.5f,
u,
f * -1.f,
code);
150 emitBoxFace(out, noise,
origin -
u * 0.5f,
r,
f * -1.f,
code);
152 emitBoxFace(out, noise,
origin +
f * 0.5f,
r,
u * -1.f,
code);
158 WallMesher(
const HexMap& map, HexMeshData& out) noexcept : map_(map), out_(out) {}
161 void run(std::int32_t chunkIndex) {
164 buildCell(map_.chunkCell(chunkIndex,
column, row));
171 void buildCell(HexCoordinates coordinates) {
172 if (!map_.contains(coordinates))
return;
173 const HexCellData* cellData = map_.cell(coordinates);
174 if (cellData ==
nullptr)
return;
176 const HexVec3
center = map_.cellPosition(coordinates);
180 HexCoordinates neighbour{};
181 const bool hasNeighbour = map_.getNeighbor(coordinates,
direction, neighbour);
182 const HexCellData* neighbourData = hasNeighbour ? map_.cell(neighbour) :
nullptr;
183 const HexVec3 neighbourPosition = hasNeighbour ? map_.cellPosition(neighbour) : HexVec3{};
185 const bool isOwnDirection =
188 const float farY = neighbourData !=
nullptr ? neighbourPosition.y : near.v1.y;
195 if (isOwnDirection || neighbourData ==
nullptr) {
196 const bool hasRiver = map_.hasRiverThrough(coordinates,
direction) ||
198 const bool hasRoad = cellData->flags.hasRoad(
direction) ||
200 addWall(near, cellData, far, neighbourData, hasRiver, hasRoad);
202 if (neighbourData !=
nullptr && cellData->flags.hasRoad(
direction) && hasRiver) {
203 addBridge(
center, neighbourPosition);
207 if (!isOwnDirection)
continue;
209 HexCoordinates nextCoordinates{};
210 if (!map_.getNeighbor(coordinates,
next(
direction), nextCoordinates))
continue;
211 const HexCellData* nextCell = map_.cell(nextCoordinates);
212 if (nextCell ==
nullptr)
continue;
215 left.y = map_.cellPosition(nextCoordinates).y;
220 addWallCorner(near.v5, cellData,
left, nextCell, far.v5, neighbourData);
225 void addWall(
const EdgeVertices& near,
const HexCellData* nearCell,
const EdgeVertices& far,
226 const HexCellData* farCell,
bool hasRiver,
bool hasRoad) {
227 if (farCell ==
nullptr)
return;
228 if (nearCell->flags.isWalled() == farCell->flags.isWalled())
return;
229 if (nearCell->values.isUnderwater() || farCell->values.isUnderwater())
return;
232 addWallSegment(near.v1, far.v1, near.v2, far.v2,
false);
233 if (hasRiver || hasRoad) {
234 addWallCap(near.v2, far.v2);
235 addWallCap(far.v4, near.v4);
237 addWallSegment(near.v2, far.v2, near.v3, far.v3,
false);
238 addWallSegment(near.v3, far.v3, near.v4, far.v4,
false);
240 addWallSegment(near.v4, far.v4, near.v5, far.v5,
false);
244 void addWallSegment(HexVec3 nearLeft, HexVec3 farLeft, HexVec3 nearRight, HexVec3 farRight,
bool addTower) {
250 const HexVec3 leftLow =
left - leftOffset;
251 const HexVec3 rightLow =
right - rightOffset;
252 HexVec3 leftTop = leftLow;
254 HexVec3 rightTop = rightLow;
257 emitEdgeQuad(out_, map_.noise(), leftLow, rightLow - leftLow, leftTop - leftLow, kWallCode);
259 const HexVec3 leftInnerLow =
left + leftOffset;
260 const HexVec3 rightInnerLow =
right + rightOffset;
261 HexVec3 leftInnerTop = leftInnerLow;
263 HexVec3 rightInnerTop = rightInnerLow;
266 emitEdgeQuad(out_, map_.noise(), rightInnerLow, leftInnerLow - rightInnerLow, rightInnerTop - rightInnerLow,
268 emitEdgeQuad(out_, map_.noise(), leftTop, rightTop - leftTop, leftInnerTop - leftTop, kWallCode);
274 void addTowerAt(HexVec3
left, HexVec3
right) {
276 if (
dot(along, along) <= 1e-8f)
return;
279 const HexVec3 rightAxis =
normalize(
cross(fwd, HexVec3{0.f, 1.f, 0.f}));
280 if (isZero(rightAxis))
return;
286 emitBox(out_, map_.noise(), centre, rightAxis, HexVec3{0.f, 1.f, 0.f}, fwd, halfExtents, kTowerCode);
290 void addWallCap(HexVec3 near, HexVec3 far) {
306 emitEdgeQuad(out_, map_.noise(), topA, lowA - topA, topB - topA, kWallCode);
310 void addWallWedge(HexVec3 near, HexVec3 far, HexVec3
point) {
314 HexVec3 flatPoint =
point;
316 HexVec3 topPoint = flatPoint;
328 emitEdgeQuad(out_, map_.noise(), flatPoint, topPoint - flatPoint, lowA - flatPoint, kWallCode);
329 emitEdgeQuad(out_, map_.noise(), flatPoint, lowB - flatPoint, topPoint - flatPoint, kWallCode);
330 emitTriangle(out_, map_.noise(), topPoint, topB, topA, kWallCode);
334 void addWallSegmentAtPivot(HexVec3 pivot,
const HexCellData* pivotCell, HexVec3
left,
const HexCellData*
leftCell,
336 if (pivotCell->values.isUnderwater())
return;
338 const bool hasLeftWall =
leftCell !=
nullptr && !
leftCell->values.isUnderwater() &&
341 const bool hasRightWall =
rightCell !=
nullptr && !
rightCell->values.isUnderwater() &&
345 if (hasLeftWall && hasRightWall) {
346 bool hasTower =
false;
348 const HexVec3 midpoint = (pivot +
left +
right) * (1.f / 3.f);
349 const HexHash
hash = HexHashGrid(map_.seed()).sample(midpoint);
352 addWallSegment(pivot,
left, pivot,
right, hasTower);
353 }
else if (hasLeftWall) {
357 addWallCap(pivot,
left);
359 }
else if (hasRightWall) {
363 addWallCap(
right, pivot);
369 void addWallCorner(HexVec3 c1,
const HexCellData* cell1, HexVec3 c2,
const HexCellData* cell2, HexVec3 c3,
370 const HexCellData* cell3) {
371 if (cell2 ==
nullptr || cell3 ==
nullptr)
return;
372 const bool w1 = cell1->flags.isWalled();
373 const bool w2 = cell2->flags.isWalled();
374 const bool w3 = cell3->flags.isWalled();
378 if (!w3) addWallSegmentAtPivot(c3, cell3, c1, cell1, c2, cell2);
380 addWallSegmentAtPivot(c2, cell2, c3, cell3, c1, cell1);
382 addWallSegmentAtPivot(c1, cell1, c2, cell2, c3, cell3);
386 addWallSegmentAtPivot(c1, cell1, c2, cell2, c3, cell3);
388 addWallSegmentAtPivot(c2, cell2, c3, cell3, c1, cell1);
391 addWallSegmentAtPivot(c3, cell3, c1, cell1, c2, cell2);
396 void addBridge(HexVec3
center, HexVec3 neighbourPosition) {
397 const HexVec3 span = neighbourPosition -
center;
398 const float length = std::sqrt(
dot(span, span));
399 if (
length <= 1e-6f)
return;
401 const HexVec3 fwd = span * (1.f /
length);
402 const HexVec3 rightAxis =
normalize(
cross(HexVec3{0.f, 1.f, 0.f}, fwd));
403 if (isZero(rightAxis))
return;
408 emitBox(out_, map_.noise(),
center, rightAxis, HexVec3{0.f, 1.f, 0.f}, fwd, halfExtents, kBridgeCode);
441 explicit FeatureMesher(
const HexMap& map, HexMeshData& out) noexcept
442 : map_(map), out_(out), grid_(map.seed()) {}
445 void run(std::int32_t chunkIndex) {
448 buildCell(map_.chunkCell(chunkIndex,
column, row));
455 void buildCell(HexCoordinates coordinates) {
456 if (!map_.contains(coordinates))
return;
457 const HexCellData* cellData = map_.cell(coordinates);
458 if (cellData ==
nullptr)
return;
459 if (cellData->values.isUnderwater())
return;
461 const std::int32_t urbanLevel = cellData->values.urbanLevel();
462 const std::int32_t farmLevel = cellData->values.farmLevel();
463 const std::int32_t plantLevel = cellData->values.plantLevel();
464 const std::int32_t special = cellData->values.specialIndex();
465 if (urbanLevel == 0 && farmLevel == 0 && plantLevel == 0 && special == 0)
return;
467 const HexVec3
position = map_.cellPosition(coordinates);
471 const int urban = pick(urbanLevel,
hash.a);
472 const int farm = pick(farmLevel,
hash.b);
473 const int plant = pick(plantLevel,
hash.c);
477 std::int32_t collection = -1;
478 float usedHash =
hash.a;
479 if (urban >= 0) collection = 0;
480 if (farm >= 0 && (collection < 0 ||
hash.b < usedHash)) {
484 if (
plant >= 0 && (collection < 0 ||
hash.c < usedHash)) collection = 2;
486 if (collection == 0) addUrban(
position, urbanLevel,
hash.e);
501 [[nodiscard]]
static int pick(std::int32_t
level,
float hash)
noexcept {
502 if (
level <= 0)
return -1;
510 void addUrban(HexVec3
position, std::int32_t
level,
float orientation) {
514 const float piece = total /
static_cast<float>(boxes);
517 const HexVec3
right =
yaw(orientation, HexVec3{1.f, 0.f, 0.f});
518 const HexVec3 fwd =
yaw(orientation, HexVec3{0.f, 0.f, 1.f});
522 const float scale = 1.f - 0.25f *
static_cast<float>(
index);
523 const HexVec3 halfExtents{base *
scale, piece * 0.5f, base *
scale};
524 const HexVec3 centre =
position + HexVec3{0.f, lift + piece * 0.5f, 0.f};
525 emitBox(out_, map_.noise(), centre,
right, HexVec3{0.f, 1.f, 0.f}, fwd, halfExtents, kUrbanCode);
531 void addFarm(HexVec3
position,
float orientation) {
533 const HexVec3
right =
yaw(orientation, HexVec3{1.f, 0.f, 0.f});
534 const HexVec3 fwd =
yaw(orientation, HexVec3{0.f, 0.f, 1.f});
536 emitBox(out_, map_.noise(),
position + HexVec3{0.f, half, 0.f},
right, HexVec3{0.f, 1.f, 0.f}, fwd,
537 halfExtents, kFarmCode);
541 void addPlant(HexVec3
position,
float orientation) {
547 const HexVec3
first =
yaw(orientation, HexVec3{
radius, 0.f, 0.f});
555 emitTriangle(out_, map_.noise(), ring[
index],
top, ring[
next], kPlantCode);
556 emitTriangle(out_, map_.noise(), ring[
next],
bottom, ring[
index], kPlantCode);
561 void addSpecial(HexVec3
position,
float orientation) {
564 const float footer =
height * 0.2f;
565 const float shaft =
height * 0.55f;
566 const HexVec3
right =
yaw(orientation, HexVec3{1.f, 0.f, 0.f});
567 const HexVec3 fwd =
yaw(orientation, HexVec3{0.f, 0.f, 1.f});
569 emitBox(out_, map_.noise(),
position + HexVec3{0.f, footer * 0.5f, 0.f},
right, HexVec3{0.f, 1.f, 0.f}, fwd,
570 HexVec3{base, footer * 0.5f, base}, kSpecialCode);
571 emitBox(out_, map_.noise(),
position + HexVec3{0.f, footer + shaft * 0.5f, 0.f},
right, HexVec3{0.f, 1.f, 0.f},
572 fwd, HexVec3{base * 0.6f, shaft * 0.5f, base * 0.6f}, kSpecialCode);
583 const auto wrap = [](std::int32_t
value)
noexcept {
584 std::int32_t wrapped =
value % kSize;
585 if (wrapped < 0) wrapped += kSize;
586 return static_cast<std::uint32_t
>(wrapped);
588 const auto grid = [](
float coordinate)
noexcept {
589 return static_cast<std::int32_t
>(std::floor(coordinate * kScale));
592 const std::uint32_t
x = wrap(grid(
position.x));
593 const std::uint32_t
z = wrap(grid(
position.z));
597 const auto lane = [&](std::uint32_t channel)
noexcept {
598 std::uint32_t
h =
x * 0x9E3779B1u;
599 h ^=
z * 0x85EBCA77u;
600 h = (
h ^ (
h >> 15)) * 0xC2B2AE3Du;
601 h ^= (seed_ + channel * 0x9E3779B9u) * 0x27D4EB2Fu;
602 h = (
h ^ (
h >> 13)) * 0x165667B1u;
604 return static_cast<float>(
h >> 8) * (1.f / 16777216.f);
629 return kThresholds[
static_cast<std::size_t
>(
level)][
static_cast<std::size_t
>(
index)];
639 WallMesher mesher(map, out);
640 mesher.run(chunkIndex);
651 FeatureMesher mesher(map, out);
652 mesher.run(chunkIndex);
std::array< std::uint8_t, 32 > hash
Packed per-cell state records of the hex map.
Axial hex coordinates and world-space conversion.
Deterministic feature placement for hex decorations and walls.
Editable hex cell grid: topology, queries, picking and authoring.
CPU vertex/index container shared by the hex surface builders.
Pointy-top hex metrics, directions and vertex helpers.
Deterministic asset-free noise for hex perturbation and generation.
const HexCellData * leftCell
const HexCellData * rightCell
std::array< float, 3 > position
std::array< float, 3 > scale
RoadLaneDirection direction
HexHash sample(HexVec3 position) const noexcept
Samples the five-component hash at a world-space position.
An editable, chunked, pointy-top hex map.
std::int32_t chunkCount() const noexcept
Total number of chunks.
bool empty() const noexcept
Whether the map holds any cell.
CPU-side triangle soup for one hex chunk surface.
void finalize() noexcept
Computes flat per-face normals. Must be called before upload.
void clear() noexcept
Removes every vertex and index, keeping the allocated capacity.
static HexVec3 firstSolidCorner(HexDirection d) noexcept
First solid corner of direction.
static constexpr float kWallThickness
Wall thickness.
static HexVec3 wallThicknessOffset(HexVec3 near, HexVec3 far) noexcept
Half-thickness offset applied to a wall segment.
static HexVec3 secondSolidCorner(HexDirection d) noexcept
Second solid corner of direction.
static HexVec3 wallLerp(HexVec3 near, HexVec3 far) noexcept
Position of the midpoint of a wall between two elevations.
static HexVec3 bridge(HexDirection d) noexcept
Bridge vector from the solid edge of direction to the neighbour.
static constexpr float kWallHeight
Height of a city/farm wall.
static constexpr float kCellPerturbStrength
Strength of the XZ position perturbation.
static constexpr int kChunkSizeZ
Chunk size in the Z dimension.
static constexpr float innerRadius() noexcept
Inner radius of a hex cell.
static constexpr int kChunkSizeX
Chunk size in the X dimension.
void buildFeatureMesh(const HexMap &map, std::int32_t chunkIndex, HexMeshData &out)
Builds the urban, farm, plant and special decorations of one chunk.
constexpr float kWallTowerThreshold
Height of a city/farm wall, matching HexMetrics::kWallHeight.
float featureThreshold(std::int32_t level, std::int32_t index) noexcept
Feature-placement thresholds of the reference project.
constexpr HexDirection opposite(HexDirection d) noexcept
The direction opposite to d.
constexpr std::int32_t kHexDirectionCount
Number of hex edges / facing directions.
constexpr HexEdgeType edgeType(int elevation1, int elevation2) noexcept
The relationship between two elevations (single-step changes are slopes).
void buildWallMesh(const HexMap &map, std::int32_t chunkIndex, HexMeshData &out)
Builds the wall and bridge geometry of one chunk.
constexpr std::int32_t kFeatureThresholdCount
Number of threshold entries per feature level.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
HexDirection
Hex facing directions, counter-clockwise from north-east.
double sample(const Heightmap &map, double u, double v)
Sample.
Vec2 normalize(const Vec2 &a)
Normalize.
double dot(const Vec2 &a, const Vec2 &b)
Dot.
double cross(const Vec2 &a, const Vec2 &b)
Cross.
WidgetDesc row(std::vector< WidgetDesc > children, std::string id)
Horizontal elastic layout row.
Five-component pseudo-random value used to pick and orient one feature.
Minimal 3-component float vector used by the hex mesh builders.