Skip to content
3D Terrain

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
StyleFlatWith terrain
Summersummer.jsonsummer-3d.json
Winterwinter.jsonwinter-3d.json
Hikinghiking.jsonhiking-3d.json
Cyclingcycling.jsoncycling-3d.json
Streetstreet.jsonstreet-3d.json
Lightlight.jsonlight-3d.json
Darkdark.jsondark-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) - 32768

The -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.json

There 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

Library3D terrainExample
MapLibre GL JSYesTerrain
deck.glYes, with the terrarium decoderTerrain
MapLibre Native AndroidNoNone, not possible
MapLibre Native iOSNoNone, not possible
FlutterNoNone, not possible
React NativeNoNone, not possible
kepler.glWhatever the style you load carriesNone
LeafletNoNone, not possible
OpenLayersNoNone, not possible
CesiumJSNoNone, 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.

Examples