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

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

Pré-requisitos

Para concluir este tutorial, verifique se você 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). A VSIX (Extensão do Visual Studio) C++/WinRT para o Visual Studio deve ser instalada do Visual Studio Marketplace.
  3. HoloLens - um dispositivo do HoloLens com o modo de desenvolvedor habilitado. Este artigo requer um dispositivo HoloLens com a Atualização do Windows 10 de maio de 2020. Para atualizar para a versão mais recente no HoloLens, abra o aplicativo de Configurações, acesse Atualização e Segurança e, em seguida, selecione o botão Verificar se há atualizações.
  4. Unidade - Unity 2020.3.25 com módulos do Suporte de Compilação da Plataforma Universal do Windows e suporte de compilação do Windows(IL2CPP)

Criando e configurando o Unity Project

Criar um novo projeto

  1. No Hub do Unity, selecione Novo projeto
  2. Selecione 3D
  3. Insira o nome do projeto e insira um local para salvar
  4. Selecione Criar projeto e aguarde até que o Unity crie seu projeto

Alterar a plataforma de compilação

  1. No editor do Unity, selecione Configurações de >compilação de arquivo
  2. Selecione Plataforma Universal do Windowse, em seguida, Alternar plataforma. Aguarde até que o Unity tenha concluído o processamento de todos os arquivos.

Importar ASA e OpenXR

  1. Iniciar a Ferramenta de Recurso de Realidade Misturada
  2. Selecione o caminho do projeto – a pasta que contém pastas como Ativos, Pacotes, ProjectSettings e assim por diante – e selecione Descobrir Recursos
  3. Em Serviços de Realidade Misturada do Azure, selecione ambos
    1. Núcleo do SDK das Âncoras Espaciais do Azure
    2. SDK das Âncoras Espaciais do Azure para Windows
  4. Em Suporte à Plataforma, selecione
    1. Plug-in OpenXR de Realidade Misturada

Observação

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

MRFT - Feature Selection

  1. Pressione Obter Recursos – >Importar –>Aprovar – >Sair
  2. Ao refocar a janela do Unity, o 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 habilitar os back-ends.

Definir as Configurações do Projeto

Vamos agora definir algumas configurações de projeto do Unity que nos ajudam a direcionar ao SDK do Windows Holographic para desenvolvimento.

Alterar o Configurações

  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 do XR
  4. Verifique se a guia Configurações da Plataforma Universal do Windows está selecionada e marque a caixa ao lado de OpenXR e ao lado Grupo de recursos do Microsoft HoloLens
  5. Selecione o sinal de aviso amarelo ao lado de OpenXR para ver 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 de Projeto do OpenXR. Em seguida, em Perfis de interação selecione o+ símbolo e selecione a Unity Perfil de interação manual da MicrosoftUnity - OpenXR Setup

Alterar Configurações de Qualidade

  1. Selecione Editar>Configurações do Projeto>Qualidade
  2. Na coluna sob o logotipo da Plataforma Universal do Windows, selecione a seta na linha Padrão e escolha Muito Baixa. Você saberá que a configuração foi aplicada corretamente quando a caixa na coluna Plataforma Universal do Windows e a linha Muito Baixa estiver verde.

Definir capacidades

  1. Acesse Editar>Configurações do Projeto>Player (você ainda poderá tê-lo aberto da etapa anterior).
  2. Certifique-se de que a guia Configurações da Plataforma Universal do Windows esteja selecionada
  3. Na seção Definições de Configurações de Publicação, habilita o seguinte
    1. InternetClient
    2. InternetClientServer
    3. PrivateNetworkClientServer
    4. SpatialPerception (pode já estar habilitado)

Configurar a câmera principal

  1. No Painel de Hierarquia, selecione Câmera Principal.
  2. No Inspetor, defina sua posição de transformação para 0,0,0.
  3. Localize a propriedade Limpar Sinalizadores e altere a lista suspensa de Skybox para Cor Sólida.
  4. Selecione o campo Tela de fundo para abrir o seletor de cor.
  5. Defina R, G, B e A para 0.
  6. Selecione Adicionar Componente na parte inferior e adicione o componente de Driver de Pose Rastreada à câmera Unity - Camera Setup

Experimente #1

Agora você deve ter uma cena vazia pronta para ser implantada em seu HoloLens dispositivo. Para testar se tudo está funcionando, compile o seu aplicativo Unity e implante-o do Visual Studio. Siga Usando o Visual Studio para implantar e depurar para fazê-lo. Você deve ver a tela inicial do Unity e, em seguida, uma tela clara.

Criar um recurso Âncoras Espaciais

Vá para o Portal do Azure.

No painel esquerdo, clique em Criar um recurso.

Use a caixa de pesquisa para Âncoras Espaciais.

Screenshot showing the results of a search for Spatial Anchors.

