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 including Wise, PayPal, 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