Tutorial: Instruções passo a passo para criar um novo aplicativo HoloLens Unity usando as Âncoras Espaciais do Azure

Este tutorial mostrará como criar um novo aplicativo HoloLens Unity com as Âncoras Espaciais do Azure.

Pré-requisitos

Para concluir este tutorial, confirme que tem:

  1. PC - UM PC com Windows
  2. Visual Studio Visual Studio - 2019 instalado com a carga de trabalho de desenvolvimento da Plataforma Universal do Windows e o componente SDK do Windows 10 (10.0.18362.0 ou mais recente). O C++/WinRT Visual Studio Extension (VSIX) para Visual Studio deve ser instalado a partir do Visual Studio Marketplace.
  3. HoloLens - Um dispositivo HoloLens com o modo de desenvolvedor ativado. Este artigo requer um dispositivo HoloLens com a Atualização de maio de 2020 do Windows 10. Para atualizar para a versão mais recente no HoloLens, abra o aplicativo Configurações, vá para Atualizar & Segurança e selecione o botão Verificar se há atualizações.
  4. Unity Unity - 2020.3.25 com módulos Universal Windows Platform Build Support e Windows Build Support (IL2CPP)

Criando e configurando o Projeto Unity

Create New Project (Funções do Azure: Criar Novo Projeto)

  1. No Unity Hub, selecione Novo projeto
  2. Selecione 3D
  3. Introduza o nome do seu Projeto e introduza uma Localização para guardar
  4. Selecione Criar projeto e aguarde que o Unity crie seu projeto

Alterar plataforma de compilação

  1. No editor da unidade, selecione Configurações de compilação de arquivos>
  2. Selecione Plataforma Universal do Windows e, em seguida, Mudar de Plataforma. Aguarde até que Unity termine de processar todos os arquivos.

Importar ASA e OpenXR

  1. Iniciar ferramenta de recurso de realidade mista
  2. Selecione o caminho do projeto - a pasta que contém pastas como Assets, Packages, ProjectSettings e assim por diante - e selecione Discover Features
  3. Em Serviços de Realidade Mista do Azure, selecione ambos
    1. Azure Spatial Anchors SDK Core
    2. SDK de Âncoras Espaciais do Azure para Windows
  4. Em Suporte à plataforma, selecione
    1. Plugin OpenXR de Realidade Mista

Nota

Certifique-se de ter atualizado o catálogo e a versão mais recente está selecionada para cada

MRFT - Feature Selection

  1. Pressione Obter recursos --Importar --Aprovar -->>>Sair
  2. Ao refocalizar sua janela Unity, Unity começará a importar os módulos
  3. Se você receber uma mensagem sobre como usar o novo sistema de entrada, selecione Sim para reiniciar o Unity e ativar os back-ends.

Configurar as configurações do projeto

Agora vamos definir algumas configurações do projeto Unity que nos ajudam a direcionar o SDK Holográfico do Windows para desenvolvimento.

Alterar configurações do OpenXR

  1. Selecione Configurações de compilação de arquivo>(ele ainda pode estar aberto na etapa anterior)
  2. Selecione Configurações do Player...
  3. Selecione Gerenciamento de plug-in XR
  4. Verifique se a guia Configurações da Plataforma Universal do Windows está marcada e marque a caixa ao lado de OpenXR e ao lado do grupo de recursos Microsoft HoloLens
  5. Selecione o sinal de aviso amarelo ao lado de OpenXR para exibir todos os problemas do OpenXR .
  6. Selecione Corrigir tudo
  7. Para corrigir o problema "Pelo menos um perfil de interação deve ser adicionado", selecione Editar para abrir as configurações do Projeto OpenXR. Em seguida, em Perfis de Interação , selecione o + símbolo e selecione Perfil de Interação Microsoft HandUnity - OpenXR Setup

