Vixen
02b45cc4
csharp
public readonly record struct TerrainAtlas

The 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.

Read the guide page for this →

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 TileSamples

    How many samples a tile is, on a side. A power of two.

  • public int TileQuads

    How many quads that is — one fewer, because the tiles share their boundary.

  • public int TilesX

    How many tiles along X.

  • public int TilesZ

    And along Z.

  • public int TileCount

    How many tiles there are.

  • public int Width

    How many texels the atlas is, along X.

  • public int Height

    And along Z.

  • public int LevelCount

    How 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