Skip to main content

3D Lights

Configure lights in 3D environment.
Lights3DExample.swift
import UIKit
@_spi(Experimental) 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)
setDefaultCamera()
view.addSubview(mapView)

mapView.mapboxMap.onMapLoaded.observeNext { [weak self] _ in
self?.setupExample()

}.store(in: &cancelables)

navigationItem.rightBarButtonItem = sunAnimationButton()
}

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 setupExample() {
add3DLights()
}

func add3DLights(azimuth: Double = 210, polarAngle: Double = 30, ambientColor: UIColor = .white) {
do {
var directionalLight = DirectionalLight(id: directionalLightId)
directionalLight.intensity = .constant(0.5)
directionalLight.direction = .constant([azimuth, polarAngle])
directionalLight.directionTransition = StyleTransition(duration: 0, delay: 0)
directionalLight.castShadows = .constant(true)
directionalLight.shadowIntensity = .constant(1)

var ambientLight = AmbientLight(id: ambientLightId)
ambientLight.color = .constant(.init(ambientColor))
ambientLight.intensity = .constant(0.5)

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

var atmosphere = Atmosphere()
atmosphere.range = .constant([0, 12])
atmosphere.horizonBlend = .constant(0.1)
atmosphere.starIntensity = .constant(0.2)
atmosphere.color = .constant(StyleColor(red: 240, green: 196, blue: 152, alpha: 1)!)
atmosphere.highColor = .constant(StyleColor(red: 221, green: 209, blue: 197, alpha: 1)!)
atmosphere.spaceColor = .constant(StyleColor(red: 153, green: 180, blue: 197, alpha: 1)!)

try mapView.mapboxMap.setAtmosphere(atmosphere)
} catch {
print("Failed to set 3D light due to:", error)
}
}

func updateSunLight(animationProgress: Double) {
// Calculate azimuth and polar angle based on animation progress
let azimuth = 180.0 - (360.0 * animationProgress)
let polarAngle = 45.0 * sin(animationProgress * Double.pi)

do {
let lightIntensity = generateLightIntensityValue(progress: animationProgress)
try mapView.mapboxMap.setLightProperty(
for: directionalLightId,
property: "direction",
value: [azimuth, polarAngle]
)
try mapView.mapboxMap.setLightProperty(
for: directionalLightId,
property: "intensity",
value: lightIntensity
)

let color = StyleColor(generateSunColor(progress: animationProgress))
try mapView.mapboxMap.setLightProperty(for: ambientLightId, property: "color", value: color.rawValue)
try mapView.mapboxMap.setLightProperty(for: ambientLightId, property: "intensity", value: lightIntensity)
} catch {
print("Failed to update sun light due to:", error)
}
}

func setDefaultCamera() {
let cameraOptions = CameraOptions(center: CLLocationCoordinate2D(latitude: 40.713589095634475,
longitude: -74.00370030835559),
zoom: 16.868,
bearing: 60.54,
pitch: 60)
mapView.mapboxMap.setCamera(to: cameraOptions)
}

var animationProgress = 0.0 {
didSet {
updateSunLight(animationProgress: animationProgress)
}
}

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

Was this example helpful?