Tutorial: Instruksi langkah demi langkah untuk membuat aplikasi HoloLens Unity baru menggunakan Azure Spatial Anchors
Tutorial ini akan menunjukkan cara membuat aplikasi HoloLens Unity baru dengan Azure Spatial Anchors.
Prasyarat
Untuk menyelesaikan tutorial ini, pastikan Anda memiliki:
- PC - PC yang menjalankan Windows
- Visual Studio - Visual Studio 2019 diinstal dengan beban kerja pengembangan Platform Windows Universal dan komponen Windows 10 SDK (10.0.18362.0 atau yang lebih baru). C++/WinRT Visual Studio Extension (VSIX) untuk Visual Studio harus diinstal dari Visual Studio Marketplace.
- HoloLens - Perangkat HoloLens dengan mode pengembang diaktifkan. Artikel ini memerlukan perangkat HoloLens dengan Pembaruan Windows 10 Mei 2020. Untuk memperbarui ke rilis terbaru HoloLens, buka aplikasi Pengaturan, buka Pembaruan & Keamanan, lalu pilih tombol Periksa pembaruan.
- Unity - Unity 2020.3.25 dengan modul Platform Windows Universal Dukungan Build dan Dukungan Build Windows (IL2CPP)
Membuat dan menyiapkan Proyek Unity
Buat Proyek Baru
- Di Unity Hub, pilih Proyek baru
- Pilih 3D
- Masukkan Nama proyek Anda dan masukkan Lokasi penyimpanan
- Pilih Buat proyek dan tunggu Unity membuat proyek Anda
Ubah Platform Build
- Di editor unity Anda, pilih Pengaturan Build File>
- Pilih Platform Windows Universal lalu Beralih Platform. Tunggu hingga Unity selesai memproses semua file.
Mengimpor ASA dan OpenXR
- Luncurkan Alat Fitur Mixed Reality
- Pilih jalur proyek Anda - folder yang berisi folder seperti Aset, Paket, ProjectSettings, dan sebagainya - dan pilih Temukan Fitur
- Di bawah Azure Mixed Reality Services, pilih keduanya
- Azure Spatial Anchors SDK Core
- Azure Spatial Anchors SDK untuk Windows
- Di bawah Dukungan Platform, pilih
- Mixed Reality OpenXR Plugin
Catatan
Pastikan Anda telah menyegarkan katalog dan versi terbaru dipilih untuk masing-masing katalog
- Tekan Dapatkan Fitur -->Impor -->Setujui -->Exit
- Saat memfokuskan ulang jendela Unity Anda, Unity akan mulai mengimpor modul
- Jika Anda mendapatkan pesan tentang menggunakan sistem input baru, pilih Ya untuk memulai ulang Unity dan mengaktifkan backend.
Menyiapkan pengaturan proyek
Kami sekarang akan menetapkan beberapa pengaturan proyek Unity yang membantu kami menargetkan SDK Windows Holographic untuk pengembangan.
Ubah Pengaturan OpenXR
- Pilih Pengaturan Build File>(mungkin masih terbuka dari langkah sebelumnya)
- Pilih Pengaturan Pemutar...
- Pilih Manajemen Plug-in XR
- Pastikan tab Pengaturan Platform Windows Universal dipilih dan centang kotak di samping OpenXR dan di samping grup fitur Microsoft HoloLens
- Pilih tanda peringatan kuning di samping OpenXR untuk menampilkan semua masalah OpenXR.
- Pilih Perbaiki semua
- Untuk memperbaiki masalah "Setidaknya satu profil interaksi harus ditambahkan", pilih Edit untuk membuka pengaturan Proyek OpenXR. Kemudian di bawah Profil Interaksi pilih + simbol dan pilih Profil Interaksi Tangan Microsoft
Ubah Pengaturan Kualitas
- Pilih Edit>Pengaturan Proyek>Kualitas
- Di kolom di bawah logo Platform Windows Universal, pilih panah di baris Default dan pilih Sangat Rendah. Anda akan tahu pengaturan diterapkan dengan benar ketika kotak di kolom Universal Windows Platform dan baris Sangat Rendah berwarna hijau.
Mengatur kemampuan
- Buka Edit>Pemutar Pengaturan>Proyek (Anda mungkin masih membukanya dari langkah sebelumnya).
- Pastikan tab Pengaturan Platform Windows Universal dipilih
- Di bagian Konfigurasi Pengaturan Penerbitan, aktifkan yang berikut ini
- InternetClient
- InternetClientServer
- PrivateNetworkClientServer
- SpatialPerception (mungkin sudah diaktifkan)
Menyiapkan kamera utama
- Di Panel Hierarki, pilih Kamera Utama.
- Dalam Pemeriksa, atur posisi transformasinya menjadi 0,0,0.
- Temukan properti Bersihkan Bendera, dan ubah menu dropdown dari Skybox menjadi Warna Solid.
- Pilih bidang Latar Belakang untuk membuka pemilih warna.
- Atur R, G, B, dan A ke 0.
- Pilih Tambahkan Komponen di bagian bawah dan tambahkan Komponen Driver Pose Terlacak ke kamera
Cobalah #1
Anda sekarang harus memiliki adegan kosong yang siap untuk disebarkan ke perangkat HoloLens Anda. Untuk menguji bahwa semuanya berfungsi, buat aplikasi Anda di Unity dan terapkan dari Visual Studio. Ikuti Menggunakan Visual Studio untuk menyebarkan dan men-debug untuk melakukannya. Anda akan melihat layar mulai Unity, lalu tampilan yang jelas.
Buat sumber daya Spatial Anchors
Buka portal Microsoft Azure.
Di panel kiri, pilih Buat sumber daya.
Gunakan kotak pencarian untuk mencari Spatial Anchors.
Pilih Spatial Anchors, lalu pilih Buat.
Pada panel Akun Spatial Anchors, lakukan hal berikut ini:
Masukkan nama sumber daya unik dengan menggunakan karakter alfanumerik biasa.
Pilih langganan yang ingin Anda lampirkan sumber dayanya.
Buat grup sumber daya dengan memilih Buat baru. Beri nama myResourceGroup, lalu pilih OK.
Grup sumber daya Azure adalah kontainer logis tempat sumber daya Azure seperti aplikasi web, database, dan akun penyimpanan disebarkan dan dikelola. Misalnya, Anda dapat memilih untuk menghapus seluruh grup sumber daya dalam satu langkah sederhana nanti.
Pilih lokasi (kawasan) tempat menempatkan sumber daya.
Pilih Buat untuk memulai pembuatan sumber daya.
Setelah sumber daya dibuat, portal Azure menunjukkan bahwa penyebaran Anda selesai.
Pilih Buka sumber daya. Anda sekarang dapat melihat properti sumber daya.
Salin nilai ID Akun sumber daya ke editor teks untuk digunakan nanti.
Salin nilai Domain Akun sumber daya juga ke editor teks untuk digunakan nanti.
Di Pengaturan, pilih Kunci akses. Salin nilai Kunci utama, Kunci Akun, ke editor teks untuk digunakan nanti.
Membuat & Menambahkan Skrip
- Di Unity di panel Proyek, buat folder baru bernama Skrip, di folder Aset.
- Di folder klik kanan ->Create ->C# Script. Beri judul AzureSpatialAnchorsScript
- Buka GameObject ->Buat Kosong.
- Pilih, dan di Inspektur mengganti namanya dari GameObject ke AzureSpatialAnchors.
- Masih di
GameObject
- Atur posisinya ke 0,0,0
- Pilih Tambahkan Komponen dan cari dan tambahkan AzureSpatialAnchorsScript
- Pilih Tambahkan Komponen lagi dan cari dan tambahkan AR Anchor Manager. Ini akan secara otomatis menambahkan Asal Sesi AR juga.
- Pilih Tambahkan Komponen lagi dan cari dan tambahkan skrip SpatialAnchorManager
- Dalam komponen SpatialAnchorManager yang ditambahkan, isi ID Akun, Kunci Akun, dan Domain Akun yang telah Anda salin di langkah sebelumnya dari sumber daya jangkar spasial di portal Azure.
Gambaran Umum Aplikasi
Aplikasi kami akan mendukung interaksi berikut:
Gerakan | Perbuatan |
---|---|
Ketuk di mana saja | Mulai/Lanjutkan Sesi + Buat jangkar di Posisi Tangan |
Mengetuk jangkar | Hapus GameObject + Hapus Jangkar di ASA Cloud Service |
Ketuk + Tahan selama 2 detik (+ sesi sedang berjalan) | Hentikan sesi dan hapus semua GameObjects . Simpan jangkar di ASA Cloud Service |
Ketuk + Tahan selama 2 detik (+ sesi tidak berjalan) | Mulai sesi dan cari semua jangkar. |
Tambahkan Pengenalan Ketuk
Mari kita tambahkan beberapa kode ke skrip kita untuk dapat mengenali gerakan mengetuk pengguna.
- Buka
AzureSpatialAnchorsScript.cs
di Visual Studio dengan mengklik dua kali pada skrip di panel Proyek Unity Anda. - Tambahkan array berikut ke kelas Anda
public class AzureSpatialAnchorsScript : MonoBehaviour
{
/// <summary>
/// Used to distinguish short taps and long taps
/// </summary>
private float[] _tappingTimer = { 0, 0 };
- Tambahkan dua metode berikut di bawah metode Update(). Kami akan menambahkan implementasi pada tahap selanjutnya
// 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()
{
}
- Tambahkan impor berikut
using UnityEngine.XR;
- Tambahkan kode berikut di atas
Update()
metode . Ini akan memungkinkan aplikasi mengenali gerakan mengetuk tangan pendek dan panjang (2 detik)
// 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
}
}
}
}
}
Menambahkan & Mengonfigurasi SpatialAnchorManager
ASA SDK menawarkan antarmuka sederhana yang disebut SpatialAnchorManager
untuk melakukan panggilan ke layanan ASA. Mari kita tambahkan sebagai variabel ke AzureSpatialAnchorsScript.cs
Pertama tambahkan impor
using Microsoft.Azure.SpatialAnchors.Unity;
Kemudian deklarasikan variabel
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()
Dalam metode , tetapkan variabel ke komponen yang kami tambahkan di langkah sebelumnya
// Start is called before the first frame update
void Start()
{
_spatialAnchorManager = GetComponent<SpatialAnchorManager>();
}
Untuk menerima debug dan log kesalahan, kita perlu berlangganan panggilan balik yang berbeda
// 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}");
}
Catatan
Untuk melihat log, pastikan setelah Anda membuat proyek dari Unity dan Anda membuka solusi .sln
visual studio, pilih Debug --> Jalankan dengan Debugging dan biarkan HoloLens anda tersambung ke komputer saat aplikasi berjalan.
Mulai Sesi
Untuk membuat dan menemukan jangkar, pertama-tama kita harus memulai sesi. Saat memanggil StartSessionAsync()
, SpatialAnchorManager
akan membuat sesi jika perlu lalu memulainya. Mari kita tambahkan ini ke metode kita 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();
}
Buat Anchor
Sekarang setelah kita memiliki sesi yang berjalan, kita dapat membuat jangkar. Dalam aplikasi ini, kami ingin melacak jangkar GameObjects
yang dibuat dan pengidentifikasi jangkar yang dibuat (ID jangkar). Mari kita tambahkan dua daftar ke kode kita.
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>();
Mari kita buat metode CreateAnchor
yang membuat jangkar pada posisi yang ditentukan oleh parameternya.
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.
}
Karena jangkar spasial tidak hanya memiliki posisi tetapi juga rotasi, mari kita atur rotasi untuk selalu berorientasi pada HoloLens pada pembuatan.
/// <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);
}
Sekarang setelah kita memiliki posisi dan rotasi jangkar yang diinginkan, mari kita buat yang terlihat GameObject
. Perhatikan bahwa Spatial Anchors tidak mengharuskan jangkar GameObject
terlihat oleh pengguna akhir karena tujuan utama Spatial Anchors adalah untuk menyediakan bingkai referensi umum dan persisten. Untuk tujuan tutorial ini, kami akan memvisualisasikan jangkar sebagai kubus. Setiap jangkar akan diinisialisasi sebagai kubus putih , yang akan berubah menjadi kubus hijau setelah proses pembuatan berhasil.
/// <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;
}
Catatan
Kami menggunakan shader warisan, karena disertakan dalam build Unity default. Shader lain seperti shader default hanya disertakan jika ditentukan secara manual atau secara langsung merupakan bagian dari adegan. Jika shader tidak disertakan dan aplikasi mencoba merendernya, itu akan menghasilkan bahan merah muda.
Sekarang mari kita tambahkan dan konfigurasikan komponen Spatial Anchor. Kami mengatur kedaluwarsa jangkar menjadi 3 hari dari pembuatan jangkar. Setelah itu mereka akan secara otomatis dihapus dari cloud. Ingatlah untuk menambahkan impor
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);
}
Untuk menyimpan jangkar, pengguna harus mengumpulkan data lingkungan.
/// <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%}");
}
}
Catatan
HoloLens mungkin dapat menggunakan kembali data lingkungan yang sudah diambil di sekitar jangkar, yang mengakibatkan benar sudah ketika dipanggil IsReadyForCreate
untuk pertama kalinya.
Sekarang setelah jangkar spasial cloud telah disiapkan, kita dapat mencoba penyimpanan aktual di sini.
/// <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);
}
}
Terakhir mari kita tambahkan panggilan fungsi ke metode kita 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);
}
Aplikasi kami sekarang dapat membuat beberapa jangkar. Perangkat apa pun sekarang dapat menemukan jangkar yang dibuat (jika belum kedaluwarsa) selama mereka mengetahui ID jangkar dan memiliki akses ke Sumber Daya Spatial Anchors yang sama di Azure.
Hentikan Sesi & Hancurkan GameObjects
Untuk meniru perangkat kedua yang menemukan semua jangkar, kami sekarang akan menghentikan sesi dan menghapus semua jangkar GameObjects (kami akan menyimpan ID jangkar). Setelah itu kita akan memulai sesi baru dan mengkueri jangkar menggunakan ID jangkar yang disimpan.
SpatialAnchorManager
dapat mengurus sesi berhenti hanya dengan memanggil metodenya DestroySession()
. Mari kita tambahkan ini ke metode kita LongTap()
/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
_spatialAnchorManager.DestroySession();
}
Mari kita buat metode untuk menghapus semua jangkar GameObjects
/// <summary>
/// Destroys all Anchor GameObjects
/// </summary>
private void RemoveAllAnchorGameObjects()
{
foreach (var anchorGameObject in _foundOrCreatedAnchorGameObjects)
{
Destroy(anchorGameObject);
}
_foundOrCreatedAnchorGameObjects.Clear();
}
Dan sebut saja setelah menghancurkan sesi di 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");
}
Temukan Jangkar
Kami sekarang akan mencoba menemukan jangkar lagi dengan posisi dan rotasi yang benar yang kami buat. Untuk melakukannya, kita perlu memulai sesi dan membuat Watcher
yang akan mencari jangkar yang sesuai dengan kriteria yang diberikan. Sebagai kriteria, kita akan memberinya UMPAN ID jangkar yang sebelumnya kita buat. Mari kita buat metode LocateAnchor()
dan gunakan SpatialAnchorManager
untuk membuat Watcher
. Untuk menemukan strategi selain menggunakan ID jangkar, lihat Strategi menemukan Jangkar
/// <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!");
}
}
Setelah pengamat dimulai, pengamat akan mengaktifkan panggilan balik ketika menemukan jangkar yang sesuai dengan kriteria yang diberikan. Mari kita pertama-tama buat metode jangkar kita yang terletak disebut SpatialAnchorManager_AnchorLocated()
bahwa kita akan mengonfigurasi untuk dipanggil ketika pengamat telah menemukan jangkar. Metode ini akan membuat visual GameObject
dan melampirkan komponen jangkar asli ke dalamnya. Komponen jangkar asli akan memastikan posisi dan rotasi yang GameObject
benar diatur.
Mirip dengan proses pembuatan, jangkar dilampirkan ke GameObject. GameObject ini tidak harus terlihat di adegan Anda agar jangkar spasial berfungsi. Untuk tujuan tutorial ini, kami akan memvisualisasikan setiap jangkar sebagai kubus biru setelah mereka ditemukan. Jika Anda hanya menggunakan jangkar untuk membuat sistem koordinat bersama, Anda tidak perlu memvisualisasikan GameObject yang dibuat.
/// <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);
});
}
}
Sekarang mari kita berlangganan panggilan balik AnchorLocated dari SpatialAnchorManager
untuk memastikan metode kami SpatialAnchorManager_AnchorLocated()
dipanggil setelah pengamat menemukan jangkar.
// 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;
}
Terakhir, mari kita perluas metode kita LongTap()
untuk menyertakan menemukan jangkar. Kita akan menggunakan IsSessionStarted
boolean untuk memutuskan apakah kita mencari semua jangkar atau menghancurkan semua jangkar seperti yang dijelaskan dalam Gambaran Umum Aplikasi
/// <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();
}
}
Cobalah #2
Aplikasi Anda sekarang mendukung pembuatan jangkar dan menemukannya. Buat aplikasi Anda di Unity dan sebarkan dari Visual Studio dengan mengikuti Menggunakan Visual Studio untuk menyebarkan dan men-debug.
Pastikan HoloLens Anda terhubung ke internet. Setelah aplikasi dimulai dan pesan yang dibuat dengan Unity menghilang, ketukan singkat di sekitar Anda. Kubus putih akan muncul untuk menunjukkan posisi dan rotasi jangkar yang akan dibuat. Proses pembuatan jangkar secara otomatis dipanggil. Saat Anda perlahan-lahan melihat sekitar Anda, Anda menangkap data lingkungan. Setelah data lingkungan yang cukup dikumpulkan, aplikasi kami akan mencoba membuat jangkar di lokasi yang ditentukan. Setelah proses pembuatan jangkar selesai, kubus akan berubah menjadi hijau. Periksa log debug Anda di visual studio untuk melihat apakah semuanya berfungsi seperti yang dimaksudkan.
Ketukan panjang untuk menghapus semua GameObjects
dari adegan Anda dan menghentikan sesi jangkar spasial.
Setelah adegan dibersihkan, Anda dapat mengetuk lagi, yang akan memulai sesi dan mencari jangkar yang telah Anda buat sebelumnya. Setelah ditemukan, mereka divisualisasikan oleh kubus biru pada posisi dan rotasi yang berlabuh. Jangkar ini (selama tidak kedaluwarsa) dapat ditemukan oleh perangkat yang didukung selama mereka memiliki ID jangkar yang benar dan memiliki akses ke sumber daya jangkar spasial Anda.
Hapus Anchor
Saat ini aplikasi kami dapat membuat dan menemukan jangkar. Meskipun menghapus GameObjects
, itu tidak menghapus jangkar di cloud. Mari kita tambahkan fungsionalitas untuk juga menghapusnya di cloud jika Anda mengetuk jangkar yang ada.
Mari kita tambahkan metode DeleteAnchor
yang menerima GameObject
. Kami kemudian akan menggunakan SpatialAnchorManager
bersama-sama dengan komponen objek CloudNativeAnchor
untuk meminta penghapusan jangkar di cloud.
/// <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!");
}
Untuk memanggil metode ini dari ShortTap
, kita harus dapat menentukan apakah ketukan telah mendekati jangkar yang terlihat yang ada. Mari kita buat metode pembantu yang mengurusnya
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;
}
}
Kita sekarang dapat memperluas metode kita ShortTap
untuk menyertakan DeleteAnchor
panggilan
/// <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);
}
}
Coba #3
Buat aplikasi Anda di Unity dan sebarkan dari Visual Studio dengan mengikuti Menggunakan Visual Studio untuk menyebarkan dan men-debug.
Perhatikan bahwa lokasi gerakan mengetuk tangan Anda adalah bagian tengah tangan Anda di aplikasi ini dan bukan ujung jari Anda.
Saat Anda mengetuk jangkar, baik dibuat (hijau) atau terletak (biru) permintaan dikirim ke layanan jangkar spasial untuk menghapus jangkar ini dari akun. Hentikan sesi (ketukan panjang) dan mulai sesi lagi (ketuk panjang) untuk mencari semua jangkar. Jangkar yang dihapus tidak akan lagi ditemukan.
Menggabungkan semuanya
Berikut adalah tampak file kelas AzureSpatialAnchorsScript
lengkap yang seharusnya, setelah semua elemen yang berbeda telah digabung. Anda dapat menggunakannya sebagai referensi untuk dibandingkan dengan file Anda sendiri, dan melihat jika Anda mungkin memiliki perbedaan yang tersisa.
Catatan
Anda akan melihat bahwa kami telah menyertakan [RequireComponent(typeof(SpatialAnchorManager))]
ke skrip. Dengan ini, Unity akan memastikan bahwa GameObject tempat kita melampirkan AzureSpatialAnchorsScript
, juga memiliki yang SpatialAnchorManager
melekat padanya.
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>
}
Langkah berikutnya
Dalam tutorial ini, Anda mempelajari cara menerapkan aplikasi Spatial Anchors dasar untuk HoloLens menggunakan Unity. Untuk mempelajari selengkapnya tentang cara menggunakan Azure Spatial Anchors di aplikasi Android baru, lanjutkan ke tutorial berikutnya.