> For the complete documentation index, see [llms.txt](https://docs.mapbox.com/ios/maps/llms.txt)

# Display 3D buildings

![Extrude the building layer in the Mapbox Light style using FillExtrusionLayer and set up the light position.](https://docs.mapbox.com/ios/assets/ideal-img/maps-examples-3D-buildings.01dda5e.480.png)

This example demonstrates how to create a [fill extrusion layer](https://docs.mapbox.com/style-spec/reference/layers/#fill-extrusion) at runtime to display 3D buildings on the map. The code sets up a view controller with buttons to adjust the configuration of ambient and directional light sources. The `setupExample()` method initializes the map view with specified camera options and adds building extrusions based on the provided data and styling parameters. The `addBuildingExtrusions()` function uses the [`FillExtrusionLayer`](https://docs.mapbox.com/ios/maps/api/latest/documentation/mapboxmaps/fillextrusionlayer/) API to create the layer for 3D buildings using data from the predefined `building` source to define the base and height of the extrusion.

The interactive functionality includes changing the ambient light color and adjusting the direction of the directional light source upon tapping the respective buttons, using the [`AmbientLight`](https://docs.mapbox.com/ios/maps/api/latest/documentation/mapboxmaps/ambientlight/) and [`DirectionalLight`](https://docs.mapbox.com/ios/maps/api/latest/documentation/mapboxmaps/directionallight/) classes.

> **Note: iOS Examples App Available**
> 
> This example code is part of the **Maps SDK for iOS Examples App**, a working iOS project [available on Github](https://github.com/mapbox/mapbox-maps-ios/tree/11.29.0/Sources/Examples). iOS developers are encouraged to run the examples app locally to interact with this example in an emulator and explore other features of the Maps SDK.
> 
> See our [Run the Maps SDK for iOS Examples App](https://docs.mapbox.com/help/tutorials/maps-sdk-ios-examples-app/) tutorial for step-by-step instructions.

**Swift**

Title: `BuildingExtrusionsExample.swift`

[View on GitHub](https://github.com/mapbox/mapbox-maps-ios/blob/main/Sources/Examples/All%20Examples/BuildingExtrusionsExample.swift)

```swift
import UIKit
@_spi(Experimental) import MapboxMaps

extension UIButton {
    static func exampleActionButton() -> UIButton {
        let button = UIButton(type: .custom)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.backgroundColor = .systemBlue
        button.tintColor = .white
        button.layer.cornerRadius = 4
        button.clipsToBounds = true
        button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
        return button
    }
}

final class ViewController: UIViewController {
    private var cancelables = Set<AnyCancelable>()

    private lazy var lightPositionButton: UIButton = {
        let button = UIButton.exampleActionButton()
        button.setImage(UIImage(systemName: "flashlight.on.fill"), for: .normal)
        button.addTarget(self, action: #selector(lightPositionButtonTapped(_:)), for: .primaryActionTriggered)
        return button
    }()

    private lazy var lightColorButton: UIButton = {
        let button = UIButton.exampleActionButton()
        button.setImage(UIImage(systemName: "paintbrush.fill"), for: .normal)
        button.addTarget(self, action: #selector(lightColorButtonTapped(_:)), for: .primaryActionTriggered)
        return button
    }()

    private lazy var heightAlignmentButton: UIButton = {
        let button = UIButton.exampleActionButton()

        button.setImage(UIImage(systemName: "align.vertical.top"), for: .normal)
        button.setImage(UIImage(systemName: "align.vertical.top.fill"), for: .selected)
        button.addTarget(self, action: #selector(heightAlignmentButtonTapped(_:)), for: .primaryActionTriggered)
        return button
    }()

    private lazy var baseAlignmentButton: UIButton = {
        let button = UIButton.exampleActionButton()

        button.setImage(UIImage(systemName: "align.vertical.bottom"), for: .normal)
        button.setImage(UIImage(systemName: "align.vertical.bottom.fill"), for: .selected)
        button.addTarget(self, action: #selector(baseAlignmentButtonTapped(_:)), for: .primaryActionTriggered)
        return button
    }()

    private lazy var terrainSwitchButton: UIButton = {
        let button = UIButton.exampleActionButton()
        button.setImage(UIImage(systemName: "mountain.2"), for: .normal)
        button.setImage(UIImage(systemName: "mountain.2.fill"), for: .selected)

        button.addTarget(self, action: #selector(terrainButtonTapped(_:)), for: .primaryActionTriggered)
        return button
    }()

    lazy var buttons = [
        heightAlignmentButton,
        baseAlignmentButton,
        lightPositionButton,
        lightColorButton,
        terrainSwitchButton
    ]

    private var ambientLight: AmbientLight = {
        var light = AmbientLight()
        light.color = .constant(StyleColor(.blue))
        light.intensity = .constant(0.9)
        return light
    }()

    private var directionalLight: DirectionalLight = {
        var light = DirectionalLight()
        light.color = .constant(StyleColor(.white))
        light.intensity = .constant(0.9)
        light.castShadows = .constant(true)
        light.direction = .constant([0.0, 15.0])
        return light
    }()

    private var mapView: MapView!

    override func viewDidLoad() {
        super.viewDidLoad()

        mapView = MapView(frame: view.bounds)
        mapView.mapboxMap.mapStyle = .standard(theme: .monochrome, lightPreset: .day)
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(mapView)

        mapView.mapboxMap.onStyleLoaded.observeNext { _ in
            self.setupExample()
        }.store(in: &cancelables)

        buttons.forEach(view.addSubview(_:))
        terrainSwitchButton.isSelected = isTerrainEnabled

        let accessoryButtonsStackView = UIStackView(arrangedSubviews: buttons)
        accessoryButtonsStackView.axis = .vertical
        accessoryButtonsStackView.spacing = 20
        accessoryButtonsStackView.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(accessoryButtonsStackView)

        NSLayoutConstraint.activate([
            mapView.ornaments.attributionButton.topAnchor.constraint(equalToSystemSpacingBelow: accessoryButtonsStackView.bottomAnchor, multiplier: 1),
            view.trailingAnchor
                .constraint(equalToSystemSpacingAfter: accessoryButtonsStackView.trailingAnchor, multiplier: 1)
        ])
    }

    internal func setupExample() {
        try! addTerrain()
        try! addBuildingExtrusions()

        let cameraOptions = CameraOptions(center: CLLocationCoordinate2D(latitude: 40.7135, longitude: -74.0066),
                                          zoom: 15.5,
                                          bearing: -17.6,
                                          pitch: 45)
        mapView.mapboxMap.setCamera(to: cameraOptions)

        try! mapView.mapboxMap.setLights(ambient: ambientLight, directional: directionalLight)

        // The below lines are used for internal testing purposes only.
        
    }

    // See https://docs.mapbox.com/mapbox-gl-js/example/3d-buildings/ for equivalent gl-js example
    internal func addBuildingExtrusions() throws {
        let wallOnlyThreshold = 20
        let extrudeFilter = Exp(.eq, Exp(.get, "extrude"), "true")
        var layer = FillExtrusionLayer(id: "3d-buildings", source: "composite")
            .minZoom(15)
            .sourceLayer("building")
            .fillExtrusionColor(.lightGray)
            .fillExtrusionOpacity(0.8)
            .fillExtrusionAmbientOcclusionIntensity(0.3)
            .fillExtrusionAmbientOcclusionRadius(3.0)
            .fillExtrusionHeight(Exp(.get, "height"))
            .fillExtrusionBase(Exp(.get, "min_height"))
            .fillExtrusionVerticalScale(Exp(.interpolate, Exp(.linear), Exp(.zoom), 15, 0, 15.05, 1))

        layer.filter = Exp(.all) {
            extrudeFilter
            Exp(.gt) {
                Exp(.get) { "height" }
                wallOnlyThreshold
            }
        }

        try mapView.mapboxMap.addLayer(layer)

        var wallsOnlyExtrusionLayer = layer
            .fillExtrusionLineWidth(2)
        wallsOnlyExtrusionLayer.id = "3d-buildings-wall"
        wallsOnlyExtrusionLayer.filter = Exp(.all) {
            extrudeFilter
            Exp(.lte) {
                Exp(.get) { "height" }
                wallOnlyThreshold
            }
        }

        try mapView.mapboxMap.addLayer(wallsOnlyExtrusionLayer)
    }

    func addTerrain() throws {
        let terrainSourceID = "mapbox-dem"

        if !mapView.mapboxMap.sourceExists(withId: terrainSourceID) {
            try addTerrainSource(id: terrainSourceID)
        }

        try mapView.mapboxMap.setTerrain(Terrain(sourceId: terrainSourceID)
            .exaggeration(1.5))
    }

    func addTerrainSource(id: String) throws {
        var demSource = RasterDemSource(id: id)
        demSource.url = "mapbox://mapbox.mapbox-terrain-dem-v1"
        // Setting the `tileSize` to 514 provides better performance and adds padding around the outside
        // of the tiles.
        demSource.tileSize = 514
        demSource.maxzoom = 14.0
        try mapView.mapboxMap.addSource(demSource)
    }

    // MARK: - Actions

    var isTerrainEnabled = true

    @objc private func terrainButtonTapped(_ sender: UIButton) {
        if isTerrainEnabled {
            mapView.mapboxMap.removeTerrain()
        } else {
            try! addTerrain()
        }

        isTerrainEnabled.toggle()
        sender.isSelected = isTerrainEnabled
    }

    var baseAlignment: FillExtrusionBaseAlignment = .flat
    var heightAlignment: FillExtrusionHeightAlignment = .flat

    @objc private func baseAlignmentButtonTapped(_ sender: UIButton) {
        if baseAlignment == .flat {
            baseAlignment = .terrain
        } else {
            baseAlignment = .flat
        }
        sender.backgroundColor = .systemBlue
        sender.isSelected = baseAlignment == .terrain

        try! mapView.mapboxMap.updateLayer(withId: "3d-buildings", type: FillExtrusionLayer.self) { layer in
            layer.fillExtrusionBaseAlignment = .constant(baseAlignment)
        }
    }

    @objc private func heightAlignmentButtonTapped(_ sender: UIButton) {
        if heightAlignment == .flat {
            heightAlignment = .terrain
        } else {
            heightAlignment = .flat
        }
        sender.isSelected = heightAlignment == .terrain

        try! mapView.mapboxMap.updateLayer(withId: "3d-buildings", type: FillExtrusionLayer.self) { layer in
            layer.fillExtrusionHeightAlignment = .constant(heightAlignment)
        }
    }

    @objc private func lightColorButtonTapped(_ sender: UIButton) {
        if case .constant(let color) = ambientLight.color, color == StyleColor(.red) {
            ambientLight.color = .constant(StyleColor(.blue))
            sender.tintColor = .blue
        } else {
            ambientLight.color = .constant(StyleColor(.red))
            sender.tintColor = .red
        }

        try! mapView.mapboxMap.setLights(ambient: ambientLight, directional: directionalLight)
    }

    @objc private func lightPositionButtonTapped(_ sender: UIButton) {
        let firstPosition: [Double] = [0, 15]
        let secondPosition: [Double] = [90, 60]

        if case .constant(let position) = directionalLight.direction, position == firstPosition {
            directionalLight.direction = .constant(secondPosition)
            sender.imageView?.transform = .identity
        } else {
            directionalLight.direction = .constant(firstPosition)
            sender.imageView?.transform = CGAffineTransform(rotationAngle: 2.0 * .pi / 3.0)
        }

        try! mapView.mapboxMap.setLights(ambient: ambientLight, directional: directionalLight)
    }
}
```