Alterar configurações de qualidade

  1. Selecione Editar>qualidade das configurações do projeto>
  2. Na coluna sob o logotipo da Plataforma Universal do Windows, selecione a seta na linha Padrão e selecione Muito baixo. Você saberá que a configuração é aplicada corretamente quando a caixa na coluna Plataforma Universal do Windows e na linha Muito Baixo estiver verde.

Definir capacidades

  1. Vá para Editar>Configurações>do Projeto Player (você ainda pode tê-lo aberto na etapa anterior).
  2. Verifique se a guia Configurações da Plataforma Universal do Windows está selecionada
  3. Na seção Configuração de Definições de Publicação, habilite o seguinte
    1. InternetClient
    2. InternetClientServer
    3. PrivateNetworkClientServer
    4. SpatialPerception (pode já estar ativado)

Configurar a câmara principal

  1. No Painel Hierarquia, selecione Câmara Principal.
  2. No Inspetor, defina sua posição de transformação como 0,0,0.
  3. Encontre a propriedade Clear Flags e altere a lista suspensa de Skybox para Solid Color.
  4. Selecione o campo Plano de fundo para abrir um seletor de cores.
  5. Defina R, G, B e A como 0.
  6. Selecione Adicionar componente na parte inferior e adicione o componente Driver de pose rastreada à câmeraUnity - Camera Setup

Experimente #1

Agora você deve ter uma cena vazia que está pronta para ser implantada no seu dispositivo HoloLens. Para testar se tudo está funcionando, crie seu aplicativo no Unity e implante-o a partir do Visual Studio. Siga Usando o Visual Studio para implantar e depurar para fazer isso. Você deve ver a tela inicial do Unity e, em seguida, uma exibição clara.

Criar um recurso de Âncoras Espaciais

Aceda ao portal do Azure.

No painel esquerdo, selecione Criar um recurso.

Use a caixa de pesquisa para procurar Âncoras Espaciais.

Screenshot showing the results of a search for Spatial Anchors.

Selecione Âncoras espaciais e, em seguida, selecione Criar.

No painel Conta de Âncoras Espaciais, faça o seguinte:

  • Insira um nome de recurso exclusivo usando caracteres alfanuméricos regulares.

  • Selecione a subscrição à qual pretende anexar o recurso.

  • Crie um grupo de recursos selecionando Criar novo. Nomeie-o myResourceGroup e selecione OK.

    Um grupo de recursos é um contêiner lógico no qual os recursos do Azure, como aplicativos Web, bancos de dados e contas de armazenamento, são implantados e gerenciados. Por exemplo, pode optar por eliminar todo o grupo de recursos num único passo simples mais tarde.

  • Selecione um local (região) no qual colocar o recurso.

  • Selecione Criar para começar a criar o recurso.

Screenshot of the Spatial Anchors pane for creating a resource.

Depois que o recurso é criado, o portal do Azure mostra que sua implantação foi concluída.

Screenshot showing that the resource deployment is complete.

Selecione Ir para recurso. Agora você pode exibir as propriedades do recurso.

Copie o valor de ID de conta do recurso em um editor de texto para uso posterior.

Screenshot of the resource properties pane.

Copie também o valor Domínio da Conta do recurso em um editor de texto para uso posterior.

Screenshot showing the resource's account domain value.

Em Configurações, selecione Chave de acesso. Copie o valor da chave primária, Chave de conta, em um editor de texto para uso posterior.

Screenshot of the Keys pane for the account.

Criando & Adicionando scripts

  1. Em Unity, no painel Projeto, crie uma nova pasta chamada Scripts, na pasta Ativos .
  2. Na pasta, clique com o botão direito do mouse em -Create ->>C# Script. Título: AzureSpatialAnchorsScript
  3. Vá para GameObject ->Create Empty.
  4. Selecione-o e, no Inspetor , renomeie-o de GameObject para AzureSpatialAnchors.
  5. Ainda sobre o GameObject
    1. Defina a sua posição para 0,0,0
    2. Selecione Adicionar Componente e procure e adicione o AzureSpatialAnchorsScript
    3. Selecione Adicionar componente novamente e procure e adicione o Gerenciador de âncora de RA. Isso adicionará automaticamente o AR Session Origin também.
    4. Selecione Adicionar componente novamente e procure e adicione o script SpatialAnchorManager
    5. No componente SpatialAnchorManager adicionado, preencha a ID da Conta, a Chave da Conta e o Domínio da Conta que você copiou na etapa anterior do recurso de âncoras espaciais no portal do Azure.

