Terrain Viewer
Dev

Draping custom layers

What MapLibre drapes over its terrain and what it does not, why Matcap and Phong have a raster path and a live GL path, and the tile-seam fix

Draping and MapLibre 6

MapLibre 6 does not drape custom layers. A custom layer still draws itself in 3D with the projection data it is handed (getProjectionData is now part of the render arguments), while the render-to-texture path that drapes style layers over terrain has no public hook for a custom layer; 6 only refined that path (mipmap sampling, drape pooling, one stale drape re-rendered per frame). What does ride it is the raster mode of matcap and phong: the protocol emits tiles and MapLibre drapes them like any raster layer, with no seams. The live GL layers exist only for instant light and rotation changes, and they draw their own tile meshes. On 6 that showed as white dashes along every tile seam on the globe: MapLibre's terrain skirts, coloured by the draped layers, peeking through wherever a live quad stopped at the tile edge. The fix is the same technique MapLibre uses: the mesh is built with a border ring (generateBorders) and the vertex shader folds those vertices back onto the edge and drops them by Terrain.getSkirtLength(zoom), a vertical wall under each edge, so tiles neither gap nor overlap. Both follow MapLibre's default (terrainSkirtLength: "auto"), which is wanted over the plain background layer.

What can be draped, and why there are two paths

A question that comes back often, with MapLibre's own advice in issue #3001: can a Phong or Matcap render made from terrain tiles simply be draped on MapLibre's 3D terrain, without a mesh of our own? Yes, and the raster path already does exactly that. With terrain on, MapLibre renders every non-symbol style layer into per-tile textures and drapes them on its terrain mesh (render-to-texture). A custom protocol only has to return an image: matcap-protocol.ts and phong-protocol.ts fetch the DEM tile, compute normals with a one-tile border (so there are no seams, the caveat usually raised about this route), shade on the GPU (a fragment shader draw and readPixels, see gpu-matcap-compute.ts / gpu-phong-compute.ts) and return the tile. MapLibre drapes it like any raster. Normals come from the full-resolution DEM while the mesh is coarser, which is what you want: fine shading on the draped surface.

What decides between the two paths is whether a term depends on the view, not how free the camera is:

  • View-independent terms can be draped at any pitch or bearing: diffuse (Lambert) from a fixed light, slope and aspect colouring, sky-view factor, openness, hard shadows. They depend only on the normal and a world-space light, so the result lives in tile space, the way hillshade does.
  • View-dependent terms cannot: specular highlights, a camera-relative matcap (a lookup on the view-space normal), fresnel. Baked into a draped tile they are frozen at one view, the nadir, and wrong as soon as the map pitches or rotates; re-baking them on every camera move would mean reloading every tile.

So the raster path bakes: its matcap samples the tile-space normal (pinned to compass directions) and its Phong specular assumes a top-down view. The live path, a CustomLayerInterface, exists for the view-dependent version and for instant changes (a slider is a uniform, not a new tile). MapLibre does not drape custom layers, so it draws its own tile meshes over the terrain, with borders and skirts built the way MapLibre builds its own (above). Two other routes exist and were not taken: re-rendering MapLibre's own terrain tiles with our shader by reading map.terrain internals (private, and they moved between 5.x minors, sourceCache to tileManager), or a fork adding a shading mode to MapLibre's terrain fragment shader.

Issue #3001 itself is about something else: while a paint transition runs on a draped layer (an animated opacity, say), the render-to-texture cache kept the texture of the transition's first frame. It does not affect these modes: a new light direction or a new parameter goes into the tile URL, which reloads the tiles and invalidates the drape, and nothing here animates a paint property.

On this page