Руководство. Пошаговые инструкции по созданию нового приложения HoloLens Unity с помощью пространственных привязок Azure

В этом учебнике описывается, как создать приложение HoloLens Unity с помощью Пространственных привязок Azure.

Необходимые компоненты

В рамках этого руководства вам потребуются:

  1. Компьютер — компьютер под управлением Windows
  2. Visual Studio Visual Studio - 2019, установленная с рабочей нагрузкой разработки универсальная платформа Windows и компонентом пакета SDK для Windows 10 (10.0.18362.0 или более поздней версии). Установленное расширение Visual Studio C++/WinRT из Visual Studio Marketplace.
  3. HoloLens — устройство HoloLens с включенным режимом разработчика. Для выполнения инструкций из этой статьи требуется устройство HoloLens с обновлением Windows 10 за май 2020 г. Чтобы обновить HoloLens до последней версии, откройте приложение Параметры, перейдите в раздел Обновление и безопасность, а затем нажмите кнопку Проверить обновления.
  4. Unity Unity - 2020.3.25 с модулями, универсальная платформа Windows поддержка сборки и поддержка сборки Windows (IL2CPP)

Создание и настройка проекта Unity

Create New Project (Функции Azure: Создать проект)

  1. В Центре Unity выберите новый проект
  2. Выберите 3D
  3. Введите имя проекта и введите расположение сохранения
  4. Выберите " Создать проект" и дождитесь создания проекта Unity

Изменение платформы сборки

  1. В редакторе unity выберите "Сборка файлов>" Параметры
  2. Выберите универсальная платформа Windows затем переключите платформу. Подождите, пока Unity не завершит обработку всех файлов.

Импорт ASA и OpenXR

  1. Запуск средства компонента Смешанная реальность
  2. Выберите путь к проекту — папка, содержащая такие папки, как активы, пакеты, проект Параметры и т. д., и выберите пункт "Обнаружение функций"
  3. В разделе "Службы azure Смешанная реальность" выберите оба
    1. Ядро пакета SDK для пространственных привязок Azure
    2. Пакет SDK для пространственных привязок Azure для Windows
  4. В разделе "Поддержка платформы" выберите
    1. Подключаемый модуль OpenXR Смешанная реальность

Примечание.

Убедитесь, что вы обновили каталог и выбрана последняя версия для каждого из них.

MRFT - Feature Selection

  1. Нажмите клавиши Get Features -->Import -->Утвердить -->Exit
  2. При перефокусировании окна Unity Unity начнет импорт модулей.
  3. Если вы получите сообщение об использовании новой входной системы, выберите "Да ", чтобы перезапустить Unity и включить серверные части.

Настройка параметров проекта

Теперь мы установим некоторые параметры проекта Unity, которые помогут нам выбрать пакет SDK Windows Holographic для разработки.

Изменение Параметры OpenXR

  1. Выберите Параметры сборки файлов>(он по-прежнему открыт на предыдущем шаге)
  2. Выберите проигрыватель Параметры...
  3. Выбор подключаемого модуля XR
  4. Убедитесь, что выбрана вкладка универсальная платформа Windows Параметры и проверка поле рядом с OpenXR и рядом с группой функций Microsoft HoloLens
  5. Щелкните желтый знак предупреждения рядом с OpenXR, чтобы отобразить все проблемы OpenXR .
  6. Выберите "Исправить все"
  7. Чтобы устранить проблему "Необходимо добавить хотя бы один профиль взаимодействия", выберите "Изменить ", чтобы открыть параметры проекта OpenXR. Затем в разделе "Профили взаимодействия" выберите символ и выберите профиль взаимодействия с рукой + МайкрософтUnity - OpenXR Setup

Изменение Параметры качества

  1. Выберите Изменить>Параметры проекта>Качество
  2. В столбце под логотипом универсальная платформа Windows щелкните стрелку в строке по умолчанию и выберите "Очень низкий". Вы поймете, что этот параметр применяется правильно, когда поле в столбце Универсальная платформа Windows и строка Очень низкое станут зеленого цвета.

Установка возможностей

  1. Перейдите к разделу "Изменить>проект Параметры> Player" (возможно, вы все еще откроете его с предыдущего шага).
  2. Убедитесь, что выбрана вкладка универсальная платформа Windows Параметры
  3. В разделе "Публикация Параметры конфигурация" включите следующее.
    1. InternetClient
    2. InternetClientServer;
    3. PrivateNetworkClientServer;
    4. SpatialPerception (возможно, уже включен)

Настройка основной камеры

  1. На панели Иерархия выберите объект Main Camera.
  2. На панели Инспектор задайте для положения преобразования значение 0,0,0.
  3. Найдите свойство Clear Flags (Очистить флаги) и измените раскрывающийся список из Skybox на Solid Color (Сплошной цвет).
  4. Выберите поле фона, чтобы открыть средство выбора цветов.
  5. Задайте для R, G, B, and A (Красный, зеленый, синий и альфа-компонент) значение 0.
  6. Выберите " Добавить компонент " в нижней части экрана и добавьте компонент драйвера отслеживаемой позы в камеру Unity - Camera Setup

Попробуйте использовать #1

Теперь у вас должна быть пустая сцена, которая готова к развертыванию на устройстве HoloLens. Чтобы проверить, что все работает, создайте приложение в Unity и разверните его из Visual Studio. Выполните действия с помощью Visual Studio для развертывания и отладки. Вы должны увидеть начальный экран Unity, а затем чистый экран.

