Added: Initial world generation tool
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
// 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()
|
||||
}
|
||||
Reference in New Issue
Block a user