Selecione Âncoras Espaciais e clique em Criar.

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

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

  • Selecione a assinatura que você deseja anexar o recurso.

  • Crie um grupo de recursos, selecionando Criar novo. Nomeie-o myResourceGroup e clique em 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, é possível excluir posteriormente todo o grupo de recursos com uma única etapa simples.

  • Selecione um local (região) para criar 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 for criado, o portal do Azure mostrará que a implantação foi concluída.

Screenshot showing that the resource deployment is complete.

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

Copie o valor da ID da Conta do recurso em um editor de texto para usar posteriormente.

Screenshot of the resource properties pane.

Copie também o valor do Domínio de Contas do recurso em um editor de texto para usar posteriormente.

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 usar posteriormente.

Screenshot of the Keys pane for the account.

Criando & Adicionando scripts

  1. No Unity no painel Projeto, crie uma pasta chamada Scripts na pasta Ativos.
  2. Na pasta, clique com o botão direito do mouse ->Criar ->Script C#. Dê a ela o título AzureSpatialAnchorsScript
  3. Vá para GameObject ->Criar Vazio.
  4. Selecione-o e, em Inspetor, renomeie-o de GameObject para AzureSpatialAnchors.
  5. Ainda no GameObject
    1. Definir sua posição como 0,0,0
    2. Selecione Adicionar Componente e pesquise por AzureSpatialAnchorsScript e adicione-o
    3. Selecione Adicionar Componente novamente e pesquise por Gerente de âncora de RA e adicione-o. Isso também adicionará automaticamente a Origem da Sessão de AR.
    4. Selecione Adicionar Componente novamente e pesquise por SpatialAnchorManager e adicione o script
    5. No componente SpatialAnchorManager adicionado, preencha a ID da Conta, a Chave de Conta e o Domínio da Conta copiados na etapa anterior do recurso de âncoras espaciais no portal do Azure.

Unity - ASA GameObject

Visão geral do aplicativo

Nosso aplicativo dará suporte às seguintes interações:

Gesto Ação
Toque em qualquer lugar Iniciar/continuar sessão + Criar âncora na posição da mão
Tocar em uma âncora Excluir GameObject + Excluir Âncora no Serviço de Nuvem do ASA
Toque em + Manter por 2 segundos (+ sessão está em execução) Pare a sessão e remova todos GameObjects. Manter âncoras no Serviço de Nuvem do ASA
Toque em + Manter por 2 segundos (+ sessão não está em execução) Inicie a sessão e procure todas as âncoras.

Adicionar reconhecimento de toque

Vamos adicionar um 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 do Projeto Unity.
  2. Adicione o 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(). Adicionaremos a implementação em um estágio 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 importação a seguir
using UnityEngine.XR;
  1. Adicione o seguinte código ao método de Update(). Isso permitirá que o aplicativo reconheça gestos de toque de mão curtos e longos (2 segundos)
// 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 SDK do ASA oferece uma interface simples chamada SpatialAnchorManager para fazer chamadas ao 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;

No método Start(), 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 erro, precisamos assinar os 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}");
}

Observação

Para exibir os logs, certifique-se de que, depois de ter criado o projeto do Unity e aberto a solução do Visual Studio.sln, selecione Depurar –> Executar com Depuração e deixe seu HoloLens conectado ao computador enquanto o aplicativo está em execução.

Iniciar Sessão

Para criar e encontrar âncoras, primeiro precisamos iniciar uma sessão. Ao chamar StartSessionAsync(), SpatialAnchorManager criará uma sessão, se necessário, e a iniciará. Vamos adicionar isso ao nosso método 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();
}

Criar âncora

Agora que temos uma sessão em execução, podemos criar âncoras. Neste aplicativo, queremos manter o controle da âncora criada GameObjects e dos identificadores de âncora criados (IDs de âncora). 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 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ívelGameObject. Observe que as Âncoras Espaciais não exigem que a âncora GameObject seja visível para o usuário final, pois a principal finalidade das Âncoras Espaciais é fornecer um quadro de referência comum e persistente. Para a finalidade deste tutorial, visualizaremos as âncoras como cubos. Cada âncora será inicializada como um cubo branco, que se transformará em um cubo verde depois 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;

}

Observação

Estamos usando um sombreador herdado, pois ele está incluído uma compilação padrão do Unity. Outros sombreadores, como o sombreador padrão, só serão incluídos se especificados manualmente ou se fazem parte diretamente da cena. Se um sombreador não estiver incluído e o aplicativo estiver tentando renderizar, isso resultará em um material rosa.

Agora, vamos adicionar e configurar os componentes de Âncora Espacial. Estamos definindo a expiração da âncora como 3 dias após a 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 de 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%}");
    }

}

Observação

Um HoloLens pode reutilizar os dados de ambiente já capturados em torno da âncora, resultando em IsReadyForCreateverdadeiro já quando chamado pela primeira vez.

Agora que a âncora espacial de nuvem foi preparada, podemos tentar salvar o real 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);
    }
}