Создание ресурса Пространственных привязок

Переход на портал Azure.

В области слева выберите Создать ресурс.

Выполните поиск по запросу Пространственные привязки с помощью поля поиска.

Screenshot showing the results of a search for Spatial Anchors.

Выберите Пространственные привязки, а затем щелкните Создать.

В области Учетная запись Пространственных привязок выполните следующие действия.

  • Введите уникальное имя ресурса, используя обычные буквенно-цифровые символы.

  • Выберите подписку, к которой нужно присоединить ресурс.

  • Создайте группу ресурсов, нажав Создать. Назовите ее myResourceGroup и нажмите ОК.

    Группа ресурсов — это логический контейнер, в котором происходит развертывание ресурсов Azure (например, веб-приложений, баз данных и учетных записей хранения) и управление ими. Например, в дальнейшем можно удалить всю группу ресурсов при помощи одного простого действия.

  • Выберите расположение (регион), в котором будет размещен ресурс.

  • Выберите Создать, чтобы начать создание ресурса.

Screenshot of the Spatial Anchors pane for creating a resource.

После создания ресурса на портале Azure отобразится оповещение о завершении развертывания.

Screenshot showing that the resource deployment is complete.

Выберите Перейти к ресурсу. Теперь можно просмотреть свойства ресурса.

Скопируйте значение идентификатора учетной записи ресурса в текстовый редактор для дальнейшего использования.

Screenshot of the resource properties pane.

Скопируйте также значение домена учетной записи ресурса в текстовый редактор для дальнейшего использования.

Screenshot showing the resource's account domain value.

В разделе Параметры выберите элемент Ключ доступа. Скопируйте значение первичного ключа (ключа учетной записи) в текстовый редактор для дальнейшего использования.

Screenshot of the Keys pane for the account.

Создание и добавление скриптов

  1. В Unity на панели "Проект" создайте в папке "Активы" новую папку с именем "Скрипты".
  2. В папке щелкните правой кнопкой мыши ->Create ->C# Script. Заголовок azureSpatialAnchorsScript
  3. Перейдите в GameObject ->Create Empty.
  4. Выберите его и в инспекторе переименуйте его из GameObject в AzureSpatialAnchors.
  5. По-прежнему на GameObject
    1. Задайте для его положения значение 0,0,0
    2. Выберите " Добавить компонент " и найдите и добавьте AzureSpatialAnchorsScript
    3. Нажмите кнопку "Добавить компонент" еще раз и найдите и добавьте диспетчер привязки AR. Это также приведет к добавлению источника сеанса AR.
    4. Нажмите кнопку "Добавить компонент" еще раз и найдите и добавьте скрипт SpatialAnchorManager
    5. В добавленном компоненте SpatialAnchorManager заполните идентификатор учетной записи, ключ учетной записи и домен учетной записи, скопированный на предыдущем шаге из ресурса пространственных привязок в портал Azure.

Unity - ASA GameObject

Обзор приложения

Наше приложение будет поддерживать следующие взаимодействия:

Жест Действие
Коснитесь любого места Запуск и продолжение сеанса и создание привязки в позиции руки
Касание привязки Удаление GameObject и удаление привязки в облачной службе ASA
Коснитесь и удерживайте в течение 2 секунд (+ сеанс выполняется) Остановите сеанс и удалите все GameObjects. Сохранение привязок в облачной службе ASA
Коснитесь и удерживайте в течение 2 секунд (+ сеанс не выполняется) Запустите сеанс и найдите все привязки.

Добавление распознавания касаний

Давайте добавим некоторый код в наш скрипт, чтобы узнать жест касания пользователя.

  1. Откройте AzureSpatialAnchorsScript.cs в Visual Studio, дважды щелкнув скрипт в области проекта Unity.
  2. Добавьте следующий массив в класс
public class AzureSpatialAnchorsScript : MonoBehaviour
{
    /// <summary>
    /// Used to distinguish short taps and long taps
    /// </summary>
    private float[] _tappingTimer = { 0, 0 };
  1. Добавьте следующие два метода под методом Update(). Мы добавим реализацию на более позднем этапе
// Update is called once per frame
void Update()
{
}

/// <summary>
/// Called when a user is air tapping for a short time 
/// </summary>
/// <param name="handPosition">Location where tap was registered</param>
private async void ShortTap(Vector3 handPosition)
{
}

/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
}
  1. Добавьте следующий импорт
using UnityEngine.XR;
  1. Добавьте следующий код в Update() начало метода. Это позволит приложению распознавать короткие и длинные (2 с) жесты касания рукой
// Update is called once per frame
void Update()
{

    //Check for any air taps from either hand
    for (int i = 0; i < 2; i++)
    {
        InputDevice device = InputDevices.GetDeviceAtXRNode((i == 0) ? XRNode.RightHand : XRNode.LeftHand);
        if (device.TryGetFeatureValue(CommonUsages.primaryButton, out bool isTapping))
        {
            if (!isTapping)
            {
                //Stopped Tapping or wasn't tapping
                if (0f < _tappingTimer[i] && _tappingTimer[i] < 1f)
                {
                    //User has been tapping for less than 1 sec. Get hand position and call ShortTap
                    if (device.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 handPosition))
                    {
                        ShortTap(handPosition);
                    }
                }
                _tappingTimer[i] = 0;
            }
            else
            {
                _tappingTimer[i] += Time.deltaTime;
                if (_tappingTimer[i] >= 2f)
                {
                    //User has been air tapping for at least 2sec. Get hand position and call LongTap
                    if (device.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 handPosition))
                    {
                        LongTap();
                    }
                    _tappingTimer[i] = -float.MaxValue; // reset the timer, to avoid retriggering if user is still holding tap
                }
            }
        }

    }
}

