Skip To Content ArcGIS for Developers Sign In Dashboard

ArcGIS Runtime SDK for Qt

Display scenes in tabletop AR

Sample Viewer View Sample on GitHub

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

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 ArcGISArView 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 the user to tap the view, then use ArcGISArView.setInitialTransformation(mouse.x, mouse.y) to set the initial transformation, which allows you to place the scene. This method uses ARKit's built-in plane detection.
  3. To enable looking at the scene from below, set scene.baseSurface.navigationConstraint to Enums.NavigationConstraintNone.
  4. Set the clipping distance property of the AR view. This will clip the scene to the area you want to show.
  5. 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.
  6. Set ArcGISArView.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

  • ArcGISArView
  • Surface

Offline data

Read more about how to set up the sample's offline data here.

Link Local Location
Philadelphia MSPK <userhome>/ArcGIS/Runtime/Data/mspk/philadelphia.mspk

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

  1. Clone the ArcGIS Runtime Toolkit repository.
  2. In the project's .pro file, make sure to set the path, AR_TOOLKIT_SOURCE_PATH, to the ArcGIS Toolkit source folder you just cloned.
  3. The sample contains the necessary changes to deploy to iOS or Android as is.

Note: Filesystem permissions is required for this sample.

For more information on the Augmented Reality (AR) toolkit see the AR README on GitHub.

Tags

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

Sample Code

import QtQuick 2.6
import QtQuick.Controls 2.12
import Esri.ArcGISRuntime 100.9
import Esri.ArcGISExtras 1.1
import Esri.ArcGISArToolkit 1.0
import Esri.Samples 1.0

Rectangle {
    id: rootRectangle
    clip: true
    width: 800
    height: 600

    readonly property url dataPath: System.userHomePath + "/ArcGIS/Runtime/Data/mspk"
    readonly property double sceneWidth: 800.0
    readonly property double tableTopWidth: 1.0
    property var philadelphiaScene: null

    PermissionsHelper {
        id: permissionsHelper
        onRequestFilesystemAccessCompleted: mspk.load();
    }

    ArcGISArView {
        id: arcGISArView
        anchors.fill: parent
        tracking: true
        sceneView: sceneView
    }

    SceneView {
        id: sceneView
        anchors.fill: parent

        onMouseClicked: {
            if (mspk.loadStatus !== Enums.LoadStatusLoaded || !arcGISArView.tracking || !philadelphiaScene)
                return;

            // Display the scene
            if (sceneView.scene !== philadelphiaScene) {
                sceneView.scene = philadelphiaScene;
                sceneView.scene.sceneViewTilingScheme = Enums.SceneViewTilingSchemeGeographic;
            }

            // Set the clipping distance for the scene.
            arcGISArView.clippingDistance = 400;

            // Set the surface opacity to 0.
            arcGISArView.sceneView.scene.baseSurface.opacity = 0.0;

            // Enable subsurface navigation. This allows you to look at the scene from below.
            arcGISArView.sceneView.scene.baseSurface.navigationConstraint = Enums.NavigationConstraintNone;

            // Set the initial transformation using the point clicked on the screen
            arcGISArView.setInitialTransformation(mouse.x, mouse.y);

            // Set the origin camera.
            arcGISArView.originCamera = philadelphiaCamera;

            // Set the translation factor based on the scene content width and desired physical size.
            arcGISArView.translationFactor = sceneWidth/tableTopWidth;
        }

        Rectangle {
            anchors {
                bottom: sceneView.attributionTop
                horizontalCenter: parent.horizontalCenter
                margins: 5
            }
            width: childrenRect.width
            height: childrenRect.height
            color: "#88ffffff" // transparent white
            radius: 5
            visible: sceneView.scene !== philadelphiaScene ? true : false

            Text {
                anchors.centerIn: parent
                padding: 2
                font.bold: true
                text: qsTr("Touch screen to place the tabletop scene...")
            }
        }
    }

    Component.onCompleted: {
        if (!permissionsHelper.fileSystemAccessGranted)
            permissionsHelper.requestFilesystemAccess();
        else
            mspk.load();
    }

    MobileScenePackage {
        id: mspk
        path: dataPath + "/philadelphia.mspk"

        onLoadStatusChanged: {
            if (loadStatus !== Enums.LoadStatusLoaded)
                return;

            if (mspk.scenes.length < 1)
                return;

            // Obtain the first scene in the list of scenes
            philadelphiaScene = mspk.scenes[0];
        }

        onErrorChanged: {
            console.log("Mobile Scene Package Error: %1 %2".arg(error.message).arg(error.additionalMessage));
        }
    }

    Camera {
        id: philadelphiaCamera
        location: Point {
            //latitude
            y: 39.95787000283599
            //longitude
            x: -75.16996728256345
            //altitide
            z: 8.813445091247559
        }
        heading: 0
        pitch: 90
        roll:0
    }
}
// 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.

#include "PermissionsHelper.h"

#include <QtGlobal>

#ifdef Q_OS_ANDROID
#include <QtAndroid>
#include <algorithm>
namespace {
  constexpr const char* s_android_fs_read_permission = "android.permission.READ_EXTERNAL_STORAGE";
  constexpr const char* s_android_fs_write_permission = "android.permission.WRITE_EXTERNAL_STORAGE";
} // anonymous namespace
#endif

PermissionsHelper::PermissionsHelper(QObject* parent) :
  QObject(parent)
{
}

PermissionsHelper::~PermissionsHelper() = default;

bool PermissionsHelper::fileSystemAccessGranted() const
{
#ifdef Q_OS_ANDROID
  return QtAndroid::checkPermission(s_android_fs_read_permission) == QtAndroid::PermissionResult::Granted &&
         QtAndroid::checkPermission(s_android_fs_write_permission) == QtAndroid::PermissionResult::Granted;
#else
  return true;
#endif
}

void PermissionsHelper::requestFilesystemAccess()
{
#ifdef Q_OS_ANDROID
  if (!fileSystemAccessGranted())
  {
    const QtAndroid::PermissionResultCallback callback = [this](const QtAndroid::PermissionResultMap& resultsMap)
    {
      const bool anyDenied = std::any_of(resultsMap.cbegin(), resultsMap.cend(),
      [](QtAndroid::PermissionResult result)
      {
        return result == QtAndroid::PermissionResult::Denied;
      });

      if (!anyDenied)
        emit fileSystemAccessGrantedChanged();

      emit requestFilesystemAccessCompleted();
    };

    QtAndroid::requestPermissions({s_android_fs_read_permission, s_android_fs_write_permission}, callback);
  }
  else
  {
    emit requestFilesystemAccessCompleted();
  }
#else
  emit requestFilesystemAccessCompleted();
#endif
}
// 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.

#ifndef PERMISSIONSHELPER_H
#define PERMISSIONSHELPER_H

#include <QObject>

class PermissionsHelper : public QObject
{  
  Q_OBJECT

  Q_PROPERTY(bool fileSystemAccessGranted READ fileSystemAccessGranted NOTIFY fileSystemAccessGrantedChanged)

public:
  explicit PermissionsHelper(QObject* parent = nullptr);
  ~PermissionsHelper();

  Q_INVOKABLE void requestFilesystemAccess();

  bool fileSystemAccessGranted() const;

signals:
  void fileSystemAccessGrantedChanged();
  void requestFilesystemAccessCompleted(); // notifies even if request denied
};

#endif // PERMISSIONSHELPER_H