Zum Inhalt springen
Michael Malura
718 Github Stars 5795 npm Downloads 55 Projekte

Höhenkurven und Terrassen

Das Rauschen aus Gelände aus Rauschen liefert Hügel, die alle ungefähr gleich hoch sind. Was fehlt, sind weite Ebenen und spitze Gipfel. Dafür braucht es kein anderes Rauschen. Man formt dieselben Zahlen um, nachdem sie ausgerechnet sind.

Links die Karte, rechts oben die Höhenkurve und ein Histogramm der Höhen, darunter ein Querschnitt entlang der gestrichelten Zeile auf der Karte. Die fünf Knoten der Kurve lassen sich mit der Maus ziehen, ein Zug auf der Karte verschiebt den Ausschnitt, und die Maus über der Karte wählt die Zeile für den Querschnitt. Die Chips schalten Kurvenformen, Terrassen, Ridges und die naive Variante um. Die Demo startet mit der Kurve "Valleys" und Ridges. Chip 1 ist die gerade Linie, dann zeigt die Karte wieder das Rauschen von vorher.

Eine Kurve zwischen Rauschen und Bild

Jede Höhe geht durch eine Funktion, die 0 bis 1 auf 0 bis 1 abbildet. Bei der Geraden ändert sich nichts. Hängt die Kurve durch, rutschen die mittleren Höhen nach unten, und ein großer Teil der Karte wird flach. Red Blob Games erklärt das ausführlich mit pow(e, exponent), und das ist der einfachste Einstieg. Ich wollte die Kurve zeichnen und anfassen können, also liegt sie hier auf fünf Knoten. Zwischen den Knoten läuft ein kubisches Polynom, dessen Steigungen so gewählt sind, dass bei steigenden Knoten nie ein Überschwinger entsteht (Fritsch und Carlson):

c.m[k] = d[k - 1] * d[k] <= 0.0 ? 0.0 : 2.0 * d[k - 1] * d[k] / (d[k - 1] + d[k]);

d sind die Steigungen der Sekanten zwischen den Knoten. Wechselt sie das Vorzeichen oder ist eine davon 0, wird die Steigung am Knoten 0, sonst gilt das harmonische Mittel. Dazu kommt eine Regel beim Ziehen: Ein Knoten darf seine Nachbarn nicht überholen.

const double lo = i == 0 ? 0.0 : y[i - 1];
const double hi = i + 1 == y.size() ? 1.0 : y[i + 1];
y[i] = std::clamp(value, lo, hi);

Ohne diese Klemme könnte ein höherer Wert links von einem niedrigeren stehen. Die Kurve würde sich umkehren, und die Karte läge an der Stelle auf dem Kopf: Täler wären plötzlich höher als die Hänge davor. Der Test dazu ist handgerechnet. Die Knotenwerte 0, 0, 0, 1, 1 ergeben zwischen dem dritten und vierten Knoten ein S, dessen Mitte exakt bei 0,5 liegt.

Das Histogramm zeigt, was die Kurve mit der Verteilung macht. Die Umrisse sind die Höhen vor der Kurve, die gefüllten Balken danach. Bei "Valleys" (x zum Quadrat auf den Knoten) wandert fast alles nach links, im Start-Zustand sind 76 Prozent der Fläche Land, das Meer liegt bei 0,15. Die Gipfel dehnen sich dafür nach oben, weil das obere Ende der Kurve steil ist. Chip 3 ist eine S-Kurve: Ebenen unten, Ebenen oben und ein steiler Hang dazwischen.

Terrassen

Höhen auf Stufen zu runden ist eine Zeile, round(e * n) / n (steht auch bei Red Blob). Das Ergebnis sind lauter senkrechte Klippen. Ein Hang, den man hochlaufen kann, kommt in der Funktion nicht vor. Ich lasse deshalb jede Stufe aus einer flachen Fläche und einem geneigten Stück in der Mitte bestehen:

const double t = h * steps;
const double level = std::floor(t);
const double half = 0.5 * clamp01(riser);
return (level + smoothstep(0.5 - half, 0.5 + half, t - level)) / steps;

