Guide

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.

Add 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
});
Exaggeration
Height under the mouse: –
Go to:
Loading the map…
Tilt and turn with the right mouse button, Ctrl and drag, or two fingers.

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.json gives 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:

ViewTiles
Zurich at zoom 13, light style, flat6
Valais at zoom 9.6, light style with hillshading16
Matterhorn at zoom 12.6, pitch 72, light style in 3D39
The same view in the outdoor style in 3D75

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.