Por fim, vamos adicionar a chamada de função ao nosso método 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);
}

Nosso aplicativo agora pode criar várias âncoras. Agora, qualquer dispositivo pode localizar as âncoras criadas (se ainda não expiraram), desde que elas saibam as IDs de âncora e tenham acesso ao mesmo Recurso de Âncoras Espaciais no Azure.

Parar sessão & Destruir GameObjects

Para emular um segundo dispositivo que encontra todas as âncoras, agora interromperemos a sessão e removeremos todos os GameObjects de âncora (manteremos as IDs de âncora). Depois disso, iniciaremos uma nova sessão e consultaremos as âncoras usando as IDs de âncora armazenadas.

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

/// <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 chame-o 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 Âncora

Agora, tentaremos encontrar as âncoras novamente com a posição e a rotação corretas em que as criamos. Para fazer isso, precisamos iniciar uma sessão e criar um Watcher que procurará âncoras que se ajustem aos critérios determinados. Como critérios, alimentaremos as IDs das âncoras que criamos anteriormente. Vamos criar um método LocateAnchor() e usar SpatialAnchorManager para criar um Watcher. Para localizar estratégias diferentes de usar 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!");
    }
}

Depois que um inspetor for iniciado, ele disparará um retorno de chamada quando encontrar uma âncora que se ajuste aos critérios determinados. Primeiro, vamos criar nosso método localizado em âncora chamado SpatialAnchorManager_AnchorLocated() que configuraremos para ser chamado quando o inspetor tiver localizado uma âncora. Esse método criará um visual GameObject e anexa o componente de âncora nativo a ele. O componente de âncora nativa garantirá que a posição e a rotação corretas do GameObject estão definidas.

Semelhante ao processo de criação, a âncora é anexada a um GameObject. Esse GameObject não precisa estar visível em sua cena para que as âncoras espaciais funcionem. Para a finalidade deste tutorial, visualizaremos cada âncora como um cubo azul depois que elas foram localizadas. Se você usar apenas a âncora para estabelecer um sistema de coordenadas compartilhado, não será necessário 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);
        });
    }
}

Agora, vamos assinar o retorno de chamada AnchorLocated de SpatialAnchorManager para garantir que nosso método SpatialAnchorManager_AnchorLocated() seja chamado quando o inspetor 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;
}

Por fim, vamos expandir nosso método LongTap() para incluir a descoberta da âncora. Vamos usar o booliana IsSessionStarted para decidir se estamos procurando todas as âncoras ou destruir 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 dá suporte à criação de âncoras e à localização delas. Compile seu aplicativo no Unity e implante-o do Visual Studio seguindo Usando o Visual Studio para implantar e depurar.

Certifique-se de que o HoloLens está conectado à Internet. Depois que o aplicativo tiver sido iniciado e o feito com a mensagem do Unity desaparecer, toque curto em seu entorno. Um cubo branco deve aparecer para mostrar a posição e a rotação da âncora a ser criada. O processo de criação de âncora é chamado automaticamente. Ao olhar lentamente ao redor, você está capturando dados de ambiente. Depois que dados de ambiente suficientes são coletados, nosso aplicativo tentará criar uma âncora no local especificado. Depois que o processo de criação da âncora for concluído, o cubo ficará verde. Verifique os logs de depuração no Visual Studio para ver se tudo funcionou conforme o esperado.

Toque longo para remover tudo GameObjects da cena e interromper a sessão de âncora espacial.

Depois que a cena for limpa, você poderá tocar por muito tempo novamente, o que iniciará uma sessão e procurará as âncoras criadas anteriormente. Depois de encontrados, eles 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 com suporte, desde que tenham as IDs de âncora corretas e tenham acesso ao recurso de âncora espacial.

Excluir âncora

No momento, nosso aplicativo 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í-la na nuvem se você tocar em uma âncora existente.

Vamos adicionar um método DeleteAnchor que recebe um GameObject. Em seguida, vamos usar o SpatialAnchorManager junto com o componente do 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 ShortTap,precisamos ser capazes de determinar se um toque foi próximo a uma âncora visível existente. Vamos criar um método auxiliar que cuida 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 chamada 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);
    }
}

Experimente #3

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

Observe que o local do gesto de tocar com a mão é o centro da mão neste aplicativo e não a ponta dos 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 pesquisar todas as âncoras. As âncoras excluídas não estarão mais localizadas.

Juntando tudo

Vemos aqui como deve ser a aparência do arquivo de classe AzureSpatialAnchorsScript completo após todos os elementos diferentes terem sido colocados juntos. Você pode usar isso como uma referência para comparar com seu próprio arquivo e identificar se você tem alguma diferença restante.

Observação

Você observará que incluímos [RequireComponent(typeof(SpatialAnchorManager))] no script. Com isso, o Unity garantirá que o GameObject ao qual anexamos AzureSpatialAnchorsScript também tenha SpatialAnchorManager o anexado 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óximas etapas

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