Unity - ASA GameObject

Visão geral do aplicativo

Nosso aplicativo suportará as seguintes interações:

Gesto Ação
Toque em qualquer lugar Iniciar/Continuar Sessão + Criar âncora na Posição da Mão
Tocar numa âncora Excluir + Excluir GameObject âncora no ASA Cloud Service
Toque + Segure por 2 segundos (+ sessão está em execução) Pare a sessão e remova todos os GameObjectsarquivos . Mantenha âncoras no ASA Cloud Service
Toque + Segure por 2 segundos (+ a sessão não está em execução) Inicie a sessão e procure todas as âncoras.

Adicionar reconhecimento de toque

Vamos adicionar algum código ao nosso script para poder reconhecer o gesto de toque de um usuário.

  1. Abra AzureSpatialAnchorsScript.cs no Visual Studio clicando duas vezes no script no painel Projeto Unity.
  2. Adicione a seguinte matriz à sua classe
public class AzureSpatialAnchorsScript : MonoBehaviour
{
    /// <summary>
    /// Used to distinguish short taps and long taps
    /// </summary>
    private float[] _tappingTimer = { 0, 0 };
  1. Adicione os dois métodos a seguir abaixo do método Update(). Acrescentaremos a implementação numa fase posterior
// 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. Adicione a seguinte importação
using UnityEngine.XR;
  1. Adicione o seguinte código sobre o Update() método. Isso permitirá que o aplicativo reconheça gestos curtos e longos (2 segundos) de toque com a mão
// 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
                }
            }
        }

    }
}

Adicionar & Configurar SpatialAnchorManager

O ASA SDK oferece uma interface simples chamada SpatialAnchorManager para fazer chamadas para o serviço ASA. Vamos adicioná-lo como uma variável ao nosso AzureSpatialAnchorsScript.cs

Primeiro, adicione a importação

using Microsoft.Azure.SpatialAnchors.Unity;

Em seguida, declare a variável

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() No método, atribua a variável ao componente que adicionamos em uma etapa anterior

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

Para receber logs de depuração e erros, precisamos nos inscrever nos diferentes retornos de chamada

// 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}");
}

Nota

Para visualizar os logs, certifique-se de que, depois de criar o projeto a partir do Unity e abrir a solução .slndo Visual Studio, selecione Depurar --> Executar com Depuração e deixe o HoloLens conectado ao computador enquanto o aplicativo está em execução.

Iniciar sessão

Para criar e encontrar âncoras, primeiro temos que iniciar uma sessão. Ao chamar StartSessionAsync(), criará uma sessão, se necessário, SpatialAnchorManager e depois iniciá-la-á. Vamos adicionar isso ao nosso ShortTap() método.

/// <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();
}

Criar Âncora

Agora que temos uma sessão em execução, podemos criar âncoras. Neste aplicativo, gostaríamos de acompanhar a âncora criada e os identificadores de âncora criados (IDs de âncora GameObjects ). Vamos adicionar duas listas ao nosso código.

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>();

Vamos criar um método CreateAnchor que cria uma âncora em uma posição definida por seu parâmetro.

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.
}

Como as âncoras espaciais não só têm uma posição, mas também uma rotação, vamos definir a rotação para sempre se orientar em direção ao HoloLens na criação.

/// <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);

}

Agora que temos a posição e a rotação da âncora desejada, vamos criar um visível GameObject. Observe que as Âncoras Espaciais não exigem que a âncora seja visível para o usuário final, uma vez que o principal objetivo das Âncoras GameObject Espaciais é fornecer um quadro de referência comum e persistente. Para o propósito deste tutorial, vamos visualizar as âncoras como cubos. Cada âncora será inicializada como um cubo branco, que se transformará em um cubo verde assim que o processo de criação for bem-sucedido.

