载入中...
搜索中...
未找到
HexMapGenerator.cpp
浏览该文件的文档.
1
7
8#include "common/Diagnostic.h"
9#include "hexmap/HexSearch.h"
10
11#include <algorithm>
12#include <cmath>
13#include <cstddef>
14#include <cstdint>
15#include <limits>
16#include <string>
17#include <utility>
18#include <vector>
19
20namespace eve::hexmap {
21namespace {
22
23// ---------------------------------------------------------------------------
24// Deterministic randomness
25// ---------------------------------------------------------------------------
26
34class HexRandom {
35public:
37 explicit HexRandom(std::uint32_t seed) noexcept : state_(seed == 0u ? 0x9E3779B9u : seed) {}
38
43 [[nodiscard]] float unit() noexcept {
44 state_ ^= state_ << 13u;
45 state_ ^= state_ >> 17u;
46 state_ ^= state_ << 5u;
47 // 24 significant bits keep the conversion exact in single precision.
48 return static_cast<float>(state_ >> 8) * (1.f / 16777216.f);
49 }
50
56 [[nodiscard]] std::int32_t index(std::int32_t count) noexcept {
57 if (count <= 0) return 0;
58 const auto span = static_cast<std::uint32_t>(count);
59 return static_cast<std::int32_t>(static_cast<std::uint32_t>(unit() * static_cast<float>(span))) % count;
60 }
61
68 [[nodiscard]] std::int32_t range(std::int32_t minimum, std::int32_t maximumExclusive) noexcept {
69 if (maximumExclusive <= minimum) return minimum;
70 return minimum + index(maximumExclusive - minimum);
71 }
72
78 [[nodiscard]] bool chance(float probability) noexcept { return unit() < probability; }
79
80private:
81 std::uint32_t state_;
82};
83
90[[nodiscard]] std::uint32_t mix(std::uint32_t x, std::uint32_t y, std::uint32_t z) noexcept {
91 std::uint32_t h = x * 0x9E3779B1u;
92 h ^= y * 0x85EBCA77u;
93 h = (h ^ (h >> 15)) * 0xC2B2AE3Du;
94 h ^= z * 0x27D4EB2Fu;
95 h = (h ^ (h >> 13)) * 0x165667B1u;
96 return h ^ (h >> 16);
97}
98
100[[nodiscard]] float lattice(std::int32_t ix, std::int32_t iz, std::uint32_t channel) noexcept {
101 const std::uint32_t hashed = mix(static_cast<std::uint32_t>(ix), static_cast<std::uint32_t>(iz), channel);
102 return static_cast<float>(hashed >> 8) * (1.f / 16777216.f);
103}
104
117[[nodiscard]] float cellNoise(std::int32_t x, std::int32_t z, std::uint32_t channel) noexcept {
118 constexpr float kScale = 0.1f;
119 const float fx = static_cast<float>(x) * kScale;
120 const float fz = static_cast<float>(z) * kScale;
121 const float bx = std::floor(fx);
122 const float bz = std::floor(fz);
123 const auto ix = static_cast<std::int32_t>(bx);
124 const auto iz = static_cast<std::int32_t>(bz);
125 const float tx = fx - bx;
126 const float tz = fz - bz;
127 const float sx = tx * tx * (3.f - 2.f * tx);
128 const float sz = tz * tz * (3.f - 2.f * tz);
129
130 const float c00 = lattice(ix, iz, channel);
131 const float c10 = lattice(ix + 1, iz, channel);
132 const float c01 = lattice(ix, iz + 1, channel);
133 const float c11 = lattice(ix + 1, iz + 1, channel);
134 const float top = c00 + (c10 - c00) * sx;
135 const float bottom = c01 + (c11 - c01) * sx;
136 return top + (bottom - top) * sz;
137}
138
140[[nodiscard]]
142[[nodiscard]] std::int32_t roundToInt(float value) noexcept {
143 return static_cast<std::int32_t>(std::floor(value + 0.5f));
144}
145
147[[nodiscard]] std::int32_t clampInt(std::int32_t value, std::int32_t low, std::int32_t high) noexcept {
148 return value < low ? low : (value > high ? high : value);
149}
150
158[[nodiscard]] std::int32_t clampSetting(std::int32_t value, std::int32_t low, std::int32_t high,
159 std::int32_t fallback) noexcept {
160 if (value < low || value > high) return fallback;
161 return value;
162}
163
164// ---------------------------------------------------------------------------
165// Settings normalisation
166// ---------------------------------------------------------------------------
167
169struct NormalizedSettings {
170 float highRiseProbability = 0.25f;
171 float sinkProbability = 0.2f;
172 float jitterProbability = 0.25f;
173 std::int32_t chunkSizeMin = 30;
174 std::int32_t chunkSizeMax = 100;
175 std::int32_t landPercentage = 50;
176 std::int32_t waterLevel = 3;
177 std::int32_t elevationMinimum = -2;
178 std::int32_t elevationMaximum = 8;
179 std::int32_t mapBorderX = 5;
180 std::int32_t mapBorderZ = 5;
181 std::int32_t regionBorder = 5;
182 std::int32_t regionCount = 1;
183 std::int32_t erosionPercentage = 50;
184 float startingMoisture = 0.1f;
185 float evaporationFactor = 0.5f;
186 float precipitationFactor = 0.25f;
187 float runoffFactor = 0.25f;
188 float seepageFactor = 0.25f;
190 float windStrength = 4.f;
191 std::int32_t riverPercentage = 10;
192 float extraLakeProbability = 0.25f;
193 float lowTemperature = 0.f;
194 float highTemperature = 1.f;
195 float temperatureJitter = 0.1f;
196};
197
211[[nodiscard]] NormalizedSettings normalize(const HexMapGeneratorSettings& settings) noexcept {
212 NormalizedSettings out;
213 out.highRiseProbability = std::clamp(settings.highRiseProbability, 0.f, 1.f);
214 out.sinkProbability = std::clamp(settings.sinkProbability, 0.f, 1.f);
215 out.jitterProbability = std::clamp(settings.jitterProbability, 0.f, 1.f);
216 out.chunkSizeMin = clampSetting(settings.chunkSizeMin, 1, 100000, out.chunkSizeMin);
217 out.chunkSizeMax = clampSetting(settings.chunkSizeMax, 2, 100000, out.chunkSizeMax);
218 out.landPercentage = clampSetting(settings.landPercentage, 0, 100, out.landPercentage);
219 out.waterLevel = clampInt(settings.waterLevel, 0, HexMetrics::kMaxElevation);
220 out.elevationMinimum = clampInt(settings.elevationMinimum, HexMetrics::kMinElevation, 0);
221 out.elevationMaximum = clampInt(settings.elevationMaximum, 1, HexMetrics::kMaxElevation);
222 out.mapBorderX = clampSetting(settings.mapBorderX, 0, 1000, out.mapBorderX);
223 out.mapBorderZ = clampSetting(settings.mapBorderZ, 0, 1000, out.mapBorderZ);
224 out.regionBorder = clampSetting(settings.regionBorder, 0, 1000, out.regionBorder);
225 out.regionCount = clampSetting(settings.regionCount, 1, 4, out.regionCount);
226 out.erosionPercentage = clampSetting(settings.erosionPercentage, 0, 100, out.erosionPercentage);
227 out.startingMoisture = std::clamp(settings.startingMoisture, 0.f, 1.f);
228 out.evaporationFactor = std::clamp(settings.evaporationFactor, 0.f, 1.f);
229 out.precipitationFactor = std::clamp(settings.precipitationFactor, 0.f, 1.f);
230 out.runoffFactor = std::clamp(settings.runoffFactor, 0.f, 1.f);
231 out.seepageFactor = std::clamp(settings.seepageFactor, 0.f, 1.f);
232 out.windDirection = settings.windDirection;
233 out.windStrength = std::clamp(settings.windStrength, 0.f, 1000.f);
234 out.riverPercentage = clampSetting(settings.riverPercentage, 0, 100, out.riverPercentage);
235 out.extraLakeProbability = std::clamp(settings.extraLakeProbability, 0.f, 1.f);
236 out.lowTemperature = std::clamp(settings.lowTemperature, -10.f, 10.f);
237 out.highTemperature = std::clamp(settings.highTemperature, -10.f, 10.f);
238 out.temperatureJitter = std::clamp(settings.temperatureJitter, 0.f, 10.f);
239 return out;
240}
241
243struct MapRegion {
244 std::int32_t xMin = 0;
245 std::int32_t xMax = 0;
246 std::int32_t zMin = 0;
247 std::int32_t zMax = 0;
248
250 [[nodiscard]] bool valid() const noexcept { return xMax > xMin && zMax > zMin; }
251};
252
264void createRegions(std::int32_t count, std::int32_t cellCountX, std::int32_t cellCountZ,
265 const NormalizedSettings& settings, std::vector<MapRegion>& out) {
266 out.clear();
267 const std::int32_t borderX = settings.mapBorderX;
268 const std::int32_t borderZ = settings.mapBorderZ;
269 const std::int32_t midX = cellCountX / 2;
270 const std::int32_t midZ = cellCountZ / 2;
271 const std::int32_t thirdX = cellCountX / 3;
272 const std::int32_t border = settings.regionBorder;
273
274 switch (count) {
275 case 2: {
276 MapRegion first;
277 MapRegion second;
278 first.zMin = borderZ;
279 first.zMax = cellCountZ - borderZ;
280 second.zMin = borderZ;
281 second.zMax = cellCountZ - borderZ;
282 first.xMin = borderX;
283 first.xMax = midX - border;
284 second.xMin = midX + border;
285 second.xMax = cellCountX - borderX;
286 out.push_back(first);
287 out.push_back(second);
288 break;
289 }
290 case 3: {
291 MapRegion first;
292 MapRegion second;
293 MapRegion third;
294 first.zMin = borderZ;
295 first.zMax = cellCountZ - borderZ;
296 second.zMin = borderZ;
297 second.zMax = cellCountZ - borderZ;
298 third.zMin = borderZ;
299 third.zMax = cellCountZ - borderZ;
300 first.xMin = borderX;
301 first.xMax = thirdX - border;
302 second.xMin = thirdX + border;
303 second.xMax = cellCountX * 2 / 3 - border;
304 third.xMin = cellCountX * 2 / 3 + border;
305 third.xMax = cellCountX - borderX;
306 out.push_back(first);
307 out.push_back(second);
308 out.push_back(third);
309 break;
310 }
311 case 4: {
312 MapRegion first;
313 MapRegion second;
314 MapRegion third;
315 MapRegion fourth;
316 first.xMin = borderX;
317 first.zMin = borderZ;
318 first.xMax = midX - border;
319 first.zMax = midZ - border;
320 second.xMin = midX + border;
321 second.zMin = borderZ;
322 second.xMax = cellCountX - borderX;
323 second.zMax = midZ - border;
324 third.xMin = midX + border;
325 third.zMin = midZ + border;
326 third.xMax = cellCountX - borderX;
327 third.zMax = cellCountZ - borderZ;
328 fourth.xMin = borderX;
329 fourth.zMin = midZ + border;
330 fourth.xMax = midX - border;
331 fourth.zMax = cellCountZ - borderZ;
332 out.push_back(first);
333 out.push_back(second);
334 out.push_back(third);
335 out.push_back(fourth);
336 break;
337 }
338 default: {
339 MapRegion only;
340 only.xMin = borderX;
341 only.xMax = cellCountX - borderX;
342 only.zMin = borderZ;
343 only.zMax = cellCountZ - borderZ;
344 out.push_back(only);
345 break;
346 }
347 }
348}
349
356[[nodiscard]] bool regionsUsable(const std::vector<MapRegion>& regions, std::int32_t regionCount) {
357 if (static_cast<std::int32_t>(regions.size()) != regionCount) return false;
358 for (const MapRegion& region : regions) {
359 if (!region.valid()) return false;
360 }
361 return true;
362}
363
365struct ClimateData {
366 float clouds = 0.f;
367 float moisture = 0.f;
368};
369
371struct Biome {
372 std::int32_t terrain = 0;
373 std::int32_t plant = 0;
374};
375
377constexpr float kTemperatureBands[3] = {0.1f, 0.3f, 0.6f};
379constexpr float kMoistureBands[3] = {0.12f, 0.28f, 0.85f};
381constexpr Biome kBiomes[16] = {{0, 0}, {4, 0}, {4, 0}, {4, 0}, {0, 0}, {2, 0}, {2, 1}, {2, 2},
382 {0, 0}, {1, 0}, {1, 1}, {1, 2}, {0, 0}, {1, 1}, {1, 2}, {1, 3}};
383
385struct GeneratorScratch {
387 HexSearchContext search;
389 std::vector<std::int32_t> erodible;
391 std::vector<std::uint8_t> erodibleFlag;
393 std::vector<HexDirection> flowDirections;
395 std::vector<std::int32_t> riverOrigins;
397 std::vector<ClimateData> climate;
399 std::vector<ClimateData> nextClimate;
401 std::int32_t landCells = 0;
402};
403
404// ---------------------------------------------------------------------------
405// Elevation helpers
406// ---------------------------------------------------------------------------
407
418void writeElevation(HexMap& map, HexCoordinates coordinates, std::int32_t elevation) {
419 map.setElevation(coordinates, elevation).ignore("generator writes are clamped per cell");
420}
421
428[[nodiscard]] std::int32_t waterLevelOf(const HexMap& map, HexCoordinates coordinates) noexcept {
429 return map.waterLevel(coordinates);
430}
431
433void writeWaterLevel(HexMap& map, HexCoordinates coordinates, std::int32_t waterLevel) {
434 map.setWaterLevel(coordinates, waterLevel).ignore("generator writes are clamped per cell");
435}
436
437// ---------------------------------------------------------------------------
438// Land growth
439// ---------------------------------------------------------------------------
440
449[[nodiscard]] std::int32_t randomCellIndex(const HexMap& map, const MapRegion& region, HexRandom& random) {
450 const std::int32_t offsetX = random.range(region.xMin, region.xMax);
451 const std::int32_t offsetZ = random.range(region.zMin, region.zMax);
452 const HexCoordinates cell = HexCoordinates::fromOffset(offsetX, offsetZ);
453 const std::int32_t index = map.indexOf(cell);
454 return index < 0 ? 0 : index;
455}
456
474std::int32_t raiseTerrain(HexMap& map, GeneratorScratch& scratch, HexRandom& random, const NormalizedSettings& settings,
475 const MapRegion& region, std::int32_t chunkSize, std::int32_t budget) {
476 const std::int32_t phase = scratch.search.beginPhase();
477 const std::int32_t first = randomCellIndex(map, region, random);
478 scratch.search.data(first).searchPhase = phase;
479 scratch.search.enqueue(first);
480 const HexCoordinates center = map.coordinatesAt(first);
481
482 const std::int32_t rise = random.chance(settings.highRiseProbability) ? 2 : 1;
483 std::int32_t size = 0;
484 while (size < chunkSize) {
485 std::int32_t index = -1;
486 if (scratch.search.dequeue(index) != HexSearchPop::Cell) break;
487
488 const std::int32_t originalElevation = map.elevation(map.coordinatesAt(index));
489 const std::int32_t newElevation = originalElevation + rise;
490 if (newElevation > settings.elevationMaximum) continue;
491
492 const HexCoordinates coordinates = map.coordinatesAt(index);
493 writeElevation(map, coordinates, newElevation);
494 if (originalElevation < settings.waterLevel && newElevation >= settings.waterLevel) {
495 --budget;
496 if (budget == 0) break;
497 }
498 ++size;
499
500 for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
501 HexCoordinates neighbour{};
502 if (!map.getNeighbor(coordinates, static_cast<HexDirection>(i), neighbour)) continue;
503 const std::int32_t neighbourIndex = map.indexOf(neighbour);
504 if (neighbourIndex < 0) continue;
505 if (scratch.search.data(neighbourIndex).searchPhase >= phase) continue;
506 HexSearchData& record = scratch.search.data(neighbourIndex);
507 record.searchPhase = phase;
508 record.distance = neighbour.distanceTo(center);
509 record.heuristic = random.chance(settings.jitterProbability) ? 1 : 0;
510 scratch.search.enqueue(neighbourIndex);
511 }
512 }
513 return budget;
514}
515
532std::int32_t sinkTerrain(HexMap& map, GeneratorScratch& scratch, HexRandom& random, const NormalizedSettings& settings,
533 const MapRegion& region, std::int32_t chunkSize, std::int32_t budget) {
534 const std::int32_t phase = scratch.search.beginPhase();
535 const std::int32_t first = randomCellIndex(map, region, random);
536 scratch.search.data(first).searchPhase = phase;
537 scratch.search.enqueue(first);
538 const HexCoordinates center = map.coordinatesAt(first);
539
540 const std::int32_t sink = random.chance(settings.highRiseProbability) ? 2 : 1;
541 std::int32_t size = 0;
542 while (size < chunkSize) {
543 std::int32_t index = -1;
544 if (scratch.search.dequeue(index) != HexSearchPop::Cell) break;
545
546 const HexCoordinates coordinates = map.coordinatesAt(index);
547 const std::int32_t originalElevation = map.elevation(coordinates);
548 const std::int32_t newElevation = originalElevation - sink;
549 if (newElevation < settings.elevationMinimum) continue;
550
551 writeElevation(map, coordinates, newElevation);
552 if (originalElevation >= settings.waterLevel && newElevation < settings.waterLevel) ++budget;
553 ++size;
554
555 for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
556 HexCoordinates neighbour{};
557 if (!map.getNeighbor(coordinates, static_cast<HexDirection>(i), neighbour)) continue;
558 const std::int32_t neighbourIndex = map.indexOf(neighbour);
559 if (neighbourIndex < 0) continue;
560 if (scratch.search.data(neighbourIndex).searchPhase >= phase) continue;
561 HexSearchData& record = scratch.search.data(neighbourIndex);
562 record.searchPhase = phase;
563 record.distance = neighbour.distanceTo(center);
564 record.heuristic = random.chance(settings.jitterProbability) ? 1 : 0;
565 scratch.search.enqueue(neighbourIndex);
566 }
567 }
568 return budget;
569}
570
585void createLand(HexMap& map, GeneratorScratch& scratch, HexRandom& random, const NormalizedSettings& settings,
586 const std::vector<MapRegion>& regions) {
587 const std::int32_t cellCount = map.cellCount();
588 const float cellShare = static_cast<float>(cellCount) * 0.01f;
589 std::int32_t landBudget = roundToInt(cellShare * static_cast<float>(settings.landPercentage));
590 scratch.landCells = landBudget;
591
592 for (std::int32_t guard = 0; guard < 10000; ++guard) {
593 const bool sink = random.chance(settings.sinkProbability);
594 for (const MapRegion& region : regions) {
595 const std::int32_t chunkSize = random.range(settings.chunkSizeMin, settings.chunkSizeMax - 1);
596 if (sink) {
597 landBudget = sinkTerrain(map, scratch, random, settings, region, chunkSize, landBudget);
598 } else {
599 landBudget = raiseTerrain(map, scratch, random, settings, region, chunkSize, landBudget);
600 if (landBudget == 0) return;
601 }
602 }
603 }
604 if (landBudget > 0) {
605 // The reference logs a warning here; the port records the shortfall in the
606 // land count instead, which is what the river budget is derived from.
607 scratch.landCells -= landBudget;
608 }
609}
610
611// ---------------------------------------------------------------------------
612// Erosion
613// ---------------------------------------------------------------------------
614
621[[nodiscard]] bool isErodible(const HexMap& map, HexCoordinates coordinates) {
622 const std::int32_t threshold = map.elevation(coordinates) - 2;
623 for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
624 HexCoordinates neighbour{};
625 if (!map.getNeighbor(coordinates, static_cast<HexDirection>(i), neighbour)) continue;
626 if (map.elevation(neighbour) <= threshold) return true;
627 }
628 return false;
629}
630
642[[nodiscard]] std::int32_t erosionTarget(const HexMap& map, HexCoordinates coordinates) {
643 const std::int32_t threshold = map.elevation(coordinates) - 2;
644 for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
645 HexCoordinates neighbour{};
646 if (!map.getNeighbor(coordinates, static_cast<HexDirection>(i), neighbour)) continue;
647 if (map.elevation(neighbour) <= threshold) return map.indexOf(neighbour);
648 }
649 return -1;
650}
651
664void addErodible(GeneratorScratch& scratch, std::int32_t index, std::int32_t elevation) {
665 if (index < 0 || static_cast<std::size_t>(index) >= scratch.erodibleFlag.size()) return;
666 if (scratch.erodibleFlag[static_cast<std::size_t>(index)] != 0u) return;
667 scratch.erodibleFlag[static_cast<std::size_t>(index)] = 1u;
668 scratch.erodible.push_back(index);
669 HexSearchData& record = scratch.search.data(index);
670 record.distance = elevation + 15;
671 record.heuristic = 0;
672 scratch.search.enqueue(index);
673}
674
680void removeErodible(GeneratorScratch& scratch, std::int32_t index) {
681 if (index < 0 || static_cast<std::size_t>(index) >= scratch.erodibleFlag.size()) return;
682 if (scratch.erodibleFlag[static_cast<std::size_t>(index)] == 0u) return;
683 scratch.erodibleFlag[static_cast<std::size_t>(index)] = 0u;
684 if (!scratch.erodible.empty() && scratch.erodible.back() == index) {
685 scratch.erodible.pop_back();
686 return;
687 }
688 const auto position = std::find(scratch.erodible.begin(), scratch.erodible.end(), index);
689 if (position == scratch.erodible.end()) return;
690 *position = scratch.erodible.back();
691 scratch.erodible.pop_back();
692}
693
704void requeueErodible(GeneratorScratch& scratch, std::int32_t index, std::int32_t previousElevation) {
705 if (index < 0 || static_cast<std::size_t>(index) >= scratch.erodibleFlag.size()) return;
706 if (scratch.erodibleFlag[static_cast<std::size_t>(index)] == 0u) return;
707 scratch.search.change(index, previousElevation + 15);
708}
709
722void erodeOnce(HexMap& map, GeneratorScratch& scratch, std::int32_t cellIndex) {
723 if (cellIndex < 0 || cellIndex >= map.cellCount()) return;
724 const HexCoordinates cellCoordinates = map.coordinatesAt(cellIndex);
725 const std::int32_t cellElevation = map.elevation(cellCoordinates);
726 const std::int32_t targetIndex = erosionTarget(map, cellCoordinates);
727 if (targetIndex < 0) {
728 removeErodible(scratch, cellIndex);
729 return;
730 }
731 const HexCoordinates targetCoordinates = map.coordinatesAt(targetIndex);
732 const std::int32_t targetElevation = map.elevation(targetCoordinates);
733
734 // Erosion moves land around rather than removing it, so the cell itself stays
735 // land; the running count only has to follow the erosion it performs.
736 --scratch.landCells;
737 writeElevation(map, cellCoordinates, cellElevation - 1);
738 writeElevation(map, targetCoordinates, targetElevation + 1);
739 requeueErodible(scratch, cellIndex, cellElevation);
740 requeueErodible(scratch, targetIndex, targetElevation);
741
742 if (!isErodible(map, cellCoordinates)) removeErodible(scratch, cellIndex);
743
744 for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
745 HexCoordinates neighbour{};
746 if (!map.getNeighbor(cellCoordinates, static_cast<HexDirection>(i), neighbour)) continue;
747 const std::int32_t neighbourIndex = map.indexOf(neighbour);
748 if (neighbourIndex < 0) continue;
749 const std::int32_t neighbourElevation = map.elevation(neighbour);
750 if (neighbourElevation != cellElevation + 2) continue;
751 addErodible(scratch, neighbourIndex, neighbourElevation);
752 }
753
754 if (isErodible(map, targetCoordinates)) addErodible(scratch, targetIndex, map.elevation(targetCoordinates));
755
756 for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
757 HexCoordinates neighbour{};
758 if (!map.getNeighbor(targetCoordinates, static_cast<HexDirection>(i), neighbour)) continue;
759 const std::int32_t neighbourIndex = map.indexOf(neighbour);
760 if (neighbourIndex < 0 || neighbourIndex == cellIndex) continue;
761 if (map.elevation(neighbour) != targetElevation + 1) continue;
762 if (!isErodible(map, neighbour)) removeErodible(scratch, neighbourIndex);
763 }
764}
765
778void erodeLand(HexMap& map, GeneratorScratch& scratch, const NormalizedSettings& settings) {
779 const std::int32_t cellCount = map.cellCount();
780 scratch.erodible.clear();
781 scratch.erodibleFlag.assign(static_cast<std::size_t>(cellCount), 0u);
782 // The erosion frontier is the only user of the context here, and it is keyed
783 // by elevation rather than by a visited phase, so the new phase is unused.
784 (void)scratch.search.beginPhase();
785
786 for (std::int32_t index = 0; index < cellCount; ++index) {
787 const HexCoordinates coordinates = map.coordinatesAt(index);
788 if (isErodible(map, coordinates)) addErodible(scratch, index, map.elevation(coordinates));
789 }
790
791 const auto target = static_cast<std::size_t>(static_cast<float>(scratch.erodible.size()) *
792 (100.f - static_cast<float>(settings.erosionPercentage)) * 0.01f);
793 while (scratch.erodible.size() > target) {
794 std::int32_t index = -1;
795 if (scratch.search.dequeue(index) != HexSearchPop::Cell) break;
796 if (scratch.erodibleFlag[static_cast<std::size_t>(index)] == 0u) continue;
797 erodeOnce(map, scratch, index);
798 }
799}
800
801// ---------------------------------------------------------------------------
802// Climate
803// ---------------------------------------------------------------------------
804
821[[nodiscard]] float determineTemperature(const HexMap& map, HexCoordinates coordinates,
822 const NormalizedSettings& settings, std::int32_t jitterChannel) noexcept {
823 const float latitude = static_cast<float>(coordinates.z) / static_cast<float>(map.cellCountZ());
824 float temperature = settings.lowTemperature + (settings.highTemperature - settings.lowTemperature) * latitude;
825
826 const std::int32_t viewElevation = map.values(coordinates).viewElevation();
827 const float span = static_cast<float>(settings.elevationMaximum - settings.waterLevel + 1);
828 const float elevationFactor =
829 1.f - static_cast<float>(viewElevation - settings.waterLevel) / (span == 0.f ? 1.f : span);
830 temperature *= elevationFactor;
831
832 const HexVec3 position = map.cellGroundPosition(coordinates);
833 const auto channel = static_cast<std::uint32_t>(clampInt(jitterChannel, 0, 3)) * 0x9E3779B9u;
834 const float jitter =
835 cellNoise(static_cast<std::int32_t>(position.x), static_cast<std::int32_t>(position.z), channel);
836 return temperature + (jitter * 2.f - 1.f) * settings.temperatureJitter;
837}
838
851void evolveClimate(const HexMap& map, GeneratorScratch& scratch, const NormalizedSettings& settings,
852 std::int32_t cellIndex) {
853 const HexCoordinates coordinates = map.coordinatesAt(cellIndex);
854 const HexValues values = map.values(coordinates);
855 ClimateData cellClimate = scratch.climate[static_cast<std::size_t>(cellIndex)];
856
857 if (values.isUnderwater()) {
858 cellClimate.moisture = 1.f;
859 cellClimate.clouds += settings.evaporationFactor;
860 } else {
861 const float evaporation = cellClimate.moisture * settings.evaporationFactor;
862 cellClimate.moisture -= evaporation;
863 cellClimate.clouds += evaporation;
864 }
865
866 const float precipitation = cellClimate.clouds * settings.precipitationFactor;
867 cellClimate.clouds -= precipitation;
868 cellClimate.moisture += precipitation;
869
870 const float maximumElevation = static_cast<float>(settings.elevationMaximum);
871 const float cloudMaximum = 1.f - static_cast<float>(values.viewElevation()) / (maximumElevation + 1.f);
872 if (cellClimate.clouds > cloudMaximum) {
873 cellClimate.moisture += cellClimate.clouds - cloudMaximum;
874 cellClimate.clouds = cloudMaximum;
875 }
876
877 const HexDirection mainDispersalDirection = opposite(settings.windDirection);
878 const float cloudDispersal = cellClimate.clouds * (1.f / (5.f + settings.windStrength));
879 const float runoff = cellClimate.moisture * settings.runoffFactor * (1.f / 6.f);
880 const float seepage = cellClimate.moisture * settings.seepageFactor * (1.f / 6.f);
881
882 for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
883 const auto direction = static_cast<HexDirection>(i);
884 HexCoordinates neighbour{};
885 if (!map.getNeighbor(coordinates, direction, neighbour)) continue;
886 const std::int32_t neighbourIndex = map.indexOf(neighbour);
887 if (neighbourIndex < 0) continue;
888
889 ClimateData neighbourClimate = scratch.nextClimate[static_cast<std::size_t>(neighbourIndex)];
890 if (direction == mainDispersalDirection) {
891 neighbourClimate.clouds += cloudDispersal * settings.windStrength;
892 } else {
893 neighbourClimate.clouds += cloudDispersal;
894 }
895
896 const std::int32_t elevationDelta = map.values(neighbour).viewElevation() - values.viewElevation();
897 if (elevationDelta < 0) {
898 cellClimate.moisture -= runoff;
899 neighbourClimate.moisture += runoff;
900 } else if (elevationDelta == 0) {
901 cellClimate.moisture -= seepage;
902 neighbourClimate.moisture += seepage;
903 }
904 scratch.nextClimate[static_cast<std::size_t>(neighbourIndex)] = neighbourClimate;
905 }
906
907 ClimateData nextCell = scratch.nextClimate[static_cast<std::size_t>(cellIndex)];
908 nextCell.moisture += cellClimate.moisture;
909 if (nextCell.moisture > 1.f) nextCell.moisture = 1.f;
910 scratch.nextClimate[static_cast<std::size_t>(cellIndex)] = nextCell;
911 scratch.climate[static_cast<std::size_t>(cellIndex)] = ClimateData{};
912}
913
924void createClimate(const HexMap& map, GeneratorScratch& scratch, const NormalizedSettings& settings) {
925 const auto cellCount = static_cast<std::size_t>(map.cellCount());
926 ClimateData initial;
927 initial.moisture = settings.startingMoisture;
928 scratch.climate.assign(cellCount, initial);
929 scratch.nextClimate.assign(cellCount, ClimateData{});
930
931 for (std::int32_t cycle = 0; cycle < 40; ++cycle) {
932 for (std::int32_t index = 0; index < map.cellCount(); ++index) evolveClimate(map, scratch, settings, index);
933 std::swap(scratch.climate, scratch.nextClimate);
934 }
935}
936
937// ---------------------------------------------------------------------------
938// Rivers
939// ---------------------------------------------------------------------------
940
952void collectRiverOrigins(const HexMap& map, GeneratorScratch& scratch, const NormalizedSettings& settings) {
953 scratch.riverOrigins.clear();
954 const float span = static_cast<float>(settings.elevationMaximum - settings.waterLevel);
955 for (std::int32_t index = 0; index < map.cellCount(); ++index) {
956 const HexCoordinates coordinates = map.coordinatesAt(index);
957 const HexValues values = map.values(coordinates);
958 if (values.isUnderwater()) continue;
959 const float weight = scratch.climate[static_cast<std::size_t>(index)].moisture *
960 static_cast<float>(values.elevation() - settings.waterLevel) / (span == 0.f ? 1.f : span);
961 if (weight > 0.75f) {
962 scratch.riverOrigins.push_back(index);
963 scratch.riverOrigins.push_back(index);
964 }
965 if (weight > 0.5f) scratch.riverOrigins.push_back(index);
966 if (weight > 0.25f) scratch.riverOrigins.push_back(index);
967 }
968}
969
986std::int32_t createRiver(HexMap& map, GeneratorScratch& scratch, HexRandom& random, const NormalizedSettings& settings,
987 std::int32_t originIndex) {
988 std::int32_t length = 1;
989 HexCoordinates cellCoordinates = map.coordinatesAt(originIndex);
990 HexValues cellValues = map.values(cellCoordinates);
991 bool cellUnderwater = cellValues.isUnderwater();
992 // The direction taken on the previous step; it is only read once the river is
993 // longer than one cell, which is exactly when the reference starts using it.
995
996 while (!cellUnderwater) {
997 std::int32_t minNeighborElevation = std::numeric_limits<std::int32_t>::max();
998 scratch.flowDirections.clear();
999 for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
1000 const auto candidate = static_cast<HexDirection>(i);
1001 HexCoordinates neighbour{};
1002 if (!map.getNeighbor(cellCoordinates, candidate, neighbour)) continue;
1003 const std::int32_t neighbourIndex = map.indexOf(neighbour);
1004 if (neighbourIndex < 0) continue;
1005 const HexCellData* neighbourCell = map.cellAt(neighbourIndex);
1006 if (neighbourCell == nullptr) continue;
1007
1008 const std::int32_t neighbourElevation = neighbourCell->values.elevation();
1009 if (neighbourElevation < minNeighborElevation) minNeighborElevation = neighbourElevation;
1010
1011 if (neighbourIndex == originIndex || neighbourCell->flags.hasAnyRiverIn()) continue;
1012
1013 const std::int32_t delta = neighbourElevation - cellValues.elevation();
1014 if (delta > 0) continue;
1015
1016 if (neighbourCell->flags.hasAnyRiverOut()) {
1017 map.setOutgoingRiver(cellCoordinates, candidate).ignore("river join is validated by the map");
1018 return length;
1019 }
1020
1021 if (delta < 0) {
1022 scratch.flowDirections.push_back(candidate);
1023 scratch.flowDirections.push_back(candidate);
1024 scratch.flowDirections.push_back(candidate);
1025 }
1026 // A reversal is discouraged once the river has a direction; on the first
1027 // step there is none, so every candidate keeps its weight.
1028 if (length == 1 || (candidate != next2(direction) && candidate != previous2(direction))) {
1029 scratch.flowDirections.push_back(candidate);
1030 }
1031 scratch.flowDirections.push_back(candidate);
1032 }
1033
1034 if (scratch.flowDirections.empty()) {
1035 if (length == 1) return 0;
1036 if (minNeighborElevation >= cellValues.elevation()) {
1037 writeWaterLevel(map, cellCoordinates, minNeighborElevation);
1038 if (minNeighborElevation == cellValues.elevation()) {
1039 writeElevation(map, cellCoordinates, minNeighborElevation - 1);
1040 }
1041 }
1042 break;
1043 }
1044
1045 const auto chosen = scratch.flowDirections[static_cast<std::size_t>(
1046 random.index(static_cast<std::int32_t>(scratch.flowDirections.size())))];
1047 const std::int32_t outgoingIndex = map.indexOf(cellCoordinates.step(chosen));
1048 if (outgoingIndex < 0) break;
1049
1050 map.setOutgoingRiver(cellCoordinates, chosen).ignore("flow direction was selected downhill");
1051 direction = chosen;
1052 ++length;
1053
1054 if (minNeighborElevation >= cellValues.elevation() && random.chance(settings.extraLakeProbability)) {
1055 writeWaterLevel(map, cellCoordinates, cellValues.elevation());
1056 writeElevation(map, cellCoordinates, cellValues.elevation() - 1);
1057 }
1058
1059 cellCoordinates = map.coordinatesAt(outgoingIndex);
1060 cellValues = map.values(cellCoordinates);
1061 cellUnderwater = cellValues.isUnderwater();
1062 }
1063 return length;
1064}
1065
1078void createRivers(HexMap& map, GeneratorScratch& scratch, HexRandom& random, const NormalizedSettings& settings) {
1079 collectRiverOrigins(map, scratch, settings);
1080 std::int32_t riverBudget =
1081 roundToInt(static_cast<float>(scratch.landCells) * static_cast<float>(settings.riverPercentage) * 0.01f);
1082 while (riverBudget > 0 && !scratch.riverOrigins.empty()) {
1083 const auto position =
1084 static_cast<std::size_t>(random.index(static_cast<std::int32_t>(scratch.riverOrigins.size())));
1085 const std::int32_t originIndex = scratch.riverOrigins[position];
1086 scratch.riverOrigins[position] = scratch.riverOrigins.back();
1087 scratch.riverOrigins.pop_back();
1088
1089 const HexCellData* origin = map.cellAt(originIndex);
1090 if (origin == nullptr || origin->flags.hasRiver()) continue;
1091 bool validOrigin = true;
1092 for (std::int32_t i = 0; i < kHexDirectionCount && validOrigin; ++i) {
1093 HexCoordinates neighbour{};
1094 if (!map.getNeighbor(map.coordinatesAt(originIndex), static_cast<HexDirection>(i), neighbour)) continue;
1095 const HexCellData* neighbourCell = map.cell(neighbour);
1096 if (neighbourCell == nullptr) continue;
1097 if (neighbourCell->flags.hasRiver() || neighbourCell->values.isUnderwater()) validOrigin = false;
1098 }
1099 if (!validOrigin) continue;
1100 riverBudget -= createRiver(map, scratch, random, settings, originIndex);
1101 }
1102}
1103
1104// ---------------------------------------------------------------------------
1105// Terrain types
1106// ---------------------------------------------------------------------------
1107
1120[[nodiscard]] std::int32_t underwaterTerrain(const HexMap& map, HexCoordinates coordinates,
1121 const NormalizedSettings& settings) {
1122 const std::int32_t elevation = map.elevation(coordinates);
1123 if (elevation == settings.waterLevel - 1) {
1124 const std::int32_t water = waterLevelOf(map, coordinates);
1125 std::int32_t cliffs = 0;
1126 std::int32_t slopes = 0;
1127 for (std::int32_t i = 0; i < kHexDirectionCount; ++i) {
1128 HexCoordinates neighbour{};
1129 if (!map.getNeighbor(coordinates, static_cast<HexDirection>(i), neighbour)) continue;
1130 const std::int32_t delta = map.elevation(neighbour) - water;
1131 if (delta == 0) {
1132 ++slopes;
1133 } else if (delta > 0) {
1134 ++cliffs;
1135 }
1136 }
1137 if (cliffs + slopes > 3) return 1;
1138 if (cliffs > 0) return 3;
1139 if (slopes > 0) return 0;
1140 return 1;
1141 }
1142 if (elevation >= settings.waterLevel) return 1;
1143 if (elevation < 0) return 3;
1144 return 2;
1145}
1146
1160void setTerrainType(HexMap& map, GeneratorScratch& scratch, HexRandom& random, const NormalizedSettings& settings) {
1161 const std::int32_t jitterChannel = random.index(4);
1162 const std::int32_t rockDesertElevation =
1163 settings.elevationMaximum - (settings.elevationMaximum - settings.waterLevel) / 2;
1164
1165 for (std::int32_t index = 0; index < map.cellCount(); ++index) {
1166 const HexCoordinates coordinates = map.coordinatesAt(index);
1167 HexValues values = map.values(coordinates);
1168 const float temperature = determineTemperature(map, coordinates, settings, jitterChannel);
1169 const float moisture = scratch.climate[static_cast<std::size_t>(index)].moisture;
1170
1171 if (!values.isUnderwater()) {
1172 std::int32_t temperatureBand = 0;
1173 for (; temperatureBand < 3; ++temperatureBand) {
1174 if (temperature < kTemperatureBands[temperatureBand]) break;
1175 }
1176 std::int32_t moistureBand = 0;
1177 for (; moistureBand < 3; ++moistureBand) {
1178 if (moisture < kMoistureBands[moistureBand]) break;
1179 }
1180 Biome biome = kBiomes[static_cast<std::size_t>(temperatureBand * 4 + moistureBand)];
1181
1182 if (biome.terrain == 0) {
1183 if (values.elevation() >= rockDesertElevation) biome.terrain = 3;
1184 } else if (values.elevation() == settings.elevationMaximum) {
1185 biome.terrain = 4;
1186 }
1187
1188 if (biome.terrain == 4) {
1189 biome.plant = 0;
1190 } else if (biome.plant < 3 && map.hasRiver(coordinates)) {
1191 ++biome.plant;
1192 }
1193 values = values.withTerrainType(biome.terrain).withPlantLevel(biome.plant);
1194 } else {
1195 std::int32_t terrain = underwaterTerrain(map, coordinates, settings);
1196 if (terrain == 1 && temperature < kTemperatureBands[0]) terrain = 2;
1197 values = values.withTerrainType(terrain);
1198 }
1199 map.setCellState(coordinates, values, map.flags(coordinates))
1200 .ignore("terrain classification writes the values it just read back");
1201 }
1202}
1203
1204// ---------------------------------------------------------------------------
1205// Pipeline
1206// ---------------------------------------------------------------------------
1207
1217void finalizeGrid(HexMap& map) { map.markAllChunksDirty(); }
1218
1219} // namespace
1220
1222 if (map.empty()) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "cannot generate a hex map into an empty grid", "hexmap"));
1223
1224 const NormalizedSettings normalized = normalize(settings);
1225
1226 std::vector<MapRegion> regions;
1227 createRegions(normalized.regionCount, map.cellCountX(), map.cellCountZ(), normalized, regions);
1228 if (!regionsUsable(regions, normalized.regionCount))
1229 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "generator regions do not fit the hex map; enlarge the map or shrink the borders", "hexmap"));
1230
1231 GeneratorScratch scratch;
1232 scratch.search.resize(map.cellCount());
1233 scratch.erodibleFlag.assign(static_cast<std::size_t>(map.cellCount()), 0u);
1234 scratch.climate.assign(static_cast<std::size_t>(map.cellCount()), ClimateData{});
1235 scratch.nextClimate.assign(static_cast<std::size_t>(map.cellCount()), ClimateData{});
1236 scratch.landCells = 0;
1237
1238 HexRandom random(settings.seed);
1239
1240 for (std::int32_t index = 0; index < map.cellCount(); ++index) {
1241 const HexCoordinates coordinates = map.coordinatesAt(index);
1242 writeElevation(map, coordinates, normalized.elevationMinimum);
1243 writeWaterLevel(map, coordinates, normalized.waterLevel);
1244 }
1245
1246 createLand(map, scratch, random, normalized, regions);
1247 erodeLand(map, scratch, normalized);
1248 createClimate(map, scratch, normalized);
1249 createRivers(map, scratch, random, normalized);
1250 setTerrainType(map, scratch, random, normalized);
1251 finalizeGrid(map);
1252
1253 return Result<void>::success();
1254}
1255
1256} // namespace eve::hexmap
LogicalId target
double value
SQInteger top
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
std::string terrain
float phase
Definition CaveMesh.cpp:58
int bz
Definition CaveMesh.cpp:114
int bx
Definition CaveMesh.cpp:114
float length
Definition CaveMesh.cpp:94
Stable, structured diagnostics shared by engine modules.
std::map< std::string, Var > values
float minimum[3]
float u
Definition Grass.cpp:233
bool border
std::int32_t riverPercentage
std::int32_t regionCount
float precipitationFactor
std::int32_t mapBorderX
std::int32_t regionBorder
float windStrength
std::int32_t landCells
Number of cells that count as land, driving the river budget.
std::vector< HexDirection > flowDirections
Candidate directions considered by one river step.
std::int32_t elevationMinimum
float seepageFactor
float startingMoisture
std::int32_t mapBorderZ
HexSearchContext search
Frontier reused by the land-growth and erosion searches.
std::int32_t plant
float sinkProbability
std::int32_t zMax
std::int32_t erosionPercentage
float evaporationFactor
std::int32_t xMin
float runoffFactor
float lowTemperature
float clouds
float moisture
std::vector< std::int32_t > riverOrigins
Weighted river-origin candidates.
std::vector< std::int32_t > erodible
Cells still eligible for erosion.
std::int32_t chunkSizeMax
float highTemperature
float highRiseProbability
float temperatureJitter
std::int32_t zMin
std::int32_t landPercentage
std::int32_t xMax
std::int32_t elevationMaximum
std::vector< ClimateData > climate
Current moisture and cloud state per cell.
HexDirection windDirection
std::int32_t chunkSizeMin
float extraLakeProbability
std::vector< std::uint8_t > erodibleFlag
Membership flags parallel to the erodible list.
std::int32_t waterLevel
float jitterProbability
std::vector< ClimateData > nextClimate
Per-cell accumulation target of the next climate cycle.
Deterministic procedural map generation for the hex grid.
Cell-graph search used by pathfinding and visibility.
std::int32_t second
std::int32_t first
float elevation
int biome
float temperature
int h
Range range
std::array< float, 3 > position
bool valid
std::uint32_t seed
Definition PointSet.cpp:807
V3 origin
Definition RoadBake.cpp:138
RoadLaneDirection direction
std::uint16_t third
std::uint32_t count
Cell cell
TacticalUnit * unit
Battle::Random random
TerrainWaterField water
TerrainThermalSettings settings
float offsetX
float size
Definition TreeMesh.cpp:156
uint32_t index
float bottom
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
An editable, chunked, pointy-top hex map.
Definition HexMap.h:72
bool empty() const noexcept
Whether the map holds any cell.
Definition HexMap.h:92
HexCoordinates coordinatesAt(std::int32_t index) const noexcept
Coordinates of a linear cell index; out-of-range indices return (0, 0).
Definition HexMap.cpp:69
std::int32_t cellCountX() const noexcept
Number of columns.
Definition HexMap.h:94
std::int32_t cellCount() const noexcept
Total number of cells.
Definition HexMap.h:104
std::int32_t cellCountZ() const noexcept
Number of rows.
Definition HexMap.h:96
static constexpr int kMinElevation
Minimum editable elevation.
Definition HexMetrics.h:160
static constexpr int kMaxElevation
Maximum editable elevation.
Definition HexMetrics.h:162
@ Cell
A cell was removed; the out-parameter holds it.
constexpr HexDirection opposite(HexDirection d) noexcept
The direction opposite to d.
Definition HexMetrics.h:65
constexpr HexDirection previous2(HexDirection d) noexcept
Two steps counter-clockwise.
Definition HexMetrics.h:81
constexpr std::int32_t kHexDirectionCount
Number of hex edges / facing directions.
Definition HexMetrics.h:62
constexpr HexDirection next2(HexDirection d) noexcept
Two steps clockwise.
Definition HexMetrics.h:87
HexDirection
Hex facing directions, counter-clockwise from north-east.
Definition HexMetrics.h:59
Result< void > generateHexMap(HexMap &map, const HexMapGeneratorSettings &settings)
Replaces the grid's contents with a procedurally generated map.
Vec2 normalize(const Vec2 &a)
Normalize.
Definition UrbanTypes.h:44
Axial coordinates of one hex cell.
static constexpr HexCoordinates fromOffset(std::int32_t offsetX, std::int32_t offsetZ) noexcept
Converts an odd-row offset pair into axial coordinates.
Tunables of the reference project's map generator.