public readonly record struct TerrainAtlasThe terrain's heights and weights as an atlas of per-tile blocks, which is what makes the upload per tile and the mip chain possible.
Remarks
[docs/plan/31 § T2]'s owed per-tile split, and it is a split of the layout rather than of the texture. The plan wants the tile to be the unit of everything including load ([§ D2]); the draw wants one texture, because a CDLOD patch straddles a tile boundary except by luck and a texture per tile makes every straddling patch either two draws or a shader sampling two textures. An atlas is both: one texture to bind, and a block per tile to upload, evict and mip independently.
⚠ The blocks do not share their boundary samples — they duplicate them. The packed heightfield is TilesX × TileQuads + 1 samples wide because adjacent tiles share the sample between them; the atlas gives each tile all TileSamples of its own, so tile k's last column and tile k+1's first hold the same number. That costs (TileSamples / TileQuads)² — 1.6% at 128 — and buys the thing the whole layout is for: a block whose size is a power of two, starting at a multiple of one.
⚠ Which is what makes the mip chain legal. A 2×2 reduction of the atlas never crosses a block boundary, because every block is TileSamples texels and starts at a multiple of it. Reducing the packed grid instead would mix two tiles' texels at every level — [§ D2]'s seam arriving through the mip chain, which is exactly the failure TerrainMips's remarks refuse.
⚠ And the duplication is what keeps filtering continuous. A bilinear tap just inside tile k's last texel blends two of that tile's samples; a tap just past it lands in tile k+1's first two, whose first is the same number. There is no seam, and there is no half-texel stretch, because the sample-to-texel map has the same scale inside every block.
⚠ A sample on a boundary belongs to the upper tile, and the choice has to be the same everywhere. TileOf already made it — x / TileQuads sends sample 127 of a 128-sample tiling to tile 1, not tile 0 — and this follows it rather than making it again. Two answers to "which tile is sample 127 in" is a terrain that reads one block and was written into another.
The lower tile still holds that sample: its block is TileSamples wide and its last column is the boundary. That is the duplication, and it is why a tap that crosses the seam blends a value with itself rather than with a neighbour's.
Fields and properties (8)
public int TileSamplesHow many samples a tile is, on a side. A power of two.
public int TileQuadsHow many quads that is — one fewer, because the tiles share their boundary.
public int TilesXHow many tiles along X.
public int TilesZAnd along Z.
public int TileCountHow many tiles there are.
public int WidthHow many texels the atlas is, along X.
public int HeightAnd along Z.
public int LevelCountHow many mip levels it has, level 0 included.
Methods (7)
public TerrainAtlas(in TerrainDescription description)Describes the atlas a terrain's tiles pack into.
public int BlockSizeAt(int level)How many texels a block is at a level.
public int WidthAt(int level)How many texels the atlas is at a level, along X.
public int HeightAt(int level)And along Z.
public TerrainRect BlockOf(int tileX, int tileZ, int level = 0)Where a tile's block starts at a level, in texels.
public TerrainAtlasTexel Locate(int x, int z)Where a sample of the packed grid lands in the atlas at level 0.
public Vector2 UvOf(Vector2 sample)The texture coordinate a sample coordinate reads, at level 0.
Used by (3)
- TerrainAtlasTestsVixen.Terrain.Tests
- TerrainRendererVixen.Rendering.Terrain
- TerrainRendererTestsVixen.Rendering.Terrain.Tests