Добавление и настройка SpatialAnchorManager

Пакет SDK ASA предлагает простой интерфейс, который вызывается SpatialAnchorManager для вызова службы ASA. Давайте добавим его в качестве переменной в нашу AzureSpatialAnchorsScript.cs

Сначала добавьте импорт

using Microsoft.Azure.SpatialAnchors.Unity;

Затем объявите переменную

public class AzureSpatialAnchorsScript : MonoBehaviour
{
    /// <summary>
    /// Used to distinguish short taps and long taps
    /// </summary>
    private float[] _tappingTimer = { 0, 0 };

    /// <summary>
    /// Main interface to anything Spatial Anchors related
    /// </summary>
    private SpatialAnchorManager _spatialAnchorManager = null;

В методе Start() назначьте переменную компоненту, который мы добавили на предыдущем шаге.

// Start is called before the first frame update
void Start()
{
    _spatialAnchorManager = GetComponent<SpatialAnchorManager>();
}

Чтобы получать журналы отладки и ошибок, необходимо подписаться на различные обратные вызовы.

// Start is called before the first frame update
void Start()
{
    _spatialAnchorManager = GetComponent<SpatialAnchorManager>();
    _spatialAnchorManager.LogDebug += (sender, args) => Debug.Log($"ASA - Debug: {args.Message}");
    _spatialAnchorManager.Error += (sender, args) => Debug.LogError($"ASA - Error: {args.ErrorMessage}");
}

Примечание.

Чтобы просмотреть журналы, убедитесь, что после создания проекта из Unity и откройте решение .slnVisual Studio, выберите "Отладка"> и "Выполнить с отладкой" и оставить HoloLens подключенным к компьютеру во время работы приложения.

Начать сеанс

Чтобы создать и найти привязки, сначала необходимо запустить сеанс. При вызове StartSessionAsync()SpatialAnchorManager создадит сеанс при необходимости, а затем запустите его. Давайте добавим это в наш ShortTap() метод.

/// <summary>
/// Called when a user is air tapping for a short time 
/// </summary>
/// <param name="handPosition">Location where tap was registered</param>
private async void ShortTap(Vector3 handPosition)
{
    await _spatialAnchorManager.StartSessionAsync();
}

Создать привязку

Теперь, когда у нас есть сеанс, который выполняется, мы можем создать привязки. В этом приложении мы хотим отслеживать созданные GameObjects привязки и созданные идентификаторы привязки (идентификаторы привязки). Давайте добавим два списка в наш код.

using Microsoft.Azure.SpatialAnchors.Unity;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
    /// <summary>
    /// Main interface to anything Spatial Anchors related
    /// </summary>
    private SpatialAnchorManager _spatialAnchorManager = null;

    /// <summary>
    /// Used to keep track of all GameObjects that represent a found or created anchor
    /// </summary>
    private List<GameObject> _foundOrCreatedAnchorGameObjects = new List<GameObject>();

    /// <summary>
    /// Used to keep track of all the created Anchor IDs
    /// </summary>
    private List<String> _createdAnchorIDs = new List<String>();

Давайте создадим методCreateAnchor, который создает привязку в позиции, определенной его параметром.

using System.Threading.Tasks;
/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
    //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
}

Так как пространственные привязки не только имеют позицию, но и поворот, давайте зададим поворот всегда ориентируться на HoloLens при создании.

/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
    //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
    if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
    {
        headPosition = Vector3.zero;
    }

    Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);

}

Теперь, когда у нас есть позиция и поворот требуемой привязки, давайте создадим видимый GameObject. Обратите внимание, что пространственные привязки не требуют отображения привязки GameObject для конечного пользователя, так как основная цель пространственных привязок заключается в предоставлении общего и постоянного эталонного кадра. В этом руководстве мы визуализируем привязки в виде кубов. Каждая привязка будет инициализирована как белый куб, который преобразуется в зеленыйкуб после успешного создания.

/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
    //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
    if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
    {
        headPosition = Vector3.zero;
    }

    Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);

    GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
    anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
    anchorGameObject.transform.position = position;
    anchorGameObject.transform.rotation = orientationTowardsHead;
    anchorGameObject.transform.localScale = Vector3.one * 0.1f;

}

Примечание.

Мы используем устаревший шейдер, так как он включен в сборку Unity по умолчанию. Другие шейдеры, такие как шейдер по умолчанию, включаются только в том случае, если они указаны вручную или они являются непосредственно частью сцены. Если шейдер не включен и приложение пытается отрисовать его, это приведет к розовому материалу.

Теперь давайте добавим и настроим компоненты пространственных привязок. Мы устанавливаем срок действия привязки на 3 дня после создания привязки. После этого они будут автоматически удалены из облака. Не забудьте добавить импорт

