Files
UnrealPrototyping/Tools/Terrain/internal/overlay/generate_roads.go
T
2026-09-25 17:02:24 +03:00

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
}