/* * 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; 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 associate with 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 associate an existing spatial anchor with this component, you will need the spatial anchor's space handle and its /// uuid, which can be obtained using . After adding this component to a GameObject, /// call before the next frame, i.e., before `Start` is called on this component. /// [DisallowMultipleComponent] public class OVRSpatialAnchor : MonoBehaviour { private bool _startCalled; private ulong _requestId; /// /// The space associated with this 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 this spatial anchor. /// /// /// UUIDs persist across sessions and applications. If you load a persisted anchor, you can use the UUID to identify /// it. /// public Guid Uuid { get; private set; } /// /// Whether the spatial anchor has been created. /// /// /// Creation is asynchronous and may take several frames. If creation fails, this component is destroyed. /// public bool Created => Space.Valid; /// /// Initializes this component from an existing space handle and uuid, e.g., the result of a call to /// . /// /// /// This method allows you to associate this component with an existing spatial anchor, e.g., 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."); if (!space.Valid) { Destroy(this); throw new ArgumentException($"Invalid space {space}.", nameof(space)); } 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 or application. Use the /// to identify the same at a future time. /// /// /// 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) { if (OVRPlugin.SaveSpace(Space, OVRPlugin.SpaceStorageLocation.Local, OVRPlugin.SpaceStoragePersistenceMode.Indefinite, out var requestId)) { Development.LogRequest(requestId, $"[{Uuid}] Saving spatial anchor..."); if (onComplete != null) { _completionDelegates[requestId] = new AnchorDelegatePair { Anchor = this, Delegate = onComplete }; } } else { Development.LogError($"[{Uuid}] {nameof(OVRPlugin)}.{nameof(OVRPlugin.SaveSpace)} failed."); onComplete?.Invoke(this, false); } } /// /// Erases the from persistent storage. /// /// /// This method is asynchronous; use to be notified of completion. /// /// /// 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) { if (OVRPlugin.EraseSpace(Space, OVRPlugin.SpaceStorageLocation.Local, out var requestId)) { Development.LogRequest(requestId, $"[{Uuid}] Erasing spatial anchor..."); if (onComplete != null) { _completionDelegates[requestId] = new AnchorDelegatePair { Anchor = this, Delegate = onComplete }; } } else { Development.LogError($"[{Uuid}] {nameof(OVRPlugin)}.{nameof(OVRPlugin.EraseSpace)} failed."); onComplete?.Invoke(this, false); } } // Initializes this component without checking preconditions private void InitializeUnchecked(OVRSpace space, Guid uuid) { Space = space; Uuid = uuid; OVRPlugin.SetSpaceComponentStatus(Space, OVRPlugin.SpaceComponentType.Locatable, true, 0, out _); OVRPlugin.SetSpaceComponentStatus(Space, OVRPlugin.SpaceComponentType.Storable, true, 0, out _); // Try to update the pose as soon as we can. UpdateTransform(); } private void Start() { _startCalled = true; if (Space.Valid) { Development.Log($"[{Space}] Created spatial anchor from existing space."); } else { CreateSpatialAnchor(); } } private void Update() { if (Space.Valid) { UpdateTransform(); } } private void OnDestroy() { if (Space.Valid) { OVRPlugin.DestroySpace(Space); } } 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() { if (OVRPlugin.CreateSpatialAnchor(new OVRPlugin.SpatialAnchorCreateInfo { BaseTracking = OVRPlugin.GetTrackingOriginType(), PoseInSpace = GetTrackingSpacePose().ToPosef(), Time = OVRPlugin.GetTimeInSeconds(), }, out _requestId)) { Development.LogRequest(_requestId, $"Creating spatial anchor..."); _creationRequests[_requestId] = this; } else { Destroy(this); Development.LogError($"{nameof(OVRPlugin)}.{nameof(OVRPlugin.CreateSpatialAnchor)} failed. Destroying {nameof(OVRSpatialAnchor)} component."); } } private void UpdateTransform() { if (!OVRPlugin.TryLocateSpace(Space, OVRPlugin.GetTrackingOriginType(), out var posef)) return; var pose = posef.ToOVRPose(); var mainCamera = Camera.main; if (mainCamera) { pose = pose.ToWorldSpacePose(mainCamera); } transform.SetPositionAndRotation(pose.position, pose.orientation); } private struct AnchorDelegatePair { public OVRSpatialAnchor Anchor; public Action Delegate; } private static readonly Dictionary _creationRequests = new Dictionary(); private static readonly Dictionary _completionDelegates = new Dictionary(); static OVRSpatialAnchor() { OVRManager.SpatialAnchorCreateComplete += OnSpatialAnchorCreateComplete; OVRManager.SpaceSaveComplete += OnSpaceSaveComplete; OVRManager.SpaceEraseComplete += OnSpaceEraseComplete; } private static void InvokeDelegate(ulong requestId, bool result) { if (_completionDelegates.TryGetValue(requestId, out var value)) { _completionDelegates.Remove(requestId); value.Delegate(value.Anchor, result); } } 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 (!_creationRequests.TryGetValue(requestId, out var anchor)) return; _creationRequests.Remove(requestId); if (success && anchor) { // All good; complete setup of OVRSpatialAnchor component. anchor.InitializeUnchecked(space, uuid); } else 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."); InvokeDelegate(requestId, result); } private static void OnSpaceEraseComplete(ulong requestId, bool result, Guid uuid, OVRPlugin.SpaceStorageLocation location) { Development.LogRequestResult(requestId, result, $"[{uuid}] Erased.", $"[{uuid}] Erase failed."); InvokeDelegate(requestId, result); } private static class Development { [Conditional("DEVELOPMENT_BUILD")] public static void Log(string message) => Debug.Log($"[{nameof(OVRSpatialAnchor)}] {message}"); [Conditional("DEVELOPMENT_BUILD")] public static void LogWarning(string message) => Debug.LogWarning($"[{nameof(OVRSpatialAnchor)}] {message}"); [Conditional("DEVELOPMENT_BUILD")] 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}"); } } } }