using Microsoft.Azure.SpatialAnchors;
/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
    //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
    if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
    {
        headPosition = Vector3.zero;
    }

    Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);

    GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
    anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
    anchorGameObject.transform.position = position;
    anchorGameObject.transform.rotation = orientationTowardsHead;
    anchorGameObject.transform.localScale = Vector3.one * 0.1f;

    //Add and configure ASA components
    CloudNativeAnchor cloudNativeAnchor = anchorGameObject.AddComponent<CloudNativeAnchor>();
    await cloudNativeAnchor.NativeToCloud();
    CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
    cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(3);

}

Чтобы сохранить привязку, пользователь должен собирать данные среды.

/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
    //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
    if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
    {
        headPosition = Vector3.zero;
    }

    Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);

    GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
    anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
    anchorGameObject.transform.position = position;
    anchorGameObject.transform.rotation = orientationTowardsHead;
    anchorGameObject.transform.localScale = Vector3.one * 0.1f;

    //Add and configure ASA components
    CloudNativeAnchor cloudNativeAnchor = anchorGameObject.AddComponent<CloudNativeAnchor>();
    await cloudNativeAnchor.NativeToCloud();
    CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
    cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(3);

    //Collect Environment Data
    while (!_spatialAnchorManager.IsReadyForCreate)
    {
        float createProgress = _spatialAnchorManager.SessionStatus.RecommendedForCreateProgress;
        Debug.Log($"ASA - Move your device to capture more environment data: {createProgress:0%}");
    }

}

Примечание.

HoloLens может повторно использовать уже захваченные данные среды, окружающие привязку, что приводит IsReadyForCreate к действительности уже при первом вызове.

Теперь, когда подготовлена облачная пространственная привязка, мы можем попытаться сохранить здесь.

/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
    //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
    if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
    {
        headPosition = Vector3.zero;
    }

    Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);

    GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
    anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
    anchorGameObject.transform.position = position;
    anchorGameObject.transform.rotation = orientationTowardsHead;
    anchorGameObject.transform.localScale = Vector3.one * 0.1f;

    //Add and configure ASA components
    CloudNativeAnchor cloudNativeAnchor = anchorGameObject.AddComponent<CloudNativeAnchor>();
    await cloudNativeAnchor.NativeToCloud();
    CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
    cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(3);

    //Collect Environment Data
    while (!_spatialAnchorManager.IsReadyForCreate)
    {
        float createProgress = _spatialAnchorManager.SessionStatus.RecommendedForCreateProgress;
        Debug.Log($"ASA - Move your device to capture more environment data: {createProgress:0%}");
    }

    Debug.Log($"ASA - Saving cloud anchor... ");

    try
    {
        // Now that the cloud spatial anchor has been prepared, we can try the actual save here.
        await _spatialAnchorManager.CreateAnchorAsync(cloudSpatialAnchor);

        bool saveSucceeded = cloudSpatialAnchor != null;
        if (!saveSucceeded)
        {
            Debug.LogError("ASA - Failed to save, but no exception was thrown.");
            return;
        }

        Debug.Log($"ASA - Saved cloud anchor with ID: {cloudSpatialAnchor.Identifier}");
        _foundOrCreatedAnchorGameObjects.Add(anchorGameObject);
        _createdAnchorIDs.Add(cloudSpatialAnchor.Identifier);
        anchorGameObject.GetComponent<MeshRenderer>().material.color = Color.green;
    }
    catch (Exception exception)
    {
        Debug.Log("ASA - Failed to save anchor: " + exception.ToString());
        Debug.LogException(exception);
    }
}

Наконец, добавим вызов функции в наш ShortTap метод

/// <summary>
/// Called when a user is air tapping for a short time 
/// </summary>
/// <param name="handPosition">Location where tap was registered</param>
private async void ShortTap(Vector3 handPosition)
{
    await _spatialAnchorManager.StartSessionAsync();
        await CreateAnchor(handPosition);
}

Теперь приложение может создавать несколько привязок. Теперь любое устройство может найти созданные привязки (если срок действия еще не истек) до тех пор, пока они знают идентификаторы привязки и имеют доступ к тому же ресурсу пространственных привязок в Azure.

Остановка сеанса и уничтожение gameObjects

Чтобы эмулировать второе устройство, найдя все привязки, мы остановим сеанс и удалите все привязки GameObjects (мы будем хранить идентификаторы привязки). После этого мы начнем новый сеанс и запрашиваем привязки с помощью хранимых идентификаторов привязки.

SpatialAnchorManager может заботиться о остановке сеанса, просто вызывая его DestroySession() метод. Давайте добавим это в наш LongTap() метод

/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
        _spatialAnchorManager.DestroySession();
}

Давайте создадим метод для удаления всех привязок GameObjects

/// <summary>
/// Destroys all Anchor GameObjects
/// </summary>
private void RemoveAllAnchorGameObjects()
{
    foreach (var anchorGameObject in _foundOrCreatedAnchorGameObjects)
    {
        Destroy(anchorGameObject);
    }
    _foundOrCreatedAnchorGameObjects.Clear();
}

И вызовите его после уничтожения сеанса в LongTap()

/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
        // Stop Session and remove all GameObjects. This does not delete the Anchors in the cloud
        _spatialAnchorManager.DestroySession();
        RemoveAllAnchorGameObjects();
        Debug.Log("ASA - Stopped Session and removed all Anchor Objects");
}

Поиск привязки

Теперь мы попытаемся найти привязки снова с правильным положением и поворотом, в который мы создали их. Для этого необходимо запустить сеанс и создать объект Watcher , который будет искать привязки, соответствующие заданным критериям. В качестве критерия мы будем передавать идентификаторы созданных ранее привязок. Давайте создадим метод LocateAnchor() и используем SpatialAnchorManager для создания Watcher. Поиск стратегий, отличных от использования идентификаторов привязки, см . в стратегии поиска привязки