/// <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;

}

Nota

Estamos usando um sombreador legado, já que ele está incluído em uma compilação padrão do Unity. Outros sombreadores, como o sombreador padrão, só são incluídos se especificados manualmente ou se fizerem parte diretamente da cena. Se um sombreador não estiver incluído e o aplicativo estiver tentando renderizá-lo, isso resultará em um material rosa.

Agora vamos adicionar e configurar os componentes da Âncora Espacial. Estamos definindo o vencimento da âncora para 3 dias a partir da criação da âncora. Depois disso, eles serão excluídos automaticamente da nuvem. Lembre-se de adicionar a importação

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);

}

Para salvar uma âncora, o usuário deve coletar dados do ambiente.

/// <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%}");
    }

}

Nota

Um HoloLens pode possivelmente reutilizar dados de ambiente já capturados ao redor da âncora, resultando em IsReadyForCreate ser verdadeiro já quando chamado pela primeira vez.

Agora que a âncora espacial da nuvem foi preparada, podemos tentar salvar aqui.

/// <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);
    }
}

Finalmente, vamos adicionar a chamada de função ao nosso ShortTap método

/// <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);
}

Nosso aplicativo agora pode criar várias âncoras. Agora, qualquer dispositivo pode localizar as âncoras criadas (se ainda não tiver expirado), desde que conheça as IDs de âncora e tenha acesso ao mesmo Recurso de Âncoras Espaciais no Azure.

Parar Sessão & Destruir GameObjects

Para emular um segundo dispositivo encontrando todas as âncoras, agora vamos parar a sessão e remover todos os GameObjects âncora (manteremos os IDs de âncora). Depois disso, iniciaremos uma nova sessão e consultaremos as âncoras usando os IDs de âncora armazenados.

SpatialAnchorManager pode cuidar da sessão parando simplesmente chamando seu DestroySession() método. Vamos adicionar isso ao nosso LongTap() método

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

Vamos criar um método para remover todas as âncoras GameObjects

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

E chamá-lo depois de destruir a sessão em 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");
}

Localizar Anchor

Vamos agora tentar encontrar as âncoras novamente com a posição e rotação corretas em que as criamos. Para fazer isso, precisamos iniciar uma sessão e criar uma Watcher que procure âncoras que se encaixem nos critérios determinados. Como critérios, iremos alimentá-lo com os IDs das âncoras que criamos anteriormente. Vamos criar um método LocateAnchor() e usar SpatialAnchorManager para criar um Watcherarquivo . Para estratégias de localização diferentes do uso de IDs de âncora, consulte Estratégia de localização de âncora

/// <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!");
    }
}

Uma vez que um observador é iniciado, ele disparará um retorno de chamada quando encontrar uma âncora que se encaixa nos critérios fornecidos. Vamos primeiro criar nosso método de localização de âncora chamado que configuraremos para ser chamado SpatialAnchorManager_AnchorLocated() quando o observador tiver localizado uma âncora. Esse método criará um visual GameObject e anexará o componente âncora nativo a ele. O componente de ancoragem nativo garantirá que a posição e a rotação corretas do estejam definidas GameObject .

Semelhante ao processo de criação, a âncora é anexada a um GameObject. Este GameObject não precisa estar visível em sua cena para que as âncoras espaciais funcionem. Para o propósito deste tutorial, visualizaremos cada âncora como um cubo azul uma vez que tenham sido localizadas. Se você usar apenas a âncora para estabelecer um sistema de coordenadas compartilhado, não há necessidade de visualizar o GameObject criado.

/// <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);
        });
    }
}

Vamos agora assinar o retorno de chamada AnchorLocated SpatialAnchorManager para garantir que nosso SpatialAnchorManager_AnchorLocated() método seja chamado assim que o observador encontrar uma âncora.

// 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;
}

