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

140 lines
5.1 KiB
Go

package planet
import (
"math"
"os"
"path/filepath"
"salty/terrain/internal/field"
"salty/terrain/internal/plates"
)
// The tectonic map: which plate every place belongs to, and what is happening where two of them meet.
//
// It earns its place the same way map_uplift does. Every belt, every fault and every basin this model
// produces is a consequence of one line and what is happening across it, so when a range comes out in the
// wrong place the question is always "what did the boundary there do", and this is the only picture that
// answers it. The lines are drawn by *kind* rather than by rate, for the reason drawFaults gives about
// stroking traces: a collision belt is tens of kilometres wide, a map of a whole planet is a few thousand
// pixels, and the thing an author needs from it is where the margins are and which ones are closing.
// plateInk is the colour of each kind of margin. They are picked to be distinguishable from each other and
// from the plate fills underneath, which are pastel by construction so that these read on top of them.
var plateInk = map[plates.Kind][3]uint8{
plates.Collision: {255, 64, 48}, // red: two continents, the thing that makes mountains
plates.Subduction: {255, 156, 32}, // orange: an ocean going under
plates.Rift: {96, 240, 120}, // green: a continent pulling apart
plates.Ridge: {72, 196, 255}, // blue: new ocean floor
plates.Transform: {236, 232, 128}, // yellow: sliding, neither up nor down
}
// WritePlateMap draws the tectonic model over the painted land.
func WritePlateMap(dir string, in *Inputs, width int) error {
m := in.Plates
if m == nil {
return nil
}
// A hue per plate, walked round the wheel by a step coprime-ish with the count so that neighbouring ids
// are not neighbouring hues - plates 3 and 4 are usually adjacent on the map, and two greens meeting
// would hide the very boundary this map exists to show.
n := len(m.Plates)
hue := make([]float64, n)
for i := range hue {
hue[i] = math.Mod(float64(i)*0.61803398875, 1)
}
px, w, h := renderRGB(in, width, func(i, img int) [3]uint8 {
id := m.PlateAt(in.P.XM(i%in.P.W), in.P.YM(i/in.P.W))
if id < 0 || id >= n {
return water
}
// Land is the plate's hue at full strength and sea is the same hue dimmed, so the painting stays
// legible underneath: a margin is only interesting relative to where the coasts are.
sat, val := 0.55, 0.78
if in.Map.Sea[i] {
sat, val = 0.38, 0.34
}
// A continental plate is warmer than an oceanic one at the same hue, because which of the two a
// plate is decides what every convergent margin around it does.
if !m.Plates[id].Continental {
sat *= 0.5
}
c := field.HSV(hue[id]*360, sat, val)
return [3]uint8{clamp8(c[0]), clamp8(c[1]), clamp8(c[2])}
})
drawBoundaries(m, in, px, w, h)
return write(filepath.Join(dir, "map_plates.png"), px, w, h)
}
// WritePlateProposal writes a tectonic layer and its legend for an author to open and edit.
//
// A blank canvas is the wrong place to start this. Seven plates with plausible motions is a second's work for
// the generator and an afternoon's by hand, and what an author actually wants to do is move two of them and
// change a heading - which is editing. The pair it writes is exactly what `planet.plates.layer` and
// `planet.plates.legend` take, so adopting it is two lines in the manifest.
func WritePlateProposal(dir string, in *Inputs, width int) error {
if in.Plates == nil {
return nil
}
px, w, h, lg := in.Plates.Propose(width)
const name = "plates_proposal"
if err := write(filepath.Join(dir, name+".png"), px, w, h); err != nil {
return err
}
lg.Image = name + ".png"
data, err := plates.MarshalLegend(lg)
if err != nil {
return err
}
return os.WriteFile(filepath.Join(dir, name+".json"), data, 0o644)
}
// boundaryWidthMYr is the closing rate, in metres a year, at which a margin is drawn at its full width. A
// margin at a tenth of it is still one pixel, so a slow boundary is visible without a fast one being a blot.
const boundaryWidthMYr = 0.08
// drawBoundaries strokes every margin, coloured by what it is doing and thickened by how fast.
func drawBoundaries(m *plates.Model, in *Inputs, px []uint8, w, h int) {
p := in.P
sx := float64(w) / p.CircumferenceM()
sy := float64(h) / p.HeightM()
set := func(x, y int, c [3]uint8) {
if y < 0 || y >= h {
return
}
x = ((x % w) + w) % w // X wraps, because the boundaries do
o := (y*w + x) * 3
px[o], px[o+1], px[o+2] = c[0], c[1], c[2]
}
disc := func(x, y, r int, c [3]uint8) {
for dy := -r; dy <= r; dy++ {
for dx := -r; dx <= r; dx++ {
if dx*dx+dy*dy <= r*r {
set(x+dx, y+dy, c)
}
}
}
}
for _, b := range m.Boundaries {
for j := 0; j+1 < len(b.V); j++ {
v := b.V[j]
ink := plateInk[v.Kind]
r := int(math.Abs(v.ClosingMYr)/boundaryWidthMYr*2 + 0.5)
if r > 2 {
r = 2
}
ax, ay := v.XM*sx, v.YM*sy
bx, by := b.V[j+1].XM*sx, b.V[j+1].YM*sy
steps := int(math.Hypot(bx-ax, by-ay)) + 1
for k := 0; k <= steps; k++ {
t := float64(k) / float64(steps)
disc(int(ax+(bx-ax)*t), int(ay+(by-ay)*t), r, ink)
}
}
}
}