Show 3D Buildings and Trees in MapLibre GL JS
On this page
A MapLibre GL JS control plugin that adds the 3D scene of a Maptoolkit style to the map: 3D buildings and 3D trees.
When you tilt the map, you see:
- 3D buildings from zoom 12.
- Roof shapes from zoom 15, for buildings whose roof is mapped in OpenStreetMap (gabled, hipped, pyramidal, dome, onion and more).
- Official 3D city models where they are available, for example in Vienna. There they replace the OpenStreetMap buildings.
- Trees as 3D models from zoom 15: single trees and forests.
The scene is part of the style. The Street and Summer styles carry one. Winter, Hiking, Cycling, Light and Dark have none, so the control adds nothing on them. When you switch styles, the control shows the scene of the new style.
Install
npm install @maptoolkit/maplibre-scene3d-control maplibre-glThe control draws the buildings and trees with @maptoolkit/maplibre-3d-layers, which uses three.js. Both are dependencies of the control, so npm installs them with it and you do not import them yourself. The control loads the library only once the map reaches zoom 12, so a map that stays zoomed out does not download it.
Usage
import * as maplibregl from "maplibre-gl";
import { Scene3dControl } from "@maptoolkit/maplibre-scene3d-control";
import "@maptoolkit/maplibre-scene3d-control/style.css";
const API_KEY = "YOUR_API_KEY";
const map = new maplibregl.Map({
container: "map",
style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
center: [16.3731, 48.2085],
zoom: 16,
pitch: 70,
maxPitch: 85,
canvasContextAttributes: { antialias: true }, // smoother edges on distant trees
});
map.addControl(new Scene3dControl({ apiKey: API_KEY }));The control has no button by default, so nothing appears in the map corner. Pass button: true to add a 3D button that switches the scene on and off.
In the Maptoolkit styles, the 3D buildings fade in as you tilt the map.
API key
Pass the same API key as in the style URL. The control adds it to the URL of the buildings tileset (https://tiles.maptoolkit.net/buildings.json).
Without a valid key, the buildings tileset answers 403. The control then leaves the style’s own buildings in place, so the map still shows buildings, but without roof shapes and city models. With a valid key, the control hides the style’s own 3D buildings (building_3d, building_3d_multicolored) once the buildings tileset has loaded, so buildings are not drawn twice.
Without a bundler
The package is ESM-only (no UMD/CJS build). Load its standalone build, dist/maplibre-scene3d-control.standalone.js, from a CDN with an import map that resolves the bare maplibre-gl specifier. The standalone build ships the 3D library, with three.js included, and loads it from next to itself, so there is nothing else to load:
<link href="https://unpkg.com/maplibre-gl@^6.0.0/dist/maplibre-gl.css" rel="stylesheet" />
<link href="https://unpkg.com/@maptoolkit/maplibre-scene3d-control@^0.3.0/dist/maplibre-scene3d-control.css" rel="stylesheet" />
<script type="importmap">
{
"imports": {
"maplibre-gl": "https://unpkg.com/maplibre-gl@^6.0.0/dist/maplibre-gl.mjs"
}
}
</script>
<script type="module">
import * as maplibregl from "maplibre-gl";
import { Scene3dControl } from "https://unpkg.com/@maptoolkit/maplibre-scene3d-control@^0.3.0/dist/maplibre-scene3d-control.standalone.js";
const API_KEY = "YOUR_API_KEY";
const map = new maplibregl.Map({
container: "map",
style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
center: [16.3731, 48.2085],
zoom: 16,
pitch: 70,
canvasContextAttributes: { antialias: true },
});
map.addControl(new Scene3dControl({ apiKey: API_KEY }));
</script>Point the import map at MapLibre’s own dist/maplibre-gl.mjs, not at a jsDelivr +esm URL, and do not load three.js a second time on the same page. The standalone build brings its own copy.
Options
| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | – | Your Maptoolkit API key. The control adds it to the buildings tileset URL as api_key. Without it the tileset answers 403 and the style keeps its own buildings. |
scene | Scene3dManifest | string | null | the style’s scene | A scene, or the URL of one, to show instead of the style’s own. null shows none. |
enabled | boolean | true | Show the scene right away. |
button | boolean | false | Add a 3D button that switches the scene on and off. Without it the control has no UI. |
minzoom | number | 12 | Zoom from which the 3D library is loaded. The buildings tileset has no tiles below zoom 12. |
hideLayers | string[] | ["building_3d", "building_3d_multicolored"] | Style layers to hide while the scene’s buildings are shown. They are hidden only once the buildings tileset has loaded. |
modelBase | string | the page’s base URL | Base URL for relative model URLs in a scene. |
library | () => Promise<Scene3dLibrary> | the 3D library that comes with the control | Loads the 3D library. Leave it out: the control loads the one it brings, in the standalone build from next to itself. Pass a loader only for another build or version. |
idPrefix | string | "scene3d-" | Prefix of the source and layer ids the control adds. |
The control’s default position is top-right. It only matters with button: true.
Methods
const control = new Scene3dControl({ apiKey: API_KEY });
map.addControl(control);
control.setEnabled(false); // hide the scene
control.setScene(null); // show no scene
control.setScene(undefined); // back to the style's own scene
| Method | Description |
|---|---|
setScene(scene) | Shows another scene: a scene object, the URL of one, null for none, or undefined for the style’s own scene. Passing the same scene again keeps the layers. |
getScene() | Returns the scene shown now, or null. |
setEnabled(on) / isEnabled() | Switches the scene on or off, and returns whether it is on. |
getLayers() | Returns the 3D layers the control added to the map. |
loadLibrary() / getLibrary() | Loads the 3D library now instead of at minzoom, and returns the loaded library. |
Events
The control extends MapLibre’s Evented, so you subscribe to it the same way as to the map:
control.on("scene.change", (e) => console.log(e.scene));| Event | Payload | Fired when… |
|---|---|---|
scene.change | { scene: Scene3dManifest | null } | the scene is built, rebuilt or removed. |
The scene
A Maptoolkit style names its scene in metadata["maptoolkit:scene3d"]. All values are plain MapLibre or @maptoolkit/maplibre-3d-layers options:
{
version: 1,
source: "https://tiles.maptoolkit.net/buildings.json", // buildings tileset, without api_key
lod1: { filter, paint, layout }, // fill-extrusion on the source layer buildings2d
fill3d: { paint }, // Fill3DLayer on the source layer buildings3d
models: { tree_1: "https://…/Tree.gltf" }, // glTF models by id
gltf: [{ id, models: ["tree_1"], source: "mtk", "source-layer": "natural", filter, minzoom, layout, paint }],
}Only version and source are required. By default every layer of the scene goes below the style’s labels. Set position on lod1, fill3d or a gltf layer to "top" to draw it above everything, or to the id of a style layer to draw it right below that layer. The trees need the style’s mtk source. A tree layer whose source the style lacks is left out with a console warning.
To show your own scene, pass it as scene or call setScene(). If you change only the scene in a style and call map.setStyle() with the default diff: true, MapLibre ignores the change. Use setScene(), or setStyle() with diff: false.
Requirements
- MapLibre GL JS 6 or later.
@maptoolkit/maplibre-3d-layersand three.js come with the control (dependencies, and bundled in the standalone build).- A Maptoolkit API key with access to the buildings tileset.
- A style with a scene (Street or Summer), or a scene passed as
scene. - A map created with
canvasContextAttributes: { antialias: true }, as in the examples above. MapLibre leaves multisampling off; without it the buildings and trees render with pixelated edges, and the far trees lose their soft leaf edges. The control cannot switch it on itself, because the WebGL context exists before it is added; it warns once in the console when it finds a context without multisampling.
Styling
The button takes its colors from two CSS custom properties on .maplibre-scene3d-control:
.maplibre-scene3d-control {
--scene3d-control-color: #333;
--scene3d-control-color-active: #0074d9;
}License
maplibre-scene3d-control is open-source under the BSD 3-Clause License.