// 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) }