Tutorial: instrucciones paso a paso para crear una aplicación de HoloLens Unity mediante Azure Spatial Anchors
En este tutorial se muestra cómo crear una aplicación de HoloLens Unity con Azure Spatial Anchors.
Requisitos previos
Para completar este tutorial, asegúrese de disponer de los siguientes elementos:
- Una máquina Windows con Visual Studio 2017 o superior instalado, con la carga de trabajo Desarrollo de la plataforma universal de Windows y el componente Windows 10 SDK (10.0.18362.0 o versiones posteriores) y Git para Windows.
- La extensión de Visual Studio para C++/WinRT (VSIX) para Visual Studio instalada desde Visual Studio Marketplace.
- Un dispositivo HoloLens con el modo de desarrollador habilitado. Para este artículo se necesita un dispositivo HoloLens con la actualización de mayo de 2020 de Windows 10. Para actualizar a la versión más reciente en HoloLens, abra la aplicación Settings (Configuración), vaya a Update & Security (Actualización y seguridad) y, a continuación, seleccione el botón Check for updates (Buscar actualizaciones).
Introducción
Lo primero es configurar el proyecto y la escena de Unity:
- Inicie Unity.
- Seleccione Nuevo.
- Asegúrese de que la opción 3D está seleccionada.
- Asigne un nombre al proyecto y escriba una ubicación donde guardarlo.
- Seleccione Create project (Crear proyecto).
- Guarde la escena predeterminada vacía en un nuevo archivo con: File > Save As (Archivo > Guardar como).
- Asigne a la nueva escena el nombre Principal y presione el botón Save (Guardar).
Configuración del proyecto
A continuación, se van a establecer algunos valores de configuración del proyecto de Unity que nos ayuden a trabajar con el SDK de Windows Holographic para el desarrollo.
En primer lugar, veremos la configuración de calidad de nuestra aplicación.
- Seleccione Edit > Project Settings > Quality (Editar > Configuración del proyecto > Calidad).
- En la columna que aparece bajo el logotipo de Windows Store (Tienda Windows), haga clic en la flecha situada en la fila Default (Predeterminado) y seleccione Very Low (Muy baja). Sabrá que la configuración se aplica correctamente cuando el cuadro de la columna Windows Store (Tienda Windows) y la fila Very Low (Muy baja) estén en verde.
Hay que configurar la aplicación de Unity con una vista inmersiva, en lugar de una vista 2D. Para crear una vista inmersiva, se puede habilitar la compatibilidad con Virtual Reality en Unity y apuntar a Windows 10 SDK.
- Vaya a Edit > Project Settings > Player (Editar > Configuración del proyecto > Reproductor).
- En Inspector Panel (Panel de inspector) de Player Settings (Configuración del Reproductor), seleccione el icono de Windows.
- Expanda el grupo XR Settings (Configuración de XR).
- En la sección Rendering (Representación), active la casilla Virtual Reality Supported (Se admite Virtual Reality) para agregar una nueva lista de SDK de Virtual Reality.
- Compruebe que Windows Mixed Reality (Mixed Reality de Windows) aparece en la lista. En caso contrario, seleccione el botón + situado en la parte inferior de la lista y elija Windows Mixed Reality (Mixed Reality de Windows).
Nota
Si no ve el icono de Windows, verifique que ha seleccionado el back-end de scripting de .NET de Windows. Si no, es posible que deba volver a instalar Unity con la instalación correcta de Windows.
Verificar la configuración de back-end de script
- Vaya a Edit > Project Settings > Player (Editar > Configuración del proyecto > Reproductor) (puede que aún tenga abierto el Reproductor del paso anterior).
- En Inspector Panel (Panel de inspector) de Player Settings (Configuración del Reproductor), seleccione el icono de Windows Store (Tienda Windows).
- En la sección de configuración Other Settings (Otra configuración), asegúrese de que Scripting Backend (Back-end de scripting) esté establecido en IL2CPP.
Establecimiento de funcionalidades
- Vaya a Edit > Project Settings > Player (Editar > Configuración del proyecto > Reproductor) (puede que aún tenga abierto el Reproductor del paso anterior).
- En Inspector Panel (Panel de inspector) de Player Settings (Configuración del Reproductor), seleccione el icono de Windows Store (Tienda Windows).
- En la sección de configuración Publishing Settings (Configuración de publicación), active InternetClientServer y SpatialPerception.
Configuración de la cámara virtual principal
- En Hierarchy Panel (Panel de jerarquía), seleccione Main Camera (Cámara principal).
- En Inspector, establezca su posición de transformación en 0,0,0.
- Busque la propiedad Clear Flags (Borrar marcas) y cambie la lista desplegable de Skybox a Solid Color (Color sólido).
- Haga clic en el campo Background (Fondo) para abrir un selector de colores.
- Establezca R, G, B, and A (R, G, B y A) en 0.
- Seleccione Add Component (Agregar componente) y busque Spatial Mapping Collider.
Creación del script
- En el panel Project (Proyecto), cree una carpeta, Scripts, en la carpeta Assets (Activos).
- Haga clic con el botón derecho en la carpeta y seleccione Create (Crear) > , C# Script (Script de C#). Asígnele el nombre AzureSpatialAnchorsScript.
- Vaya a GameObject -> Create Empty (Crear vacío).
- Selecciónelo y, en Inspector, cambie su nombre de GameObject a MixedRealityCloud. Seleccione Add Component (Agregar componente) y agregue AzureSpatialAnchorsScript.
Creación del objeto prefabricado de esfera
- Vaya a GameObject -> 3D Object -> Sphere (GameObject > Objeto 3D > Esfera).
- En el Inspector, establezca su escala en 0,25, 0,25, 0,25.
- Buscar el objeto Sphere en el panel de jerarquía. Haga clic en él y arrástrelo a la carpeta Assets carpeta en el panel Project (Proyecto).
- Haga clic con el botón derecho y elija Eliminar la esfera original que ha creado en el panel Hierarchy (Jerarquía).
Ahora debería tener una esfera prefabricada en su panel Project (Proyecto).
Prueba
Para probar que todo funciona, compile la aplicación en Unity e impleméntela desde Visual Studio. Para ello, siga el capítulo 6 del curso MR Basics 100: Introducción a Unity. Verá la pantalla de inicio de Unity y, luego, una opción para borrar la pantalla.
Colocación de un objeto en el mundo real
Vamos a crear y colocar un objeto mediante la aplicación. Abra la solución de Visual Studio que se creó cuando se implementó nuestra aplicación.
En primer lugar, agregue las importaciones siguientes a Assembly-CSharp (Universal Windows)\Scripts\AzureSpatialAnchorsScript.cs:
```csharp
using Microsoft.Azure.SpatialAnchors;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
Luego, agregue las siguientes variables de miembro a la clase AzureSpatialAnchorsScript:
using UnityEngine.XR.WSA;
using UnityEngine.XR.WSA.Input;
public class AzureSpatialAnchorsScript : MonoBehaviour
{
/// <summary>
/// The sphere prefab.
/// </summary>
public GameObject spherePrefab;
/// <summary>
/// Set this string to the Spatial Anchors account id provided in the Spatial Anchors resource.
/// </summary>
protected string SpatialAnchorsAccountId = "Set me";
/// <summary>
/// Set this string to the Spatial Anchors account key provided in the Spatial Anchors resource.
/// </summary>
protected string SpatialAnchorsAccountKey = "Set me";
/// <summary>
/// Set this string to the Spatial Anchors account domain provided in the Spatial Anchors resource.
/// </summary>
private readonly Queue<Action> dispatchQueue = new Queue<Action>();
/// <summary>
/// Use the recognizer to detect air taps.
/// </summary>
protected CloudSpatialAnchor currentCloudAnchor;
/// <summary>
/// True if we are 1) creating + saving an anchor or 2) looking for an anchor.
/// </summary>
protected bool tapExecuted = false;
/// <summary>
/// The ID of the CloudSpatialAnchor that was saved. Use it to find the CloudSpatialAnchor
/// </summary>
protected string cloudSpatialAnchorId = "";
/// <summary>
/// The sphere rendered to show the position of the CloudSpatialAnchor.
/// </summary>
protected GameObject sphere;
protected Material sphereMaterial;
/// <summary>
Antes de continuar, necesitamos establecer la esfera prefabricada que creamos en nuestra variable miembro de spherePrefab. Vuelva a Unity.
- En Unity, seleccione el objeto MixedRealityCloud en el panel Hierarchy (Jerarquía).
- Haga clic en el objeto prefabricado Sphere que guardó en el panel de proyecto. Arrastre la esfera en la que hizo clic en el área del objeto prefabricado esfera bajo Azure Spatial Anchors Script (Script) en el panel Inspector.
Ahora debería tener el objeto Sphere establecido como prefabricado en el script. Compílelos en Unity y, luego, abra de nuevo la solución de Visual Studio resultante, como ha hecho en Prueba.
En Visual Studio, abra el archivo AzureSpatialAnchorsScript.cs. Agregue el código siguiente al método Start(). Este código enlazará GestureRecognizer, que llamará a HandleTap cuando detecte una pulsación en el aire.
/// </summary>
protected float recommendedForCreate = 0;
// Start is called before the first frame update
void Start()
{
recognizer = new GestureRecognizer();
recognizer.StartCapturingGestures();
recognizer.Tapped += HandleTap;
Ahora es necesario agregar el siguiente método HandleTap() debajo de Update(). Realizará un lanzamiento de rayos (ray casting) y obtendrá un punto de posicionamiento en el que colocar una esfera.
Debug.Log("ASA Info: Locate anchors completed. Watcher identifier: " + args.Watcher.Identifier);
}
/// <summary>
/// Called by GestureRecognizer when a tap is detected.
/// </summary>
/// <param name="tapEvent">The tap.</param>
public void HandleTap(TappedEventArgs tapEvent)
{
if (tapExecuted)
{
return;
}
// Clean up any anchors that have been placed.
CleanupObjects();
// Construct a Ray using forward direction of the HoloLens.
Ray GazeRay = new Ray(tapEvent.headPose.position, tapEvent.headPose.forward);
// Raycast to get the hit point in the real world.
RaycastHit hitInfo;
Physics.Raycast(GazeRay, out hitInfo, float.MaxValue);
Ahora debemos crear la esfera. La esfera inicialmente será blanca, pero este valor se ajustará más adelante. Agregue el siguiente método CreateAndSaveSphere():
this.CreateAndSaveSphere(hitInfo.point);
}
/// <summary>
/// Creates a sphere at the hit point, and then saves a CloudSpatialAnchor there.
/// </summary>
/// <param name="hitPoint">The hit point.</param>
protected virtual void CreateAndSaveSphere(Vector3 hitPoint)
{
// Create a white sphere.
sphere = GameObject.Instantiate(spherePrefab, hitPoint, Quaternion.identity) as GameObject;
sphere.AddComponent<WorldAnchor>();
Debug.LogError("ASA Error: " + ex.Message);
Ejecute la aplicación desde Visual Studio para validarla una vez más. Luego, toque la pantalla para crear la esfera blanca y colocarla sobre la superficie que haya elegido.
Configuración del patrón de distribuidor
Cuando se trabaja con Unity, todas las API de Unity (por ejemplo, las API que se usan para realizar actualizaciones de la interfaz de usuario) deben tener lugar en el subproceso principal. Sin embargo, en el código que vamos a escribir, obtendremos devoluciones de llamada en otros subprocesos. Queremos actualizar la interfaz de usuario en estas devoluciones de llamada, por lo que necesitamos una manera de pasar de un subproceso secundario al subproceso principal. Para ejecutar código en el subproceso principal desde un subproceso secundario, vamos a usar el patrón de distribuidor.
Utilizaremos la variable de miembro dispatchQueue, que consiste una cola de acciones. Insertaremos acciones en la cola y, luego, las quitaremos de la cola y ejecutaremos las acciones en el subproceso principal.
/// </summary>
protected string SpatialAnchorsAccountKey = "Set me";
/// <summary>
/// Set this string to the Spatial Anchors account domain provided in the Spatial Anchors resource.
/// </summary>
protected string SpatialAnchorsAccountDomain = "Set me";
/// <summary>
/// Our queue of actions that will be executed on the main thread.
/// </summary>
private readonly Queue<Action> dispatchQueue = new Queue<Action>();
/// <summary>
Luego, agregaremos una manera de incorporar una acción a la cola. Agregaremos QueueOnUpdate() justo después de Update():
}
}
/// <summary>
/// Queues the specified <see cref="Action"/> on update.
/// </summary>
/// <param name="updateAction">The update action.</param>
protected void QueueOnUpdate(Action updateAction)
{
lock (dispatchQueue)
{
Podemos usar el bucle Update() para comprobar si hay una acción en cola. En caso afirmativo, la quitaremos de la cola y la ejecutaremos.
InitializeSession();
}
// Update is called once per frame
void Update()
{
lock (dispatchQueue)
{
if (dispatchQueue.Count > 0)
{
dispatchQueue.Dequeue()();
Obtención del SDK de Azure Spatial Anchors
Descarga de paquetes
El siguiente paso es descargar los paquetes de Azure Spatial Anchors para Unity.
Importante
El SDK 2.7.0 de ASA es la versión mínima compatible. Si usa Unity 2020, la versión mínima compatible es el SDK 2.9.0 de ASA. Si usa el complemento OpenXR de Mixed Reality, la versión mínima compatible es el SDK 2.10.0 de ASA.
Para usar Azure Spatial Anchors en Unity, debe descargar el paquete principal y un paquete específico para cada plataforma (Android/iOS/HoloLens) que desee utilizar. Esto significa que descargará al menos dos de los siguientes paquetes:
| Plataforma | Nombre del paquete |
|---|---|
| Todas las plataformas | com.microsoft.azure.spatial-anchors-sdk.core@<version_number> |
| Android | com.microsoft.azure.spatial-anchors-sdk.android@<version_number> |
| iOS | com.microsoft.azure.spatial-anchors-sdk.ios@<version_number> |
| HoloLens | com.microsoft.azure.spatial-anchors-sdk.windows@<version_number> |
- Descarga con explorador web
- Descarga con NPM
- Instalación con la herramienta de características de Mixed Reality
Busque el paquete principal de Azure Spatial Anchors (com.microsoft.azure.spatial-anchors-sdk.core) para Unity aquí. Seleccione la versión que desee y descargue el paquete con el botón Download (Descargar). Repita el paso para descargar el paquete de cada plan que tenga previsto usar.
Importación de paquetes
Siga las instrucciones incluidas aquí para importar los paquetes de Azure Spatial Anchors que descargó en el proyecto de Unity mediante el administrador de paquetes de Unity.
Preparación del código
En la solución de Visual Studio, agregue la siguiente importación a <ProjectName>\Assets\Scripts\AzureSpatialAnchorsScript.cs:
Así es como debería ser el archivo de clase `AzureSpatialAnchorsScript` completo después de unir los distintos elementos. Puede usarlo como referencia para compararlo con su propio archivo, con el fin de detectar si es posible que queden diferencias.
```csharp
using Microsoft.Azure.SpatialAnchors;
Luego, agregue las siguientes variables de miembro a la clase AzureSpatialAnchorsScript:
/// </summary>
private readonly Queue<Action> dispatchQueue = new Queue<Action>();
/// <summary>
/// Use the recognizer to detect air taps.
/// </summary>
private GestureRecognizer recognizer;
protected CloudSpatialAnchorSession cloudSpatialAnchorSession;
/// <summary>
/// The CloudSpatialAnchor that we either 1) placed and are saving or 2) just located.
/// </summary>
protected CloudSpatialAnchor currentCloudAnchor;
/// <summary>
Asociación de una instancia local de Azure Spatial Anchors con un anclaje local
Vamos a configurar CloudSpatialAnchorSession de Azure Spatial Anchors. Para comenzar, agregaremos el siguiente método InitializeSession() dentro de su clase AzureSpatialAnchorsScript. Una vez que se le llama, garantiza que se crea una sesión de Azure Spatial Anchors y que se inicializa correctamente durante el inicio de la aplicación.
}
}
/// <summary>
/// Initializes a new CloudSpatialAnchorSession.
/// </summary>
void InitializeSession()
{
Debug.Log("ASA Info: Initializing a CloudSpatialAnchorSession.");
if (string.IsNullOrEmpty(SpatialAnchorsAccountId))
{
Debug.LogError("No account id set.");
return;
}
if (string.IsNullOrEmpty(SpatialAnchorsAccountKey))
{
Debug.LogError("No account key set.");
return;
}
cloudSpatialAnchorSession = new CloudSpatialAnchorSession();
cloudSpatialAnchorSession.Configuration.AccountId = SpatialAnchorsAccountId.Trim();
cloudSpatialAnchorSession.Configuration.AccountKey = SpatialAnchorsAccountKey.Trim();
cloudSpatialAnchorSession.Configuration.AccountDomain = SpatialAnchorsAccountDomain.Trim();
cloudSpatialAnchorSession.LogLevel = SessionLogLevel.All;
cloudSpatialAnchorSession.SessionUpdated += CloudSpatialAnchorSession_SessionUpdated;
cloudSpatialAnchorSession.AnchorLocated += CloudSpatialAnchorSession_AnchorLocated;
cloudSpatialAnchorSession.LocateAnchorsCompleted += CloudSpatialAnchorSession_LocateAnchorsCompleted;
cloudSpatialAnchorSession.Start();
Ahora es necesario escribir código para controlar las llamadas de delegado. Les agregaremos más código mientras continuamos.
Debug.Log("ASA Info: Session was initialized.");
}
private void CloudSpatialAnchorSession_Error(object sender, SessionErrorEventArgs args)
{
Debug.LogError("ASA Error: " + args.ErrorMessage );
}
private void CloudSpatialAnchorSession_OnLogDebug(object sender, OnLogDebugEventArgs args)
{
Debug.Log("ASA Log: " + args.Message);
System.Diagnostics.Debug.WriteLine("ASA Log: " + args.Message);
}
private void CloudSpatialAnchorSession_SessionUpdated(object sender, SessionUpdatedEventArgs args)
{
Ahora, vamos a enlazar el método initializeSession() a su método Start().
/// </summary>
protected float recommendedForCreate = 0;
// Start is called before the first frame update
void Start()
{
recognizer = new GestureRecognizer();
recognizer.StartCapturingGestures();
recognizer.SetRecognizableGestures(GestureSettings.Tap);
recognizer.Tapped += HandleTap;
Por último, agregue el código siguiente al método CreateAndSaveSphere(). Se asociará una instancia local de Azure Spatial Anchors a la esfera que vamos a colocar en el mundo real.
this.CreateAndSaveSphere(hitInfo.point);
}
/// <summary>
/// Creates a sphere at the hit point, and then saves a CloudSpatialAnchor there.
/// </summary>
/// <param name="hitPoint">The hit point.</param>
protected virtual void CreateAndSaveSphere(Vector3 hitPoint)
{
// Create a white sphere.
sphere = GameObject.Instantiate(spherePrefab, hitPoint, Quaternion.identity) as GameObject;
sphere.AddComponent<WorldAnchor>();
sphereMaterial = sphere.GetComponent<MeshRenderer>().material;
sphereMaterial.color = Color.white;
Debug.Log("ASA Info: Created a local anchor.");
// Create the CloudSpatialAnchor.
currentCloudAnchor = new CloudSpatialAnchor();
// Set the LocalAnchor property of the CloudSpatialAnchor to the WorldAnchor component of our white sphere.
WorldAnchor worldAnchor = sphere.GetComponent<WorldAnchor>();
if (worldAnchor == null)
{
throw new Exception("ASA Error: Couldn't get the local anchor pointer.");
}
Debug.LogError("ASA Error: " + ex.Message);
Antes de continuar, tiene que crear una cuenta de Azure Spatial Anchors para obtener el identificador, la clave y el dominio de la cuenta. Si aún no dispone de esos valores, vaya a la sección siguiente para obtenerlos.
Creación de un recurso de Spatial Anchors
Vaya a Azure Portal.
En el menú izquierdo, seleccione Crear un recurso.
Use el cuadro de búsqueda para buscar Spatial Anchors.

Seleccione Spatial Anchors y, después, seleccione Crear.
En el panel Cuenta de Spatial Anchors, haga lo siguiente:
Escriba un nombre de recurso único con caracteres alfanuméricos normales.
Seleccione la suscripción a la que desea asociar el recurso.
Cree un grupo de recursos, para lo que debe seleccionar Crear nuevo. Asígnele el nombre myResourceGroup y luego seleccione Aceptar.
Un grupo de recursos es un contenedor lógico en el que los recursos de Azure, como aplicaciones web, bases de datos y cuentas de almacenamiento, se implementen y administren. Por ejemplo, más adelante puede elegir eliminar todo el grupo de recursos en un solo paso.
Seleccione la ubicación (región) en la que desea colocar el recurso.
Seleccione Crear para empezar a crear el recurso.

Una vez creado el recurso, Azure Portal muestra que se ha completado la implementación.

Haga clic en Go to resource (Ir al recurso). Ahora puede ver las propiedades del recurso.
Copie el valor de Id. de cuenta del recurso en un editor de texto para usarlo más adelante.

Copie también el valor de Account Domain (Dominio de cuenta) del recurso en un editor de texto para usarlo más adelante.

En Configuración, seleccione Clave de acceso. Copie el valor de Clave principal, Clave de cuenta, en un editor de texto para su uso posterior.

Carga del anclaje local en la nube
Una vez que tenga el identificador, la clave y el dominio de la cuenta de Azure Spatial Anchors, vaya y pegue el valor de Account Id en SpatialAnchorsAccountId, el valor de Account Key en SpatialAnchorsAccountKey y el valor de Account Domain en SpatialAnchorsAccountDomain.
Por último, vamos a enlazar todos los elementos. En el método CreateAndSaveSphere(), agregue la siguiente línea. Esta línea invocará el método CreateAnchorAsync() en cuanto se cree la esfera. Una vez que el método devuelva resultados, el código siguiente actualizará la esfera una última vez y el color cambiará a azul.
this.CreateAndSaveSphere(hitInfo.point);
}
/// <summary>
/// Creates a sphere at the hit point, and then saves a CloudSpatialAnchor there.
/// </summary>
/// <param name="hitPoint">The hit point.</param>
protected virtual void CreateAndSaveSphere(Vector3 hitPoint)
{
// Create a white sphere.
sphere = GameObject.Instantiate(spherePrefab, hitPoint, Quaternion.identity) as GameObject;
sphere.AddComponent<WorldAnchor>();
sphereMaterial = sphere.GetComponent<MeshRenderer>().material;
sphereMaterial.color = Color.white;
Debug.Log("ASA Info: Created a local anchor.");
// Create the CloudSpatialAnchor.
currentCloudAnchor = new CloudSpatialAnchor();
// Set the LocalAnchor property of the CloudSpatialAnchor to the WorldAnchor component of our white sphere.
WorldAnchor worldAnchor = sphere.GetComponent<WorldAnchor>();
if (worldAnchor == null)
{
throw new Exception("ASA Error: Couldn't get the local anchor pointer.");
}
// Save the CloudSpatialAnchor to the cloud.
currentCloudAnchor.LocalAnchor = worldAnchor.GetNativeSpatialAnchorPtr();
Task.Run(async () =>
{
// Wait for enough data about the environment.
while (recommendedForCreate < 1.0F)
{
await Task.Delay(330);
}
bool success = false;
try
{
QueueOnUpdate(() =>
{
// We are about to save the CloudSpatialAnchor to the Azure Spatial Anchors, turn it yellow.
sphereMaterial.color = Color.yellow;
});
await cloudSpatialAnchorSession.CreateAnchorAsync(currentCloudAnchor);
success = currentCloudAnchor != null;
if (success)
{
// Allow the user to tap again to clear state and look for the anchor.
tapExecuted = false;
// Record the identifier to locate.
cloudSpatialAnchorId = currentCloudAnchor.Identifier;
QueueOnUpdate(() =>
{
// Turn the sphere blue.
sphereMaterial.color = Color.blue;
});
Debug.Log("ASA Info: Saved anchor to Azure Spatial Anchors! Identifier: " + cloudSpatialAnchorId);
}
else
{
sphereMaterial.color = Color.red;
Debug.LogError("ASA Error: Failed to save, but no exception was thrown.");
}
}
catch (Exception ex)
{
QueueOnUpdate(() =>
{
sphereMaterial.color = Color.red;
});
Debug.LogError("ASA Error: " + ex.Message);
}
Ejecute la aplicación desde Visual Studio una vez más. Mueva la cabeza y, luego, pulse en el aire para colocar la esfera. Una vez que tengamos suficientes fotogramas, la esfera se pondrá amarilla y se iniciará la carga en la nube. Cuando finalice la carga, la esfera pasará a ser azul. También puede usar la ventana de salida al realizar la depuración dentro de Visual Studio para supervisar los mensajes de registro que envía la aplicación. Asegúrese de implementar la configuración Debug de la aplicación desde Visual Studio para ver los mensajes de registro. Puede observar RecommendedForCreateProgress y, una vez completada la carga, verá el identificador de delimitador que ha devuelto la nube.
Nota
Si aparece la excepción "DllNotFoundException: No se puede cargar el archivo DLL "AzureSpatialAnchors": No se ha podido encontrar el módulo especificado", debe seleccionar Clean (Limpiar) y Build (Compilar) para limpiar y volver a compilar la solución.
Búsqueda del anclaje espacial de la nube
Una vez que el anclaje se carga en la nube, ya es posible volver a intentar localizarlo. Vamos a agregar el siguiente código al método HandleTap(). Este código hará lo siguiente:
- Llame a
ResetSession(), que detendráCloudSpatialAnchorSessiony quitará la esfera azul existente de la pantalla. - Inicialice de nuevo
CloudSpatialAnchorSession. Esta acción garantiza que el anclaje que vamos a buscar proviene de la nube, y no es el anclaje local que hemos creado. - Cree un elemento Watcher (Monitor) que buscará el anclaje que hemos cargado en Azure Spatial Anchors.
Debug.Log("ASA Info: Locate anchors completed. Watcher identifier: " + args.Watcher.Identifier);
}
/// <summary>
/// Called by GestureRecognizer when a tap is detected.
/// </summary>
/// <param name="tapEvent">The tap.</param>
public void HandleTap(TappedEventArgs tapEvent)
{
if (tapExecuted)
{
return;
}
tapExecuted = true;
// We have saved an anchor, so we will now look for it.
if (!String.IsNullOrEmpty(cloudSpatialAnchorId))
{
Debug.Log("ASA Info: We will look for a placed anchor.");
ResetSession(() =>
{
InitializeSession();
// Create a Watcher to look for the anchor we created.
AnchorLocateCriteria criteria = new AnchorLocateCriteria();
criteria.Identifiers = new string[] { cloudSpatialAnchorId };
cloudSpatialAnchorSession.CreateWatcher(criteria);
Debug.Log("ASA Info: Watcher created. Number of active watchers: " + cloudSpatialAnchorSession.GetActiveWatchers().Count);
});
return;
}
Debug.Log("ASA Info: We will create a new anchor.");
// Clean up any anchors that have been placed.
CleanupObjects();
Ahora agreguemos nuestros métodos ResetSession() y CleanupObjects(). Puede colocarlos debajo de QueueOnUpdate().
}
}
/// <summary>
/// Cleans up objects.
/// </summary>
public void CleanupObjects()
{
if (sphere != null)
{
Destroy(sphere);
sphere = null;
}
if (sphereMaterial != null)
{
Destroy(sphereMaterial);
sphereMaterial = null;
}
currentCloudAnchor = null;
}
/// <summary>
/// Cleans up objects and stops the CloudSpatialAnchorSessions.
/// </summary>
public void ResetSession(Action completionRoutine = null)
{
Debug.Log("ASA Info: Resetting the session.");
if (cloudSpatialAnchorSession.GetActiveWatchers().Count > 0)
{
Debug.LogError("ASA Error: We are resetting the session with active watchers, which is unexpected.");
}
CleanupObjects();
this.cloudSpatialAnchorSession.Reset();
lock (this.dispatchQueue)
{
this.dispatchQueue.Enqueue(() =>
{
if (cloudSpatialAnchorSession != null)
{
cloudSpatialAnchorSession.Stop();
cloudSpatialAnchorSession.Dispose();
Debug.Log("ASA Info: Session was reset.");
completionRoutine?.Invoke();
}
else
{
Debug.LogError("ASA Error: cloudSpatialAnchorSession was null, which is unexpected.");
}
A continuación, debemos enlazar el código que se invocará cuando se encuentre el anclaje que estamos consultando. En InitializeSession(), agregue las siguientes devoluciones de llamada:
cloudSpatialAnchorSession.LogLevel = SessionLogLevel.All;
cloudSpatialAnchorSession.Error += CloudSpatialAnchorSession_Error;
cloudSpatialAnchorSession.OnLogDebug += CloudSpatialAnchorSession_OnLogDebug;
cloudSpatialAnchorSession.SessionUpdated += CloudSpatialAnchorSession_SessionUpdated;
cloudSpatialAnchorSession.AnchorLocated += CloudSpatialAnchorSession_AnchorLocated;
Ahora vamos a agregar código con el que se creará y colocará una esfera verde una vez que se encuentre CloudSpatialAnchor. También volverá a habilitar la opción de tocar la pantalla, con el fin de que pueda repetir el escenario completo una vez más: crear otro anclaje local, cargarlo y volver a buscarlo.
recommendedForCreate = args.Status.RecommendedForCreateProgress;
}
private void CloudSpatialAnchorSession_AnchorLocated(object sender, AnchorLocatedEventArgs args)
{
switch (args.Status)
{
case LocateAnchorStatus.Located:
Debug.Log("ASA Info: Anchor located! Identifier: " + args.Identifier);
QueueOnUpdate(() =>
{
// Create a green sphere.
sphere = GameObject.Instantiate(spherePrefab, Vector3.zero, Quaternion.identity) as GameObject;
sphere.AddComponent<WorldAnchor>();
sphereMaterial = sphere.GetComponent<MeshRenderer>().material;
sphereMaterial.color = Color.green;
// Get the WorldAnchor from the CloudSpatialAnchor and use it to position the sphere.
sphere.GetComponent<UnityEngine.XR.WSA.WorldAnchor>().SetNativeSpatialAnchorPtr(args.Anchor.LocalAnchor);
// Clean up state so that we can start over and create a new anchor.
cloudSpatialAnchorId = "";
tapExecuted = false;
});
break;
case LocateAnchorStatus.AlreadyTracked:
Debug.Log("ASA Info: Anchor already tracked. Identifier: " + args.Identifier);
break;
case LocateAnchorStatus.NotLocated:
Debug.Log("ASA Info: Anchor not located. Identifier: " + args.Identifier);
break;
case LocateAnchorStatus.NotLocatedAnchorDoesNotExist:
Debug.LogError("ASA Error: Anchor not located does not exist. Identifier: " + args.Identifier);
break;
}
}
private void CloudSpatialAnchorSession_LocateAnchorsCompleted(object sender, LocateAnchorsCompletedEventArgs args)
Eso es todo. Ejecute la aplicación desde Visual Studio una última vez para probar el escenario completo de extremo a extremo. Mueva el dispositivo y coloque la esfera negra. Luego, siga moviendo la cabeza para capturar los datos del entorno hasta que la esfera se vuelva amarilla. Se cargará el anclaje local y la esfera volverá a ser azul. Por último, toque la pantalla una vez más para quitar el delimitador local e iniciar una consulta para su equivalente en la nube. Siga desplazando el dispositivo hasta que se encuentre el anclaje espacial en la nube. Debe mostrarse una esfera verde en la ubicación correcta, y todo el escenario se puede volver a repetir.
Todo junto
Así es como debería ser el archivo de clase AzureSpatialAnchorsScript completo después de unir los distintos elementos. Puede usarlo como referencia para compararlo con su propio archivo, con el fin de detectar si es posible que queden diferencias.
using Microsoft.Azure.SpatialAnchors;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.XR.WSA;
using UnityEngine.XR.WSA.Input;
public class AzureSpatialAnchorsScript : MonoBehaviour
{
/// <summary>
/// The sphere prefab.
/// </summary>
public GameObject spherePrefab;
/// <summary>
/// Set this string to the Spatial Anchors account id provided in the Spatial Anchors resource.
/// </summary>
protected string SpatialAnchorsAccountId = "Set me";
/// <summary>
/// Set this string to the Spatial Anchors account key provided in the Spatial Anchors resource.
/// </summary>
protected string SpatialAnchorsAccountKey = "Set me";
/// <summary>
/// Set this string to the Spatial Anchors account domain provided in the Spatial Anchors resource.
/// </summary>
protected string SpatialAnchorsAccountDomain = "Set me";
/// <summary>
/// Our queue of actions that will be executed on the main thread.
/// </summary>
private readonly Queue<Action> dispatchQueue = new Queue<Action>();
/// <summary>
/// Use the recognizer to detect air taps.
/// </summary>
private GestureRecognizer recognizer;
protected CloudSpatialAnchorSession cloudSpatialAnchorSession;
/// <summary>
/// The CloudSpatialAnchor that we either 1) placed and are saving or 2) just located.
/// </summary>
protected CloudSpatialAnchor currentCloudAnchor;
/// <summary>
/// True if we are 1) creating + saving an anchor or 2) looking for an anchor.
/// </summary>
protected bool tapExecuted = false;
/// <summary>
/// The ID of the CloudSpatialAnchor that was saved. Use it to find the CloudSpatialAnchor
/// </summary>
protected string cloudSpatialAnchorId = "";
/// <summary>
/// The sphere rendered to show the position of the CloudSpatialAnchor.
/// </summary>
protected GameObject sphere;
protected Material sphereMaterial;
/// <summary>
/// Indicate if we are ready to save an anchor. We can save an anchor when value is greater than 1.
/// </summary>
protected float recommendedForCreate = 0;
// Start is called before the first frame update
void Start()
{
recognizer = new GestureRecognizer();
recognizer.StartCapturingGestures();
recognizer.SetRecognizableGestures(GestureSettings.Tap);
recognizer.Tapped += HandleTap;
InitializeSession();
}
// Update is called once per frame
void Update()
{
lock (dispatchQueue)
{
if (dispatchQueue.Count > 0)
{
dispatchQueue.Dequeue()();
}
}
}
/// <summary>
/// Queues the specified <see cref="Action"/> on update.
/// </summary>
/// <param name="updateAction">The update action.</param>
protected void QueueOnUpdate(Action updateAction)
{
lock (dispatchQueue)
{
dispatchQueue.Enqueue(updateAction);
}
}
/// <summary>
/// Cleans up objects.
/// </summary>
public void CleanupObjects()
{
if (sphere != null)
{
Destroy(sphere);
sphere = null;
}
if (sphereMaterial != null)
{
Destroy(sphereMaterial);
sphereMaterial = null;
}
currentCloudAnchor = null;
}
/// <summary>
/// Cleans up objects and stops the CloudSpatialAnchorSessions.
/// </summary>
public void ResetSession(Action completionRoutine = null)
{
Debug.Log("ASA Info: Resetting the session.");
if (cloudSpatialAnchorSession.GetActiveWatchers().Count > 0)
{
Debug.LogError("ASA Error: We are resetting the session with active watchers, which is unexpected.");
}
CleanupObjects();
this.cloudSpatialAnchorSession.Reset();
lock (this.dispatchQueue)
{
this.dispatchQueue.Enqueue(() =>
{
if (cloudSpatialAnchorSession != null)
{
cloudSpatialAnchorSession.Stop();
cloudSpatialAnchorSession.Dispose();
Debug.Log("ASA Info: Session was reset.");
completionRoutine?.Invoke();
}
else
{
Debug.LogError("ASA Error: cloudSpatialAnchorSession was null, which is unexpected.");
}
});
}
}
/// <summary>
/// Initializes a new CloudSpatialAnchorSession.
/// </summary>
void InitializeSession()
{
Debug.Log("ASA Info: Initializing a CloudSpatialAnchorSession.");
if (string.IsNullOrEmpty(SpatialAnchorsAccountId))
{
Debug.LogError("No account id set.");
return;
}
if (string.IsNullOrEmpty(SpatialAnchorsAccountKey))
{
Debug.LogError("No account key set.");
return;
}
cloudSpatialAnchorSession = new CloudSpatialAnchorSession();
cloudSpatialAnchorSession.Configuration.AccountId = SpatialAnchorsAccountId.Trim();
cloudSpatialAnchorSession.Configuration.AccountKey = SpatialAnchorsAccountKey.Trim();
cloudSpatialAnchorSession.Configuration.AccountDomain = SpatialAnchorsAccountDomain.Trim();
cloudSpatialAnchorSession.LogLevel = SessionLogLevel.All;
cloudSpatialAnchorSession.Error += CloudSpatialAnchorSession_Error;
cloudSpatialAnchorSession.OnLogDebug += CloudSpatialAnchorSession_OnLogDebug;
cloudSpatialAnchorSession.SessionUpdated += CloudSpatialAnchorSession_SessionUpdated;
cloudSpatialAnchorSession.AnchorLocated += CloudSpatialAnchorSession_AnchorLocated;
cloudSpatialAnchorSession.LocateAnchorsCompleted += CloudSpatialAnchorSession_LocateAnchorsCompleted;
cloudSpatialAnchorSession.Start();
Debug.Log("ASA Info: Session was initialized.");
}
private void CloudSpatialAnchorSession_Error(object sender, SessionErrorEventArgs args)
{
Debug.LogError("ASA Error: " + args.ErrorMessage );
}
private void CloudSpatialAnchorSession_OnLogDebug(object sender, OnLogDebugEventArgs args)
{
Debug.Log("ASA Log: " + args.Message);
System.Diagnostics.Debug.WriteLine("ASA Log: " + args.Message);
}
private void CloudSpatialAnchorSession_SessionUpdated(object sender, SessionUpdatedEventArgs args)
{
Debug.Log("ASA Log: recommendedForCreate: " + args.Status.RecommendedForCreateProgress);
recommendedForCreate = args.Status.RecommendedForCreateProgress;
}
private void CloudSpatialAnchorSession_AnchorLocated(object sender, AnchorLocatedEventArgs args)
{
switch (args.Status)
{
case LocateAnchorStatus.Located:
Debug.Log("ASA Info: Anchor located! Identifier: " + args.Identifier);
QueueOnUpdate(() =>
{
// Create a green sphere.
sphere = GameObject.Instantiate(spherePrefab, Vector3.zero, Quaternion.identity) as GameObject;
sphere.AddComponent<WorldAnchor>();
sphereMaterial = sphere.GetComponent<MeshRenderer>().material;
sphereMaterial.color = Color.green;
// Get the WorldAnchor from the CloudSpatialAnchor and use it to position the sphere.
sphere.GetComponent<UnityEngine.XR.WSA.WorldAnchor>().SetNativeSpatialAnchorPtr(args.Anchor.LocalAnchor);
// Clean up state so that we can start over and create a new anchor.
cloudSpatialAnchorId = "";
tapExecuted = false;
});
break;
case LocateAnchorStatus.AlreadyTracked:
Debug.Log("ASA Info: Anchor already tracked. Identifier: " + args.Identifier);
break;
case LocateAnchorStatus.NotLocated:
Debug.Log("ASA Info: Anchor not located. Identifier: " + args.Identifier);
break;
case LocateAnchorStatus.NotLocatedAnchorDoesNotExist:
Debug.LogError("ASA Error: Anchor not located does not exist. Identifier: " + args.Identifier);
break;
}
}
private void CloudSpatialAnchorSession_LocateAnchorsCompleted(object sender, LocateAnchorsCompletedEventArgs args)
{
Debug.Log("ASA Info: Locate anchors completed. Watcher identifier: " + args.Watcher.Identifier);
}
/// <summary>
/// Called by GestureRecognizer when a tap is detected.
/// </summary>
/// <param name="tapEvent">The tap.</param>
public void HandleTap(TappedEventArgs tapEvent)
{
if (tapExecuted)
{
return;
}
tapExecuted = true;
// We have saved an anchor, so we will now look for it.
if (!String.IsNullOrEmpty(cloudSpatialAnchorId))
{
Debug.Log("ASA Info: We will look for a placed anchor.");
ResetSession(() =>
{
InitializeSession();
// Create a Watcher to look for the anchor we created.
AnchorLocateCriteria criteria = new AnchorLocateCriteria();
criteria.Identifiers = new string[] { cloudSpatialAnchorId };
cloudSpatialAnchorSession.CreateWatcher(criteria);
Debug.Log("ASA Info: Watcher created. Number of active watchers: " + cloudSpatialAnchorSession.GetActiveWatchers().Count);
});
return;
}
Debug.Log("ASA Info: We will create a new anchor.");
// Clean up any anchors that have been placed.
CleanupObjects();
// Construct a Ray using forward direction of the HoloLens.
Ray GazeRay = new Ray(tapEvent.headPose.position, tapEvent.headPose.forward);
// Raycast to get the hit point in the real world.
RaycastHit hitInfo;
Physics.Raycast(GazeRay, out hitInfo, float.MaxValue);
this.CreateAndSaveSphere(hitInfo.point);
}
/// <summary>
/// Creates a sphere at the hit point, and then saves a CloudSpatialAnchor there.
/// </summary>
/// <param name="hitPoint">The hit point.</param>
protected virtual void CreateAndSaveSphere(Vector3 hitPoint)
{
// Create a white sphere.
sphere = GameObject.Instantiate(spherePrefab, hitPoint, Quaternion.identity) as GameObject;
sphere.AddComponent<WorldAnchor>();
sphereMaterial = sphere.GetComponent<MeshRenderer>().material;
sphereMaterial.color = Color.white;
Debug.Log("ASA Info: Created a local anchor.");
// Create the CloudSpatialAnchor.
currentCloudAnchor = new CloudSpatialAnchor();
// Set the LocalAnchor property of the CloudSpatialAnchor to the WorldAnchor component of our white sphere.
WorldAnchor worldAnchor = sphere.GetComponent<WorldAnchor>();
if (worldAnchor == null)
{
throw new Exception("ASA Error: Couldn't get the local anchor pointer.");
}
// Save the CloudSpatialAnchor to the cloud.
currentCloudAnchor.LocalAnchor = worldAnchor.GetNativeSpatialAnchorPtr();
Task.Run(async () =>
{
// Wait for enough data about the environment.
while (recommendedForCreate < 1.0F)
{
await Task.Delay(330);
}
bool success = false;
try
{
QueueOnUpdate(() =>
{
// We are about to save the CloudSpatialAnchor to the Azure Spatial Anchors, turn it yellow.
sphereMaterial.color = Color.yellow;
});
await cloudSpatialAnchorSession.CreateAnchorAsync(currentCloudAnchor);
success = currentCloudAnchor != null;
if (success)
{
// Allow the user to tap again to clear state and look for the anchor.
tapExecuted = false;
// Record the identifier to locate.
cloudSpatialAnchorId = currentCloudAnchor.Identifier;
QueueOnUpdate(() =>
{
// Turn the sphere blue.
sphereMaterial.color = Color.blue;
});
Debug.Log("ASA Info: Saved anchor to Azure Spatial Anchors! Identifier: " + cloudSpatialAnchorId);
}
else
{
sphereMaterial.color = Color.red;
Debug.LogError("ASA Error: Failed to save, but no exception was thrown.");
}
}
catch (Exception ex)
{
QueueOnUpdate(() =>
{
sphereMaterial.color = Color.red;
});
Debug.LogError("ASA Error: " + ex.Message);
}
});
}
}
Pasos siguientes
En este tutorial, aprenderá más información sobre cómo usar Azure Spatial Anchors en una nueva aplicación de Unity HoloLens. Para más información acerca de cómo usar Azure Spatial Anchors en una nueva aplicación Android, continúe con el siguiente tutorial.
