175 lines
5.3 KiB
Go
175 lines
5.3 KiB
Go
// Package dt is the exact Euclidean distance transform, with a feature index and an optional cylinder.
|
|
//
|
|
// It lives on its own because three different things need it and two of them are nowhere near the coast:
|
|
// the coastal pass writes every one of its processes as "how far is this cell from the waterline and which
|
|
// stretch of shore does it belong to"; the region partitioner dilates the land mask to decide which
|
|
// landmasses are close enough to be solved together; and the template classifier dissolves the decorative
|
|
// stroke an artist drew by handing each of its pixels to the nearest pixel that means something.
|
|
//
|
|
// Exact, not a chamfer approximation: Felzenszwalb and 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 wider along the grid axes than across them.
|
|
package dt
|
|
|
|
import (
|
|
"math"
|
|
|
|
"salty/terrain/internal/field"
|
|
)
|
|
|
|
// Transform returns, for every cell, the squared distance in cells to the nearest seed cell and the flat
|
|
// 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.
|
|
//
|
|
// With wrapX the row pass is periodic, so the left and right edges of the grid are neighbours. That is what
|
|
// a planet needs: a landmass straddling the seam is one landmass, and the shelf in front of it is one shelf.
|
|
//
|
|
// 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 grid has no seeds
|
|
// anywhere, in which case every near index comes back -1.
|
|
func Transform(seed []bool, w, h int, wrapX bool) (d2 []float32, near []int32) {
|
|
return transform(seed, w, h, wrapX, true)
|
|
}
|
|
|
|
// Distance2 is Transform without the feature index, for a caller that only wants "how far".
|
|
//
|
|
// It is a separate entry point rather than a nil argument because the saving is the point: at planet scale
|
|
// the index and the column scratch it needs are two more arrays of four bytes a cell, which is most of a
|
|
// gigabyte for an answer nobody reads. The region partitioner only asks whether a cell is within a margin
|
|
// of land.
|
|
func Distance2(seed []bool, w, h int, wrapX bool) []float32 {
|
|
d2, _ := transform(seed, w, h, wrapX, false)
|
|
return d2
|
|
}
|
|
|
|
func transform(seed []bool, w, h int, wrapX, wantNear bool) (d2 []float32, near []int32) {
|
|
d2 = make([]float32, w*h)
|
|
if wantNear {
|
|
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
|
|
var colN []int32 // that seed's row, or -1; only needed for the feature index
|
|
if wantNear {
|
|
colN = make([]int32, w*h)
|
|
}
|
|
|
|
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] = bigD
|
|
if wantNear {
|
|
colN[i] = -1
|
|
}
|
|
} else {
|
|
colD[i] = float32(y - best)
|
|
if wantNear {
|
|
colN[i] = 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] = d
|
|
if wantNear {
|
|
colN[i] = int32(best)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
// The row pass. On a cylinder the row is laid out three times - one turn to the left, the row itself,
|
|
// one turn to the right - and the answer is read out of the middle copy. From a cell in the middle copy
|
|
// the three images of any column sit at offsets d, d-w and d+w, whose smallest absolute value is the
|
|
// cyclic distance, so the envelope returns exactly the wrapped answer with no special cases in it.
|
|
span := w
|
|
off := 0
|
|
if wrapX {
|
|
span = 3 * w
|
|
off = w
|
|
}
|
|
field.Rows(h, func(y0, y1 int) {
|
|
f := make([]float64, span)
|
|
v := make([]int, span)
|
|
z := make([]float64, span+1)
|
|
for y := y0; y < y1; y++ {
|
|
row := y * w
|
|
for j := 0; j < span; j++ {
|
|
d := float64(colD[row+srcX(j, off, w)])
|
|
f[j] = d * d
|
|
}
|
|
k := 0
|
|
v[0] = 0
|
|
z[0] = math.Inf(-1)
|
|
z[1] = math.Inf(1)
|
|
for q := 1; q < span; 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 < span; q++ {
|
|
for z[k+1] < float64(q) {
|
|
k++
|
|
}
|
|
if q < off || q >= off+w {
|
|
continue // a replica column; only the middle copy is the answer
|
|
}
|
|
dx := float64(q - v[k])
|
|
o := row + q - off
|
|
d2[o] = float32(dx*dx + f[v[k]])
|
|
if !wantNear {
|
|
continue
|
|
}
|
|
sx := srcX(v[k], off, w)
|
|
if n := colN[row+sx]; n < 0 {
|
|
near[o] = -1
|
|
} else {
|
|
near[o] = n*int32(w) + int32(sx)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
return d2, near
|
|
}
|
|
|
|
// srcX maps a column of the (possibly replicated) row back to a real column.
|
|
func srcX(j, off, w int) int {
|
|
x := j - off
|
|
for x < 0 {
|
|
x += w
|
|
}
|
|
for x >= w {
|
|
x -= w
|
|
}
|
|
return x
|
|
}
|
|
|
|
// 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)
|
|
}
|