Interactive 3D urban explorer with animated building components
Explore a dynamic 3D urban environment where buildings come to life. Hover over architectural layers to see smooth animations, highlighting, and detailed information in this advanced MapTiler 3D SDK demonstration.
This demonstration showcases the advanced capabilities of the MapTiler 3D SDK in creating immersive and reactive urban environments.
NPM module setup
npm install --save @maptiler/sdk @maptiler/3d
import { Map, MapStyle, config, Popup, Marker } from '@maptiler/sdk';
import '@maptiler/sdk/dist/maptiler-sdk.css';
import { Layer3D, AltitudeReference } from '@maptiler/3d';
import * as THREE from 'three';
const center = [16.979498863220215, 51.09624562642054];
const originalColor = "#777";
const buildings = [
{
center: [16.980518102645874, 51.09648818873717],
model: "apartment-block-g-shape.glb",
markerId: "residential-complex-marker",
title: "Residential Complex",
description: "A residential complex with 100 apartments and 1000 residents.",
hoverColor: "dodgerblue",
},
{
center: [16.979032158851624, 51.09587504264633],
model: "apartment-block-sw.glb",
markerId: "business-center-marker",
title: "Business Center & Retail Hub",
description: "A business center and retail hub with 100 offices and 1000 employees.",
hoverColor: "dodgerblue",
},
{
center: [16.977733969688416, 51.096447761772765],
model: "farmers-market.glb",
altitude: 30,
markerId: "farmers-market-marker",
title: "Farmers Market & Exhibition Center",
description: "A farmers market and exhibition center with 100 vendors and 1000 visitors.",
hoverColor: "dodgerblue",
},
{
center: [16.98014795780182, 51.09567627376188],
model: "four-group-block.glb",
markerId: "research-hub-marker",
title: "Office Buildings, Research Labs & Innovation Hub",
description: "Office buildings, research labs and an innovation hub with 100 offices and 1000 employees.",
hoverColor: "dodgerblue",
},
];
// Helper functions
function lerp(a, b, t) {
return a + (b - a) * t;
}
function cubicInOut(t) {
return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
}
// Animation Manager Class
class FloorAnimationManager {
constructor() {
this.animationSpeed = 0.02;
this.activeFloor = null;
this.floors = new Map();
this.animationFrameId = null;
this.config = {
baseOffsetX: 0,
baseOpacity: 0.75,
baseColor: new THREE.Color(originalColor),
activeOffsetX: -20,
activeOpacity: 1,
activeColor: new THREE.Color("dodgerblue").multiplyScalar(0.5),
};
}
addFloor(floor) {
this.floors.set(floor, 0);
floor.mesh?.traverse((child) => {
if (child.isMesh) {
child.material.map = null;
child.material.color.set(this.config.baseColor);
}
});
}
animateFloorToDelta(floor, delta) {
const {
baseOffsetX, activeOffsetX,
baseOpacity, activeOpacity,
baseColor, activeColor
} = this.config;
const easedDelta = cubicInOut(delta);
const newOpacity = lerp(baseOpacity, activeOpacity, easedDelta);
floor.setOpacity(newOpacity);
if (floor.mesh) {
const newOffsetX = lerp(baseOffsetX, activeOffsetX, easedDelta);
floor.mesh.position.x = newOffsetX;
floor.mesh.position.z = -newOffsetX;
floor.mesh.traverse((child) => {
if (child.isMesh) {
child.material.color.lerpColors(baseColor, activeColor, easedDelta);
}
});
}
}
setActiveFloor(floor) {
this.activeFloor = floor;
}
getActiveFloor() {
return this.activeFloor;
}
animate() {
this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
this.floors.forEach((currentDelta, floor) => {
const delta =
floor === this.activeFloor
? Math.min(1, currentDelta + this.animationSpeed)
: Math.max(0, currentDelta - this.animationSpeed);
if (delta !== currentDelta) {
this.floors.set(floor, delta);
this.animateFloorToDelta(floor, delta);
}
});
}
}
config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';
const map = new Map({
container: 'map',
style: MapStyle.STREETS,
center: center,
maxPitch: 85,
terrainControl: true,
maptilerLogo: true,
projectionControl: true,
zoom: 18,
pitch: 60,
bearing: 55,
hash: true
});
// Waiting for the map to be ready
map.on('ready', async () => {
// Hide default 3D buildings to replace them with our models
hideBuildingsInPolygon(map);
// Add 3D Layer
const layer3D = new Layer3D("xx-dev");
map.addLayer(layer3D);
// Lights
layer3D.setAmbientLight({ intensity: 8 });
layer3D.addPointLight("point-light", {
intensity: 10,
altitude: 5000,
lngLat: center,
altitudeReference: AltitudeReference.MEAN_SEA_LEVEL,
});
layer3D.addPointLight("point-light-2", {
intensity: 10,
altitude: 5000,
lngLat: [-0.016979498863220217, -0.05109624562642054],
altitudeReference: AltitudeReference.MEAN_SEA_LEVEL,
});
// Load general buildings
for (const building of buildings) {
const buildingItem3D = await layer3D.addMeshFromURL(
building.model,
`https://docs-media.maptiler.com/docs/models/buildings/${building.model}`,
{
lngLat: building.center,
altitude: building.altitude || 0,
heading: 20,
scale: 0.6,
altitudeReference: AltitudeReference.GROUND,
}
);
// Add Markers
const markerElement = document.getElementById(building.markerId);
const buildingMarker = new Marker({ element: markerElement, subpixelPositioning: true, })
.setLngLat(building.center)
.setPopup(new Popup({ offset: [0, -30] })
.setHTML(`<h3>${building.title}</h3><p>${building.description}</p>`))
.addTo(map);
// Interaction
buildingItem3D.mesh?.traverse((child) => {
if (child.isMesh) {
child.material.map = null;
child.material.color.set(originalColor);
}
});
buildingItem3D.on("mouseenter", () => {
buildingItem3D.mesh?.traverse((child) => {
if (child.isMesh) {
child.material.color.set(new THREE.Color(building.hoverColor));
}
});
});
buildingItem3D.on("mouseleave", () => {
buildingItem3D.mesh?.traverse((child) => {
if (child.isMesh) child.material.color.set(originalColor);
});
});
}
// Special: Luxury Apartment with animated floors
const numFloorsInApartment = 10;
const floorAnimationManager = new FloorAnimationManager();
const apartmentFloorBase = await layer3D.addMeshFromURL(
"apartment-floor",
`https://docs-media.maptiler.com/docs/models/buildings/apartment-floor.glb`,
{
lngLat: [16.978970468042007, 51.09669537637524],
altitude: 0,
heading: 20,
scale: 0.75,
altitudeReference: AltitudeReference.GROUND,
opacity: 0.5,
}
);
// Marker for Luxury Apartment
new Marker({ element: document.getElementById("luxury-apartment-marker") })
.setLngLat([16.978656377757375, 51.09634493829725])
.setPopup(new Popup({ offset: [0, -30] })
.setHTML(`<h3>Luxury Apartment</h3><p>A luxury apartment with 3 bedrooms and 2 bathrooms.</p>`))
.addTo(map);
// Create floors
for (let i = 0; i < numFloorsInApartment; i++) {
const newFloor = apartmentFloorBase.clone(`apartment-floor-${i}`);
newFloor.setAltitude(0.65 * i * 10);
floorAnimationManager.addFloor(newFloor);
newFloor.on("mouseenter", () => floorAnimationManager.setActiveFloor(newFloor));
newFloor.on("mouseleave", () => {
if (floorAnimationManager.getActiveFloor() === newFloor) {
floorAnimationManager.setActiveFloor(null);
}
});
}
// Add Roof
await layer3D.addMeshFromURL(
"apartment-roof",
`https://docs-media.maptiler.com/docs/models/buildings/apartment-roof.glb`,
{
lngLat: [16.978962032459123, 51.09671170177478],
altitude: numFloorsInApartment * 0.65 * 10 - 1,
heading: 20,
scale: 0.76,
opacity: 0.2,
altitudeReference: AltitudeReference.GROUND,
}
);
floorAnimationManager.animate();
});
function replaceLayerSource(map, layerId, newSourceId) {
const style = map.getStyle();
const layer = style.layers.find(l => l.id === layerId);
if (!layer) {
throw new Error(`Layer ${layerId} no existe`);
}
const layerIndex = style.layers.findIndex(l => l.id === layerId);
const beforeLayerId = style.layers[layerIndex + 1]?.id;
const newLayer = JSON.parse(JSON.stringify(layer));
newLayer.source = newSourceId;
map.removeLayer(layerId);
map.addLayer(newLayer, beforeLayerId);
}
function hideBuildingsInPolygon(map) {
//use MapTiler building source for buildings IDs
map.addSource("buildings", {
type: "vector",
url: "https://api.maptiler.com/tiles/buildings/tiles.json",
});
replaceLayerSource(map, "Building 3D", "buildings");
// Simple building filter to hide existing map buildings in this area
const internalBuildingIds = ["3229742242", "3229937730", "3229844962", "3230119458",
"3230021218", "3229314786", "3229333346", "3229530402", "3229295266", "3229567618",
"3229911586", "3229729666", "3229348162", "3229797666", "3229905506", "3229957698",
"3229844066"];
map.setFilter("Building 3D", [
"match",
["to-string", ["id"]],
internalBuildingIds,
false,
true,
]);
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<title>MapTiler 3D | Item3D UI States</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div id="map"></div>
<div id="research-hub-marker" class="marker"></div>
<div id="business-center-marker" class="marker"></div>
<div id="farmers-market-marker" class="marker"></div>
<div id="residential-complex-marker" class="marker"></div>
<div id="luxury-apartment-marker" class="marker"></div>
<script type="module" src="./main.js"></script>
</body>
</html>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
.marker {
background-size: cover;
width: 50px;
height: 50px;
border-radius: 50%;
cursor: pointer;
left: -100px;
position: absolute;
}
#research-hub-marker {
background-image: url("../img/research-hub.png");
}
#business-center-marker {
background-image: url("../img/business-center.png");
}
#farmers-market-marker {
background-image: url("../img/farmers-market.png");
}
#residential-complex-marker {
background-image: url("../img/residential-complex.png");
}
#luxury-apartment-marker {
background-image: url("../img/luxury-apartment.png");
}
Learn more
The app integrates high-quality GLB models directly into a high-performance geographic context.
The highlight of this example is the sophisticated interactive floor system implemented in the “Luxury Apartment” building. Unlike static 3D models, the building is composed of individual floor layers that respond dynamically to user input. When a user hovers their cursor over a specific floor, the app triggers a smooth, non-linear animation on the selected floor.
In addition to these micro-interactions, the map features custom HTML markers with unique icons for each landmark. Clicking these markers reveals detailed popups, providing a rich narrative layer to the 3D scene. This example serves as a powerful reference for urban planning, real estate presentations, and any application requiring granular interaction with complex 3D structures.
Related examples
Animation with 3D Module
ExamplesAnimates a 3D model (UFO) around the Burj Khalifa using MaptilerAnimation and the 3D module.
Animate routes dynamic camera
ExamplesAnimate multidimensional routes with a dynamic camera that follows the journey in real-time.