336 lines
10 KiB
Go
336 lines
10 KiB
Go
package overlay
|
|
|
|
import (
|
|
"math"
|
|
"sort"
|
|
)
|
|
|
|
// Roads: the least-cost paths between the settlements that were just placed.
|
|
//
|
|
// A road is the one mark whose shape is not a judgement at all. Given where two towns are, the line between
|
|
// them is whatever the ground allows - up the valley, round the spur, across the saddle - and that is a
|
|
// shortest-path problem with a cost function, not a drawing. It is also the single most tedious thing to
|
|
// paint by hand, because getting it right means reading a heightmap pixel by pixel.
|
|
//
|
|
// Three decisions worth stating:
|
|
//
|
|
// - **Water is impassable, so roads never swim.** Each landmass gets its own network. A bridge or a ferry
|
|
// is a deliberate act and belongs to the author, and a generator that guessed at them would put a
|
|
// motorway across a strait it has no idea is thirty kilometres wide.
|
|
// - **A minimum spanning tree, not every pair.** Joining all pairs gives a cobweb; the tree gives exactly
|
|
// enough road to reach everywhere, which is both what a road network minimally is and the thing an
|
|
// author can most easily add to. Edges are weighted by path *cost*, not by straight-line distance, so
|
|
// two towns either side of a range are correctly further apart than the map says.
|
|
// - **It runs on a coarsened grid.** A road at the overlay's full resolution would be a Dijkstra over
|
|
// twenty-nine million cells per settlement. The cost surface is smooth at the scale a road cares about,
|
|
// so it is pooled to a few hundred cells across, solved there, and the resulting polyline is stamped
|
|
// back at full resolution with the mark's real width.
|
|
|
|
// roadGrid is the coarsened cost surface the paths are solved on.
|
|
type roadGrid struct {
|
|
w, h int
|
|
step int // overlay pixels per coarse cell
|
|
cost []float32 // per coarse cell, +Inf where impassable
|
|
scale float64 // overlay pixels per coarse cell, as a float
|
|
}
|
|
|
|
func buildRoadGrid(d *genData, maxSlopeDeg float64) *roadGrid {
|
|
in := d.in
|
|
// About six hundred cells around the world: fine enough that a coarse cell is well under a kilometre on
|
|
// any world this tool makes, coarse enough that fifty Dijkstras are a second's work.
|
|
step := int(math.Max(1, math.Round(float64(in.W)/600)))
|
|
gw := (in.W + step - 1) / step
|
|
gh := (in.H + step - 1) / step
|
|
g := &roadGrid{w: gw, h: gh, step: step, scale: float64(step), cost: make([]float32, gw*gh)}
|
|
|
|
inf := float32(math.Inf(1))
|
|
for gy := 0; gy < gh; gy++ {
|
|
for gx := 0; gx < gw; gx++ {
|
|
// Pool the block: any sea in it makes the cell water, because a road that clips a bay is a road
|
|
// in the sea. The slope taken is the worst in the block, for the same reason.
|
|
var worst float64
|
|
wet := false
|
|
for y := gy * step; y < (gy+1)*step && y < in.H; y++ {
|
|
for x := gx * step; x < (gx+1)*step && x < in.W; x++ {
|
|
i := y*in.W + x
|
|
if in.Sea[i] {
|
|
wet = true
|
|
break
|
|
}
|
|
if s := float64(d.slopeDeg[i]); s > worst {
|
|
worst = s
|
|
}
|
|
}
|
|
if wet {
|
|
break
|
|
}
|
|
}
|
|
gi := gy*gw + gx
|
|
switch {
|
|
case wet:
|
|
g.cost[gi] = inf
|
|
case worst > maxSlopeDeg:
|
|
g.cost[gi] = inf
|
|
default:
|
|
// Slope is what a road pays for. Quadratic rather than linear so that a route prefers a long
|
|
// gentle way round to a short steep one, which is what a real road does.
|
|
t := worst / math.Max(maxSlopeDeg, 1e-6)
|
|
g.cost[gi] = float32(1 + 12*t*t)
|
|
}
|
|
}
|
|
}
|
|
return g
|
|
}
|
|
|
|
func (g *roadGrid) idx(x, y int) int { return y*g.w + x }
|
|
|
|
// dijkstra returns the cost to every reachable coarse cell from a source, and the predecessor chain to walk
|
|
// a path back. A binary heap over a few hundred thousand cells; the graph is eight-connected and X wraps.
|
|
func (g *roadGrid) dijkstra(src int) (cost []float32, pred []int32) {
|
|
n := g.w * g.h
|
|
cost = make([]float32, n)
|
|
pred = make([]int32, n)
|
|
inf := float32(math.Inf(1))
|
|
for i := range cost {
|
|
cost[i] = inf
|
|
pred[i] = -1
|
|
}
|
|
if math.IsInf(float64(g.cost[src]), 1) {
|
|
return cost, pred
|
|
}
|
|
cost[src] = 0
|
|
h := &costHeap{keys: []float32{0}, items: []int32{int32(src)}}
|
|
for h.Len() > 0 {
|
|
c := int(h.pop())
|
|
cx, cy := c%g.w, c/g.w
|
|
base := cost[c]
|
|
for _, o := range neighbours8 {
|
|
nx := (cx + o[0] + g.w) % g.w
|
|
ny := cy + o[1]
|
|
if ny < 0 || ny >= g.h {
|
|
continue
|
|
}
|
|
n := g.idx(nx, ny)
|
|
cc := g.cost[n]
|
|
if math.IsInf(float64(cc), 1) {
|
|
continue
|
|
}
|
|
// Diagonal steps cost their real length, or the network shows a bias along the axes.
|
|
step := float32(1.0)
|
|
if o[0] != 0 && o[1] != 0 {
|
|
step = float32(math.Sqrt2)
|
|
}
|
|
next := base + cc*step
|
|
if next < cost[n] {
|
|
cost[n] = next
|
|
pred[n] = int32(c)
|
|
h.push(int32(n), next)
|
|
}
|
|
}
|
|
}
|
|
return cost, pred
|
|
}
|
|
|
|
// costHeap is a binary min-heap of coarse cells. Lazy deletion is not needed because a cell is only pushed
|
|
// when its cost strictly improves, and a stale entry pops with a cost no better than the settled one.
|
|
type costHeap struct {
|
|
keys []float32
|
|
items []int32
|
|
}
|
|
|
|
func (h *costHeap) Len() int { return len(h.items) }
|
|
|
|
func (h *costHeap) push(item int32, key float32) {
|
|
h.keys = append(h.keys, key)
|
|
h.items = append(h.items, item)
|
|
i := len(h.items) - 1
|
|
for i > 0 {
|
|
p := (i - 1) / 2
|
|
if h.keys[p] <= h.keys[i] {
|
|
break
|
|
}
|
|
h.keys[p], h.keys[i] = h.keys[i], h.keys[p]
|
|
h.items[p], h.items[i] = h.items[i], h.items[p]
|
|
i = p
|
|
}
|
|
}
|
|
|
|
func (h *costHeap) pop() int32 {
|
|
top := h.items[0]
|
|
last := len(h.items) - 1
|
|
h.keys[0], h.items[0] = h.keys[last], h.items[last]
|
|
h.keys = h.keys[:last]
|
|
h.items = h.items[:last]
|
|
i := 0
|
|
for {
|
|
l := 2*i + 1
|
|
if l >= last {
|
|
break
|
|
}
|
|
if r := l + 1; r < last && h.keys[r] < h.keys[l] {
|
|
l = r
|
|
}
|
|
if h.keys[l] >= h.keys[i] {
|
|
break
|
|
}
|
|
h.keys[l], h.keys[i] = h.keys[i], h.keys[l]
|
|
h.items[l], h.items[i] = h.items[i], h.items[l]
|
|
i = l
|
|
}
|
|
return top
|
|
}
|
|
|
|
// paintRoads traces a spanning tree over the settlements of each landmass and stamps it.
|
|
func (l *Legend) paintRoads(m *Mark, d *genData, out *Raster, idx uint8, placed []Placed) (int, int) {
|
|
if len(placed) < 2 {
|
|
return 0, 0
|
|
}
|
|
g := m.Generate
|
|
maxSlope := g.MaxSlopeDeg
|
|
if maxSlope <= 0 {
|
|
maxSlope = 22
|
|
}
|
|
widthM := g.WidthM
|
|
if widthM <= 0 {
|
|
widthM = m.WidthM
|
|
}
|
|
if widthM <= 0 {
|
|
widthM = 8
|
|
}
|
|
// A road eight metres wide is less than one overlay pixel at 12.9 m, and a mark thinner than a pixel is
|
|
// not a mark. It is painted at least one pixel wide and the true width travels in the legend, which is
|
|
// exactly how `width_m` is meant to be read.
|
|
halfPx := int(math.Max(0, math.Round(widthM/d.in.CellM/2)))
|
|
|
|
rg := buildRoadGrid(d, maxSlope)
|
|
|
|
// Settlements grouped by landmass: a spanning tree per island, never between islands.
|
|
byRegion := map[int][]int{}
|
|
for i, p := range placed {
|
|
if p.Region < 0 {
|
|
continue
|
|
}
|
|
byRegion[p.Region] = append(byRegion[p.Region], i)
|
|
}
|
|
regions := make([]int, 0, len(byRegion))
|
|
for r := range byRegion {
|
|
regions = append(regions, r)
|
|
}
|
|
sort.Ints(regions) // deterministic order, cross-cutting rule 12
|
|
|
|
total, pieces := 0, 0
|
|
for _, r := range regions {
|
|
members := byRegion[r]
|
|
if len(members) < 2 {
|
|
continue
|
|
}
|
|
total += l.connectRegion(rg, d, out, idx, placed, members, halfPx, &pieces)
|
|
}
|
|
return total, pieces
|
|
}
|
|
|
|
// connectRegion solves the paths among one landmass's settlements and stamps its spanning tree.
|
|
func (l *Legend) connectRegion(rg *roadGrid, d *genData, out *Raster, idx uint8,
|
|
placed []Placed, members []int, halfPx int, pieces *int) int {
|
|
|
|
n := len(members)
|
|
src := make([]int, n)
|
|
for k, pi := range members {
|
|
p := placed[pi]
|
|
gx := (p.X / rg.step) % rg.w
|
|
gy := p.Y / rg.step
|
|
if gy >= rg.h {
|
|
gy = rg.h - 1
|
|
}
|
|
src[k] = rg.idx(gx, gy)
|
|
}
|
|
|
|
// One Dijkstra per settlement, kept: the coarse grid is a few hundred thousand cells and a landmass has
|
|
// a handful of towns, so holding the predecessor chains costs a few megabytes and saves solving twice.
|
|
costs := make([][]float32, n)
|
|
preds := make([][]int32, n)
|
|
for k := range members {
|
|
costs[k], preds[k] = rg.dijkstra(src[k])
|
|
}
|
|
|
|
// Prim's, on path cost. Unreachable pairs are skipped, so a landmass whose towns are separated by ground
|
|
// too steep for a road comes out as two networks rather than one impossible line.
|
|
inTree := make([]bool, n)
|
|
inTree[0] = true
|
|
painted := 0
|
|
for added := 1; added < n; added++ {
|
|
bestA, bestB := -1, -1
|
|
best := float32(math.Inf(1))
|
|
for a := 0; a < n; a++ {
|
|
if !inTree[a] {
|
|
continue
|
|
}
|
|
for b := 0; b < n; b++ {
|
|
if inTree[b] {
|
|
continue
|
|
}
|
|
if c := costs[a][src[b]]; c < best {
|
|
best, bestA, bestB = c, a, b
|
|
}
|
|
}
|
|
}
|
|
if bestA < 0 || math.IsInf(float64(best), 1) {
|
|
break // nothing else on this landmass is reachable by road
|
|
}
|
|
inTree[bestB] = true
|
|
painted += stampPath(rg, preds[bestA], src[bestA], src[bestB], d, out, idx, halfPx)
|
|
*pieces++
|
|
}
|
|
return painted
|
|
}
|
|
|
|
// stampPath walks the predecessor chain back from dst to src and paints it at full resolution.
|
|
func stampPath(rg *roadGrid, pred []int32, src, dst int, d *genData, out *Raster, idx uint8, halfPx int) int {
|
|
var chain []int
|
|
for c := dst; c >= 0; {
|
|
chain = append(chain, c)
|
|
if c == src {
|
|
break
|
|
}
|
|
p := pred[c]
|
|
if p < 0 {
|
|
return 0 // no route; leave the ground unpainted rather than drawing a guess
|
|
}
|
|
c = int(p)
|
|
}
|
|
painted := 0
|
|
for k := 0; k+1 < len(chain); k++ {
|
|
ax, ay := coarseCentre(rg, chain[k])
|
|
bx, by := coarseCentre(rg, chain[k+1])
|
|
painted += stampSegment(out, d, ax, ay, bx, by, halfPx, idx)
|
|
}
|
|
return painted
|
|
}
|
|
|
|
func coarseCentre(rg *roadGrid, c int) (int, int) {
|
|
gx, gy := c%rg.w, c/rg.w
|
|
return gx*rg.step + rg.step/2, gy*rg.step + rg.step/2
|
|
}
|
|
|
|
// stampSegment draws one straight run between two coarse-cell centres, wrapping in X the short way so a road
|
|
// crossing the seam is one road rather than a line back across the whole map.
|
|
func stampSegment(out *Raster, d *genData, ax, ay, bx, by, halfPx int, idx uint8) int {
|
|
dx := wrapDelta(bx-ax, out.W)
|
|
dy := by - ay
|
|
steps := int(math.Max(math.Abs(float64(dx)), math.Abs(float64(dy))))
|
|
if steps == 0 {
|
|
return stampDisc(out, d, ax, ay, halfPx, idx, true)
|
|
}
|
|
painted := 0
|
|
for s := 0; s <= steps; s++ {
|
|
t := float64(s) / float64(steps)
|
|
x := ax + int(math.Round(float64(dx)*t))
|
|
y := ay + int(math.Round(float64(dy)*t))
|
|
if y < 0 || y >= out.H {
|
|
continue
|
|
}
|
|
painted += stampDisc(out, d, x, y, halfPx, idx, true)
|
|
}
|
|
return painted
|
|
}
|