237 lines
7.0 KiB
Go
237 lines
7.0 KiB
Go
package template
|
|
|
|
import (
|
|
"salty/terrain/internal/field"
|
|
"salty/terrain/internal/world"
|
|
)
|
|
|
|
// Map is a classified template projected onto a planet grid: one legend index per planet cell, including
|
|
// the polar pad.
|
|
type Map struct {
|
|
P world.Planet
|
|
L *Legend
|
|
Class []uint8
|
|
Sea []bool
|
|
}
|
|
|
|
// Project resamples a paint-resolution raster onto the planet grid by nearest neighbour, and fills the
|
|
// polar pad with padClass.
|
|
//
|
|
// Nearest neighbour is not a shortcut, it is the only correct choice: a class index is a name, not a
|
|
// quantity, and interpolating between "desert" and "ocean" would invent a class that is neither. The blend
|
|
// rule in Docs/Terrain-Next.md 3.2 - the painted map owns the wavelengths above its pixel size and noise
|
|
// owns those below - is honoured downstream, where the continuous fields the classes stand for are smoothed
|
|
// and then given sub-pixel variation. Doing it here instead would smear the coastline, which is the one
|
|
// thing in the whole template an author draws deliberately.
|
|
func (r *Raster) Project(p world.Planet, l *Legend, padClass int) *Map {
|
|
m := &Map{P: p, L: l, Class: make([]uint8, p.W*p.H), Sea: make([]bool, p.W*p.H)}
|
|
|
|
sea := make([]bool, len(l.Classes))
|
|
for i := range l.Classes {
|
|
sea[i] = l.Classes[i].Sea
|
|
}
|
|
|
|
pad := uint8(padClass)
|
|
paintH := p.PaintH()
|
|
field.Rows(p.H, func(y0, y1 int) {
|
|
for y := y0; y < y1; y++ {
|
|
if p.InPad(y) {
|
|
for x := 0; x < p.W; x++ {
|
|
i := y*p.W + x
|
|
m.Class[i] = pad
|
|
m.Sea[i] = sea[pad]
|
|
}
|
|
continue
|
|
}
|
|
// Sample at the cell's centre, so a run of planet cells maps evenly across the paint rather
|
|
// than favouring its left edge.
|
|
py := (2*(y-p.PadY) + 1) * r.H / (2 * paintH)
|
|
if py >= r.H {
|
|
py = r.H - 1
|
|
}
|
|
for x := 0; x < p.W; x++ {
|
|
px := (2*x + 1) * r.W / (2 * p.W)
|
|
if px >= r.W {
|
|
px = r.W - 1
|
|
}
|
|
i := y*p.W + x
|
|
c := r.Class[py*r.W+px]
|
|
m.Class[i] = c
|
|
m.Sea[i] = sea[c]
|
|
}
|
|
}
|
|
})
|
|
return m
|
|
}
|
|
|
|
// Counts is how many planet cells each class covers, and how many of them are land. The pad is excluded,
|
|
// because it is not part of anybody's world.
|
|
func (m *Map) Counts() (perClass []int, land, total int) {
|
|
perClass = make([]int, len(m.L.Classes))
|
|
for y := m.P.PadY; y < m.P.H-m.P.PadY; y++ {
|
|
for x := 0; x < m.P.W; x++ {
|
|
i := y*m.P.W + x
|
|
perClass[m.Class[i]]++
|
|
total++
|
|
if !m.Sea[i] {
|
|
land++
|
|
}
|
|
}
|
|
}
|
|
return perClass, land, total
|
|
}
|
|
|
|
// Rates is the uplift rate in metres a year for every class, indexed by class. Sea classes are zero: the
|
|
// solve holds an ocean cell at base level for its whole run and never reads the rate there.
|
|
func (l *Legend) Rates() []float32 {
|
|
out := make([]float32, len(l.Classes))
|
|
for i := range l.Classes {
|
|
if l.Classes[i].Land() {
|
|
out[i] = float32(l.Classes[i].RateMYr())
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Erodibilities is the multiplier on stream-power K for every class. Sea classes get 1 rather than 0, so
|
|
// that a field built from this never carries a zero into a division.
|
|
func (l *Legend) Erodibilities() []float32 {
|
|
out := make([]float32, len(l.Classes))
|
|
for i := range l.Classes {
|
|
out[i] = 1
|
|
if l.Classes[i].Land() {
|
|
out[i] = float32(l.Classes[i].K())
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// CoastalPlains is, per class, how far inland the rate ramps up to its full value, in metres, and the rate
|
|
// it starts from at the waterline.
|
|
func (l *Legend) CoastalPlains() (plainM []float64, floor []float32) {
|
|
plainM = make([]float64, len(l.Classes))
|
|
floor = make([]float32, len(l.Classes))
|
|
for i := range l.Classes {
|
|
c := l.Classes[i]
|
|
if c.Land() && c.CoastalPlainKm > 0 {
|
|
plainM[i] = c.CoastalPlainKm * 1000
|
|
floor[i] = float32(c.PlainFloorMYr())
|
|
}
|
|
}
|
|
return plainM, floor
|
|
}
|
|
|
|
// Massifs is, per class, the plain's uplift rate in metres a year and the share of the class that stands
|
|
// above the midpoint between that floor and the class rate. A class with no massif reports a zero fraction,
|
|
// which is what internal/uplift reads as "one rate all over", and its floor is then its own rate.
|
|
func (l *Legend) Massifs() (floor []float32, fraction []float64) {
|
|
floor = make([]float32, len(l.Classes))
|
|
fraction = make([]float64, len(l.Classes))
|
|
for i := range l.Classes {
|
|
c := l.Classes[i]
|
|
if !c.Land() {
|
|
continue
|
|
}
|
|
floor[i] = float32(c.MassifFloorMYr())
|
|
fraction[i] = c.MassifFraction()
|
|
}
|
|
return floor, fraction
|
|
}
|
|
|
|
// LithologyMixes is, per class, how much of the planet's rock field shows through. Sea is zero: the solve
|
|
// holds every sea cell at base level and never reads K there, and leaving it at 1 would put rock provinces on
|
|
// the diagnostic map out in the open ocean.
|
|
func (l *Legend) LithologyMixes() []float64 {
|
|
out := make([]float64, len(l.Classes))
|
|
for i := range l.Classes {
|
|
if l.Classes[i].Land() {
|
|
out[i] = l.Classes[i].LithMix()
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Snow is, per class, whether it is permanently under ice. A display and material hint; no pass reads it.
|
|
func (l *Legend) Snow() []bool {
|
|
out := make([]bool, len(l.Classes))
|
|
for i := range l.Classes {
|
|
out[i] = l.Classes[i].Snow
|
|
}
|
|
return out
|
|
}
|
|
|
|
// SnowMask marks every planet cell whose class is permanently under ice, painted rows only.
|
|
func (m *Map) SnowMask() []bool {
|
|
snow := m.L.Snow()
|
|
any := false
|
|
for _, s := range snow {
|
|
any = any || s
|
|
}
|
|
if !any {
|
|
return nil
|
|
}
|
|
p := m.P
|
|
out := make([]bool, p.W*p.PaintH())
|
|
for i := range out {
|
|
out[i] = snow[m.Class[p.PadY*p.W+i]]
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Depths is how far below sea level the open water of each class sits, in metres, positive. Land is zero.
|
|
func (l *Legend) Depths() []float32 {
|
|
out := make([]float32, len(l.Classes))
|
|
for i := range l.Classes {
|
|
if l.Classes[i].Sea {
|
|
out[i] = float32(l.Classes[i].DepthM)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ClassDetailTables are the per-class detail overrides, resolved against the pipeline's own numbers so a pass
|
|
// can index them without asking whether a class overrode anything.
|
|
type ClassDetailTables struct {
|
|
Droplets []float64
|
|
AmpLo []float64
|
|
AmpHi []float64
|
|
Contrast []float64
|
|
}
|
|
|
|
// DetailTables resolves every class against the pipeline defaults it is given.
|
|
func (l *Legend) DetailTables(droplets, ampLo, ampHi, contrast float64) ClassDetailTables {
|
|
n := len(l.Classes)
|
|
t := ClassDetailTables{
|
|
Droplets: make([]float64, n), AmpLo: make([]float64, n),
|
|
AmpHi: make([]float64, n), Contrast: make([]float64, n),
|
|
}
|
|
for i := range l.Classes {
|
|
t.Droplets[i], t.AmpLo[i], t.AmpHi[i], t.Contrast[i] = droplets, ampLo, ampHi, contrast
|
|
d := l.Classes[i].Detail
|
|
if d == nil {
|
|
continue
|
|
}
|
|
if d.DropletsPerCell > 0 {
|
|
t.Droplets[i] = d.DropletsPerCell
|
|
}
|
|
if d.AmplitudeM != nil {
|
|
t.AmpLo[i], t.AmpHi[i] = d.AmplitudeM[0], d.AmplitudeM[1]
|
|
}
|
|
if d.StrataContrast > 0 {
|
|
t.Contrast[i] = d.StrataContrast
|
|
}
|
|
}
|
|
return t
|
|
}
|
|
|
|
// Overrides reports whether any class asks the detail passes for anything different, so a caller can skip
|
|
// carrying a class raster through them when nothing would read it.
|
|
func (l *Legend) Overrides() bool {
|
|
for i := range l.Classes {
|
|
if l.Classes[i].Detail != nil {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|