载入中...
搜索中...
未找到
TerrainPipeline.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <array>
5#include <cmath>
6#include <limits>
7#include <numeric>
8#include <queue>
9#include <utility>
10
11namespace eve::procgen {
12namespace {
13constexpr std::array<int, 8> dx{-1, 0, 1, -1, 1, -1, 0, 1};
14constexpr std::array<int, 8> dy{-1, -1, -1, 0, 0, 1, 1, 1};
15constexpr std::array<float, 8> distance{1.41421356f, 1.f, 1.41421356f, 1.f, 1.f, 1.41421356f, 1.f, 1.41421356f};
16size_t at(int x, int y, int width) { return size_t(y) * size_t(width) + size_t(x); }
17float saturate(float value) { return std::clamp(value, 0.f, 1.f); }
18float routeHash(int x, int y) {
19 uint32_t n = uint32_t(x) * 1597334677u ^ uint32_t(y) * 3812015801u;
20 n = (n ^ (n >> 15u)) * 2246822519u;
21 return float(n & 0x00ffffffu) / float(0x00ffffffu);
22}
23float routeNoise(float x, float y) {
24 const int ix = int(std::floor(x)), iy = int(std::floor(y));
25 float fx = x - float(ix), fy = y - float(iy);
26 fx = fx * fx * (3.f - 2.f * fx);
27 fy = fy * fy * (3.f - 2.f * fy);
28 const float a = routeHash(ix, iy), b = routeHash(ix + 1, iy);
29 const float c = routeHash(ix, iy + 1), d = routeHash(ix + 1, iy + 1);
30 return std::lerp(std::lerp(a, b, fx), std::lerp(c, d, fx), fy);
31}
32
33std::vector<uint8_t> protectedLakeBasins(const HydrologyMap &hydro, int w, int h,
34 float maxBreachDepth) {
35 const size_t count = size_t(w) * size_t(h);
36 std::vector<uint8_t> protectedCells(count, 0), visited(count, 0);
37 std::vector<size_t> component;
38 std::queue<size_t> frontier;
39 for (size_t seed = 0; seed < count; ++seed) {
40 if (visited[seed] || hydro.lakeDepth[seed] <= 0.f) continue;
41 visited[seed] = 1;
42 frontier.push(seed);
43 component.clear();
44 float basinDepth = 0.f;
45 while (!frontier.empty()) {
46 const size_t i = frontier.front(); frontier.pop();
47 component.push_back(i);
48 basinDepth = std::max(basinDepth, hydro.lakeDepth[i]);
49 const int x = int(i % size_t(w)), y = int(i / size_t(w));
50 for (int d = 0; d < 8; ++d) {
51 const int nx = x + dx[d], ny = y + dy[d];
52 if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
53 const size_t n = at(nx, ny, w);
54 if (!visited[n] && hydro.lakeDepth[n] > 0.f) {
55 visited[n] = 1;
56 frontier.push(n);
57 }
58 }
59 }
60 if (basinDepth > std::max(0.f, maxBreachDepth))
61 for (size_t i : component) protectedCells[i] = 1;
62 }
63 return protectedCells;
64}
65} // namespace
66
68 : hydrology_(std::move(hydrology)), climate_(std::move(climate)) {}
69
70namespace {
71float erosionSample(const std::vector<float> &values, int width, int height, int x, int y) {
72 if (x < 0 || y < 0 || x >= width || y >= height) return 0.f;
73 const size_t i = size_t(y) * size_t(width) + size_t(x);
74 return i < values.size() ? values[i] : 0.f;
75}
76} // namespace
77
78float TerrainErosionMap::getWear(int x, int y) const {
79 return erosionSample(wear, width, height, x, y);
80}
81float TerrainErosionMap::getDeposition(int x, int y) const {
82 return erosionSample(deposition, width, height, x, y);
83}
84float TerrainErosionMap::getHeightDelta(int x, int y) const {
85 return erosionSample(heightDelta, width, height, x, y);
86}
87
88int TerrainLayers::getWidth() const { return hydrology_.width; }
89int TerrainLayers::getHeight() const { return hydrology_.height; }
90
91size_t TerrainLayers::index(int x, int y) const {
92 if (x < 0 || y < 0 || x >= getWidth() || y >= getHeight()) return size_t(-1);
93 return size_t(y) * size_t(getWidth()) + size_t(x);
94}
95
96float TerrainLayers::getFlowAccumulation(int x, int y) const {
97 const size_t i = index(x, y); return i < hydrology_.flowAccumulation.size() ? hydrology_.flowAccumulation[i] : 0.f;
98}
99int TerrainLayers::getFlowDirection(int x, int y) const {
100 const size_t i = index(x, y);
101 return i < hydrology_.flowDirection.size() ? int(hydrology_.flowDirection[i]) : -1;
102}
103float TerrainLayers::getFlowVectorX(int x, int y) const {
104 const size_t i = index(x, y);
105 if (i < hydrology_.flowVectorX.size()) return hydrology_.flowVectorX[i];
106 const int d = getFlowDirection(x, y);
107 return d >= 0 && d < 8 ? float(dx[d]) / distance[d] : 0.f;
108}
109float TerrainLayers::getFlowVectorY(int x, int y) const {
110 const size_t i = index(x, y);
111 if (i < hydrology_.flowVectorY.size()) return hydrology_.flowVectorY[i];
112 const int d = getFlowDirection(x, y);
113 return d >= 0 && d < 8 ? float(dy[d]) / distance[d] : 0.f;
114}
115bool TerrainLayers::isRiver(int x, int y) const {
116 const size_t i = index(x, y); return i < hydrology_.rivers.size() && hydrology_.rivers[i] != 0;
117}
118int TerrainLayers::getStreamOrder(int x, int y) const {
119 const size_t i = index(x, y);
120 return i < hydrology_.streamOrder.size() ? int(hydrology_.streamOrder[i]) : 0;
121}
122float TerrainLayers::getLakeDepth(int x, int y) const {
123 const size_t i = index(x, y);
124 return i < hydrology_.lakeDepth.size() ? hydrology_.lakeDepth[i] : 0.f;
125}
126bool TerrainLayers::isLake(int x, int y, float minimumDepth) const {
127 return getLakeDepth(x, y) >= std::max(0.f, minimumDepth);
128}
129float TerrainLayers::getTemperature(int x, int y) const {
130 const size_t i = index(x, y); return i < climate_.temperature.size() ? climate_.temperature[i] : 0.f;
131}
132float TerrainLayers::getMoisture(int x, int y) const {
133 const size_t i = index(x, y); return i < climate_.moisture.size() ? climate_.moisture[i] : 0.f;
134}
135int TerrainLayers::getBiome(int x, int y) const {
136 const size_t i = index(x, y); return i < climate_.biomes.size() ? int(climate_.biomes[i]) : -1;
137}
138std::string TerrainLayers::getBiomeName(int x, int y) const {
139 static constexpr const char *names[] = {"ocean", "beach", "desert", "grassland", "forest",
140 "rainforest", "tundra", "taiga", "alpine", "river",
141 "lake", "wetland"};
142 const int biome = getBiome(x, y);
143 return biome >= 0 && biome < int(sizeof(names) / sizeof(names[0])) ? names[biome] : std::string{};
144}
145
147 const int w = hm.getWidth(), h = hm.getHeight();
148 if (w < 2 || h < 2 || s.iterations <= 0 || s.strength <= 0.f) return;
149 auto &height = hm.data();
150 std::vector<float> delta(height.size());
151 for (int iteration = 0; iteration < s.iterations; ++iteration) {
152 std::fill(delta.begin(), delta.end(), 0.f);
153 for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) {
154 const size_t from = at(x, y, w);
155 float total = 0.f;
156 std::array<float, 8> excess{};
157 for (int d = 0; d < 8; ++d) {
158 const int nx = x + dx[d], ny = y + dy[d];
159 if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
160 excess[d] = std::max(0.f, height[from] - height[at(nx, ny, w)] - s.talus * distance[d]);
161 total += excess[d];
162 }
163 if (total <= 0.f) continue;
164 // Multiple downhill neighbours must not each spend the same height
165 // difference. Cap the aggregate transfer below half of the steepest
166 // excess so one Jacobi step cannot invert a slope and amplify it on
167 // the following iteration.
168 const float maxExcess = *std::max_element(excess.begin(), excess.end());
169 const float moved = std::min(total * std::clamp(s.strength, 0.f, 0.5f),
170 maxExcess * 0.5f);
171 delta[from] -= moved;
172 for (int d = 0; d < 8; ++d)
173 if (excess[d] > 0.f) delta[at(x + dx[d], y + dy[d], w)] += moved * excess[d] / total;
174 }
175 for (size_t i = 0; i < height.size(); ++i) height[i] += delta[i];
176 }
177}
178
180 const int w = hm.getWidth(), h = hm.getHeight();
181 if (w < 2 || h < 2 || s.iterations <= 0) return;
182 auto &terrain = hm.data();
183 std::vector<float> water(terrain.size()), sediment(terrain.size()), nextWater, nextSediment;
184 for (int iteration = 0; iteration < s.iterations; ++iteration) {
185 for (float &v : water) v += std::max(0.f, s.rainfall);
186 nextWater.assign(terrain.size(), 0.f); nextSediment.assign(terrain.size(), 0.f);
187 for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) {
188 const size_t i = at(x, y, w);
189 int best = -1; float bestDrop = 0.f;
190 for (int d = 0; d < 8; ++d) {
191 const int nx = x + dx[d], ny = y + dy[d];
192 if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
193 const size_t n = at(nx, ny, w);
194 const float drop = (terrain[i] + water[i] - terrain[n] - water[n]) / distance[d];
195 if (drop > bestDrop) { bestDrop = drop; best = int(n); }
196 }
197 const float capacity = bestDrop * water[i] * std::max(0.f, s.capacity);
198 if (sediment[i] < capacity) {
199 const float amount = std::min(std::max(0.f, terrain[i]), (capacity - sediment[i]) * std::max(0.f, s.erosion));
200 terrain[i] -= amount; sediment[i] += amount;
201 } else {
202 const float amount = (sediment[i] - capacity) * std::clamp(s.deposition, 0.f, 1.f);
203 terrain[i] += amount; sediment[i] -= amount;
204 }
205 const float outflow = best >= 0 ? water[i] * 0.5f : 0.f;
206 const float ratio = water[i] > 0.f ? outflow / water[i] : 0.f;
207 nextWater[i] += water[i] - outflow; nextSediment[i] += sediment[i] * (1.f - ratio);
208 if (best >= 0) { nextWater[size_t(best)] += outflow; nextSediment[size_t(best)] += sediment[i] * ratio; }
209 }
210 const float keep = 1.f - std::clamp(s.evaporation, 0.f, 1.f);
211 for (float &v : nextWater) v *= keep;
212 water.swap(nextWater); sediment.swap(nextSediment);
213 }
214 // Sediment still suspended when the simulation ends represents material
215 // exported through the open drainage boundary. Dumping it into its final
216 // cells creates needle-like sink deposits and is numerically unstable.
217}
218
222
224 Heightmap &hm, const FluvialErosionSettings &s) {
225 TerrainErosionMap diagnostics;
226 const int w = hm.getWidth(), h = hm.getHeight();
227 if (w < 3 || h < 3 || s.iterations <= 0 || s.incision <= 0.f ||
228 s.maxDepth <= 0.f || s.bankWidth < 0.f || !std::isfinite(s.coordinateScale) ||
229 s.coordinateScale <= 0.f) return diagnostics;
230 auto &terrain = hm.data();
231 const std::vector<float> original = terrain;
232 const size_t count = terrain.size();
233 diagnostics.width = w; diagnostics.height = h;
234 diagnostics.wear.assign(count, 0.f);
235 diagnostics.deposition.assign(count, 0.f);
236 diagnostics.heightDelta.assign(count, 0.f);
237 std::vector<float> next(count), relaxed(count);
238 std::vector<float> valleyTarget(count), valleyWeight(count);
239 std::vector<float> floodplainTarget(count), floodplainWeight(count);
240 std::vector<int> channelDistance(count);
241 std::vector<size_t> order(count);
242 for (int iteration = 0; iteration < s.iterations; ++iteration) {
243 const HydrologyMap hydro = buildHydrology(hm, s.riverThreshold,
244 -std::numeric_limits<float>::infinity(),
245 s.coordinateScale, false);
246 const float cutoff = s.riverThreshold <= 1.f
247 ? std::max(2.f, s.riverThreshold * float(terrain.size()))
248 : s.riverThreshold;
249 std::iota(order.begin(), order.end(), size_t(0));
250 std::stable_sort(order.begin(), order.end(), [&](size_t a, size_t b) {
251 return hydro.flowAccumulation[a] > hydro.flowAccumulation[b];
252 });
253
254 // Channel heads must be allowed to form before they satisfy the final
255 // mapped-river cutoff. Using the display cutoff as the erosion cutoff
256 // leaves broad convex slopes forever smooth: there is no initial groove
257 // to capture adjacent runoff. A smaller initiation area, combined with
258 // coherent erodibility, seeds a few persistent rills. Recomputing the
259 // drainage field each iteration then lets successful rills capture flow
260 // and grow into a dendritic network while the others die out.
261 // Channel-initiation area scales with the requested drainage density.
262 // sqrt(cutoff) is much too permissive on medium/large maps and lets
263 // nearly every raster column become a permanent rill on smooth slopes.
264 const float initiationCutoff = std::max(4.f, cutoff * 0.30f);
265 const float wideningCutoff = std::lerp(initiationCutoff, cutoff, 0.42f);
266 // Priority-Flood supplies a receiver graph through both shallow sills
267 // and genuinely endorheic basins. Only depressions that can plausibly
268 // be opened within this erosion job's depth budget may become channels.
269 // Otherwise the virtual routing tree would be excavated into radial
270 // spokes across a lake floor.
271 const std::vector<uint8_t> protectedLake =
272 protectedLakeBasins(hydro, w, h, s.maxBreachDepth);
273
274 // Breach spill sills on the depression-free receiver graph. Priority
275 // Flood can route across a closed basin, but the corresponding receiver
276 // may still be uphill on the real surface. Merely skipping that link
277 // strands channels in the basin. Propagating a shallow descending grade
278 // through the sill opens a physical outlet, bounded by maxDepth so a
279 // deep endorheic basin is not flattened wholesale.
280 next = terrain;
281 const float breachGrade = 0.00035f / std::max(0.001f, s.coordinateScale);
282 for (size_t i : order) {
283 const int d = hydro.flowDirection[i];
284 if (d < 0 || hydro.flowAccumulation[i] < initiationCutoff) continue;
285 if (protectedLake[i]) continue;
286 const int x = int(i % size_t(w)), y = int(i / size_t(w));
287 const size_t receiver = at(x + dx[d], y + dy[d], w);
288 const float outletTarget = next[i] - breachGrade * distance[d];
289 if (next[receiver] > outletTarget) {
290 const int rx = x + dx[d], ry = y + dy[d];
291 const float boundaryDistance = float(std::min({rx, ry, w - 1 - rx, h - 1 - ry}));
292 const float boundaryT = saturate(boundaryDistance /
293 std::max(1.f, s.bankWidth + 1.f));
294 const float boundaryFade = boundaryT * boundaryT * (3.f - 2.f * boundaryT);
295 const float target = std::max(original[receiver] - s.maxDepth, outletTarget);
296 next[receiver] += (target - next[receiver]) * boundaryFade;
297 }
298 }
299
300 // Braun-Willett implicit update for n=1. Receivers have no smaller
301 // contributing area, so this order establishes their new height first.
302 for (size_t i : order) {
303 const int d = hydro.flowDirection[i];
304 if (d < 0 || hydro.flowAccumulation[i] < initiationCutoff) continue;
305 if (protectedLake[i]) continue;
306 const int x = int(i % size_t(w)), y = int(i / size_t(w));
307 const size_t receiver = at(x + dx[d], y + dy[d], w);
308 if (next[receiver] >= terrain[i]) continue;
309 const float normalizedArea = hydro.flowAccumulation[i] / cutoff;
310 const float maturity = saturate((hydro.flowAccumulation[i] - initiationCutoff) /
311 std::max(1.f, cutoff - initiationCutoff));
312 const float erodibility = 0.72f + 0.56f *
313 routeNoise(float(x) * 0.085f + 31.7f, float(y) * 0.085f - 19.3f);
314 const float boundaryDistance = float(std::min({x, y, w - 1 - x, h - 1 - y}));
315 const float boundaryT = saturate(boundaryDistance /
316 std::max(1.f, s.bankWidth + 1.f));
317 const float boundaryFade = boundaryT * boundaryT * (3.f - 2.f * boundaryT);
318 const float c = s.incision * erodibility * boundaryFade *
319 std::pow(std::max(0.04f, normalizedArea), 0.45f) / distance[d];
320 const float implicitHeight = (terrain[i] + c * next[receiver]) / (1.f + c);
321 // A small detachment term supplies the positive feedback missing
322 // from pure slope relaxation. It is weak at a newly initiated head
323 // and approaches the configured incision rate only in mature flow.
324 // `incision` is a per-pass terrain-height scale. The old
325 // 0.00045--0.0018 multiplier made even an aggressively configured
326 // gallery river cut less than one screen pixel after many passes:
327 // hydrology was visible only because a water ribbon was drawn on
328 // top of an essentially unchanged slope. Mature channels need a
329 // meaningful detachment term so their beds establish a longitudinal
330 // profile and the bank pass below has a real valley floor to widen.
331 // Channel heads remain deliberately weak to avoid turning every
332 // drainage pixel into a deep raster trench.
333 const float detachment = s.incision * erodibility * boundaryFade *
334 (0.0030f + 0.0090f * maturity) *
335 std::pow(std::max(0.04f, normalizedArea), 0.30f);
336 next[i] = std::max(original[i] - s.maxDepth,
337 std::min(terrain[i], implicitHeight - detachment));
338 }
339
340 // Limited hillslope diffusion near persistent channels turns the
341 // one-cell bed into a V-shaped valley without excavating circular pits.
342 const int radius = std::max(0, int(std::ceil(s.bankWidth)));
343 std::fill(channelDistance.begin(), channelDistance.end(), radius + 1);
344 std::queue<size_t> queue;
345 for (size_t i = 0; i < count; ++i)
346 if (hydro.flowAccumulation[i] >= wideningCutoff &&
347 !protectedLake[i]) {
348 channelDistance[i] = 0; queue.push(i);
349 }
350 constexpr std::array<int, 4> cardinal{1, 3, 4, 6};
351 while (!queue.empty()) {
352 const size_t i = queue.front(); queue.pop();
353 if (channelDistance[i] >= radius) continue;
354 const int x = int(i % size_t(w)), y = int(i / size_t(w));
355 for (int d : cardinal) {
356 const int nx = x + dx[d], ny = y + dy[d];
357 if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
358 const size_t n = at(nx, ny, w);
359 if (channelDistance[n] > channelDistance[i] + 1) {
360 channelDistance[n] = channelDistance[i] + 1; queue.push(n);
361 }
362 }
363 }
364 relaxed = next;
365 for (int y = 1; y + 1 < h; ++y) for (int x = 1; x + 1 < w; ++x) {
366 const size_t i = at(x, y, w);
367 if (channelDistance[i] == 0 || channelDistance[i] > radius) continue;
368 const float average = (next[at(x - 1, y, w)] + next[at(x + 1, y, w)] +
369 next[at(x, y - 1, w)] + next[at(x, y + 1, w)]) * 0.25f;
370 const float alpha = 0.12f * (1.f - float(channelDistance[i]) / float(radius + 1));
371 relaxed[i] = std::clamp(next[i] + (average - next[i]) * alpha,
372 original[i] - s.maxDepth, original[i]);
373 }
374 terrain.swap(relaxed);
375 }
376
377 // Widen the final, stable drainage network once. Reapplying this operation
378 // inside the incision loop over-deepens banks and turns valleys into trenches.
379 const HydrologyMap hydro = buildHydrology(hm, s.riverThreshold,
380 -std::numeric_limits<float>::infinity(),
381 s.coordinateScale, false);
382 const float cutoff = s.riverThreshold <= 1.f
383 ? std::max(2.f, s.riverThreshold * float(count))
384 : s.riverThreshold;
385 const std::vector<uint8_t> protectedLake =
386 protectedLakeBasins(hydro, w, h, s.maxBreachDepth);
387 std::fill(valleyTarget.begin(), valleyTarget.end(),
388 std::numeric_limits<float>::infinity());
389 std::fill(valleyWeight.begin(), valleyWeight.end(), 0.f);
390 std::fill(floodplainTarget.begin(), floodplainTarget.end(), 0.f);
391 std::fill(floodplainWeight.begin(), floodplainWeight.end(), 0.f);
392 for (size_t i = 0; i < count; ++i) {
393 const float flow = hydro.flowAccumulation[i];
394 const int d = hydro.flowDirection[i];
395 if (flow < cutoff || d < 0 || protectedLake[i]) continue;
396 const int x = int(i % size_t(w)), y = int(i / size_t(w));
397 const size_t receiver = at(x + dx[d], y + dy[d], w);
398 // Estimate a reach tangent from the strongest upstream donor through
399 // this cell to its receiver. Expanding circular stamps around D8
400 // pixels produces a chain of pits and swollen confluences; a local
401 // tangent lets the valley grow across the channel while adjacent
402 // stamps overlap smoothly along it.
403 int donorX = x, donorY = y;
404 float donorHeight = terrain[i];
405 float donorFlow = -1.f;
406 for (int upstreamDirection = 0; upstreamDirection < 8; ++upstreamDirection) {
407 const int ux = x + dx[upstreamDirection], uy = y + dy[upstreamDirection];
408 if (ux < 0 || uy < 0 || ux >= w || uy >= h) continue;
409 const size_t upstream = at(ux, uy, w);
410 const int upstreamReceiver = hydro.flowDirection[upstream];
411 if (upstreamReceiver < 0 ||
412 ux + dx[upstreamReceiver] != x || uy + dy[upstreamReceiver] != y)
413 continue;
414 if (hydro.flowAccumulation[upstream] > donorFlow) {
415 donorFlow = hydro.flowAccumulation[upstream];
416 donorX = ux; donorY = uy; donorHeight = terrain[upstream];
417 }
418 }
419 // Use the continuous MFD gradient for the corridor frame. The D8 donor
420 // and receiver remain the authoritative longitudinal graph, but their
421 // eight discrete headings leave a chain of overlapping oval pits when
422 // they are also used as the valley-carving orientation.
423 float tangentX = hydro.flowVectorX[i];
424 float tangentY = hydro.flowVectorY[i];
425 float tangentLength = std::sqrt(tangentX * tangentX + tangentY * tangentY);
426 if (tangentLength < 0.001f) {
427 tangentX = float(x + dx[d] - donorX);
428 tangentY = float(y + dy[d] - donorY);
429 tangentLength = std::hypot(tangentX, tangentY);
430 }
431 if (tangentLength < 0.001f) {
432 tangentX = float(dx[d]); tangentY = float(dy[d]); tangentLength = distance[d];
433 }
434 tangentX /= tangentLength; tangentY /= tangentLength;
435 const float normalX = -tangentY, normalY = tangentX;
436 const float areaRatio = std::max(1.f, flow / cutoff);
437 // Hydraulic geometry is controlled by contributing area, not by the
438 // largest river present in this particular tile. Normalising every
439 // reach against maxFlow made a trunk river on one seed as narrow as a
440 // tributary on another. A bounded Hack-style power law preserves
441 // visible stream hierarchy while remaining stable across map sizes.
442 const float areaScale = std::min(3.4f, std::pow(areaRatio, 0.32f));
443 const float riverMaturity = saturate(std::log2(areaRatio) / 6.f);
444 const float bedSlope = std::max(0.f, terrain[i] - terrain[receiver]) / distance[d];
445 // Mature low-gradient reaches exchange sediment laterally and produce
446 // a recognisable valley floor. Treating every reach as bedrock incision
447 // leaves a narrow V-notch all the way to the outlet.
448 const bool alluvial = areaRatio > 3.5f && bedSlope < 0.045f;
449 const float width = std::max(0.75f, s.bankWidth *
450 (0.18f + 0.50f * areaScale) * (alluvial ? 1.28f : 1.f));
451 // A short longitudinal footprint bridges diagonal D8 steps without
452 // allowing one reach to excavate a circular area around itself.
453 // A longer overlap turns cell stamps into one continuous geomorphic
454 // corridor. It is still much shorter than a bend wavelength, so it
455 // cannot cut across separate neighbouring reaches.
456 const float alongReach = 2.75f;
457 const int radius = int(std::ceil(width + alongReach));
458 // bankWidth is expressed in raster cells. Normalize the per-cell rise
459 // so doubling resolution (and therefore bankWidth) preserves the same
460 // approximate world-space cross-section instead of doubling relief.
461 const float bankResolutionScale = 3.f / std::max(1.f, s.bankWidth);
462 const float bankRise = (alluvial ? 0.0032f
463 : 0.010f + 0.020f * (1.f - riverMaturity)) *
464 bankResolutionScale;
465 // Establish a scale-separated trunk bed after the iterative stream
466 // power passes. This is deliberately tied to maxDepth so it cannot
467 // bypass the caller's erosion budget. Tributaries get only a shallow
468 // notch; high-order reaches acquire enough relief for a readable V
469 // valley or alluvial floor.
470 const float bedOffset = s.maxDepth * (0.025f + 0.19f * riverMaturity);
471 for (int oy = -radius; oy <= radius; ++oy) for (int ox = -radius; ox <= radius; ++ox) {
472 const int nx = x + ox, ny = y + oy;
473 if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
474 const float lateral = std::abs(float(ox) * normalX + float(oy) * normalY);
475 const float along = float(ox) * tangentX + float(oy) * tangentY;
476 if (lateral > width || std::abs(along) > alongReach) continue;
477 const size_t n = at(nx, ny, w);
478 // Continue the channel's longitudinal grade through this stamp.
479 // Upstream and downstream samples are taken from the actual
480 // receiver graph so overlapping stamps agree on bed elevation.
481 float bed = terrain[i] - bedOffset;
482 if (along >= 0.f)
483 bed += along * (terrain[receiver] - terrain[i]) / distance[d];
484 else if (donorFlow >= 0.f) {
485 const float donorDistance = std::hypot(float(x - donorX), float(y - donorY));
486 bed += along * (terrain[i] - donorHeight) /
487 std::max(1.f, donorDistance);
488 }
489 // Four-part geomorphic cross-section:
490 // bed -> floor -> valley wall -> rounded shoulder.
491 // A single linear ramp reads as a raster trench and has no break
492 // in slope for lighting to reveal. Bedrock reaches retain a small
493 // channel floor and a concave V-wall; mature reaches get a broad,
494 // almost-level alluvial floor and a steeper outer valley wall.
495 const float channelHalfWidth = std::max(0.42f,
496 width * (0.045f + 0.025f * riverMaturity));
497 const float floorHalfWidth = alluvial
498 ? width * (0.25f + 0.25f * riverMaturity)
499 : channelHalfWidth;
500 const float shoulderStart = std::max(floorHalfWidth + 0.01f, width * 0.78f);
501 float profileRise = 0.f;
502 if (lateral <= channelHalfWidth) {
503 profileRise = 0.0007f * lateral;
504 } else if (lateral <= floorHalfWidth) {
505 profileRise = 0.0007f * channelHalfWidth +
506 0.0012f * (lateral - channelHalfWidth);
507 } else if (lateral <= shoulderStart) {
508 const float wallT = (lateral - floorHalfWidth) /
509 std::max(0.001f, shoulderStart - floorHalfWidth);
510 const float wallShape = std::pow(wallT, alluvial ? 1.32f : 0.78f);
511 profileRise = 0.0007f * channelHalfWidth +
512 bankRise * (shoulderStart - floorHalfWidth) * wallShape;
513 } else {
514 const float shoulderRise = bankRise * (shoulderStart - floorHalfWidth);
515 const float shoulderT = (lateral - shoulderStart) /
516 std::max(0.001f, width - shoulderStart);
517 // Smoothly flatten the outer wall into the untouched hillslope.
518 const float rounded = shoulderT * shoulderT * (3.f - 2.f * shoulderT);
519 profileRise = 0.0007f * channelHalfWidth + shoulderRise +
520 bankRise * (width - shoulderStart) * 0.22f * rounded;
521 }
522 const float target = bed + profileRise;
523 const float crossFade = 1.f - lateral / width;
524 const float alongFade = 1.f - std::abs(along) / alongReach;
525 const float smoothCrossFade = crossFade * crossFade * (3.f - 2.f * crossFade);
526 const float domainDistance = float(std::min({nx, ny, w - 1 - nx, h - 1 - ny}));
527 const float domainT = saturate(domainDistance /
528 std::max(1.f, s.bankWidth + 1.f));
529 const float domainFade = domainT * domainT * (3.f - 2.f * domainT);
530 // Priority-Flood deliberately drains to the finite map boundary.
531 // Widening every one of those numerical outlets stamps a bright
532 // rectangular moat into wear maps. A production tiled build uses
533 // a halo and crops it; this fade provides the equivalent behaviour
534 // for standalone heightfields while leaving interior valleys intact.
535 const float edgeFade = smoothCrossFade * (0.68f + 0.32f * alongFade) *
536 domainFade;
537 if (target < valleyTarget[n]) valleyTarget[n] = target;
538 valleyWeight[n] = std::max(valleyWeight[n], edgeFade);
539 if (alluvial && lateral <= floorHalfWidth) {
540 const float floorFade = 1.f - lateral / std::max(0.001f, floorHalfWidth);
541 // A weak cross-valley grade avoids an unnaturally perfect
542 // tabletop while still filling D8-scale ruts and point pits.
543 const float floor = bed + 0.0015f * lateral;
544 const float weight = floorFade * floorFade * (0.5f + 0.5f * alongFade) *
545 domainFade;
546 floodplainTarget[n] += floor * weight;
547 floodplainWeight[n] += weight;
548 }
549 }
550 }
551 for (size_t i = 0; i < count; ++i) if (std::isfinite(valleyTarget[i])) {
552 const float target = std::max(original[i] - s.maxDepth, valleyTarget[i]);
553 if (target < terrain[i])
554 terrain[i] += (target - terrain[i]) * (0.82f * valleyWeight[i]);
555 }
556 // Deposit exported channel sediment back into mature, low-slope reaches.
557 // The pre-fluvial surface is an upper bound, so this cannot inflate hills or
558 // violate maxDepth; it only fills local over-incision and establishes a
559 // continuous alluvial floor inside the wider carved valley.
560 for (size_t i = 0; i < count; ++i) if (floodplainWeight[i] > 0.f) {
561 const float desired = std::clamp(floodplainTarget[i] / floodplainWeight[i],
562 original[i] - s.maxDepth, original[i]);
563 const float alpha = 0.34f * saturate(floodplainWeight[i]);
564 const float before = terrain[i];
565 terrain[i] += (desired - terrain[i]) * alpha;
566 diagnostics.deposition[i] += std::max(0.f, terrain[i] - before);
567 }
568
569 // Lake-inlet alluvial aprons. A channel entering standing water rapidly
570 // loses transport capacity; without this pass the incised V-groove simply
571 // terminates at the shoreline. Build a widening, low-gradient fan on the
572 // landward side of each high-flow inlet. Deposition may only restore
573 // material removed by this erosion job, never raise the original terrain
574 // or fill a protected lake basin wholesale.
575 std::vector<float> deltaTarget(count, -std::numeric_limits<float>::infinity());
576 std::vector<float> deltaWeight(count, 0.f);
577 for (size_t i = 0; i < count; ++i) {
578 const int d = hydro.flowDirection[i];
579 if (d < 0 || protectedLake[i] || hydro.flowAccumulation[i] < cutoff) continue;
580 const int x = int(i % size_t(w)), y = int(i / size_t(w));
581 const int rx = x + dx[d], ry = y + dy[d];
582 if (rx < 0 || ry < 0 || rx >= w || ry >= h) continue;
583 const size_t receiver = at(rx, ry, w);
584 if (!protectedLake[receiver]) continue;
585 const float areaRatio = std::max(1.f, hydro.flowAccumulation[i] / cutoff);
586 const float fanLength = s.bankWidth * (0.85f + 0.30f *
587 std::min(3.f, std::pow(areaRatio, 0.28f)));
588 const float fanWidth = fanLength * 0.72f;
589 const int radius = int(std::ceil(std::max(fanLength, fanWidth)));
590 const float tangentX = float(dx[d]) / distance[d];
591 const float tangentY = float(dy[d]) / distance[d];
592 const float normalX = -tangentY, normalY = tangentX;
593 const float lakeSurface = terrain[receiver] + hydro.lakeDepth[receiver];
594 for (int oy = -radius; oy <= radius; ++oy) for (int ox = -radius; ox <= radius; ++ox) {
595 const int nx = x + ox, ny = y + oy;
596 if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
597 const size_t n = at(nx, ny, w);
598 if (protectedLake[n]) continue;
599 const float along = float(ox) * tangentX + float(oy) * tangentY;
600 if (along > 0.35f || along < -fanLength) continue;
601 const float upstreamT = std::clamp(-along / std::max(0.001f, fanLength), 0.f, 1.f);
602 const float localHalfWidth = std::lerp(fanWidth, std::max(0.7f, s.bankWidth * 0.22f),
603 upstreamT);
604 const float lateral = std::abs(float(ox) * normalX + float(oy) * normalY);
605 if (lateral > localHalfWidth) continue;
606 const float crossFade = 1.f - lateral / localHalfWidth;
607 const float lengthFade = 1.f - upstreamT;
608 const float target = lakeSurface + (-along) *
609 (0.0018f / std::max(0.001f, s.coordinateScale));
610 deltaTarget[n] = std::max(deltaTarget[n], target);
611 deltaWeight[n] = std::max(deltaWeight[n], crossFade * crossFade *
612 (0.35f + 0.65f * lengthFade));
613 }
614 }
615 for (size_t i = 0; i < count; ++i) if (deltaWeight[i] > 0.f) {
616 const float target = std::clamp(deltaTarget[i], terrain[i], original[i]);
617 const float before = terrain[i];
618 terrain[i] += (target - terrain[i]) * (0.58f * deltaWeight[i]);
619 diagnostics.deposition[i] += std::max(0.f, terrain[i] - before);
620 }
621
622 // Bank-failure relaxation. Stream-power incision establishes the bed but
623 // does not by itself enforce a stable hillslope angle, so a deeply lowered
624 // raster cell can leave an implausible near-vertical wall. Move material
625 // downslope only around the final valley footprint. This behaves like a
626 // compact talus/debris pass without blurring unaffected mountain ridges.
627 std::vector<uint8_t> valleyRegion(count, 0);
628 for (size_t i = 0; i < count; ++i)
629 valleyRegion[i] = uint8_t(valleyWeight[i] > 1e-4f || floodplainWeight[i] > 1e-4f ||
630 deltaWeight[i] > 0.f);
631 std::vector<float> bankDelta(count, 0.f);
632 constexpr std::array<int, 4> bankDx{1, 0, 1, -1};
633 constexpr std::array<int, 4> bankDy{0, 1, 1, 1};
634 constexpr std::array<float, 4> bankDistance{1.f, 1.f, 1.41421356f, 1.41421356f};
635 const float stableBankStep = 0.014f / std::max(0.001f, s.coordinateScale);
636 for (int pass = 0; pass < 4; ++pass) {
637 std::fill(bankDelta.begin(), bankDelta.end(), 0.f);
638 for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) {
639 const size_t a = at(x, y, w);
640 for (size_t edge = 0; edge < bankDx.size(); ++edge) {
641 const int nx = x + bankDx[edge], ny = y + bankDy[edge];
642 if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
643 const size_t b = at(nx, ny, w);
644 if (!valleyRegion[a] && !valleyRegion[b]) continue;
645 const float difference = terrain[a] - terrain[b];
646 const float excess = std::abs(difference) - stableBankStep * bankDistance[edge];
647 if (excess <= 0.f) continue;
648 const float moved = excess * 0.11f;
649 if (difference > 0.f) { bankDelta[a] -= moved; bankDelta[b] += moved; }
650 else { bankDelta[a] += moved; bankDelta[b] -= moved; }
651 }
652 }
653 for (size_t i = 0; i < count; ++i) {
654 const float before = terrain[i];
655 terrain[i] = std::clamp(terrain[i] + bankDelta[i],
656 original[i] - s.maxDepth, original[i]);
657 diagnostics.deposition[i] += std::max(0.f, terrain[i] - before);
658 }
659 }
660 for (size_t i = 0; i < count; ++i) {
661 diagnostics.heightDelta[i] = terrain[i] - original[i];
662 // Gross wear equals the remaining net lowering plus material that was
663 // subsequently returned to this cell. This makes wear/deposit useful
664 // independently while preserving wear - deposit == net lowering.
665 diagnostics.wear[i] = std::max(0.f, original[i] - terrain[i]) +
666 diagnostics.deposition[i];
667 }
668 return diagnostics;
669}
670
671HydrologyMap TerrainPipeline::buildHydrology(const Heightmap &hm, float threshold, float seaLevel,
672 float coordinateScale, bool classifyLakes) {
673 HydrologyMap out;
674 out.width = hm.getWidth(); out.height = hm.getHeight();
675 const int w = out.width, h = out.height; const size_t count = size_t(w) * size_t(h);
676 out.flowDirection.assign(count, -1); out.flowAccumulation.assign(count, 1.f);
677 out.flowVectorX.assign(count, 0.f); out.flowVectorY.assign(count, 0.f);
678 out.lakeDepth.assign(count, 0.f); out.rivers.assign(count, 0);
679 out.streamOrder.assign(count, 0);
680 if (w <= 0 || h <= 0) return out;
681 const auto &z = hm.data();
682 // Priority-flood produces a minimally raised routing surface. Every inland
683 // cell then has a path to the map edge instead of terminating in a noise pit.
684 struct FloodCell { float elevation; size_t index; };
685 auto greater = [](const FloodCell &a, const FloodCell &b) {
686 return a.elevation > b.elevation || (a.elevation == b.elevation && a.index > b.index);
687 };
688 std::priority_queue<FloodCell, std::vector<FloodCell>, decltype(greater)> frontier(greater);
689 std::vector<float> routed = z;
690 std::vector<uint8_t> visited(count, 0);
691 auto seed = [&](int x, int y) {
692 const size_t i = at(x, y, w);
693 if (!visited[i]) { visited[i] = 1; frontier.push({routed[i], i}); }
694 };
695 for (int x = 0; x < w; ++x) { seed(x, 0); seed(x, h - 1); }
696 for (int y = 1; y + 1 < h; ++y) { seed(0, y); seed(w - 1, y); }
697 constexpr float epsilon = 1e-6f;
698 while (!frontier.empty()) {
699 const FloodCell cell = frontier.top(); frontier.pop();
700 const int x = int(cell.index % size_t(w)), y = int(cell.index / size_t(w));
701 for (int d = 0; d < 8; ++d) {
702 const int nx = x + dx[d], ny = y + dy[d];
703 if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
704 const size_t n = at(nx, ny, w);
705 if (visited[n]) continue;
706 visited[n] = 1;
707 routed[n] = std::max(routed[n], cell.elevation + epsilon);
708 for (int back = 0; back < 8; ++back)
709 if (nx + dx[back] == x && ny + dy[back] == y) { out.flowDirection[n] = int8_t(back); break; }
710 frontier.push({routed[n], n});
711 }
712 }
713 // Preserve the physical meaning of depression filling instead of silently
714 // drawing rivers across the dry basin floor. Sub-epsilon depths are routing
715 // gradients, not visible water.
716 for (size_t i = 0; i < count; ++i) {
717 const float depth = routed[i] - z[i];
718 out.lakeDepth[i] = depth > 1e-4f ? depth : 0.f;
719 }
720 // Priority-Flood defines the depression-free routing surface, but its
721 // visitation parent is not a physical flow direction: using that tree
722 // directly imprints queue-order spokes into smooth mountains. Route each
723 // cell to its locally steepest downslope neighbour on the filled surface.
724 for (int y = 1; y + 1 < h; ++y) for (int x = 1; x + 1 < w; ++x) {
725 const size_t i = at(x, y, w);
726 int bestDirection = -1;
727 float gradientX = 0.f, gradientY = 0.f, totalGradientWeight = 0.f;
728 for (int d = 0; d < 8; ++d) {
729 const size_t n = at(x + dx[d], y + dy[d], w);
730 const float slope = (routed[i] - routed[n]) / distance[d];
731 if (slope <= 0.f) continue;
732 const float weight = std::pow(slope, 1.1f);
733 gradientX += weight * float(dx[d]) / distance[d];
734 gradientY += weight * float(dy[d]) / distance[d];
735 totalGradientWeight += weight;
736 }
737 float bestScore = -std::numeric_limits<float>::infinity();
738 const float gradientLength = std::hypot(gradientX, gradientY);
739 if (gradientLength > 1e-12f) {
740 out.flowVectorX[i] = gradientX / gradientLength;
741 out.flowVectorY[i] = gradientY / gradientLength;
742 }
743 for (int d = 0; d < 8; ++d) {
744 const int nx = x + dx[d], ny = y + dy[d];
745 const size_t n = at(nx, ny, w);
746 const float slope = (routed[i] - routed[n]) / distance[d];
747 if (slope <= 0.f) continue;
748 // Quantise the continuous multi-neighbour gradient only for the
749 // single-channel receiver graph. The contributing area below is
750 // still distributed to every downslope neighbour.
751 const float alignment = totalGradientWeight > 0.f
752 ? (gradientX * float(dx[d]) + gradientY * float(dy[d])) /
753 (totalGradientWeight * distance[d]) : 0.f;
754 const float score = alignment + slope * 0.08f;
755 if (score > bestScore) { bestScore = score; bestDirection = d; }
756 }
757 if (bestDirection >= 0) out.flowDirection[i] = int8_t(bestDirection);
758 }
759 std::vector<size_t> order(count); std::iota(order.begin(), order.end(), size_t(0));
760 std::stable_sort(order.begin(), order.end(), [&](size_t a, size_t b) { return routed[a] > routed[b]; });
761 // Freeman multiple-flow-direction accumulation. Sheet flow can converge
762 // continuously before a channel is selected, avoiding D8's artificial
763 // capture of an entire raster column by one early diagonal decision.
764 for (size_t i : order) {
765 const int x = int(i % size_t(w)), y = int(i / size_t(w));
766 std::array<float, 8> weights{};
767 float weightSum = 0.f;
768 for (int d = 0; d < 8; ++d) {
769 const int nx = x + dx[d], ny = y + dy[d];
770 if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
771 const float slope = (routed[i] - routed[at(nx, ny, w)]) / distance[d];
772 if (slope <= 0.f) continue;
773 weights[size_t(d)] = std::pow(slope, 1.1f);
774 weightSum += weights[size_t(d)];
775 }
776 if (weightSum <= 0.f) continue;
777 for (int d = 0; d < 8; ++d) if (weights[size_t(d)] > 0.f) {
778 const int nx = x + dx[d], ny = y + dy[d];
779 out.flowAccumulation[at(nx, ny, w)] +=
780 out.flowAccumulation[i] * weights[size_t(d)] / weightSum;
781 }
782 }
783 const float cutoff = threshold <= 1.f ? std::max(2.f, threshold * float(count)) : threshold;
784 std::vector<uint8_t> suppressedBoundaryBasin(count, 0);
785 std::vector<uint8_t> boundaryOutletChannel(count, 0);
786 std::vector<size_t> boundaryBasinMouths;
787
788 // Priority-Flood reports every numerical depression, but most tiny noise
789 // pits are seasonal wet ground rather than perennial lakes. Classify whole
790 // connected basins using physical area, depth/volume and contributing
791 // catchment. Filtering individual pixels by depth leaves dotted puddles and
792 // can cut holes through one coherent lake shoreline.
793 if (classifyLakes) {
794 std::fill(visited.begin(), visited.end(), uint8_t(0));
795 std::vector<size_t> basin;
796 std::queue<size_t> basinQueue;
797 const float samplesPerReferenceArea = coordinateScale * coordinateScale;
798 for (size_t seedIndex = 0; seedIndex < count; ++seedIndex) {
799 if (visited[seedIndex] || out.lakeDepth[seedIndex] <= 0.f) continue;
800 basin.clear();
801 visited[seedIndex] = 1;
802 basinQueue.push(seedIndex);
803 float maximumDepth = 0.f, depthVolume = 0.f, maximumCatchment = 0.f;
804 size_t maximumCatchmentCell = seedIndex;
805 int minimumBoundaryDistance = std::min(w, h);
806 while (!basinQueue.empty()) {
807 const size_t i = basinQueue.front(); basinQueue.pop();
808 basin.push_back(i);
809 maximumDepth = std::max(maximumDepth, out.lakeDepth[i]);
810 depthVolume += out.lakeDepth[i];
811 if (out.flowAccumulation[i] > maximumCatchment) {
812 maximumCatchment = out.flowAccumulation[i];
813 maximumCatchmentCell = i;
814 }
815 const int x = int(i % size_t(w)), y = int(i / size_t(w));
816 minimumBoundaryDistance = std::min(minimumBoundaryDistance,
817 std::min({x, y, w - 1 - x, h - 1 - y}));
818 for (int d = 0; d < 8; ++d) {
819 const int nx = x + dx[d], ny = y + dy[d];
820 if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
821 const size_t n = at(nx, ny, w);
822 if (!visited[n] && out.lakeDepth[n] > 0.f) {
823 visited[n] = 1; basinQueue.push(n);
824 }
825 }
826 }
827 const float referenceArea = float(basin.size()) /
828 std::max(0.001f, samplesPerReferenceArea);
829 const float meanDepth = depthVolume / float(basin.size());
830 const float catchmentRatio = maximumCatchment / std::max(1.f, cutoff);
831 const int boundaryHalo = std::max(1, int(std::ceil(2.f * coordinateScale)));
832 const bool perennial = minimumBoundaryDistance > boundaryHalo &&
833 referenceArea >= 6.f && maximumDepth >= 0.004f &&
834 (meanDepth >= 0.0015f || maximumDepth >= 0.020f) &&
835 (catchmentRatio >= 0.35f || referenceArea >= 20.f);
836 if (!perennial) {
837 if (minimumBoundaryDistance <= boundaryHalo) {
838 for (size_t i : basin) suppressedBoundaryBasin[i] = 1;
839 boundaryBasinMouths.push_back(maximumCatchmentCell);
840 }
841 for (size_t i : basin) out.lakeDepth[i] = 0.f;
842 }
843 }
844 }
845 // A boundary-touching depression lacks enough off-map context for lake
846 // classification. Suppress its Priority-Flood routing tree, then retain
847 // only the highest-discharge trunk from the basin mouth to the open edge.
848 // This is a deterministic single-outlet fallback until a neighbouring halo
849 // is available, and avoids both a fake lake and radial blue spokes.
850 for (size_t mouth : boundaryBasinMouths) {
851 size_t current = mouth;
852 for (size_t steps = 0; steps < count && suppressedBoundaryBasin[current]; ++steps) {
853 boundaryOutletChannel[current] = 1;
854 const int direction = out.flowDirection[current];
855 if (direction < 0 || direction >= 8) break;
856 const int x = int(current % size_t(w)), y = int(current / size_t(w));
857 const int nx = x + dx[size_t(direction)], ny = y + dy[size_t(direction)];
858 if (nx < 0 || ny < 0 || nx >= w || ny >= h) break;
859 const size_t nextCell = at(nx, ny, w);
860 if (nextCell == current) break;
861 current = nextCell;
862 }
863 current = mouth;
864 for (size_t steps = 0; steps < count && suppressedBoundaryBasin[current]; ++steps) {
865 boundaryOutletChannel[current] = 1;
866 const int x = int(current % size_t(w)), y = int(current / size_t(w));
867 size_t strongestDonor = current;
868 float strongestFlow = -1.f;
869 for (int neighbour = 0; neighbour < 8; ++neighbour) {
870 const int nx = x + dx[size_t(neighbour)], ny = y + dy[size_t(neighbour)];
871 if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
872 const size_t donor = at(nx, ny, w);
873 if (!suppressedBoundaryBasin[donor]) continue;
874 const int donorDirection = out.flowDirection[donor];
875 if (donorDirection < 0 || donorDirection >= 8 ||
876 nx + dx[size_t(donorDirection)] != x ||
877 ny + dy[size_t(donorDirection)] != y) continue;
878 if (out.flowAccumulation[donor] > strongestFlow) {
879 strongestFlow = out.flowAccumulation[donor];
880 strongestDonor = donor;
881 }
882 }
883 if (strongestDonor == current) break;
884 current = strongestDonor;
885 }
886 }
887 for (size_t i = 0; i < count; ++i)
888 out.rivers[i] = uint8_t(z[i] > seaLevel && out.lakeDepth[i] <= 0.f &&
889 (!suppressedBoundaryBasin[i] || boundaryOutletChannel[i]) &&
890 (out.flowAccumulation[i] >= cutoff || boundaryOutletChannel[i]));
891 // MFD accumulation is physically smoother than single-receiver D8, but a
892 // portion of the discharge can leave the selected main receiver and make
893 // one intermediate cell fall just below the display threshold. A river
894 // mask made from the threshold alone then contains one-cell holes even
895 // though both reaches belong to the same drainage path. Close the semantic
896 // network downstream without changing the MFD accumulation values.
897 const std::vector<uint8_t> thresholdRivers = out.rivers;
898 for (size_t seed = 0; seed < count; ++seed) {
899 if (!thresholdRivers[seed]) continue;
900 size_t current = seed;
901 for (size_t steps = 0; steps < count; ++steps) {
902 const int direction = out.flowDirection[current];
903 if (direction < 0 || direction >= 8) break;
904 const int x = int(current % size_t(w)), y = int(current / size_t(w));
905 const int nx = x + dx[size_t(direction)], ny = y + dy[size_t(direction)];
906 if (nx < 0 || ny < 0 || nx >= w || ny >= h) break;
907 const size_t nextCell = at(nx, ny, w);
908 if (z[nextCell] <= seaLevel || out.lakeDepth[nextCell] > 0.f ||
909 (suppressedBoundaryBasin[nextCell] && !boundaryOutletChannel[nextCell])) break;
910 out.rivers[nextCell] = 1;
911 if (nextCell == current) break;
912 current = nextCell;
913 }
914 }
915 // Strahler ordering converts the binary river mask into a stable hierarchy:
916 // headwaters are order 1 and the order increases only when two tributaries
917 // of the same order meet. This is less sensitive to local MFD discharge
918 // fluctuations than deriving every geomorphic decision from area alone.
919 std::vector<uint16_t> riverIndegree(count, 0);
920 for (size_t i = 0; i < count; ++i) {
921 if (!out.rivers[i]) continue;
922 const int direction = out.flowDirection[i];
923 if (direction < 0 || direction >= 8) continue;
924 const int x = int(i % size_t(w)), y = int(i / size_t(w));
925 const int nx = x + dx[size_t(direction)], ny = y + dy[size_t(direction)];
926 if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
927 const size_t receiver = at(nx, ny, w);
928 if (out.rivers[receiver]) ++riverIndegree[receiver];
929 }
930 std::vector<uint8_t> maximumIncomingOrder(count, 0), equalMaximumDonors(count, 0);
931 std::queue<size_t> orderQueue;
932 for (size_t i = 0; i < count; ++i) if (out.rivers[i] && riverIndegree[i] == 0) {
933 out.streamOrder[i] = 1; orderQueue.push(i);
934 }
935 while (!orderQueue.empty()) {
936 const size_t i = orderQueue.front(); orderQueue.pop();
937 const int direction = out.flowDirection[i];
938 if (direction < 0 || direction >= 8) continue;
939 const int x = int(i % size_t(w)), y = int(i / size_t(w));
940 const int nx = x + dx[size_t(direction)], ny = y + dy[size_t(direction)];
941 if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
942 const size_t receiver = at(nx, ny, w);
943 if (!out.rivers[receiver]) continue;
944 const uint8_t donorOrder = out.streamOrder[i];
945 if (donorOrder > maximumIncomingOrder[receiver]) {
946 maximumIncomingOrder[receiver] = donorOrder;
947 equalMaximumDonors[receiver] = 1;
948 } else if (donorOrder == maximumIncomingOrder[receiver]) {
949 equalMaximumDonors[receiver] = uint8_t(std::min(255,
950 int(equalMaximumDonors[receiver]) + 1));
951 }
952 if (riverIndegree[receiver] > 0 && --riverIndegree[receiver] == 0) {
953 out.streamOrder[receiver] = uint8_t(std::min(255,
954 int(maximumIncomingOrder[receiver]) +
955 (equalMaximumDonors[receiver] >= 2 ? 1 : 0)));
956 orderQueue.push(receiver);
957 }
958 }
959 return out;
960}
961
963 float seaLevel, float latitude, float coordinateScale) {
964 ClimateMap out;
965 out.width = hm.getWidth(); out.height = hm.getHeight();
966 const int w = out.width, h = out.height; const size_t count = size_t(w) * size_t(h);
967 out.temperature.resize(count); out.moisture.resize(count); out.biomes.resize(count);
968 if (w <= 0 || h <= 0) return out;
969 const auto &z = hm.data();
970
971 // World-scale freshwater distance is a more stable ecological signal than
972 // repeatedly blurring a one-cell river mask. It creates a continuous
973 // riparian corridor across chunk boundaries and a broad lake-shore ecotone
974 // while preserving the categorical channel/lake cells themselves.
975 const int waterInfluenceRadius = std::max(1, int(std::ceil(4.f * coordinateScale)));
976 std::vector<int> freshWaterDistance(count, waterInfluenceRadius + 1);
977 std::queue<size_t> waterQueue;
978 for (size_t i = 0; i < count; ++i) {
979 const bool freshWater = (i < hydro.rivers.size() && hydro.rivers[i]) ||
980 (i < hydro.lakeDepth.size() && hydro.lakeDepth[i] > 0.001f);
981 if (freshWater) { freshWaterDistance[i] = 0; waterQueue.push(i); }
982 }
983 constexpr std::array<int, 4> climateDx{-1, 1, 0, 0};
984 constexpr std::array<int, 4> climateDy{0, 0, -1, 1};
985 while (!waterQueue.empty()) {
986 const size_t i = waterQueue.front(); waterQueue.pop();
987 if (freshWaterDistance[i] >= waterInfluenceRadius) continue;
988 const int x = int(i % size_t(w)), y = int(i / size_t(w));
989 for (size_t d = 0; d < climateDx.size(); ++d) {
990 const int nx = x + climateDx[d], ny = y + climateDy[d];
991 if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
992 const size_t n = at(nx, ny, w);
993 if (freshWaterDistance[n] > freshWaterDistance[i] + 1) {
994 freshWaterDistance[n] = freshWaterDistance[i] + 1;
995 waterQueue.push(n);
996 }
997 }
998 }
999 for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) {
1000 const size_t i = at(x, y, w); const float lat = std::abs((float(y) + 0.5f) / float(h) * 2.f - 1.f);
1001 out.temperature[i] = saturate(1.f - lat * saturate(latitude) - std::max(0.f, z[i] - seaLevel) * 0.7f);
1002 const float river = i < hydro.rivers.size() && hydro.rivers[i] ? 1.f : 0.f;
1003 const float lake = i < hydro.lakeDepth.size() && hydro.lakeDepth[i] > 0.001f ? 1.f : 0.f;
1004 const float drainage = i < hydro.flowAccumulation.size() ? std::log1p(hydro.flowAccumulation[i]) / std::log1p(float(count)) : 0.f;
1005 const float waterProximity = freshWaterDistance[i] <= waterInfluenceRadius
1006 ? 1.f - float(freshWaterDistance[i]) / float(waterInfluenceRadius + 1) : 0.f;
1007 out.moisture[i] = saturate(0.12f + 0.55f * drainage + 0.45f * river +
1008 0.70f * lake + 0.34f * waterProximity * waterProximity +
1009 (z[i] <= seaLevel ? 1.f : 0.f));
1010 }
1011 // Drainage is a one-cell D8 field. Diffusing its climatic contribution
1012 // produces coherent riparian and regional biomes instead of pixel-wide
1013 // vegetation stripes, while semantic river/ocean cells remain explicit.
1014 std::vector<float> smoothed = out.moisture;
1015 for (int iteration = 0; iteration < 5; ++iteration) {
1016 for (int y = 1; y + 1 < h; ++y) for (int x = 1; x + 1 < w; ++x) {
1017 const size_t i = at(x, y, w);
1018 if (z[i] <= seaLevel || (i < hydro.rivers.size() && hydro.rivers[i])) {
1019 smoothed[i] = out.moisture[i];
1020 continue;
1021 }
1022 const float neighbours = (out.moisture[at(x - 1, y, w)] +
1023 out.moisture[at(x + 1, y, w)] +
1024 out.moisture[at(x, y - 1, w)] +
1025 out.moisture[at(x, y + 1, w)]) * 0.25f;
1026 smoothed[i] = out.moisture[i] * 0.45f + neighbours * 0.55f;
1027 }
1028 out.moisture.swap(smoothed);
1029 }
1030 for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) {
1031 const size_t i = at(x, y, w);
1032 const float river = i < hydro.rivers.size() && hydro.rivers[i] ? 1.f : 0.f;
1033 const bool lake = i < hydro.lakeDepth.size() && hydro.lakeDepth[i] > 0.001f;
1034 const bool nearFreshWater = freshWaterDistance[i] <=
1035 std::max(1, int(std::ceil(2.5f * coordinateScale)));
1036 const float localSlope = std::max(
1037 std::abs(z[at(std::max(0, x - 1), y, w)] - z[at(std::min(w - 1, x + 1), y, w)]),
1038 std::abs(z[at(x, std::max(0, y - 1), w)] - z[at(x, std::min(h - 1, y + 1), w)]));
1039 if (z[i] <= seaLevel) out.biomes[i] = Biome::Ocean;
1040 else if (lake && hydro.lakeDepth[i] <= 0.006f && localSlope < 0.045f)
1041 out.biomes[i] = Biome::Wetland;
1042 else if (lake) out.biomes[i] = Biome::Lake;
1043 else if (river > 0.f) out.biomes[i] = Biome::River;
1044 else if (z[i] < seaLevel + 0.035f) out.biomes[i] = Biome::Beach;
1045 else if (nearFreshWater && out.moisture[i] > 0.62f && localSlope < 0.045f)
1046 out.biomes[i] = Biome::Wetland;
1047 else if (z[i] > 0.86f) out.biomes[i] = Biome::Alpine;
1048 else if (out.temperature[i] < 0.22f) out.biomes[i] = out.moisture[i] > 0.38f ? Biome::Taiga : Biome::Tundra;
1049 else if (out.moisture[i] < 0.22f) out.biomes[i] = Biome::Desert;
1050 else if (out.moisture[i] < 0.48f) out.biomes[i] = Biome::Grassland;
1051 else if (out.moisture[i] < 0.75f) out.biomes[i] = Biome::Forest;
1052 else out.biomes[i] = Biome::Rainforest;
1053 }
1054 return out;
1055}
1056
1057} // namespace eve::procgen
LogicalId target
double value
double score
Definition Agent.cpp:50
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
std::string terrain
std::string from
const std::string & s
float nx
float ny
std::map< std::string, Var > values
std::uint32_t capacity
glm::vec3 n
Definition Grass.cpp:63
std::vector< ClimateData > climate
Current moisture and cloud state per cell.
float v
std::int32_t c
float elevation
float river
int biome
bool lake
int h
std::uint32_t height
std::uint32_t width
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float distance
std::vector< std::int32_t > order
float radius
std::uint32_t seed
Definition PointSet.cpp:807
float d
int steps
RoadLaneDirection direction
const RoadEdge * edge
double current
float dy
float dx
std::uint32_t count
Cell cell
std::map< Cell, int > best
TerrainWaterField water
Heightmap sediment
float weights[3]
uint32_t index
std::uint32_t depth
std::size_t at
double oy
double ox
In-memory terrain heightmap: a dense float grid (row-major, index = y * width + x) materialized from ...
Definition Heightmap.h:21
int getHeight() const
Returns the height.
Definition Heightmap.cpp:17
const std::vector< float > & data() const
Data.
Definition Heightmap.h:48
int getWidth() const
Returns the width.
Definition Heightmap.cpp:16
float getMoisture(int x, int y) const
Returns the moisture.
TerrainLayers()=default
Terrain layers.
int getStreamOrder(int x, int y) const
Return Strahler river order, or zero outside the river network.
int getBiome(int x, int y) const
Returns the biome.
int getWidth() const
Returns the width.
int getHeight() const
Returns the height.
bool isLake(int x, int y, float minimumDepth=0.001f) const
Return whether the cell belongs to a resolved closed-basin lake.
bool isRiver(int x, int y) const
True when river.
int getFlowDirection(int x, int y) const
Return the D8 receiver direction for one cell, or -1 at an outlet/out of bounds.
float getFlowAccumulation(int x, int y) const
Returns the flow accumulation.
float getFlowVectorY(int x, int y) const
Return the continuous normalized downslope Y component.
float getTemperature(int x, int y) const
Returns the temperature.
std::string getBiomeName(int x, int y) const
Returns the biome name.
float getLakeDepth(int x, int y) const
Return filled-depression water depth, or zero outside a lake.
float getFlowVectorX(int x, int y) const
Return the continuous normalized downslope X component.
static TerrainErosionMap erodeFluvialDetailed(Heightmap &heightmap, const FluvialErosionSettings &settings={})
Cut river valleys and return wear, deposition, and net-change diagnostic layers.
static void erodeHydraulic(Heightmap &heightmap, const HydraulicErosionSettings &settings={})
Grid-based water/sediment simulation that carves channels and deposits sediment.
static HydrologyMap buildHydrology(const Heightmap &heightmap, float riverThreshold=0.025f, float seaLevel=0.25f, float coordinateScale=1.f, bool classifyLakes=true)
Compute D8 drainage, upstream contributing area, and a thresholded river mask.
static ClimateMap buildClimate(const Heightmap &heightmap, const HydrologyMap &hydrology, float seaLevel=0.25f, float latitude=0.35f, float coordinateScale=1.f)
Derive temperature, moisture, and biome layers from terrain and hydrology.
static void erodeThermal(Heightmap &heightmap, const ThermalErosionSettings &settings={})
Relax slopes exceeding the configured talus angle while conserving mass.
static void erodeFluvial(Heightmap &heightmap, const FluvialErosionSettings &settings={})
Cut branching river valleys using depression-free drainage and stream power.
ClimateMap public API.
std::vector< float > temperature
std::vector< Biome > biomes
std::vector< float > moisture
FluvialErosionSettings public API.
HydraulicErosionSettings public API.
HydrologyMap public API.
std::vector< float > flowVectorX
std::vector< uint8_t > rivers
std::vector< float > flowAccumulation
std::vector< float > flowVectorY
Normalized continuous downslope direction.
std::vector< int8_t > flowDirection
D8 neighbour index, or -1 for a sink.
std::vector< float > lakeDepth
Priority-Flood water depth; zero on drained terrain.
std::vector< uint8_t > streamOrder
Strahler order; zero outside the resolved river network.
Per-cell diagnostic outputs produced by an erosion stage.
float getDeposition(int x, int y) const
Sample deposited material, or zero outside the map.
float getWear(int x, int y) const
Sample gross erosion, or zero outside the map.
std::vector< float > deposition
Material deposited after transport, in heightmap units.
std::vector< float > heightDelta
Final minus initial elevation; negative means net erosion.
float getHeightDelta(int x, int y) const
Sample final-minus-initial elevation, or zero outside the map.
std::vector< float > wear
Gross material removed, in heightmap units.
ThermalErosionSettings public API.