3D Terrain
Terrain raises the map surface using an elevation model, so mountains and valleys stand up when you tilt the map. For buildings in 3D, see 3D Buildings.
Add -3d to the style name
Every one of the seven community styles has a -3d variant that ships terrain already configured. Nothing else is needed.
https://styles.maptoolkit.org/winter-3d.json| Style | Flat | With terrain |
|---|---|---|
| Summer | summer.json | summer-3d.json |
| Winter | winter.json | winter-3d.json |
| Hiking | hiking.json | hiking-3d.json |
| Cycling | cycling.json | cycling-3d.json |
| Street | street.json | street-3d.json |
| Light | light.json | light-3d.json |
| Dark | dark.json | dark-3d.json |
The base styles carry no terrain block. The -3d variants add a raster-dem source named rgb-tiles and set it as the map terrain with an exaggeration of 1. The 3D buildings of Street and Summer stand on the terrain.
What the terrain source is
The elevation model is terrarium-encoded, not Mapbox Terrain-RGB. The two encodings decode differently, so outside a MapLibre style the wrong decoder produces plausible but incorrect elevations.
{
"type": "raster-dem",
"url": "https://tiles.maptoolkit.org/terrainrgb.json",
"encoding": "terrarium"
}The TileJSON at that URL serves 512 px WebP tiles from tiles.mapterhorn.com. Terrarium decodes as:
height = (R * 256 + G + B / 256) - 32768The -3d styles also carry hillshade and, in the outdoor styles, a color-relief bathymetry layer. Those are drawn from the same elevation data and render without terrain enabled, which is why the flat styles still show relief.
-3d and a language cannot be combined
Language variants use a suffix on the style name:
https://styles.maptoolkit.org/winter-de.jsonThere is no combined form. winter-3d-de.json and winter-de-3d.json both return 404. If you need terrain and a fixed label language at the same time, start from the -3d style and override the label layers yourself, or host your own style JSON.
What each library supports
| Library | 3D terrain | Example |
|---|---|---|
| MapLibre GL JS | Yes | Terrain |
| deck.gl | Yes, with the terrarium decoder | Terrain |
| MapLibre Native Android | No | None, not possible |
| MapLibre Native iOS | No | None, not possible |
| Flutter | No | None, not possible |
| React Native | No | None, not possible |
| kepler.gl | Whatever the style you load carries | None |
| Leaflet | No | None, not possible |
| OpenLayers | No | None, not possible |
| CesiumJS | No | None, not possible |
kepler.gl has no setting of its own: it loads a style URL and renders what that style contains, so a -3d style gives terrain.
Where terrain is not possible, and why
MapLibre Native, and the Flutter and React Native wrappers built on it. setTerrain() raises an UnsupportedError because MapLibre Native has not implemented terrain. Terrain support for Native is in development; check your SDK version.
Leaflet. The maplibre-gl-leaflet plugin renders a MapLibre style inside Leaflet, but its README states plainly that there is no rotation, bearing or pitch support. Without pitch the map is always viewed from directly above, where terrain has no visible effect. Use MapLibre GL JS directly for 3D.
OpenLayers. There is no terrain. OpenLayers is a 2D renderer; 3D requires pairing it with a separate engine such as Cesium.
CesiumJS. Our CesiumJS setup uses the basemap as an imagery layer over EllipsoidTerrainProvider, which is a smooth ellipsoid. CesiumTerrainProvider expects quantized-mesh, not a terrarium raster DEM, so our elevation data cannot drive Cesium’s terrain. You get a 3D globe with our cartography painted on it, not 3D terrain from our data.
Using the elevation data outside MapLibre
Any renderer that can decode a terrarium raster DEM can use it. In deck.gl, TerrainLayer needs the terrarium decoder rather than the Mapbox one shown in most examples:
import { TerrainLayer } from '@deck.gl/geo-layers';
new TerrainLayer({
id: 'terrain',
// Maptoolkit's DEM is terrarium-encoded. The Mapbox Terrain-RGB decoder
// that most deck.gl examples show will silently produce wrong elevations.
elevationDecoder: { rScaler: 256, gScaler: 1, bScaler: 1 / 256, offset: -32768 },
elevationData: 'https://tiles.mapterhorn.com/{z}/{x}/{y}.webp',
maxZoom: 13
});This reaches the tile host directly rather than going through a style, so the attribution requirements are yours to satisfy in your own UI.