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. This page covers the following spatial anchor functionalities:
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.
Consider using the Mixed Reality Utility Kit World Locking feature
Consider using the Mixed Reality Utility Kit (MRUK) World Locking feature, which adds world-locking to virtual objects without you having to use anchors directly. For more advanced use cases, such as persistence or sharing, use spatial anchors.
Prerequisites
Hardware requirements
Development machine running one of the following:
Windows 10+ (64-bit)
macOS Sierra 10.10+ (x86 or ARM)
Supported Meta Quest headsets:
Quest 2
Quest Pro
Quest 3
Quest 3S and 3S Xbox Edition
Software requirements
Unity version 6.1 or later is recommended
Project setup
Before working with Spatial Anchors, make sure your project is set up for Meta Quest VR development. See Set up Unity for VR development for information on project setup.
Note: For a sample Unity project that you can use to explore spatial anchors, see Spatial Anchors Sample.
In order to use spatial anchors, you will also need to enable Anchor Support. This can be done in one of the following ways:
Option 1:
Update your app’s AndroidManifest.xml file to 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" />
Option 2:
Enable Anchor Support in the Unity Editor:
On the Hierarchy tab of your Unity Project, select the OVRCameraRig.
On the Inspector tab, enable the following settings:
OVRManager > Quest Features > General > Anchor Support
OVRManager > Quest Features > General > Anchor Sharing Support (Only needed if you intend to share anchors)
Implementation
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.
Create a spatial anchor
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.
Save a spatial anchor
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}");
}
}
Load a spatial anchor
You can load anchors that have been saved or shared with you. Anchors are loaded in three steps:
An unbound anchor represents an anchor instance that is 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.
Example
// 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}.");
}
}
Localize each anchor
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. Typically, 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, which means one argument is allowed to be null).
Destroy spatial anchors
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 persistent storage, you must erase the anchor.
If you previously persisted the anchor, you can reload the destroyed spatial anchor object by its UUID.
Example
This example is similar to the OnHideButtonPressed() action in the Anchor.cs script:
public void OnHideButtonPressed()
{
Destroy(this.gameObject);
}
Learn more
Continue learning about spatial anchors by reading these pages: