3D Fill Extrusion Buildings
This tutorial shows how to add 3D extruded buildings and shapes to your MapMetrics Android map using FillExtrusionLayer.
Prerequisites
- Completed the Getting Started Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
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
| Property | Type | Description |
|---|---|---|
fillExtrusionHeight | Double/Expression | Top height in meters |
fillExtrusionBase | Double/Expression | Base height (for stacked floors) |
fillExtrusionColor | Color/Expression | Fill color |
fillExtrusionOpacity | Float | Transparency (0.0 - 1.0) |
Next Steps
- Building Layer — More building layer examples
- Set Pitch and Bearing — 3D camera angles
- Data-Driven Styling — Style by data properties
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.