Tooling
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
// Package tile cuts the detail grid into pieces that can be baked one at a time.
|
||||
//
|
||||
// Docs/Terrain-Next.md 3.3 splits the work in two and this is the easy half. The fluvial solve is global in a
|
||||
// way that cannot be tiled - drainage area is an integral over the whole upstream catchment - and it is
|
||||
// handled by decomposing the planet per landmass instead (internal/region). Every pass after it is *local*:
|
||||
// noise is pointwise, thermal weathering propagates a cell at a time, and a droplet travels at most its
|
||||
// lifetime in cells. So a tile is cut with an overlap margin sized by how far the pass it runs can move
|
||||
// material, the passes run, and the margin is thrown away. Nothing is exchanged between tiles and nothing
|
||||
// needs to be.
|
||||
//
|
||||
// That only works because of rule 1. Every hash and every noise lattice is keyed on absolute world position,
|
||||
// so a cell reached in a tile's interior and the same cell reached inside a neighbour's margin get the same
|
||||
// answer to the bit. Key anything on a tile-local index and every seam shows.
|
||||
package tile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"salty/terrain/internal/field"
|
||||
"salty/terrain/internal/world"
|
||||
)
|
||||
|
||||
// Grid is the tiling of one planet's detail resolution.
|
||||
type Grid struct {
|
||||
P world.Planet // the geology cylinder the tiles are cut from
|
||||
|
||||
Factor int // detail cells per geology cell, the manifest's geology_factor
|
||||
SideGeo int // interior side of a tile, in geology cells
|
||||
MarginGeo int // overlap carried on every side, in geology cells
|
||||
|
||||
NX, NY int // tiles across and down
|
||||
GeoW, GeoH int // the painted geology raster the tiles cover
|
||||
}
|
||||
|
||||
// NewGrid works out the tiling. sidePx is the interior side of a tile in *detail* cells, and must be a whole
|
||||
// number of geology cells; marginPx is the overlap in detail cells.
|
||||
//
|
||||
// X must divide exactly, because it wraps: a tile grid that did not come out whole would leave the last tile
|
||||
// overlapping the first by an arbitrary amount and there would be no honest way to name the seam.
|
||||
func NewGrid(p world.Planet, factor, sidePx, marginPx int) (*Grid, error) {
|
||||
if factor < 1 {
|
||||
return nil, fmt.Errorf("detail factor is %d", factor)
|
||||
}
|
||||
if sidePx < factor || sidePx%factor != 0 {
|
||||
return nil, fmt.Errorf("tile side %d detail cells is not a whole number of %d-cell geology blocks",
|
||||
sidePx, factor)
|
||||
}
|
||||
side := sidePx / factor
|
||||
if p.W%side != 0 {
|
||||
return nil, fmt.Errorf("a %d cell planet does not divide into %d cell tiles; X wraps, so it must. "+
|
||||
"The nearest sides that work are %s", p.W, side, divisorsNear(p.W, side))
|
||||
}
|
||||
margin := (marginPx + factor - 1) / factor
|
||||
if margin < 1 {
|
||||
margin = 1
|
||||
}
|
||||
g := &Grid{
|
||||
P: p, Factor: factor, SideGeo: side, MarginGeo: margin,
|
||||
GeoW: p.W, GeoH: p.PaintH(),
|
||||
}
|
||||
g.NX = g.GeoW / side
|
||||
g.NY = (g.GeoH + side - 1) / side
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// Tile is one piece: where it sits and how big it is.
|
||||
type Tile struct {
|
||||
IX, IY int
|
||||
|
||||
// X0, Y0 and W, H are the interior, in geology cells of the painted raster. The last row of tiles is
|
||||
// short wherever the planet's height is not a whole number of tiles, and that is recorded rather than
|
||||
// padded: padding would put invented ground in the output.
|
||||
X0, Y0, W, H int
|
||||
}
|
||||
|
||||
// Tiles lists every tile in row-major order.
|
||||
func (g *Grid) Tiles() []Tile {
|
||||
out := make([]Tile, 0, g.NX*g.NY)
|
||||
for iy := 0; iy < g.NY; iy++ {
|
||||
y0 := iy * g.SideGeo
|
||||
h := g.SideGeo
|
||||
if y0+h > g.GeoH {
|
||||
h = g.GeoH - y0
|
||||
}
|
||||
for ix := 0; ix < g.NX; ix++ {
|
||||
out = append(out, Tile{IX: ix, IY: iy, X0: ix * g.SideGeo, Y0: y0, W: g.SideGeo, H: h})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// DetailW and DetailH are the tile's interior at detail resolution.
|
||||
func (g *Grid) DetailW(t Tile) int { return t.W * g.Factor }
|
||||
func (g *Grid) DetailH(t Tile) int { return t.H * g.Factor }
|
||||
|
||||
// OriginXM and OriginYM are the world position of the tile's first interior detail cell.
|
||||
func (g *Grid) OriginXM(t Tile) float64 { return g.P.XM(t.X0) }
|
||||
func (g *Grid) OriginYM(t Tile) float64 { return g.P.YM(t.Y0 + g.P.PadY) }
|
||||
|
||||
// Cut extracts a tile's geology source: the interior plus the margin, wrapping in X and clamping in Y.
|
||||
//
|
||||
// The extra sample is the upsample's: field.UpsampleInt turns N samples into (N-1)*factor+1, so covering
|
||||
// W*factor interior detail cells needs W+1 geology samples, and the margin is on top of that.
|
||||
//
|
||||
// Clamping in Y rather than wrapping is not a shortcut - the top and bottom of the map are the poles, not
|
||||
// each other - and it only ever touches the polar pad, which is water.
|
||||
func (g *Grid) Cut(t Tile, src *field.Field, padY int) (out *field.Field, interiorX, interiorY int) {
|
||||
w := t.W + 2*g.MarginGeo + 1
|
||||
h := t.H + 2*g.MarginGeo + 1
|
||||
out = field.New(w, h, src.CellM)
|
||||
x0 := t.X0 - g.MarginGeo
|
||||
y0 := t.Y0 - g.MarginGeo
|
||||
for y := 0; y < h; y++ {
|
||||
sy := y0 + y
|
||||
if sy < 0 {
|
||||
sy = 0
|
||||
} else if sy >= g.GeoH {
|
||||
sy = g.GeoH - 1
|
||||
}
|
||||
row := (sy + padY) * src.W
|
||||
for x := 0; x < w; x++ {
|
||||
out.Data[y*w+x] = src.Data[row+g.P.WrapX(x0+x)]
|
||||
}
|
||||
}
|
||||
return out, g.MarginGeo * g.Factor, g.MarginGeo * g.Factor
|
||||
}
|
||||
|
||||
// CutMask is Cut for a boolean field, nearest by construction.
|
||||
func (g *Grid) CutMask(t Tile, src []bool, srcW, padY int) []bool {
|
||||
w := t.W + 2*g.MarginGeo + 1
|
||||
h := t.H + 2*g.MarginGeo + 1
|
||||
out := make([]bool, w*h)
|
||||
x0 := t.X0 - g.MarginGeo
|
||||
y0 := t.Y0 - g.MarginGeo
|
||||
for y := 0; y < h; y++ {
|
||||
sy := y0 + y
|
||||
if sy < 0 {
|
||||
sy = 0
|
||||
} else if sy >= g.GeoH {
|
||||
sy = g.GeoH - 1
|
||||
}
|
||||
row := (sy + padY) * srcW
|
||||
for x := 0; x < w; x++ {
|
||||
out[y*w+x] = src[row+g.P.WrapX(x0+x)]
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// CutClass is Cut for the painted class raster, which is indexed over the whole planet including the polar
|
||||
// pad, so it takes the pad offset rather than assuming the painted rows.
|
||||
func (g *Grid) CutClass(t Tile, src []uint8, srcW, padY int) []uint8 {
|
||||
w := t.W + 2*g.MarginGeo + 1
|
||||
h := t.H + 2*g.MarginGeo + 1
|
||||
out := make([]uint8, w*h)
|
||||
x0 := t.X0 - g.MarginGeo
|
||||
y0 := t.Y0 - g.MarginGeo
|
||||
for y := 0; y < h; y++ {
|
||||
sy := y0 + y
|
||||
if sy < 0 {
|
||||
sy = 0
|
||||
} else if sy >= g.GeoH {
|
||||
sy = g.GeoH - 1
|
||||
}
|
||||
row := (sy + padY) * srcW
|
||||
for x := 0; x < w; x++ {
|
||||
out[y*w+x] = src[row+g.P.WrapX(x0+x)]
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Frame is the tile's cut rectangle as a world frame at *detail* resolution, which is what the noise and the
|
||||
// hashes are indexed by.
|
||||
func (g *Grid) Frame(t Tile) world.Frame {
|
||||
detail := g.P
|
||||
detail.CellM = g.P.CellM / float64(g.Factor)
|
||||
detail.W = g.P.W * g.Factor
|
||||
detail.H = g.P.H * g.Factor
|
||||
detail.PadY = g.P.PadY * g.Factor
|
||||
w := (t.W + 2*g.MarginGeo) * g.Factor
|
||||
h := (t.H + 2*g.MarginGeo) * g.Factor
|
||||
return world.Frame{
|
||||
P: detail,
|
||||
X0: detail.WrapX((t.X0 - g.MarginGeo) * g.Factor),
|
||||
Y0: (t.Y0 - g.MarginGeo + g.P.PadY) * g.Factor,
|
||||
W: w + 1, H: h + 1,
|
||||
}
|
||||
}
|
||||
|
||||
// Name is the file stem a tile is written under.
|
||||
func (t Tile) Name(prefix string) string { return fmt.Sprintf("%s_x%02d_y%02d", prefix, t.IX, t.IY) }
|
||||
|
||||
// divisorsNear lists a few tile sides that do divide, for the error message.
|
||||
func divisorsNear(w, want int) string {
|
||||
var below, above int
|
||||
for d := want; d >= 1; d-- {
|
||||
if w%d == 0 {
|
||||
below = d
|
||||
break
|
||||
}
|
||||
}
|
||||
for d := want; d <= w; d++ {
|
||||
if w%d == 0 {
|
||||
above = d
|
||||
break
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%d and %d geology cells", below, above)
|
||||
}
|
||||
Reference in New Issue
Block a user