/// <summary>
/// Looking for anchors with ID in _createdAnchorIDs
/// </summary>
private void LocateAnchor()
{
    if (_createdAnchorIDs.Count > 0)
    {
        //Create watcher to look for all stored anchor IDs
        Debug.Log($"ASA - Creating watcher to look for {_createdAnchorIDs.Count} spatial anchors");
        AnchorLocateCriteria anchorLocateCriteria = new AnchorLocateCriteria();
        anchorLocateCriteria.Identifiers = _createdAnchorIDs.ToArray();
        _spatialAnchorManager.Session.CreateWatcher(anchorLocateCriteria);
        Debug.Log($"ASA - Watcher created!");
    }
}

После запуска наблюдателя он запустит обратный вызов, когда он нашел привязку, которая соответствует заданным критериям. Сначала создадим метод SpatialAnchorManager_AnchorLocated() , расположенный на привязке, который мы настроим для вызова, когда наблюдатель настроил привязку. Этот метод создаст визуальный элемент GameObject и прикрепит к нему собственный компонент привязки. Компонент собственной привязки гарантирует правильную позицию и поворот заданного GameObject объекта.

Как и в процессе создания, привязка присоединяется к GameObject. Этот gameObject не должен отображаться в сцене для работы пространственных привязок. В этом руководстве мы визуализируем каждую привязку как синий куб после их расположения. Если для установки общей системы координат используется только привязка, не требуется визуализировать созданный GameObject.

/// <summary>
/// Callback when an anchor is located
/// </summary>
/// <param name="sender">Callback sender</param>
/// <param name="args">Callback AnchorLocatedEventArgs</param>
private void SpatialAnchorManager_AnchorLocated(object sender, AnchorLocatedEventArgs args)
{
    Debug.Log($"ASA - Anchor recognized as a possible anchor {args.Identifier} {args.Status}");

    if (args.Status == LocateAnchorStatus.Located)
    {
        //Creating and adjusting GameObjects have to run on the main thread. We are using the UnityDispatcher to make sure this happens.
        UnityDispatcher.InvokeOnAppThread(() =>
        {
            // Read out Cloud Anchor values
            CloudSpatialAnchor cloudSpatialAnchor = args.Anchor;

            //Create GameObject
            GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
            anchorGameObject.transform.localScale = Vector3.one * 0.1f;
            anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
            anchorGameObject.GetComponent<MeshRenderer>().material.color = Color.blue;

            // Link to Cloud Anchor
            anchorGameObject.AddComponent<CloudNativeAnchor>().CloudToNative(cloudSpatialAnchor);
            _foundOrCreatedAnchorGameObjects.Add(anchorGameObject);
        });
    }
}

Теперь подпишитесь на обратный вызов AnchorLocated, SpatialAnchorManager чтобы убедиться, что наш SpatialAnchorManager_AnchorLocated() метод вызывается после того, как наблюдатель находит привязку.

// Start is called before the first frame update
void Start()
{
    _spatialAnchorManager = GetComponent<SpatialAnchorManager>();
    _spatialAnchorManager.LogDebug += (sender, args) => Debug.Log($"ASA - Debug: {args.Message}");
    _spatialAnchorManager.Error += (sender, args) => Debug.LogError($"ASA - Error: {args.ErrorMessage}");
    _spatialAnchorManager.AnchorLocated += SpatialAnchorManager_AnchorLocated;
}

Наконец, давайте развернем наш LongTap() метод, чтобы включить поиск привязки. Мы будем использовать IsSessionStarted логическое значение, чтобы решить, ищете ли мы все привязки или уничтожаем все привязки, как описано в обзоре приложения

/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
    if (_spatialAnchorManager.IsSessionStarted)
    {
        // Stop Session and remove all GameObjects. This does not delete the Anchors in the cloud
        _spatialAnchorManager.DestroySession();
        RemoveAllAnchorGameObjects();
        Debug.Log("ASA - Stopped Session and removed all Anchor Objects");
    }
    else
    {
        //Start session and search for all Anchors previously created
        await _spatialAnchorManager.StartSessionAsync();
        LocateAnchor();
    }
}

Попробуйте использовать #2

Теперь ваше приложение поддерживает создание привязок и их поиск. Создайте приложение в Unity и разверните его из Visual Studio, выполнив инструкции с помощью Visual Studio для развертывания и отладки.

Убедитесь, что HoloLens подключен к Интернету. Когда приложение начнется, и сообщение Unity исчезнет, короткое касание в вашем окружении. Белый куб должен отображать положение и поворот создаваемой привязки. Процесс создания привязки вызывается автоматически. При медленном просмотре окружающего окружения вы собираете данные среды. После сбора достаточного количества данных среды приложение попытается создать привязку в указанном расположении. После завершения процесса создания привязки куб станет зеленым. Проверьте журналы отладки в Visual Studio, чтобы узнать, работает ли все, как это было.

Длинный касание, чтобы удалить все GameObjects из сцены и остановить сеанс пространственной привязки.

После очистки сцены вы можете снова коснуться, который запустит сеанс и ищет созданные ранее привязки. После их обнаружения они визуализированы синим кубами в привязавленной позиции и повороте. Эти привязки (если срок их действия не истек) можно найти любым поддерживаемым устройством до тех пор, пока они имеют правильные идентификаторы привязки и имеют доступ к ресурсу пространственной привязки.

