Rendering Real‑Time 3D Terrain in Mapbox GL JS with Terrain‑RGB Tiles
Mapbox GL JS developers can now generate realistic 3D terrain on‑the‑fly using Terrain‑RGB tiles. This guide walks through adding a raster‑dem source, setting terrain, and optional custom shaders, while highlighting trade‑offs and monitoring tips.
18 Nov 2025, 22:20 UTC

Why 3D Terrain Matters and Where Terrain‑RGB Fits
Map developers often need a realistic elevation surface for flight simulators, game worlds, or urban planning tools. Traditional approaches pull separate DEM (digital elevation model) files and stitch them into a heightmap, which doubles bandwidth and adds a preprocessing step. Mapbox’s Terrain‑RGB tiles solve this by encoding elevation directly into the RGB channels of a standard vector tile. The same tiles can be used for 2D map rendering and, with a custom GLSL shader, for on‑the‑fly 3D displacement.
Feature Overview: What Terrain‑RGB Gives You
- Elevations are stored as 8‑bit per channel RGB values, a compact 24‑bit representation.
- The tiles are served from the same CDN that hosts Mapbox’s vector tiles, so no extra server is required.
- Mapbox GL JS can access the tiles via a
raster-demsource type, enabling theterrainproperty on the map. - Because the tiles are vector‑style, you can toggle the same layer for 2D or 3D with a single source URL.
Implementation Steps
- Enable the Terrain‑RGB service
Go to your Mapbox account, navigate to the Data section, and enable the Terrain‑RGB dataset for your region. Note the
mapbox://mapbox.terrain-rgbstyle URL that will be used in the source definition. - Set up the Mapbox GL JS map
mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN'; const map = new mapboxgl.Map({ container: 'map', style: 'mapbox://styles/mapbox/streets-v12', center: [-122.4194, 37.7749], zoom: 12, pitch: 60, bearing: -20 }); - Add a terrain source
map.addSource('terrain', { type: 'raster-dem', url: 'mapbox://mapbox.terrain-rgb', tileSize: 512, maxzoom: 14, encoding: 'raster-dem' // optional, defaults to 8-bit RGB }); - Apply the terrain to the map
map.setTerrain({ source: 'terrain', exaggeration: 1.0 });The
exaggerationfactor scales the height values; 1.0 shows true elevation, higher values accentuate ridges. - Optional: Add a custom shader for finer control
Mapbox GL JS exposes a
customLayertype that allows a GLSL fragment shader to manipulate the elevation data further. Below is a minimal example that demonstrates the RGB‑to‑height conversion formula used by Mapbox internally.map.addLayer({ id: 'terrain-shader', type: 'custom', renderingMode: '3d', onAdd: function (map, gl) { this.program = createShaderProgram(gl); }, render: function (gl, matrix) { gl.useProgram(this.program); // bind uniforms, textures, etc. gl.drawArrays(gl.TRIANGLES, 0, 6); } }); function createShaderProgram(gl) { const vertSrc = `#version 300 es in vec4 a_position; uniform mat4 u_matrix; void main() { gl_Position = u_matrix * a_position; }`; const fragSrc = `#version 300 es precision highp float; uniform sampler2D u_terrain; out vec4 outColor; const float elevationScale = 0.1; // meters per unit void main() { vec4 tex = texture(u_terrain, gl_FragCoord.xy / vec2(512.0)); float height = (tex.r * 256.0 + tex.g + tex.b / 256.0) * elevationScale; outColor = vec4(height, height, height, 1.0); }`; // compile and link shaders (omitted for brevity) return program; }In practice, you would use Mapbox’s built‑in
terrainproperty instead of writing a custom shader unless you need extra filtering or stylized displacement. - Monitor tile usage
Because Terrain‑RGB tiles count against your Mapbox quota, inspect the Analytics dashboard. A
429 Too Many Requestsresponse indicates you’ve hit the limit. Usemap.on('tileerror', …)to catch these errors in the browser console.
Trade‑Offs and Limitations
- Bandwidth vs. Detail – The 512‑pixel tiles are relatively small, but at high zoom levels (e.g.,
zoom > 14) they can still consume significant bandwidth if many tiles are requested simultaneously. Implement request throttling or limitmaxzoomto reduce traffic. - Memory Footprint – Each active tile occupies GPU memory for the height texture. On low‑end devices, this can cause frame drops. Consider using a lower
tileSize(e.g., 256) or amaxzoomthat matches your target audience. - Elevation Accuracy – The 24‑bit RGB encoding limits vertical resolution to about 0.1 m per unit. At very low zoom levels, the decoded height may appear flat. Adjust
exaggerationor use a higher‑resolution tileset if you need finer detail. - Licensing – The Terrain‑RGB service is part of Mapbox’s paid plans. Free tier usage may be constrained, and commercial projects must ensure they have the appropriate license.
- Shader Complexity – Custom GLSL shaders can introduce bugs if the RGB‑to‑height conversion is mis‑implemented. Stick with Mapbox’s built‑in terrain if you’re not comfortable with WebGL.
Actionable Next Steps
- Enable Terrain‑RGB in your Mapbox account and note the
mapbox://mapbox.terrain-rgbURL. - Create a simple Mapbox GL JS map, add the
raster-demsource, and callmap.setTerrain({ source: 'terrain' });. - Adjust
exaggerationto match your visual style and test on the target device family. - Set up a monitoring routine: add
map.on('tileerror', …)and check the Analytics dashboard for request counts. - If you need advanced displacement (e.g., custom shading or terrain‑based lighting), experiment with a
customlayer and the GLSL example above. - Review your Mapbox plan and consider upgrading if your tile usage regularly approaches the quota.
With these steps you can turn a single set of vector tiles into a fully interactive 3D terrain experience, all while keeping bandwidth low and keeping your asset pipeline simple.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.