CHAPTER 24
Terrain and elevation data
Digital Elevation Models, Terrain RGB encoding, hillshading math, slope and aspect: the foundation of 3D maps.
Without elevation, a 3D map is just a 2D map at an angle. Hillshading, terrain exaggeration, and depth cues all come from the same source: a raster grid where every pixel encodes a height value. This chapter covers how that data is stored, decoded, and put to work.
3D terrain mesh · drag to rotate
Digital Elevation Models (DEM)
A DEM is a raster where each pixel encodes the elevation of that geographic cell.
| Dataset | Resolution | Coverage | Access |
|---|---|---|---|
| SRTM (NASA) | 30 m | ±60° lat | Free |
| ASTER | 30 m | ±83° lat | Free |
| Copernicus DEM | 30 m | Global | Free |
| Mapbox Terrain-DEM | Variable | Global | API key |
| USGS 3DEP | 1 m (US) | USA | Free |
Resolution here means the geographic footprint of one pixel. At 30 m resolution, a pixel covers a 30 m × 30 m cell on the ground, fine enough to see individual buildings, but not individual cars. The vertical accuracy is typically ±5–15 m for 30 m DEMs, meaning the stored elevation value can be 5–15 m off from the true elevation.
Terrain RGB encoding
Raw elevation floats don't fit in PNG pixels. Mapbox's Terrain-DEM tiles encode elevation into RGB channels:
// Decode a pixel from a Terrain-DEM tile
function decodeElevation(r, g, b) {
return -10_000 + (r * 6553.6 + g * 25.6 + b * 0.1);
}
// Fetch and decode a terrain tile
async function getElevationAt(lat, lon, zoom = 12) {
const tx = lonToTileX(lon, zoom), ty = latToTileY(lat, zoom);
const img = await fetchTileAsImageData(zoom, tx, ty);
const [r, g, b] = pixelAt(img, lat, lon, zoom, tx, ty);
return decodeElevation(r, g, b);
}
The encoding spreads a 24-bit value across three 8-bit channels. R is the most significant byte (scaled by 6553.6 = 256² / 10), G the middle byte, B the least significant (scaled by 0.1 = 1/10). The -10,000 offset allows encoding values below sea level down to the deepest ocean trenches (~−9,000 m).
The Terrarium format (Mapzen/AWS) uses a different encoding:
Hillshading
Hillshading simulates sunlight on terrain, producing the shadow effect that makes mountains look 3D on a map. The math:
- Surface normal at each pixel: computed from the elevation gradient
- Light direction: typically northwest (azimuth 315°, altitude 45°)
- Intensity: dot product of normal and light direction
The northwest light source is a cartographic convention: terrain lit from the northwest matches what humans expect from light coming from the upper-left of a printed page. Lighting from other directions can make valleys look like ridges (the "relief inversion" illusion), because the human visual system expects top-lit surfaces to be convex.
// Fragment shader for hillshading
float dzdx = (elevRight - elevLeft) / (2.0 * cellSize);
float dzdy = (elevBottom - elevTop) / (2.0 * cellSize);
vec3 normal = normalize(vec3(-dzdx, -dzdy, 1.0));
vec3 light = normalize(vec3(cos(azimuth), sin(azimuth), sin(altitude)));
float intensity = max(0.0, dot(normal, light));
gl_FragColor = vec4(vec3(intensity), 1.0);
Continue reading "Terrain and elevation data"
You've reached the end of the free preview. Unlock all 22 paid chapters, including distance math, bearings, polygons, spatial indexing, and 3D map rendering, plus a downloadable PDF and the companion code repo.
- All 22 paid chapters with worked examples
- Downloadable PDF for offline reading
- Companion GitHub repo (JavaScript + Python)
- Free updates for life
Multiple payment options: card, Wise, and bank transfer.
Related chapters
- ECEF and 3D coordinate systems, elevation lives in the 3D frame
- Interpolation and heat maps, terrain interpolation is the same math
- 3D buildings and feature extrusion, terrain and buildings together form 3D scenes
