Orbit camera around object

View on GitHub
Sample viewer app

Fix the camera to point at and rotate around a target object.

Image of orbit camera around object

Use case

The orbit geoelement camera controller provides control over the following camera behaviors:

  • Automatically track the target
  • Stay near the target by setting a minimum and maximum distance offset
  • Restrict where you can rotate around the target
  • Automatically rotate the camera when the target's heading and pitch changes
  • Disable user interactions for rotating the camera
  • Animate camera movement over a specified duration
  • Control the vertical positioning of the target on the screen
  • Set a target offset (e.g.to orbit around the tail of the plane) instead of defaulting to orbiting the center of the object

How to use the sample

The sample loads with the camera orbiting an airplane model. The camera is preset with a restricted camera heading and pitch, and a limited minimum and maximum camera distance set from the plane. The position of the plane on the screen is also set just below center.

Use the sliders to adjust the camera heading and the plane's pitch. When not in Cockpit view, the plane's pitch will change independently to that of the camera pitch. Toggle on the switch to allow zooming in and out by pinching; when the switch is off, the user won't be able to adjust with the camera distance.

Tap the "Cockpit view" button to offset and fix the camera into the cockpit of the airplane. Tap the "Center view" button to exit cockpit view mode and fix the camera controller on the center of the plane.

How it works

  1. Instantiate an AGSOrbitGeoElementCameraController object.
  2. Set the camera controller to the scene view.
  3. Set the heading, pitch, and distance properties for the camera controller.
  4. Set the minimum and maximum angle of heading and pitch, and minimum and maximum distance for the camera.
  5. Set the distance from which the camera is offset from the plane.
  6. Set the targetVerticalScreenFactor property to determine where the plane appears in the scene.
  7. Animate the camera to the cockpit using AGSOrbitGeoElementCameraController.setTargetOffsetX(_:targetOffsetY:targetOffsetZ:duration:completion:).
  8. Set isCameraDistanceInteractive if the camera distance will adjust when zooming or panning by pinching (default is true).
  9. Set isAutoPitchEnabled if the camera will follow the pitch of the plane (default is true).

Relevant API

  • AGSOrbitGeoElementCameraController

Tags

3D, camera, object, orbit, rotate, scene

Sample Code

OrbitCameraAroundObjectViewController.swiftOrbitCameraSettingsViewController.swift
                                                                                                                                                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
// Copyright 2021 Esri
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import UIKit
import ArcGIS

class OrbitCameraAroundObjectViewController: UIViewController {
    // MARK: Storyboard views

    /// The scene view managed by the view controller.
    @IBOutlet var sceneView: AGSSceneView! {
        didSet {
            sceneView.scene = makeScene()
            sceneView.graphicsOverlays.add(makeSceneGraphicsOverlay())
            // Create and set the orbit camera controller to the scene view.
            sceneView.cameraController = makeOrbitGeoElementCameraController()
        }
    }

    @IBOutlet var changeViewBarButtonItem: UIBarButtonItem! {
        didSet {
            changeViewBarButtonItem.possibleTitles = Set(ChangeViewButtonState.allCases.map(\.title))
        }
    }

    // MARK: Properties

    /// A graphic of a plane model.
    let planeGraphic: AGSGraphic = {
        let planeSymbol = AGSModelSceneSymbol(name: "Bristol", extension: "dae", scale: 1)
        let planePosition = AGSPoint(x: 6.637, y: 45.399, z: 100, spatialReference: .wgs84())
        let planeGraphic = AGSGraphic(geometry: planePosition, symbol: planeSymbol, attributes: ["HEADING": 45.0, "PITCH": 0])
        return planeGraphic
    }()

    var moveCameraAnimationCancelable: AGSCancelable?

    enum ChangeViewButtonState: CaseIterable {
        case cockpitView
        case centerView

        var title: String {
            switch self {
            case .cockpitView:
                return "Cockpit View"
            case .centerView:
                return "Center View"
            }
        }
    }

    var changeViewButtonState: ChangeViewButtonState = .cockpitView

    // MARK: Instance methods

    /// Create a scene.
    func makeScene() -> AGSScene {
        let scene = AGSScene(basemap: .imagery())
        // Create an elevation source from Terrain3D REST service.
        let elevationServiceURL = URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!
        let elevationSource = AGSArcGISTiledElevationSource(url: elevationServiceURL)
        let surface = AGSSurface()
        surface.elevationSources.append(elevationSource)
        scene.baseSurface = surface
        return scene
    }

    /// Create a graphics overlay for the scene.
    func makeSceneGraphicsOverlay() -> AGSGraphicsOverlay {
        let graphicsOverlay = AGSGraphicsOverlay()
        graphicsOverlay.sceneProperties?.surfacePlacement = .relative
        let renderer = AGSSimpleRenderer()
        renderer.sceneProperties?.headingExpression = "[HEADING]"
        renderer.sceneProperties?.pitchExpression = "[PITCH]"
        graphicsOverlay.renderer = renderer
        graphicsOverlay.graphics.add(planeGraphic)
        return graphicsOverlay
    }

