// 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) }