/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * Licensed under the Oculus SDK License Agreement (the "License"); * you may not use the Oculus SDK except in compliance with the License, * which is provided at the time of installation or download, or which * otherwise accompanies this software in either electronic or hard copy form. * * You may obtain a copy of the License at * * https://developer.oculus.com/licenses/oculussdk/ * * Unless required by applicable law or agreed to in writing, the Oculus SDK * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Diagnostics; #if DEVELOPMENT_BUILD using System.Linq; #endif using Unity.Collections; using UnityEngine; using Debug = UnityEngine.Debug; /// /// Represents a spatial anchor. /// /// /// This component can be used in two ways: to create a new spatial anchor or to bind to an existing spatial anchor. /// /// To create a new spatial anchor, simply add this component to any GameObject. The transform of the GameObject is used /// to create a new spatial anchor in the Oculus Runtime. Afterwards, the GameObject's transform will be updated /// automatically. The creation operation is asynchronous, and, if it fails, this component will be destroyed. /// /// To load previously saved anchors and bind them to an , see /// . /// [DisallowMultipleComponent] [HelpURL("https://developer.oculus.com/reference/unity/latest/class_o_v_r_spatial_anchor")] public class OVRSpatialAnchor : MonoBehaviour { private bool _startCalled; private ulong _requestId; private readonly SaveOptions _defaultSaveOptions = new SaveOptions { Storage = OVRSpace.StorageLocation.Local, }; private readonly EraseOptions _defaultEraseOptions = new EraseOptions { Storage = OVRSpace.StorageLocation.Local, }; /// /// Event that is dispatched when the localization process finishes. /// public event Action OnLocalize; /// /// The space associated with the spatial anchor. /// /// /// The represents the runtime instance of the spatial anchor and will change across /// different sessions. /// public OVRSpace Space { get; private set; } /// /// The UUID associated with the spatial anchor. /// /// /// UUIDs persist across sessions. If you load a persisted anchor, you can use the UUID to identify /// it. /// public Guid Uuid { get; private set; } /// /// Checks whether the spatial anchor is created. /// /// /// Creation is asynchronous and may take several frames. If creation fails, the component is destroyed. /// public bool Created => Space.Valid; /// /// Checks whether the spatial anchor is pending creation. /// public bool PendingCreation => _requestId != 0; /// /// Checks whether the spatial anchor is localized. /// /// /// When you create a new spatial anchor, it may take a few frames before it is localized. Once localized, /// its transform updates automatically. /// public bool Localized => Space.Valid && OVRPlugin.GetSpaceComponentStatus(Space, OVRPlugin.SpaceComponentType.Locatable, out var isEnabled, out _) && isEnabled; /// /// Initializes this component from an existing space handle and uuid, e.g., the result of a call to /// . /// /// /// This method associates the component with an existing spatial anchor, for example, the one that was saved in /// a previous session. Do not call this method to create a new spatial anchor. /// /// If you call this method, you must do so prior to the component's `Start` method. You cannot change the spatial /// anchor associated with this component after that. /// /// The existing to associate with this spatial anchor. /// The universally unique identifier to associate with this spatial anchor. /// Thrown if `Start` has already been called on this component. /// Thrown if is not . public void InitializeFromExisting(OVRSpace space, Guid uuid) { if (_startCalled) throw new InvalidOperationException( $"Cannot call {nameof(InitializeFromExisting)} after {nameof(Start)}. This must be set once upon creation."); try { if (!space.Valid) throw new ArgumentException($"Invalid space {space}.", nameof(space)); ThrowIfBound(uuid); } catch { Destroy(this); throw; } InitializeUnchecked(space, uuid); } /// /// Saves the to local persistent storage. /// /// /// This method is asynchronous. Use to be notified of completion. /// /// When saved, an can be loaded by a different session. Use the /// to identify the same at a future time. /// /// This operation fully succeeds or fails, which means, either all anchors are successfully saved, /// or the operation fails. /// /// /// Invoked when the save operation completes. May be null. Parameters are /// - : The anchor being saved. /// - `bool`: A value indicating whether the save operation succeeded. /// public void Save(Action onComplete = null) { Save(_defaultSaveOptions, onComplete); } private static NativeArray ToNativeArray(ICollection anchors) { var count = anchors.Count; var spaces = new NativeArray(count, Allocator.Temp); var i = 0; foreach (var anchor in anchors.ToNonAlloc()) { spaces[i++] = anchor ? anchor.Space : 0; } return spaces; } /// /// Saves the with specified . /// /// /// This method is asynchronous. Use to be notified of completion. /// When saved, the can be loaded by a different session. Use the /// to identify the same at a future time. /// /// This operation fully succeeds or fails; that is, either all anchors are successfully saved, /// or the operation fails. /// /// Options how the anchor is saved. whether local or cloud. /// /// Invoked when the save operation completes. May be null. Parameters are /// - : The anchor being saved. /// - `bool`: A value indicating whether the save operation succeeded. /// public void Save(SaveOptions saveOptions, Action onComplete = null) { var task = SaveAsync(saveOptions); if (onComplete != null) { InvertedCapture.ContinueTaskWith(task, onComplete, this); } } /// /// Saves the with specified . /// /// /// This method is asynchronous; use the returned to be notified of completion. /// When saved, the can be loaded by a different session. Use the /// to identify the same at a future time. /// /// This operation fully succeeds or fails; that is, either all anchors are successfully saved, /// or the operation fails. /// /// /// An with a boolean type parameter indicating the success of the save operation. /// public OVRTask SaveAsync() => SaveAsync(_defaultSaveOptions); /// /// Saves the with specified . /// /// /// This method is asynchronous; use the returned to be notified of completion. /// When saved, the can be loaded by a different session. Use the /// to identify the same at a future time. /// /// This operation fully succeeds or fails; that is, either all anchors are successfully saved, /// or the operation fails. /// /// Options for how the anchor will be saved. /// /// An with a boolean type parameter indicating the success of the save operation. /// public OVRTask SaveAsync(SaveOptions saveOptions) { var requestId = Guid.NewGuid(); SaveRequests[saveOptions.Storage].Add(this); AsyncRequestTaskIds[this] = requestId; return OVRTask.FromGuid(requestId); } /// /// Saves a collection of to specified storage. /// /// /// This method is asynchronous. Use to be notified of completion. /// When saved, an can be loaded by a different session. Use the /// to identify the same at a future time. /// /// Collection of anchors /// Options how the anchors are saved whether local or cloud. /// /// Invoked when the save operation completes. May be null. receives two parameters: /// - `ICollection<OVRSpatialAnchor>`: The same collection as in parameter /// - `OperationResult`: An error code indicating whether the save operation succeeded or not. /// /// Thrown if is `null`. public static void Save(ICollection anchors, SaveOptions saveOptions, Action, OperationResult> onComplete = null) { if (anchors == null) throw new ArgumentNullException(nameof(anchors)); using var spaces = ToNativeArray(anchors); var saveResult = OVRPlugin.SaveSpaceList(spaces, saveOptions.Storage.ToSpaceStorageLocation(), out var requestId); if (saveResult.IsSuccess()) { Development.LogRequest(requestId, $"Saving spatial anchors..."); MultiAnchorCompletionDelegates[requestId] = new MultiAnchorDelegatePair { Anchors = CopyAnchorListIntoListFromPool(anchors), Delegate = onComplete }; if (onComplete != null) { OVRTelemetry.Client.MarkerStart(OVRTelemetryConstants.Scene.MarkerId.SpatialAnchorSave, requestId.GetHashCode()); } } else { Development.LogError( $"{nameof(OVRPlugin)}.{nameof(OVRPlugin.SaveSpaceList)} failed with error {saveResult}."); onComplete?.Invoke(anchors, (OperationResult)saveResult); } } private static List CopyAnchorListIntoListFromPool( IEnumerable anchorList) { var poolList = OVRObjectPool.List(); poolList.AddRange(anchorList); return poolList; } /// /// Shares the anchor to an . /// The specified user will be able to download, track, and share specified anchors. /// /// /// This method is asynchronous. Use to be notified of completion. /// /// An Oculus user to share the anchor with. /// /// Invoked when the share operation completes. May be null. Delegate parameter is /// - `OperationResult`: An error code that indicates whether the share operation succeeded or not. /// public void Share(OVRSpaceUser user, Action onComplete = null) { var task = ShareAsync(user); if (onComplete != null) { task.ContinueWith(onComplete); } } /// /// Shares the anchor to an . /// The specified user will be able to download, track, and share specified anchors. /// /// /// This method is asynchronous; use the returned to be notified of completion. /// /// An Oculus user to share the anchor with. /// /// An with a type parameter indicating the success of the share operation. /// public OVRTask ShareAsync(OVRSpaceUser user) { var userList = OVRObjectPool.List(); userList.Add(user); return ShareAsyncInternal(userList); } /// /// Shares the anchor with two . /// Specified users will be able to download, track, and share specified anchors. /// /// /// This method is asynchronous. Use to be notified of completion. /// /// An Oculus user to share the anchor with. /// An Oculus user to share the anchor with. /// /// Invoked when the share operation completes. May be null. Delegate parameter is /// - `OperationResult`: An error code that indicates whether the share operation succeeded or not. /// public void Share(OVRSpaceUser user1, OVRSpaceUser user2, Action onComplete = null) { var task = ShareAsync(user1, user2); if (onComplete != null) { task.ContinueWith(onComplete); } } /// /// Shares the anchor to an . /// The specified user will be able to download, track, and share specified anchors. /// /// /// This method is asynchronous; use the returned to be notified of completion. /// /// An Oculus user to share the anchor with. /// An Oculus user to share the anchor with. /// /// An with a type parameter indicating the success of the share operation. /// public OVRTask ShareAsync(OVRSpaceUser user1, OVRSpaceUser user2) { var userList = OVRObjectPool.List(); userList.Add(user1); userList.Add(user2); return ShareAsyncInternal(userList); } /// /// Shares the anchor with three . /// Specified users will be able to download, track, and share specified anchors. /// /// /// This method is asynchronous. Use to be notified of completion. /// /// An Oculus user to share the anchor with. /// An Oculus user to share the anchor with. /// An Oculus user to share the anchor with. /// /// Invoked when the share operation completes. May be null. Delegate parameter is /// - `OperationResult`: An error code that indicates whether the share operation succeeded or not. /// public void Share(OVRSpaceUser user1, OVRSpaceUser user2, OVRSpaceUser user3, Action onComplete = null) { var task = ShareAsync(user1, user2, user3); if (onComplete != null) { task.ContinueWith(onComplete); } } /// /// Shares the anchor to an . /// The specified user will be able to download, track, and share specified anchors. /// /// /// This method is asynchronous; use the returned to be notified of completion. /// /// An Oculus user to share the anchor with. /// An Oculus user to share the anchor with. /// An Oculus user to share the anchor with. /// /// An with a type parameter indicating the success of the share operation. /// public OVRTask ShareAsync(OVRSpaceUser user1, OVRSpaceUser user2, OVRSpaceUser user3) { var userList = OVRObjectPool.List(); userList.Add(user1); userList.Add(user2); userList.Add(user3); return ShareAsyncInternal(userList); } /// /// Shares the anchor with four . /// Specified users will be able to download, track, and share specified anchors. /// /// /// This method is asynchronous. Use to be notified of completion. /// /// An Oculus user to share the anchor with. /// An Oculus user to share the anchor with. /// An Oculus user to share the anchor with. /// An Oculus user to share the anchor with. /// /// Invoked when the share operation completes. May be null. Delegate parameter is /// - `OperationResult`: An error code that indicates whether the share operation succeeded or not. /// public void Share(OVRSpaceUser user1, OVRSpaceUser user2, OVRSpaceUser user3, OVRSpaceUser user4, Action onComplete = null) { var task = ShareAsync(user1, user2, user3, user4); if (onComplete != null) { task.ContinueWith(onComplete); } } /// /// Shares the anchor to an . /// The specified user will be able to download, track, and share specified anchors. /// /// /// This method is asynchronous; use the returned to be notified of completion. /// /// An Oculus user to share the anchor with. /// An Oculus user to share the anchor with. /// An Oculus user to share the anchor with. /// An Oculus user to share the anchor with. /// /// An with a type parameter indicating the success of the share operation. /// public OVRTask ShareAsync(OVRSpaceUser user1, OVRSpaceUser user2, OVRSpaceUser user3, OVRSpaceUser user4) { var userList = OVRObjectPool.List(); userList.Add(user1); userList.Add(user2); userList.Add(user3); userList.Add(user4); return ShareAsyncInternal(userList); } /// /// Shares the anchor to a collection of . /// Specified users will be able to download, track, and share specified anchors. /// /// /// This method is asynchronous. Use to be notified of completion. /// /// A collection of Oculus users to share the anchor with. /// /// Invoked when the share operation completes. May be null. Delegate parameter is /// - `OperationResult`: An error code that indicates whether the share operation succeeded or not. /// public void Share(IEnumerable users, Action onComplete = null) { var task = ShareAsync(users); if (onComplete != null) { task.ContinueWith(onComplete); } } /// /// Shares the anchor to an . /// The specified user will be able to download, track, and share specified anchors. /// /// /// This method is asynchronous; use the returned to be notified of completion. /// /// A collection of Oculus users to share the anchor with. /// /// An with a type parameter indicating the success of the share operation. /// public OVRTask ShareAsync(IEnumerable users) { var userList = OVRObjectPool.List(); userList.AddRange(users); return ShareAsyncInternal(userList); } /// /// Shares a collection of to specified users. /// Specified users will be able to download, track, and share specified anchors. /// /// /// This method is asynchronous. Use to be notified of completion. /// /// This operation fully succeeds or fails, which means, either all anchors are successfully shared /// or the operation fails. /// /// The collection of anchors to share. /// An array of Oculus users to share these anchors with. /// /// Invoked when the share operation completes. May be null. Delegate parameter is /// - `ICollection<OVRSpatialAnchor>`: The collection of anchors being shared. /// - `OperationResult`: An error code that indicates whether the share operation succeeded or not. /// /// Thrown if is `null`. public static void Share(ICollection anchors, ICollection users, Action, OperationResult> onComplete = null) { if (anchors == null) throw new ArgumentNullException(nameof(anchors)); using var spaces = ToNativeArray(anchors); var handles = new NativeArray(users.Count, Allocator.Temp); using var disposer = handles; int i = 0; foreach (var user in users) { handles[i++] = user._handle; } var shareResult = OVRPlugin.ShareSpaces(spaces, handles, out var requestId); if (shareResult.IsSuccess()) { Development.LogRequest(requestId, $"Sharing {(uint)spaces.Length} spatial anchors..."); MultiAnchorCompletionDelegates[requestId] = new MultiAnchorDelegatePair { Anchors = CopyAnchorListIntoListFromPool(anchors), Delegate = onComplete }; } else { Development.LogError( $"{nameof(OVRPlugin)}.{nameof(OVRPlugin.ShareSpaces)} failed with error {shareResult}."); onComplete?.Invoke(anchors, (OperationResult)shareResult); } } private OVRTask ShareAsyncInternal(List users) { var shareRequestAnchors = GetListToStoreTheShareRequest(users); shareRequestAnchors.Add(this); var requestId = Guid.NewGuid(); AsyncRequestTaskIds[this] = requestId; return OVRTask.FromGuid(requestId); } private List GetListToStoreTheShareRequest(List users) { users.Sort((x, y) => x.Id.CompareTo(y.Id)); foreach (var (shareRequestUsers, shareRequestAnchors) in ShareRequests) { if (!AreSortedUserListsEqual(users, shareRequestUsers)) { continue; } // reuse the current request return shareRequestAnchors; } // add a new request var anchorList = OVRObjectPool.List(); ShareRequests.Add((users, anchorList)); return anchorList; } private static bool AreSortedUserListsEqual(IReadOnlyList sortedList1, IReadOnlyList sortedList2) { if (sortedList1.Count != sortedList2.Count) { return false; } for (var i = 0; i < sortedList1.Count; i++) { if (sortedList1[i].Id != sortedList2[i].Id) { return false; } } return true; } /// /// Erases the from persistent storage. /// /// /// This method is asynchronous. Use to be notified of completion. /// Erasing an does not destroy the anchor. /// /// /// Invoked when the erase operation completes. May be null. Parameters are /// - : The anchor being erased. /// - `bool`: A value indicating whether the erase operation succeeded. /// public void Erase(Action onComplete = null) { Erase(_defaultEraseOptions, onComplete); } /// /// Erases the from specified storage. /// /// /// This method is asynchronous. Use to be notified of completion. /// Erasing an does not destroy the anchor. /// /// Options how the anchor should be erased. /// /// Invoked when the erase operation completes. May be null. Parameters are /// - : The anchor being erased. /// - `bool`: A value indicating whether the erase operation succeeded. /// public void Erase(EraseOptions eraseOptions, Action onComplete = null) { var task = EraseAsync(eraseOptions); if (onComplete != null) { InvertedCapture.ContinueTaskWith(task, onComplete, this); } } /// /// Erases the from specified storage. /// /// /// This method is asynchronous; use the returned to be notified of completion. /// Erasing an does not destroy the anchor. /// /// /// An with a boolean type parameter indicating the success of the erase operation. /// public OVRTask EraseAsync() => EraseAsync(_defaultEraseOptions); /// /// Erases the from specified storage. /// /// /// This method is asynchronous; use the returned to be notified of completion. /// Erasing an does not destroy the anchor. /// /// Options for how the anchor should be erased. /// /// An with a boolean type parameter indicating the success of the erase operation. /// public OVRTask EraseAsync(EraseOptions eraseOptions) => OVRPlugin.EraseSpace(Space, eraseOptions.Storage.ToSpaceStorageLocation(), out var requestId) ? OVRTask.FromRequest(requestId) : OVRTask.FromResult(false); private static void ThrowIfBound(Guid uuid) { if (SpatialAnchors.ContainsKey(uuid)) throw new InvalidOperationException( $"Spatial anchor with uuid {uuid} is already bound to an {nameof(OVRSpatialAnchor)}."); } // Initializes this component without checking preconditions private void InitializeUnchecked(OVRSpace space, Guid uuid) { SpatialAnchors.Add(uuid, this); _requestId = 0; Space = space; Uuid = uuid; OVRPlugin.SetSpaceComponentStatus(Space, OVRPlugin.SpaceComponentType.Locatable, true, 0, out _); OVRPlugin.SetSpaceComponentStatus(Space, OVRPlugin.SpaceComponentType.Storable, true, 0, out _); OVRPlugin.SetSpaceComponentStatus(Space, OVRPlugin.SpaceComponentType.Sharable, true, 0, out _); // Try to update the pose as soon as we can. UpdateTransform(); } private void Start() { _startCalled = true; if (Space.Valid) { Development.Log($"[{Uuid}] Created spatial anchor from existing an existing space."); } else { CreateSpatialAnchor(); } } private void Update() { if (Space.Valid) { UpdateTransform(); } } private void LateUpdate() { SaveBatchAnchors(); ShareBatchAnchors(); } private static void SaveBatchAnchors() { foreach (var pair in SaveRequests) { if (pair.Value.Count == 0) { continue; } Save(pair.Value, new SaveOptions { Storage = pair.Key }); pair.Value.Clear(); } } private static void ShareBatchAnchors() { foreach (var (userList, anchorList) in ShareRequests) { if (userList.Count > 0 && anchorList.Count > 0) { Share(anchorList, userList); } OVRObjectPool.Return(userList); OVRObjectPool.Return(anchorList); } ShareRequests.Clear(); } private void OnDestroy() { if (Space.Valid) { OVRPlugin.DestroySpace(Space); } SpatialAnchors.Remove(Uuid); } private OVRPose GetTrackingSpacePose() { var mainCamera = Camera.main; if (mainCamera) { return transform.ToTrackingSpacePose(mainCamera); } Development.LogWarning($"No main camera found. Using world-space pose."); return transform.ToOVRPose(isLocal: false); } private void CreateSpatialAnchor() { var created = OVRPlugin.CreateSpatialAnchor(new OVRPlugin.SpatialAnchorCreateInfo { BaseTracking = OVRPlugin.GetTrackingOriginType(), PoseInSpace = GetTrackingSpacePose().ToPosef(), Time = OVRPlugin.GetTimeInSeconds(), }, out _requestId); OVRTelemetry.Client.MarkerStart(OVRTelemetryConstants.Scene.MarkerId.SpatialAnchorCreate, _requestId.GetHashCode()); if (created) { Development.LogRequest(_requestId, $"Creating spatial anchor..."); CreationRequests[_requestId] = this; } else { OVRTelemetry.Client.MarkerEnd(OVRTelemetryConstants.Scene.MarkerId.SpatialAnchorCreate, OVRPlugin.Qpl.ResultType.Fail, _requestId.GetHashCode()); Development.LogError( $"{nameof(OVRPlugin)}.{nameof(OVRPlugin.CreateSpatialAnchor)} failed. Destroying {nameof(OVRSpatialAnchor)} component."); Destroy(this); } } internal static bool TryGetPose(OVRSpace space, out OVRPose pose) { var tryLocateSpace = OVRPlugin.TryLocateSpace(space, OVRPlugin.GetTrackingOriginType(), out var posef, out var locationFlags); if (!tryLocateSpace || !locationFlags.IsOrientationValid() || !locationFlags.IsPositionValid()) { pose = OVRPose.identity; return false; } pose = posef.ToOVRPose(); var mainCamera = Camera.main; if (mainCamera) { pose = pose.ToWorldSpacePose(mainCamera); } return true; } private void UpdateTransform() { if (TryGetPose(Space, out var pose)) { transform.SetPositionAndRotation(pose.position, pose.orientation); } } private static bool TryExtractValue(Dictionary dict, TKey key, out TValue value) => dict.TryGetValue(key, out value) && dict.Remove(key); private struct MultiAnchorDelegatePair { public List Anchors; public Action, OperationResult> Delegate; } internal static readonly Dictionary SpatialAnchors = new Dictionary(); private static readonly Dictionary CreationRequests = new Dictionary(); private static readonly Dictionary> SaveRequests = new Dictionary> { { OVRSpace.StorageLocation.Cloud, new List() }, { OVRSpace.StorageLocation.Local, new List() }, }; private static readonly Dictionary AsyncRequestTaskIds = new Dictionary(); private static readonly List<(List, List)> ShareRequests = new List<(List, List)>(); private static readonly Dictionary MultiAnchorCompletionDelegates = new Dictionary(); private static readonly List UnboundAnchorBuffer = new List(); private static readonly OVRPlugin.SpaceComponentType[] ComponentTypeBuffer = new OVRPlugin.SpaceComponentType[32]; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void InitializeOnLoad() { CreationRequests.Clear(); MultiAnchorCompletionDelegates.Clear(); UnboundAnchorBuffer.Clear(); SpatialAnchors.Clear(); } static OVRSpatialAnchor() { OVRManager.SpatialAnchorCreateComplete += OnSpatialAnchorCreateComplete; OVRManager.SpaceSaveComplete += OnSpaceSaveComplete; OVRManager.SpaceListSaveComplete += OnSpaceListSaveComplete; OVRManager.ShareSpacesComplete += OnShareSpacesComplete; OVRManager.SpaceEraseComplete += OnSpaceEraseComplete; OVRManager.SpaceQueryComplete += OnSpaceQueryComplete; OVRManager.SpaceSetComponentStatusComplete += OnSpaceSetComponentStatusComplete; } private static void InvokeMultiAnchorDelegate(ulong requestId, OperationResult result, MultiAnchorActionType actionType) { if (!TryExtractValue(MultiAnchorCompletionDelegates, requestId, out var value)) { return; } if (actionType == MultiAnchorActionType.Save) { OVRTelemetry.Client.MarkerEnd(OVRTelemetryConstants.Scene.MarkerId.SpatialAnchorSave, result == OperationResult.Success ? OVRPlugin.Qpl.ResultType.Success : OVRPlugin.Qpl.ResultType.Fail, requestId.GetHashCode()); } value.Delegate?.Invoke(value.Anchors, result); try { foreach (var anchor in value.Anchors) { switch (actionType) { case MultiAnchorActionType.Save: { if (result != OperationResult.Success) { Development.LogError( $"[{anchor.Uuid}] {nameof(OVRPlugin)}.{nameof(OVRPlugin.SaveSpaceList)} failed with result: {result}."); } if (AsyncRequestTaskIds.TryGetValue(anchor, out var taskId)) { AsyncRequestTaskIds.Remove(anchor); OVRTask.GetExisting(taskId).SetResult(result == OperationResult.Success); } break; } case MultiAnchorActionType.Share: { if (result != OperationResult.Success) { Development.LogError( $"[{anchor.Uuid}] {nameof(OVRPlugin)}.{nameof(OVRPlugin.ShareSpaces)} failed with result: {result}."); } if (AsyncRequestTaskIds.TryGetValue(anchor, out var taskId)) { AsyncRequestTaskIds.Remove(anchor); OVRTask.GetExisting(taskId).SetResult(result); } break; } default: throw new ArgumentOutOfRangeException(nameof(actionType), actionType, null); } } } finally { OVRObjectPool.Return(value.Anchors); } } private static void OnSpatialAnchorCreateComplete(ulong requestId, bool success, OVRSpace space, Guid uuid) { Development.LogRequestResult(requestId, success, $"[{uuid}] Spatial anchor created.", $"Failed to create spatial anchor. Destroying {nameof(OVRSpatialAnchor)} component."); if (!TryExtractValue(CreationRequests, requestId, out var anchor)) return; OVRTelemetry.Client.MarkerEnd(OVRTelemetryConstants.Scene.MarkerId.SpatialAnchorCreate, success ? OVRPlugin.Qpl.ResultType.Success : OVRPlugin.Qpl.ResultType.Fail, requestId.GetHashCode()); if (success && anchor) { // All good; complete setup of OVRSpatialAnchor component. anchor.InitializeUnchecked(space, uuid); return; } if (success && !anchor) { // Creation succeeded, but the OVRSpatialAnchor component was destroyed before the callback completed. OVRPlugin.DestroySpace(space); } else if (!success && anchor) { // The OVRSpatialAnchor component exists but creation failed. Destroy(anchor); } // else if creation failed and the OVRSpatialAnchor component was destroyed, nothing to do. } private static void OnSpaceSaveComplete(ulong requestId, OVRSpace space, bool result, Guid uuid) { Development.LogRequestResult(requestId, result, $"[{uuid}] Saved.", $"[{uuid}] Save failed."); } private static void OnSpaceEraseComplete(ulong requestId, bool result, Guid uuid, OVRPlugin.SpaceStorageLocation location) { Development.LogRequestResult(requestId, result, $"[{uuid}] Erased.", $"[{uuid}] Erase failed."); } /// /// Options for loading unbound spatial anchors used by . /// /// /// This example shows how to create LoadOptions for loading anchors when given a set of UUIDs. /// /// OVRSpatialAnchor.LoadOptions options = new OVRSpatialAnchor.LoadOptions /// { /// StorageLocation = OVRSpace.StorageLocation.Local, /// Timeout = 0, /// Uuids = savedAnchorUuids /// }; /// /// public struct LoadOptions { /// /// The maximum number of uuids that may be present in the collection. /// public const int MaxSupported = OVRSpaceQuery.Options.MaxUuidCount; /// /// The storage location from which to query spatial anchors. /// public OVRSpace.StorageLocation StorageLocation { get; set; } /// /// (Obsolete) The maximum number of anchors to query. /// /// /// In prior SDK versions, it was mandatory to set this property to receive any /// results. However, this property is now obsolete. If is zero, /// i.e., the default initialized value, it will automatically be set to the count of /// . /// /// If non-zero, the number of anchors in the result will be limited to /// , preserving the previous behavior. /// [Obsolete( "This property is no longer required. MaxAnchorCount will be automatically set to the number of uuids to load.")] public int MaxAnchorCount { get; set; } /// /// The timeout, in seconds, for the query operation. /// /// /// A value of zero indicates no timeout. /// public double Timeout { get; set; } /// /// The set of spatial anchors to query, identified by their UUIDs. /// /// /// The UUIDs are copied by the method and no longer /// referenced internally afterwards. /// /// You must supply a list of UUIDs. will throw if this /// property is null. /// /// Thrown if contains more /// than elements. public IReadOnlyList Uuids { get => _uuids; set { if (value?.Count > OVRSpaceQuery.Options.MaxUuidCount) throw new ArgumentException( $"There must not be more than {MaxSupported} UUIDs (new value contains {value.Count} UUIDs).", nameof(value)); _uuids = value; } } private IReadOnlyList _uuids; internal OVRSpaceQuery.Options ToQueryOptions() => new OVRSpaceQuery.Options { Location = StorageLocation, #pragma warning disable CS0618 MaxResults = MaxAnchorCount == 0 ? Uuids?.Count ?? 0 : MaxAnchorCount, #pragma warning restore CS0618 Timeout = Timeout, UuidFilter = Uuids, QueryType = OVRPlugin.SpaceQueryType.Action, ActionType = OVRPlugin.SpaceQueryActionType.Load, }; } /// /// A spatial anchor that has not been bound to an . /// /// /// Use this object to bind an unbound spatial anchor to an . /// public readonly struct UnboundAnchor { internal readonly OVRSpace _space; /// /// The universally unique identifier associated with this anchor. /// public Guid Uuid { get; } /// /// Whether the anchor has been localized. /// /// /// Prior to localization, the anchor's cannot be determined. /// /// /// public bool Localized => OVRPlugin.GetSpaceComponentStatus(_space, OVRPlugin.SpaceComponentType.Locatable, out var enabled, out _) && enabled; /// /// Whether the anchor is in the process of being localized. /// /// /// public bool Localizing => OVRPlugin.GetSpaceComponentStatus(_space, OVRPlugin.SpaceComponentType.Locatable, out var enabled, out var pending) && !enabled && pending; /// /// The world space pose of the spatial anchor. /// public Pose Pose { get { if (!TryGetPose(_space, out var pose)) throw new InvalidOperationException( $"[{Uuid}] Anchor must be localized before obtaining its pose."); return new Pose(pose.position, pose.orientation); } } /// /// Localizes an anchor. /// /// /// The delegate supplied to receives an array of unbound /// spatial anchors. You can choose whether to localize each one and be notified when localization completes. /// /// The delegate receives two arguments: /// - `bool`: Whether localization was successful /// - : The anchor to bind /// /// Upon successful localization, your delegate should instantiate an , then bind /// the to the by calling /// . Once an is bound to an /// , it cannot be used again; that is, it cannot be bound to multiple /// components. /// /// A delegate invoked when localization completes (which may fail). The delegate /// receives two arguments: /// - : The anchor to bind /// - `bool`: Whether localization was successful /// /// The timeout, in seconds, to attempt localization, or zero to indicate no timeout. /// Thrown if /// - The anchor does not support localization, e.g., because it is invalid. /// - The anchor has already been localized. /// - The anchor is being localized, e.g., because was previously called. /// public void Localize(Action onComplete = null, double timeout = 0) { var task = LocalizeAsync(timeout); if (onComplete != null) { InvertedCapture.ContinueTaskWith(task, onComplete, this); } } private void ValidateLocalization() { if (!OVRPlugin.GetSpaceComponentStatus(_space, OVRPlugin.SpaceComponentType.Locatable, out var enabled, out var changePending)) throw new InvalidOperationException($"[{Uuid}] {nameof(UnboundAnchor)} does not support localization."); if (enabled) throw new InvalidOperationException($"[{Uuid}] Anchor has already been localized."); if (changePending) throw new InvalidOperationException($"[{Uuid}] Anchor is currently being localized."); } /// /// Localizes an anchor. /// /// /// The delegate supplied to receives an array of unbound /// spatial anchors. You can choose whether to localize each one and be notified when localization completes. /// /// Upon successful localization, your delegate should instantiate an , then bind /// the to the by calling /// . Once an is bound to an /// , it cannot be used again; that is, it cannot be bound to multiple /// components. /// /// The timeout, in seconds, to attempt localization, or zero to indicate no timeout. /// /// An with a boolean type parameter indicating the success of the localization. /// /// Thrown if /// - The anchor does not support localization, e.g., because it is invalid. /// - The anchor has already been localized. /// - The anchor is being localized, e.g., because was previously called. /// public OVRTask LocalizeAsync(double timeout = 0) { ValidateLocalization(); var setStatus = OVRPlugin.SetSpaceComponentStatus(_space, OVRPlugin.SpaceComponentType.Locatable, true, timeout, out var requestId); if (!setStatus) { Development.LogError($"[{Uuid}] {nameof(OVRPlugin.SetSpaceComponentStatus)} failed."); return OVRTask.FromResult(false); } Development.LogRequest(requestId, $"[{Uuid}] {nameof(OVRPlugin.SetSpaceComponentStatus)} enable {nameof(OVRPlugin.SpaceComponentType.Locatable)}."); AddStorableAndShareableComponents(); return OVRTask.FromRequest(requestId); } private void AddStorableAndShareableComponents() { OVRPlugin.SetSpaceComponentStatus(_space, OVRPlugin.SpaceComponentType.Storable, true, 0, out _); OVRPlugin.SetSpaceComponentStatus(_space, OVRPlugin.SpaceComponentType.Sharable, true, 0, out _); } /// /// Binds an unbound anchor to an component. /// /// /// Use this to bind an unbound anchor to an . After is used /// to bind an to an , the /// is no longer valid; that is, it cannot be bound to another /// . /// /// The component to which this unbound anchor should be bound. /// Thrown if this does not refer to a valid anchor. /// Thrown if is `null`. /// Thrown if an anchor is already bound to . /// Thrown if is pending creation (see ). /// Thrown if this is already bound to an . public void BindTo(OVRSpatialAnchor spatialAnchor) { if (!_space.Valid) throw new InvalidOperationException($"{nameof(UnboundAnchor)} does not refer to a valid anchor."); if (spatialAnchor == null) throw new ArgumentNullException(nameof(spatialAnchor)); if (spatialAnchor.Created) throw new ArgumentException( $"Cannot bind {Uuid} to {nameof(spatialAnchor)} because {nameof(spatialAnchor)} is already bound to {spatialAnchor.Uuid}.", nameof(spatialAnchor)); if (spatialAnchor.PendingCreation) throw new ArgumentException( $"Cannot bind {Uuid} to {nameof(spatialAnchor)} because {nameof(spatialAnchor)} is being used to create a new spatial anchor.", nameof(spatialAnchor)); ThrowIfBound(Uuid); spatialAnchor.InitializeUnchecked(_space, Uuid); } internal UnboundAnchor(OVRSpace space, Guid uuid) { _space = space; Uuid = uuid; } } /// /// Performs a query for anchors with the specified . /// /// /// Use this method to find anchors that were previously persisted with /// . The query is asynchronous; when the query completes, /// is invoked with an array of s for which tracking /// may be requested. /// /// Options that affect the query. /// A delegate invoked when the query completes. The delegate accepts one argument: /// - `UnboundAnchor[]`: An array of unbound anchors. /// /// If the operation fails, is invoked with `null`. /// Returns `true` if the operation could be initiated; otherwise `false`. /// Thrown if is `null`. /// Thrown if of is `null`. public static bool LoadUnboundAnchors(LoadOptions options, Action onComplete) { var task = LoadUnboundAnchorsAsync(options); task.ContinueWith(onComplete); return task.IsPending; } /// /// Performs a query for anchors with the specified . /// /// /// Use this method to find anchors that were previously persisted with /// . The query is asynchronous; when the query completes, /// the returned will contain an array of s for which tracking /// may be requested. /// /// Options that affect the query. /// /// An with a type parameter containing the loaded unbound anchors. /// /// Thrown if of is `null`. public static OVRTask LoadUnboundAnchorsAsync(LoadOptions options) { if (options.Uuids == null) { throw new InvalidOperationException($"{nameof(LoadOptions)}.{nameof(LoadOptions.Uuids)} must not be null."); } if (!options.ToQueryOptions().TryQuerySpaces(out var requestId)) { Development.LogError($"{nameof(OVRPlugin.QuerySpaces)} failed."); return OVRTask.FromResult(null); } Development.LogRequest(requestId, $"{nameof(OVRPlugin.QuerySpaces)}: Query created."); return OVRTask.FromRequest(requestId); } private static void OnSpaceQueryComplete(ulong requestId, bool queryResult) { Development.LogRequestResult(requestId, queryResult, $"{nameof(OVRPlugin.QuerySpaces)}: Query succeeded.", $"{nameof(OVRPlugin.QuerySpaces)}: Query failed."); var hasPendingTask = OVRTask.GetExisting(requestId).IsPending; if (!hasPendingTask) { return; } OVRTelemetry.Client.MarkerEnd(OVRTelemetryConstants.Scene.MarkerId.SpatialAnchorQuery, queryResult ? OVRPlugin.Qpl.ResultType.Success : OVRPlugin.Qpl.ResultType.Fail, requestId.GetHashCode()); if (!queryResult) { OVRTask.GetExisting(requestId).SetResult(null); return; } if (OVRPlugin.RetrieveSpaceQueryResults(requestId, out var results, Allocator.Temp)) { Development.Log( $"{nameof(OVRPlugin.RetrieveSpaceQueryResults)}({requestId}): Retrieved {results.Length} results."); } else { Development.LogError( $"{nameof(OVRPlugin.RetrieveSpaceQueryResults)}({requestId}): Failed to retrieve results."); OVRTask.GetExisting(requestId).SetResult(null); return; } using var disposer = results; UnboundAnchorBuffer.Clear(); foreach (var result in results) { PopulateUnbound(result.uuid, result.space); } Development.Log( $"Invoking callback with {UnboundAnchorBuffer.Count} unbound anchor{(UnboundAnchorBuffer.Count == 1 ? "" : "s")}."); var unboundAnchors = UnboundAnchorBuffer.Count == 0 ? Array.Empty() : UnboundAnchorBuffer.ToArray(); OVRTask.GetExisting(requestId).SetResult(unboundAnchors); } private static void PopulateUnbound(Guid uuid, UInt64 space) { if (SpatialAnchors.ContainsKey(uuid)) { Development.Log($"[{uuid}] Anchor is already bound to an {nameof(OVRSpatialAnchor)}. Ignoring."); return; } // See if it supports localization if (!OVRPlugin.EnumerateSpaceSupportedComponents(space, out var numSupportedComponents, ComponentTypeBuffer)) { Development.LogWarning($"[{uuid}] Unable to enumerate supported component types. Ignoring."); return; } var supportsLocatable = false; for (var i = 0; i < numSupportedComponents; i++) { supportsLocatable |= ComponentTypeBuffer[i] == OVRPlugin.SpaceComponentType.Locatable; } #if DEVELOPMENT_BUILD var supportedComponentTypesMsg = $"[{uuid}] Supports {numSupportedComponents} component type(s): {(numSupportedComponents == 0 ? "(none)" : string.Join(", ", ComponentTypeBuffer.Take((int)numSupportedComponents).Select(c => c.ToString())))}"; #endif if (!supportsLocatable) { #if DEVELOPMENT_BUILD Development.Log($"{supportedComponentTypesMsg} -- ignoring because it does not support localization."); #endif return; } if (!OVRPlugin.GetSpaceComponentStatus(space, OVRPlugin.SpaceComponentType.Locatable, out var enabled, out var changePending)) { #if DEVELOPMENT_BUILD Development.Log($"{supportedComponentTypesMsg} -- ignoring because failed to get localization."); #endif return; } #if DEVELOPMENT_BUILD Development.Log($"{supportedComponentTypesMsg}."); #endif Debug.Log($"{uuid}: locatable enabled? {enabled} changePending? {changePending}"); UnboundAnchorBuffer.Add(new UnboundAnchor(space, uuid)); } private static void OnSpaceSetComponentStatusComplete(ulong requestId, bool result, OVRSpace space, Guid uuid, OVRPlugin.SpaceComponentType componentType, bool enabled) { Development.LogRequestResult(requestId, result, $"[{uuid}] {componentType} {(enabled ? "enabled" : "disabled")}.", $"[{uuid}] Failed to set {componentType} status."); if (componentType == OVRPlugin.SpaceComponentType.Locatable && SpatialAnchors.TryGetValue(uuid, out var anchor)) { anchor.OnLocalize?.Invoke(enabled ? OperationResult.Success : OperationResult.Failure); } } private enum MultiAnchorActionType { Save, Share } private static void OnSpaceListSaveComplete(ulong requestId, OperationResult result) { Development.LogRequestResult(requestId, result >= 0, $"Spaces saved.", $"Spaces save failed with error {result}."); InvokeMultiAnchorDelegate(requestId, result, MultiAnchorActionType.Save); } private static void OnShareSpacesComplete(ulong requestId, OperationResult result) { Development.LogRequestResult(requestId, result >= 0, $"Spaces shared.", $"Spaces share failed with error {result}."); InvokeMultiAnchorDelegate(requestId, result, MultiAnchorActionType.Share); } private static class Development { [Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")] public static void Log(string message) => Debug.Log($"[{nameof(OVRSpatialAnchor)}] {message}"); [Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")] public static void LogWarning(string message) => Debug.LogWarning($"[{nameof(OVRSpatialAnchor)}] {message}"); [Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")] public static void LogError(string message) => Debug.LogError($"[{nameof(OVRSpatialAnchor)}] {message}"); #if DEVELOPMENT_BUILD private static readonly HashSet _requests = new HashSet(); #endif // DEVELOPMENT_BUILD [Conditional("DEVELOPMENT_BUILD")] public static void LogRequest(ulong requestId, string message) { #if DEVELOPMENT_BUILD _requests.Add(requestId); #endif // DEVELOPMENT_BUILD Log($"({requestId}) {message}"); } [Conditional("DEVELOPMENT_BUILD")] public static void LogRequestResult(ulong requestId, bool result, string successMessage, string failureMessage) { #if DEVELOPMENT_BUILD // Not a request we're tracking if (!_requests.Remove(requestId)) return; #endif // DEVELOPMENT_BUILD if (result) { Log($"({requestId}) {successMessage}"); } else { LogError($"({requestId}) {failureMessage}"); } } } /// /// Represents options for saving . /// public struct SaveOptions { /// /// Location where will be saved. /// public OVRSpace.StorageLocation Storage; } /// /// Represents options for erasing . /// public struct EraseOptions { /// /// Location from where will be erased. /// public OVRSpace.StorageLocation Storage; } public enum OperationResult { /// Operation succeeded. Success = 0, /// Operation failed. Failure = -1000, /// Saving anchors to cloud storage is not permitted by the user. Failure_SpaceCloudStorageDisabled = -2000, /// /// The user was able to download the anchors, but the device was unable to localize /// itself in the spatial data received from the sharing device. /// Failure_SpaceMappingInsufficient = -2001, /// /// The user was able to download the anchors, but the device was unable to localize them. /// Failure_SpaceLocalizationFailed = -2002, /// Network operation timed out. Failure_SpaceNetworkTimeout = -2003, /// Network operation failed. Failure_SpaceNetworkRequestFailed = -2004, } /// /// This struct helped inverting callback signature /// when using OVRTasks. OVRTasks expect Action{TResult, TCapture} signature /// but public API requires Action{TCapture, TResult} signature. /// private readonly struct InvertedCapture { private static readonly Action> Delegate = Invoke; private readonly TCapture _capture; private readonly Action _callback; private InvertedCapture(Action callback, TCapture capture) { _callback = callback; _capture = capture; } private static void Invoke(TResult result, InvertedCapture invertedCapture) { invertedCapture._callback?.Invoke(invertedCapture._capture, result); } public static void ContinueTaskWith(OVRTask task, Action onCompleted, TCapture state) { task.ContinueWith(Delegate, new InvertedCapture(onCompleted, state)); } } } public static class OperationResultExtensions { public static bool IsSuccess(this OVRSpatialAnchor.OperationResult res) => res == OVRSpatialAnchor.OperationResult.Success; public static bool IsError(this OVRSpatialAnchor.OperationResult res) => res < 0; public static bool IsWarning(this OVRSpatialAnchor.OperationResult res) => res > 0; } /// /// Represents a user for purposes of sharing scene anchors /// public struct OVRSpaceUser : IDisposable { internal ulong _handle; /// /// Checks if the user is valid /// public bool Valid => _handle != 0 && Id != 0; /// /// Creates a space user handle for given Facebook user ID /// /// The Facebook user ID obtained from the other party over the network public OVRSpaceUser(ulong spaceUserId) { OVRPlugin.CreateSpaceUser(spaceUserId, out _handle); } /// /// The user ID associated with this . /// public ulong Id => OVRPlugin.GetSpaceUserId(_handle, out var userId) ? userId : 0; /// /// Disposes of the . /// /// /// This method does not destroy the user account. It disposes the handle used to reference it. /// public void Dispose() { OVRPlugin.DestroySpaceUser(_handle); _handle = 0; } }