Added: Initial world generation tool
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
// Package thermal is mass-conserving thermal weathering at an angle of repose: where a cell stands above a
|
||||
// neighbour by more than the angle allows, material slides down.
|
||||
//
|
||||
// It is not decoration, and this is worth stating because it looks like decoration. Stream power puts *no
|
||||
// upper bound* on hillslope angle: the steady state S = (U/K)^(1/n) * A^(-m/n) is evaluated per cell, and a
|
||||
// cell next to a divide drains one cell, so A is tiny and S is enormous. Raising the intraplate uplift rate
|
||||
// to give the plains some relief therefore made every hillslope on the map proportionally steeper, and the
|
||||
// median slope went from 10 to 39 degrees. What limits a real hillslope is not fluvial incision, it is
|
||||
// landsliding, and this is landsliding.
|
||||
//
|
||||
// Ported from heightmap_erosion.thermal, including the lesson attached to it: shed half the *largest* excess
|
||||
// rather than half the mean, because the mean converges far slower. Mass is conserved, so a cliff keeps its
|
||||
// face and scree gathers at its foot instead of evaporating.
|
||||
package thermal
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"salty/terrain/internal/field"
|
||||
)
|
||||
|
||||
var (
|
||||
dx8 = [8]int{-1, 0, 1, -1, 1, -1, 0, 1}
|
||||
dy8 = [8]int{-1, -1, -1, 0, 0, 1, 1, 1}
|
||||
)
|
||||
|
||||
// Apply runs `passes` iterations over h in place. talus is the maximum slope as rise over run (tan of the
|
||||
// angle of repose). fixed marks cells that may not move, which is base level.
|
||||
//
|
||||
// Each pass reads h and writes a delta, then applies it: no cell is read after any cell has been written, so
|
||||
// the result does not depend on the order the rows were processed and the parallel version is identical to
|
||||
// the serial one.
|
||||
func Apply(h []float32, w, hgt int, cellM, talus float64, passes int, fixed []bool, scratch []float32) {
|
||||
if passes <= 0 || talus <= 0 {
|
||||
return
|
||||
}
|
||||
delta := scratch[:len(h)]
|
||||
card := cellM
|
||||
diag := cellM * math.Sqrt2
|
||||
|
||||
for p := 0; p < passes; p++ {
|
||||
for i := range delta {
|
||||
delta[i] = 0
|
||||
}
|
||||
// Deltas are accumulated per source cell into a private view; each row range owns its own writes for
|
||||
// the cell it is standing on, and the neighbour credits are gathered rather than scattered, which is
|
||||
// what keeps this free of races without a lock.
|
||||
field.Rows(hgt, func(y0, y1 int) {
|
||||
for y := y0; y < y1; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
i := y*w + x
|
||||
// Gather: how much this cell RECEIVES from higher neighbours, and how much it sheds.
|
||||
var give, take float64
|
||||
if fixed == nil || !fixed[i] {
|
||||
give = shed(h, w, hgt, x, y, card, diag, talus)
|
||||
}
|
||||
take = 0
|
||||
for k := 0; k < 8; k++ {
|
||||
nx, ny := x+dx8[k], y+dy8[k]
|
||||
if nx < 0 || ny < 0 || nx >= w || ny >= hgt {
|
||||
continue
|
||||
}
|
||||
ni := ny*w + nx
|
||||
if fixed != nil && fixed[ni] {
|
||||
continue
|
||||
}
|
||||
// The share this cell gets of that neighbour's shed material.
|
||||
if share := shedShare(h, w, hgt, nx, ny, i, card, diag, talus); share > 0 {
|
||||
take += share
|
||||
}
|
||||
}
|
||||
delta[i] = float32(take - give)
|
||||
}
|
||||
}
|
||||
})
|
||||
for i := range h {
|
||||
h[i] += delta[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shed is the total a cell gives away this pass: half the largest excess over the angle of repose.
|
||||
func shed(h []float32, w, hgt, x, y int, card, diag, talus float64) float64 {
|
||||
i := y*w + x
|
||||
maxExcess := 0.0
|
||||
for k := 0; k < 8; k++ {
|
||||
nx, ny := x+dx8[k], y+dy8[k]
|
||||
if nx < 0 || ny < 0 || nx >= w || ny >= hgt {
|
||||
continue
|
||||
}
|
||||
d := card
|
||||
if dx8[k] != 0 && dy8[k] != 0 {
|
||||
d = diag
|
||||
}
|
||||
if e := float64(h[i]-h[ny*w+nx]) - talus*d; e > maxExcess {
|
||||
maxExcess = e
|
||||
}
|
||||
}
|
||||
return maxExcess / 2
|
||||
}
|
||||
|
||||
// shedShare is how much of cell (x, y)'s shed material lands on cell target, proportional to target's share
|
||||
// of the total excess below it.
|
||||
func shedShare(h []float32, w, hgt, x, y, target int, card, diag, talus float64) float64 {
|
||||
i := y*w + x
|
||||
maxExcess, total, mine := 0.0, 0.0, 0.0
|
||||
for k := 0; k < 8; k++ {
|
||||
nx, ny := x+dx8[k], y+dy8[k]
|
||||
if nx < 0 || ny < 0 || nx >= w || ny >= hgt {
|
||||
continue
|
||||
}
|
||||
d := card
|
||||
if dx8[k] != 0 && dy8[k] != 0 {
|
||||
d = diag
|
||||
}
|
||||
ni := ny*w + nx
|
||||
e := float64(h[i]-h[ni]) - talus*d
|
||||
if e <= 0 {
|
||||
continue
|
||||
}
|
||||
if e > maxExcess {
|
||||
maxExcess = e
|
||||
}
|
||||
total += e
|
||||
if ni == target {
|
||||
mine = e
|
||||
}
|
||||
}
|
||||
if total <= 0 || mine <= 0 {
|
||||
return 0
|
||||
}
|
||||
return (maxExcess / 2) * (mine / total)
|
||||
}
|
||||
|
||||
// TalusFromDegrees converts an angle of repose to the rise-over-run the solver wants.
|
||||
func TalusFromDegrees(deg float64) float64 { return math.Tan(deg * math.Pi / 180) }
|
||||
Reference in New Issue
Block a user