Tooling
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user