Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

3D Fill Extrusion Buildings ​

This tutorial shows how to add 3D extruded buildings and shapes to your MapMetrics Android map using FillExtrusionLayer.

Prerequisites ​

3D Buildings from Style Data ​

Extrude building footprints from the map style's built-in data:

kotlin
import android.graphics.Color
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.Style
import org.maplibre.android.style.expressions.Expression
import org.maplibre.android.style.expressions.Expression.*
import org.maplibre.android.style.layers.FillExtrusionLayer
import org.maplibre.android.style.layers.PropertyFactory.*

class Extrusion3DActivity : AppCompatActivity() {

    private lateinit var mapView: MapView
    private lateinit var map: MapLibreMap

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_map)

        mapView = findViewById(R.id.mapView)
        mapView.onCreate(savedInstanceState)
        mapView.getMapAsync { maplibreMap ->
            map = maplibreMap

            map.setStyle(
                Style.Builder().fromUri(
                    "https://gateway.mapmetrics-atlas.net/styles/YOUR_STYLE_ID?token=YOUR_API_KEY"
                )
            ) { style ->
                add3DBuildings(style)
            }
        }
    }

    private fun add3DBuildings(style: Style) {
        // Add 3D building extrusion layer
        val extrusionLayer = FillExtrusionLayer("3d-buildings", "composite")
        extrusionLayer.setSourceLayer("building")
        extrusionLayer.minZoom = 14f

        extrusionLayer.setProperties(
            // Height from data property
            fillExtrusionHeight(
                interpolate(
                    linear(), zoom(),
                    stop(14, literal(0)),
                    stop(14.05, get("height"))
                )
            ),
            // Base height for multi-part buildings
            fillExtrusionBase(
                interpolate(
                    linear(), zoom(),
                    stop(14, literal(0)),
                    stop(14.05, get("min_height"))
                )
            ),
            // Color
            fillExtrusionColor(Color.parseColor("#AAAAAA")),
            // Opacity that increases with zoom
            fillExtrusionOpacity(
                interpolate(
                    linear(), zoom(),
                    stop(14, 0.0f),
                    stop(15, 0.6f)
                )
            )
        )

        style.addLayer(extrusionLayer)

        // Set 3D perspective camera
        map.cameraPosition = CameraPosition.Builder()
            .target(LatLng(48.8584, 2.2945))
            .zoom(15.5)
            .tilt(55.0)
            .bearing(-20.0)
            .build()
    }

    override fun onStart() { super.onStart(); mapView.onStart() }
    override fun onResume() { super.onResume(); mapView.onResume() }
    override fun onPause() { super.onPause(); mapView.onPause() }
    override fun onStop() { super.onStop(); mapView.onStop() }
    override fun onDestroy() { super.onDestroy(); mapView.onDestroy() }
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        mapView.onSaveInstanceState(outState)
    }
}

Color Buildings by Height ​

Apply a height-based color gradient:

kotlin
private fun addColoredBuildings(style: Style) {
    val extrusionLayer = FillExtrusionLayer("3d-buildings", "composite")
    extrusionLayer.setSourceLayer("building")
    extrusionLayer.minZoom = 14f

    extrusionLayer.setProperties(
        fillExtrusionHeight(get("height")),
        fillExtrusionBase(get("min_height")),

        // Color gradient based on height
        fillExtrusionColor(
            interpolate(
                linear(), get("height"),
                stop(0, color(Color.parseColor("#2DC4B2"))),    // Low: teal
                stop(20, color(Color.parseColor("#3BB3C3"))),   // Medium
                stop(40, color(Color.parseColor("#669EC4"))),   // Tall
                stop(60, color(Color.parseColor("#8B88B6"))),   // Taller
                stop(100, color(Color.parseColor("#A2719B"))),  // Very tall
                stop(200, color(Color.parseColor("#AA5E79")))   // Skyscraper
            )
        ),
        fillExtrusionOpacity(0.7f)
    )

    style.addLayer(extrusionLayer)
}

Custom 3D Shapes from GeoJSON ​

Extrude custom polygon data:

kotlin
import com.google.gson.JsonObject
import org.maplibre.android.style.sources.GeoJsonSource
import org.maplibre.geojson.Feature
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.Point
import org.maplibre.geojson.Polygon

private fun addCustomExtrusions(style: Style) {
    // Create features with height properties
    val features = listOf(
        createBuildingFeature(
            listOf(
                Point.fromLngLat(2.2930, 48.8580),
                Point.fromLngLat(2.2945, 48.8580),
                Point.fromLngLat(2.2945, 48.8570),
                Point.fromLngLat(2.2930, 48.8570),
                Point.fromLngLat(2.2930, 48.8580),
            ),
            height = 50.0, name = "Building A", color = "#FF6B35"
        ),
        createBuildingFeature(
            listOf(
                Point.fromLngLat(2.2950, 48.8580),
                Point.fromLngLat(2.2965, 48.8580),
                Point.fromLngLat(2.2965, 48.8570),
                Point.fromLngLat(2.2950, 48.8570),
                Point.fromLngLat(2.2950, 48.8580),
            ),
            height = 80.0, name = "Building B", color = "#4285F4"
        ),
    )

    val collection = FeatureCollection.fromFeatures(features)

    // Add source
    style.addSource(GeoJsonSource("custom-buildings", collection))

    // Add extrusion layer
    style.addLayer(
        FillExtrusionLayer("custom-3d", "custom-buildings")
            .withProperties(
                fillExtrusionHeight(get("height")),
                fillExtrusionBase(literal(0)),
                fillExtrusionColor(get("color")),
                fillExtrusionOpacity(0.8f)
            )
    )
}

private fun createBuildingFeature(
    points: List<Point>,
    height: Double,
    name: String,
    color: String
): Feature {
    val properties = JsonObject().apply {
        addProperty("height", height)
        addProperty("name", name)
        addProperty("color", color)
    }
    val polygon = Polygon.fromLngLats(listOf(points))
    return Feature.fromGeometry(polygon, properties)
}

Light and Shadow Settings ​

The 3D appearance uses the map style's light configuration. Buildings facing the light source appear brighter while the opposite side is shadowed — this happens automatically based on the camera bearing and style light settings.

Fill Extrusion Properties ​

PropertyTypeDescription
fillExtrusionHeightDouble/ExpressionTop height in meters
fillExtrusionBaseDouble/ExpressionBase height (for stacked floors)
fillExtrusionColorColor/ExpressionFill color
fillExtrusionOpacityFloatTransparency (0.0 - 1.0)

Next Steps ​


Tip: 3D extrusions only look good at tilt angles of 30-60°. At 0° tilt (top-down) they're invisible. Always pair FillExtrusionLayer with a tilted camera for the best visual effect.