Finalmente, vamos expandir nosso LongTap() método para incluir encontrar a âncora. Usaremos o IsSessionStarted booleano para decidir se estamos procurando todas as âncoras ou destruindo todas as âncoras, conforme descrito na Visão geral do aplicativo

/// <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();
    }
}

Experimente #2

Seu aplicativo agora oferece suporte à criação de âncoras e à sua localização. Crie seu aplicativo no Unity e implante-o a partir do Visual Studio seguindo Usando o Visual Studio para implantar e depurar.

Certifique-se de que o HoloLens está ligado à Internet. Assim que o aplicativo for iniciado e a mensagem feita com Unity desaparecer, toque rapidamente em seus arredores. Um cubo branco deve aparecer para mostrar a posição e a rotação da âncora a ser criada. O processo de criação da âncora é chamado automaticamente. À medida que você olha lentamente ao redor, você está capturando dados do ambiente. Assim que dados de ambiente suficientes forem coletados, nosso aplicativo tentará criar uma âncora no local especificado. Quando o processo de criação da âncora for concluído, o cubo ficará verde. Verifique seus logs de depuração no visual studio para ver se tudo funcionou como pretendido.

Toque longo para remover tudo GameObjects da cena e parar a sessão de ancoragem espacial.

Uma vez que sua cena é limpa, você pode tocar por muito tempo novamente, o que iniciará uma sessão e procurará as âncoras que você criou anteriormente. Uma vez encontrados, são visualizados por cubos azuis na posição e rotação ancoradas. Essas âncoras (desde que não tenham expirado) podem ser encontradas por qualquer dispositivo suportado, desde que tenham os IDs de âncora corretos e tenham acesso ao seu recurso de âncora espacial.

Excluir âncora

Neste momento, a nossa aplicação pode criar e localizar âncoras. Embora exclua o GameObjects, ele não exclui a âncora na nuvem. Vamos adicionar a funcionalidade para também excluí-lo na nuvem se você tocar em uma âncora existente.

Vamos adicionar um método DeleteAnchor que recebe um GameObjectarquivo . Em seguida, usaremos o componente juntamente com o SpatialAnchorManager objeto CloudNativeAnchor para solicitar a exclusão da âncora na nuvem.

/// <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!");
}

Para chamar esse método de , precisamos ser capazes de determinar se uma torneira esteve perto de ShortTapuma âncora visível existente. Vamos criar um método auxiliar que cuide disso

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;
    }
}

Agora podemos estender nosso ShortTap método para incluir a DeleteAnchor chamada

/// <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);
    }
}

Experimente #3

Crie seu aplicativo no Unity e implante-o a partir do Visual Studio seguindo Usando o Visual Studio para implantar e depurar.

Note que a localização do seu gesto de tocar com a mão é o centro da sua mão neste aplicativo e não a ponta dos seus dedos.

Quando você toca em uma âncora, criada (verde) ou localizada (azul), uma solicitação é enviada ao serviço de âncora espacial para remover essa âncora da conta. Pare a sessão (toque longo) e inicie a sessão novamente (toque longo) para procurar todas as âncoras. As âncoras excluídas não serão mais localizadas.

Juntando tudo

Aqui está como o arquivo de classe completo AzureSpatialAnchorsScript deve parecer, depois de todos os diferentes elementos terem sido montados. Você pode usá-lo como uma referência para comparar com seu próprio arquivo e identificar se você pode ter alguma diferença.

Nota

Você notará que incluímos [RequireComponent(typeof(SpatialAnchorManager))] o roteiro. Com isso, Unity irá certificar-se de que o GameObject onde nos anexamos, também tem o anexado AzureSpatialAnchorsScriptSpatialAnchorManager a ele.

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>

}

Próximos passos

Neste tutorial, você aprendeu como implementar um aplicativo básico de Âncoras Espaciais para HoloLens usando o Unity. Para saber mais sobre como usar as Âncoras Espaciais do Azure em um novo aplicativo Android, continue para o próximo tutorial.