Spatial Anchors
Updated: Aug 2, 2024
Spatial anchors enable you to provide users with an environment that is consistent and familiar. Users expect objects they place or encounter to be in the same location the next time they enter the same space. On this page, the following spatial anchor functionalities will be covered:
- Create
- Save
- Load
- Erase
- Destroy
- Share
After reading this page you should be able to:
- Recognize the functionalities covered by spatial anchors such as create, save, load, erase, destroy, and share.
- Explain the lifecycle of a spatial anchor using the OVRSpatialAnchor Unity component.
- Describe the process of destroying a spatial anchor and its implications on system resources.
Mixed Reality Utility Kit
For most use cases, consider using the World Locking feature of
Mixed Reality Utility Kit (MRUK), which uses anchors to facilitate world-locking without you having to use anchors directly. It does this by subtly adjusting the HMD pose so that virtual objects appear to be world-locked in the physical environment, but you do not need to create (or interact with) anchors yourself.
There are some cases where you may find anchors useful, such as persistence or sharing. This document covers all aspects of spatial anchors: how to create, persist, and share them.
In order to use spatial anchors, your app’s AndroidManifest must include the following:
<!-- Anchors -->
<uses-permission android:name="com.oculus.permission.USE_ANCHOR_API" />
<!-- Only required for sharing -->
<uses-permission android:name="com.oculus.permission.IMPORT_EXPORT_IOT_MAP_DATA" />
The Core SDK can do this automatically by enabling the following settings:
- OVRManager > Quest Features > General > Anchor Support
- If you intend to share anchors also enable OVRManager > Quest Features > General > Anchor Sharing Support
OVRSpatialAnchor component
The
OVRSpatialAnchor
Unity component encapsulates a spatial anchor’s entire lifecycle, including creation, destruction, and persistence. Each spatial anchor has a unique identifier (UUID) that is assigned upon creation and remains constant throughout the life of the spatial anchor.
For a full working example, see the SpatialAnchor scene in the
Starter Samples.
To create a new spatial anchor, add the
OVRSpatialAnchor
component to any
GameObject
:
var anchor = gameObject.AddComponent<OVRSpatialAnchor>();
Once it is created, the new
OVRSpatialAnchor
is assigned a unique identifier (UUID) represented by a
System.Guid
in Unity, which you can use to load the spatial anchor after it has been persisted. In the frame following its instantiation, the
OVRSpatialAnchor
component uses its current transform to generate a new spatial anchor in the Meta Quest runtime. Because the creation of the spatial anchor is asynchronous, its UUID might not be valid immediately. Use the
Created
property on the
OVRSpatialAnchor
to ensure anchor creation has completed before you attempt to use it.
IEnumerator CreateSpatialAnchor()
{
var go = new GameObject();
var anchor = go.AddComponent<OVRSpatialAnchor>();
// Wait for the async creation
yield return new WaitUntil(() => anchor.Created);
Debug.Log($"Created anchor {anchor.Uuid}");
}
Once created, an
OVRSpatialAnchor
will update its transform automatically. Because anchors may drift slightly over time, this automatic update keeps the virtual transform world-locked.
Use the
SaveAnchorAsync
method to persist an anchor. This operation is also asynchronous:
public async void OnSaveButtonPressed(OVRSpatialAnchor anchor)
{
var result = await anchor.SaveAnchorAsync();
if (result.Success)
{
Debug.Log($"Anchor {anchor.Uuid} saved successfully.");
}
else
{
Debug.LogError($"Anchor {anchor.Uuid} failed to save with error {result.Status}");
}
}
You can also save a collection of anchors. This is more efficient than calling
SaveAnchorAsync
on each anchor individually:
async void SaveAnchors(IEnumerable<OVRSpatialAnchor> anchors)
{
var result = await OVRSpatialAnchor.SaveAnchorsAsync(anchors);
if (result.Success)
{
Debug.Log($"Anchors saved successfully.");
}
else
{
Debug.LogError($"Failed to save {anchors.Count()} anchor(s) with error {result.Status}");
}
}
You can load anchors that have been saved or shared with you. Anchors are loaded in three steps:
- Load unbound spatial anchors using their UUID
- Localize each spatial anchor
- Bind each spatial anchor to an
OVRSpatialAnchor
An unbound anchor represents an anchor instance that it not associated with an
OVRSpatialAnchor
component. The results of
LoadUnboundAnchorsAsync
only include anchors that have not already been bound to another
OVRSpatialAnchor
in the scene.
This intermediate representation allows you to access the anchor’s pose (position and orientation) before instantiating a GameObject or other content that relies on a correct pose. This avoids situations where you instantiate content at the origin only to have it “snap” to the correct pose on the following frame.
// This reusable buffer helps reduce pressure on the garbage collector
List<OVRSpatialAnchor.UnboundAnchor> _unboundAnchors = new();
async void LoadAnchorsByUuid(IEnumerable<Guid> uuids)
{
// Step 1: Load
var result = await OVRSpatialAnchor.LoadUnboundAnchorsAsync(uuids, _unboundAnchors);
if (result.Success)
{
Debug.Log($"Anchors loaded successfully.");
}
else
{
Debug.LogError($"Load failed with error {result.Status}.");
}
}
Localizing an anchor causes the system to determine the anchor’s pose in the world. Anchors should be localized before instantiating a GameObject or other content. Typcially, you should localize an unbound anchor, instantiate a GameObject+OVRSpatialAnchor, then bind the unbound anchor to it. This allows the anchor to be instantiated at the correct pose in the scene, rather than starting at the origin.
The term
localize is in the context of Simultaneous Localization and Mapping (
SLAM).
async void LoadAnchorsByUuid(IEnumerable<Guid> uuids)
{
// Step 1: Load
var result = await OVRSpatialAnchor.LoadUnboundAnchorsAsync(uuids, _unboundAnchors);
if (result.Success)
{
Debug.Log($"Anchors loaded successfully.");
// Note result.Value is the same as _unboundAnchors passed to LoadUnboundAnchorsAsync
foreach (var unboundAnchor in result.Value)
{
// Step 2: Localize
unboundAnchor.LocalizeAsync();
}
}
else
{
Debug.LogError($"Load failed with error {result.Status}.");
}
}
If you have content associated with the spatial anchor, you should make sure that you have localized the spatial anchor before instantiating its associated content. You may skip this step if you do not need the spatial anchor’s pose immediately.
foreach (var anchor in _unboundAnchors)
{
if (anchor.Localized)
{
Debug.Log("Anchor localized!");
}
}
LocalizeAsync
will immediately return with a successful result if the anchor is already localized.
Localization may fail if the spatial anchor is in a part of the environment that is not perceived or is poorly mapped. In that case, you can try to localize the spatial anchor at a later time. You might also consider guiding the user to look around their environment.
Bind each spatial anchor to an OVRSpatialAnchor In the third step, you bind a spatial anchor to its intended game object’s
OVRSpatialAnchor
component. Unbound spatial anchors should be bound to an
OVRSpatialAnchor
component to manage their lifecycle and to provide access to other features such as save and erase.
// This reusable buffer helps reduce pressure on the garbage collector
List<OVRSpatialAnchor.UnboundAnchor> _unboundAnchors = new();
async void LoadAnchorsByUuid(IEnumerable<Guid> uuids)
{
// Step 1: Load
var result = await OVRSpatialAnchor.LoadUnboundAnchorsAsync(uuids, _unboundAnchors);
if (result.Success)
{
Debug.Log($"Anchors loaded successfully.");
// Note result.Value is the same as _unboundAnchors
foreach (var unboundAnchor in result.Value)
{
// Step 2: Localize
unboundAnchor.LocalizeAsync().ContinueWith((success, anchor) =>
{
if (success)
{
// Create a new game object with an OVRSpatialAnchor component
var spatialAnchor = new GameObject($"Anchor {unboundAnchor.Uuid}")
.AddComponent<OVRSpatialAnchor>();
// Step 3: Bind
// Because the anchor has already been localized, BindTo will set the
// transform component immediately.
unboundAnchor.BindTo(spatialAnchor);
}
else
{
Debug.LogError($"Localization failed for anchor {unboundAnchor.Uuid}");
}
}, unboundAnchor);
}
}
else
{
Debug.LogError($"Load failed with error {result.Status}.");
}
}
If you create a new
OVRSpatialAnchor
but do not bind anything to it within the same frame, it will create a new spatial anchor. This allows the
OVRSpatialAnchor
to either create a new anchor or assume control of an existing anchor.
async void OnEraseButtonPressed()
{
var result = await _spatialAnchor.EraseAnchorAsync();
if (result.Success)
{
Debug.Log($"Successfully erased anchor.");
}
else
{
Debug.LogError($"Failed to erase anchor {_spatialAnchor.Uuid} with result {result.Status}");
}
}
Similar to saving, it is more efficient to erase a collection of anchors in a single batch:
async void OnEraseButtonPressed(IEnumerable<OVRSpatialAnchor> anchors)
{
var result = await OVRSpatialAnchor.EraseAnchorsAsync(anchors, null);
if (result.Success)
{
Debug.Log($"Successfully erased anchors.");
}
else
{
Debug.LogError($"Failed to erase anchors {anchors.Count()} with result {result.Status}");
}
}
You can erase anchors by instance (
OVRSpatialAnchor
) or by UUID. This means that you do not need to load an anchor into memory in order to erase it.
EraseAnchorsAsync
accepts two arguments: a collection of
OVRSpatialAnchor
and a collection of
Guid
. You may specify one or the other or both (i.e., one argument is allowed to be
null
).
When you destroy an
OVRSpatialAnchor
component, this causes the Meta Quest runtime to stop tracking the anchor, freeing CPU and memory resources.
Destroying a spatial anchor only destroys the runtime instance and does not affect spatial anchors in persistent storage. To remove an anchor from peristent storage, you must
erase the anchor.
If you previously persisted the anchor, you can reload the destroyed spatial anchor object by its UUID.
This example is similar to the OnHideButtonPressed()
action in the Anchor.cs
script:
public void OnHideButtonPressed()
{
Destroy(this.gameObject);
}
Continue learning about spatial anchors by reading these pages:
You can find more examples of using spatial anchors with Meta Quest in the oculus-samples GitHub repository: