Skip to content

Show 3D Buildings and Trees in MapLibre GL JS

On this page

License Version Downloads

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

The 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

OptionTypeDefaultDescription
apiKeystring–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.
sceneScene3dManifest | string | nullthe style’s sceneA scene, or the URL of one, to show instead of the style’s own. null shows none.
enabledbooleantrueShow the scene right away.
buttonbooleanfalseAdd a 3D button that switches the scene on and off. Without it the control has no UI.
minzoomnumber12Zoom from which the 3D library is loaded. The buildings tileset has no tiles below zoom 12.
hideLayersstring[]["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.
modelBasestringthe page’s base URLBase URL for relative model URLs in a scene.
library() => Promise<Scene3dLibrary>the 3D library that comes with the controlLoads 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.
idPrefixstring"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
MethodDescription
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));
EventPayloadFired 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-layers and 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.