3D Terrain in Maptoolkit Maps JS
Maptoolkit Maps JS supports true 3D terrain rendering by combining a raster-dem elevation source with the map’s terrain option. This example shows how to enable 3D terrain with pitch and a sky layer to create a realistic mountain landscape view. Use this technique to build hiking apps, route planners, or any visualization where actual elevation matters more than a flat hillshade.
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: 12,
center: [11.39085, 47.27574],
pitch: 70,
maxZoom: 18,
maxPitch: 85,
attributionControl: { compact: false }
});
map.addControl(new maptoolkit.NavigationControl(), 'top-right');
map.on('load', () => {
map.addSource('terrain-rgb', {
type: 'raster-dem',
tiles: [`https://tiles.maptoolkit.net/terrain/{z}/{x}/{y}.webp?api_key=${API_KEY}`],
tileSize: 256,
minzoom: 5,
maxzoom: 12,
encoding: 'terrarium'
});
map.setTerrain({ source: 'terrain-rgb', exaggeration: 1.0 });
// Force one render after the DEM source loads so terrain shows on initial load
map.once('idle', () => map.triggerRepaint());
map.setSky({
'sky-color': '#199EF3',
'sky-horizon-blend': 0.5,
'horizon-color': '#ffffff',
'horizon-fog-blend': 0.5,
'fog-color': '#0000ff',
'fog-ground-blend': 0.9
});
});<!DOCTYPE html>
<html lang="en">
<head>
<title>3D Terrain - Maptoolkit Maps JS</title>
<meta property="og:description" content="Go beyond hillshade and show elevation in actual 3D." />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@maptoolkit/maps@11.0.0-beta.3/dist/maptoolkit.css" />
<style>
html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
#map { width: 100%; height: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
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: 12,
center: [11.39085, 47.27574],
pitch: 70,
maxZoom: 18,
maxPitch: 85,
attributionControl: { compact: false }
});
map.addControl(new maptoolkit.NavigationControl(), 'top-right');
map.on('load', () => {
map.addSource('terrain-rgb', {
type: 'raster-dem',
tiles: [`https://tiles.maptoolkit.net/terrain/{z}/{x}/{y}.webp?api_key=${API_KEY}`],
tileSize: 256,
minzoom: 5,
maxzoom: 12,
encoding: 'terrarium'
});
map.setTerrain({ source: 'terrain-rgb', exaggeration: 1.0 });
// Force one render after the DEM source loads so terrain shows on initial load
map.once('idle', () => map.triggerRepaint());
map.setSky({
'sky-color': '#199EF3',
'sky-horizon-blend': 0.5,
'horizon-color': '#ffffff',
'horizon-fog-blend': 0.5,
'fog-color': '#0000ff',
'fog-ground-blend': 0.9
});
});
</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.
How it works
Terrain is two separate things: a source that carries elevation, and a camera angle that makes it visible.
The source is raster-dem, not raster. A raster source would paint the tiles as a
picture; raster-dem tells the renderer to read each pixel as a height value and build a
mesh from it.
encoding: 'terrarium' is required. The tiles are Terrarium-encoded even though the path says terrain, and the wrong encoding does not error: it decodes the same pixels with the wrong formula and produces plausible but incorrect elevation.
setTerrain runs inside map.on('load') because it attaches to the loaded style. Calling
it at construction time throws.
pitch: 70 is what makes the relief visible, and maxPitch: 85 raises the ceiling above
the default so a user can tilt further toward the horizon. Looking straight down hides the
effect completely.
minzoom: 5 and maxzoom: 12 describe the tileset rather than a preference. Zoom 12 is its
maximum, so the renderer overzooms past that point instead of fetching more detail.
Next steps
Tuning comes first. exaggeration on setTerrain scales the relief, and above 1 it flatters
the landscape rather than describing it, which matters if anyone plans from the map.
Terrain then works as a stage for something else. A route drawn over tilted terrain is much easier to read than the same route flat, and the Routing API returns that geometry while the Elevation API turns it into a climb profile. Giving the user a toggle rather than starting tilted is usually right, since terrain is the most expensive thing on the page.