201 lines
5.7 KiB
Go
201 lines
5.7 KiB
Go
package coast
|
|
|
|
import (
|
|
"math"
|
|
|
|
"salty/terrain/internal/field"
|
|
)
|
|
|
|
// The coast is a *distance*, not a line. Every coastal process is written in terms of how far a cell is from
|
|
// the waterline and which stretch of waterline it belongs to: the shelf deepens with distance offshore, the
|
|
// surf planes the land within a reach of it, sediment settles in the shallows behind it, and shelter is a
|
|
// property of a stretch of shore that every cell near it inherits. So the first thing the pass builds is an
|
|
// exact signed distance field with a feature index, and everything after it is a lookup.
|
|
//
|
|
// Exact, not a chamfer approximation: Felzenszwalb & Huttenlocher's transform is two 1-D passes and O(n)
|
|
// whatever the radius, so there is nothing to buy by approximating, and a chamfer's 2 % anisotropy would show
|
|
// up directly as a shelf that is wider along the grid axes than across them.
|
|
|
|
// edt returns, for every cell, the squared distance in cells to the nearest seed cell and the index of that
|
|
// seed. A column pass finds the nearest seed in each column; a row pass takes the lower envelope of the
|
|
// parabolas those distances define.
|
|
//
|
|
// Cells in a column with no seed at all are given a cost above any real distance rather than an infinity, so
|
|
// the envelope arithmetic never sees a NaN; they are then never chosen unless the map has no seeds anywhere,
|
|
// which the caller checks for.
|
|
func edt(seed []bool, w, h int) (d2 []float32, near []int32) {
|
|
d2 = make([]float32, w*h)
|
|
near = make([]int32, w*h)
|
|
|
|
bigF := float64(w*w+h*h) * 4 // above any achievable dx² + dy²
|
|
bigD := float32(math.Sqrt(bigF))
|
|
|
|
colD := make([]float32, w*h) // distance in cells to the nearest seed in this column
|
|
colN := make([]int32, w*h) // that seed's row, or -1
|
|
|
|
field.Rows(w, func(x0, x1 int) {
|
|
for x := x0; x < x1; x++ {
|
|
best := -1
|
|
for y := 0; y < h; y++ {
|
|
i := y*w + x
|
|
if seed[i] {
|
|
best = y
|
|
}
|
|
if best < 0 {
|
|
colD[i], colN[i] = bigD, -1
|
|
} else {
|
|
colD[i], colN[i] = float32(y-best), int32(best)
|
|
}
|
|
}
|
|
best = -1
|
|
for y := h - 1; y >= 0; y-- {
|
|
i := y*w + x
|
|
if seed[i] {
|
|
best = y
|
|
}
|
|
if best >= 0 {
|
|
if d := float32(best - y); d < colD[i] {
|
|
colD[i], colN[i] = d, int32(best)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
field.Rows(h, func(y0, y1 int) {
|
|
f := make([]float64, w)
|
|
v := make([]int, w)
|
|
z := make([]float64, w+1)
|
|
for y := y0; y < y1; y++ {
|
|
row := y * w
|
|
for x := 0; x < w; x++ {
|
|
d := float64(colD[row+x])
|
|
f[x] = d * d
|
|
}
|
|
k := 0
|
|
v[0] = 0
|
|
z[0] = math.Inf(-1)
|
|
z[1] = math.Inf(1)
|
|
for q := 1; q < w; q++ {
|
|
s := intersect(f, v[k], q)
|
|
for s <= z[k] {
|
|
k--
|
|
s = intersect(f, v[k], q)
|
|
}
|
|
k++
|
|
v[k] = q
|
|
z[k] = s
|
|
z[k+1] = math.Inf(1)
|
|
}
|
|
k = 0
|
|
for q := 0; q < w; q++ {
|
|
for z[k+1] < float64(q) {
|
|
k++
|
|
}
|
|
dx := float64(q - v[k])
|
|
d2[row+q] = float32(dx*dx + f[v[k]])
|
|
if n := colN[row+v[k]]; n < 0 {
|
|
near[row+q] = -1
|
|
} else {
|
|
near[row+q] = n*int32(w) + int32(v[k])
|
|
}
|
|
}
|
|
}
|
|
})
|
|
return d2, near
|
|
}
|
|
|
|
// intersect is where the parabolas rooted at p and q cross.
|
|
func intersect(f []float64, p, q int) float64 {
|
|
return ((f[q] + float64(q*q)) - (f[p] + float64(p*p))) / float64(2*q-2*p)
|
|
}
|
|
|
|
// Geometry is the coastline as the rest of the pass sees it.
|
|
type Geometry struct {
|
|
W, H int
|
|
CellM float64
|
|
|
|
// Dist is metres to the waterline: positive inland, negative offshore.
|
|
Dist *field.Field
|
|
|
|
// Ref is, for every cell, the waterline cell whose stretch of shore it belongs to. A land cell takes the
|
|
// sea cell nearest to it, which is on the waterline by construction; a sea cell takes the waterline cell
|
|
// nearest to the land cell nearest to it, which is the stretch of shore facing it. Every per-shore
|
|
// quantity — shelter, shelf width, the backshore relief — is computed once on the waterline and read
|
|
// everywhere else through this.
|
|
Ref []int32
|
|
|
|
// Waterline is the sea cells that touch land, in row-major order so anything iterating them is
|
|
// deterministic.
|
|
Waterline []int32
|
|
|
|
// ShoreM is the length of the land/sea boundary in metres, counted as boundary edges. It overestimates a
|
|
// diagonal coast by about 4/pi, as any edge-counted perimeter does.
|
|
ShoreM float64
|
|
}
|
|
|
|
// Measure builds the signed distance field and the shore reference from a land/sea mask.
|
|
func Measure(sea []bool, w, h int, cellM float64) *Geometry {
|
|
anySea, anyLand := false, false
|
|
land := make([]bool, len(sea))
|
|
for i, s := range sea {
|
|
land[i] = !s
|
|
if s {
|
|
anySea = true
|
|
} else {
|
|
anyLand = true
|
|
}
|
|
}
|
|
g := &Geometry{W: w, H: h, CellM: cellM, Dist: field.New(w, h, cellM), Ref: make([]int32, w*h)}
|
|
for i := range g.Ref {
|
|
g.Ref[i] = -1
|
|
}
|
|
if !anySea || !anyLand {
|
|
return g // an all-land or all-sea map has no coast; every pass below is a no-op on it
|
|
}
|
|
|
|
d2Sea, nearSea := edt(sea, w, h) // for a land cell: how far to water, and where
|
|
d2Land, nearLand := edt(land, w, h) // for a sea cell: how far to land, and where
|
|
|
|
for i := range sea {
|
|
if sea[i] {
|
|
g.Dist.Data[i] = float32(-math.Sqrt(float64(d2Land[i])) * cellM)
|
|
} else {
|
|
g.Dist.Data[i] = float32(math.Sqrt(float64(d2Sea[i])) * cellM)
|
|
}
|
|
}
|
|
|
|
// The waterline: sea cells with land in the eight-neighbourhood, which is d2Land of 1 or 2.
|
|
for i := range sea {
|
|
if sea[i] && d2Land[i] <= 2.001 {
|
|
g.Waterline = append(g.Waterline, int32(i))
|
|
}
|
|
}
|
|
|
|
for i := range sea {
|
|
if sea[i] {
|
|
if l := nearLand[i]; l >= 0 {
|
|
g.Ref[i] = nearSea[l]
|
|
}
|
|
} else {
|
|
g.Ref[i] = nearSea[i]
|
|
}
|
|
}
|
|
|
|
// Perimeter by boundary edges, which is what a shoreline length means on a grid.
|
|
edges := 0
|
|
for y := 0; y < h; y++ {
|
|
for x := 0; x < w; x++ {
|
|
i := y*w + x
|
|
if x+1 < w && sea[i] != sea[i+1] {
|
|
edges++
|
|
}
|
|
if y+1 < h && sea[i] != sea[i+w] {
|
|
edges++
|
|
}
|
|
}
|
|
}
|
|
g.ShoreM = float64(edges) * cellM
|
|
return g
|
|
}
|