3D Buildings and Terrain
3D terrain and 3D buildings are independent features. Terrain raises the map surface using an elevation model. 3D buildings extrude building footprints into blocks using their height attributes. A style can carry either, both, or neither.
3D terrain: 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.
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.
3D buildings: the Street style ships them
Only Street contains a fill-extrusion layer. Its building_3D_multicolored layer draws from zoom 14, filters on extrude, and takes its geometry from the height and min_height attributes. street.json and street-3d.json both have it; the other six styles draw buildings flat.
To add buildings to one of the other styles, add the layer yourself. The 3D Buildings example is a complete, runnable page.
The building layer
The building source layer exists from zoom 13 to 15. Zoom 15 is the maximum zoom of the tileset, so beyond it tiles overzoom: the geometry stays, no new detail arrives.
| Field | Type | Use |
|---|---|---|
extrude | Boolean | Whether the footprint is meant to stand up |
height | Number | Building height in metres |
min_height | Number | Height at which the building starts, for parts above ground |
colour_index | Number | Index for varying the fill across neighbouring buildings |
building_colour | String | Colour where OpenStreetMap records one |
roof_shape, roof_colour, roof_angle, roof_direction, roof_height, roof_levels, roof_material, roof_orientation | String | Roof attributes |
Filter on extrude. Not every footprint is meant to be extruded, and the Street style filters on the same flag. At a Midtown Manhattan view it excludes roughly 120 of about 16,700 footprints, mostly building parts that would otherwise stack on top of the building they belong to.
What each library supports
| Library | 3D terrain | 3D buildings |
|---|---|---|
| MapLibre GL JS | Yes | Yes |
| deck.gl | Yes, with the terrarium decoder | Yes, through a MapLibre basemap or your own extruded layer |
| MapLibre Native Android | No | Yes |
| MapLibre Native iOS | No | Yes |
| Flutter | No | Yes |
| React Native | No | Yes |
| kepler.gl | Depends on the style you load | Depends on the style you load |
| Leaflet | No | No |
| OpenLayers | No | No |
| CesiumJS | No | No |
Where 3D is not possible, and why
MapLibre Native, and the Flutter and React Native wrappers built on it. fill-extrusion is fully supported, so 3D buildings work. Terrain does not: setTerrain() raises an UnsupportedError because MapLibre Native has not implemented it. 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 and extrusions have no visible effect. Use MapLibre GL JS directly for 3D.
OpenLayers. There is no fill-extrusion equivalent and 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, and Cesium’s 3D buildings come from 3D Tiles rather than vector tile footprints. You get a 3D globe with our cartography painted on it, not 3D geometry 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
- 3D Buildings, extruding footprints in a style that does not ship them
- Enable 3D Terrain, calling
setTerrain()and adding a sky - Map Styles, the full catalogue and the language rules