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:
- PC - UM PC com Windows
- 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.
- 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.
- 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)
- No Unity Hub, selecione Novo projeto
- Selecione 3D
- Introduza o nome do seu Projeto e introduza uma Localização para guardar
- Selecione Criar projeto e aguarde que o Unity crie seu projeto
Alterar plataforma de compilação
- No editor da unidade, selecione Configurações de compilação de arquivos>
- 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
- Iniciar ferramenta de recurso de realidade mista
- Selecione o caminho do projeto - a pasta que contém pastas como Assets, Packages, ProjectSettings e assim por diante - e selecione Discover Features
- Em Serviços de Realidade Mista do Azure, selecione ambos
- Azure Spatial Anchors SDK Core
- SDK de Âncoras Espaciais do Azure para Windows
- Em Suporte à plataforma, selecione
- Plugin OpenXR de Realidade Mista
Nota
Certifique-se de ter atualizado o catálogo e a versão mais recente está selecionada para cada
- Pressione Obter recursos --Importar --Aprovar -->>>Sair
- Ao refocalizar sua janela Unity, Unity começará a importar os módulos
- 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
- Selecione Configurações de compilação de arquivo>(ele ainda pode estar aberto na etapa anterior)
- Selecione Configurações do Player...
- Selecione Gerenciamento de plug-in XR
- 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
- Selecione o sinal de aviso amarelo ao lado de OpenXR para exibir todos os problemas do OpenXR .
- Selecione Corrigir tudo
- 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 Hand
Alterar configurações de qualidade
- Selecione Editar>qualidade das configurações do projeto>
- 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
- Vá para Editar>Configurações>do Projeto Player (você ainda pode tê-lo aberto na etapa anterior).
- Verifique se a guia Configurações da Plataforma Universal do Windows está selecionada
- Na seção Configuração de Definições de Publicação, habilite o seguinte
- InternetClient
- InternetClientServer
- PrivateNetworkClientServer
- SpatialPerception (pode já estar ativado)
Configurar a câmara principal
- No Painel Hierarquia, selecione Câmara Principal.
- No Inspetor, defina sua posição de transformação como 0,0,0.
- Encontre a propriedade Clear Flags e altere a lista suspensa de Skybox para Solid Color.
- Selecione o campo Plano de fundo para abrir um seletor de cores.
- Defina R, G, B e A como 0.
- Selecione Adicionar componente na parte inferior e adicione o componente Driver de pose rastreada à câmera
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.
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.
Depois que o recurso é criado, o portal do Azure mostra que sua implantação foi concluída.
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.
Copie também o valor Domínio da Conta do recurso em um editor de texto para uso posterior.
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.
Criando & Adicionando scripts
- Em Unity, no painel Projeto, crie uma nova pasta chamada Scripts, na pasta Ativos .
- Na pasta, clique com o botão direito do mouse em -Create ->>C# Script. Título: AzureSpatialAnchorsScript
- Vá para GameObject ->Create Empty.
- Selecione-o e, no Inspetor , renomeie-o de GameObject para AzureSpatialAnchors.
- Ainda sobre o
GameObject
- Defina a sua posição para 0,0,0
- Selecione Adicionar Componente e procure e adicione o AzureSpatialAnchorsScript
- Selecione Adicionar componente novamente e procure e adicione o Gerenciador de âncora de RA. Isso adicionará automaticamente o AR Session Origin também.
- Selecione Adicionar componente novamente e procure e adicione o script SpatialAnchorManager
- 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.
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 GameObjects arquivos . 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.
- Abra
AzureSpatialAnchorsScript.cs
no Visual Studio clicando duas vezes no script no painel Projeto Unity. - 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 };
- 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()
{
}
- Adicione a seguinte importação
using UnityEngine.XR;
- 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 .sln
do 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 Watcher
arquivo . 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 GameObject
arquivo . 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 ShortTap
uma â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 AzureSpatialAnchorsScript
SpatialAnchorManager
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.