using System; using System.Collections.Generic; using System.Linq; using System.Threading; using Unity.Collections; using UnityEngine.Serialization; using UnityEngine.XR.ARSubsystems; using Unity.XR.CoreUtils; using Unity.XR.CoreUtils.Collections; using UnityEngine.XR.ARFoundation.InternalUtils; using SerializableGuid = UnityEngine.XR.ARSubsystems.SerializableGuid; namespace UnityEngine.XR.ARFoundation { /// /// A [trackable manager](xref:arfoundation-managers#trackables-and-trackable-managers) that enables you to add and /// track anchors. Add this component to your XR Origin GameObject to enable anchor tracking in your app. /// /// /// An anchor is a pose (position and rotation) in the physical environment that is tracked by an XR device. /// Anchors are updated as the device refines its understanding of the environment, allowing you to reliably place /// mixed reality content at a physical pose. /// /// Related information: Anchors /// [DefaultExecutionOrder(ARUpdateOrder.k_AnchorManager)] [DisallowMultipleComponent] [RequireComponent(typeof(XROrigin))] [AddComponentMenu("XR/AR Foundation/AR Anchor Manager")] [HelpURL("features/anchors/aranchormanager")] public sealed class ARAnchorManager : ARTrackableManager< XRAnchorSubsystem, XRAnchorSubsystemDescriptor, XRAnchorSubsystem.Provider, XRAnchor, ARAnchor> { /// /// Invoked once per frame to communicate changes: new anchors, updates to existing /// anchors, and removed anchors. /// [Obsolete("anchorsChanged has been deprecated in AR Foundation version 6.0. Use trackablesChanged instead.", false)] public event Action anchorsChanged; Pool.ObjectPool>> m_AnchorCompletionSources = new( createFunc: () => new AwaitableCompletionSource>(), actionOnGet: null, actionOnRelease: null, actionOnDestroy: null, collectionCheck: false, defaultCapacity: 8, maxSize: 1024); [SerializeField] [Tooltip("If not null, this prefab is instantiated for each detected 3D bounding box.")] [FormerlySerializedAs("m_ReferencePointPrefab")] GameObject m_AnchorPrefab; static readonly Pool.ObjectPool> s_AnchorByTrackableIdMaps = ObjectPoolCreateUtil.Create>(); /// /// The name to assign to the GameObject instantiated for each . /// protected override string gameObjectName => "Anchor"; /// /// This prefab will be instantiated for each . May be . /// /// /// The purpose of this property is to extend the functionality of s. /// It is not the recommended way to instantiate content associated with an . /// public GameObject anchorPrefab { get => m_AnchorPrefab; set => m_AnchorPrefab = value; } internal bool TryAddAnchor(ARAnchor anchor) { if (!CanBeAddedToSubsystem(anchor)) return false; var t = anchor.transform; var sessionRelativePose = origin.TrackablesParent.InverseTransformPose(new Pose(t.position, t.rotation)); // Add the anchor to the XRAnchorSubsystem if (subsystem.TryAddAnchor(sessionRelativePose, out var sessionRelativeData)) { CreateTrackableFromExisting(anchor, sessionRelativeData); return true; } return false; } /// /// Attempts to create a new anchor at the given . /// /// /// Use this API with C# async/await syntax as shown below: /// /// var result = await TryAddAnchorAsync(pose); /// if (result.status.IsSuccess()) /// DoSomethingWith(result.value); /// /// /// The pose, in Unity world space, of the anchor. /// The result of the async operation. public async Awaitable> TryAddAnchorAsync(Pose pose) { var completionSource = m_AnchorCompletionSources.Get(); var sessionRelativePose = origin.TrackablesParent.InverseTransformPose(pose); var subsystemResult = await subsystem.TryAddAnchorAsync(sessionRelativePose); completionSource.SetResult(new Result( subsystemResult.status, CreateTrackableImmediate(subsystemResult.value))); var resultAwaitable = completionSource.Awaitable; completionSource.Reset(); m_AnchorCompletionSources.Release(completionSource); return await resultAwaitable; } /// /// Attempts to create a new anchor that is attached to an existing . /// /// The to which to attach. /// The initial pose, in Unity world space, of the anchor. /// A new if successful, otherwise . public ARAnchor AttachAnchor(ARPlane plane, Pose pose) { if (!enabled) throw new InvalidOperationException("Cannot create an anchor from a disabled anchor manager."); if (subsystem == null) throw new InvalidOperationException("Anchor manager has no subsystem. Enable the manager first."); if (plane == null) throw new ArgumentNullException(nameof(plane)); var sessionRelativePose = origin.TrackablesParent.InverseTransformPose(pose); if (subsystem.TryAttachAnchor(plane.trackableId, sessionRelativePose, out var sessionRelativeData)) { return CreateTrackableImmediate(sessionRelativeData); } return null; } /// /// Attempts to remove an anchor. /// /// The to remove. /// if successful, otherwise /// Thrown if is `null` public bool TryRemoveAnchor(ARAnchor anchor) { if (!enabled) throw new InvalidOperationException("Cannot remove an anchor from a disabled anchor manager."); if (anchor == null) throw new ArgumentNullException(nameof(anchor)); if (subsystem == null) return false; if (subsystem.TryRemoveAnchor(anchor.trackableId)) { anchor.pending = false; DestroyPendingTrackable(anchor.trackableId); return true; } return false; } /// /// Gets the with given , or if /// no such anchor exists. /// /// The of the to retrieve. /// The or . public ARAnchor GetAnchor(TrackableId trackableId) { if (m_Trackables.TryGetValue(trackableId, out var anchor)) return anchor; return null; } /// /// Get the prefab to instantiate for each . /// /// The prefab to instantiate for each . protected override GameObject GetPrefab() => m_AnchorPrefab; /// /// Attempts to persistently save the given anchor so that it can be loaded in a future AR session. Use the /// `SerializableGuid` returned by this method as an input to or /// . /// /// The anchor to save. You can create an anchor using . /// An optional `CancellationToken` that you can use to cancel the operation /// in progress if the loaded provider . /// The result of the async operation, containing a new persistent anchor GUID if the operation /// succeeded. You are responsible to this result. /// public Awaitable> TrySaveAnchorAsync( ARAnchor anchor, CancellationToken cancellationToken = default) { if (anchor == null) throw new ArgumentNullException(nameof(anchor)); return subsystem.TrySaveAnchorAsync(anchor.trackableId, cancellationToken); } /// /// Attempts to persistently save the given anchors so that they can be loaded in a future AR session. Use the /// from this method as an input to or /// . /// /// The anchors to save. You can create an anchor using . /// The list that will be cleared and then populated with results. /// An optional `CancellationToken` that you can use to cancel the operation /// in progress if the loaded provider . /// The result of the async operation. You are responsible to this result. /// /// Thrown if /// is false for this provider. /// Thrown if either or /// is null. public async Awaitable TrySaveAnchorsAsync( IEnumerable anchorsToSave, List outputSaveAnchorResults, CancellationToken cancellationToken = default) { if (anchorsToSave == null) throw new ArgumentNullException(nameof(anchorsToSave)); if (outputSaveAnchorResults == null) throw new ArgumentNullException(nameof(outputSaveAnchorResults)); outputSaveAnchorResults.Clear(); if (!anchorsToSave.Any()) return; var anchorIds = new NativeArray(anchorsToSave.Count(), Allocator.Temp); var anchorsByTrackableId = s_AnchorByTrackableIdMaps.Get(); var index = 0; foreach (var anchor in anchorsToSave) { anchorIds[index] = anchor.trackableId; anchorsByTrackableId.Add(anchor.trackableId, anchor); index += 1; } var xrSaveAnchorResults = await subsystem.TrySaveAnchorsAsync( anchorIds, Allocator.Temp, cancellationToken); for (var i = 0; i < xrSaveAnchorResults.Length; i += 1) { var saveAnchorResult = new ARSaveOrLoadAnchorResult { resultStatus = xrSaveAnchorResults[i].resultStatus, anchor = anchorsByTrackableId[xrSaveAnchorResults[i].trackableId], savedAnchorGuid = xrSaveAnchorResults[i].savedAnchorGuid, }; outputSaveAnchorResults.Add(saveAnchorResult); } anchorsByTrackableId.Clear(); s_AnchorByTrackableIdMaps.Release(anchorsByTrackableId); } /// /// Attempts to load an anchor given its persistent anchor GUID. /// /// A persistent anchor GUID created by . /// An optional `CancellationToken` that you can use to cancel the operation /// in progress if the loaded provider . /// The result of the async operation, containing the newly added anchor if the operation succeeded. /// You are responsible to this result. /// public async Awaitable> TryLoadAnchorAsync( SerializableGuid savedAnchorGuid, CancellationToken cancellationToken = default) { var completionSource = m_AnchorCompletionSources.Get(); var subsystemResult = await subsystem.TryLoadAnchorAsync(savedAnchorGuid, cancellationToken); completionSource.SetResult(new Result( subsystemResult.status, subsystemResult.status.IsSuccess() ? CreateTrackableImmediate(subsystemResult.value) : null)); var resultAwaitable = completionSource.Awaitable; completionSource.Reset(); m_AnchorCompletionSources.Release(completionSource); return await resultAwaitable; } /// /// Attempts to load a batch of anchors given their persistent anchor GUIDs. /// /// The persistent anchor GUIDs created by or /// . /// The list that will be populated with /// results as the runtime loads the anchors. /// A callback method that will be called when any requested /// anchors are loaded. This callback is invoked at least once if any anchors are successfully /// loaded, and possibly multiple times before the async operation completes. Pass a `null` argument for this /// parameter to ignore it. /// An optional `CancellationToken` that you can use to cancel the operation /// in progress if the loaded provider . /// The result of the async operation. You are responsible to this result. /// Thrown if /// is false for this provider. /// Thrown if the `NativeArray` of anchor GUIDs to load is empty. /// Thrown if the `IEnumerable` of saved anchor GUIDS is null or the output list for `LoadAnchorResult`s is null. /// /// The order in which anchors are loaded may not match the enumeration order of . To check /// if an anchor loaded successfully, check the . /// public async Awaitable TryLoadAnchorsAsync( IEnumerable savedAnchorGuidsToLoad, List outputLoadAnchorResults, Action> incrementalResultsCallback, CancellationToken cancellationToken = default) { if (savedAnchorGuidsToLoad == null) throw new ArgumentNullException(nameof(savedAnchorGuidsToLoad)); if (outputLoadAnchorResults == null) throw new ArgumentNullException(nameof(outputLoadAnchorResults)); if (!savedAnchorGuidsToLoad.Any()) return; var savedAnchorGuids = new NativeArray( savedAnchorGuidsToLoad.Count(), Allocator.Persistent); var index = 0; foreach (var savedAnchorGuid in savedAnchorGuidsToLoad) { savedAnchorGuids[index] = savedAnchorGuid; index += 1; } var completed = 0; var finalLoadAnchorResults = await subsystem.TryLoadAnchorsAsync( savedAnchorGuids, Allocator.Temp, xrLoadAnchorResults => { foreach (var xrLoadAnchorResult in xrLoadAnchorResults) { var loadAnchorResult = new ARSaveOrLoadAnchorResult { resultStatus = xrLoadAnchorResult.resultStatus, savedAnchorGuid = xrLoadAnchorResult.savedAnchorGuid, anchor = CreateTrackableImmediate(xrLoadAnchorResult.xrAnchor), }; outputLoadAnchorResults.Add(loadAnchorResult); } var loadAnchorResults = new ReadOnlyListSpan( outputLoadAnchorResults, completed, xrLoadAnchorResults.Length); incrementalResultsCallback?.Invoke(loadAnchorResults); completed = outputLoadAnchorResults.Count; }, cancellationToken); for (var i = outputLoadAnchorResults.Count; i < finalLoadAnchorResults.Length; i += 1) { var failedAnchorResult = new ARSaveOrLoadAnchorResult { resultStatus = finalLoadAnchorResults[i].resultStatus, savedAnchorGuid = finalLoadAnchorResults[i].savedAnchorGuid, anchor = null, }; outputLoadAnchorResults.Add(failedAnchorResult); } savedAnchorGuids.Dispose(); } /// /// Attempts to erase the persistent saved data associated with an anchor given its persistent anchor GUID. /// /// A persistent anchor GUID created by . /// An optional `CancellationToken` that you can use to cancel the operation /// in progress if the loaded provider . /// The result of the async operation. You are responsible to this result. /// public Awaitable TryEraseAnchorAsync( SerializableGuid savedAnchorGuid, CancellationToken cancellationToken = default) { return subsystem.TryEraseAnchorAsync(savedAnchorGuid, cancellationToken); } /// /// Attempts to erase the persistent saved data associated with a batch of anchors given their persistent anchor /// GUIDs. /// The persistent anchor GUIDs created by or /// . /// The output list that will be cleared and populated with `EraseAnchorResult`s. /// An optional `CancellationToken` that you can use to cancel the operation /// in progress if the loaded provider . /// The result of the async operation. You are responsible to this result. /// Thrown if /// is false for this provider. /// Thrown if the `IEnumerable` of anchor GUIDs to erase is null or the output list of `EraseAnchorResult`s is null. public async Awaitable TryEraseAnchorsAsync( IEnumerable savedAnchorGuidsToErase, List outputEraseAnchorResults, CancellationToken cancellationToken = default) { if (savedAnchorGuidsToErase == null) throw new ArgumentNullException(nameof(savedAnchorGuidsToErase)); if (outputEraseAnchorResults == null) throw new ArgumentNullException(nameof(outputEraseAnchorResults)); if (!savedAnchorGuidsToErase.Any()) return; var nativeSavedAnchorGuids = new NativeArray( savedAnchorGuidsToErase.Count(), Allocator.Persistent); var index = 0; foreach (var savedAnchorGuid in savedAnchorGuidsToErase) { nativeSavedAnchorGuids[index] = savedAnchorGuid; index += 1; } var eraseAnchorResults = await subsystem.TryEraseAnchorsAsync( nativeSavedAnchorGuids, Allocator.Temp, cancellationToken); outputEraseAnchorResults.Clear(); foreach (var eraseAnchorResult in eraseAnchorResults) { outputEraseAnchorResults.Add(eraseAnchorResult); } nativeSavedAnchorGuids.Dispose(); } /// /// Attempts to get a `NativeArray` containing all saved persistent anchor GUIDs. /// /// The allocation strategy to use for the resulting `NativeArray`. /// An optional `CancellationToken` that you can use to cancel the operation /// in progress if the loaded provider . /// The result of the async operation, containing a `NativeArray` of persistent anchor GUIDs /// allocated with the given if the operation succeeded. /// You are responsible to this result. /// public Awaitable>> TryGetSavedAnchorIdsAsync( Allocator allocator, CancellationToken cancellationToken = default) { return subsystem.TryGetSavedAnchorIdsAsync(allocator, cancellationToken); } /// /// Invoked when the base class detects trackable changes. /// /// The list of added anchors. /// The list of updated anchors. /// The list of removed anchors. [Obsolete("OnTrackablesChanged() has been deprecated in AR Foundation version 6.0.", false)] protected override void OnTrackablesChanged( List added, List updated, List removed) { if (anchorsChanged == null) return; using (new ScopedProfiler("OnAnchorsChanged")) { anchorsChanged?.Invoke(new ARAnchorsChangedEventArgs(added, updated, removed)); } } } }