riser ist der Anteil der Stufe, der Hang ist. Bei 0 kommt die harte Klippe heraus, bei 0,35 (so steht es in der Demo) etwas, das nach Ackerterrasse aussieht. In der Mitte jedes Hangs ist das Ergebnis gleich der Eingabe, der Gesamtanstieg des Geländes bleibt also erhalten. Handgerechnet für vier Stufen und riser 0,4: Bei 0,375 kommt 0,375 heraus. Chip T schaltet Terrassen ein, mit Chip S wählst du die Stufenzahl. Die orange Linie in der Kurve zeigt, was nach den Terrassen übrig bleibt. Chip N auf naiv macht aus den Hängen wieder Klippen: Auf der Karte verschwindet die Schattierung auf den Flächen und die Stufenkanten werden zu harten Linien.

Ridges

Gewöhnliches Rauschen ist an den Gipfeln rund. Spitze Grate entstehen, wenn man die Nulldurchgänge des Rauschens anhebt:

const double r = 1.0 - std::fabs(n);
ridgeSum += amp * r * r;

Dort, wo das Rauschen 0 kreuzt, ist r gleich 1, überall sonst kleiner, und das Quadrat schärft die Kante. Red Blob Games gewichtet dafür die höheren Oktaven mit dem Wert der ersten, sodass nur die Berge das Zusatzrauschen bekommen. Ich habe es anders gebaut: Das Ridge-Feld wird mit einer Maske, die erst ab Höhe 0,5 anfängt und bei 0,8 voll wirkt, auf die glatte Höhe aufgeschlagen:

const double weight = s.naive ? 1.0 : ridgeMask(base);
return clamp01(base + weight * (clamp01(ridge) - 0.5) * kRidgeAmount);

Mit ridgeMask als smoothstep(0.5, 0.8, h). Das Ridge-Feld ist um 0,5 zentriert, es hebt und senkt also nur die Details, im Mittel bleibt das Gelände, wie es ist. Mit R schaltest du es ein, mit N wirkt es überall, auch in den Tälern, und die Ebenen werden von Falten durchzogen.

Meine erste Fassung hatte einen Fehler, den ich nur an der Zahl unter der Karte gemerkt habe. Der höchste Punkt lag bei allen sechs Seeds zwischen 0,48 und 0,54 (damals war "Valleys" noch x hoch 2,5), obwohl das glatte Rauschen bei jedem Seed über 0,9 kommt. Ich hatte das Ridge-Feld aus allen Oktaven gebaut, auch aus der ersten. Die erste Oktave formt die großen Hügel, und ein Gipfel ist dort, wo das Rauschen groß ist, also |n| groß und r klein. Die Ridges haben jeden Gipfel nach unten gezogen. Jetzt lässt die Summe die erste Oktave aus, sobald es mehr als eine gibt. Danach liegt der höchste Punkt mit der jetzigen Kurve zwischen 0,92 und 1,00.

Stolperfallen

Die Eingabe der Kurve muss in 0 bis 1 liegen. pow mit einer negativen Basis und gebrochenem Exponenten ergibt NaN, und ein NaN, das später in einen Index umgerechnet wird, ist undefiniertes Verhalten. Der Test prüft deshalb auch die Ränder: Werte unter 0 und über 1 werden vor der Kurve geklemmt, NaN wird zu 0. Die Konstanten für die Streckung des Rohrauschens stehen fest im Code und werden nicht pro Bild aus Minimum und Maximum berechnet, sonst würde die Karte beim Verschieben flackern. Und Terrassen gehören hinter die Kurve, nicht davor, sonst verschiebt die Kurve die Stufenhöhen wieder.

Den Code findest du im Repo unter terrain-shaping. Alle Demos der Serie sammelt das Projekt Game Dev Snippets.

13.11.2026 aktualisiert 06.11.2026
Teilen X Reddit Hacker News LinkedIn E-Mail
Neue Beiträge abonnieren RSS JSON