Display scenes in tabletop AR

View inAndroidiOSView on GitHub

Use augmented reality (AR) to pin a scene to a table or desk for easy exploration.

Scene content shown sitting on a surface, as if it were a 3D printed model

Use case

Tabletop scenes allow you to use your device to interact with scenes as if they are 3D-printed model models sitting on your desk. You could use this to virtually explore a proposed development without needing to create a physical model.

How to use the sample

You'll see a feed from the camera when you open the sample. Tap on any flat, horizontal surface (like a desk or table) to place the scene. With the scene placed, you can move the camera around the scene to explore. You can also pan and zoom with touch to adjust the position of the scene.

How it works

  1. Create an ARSceneView and add it to the view.
    • Note: this sample uses content in the WGS 84 geographic tiling scheme, rather than the web mercator tiling scheme. Once a scene has been displayed, the scene view cannot display another scene with a non-matching tiling scheme. To avoid that, the sample starts by showing a blank scene with an invisible base surface. Touch events will not be raised for the scene view unless a scene is displayed.
  2. Wait for plane detection using arView.PlanesDetectedChanged before allowing the user to tap to place the scene.
  3. Wait for the user to tap the view, then use arView.SetInitialTransforamtion(tappedPoint) to set the initial transformation, which allows you to place the scene. This method uses ARKit's built-in plane detection.
  4. If the call to SetInitialTransformation returns true, proceed to load the scene and display it.
  5. To enable looking at the scene from below, set scene.BaseSurface.NavigationConstraint to 0.
  6. Set the origin camera to the point in the scene where it should be anchored to the real-world surface you tapped. Typically that is the point at the center of the scene, with the altitude of the lowest point in the scene.
  7. Set arView.TranslationFactor such that the user can view the entire scene by moving the device around it. The translation factor defines how far the virtual camera moves when the physical camera moves.
    • A good formula for determining translation factor to use in a tabletop map experience is translationFactor = sceneWidth / tableTopWidth. The scene width is the width/length of the scene content you wish to display in meters. The tabletop width is the length of the area on the physical surface that you want the scene content to fill. For simplicity, the sample assumes a scene width of 800 meters and physical size of 1 meter.

Relevant API

  • ARSceneView
  • Surface

Offline data

This sample uses offline data, available as an item on ArcGIS Online.

About the data

This sample uses the Philadelphia Mobile Scene Package. It was chosen because it is a compact scene ideal for tabletop use. Note that tabletop mapping experiences work best with small, focused scenes. The small, focused area with basemap tiles defines a clear boundary for the scene.

Additional information

Tabletop AR is one of three main patterns for working with geographic information in augmented reality. See Display scenes in augmented reality in the guide for more information.

This sample uses the ArcGIS Runtime Toolkit. See Display scenes in augmented reality in the guide to learn about the toolkit and how to add it to your app.

Tags

augmented reality, drop, mixed reality, model, pin, place, table-top, tabletop

Sample Code

DisplayScenesInTabletopAR.cs
Use dark colors for code blocksCopy
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
// Copyright 2020 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: http://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.

using ArcGISRuntime;
using ARKit;
using Esri.ArcGISRuntime.ARToolkit;
using Esri.ArcGISRuntime.Mapping;
using Foundation;
using System;
using UIKit;

namespace ArcGISRuntimeXamarin.Samples.DisplayScenesInTabletopAR
{
    [Register("DisplayScenesInTabletopAR")]
    [ArcGISRuntime.Samples.Shared.Attributes.Sample(
        name: "Display scenes in tabletop AR",
        category: "Augmented reality",
        description: "Use augmented reality (AR) to pin a scene to a table or desk for easy exploration.",
        instructions: "You'll see a feed from the camera when you open the sample. Tap on any flat, horizontal surface (like a desk or table) to place the scene. With the scene placed, you can move the camera around the scene to explore. You can also pan and zoom with touch to adjust the position of the scene.",
        tags: new[] { "augmented reality", "drop", "mixed reality", "model", "pin", "place", "table-top", "tabletop" })]
    public class DisplayScenesInTabletopAR : UIViewController
    {
        // Hold references to UI controls.
        private ARSceneView _arSceneView;
        private UILabel _arKitStatusLabel;
        private UILabel _helpLabel;

