Show 3D buildings and relief
Height data helps users see the shape of a city or a landscape. You can draw buildings as 3D blocks, shade mountains from elevation tiles, and show the whole world as a globe.
MapLibre Compose has no API for 3D terrain, which raises the ground itself. Hillshade and color relief shade the flat map instead.
Show 3D buildings
Section titled “Show 3D buildings”FillExtrusionLayer raises each polygon to a height in meters.
OpenFreeMap serves vector tiles with a building source layer, where each
building has a render_height and a render_min_height:

val mapState = rememberMapState( baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/bright"), initialCameraPosition = CameraPosition( center = Position(latitude = 40.7135, longitude = -74.0066), zoom = 15.5, pitch = 45.0, bearing = -17.6, ), ) { getBaseSource<VectorTileSource>(id = "openmaptiles")?.let { tiles -> // Below the base style's labels Anchor.Below({ it.type == "symbol" }) { FillExtrusionLayer( id = "3d-buildings", source = tiles, sourceLayer = "building", minZoom = 15f, color = const(Color.LightGray), height = feature["render_height"].asNumber(const(0f)), base = feature["render_min_height"].asNumber(const(0f)), ) } } }MaplibreMap(state = mapState)The camera starts tilted with pitch, so the heights are visible. The layer
goes below the base style’s labels, so buildings do not cover them. Some
styles, such as OpenFreeMap Liberty, already include 3D buildings.
Light 3D buildings
Section titled “Light 3D buildings”The style’s light shades the sides of extruded buildings. Set it with
StyleLight.set when the style is ready:
LaunchedEffect(mapState, mapState.style.loadState) { if (mapState.style.loadState == StyleLoadState.Ready) { mapState.style.light.set( Light( // Light from the west, whichever way the map is rotated anchor = const(IlluminationAnchor.Map), position = const(listOf(1.5f, 270f, 60f)), color = const(Color(0xFFFFE0B2)), intensity = const(0.6f), ) ) }}position lists the light’s distance, its direction in degrees clockwise from
north, and its angle in degrees from straight above. With
IlluminationAnchor.Map, the light keeps its direction when the user rotates
the map.
Loading another base style replaces the light with the new style’s light. The
effect sets it again each time MapStyleState.loadState becomes
StyleLoadState.Ready.
Shade hills
Section titled “Shade hills”A raster DEM source loads elevation tiles: images that store a height in the
color of each pixel. HillshadeLayer draws shadows and highlights
from those heights, so mountains and valleys stand out:

val mapState = rememberMapState( baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/positron"), initialCameraPosition = CameraPosition(center = Position(latitude = 47.27, longitude = 11.39), zoom = 10.0), ) { val dem = rememberRasterDemTileSource( uri = "https://demotiles.maplibre.org/terrain-tiles/tiles.json", tileSize = 256, ) Anchor.Below({ it.type == "symbol" }) { HillshadeLayer(id = "hillshade", source = dem, exaggeration = const(0.7f)) } }MaplibreMap(state = mapState)The sample tiles from demotiles.maplibre.org cover only a small area of the
Alps around Innsbruck, Austria, so the camera starts there. For other areas,
use a provider that serves elevation tiles. Pass its tile URLs and its
RasterDemEncoding to rememberRasterDemTileSource.
Color by elevation
Section titled “Color by elevation”ColorReliefLayer colors each pixel by its height. Build the colors
with interpolate over elevation, in meters:

val state = rememberMapState { val dem = rememberRasterDemTileSource( uri = "https://demotiles.maplibre.org/terrain-tiles/tiles.json", tileSize = 256, ) ColorReliefLayer( id = "color-relief", source = dem, color = interpolate( type = linear(), input = elevation(), 500 to const(Color(0xFF4CAF50)), 1500 to const(Color(0xFFF0E68C)), 2500 to const(Color(0xFF8B5A2B)), 3000 to const(Color.White), ), opacity = const(0.6f), )}MaplibreMap(state = state)Without color, the layer uses LayerDefaults.ColorReliefColors.
With the sample tiles from the previous section, start the camera over
Innsbruck as that example does; elsewhere the layer has no data to color.
Show a globe
Section titled “Show a globe”Set the style’s projection to ProjectionType.Globe to show the
world as a globe when the user zooms out:
@OptIn(ExperimentalMaplibreComposeApi::class)@Composablefun GlobeMap() { val mapState = rememberMapState(initialCameraPosition = CameraPosition(zoom = 1.5)) LaunchedEffect(mapState, mapState.style.loadState) { if (mapState.style.loadState == StyleLoadState.Ready) { mapState.style.projection.set(Projection(type = const(ProjectionType.Globe))) } } MaplibreMap(state = mapState)}Below zoom 11, the map is a globe. Between zoom 11 and zoom 12, it turns into
the flat Mercator map. ProjectionType.Globe is experimental, so the function
opts in to ExperimentalMaplibreComposeApi. As with the light, a new base
style replaces the projection, so the effect sets it again.
Only the browser draws a globe. MapLibre Native supports only the Mercator
projection, so it logs a warning and keeps the flat map. On the browser, the
globe has an atmosphere around it. StyleSky.set changes the
atmosphere and the color of the sky above the horizon. MapLibre Native does not
draw a sky.