3D Lights
This example demonstrates the implementation of 3D lighting effects in an application using
the Mapbox Maps SDK for iOS.
The ViewController
class includes functionality to set up directional and ambient lighting on a map view.
The lighting setup utilizes the DirectionalLight
,
AmbientLight
and
Atmosphere
classes to define properties such as intensity, color, direction, shadow casting and fog.
A sun animation feature is provided, allowing for dynamic changes in lighting properties based on user interaction.
The AnimationData
class manages the animation progress and timing using a CADisplayLink
.
Additionally, the generateSunColor
method dynamically generates sun colors based on specified
progress values, and the interpolateColor
method smoothly transitions between two UIColor
values.
By utilizing these features, developers can create engaging and interactive 3D lighting scenarios
within their applications using the Mapbox Maps SDK for iOS.
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
import MapboxMaps
final class ViewController: UIViewController {
private var mapView: MapView!
let directionalLightId = "directional-light"
let ambientLightId = "ambient-light"
private var cancelables = Set<AnyCancelable>()
override func viewDidLoad() {
super.viewDidLoad()
mapView = MapView(frame: view.bounds)
let cameraOptions = CameraOptions(center: CLLocationCoordinate2D(latitude: 40.713589095634475,
longitude: -74.00370030835559),
zoom: 16.868,
bearing: 60.54,
pitch: 60)
mapView.mapboxMap.setCamera(to: cameraOptions)
view.addSubview(mapView)
mapView.mapboxMap.onMapLoaded.observeNext { [weak self] _ in
}.store(in: &cancelables)
navigationItem.rightBarButtonItem = sunAnimationButton()
updateMapState()
}
func sunAnimationButton() -> UIBarButtonItem {
if #available(iOS 13, *) {
return UIBarButtonItem(image: UIImage(systemName: "sunrise"), style: .plain, target: self, action: #selector(startSunAnimation))
} else {
return UIBarButtonItem(title: "Sun", style: .plain, target: self, action: #selector(startSunAnimation))
}
}
func updateMapState(azimuth: Double = 210, polarAngle: Double = 30, ambientColor: UIColor = .lightGray) {
if #available(iOS 13.0, *) {
/// We use the new experimental style content feature to set the lights.
mapView.mapboxMap.setMapStyleContent {
Atmosphere()
.range(start: 0, end: 12)
.horizonBlend(0.1)
.starIntensity(0.2)
.color(StyleColor(red: 240, green: 196, blue: 152, alpha: 1)!)
.highColor(StyleColor(red: 221, green: 209, blue: 197, alpha: 1)!)
.spaceColor(StyleColor(red: 153, green: 180, blue: 197, alpha: 1)!)
DirectionalLight(id: directionalLightId)
.intensity(0.5)
.direction(azimuthal: azimuth, polar: polarAngle)
.directionTransition(.zero)
.castShadows(true)
.shadowIntensity(1)
AmbientLight(id: ambientLightId)
.color(ambientColor)
.intensity(0.5)
}
}
}
var animationProgress = 0.0 {
didSet {
// Calculate azimuth and polar angle based on animation progress
let azimuth = 180.0 - (360.0 * animationProgress)
let polarAngle = 45.0 * sin(animationProgress * Double.pi)
let color = generateSunColor(progress: animationProgress)
updateMapState(azimuth: azimuth, polarAngle: polarAngle, ambientColor: color)
}
}
var animation: AnimationData?
class AnimationData {
let animationStart: CFTimeInterval
let animationEnd: CFTimeInterval
let displayLink: CADisplayLink
init(start: CFTimeInterval = CACurrentMediaTime(), duration: CFTimeInterval, displayLink: CADisplayLink) {
self.animationStart = start
self.animationEnd = start + duration
self.displayLink = displayLink
displayLink.add(to: .main, forMode: .default)
}
func animationProgress(for time: CFTimeInterval) -> Double {
guard time < animationEnd else { return 1 }
let duration = time - animationStart
guard duration > 0 else { return 0 }
let expectedDuration = animationEnd - animationStart
return duration / expectedDuration
}
deinit {
displayLink.remove(from: .main, forMode: .default)
}
}
@objc func startSunAnimation() {
let duration: TimeInterval = 8
let displayLink = CADisplayLink(target: self, selector: #selector(tickAnimation(displayLink:)))
animation = AnimationData(duration: duration, displayLink: displayLink)
}
@objc func tickAnimation(displayLink: CADisplayLink) {
guard let animation = animation else { return assertionFailure("DisplayLink should not trigger without animation") }
animationProgress = animation.animationProgress(for: displayLink.targetTimestamp)
if animationProgress >= 1 {
self.animation = nil
}
}
func generateSunColor(progress: Double) -> UIColor {
let yellowSunColor = UIColor(red: 255/255, green: 242/255, blue: 0/255, alpha: 1)
let orangeSunColor = UIColor(red: 255/255, green: 180/255, blue: 0/255, alpha: 1)
let darkOrangeSunColor = UIColor(red: 204/255, green: 51/255, blue: 0/255, alpha: 1)
switch progress {
case 0..<0.2:
return interpolateColor(from: .white,
to: yellowSunColor,
with: (progress) / 0.2)
case 0.2..<0.4:
return interpolateColor(from: yellowSunColor,
to: .white,
with: (progress - 0.2) / 0.2)
case 0.4..<0.7:
return .white
case 0.7..<0.85:
return interpolateColor(from: .white,
to: orangeSunColor,
with: (progress - 0.7) / 0.15)
case 0.85...1.0:
return interpolateColor(from: orangeSunColor,
to: darkOrangeSunColor,
with: (progress - 0.85) / 0.15)
default:
return .purple
}
}
func generateLightIntensityValue(progress: Double, acceptableValues: ClosedRange<Double> = 0.1...0.5) -> Double {
let intervalWidth = acceptableValues.upperBound - acceptableValues.lowerBound
if progress <= 0.5 {
return acceptableValues.lowerBound + intervalWidth * (2 * progress)
} else {
return acceptableValues.upperBound - (progress - 0.5) * (2 * intervalWidth)
}
}
func interpolateColor(from startColor: UIColor, to endColor: UIColor, with interpolationFactor: Double) -> UIColor {
var startRed: CGFloat = 0
var startGreen: CGFloat = 0
var startBlue: CGFloat = 0
var startAlpha: CGFloat = 0
startColor.getRed(&startRed, green: &startGreen, blue: &startBlue, alpha: &startAlpha)
var endRed: CGFloat = 0
var endGreen: CGFloat = 0
var endBlue: CGFloat = 0
var endAlpha: CGFloat = 0
endColor.getRed(&endRed, green: &endGreen, blue: &endBlue, alpha: &endAlpha)
let interpolatedRed = startRed + CGFloat(interpolationFactor) * (endRed - startRed)
let interpolatedGreen = startGreen + CGFloat(interpolationFactor) * (endGreen - startGreen)
let interpolatedBlue = startBlue + CGFloat(interpolationFactor) * (endBlue - startBlue)
let interpolatedAlpha = startAlpha + CGFloat(interpolationFactor) * (endAlpha - startAlpha)
return UIColor(red: min(max(0, interpolatedRed), 1),
green: min(max(0, interpolatedGreen), 1),
blue: min(max(0, interpolatedBlue), 1),
alpha: min(max(0, interpolatedAlpha), 1))
}
}