Import and play GLTF animations from GLTF files
Use the MapTiler 3D JS module to play animations in a GLTF model and move the models to simulate a flight over the map
This demonstration showcases how to add a animated 3D model in a map and moved it acroos the map.
NPM module setup
npm install --save @maptiler/sdk @maptiler/3d lil-gui
import { Map, MapStyle, config, math } from '@maptiler/sdk';
import '@maptiler/sdk/dist/maptiler-sdk.css';
import { Layer3D, AltitudeReference } from '@maptiler/3d';
import { GUI } from 'lil-gui';
config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';
const lakeNatron = [36.01695746068115, -2.3536069164210653];
const makadikadi = [25.5527055978211, -20.78468929963636];
function calculateHeading(lat1, lon1, lat2, lon2) {
const φ1 = math.toRadians(lat1);
const φ2 = math.toRadians(lat2);
const Δλ = math.toRadians(lon2 - lon1);
const y = Math.sin(Δλ) * Math.cos(φ2);
const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);
let θ = Math.atan2(y, x);
θ = math.toDegrees(θ);
return ((θ + 360) % 360) + 90; // Normalize to 0–360 and add 90 degrees
}
const map = new Map({
container: 'map',
style: MapStyle.AQUARELLE.VIVID,
center: lakeNatron, // starting position [lng, lat]
zoom: 10, // starting zoom
hash: true,
bearing: 0,
pitch: 45,
attributionControl: {
customAttribution: "Model by <a href='https://mirada.com/' target='_blank'>Mirada</a> for <a href='https://experiments.withgoogle.com/3-dreams-of-black' target='_blank'>3 Dreams of Black</a>",
}
});
// Waiting for the map to be ready
map.on('ready', async () => {
map.setSky({
"sky-color": "#0C2E4B",
"horizon-color": "#09112F",
"fog-color": "#09112F",
"fog-ground-blend": 0.5,
"horizon-fog-blend": 0.1,
"sky-horizon-blend": 1.0,
"atmosphere-blend": 0.5,
});
// Create a Layer3D and add it
const layer3D = new Layer3D("custom-3D-layer");
map.addLayer(layer3D);
// Increasing the intensity of the ambient light
layer3D.setAmbientLight({ intensity: 2 });
// Adding a point light
layer3D.addPointLight("point-light", { intensity: 30 });
const flamingoIDOne = "flamingo";
const flamingoIDTwo = "flamingo-clone";
// The call can be awaited for the whole download of the mesh to complete
await layer3D.addMeshFromURL(
// ID to give to this mesh, unique within this Layer3D instance
flamingoIDOne,
// The URL of the mesh
"https://docs-media.maptiler.com/docs/models/flamingo.glb",
// A set of options, these can be modified later
{
lngLat: lakeNatron,
heading: 12,
scale: 100,
animationMode: "continuous",
altitudeReference: AltitudeReference.MEAN_SEA_LEVEL,
transform: {
rotation: {
x: 0,
y: Math.PI / 2,
z: 0,
},
offset: {
x: 0,
y: 0,
z: -150,
},
},
}
);
const fly = "fly!";
const migrate = "migrate with the flamingo";
const speed = "migration speed";
const guiObj = {
[fly]: true,
[migrate]: true,
[speed]: 0.0005,
};
const gui = new GUI({ width: 500 });
gui.add(guiObj, migrate);
gui.add(guiObj, fly).onChange((play) => {
if (play) {
playAnimation();
}
});
gui.add(guiObj, speed, 0, 0.001);
const mesh = layer3D.getItem3D("flamingo");
let progress = 0;
const animationNames = mesh?.getAnimationNames();
const animationName = animationNames?.[0];
if (!animationName) {
throw new Error(`No animation found with name '${animationName}'`);
}
layer3D.cloneMesh(flamingoIDOne, flamingoIDTwo, {
animationMode: "manual",
transform: {
offset: {
x: 0,
y: 0,
z: 300,
},
},
});
layer3D.getItem3D(flamingoIDOne)?.playAnimation(
animationName,
"loop",
);
layer3D.getItem3D(flamingoIDTwo)?.playAnimation(
animationName,
"loop",
);
const distance = math.haversineDistanceWgs84(lakeNatron, makadikadi);
const initialHeading = calculateHeading(makadikadi[1], makadikadi[0], lakeNatron[1], lakeNatron[0]);
const flamingoOneMesh = layer3D.getItem3D(flamingoIDOne);
const flamingoTwoMesh = layer3D.getItem3D(flamingoIDTwo);
flamingoOneMesh.modify({ heading: initialHeading });
flamingoTwoMesh.modify({ heading: initialHeading });
function playAnimation() {
progress += guiObj[speed];
if (progress > 1) {
progress = 0;
}
const nextPosition = math.haversineIntermediateWgs84(lakeNatron, makadikadi, progress - (guiObj[speed] === 0 ? 0.001 : guiObj[speed]));
const position = math.haversineIntermediateWgs84(lakeNatron, makadikadi, progress);
const roughHeading = calculateHeading(position[1], position[0], nextPosition[1], nextPosition[0]);
// `updateAnimation` is only needed if you want to control the animation manually
// to automatically play the animation independently of map updates, set the item
// animationMode: "continuous"
flamingoTwoMesh.updateAnimation(guiObj[speed] * 50);
const distanceFromStart = math.haversineDistanceWgs84(lakeNatron, [position[0], position[1]]);
const progressPercentage = distanceFromStart / distance;
flamingoOneMesh.modify({ lngLat: position, heading: roughHeading });
flamingoTwoMesh.modify({ lngLat: position, heading: roughHeading });
if (guiObj[migrate]) {
map.setCenter(position);
map.setZoom(10 - Math.sin(progressPercentage * Math.PI) * 2);
map.setBearing(progressPercentage * -45);
}
if (guiObj[fly]) {
requestAnimationFrame(playAnimation);
}
}
if (guiObj[fly]) {
playAnimation()
}
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<title>Import and play GLTF animations from GLTF files | NPM example</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div id="map"></div>
<script type="module" src="./main.js"></script>
</body>
</html>
body {margin: 0; padding: 0;}
#map {position: absolute; top: 0; bottom: 0; width: 100%;}
.lil-gui.autoPlace {
right: inherit;
left: 15px;
}
Model by Mirada for 3 Dreams of Black ↩