        // Scene to be displayed on the tabletop.
        private Scene _tabletopScene;

        public DisplayScenesInTabletopAR()
        {
            Title = "Display scenes in tabletop AR";
        }

        public override void LoadView()
        {
            // Create the views.
            View = new UIView() { BackgroundColor = ApplicationTheme.BackgroundColor };

            _arSceneView = new ARSceneView();
            _arSceneView.TranslatesAutoresizingMaskIntoConstraints = false;

            _arKitStatusLabel = new UILabel();
            _arKitStatusLabel.TranslatesAutoresizingMaskIntoConstraints = false;
            _arKitStatusLabel.TextAlignment = UITextAlignment.Center;
            _arKitStatusLabel.TextColor = UIColor.Black;
            _arKitStatusLabel.BackgroundColor = UIColor.FromWhiteAlpha(1.0f, 0.6f);
            _arKitStatusLabel.Text = "Setting up ARKit";

            _helpLabel = new UILabel();
            _helpLabel.TranslatesAutoresizingMaskIntoConstraints = false;
            _helpLabel.TextAlignment = UITextAlignment.Center;
            _helpLabel.TextColor = UIColor.White;
            _helpLabel.BackgroundColor = UIColor.FromWhiteAlpha(0f, 0.6f);
            _helpLabel.Text = "Tap to place scene";
            _helpLabel.Hidden = true;

            // Add the views.
            View.AddSubviews(_arSceneView, _arKitStatusLabel, _helpLabel);

            // Lay out the views.
            NSLayoutConstraint.ActivateConstraints(new[]
            {
                _arSceneView.TopAnchor.ConstraintEqualTo(View.TopAnchor),
                _arSceneView.BottomAnchor.ConstraintEqualTo(View.BottomAnchor),
                _arSceneView.LeadingAnchor.ConstraintEqualTo(View.LeadingAnchor),
                _arSceneView.TrailingAnchor.ConstraintEqualTo(View.TrailingAnchor),
                _arKitStatusLabel.TopAnchor.ConstraintEqualTo(View.SafeAreaLayoutGuide.TopAnchor),
                _arKitStatusLabel.LeadingAnchor.ConstraintEqualTo(View.LeadingAnchor),
                _arKitStatusLabel.TrailingAnchor.ConstraintEqualTo(View.TrailingAnchor),
                _arKitStatusLabel.HeightAnchor.ConstraintEqualTo(40),
                _helpLabel.BottomAnchor.ConstraintEqualTo(_arSceneView.SafeAreaLayoutGuide.BottomAnchor),
                _helpLabel.LeadingAnchor.ConstraintEqualTo(View.LeadingAnchor),
                _helpLabel.TrailingAnchor.ConstraintEqualTo(View.TrailingAnchor),
                _helpLabel.HeightAnchor.ConstraintEqualTo(40)
            });

            // Listen for tracking status changes and provide feedback to the user.
            _arSceneView.ARSCNViewCameraDidChangeTrackingState += ARSceneView_TrackingStateChanged;
        }

        private void Initialize()
        {
            // Display an empty scene to enable tap-to-place.
            _arSceneView.Scene = new Scene(SceneViewTilingScheme.WebMercator);

            // Render the scene invisible to start.
            _arSceneView.Scene.BaseSurface.Opacity = 0;

            // Get notification when planes are detected
            _arSceneView.PlanesDetectedChanged += ARSceneView_PlanesDetectedChanged;

            // Configure the AR scene view to render detected planes.
            _arSceneView.RenderPlanes = true;
        }

