载入中...
搜索中...
未找到
HexSphereMesh.cpp
浏览该文件的文档.
2
3#include "hexmap/HexMapMesh.h"
4#include "hexmap/HexMetrics.h"
5#include "hexmap/HexNoise.h"
6
7#include <algorithm>
8#include <array>
9#include <cmath>
10#include <cstdint>
11#include <cstddef>
12#include <cstdio>
13#include <cstdlib>
14#include <map>
15#include <string>
16
17namespace eve::hexmap {
18
19namespace {
20
34[[nodiscard]] float dotOf(HexVec3 a, HexVec3 b) noexcept { return a.x * b.x + a.y * b.y + a.z * b.z; }
35
36[[nodiscard]] float lengthOf(HexVec3 v) noexcept { return std::sqrt(dotOf(v, v)); }
37
38[[nodiscard]] HexVec3 crossOf(HexVec3 a, HexVec3 b) noexcept {
39 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};
40}
41
42[[nodiscard]] HexVec3 normalize(HexVec3 v) noexcept {
43 const float len = lengthOf(v);
44 if (!(len > 1e-9f)) return HexVec3{0.f, 1.f, 0.f};
45 const float inverse = 1.f / len;
46 return HexVec3{v.x * inverse, v.y * inverse, v.z * inverse};
47}
48
50[[nodiscard]] HexVec3 slerpDirection(HexVec3 a, HexVec3 b, float t) noexcept {
51 const float cosine = std::clamp(dotOf(a, b), -1.f, 1.f);
52 const float angle = std::acos(cosine);
53 if (angle < 1e-5f) {
54 return normalize(HexVec3{a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t});
55 }
56 const float sine = std::sin(angle);
57 const float wa = std::sin((1.f - t) * angle) / sine;
58 const float wb = std::sin(t * angle) / sine;
59 return HexVec3{a.x * wa + b.x * wb, a.y * wa + b.y * wb, a.z * wa + b.z * wb};
60}
61
69[[nodiscard]] EdgeVertices sphereEdge(HexVec3 a, HexVec3 b) noexcept {
70 const HexVec3 dirA = normalize(a);
71 const HexVec3 dirB = normalize(b);
72 const float ra = lengthOf(a);
73 const float rb = lengthOf(b);
74
75 EdgeVertices edge;
76 HexVec3* samples[5] = {&edge.v1, &edge.v2, &edge.v3, &edge.v4, &edge.v5};
77 for (std::int32_t i = 0; i < 5; ++i) {
78 const float t = static_cast<float>(i) * 0.25f;
79 const float radius = ra + (rb - ra) * t;
80 *samples[i] = slerpDirection(dirA, dirB, t) * radius;
81 }
82 return edge;
83}
84
93[[nodiscard]] HexVec3 sphereTerraceLerp(HexVec3 a, HexVec3 b, std::int32_t step) noexcept {
94 const float horizontal = static_cast<float>(step) * HexMetrics::kHorizontalTerraceStepSize;
95 const float vertical = static_cast<float>((step + 1) / 2) * HexMetrics::kVerticalTerraceStepSize;
96 const HexVec3 direction = slerpDirection(normalize(a), normalize(b), horizontal);
97 const float ra = lengthOf(a);
98 const float rb = lengthOf(b);
99 return direction * (ra + (rb - ra) * vertical);
100}
101
103[[nodiscard]] EdgeVertices sphereEdgeTerraceLerp(const EdgeVertices& a, const EdgeVertices& b,
104 std::int32_t step) noexcept {
105 EdgeVertices result;
106 result.v1 = sphereTerraceLerp(a.v1, b.v1, step);
107 result.v2 = sphereTerraceLerp(a.v2, b.v2, step);
108 result.v3 = sphereTerraceLerp(a.v3, b.v3, step);
109 result.v4 = sphereTerraceLerp(a.v4, b.v4, step);
110 result.v5 = sphereTerraceLerp(a.v5, b.v5, step);
111 return result;
112}
113
122[[nodiscard]] bool perturbationDisabled() noexcept {
123#ifndef NDEBUG
124 static const bool disabled = std::getenv("EVP_NO_PERTURB") != nullptr;
125 return disabled;
126#else
127 return false;
128#endif
129}
130
144[[nodiscard]] HexVec3 tangentPerturb(const HexNoise& noise, HexVec3 position, float strength) noexcept {
145 if (perturbationDisabled()) return position;
146 const float radius = lengthOf(position);
147 if (!(radius > 1e-6f)) return position;
148 const HexVec3 up = position * (1.f / radius);
149 // A tangent frame that stays well conditioned over the whole sphere: the world axis
150 // least aligned with `up` is never parallel to it.
151 const HexVec3 axis = std::fabs(up.y) < 0.9f ? HexVec3{0.f, 1.f, 0.f} : HexVec3{1.f, 0.f, 0.f};
152 const HexVec3 tangentX = normalize(crossOf(axis, up));
153 const HexVec3 tangentY = crossOf(up, tangentX);
154
155 const HexVec4 first = noise.sample(position.x, position.z);
156 const HexVec4 second = noise.sample(position.y + 37.f, position.x - 11.f);
157 const float alongX = (first.x * 2.f - 1.f) * strength;
158 const float alongY = (second.z * 2.f - 1.f) * strength;
159 return normalize(position + tangentX * alongX + tangentY * alongY) * radius;
160}
161
163[[nodiscard]] std::int32_t terrainOf(const HexCellData* cell) noexcept {
164 return cell == nullptr ? 0 : cell->values.terrainType();
165}
166
168[[nodiscard]] HexVec3 sampleAt(const EdgeVertices& edge, std::int32_t index) noexcept {
169 switch (index) {
170 case 0: return edge.v1;
171 case 1: return edge.v2;
172 case 2: return edge.v3;
173 case 3: return edge.v4;
174 default: return edge.v5;
175 }
176}
177
179[[nodiscard]] std::int32_t elevationOf(const HexCellData* cell) noexcept {
180 return cell == nullptr ? 0 : cell->values.elevation();
181}
182
184[[nodiscard]] bool samePoint(HexVec3 a, HexVec3 b) noexcept {
185 // Matches the 1e-3 quantum the weld census uses, so "the same point" means the same thing
186 // here and there. The shortest legitimate edge on the sphere is a couple of units, so this
187 // cannot swallow real geometry.
188 constexpr float kEpsilon = 1e-3f;
189 return std::abs(a.x - b.x) < kEpsilon && std::abs(a.y - b.y) < kEpsilon && std::abs(a.z - b.z) < kEpsilon;
190}
191
207[[nodiscard]] bool facesInward(HexVec3 a, HexVec3 b, HexVec3 c) noexcept {
208 const HexVec3 ab = b - a;
209 const HexVec3 ac = c - a;
210 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};
211 // The centroid's direction from the origin is the outward direction; leaving both vectors
212 // unnormalised is fine because only the sign matters.
213 return normal.x * (a.x + b.x + c.x) + normal.y * (a.y + b.y + c.y) + normal.z * (a.z + b.z + c.z) < 0.f;
214}
215
217struct CornerCells {
218 HexVec3 up{};
219 HexVec3 left{};
220 HexVec3 right{};
221 const HexCellData* upCell = nullptr;
222 const HexCellData* leftCell = nullptr;
223 const HexCellData* rightCell = nullptr;
224};
225
227class SphereMesher {
228public:
235 [[nodiscard]] static float perturbScaleFromEnvironment() noexcept {
236#ifndef NDEBUG
237 static const float scale = [] {
238 if (const char* value = std::getenv("EVP_PERTURB_SCALE")) return static_cast<float>(std::strtod(value, nullptr));
239 return 1.f;
240 }();
241 return scale;
242#else
243 return 1.f;
244#endif
245 }
246
247 SphereMesher(const HexSphereMap& map, HexMeshData& out) noexcept
248 : map_(map), out_(out),
249 // The planar mesher perturbs by a fixed 40% of a cell's outer radius. `cellSpacing()` is
250 // the centre-to-centre distance, which is `sqrt(3)` times that radius rather than equal to
251 // it, so the fraction has to be divided by `sqrt(3)` to keep the same relative wobble.
252 perturbStrength_((HexMetrics::kCellPerturbStrength / (HexMetrics::kOuterRadius * 1.7320508075688772f)) *
253 map.cellSpacing() * perturbScaleFromEnvironment()), // River beds drop by the planar stream-bed offset expressed in elevation
254 // steps, so the channel keeps the same depth relative to the relief.
255 streamBedDrop_((HexMetrics::kStreamBedElevationOffset / HexMetrics::kElevationStep) * map.elevationStep()) {}
256
258 void run() {
259 for (HexSphereCell cell = 0; cell < map_.cellCount(); ++cell) buildCell(cell);
260 }
261
262private:
263 // --- edge frames --------------------------------------------------------
264
266 [[nodiscard]] HexVec3 solidCornerOf(HexSphereCell cell, std::int32_t corner) const noexcept {
267 const HexVec3 mixed =
268 slerpDirection(map_.direction(cell), map_.cornerDirection(cell, corner), HexMetrics::kSolidFactor);
269 return mixed * map_.surfaceRadius(cell);
270 }
271
273 [[nodiscard]] EdgeVertices solidEdgeOf(HexSphereCell cell, std::int32_t d) const noexcept {
274 const std::int32_t edges = map_.neighborCount(cell);
275 const std::int32_t nextD = (d + 1) % edges;
276 return sphereEdge(solidCornerOf(cell, d), solidCornerOf(cell, nextD));
277 }
278
286 [[nodiscard]] EdgeVertices farEdgeOf(HexSphereCell cell, std::int32_t d, HexSphereCell neighbour) const noexcept {
287 const std::int32_t back = map_.oppositeDirection(cell, d);
288 const std::int32_t nEdges = map_.neighborCount(neighbour);
289 if (back < 0 || nEdges <= 0) return EdgeVertices{};
290 return sphereEdge(solidCornerOf(neighbour, (back + 1) % nEdges), solidCornerOf(neighbour, back));
291 }
292
294 [[nodiscard]] bool riverThrough(HexSphereCell cell, std::int32_t d, const HexCellData* cellData) const noexcept {
295 if (cellData != nullptr && cellData->flags.hasRiverThrough(static_cast<HexDirection>(d))) return true;
296 const HexSphereCell neighbour = map_.neighbor(cell, d);
297 if (neighbour == kNoHexSphereCell) return false;
298 const HexCellData* other = map_.cellAt(neighbour);
299 const std::int32_t back = map_.oppositeDirection(cell, d);
300 return other != nullptr && back >= 0 && other->flags.hasRiverThrough(static_cast<HexDirection>(back));
301 }
302
303 // --- per-cell entry point -----------------------------------------------
304
306 void buildCell(HexSphereCell cell) {
307 const HexCellData* cellData = map_.cellAt(cell);
308 if (cellData == nullptr) return;
309
310 const HexVec3 center = map_.cellPosition(cell);
311 const std::int32_t edges = map_.neighborCount(cell);
312 for (std::int32_t d = 0; d < edges; ++d) {
313 EdgeVertices near = solidEdgeOf(cell, d);
314 if (riverThrough(cell, d, cellData)) {
315 // Carve a shallow channel along the river centreline (the middle sample only).
316 near.v3 = normalize(near.v3) * (lengthOf(near.v3) + streamBedDrop_);
317 }
318 appendEdgeFan(center, near, cellData);
319
320 const HexSphereCell neighbour = map_.neighbor(cell, d);
321 if (neighbour == kNoHexSphereCell) continue;
322 const HexCellData* neighbourData = map_.cellAt(neighbour);
323 if (neighbourData == nullptr) continue;
324 appendConnection(cell, d, near, cellData, neighbour, neighbourData);
325 }
326 }
327
329 void appendConnection(HexSphereCell cell, std::int32_t d, const EdgeVertices& near, const HexCellData* cellData,
330 HexSphereCell neighbour, const HexCellData* neighbourData) {
331 // Edge ownership rule. The planar mesher leans on the chunk grid here; a sphere
332 // has none, so the lower cell id owns the edge instead. That is a total order
333 // over the same set of shared edges, so every boundary is still emitted exactly
334 // once, and never by both of its cells.
335 if (cell > neighbour) return;
336
337 const EdgeVertices far = farEdgeOf(cell, d, neighbour);
338
339 const HexTerrainWeights nearWeights = HexTerrainWeights::primary();
340 const HexTerrainWeights farWeights = HexTerrainWeights::blend(1.f);
341
342 switch (map_.edgeTypeTo(cell, neighbour)) {
344 appendBlendStrip(near, far, nearWeights, farWeights, cellData, neighbourData);
345 break;
347 appendEdgeTerraces(near, nearWeights, far, farWeights, cellData, neighbourData);
348 break;
350 // The blend strip would already be a radial wall here, but its winding
351 // flips with the sign of the elevation step; the vertical winding is
352 // correct in both directions. Split into the same four sub-quads as a
353 // flat strip so the wall does not leave a T-junction against the fan.
354 for (std::int32_t i = 0; i < 4; ++i) {
355 emitQuadVertical(sampleAt(near, i), sampleAt(near, i + 1), sampleAt(far, i), sampleAt(far, i + 1),
356 nearWeights, nearWeights, farWeights, farWeights, cellData, neighbourData, cellData);
357 }
358 break;
359 }
360
361 appendCorner(cell, d, cellData, neighbour, neighbourData);
362 }
363
364 // --- corner -------------------------------------------------------------
365
375 void appendCorner(HexSphereCell cell, std::int32_t d, const HexCellData* cellData, HexSphereCell neighbour,
376 const HexCellData* neighbourData) {
377 const std::int32_t edges = map_.neighborCount(cell);
378 const std::int32_t nextD = (d + 1) % edges;
379 const HexSphereCell nextCell = map_.neighbor(cell, nextD);
380 if (nextCell == kNoHexSphereCell) return;
381 const HexCellData* nextData = map_.cellAt(nextCell);
382 if (nextData == nullptr) return;
383
384 // Corner ownership. `appendConnection` already returned unless `cell < neighbour`,
385 // which is the right total order for an *edge* - two cells share it. A corner is
386 // shared by three, so reusing that gate emits the patch for one or two of them and
387 // half of all corners end up with two coincident patches (non-manifold edges and a
388 // broken Euler characteristic). The lowest id of the three cells owns the corner,
389 // which is exactly one emitter and is always a cell that owns its preceding edge.
390 if (nextCell < cell) return;
391
392 // The corner between edge `d` and edge `d + 1` of `cell` is corner `d + 1`. Each
393 // neighbour reaches that same corner from the far end of its own shared edge,
394 // which the topology stores in the opposite order.
395 const std::int32_t back = map_.oppositeDirection(cell, d);
396 const std::int32_t back2 = map_.oppositeDirection(cell, nextD);
397 const std::int32_t nextEdges = map_.neighborCount(nextCell);
398 if (back < 0 || back2 < 0 || nextEdges <= 0) return;
399
400 CornerCells corner{};
401 corner.up = solidCornerOf(cell, nextD);
402 corner.right = solidCornerOf(neighbour, back);
403 corner.left = solidCornerOf(nextCell, (back2 + 1) % nextEdges);
404 corner.upCell = cellData;
405 corner.rightCell = neighbourData;
406 corner.leftCell = nextData;
407 appendCornerTriangles(corner);
408 }
409
411 void appendCornerTriangles(const CornerCells& corner) {
412 const std::array<HexVec3, 3> positions{corner.up, corner.left, corner.right};
413 const std::array<const HexCellData*, 3> cells{corner.upCell, corner.leftCell, corner.rightCell};
414 std::array<std::int32_t, 3> order{0, 1, 2};
415
416 // Sorting by elevation is what lets the seven cases below be written against
417 // "bottom/low/high", but it is a permutation of the canonical {up, left, right} order and
418 // the parity of that permutation is *lost* when the three roles are renamed. An odd
419 // permutation reverses the cyclic sense of the corner, so every triangle emitted for it
420 // has to be wound the other way. The dispatch below only ever rotates the triple, which
421 // is even, so this sort is the single source of parity for the whole corner.
422 std::int32_t swaps = 0;
423 for (std::int32_t i = 0; i < 2; ++i) {
424 for (std::int32_t j = i + 1; j < 3; ++j) {
425 if (elevationOf(cells[static_cast<std::size_t>(order[static_cast<std::size_t>(j)])]) <
426 elevationOf(cells[static_cast<std::size_t>(order[static_cast<std::size_t>(i)])])) {
427 std::swap(order[static_cast<std::size_t>(i)], order[static_cast<std::size_t>(j)]);
428 ++swaps;
429 }
430 }
431 }
432
433 const HexVec3 bottom = positions[static_cast<std::size_t>(order[0])];
434 const HexVec3 low = positions[static_cast<std::size_t>(order[1])];
435 const HexVec3 high = positions[static_cast<std::size_t>(order[2])];
436 const HexCellData* bottomCell = cells[static_cast<std::size_t>(order[0])];
437 const HexCellData* lowCell = cells[static_cast<std::size_t>(order[1])];
438 const HexCellData* highCell = cells[static_cast<std::size_t>(order[2])];
439
440 const HexEdgeType lowEdge = edgeType(elevationOf(bottomCell), elevationOf(lowCell));
441 const HexEdgeType highEdge = edgeType(elevationOf(bottomCell), elevationOf(highCell));
442
443 // Three cells at one elevation put all three solid corners on the same point, so the
444 // patch has no area to fill. Emitting it anyway adds a triangle whose every edge is a
445 // self-loop, and the census counts those as an edge used twice.
446 if (samePoint(bottom, low) && samePoint(bottom, high)) return;
447
448 const bool previousMirror = mirrorCorner_;
449 const bool previousIn = inCorner_;
450 cornerParity_ = (swaps % 2) != 0;
451 mirrorCorner_ = cornerParity_;
452 inCorner_ = true;
453 if (lowEdge == HexEdgeType::Slope && highEdge == HexEdgeType::Slope) {
454 mirrorCorner_ = cornerParity_;
455 cornerTerraces(bottom, low, high, bottomCell, lowCell, highCell);
456 } else if (lowEdge == HexEdgeType::Slope && highEdge == HexEdgeType::Flat) {
457 // `low` is the odd cell out and `high` sits level with `bottom`, so the fan climbs
458 // from that flat pair to `low` rather than descending from it.
459 mirrorCorner_ = cornerParity_;
460 cornerTerracesToApex(high, highCell, bottom, bottomCell, low, lowCell);
461 } else if (lowEdge == HexEdgeType::Flat && highEdge == HexEdgeType::Slope) {
462 // Mirror of the above: `bottom` and `low` are level and `high` is the apex.
463 mirrorCorner_ = cornerParity_;
464 cornerTerracesToApex(bottom, bottomCell, low, lowCell, high, highCell);
465 } else if (lowEdge == HexEdgeType::Slope && highEdge == HexEdgeType::Cliff) {
466 mirrorCorner_ = cornerParity_;
467 cornerTerracesCliff(bottom, low, high, bottomCell, lowCell, highCell);
468 } else if (lowEdge == HexEdgeType::Cliff && highEdge == HexEdgeType::Slope) {
469 mirrorCorner_ = cornerParity_;
470 cornerCliffTerraces(bottom, low, high, bottomCell, lowCell, highCell);
471 } else if (edgeType(elevationOf(lowCell), elevationOf(highCell)) == HexEdgeType::Slope) {
472 // Neither side out of `bottom` is a slope: both are cliffs, so both of the corner's
473 // sides there are those walls' single straight end edges, and the only side carrying
474 // rungs is `low -> high`. Fanning from `bottom` puts the two straight chords along the
475 // walls and lays the ladder on the third side.
476 //
477 // The reference rotates its three cells here and hands them to a cliff-corner builder
478 // in that rotated order; those builders read their arguments as bottom/low/high, so a
479 // corner whose three cells all sit at different heights came out with the wrong side
480 // terraced. It takes all three differing, which is why the sparse elevation patterns
481 // never reached it and a generated planet does.
482 mirrorCorner_ = cornerParity_;
483 const HexTerrainWeights w = HexTerrainWeights::primary();
484 appendBoundaryTriangle(bottom, w, low, w, high, w, bottomCell, lowCell, highCell);
485 } else {
486 mirrorCorner_ = cornerParity_;
487 // Mirror of the reference winding: this engine's front face is the opposite
488 // handedness, so the flat corner fan is emitted high-before-low.
489 {
490 emitTriangle(bottom, high, low, HexTerrainWeights::primary(), bottomCell, lowCell, highCell);
491 }
492 }
493 mirrorCorner_ = previousMirror;
494 inCorner_ = previousIn;
495 }
496
498 void cornerTerraces(const HexVec3& bottom, const HexVec3& left, const HexVec3& right, const HexCellData* bottomCell,
499 const HexCellData* leftCell, const HexCellData* rightCell) {
500 const HexTerrainWeights bottomWeights = HexTerrainWeights::primary();
501 const HexTerrainWeights leftWeights = HexTerrainWeights::primary();
502 const HexTerrainWeights rightWeights = HexTerrainWeights::primary();
503
504 HexVec3 lastLeft = bottom;
505 HexVec3 lastRight = bottom;
506 HexTerrainWeights lastLeftWeights = bottomWeights;
507 HexTerrainWeights lastRightWeights = bottomWeights;
508 for (std::int32_t step = 1; step < HexMetrics::kTerracesPerSlope * 2; ++step) {
509 const float t = static_cast<float>(step) * HexMetrics::kHorizontalTerraceStepSize;
510 const HexTerrainWeights wl = HexTerrainWeights::lerp(bottomWeights, leftWeights, t);
511 const HexTerrainWeights wr = HexTerrainWeights::lerp(bottomWeights, rightWeights, t);
512 const HexVec3 boundaryLeft = sphereTerraceLerp(bottom, left, step);
513 const HexVec3 boundaryRight = sphereTerraceLerp(bottom, right, step);
514
515 if (step == 1) {
516 // The first band is a triangle, not a quad. Both trailing points are `bottom`,
517 // so `emitQuadForward` would add the degenerate `(boundaryRight, bottom, bottom)`
518 // alongside the real triangle, and both of them traverse `bottom -> boundaryRight`
519 // the same way - one directed edge used twice, i.e. a face wound against its
520 // neighbour. The planar builder emits a triangle here for the same reason.
521 emitTriangle(boundaryLeft, bottom, boundaryRight, wl, lastLeftWeights, wr, bottomCell, leftCell,
522 rightCell);
523 } else if (!samePoint(boundaryLeft, lastLeft) || !samePoint(boundaryRight, lastRight)) {
524 // The ladder always runs `kTerracesPerSlope * 2` steps, so a slope shallower than
525 // that lands the later steps on the same point: the quad has no width and its two
526 // triangles traverse the shared edge the same way. Skip it rather than emit it.
527 emitQuadForward(boundaryLeft, boundaryRight, lastLeft, lastRight, wl, wr, lastLeftWeights,
528 lastRightWeights, bottomCell, leftCell, rightCell);
529 }
530 lastLeft = boundaryLeft;
531 lastRight = boundaryRight;
532 lastLeftWeights = wl;
533 lastRightWeights = wr;
534 }
535 if (!samePoint(lastLeft, left) || !samePoint(lastRight, right)) {
536 // The closing band leads with the destination and trails with the last rung, exactly
537 // like every band in the loop above. Passing `(lastLeft, lastRight, left, right)`
538 // instead puts the last rung at the leading end of both this band and the one emitted
539 // for the final step, so the two traverse their shared edge the same way.
540 emitQuadForward(left, right, lastLeft, lastRight, leftWeights, rightWeights, lastLeftWeights,
541 lastRightWeights, bottomCell, leftCell, rightCell);
542 }
543 }
544
557 void cornerTerracesToApex(const HexVec3& left, const HexCellData* leftCell, const HexVec3& right,
558 const HexCellData* rightCell, const HexVec3& apex, const HexCellData* apexCell) {
559 const HexTerrainWeights leftWeights = HexTerrainWeights::primary();
560 const HexTerrainWeights rightWeights = HexTerrainWeights::primary();
561 const HexTerrainWeights apexWeights = HexTerrainWeights::primary();
562
563 HexVec3 lastLeft = left;
564 HexVec3 lastRight = right;
565 HexTerrainWeights lastLeftWeights = leftWeights;
566 HexTerrainWeights lastRightWeights = rightWeights;
567 for (std::int32_t step = 1; step < HexMetrics::kTerracesPerSlope * 2; ++step) {
568 const float t = static_cast<float>(step) * HexMetrics::kHorizontalTerraceStepSize;
569 const HexTerrainWeights wl = HexTerrainWeights::lerp(leftWeights, apexWeights, t);
570 const HexTerrainWeights wr = HexTerrainWeights::lerp(rightWeights, apexWeights, t);
571 const HexVec3 bl = sphereTerraceLerp(left, apex, step);
572 const HexVec3 br = sphereTerraceLerp(right, apex, step);
573
574 emitQuadForward(lastLeft, lastRight, bl, br, lastLeftWeights, lastRightWeights, wl, wr, apexCell,
576 lastLeft = bl;
577 lastRight = br;
578 lastLeftWeights = wl;
579 lastRightWeights = wr;
580 }
581 emitTriangle(lastLeft, apex, lastRight, lastLeftWeights, apexWeights, lastRightWeights, apexCell, leftCell,
582 rightCell);
583 }
584
596 void appendBoundaryTriangle(HexVec3 apex, const HexTerrainWeights& apexWeights, HexVec3 from,
597 const HexTerrainWeights& fromWeights, HexVec3 to,
598 const HexTerrainWeights& toWeights, const HexCellData* t0, const HexCellData* t1,
599 const HexCellData* t2) {
600 // When the ladder starts at the apex the first band has two coincident corners and covers
601 // nothing, but it would still contribute three directed edges.
602 const bool ladderStartsAtApex = samePoint(apex, from);
603 HexVec3 last = from;
604 HexTerrainWeights lastWeights = fromWeights;
605 for (std::int32_t step = 1; step < HexMetrics::kTerracesPerSlope * 2; ++step) {
606 const float t = static_cast<float>(step) * HexMetrics::kHorizontalTerraceStepSize;
607 const HexTerrainWeights w = HexTerrainWeights::lerp(fromWeights, toWeights, t);
608 const HexVec3 rung = sphereTerraceLerp(from, to, step);
609 if (step > 1 || !ladderStartsAtApex) {
610 emitTriangle(rung, last, apex, w, lastWeights, apexWeights, t0, t1, t2);
611 }
612 last = rung;
613 lastWeights = w;
614 }
615 emitTriangle(to, last, apex, toWeights, lastWeights, apexWeights, t0, t1, t2);
616 }
617
633 void cornerTerracesCliff(const HexVec3& bottom, const HexVec3& low, const HexVec3& high,
634 const HexCellData* bottomCell, const HexCellData* lowCell, const HexCellData* highCell) {
635 const HexTerrainWeights w = HexTerrainWeights::primary();
636 if (edgeType(elevationOf(lowCell), elevationOf(highCell)) == HexEdgeType::Slope) {
637 // Both `bottom -> low` and `low -> high` carry rungs, and each is measured from its own
638 // lower end, which is the direction the bands along those edges use. The corner is
639 // therefore a fan from `bottom` over the two ladders laid end to end, closed by the
640 // straight `high -> bottom` chord, with the two calls meeting along the interior
641 // `bottom -> low` chord.
642 appendBoundaryTriangle(bottom, w, bottom, w, low, w, bottomCell, lowCell, highCell);
643 appendBoundaryTriangle(bottom, w, low, w, high, w, bottomCell, lowCell, highCell);
644 } else {
645 // Only `bottom -> low` carries rungs. Fanning from `high` leaves `low -> high` and
646 // `high -> bottom` as single segments, so the wall's end edge is met edge for edge.
647 appendBoundaryTriangle(high, w, bottom, w, low, w, bottomCell, lowCell, highCell);
648 }
649 }
650
659 void cornerCliffTerraces(const HexVec3& bottom, const HexVec3& low, const HexVec3& high,
660 const HexCellData* bottomCell, const HexCellData* lowCell, const HexCellData* highCell) {
661 const HexTerrainWeights w = HexTerrainWeights::primary();
662 if (edgeType(elevationOf(highCell), elevationOf(lowCell)) == HexEdgeType::Slope) {
663 appendBoundaryTriangle(bottom, w, bottom, w, high, w, bottomCell, lowCell, highCell);
664 appendBoundaryTriangle(bottom, w, high, w, low, w, bottomCell, lowCell, highCell);
665 } else {
666 appendBoundaryTriangle(low, w, bottom, w, high, w, bottomCell, lowCell, highCell);
667 }
668 }
669
670 // --- vertex and triangle primitives -------------------------------------
671
679 void vertexAt(HexVec3 perturbed, const HexTerrainWeights& weights, const HexCellData* t0, const HexCellData* t1,
680 const HexCellData* t2) {
681 const float u = HexTerrainVertexCode::encodeIndices(terrainOf(t0), terrainOf(t1), terrainOf(t2));
683 out_.addVertex(perturbed, u, v);
684 }
685
687 [[nodiscard]] HexVec3 perturbedPosition(HexVec3 position) const noexcept {
688 return tangentPerturb(map_.noise(), position, perturbStrength_);
689 }
690
700 [[nodiscard]] bool reverseWinding() const noexcept { return !mirrorCorner_; }
701
703 [[nodiscard]] static bool degenerate(HexVec3 a, HexVec3 b, HexVec3 c) noexcept {
704 return samePoint(a, b) || samePoint(b, c) || samePoint(a, c);
705 }
706
708 void emitTriangle(const HexVec3& p0, const HexVec3& p1, const HexVec3& p2, const HexTerrainWeights& w,
709 const HexCellData* t0, const HexCellData* t1, const HexCellData* t2) {
710 // A triangle whose corners weld together covers no area, but its edges are real directed
711 // edges: leaving it in splits one vertex into two and reports a boundary loop that is not
712 // a hole. Dropping it cannot open one, because it covers nothing.
713 const HexVec3 a0 = perturbedPosition(p0);
714 const HexVec3 a1 = perturbedPosition(p1);
715 const HexVec3 a2 = perturbedPosition(p2);
716 if (degenerate(a0, a1, a2)) return;
717 const std::uint32_t i0 = static_cast<std::uint32_t>(out_.vertexCount());
718 const bool reverse = reverseWinding();
719 vertexAt(a0, w, t0, t1, t2);
720 vertexAt(a1, w, t0, t1, t2);
721 vertexAt(a2, w, t0, t1, t2);
722 if (reverse)
723 out_.addTriangle(i0, i0 + 2u, i0 + 1u);
724 else
725 out_.addTriangle(i0, i0 + 1u, i0 + 2u);
726 }
727
729 void emitTriangle(const HexVec3& p0, const HexVec3& p1, const HexVec3& p2, const HexTerrainWeights& w0,
730 const HexTerrainWeights& w1, const HexTerrainWeights& w2, const HexCellData* t0,
731 const HexCellData* t1, const HexCellData* t2) {
732 const HexVec3 a0 = perturbedPosition(p0);
733 const HexVec3 a1 = perturbedPosition(p1);
734 const HexVec3 a2 = perturbedPosition(p2);
735 if (degenerate(a0, a1, a2)) return;
736 const std::uint32_t i0 = static_cast<std::uint32_t>(out_.vertexCount());
737 const bool reverse = reverseWinding();
738 vertexAt(a0, w0, t0, t1, t2);
739 vertexAt(a1, w1, t0, t1, t2);
740 vertexAt(a2, w2, t0, t1, t2);
741 if (reverse)
742 out_.addTriangle(i0, i0 + 2u, i0 + 1u);
743 else
744 out_.addTriangle(i0, i0 + 1u, i0 + 2u);
745 }
746
754 void emitQuadForward(const HexVec3& p0, const HexVec3& p1, const HexVec3& p2, const HexVec3& p3,
755 const HexTerrainWeights& w0, const HexTerrainWeights& w1, const HexTerrainWeights& w2,
756 const HexTerrainWeights& w3, const HexCellData* t0, const HexCellData* t1,
757 const HexCellData* t2) {
758 const std::uint32_t i0 = static_cast<std::uint32_t>(out_.vertexCount());
759 const HexVec3 a0 = perturbedPosition(p0);
760 const HexVec3 a1 = perturbedPosition(p1);
761 const HexVec3 a2 = perturbedPosition(p2);
762 const HexVec3 a3 = perturbedPosition(p3);
763 // Each half is dropped on its own: a quad spanning a step can have one half collapse while
764 // the other still covers area.
765 const bool firstOk = !degenerate(a0, a2, a1);
766 const bool secondOk = !degenerate(a1, a2, a3);
767 if (!firstOk && !secondOk) return;
768 vertexAt(a0, w0, t0, t1, t2);
769 vertexAt(a1, w1, t0, t1, t2);
770 vertexAt(a2, w2, t0, t1, t2);
771 vertexAt(a3, w3, t0, t1, t2);
772 // Oriented as a unit: deciding each half on its own leaves the shared diagonal traversed
773 // the same way twice whenever the two halves disagree, and one of them is then culled.
774 const bool reverse = reverseWinding();
775 if (firstOk) {
776 if (reverse)
777 out_.addTriangle(i0, i0 + 1u, i0 + 2u);
778 else
779 out_.addTriangle(i0, i0 + 2u, i0 + 1u);
780 }
781 if (secondOk) {
782 if (reverse)
783 out_.addTriangle(i0 + 2u, i0 + 1u, i0 + 3u);
784 else
785 out_.addTriangle(i0 + 1u, i0 + 2u, i0 + 3u);
786 }
787 }
788
790 void emitQuadVertical(const HexVec3& p0, const HexVec3& p1, const HexVec3& p2, const HexVec3& p3,
791 const HexTerrainWeights& w0, const HexTerrainWeights& w1, const HexTerrainWeights& w2,
792 const HexTerrainWeights& w3, const HexCellData* t0, const HexCellData* t1,
793 const HexCellData* t2) {
794 emitQuadForward(p0, p1, p2, p3, w0, w1, w2, w3, t0, t1, t2);
795 }
796
798 void appendEdgeFan(HexVec3 center, const EdgeVertices& edge, const HexCellData* cellData) {
799 const HexTerrainWeights w = HexTerrainWeights::primary();
800 const HexVec3 samples[5] = {edge.v1, edge.v2, edge.v3, edge.v4, edge.v5};
801 for (std::int32_t i = 0; i < 4; ++i) {
802 emitTriangle(center, samples[i], samples[i + 1], w, cellData, cellData, cellData);
803 }
804 }
805
807 void appendBlendStrip(const EdgeVertices& near, const EdgeVertices& far, const HexTerrainWeights& nearWeights,
808 const HexTerrainWeights& farWeights, const HexCellData* nearCell,
809 const HexCellData* farCell) {
810 const HexVec3 nearPositions[5] = {near.v1, near.v2, near.v3, near.v4, near.v5};
811 const HexVec3 farPositions[5] = {far.v1, far.v2, far.v3, far.v4, far.v5};
812 for (std::int32_t i = 0; i < 4; ++i) {
813 const float t0 = static_cast<float>(i) * 0.25f;
814 const float t1 = static_cast<float>(i + 1) * 0.25f;
815 const HexTerrainWeights w0 = HexTerrainWeights::lerp(nearWeights, farWeights, t0);
816 const HexTerrainWeights w1 = HexTerrainWeights::lerp(nearWeights, farWeights, t1);
817 // Both vertices of an edge share their parameter: the near pair carries w0
818 // and the far pair carries w1.
819 emitQuadForward(nearPositions[i], nearPositions[i + 1], farPositions[i], farPositions[i + 1], w0, w0, w1,
820 w1, nearCell, farCell, nearCell);
821 }
822 }
823
825 void appendEdgeTerraces(EdgeVertices near, const HexTerrainWeights& nearWeights, EdgeVertices far,
826 const HexTerrainWeights& farWeights, const HexCellData* nearCell,
827 const HexCellData* farCell) {
828 // The ladder always climbs from the low side to the high side, whichever cell is emitting.
829 // `sphereTerraceLerp` measures its vertical term from its *first* argument, so a ladder
830 // built high-to-low lands on a different set of rungs than the neighbouring patches use;
831 // the emitting side therefore only decides the band's argument order, which is winding.
832 // The planar builder splits the same two cases (`appendSlopeTerraces`).
833 const bool nearIsLow = elevationOf(nearCell) <= elevationOf(farCell);
834 const EdgeVertices& low = nearIsLow ? near : far;
835 const EdgeVertices& high = nearIsLow ? far : near;
836 const HexTerrainWeights& lowW = nearIsLow ? nearWeights : farWeights;
837 const HexTerrainWeights& highW = nearIsLow ? farWeights : nearWeights;
838
839 EdgeVertices previous = low;
840 HexTerrainWeights previousW = lowW;
841 for (std::int32_t step = 1; step < HexMetrics::kTerracesPerSlope * 2; ++step) {
842 const HexTerrainWeights w = HexTerrainWeights::lerp(
843 lowW, highW, static_cast<float>(step) * HexMetrics::kHorizontalTerraceStepSize);
844 const EdgeVertices boundary = sphereEdgeTerraceLerp(low, high, step);
845 if (nearIsLow)
846 appendEdgeBand(previous, boundary, previousW, w, nearCell, farCell);
847 else
848 appendEdgeBand(boundary, previous, w, previousW, nearCell, farCell);
850 previousW = w;
851 }
852 if (nearIsLow)
853 appendEdgeBand(previous, high, previousW, highW, nearCell, farCell);
854 else
855 appendEdgeBand(high, previous, highW, previousW, nearCell, farCell);
856 }
857
867 void appendEdgeBand(const EdgeVertices& from, const EdgeVertices& to, const HexTerrainWeights& fromWeights,
868 const HexTerrainWeights& toWeights, const HexCellData* nearCell, const HexCellData* farCell) {
869 for (std::int32_t i = 0; i < 4; ++i) {
870 emitQuadForward(sampleAt(from, i), sampleAt(from, i + 1), sampleAt(to, i), sampleAt(to, i + 1),
871 fromWeights, fromWeights, toWeights, toWeights, nearCell, farCell, nearCell);
872 }
873 }
874
875 const HexSphereMap& map_;
876 HexMeshData& out_;
877 float perturbStrength_;
878 float streamBedDrop_;
879
888 bool mirrorCorner_ = false;
890 bool inCorner_ = false;
892 bool cornerParity_ = false;
893};
894
896class SphereWaterMesher {
897public:
898 SphereWaterMesher(const HexSphereMap& map, HexMeshData& out) noexcept : map_(map), out_(out) {}
899
901 void run() {
902 for (HexSphereCell cell = 0; cell < map_.cellCount(); ++cell) buildCell(cell);
903 }
904
905private:
913 [[nodiscard]] float waterRadius(std::int32_t waterLevel) const noexcept {
915 return map_.sphereRadius() + (static_cast<float>(waterLevel) + offset) * map_.elevationStep();
916 }
917
918 void buildCell(HexSphereCell cell) {
919 const HexCellData* data = map_.cellAt(cell);
920 if (data == nullptr || !data->values.isUnderwater()) return;
921
922 // The shore parameter the water shader expects: 1 where the water ends against
923 // land, 0 in open ocean.
924 float shore = 0.f;
925 const std::int32_t edges = map_.neighborCount(cell);
926 for (std::int32_t d = 0; d < edges; ++d) {
927 const HexCellData* neighbour = map_.cellAt(map_.neighbor(cell, d));
928 if (neighbour == nullptr || !neighbour->values.isUnderwater()) {
929 shore = 1.f;
930 break;
931 }
932 }
933
934 const float radius = waterRadius(data->values.waterLevel());
935 const HexVec3 center = map_.direction(cell) * radius;
936 const std::int32_t corners = map_.cornerCountOf(cell);
937 for (std::int32_t k = 0; k < corners; ++k) {
938 const HexVec3 a = map_.cornerDirection(cell, k) * radius;
939 const HexVec3 b = map_.cornerDirection(cell, (k + 1) % corners) * radius;
940 const std::uint32_t i0 = static_cast<std::uint32_t>(out_.vertexCount());
941 out_.addVertex(center, shore, 0.f);
942 out_.addVertex(a, shore, 0.f);
943 out_.addVertex(b, shore, 0.f);
944 // A cap wound inward is culled, which reads as a hole in the sea, so the orientation
945 // is taken from the vertices rather than from the corner ordering.
946 if (facesInward(center, a, b))
947 out_.addTriangle(i0, i0 + 2u, i0 + 1u);
948 else
949 out_.addTriangle(i0, i0 + 1u, i0 + 2u);
950 }
951 }
952
953 const HexSphereMap& map_;
954 HexMeshData& out_;
955};
956
957} // namespace
958
960 out.clear();
961 if (map.empty()) {
962 out.finalize();
963 return;
964 }
965
966 SphereMesher mesher(map, out);
967 mesher.run();
968 out.finalize();
969}
970
972 out.clear();
973 if (map.empty()) {
974 out.finalize();
975 return;
976 }
977
978 SphereWaterMesher mesher(map, out);
979 mesher.run();
980 out.finalize();
981}
982
983} // namespace eve::hexmap
double value
float w
Definition AnimClip.cpp:738
std::string from
uint32_t i0
Definition Grass.cpp:61
float u
Definition Grass.cpp:233
std::uint32_t ab
std::uint32_t ac
std::vector< float > positions
std::int32_t waterLevel
Per-chunk mesh generation for every hex map surface.
Pointy-top hex metrics, directions and vertex helpers.
Deterministic asset-free noise for hex perturbation and generation.
float v
HexVec3 up
const HexCellData * leftCell
HexVec3 left
HexVec3 right
const HexCellData * rightCell
const HexCellData * upCell
Mesh generation for the spherical hex map surface.
std::int32_t second
std::int32_t c
std::int32_t first
HexCoordinates to
Cell the unit walks towards on this segment.
Definition HexUnits.cpp:64
size_t offset
std::array< float, 3 > position
std::array< float, 3 > scale
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::vector< std::int32_t > order
Texture * normal
graphics::Canvas * previous
std::weak_ptr< Run > run
Definition OnnxGpgpu.cpp:25
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
float radius
float d
float t
RoadLaneDirection direction
const RoadEdge * edge
Cell cell
CommandLogBoundary boundary
Anchor rule, see above.
float weights[3]
float step
Definition TreeMesh.cpp:314
uint32_t index
std::vector< int > edges
float bottom
float angle
CPU-side triangle soup for one hex chunk surface.
Definition HexMeshData.h:24
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 constexpr int kTerracesPerSlope
Terraces generated per slope.
Definition HexMetrics.h:122
static constexpr float kVerticalTerraceStepSize
Vertical fraction of one terrace interpolation step.
Definition HexMetrics.h:128
static constexpr float kElevationStep
Vertical distance between two elevation levels.
Definition HexMetrics.h:120
static constexpr float kHorizontalTerraceStepSize
Horizontal fraction of one terrace interpolation step.
Definition HexMetrics.h:126
static constexpr float kWaterElevationOffset
Elevation offset of a water surface relative to the water level.
Definition HexMetrics.h:144
static constexpr float kCellPerturbStrength
Strength of the XZ position perturbation.
Definition HexMetrics.h:138
static constexpr float kOuterRadius
Outer (corner) radius of one hex cell in world units.
Definition HexMetrics.h:165
static constexpr float kStreamBedElevationOffset
Elevation offset of a carved river bed relative to the cell.
Definition HexMetrics.h:142
static constexpr float kSolidFactor
Factor of the solid, uniform-colour hex inscribed in a cell.
Definition HexMetrics.h:130
An editable hex map wrapped onto a sphere.
bool empty() const noexcept
Whether the map holds any cell.
void buildSphereWaterMesh(const HexSphereMap &map, HexMeshData &out)
Builds the ocean surface of a spherical hex map.
constexpr HexEdgeType edgeType(int elevation1, int elevation2) noexcept
The relationship between two elevations (single-step changes are slopes).
Definition HexMetrics.h:96
HexEdgeType
Relationship between two neighbouring cells of different elevation.
Definition HexMetrics.h:93
constexpr HexSphereCell kNoHexSphereCell
Returned by a spherical cell query that has no answer.
std::int32_t HexSphereCell
Dense identifier of one cell of a spherical hex topology.
void buildSphereTerrainMesh(const HexSphereMap &map, HexMeshData &out)
Builds the ground, terrace and cliff surface of a spherical hex map.
HexDirection
Hex facing directions, counter-clockwise from north-east.
Definition HexMetrics.h:59
Vec2 normalize(const Vec2 &a)
Normalize.
Definition UrbanTypes.h:44
int axis(int64_t a, size_t rank)
Axis.
static float encodeIndices(std::int32_t a, std::int32_t b, std::int32_t c) noexcept
First texture coordinate, carrying the three terrain indices.
Definition HexMapMesh.h:31
static float encodeWeights(float weightB, float weightC) noexcept
Second texture coordinate, carrying the secondary/tertiary weights.
Definition HexMapMesh.h:35
static HexTerrainWeights blend(float t) noexcept
Weights of two cells, t towards the second.
Definition HexMapMesh.h:47
static HexTerrainWeights lerp(const HexTerrainWeights &a, const HexTerrainWeights &b, float t) noexcept
Interpolates two weight sets.
Definition HexMapMesh.h:53
static HexTerrainWeights primary() noexcept
Weights of the first cell alone.
Definition HexMapMesh.h:45