Chapter 6: Terrain#
Welcome back! In the previous chapters, we've built up our map step-by-step. We created the fundamental map "window" (Chapter 1: Map Instance), defined its look and feel using the style (Chapter 2: Map Style), added different types of data sources (Chapter 3: Data Sources), and even learned how to efficiently load that data using the Chapter 4: PMTiles Protocol. Finally, in Chapter 5: Map Layers, we told MapLibre GL JS how to draw features like state boundaries, streams, and water bodies from those sources.
But if you look at the map we have so far, it's still flat. The mountains and valleys of Kentucky are represented only by the stream lines and perhaps a subtle hillshade image (raster
source). wouldn't it be amazing if we could make the map pop out, showing the actual height differences in the landscape?
This is where the Terrain feature comes in.
What is Terrain?#
Think about a physical relief map you might see in a museum or classroom. It's not just a flat picture; it has bumps and dips that show where the mountains are high and the valleys are low.
The Terrain feature in MapLibre GL JS allows your digital map to act like one of these physical relief maps. It uses special data that contains elevation (height above sea level) for every point on the map. By reading this elevation data, MapLibre can display the landscape in 3D, making mountains and valleys look realistic and giving depth to your map.
Instead of drawing layers on a perfectly flat surface, MapLibre draws them on a dynamically generated 3D surface based on the elevation data.
Our Goal: Enable 3D Terrain#
Our goal for this chapter is to understand how to enable this 3D terrain feature on our map using the elevation data source we already defined.
How to Enable Terrain#
Enabling terrain involves two main steps:
- Having a Data Source that provides elevation information.
- Telling the Map Instance to use that source for terrain and how much to "exaggerate" the height visually.
We've already completed the first step back in Chapter 3: Data Sources! Remember the dem
source?
// Inside the style object, within the sources...
dem: { // A name for our elevation source
type: "raster-dem", // <-- Special type for elevation data
url: "https://contig.us/data/tiles/pine-mtn/terrain.json", // <-- Points to elevation data config
},
dem
source, of type raster-dem
, is specifically designed to provide the elevation data needed for terrain. MapLibre knows how to read this data and understand the height values from it. Just like our vector sources, this raster-dem
source also uses the Chapter 4: PMTiles Protocol behind the scenes to fetch the data efficiently from a single file pointed to by the TileJSON URL.
Now, we need to tell the map instance to use this source for terrain. We do this using the map.setTerrain()
function. This function is typically called after the map has loaded its initial style and data, because the map needs to know about the dem
source defined in the style before it can use it for terrain.
In our map/main.js
file, you'll find this code within the map.on('load', ...)
event handler:
map.on("load", function () { // Wait for the map to fully load its style and data
map.setTerrain({ // <-- Tell the map to enable terrain
source: "dem", // <-- Use the source named "dem"
// exaggeration: 2, // Example for a different data type (Terrarium)
exaggeration: 0.0005, // <-- How much to visually stretch the height
});
});
Let's break this down:
map.on("load", function () { ... });
: This is an event listener. The code inside this function will only run after the map instance has finished loading its style and is ready to display. This is the correct place to enable terrain.map.setTerrain({...});
: This is the function that enables the terrain feature. You pass it a configuration object.source: "dem"
: This parameter is crucial. It tells MapLibre which of the sources defined in your map style (Chapter 3: Data Sources) contains the elevation data for the terrain. The value"dem"
must match the name (key
) of theraster-dem
source we defined earlier.exaggeration: 0.0005
: This parameter controls how much the height differences are visually amplified. A value of1
would show the true scale of height relative to distance (often looks very flat). A value higher than1
makes mountains look taller, and a value lower than1
makes them look flatter. The correct value depends on the specificraster-dem
data source used and how its values are encoded. The value0.0005
works well for the specific type of terrain data we are using in this project to make the height noticeable but not wildly distorted.
When this code runs after the map loads, MapLibre GL JS starts using the elevation data from the dem
source to render all the map layers on a 3D surface instead of a flat plane. You'll immediately see the landscape take shape!
Under the Hood: How Terrain is Rendered#
What happens internally when you call map.setTerrain({ source: "dem", exaggeration: 0.0005 })
?
Here's a simplified view:
sequenceDiagram
participant MapLibre as MapLibre GL JS
participant TerrainModule as Terrain Module
participant DEMSource as raster-dem Source ("dem")
participant DrawingEngine as Drawing Engine
MapLibre->MapLibre: Map finishes loading style
MapLibre->TerrainModule: Call map.setTerrain({source: "dem", exaggeration: X})
TerrainModule->TerrainModule: Get configuration (source name, exaggeration)
TerrainModule->MapLibre: Register terrain enabled with source "dem"
alt Whenever map view changes (pan/zoom)
MapLibre->TerrainModule: "Need elevation data for this area"
TerrainModule->DEMSource: Request necessary DEM tiles (e.g., z/x/y)
DEMSource-->TerrainModule: Provide DEM tile data (bytes representing height)
TerrainModule->TerrainModule: Decode raw height values from tile data
TerrainModule->DrawingEngine: Provide height data and exaggeration factor (X)
MapLibre->DrawingEngine: Also provide data from other layers (lines, fills, raster images)
DrawingEngine->DrawingEngine: Calculate 3D surface based on height data and exaggeration
DrawingEngine->DrawingEngine: Project and draw all map layers onto this 3D surface
end
DrawingEngine->MapLibre: Display updated canvas
Essentially:
- When terrain is enabled, MapLibre knows to treat the
raster-dem
source differently. - As the map view changes (you pan, zoom, or tilt), MapLibre's rendering engine needs to know the height of the land in the visible area.
- It requests the necessary elevation data tiles from the specified
raster-dem
source ("dem"). If the source URL usespmtiles://
(like ours does), the PMTiles Protocol handler intercepts the request and fetches the needed bytes from the single.pmtiles
file containing the elevation data. - MapLibre decodes the raw height values from the received elevation data tiles.
- It then uses these decoded height values and the
exaggeration
factor you provided to calculate the 3D shape of the ground for the current view. - Crucially, MapLibre's drawing engine then renders all the other layers (the boundaries, streams, hillshade, etc.) on top of this calculated 3D terrain surface.
- The result is a map where the layers appear to draped over the landscape, showing mountains and valleys popping out.
This process happens continuously and efficiently as you interact with the map, ensuring the terrain and layers are always rendered correctly for the current 3D view.
Visualizing Terrain#
Once map.setTerrain
is called, you can interact with the map in new ways. In addition to panning and zooming, you can often hold down the Ctrl
key (or β Command
on Mac) and drag your mouse to tilt the map, getting a better view of the 3D landscape. You can also rotate it by holding down Shift
and dragging. These interactions are automatically handled by MapLibre when terrain is enabled.
You'll see the dark gray USA boundary from our first layer, the blue streams and water bodies, and the hillshade image all rendered over the hills and mountains of Kentucky, bringing the landscape to life!
Conclusion#
In this chapter, we learned how to add a new dimension to our map by enabling Terrain. This feature transforms a flat map into a dynamic 3D landscape by using elevation data. We saw that this requires a special raster-dem
data source (like the dem
source we defined) and enabling the feature on the map instance using the map.setTerrain()
function, typically within the map.on('load', ...)
event. We also understood how the exaggeration
parameter controls the visual height of the terrain and how MapLibre uses the elevation data to render all layers on a 3D surface.
This completes our exploration of the core concepts in this tutorial project, covering how to get a map on the screen, style it with different data sources (including vector and raster), efficiently load data using PMTiles, draw features using layers, and finally, add realistic 3D terrain.
Generated by AI Codebase Knowledge Builder