        private void ARSceneView_PlanesDetectedChanged(object sender, bool planeDetected)
        {
            if (planeDetected)
            {
                BeginInvokeOnMainThread(EnableTapToPlace);
                _arSceneView.PlanesDetectedChanged -= ARSceneView_PlanesDetectedChanged;
            }
        }

        private void EnableTapToPlace()
        {
            // Show the help label.
            _helpLabel.Hidden = false;
            _helpLabel.Text = "Tap to place the scene.";

            // Wait for the user to tap.
            _arSceneView.GeoViewTapped += _arSceneView_GeoViewTapped;
        }

        private void _arSceneView_GeoViewTapped(object sender, Esri.ArcGISRuntime.UI.Controls.GeoViewInputEventArgs e)
        {
            if (_arSceneView.SetInitialTransformation(e.Position))
            {
                DisplayScene();
                _arKitStatusLabel.Hidden = true;
            }
        }

        private async void DisplayScene()
        {
            // Hide the help label.
            _helpLabel.Hidden = true;

            if (_tabletopScene == null)
            {
                // Load a scene from ArcGIS Online.
                _tabletopScene = new Scene(new Uri("https://www.arcgis.com/home/item.html?id=31874da8a16d45bfbc1273422f772270"));
                await _tabletopScene.LoadAsync();

                // Set the clipping distance for the scene.
                _arSceneView.ClippingDistance = 400;

                // Enable subsurface navigation. This allows you to look at the scene from below.
                _tabletopScene.BaseSurface.NavigationConstraint = NavigationConstraint.None;

                _arSceneView.Scene = _tabletopScene;
            }

            // Create a camera at the bottom and center of the scene.
            //    This camera is the point at which the scene is pinned to the real-world surface.
            Camera originCamera = new Camera(52.52083, 13.40944, 8.813445091247559, 0, 90, 0);

            // Set the origin camera.
            _arSceneView.OriginCamera = originCamera;

            // The width of the scene content is about 800 meters.
            double geographicContentWidth = 800;

            // The desired physical width of the scene is 1 meter.
            double tableContainerWidth = 1;

            // Set the translation factor based on the scene content width and desired physical size.
            _arSceneView.TranslationFactor = geographicContentWidth / tableContainerWidth;
        }

        private void ARSceneView_TrackingStateChanged(object sender, ARSCNViewCameraTrackingStateEventArgs e)
        {
            // Provide clear feedback to the user in terms they will understand.
            switch (e.Camera.TrackingState)
            {
                case ARTrackingState.Normal:
                    _arKitStatusLabel.Hidden = true;
                    break;

                case ARTrackingState.NotAvailable:
                    _arKitStatusLabel.Hidden = false;
                    _arKitStatusLabel.Text = "ARKit location not available";
                    break;

                case ARTrackingState.Limited:
                    _arKitStatusLabel.Hidden = false;
                    switch (e.Camera.TrackingStateReason)
                    {
                        case ARTrackingStateReason.ExcessiveMotion:
                            _arKitStatusLabel.Text = "Try moving your device more slowly.";
                            break;

                        case ARTrackingStateReason.Initializing:
                            _arKitStatusLabel.Text = "Keep moving your device.";
                            break;

                        case ARTrackingStateReason.InsufficientFeatures:
                            _arKitStatusLabel.Text = "Try turning on more lights and moving around.";
                            break;

                        case ARTrackingStateReason.Relocalizing:
                            // This won't happen as this sample doesn't use relocalization.
                            break;
                    }

                    break;
            }
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
            Initialize();
        }

        public override void ViewDidAppear(bool animated)
        {
            base.ViewDidAppear(animated);
            _arSceneView.StartTrackingAsync();
        }

        public override async void ViewDidDisappear(bool animated)
        {
            base.ViewDidDisappear(animated);

            if (_arSceneView != null)
            {
                _arSceneView.PlanesDetectedChanged -= ARSceneView_PlanesDetectedChanged;
                await _arSceneView.StopTrackingAsync();
            }
        }
    }
}

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