202 lines
5.2 KiB
Go
202 lines
5.2 KiB
Go
// Package field is the one array type the whole generator passes around: a square-ish grid of float32 in a
|
|
// known unit, with the cell size in metres attached so no pass has to be told the scale twice.
|
|
//
|
|
// Determinism (cross-cutting rule 12) is a property of this package as much as of the passes. Everything
|
|
// parallel here partitions rows into disjoint, contiguous ranges and writes only into its own range, so the
|
|
// result does not depend on how the goroutines were scheduled. Nothing reduces through a channel.
|
|
package field
|
|
|
|
import (
|
|
"math"
|
|
"runtime"
|
|
"sort"
|
|
"sync"
|
|
)
|
|
|
|
// Field is a W x H grid, row-major, with CellM metres between neighbouring samples.
|
|
type Field struct {
|
|
W, H int
|
|
CellM float64
|
|
Data []float32
|
|
}
|
|
|
|
func New(w, h int, cellM float64) *Field {
|
|
return &Field{W: w, H: h, CellM: cellM, Data: make([]float32, w*h)}
|
|
}
|
|
|
|
// NewLike is an empty field with another's shape and scale.
|
|
func NewLike(f *Field) *Field { return New(f.W, f.H, f.CellM) }
|
|
|
|
func (f *Field) Idx(x, y int) int { return y*f.W + x }
|
|
func (f *Field) At(x, y int) float32 { return f.Data[y*f.W+x] }
|
|
func (f *Field) Set(x, y int, v float32) { f.Data[y*f.W+x] = v }
|
|
func (f *Field) Len() int { return len(f.Data) }
|
|
|
|
// AtClamped samples with edge clamping, which is what every stencil in the generator wants at the border.
|
|
func (f *Field) AtClamped(x, y int) float32 {
|
|
if x < 0 {
|
|
x = 0
|
|
} else if x >= f.W {
|
|
x = f.W - 1
|
|
}
|
|
if y < 0 {
|
|
y = 0
|
|
} else if y >= f.H {
|
|
y = f.H - 1
|
|
}
|
|
return f.Data[y*f.W+x]
|
|
}
|
|
|
|
func (f *Field) Clone() *Field {
|
|
c := New(f.W, f.H, f.CellM)
|
|
copy(c.Data, f.Data)
|
|
return c
|
|
}
|
|
|
|
func (f *Field) Fill(v float32) {
|
|
for i := range f.Data {
|
|
f.Data[i] = v
|
|
}
|
|
}
|
|
|
|
func (f *Field) MinMax() (float32, float32) {
|
|
if len(f.Data) == 0 {
|
|
return 0, 0
|
|
}
|
|
lo, hi := f.Data[0], f.Data[0]
|
|
for _, v := range f.Data {
|
|
if v < lo {
|
|
lo = v
|
|
}
|
|
if v > hi {
|
|
hi = v
|
|
}
|
|
}
|
|
return lo, hi
|
|
}
|
|
|
|
func (f *Field) Mean() float64 {
|
|
if len(f.Data) == 0 {
|
|
return 0
|
|
}
|
|
// Summed as float64 in index order: the same total every run, whatever the machine.
|
|
var sum float64
|
|
for _, v := range f.Data {
|
|
sum += float64(v)
|
|
}
|
|
return sum / float64(len(f.Data))
|
|
}
|
|
|
|
// Percentile sorts a copy, so it costs a copy and a sort; used for thresholds, not in inner loops.
|
|
func (f *Field) Percentile(p float64) float32 {
|
|
if len(f.Data) == 0 {
|
|
return 0
|
|
}
|
|
c := make([]float32, len(f.Data))
|
|
copy(c, f.Data)
|
|
sort.Slice(c, func(i, j int) bool { return c[i] < c[j] })
|
|
i := int(p / 100 * float64(len(c)-1))
|
|
if i < 0 {
|
|
i = 0
|
|
} else if i >= len(c) {
|
|
i = len(c) - 1
|
|
}
|
|
return c[i]
|
|
}
|
|
|
|
// Normalise maps the field onto [0, 1]. A flat field becomes zero rather than a division by nothing.
|
|
func (f *Field) Normalise() {
|
|
lo, hi := f.MinMax()
|
|
span := float64(hi - lo)
|
|
if span < 1e-9 {
|
|
f.Fill(0)
|
|
return
|
|
}
|
|
for i, v := range f.Data {
|
|
f.Data[i] = float32((float64(v) - float64(lo)) / span)
|
|
}
|
|
}
|
|
|
|
// Slope returns rise over run per cell, the central difference used by the layer rules and the statistics.
|
|
func (f *Field) Slope() *Field {
|
|
out := NewLike(f)
|
|
inv := float32(1.0 / (2.0 * f.CellM))
|
|
Rows(f.H, func(y0, y1 int) {
|
|
for y := y0; y < y1; y++ {
|
|
for x := 0; x < f.W; x++ {
|
|
gx := (f.AtClamped(x+1, y) - f.AtClamped(x-1, y)) * inv
|
|
gy := (f.AtClamped(x, y+1) - f.AtClamped(x, y-1)) * inv
|
|
out.Data[out.Idx(x, y)] = float32(math.Hypot(float64(gx), float64(gy)))
|
|
}
|
|
}
|
|
})
|
|
return out
|
|
}
|
|
|
|
// Curvature is the Laplacian in metres per cell squared: positive on ridges and convex shoulders, negative in
|
|
// gullies and sediment traps. Ported from heightmap_erosion.curvature, which blurs lightly first.
|
|
func (f *Field) Curvature() *Field {
|
|
h := f.Blur(2)
|
|
out := NewLike(f)
|
|
inv := float32(1.0 / f.CellM)
|
|
Rows(f.H, func(y0, y1 int) {
|
|
for y := y0; y < y1; y++ {
|
|
for x := 0; x < f.W; x++ {
|
|
lap := h.AtClamped(x-1, y) + h.AtClamped(x+1, y) + h.AtClamped(x, y-1) + h.AtClamped(x, y+1) - 4*h.At(x, y)
|
|
out.Data[out.Idx(x, y)] = lap * inv
|
|
}
|
|
}
|
|
})
|
|
return out
|
|
}
|
|
|
|
// Blur is the five-point box blur the numpy pipeline used, repeated. Edge-clamped, so it does not darken
|
|
// the border the way a zero-padded one would.
|
|
func (f *Field) Blur(passes int) *Field {
|
|
cur := f.Clone()
|
|
if passes <= 0 {
|
|
return cur
|
|
}
|
|
next := NewLike(f)
|
|
for p := 0; p < passes; p++ {
|
|
Rows(f.H, func(y0, y1 int) {
|
|
for y := y0; y < y1; y++ {
|
|
for x := 0; x < cur.W; x++ {
|
|
s := cur.At(x, y) + cur.AtClamped(x-1, y) + cur.AtClamped(x+1, y) + cur.AtClamped(x, y-1) + cur.AtClamped(x, y+1)
|
|
next.Data[next.Idx(x, y)] = s / 5
|
|
}
|
|
}
|
|
})
|
|
cur, next = next, cur
|
|
}
|
|
return cur
|
|
}
|
|
|
|
// Rows runs fn over disjoint contiguous row ranges, one per core. The ranges are fixed before any goroutine
|
|
// starts and each writes only into its own, so the output is identical at any GOMAXPROCS. Every parallel
|
|
// loop in the generator goes through here; none spawns goroutines of its own.
|
|
func Rows(h int, fn func(y0, y1 int)) {
|
|
workers := runtime.GOMAXPROCS(0)
|
|
if workers > h {
|
|
workers = h
|
|
}
|
|
if workers <= 1 {
|
|
fn(0, h)
|
|
return
|
|
}
|
|
var wg sync.WaitGroup
|
|
step := (h + workers - 1) / workers
|
|
for y0 := 0; y0 < h; y0 += step {
|
|
y1 := y0 + step
|
|
if y1 > h {
|
|
y1 = h
|
|
}
|
|
wg.Add(1)
|
|
go func(a, b int) {
|
|
defer wg.Done()
|
|
fn(a, b)
|
|
}(y0, y1)
|
|
}
|
|
wg.Wait()
|
|
}
|