237 lines
7.7 KiB
Go
237 lines
7.7 KiB
Go
package field
|
|
|
|
// SlidingMax is the maximum over a square window, separable and O(1) a cell whatever the radius.
|
|
//
|
|
// The naive form is a loop over the window, which is what internal/stats' localRelief used to do and is fine
|
|
// on the 500 m window it uses at 8 m cells - until the map is a planet. 28 million land cells times a 63-cell
|
|
// radius is 1.1e11 comparisons, which is not a slow diagnostic, it is one nobody will ever see the end of.
|
|
// The monotonic deque is the standard answer: each index enters and leaves once, so the row pass is linear in
|
|
// the row however wide the window.
|
|
//
|
|
// wrapX makes the row pass periodic, which is what a cylinder needs; the column pass always clamps, because
|
|
// the top and bottom of the map are the poles and not each other.
|
|
func SlidingMax(f *Field, radius int, wrapX bool) *Field {
|
|
return sliding(f, radius, wrapX, func(inDeque, arriving float32) bool { return inDeque <= arriving })
|
|
}
|
|
|
|
// SlidingMin is the same window, the other way up. The pair is what local relief is made of.
|
|
func SlidingMin(f *Field, radius int, wrapX bool) *Field {
|
|
return sliding(f, radius, wrapX, func(inDeque, arriving float32) bool { return inDeque >= arriving })
|
|
}
|
|
|
|
// LocalRelief is max minus min over a square window: the standard field measure of how rugged a place is, and
|
|
// the one thing slope cannot tell you. A 5 m hummock and a 500 m mountainside both stand at 30 degrees.
|
|
//
|
|
// Two sliding passes and a subtract, so it costs the same as one of them twice and nothing per radius. It
|
|
// holds two fields at once at the peak, which at planet scale is 600 MB - worth saying, because the naive
|
|
// version held none and could not finish.
|
|
func LocalRelief(f *Field, radius int, wrapX bool) *Field {
|
|
hi := SlidingMax(f, radius, wrapX)
|
|
lo := SlidingMin(f, radius, wrapX)
|
|
for i := range hi.Data {
|
|
hi.Data[i] -= lo.Data[i]
|
|
}
|
|
return hi
|
|
}
|
|
|
|
// sliding is the shared separable pass. keep reports whether the value already at the back of the deque can
|
|
// be dropped when a new one arrives, which is the only thing that differs between the maximum and the
|
|
// minimum: the deque holds indices whose values are monotone, so its front is always the answer for the live
|
|
// window and anything the arriving value dominates can never be the answer again.
|
|
func sliding(f *Field, radius int, wrapX bool, keep func(inDeque, arriving float32) bool) *Field {
|
|
if radius < 1 {
|
|
return f.Clone()
|
|
}
|
|
w, h := f.W, f.H
|
|
row := New(w, h, f.CellM)
|
|
buf := make([]float32, 0, w+2*radius)
|
|
idx := make([]int, 0, w+2*radius)
|
|
|
|
for y := 0; y < h; y++ {
|
|
// The row, extended by the radius at each end so the deque never has to special-case an edge.
|
|
buf = buf[:0]
|
|
for x := -radius; x < w+radius; x++ {
|
|
sx := x
|
|
if wrapX {
|
|
sx = ((sx % w) + w) % w
|
|
} else if sx < 0 {
|
|
sx = 0
|
|
} else if sx >= w {
|
|
sx = w - 1
|
|
}
|
|
buf = append(buf, f.Data[y*w+sx])
|
|
}
|
|
slide(buf, idx[:0], 2*radius+1, keep, func(i int, v float32) {
|
|
if i < w {
|
|
row.Data[y*w+i] = v
|
|
}
|
|
})
|
|
}
|
|
|
|
out := New(w, h, f.CellM)
|
|
col := make([]float32, 0, h+2*radius)
|
|
for x := 0; x < w; x++ {
|
|
col = col[:0]
|
|
for y := -radius; y < h+radius; y++ {
|
|
sy := y
|
|
if sy < 0 {
|
|
sy = 0
|
|
} else if sy >= h {
|
|
sy = h - 1
|
|
}
|
|
col = append(col, row.Data[sy*w+x])
|
|
}
|
|
slide(col, idx[:0], 2*radius+1, keep, func(i int, v float32) {
|
|
if i < h {
|
|
out.Data[i*w+x] = v
|
|
}
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// slide walks a padded line with a monotonic deque and reports the window's answer ending at each output
|
|
// position.
|
|
func slide(line []float32, dq []int, window int, keep func(inDeque, arriving float32) bool,
|
|
emit func(i int, v float32)) {
|
|
|
|
dq = dq[:0]
|
|
for i, v := range line {
|
|
for len(dq) > 0 && keep(line[dq[len(dq)-1]], v) {
|
|
dq = dq[:len(dq)-1]
|
|
}
|
|
dq = append(dq, i)
|
|
if dq[0] <= i-window {
|
|
dq = dq[1:]
|
|
}
|
|
if out := i - window + 1; out >= 0 {
|
|
emit(out, line[dq[0]])
|
|
}
|
|
}
|
|
}
|
|
|
|
// BoxSmooth blurs a field in place with `passes` of a separable box blur of the given radius, clamping at the
|
|
// edges. Two passes are near enough to a Gaussian for anything here and cost four linear sweeps.
|
|
//
|
|
// Deterministic by construction: fixed traversal order, running sums, no goroutines. It lives here rather than
|
|
// in the pass that first wanted it because two now do - the coastal detail pass smooths the signed distance to
|
|
// the shoreline, and the tile bake smooths the interpolated sea floor.
|
|
func BoxSmooth(data []float32, w, h, radius, passes int) {
|
|
if radius < 1 || passes < 1 || len(data) < w*h {
|
|
return
|
|
}
|
|
tmp := make([]float32, len(data))
|
|
for p := 0; p < passes; p++ {
|
|
boxRows(data, tmp, w, h, radius)
|
|
boxCols(tmp, data, w, h, radius)
|
|
}
|
|
}
|
|
|
|
func boxRows(src, dst []float32, w, h, radius int) {
|
|
n := float32(2*radius + 1)
|
|
for y := 0; y < h; y++ {
|
|
row := y * w
|
|
var sum float32
|
|
for k := -radius; k <= radius; k++ {
|
|
sum += src[row+clampIdx(k, w)]
|
|
}
|
|
for x := 0; x < w; x++ {
|
|
dst[row+x] = sum / n
|
|
sum += src[row+clampIdx(x+radius+1, w)] - src[row+clampIdx(x-radius, w)]
|
|
}
|
|
}
|
|
}
|
|
|
|
func boxCols(src, dst []float32, w, h, radius int) {
|
|
n := float32(2*radius + 1)
|
|
for x := 0; x < w; x++ {
|
|
var sum float32
|
|
for k := -radius; k <= radius; k++ {
|
|
sum += src[clampIdx(k, h)*w+x]
|
|
}
|
|
for y := 0; y < h; y++ {
|
|
dst[y*w+x] = sum / n
|
|
sum += src[clampIdx(y+radius+1, h)*w+x] - src[clampIdx(y-radius, h)*w+x]
|
|
}
|
|
}
|
|
}
|
|
|
|
func clampIdx(i, n int) int {
|
|
if i < 0 {
|
|
return 0
|
|
}
|
|
if i >= n {
|
|
return n - 1
|
|
}
|
|
return i
|
|
}
|
|
|
|
// BoxSmoothMasked is BoxSmooth restricted to the cells the mask selects: a cell outside it is neither read
|
|
// nor written, so the blur never averages across the boundary.
|
|
//
|
|
// That distinction is the whole reason it exists. The coastal detail pass damps the metre-scale texture near
|
|
// the shore, and an unmasked blur there does not damp texture, it bridges the waterline: measured on a
|
|
// fixture with forty metres of water against the land, the plain blur lifted the sea floor by twenty metres.
|
|
// The step at a shoreline is a landform, not roughness, and a filter that cannot tell them apart is the wrong
|
|
// filter.
|
|
//
|
|
// Separable and weighted: the row pass carries a running sum of values and of weights, the column pass sums
|
|
// those, and the quotient is the mean over the masked cells in the window. Deterministic, like BoxSmooth.
|
|
func BoxSmoothMasked(data []float32, mask []bool, w, h, radius, passes int) {
|
|
if radius < 1 || passes < 1 || len(data) < w*h || len(mask) < w*h {
|
|
return
|
|
}
|
|
n := w * h
|
|
val := make([]float32, n)
|
|
wgt := make([]float32, n)
|
|
tv := make([]float32, n)
|
|
tw := make([]float32, n)
|
|
for p := 0; p < passes; p++ {
|
|
for i := 0; i < n; i++ {
|
|
if mask[i] {
|
|
val[i], wgt[i] = data[i], 1
|
|
} else {
|
|
val[i], wgt[i] = 0, 0
|
|
}
|
|
}
|
|
boxRowsSum(val, tv, w, h, radius)
|
|
boxRowsSum(wgt, tw, w, h, radius)
|
|
boxColsSum(tv, val, w, h, radius)
|
|
boxColsSum(tw, wgt, w, h, radius)
|
|
for i := 0; i < n; i++ {
|
|
if mask[i] && wgt[i] > 0 {
|
|
data[i] = val[i] / wgt[i]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// boxRowsSum and boxColsSum are the running sums BoxSmooth uses, without the division: a masked blur needs
|
|
// the weight sum as well as the value sum, and dividing in the middle would be dividing by the wrong thing.
|
|
func boxRowsSum(src, dst []float32, w, h, radius int) {
|
|
for y := 0; y < h; y++ {
|
|
row := y * w
|
|
var sum float32
|
|
for k := -radius; k <= radius; k++ {
|
|
sum += src[row+clampIdx(k, w)]
|
|
}
|
|
for x := 0; x < w; x++ {
|
|
dst[row+x] = sum
|
|
sum += src[row+clampIdx(x+radius+1, w)] - src[row+clampIdx(x-radius, w)]
|
|
}
|
|
}
|
|
}
|
|
|
|
func boxColsSum(src, dst []float32, w, h, radius int) {
|
|
for x := 0; x < w; x++ {
|
|
var sum float32
|
|
for k := -radius; k <= radius; k++ {
|
|
sum += src[clampIdx(k, h)*w+x]
|
|
}
|
|
for y := 0; y < h; y++ {
|
|
dst[y*w+x] = sum
|
|
sum += src[clampIdx(y+radius+1, h)*w+x] - src[clampIdx(y-radius, h)*w+x]
|
|
}
|
|
}
|
|
}
|