Terrain RGB — encoding elevation into PNG tiles for Mapbox and MapTiler
Terrain RGB is a Mapbox format in which elevation data is packed into standard 256×256 PNG tiles. Inside is a regular 8-bit RGB, but together the three R, G, B channels encode a 24-bit elevation number with 0.1-meter precision. Any browser, CDN, GIS framework that supports PNG automatically supports Terrain RGB — no new decoders required. Mapbox published the spec in 2016, MapTiler adopted it as the de-facto standard in 2017, and today Terrain RGB is the most widespread way to deliver elevation to WebGL maps like MapLibre GL, Leaflet, Cesium, Three.js.
History: terrain in PNG for web maps
In 2015 Mapbox faced a problem delivering elevation data to the browser. GeoTIFF and HGT standards are large, require special client-side decoders, and don't suit HTTP streaming well. Mapbox GL JS WebGL maps wanted to draw terrain interactively, with dynamic hillshading and contours — but a 24-megabyte HGT tile per user click — doesn't work.
The solution turned out elegant. A 24-bit elevation number easily packs into RGB channels of a standard PNG: 8 bits R, 8 G, 8 B = 24 bits total, range 0–16,777,215. The Mapbox specification of February 2016 formalized the formula:
height (m) = -10000 + ((R * 256 * 256 + G * 256 + B) * 0.1)
This gives an elevation range from -10,000 m (Mariana Trench) to +1,667,721.5 m (huge headroom). Step — 0.1 meters, finer than any open global DEM. PNG compression yields a typical 256×256 tile weighing 30–80 KB — 300× smaller than the equivalent HGT fragment.
In 2017 MapTiler — the Swiss Mapbox competitor — adopted the same formula with identical RGB encoding, cementing Terrain RGB as the de-facto standard. AWS Open Data, MapBox Terrain v2, MapTiler Terrain — three largest public sources. Since 2022 support added by Esri Vector Tile Service, Cesium World Terrain, OpenLayers (via terrain-source plugin).
In parallel an alternative encoding standard Terrarium emerged from Mapzen (now supported by Tilezen and Stadia Maps), with a slightly different formula and range. Mapbox Terrain RGB remains more widespread, but Terrarium is better for negative elevations.
What's inside a Terrain RGB tile
From a file standpoint Terrain RGB is a regular PNG. Any program that opens PNG opens Terrain RGB. Extension .png, MIME image/png. Tile size — standard 256×256 or 512×512 pixels. Bits per channel — 8, three channels total (no alpha — transparency isn't needed).
Inside each pixel is an encoded elevation. To get meters, read R, G, B and apply the formula:
function decodeTerrainRGB(r, g, b) {
return -10000 + ((r * 256 * 256 + g * 256 + b) * 0.1);
}
In a WebGL shader the same formula lives in the fragment shader, executes on GPU a million times per second:
float decodeElevation(vec3 color) {
return -10000.0 + ((color.r * 65536.0 + color.g * 256.0 + color.b) * 0.1);
}
Tiles organize into zoom-level hierarchies via the standard Web Mercator XYZ scheme. At zoom 0 — one tile for all of Earth at ~150 km per pixel. At zoom 15 — 1024 tiles per longitude degree, ~1 meter per pixel. The server stores a tree, the client requests only visible tiles — efficient for large maps.
Who needs Terrain RGB: five roles
Frontend map developer. An interactive web map on MapLibre GL JS or Mapbox GL JS shows 3D city terrain when the "Terrain" button is on: mountains become volumetric, valleys become visible. The source of the high-tech feel — a Terrain RGB layer added in one line of YAML: sources.terrain.url = 'mapbox://mapbox.terrain-rgb'.
Tourist site with routes. A ski-resort site, trekking portal, cycling-route platform — all want to show elevation profiles along routes and 3D landscape visualization. Terrain RGB covers both requirements: profile computed client-side from route tiles, 3D scene built from the same layer as a mesh.
Game and VR marketplace. In Unity, Unreal, Three.js Terrain RGB is used as a heightmap source for procedurally generated landscapes. Loading the Alps region — 50 requests to CDN. CPU-side decode to Float32Array, GPU upload as a texture. A ready landscape in seconds without downloading megabytes of source DEM.
Climatologist and ecologist. Web maps for science-popular projects (e.g., ESA Climate Viewer, NASA Worldview) often combine a climate layer with topographic relief. Terrain RGB is cheap to serve (via standard PNG CDN) and fast to decode in WebGL.
Architect and planner. Tools like Spacemaker AI, Sefaira, Autodesk Forma load surrounding terrain from a Terrain RGB CDN. An architect shows the client a 3D model of a new building with realistic surrounding terrain without lengthy DEM downloads.
Software for Terrain RGB: six tools
MapLibre GL JS. Open-source fork of Mapbox GL JS, the best tool for web maps with terrain. Enable Terrain RGB — two YAML blocks in the map style. Free. maplibre.org
Mapbox GL JS. Paid (via token), the original engine. Full Terrain RGB support via the native API map.setTerrain(). mapbox.com
rio-rgbify. Python CLI for GeoTIFF/HGT → Terrain RGB conversion. rio rgbify input.tif output.tif --base-val -10000 --interval 0.1. Used for in-house DEMs. github.com/mapbox/rio-rgbify
Cesium. WebGL framework for 3D maps. Cesium World Terrain serves its own quantized format but also accepts Terrain RGB via a terrain-server plugin. cesium.com
Three.js + custom shader. If you're building a custom 3D engine, Terrain RGB loads as a THREE.TextureLoader, decodes in a fragment shader. Minimal dependencies. threejs.org
MapTiler Engine. Paid SDK, swiss army knife for custom tiles. Converts DEM to Terrain RGB, hosts on a proprietary CDN or AWS S3. maptiler.com/engine
Pitfalls and tips
Mapbox vs Terrarium — two different formulas. Mapbox Terrain RGB: -10000 + (R*65536 + G*256 + B) * 0.1. Mapzen Terrarium: (R*256 + G + B/256) - 32768. Files look identical (regular PNG) but plugged into the wrong decoder yield chaotic artifacts instead of terrain. Always check source documentation and verify before integration.
PNG compression is lossless. PNG is a lossless format, so Terrain RGB preserves 0.1 m precision without compression artifacts. Key advantage over JPEG/WebP/AVIF: lossy compression even by 1 bit changes pixel value, which becomes ±100 m noise in elevation. For terrain PNG is mandatory.
Antialiasing on zoom boundaries. Transitioning between zoom levels (z=10 → z=11), bilinear interpolation on standard PNG tiles works correctly. But interpolating RGB values before decoding is mathematically wrong! The proper path: decode to elevation first, then interpolate. In practice this works acceptably thanks to lossless PNG, but in edge cases (sharp slopes) can produce stairsteps.
Alpha channel not used. Terrain RGB standard is PNG without alpha. If you accidentally save with alpha (RGBA), most decoders will ignore it, but some may interpret R=0,G=0,B=0,A=0 as "no data" — visual gap in the tile. Always write clean RGB via cv2.imwrite(..., [cv2.IMWRITE_PNG_COMPRESSION, 9]) or Sharp with removeAlpha: true.
Tile size 256 vs 512. Mapbox standard — 512×512 (retina). Old XYZ tile-scheme standard — 256×256. Trade-off: CDN bandwidth, number of tiles per screen. For 4K screens 512 is recommended.
CDN requests aren't free. Mapbox Terrain RGB via Mapbox API is paid (via token, free tier 50K req/mo). Alternative — your own CDN on AWS S3 or Cloudflare with rio-rgbify tiles. For startups this matters: one user at zoom 14 loads 30–50 tiles per session.
Glaring artifacts over water. Standard DEMs sometimes have errors over large water bodies — elevation spots instead of 0. Terrain RGB inherits these artifacts. Before encoding mask the DEM with water polygons (take them from OSM) and zero out elevation over water.
How to use Terrain RGB with an OSM export from osm2cdr.ru
Direct Terrain RGB export is not in the osm2cdr.ru standard list — that's an elevation format, while the service specializes in OSM vector layers. But the combination is ideal: terrain from Terrain RGB + city vector map from osm2cdr.ru = a complete 3D map.
Step 1. Get Terrain RGB tiles. Either plug in Mapbox API (mapbox://mapbox.terrain-rgb), or MapTiler (https://api.maptiler.com/tiles/terrain-rgb/), or generate your own from SRTM:
rio rgbify input.tif output_directory/ \
--base-val -10000 --interval 0.1 \
--format png --zoom 0,14
Step 2. On osm2cdr.ru export the vector layers of the needed city to SVG, GeoJSON, PMTiles, or Mapbox Vector Tiles (MVT).
Step 3. In MapLibre GL JS plug both sources into the map style:
{
"sources": {
"terrain-rgb": {
"type": "raster-dem",
"tiles": ["https://yourcdn.com/terrain/{z}/{x}/{y}.png"],
"tileSize": 512,
"encoding": "mapbox"
},
"osm-vector": {
"type": "vector",
"url": "pmtiles://https://yourcdn.com/moscow.pmtiles"
}
},
"terrain": { "source": "terrain-rgb", "exaggeration": 1.5 },
"layers": [
{ "id": "buildings", "type": "fill-extrusion", "source": "osm-vector", "source-layer": "buildings",
"paint": { "fill-extrusion-height": ["get", "height"], "fill-extrusion-color": "#ccc" }}
]
}
Step 4. Open in a browser. You get a 3D map with realistic terrain and OSM vector layers on top — buildings extrude based on height tags.
Related formats and resources
- HGT — native SRTM tiles from NASA
- ASCII Grid — text elevation format for GIS
- PNG — base format for the Terrain RGB container
- PMTiles — single-file tile archive for the web
- COG — Cloud-Optimized GeoTIFF for server-side DEM
Sources
- Mapbox Terrain-RGB specification — docs.mapbox.com/data/tilesets/guides/access-elevation-data
- Mapzen Terrarium specification — github.com/tilezen/joerd/blob/master/docs/formats.md
- MapTiler Terrain RGB — docs.maptiler.com/cloud/terrain-rgb
- MapLibre GL JS terrain documentation — maplibre.org/maplibre-gl-js-docs/api/handlers/terrain
- rio-rgbify, Mapbox repo — github.com/mapbox/rio-rgbify
- AWS Open Data, Terrain Tiles — registry.opendata.aws/terrain-tiles