    /// Create a controller that allows a scene view's camera to orbit the plane.
    func makeOrbitGeoElementCameraController() -> AGSOrbitGeoElementCameraController {
        let cameraController = AGSOrbitGeoElementCameraController(targetGeoElement: planeGraphic, distance: 50)

        // Restrict the camera's heading to stay behind the plane.
        cameraController.minCameraHeadingOffset = -45
        cameraController.maxCameraHeadingOffset = 45

        // Restrict the camera's pitch so it doesn't collide with the ground.
        cameraController.minCameraPitchOffset = 10
        cameraController.maxCameraPitchOffset = 100

        // Restrict the camera to stay between 10 and 100 meters from the plane.
        cameraController.minCameraDistance = 10
        cameraController.maxCameraDistance = 100

        // Position the plane a third from the bottom of the screen.
        cameraController.targetVerticalScreenFactor = 0.33

        // Don't pitch the camera when the plane pitches.
        cameraController.isAutoPitchEnabled = false
        return cameraController
    }

    // MARK: Actions

    @IBAction func changeViewBarButtonItemTapped(_ sender: UIBarButtonItem) {
        moveCameraAnimationCancelable?.cancel()
        switch changeViewButtonState {
        case .cockpitView:
            cockpitViewBarButtonItemTapped(sender)
            changeViewButtonState = .centerView
        case .centerView:
            centerViewBarButtonItemTapped(sender)
            changeViewButtonState = .cockpitView
        }
        sender.title = changeViewButtonState.title
    }

    func centerViewBarButtonItemTapped(_ sender: UIBarButtonItem) {
        let cameraController = sceneView.cameraController as! AGSOrbitGeoElementCameraController

        cameraController.isCameraDistanceInteractive = true
        cameraController.isAutoPitchEnabled = false

        cameraController.targetOffsetX = 0
        cameraController.targetOffsetY = 0
        cameraController.targetOffsetZ = 0

        cameraController.cameraHeadingOffset = 0

        cameraController.minCameraPitchOffset = 10
        cameraController.maxCameraPitchOffset = 100
        cameraController.cameraPitchOffset = 45

        cameraController.minCameraDistance = 10
        cameraController.cameraDistance = 50
    }

    func cockpitViewBarButtonItemTapped(_ sender: UIBarButtonItem) {
        let cameraController = sceneView.cameraController as! AGSOrbitGeoElementCameraController

        cameraController.isCameraDistanceInteractive = false
        cameraController.minCameraDistance = 0.1
        // Unlock the camera pitch for the rotation animation.
        cameraController.minCameraPitchOffset = -180
        cameraController.maxCameraPitchOffset = 180

        // Animate the camera target to the cockpit.
        cameraController.setTargetOffsetX(0, targetOffsetY: -2, targetOffsetZ: 1.1, duration: 1)

        // If the camera is already tracking the plane's pitch, subtract it from
        // the delta angle for the animation.
        let pitchDelta = cameraController.isAutoPitchEnabled ? 0 : 90 - cameraController.cameraPitchOffset + (planeGraphic.attributes["PITCH"] as! Double)
        // Animate the camera so that it is at the target (cockpit), facing
        // forward (0 deg heading), and aligned with the horizon (90 deg pitch).
        moveCameraAnimationCancelable = cameraController.moveCamera(
            withDistanceDelta: 0.1 - cameraController.cameraDistance,
            headingDelta: -cameraController.cameraHeadingOffset,
            pitchDelta: pitchDelta,
            duration: 1
        ) { [weak self] animationFinished in
            self?.moveCameraAnimationCancelable = nil
            // If the animation was interrupted, don't lock the camera pitch.
            guard animationFinished else { return }
            // When the animation finishes, lock the camera pitch.
            cameraController.minCameraPitchOffset = 90
            cameraController.maxCameraPitchOffset = 90
            cameraController.isAutoPitchEnabled = true
        }
    }

    // MARK: UIViewController

    @IBSegueAction
    func makeSettingsViewController(_ coder: NSCoder) -> OrbitCameraSettingsViewController? {
        let settingsViewController = OrbitCameraSettingsViewController(
            coder: coder,
            cameraController: sceneView.cameraController as! AGSOrbitGeoElementCameraController,
            graphic: planeGraphic
        )
        settingsViewController?.modalPresentationStyle = .popover
        settingsViewController?.presentationController?.delegate = self
        return settingsViewController
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Add the source code button item to the right of navigation bar.
        (navigationItem.rightBarButtonItem as? SourceCodeBarButtonItem)?.filenames = ["OrbitCameraAroundObjectViewController", "OrbitCameraSettingsViewController"]
    }
}

extension OrbitCameraAroundObjectViewController: UIAdaptivePresentationControllerDelegate {
    func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
        return .none
    }
}

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.