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.
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.
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)
}
}