Add 3D Buildings
The building layer of the Maptoolkit vector tile schema carries the fields needed to extrude footprints into 3D: height and min_height in metres, an extrude flag, and colour_index for varying the fill.
The Street style already includes a fill-extrusion layer and shows 3D buildings from zoom 14 without any work. Use this example when you want 3D buildings in a style that does not ship them, which is Summer, Light, Dark, Hiking and Cycling.
Two things worth knowing before you copy it. Filter on extrude being true: not every footprint is meant to be extruded, and the flag is what the Street style itself filters on. And set fill-extrusion-base from min_height, otherwise buildings that start above ground level, such as an upper storey bridging a street, sink into the terrain.
import * as maplibregl from 'https://cdn.jsdelivr.net/npm/maplibre-gl@6.0.0/dist/maplibre-gl.mjs';
import { MaptoolkitLogoControl } from 'https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-gl-logo/+esm';
const map = new maplibregl.Map({
container: 'map',
attributionControl: { customAttribution: '', compact: false },
style: 'https://styles.maptoolkit.org/summer.json',
center: [-0.12025, 51.50222],
zoom: 15.5,
pitch: 60,
bearing: 45
});
// Arabic/Hebrew: MapLibre has no right-to-left support built in, this plugin adds it
maplibregl.setRTLTextPlugin('https://cdn.jsdelivr.net/npm/@mapbox/mapbox-gl-rtl-text@0.4.0/dist/mapbox-gl-rtl-text.js', false);
map.on('load', function () {
// Insert the extrusion below the label layers, so street and place names
// stay on top of the buildings instead of being hidden behind them.
const layers = map.getStyle().layers;
let firstLabel;
for (const layer of layers) {
if (layer.type === 'symbol') { firstLabel = layer.id; break; }
}
map.addLayer({
id: 'buildings-3d',
type: 'fill-extrusion',
source: 'mtk',
'source-layer': 'building',
minzoom: 14,
// Only footprints the data marks as extrudable. This is the same filter
// the Street style uses.
filter: ['==', ['get', 'extrude'], true],
paint: {
'fill-extrusion-height': ['coalesce', ['get', 'height'], 8],
// min_height matters: without it, anything starting above ground
// level is drawn from the ground up and looks wrong.
'fill-extrusion-base': ['coalesce', ['get', 'min_height'], 0],
'fill-extrusion-color': [
'case',
['has', 'colour_index'],
['interpolate', ['linear'], ['get', 'colour_index'],
0, 'hsl(45, 37%, 88%)',
5, 'hsl(45, 18%, 92%)',
10, 'hsl(45, 22%, 85%)'
],
'hsl(45, 22%, 85%)'
],
'fill-extrusion-vertical-gradient': true,
// Fade the extrusion in rather than having it appear at zoom 14.
'fill-extrusion-opacity': ['interpolate', ['linear'], ['zoom'], 14, 0.2, 14.5, 0.9]
}
}, firstLabel);
});
map.addControl(new MaptoolkitLogoControl());<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.0.0/dist/maplibre-gl.css" />
<style>
html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
#map { width: 100%; height: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script type="module">
import * as maplibregl from 'https://cdn.jsdelivr.net/npm/maplibre-gl@6.0.0/dist/maplibre-gl.mjs';
import { MaptoolkitLogoControl } from 'https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-gl-logo/+esm';
const map = new maplibregl.Map({
container: 'map',
attributionControl: { customAttribution: '', compact: false },
style: 'https://styles.maptoolkit.org/summer.json',
center: [-0.12025, 51.50222],
zoom: 15.5,
pitch: 60,
bearing: 45
});
// Arabic/Hebrew: MapLibre has no right-to-left support built in, this plugin adds it
maplibregl.setRTLTextPlugin('https://cdn.jsdelivr.net/npm/@mapbox/mapbox-gl-rtl-text@0.4.0/dist/mapbox-gl-rtl-text.js', false);
map.on('load', function () {
// Insert the extrusion below the label layers, so street and place names
// stay on top of the buildings instead of being hidden behind them.
const layers = map.getStyle().layers;
let firstLabel;
for (const layer of layers) {
if (layer.type === 'symbol') { firstLabel = layer.id; break; }
}
map.addLayer({
id: 'buildings-3d',
type: 'fill-extrusion',
source: 'mtk',
'source-layer': 'building',
minzoom: 14,
// Only footprints the data marks as extrudable. This is the same filter
// the Street style uses.
filter: ['==', ['get', 'extrude'], true],
paint: {
'fill-extrusion-height': ['coalesce', ['get', 'height'], 8],
// min_height matters: without it, anything starting above ground
// level is drawn from the ground up and looks wrong.
'fill-extrusion-base': ['coalesce', ['get', 'min_height'], 0],
'fill-extrusion-color': [
'case',
['has', 'colour_index'],
['interpolate', ['linear'], ['get', 'colour_index'],
0, 'hsl(45, 37%, 88%)',
5, 'hsl(45, 18%, 92%)',
10, 'hsl(45, 22%, 85%)'
],
'hsl(45, 22%, 85%)'
],
'fill-extrusion-vertical-gradient': true,
// Fade the extrusion in rather than having it appear at zoom 14.
'fill-extrusion-opacity': ['interpolate', ['linear'], ['zoom'], 14, 0.2, 14.5, 0.9]
}
}, firstLabel);
});
map.addControl(new MaptoolkitLogoControl());
</script>
</body>
</html>Fields on the building layer
height and min_height are the two you need for a basic extrusion. The rest are there for renderers doing more detailed 3D work than a flat-topped prism.
| Field | Type | What it is |
|---|---|---|
height | Number | Total building height in metres |
min_height | Number | Height of the underside above ground, in metres |
extrude | Boolean | Whether this footprint is meant to be extruded |
building | String | The building classification from OpenStreetMap |
building_colour | String | Building colour where it is mapped |
colour_index | Number | Index used to vary the fill across neighbouring buildings |
roof_shape | String | Roof form, for example gabled, hipped, pyramidal |
roof_angle | String | Roof pitch |
roof_direction | String | Direction the roof faces |
roof_orientation | String | Whether the ridge runs along or across the footprint |
roof_height | String | Roof height |
roof_levels | String | Number of levels inside the roof |
roof_material | String | Roof material |
roof_colour | String | Roof colour where it is mapped |
MapLibre’s fill-extrusion cannot express a pitched roof, so the roof fields are only useful if you are rendering geometry yourself, in Deck.gl or CesiumJS for instance. Coverage of all of these depends on what OpenStreetMap contributors have mapped locally, and it varies a lot between cities.
Further Reading
- Schema Reference - the full vector tile schema.
- Map Styles - the style catalog, including the Street style that ships 3D buildings.
- 3D Styles - the
-3dvariants, which add terrain relief rather than buildings.