Cesium terrain
Reader support for the two Cesium terrain tile formats (.terrain files) used for 3D terrain
visualization in web mapping: quantized-mesh-1.0 (adaptive triangle mesh, compressed via
quantization and delta encoding) and heightmap-1.0 (regular elevation grid). This module is
read-only — it parses tiles, answers height queries across tile sets, and converts terrain data
to raster DEMs; it does not write .terrain files. Ships in the
IRI.Maptor.Core.Spatial package.
Supported capabilities
| Capability | Supported | Implemented in |
|---|---|---|
| Read quantized-mesh-1.0 | Yes | QuantizedMeshReader.Read (path/stream/reader) |
| Read heightmap-1.0 | Yes | HeightmapReader.Read, IsHeightmapFormat |
| Auto-detect format | Yes | TerrainReader.ReadAuto, DetectFormat |
| Height queries (single tile / tile set) | Yes | TerrainReader.GetHeightAt/GetHeightAtPixel, TerrainHeightQuery |
Terrain → raster DEM (RasterGeoTiff) |
Yes | HeightmapRasterConverter.ToRasterGeoTiff, QuantizedMeshRasterConverter.ToRasterGeoTiff |
| Raster DEM → in-memory terrain data | Yes | HeightmapRasterConverter.FromRasterGeoTiff, QuantizedMeshRasterConverter.FromRasterGeoTiff |
Write .terrain files (either format) |
No | — |
Usage
Auto-detection and reading
using IRI.Maptor.Core.Spatial.IO.CesiumTerrain;
var (format, data) = TerrainReader.ReadAuto("path/to/tile/15/12345/67890.terrain");
if (format == TerrainFormat.QuantizedMesh)
{
var meshData = (QuantizedMeshData)data;
Console.WriteLine($"Mesh: {meshData.VertexCount} vertices");
}
else if (format == TerrainFormat.Heightmap)
{
var heightmapData = (HeightmapData)data;
Console.WriteLine($"Grid: {heightmapData.GridSize}x{heightmapData.GridSize}");
}
Height queries
// By file path and normalized tile coordinates (0-1)
float height = TerrainReader.GetHeightAt("tile.terrain", u: 0.5, v: 0.5);
// By z/x/y tile coordinates and pixel position
float pixelHeight = TerrainReader.GetHeightAtPixel(
terrainBasePath: @"C:\terrain",
zoom: 13, tileX: 4096, tileY: 2048,
pixelX: 128, pixelY: 128); // pixel in a 256x256 tile
TerrainHeightQuery handles multi-tile queries against a tile directory: GetHeightsForBoundary
returns a Matrix of elevations for a WGS84 bounding box at a zoom level (loading and merging all
intersecting tiles), GetHeightsForBoundaryWithSize/GetHeightsForDisplay resample to exact
output dimensions, and GetElevationProfile samples elevations along a list of lon/lat waypoints.
var (heights, actualBounds) = TerrainHeightQuery.GetHeightsForBoundary(
boundingBox, zoomLevel: 6, terrainBasePath: @"C:\terrain");
Reading quantized-mesh tiles
var terrainData = QuantizedMeshReader.Read("path/to/tile/15/12345/67890.terrain");
Console.WriteLine($"Height range: {terrainData.Header.MinimumHeight}m to {terrainData.Header.MaximumHeight}m");
Console.WriteLine($"Vertices: {terrainData.VertexCount}, Triangles: {terrainData.TriangleCount}");
// Vertex data (dequantized on access)
for (int i = 0; i < terrainData.VertexCount; i++)
{
double u = terrainData.GetNormalizedU(i); // [0, 1]
double v = terrainData.GetNormalizedV(i); // [0, 1]
double height = terrainData.GetHeight(i); // meters
}
// Triangle indices
for (int i = 0; i < terrainData.Indices.Length; i += 3)
{
uint v0 = terrainData.Indices[i];
uint v1 = terrainData.Indices[i + 1];
uint v2 = terrainData.Indices[i + 2];
}
Edge vertex indices (WestIndices, SouthIndices, EastIndices, NorthIndices) are exposed for
tile stitching.
Reading heightmap tiles
var heightmapData = HeightmapReader.Read("tile.terrain");
Console.WriteLine($"Grid size: {heightmapData.GridSize}x{heightmapData.GridSize}");
Console.WriteLine($"Height range: {heightmapData.MinHeight}m to {heightmapData.MaxHeight}m");
float heightAt = heightmapData.GetHeight(row: 128, col: 128);
float interpolated = heightmapData.GetInterpolatedHeight(u: 0.5, v: 0.5);
Tile coordinates
var tileCoord = new TerrainTileCoordinate(level: 15, x: 12345, y: 67890);
string fileName = tileCoord.GetFileName(); // "15/12345/67890.terrain"
var (west, south, east, north) = tileCoord.GetBoundingBox(); // WGS84 degrees
var parent = tileCoord.GetParent();
var children = tileCoord.GetChildren();
var coord = TerrainTileCoordinate.FromPath("data/terrain/15/12345/67890.terrain");
var fromGeo = TerrainTileCoordinate.FromGeographic(longitude: 51.4, latitude: 35.7, zoom: 12);
To convert vertex positions to geographic coordinates, combine the tile's bounding box with the
normalized vertex coordinates: longitude = west + u * (east - west),
latitude = south + v * (north - south).
Quantized-mesh extensions
var terrainData = QuantizedMeshReader.Read("tile.terrain");
if (terrainData.Extensions != null)
{
if (terrainData.Extensions.HasWaterMask)
{
byte[] waterMask = terrainData.Extensions.WaterMask; // 0 = land, 255 = water
}
if (terrainData.Extensions.HasVertexNormals)
{
byte[] normals = terrainData.Extensions.VertexNormals; // oct-encoded, 2 bytes/vertex
var (nx, ny, nz) = QuantizedMeshReader.DecodeOctNormal(normals[0], normals[1]);
}
if (terrainData.Extensions.HasMetadata)
{
string metadata = terrainData.Extensions.Metadata; // JSON string
}
}
Converting terrain to raster DEM
Both formats convert to RasterGeoTiff (see ../GeoTiff):
// Quantized-mesh -> raster (barycentric interpolation over the mesh)
var terrainData = QuantizedMeshReader.Read("15/12345/67890.terrain");
var tileCoord = new TerrainTileCoordinate(15, 12345, 67890);
var raster = QuantizedMeshRasterConverter.ToRasterGeoTiff(
terrainData, tileCoord, outputWidth: 512, outputHeight: 512);
Console.WriteLine($"Bounds: {raster.GeodeticWgs84BoundingBox}");
double elevation = raster.Data[256, 256];
// Heightmap -> raster
var heightmapData = HeightmapReader.Read("tile.terrain");
var raster2 = HeightmapRasterConverter.ToRasterGeoTiff(heightmapData, new TerrainTileCoordinate(5, 39, 20));
Converting raster DEM to in-memory terrain data
The reverse converters build in-memory HeightmapData/QuantizedMeshData objects from a raster
DEM. Because there is no .terrain writer, the result can be queried or converted back but
cannot be saved as a terrain tile.
var raster = TiffReader.ReadGeoTiff32bitDEM("dem.tif"); // IRI.Maptor.Core.Spatial.IO
var heightmap = HeightmapRasterConverter.FromRasterGeoTiff(raster, targetGridSize: 257);
var resampled = HeightmapRasterConverter.Resample(heightmap, targetGridSize: 65);
var mesh = QuantizedMeshRasterConverter.FromRasterGeoTiff(raster, new TerrainTileCoordinate(15, 12345, 67890));
Quantized-mesh binary layout
Header (88 bytes)
Vertex count uint32
U coordinates zigzag-encoded deltas
V coordinates zigzag-encoded deltas
Height values zigzag-encoded deltas
Triangle count uint32
Indices high-water-mark encoding
West/South/East/North edge vertex indices (for tile stitching)
Extensions optional (normals, water mask, metadata)
Header fields (all little-endian): tile center in ECEF (CenterX/Y/Z, 3 doubles),
MinimumHeight/MaximumHeight (2 floats), bounding sphere center and radius (4 doubles), and
horizon occlusion point (3 doubles).
U, V, and height values are quantized to 16-bit integers in [0, 32767];
actual = min + (quantized / 32767.0) * (max - min). Heightmap-1.0 files are a plain row-major
grid of elevation samples; the grid size is detected from the file length.
Tile pyramid
Terrain tiles live in a level/x/y pyramid addressed as {level}/{x}/{y}.terrain, with each level
doubling the resolution of the previous one. TerrainTileCoordinate implements the coordinate
arithmetic: file naming, parent/children navigation, geographic bounds, and lookup by geographic
position (see the tiling-scheme caveat under Limitations).
Format comparison
| Feature | heightmap-1.0 | quantized-mesh-1.0 |
|---|---|---|
| Structure | Regular grid | Triangle mesh |
| Sampling | Uniform | Adaptive |
| File size | Fixed (grid-based) | Variable |
| Height lookup | Direct grid access | Barycentric interpolation |
Conversion methodology
- Mesh → raster: for each output pixel, find the containing triangle, compute barycentric weights, and interpolate the height from the triangle's vertices.
- Raster → mesh: each valid raster cell becomes a vertex; each grid cell is split into two triangles; coordinates and heights are quantized to 16-bit values; edge vertices are extracted for stitching and ECEF header values computed.
- Raster → heightmap: bilinear resampling to a
2^n + 1grid.
Limitations
- Writing is not implemented for either format: there is no serializer that produces
.terrainfiles, soFromRasterGeoTiffresults exist in memory only. QuantizedMeshRasterConverter.FromRasterGeoTiffaccepts asimplificationToleranceparameter, but mesh simplification is not implemented — the full grid triangulation is always produced.- Heightmap grid sizes must be
2^n + 1(65, 129, 257, 513, ...); other sizes are rejected. TerrainTileCoordinateassumes a simplified geographic tiling with a single 2level x 2level grid per level — not the standard Cesium layout with two root tiles (2^(n+1) x 2n at level n).FromGeographicmaps latitude over 180/2level degrees per tile whileGetBoundingBoxuses 360/2^level on both axes, so providers using the standard scheme need their own coordinate mapping.- Quantized-mesh water mask, vertex normals, and metadata extensions are parsed but not interpreted beyond raw access.
- No tile caching or streaming; each query loads tiles from disk.
References
- Cesium quantized-mesh specification
- CesiumJS
- USAGE_EXAMPLES.md — additional code examples