Drift-time peaks used to rail into the hard elevation clamp and flatten into a 9000 m plateau (the clamp lived in step(), erode() AND hydrology(), so erosion re-flattened them every tick). Replace the hard ceiling with a probabilistic soft cap: above peakSoftCapStart (7000 m) the chance a tick's uplift "takes" falls linearly to 0 at peakSoftCapEnd (12000 m); a lost grow roll forfeits the uplift and shaves a random 0..peakFailDrop (200 m) off. Peaks now spread smoothly across a height band (strong orogeny reaches ~10-11 km, most cluster lower) with zero cells pinned at the ceiling. - The roll is a pure hash of (cellIndex, erodeIter, seed): never touches rngState, bit-identical across OpenMP thread counts, and since erodeIter is saved (step/erode run 1:1 in drift) F5/F9 resumes bit-identical -- no save bump. - Drift-only (gated on Planet::drifting) so Phase-1 forming still auto-settles. - The hard clamp's upper bound now tracks peakSoftCapEnd in all three places (step/erode/hydrology); lower -11000 m unchanged. - New planet.cfg knobs peakSoftCapStart / peakSoftCapEnd / peakFailDrop with validation (+ start < end cross-rule). Docs updated (CLAUDE.md, BUILD.md). Verified headless: smooth 8.5->10.5 km taper, 0 pinned, determinism + exact resume intact, test_logic + test_biota pass, full app builds clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
61 lines
3.0 KiB
C++
61 lines
3.0 KiB
C++
#include "Planet.hpp"
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
// --- Phase 2: erosion + sea level -------------------------------------------
|
|
|
|
// Mass-conserving downhill sediment transport: along each edge the higher cell
|
|
// gives material to the lower one, faster above sea level (subaerial weathering)
|
|
// than below (slow submarine). Highs wear down toward an uplift<->erosion
|
|
// equilibrium; the sediment piles in basins and below sea level, building shelves
|
|
// and deltas and slowly filling trenches. One double-buffered gather pass (each
|
|
// cell writes only its own index -> bit-identical for any thread count).
|
|
void Planet::erode(double dtMy) {
|
|
const int n = (int)cells.size();
|
|
sErode.resize(n);
|
|
const double sea = cfg.seaLevel;
|
|
|
|
#pragma omp parallel for schedule(static) if(n > 20000)
|
|
for (int i = 0; i < n; ++i) {
|
|
double ei = cells[i].elevation, delta = 0.0;
|
|
for (int j : cells[i].neighbors) {
|
|
double ej = cells[j].elevation;
|
|
if (ei > ej) { // i is higher: i -> j outflow
|
|
double rate = (ei > sea) ? cfg.erosionLandRate : cfg.erosionSeaRate;
|
|
delta -= std::min(rate * dtMy, 0.5) * (ei - ej);
|
|
} else if (ej > ei) { // j is higher: j -> i inflow
|
|
double rate = (ej > sea) ? cfg.erosionLandRate : cfg.erosionSeaRate;
|
|
delta += std::min(rate * dtMy, 0.5) * (ej - ei);
|
|
}
|
|
}
|
|
sErode[i] = delta;
|
|
}
|
|
// Upper clamp tracks peakSoftCapEnd (matches step()); the soft peak cap, not a
|
|
// fixed 9000 m wall here, governs how high mountains stand.
|
|
#pragma omp parallel for schedule(static) if(n > 20000)
|
|
for (int i = 0; i < n; ++i)
|
|
cells[i].elevation = std::clamp(cells[i].elevation + sErode[i], -11000.0, cfg.peakSoftCapEnd);
|
|
|
|
if (++erodeIter % cfg.seaLevelEvery == 0) adjustSeaLevel();
|
|
}
|
|
|
|
// Gradual eustatic controller: if the geographic land fraction is outside a
|
|
// deadband around the target, nudge sea level by a small fixed step (raising it
|
|
// floods land, lowering exposes it). Called infrequently (seaLevelEvery) so the
|
|
// coastline drifts slowly, not in a jump. A nudge is only taken if it actually
|
|
// reduces the error -- otherwise (e.g. a big mass of cells at one elevation, where
|
|
// a full step would overshoot) it rests at the closest a fixed step allows instead
|
|
// of oscillating back and forth across that "cliff".
|
|
void Planet::adjustSeaLevel() {
|
|
const int n = (int)cells.size();
|
|
if (n == 0) return;
|
|
auto landFracAt = [&](double sl) {
|
|
int a = 0; for (const auto& c : cells) if (c.elevation > sl) ++a; return (double)a / n;
|
|
};
|
|
double err = landFracAt(cfg.seaLevel) - cfg.landFractionTarget;
|
|
if (std::fabs(err) <= cfg.seaLevelTol) return; // within deadband: rest
|
|
double cand = cfg.seaLevel + (err > 0 ? cfg.seaLevelStep : -cfg.seaLevelStep);
|
|
double candErr = landFracAt(cand) - cfg.landFractionTarget;
|
|
if (std::fabs(candErr) < std::fabs(err)) cfg.seaLevel = cand; // nudge only if it helps
|
|
}
|