Add a 3D Model Using three.js
Placing a GLTF model at a real-world location on a Maptoolkit map requires a custom layer that connects three.js to the map’s WebGL context. The model is positioned using geographic coordinates converted to the map’s Mercator projection, so it stays locked to the correct spot as the camera moves. Use this technique to overlay architectural models, vehicle assets, or any 3D object at a specific address or point of interest.
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
}
}
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const API_KEY = 'YOUR_API_KEY';
const map = new maptoolkit.Map({
container: 'map',
apiKey: API_KEY,
style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
zoom: 18,
center: [11.39085, 47.27574],
pitch: 60,
attributionControl: { compact: false },
canvasContextAttributes: { antialias: true }
});
map.addControl(new maptoolkit.NavigationControl(), 'top-right');
const modelOrigin = [11.39085, 47.27574];
const modelAltitude = 0;
const modelRotate = [Math.PI / 2, 0, 0];
const modelAsMercatorCoordinate = maptoolkit.MercatorCoordinate.fromLngLat(modelOrigin, modelAltitude);
const modelTransform = {
translateX: modelAsMercatorCoordinate.x,
translateY: modelAsMercatorCoordinate.y,
translateZ: modelAsMercatorCoordinate.z,
rotateX: modelRotate[0],
rotateY: modelRotate[1],
rotateZ: modelRotate[2],
scale: modelAsMercatorCoordinate.meterInMercatorCoordinateUnits()
};
const customLayer = {
id: '3d-model',
type: 'custom',
renderingMode: '3d',
onAdd(map, gl) {
this.camera = new THREE.Camera();
this.scene = new THREE.Scene();
const directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(0, -70, 100).normalize();
this.scene.add(directionalLight);
const directionalLight2 = new THREE.DirectionalLight(0xffffff);
directionalLight2.position.set(0, 70, 100).normalize();
this.scene.add(directionalLight2);
const loader = new GLTFLoader();
loader.load(
'https://maplibre.org/maplibre-gl-js/docs/assets/34M_17/34M_17.gltf',
(gltf) => { this.scene.add(gltf.scene); }
);
this.map = map;
this.renderer = new THREE.WebGLRenderer({
canvas: map.getCanvas(),
context: gl,
antialias: true
});
this.renderer.autoClear = false;
},
render(gl, args) {
const rotationX = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(1, 0, 0), modelTransform.rotateX);
const rotationY = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0, 1, 0), modelTransform.rotateY);
const rotationZ = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0, 0, 1), modelTransform.rotateZ);
const m = new THREE.Matrix4().fromArray(args.defaultProjectionData.mainMatrix);
const l = new THREE.Matrix4()
.makeTranslation(modelTransform.translateX, modelTransform.translateY, modelTransform.translateZ)
.scale(new THREE.Vector3(modelTransform.scale, -modelTransform.scale, modelTransform.scale))
.multiply(rotationX).multiply(rotationY).multiply(rotationZ);
this.camera.projectionMatrix = m.multiply(l);
this.renderer.resetState();
this.renderer.render(this.scene, this.camera);
this.map.triggerRepaint();
}
};
map.on('style.load', () => {
map.addLayer(customLayer);
});<!DOCTYPE html>
<html lang="en">
<head>
<title>Add a 3D Model Using three.js - Maptoolkit Maps JS</title>
<meta property="og:description" content="Use a custom style layer with three.js to add a 3D model to the map." />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/@maptoolkit/[email protected]/dist/maptoolkit.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@maptoolkit/[email protected]/dist/maptoolkit.css" />
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
}
}
</script>
<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 THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const API_KEY = 'YOUR_API_KEY';
const map = new maptoolkit.Map({
container: 'map',
apiKey: API_KEY,
style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
zoom: 18,
center: [11.39085, 47.27574],
pitch: 60,
attributionControl: { compact: false },
canvasContextAttributes: { antialias: true }
});
map.addControl(new maptoolkit.NavigationControl(), 'top-right');
const modelOrigin = [11.39085, 47.27574];
const modelAltitude = 0;
const modelRotate = [Math.PI / 2, 0, 0];
const modelAsMercatorCoordinate = maptoolkit.MercatorCoordinate.fromLngLat(modelOrigin, modelAltitude);
const modelTransform = {
translateX: modelAsMercatorCoordinate.x,
translateY: modelAsMercatorCoordinate.y,
translateZ: modelAsMercatorCoordinate.z,
rotateX: modelRotate[0],
rotateY: modelRotate[1],
rotateZ: modelRotate[2],
scale: modelAsMercatorCoordinate.meterInMercatorCoordinateUnits()
};
const customLayer = {
id: '3d-model',
type: 'custom',
renderingMode: '3d',
onAdd(map, gl) {
this.camera = new THREE.Camera();
this.scene = new THREE.Scene();
const directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(0, -70, 100).normalize();
this.scene.add(directionalLight);
const directionalLight2 = new THREE.DirectionalLight(0xffffff);
directionalLight2.position.set(0, 70, 100).normalize();
this.scene.add(directionalLight2);
const loader = new GLTFLoader();
loader.load(
'https://maplibre.org/maplibre-gl-js/docs/assets/34M_17/34M_17.gltf',
(gltf) => { this.scene.add(gltf.scene); }
);
this.map = map;
this.renderer = new THREE.WebGLRenderer({
canvas: map.getCanvas(),
context: gl,
antialias: true
});
this.renderer.autoClear = false;
},
render(gl, args) {
const rotationX = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(1, 0, 0), modelTransform.rotateX);
const rotationY = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0, 1, 0), modelTransform.rotateY);
const rotationZ = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0, 0, 1), modelTransform.rotateZ);
const m = new THREE.Matrix4().fromArray(args.defaultProjectionData.mainMatrix);
const l = new THREE.Matrix4()
.makeTranslation(modelTransform.translateX, modelTransform.translateY, modelTransform.translateZ)
.scale(new THREE.Vector3(modelTransform.scale, -modelTransform.scale, modelTransform.scale))
.multiply(rotationX).multiply(rotationY).multiply(rotationZ);
this.camera.projectionMatrix = m.multiply(l);
this.renderer.resetState();
this.renderer.render(this.scene, this.camera);
this.map.triggerRepaint();
}
};
map.on('style.load', () => {
map.addLayer(customLayer);
});
</script>
</body>
</html>Use the prompt below with any LLM to get the same result. Make sure the Maptoolkit MCP server is connected first — check out AI Integration & MCP to get started.
Use the Maptoolkit Connector. Create an interactive map with zoom level 18, centered around [11.39085, 47.27574], with pitch 60. Add a 3D model using three.js from https://maplibre.org/maplibre-gl-js/docs/assets/34M_17/34M_17.gltf.