166 lines
5.8 KiB
Go
166 lines
5.8 KiB
Go
package overlay
|
|
|
|
import (
|
|
"encoding/json"
|
|
"image/png"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"salty/terrain/internal/field"
|
|
)
|
|
|
|
// What the overlay hands to whatever builds the level.
|
|
//
|
|
// Two shapes, because two different things want it. A *raster* answers "what is under this square metre" and
|
|
// is what a per-tile importer wants: one 8-bit image beside each height tile, an index per detail cell, zero
|
|
// for nothing. A *feature list* answers "where do I put the village" and "what curve does the road follow",
|
|
// and lives once at the planet root in world metres because a point is not a pixel and a spline crossing a
|
|
// tile boundary is still one spline.
|
|
//
|
|
// Nothing in the generator reads either of them back. That is the point of the layer.
|
|
|
|
// CoastScale is the per-pixel multiplier on the waterline roughening, or nil when no mark asks for one.
|
|
//
|
|
// A pixel with no instruction comes back negative rather than 1, which is the contract template.Coast.Scale
|
|
// documents: an unmarked cell takes its instruction from the far side of the waterline instead of overriding
|
|
// what the marked side said.
|
|
func (l *Legend) CoastScale(r *Raster) []float32 {
|
|
if !l.TouchesCoast() {
|
|
return nil
|
|
}
|
|
per := make([]float32, len(l.Marks)+1)
|
|
per[Blank] = -1
|
|
for i := range l.Marks {
|
|
if j, set := l.Marks[i].Jitter(); set {
|
|
per[i+1] = float32(j)
|
|
} else {
|
|
per[i+1] = -1
|
|
}
|
|
}
|
|
out := make([]float32, len(r.Mark))
|
|
field.Rows(r.H, func(y0, y1 int) {
|
|
for i := y0 * r.W; i < y1*r.W; i++ {
|
|
out[i] = per[r.Mark[i]]
|
|
}
|
|
})
|
|
return out
|
|
}
|
|
|
|
// Document is overlay.json: everything an importer needs to place what the author painted.
|
|
type Document struct {
|
|
Image string `json:"image"`
|
|
Legend string `json:"legend"`
|
|
|
|
// The frame the coordinates are in: metres east from the seam and metres south from the top painted row,
|
|
// which is the same frame world.Planet uses for a painted cell once the polar pad is taken off.
|
|
CircumferenceM float64 `json:"circumference_m"`
|
|
HeightM float64 `json:"height_m"`
|
|
PaintW int `json:"paint_w"`
|
|
PaintH int `json:"paint_h"`
|
|
MetresPerPxX float64 `json:"metres_per_px_x"`
|
|
MetresPerPxY float64 `json:"metres_per_px_y"`
|
|
|
|
Marks []MarkShare `json:"marks"`
|
|
Features []Feature `json:"features"`
|
|
}
|
|
|
|
// MarkShare is one mark and how much of the world carries it.
|
|
type MarkShare struct {
|
|
Index int `json:"index"`
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind"`
|
|
RGB [3]int `json:"rgb"`
|
|
Cells int `json:"cells_px"`
|
|
AreaKm2 float64 `json:"area_km2"`
|
|
Pieces int `json:"pieces"`
|
|
WidthM float64 `json:"width_m,omitempty"`
|
|
|
|
// Jitter and HasJitter are a pair and neither is omitempty, because the interesting value of the first
|
|
// is **zero** - a pinned coastline - and omitting it would leave every consumer of this file unable to
|
|
// tell "pin it" from "said nothing", which is the one distinction the key exists to make.
|
|
Jitter float64 `json:"coast_jitter"`
|
|
HasJitter bool `json:"has_coast_jitter"`
|
|
|
|
Note string `json:"note,omitempty"`
|
|
}
|
|
|
|
// Describe builds the document from a classified overlay.
|
|
func (l *Legend) Describe(r *Raster, m Match, s Scale, image, legend string) *Document {
|
|
feats := l.Features(r, s)
|
|
pieces := make([]int, len(l.Marks)+1)
|
|
for _, f := range feats {
|
|
pieces[f.Index]++
|
|
}
|
|
doc := &Document{
|
|
Image: image, Legend: legend,
|
|
CircumferenceM: s.CircumferenceM,
|
|
HeightM: float64(r.H) * s.MetresPerPxY,
|
|
PaintW: r.W, PaintH: r.H,
|
|
MetresPerPxX: s.MetresPerPxX, MetresPerPxY: s.MetresPerPxY,
|
|
Features: feats,
|
|
}
|
|
for i := range l.Marks {
|
|
mk := &l.Marks[i]
|
|
cells := 0
|
|
if i+1 < len(m.Counts) {
|
|
cells = m.Counts[i+1]
|
|
}
|
|
share := MarkShare{
|
|
Index: i + 1, Name: mk.Name, Kind: mk.Kind, RGB: mk.RGB,
|
|
Cells: cells, AreaKm2: float64(cells) * s.MetresPerPxX * s.MetresPerPxY / 1e6,
|
|
Pieces: pieces[i+1], WidthM: mk.WidthM, Note: mk.Note,
|
|
}
|
|
if j, set := mk.Jitter(); set {
|
|
share.Jitter, share.HasJitter = j, true
|
|
}
|
|
doc.Marks = append(doc.Marks, share)
|
|
}
|
|
return doc
|
|
}
|
|
|
|
// WriteJSON writes overlay.json.
|
|
func (d *Document) WriteJSON(dir string) error {
|
|
data, err := json.MarshalIndent(d, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(filepath.Join(dir, "overlay.json"), append(data, '\n'), 0o644)
|
|
}
|
|
|
|
// WriteMask writes an 8-bit indexed PNG: one mark index a pixel, zero for nothing. It is the per-tile
|
|
// output, and it is indexed rather than one image a mark because marks cannot overlap - the overlay is one
|
|
// painting and a pixel is one colour - so 254 masks fit in the file one would have taken.
|
|
func WriteMask(path string, w, h int, marks []uint8) error {
|
|
return field.WriteGray8(path, w, h, marks, png.BestCompression)
|
|
}
|
|
|
|
// SampleWorld reads the overlay over a rectangle of some other grid, described in world metres.
|
|
//
|
|
// World metres rather than cell indices, because the caller is a *detail* tile: it is 2 m where the overlay
|
|
// is 12.9 and the geology is 8, and it sits at an origin that is only expressible in metres. Going through
|
|
// the common frame is the only way the three agree, and it is rule 1 of the tiling plan applied to a raster
|
|
// instead of to a noise - a cell gets the same mark whichever tile reaches it.
|
|
//
|
|
// Nearest neighbour, for the same reason the class raster is: an index is a name, and the average of
|
|
// "forest" and "road" is neither.
|
|
func (r *Raster) SampleWorld(originXM, originYM, cellM float64, w, h int, s Scale) []uint8 {
|
|
out := make([]uint8, w*h)
|
|
field.Rows(h, func(b0, b1 int) {
|
|
for y := b0; y < b1; y++ {
|
|
py := int((originYM + (float64(y)+0.5)*cellM) / s.MetresPerPxY)
|
|
if py < 0 {
|
|
py = 0
|
|
} else if py >= r.H {
|
|
py = r.H - 1
|
|
}
|
|
row := py * r.W
|
|
for x := 0; x < w; x++ {
|
|
px := int((originXM + (float64(x)+0.5)*cellM) / s.MetresPerPxX)
|
|
px = ((px % r.W) + r.W) % r.W
|
|
out[y*w+x] = r.Mark[row+px]
|
|
}
|
|
}
|
|
})
|
|
return out
|
|
}
|