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:
- 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). A VSIX (Extensão do Visual Studio) C++/WinRT para o Visual Studio deve ser instalada do Visual Studio Marketplace.
- 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.
- 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
- No Hub do Unity, selecione Novo projeto
- Selecione 3D
- Insira o nome do projeto e insira um local para salvar
- Selecione Criar projeto e aguarde até que o Unity crie seu projeto
Alterar a plataforma de compilação
- No editor do Unity, selecione Configurações de >compilação de arquivo
- 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
- Iniciar a Ferramenta de Recurso de Realidade Misturada
- Selecione o caminho do projeto – a pasta que contém pastas como Ativos, Pacotes, ProjectSettings e assim por diante – e selecione Descobrir Recursos
- Em Serviços de Realidade Misturada do Azure, selecione ambos
- Núcleo do SDK das Âncoras Espaciais do Azure
- SDK das Âncoras Espaciais do Azure para Windows
- Em Suporte à Plataforma, selecione
- 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
- Pressione Obter Recursos – >Importar –>Aprovar – >Sair
- Ao refocar a janela do Unity, o 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 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
- 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 do XR
- 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
- Selecione o sinal de aviso amarelo ao lado de OpenXR para ver 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 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 Microsoft
Alterar Configurações de Qualidade
- Selecione Editar>Configurações do Projeto>Qualidade
- 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
- Acesse Editar>Configurações do Projeto>Player (você ainda poderá tê-lo aberto da etapa anterior).
- Certifique-se de que a guia Configurações da Plataforma Universal do Windows esteja selecionada
- Na seção Definições de Configurações de Publicação, habilita o seguinte
- InternetClient
- InternetClientServer
- PrivateNetworkClientServer
- SpatialPerception (pode já estar habilitado)
Configurar a câmera principal
- No Painel de Hierarquia, selecione Câmera Principal.
- No Inspetor, defina sua posição de transformação para 0,0,0.
- Localize a propriedade Limpar Sinalizadores e altere a lista suspensa de Skybox para Cor Sólida.
- Selecione o campo Tela de fundo para abrir o seletor de cor.
- Defina R, G, B e A para 0.
- Selecione Adicionar Componente na parte inferior e adicione o componente de Driver de Pose Rastreada à câmera
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.
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.
Depois que o recurso for criado, o portal do Azure mostrará que a implantação foi concluída.
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.
Copie também o valor do Domínio de Contas do recurso em um editor de texto para usar posteriormente.
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.
Criar e adicionar scripts
- No Unity no painel Projeto, crie uma pasta chamada Scripts na pasta Ativos.
- Na pasta, clique com o botão direito do mouse ->Criar ->Script C#. Dê a ela o título AzureSpatialAnchorsScript
- Vá para GameObject ->Criar Vazio.
- Selecione-o e, em Inspetor, renomeie-o de GameObject para AzureSpatialAnchors.
- Ainda no
GameObject
- Definir sua posição como 0,0,0
- Selecione Adicionar Componente e pesquise por AzureSpatialAnchorsScript e adicione-o
- 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.
- Selecione Adicionar Componente novamente e pesquise por SpatialAnchorManager e adicione o script
- 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.
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.
- Abra
AzureSpatialAnchorsScript.cs
no Visual Studio clicando duas vezes no script no painel do Projeto Unity. - 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 };
- 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()
{
}
- Adicione a importação a seguir
using UnityEngine.XR;
- 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 e 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 IsReadyForCreate
verdadeiro 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 e 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.
SpatialAnchorManager
pode 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.