Skip to main content

Display 3D buildings

This example demonstrates how to create a fill extrusion layer 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 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 and DirectionalLight classes.

iOS Examples App Available

This example code is part of the Maps SDK for iOS Examples App, a working iOS project available on Github. 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 tutorial for step-by-step instructions.

BuildingExtrusionsExample.swift
import UIKit
@_spi(Experimental) import MapboxMaps

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

private lazy var lightPositionButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .systemBlue
button.tintColor = .white
button.layer.cornerRadius = 4
button.clipsToBounds = true
button.contentEdgeInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)
if #available(iOS 13.0, *) {
button.setImage(UIImage(systemName: "flashlight.on.fill"), for: .normal)
} else {
button.setTitle("Position", for: .normal)
}
button.addTarget(self, action: #selector(lightPositionButtonTapped(_:)), for: .touchUpInside)
return button
}()

private lazy var lightColorButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .systemBlue
button.tintColor = .white
button.layer.cornerRadius = 4
button.clipsToBounds = true
button.contentEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
if #available(iOS 13.0, *) {
button.setImage(UIImage(systemName: "paintbrush.fill"), for: .normal)
} else {
button.setTitle("Color", for: .normal)
}
button.addTarget(self, action: #selector(lightColorButtonTapped(_:)), for: .touchUpInside)
return button
}()

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()

let options = MapInitOptions(styleURI: .light)
mapView = MapView(frame: view.bounds, mapInitOptions: options)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(mapView)

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

view.addSubview(lightPositionButton)
view.addSubview(lightColorButton)

NSLayoutConstraint.activate([
view.trailingAnchor.constraint(equalToSystemSpacingAfter: lightPositionButton.trailingAnchor, multiplier: 1),
view.bottomAnchor.constraint(equalTo: lightPositionButton.bottomAnchor, constant: 100),
view.trailingAnchor.constraint(equalToSystemSpacingAfter: lightColorButton.trailingAnchor, multiplier: 1),
lightPositionButton.topAnchor.constraint(equalToSystemSpacingBelow: lightColorButton.bottomAnchor, multiplier: 1),
lightColorButton.widthAnchor.constraint(equalTo: lightPositionButton.widthAnchor),
lightColorButton.heightAnchor.constraint(equalTo: lightPositionButton.heightAnchor)
])
}

internal func setupExample() {
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() {
var layer = FillExtrusionLayer(id: "3d-buildings", source: "composite")

layer.minZoom = 15
layer.sourceLayer = "building"
layer.fillExtrusionColor = .constant(StyleColor(.lightGray))
layer.fillExtrusionOpacity = .constant(0.6)

layer.filter = Exp(.eq) {
Exp(.get) {
"extrude"
}
"true"
}

layer.fillExtrusionHeight = .expression(
Exp(.get) {
"height"
}
)

layer.fillExtrusionBase = .expression(
Exp(.get) {
"min_height"
}
)

layer.fillExtrusionVerticalScale = .expression(
Exp(.interpolate) {
Exp(.linear)
Exp(.zoom)
15
0
15.05
1
}
)

layer.fillExtrusionAmbientOcclusionIntensity = .constant(0.3)

layer.fillExtrusionAmbientOcclusionRadius = .constant(3.0)

try! mapView.mapboxMap.addLayer(layer)
}

// MARK: - Actions

@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)
}
}
Was this example helpful?