Skip to content

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.MapMetricsMap
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: MapMetricsMap

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

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

            map.setStyle(
                Style.Builder().fromUri(
                    "https://gateway.mapmetrics.org/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.