Short answer

Raster tiles let you put a large DEM or hillshade on the web without forcing the browser to download gigabytes of elevation data. The surface is pre-cut into small square images (typically 256 or 512 px) arranged on the Web Mercator (EPSG:3857) XYZ pyramid, and the client fetches only the few tiles visible at the current zoom. For terrain specifically you have two routes: ship pre-rendered hillshade tiles (a fixed picture of relief) or ship Terrain-RGB tiles that encode elevation per pixel so the client can compute hillshade, contours, and 3D on the fly.

The right choice depends on whether users need to change the lighting and exaggeration interactively, and on how much GPU work you are willing to push to the browser.

The XYZ tiling scheme

The dominant scheme is the OSGeo/Google XYZ standard: the world is projected to Web Mercator and recursively quartered. Zoom 0 is a single tile covering the globe; zoom z has 2^z by 2^z tiles. A tile is addressed /{z}/{x}/{y}.png, with x increasing eastward and y increasing southward from the top-left. WMTS is the OGC-standardised equivalent with a capabilities document and named tile matrix sets; XYZ is the lighter de facto convention that MapLibre, Leaflet, and OpenLayers all consume directly.

Two consequences for terrain work:

  • Web Mercator distorts area with latitude. At 60 degrees north a Mercator pixel covers half the ground distance it does at the equator. Hillshade baked at one zoom looks correct at that zoom but the relief impression changes as you zoom, because the effective resolution per ground metre changes.
  • Resolution is quantised by zoom. Each zoom roughly halves ground sample distance. A 30 m DEM is fully resolved around zoom 12-13; serving tiles past that just upsamples.

Pre-rendered hillshade vs Terrain-RGB

Pre-rendered hillshade. You compute a hillshade once with gdaldem hillshade (choosing a sun azimuth, usually 315 degrees from the NW, and an altitude of ~45 degrees), style it, and cut it into RGB image tiles. Pros: trivial to serve, fast, works in any tile client. Cons: the light direction and vertical exaggeration are frozen at render time, and a single fixed azimuth hides landforms that run parallel to the light.

Terrain-RGB. Elevation is encoded into the 8-bit R, G, B channels of a normal raster tile using a documented formula. The widely used Mapbox/MapLibre encoding is:

height = -10000 + ((R * 256 * 256 + G * 256 + B) * 0.1)

This gives 0.1 m vertical precision over a huge range. The client decodes height per pixel on the GPU and can render hillshade with a user-controlled sun position, compute slope shading, or extrude a 3D terrain mesh. Pros: interactive lighting, exaggeration, and 3D from one tileset. Cons: more GPU load and a strict requirement that the encoding match what the client expects.

Worked example: DEM to MapLibre

Route A - static hillshade tiles.

# 1. reproject DEM to Web Mercator
gdalwarp -t_srs EPSG:3857 -r bilinear dem_utm.tif dem_3857.tif

# 2. render a hillshade
gdaldem hillshade -az 315 -alt 45 -z 1.5 dem_3857.tif hillshade.tif

# 3. cut XYZ tiles for zooms 6-13
gdal2tiles.py -p mercator -z 6-13 --xyz -w none hillshade.tif tiles/

Then in MapLibre, add a raster source pointing at tiles/{z}/{x}/{y}.png and a raster layer. Done.

Route B - Terrain-RGB with client-side hillshade. Encode the DEM to Terrain-RGB (rio-rgbify or a GDAL VRT pixel function), tile it, then in the MapLibre style declare a raster-dem source with the matching encoding and add a hillshade layer:

{
  "sources": {
    "dem": {
      "type": "raster-dem",
      "tiles": ["https://example.com/terrain/{z}/{x}/{y}.png"],
      "tileSize": 512,
      "encoding": "mapbox"
    }
  },
  "layers": [
    { "id": "relief", "type": "hillshade", "source": "dem",
      "paint": { "hillshade-illumination-direction": 315,
                 "hillshade-exaggeration": 0.6 } }
  ]
}

Now the sun direction and exaggeration are paint properties you can wire to a UI control. The same raster-dem source also drives MapLibre's 3D terrain (setTerrain) for a globe-style tilt view.

Serving: static cache vs dynamic tiler

  • Static tiles (the gdal2tiles output) are just files on object storage or a CDN. Cheapest and fastest for stable datasets, but a full pyramid for a large area is many files and a slow rebuild when the DEM updates.
  • Cloud Optimized GeoTIFF (COG) plus a dynamic tiler such as TiTiler keeps one COG and renders tiles on request, applying hillshade or a colour ramp at read time. This avoids pre-cutting millions of tiles and lets you re-style without re-tiling, at the cost of running a service and caching hot tiles.

For most geological dashboards a COG plus dynamic tiler is the pragmatic middle ground: one well-built COG (gdal_translate ... -of COG -co OVERVIEWS=AUTO) with internal overviews serves every zoom from a single file.

Common pitfalls and why they happen

  • Seams or stepped relief between tiles. Hillshade was computed per tile instead of on the full DEM before tiling. Always render the derivative on the seamless mosaic, then cut.
  • Terrain-RGB renders as garbage. The client's encoding does not match how the tiles were encoded (Mapbox vs Terrarium offsets differ). They must agree exactly.
  • Blurry terrain at high zoom. Tiles are being served above the DEM's real resolution. Cap maxzoom to where the source actually resolves detail.
  • Wrong y-axis (flipped tiles). TMS uses a bottom-origin y while XYZ uses top-origin. Use --xyz in gdal2tiles or set the source scheme correctly.
  • Heavy first paint. Overviews are missing, so every zoom reads full-resolution data. Build overviews (gdaladdo or COG OVERVIEWS=AUTO).

Validation

  • Compare a decoded Terrain-RGB pixel against the source DEM at the same coordinate; the height should match within the 0.1 m quantisation.
  • Inspect tile boundaries at several zooms for seams in the hillshade.
  • Check load performance with realistic pan/zoom, watching the number and size of tile requests, not a single static view.
  • Confirm the colour ramp or hillshade legend is documented with the DEM source, resolution, and date so viewers know the limits of interpretation.

Bathyl perspective

A terrain web map is an inspection instrument, so we keep the science visible: the DEM source, ground resolution, and vertical datum travel with the tileset, and we prefer Terrain-RGB when interpreters need to relight the surface to reveal structure that a single fixed azimuth would hide. A beautiful hillshade that freezes one light angle can quietly suppress real landforms.

Related reading

Sources