// Copyright HTC Corporation All Rights Reserved. using System; using UnityEngine; using UnityEngine.XR.OpenXR; using VIVE.OpenXR.Feature; using static VIVE.OpenXR.Feature.ViveAnchor; using System.Threading.Tasks; using System.Threading; using System.Collections.Generic; using System.Linq; namespace VIVE.OpenXR.Toolkits.Anchor { public static class AnchorManager { static ViveAnchor feature = null; static bool isSupported = false; static bool isPersistedAnchorSupported = false; static void EnsureFeature() { if (feature != null) return; feature = OpenXRSettings.Instance.GetFeature(); if (feature == null) throw new NotSupportedException("ViveAnchor feature is not enabled"); } static void EnsureCollection() { if (taskAcquirePAC != null) { Debug.Log("AnchorManager: Wait for AcquirePersistedAnchorCollection task."); taskAcquirePAC.Wait(); } if (persistedAnchorCollection == IntPtr.Zero) throw new Exception("Should create Persisted Anchor Collection first."); } /// /// Helper to get the extension feature instance. /// /// Instance of ViveAnchor feature. public static ViveAnchor GetFeature() { if (feature != null) return feature; feature = OpenXRSettings.Instance.GetFeature(); return feature; } /// /// Check if the extensions are supported. Should always check this before using the other functions. /// /// True if the extension is supported, false otherwise. public static bool IsSupported() { if (GetFeature() == null) return false; if (isSupported) return true; var ret = false; if (feature.GetProperties(out XrSystemAnchorPropertiesHTC properties) == XrResult.XR_SUCCESS) { Debug.Log("ViveAnchor: IsSupported() properties.supportedFeatures: " + properties.supportsAnchor); ret = properties.supportsAnchor > 0; isSupported = ret; } else { Debug.Log("ViveAnchor: IsSupported() GetSystemProperties failed."); } return ret; } /// /// Check if the persisted anchor extension is supported and enabled. /// Should always check this before using the other persistance function. /// /// True if persisted anchor extension is supported, false otherwise. public static bool IsPersistedAnchorSupported() { if (GetFeature() == null) return false; if (isPersistedAnchorSupported) return true; else isPersistedAnchorSupported = feature.IsPersistedAnchorSupported(); return isPersistedAnchorSupported; } /// /// Create a spatial anchor at tracking space (Camera Rig). /// /// The related pose to the tracking space (Camera Rig) /// Anchor container public static Anchor CreateAnchor(Pose pose, string name) { EnsureFeature(); if (string.IsNullOrEmpty(name)) throw new ArgumentException("The name should not be empty."); XrSpace baseSpace = feature.GetTrackingSpace(); XrSpatialAnchorCreateInfoHTC createInfo = new XrSpatialAnchorCreateInfoHTC(); createInfo.type = XrStructureType.XR_TYPE_SPATIAL_ANCHOR_CREATE_INFO_HTC; createInfo.poseInSpace = new XrPosef(); createInfo.poseInSpace.position = pose.position.ToOpenXRVector(); createInfo.poseInSpace.orientation = pose.rotation.ToOpenXRQuaternion(); createInfo.name = new XrSpatialAnchorNameHTC(name); createInfo.space = baseSpace; if (feature.CreateSpatialAnchor(createInfo, out XrSpace anchor) == XrResult.XR_SUCCESS) { return new Anchor(anchor, name); } return null; } /// /// Get the name of the spatial anchor. /// /// The anchor instance. /// Output parameter to hold the name of the anchor. /// True if the name is successfully retrieved, false otherwise. public static bool GetSpatialAnchorName(Anchor anchor, out string name) { return GetSpatialAnchorName(anchor.GetXrSpace(), out name); } /// /// Get the name of the spatial anchor. /// /// The XrSpace representing the anchor. /// Output parameter to hold the name of the anchor. /// True if the name is successfully retrieved, false otherwise. public static bool GetSpatialAnchorName(XrSpace anchor, out string name) { name = ""; EnsureFeature(); XrResult ret = feature.GetSpatialAnchorName(anchor, out XrSpatialAnchorNameHTC xrName); if (ret == XrResult.XR_SUCCESS) name = xrName.ToString(); return ret == XrResult.XR_SUCCESS; } /// /// Get the XrSpace stand for current tracking space. /// /// public static XrSpace GetTrackingSpace() { EnsureFeature(); return feature.GetTrackingSpace(); } /// /// Get the pose related to current tracking space. Only when position and orientation are both valid, the pose is valid. /// /// /// /// true if both position and rotation are valid. public static bool GetTrackingSpacePose(Anchor anchor, out Pose pose) { var sw = SpaceWrapper.Instance; return anchor.GetRelatedPose(feature.GetTrackingSpace(), ViveInterceptors.Instance.GetPredictTime(), out pose); } // Use SemaphoreSlim to make sure only one anchor's task is running at the same time. static readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1); // Use lock to make sure taskAcquirePAC and persistedAnchorCollection assignment is atomic. static readonly object asyncLock = new object(); static FutureTask<(XrResult, IntPtr)> taskAcquirePAC = null; static IntPtr persistedAnchorCollection = System.IntPtr.Zero; private static (XrResult, IntPtr) CompletePAC(IntPtr future) { Debug.Log("AnchorManager: AcquirePersistedAnchorCollectionComplete"); var ret = feature.AcquirePersistedAnchorCollectionComplete(future, out var completion); lock (asyncLock) { taskAcquirePAC = null; if (ret == XrResult.XR_SUCCESS) { ret = completion.futureResult; Debug.Log("AnchorManager: AcquirePersistedAnchorCollection: Complete"); persistedAnchorCollection = completion.persistedAnchorCollection; return (ret, persistedAnchorCollection); } else { //Debug.LogError("AcquirePersistedAnchorCollection: Complete: PersistedAnchorCollection=" + completion.persistedAnchorCollection); persistedAnchorCollection = System.IntPtr.Zero; return (ret, persistedAnchorCollection); } } } /// /// Enable the persistance anchor feature. It will acquire a persisted anchor collection. /// The first time PAC's acquiration may take time. You can to cancel the process by calling . /// You can wait for the returned task to complete, or by calling to check if the collection is ready. /// Use to free resource when no any persisted anchor operations are needed. /// /// A task representing the asynchronous operation. public static FutureTask<(XrResult, IntPtr)> AcquirePersistedAnchorCollection() { EnsureFeature(); if (!feature.IsPersistedAnchorSupported()) return FutureTask<(XrResult, IntPtr)>.FromResult((XrResult.XR_ERROR_EXTENSION_NOT_PRESENT, IntPtr.Zero)); lock (asyncLock) { if (persistedAnchorCollection != System.IntPtr.Zero) return FutureTask<(XrResult, IntPtr)>.FromResult((XrResult.XR_SUCCESS, persistedAnchorCollection)); // If the persistedAnchorCollection is not ready, and the task is started, wait for it. if (taskAcquirePAC != null) return taskAcquirePAC; } Debug.Log("ViveAnchor: AcquirePersistedAnchorCollectionAsync"); var ret = feature.AcquirePersistedAnchorCollectionAsync(out IntPtr future); if (ret != XrResult.XR_SUCCESS) { Debug.LogError("AcquirePersistedAnchorCollection failed: " + ret); return FutureTask<(XrResult, IntPtr)>.FromResult((ret, IntPtr.Zero)); } else { var task = new FutureTask<(XrResult, IntPtr)>(future, CompletePAC, 10, autoComplete: true); lock (asyncLock) { taskAcquirePAC = task; } return task; } } /// /// Check if the persisted anchor collection is acquired. /// /// True if the persisted anchor collection is acquired, false otherwise. public static bool IsPersistedAnchorCollectionAcquired() { return persistedAnchorCollection != System.IntPtr.Zero; } /// /// Call this function when no any persisted anchor operations are needed. /// Destroy the persisted anchor collection. If task is running, the task will be canceled. /// public static void ReleasePersistedAnchorCollection() { IntPtr tmp; if (taskAcquirePAC != null) { taskAcquirePAC.Cancel(); taskAcquirePAC.Dispose(); taskAcquirePAC = null; } lock (asyncLock) { if (persistedAnchorCollection == System.IntPtr.Zero) return; tmp = persistedAnchorCollection; persistedAnchorCollection = System.IntPtr.Zero; } EnsureFeature(); Task.Run(async () => { Debug.Log("ViveAnchor: ReleasePersistedAnchorCollection task is started."); await semaphoreSlim.WaitAsync(); try { feature?.ReleasePersistedAnchorCollection(tmp); } finally { semaphoreSlim.Release(); } Debug.Log("ViveAnchor: ReleasePersistedAnchorCollection task is done."); }); } private static XrResult CompletePA(IntPtr future) { Debug.Log("AnchorManager: CompletePA"); var ret = feature.PersistSpatialAnchorComplete(future, out var completion); if (ret == XrResult.XR_SUCCESS) { return completion.futureResult; } else { Debug.LogError("AcquirePersistedAnchorCollection failed: " + ret); } return ret; } /// /// Persist an anchor with the given name. The persistanceAnchorName should be unique. /// The persistance might fail if the anchor is not trackable. Check the result from the task. /// /// The anchor instance. /// The name of the persisted anchor. /// PersistAnchor may take time. If you want to cancel it, use cts. /// The task to get persisted anchor's result. public static FutureTask PersistAnchor(Anchor anchor, string persistanceAnchorName) { EnsureFeature(); EnsureCollection(); if (string.IsNullOrEmpty(persistanceAnchorName)) throw new ArgumentException("The persistanceAnchorName should not be empty."); var name = new XrSpatialAnchorNameHTC(persistanceAnchorName); var ret = feature.PersistSpatialAnchorAsync(persistedAnchorCollection, anchor.GetXrSpace(), name, out IntPtr future); if (ret == XrResult.XR_SUCCESS) { // If no auto complete, you can cancel the task and no need to free resouce. // Once it completed, you need handle the result. return new FutureTask(future, CompletePA, 10, autoComplete: false); } return FutureTask.FromResult(ret); } /// /// Unpersist the anchor by the name. The anchor created from persisted anchor will still be trackable. /// /// The name of the persisted anchor to be removed. /// The result of the operation. public static XrResult UnpersistAnchor(string persistanceAnchorName) { EnsureFeature(); EnsureCollection(); if (string.IsNullOrEmpty(persistanceAnchorName)) throw new ArgumentException("The persistanceAnchorName should not be empty."); var name = new XrSpatialAnchorNameHTC(persistanceAnchorName); var ret = feature.UnpersistSpatialAnchor(persistedAnchorCollection, name); return ret; } /// /// Get the number of persisted anchors. /// /// Output parameter to hold the number of persisted anchors. /// The result of the operation. public static XrResult GetNumberOfPersistedAnchors(out int count) { EnsureFeature(); EnsureCollection(); XrSpatialAnchorNameHTC[] xrNames = null; uint xrCount = 0; XrResult ret = feature.EnumeratePersistedAnchorNames(persistedAnchorCollection, 0, ref xrCount, ref xrNames); if (ret != XrResult.XR_SUCCESS) count = 0; else count = (int)xrCount; return ret; } /// /// List all persisted anchors. /// /// Output parameter to hold the names of the persisted anchors. /// The result of the operation. public static XrResult EnumeratePersistedAnchorNames(out string[] names) { EnsureFeature(); EnsureCollection(); XrSpatialAnchorNameHTC[] xrNames = null; uint countOut = 0; uint countIn = 0; XrResult ret = feature.EnumeratePersistedAnchorNames(persistedAnchorCollection, countIn, ref countOut, ref xrNames); if (ret != XrResult.XR_SUCCESS) { names = null; return ret; } // If Insufficient size, try again. do { countIn = countOut; xrNames = new XrSpatialAnchorNameHTC[countIn]; ret = feature.EnumeratePersistedAnchorNames(persistedAnchorCollection, countIn, ref countOut, ref xrNames); } while (ret == XrResult.XR_ERROR_SIZE_INSUFFICIENT); if (ret != XrResult.XR_SUCCESS) { names = null; return ret; } names = new string[countIn]; for (int i = 0; i < countIn; i++) { string v = xrNames[i].ToString(); names[i] = v; } return ret; } private static (XrResult, Anchor) CompleteCreateSAfromPA(IntPtr future) { Debug.Log("AnchorManager: CompleteCreateSAfromPA"); var ret = feature.CreateSpatialAnchorFromPersistedAnchorComplete(future, out var completion); if (ret == XrResult.XR_SUCCESS) { var anchor = new Anchor(completion.anchor); anchor.isTrackable = true; return (completion.futureResult, anchor); } else { Debug.LogError("CreateSpatialAnchorFromPersistedAnchor failed: " + ret); return (ret, new Anchor(0)); } } /// /// Create a spatial anchor from a persisted anchor. This will also mark the anchor as trackable. /// /// The name of the persisted anchor. /// The name of the new spatial anchor. /// Output parameter to hold the new anchor instance. /// The result of the operation. public static FutureTask<(XrResult, Anchor)> CreateSpatialAnchorFromPersistedAnchor(string persistanceAnchorName, string spatialAnchorName) { EnsureFeature(); EnsureCollection(); Debug.Log("AnchorManager: CreateSpatialAnchorFromPersistedAnchor: " + persistanceAnchorName + " -> " + spatialAnchorName); if (string.IsNullOrEmpty(persistanceAnchorName) || string.IsNullOrEmpty(spatialAnchorName)) throw new ArgumentException("The persistanceAnchorName and spatialAnchorName should not be empty."); var createInfo = new XrSpatialAnchorFromPersistedAnchorCreateInfoHTC() { type = XrStructureType.XR_TYPE_SPATIAL_ANCHOR_FROM_PERSISTED_ANCHOR_CREATE_INFO_HTC, persistedAnchorCollection = persistedAnchorCollection, persistedAnchorName = new XrSpatialAnchorNameHTC(persistanceAnchorName), spatialAnchorName = new XrSpatialAnchorNameHTC(spatialAnchorName) }; var ret = feature.CreateSpatialAnchorFromPersistedAnchorAsync(createInfo, out var future); if (ret == XrResult.XR_SUCCESS) { // If no auto complete, you can cancel the task and no need to free resouce. // Once it completed, you need handle the result. return new FutureTask<(XrResult, Anchor)>(future, CompleteCreateSAfromPA, 10, autoComplete: false); } else { return FutureTask<(XrResult, Anchor)>.FromResult((ret, new Anchor(0))); } } /// /// Clear all persisted anchors. Those anchors created from or to the persisted anchor will still be trackable. /// /// The result of the operation. public static XrResult ClearPersistedAnchors() { EnsureFeature(); EnsureCollection(); return feature.ClearPersistedAnchors(persistedAnchorCollection); } /// /// Get the properties of the persisted anchor. /// maxPersistedAnchorCount in XrPersistedAnchorPropertiesGetInfoHTC will be set to the max count of the persisted anchor. /// /// Output parameter to hold the properties of the persisted anchor. /// The result of the operation. public static XrResult GetPersistedAnchorProperties(out XrPersistedAnchorPropertiesGetInfoHTC properties) { EnsureFeature(); EnsureCollection(); return feature.GetPersistedAnchorProperties(persistedAnchorCollection, out properties); } /// /// Export the persisted anchor to a buffer. The buffer can be used to import the anchor later or save it to a file. /// Export takes time, so it is an async function. The buffer will be null if the export failed. /// /// The name of the persisted anchor to be exported. /// Output parameter to hold the buffer containing the exported anchor. /// A task representing the asynchronous operation. public static Task<(XrResult, string, byte[])> ExportPersistedAnchor(string persistanceAnchorName) { EnsureFeature(); EnsureCollection(); if (string.IsNullOrEmpty(persistanceAnchorName)) return Task.FromResult<(XrResult, string, byte[])>((XrResult.XR_ERROR_HANDLE_INVALID, "", null)); var name = new XrSpatialAnchorNameHTC(persistanceAnchorName); return Task.Run(async () => { Debug.Log($"ExportPersistedAnchor({persistanceAnchorName}) task is started."); XrResult ret = XrResult.XR_ERROR_VALIDATION_FAILURE; await semaphoreSlim.WaitAsync(); try { lock (asyncLock) { if (persistedAnchorCollection == System.IntPtr.Zero) { return (XrResult.XR_ERROR_HANDLE_INVALID, "", null); } } ret = feature.ExportPersistedAnchor(persistedAnchorCollection, name, out var buffer); Debug.Log($"ExportPersistedAnchor({persistanceAnchorName}) task is done. ret=" + ret); lock (asyncLock) { if (ret != XrResult.XR_SUCCESS) { buffer = null; return (ret, "", null); } return (ret, persistanceAnchorName, buffer); } } finally { semaphoreSlim.Release(); } }); } /// /// Import the persisted anchor from a buffer. The buffer should be created by ExportPersistedAnchor. /// Import takes time, so it is an async function. Check imported anchor by EnumeratePersistedAnchorNames. /// /// The buffer containing the persisted anchor data. /// A task representing the asynchronous operation. public static Task ImportPersistedAnchor(byte[] buffer) { EnsureFeature(); EnsureCollection(); return Task.Run(async () => { Debug.Log($"ImportPersistedAnchor task is started."); XrResult ret = XrResult.XR_ERROR_VALIDATION_FAILURE; await semaphoreSlim.WaitAsync(); try { lock (asyncLock) { if (persistedAnchorCollection == System.IntPtr.Zero) return XrResult.XR_ERROR_HANDLE_INVALID; ret = feature.ImportPersistedAnchor(persistedAnchorCollection, buffer); return ret; } } finally { semaphoreSlim.Release(); Debug.Log($"ImportPersistedAnchor task is done. ret=" + ret); } }); } /// /// Get the persisted anchor name from the buffer. The buffer should be created by ExportPersistedAnchor. /// /// True if the name is successfully retrieved, false otherwise. public static bool GetPersistedAnchorNameFromBuffer(byte[] buffer, out string name) { EnsureFeature(); EnsureCollection(); var ret = feature.GetPersistedAnchorNameFromBuffer(persistedAnchorCollection, buffer, out var xrName); if (ret == XrResult.XR_SUCCESS) name = xrName.ToString(); else name = ""; return ret == XrResult.XR_SUCCESS; } /// /// Anchor is a named Space. It can be used to create a spatial anchor, or get the anchor's name. /// After use, you should call Dispose() to release the anchor. /// IsTrackable is true if the anchor is created persisted anchor or created from persisted anchor. /// IsPersisted is true if the anchor is ever persisted. /// public class Anchor : VIVE.OpenXR.Feature.Space { /// /// The anchor's name /// string name; /// /// The anchor's name /// public string Name { get { if (string.IsNullOrEmpty(name)) name = GetSpatialAnchorName(); return name; } } internal bool isTrackable = false; /// /// If the anchor is created persisted anchor or created from persisted anchor, it will be trackable. /// public bool IsTrackable => isTrackable; internal bool isPersisted = false; /// /// If the anchor is ever persisted, it will be true. /// public bool IsPersisted => isPersisted; internal Anchor(XrSpace anchor, string name) : base(anchor) { Debug.Log($"Anchor: new Anchor({anchor}, {name})"); // Remove this line later. // Get the current tracking space. this.name = name; } internal Anchor(XrSpace anchor) : base(anchor) { Debug.Log($"Anchor: new Anchor({anchor})"); // Remove this line later. // Get the current tracking space. name = GetSpatialAnchorName(); } internal Anchor(Anchor other) : base(other.space) { // Get the current tracking space. name = other.name; isTrackable = other.isTrackable; isPersisted = other.isPersisted; } /// /// Get the anchor's name by using this anchor's handle, instead of the anchor's Name. This will update the anchor's Name. /// /// Anchor's name. Always return non null string. public string GetSpatialAnchorName() { if (space == 0) { Debug.LogError("Anchor: GetSpatialAnchorName: The anchor is invalid."); return ""; } AnchorManager.EnsureFeature(); if (AnchorManager.GetSpatialAnchorName(this, out string name)) return name; Debug.LogError("Anchor: GetSpatialAnchorName: Failed to get Anchor name."); return ""; } } } }