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,
  ]);
}

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.

Animation with 3D Module

Animation with 3D Module

Examples

Animates a 3D model (UFO) around the Burj Khalifa using MaptilerAnimation and the 3D module.

Add events on 3D models

3D model events

Examples

Listen for mouse events on 3D models in maps.

Animate routes with a dynamic camera

Animate routes dynamic camera

Examples

Animate multidimensional routes with a dynamic camera that follows the journey in real-time.

Add multiple 3D models to the map

Add multiple 3D models

Examples

Incorporate multiple 3D models with adjustable parameters on maps.

Was this helpful?