Meta Spatial Anchors
Note
Meta spatial anchors are frequently used alongside Meta scene data, read the Meta Scene Manager tutorial for more info. Check out the Meta Scene Sample for a working demo of spatial anchors.
The OpenXRFbSpatialAnchorManager node provides an
easy-to-use way to interact with Meta's Spatial Anchors. This tutorial walks through the basic features
of the node such as placing/removing spatial anchors, and saving/loading them to/from a file.
To begin, ensure the use_anchor_api
property in Export Settings under Meta XR Features is enabled.
Then, add an OpenXRFbSpatialAnchorManager
node as a child of XROrigin3D.
Creating Spatial Anchors
To place spatial anchors, the manager node's scene
property should be set to the desired "spatial anchor scene" to be instantiated.
Let's make a basic scene for our spatial anchors. Create a new scene that inherits from Node3D and name it SpatialAnchor
.
Give that node a child MeshInstance3D and assign the mesh
property to be a CylinderMesh
like below:
Assign spatial_annchor.tscn
to your main scene's OpenXRFbSpatialAnchorManager
. Let's spawn a spatial anchor at the position of one of our
XRController3D nodes using its button_pressed
signal.
To spawn a spatial anchor, use the manager node's create_anchor method.
@onready var xr_controller_3d: XRController3D = $XROrigin3D/XRController3D
@onready var spatial_anchor_manager: OpenXRFbSpatialAnchorManager = $XROrigin3D/OpenXRFbSpatialAnchorManager
func _on_xr_controller_3d_button_pressed(name: String) -> void:
if name == "ax_button":
spatial_anchor_manager.create_anchor(xr_controller_3d.transform, {})
Scene Setup Method
The OpenXRFbSpatialAnchorManager
scene_setup_method property
is the name of the method that will be called on the spatial anchor scene immediately after instantiation. By default this is named setup_scene
.
This method should take an OpenXRFbSpatialEntity as an argument. Before creating that method,
let's ammend our create_anchor
method call with some custom data to modify the color of each cylinder we spawn.
Store the hex codes of potential colors in an array constant in the script.
const COLORS = [
"#FF0000", # Red
"#00FF00", # Green
"#0000FF", # Blue
]
func _on_xr_controller_3d_button_pressed(name: String) -> void:
if name == "ax_button":
var custom_data: Dictionary
custom_data.color = COLORS[randi() % COLORS.size()]
spatial_anchor_manager.create_anchor(xr_controller_3d.transform, custom_data)
Now we can apply that custom data in our spatial anchor scene's setup_scene
method. Attach a script to the root node of spatial_anchor.tscn
.
extends Node3D
@onready var mesh_instance_3d: MeshInstance3D = $MeshInstance3D
func setup_scene(spatial_entity: OpenXRFbSpatialEntity) -> void:
var data := spatial_entity.custom_data
var color := Color(data.get('color', '#FFFFFF'))
var material := StandardMaterial3D.new()
material.albedo_color = color
mesh_instance_3d.set_surface_override_material(0, material)
Removing Spatial Anchors
To remove a spatial anchor scene, use the untrack_anchor method.
This method accepts either an OpenXRFbSpatialEntity
object or its uuid
as an argument. To demonstrate, let's store the uuid
values
of the spatial anchors we create in an array. Connect the openxr_fb_spatial_anchor_tracked
signal from OpenXRFbSpatialAnchorManager
to an _on_anchor_tracked
method:
var anchor_uuids: Array[StringName] = []
func _on_anchor_tracked(anchor_node: XRAnchor3D, spatial_entity: OpenXRFbSpatialEntity, is_new: bool) -> void:
anchor_uuids.push_front(spatial_entity.uuid)
Then, in the function handling our XRController3D
button_pressed
signal, we can add the following code that will
untrack and remove the most recently placed spatial anchor scene.
func _on_xr_controller_3d_button_pressed(name: String) -> void:
...
elif name == "by_button":
var uuid = anchor_uuids.pop_front()
if spatial_anchor_manager.get_anchor_uuids().has(uuid):
spatial_anchor_manager.untrack_anchor(uuid)
Saving Spatial Anchors
The headset can store the positions and UUIDs of the spatial anchors between sessions, but not any custom information about them for your game,
such as the color in our tutorial project. Developers need to save that extra data somewhere, probably together with any other save data used by the project.
In this tutorial, we will store the spatial anchor data in a JSON file.
Create a save_spatial_anchors_to_file
method to handle this, along with a SPATIAL_ANCHORS_FILE
constant with the desired filepath.
const SPATIAL_ANCHORS_FILE = "user://openxr_fb_spatial_anchors.json"
func save_spatial_anchors_to_file() -> void:
var file := FileAccess.open(SPATIAL_ANCHORS_FILE, FileAccess.WRITE)
if not file:
print("ERROR: Unable to open file for writing: ", SPATIAL_ANCHORS_FILE)
return
var anchor_data: Dictionary
for uuid in spatial_anchor_manager.get_anchor_uuids():
var entity: OpenXRFbSpatialEntity = spatial_anchor_manager.get_spatial_entity(uuid)
anchor_data[uuid] = entity.custom_data
file.store_string(JSON.stringify(anchor_data))
file.close()
To keep this file up to date, this save method should be called whenever spatial anchors are created or removed.
This is easily done using the openxr_fb_spatial_anchor_tracked
and openxr_fb_spatial_anchor_untracked
signals from OpenXRFbSpatialAnchorManager
.
func _on_anchor_tracked(anchor_node: XRAnchor3D, spatial_entity: OpenXRFbSpatialEntity, is_new: bool) -> void:
if is_new:
save_spatial_anchors_to_file()
func _on_anchor_untracked(anchor_node: XRAnchor3D, spatial_entity: OpenXRFbSpatialEntity) -> void:
save_spatial_anchors_to_file()
Loading Spatial Anchors
Now that there is spatial anchor data to be loaded, we can use the OpenXRFbSpatialAnchorManager
load_anchors method.
The JSON file containing the data will need to be parsed and then passed to load_anchors
, so create a load_spatial_anchors_from_file
method to handle this.
func load_spatial_anchors_from_file() -> void:
var file := FileAccess.open(SPATIAL_ANCHORS_FILE, FileAccess.READ)
if not file:
return
var json := JSON.new()
if json.parse(file.get_as_text()) != OK:
print("ERROR: Unable to parse ", SPATIAL_ANCHORS_FILE)
return
if not json.data is Dictionary:
print("ERROR: ", SPATIAL_ANCHORS_FILE, " contains invalid data")
return
var anchor_data: Dictionary = json.data
if anchor_data.size() > 0:
spatial_anchor_manager.load_anchors(anchor_data.keys(), anchor_data, OpenXRFbSpatialEntity.STORAGE_LOCAL, true)
This method should be called after the OpenXR session has been initialized. OpenXRInterface
has a session_begun
signal that we can use for this like below:
func _ready():
var xr_interface: OpenXRInterface = XRServer.find_interface("OpenXR")
if xr_interface and xr_interface.is_initialized():
xr_interface.session_begun.connect(_on_openxr_session_begun)
func _on_openxr_session_begun() -> void:
load_spatial_anchors_from_file()