Удалить привязку

Теперь наше приложение может создавать и находить привязки. Хотя он удаляет GameObjectsобъект, он не удаляет привязку в облаке. Давайте добавим функцию, чтобы также удалить ее в облаке, если вы коснитесь существующей привязки.

Давайте добавим метод DeleteAnchor , который получает GameObject. Затем мы будем использовать SpatialAnchorManager компонент объекта вместе с компонентом объекта CloudNativeAnchor для запроса удаления привязки в облаке.

/// <summary>
/// Deleting Cloud Anchor attached to the given GameObject and deleting the GameObject
/// </summary>
/// <param name="anchorGameObject">Anchor GameObject that is to be deleted</param>
private async void DeleteAnchor(GameObject anchorGameObject)
{
    CloudNativeAnchor cloudNativeAnchor = anchorGameObject.GetComponent<CloudNativeAnchor>();
    CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;

    Debug.Log($"ASA - Deleting cloud anchor: {cloudSpatialAnchor.Identifier}");

    //Request Deletion of Cloud Anchor
    await _spatialAnchorManager.DeleteAnchorAsync(cloudSpatialAnchor);

    //Remove local references
    _createdAnchorIDs.Remove(cloudSpatialAnchor.Identifier);
    _foundOrCreatedAnchorGameObjects.Remove(anchorGameObject);
    Destroy(anchorGameObject);

    Debug.Log($"ASA - Cloud anchor deleted!");
}

Чтобы вызвать этот метод, ShortTapнеобходимо определить, находится ли касание рядом с существующей видимой привязкой. Давайте создадим вспомогательный метод, который заботится об этом

using System.Linq;
/// <summary>
/// Returns true if an Anchor GameObject is within 15cm of the received reference position
/// </summary>
/// <param name="position">Reference position</param>
/// <param name="anchorGameObject">Anchor GameObject within 15cm of received position. Not necessarily the nearest to this position. If no AnchorObject is within 15cm, this value will be null</param>
/// <returns>True if a Anchor GameObject is within 15cm</returns>
private bool IsAnchorNearby(Vector3 position, out GameObject anchorGameObject)
{
    anchorGameObject = null;

    if (_foundOrCreatedAnchorGameObjects.Count <= 0)
    {
        return false;
    }

    //Iterate over existing anchor gameobjects to find the nearest
    var (distance, closestObject) = _foundOrCreatedAnchorGameObjects.Aggregate(
        new Tuple<float, GameObject>(Mathf.Infinity, null),
        (minPair, gameobject) =>
        {
            Vector3 gameObjectPosition = gameobject.transform.position;
            float distance = (position - gameObjectPosition).magnitude;
            return distance < minPair.Item1 ? new Tuple<float, GameObject>(distance, gameobject) : minPair;
        });

    if (distance <= 0.15f)
    {
        //Found an anchor within 15cm
        anchorGameObject = closestObject;
        return true;
    }
    else
    {
        return false;
    }
}

Теперь мы можем расширить наш ShortTap метод, чтобы включить DeleteAnchor вызов

/// <summary>
/// Called when a user is air tapping for a short time 
/// </summary>
/// <param name="handPosition">Location where tap was registered</param>
private async void ShortTap(Vector3 handPosition)
{
    await _spatialAnchorManager.StartSessionAsync();
    if (!IsAnchorNearby(handPosition, out GameObject anchorGameObject))
    {
        //No Anchor Nearby, start session and create an anchor
        await CreateAnchor(handPosition);
    }
    else
    {
        //Delete nearby Anchor
        DeleteAnchor(anchorGameObject);
    }
}

Попробуйте использовать #3

Создайте приложение в Unity и разверните его из Visual Studio, выполнив инструкции с помощью Visual Studio для развертывания и отладки.

Обратите внимание, что расположение жеста касания руки является центром руки в этом приложении, а не кончиком пальцев.

При касании привязки создается (зеленый) или расположенный (синий) запрос отправляется в службу пространственной привязки, чтобы удалить эту привязку из учетной записи. Остановите сеанс (длинный касание) и снова запустите сеанс (длинный касание), чтобы найти все привязки. Удаленные привязки больше не будут находиться.

Итоговое объединение

Вот так выглядит завершенный файл класса AzureSpatialAnchorsScript после объединения различных элементов. Этот файл можно использовать для сравнения с вашим файлом для обнаружения расхождений.

Примечание.

Вы заметите, что мы включили [RequireComponent(typeof(SpatialAnchorManager))] этот сценарий. С помощью этого Unity убедитесь, что GameObject, к которому мы присоединяемся, также имеет присоединение AzureSpatialAnchorsScriptSpatialAnchorManager к нему.

using Microsoft.Azure.SpatialAnchors;
using Microsoft.Azure.SpatialAnchors.Unity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.XR;


[RequireComponent(typeof(SpatialAnchorManager))]
public class AzureSpatialAnchorsScript : MonoBehaviour
{
    /// <summary>
    /// Used to distinguish short taps and long taps
    /// </summary>
    private float[] _tappingTimer = { 0, 0 };

    /// <summary>
    /// Main interface to anything Spatial Anchors related
    /// </summary>
    private SpatialAnchorManager _spatialAnchorManager = null;

