Skip to content
3D Buildings and Terrain

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

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.

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.

FieldTypeUse
extrudeBooleanWhether the footprint is meant to stand up
heightNumberBuilding height in metres
min_heightNumberHeight at which the building starts, for parts above ground
colour_indexNumberIndex for varying the fill across neighbouring buildings
building_colourStringColour where OpenStreetMap records one
roof_shape, roof_colour, roof_angle, roof_direction, roof_height, roof_levels, roof_material, roof_orientationStringRoof 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

Library3D terrain3D buildings
MapLibre GL JSYesYes
deck.glYes, with the terrarium decoderYes, through a MapLibre basemap or your own extruded layer
MapLibre Native AndroidNoYes
MapLibre Native iOSNoYes
FlutterNoYes
React NativeNoYes
kepler.glDepends on the style you loadDepends on the style you load
LeafletNoNo
OpenLayersNoNo
CesiumJSNoNo

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