3D terrain in MapLibre GL JS
MapLibre GL JS draws mountains in 3D from terrain tiles, which store a height in the colour of each pixel. With one style parameter, or with a few lines in your own style: exaggeration, sky, camera, heights under the mouse, and what 3D costs in tiles. Tested with MapLibre GL JS 6.13.0.
terrain=1 to the style URL and tilt the map:
const map = new maplibregl.Map({
container: "map",
style: "https://api.overpass.ch/v1/YOUR_KEY/maps/styles/outdoor.json?terrain=1",
center: [7.6586, 45.9763], // the Matterhorn
zoom: 12.5,
pitch: 70,
maxPitch: 85
});
With the style parameter
terrain=1 works on all nine styles. It adds two terrain sources, a hillshade layer and the terrain property of the style:
"sources": {
"terrain": { "type": "raster-dem", "url": "…/maps/terrain.json", "tileSize": 512, "encoding": "mapbox" },
"terrain-3d": { "type": "raster-dem", "url": "…/maps/terrain.json", "tileSize": 512, "encoding": "mapbox" }
},
"terrain": { "source": "terrain-3d", "exaggeration": 1 }
The hillshade layer reads terrain, the 3D surface terrain-3d. With one source for both, MapLibre warns: “You are using the same source for a hillshade layer and for 3D terrain. Please consider using two separate sources to improve rendering quality.” To make the mountains steeper, or flat again:
map.setTerrain({ source: "terrain-3d", exaggeration: 1.5 });
map.setTerrain(null); // flat, the hillshading stays
In your own style
For a style of your own, or to switch 3D on later, add the source when the style has loaded. This is the code we tested, with sky and a button that switches the terrain on and off:
const map = new maplibregl.Map({
container: "map",
style: "https://api.overpass.ch/v1/YOUR_KEY/maps/styles/outdoor.json",
center: [7.6586, 45.9763],
zoom: 12.5,
pitch: 70,
bearing: -125,
maxPitch: 85
});
map.addControl(new maplibregl.NavigationControl({ visualizePitch: true }));
map.on("load", () => {
map.addSource("terrain-3d", {
type: "raster-dem",
url: "https://api.overpass.ch/v1/YOUR_KEY/maps/terrain.json"
});
map.setTerrain({ source: "terrain-3d", exaggeration: 1.2 });
map.setSky({
"sky-color": "#7fb2e5",
"horizon-color": "#dfeaf5",
"fog-color": "#ffffff",
"sky-horizon-blend": 0.6,
"horizon-fog-blend": 0.6,
"fog-ground-blend": 0.3
});
});
map.addControl(new maplibregl.TerrainControl({ source: "terrain-3d", exaggeration: 1.2 }));
- The TileJSON at
terrain.jsongives the tile URL, the zoom range 0 to 10, the tile size of 512 pixels and the encoding, so the source needs no more options. Our tiles use the Mapbox Terrain-RGB encoding, the default of MapLibre. - MapLibre allows a pitch up to 60 degrees unless you raise
maxPitch. At 85 degrees you look almost along the ground; steep views show the horizon and load more tiles. - The sky shows when the map is tilted. Without
setSky, the space above the horizon has the background colour of the style.
The height under the mouse
map.on("mousemove", (e) => {
const t = map.getTerrain();
const h = t && map.queryTerrainElevation(e.lngLat);
if (h != null) console.log(Math.round(h / t.exaggeration), "m");
});
queryTerrainElevation returns the height multiplied by the exaggeration, and null without terrain. With exaggeration 1.2 it gave 5,052 m for the Matterhorn, which is 4,210 m in the terrain data. It reads the tiles the map has loaded, so it works for the part of the world in view.
The terrain comes from the Copernicus GLO-90 model, a grid with 90 m spacing. Valleys and slopes are close; sharp summits read low: the Matterhorn is 4,478 m high. For heights of points you have not loaded on a map, or of a whole route, use the elevation API with the same key.
What 3D costs in tiles
Every terrain tile counts as one map tile, like a vector tile. We counted the tiles of the first view of a map of 1280 × 720 pixels:
| View | Tiles |
|---|---|
| Zurich at zoom 13, light style, flat | 6 |
| Valais at zoom 9.6, light style with hillshading | 16 |
| Matterhorn at zoom 12.6, pitch 72, light style in 3D | 39 |
| The same view in the outdoor style in 3D | 75 |
A tilted map looks far into the distance and loads tiles up to the horizon; the outdoor style adds contour lines and trails. If 3D is an option in your app, start flat and switch it on when people ask for it, with the TerrainControl above. Every account includes 100'000 tiles a month; the Maps plans have more.
Limits
- The sea is flat at 0 m in 3D, and lakes have their water level. The marine style shows depths by colour and shading.
- Terrain tiles go up to zoom 10, which matches the 90 m grid. Closer in, MapLibre enlarges zoom 10, so small slopes in towns do not show.
- MapLibre GL JS 6 needs WebGL 2. 3D costs the graphics card more than a flat map; test on the phones your visitors use.