Display scenes in augmented reality (AR)
Augmented reality (AR) experiences can be implemented with three common patterns: tabletop, flyover, and world-scale.
- Flyover – With flyover AR you can explore a scene using your device as a window into the virtual world. A typical flyover AR scenario starts with the scene’s virtual camera positioned over an area of interest. You can walk around and reorient the device to focus on specific content in the scene.
- Tabletop – Tabletop AR provides scene content anchored to a physical surface, as if it were a 3D-printed model. You can walk around the tabletop and view the scene from different angles.
- World-scale – A kind of AR scenario where scene content is rendered exactly where it would be in the physical world. This is used in scenarios ranging from viewing hidden infrastructure to displaying waypoints for navigation. In AR, the real world, rather than a basemap, provides the context for your GIS data.
Flyover | Tabletop | World-scale |
---|---|---|
![]() |
![]() |
![]() |
On screen, flyover is visually indistinguishable from normal scene rendering. | In tabletop, scene content is anchored to a real-world surface. | In world-scale AR, scene content is integrated with the real world. |
Support for augmented reality is provided through tools available in each ArcGIS Runtime API Toolkit.
Enable your app for AR
- See the Toolkit repo on GitHub for the latest instructions for installing.
- Add an AR view to your app.
- Configure privacy and permissions.
- Now you're ready to add tabletop AR, add flyover AR, or add world-scale AR to your app.
Add an AR view to your app
ArcGISARView
uses an underlying ARKit or ARCore view and an ArcGIS Runtime AGSSceneView
. Use the sceneView
method to access the Runtime scene view.
Use the following methods on ArcGISARView
to configure AR:
translationFactor
- controls the relationship between physical device position changes and changes in the position of the scene view's camera. This is useful for tabletop and flyover AR.originCamera
- controls the initial position of the scene view's camera. When position tracking is started,ArcGISARView
transforms the scene view camera's position using a transformation matrix provided by ARKit or ARCore. Once the origin camera is set, the manipulation of the scene view's camera is handled automatically.setInitialTransformation
– takes a point on the screen, finds the surface represented by that point, and applies a transformation such that the origin camera is pinned to the location represented by that point. This is useful for pinning content to a surface, which is needed for tabletop AR.arSCNViewDelegate
– forwards messages from ARKit to your app. You can use this to listen for tracking status changes, render content using native iOS rendering (SceneKit).locationChangeHandlerDelegate
- forwards messages from the location data source associated with the ArcGISARView to your app. You can use this to listen for location, heading and status changes.
In addition to the toolkit, you'll need to use the following ArcGIS Runtime features provided by the underlying scene view when creating AR experiences:
- Scene view space effect control — Disable rendering the 'starry sky' effect to display scene content on top of a camera feed.
- Scene view atmosphere effect control — Disable rendering the atmosphere effect to avoid obscuring rendered content.
- Surface transparency — Hide the ground when rendering world-scale AR because the camera feed, not the basemap, is providing context for your GIS content. You can use a semitransparent surface to calibrate your position in world-scale AR.
- Scene view navigation constraint — By default, scene views constrain the camera to being above the ground. You should disable this feature to enable users to use world-scale AR underground (for example, while in a basement). The navigation constraint will interfere with tabletop AR if the user attempts to look at the scene from below.
To use ArcGISARView
, first add it to the view, then configure the lifecycle methods to start and stop tracking as needed.
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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
// ---------- General --------------------- //
import UIKit
import ARKit
import ArcGISToolkit
import ArcGIS
class BasicARExample: UIViewController {
private let arView = ArcGISARView()
override func viewDidLoad() {
super.viewDidLoad()
arView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(arView)
NSLayoutConstraint.activate([
arView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
arView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
arView.topAnchor.constraint(equalTo: view.topAnchor),
arView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
arView.startTracking(.ignore)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
arView.stopTracking()
}
}
// ---------- Tabletop --------------------- //
override viewDidLoad() {
// the rest of viewDidLoad
// Configure the ArcGISARView’s arSCNViewDelegate to receive status updates
arView.arSCNViewDelegate = self
}
extension BasicARExample: ARSCNViewDelegate {
public func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) {
switch camera.trackingState {
case .normal:
helpLabel.isHidden = true
case .notAvailable:
helpLabel.text = "Location not available"
case .limited(let reason):
switch reason {
case .excessiveMotion:
helpLabel.text = "Try moving your phone more slowly"
helpLabel.isHidden = false
case .initializing:
helpLabel.text = "Keep moving your phone"
helpLabel.isHidden = false
case .insufficientFeatures:
helpLabel.text = "Try turning on more lights and moving around"
helpLabel.isHidden = false
case .relocalizing:
// this won't happen unless you enable relocalization
break
@unknown default:
break
}
}
}
}
extension BasicARExample: ARSCNViewDelegate {
//…
public func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// Only proceed if a plane node was detected
guard anchor as? ARPlaneAnchor != nil else { return }
// hasFoundPlane is an instance variable on BasicARExample
if !hasFoundPlane {
hasFoundPlane = true
enableTapToPlace()
}
}}
extension BasicARExample: AGSGeoViewTouchDelegate {
public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) {
// TODO – implement map placement
}
private func enableTapToPlace() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// Wait for the user to tap to place the scene
self.arView.sceneView.touchDelegate = self
}
}
}
extension BasicARExample: AGSGeoViewTouchDelegate {
public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) {
// Use a screen point to set the initial transformation on the view.
if self.arView.setInitialTransformation(using: screenPoint) {
configureSceneForAR()
} else {
presentAlert(message: "Failed to place scene, try again")
}
}
private func enableTapToPlace() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.helpLabel.isHidden = false
self.helpLabel.text = "Tap a surface to place the scene"
// Wait for the user to tap to place the scene
self.arView.sceneView.touchDelegate = self
}
}
}
let package = AGSMobileScenePackage(name: "philadelphia")
private func configureSceneForAR() {
// Load the package
package.load { [weak self] (err: Error?) in
guard let self = self else { return }
if let error = err {
self.presentAlert(error: error)
} else if let scene = self.package.scenes.first {
// Display the scene
self.arView.sceneView.scene = scene
// Configure scene surface opacity and navigation constraint
if let surface = scene.baseSurface {
surface.opacity = 0
surface.navigationConstraint = .none
}
self.updateTranslationFactorAndOriginCamera(scene) // To do
}
}
}
private func updateTranslationFactorAndOriginCamera() {
// Create the origin camera to be at the bottom and in the center of the scene
let newCam = AGSCamera(latitude: 39.95787000283599, longitude: -75.16996728256345, altitude: 8.813445091247559, heading: 0, pitch: 90, roll: 0)
// Set the origin camera
self.arView.originCamera = newCam
}
private func updateTranslationFactorAndOriginCamera() {
// Continued from above...
// Scene width is about 800m
let geographicContentWidth = 800.0
// Physical width of the table area the scene will be placed on in meters
let tableContainerWidth = 0.5 // About 1.5 feet
// Set the translation factor based on scene content width and desired physical size
self.arView.translationFactor = geographicContentWidth / tableContainerWidth
// Set the origin camera
self.arView.originCamera = newCam
}
// ---------- Flyover --------------------- //
private func configureSceneForAR() {
// create scene with basemap
let scene = AGSScene(basemapType: .imagery)
// create and add mesh layer
let meshLayer = AGSIntegratedMeshLayer(url: URL(string: "https://tiles.arcgis.com/tiles/u0sSNqDXr7puKJrF/arcgis/rest/services/Frankfurt2017_v17/SceneServer/layers/0")!)
scene.operationalLayers.add(meshLayer)
// show scene
arView.sceneView.scene = scene
// TODO – configure origin camera for AR
}
private func configureSceneForAR() {
// Continued from above
// Wait for the layer to load, then set the AR camera
meshLayer.load { [weak self, weak meshLayer] (err: Error?) in
guard let self = self else { return }
guard let `meshLayer` = meshLayer else { return }
if let error = err {
self.presentAlert(error: error)
} else if let envelope = meshLayer.fullExtent {
let camera = AGSCamera(latitude: envelope.center.y,
longitude: envelope.center.x,
altitude: 600,
heading: 0,
pitch: 90,
roll: 0)
self.arView.originCamera = camera
}
}
}
private func configureAR() {
// Continued from above
// Set the translation factor to enable rapid movement through the scene
arView.translationFactor = 1000
// Disable the navigation constraint
scene.baseSurface?.navigationConstraint = .none
// Turn the space and atmosphere effects on for an immersive experience
arView.sceneView.spaceEffect = .stars
arView.sceneView.atmosphereEffect = .realistic
}
// ---------- World-scale --------------------- //
arView.locationDataSource = AGSCLLocationDataSource()
private func configureSceneForAR() {
// Create scene with imagery basemap
let scene = AGSScene(basemapType: .imagery)
// Create an elevation source and add it to the scene
let elevationSource = AGSArcGISTiledElevationSource(url:
URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!)
scene.baseSurface?.elevationSources.append(elevationSource)
// Allow camera to go beneath the surface
scene.baseSurface?.navigationConstraint = .none
// Display the scene
arView.sceneView.scene = scene
// Configure atmosphere and space effect
arView.sceneView.spaceEffect = .transparent
arView.sceneView.atmosphereEffect = .none
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Continuous update mode
arView.startTracking(.continuous, completion: nil)
// One-time update mode
// arView.startTracking(.initial, completion: nil)
}
override func viewDidLoad() {
//…
arView.arSCNViewDelegate = self
/…
}
extension ARExample: ARSCNViewDelegate {
let planeRenderingMaterial = SCNMaterial()
public init() {
planeRenderingMaterial.diffuse.contentColor = UIColor(1, 0.5f)
}
public func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
if shouldHidePlanes {
return
}
guard let _ = anchor as? ARPlaneAnchor else { return }
// Create a custom object to visualize the plane geometry and extent.
if #available(iOS 11.3, *) {
// Place content only for anchors found by plane detection.
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
let arGeometry = planeAnchor.geometry
let arPlaneSceneGeometry = ARSCNPlaneGeometry(device: renderer.device!)
arPlaneSceneGeometry?.update(from: arGeometry)
let newNode = SCNNode(geometry: arPlaneSceneGeometry)
node.addChildNode(newNode)
arPlaneSceneGeometry?.materials = [_planeRenderingMaterial]
node.geometry = arPlaneSceneGeometry
}
}
public func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
if shouldHidePlanes {
// Remove plane visualization
node.removeFromParentNode()
return
}
// Create a custom object to visualize the plane geometry and extent.
if #available(iOS 11.3, *) {
// Place content only for anchors found by plane detection.
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
let arGeometry = planeAnchor.geometry
let arPlaneSceneGeometry = ARSCNPlaneGeometry(device: renderer.device?)
arPlaneSceneGeometry?.update(from: arGeometry)
node.childNodes[0].geometry = arPlaneSceneGeometry
if let material = node.geometry?.materials {
arPlaneSceneGeometry?.materials = material
}
}
}
}
Configure privacy and permissions
Before you can use augmented reality, you'll need to request location and camera permissions.
On iOS, ensure the following properties are set in info.plist:
Privacy - Camera Usage Description
Privacy - Location When In Use Usage Description
The deployment target should be set to a supported version of iOS (see System requirements for details).
If you’d like to restrict your app to installing only on devices that support ARKit, add arkit
to the required device capabilities section of info.plist:
1
2
3
4
5
6
<!-- -->
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arkit</string>
</array>
Once you have installed the toolkit, configured your app to meet privacy requirements, requested location permissions, and added an ArcGISARView
to your app, you can begin implementing your AR experience.
Understand Common AR Patterns
There are many AR scenarios you can achieve with Runtime. This SDK recognizes the following common patterns for AR:
- Flyover – Flyover AR is a kind of AR scenario that allows you to explore a scene using your device as a window into the virtual world. A typical flyover AR scenario will start with the scene’s virtual camera positioned over an area of interest. You can walk around and reorient the device to focus on specific content in the scene.
- Tabletop – A kind of AR scenario where scene content is anchored to a physical surface, as if it were a 3D-printed model. You can walk around the tabletop and view the scene from different angles.
- World-scale – A kind of AR scenario where scene content is rendered exactly where it would be in the physical world. This is used in scenarios ranging from viewing hidden infrastructure to displaying waypoints for navigation. In AR, the real world, rather than a basemap, provides the context for your GIS data.
Each experience is built using some combination of the features in Runtime and the toolkit and some basic behavioral assumptions.
AR pattern | Origin camera | Translation factor | Scene view | Base surface |
---|---|---|---|---|
Flyover AR | Above the tallest content in the scene | A large value to enable rapid traversal; 0 to restrict movement | Space effect: Stars Atmosphere: Realistic | Displayed |
Tabletop AR | On the ground at the center or lowest point on the scene | Based on the size of the target content and the physical table | Space effect: Transparent Atmosphere: None | Optional |
World-scale AR | At the same location as the physical device camera | 1, to keep virtual content in sync with real-world environment | Space effect: Transparent Atmosphere: None | Optional for calibration |
Add tabletop AR to your app
Tabletop AR allows you to use your device to interact with scenes as if they were 3D-printed models sitting on your desk. You could, for example, use tabletop AR to virtually explore a proposed development without needing to create a physical model.
Implement tabletop AR
Tabletop AR often allows users to place scene content on a physical surface of their choice, such as the top of a desk, for example. Once the content is placed, it stays anchored to the surface as the user moves around it.
Create an
ArcGISARView
and add it to the viewListen for ARKit tracking tracking status changes and provide feedback to the user, for example when the user needs to move the phone more slowly or turn on more lights.
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 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
// ---------- General --------------------- // import UIKit import ARKit import ArcGISToolkit import ArcGIS class BasicARExample: UIViewController { private let arView = ArcGISARView() override func viewDidLoad() { super.viewDidLoad() arView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(arView) NSLayoutConstraint.activate([ arView.leadingAnchor.constraint(equalTo: view.leadingAnchor), arView.trailingAnchor.constraint(equalTo: view.trailingAnchor), arView.topAnchor.constraint(equalTo: view.topAnchor), arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) arView.startTracking(.ignore) } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) arView.stopTracking() } } // ---------- Tabletop --------------------- // override viewDidLoad() { // the rest of viewDidLoad // Configure the ArcGISARView’s arSCNViewDelegate to receive status updates arView.arSCNViewDelegate = self } extension BasicARExample: ARSCNViewDelegate { public func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) { switch camera.trackingState { case .normal: helpLabel.isHidden = true case .notAvailable: helpLabel.text = "Location not available" case .limited(let reason): switch reason { case .excessiveMotion: helpLabel.text = "Try moving your phone more slowly" helpLabel.isHidden = false case .initializing: helpLabel.text = "Keep moving your phone" helpLabel.isHidden = false case .insufficientFeatures: helpLabel.text = "Try turning on more lights and moving around" helpLabel.isHidden = false case .relocalizing: // this won't happen unless you enable relocalization break @unknown default: break } } } } extension BasicARExample: ARSCNViewDelegate { //… public func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { // Only proceed if a plane node was detected guard anchor as? ARPlaneAnchor != nil else { return } // hasFoundPlane is an instance variable on BasicARExample if !hasFoundPlane { hasFoundPlane = true enableTapToPlace() } }} extension BasicARExample: AGSGeoViewTouchDelegate { public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { // TODO – implement map placement } private func enableTapToPlace() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } // Wait for the user to tap to place the scene self.arView.sceneView.touchDelegate = self } } } extension BasicARExample: AGSGeoViewTouchDelegate { public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { // Use a screen point to set the initial transformation on the view. if self.arView.setInitialTransformation(using: screenPoint) { configureSceneForAR() } else { presentAlert(message: "Failed to place scene, try again") } } private func enableTapToPlace() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.helpLabel.isHidden = false self.helpLabel.text = "Tap a surface to place the scene" // Wait for the user to tap to place the scene self.arView.sceneView.touchDelegate = self } } } let package = AGSMobileScenePackage(name: "philadelphia") private func configureSceneForAR() { // Load the package package.load { [weak self] (err: Error?) in guard let self = self else { return } if let error = err { self.presentAlert(error: error) } else if let scene = self.package.scenes.first { // Display the scene self.arView.sceneView.scene = scene // Configure scene surface opacity and navigation constraint if let surface = scene.baseSurface { surface.opacity = 0 surface.navigationConstraint = .none } self.updateTranslationFactorAndOriginCamera(scene) // To do } } } private func updateTranslationFactorAndOriginCamera() { // Create the origin camera to be at the bottom and in the center of the scene let newCam = AGSCamera(latitude: 39.95787000283599, longitude: -75.16996728256345, altitude: 8.813445091247559, heading: 0, pitch: 90, roll: 0) // Set the origin camera self.arView.originCamera = newCam } private func updateTranslationFactorAndOriginCamera() { // Continued from above... // Scene width is about 800m let geographicContentWidth = 800.0 // Physical width of the table area the scene will be placed on in meters let tableContainerWidth = 0.5 // About 1.5 feet // Set the translation factor based on scene content width and desired physical size self.arView.translationFactor = geographicContentWidth / tableContainerWidth // Set the origin camera self.arView.originCamera = newCam } // ---------- Flyover --------------------- // private func configureSceneForAR() { // create scene with basemap let scene = AGSScene(basemapType: .imagery) // create and add mesh layer let meshLayer = AGSIntegratedMeshLayer(url: URL(string: "https://tiles.arcgis.com/tiles/u0sSNqDXr7puKJrF/arcgis/rest/services/Frankfurt2017_v17/SceneServer/layers/0")!) scene.operationalLayers.add(meshLayer) // show scene arView.sceneView.scene = scene // TODO – configure origin camera for AR } private func configureSceneForAR() { // Continued from above // Wait for the layer to load, then set the AR camera meshLayer.load { [weak self, weak meshLayer] (err: Error?) in guard let self = self else { return } guard let `meshLayer` = meshLayer else { return } if let error = err { self.presentAlert(error: error) } else if let envelope = meshLayer.fullExtent { let camera = AGSCamera(latitude: envelope.center.y, longitude: envelope.center.x, altitude: 600, heading: 0, pitch: 90, roll: 0) self.arView.originCamera = camera } } } private func configureAR() { // Continued from above // Set the translation factor to enable rapid movement through the scene arView.translationFactor = 1000 // Disable the navigation constraint scene.baseSurface?.navigationConstraint = .none // Turn the space and atmosphere effects on for an immersive experience arView.sceneView.spaceEffect = .stars arView.sceneView.atmosphereEffect = .realistic } // ---------- World-scale --------------------- // arView.locationDataSource = AGSCLLocationDataSource() private func configureSceneForAR() { // Create scene with imagery basemap let scene = AGSScene(basemapType: .imagery) // Create an elevation source and add it to the scene let elevationSource = AGSArcGISTiledElevationSource(url: URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!) scene.baseSurface?.elevationSources.append(elevationSource) // Allow camera to go beneath the surface scene.baseSurface?.navigationConstraint = .none // Display the scene arView.sceneView.scene = scene // Configure atmosphere and space effect arView.sceneView.spaceEffect = .transparent arView.sceneView.atmosphereEffect = .none } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // Continuous update mode arView.startTracking(.continuous, completion: nil) // One-time update mode // arView.startTracking(.initial, completion: nil) } override func viewDidLoad() { //… arView.arSCNViewDelegate = self /… } extension ARExample: ARSCNViewDelegate { let planeRenderingMaterial = SCNMaterial() public init() { planeRenderingMaterial.diffuse.contentColor = UIColor(1, 0.5f) } public func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { if shouldHidePlanes { return } guard let _ = anchor as? ARPlaneAnchor else { return } // Create a custom object to visualize the plane geometry and extent. if #available(iOS 11.3, *) { // Place content only for anchors found by plane detection. guard let planeAnchor = anchor as? ARPlaneAnchor else { return } let arGeometry = planeAnchor.geometry let arPlaneSceneGeometry = ARSCNPlaneGeometry(device: renderer.device!) arPlaneSceneGeometry?.update(from: arGeometry) let newNode = SCNNode(geometry: arPlaneSceneGeometry) node.addChildNode(newNode) arPlaneSceneGeometry?.materials = [_planeRenderingMaterial] node.geometry = arPlaneSceneGeometry } } public func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { if shouldHidePlanes { // Remove plane visualization node.removeFromParentNode() return } // Create a custom object to visualize the plane geometry and extent. if #available(iOS 11.3, *) { // Place content only for anchors found by plane detection. guard let planeAnchor = anchor as? ARPlaneAnchor else { return } let arGeometry = planeAnchor.geometry let arPlaneSceneGeometry = ARSCNPlaneGeometry(device: renderer.device?) arPlaneSceneGeometry?.update(from: arGeometry) node.childNodes[0].geometry = arPlaneSceneGeometry if let material = node.geometry?.materials { arPlaneSceneGeometry?.materials = material } } } }When tracking is ready and at least one plane has been found, wait for the user to tap.
ARSCNViewDelegate
definesrenderer(:didAdd node, for anchor)
which you can use to detect planes.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 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
// ---------- General --------------------- // import UIKit import ARKit import ArcGISToolkit import ArcGIS class BasicARExample: UIViewController { private let arView = ArcGISARView() override func viewDidLoad() { super.viewDidLoad() arView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(arView) NSLayoutConstraint.activate([ arView.leadingAnchor.constraint(equalTo: view.leadingAnchor), arView.trailingAnchor.constraint(equalTo: view.trailingAnchor), arView.topAnchor.constraint(equalTo: view.topAnchor), arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) arView.startTracking(.ignore) } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) arView.stopTracking() } } // ---------- Tabletop --------------------- // override viewDidLoad() { // the rest of viewDidLoad // Configure the ArcGISARView’s arSCNViewDelegate to receive status updates arView.arSCNViewDelegate = self } extension BasicARExample: ARSCNViewDelegate { public func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) { switch camera.trackingState { case .normal: helpLabel.isHidden = true case .notAvailable: helpLabel.text = "Location not available" case .limited(let reason): switch reason { case .excessiveMotion: helpLabel.text = "Try moving your phone more slowly" helpLabel.isHidden = false case .initializing: helpLabel.text = "Keep moving your phone" helpLabel.isHidden = false case .insufficientFeatures: helpLabel.text = "Try turning on more lights and moving around" helpLabel.isHidden = false case .relocalizing: // this won't happen unless you enable relocalization break @unknown default: break } } } } extension BasicARExample: ARSCNViewDelegate { //… public func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { // Only proceed if a plane node was detected guard anchor as? ARPlaneAnchor != nil else { return } // hasFoundPlane is an instance variable on BasicARExample if !hasFoundPlane { hasFoundPlane = true enableTapToPlace() } }} extension BasicARExample: AGSGeoViewTouchDelegate { public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { // TODO – implement map placement } private func enableTapToPlace() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } // Wait for the user to tap to place the scene self.arView.sceneView.touchDelegate = self } } } extension BasicARExample: AGSGeoViewTouchDelegate { public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { // Use a screen point to set the initial transformation on the view. if self.arView.setInitialTransformation(using: screenPoint) { configureSceneForAR() } else { presentAlert(message: "Failed to place scene, try again") } } private func enableTapToPlace() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.helpLabel.isHidden = false self.helpLabel.text = "Tap a surface to place the scene" // Wait for the user to tap to place the scene self.arView.sceneView.touchDelegate = self } } } let package = AGSMobileScenePackage(name: "philadelphia") private func configureSceneForAR() { // Load the package package.load { [weak self] (err: Error?) in guard let self = self else { return } if let error = err { self.presentAlert(error: error) } else if let scene = self.package.scenes.first { // Display the scene self.arView.sceneView.scene = scene // Configure scene surface opacity and navigation constraint if let surface = scene.baseSurface { surface.opacity = 0 surface.navigationConstraint = .none } self.updateTranslationFactorAndOriginCamera(scene) // To do } } } private func updateTranslationFactorAndOriginCamera() { // Create the origin camera to be at the bottom and in the center of the scene let newCam = AGSCamera(latitude: 39.95787000283599, longitude: -75.16996728256345, altitude: 8.813445091247559, heading: 0, pitch: 90, roll: 0) // Set the origin camera self.arView.originCamera = newCam } private func updateTranslationFactorAndOriginCamera() { // Continued from above... // Scene width is about 800m let geographicContentWidth = 800.0 // Physical width of the table area the scene will be placed on in meters let tableContainerWidth = 0.5 // About 1.5 feet // Set the translation factor based on scene content width and desired physical size self.arView.translationFactor = geographicContentWidth / tableContainerWidth // Set the origin camera self.arView.originCamera = newCam } // ---------- Flyover --------------------- // private func configureSceneForAR() { // create scene with basemap let scene = AGSScene(basemapType: .imagery) // create and add mesh layer let meshLayer = AGSIntegratedMeshLayer(url: URL(string: "https://tiles.arcgis.com/tiles/u0sSNqDXr7puKJrF/arcgis/rest/services/Frankfurt2017_v17/SceneServer/layers/0")!) scene.operationalLayers.add(meshLayer) // show scene arView.sceneView.scene = scene // TODO – configure origin camera for AR } private func configureSceneForAR() { // Continued from above // Wait for the layer to load, then set the AR camera meshLayer.load { [weak self, weak meshLayer] (err: Error?) in guard let self = self else { return } guard let `meshLayer` = meshLayer else { return } if let error = err { self.presentAlert(error: error) } else if let envelope = meshLayer.fullExtent { let camera = AGSCamera(latitude: envelope.center.y, longitude: envelope.center.x, altitude: 600, heading: 0, pitch: 90, roll: 0) self.arView.originCamera = camera } } } private func configureAR() { // Continued from above // Set the translation factor to enable rapid movement through the scene arView.translationFactor = 1000 // Disable the navigation constraint scene.baseSurface?.navigationConstraint = .none // Turn the space and atmosphere effects on for an immersive experience arView.sceneView.spaceEffect = .stars arView.sceneView.atmosphereEffect = .realistic } // ---------- World-scale --------------------- // arView.locationDataSource = AGSCLLocationDataSource() private func configureSceneForAR() { // Create scene with imagery basemap let scene = AGSScene(basemapType: .imagery) // Create an elevation source and add it to the scene let elevationSource = AGSArcGISTiledElevationSource(url: URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!) scene.baseSurface?.elevationSources.append(elevationSource) // Allow camera to go beneath the surface scene.baseSurface?.navigationConstraint = .none // Display the scene arView.sceneView.scene = scene // Configure atmosphere and space effect arView.sceneView.spaceEffect = .transparent arView.sceneView.atmosphereEffect = .none } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // Continuous update mode arView.startTracking(.continuous, completion: nil) // One-time update mode // arView.startTracking(.initial, completion: nil) } override func viewDidLoad() { //… arView.arSCNViewDelegate = self /… } extension ARExample: ARSCNViewDelegate { let planeRenderingMaterial = SCNMaterial() public init() { planeRenderingMaterial.diffuse.contentColor = UIColor(1, 0.5f) } public func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { if shouldHidePlanes { return } guard let _ = anchor as? ARPlaneAnchor else { return } // Create a custom object to visualize the plane geometry and extent. if #available(iOS 11.3, *) { // Place content only for anchors found by plane detection. guard let planeAnchor = anchor as? ARPlaneAnchor else { return } let arGeometry = planeAnchor.geometry let arPlaneSceneGeometry = ARSCNPlaneGeometry(device: renderer.device!) arPlaneSceneGeometry?.update(from: arGeometry) let newNode = SCNNode(geometry: arPlaneSceneGeometry) node.addChildNode(newNode) arPlaneSceneGeometry?.materials = [_planeRenderingMaterial] node.geometry = arPlaneSceneGeometry } } public func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { if shouldHidePlanes { // Remove plane visualization node.removeFromParentNode() return } // Create a custom object to visualize the plane geometry and extent. if #available(iOS 11.3, *) { // Place content only for anchors found by plane detection. guard let planeAnchor = anchor as? ARPlaneAnchor else { return } let arGeometry = planeAnchor.geometry let arPlaneSceneGeometry = ARSCNPlaneGeometry(device: renderer.device?) arPlaneSceneGeometry?.update(from: arGeometry) node.childNodes[0].geometry = arPlaneSceneGeometry if let material = node.geometry?.materials { arPlaneSceneGeometry?.materials = material } } } }Once the user has tapped a point, call
setInitialTransformation
. The toolkit will use the native platform’s plane detection to position the virtual camera relative to the plane. If the result is true, the transformation has been set successfully and you can place the scene.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 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
// ---------- General --------------------- // import UIKit import ARKit import ArcGISToolkit import ArcGIS class BasicARExample: UIViewController { private let arView = ArcGISARView() override func viewDidLoad() { super.viewDidLoad() arView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(arView) NSLayoutConstraint.activate([ arView.leadingAnchor.constraint(equalTo: view.leadingAnchor), arView.trailingAnchor.constraint(equalTo: view.trailingAnchor), arView.topAnchor.constraint(equalTo: view.topAnchor), arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) arView.startTracking(.ignore) } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) arView.stopTracking() } } // ---------- Tabletop --------------------- // override viewDidLoad() { // the rest of viewDidLoad // Configure the ArcGISARView’s arSCNViewDelegate to receive status updates arView.arSCNViewDelegate = self } extension BasicARExample: ARSCNViewDelegate { public func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) { switch camera.trackingState { case .normal: helpLabel.isHidden = true case .notAvailable: helpLabel.text = "Location not available" case .limited(let reason): switch reason { case .excessiveMotion: helpLabel.text = "Try moving your phone more slowly" helpLabel.isHidden = false case .initializing: helpLabel.text = "Keep moving your phone" helpLabel.isHidden = false case .insufficientFeatures: helpLabel.text = "Try turning on more lights and moving around" helpLabel.isHidden = false case .relocalizing: // this won't happen unless you enable relocalization break @unknown default: break } } } } extension BasicARExample: ARSCNViewDelegate { //… public func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { // Only proceed if a plane node was detected guard anchor as? ARPlaneAnchor != nil else { return } // hasFoundPlane is an instance variable on BasicARExample if !hasFoundPlane { hasFoundPlane = true enableTapToPlace() } }} extension BasicARExample: AGSGeoViewTouchDelegate { public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { // TODO – implement map placement } private func enableTapToPlace() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } // Wait for the user to tap to place the scene self.arView.sceneView.touchDelegate = self } } } extension BasicARExample: AGSGeoViewTouchDelegate { public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { // Use a screen point to set the initial transformation on the view. if self.arView.setInitialTransformation(using: screenPoint) { configureSceneForAR() } else { presentAlert(message: "Failed to place scene, try again") } } private func enableTapToPlace() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.helpLabel.isHidden = false self.helpLabel.text = "Tap a surface to place the scene" // Wait for the user to tap to place the scene self.arView.sceneView.touchDelegate = self } } } let package = AGSMobileScenePackage(name: "philadelphia") private func configureSceneForAR() { // Load the package package.load { [weak self] (err: Error?) in guard let self = self else { return } if let error = err { self.presentAlert(error: error) } else if let scene = self.package.scenes.first { // Display the scene self.arView.sceneView.scene = scene // Configure scene surface opacity and navigation constraint if let surface = scene.baseSurface { surface.opacity = 0 surface.navigationConstraint = .none } self.updateTranslationFactorAndOriginCamera(scene) // To do } } } private func updateTranslationFactorAndOriginCamera() { // Create the origin camera to be at the bottom and in the center of the scene let newCam = AGSCamera(latitude: 39.95787000283599, longitude: -75.16996728256345, altitude: 8.813445091247559, heading: 0, pitch: 90, roll: 0) // Set the origin camera self.arView.originCamera = newCam } private func updateTranslationFactorAndOriginCamera() { // Continued from above... // Scene width is about 800m let geographicContentWidth = 800.0 // Physical width of the table area the scene will be placed on in meters let tableContainerWidth = 0.5 // About 1.5 feet // Set the translation factor based on scene content width and desired physical size self.arView.translationFactor = geographicContentWidth / tableContainerWidth // Set the origin camera self.arView.originCamera = newCam } // ---------- Flyover --------------------- // private func configureSceneForAR() { // create scene with basemap let scene = AGSScene(basemapType: .imagery) // create and add mesh layer let meshLayer = AGSIntegratedMeshLayer(url: URL(string: "https://tiles.arcgis.com/tiles/u0sSNqDXr7puKJrF/arcgis/rest/services/Frankfurt2017_v17/SceneServer/layers/0")!) scene.operationalLayers.add(meshLayer) // show scene arView.sceneView.scene = scene // TODO – configure origin camera for AR } private func configureSceneForAR() { // Continued from above // Wait for the layer to load, then set the AR camera meshLayer.load { [weak self, weak meshLayer] (err: Error?) in guard let self = self else { return } guard let `meshLayer` = meshLayer else { return } if let error = err { self.presentAlert(error: error) } else if let envelope = meshLayer.fullExtent { let camera = AGSCamera(latitude: envelope.center.y, longitude: envelope.center.x, altitude: 600, heading: 0, pitch: 90, roll: 0) self.arView.originCamera = camera } } } private func configureAR() { // Continued from above // Set the translation factor to enable rapid movement through the scene arView.translationFactor = 1000 // Disable the navigation constraint scene.baseSurface?.navigationConstraint = .none // Turn the space and atmosphere effects on for an immersive experience arView.sceneView.spaceEffect = .stars arView.sceneView.atmosphereEffect = .realistic } // ---------- World-scale --------------------- // arView.locationDataSource = AGSCLLocationDataSource() private func configureSceneForAR() { // Create scene with imagery basemap let scene = AGSScene(basemapType: .imagery) // Create an elevation source and add it to the scene let elevationSource = AGSArcGISTiledElevationSource(url: URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!) scene.baseSurface?.elevationSources.append(elevationSource) // Allow camera to go beneath the surface scene.baseSurface?.navigationConstraint = .none // Display the scene arView.sceneView.scene = scene // Configure atmosphere and space effect arView.sceneView.spaceEffect = .transparent arView.sceneView.atmosphereEffect = .none } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // Continuous update mode arView.startTracking(.continuous, completion: nil) // One-time update mode // arView.startTracking(.initial, completion: nil) } override func viewDidLoad() { //… arView.arSCNViewDelegate = self /… } extension ARExample: ARSCNViewDelegate { let planeRenderingMaterial = SCNMaterial() public init() { planeRenderingMaterial.diffuse.contentColor = UIColor(1, 0.5f) } public func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { if shouldHidePlanes { return } guard let _ = anchor as? ARPlaneAnchor else { return } // Create a custom object to visualize the plane geometry and extent. if #available(iOS 11.3, *) { // Place content only for anchors found by plane detection. guard let planeAnchor = anchor as? ARPlaneAnchor else { return } let arGeometry = planeAnchor.geometry let arPlaneSceneGeometry = ARSCNPlaneGeometry(device: renderer.device!) arPlaneSceneGeometry?.update(from: arGeometry) let newNode = SCNNode(geometry: arPlaneSceneGeometry) node.addChildNode(newNode) arPlaneSceneGeometry?.materials = [_planeRenderingMaterial] node.geometry = arPlaneSceneGeometry } } public func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { if shouldHidePlanes { // Remove plane visualization node.removeFromParentNode() return } // Create a custom object to visualize the plane geometry and extent. if #available(iOS 11.3, *) { // Place content only for anchors found by plane detection. guard let planeAnchor = anchor as? ARPlaneAnchor else { return } let arGeometry = planeAnchor.geometry let arPlaneSceneGeometry = ARSCNPlaneGeometry(device: renderer.device?) arPlaneSceneGeometry?.update(from: arGeometry) node.childNodes[0].geometry = arPlaneSceneGeometry if let material = node.geometry?.materials { arPlaneSceneGeometry?.materials = material } } } }Create and display the scene. Set the navigation constraint on the scene’s base surface to
.none
.For demonstration purposes, this code uses the Philadelphia mobile scene package because it is particularly well-suited for tabletop display. You can download that .mspk and add it to your project to make the code below work. Alternatively, you can use any scene for tabletop mapping, but be sure to define a clipping distance for a proper tabletop experience.
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 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
// ---------- General --------------------- // import UIKit import ARKit import ArcGISToolkit import ArcGIS class BasicARExample: UIViewController { private let arView = ArcGISARView() override func viewDidLoad() { super.viewDidLoad() arView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(arView) NSLayoutConstraint.activate([ arView.leadingAnchor.constraint(equalTo: view.leadingAnchor), arView.trailingAnchor.constraint(equalTo: view.trailingAnchor), arView.topAnchor.constraint(equalTo: view.topAnchor), arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) arView.startTracking(.ignore) } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) arView.stopTracking() } } // ---------- Tabletop --------------------- // override viewDidLoad() { // the rest of viewDidLoad // Configure the ArcGISARView’s arSCNViewDelegate to receive status updates arView.arSCNViewDelegate = self } extension BasicARExample: ARSCNViewDelegate { public func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) { switch camera.trackingState { case .normal: helpLabel.isHidden = true case .notAvailable: helpLabel.text = "Location not available" case .limited(let reason): switch reason { case .excessiveMotion: helpLabel.text = "Try moving your phone more slowly" helpLabel.isHidden = false case .initializing: helpLabel.text = "Keep moving your phone" helpLabel.isHidden = false case .insufficientFeatures: helpLabel.text = "Try turning on more lights and moving around" helpLabel.isHidden = false case .relocalizing: // this won't happen unless you enable relocalization break @unknown default: break } } } } extension BasicARExample: ARSCNViewDelegate { //… public func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { // Only proceed if a plane node was detected guard anchor as? ARPlaneAnchor != nil else { return } // hasFoundPlane is an instance variable on BasicARExample if !hasFoundPlane { hasFoundPlane = true enableTapToPlace() } }} extension BasicARExample: AGSGeoViewTouchDelegate { public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { // TODO – implement map placement } private func enableTapToPlace() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } // Wait for the user to tap to place the scene self.arView.sceneView.touchDelegate = self } } } extension BasicARExample: AGSGeoViewTouchDelegate { public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { // Use a screen point to set the initial transformation on the view. if self.arView.setInitialTransformation(using: screenPoint) { configureSceneForAR() } else { presentAlert(message: "Failed to place scene, try again") } } private func enableTapToPlace() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.helpLabel.isHidden = false self.helpLabel.text = "Tap a surface to place the scene" // Wait for the user to tap to place the scene self.arView.sceneView.touchDelegate = self } } } let package = AGSMobileScenePackage(name: "philadelphia") private func configureSceneForAR() { // Load the package package.load { [weak self] (err: Error?) in guard let self = self else { return } if let error = err { self.presentAlert(error: error) } else if let scene = self.package.scenes.first { // Display the scene self.arView.sceneView.scene = scene // Configure scene surface opacity and navigation constraint if let surface = scene.baseSurface { surface.opacity = 0 surface.navigationConstraint = .none } self.updateTranslationFactorAndOriginCamera(scene) // To do } } } private func updateTranslationFactorAndOriginCamera() { // Create the origin camera to be at the bottom and in the center of the scene let newCam = AGSCamera(latitude: 39.95787000283599, longitude: -75.16996728256345, altitude: 8.813445091247559, heading: 0, pitch: 90, roll: 0) // Set the origin camera self.arView.originCamera = newCam } private func updateTranslationFactorAndOriginCamera() { // Continued from above... // Scene width is about 800m let geographicContentWidth = 800.0 // Physical width of the table area the scene will be placed on in meters let tableContainerWidth = 0.5 // About 1.5 feet // Set the translation factor based on scene content width and desired physical size self.arView.translationFactor = geographicContentWidth / tableContainerWidth // Set the origin camera self.arView.originCamera = newCam } // ---------- Flyover --------------------- // private func configureSceneForAR() { // create scene with basemap let scene = AGSScene(basemapType: .imagery) // create and add mesh layer let meshLayer = AGSIntegratedMeshLayer(url: URL(string: "https://tiles.arcgis.com/tiles/u0sSNqDXr7puKJrF/arcgis/rest/services/Frankfurt2017_v17/SceneServer/layers/0")!) scene.operationalLayers.add(meshLayer) // show scene arView.sceneView.scene = scene // TODO – configure origin camera for AR } private func configureSceneForAR() { // Continued from above // Wait for the layer to load, then set the AR camera meshLayer.load { [weak self, weak meshLayer] (err: Error?) in guard let self = self else { return } guard let `meshLayer` = meshLayer else { return } if let error = err { self.presentAlert(error: error) } else if let envelope = meshLayer.fullExtent { let camera = AGSCamera(latitude: envelope.center.y, longitude: envelope.center.x, altitude: 600, heading: 0, pitch: 90, roll: 0) self.arView.originCamera = camera } } } private func configureAR() { // Continued from above // Set the translation factor to enable rapid movement through the scene arView.translationFactor = 1000 // Disable the navigation constraint scene.baseSurface?.navigationConstraint = .none // Turn the space and atmosphere effects on for an immersive experience arView.sceneView.spaceEffect = .stars arView.sceneView.atmosphereEffect = .realistic } // ---------- World-scale --------------------- // arView.locationDataSource = AGSCLLocationDataSource() private func configureSceneForAR() { // Create scene with imagery basemap let scene = AGSScene(basemapType: .imagery) // Create an elevation source and add it to the scene let elevationSource = AGSArcGISTiledElevationSource(url: URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!) scene.baseSurface?.elevationSources.append(elevationSource) // Allow camera to go beneath the surface scene.baseSurface?.navigationConstraint = .none // Display the scene arView.sceneView.scene = scene // Configure atmosphere and space effect arView.sceneView.spaceEffect = .transparent arView.sceneView.atmosphereEffect = .none } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // Continuous update mode arView.startTracking(.continuous, completion: nil) // One-time update mode // arView.startTracking(.initial, completion: nil) } override func viewDidLoad() { //… arView.arSCNViewDelegate = self /… } extension ARExample: ARSCNViewDelegate { let planeRenderingMaterial = SCNMaterial() public init() { planeRenderingMaterial.diffuse.contentColor = UIColor(1, 0.5f) } public func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { if shouldHidePlanes { return } guard let _ = anchor as? ARPlaneAnchor else { return } // Create a custom object to visualize the plane geometry and extent. if #available(iOS 11.3, *) { // Place content only for anchors found by plane detection. guard let planeAnchor = anchor as? ARPlaneAnchor else { return } let arGeometry = planeAnchor.geometry let arPlaneSceneGeometry = ARSCNPlaneGeometry(device: renderer.device!) arPlaneSceneGeometry?.update(from: arGeometry) let newNode = SCNNode(geometry: arPlaneSceneGeometry) node.addChildNode(newNode) arPlaneSceneGeometry?.materials = [_planeRenderingMaterial] node.geometry = arPlaneSceneGeometry } } public func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { if shouldHidePlanes { // Remove plane visualization node.removeFromParentNode() return } // Create a custom object to visualize the plane geometry and extent. if #available(iOS 11.3, *) { // Place content only for anchors found by plane detection. guard let planeAnchor = anchor as? ARPlaneAnchor else { return } let arGeometry = planeAnchor.geometry let arPlaneSceneGeometry = ARSCNPlaneGeometry(device: renderer.device?) arPlaneSceneGeometry?.update(from: arGeometry) node.childNodes[0].geometry = arPlaneSceneGeometry if let material = node.geometry?.materials { arPlaneSceneGeometry?.materials = material } } } }Find an anchor point in the scene. You can use a known value, a user-selected value, or a computed value. For simplicity, this example uses a known value. Place the origin camera at that point.
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 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
// ---------- General --------------------- // import UIKit import ARKit import ArcGISToolkit import ArcGIS class BasicARExample: UIViewController { private let arView = ArcGISARView() override func viewDidLoad() { super.viewDidLoad() arView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(arView) NSLayoutConstraint.activate([ arView.leadingAnchor.constraint(equalTo: view.leadingAnchor), arView.trailingAnchor.constraint(equalTo: view.trailingAnchor), arView.topAnchor.constraint(equalTo: view.topAnchor), arView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) arView.startTracking(.ignore) } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) arView.stopTracking() } } // ---------- Tabletop --------------------- // override viewDidLoad() { // the rest of viewDidLoad // Configure the ArcGISARView’s arSCNViewDelegate to receive status updates arView.arSCNViewDelegate = self } extension BasicARExample: ARSCNViewDelegate { public func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) { switch camera.trackingState { case .normal: helpLabel.isHidden = true case .notAvailable: helpLabel.text = "Location not available" case .limited(let reason): switch reason { case .excessiveMotion: helpLabel.text = "Try moving your phone more slowly" helpLabel.isHidden = false case .initializing: helpLabel.text = "Keep moving your phone" helpLabel.isHidden = false case .insufficientFeatures: helpLabel.text = "Try turning on more lights and moving around" helpLabel.isHidden = false case .relocalizing: // this won't happen unless you enable relocalization break @unknown default: break } } } } extension BasicARExample: ARSCNViewDelegate { //… public func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { // Only proceed if a plane node was detected guard anchor as? ARPlaneAnchor != nil else { return } // hasFoundPlane is an instance variable on BasicARExample if !hasFoundPlane { hasFoundPlane = true enableTapToPlace() } }} extension BasicARExample: AGSGeoViewTouchDelegate { public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { // TODO – implement map placement } private func enableTapToPlace() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } // Wait for the user to tap to place the scene self.arView.sceneView.touchDelegate = self } } } extension BasicARExample: AGSGeoViewTouchDelegate { public func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) { // Use a screen point to set the initial transformation on the view. if self.arView.setInitialTransformation(using: screenPoint) { configureSceneForAR() } else { presentAlert(message: "Failed to place scene, try again") } } private func enableTapToPlace() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.helpLabel.isHidden = false self.helpLabel.text = "Tap a surface to place the scene" // Wait for the user to tap to place the scene self.arView.sceneView.touchDelegate = self } } } let package = AGSMobileScenePackage(name: "philadelphia") private func configureSceneForAR() { // Load the package package.load { [weak self] (err: Error?) in guard let self = self else { return } if let error = err { self.presentAlert(error: error) } else if let scene = self.package.scenes.first { // Display the scene self.arView.sceneView.scene = scene // Configure scene surface opacity and navigation constraint if let surface = scene.baseSurface { surface.opacity = 0 surface.navigationConstraint = .none } self.updateTranslationFactorAndOriginCamera(scene) // To do } } } private func updateTranslationFactorAndOriginCamera() { // Create the origin camera to be at the bottom and in the center of the scene let newCam = AGSCamera(latitude: 39.95787000283599, longitude: -75.16996728256345, altitude: 8.813445091247559, heading: 0, pitch: 90, roll: 0) // Set the origin camera self.arView.originCamera = newCam } private func updateTranslationFactorAndOriginCamera() { // Continued from above... // Scene width is about 800m let geographicContentWidth = 800.0 // Physical width of the table area the scene will be placed on in meters let tableContainerWidth = 0.5 // About 1.5 feet // Set the translation factor based on scene content width and desired physical size self.arView.translationFactor = geographicContentWidth / tableContainerWidth // Set the origin camera self.arView.originCamera = newCam } // ---------- Flyover --------------------- // private func configureSceneForAR() { // create scene with basemap let scene = AGSScene(basemapType: .imagery) // create and add mesh layer let meshLayer = AGSIntegratedMeshLayer(url: URL(string: "https://tiles.arcgis.com/tiles/u0sSNqDXr7puKJrF/arcgis/rest/services/Frankfurt2017_v17/SceneServer/layers/0")!) scene.operationalLayers.add(meshLayer) // show scene arView.sceneView.scene = scene // TODO – configure origin camera for AR } private func configureSceneForAR() { // Continued from above // Wait for the layer to load, then set the AR camera meshLayer.load { [weak self, weak meshLayer] (err: Error?) in guard let self = self else { return } guard let `meshLayer` = meshLayer else { return } if let error = err { self.presentAlert(error: error) } else if let envelope = meshLayer.fullExtent { let camera = AGSCamera(latitude: envelope.center.y, longitude: envelope.center.x, altitude: 600, heading: 0, pitch: 90, roll: 0) self.arView.originCamera = camera } } } private func configureAR() { // Continued from above // Set the translation factor to enable rapid movement through the scene arView.translationFactor = 1000 // Disable the navigation constraint scene.baseSurface?.navigationConstraint = .none // Turn the space and atmosphere effects on for an immersive experience arView.sceneView.spaceEffect = .stars arView.sceneView.atmosphereEffect = .realistic } // ---------- World-scale --------------------- // arView.locationDataSource = AGSCLLocationDataSource() private func configureSceneForAR() { // Create scene with imagery basemap let scene = AGSScene(basemapType: .imagery) // Create an elevation source and add it to the scene let elevationSource = AGSArcGISTiledElevationSource(url: URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!) scene.baseSurface?.elevationSources.append(elevationSource) // Allow camera to go beneath the surface scene.baseSurface?.navigationConstraint = .none // Display the scene arView.sceneView.scene = scene // Configure atmosphere and space effect arView.sceneView.spaceEffect = .transparent arView.sceneView.atmosphereEffect = .none } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // Continuous update mode arView.startTracking(.continuous, completion: nil) // One-time update mode // arView.startTracking(.initial, completion: nil) } override func viewDidLoad() { //… arView.arSCNViewDelegate = self /… } extension ARExample: ARSCNViewDelegate { let planeRenderingMaterial = SCNMaterial() public init() { planeRenderingMaterial.diffuse.contentColor = UIColor(1, 0.5f) } public func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { if shouldHidePlanes { return } guard let _ = anchor as? ARPlaneAnchor else { return } // Create a custom object to visualize the plane geometry and extent. if #available(iOS 11.3, *) { // Place content only for anchors found by plane detection. guard let planeAnchor = anchor as? ARPlaneAnchor else { return } let arGeometry = planeAnchor.geometry let arPlaneSceneGeometry = ARSCNPlaneGeometry(device: renderer.device!) arPlaneSceneGeometry?.update(from: arGeometry) let newNode = SCNNode(geometry: arPlaneSceneGeometry) node.addChildNode(newNode) arPlaneSceneGeometry?.materials = [_planeRenderingMaterial] node.geometry = arPlaneSceneGeometry } } public func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { if shouldHidePlanes { // Remove plane visualization node.removeFromParentNode() return } // Create a custom object to visualize the plane geometry and extent. if #available(iOS 11.3, *) { // Place content only for anchors found by plane detection. guard let planeAnchor = anchor as? ARPlaneAnchor else { return } let arGeometry = planeAnchor.geometry let arPlaneSceneGeometry = ARSCNPlaneGeometry(device: renderer.device?) arPlaneSceneGeometry?.update(from: arGeometry) node.childNodes[0].geometry = arPlaneSceneGeometry if let material = node.geometry?.materials { arPlaneSceneGeometry?.materials = material } } } }Set the translation factor on the ArcGIS AR view so that the whole scene can be viewed by moving around it. A useful formula for determining this value is translation factor = virtual content width / desired physical content width. The desired physical content width is the size of the physical table while virtual content width is the real-world size of the scene content; both measurements should be in meters. You can set the virtual content width by setting a clipping distance.
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 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
// ---------- General --------------------- // import UIKit import ARKit import ArcGISToolkit import ArcGIS class BasicARExample: UIViewController { private let arView = ArcGISARView() override func viewDidLoad() { super.viewDidLoad() arView.translatesAutoresizingMaskIntoConstraints = false