    /// <summary>
    /// Used to keep track of all GameObjects that represent a found or created anchor
    /// </summary>
    private List<GameObject> _foundOrCreatedAnchorGameObjects = new List<GameObject>();

    /// <summary>
    /// Used to keep track of all the created Anchor IDs
    /// </summary>
    private List<String> _createdAnchorIDs = new List<String>();

    // <Start>
    // Start is called before the first frame update
    void Start()
    {
        _spatialAnchorManager = GetComponent<SpatialAnchorManager>();
        _spatialAnchorManager.LogDebug += (sender, args) => Debug.Log($"ASA - Debug: {args.Message}");
        _spatialAnchorManager.Error += (sender, args) => Debug.LogError($"ASA - Error: {args.ErrorMessage}");
        _spatialAnchorManager.AnchorLocated += SpatialAnchorManager_AnchorLocated;
    }
    // </Start>

    // <Update>
    // Update is called once per frame
    void Update()
    {

        //Check for any air taps from either hand
        for (int i = 0; i < 2; i++)
        {
            InputDevice device = InputDevices.GetDeviceAtXRNode((i == 0) ? XRNode.RightHand : XRNode.LeftHand);
            if (device.TryGetFeatureValue(CommonUsages.primaryButton, out bool isTapping))
            {
                if (!isTapping)
                {
                    //Stopped Tapping or wasn't tapping
                    if (0f < _tappingTimer[i] && _tappingTimer[i] < 1f)
                    {
                        //User has been tapping for less than 1 sec. Get hand position and call ShortTap
                        if (device.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 handPosition))
                        {
                            ShortTap(handPosition);
                        }
                    }
                    _tappingTimer[i] = 0;
                }
                else
                {
                    _tappingTimer[i] += Time.deltaTime;
                    if (_tappingTimer[i] >= 2f)
                    {
                        //User has been air tapping for at least 2sec. Get hand position and call LongTap
                        if (device.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 handPosition))
                        {
                            LongTap();
                        }
                        _tappingTimer[i] = -float.MaxValue; // reset the timer, to avoid retriggering if user is still holding tap
                    }
                }
            }

        }
    }
    // </Update>


    // <ShortTap>
    /// <summary>
    /// Called when a user is air tapping for a short time 
    /// </summary>
    /// <param name="handPosition">Location where tap was registered</param>
    private async void ShortTap(Vector3 handPosition)
    {
        await _spatialAnchorManager.StartSessionAsync();
        if (!IsAnchorNearby(handPosition, out GameObject anchorGameObject))
        {
            //No Anchor Nearby, start session and create an anchor
            await CreateAnchor(handPosition);
        }
        else
        {
            //Delete nearby Anchor
            DeleteAnchor(anchorGameObject);
        }
    }
    // </ShortTap>

    // <LongTap>
    /// <summary>
    /// Called when a user is air tapping for a long time (>=2 sec)
    /// </summary>
    private async void LongTap()
    {
        if (_spatialAnchorManager.IsSessionStarted)
        {
            // Stop Session and remove all GameObjects. This does not delete the Anchors in the cloud
            _spatialAnchorManager.DestroySession();
            RemoveAllAnchorGameObjects();
            Debug.Log("ASA - Stopped Session and removed all Anchor Objects");
        }
        else
        {
            //Start session and search for all Anchors previously created
            await _spatialAnchorManager.StartSessionAsync();
            LocateAnchor();
        }
    }
    // </LongTap>

    // <RemoveAllAnchorGameObjects>
    /// <summary>
    /// Destroys all Anchor GameObjects
    /// </summary>
    private void RemoveAllAnchorGameObjects()
    {
        foreach (var anchorGameObject in _foundOrCreatedAnchorGameObjects)
        {
            Destroy(anchorGameObject);
        }
        _foundOrCreatedAnchorGameObjects.Clear();
    }
    // </RemoveAllAnchorGameObjects>

    // <IsAnchorNearby>
    /// <summary>
    /// Returns true if an Anchor GameObject is within 15cm of the received reference position
    /// </summary>
    /// <param name="position">Reference position</param>
    /// <param name="anchorGameObject">Anchor GameObject within 15cm of received position. Not necessarily the nearest to this position. If no AnchorObject is within 15cm, this value will be null</param>
    /// <returns>True if a Anchor GameObject is within 15cm</returns>
    private bool IsAnchorNearby(Vector3 position, out GameObject anchorGameObject)
    {
        anchorGameObject = null;

        if (_foundOrCreatedAnchorGameObjects.Count <= 0)
        {
            return false;
        }

        //Iterate over existing anchor gameobjects to find the nearest
        var (distance, closestObject) = _foundOrCreatedAnchorGameObjects.Aggregate(
            new Tuple<float, GameObject>(Mathf.Infinity, null),
            (minPair, gameobject) =>
            {
                Vector3 gameObjectPosition = gameobject.transform.position;
                float distance = (position - gameObjectPosition).magnitude;
                return distance < minPair.Item1 ? new Tuple<float, GameObject>(distance, gameobject) : minPair;
            });

        if (distance <= 0.15f)
        {
            //Found an anchor within 15cm
            anchorGameObject = closestObject;
            return true;
        }
        else
        {
            return false;
        }
    }
    // </IsAnchorNearby>
  
    // <CreateAnchor>
    /// <summary>
    /// Creates an Azure Spatial Anchor at the given position rotated towards the user
    /// </summary>
    /// <param name="position">Position where Azure Spatial Anchor will be created</param>
    /// <returns>Async Task</returns>
    private async Task CreateAnchor(Vector3 position)
    {
        //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
        if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
        {
            headPosition = Vector3.zero;
        }

        Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);

        GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
        anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
        anchorGameObject.transform.position = position;
        anchorGameObject.transform.rotation = orientationTowardsHead;
        anchorGameObject.transform.localScale = Vector3.one * 0.1f;

        //Add and configure ASA components
        CloudNativeAnchor cloudNativeAnchor = anchorGameObject.AddComponent<CloudNativeAnchor>();
        await cloudNativeAnchor.NativeToCloud();
        CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
        cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(3);

        //Collect Environment Data
        while (!_spatialAnchorManager.IsReadyForCreate)
        {
            float createProgress = _spatialAnchorManager.SessionStatus.RecommendedForCreateProgress;
            Debug.Log($"ASA - Move your device to capture more environment data: {createProgress:0%}");
        }

        Debug.Log($"ASA - Saving cloud anchor... ");

        try
        {
            // Now that the cloud spatial anchor has been prepared, we can try the actual save here.
            await _spatialAnchorManager.CreateAnchorAsync(cloudSpatialAnchor);

            bool saveSucceeded = cloudSpatialAnchor != null;
            if (!saveSucceeded)
            {
                Debug.LogError("ASA - Failed to save, but no exception was thrown.");
                return;
            }

            Debug.Log($"ASA - Saved cloud anchor with ID: {cloudSpatialAnchor.Identifier}");
            _foundOrCreatedAnchorGameObjects.Add(anchorGameObject);
            _createdAnchorIDs.Add(cloudSpatialAnchor.Identifier);
            anchorGameObject.GetComponent<MeshRenderer>().material.color = Color.green;
        }
        catch (Exception exception)
        {
            Debug.Log("ASA - Failed to save anchor: " + exception.ToString());
            Debug.LogException(exception);
        }
    }
    // </CreateAnchor>

    // <LocateAnchor>
    /// <summary>
    /// Looking for anchors with ID in _createdAnchorIDs
    /// </summary>
    private void LocateAnchor()
    {
        if (_createdAnchorIDs.Count > 0)
        {
            //Create watcher to look for all stored anchor IDs
            Debug.Log($"ASA - Creating watcher to look for {_createdAnchorIDs.Count} spatial anchors");
            AnchorLocateCriteria anchorLocateCriteria = new AnchorLocateCriteria();
            anchorLocateCriteria.Identifiers = _createdAnchorIDs.ToArray();
            _spatialAnchorManager.Session.CreateWatcher(anchorLocateCriteria);
            Debug.Log($"ASA - Watcher created!");
        }
    }
    // </LocateAnchor>

    // <SpatialAnchorManagerAnchorLocated>
    /// <summary>
    /// Callback when an anchor is located
    /// </summary>
    /// <param name="sender">Callback sender</param>
    /// <param name="args">Callback AnchorLocatedEventArgs</param>
    private void SpatialAnchorManager_AnchorLocated(object sender, AnchorLocatedEventArgs args)
    {
        Debug.Log($"ASA - Anchor recognized as a possible anchor {args.Identifier} {args.Status}");

        if (args.Status == LocateAnchorStatus.Located)
        {
            //Creating and adjusting GameObjects have to run on the main thread. We are using the UnityDispatcher to make sure this happens.
            UnityDispatcher.InvokeOnAppThread(() =>
            {
                // Read out Cloud Anchor values
                CloudSpatialAnchor cloudSpatialAnchor = args.Anchor;

                //Create GameObject
                GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
                anchorGameObject.transform.localScale = Vector3.one * 0.1f;
                anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
                anchorGameObject.GetComponent<MeshRenderer>().material.color = Color.blue;

                // Link to Cloud Anchor
                anchorGameObject.AddComponent<CloudNativeAnchor>().CloudToNative(cloudSpatialAnchor);
                _foundOrCreatedAnchorGameObjects.Add(anchorGameObject);
            });
        }
    }
    // </SpatialAnchorManagerAnchorLocated>

    // <DeleteAnchor>
    /// <summary>
    /// Deleting Cloud Anchor attached to the given GameObject and deleting the GameObject
    /// </summary>
    /// <param name="anchorGameObject">Anchor GameObject that is to be deleted</param>
    private async void DeleteAnchor(GameObject anchorGameObject)
    {
        CloudNativeAnchor cloudNativeAnchor = anchorGameObject.GetComponent<CloudNativeAnchor>();
        CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;

        Debug.Log($"ASA - Deleting cloud anchor: {cloudSpatialAnchor.Identifier}");

        //Request Deletion of Cloud Anchor
        await _spatialAnchorManager.DeleteAnchorAsync(cloudSpatialAnchor);

        //Remove local references
        _createdAnchorIDs.Remove(cloudSpatialAnchor.Identifier);
        _foundOrCreatedAnchorGameObjects.Remove(anchorGameObject);
        Destroy(anchorGameObject);

        Debug.Log($"ASA - Cloud anchor deleted!");
    }
    // </DeleteAnchor>

}

Следующие шаги

В этом руководстве вы узнали, как реализовать базовое приложение Пространственных привязок для HoloLens с помощью Unity. Чтобы узнать больше об использовании Пространственных привязок Azure в новом приложении Android, перейдите к следующему руководству.