From fbd813f1747d9485d9871017f099bd0163be5279 Mon Sep 17 00:00:00 2001 From: Bastiaan Olij Date: Thu, 1 May 2025 13:15:15 +1000 Subject: [PATCH] Add documentation about OpenXR spatial entities --- tutorials/xr/img/openxr_plane_anchor.webp | Bin 0 -> 9258 bytes ...nxr_spatial_entities_project_settings.webp | Bin 0 -> 10582 bytes tutorials/xr/index.rst | 1 + tutorials/xr/openxr_spatial_entities.rst | 1710 +++++++++++++++++ 4 files changed, 1711 insertions(+) create mode 100644 tutorials/xr/img/openxr_plane_anchor.webp create mode 100644 tutorials/xr/img/openxr_spatial_entities_project_settings.webp create mode 100644 tutorials/xr/openxr_spatial_entities.rst diff --git a/tutorials/xr/img/openxr_plane_anchor.webp b/tutorials/xr/img/openxr_plane_anchor.webp new file mode 100644 index 0000000000000000000000000000000000000000..f37ce384a50be2029ef8341bd37b3d3428ae0119 GIT binary patch literal 9258 zcmV+_B-PteNk&E@Bme+cMM6+kP&iB#Bme*}r-3B^HHYH1Z6nG1Z=Li;{}C|(KFrw- z0Ow{qmOO{@Up?l0krx&8x;__AJt=Ay$#zsnBw^+t4BE_+KweQC{)OX_!ez27DUxKXwq^NQQcv+yH2>** zX6#k9nnEN=j%3T;bwy-6@wMbn;PX%fKAQeRf+V?Z({O@|W*wCY=zMt7wf=vZqg=!K zj!Tkv*4^FR-QC^Y;_iZ1?(PmPbnCnGE_*9U-u&ks++8|{YF(<_08&{+=k6}kA{8eU z?!5RF%-IsRE-mg-ZsqP3?k=rY?vRrz9j=|jWDj$;rdk~)xTm8`k3;2d)8VArqc?zG zfJ6ZRmBayyFetm5IFvfh_5!i>x;xh0+aGuL#-6ez$u@0k&M~^jm~$^2>x-{#+qP}{ z-eWyo+gkl+#7L5oo;%!~g@L~v`TrDIa*ng7dHraZnVFfEnVETqc@_pA4Kp+I%wvA9 z-|N_}?0Sp6pbIeBvgEO)OZ>&M#u7^}B6Wd{s4L7`GUqR{)*Vv}{~L%gBia)kfH_5F ziP5T%J0njX^AXY#n)2l_vxz0}#uBqd7MCpYp1rczWCbKia>F+N|7l7Amx~3sgyf3@ zNmAohl6=V$f?XCl=|2OvjTGgWGq-f`1L&`hY0~)qGnN1n|C2cEbkwV0EyfgOn#b@t zt}YzqB#D`cIgRE{+)Hi7cmi33KqF|63EdYgZV#9LD8#^%V^oYk7#X;f7-yHPnU=p4 zy!jz%I3j*jh8Z7vz+9PofdI07`cF`0^~>Wv4cEo#>FQNL>+pbH00se)7f5j+h8h7N zhNY!HLbG-a{Fxl0{i>?>bcQ22z{>HgyHzqq#LPK!$)aVHHv@p~&;}c7W{8j}6tg4F zXzn8I1S55&+7(sW3o>`YG#}IeR~9#z6l8f6CYT8G7?guNn%^qmHlPuN z%NS9#BXnICL3a@W1Ad#Sv7Y&K3MCrg*5=oX(5k&;3*TIy=m=z)K z3aM4|vo*RSl!o9RKJmc*q))p8O zx@tiX>^=6}!|J1XX{e#=3R!F|0M5{{x~z2|btaizPt-s=K}dB>3qlqT3gHsi8JEQ3 zkjRiooTZ|U44J{di%zMg@)NR@VSn8{eU9lr!N7((SL`Z%4qti~f2@|4afj5eVTHAI z|BzJTw+Ck5fsXg@C8QP|20GgJd|6Z3^t2|?ny>pQ%AdG&B8hvQ9HOI=X;;YKkRwJU z8sSwy6{57-Mu%(w6B+VMhmhT583N3!SjF0DL||1?Ybjk&%Y!ZG*(FIbSAe>Zzm}^@ zY1~ZG&s+lL$1HTo5FD5{92RMyn-Xz4kznD@mez<$i)LnEqFEKjL=U1=YHyAs&m^7qmc%4#I{VP< zmvd}lIPxqUTMGwwGwd&|OBGFTOq#jxL3$f+^f!JV9*QQ!*rx4%I2=)V!t8MDS;XKu z9IpF`Shjd3p&?Yq`sd;aOx0Xc^*UU0#s!|^rm5X~K`}s~ zG%p9_w41iZuG*g@ky4NZdDIvmgf{1w0L^C;>#0ocIq6vgd znZz;~5Sh&1Eu5RU28Xx9$;EJZD+3+9O${}EqMDnZcwb$hh5-cJt4 z+sU3!#N;^~F8iSfY0Xe?@6a_#0izP`H=mT-RNhqiizs_5mJrRZ)TW<&vcP~;sg@qR zZ&X+lKp$`$l6|9PViiKa|v+VX%;)Of8zhuux#5g;{12*on!+hh~3# zqT>Yu`{Cr$VyQWBY^@XT;N1_y>G{mms=U#|o|s9x_m8+;fmvdP0YK!SuQ}14_dJe@ zX?hOFjC;zE{Durs{`}ELEo>-Maw0R*0NM6c_AJhsWnUtURa^&!F`E`3rc}txYs}ko zyrAd^QKEzAV%*FE0HEvVV`L_dABH)Np®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 M>QX)CXDp);0PQ;qfdBvi literal 0 HcmV?d00001 diff --git a/tutorials/xr/img/openxr_spatial_entities_project_settings.webp b/tutorials/xr/img/openxr_spatial_entities_project_settings.webp new file mode 100644 index 0000000000000000000000000000000000000000..bbbac602c71982e7bf77fb78a944f6c38c481b14 GIT binary patch literal 10582 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 ZKGpuWnurr+WDuJno20+y+lTZh{|7+;PjvtQ literal 0 HcmV?d00001 diff --git a/tutorials/xr/index.rst b/tutorials/xr/index.rst index fe3fd661774..4af2fcda100 100644 --- a/tutorials/xr/index.rst +++ b/tutorials/xr/index.rst @@ -33,6 +33,7 @@ Advanced topics openxr_composition_layers openxr_hand_tracking openxr_body_tracking + openxr_spatial_entities Godot XR Tools -------------- diff --git a/tutorials/xr/openxr_spatial_entities.rst b/tutorials/xr/openxr_spatial_entities.rst new file mode 100644 index 00000000000..bf4fcbb9166 --- /dev/null +++ b/tutorials/xr/openxr_spatial_entities.rst @@ -0,0 +1,1710 @@ +.. _doc_openxr_spatial_entities: + +OpenXR spatial entities +======================= + +For any sort of augemented reality application you need to access real world information and be able to +track real world locations. OpenXRs spatial entities API was introduced for this exact purpose. + +It has a very modular design. The core of the API defines how real world entities are structured, +how they are found and how information about them is stored and accessed. + +Various extensions are added ontop that implement specific systems such as marker tracking, +plane tracking and anchors. These are refered to as spatial capabilities. + +Each entity that can be handled by the system is broken up into smaller components which makes it easy +to extend the system and add new capabilities. + +Vendors have the ability to implement and expose additional capabilities and component types that can be +used with the core API. For Godot these can be implemented in extensions. These implementations +however fall outside of the scope of this manual. + +Finally it is important to note that the spatial entity system makes use of asynchronous functions. +This means that you can start a process, and then get informed of it finishing later on. + +Setup +----- + +In order to use spatial entities you need to enable the related project settings. +You can find these in the OpenXR section: + +.. image:: img/openxr_spatial_entities_project_settings.webp + +.. list-table:: Spatial entity settings + :header-rows: 1 + + * - Setting + - Description + * - Enabled + - Enables the core of the spatial entities system. This must be enabled for any of the spatial + entities system to work. + * - Enable spatial anchors + - Enables the spatial anchors capability that allow use to create and track spatial anchors. + * - Enable persistent anchors + - Enables the ability to make spatial anchors persistent. This means that their location is stored + and can be retrieved in subsequent sessions. + * - Enabled built-in anchor detection + - Enables our built-in anchor detection logic, this will automatically retrieve persistent anchors + and adjust the positioning of anchors when tracking is updated. + * - Enable plane tracking + - Enables the plane tracking capability that allows detection of surfaces such as floors, walls, + ceilings and tables. + * - Enable built-in plane detection + - Enables our built-in plane detection logic, this will automatically react to new plane data + becoming available. + * - Enable marker tracking + - Enables our marker tracking capability that allows detection of markers such as QR codes, + Aruco markers and April tags. + * - Enables our built-in marker detection logic, this will automatically react to new markers being + found or markers being moved around the players space. + +.. note:: + Note that various XR devices also require permission flags to be set. These will need to be + enabled in the export preset settings. + +Enabling the different capabilities activates the related OpenXR APIs but additional logic is needed +to interact with this data. +For each core system we have built-in logic that can be enabled that will do this for you. + +We'll discuss the spatial entities system under the assumption the built-in logic is enabled first. +We will then take a look at the underlying APIs and how you can implement this yourself however it +should be noted that this is often overkill and that the underlying APIs are mostly exposed to allow +GDExtension plugins to implement additional capabilities. + +Creating our spatial manager +---------------------------- + +When spatial entities are detected or created a +:ref:`OpenXRSpatialEntityTracker` +object is instantiated and registered with the :ref:`XRServer`. + +Each type of spatial entity will implement its own subclass and we can thus react differently to +each type of entity. + +Generally speaking we will instance different subscenes for each type of entity. +As the tracker objects can be used with :ref:`XRAnchor3D` nodes these subscenes +should have such a node as their root node. + +All entity trackers will expose their location through the ``default`` pose. + +We can automate creating these subscenes and adding them to our scene tree by creating a manager +object. As all locations are local to the :ref:`XROrigin3D` node we should create +our manager as a child node of our origin node. + +Below is the basis of the script that implements our manager logic: + +.. code-block:: gdscript + + class_name SpatialEntitiesManager + extends Node3D + + ## Signals a new spatial entity node was added. + signal added_spatial_entity(node : XRNode3D) + + ## Signals a spatial entity node is about to be removed. + signal removed_spatial_entity(node : XRNode3D) + + ## Scene to instantiate for spatial anchor entities. + @export var spatial_anchor_scene : PackedScene + + ## Scene to instantiate for plane tracking spatial entities. + @export var plane_tracker_scene : PackedScene + + ## Scene to instantiate for mark tracking spatial entities. + @export var marker_tracker_scene : PackedScene + + # Trackers we manage nodes for + var _managed_nodes : Dictionary[OpenXRSpatialEntityTracker, XRAnchor3D] + + # Enter tree is called whenever our node is added into our scene. + func _enter_tree(): + # Connect to signals that inform us about tracker changes. + XRServer.tracker_added.connect(_on_tracker_added) + XRServer.tracker_updated.connect(_on_tracker_updated) + XRServer.tracker_removed.connect(_on_tracker_removed) + + # Setup existing trackers + var trackers : Dictionary = XRServer.get_trackers(XRServer.TRACKER_ANCHOR) + for tracker_name in trackers: + var tracker : XRTracker = trackers[tracker_name] + if tracker and tracker is OpenXRSpatialEntityTracker: + _add_tracker(tracker) + + + # Exit tree is called whenever our node is removed from out scene. + func _exit_tree(): + # Clean up our signals. + XRServer.tracker_added.disconnect(_on_tracker_added) + XRServer.tracker_updated.disconnect(_on_tracker_updated) + XRServer.tracker_removed.disconnect(_on_tracker_removed) + + # Clean up trackers + for tracker in _managed_nodes: + removed_spatial_entity.emit(_managed_nodes[tracker]) + remove_child(_managed_nodes[tracker]) + _managed_nodes[tracker].queue_free() + _managed_nodes.clear() + + + # See if this tracker should be managed by us and add it + func _add_tracker(tracker : OpenXRSpatialEntityTracker): + var new_node : XRAnchor3D + + if _managed_nodes.has(tracker): + # Already being managed by us! + return + + if tracker is OpenXRAnchorTracker: + # Note, generally spatial anchors are controlled by the developer and + # are unlikely to be handled by our manager. + # But just for completion we'll add it in. + if spatial_anchor_scene: + var new_scene = spatial_anchor_scene.instantiate() + if new_scene is XRAnchor3D: + new_node = new_scene + else: + push_error("Spatial anchor scene doesn't have an XRAnchor3D as a root node and can't be used!") + new_scene.free() + elif tracker is OpenXRPlaneTracker: + if plane_tracker_scene: + var new_scene = plane_tracker_scene.instantiate() + if new_scene is XRAnchor3D: + new_node = new_scene + else: + push_error("Plane tracking scene doesn't have an XRAnchor3D as a root node and can't be used!") + new_scene.free() + elif tracker is OpenXRMarkerTracker: + if marker_tracker_scene: + var new_scene = marker_tracker_scene.instantiate() + if new_scene is XRAnchor3D: + new_node = new_scene + else: + push_error("Marker tracking scene doesn't have an XRAnchor3D as a root node and can't be used!") + new_scene.free() + else: + # Type of spatial entity tracker we're not supporting? + push_warning("OpenXR Spatial Entities: Unsupported anchor tracker " + tracker.get_name() + " of type " + tracker.get_class()) + + if not new_node: + # No scene defined or able to be instantiated? We're done! + return + + # Setup and add to our scene. + new_node.tracker = tracker.name + new_node.pose = "default" + _managed_nodes[tracker] = new_node + add_child(new_node) + + added_spatial_entity.emit(new_node) + + + # A new tracker was added to our XRServer. + func _on_tracker_added(tracker_name: StringName, type: int): + if type == XRServer.TRACKER_ANCHOR: + var tracker : XRTracker = XRServer.get_tracker(tracker_name) + if tracker and tracker is OpenXRSpatialEntityTracker: + _add_tracker(tracker) + + + # A tracked managed by XRServer was changed. + func _on_tracker_updated(_tracker_name: StringName, _type: int): + # For now we ignore this, there aren't changes here we need to react + # to and the instanced scene can react to this itself if needed. + pass + + + # A tracker was removed from our XRServer. + func _on_tracker_removed(tracker_name: StringName, type: int): + if type == XRServer.TRACKER_ANCHOR: + var tracker : XRTracker = XRServer.get_tracker(tracker_name) + if _managed_nodes.has(tracker): + # We emit this right before we remove it! + removed_spatial_entity.emit(_managed_nodes[tracker]) + + # Remove the node. + remove_child(_managed_nodes[tracker]) + + # Queue free the node. + _managed_nodes[tracker].queue_free() + + # And remove from our managed nodes. + _managed_nodes.erase(tracker) + +Spatial anchors +--------------- + +Spatial anchors allow us to map real world location in our virtual world in such a way that the +XR runtime will keep track of these locations and adjust them as needed. +If supported anchors can be made persistent which means the anchors will be recreated in the correct +location when your application starts again. + +You can think of use cases such as: +- placing virtual windows around your space that are recreated when your application restarts +- placing virtual objects on your table or on your walls and have them recreated + +Spatial anchors are tracked using :ref:`OpenXRAnchorTracker` objects +registered with the XRServer. + +When needed the location of the spatial anchor will be updated automatically, the pose on the +related tracker will be updated and thus the :ref:`XRAnchor3D` node will +reposition. + +When a spatial anchor has been made persistent a Universally Unique Identifier (or UUID) is +assigned to the anchor. You will need to store this with whatever information you need to +reconstruct the scene. +In our example code below we'll simply call ``set_scene_path`` and ``get_scene_path`` but you +will need to supply your own implementations for these functions. + +In order to create a persistent anchor you need to follow a specific flow: +- Create the spatial anchor +- Wait until the tracking status changes to ``OPENXR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING`` +- Make the anchor persistent +- Obtain the UUID and save it + +When an existing persistent anchor is found a new tracker is added that has the UUID already +set. It is this difference in flow that allows us to correctly react to new and existing +persistent anchors. + +.. note:: + If you remove the persistence of an anchor, the UUID is destroyed but the anchor is not + removed automatically. You will need to react to the completion of making an anchor + "unpersistent" and then clean up the anchor. + Also you will get an error if you try to destroy an anchor that is still persistent. + +To complete our anchor system we start with creating a scene that we'll set as the scene +to instantiate for anchors on our spatial manager node. + +This scene should have an :ref:`XRAnchor3D` node as the root but nothing +else. We will add a script to it that will load a subscene that contains the actual visual +aspect of our anchor so we can create different anchors in our scene. +We'll assume the intention is to make these anchors persistent and save the path to this +subscene as meta data for our UUID. + +.. code-block:: gdscript + + class_name OpenXRSpatialAnchor3D + extends XRAnchor3D + + var anchor_tracker : OpenXRAnchorTracker + var child_scene : Node + var made_persistent : bool = false + + ## Return the scene path for our UUID + func get_scene_path(p_uuid: String) -> String: + # Placeholder, implement this. + return "" + + + ## Store our scene path for our UUID + func set_scene_path(p_uuid : String, p_scene_path : String): + # Placeholder, implement this. + pass + + + ## Remove info related to our UUID + func remove_uuid(p_uuid : String): + # Placeholder, implement this. + pass + + + ## Set our child scene for this anchor, call this when creating a new anchor + func set_child_scene(p_child_scene_path : String): + var packed_scene : PackedScene = load(p_child_scene_path) + if not packed_scene: + return + + child_scene = packed_scene.instantiate() + if not child_scene: + return + + add_child(child_scene) + + + # Called when our tracking state changes + func _on_spatial_tracking_state_changed(new_state) -> void: + if new_state == OpenXRSpatialEntityTracker.OPENXR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING and not made_persistent: + # Only attempt to do this once + made_persistent = true + + # This warning is optional if you don't want to rely on persistence. + if not OpenXRSpatialAnchorCapability.is_spatial_persistence_supported(): + push_warning("Persistent spatial anchors are not supported on this device!") + return + + # Make this persistent, this will callback UUID changed on the anchor, + # we can then store our scene path which we've already applied to our + # tracked scene. + OpenXRSpatialAnchorCapability.make_anchor_persistent(anchor_tracker, RID(), Callable()) + + + func _on_uuid_changed() -> void: + if anchor_tracker.uuid != "": + made_persistent = true + + if child_scene: + # If we already have a subscene, save that with the UUID. + set_scene_path(anchor_tracker.uuid, child_scene.scene_file_path) + else: + # If we do not, lookup the UUID in our stored cache. + var scene_path : String = get_scene_path(anchor_tracker.uuid) + if scene_path.is_empty(): + # Give a warning that we don't have a scene file stored for this UUID. + push_warning("Unknown UUID given, can't determine child scene") + + # Load a default scene so we can atleast see something. + set_child_scene("res://unknown_anchor.tscn") + return + + set_child_scene(scene_path) + + + func _ready(): + anchor_tracker = XRServer.get_tracker(tracker) + if anchor_tracker: + _on_uuid_changed() + + anchor_tracker.spatial_tracking_state_changed.connect(_on_spatial_tracking_state_changed) + anchor_tracker.uuid_changed.connect(_on_uuid_changed) + +With our anchor scene in place we can add a couple of function to our spatial manager script +to create or remove anchors: + +.. code-block:: gdscript + + ... + + ## Create a new spacial anchor with the associated child scene + ## If persistent anchors are supported, this will be created as a persistent node + ## and we will store the child scene path with the anchors UUID for future recreation. + func create_spacial_anchor(p_transform : Transform3D, p_child_scene_path : String): + # Do we have anchor support? + if not OpenXRSpatialAnchorCapability.is_spatial_anchor_supported(): + push_error("Spatial anchors are not supported on this device!") + return + + # Adjust our transform to local space + var t : Transform3D = global_transform.inverse() * p_transform + + # Create anchor on our current manager. + var new_anchor = OpenXRSpatialAnchorCapability.create_new_anchor(t, RID()) + if not new_anchor: + push_error("Couldn't create an anchor for %s." % [ p_child_scene_path ]) + return + + # Creating a new anchor should have resulted in an XRAnchor being added to the scene + # by our manager. We can thus continue assuming this has happened. + + var anchor_scene = get_tracked_scene(new_anchor) + if not anchor_scene: + push_error("Couldn't locate anchor scene for %s, has the manager been configured with an applicable anchor scene?" % [ new_anchor.name ]) + return + if not anchor_scene is OpenXRSpatialAnchor3D: + push_error("Anchor scene for %s is not a OpenXRSpatialAnchor3D scene, has the manager been configured with an applicable anchor scene?" % [ new_anchor.name ]) + return + + anchor_scene.set_child_scene(p_child_scene_path) + + + ## Removes this spatial anchor from our scene. + ## If the spatial anchor is persistant, the associated UUID will be cleared. + func remove_spacial_anchor(p_anchor : XRAnchor3D): + # Do we have anchor support? + if not OpenXRSpatialAnchorCapability.is_spatial_anchor_supported(): + push_error("Spatial anchors are not supported on this device!") + return + + var tracker : XRTracker = XRServer.get_tracker(p_anchor.tracker) + if tracker and tracker is OpenXRAnchorTracker: + var anchor_tracker : OpenXRAnchorTracker = tracker + if anchor_tracker.has_uuid() and OpenXRSpatialAnchorCapability.is_spatial_persistence_supported(): + # If we have a UUID we should first make the anchor unpersistent + # and then remove it on its callback. + remove_uuid(anchor_tracker.uuid) + OpenXRSpatialAnchorCapability.make_anchor_unpersistent(anchor_tracker, RID(), _on_unperistent_complete) + else: + # Else we can just remove it. + # This will remove it from the XRServer, which in turn will trigger cleaning up our node. + OpenXRSpatialAnchorCapability.remove_anchor(tracker) + + + func _on_unperistent_complete(p_tracker : XRTracker): + # Our tracker is now no longer persistent, we can remove it. + OpenXRSpatialAnchorCapability.remove_anchor(p_tracker) + + + ## Retrieve the scene we've added for a given tracker (if any). + func get_tracked_scene(p_tracker : XRTracker) -> XRNode3D: + for node in get_children(): + if node is XRNode3D and node.tracker == p_tracker.name: + return node + + return null + +.. note:: + There seems to be a bit of magic going on in the code above. + Whenever a spatial anchor is created or removed on our anchor capability, + the related tracker object is created or destroyed. + This results in the spatial manager adding or removing the child scene for this + anchor. Hence we can rely on this here. + +Plane tracking +-------------- + +Plane tracking allows us to detect surfaces such as walls, floors, ceilings and tables in +the players vicinity. This data could come from a room capture performed by the user at +any time in the past, or detected live by optical sensors. +The plane tracking extension doesn't make a distinction here. + +.. note:: + Some XR runtimes do require vendor extensions to enable and/or configure this process + but the data will be exposed through this extension. + +The code we write up above for the spatial manager will already detect our new planes. +We do need to set up a new scene and assign that scene to the spatial manager. + +The root node for this scene must be a :ref:`XRAnchor3D` node. +We'll add a :ref:`StaticBody3D` node as a child and add a +:ref:`CollisionShape3D` and :ref:`MeshInstance3D` +node as children of the static body. + +.. image:: img/openxr_plane_anchor.webp + +The static body and collision shape will allow us to make the plane interactable. + +The mesh instance node allows us to apply a "hole punch" material to the plane, +when combined with passthrough this turns our plane into a visual occluder. +Alternatively we can assign a material that will visualise the plane for debugging. + +We configure this material as the ``material_override`` material on our MeshInstance3D. +For our "hole punch" material create a :ref:`ShaderMaterial` +and use the following code as the shader code: + +.. code-block:: glsl + + shader_type spatial; + render_mode unshaded, shadow_to_opacity; + + void fragment() { + ALBEDO = vec3(0.0, 0.0, 0.0); + } + +We also need to add a script to our scene to ensure our collision and mesh are applied. + +.. code-block:: gdscript + + extends XRAnchor3D + + var plane_tracker : OpenXRPlaneTracker + + func _update_mesh_and_collision(): + if plane_tracker: + # Place our static body using our offset so both collision + # and mesh are positioned correctly + $StaticBody3D.transform = plane_tracker.get_mesh_offset() + + # Set our mesh so we can occlude the surface + $StaticBody3D/MeshInstance3D.mesh = plane_tracker.get_mesh() + + # And set our shape so we can have things collide things with our surface + $StaticBody3D/CollisionShape3D.shape = plane_tracker.get_shape() + + func _ready(): + plane_tracker = XRServer.get_tracker(tracker) + if plane_tracker: + _update_mesh_and_collision() + + plane_tracker.mesh_changed.connect(_update_mesh_and_collision) + +If supported by the XR runtime there is additional meta data you can query on the plane tracker +object. +Of specific note is the ``plane_label`` property that, if available, identifies the type of surface. +Please consult the :ref:`OpenXRPlaneTracker` class documentation for +further information. + +Marker tracking +--------------- + +Marker tracking detects specific markers in the real world. These are usually printed images such +as QR codes. + +The API exposes support for 4 different codes, QR codes, Micro QR codes, Aruco codes and April tags, +however XR runtimes are not required to support them all. + +When markers are detected :ref:`OpenXRMarkerTracker` objects are +instantiated and registered with the XRServer. + +Our existing spatial manager code already detects these, all we need to do is create a scene +with a :ref:`XRAnchor3D` node at the root, save this and assign it to the +spatial manager as the scene to instantiate for markers. + +The marker tracker should be fully configured when assigned so all that is neaded is a +``_ready`` function that reacts to the marker data. Below is a template for the +required code: + +.. code-block:: gdscript + + extends XRAnchor3D + + var marker_tracker : OpenXRMarkerTracker + + func _ready(): + marker_tracker = XRServer.get_tracker(tracker) + if marker_tracker: + match marker_tracker.marker_type: + OpenXRSpatialComponentMarkerList.OPENXR_MARKER_TYPE_QRCODE: + var data = marker_tracker.get_marker_data() + if data.type_of() == TYPE_STRING: + # Data is a QR code as a string, usually a URL + pass + elif data.type_of() == TYPE_PACKED_BYTE_ARRAY: + # Data is binary, can be anything + pass + OpenXRSpatialComponentMarkerList.OPENXR_MARKER_TYPE_MICRO_QRCODE: + var data = marker_tracker.get_marker_data() + if data.type_of() == TYPE_STRING: + # Data is a QR code as a string, usually a URL + pass + elif data.type_of() == TYPE_PACKED_BYTE_ARRAY: + # Data is binary, can be anything + pass + OpenXRSpatialComponentMarkerList.OPENXR_MARKER_TYPE_ARUCO: + # Use marker_tracker.marker_id to identify the marker + pass + OpenXRSpatialComponentMarkerList.OPENXR_MARKER_TYPE_APRIL_TAG: + # Use marker_tracker.marker_id to identify the marker + pass + +As we can see, QR Codes provide a data block that is either a string or byte array. +Aruco and April tags provide an id that is read from the code. + +It's up to your use case how best to link the marker data to the scene that needs to be loaded. +An example would be to encode the name of the asset you wish to display in a QR code. + +**Maybe see if we can work out atleast one example here, need access to hardware that supports this, should have that soon** + +Backend access +-------------- + +For most purposes the core system, along with any vendor extensions, should be what most +users would use as provided. + +For those who are implementing vendor extensions, or those for who the built-in logic doesn't +suffice, backend access is provided through a set of singleton objects. + +These objects can also be used to query what capabilities are supported by the headset in use. +We've already added code that checks for these in our spatial manager and spatial anchor code +in the sections above. + +.. note:: + The spatial entities system will encapsulate many OpenXR entities in resources that are + returned as RIDs. + +Spatial entity core +~~~~~~~~~~~~~~~~~~~ + +The core spatial entity functionality is exposed through the +:ref:`OpenXRSpatialEntityExtension` singleton. + +Specific logic is exposed through capabilities that introduce specialised component types +and give access to specific types of entities however they all use the same mechanisms +for accessing the entity data managed by the spatial entity system. + +We'll start by having a look at the individual components that make up the core system. + +Spatial contexts +"""""""""""""""" + +A spatial context is the main object through which we query the spatial entities system. +Spatial contexts allow us to configure how we interact with one of more capabilities. + +It's recommended to create a spatial context for each capability that you wish to interact +with, in fact, this is what Godot does for its built-in logic. + +We start by setting the capability configuration objects for the capabilities we wish to +access. +Each capability will enable those components we support for that capability. +Settings can determine which components will be enabled. +We'll look at these configuration objects in more detail as we look at each supported capacity. + +Creating a spatial context is an asynchronous action. This means we ask the XR runtime to +create a spatial context, and at a point in the future the XR runtime will provide us +with the result. + +The following script is the start of our example and can be added as a node to your scene. +It shows the creation of a spatial context for plane tracking, +and sets up our entity discovery. + +.. code-block:: gdscript + + extends Node + + var spatial_context : RID + + func _setup_spatial_context(): + # Already setup? + if spatial_context: + return + + # Not supported or we're not yet ready? + if not OpenXRSpatialPlaneTrackingCapability.is_supported(): + return + + # We'll use plane tracking as an example here, our configuration object + # here does not have any additional configuration. It just needs to exist. + var plane_capability : OpenXRSpatialCapabilityConfigurationPlaneTracking = OpenXRSpatialCapabilityConfigurationPlaneTracking.new() + + var future_result : OpenXRFutureResult = OpenXRSpatialEntityExtension.create_spatial_context([ plane_capability ]) + + # Wait for async completion. + await future_result.completed + + # Obtain our result. + spatial_context = future_result.get_spatial_context() + if spatial_context: + # connect to our discovery signal. + OpenXRSpatialEntityExtension.spatial_discovery_recommended.connect(_on_perform_discovery) + + # Perform our initial discovery. + _on_perform_discovery(spatial_context) + + func _enter_tree(): + var openxr_interface : OpenXRInterface = XRServer.find_interface("OpenXR") + if openxr_interface and openxr_interface.is_initialized(): + # Just in case our session hasn't started yet, + # call our spatial context creation on start. + openxr_interface.session_begun.connect(_setup_spatial_context) + + # And in case it is already up and running, call it already, + # it will exit if we've called it too early. + _setup_spatial_context() + + func _exit_tree(): + if spatial_context: + # disconnect from our discovery signal. + OpenXRSpatialEntityExtension.spatial_discovery_recommended.disconnect(_on_perform_discovery) + + # Free our spatial context, this will clean it up. + OpenXRSpatialEntityExtension.free_spatial_context(spatial_context) + spatial_context = RID() + + var openxr_interface : OpenXRInterface = XRServer.find_interface("OpenXR") + if openxr_interface and openxr_interface.is_initialized(): + openxr_interface.session_begun.disconnect(_setup_spatial_context) + + func _on_perform_discovery(p_spatial_context): + # see next section... + pass + +Discovery snapshots +""""""""""""""""""" + +Once our spatial context has been created the XR runtime will start managing spatial entities +according to the configuration of the specified capabilities. + +In order to find new entities or to get information about our current entities, we can create +a discovery snapshot. This will tell the XR runtime to gather specific data related to all +the spatial entities currently managed by the spatial context. + +This function is asynchronous as it may take some time to gather this data and offer its results. +Generally speaking you will want to perform a discovery snapshot when new entities are found. +OpenXR issues an event when there are new entities to be processed, this results in the +``spatial_discovery_recommended`` signal being issued by our +:ref:`OpenXRSpatialEntityExtension` singleton + +Note in the example code shown above, we're already connecting to this signal and calling the +``_on_perform_discovery`` method on our node. Let's implement this: + +.. code-block:: gdscript + + ... + + var discovery_result : OpenXRFutureResult + + func _on_perform_discovery(p_spatial_context): + # We get this signal for all spatial contexts, so exit if this is not for us + if p_spatial_context != spatial_context: + return + + # If we currently have an ongoing discovery result, cancel it. + if discovery_result: + discovery_result.cancel_discovery() + + # Perform our discovery + discovery_result = OpenXRSpatialEntityExtension.discover_spatial_entities(spatial_context, [ \ + OpenXRSpatialEntityExtension.OPENXR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D, \ + OpenXRSpatialEntityExtension.OPENXR_SPATIAL_COMPONENT_TYPE_PLANE_ALIGNMENT \ + ]) + + # Wait for async completion. + await discovery_result.completed + + var snapshot : RID = discovery_result.get_spatial_snapshot() + if snapshot: + # Process our snapshot result. + _process_snapshot(snapshot) + + # And cleanup our snapshot + OpenXRSpatialEntityExtension.free_spatial_snapshot(snapshot) + + func _process_snapshot(p_snapshot): + # see further down... + pass + + +Note that when calling ``discover_spatial_entities`` we specify a list of components. +The discovery query will find any entity that is managed by the spatial context and has +atleast one of the specified components. + +Update snapshots +"""""""""""""""" + +Performing an update snapshot allows us to get updated information about entities +we already found previously with our discovery snapshot. +This function is synchronous and is mainly meant to obtain status and positioning data +and can be run every frame. + +Generally speaking you would only perform update snapshots when its likely entities +change or have a lifetime process. A good example of this are persistent anchors and +markers. Consult the documentation around a capability to determine if this is needed. + +It is not needed for plane tracking however to complete our example, here is an example +of what an update snapshot would look like for plane tracking if we needed one: + +.. code-block:: gdscript + + ... + + func _process(_delta): + if not spatial_context: + return + + var entity_rids : Array[RID] + for entity_id in entities: + entity_rids.push_back(entities[entity_id].entity) + + var snapshot : RID = OpenXRSpatialEntityExtension.update_spatial_entities(spatial_context, entity_rids, [ \ + OpenXRSpatialEntityExtension.OPENXR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D, \ + OpenXRSpatialEntityExtension.OPENXR_SPATIAL_COMPONENT_TYPE_PLANE_ALIGNMENT \ + ]) + if snapshot: + # Process our snapshot. + _process_snapshot(snapshot) + + # And cleanup our snapshot. + OpenXRSpatialEntityExtension.free_spatial_snapshot(snapshot) + +Note that in our example here we're using the same ``_process_snapshot`` function to process the snapshot. +This makes sense in most situations. However if the components you've specified when creating the snapshot +are different between your discovery snapshot and/or your update snapshot, you have to take into account +that you query different components. + +Querying snapshots +"""""""""""""""""" + +Once we have a snapshot we can run queries over those snapshots to obtain the data held within. +The snapshot is guaranteed to remain unchanged until you free it. + +For each component we've added to our snapshot we have an accompanying data object. +This data object has a double function, adding it to your query ensures we query that component type, +and it is the object into which the queried data is loaded. + +There is one special data object that must always be added to our request list as the very first +entry and that is :ref:`OpenXRSpatialQueryResultData`. +This object will hold an entry for every returned entity with its unique id and the current state +of the entity. + +Completing our discovery logic we add the following: + +.. code-block:: gdscript + + ... + + var entities : Dictionary[int, OpenXRSpatialEntityTracker] + + func _process_snapshot(p_snapshot): + # Always include our query result data + var query_result_data : OpenXRSpatialQueryResultData = OpenXRSpatialQueryResultData.new() + + # Add in our bounded 2d component data + var bounded2d_list : OpenXRSpatialComponentBounded2DList = OpenXRSpatialComponentBounded2DList.new() + + # And our plane alignment component data + var alignment_list : OpenXRSpatialComponentPlaneAlignmentList = OpenXRSpatialComponentPlaneAlignmentList.new() + + if OpenXRSpatialEntityExtension.query_snapshot(p_snapshot, [ query_result_data, bounded2d_list, alignment_list]): + for i in query_result_data.get_entity_id_size(): + var entity_id = query_result_data.get_entity_id(i) + var entity_state = query_result_data.get_entity_state(i) + + if entity_state == OpenXRSpatialEntityTracker.OPENXR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED: + # This state should only appear when doing an update snapshot + # and tells us this entity is no longer tracked. + # We thus remove it from our dictionary which should result + # in the entity being cleaned up. + if entities.has(entity_id): + var entity_tracker : OpenXRSpatialEntityTracker = entities[entity_id] + entity_tracker.spatial_tracking_state = entity_state + XRServer.remove_tracker(entity_tracker) + entities.erase(entity_id) + else: + var entity_tracker : OpenXRSpatialEntityTracker + var register_with_xr_server : bool = false + if entities.has(entity_id): + entity_tracker = entities[entity_id] + else: + entity_tracker = OpenXRSpatialEntityTracker.new() + entity_tracker.entity = OpenXRSpatialEntityExtension.make_spatial_entity(spatial_context, entity_id) + entities[entity_id] = entity_tracker + register_with_xr_server = true + + # Copy the state + entity_tracker.spatial_tracking_state = entity_state + + # Only if we're tracking, we should query the rest of our components. + if entity_state == OpenXRSpatialEntityTracker.OPENXR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING: + var center_pose : Transform3D = bounded2d_list.get_center_pose(i) + entity_tracker.set_pose("default", center_pose, Vector3(), Vector3(), XRPose.XR_TRACKING_CONFIDENCE_HIGH) + + # For this example I'm using OpenXRSpatialEntityTracker which does not + # hold further data. You should subclass this class to store the additional + # state retrieved. For plane tracking this would be OpenXRPlaneTracker + # and we can store the following data in the tracker: + var size : Vector2 = bounded2d_list.get_size(i) + var alignment = alignment_list.get_plane_alignment(i) + else: + entity_tracker.invalidate_pose("default") + + # We don't register our tracker until after we've set our initial data. + if register_with_xr_server: + XRServer.add_tracker(entity_tracker) + +.. note:: + In the above example we're relying on ``OPENXR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED`` to clean up + spatial entities that are no longer being tracked. This is only available with update snapshots. + + For capabilities that only rely on discovery snapshots you may wish to do a cleanup based on + entities that are no longer part of the snapshot instead of relying on the state change. + +Spatial entities +"""""""""""""""" + +With the above information we now know how to query our spatial entities and get information about +them but there is a little more we need to look at when it comes to the entities themselves. + +In theory we're getting all our data from our snapshots however OpenXR has an extra API +where we create a spatial entity object from our entity id. +While this object exists the XR runtime knows that we are using this entity and that the +entity is not cleaned up early. This is a prerequisit for performing an update query on +this entity. + +In our example code we do so by calling ``OpenXRSpatialEntityExtension.make_spatial_entity``. + +Some spatial entity APIs will automatically create the object for us. +In this case we need to call ``OpenXRSpatialEntityExtension.add_spatial_entity`` to register +the created object with our implementation. + +Both functions return a RID that we can use in further functions that require our entity object. + +When we're done we can call ``OpenXRSpatialEntityExtension.free_spatial_entity``. + +Note that we didn't do so in our example code. This is automatically handled when our +:ref:`OpenXRSpatialEntityTracker` instance is destroyed. + +Spatial anchor capability +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Spatial anchors are managed by our :ref:`OpenXRSpatialAnchorCapability` +singleton object. +After the OpenXR session has been created you can call ``OpenXRSpatialAnchorCapability.is_spatial_anchor_supported`` +to check if the spatial anchor feature is supported on your hardware. + +The spatial anchor capability breaks the mold a little from what we've shown above. + +The spatial anchor system allows us to identify, track, persist and share a physical location. +What makes this different is that we're creating and destroying the anchor and are thus +managing its lifecycle. + +We thus only use the discovery system to discover anchors created and persisted in previous sessions, +or anchors shared with us. + +.. note:: + Sharing of anchors is currently not supported in the spatial entities specification. + +As we shown in our example before we always start with creating a spatial context but now using the +:ref:`OpenXRSpatialCapabilityConfigurationAnchor` +configuration object. +We'll show an example of this code after we discuss persistance scopes. +First we'll look at managing local anchors. + +There is no difference in creating spatial anchors from what we've discussed around the built-in +logic. The only important thing is to pass your own spatial context as a parameter to +``OpenXRSpatialAnchorCapability.create_new_anchor``. + +Making an anchor persistent requires you to wait until the anchor is tracking, this means that you +must perform update queries for any anchor you create so you can process state changes. + +In order to enable making anchors persistent you also have to setup a persistence scope. +In the core of OpenXR two types of persistence scopes are supported: + +.. list-table:: Persistence scopes + :header-rows: 1 + + * - Enum + - Description + * - OPENXR_SPATIAL_PERSISTENCE_SCOPE_SYSTEM_MANAGED + - Provides the application with read-only access (i.e. application cannot modify this store) + to spatial entities persisted and managed by the system. + The application can use the UUID in the persistence component for this store to correlate + entities across spatial contexts and device reboots. + * - OPENXR_SPATIAL_PERSISTENCE_SCOPE_LOCAL_ANCHORS + - Persistence operations and data access is limited to spatial anchors, on the same device, + for the same user and same app (using `make_anchor_persistent` and + `make_anchor_unpersistent` functions) + +We'll start with a new script that handles our spatial anchors. It will be similar to the +script presented earlier but with a few differences. + +The first being the creation of our persistence scope. + +.. code-block:: gdscript + + extends Node + + var persistence_context : RID + + func _setup_persistence_context(): + # Already setup? + if persistence_context: + # Check our spatial context + _setup_spatial_context() + return + + # Not supported or we're not yet ready? Just exit. + if not OpenXRSpatialAnchorCapability.is_spatial_anchor_supported(): + return + + # If we can't use a persistence scope, just create our spatial context without one. + if not OpenXRSpatialAnchorCapability.is_spatial_persistence_supported(): + _setup_spatial_context() + return + + var scope : int = 0 + if OpenXRSpatialAnchorCapability.is_persistence_scope_supported(OpenXRSpatialAnchorCapability.OPENXR_SPATIAL_PERSISTENCE_SCOPE_LOCAL_ANCHORS): + scope = OpenXRSpatialAnchorCapability.OPENXR_SPATIAL_PERSISTENCE_SCOPE_LOCAL_ANCHORS + elif OpenXRSpatialAnchorCapability.is_persistence_scope_supported(OpenXRSpatialAnchorCapability.OPENXR_SPATIAL_PERSISTENCE_SCOPE_SYSTEM_MANAGED): + scope = OpenXRSpatialAnchorCapability.OPENXR_SPATIAL_PERSISTENCE_SCOPE_SYSTEM_MANAGED + else: + # Don't have a known persistence stscopeore, report and just setup without it. + push_error("No known persistence scope is supported.") + _setup_spatial_context() + return + + # Create our persistence scope + var future_result : OpenXRFutureResult = OpenXRSpatialAnchorCapability.create_persistence_context(scope) + if not future: + # Couldn't create persistence scope? Just setup without it. + _setup_spatial_context() + return + + # Now wait for our + await future_result.completed + + # Get our result + persistence_context = future_result.get_result() + if persistence_context: + # Now setup our spatial context + _setup_spatial_context() + + func _enter_tree(): + var openxr_interface : OpenXRInterface = XRServer.find_interface("OpenXR") + if openxr_interface and openxr_interface.is_initialized(): + # Just in case our session hasn't started yet, + # call our context creation on start beginning with our persistence scope. + openxr_interface.session_begun.connect(_setup_persistence_context) + + # And in case it is already up and running, call it already, + # it will exit if we've called it too early. + _setup_persistence_context() + + func _exit_tree(): + if spatial_context: + # disconnect from our discovery signal. + OpenXRSpatialEntityExtension.spatial_discovery_recommended.disconnect(_on_perform_discovery) + + # Free our spatial context, this will clean it up. + OpenXRSpatialEntityExtension.free_spatial_context(spatial_context) + spatial_context = RID() + + if persistence_context: + # Free our persistence context... + OpenXRSpatialAnchorCapability.free_persistence_context(persistence_context) + persistence_context = RID() + + var openxr_interface : OpenXRInterface = XRServer.find_interface("OpenXR") + if openxr_interface and openxr_interface.is_initialized(): + openxr_interface.session_begun.disconnect(_setup_persistence_context) + +With our persistence scope created, we can now create our spatial context. + +.. code-block:: gdscript + + ... + + var spatial_context : RID + + func _setup_spatial_context(): + # Already setup? + if spatial_context: + return + + # Not supported or we're not yet setup. + if not OpenXRSpatialAnchorCapability.is_spatial_anchor_supported(): + return + + # Create our anchor capability + var anchor_capability : OpenXRSpatialCapabilityConfigurationAnchor = OpenXRSpatialCapabilityConfigurationAnchor.new() + + # And setup our persistence configuration object (if needed) + var persistence_config : OpenXRSpatialContextPersistenceConfig + if persistence_context: + persistence_config = OpenXRSpatialContextPersistenceConfig.new() + persistence_config.add_persistence_context(persistence_context) + + var future_result : OpenXRFutureResultg = OpenXRSpatialEntityExtension.create_spatial_context([ anchor_capability ], persistence_config) + + # Wait for async completion + await future_result.completed + + # Obtain our result + spatial_context = future_result.get_spatial_context() + if spatial_context: + # connect to our discovery signal + OpenXRSpatialEntityExtension.spatial_discovery_recommended.connect(_on_perform_discovery) + + # Perform our initial discovery. + _on_perform_discovery(spatial_context) + + +Creating our discovery snapshot for our anchors is nearly the same as we did before however it only makes sense +to create our snapshot for persistent anchors. We already know the anchors we created during our session, we +just want access to those coming from the XR runtime. + +We also want to perform regular update queries, here we are only interested in state so we do want to make +process our snapshot slightly differently. + +The anchor system gives us access to two components: + +.. list-table:: Anchor components + :header-rows: 1 + + * - Component + - Data class + - Description + * - OPENXR_SPATIAL_COMPONENT_TYPE_ANCHOR + - :ref:`OpenXRSpatialComponentAnchorList` + - Provides us with the pose (location + orientation) of each anchor + * - OPENXR_SPATIAL_COMPONENT_TYPE_PERSISTENCE + - :ref:`OpenXRSpatialComponentPersistenceList` + - Provides us with the persistence state and UUID of each anchor + +.. code-block:: gdscript + + ... + + var discovery_result : OpenXRFutureResult + var entities : Dictionary[int, OpenXRAnchorTracker] + + func _on_perform_discovery(p_spatial_context): + # We get this signal for all spatial contexts, so exit if this is not for us + if p_spatial_context != spatial_context: + return + + # Skip this if we don't have a persistence context + if not persistence_context: + return + + # If we currently have an ongoing discovery result, cancel it. + if discovery_result: + discovery_result.cancel_discovery() + + # Perform our discovery + discovery_result = OpenXRSpatialEntityExtension.discover_spatial_entities(spatial_context, [ \ + OpenXRSpatialEntityExtension.OPENXR_SPATIAL_COMPONENT_TYPE_ANCHOR, \ + OpenXRSpatialEntityExtension.OPENXR_SPATIAL_COMPONENT_TYPE_PERSISTENCE \ + ]) + + # Wait for async completion. + await discovery_result.completed + + var snapshot : RID = discovery_result.get_spatial_snapshot() + if snapshot: + # Process our snapshot result. + _process_snapshot(snapshot, true) + + # And cleanup our snapshot + OpenXRSpatialEntityExtension.free_spatial_snapshot(snapshot) + + func _process(_delta): + if not spatial_context: + return + + var entity_rids : Array[RID] + for entity_id in entities: + entity_rids.push_back(entities[entity_id].entity) + + # We just want our anchor component here. + var snapshot : RID = OpenXRSpatialEntityExtension.update_spatial_entities(spatial_context, entity_rids, [ \ + OpenXRSpatialEntityExtension.OPENXR_SPATIAL_COMPONENT_TYPE_ANCHOR, \ + ]) + if snapshot: + # Process our snapshot. + _process_snapshot(snapshot) + + # And cleanup our snapshot. + OpenXRSpatialEntityExtension.free_spatial_snapshot(snapshot) + + func _process_snapshot(p_snapshot, p_get_uuids): + pass + + +Finally we can process our snapshot. Note that we are using :ref:`OpenXRAnchorTracker` +as our tracker class as this already has all the support for anchors build in. + +.. code-block:: gdscript + + ... + + func _process_snapshot(p_snapshot, p_get_uuids): + var result_data : Array + + # Always include our query result data + var query_result_data : OpenXRSpatialQueryResultData = OpenXRSpatialQueryResultData.new() + result_data.push_back(query_result_data) + + # Add in our anchor component data + var anchor_list : OpenXRSpatialComponentAnchorList = OpenXRSpatialComponentAnchorList.new() + result_data.push_back(anchor_list) + + # And our persistent component data + var persistent_list : OpenXRSpatialComponentPersistenceList + if p_get_uuids: + # Only add this when we need it + persistent_list = OpenXRSpatialComponentPersistenceList.new() + result_data.push_back(persistent_list) + + if OpenXRSpatialEntityExtension.query_snapshot(p_snapshot, result_data): + for i in query_result_data.get_entity_id_size(): + var entity_id = query_result_data.get_entity_id(i) + var entity_state = query_result_data.get_entity_state(i) + + if entity_state == OpenXRSpatialEntityTracker.OPENXR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED: + # This state should only appear when doing an update snapshot + # and tells us this entity is no longer tracked. + # We thus remove it from our dictionary which should result + # in the entity being cleaned up. + if entities.has(entity_id): + var entity_tracker : OpenXRAnchorTracker = entities[entity_id] + entity_tracker.spatial_tracking_state = entity_state + XRServer.remove_tracker(entity_tracker) + entities.erase(entity_id) + else: + var entity_tracker : OpenXRAnchorTracker + var register_with_xr_server : bool = false + if entities.has(entity_id): + entity_tracker = entities[entity_id] + else: + entity_tracker = OpenXRAnchorTracker.new() + entity_tracker.entity = OpenXRSpatialEntityExtension.make_spatial_entity(spatial_context, entity_id) + entities[entity_id] = entity_tracker + register_with_xr_server = true + + # Copy the state + entity_tracker.spatial_tracking_state = entity_state + + # Only if we're tracking, we update our position. + if entity_state == OpenXRSpatialEntityTracker.OPENXR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING: + var anchor_transform = anchor_list.get_entity_pose(i) + entity_tracker.set_pose("default", anchor_transform, Vector3(), Vector3(), XRPose.XR_TRACKING_CONFIDENCE_HIGH) + else: + entity_tracker.invalidate_pose("default") + + # But persistence data is be big exception, it can be provided even if we're not tracking. + if p_get_uuids: + var persistent_state = persistent_list.get_persistent_state(i) + if persistent_state == 1: + entity_tracker.uuid = persistent_list.get_persistent_uuid(i) + + # We don't register our tracker until after we've set our initial data. + if register_with_xr_server: + XRServer.add_tracker(entity_tracker) + +Plane tracking capability +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Plane tracking is handled by the +:ref:`OpenXRSpatialPlaneTrackingCapability` +singleton class. + +After the OpenXR session has been created you can call ``OpenXRSpatialPlaneTrackingCapability.is_supported`` +to check if the plane tracking feature is supported on your hardware. + +While we've provided most of the code for plane tracking up above, we'll present the full implementation below +as it has a few small tweaks. +There is no need for update snapshots here, we just do our discovery snapshot and implement our process function. + +Plane tracking gives access to two components that are guaranteed to be supported, and three optional components. + +.. list-table:: Plane tracking components + :header-rows: 1 + + * - Component + - Data class + - Description + * - OPENXR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D + - :ref:`OpenXRSpatialComponentBounded2DList` + - Provides us with the center pose and bounding rectangle for each plane + * - OPENXR_SPATIAL_COMPONENT_TYPE_PLANE_ALIGNMENT + - :ref:`OpenXRSpatialComponentPlaneAlignmentList` + - Provides us with the alignment of each plane + * - OPENXR_SPATIAL_COMPONENT_TYPE_MESH_2D + - :ref:`OpenXRSpatialComponentMesh2DList` + - Provides us with a 2D mesh that shapes each plane + * - OPENXR_SPATIAL_COMPONENT_TYPE_POLYGON_2D + - :ref:`OpenXRSpatialComponentPolygon2DList` + - Provides us with a 2D polygon that shapes each plane + * - OPENXR_SPATIAL_COMPONENT_TYPE_PLANE_SEMANTIC_LABEL + - :ref:`OpenXRSpatialComponentPlaneSemanticLabelList` + - Provides us with a type identification of each plane + +Our plane tracking configuration object already enables all supported components but we'll need to interogate +it so we'll store our instance in a member variable. +We can use our :ref:`OpenXRPlaneTracker` tracker object to store our component data. + +.. code-block:: gdscript + + extends Node + + var plane_capability : OpenXRSpatialCapabilityConfigurationPlaneTracking + var spatial_context : RID + var discovery_result : OpenXRFutureResult + var entities : Dictionary[int, OpenXRPlaneTracker] + + func _setup_spatial_context(): + # Already setup? + if spatial_context: + return + + # Not supported or we're not yet ready? + if not OpenXRSpatialPlaneTrackingCapability.is_supported(): + return + + # We'll use plane tracking as an example here, our configuration object + # here does not have any additional configuration. It just needs to exist. + plane_capability = OpenXRSpatialCapabilityConfigurationPlaneTracking.new() + + var future_result : OpenXRFutureResult = OpenXRSpatialEntityExtension.create_spatial_context([ plane_capability ]) + + # Wait for async completion. + await future_result.completed + + # Obtain our result. + spatial_context = future_result.get_spatial_context() + if spatial_context: + # connect to our discovery signal. + OpenXRSpatialEntityExtension.spatial_discovery_recommended.connect(_on_perform_discovery) + + # Perform our initial discovery. + _on_perform_discovery(spatial_context) + + func _enter_tree(): + var openxr_interface : OpenXRInterface = XRServer.find_interface("OpenXR") + if openxr_interface and openxr_interface.is_initialized(): + # Just in case our session hasn't started yet, + # call our spatial context creation on start. + openxr_interface.session_begun.connect(_setup_spatial_context) + + # And in case it is already up and running, call it already, + # it will exit if we've called it too early. + _setup_spatial_context() + + func _exit_tree(): + if spatial_context: + # disconnect from our discovery signal. + OpenXRSpatialEntityExtension.spatial_discovery_recommended.disconnect(_on_perform_discovery) + + # Free our spatial context, this will clean it up. + OpenXRSpatialEntityExtension.free_spatial_context(spatial_context) + spatial_context = RID() + + var openxr_interface : OpenXRInterface = XRServer.find_interface("OpenXR") + if openxr_interface and openxr_interface.is_initialized(): + openxr_interface.session_begun.disconnect(_setup_spatial_context) + + func _on_perform_discovery(p_spatial_context): + # We get this signal for all spatial contexts, so exit if this is not for us + if p_spatial_context != spatial_context: + return + + # If we currently have an ongoing discovery result, cancel it. + if discovery_result: + discovery_result.cancel_discovery() + + # Perform our discovery + discovery_result = OpenXRSpatialEntityExtension.discover_spatial_entities(spatial_context, \ + plane_capability.get_enabled_components()) + + # Wait for async completion. + await discovery_result.completed + + var snapshot : RID = discovery_result.get_spatial_snapshot() + if snapshot: + # Process our snapshot result. + _process_snapshot(snapshot) + + # And cleanup our snapshot + OpenXRSpatialEntityExtension.free_spatial_snapshot(snapshot) + + func _process_snapshot(p_snapshot): + var result_data : Array + + # Make a copy of the entities we've currently found + var org_entities : PackedInt64Array + for entity_id in entities: + org_entities.push_back(entity_id) + + # Always include our query result data + var query_result_data : OpenXRSpatialQueryResultData = OpenXRSpatialQueryResultData.new() + result_data.push_back(query_result_data) + + # Add in our bounded 2d component data. + var bounded2d_list : OpenXRSpatialComponentBounded2DList = OpenXRSpatialComponentBounded2DList.new() + result_data.push_back(bounded2d_list) + + # And our plane alignment component data. + var alignment_list : OpenXRSpatialComponentPlaneAlignmentList = OpenXRSpatialComponentPlaneAlignmentList.new() + result_data.push_back(alignment_list) + + # We need either mesh2d or polygon2d, we don't need both. + var mesh2d_list : OpenXRSpatialComponentMesh2DList + var polygon2d_list : OpenXRSpatialComponentPolygon2DList + if plane_capability.get_supports_mesh_2d(): + mesh2d_list = OpenXRSpatialComponentMesh2DList.new() + result_data.push_back(mesh2d_list) + elif plane_capability.get_supports_polygons(): + polygon2d_list = OpenXRSpatialComponentPolygon2DList.new() + result_data.push_back(polygon2d_list) + + # And add our semantic labels if supported. + var label_list : OpenXRSpatialComponentPlaneSemanticLabelList + if plane_capability.get_supports_labels(): + label_list = OpenXRSpatialComponentPlaneSemanticLabelList.new() + result_data.push_back(label_list) + + if OpenXRSpatialEntityExtension.query_snapshot(p_snapshot, result_data): + for i in query_result_data.get_entity_id_size(): + var entity_id = query_result_data.get_entity_id(i) + var entity_state = query_result_data.get_entity_state(i) + + # Remove the entity from our original list + if org_entities.has(entity_id): + org_entities.erase(entity_id) + + if entity_state == OpenXRSpatialEntityTracker.OPENXR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED: + # We're not doing update snapshots so we shouldn't get this, + # but just to future proof: + if entities.has(entity_id): + var entity_tracker : OpenXRPlaneTracker = entities[entity_id] + entity_tracker.spatial_tracking_state = entity_state + XRServer.remove_tracker(entity_tracker) + entities.erase(entity_id) + else: + var entity_tracker : OpenXRPlaneTracker + var register_with_xr_server : bool = false + if entities.has(entity_id): + entity_tracker = entities[entity_id] + else: + entity_tracker = OpenXRPlaneTracker.new() + entity_tracker.entity = OpenXRSpatialEntityExtension.make_spatial_entity(spatial_context, entity_id) + entities[entity_id] = entity_tracker + register_with_xr_server = true + + # Copy the state + entity_tracker.spatial_tracking_state = entity_state + + # Only if we're tracking, we should query the rest of our components. + if entity_state == OpenXRSpatialEntityTracker.OPENXR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING: + var center_pose : Transform3D = bounded2d_list.get_center_pose(i) + entity_tracker.set_pose("default", center_pose, Vector3(), Vector3(), XRPose.XR_TRACKING_CONFIDENCE_HIGH) + + entity_tracker.bounds_size = bounded2d_list.get_size(i) + entity_tracker.plane_alignment = alignment_list.get_plane_alignment(i) + + if mesh2d_list: + entity_tracker.set_mesh_data( \ + mesh2d_list.get_transform(i), \ + mesh2d_list.get_vertices(p_snapshot, i), \ + mesh2d_list.get_indices(p_snapshot, i)) + elif polygon2d_list: + # logic in our tracker will convert polygon to mesh + entity_tracker.set_mesh_data( \ + polygon2d_list.get_transform(i), \ + polygon2d_list.get_vertices(p_snapshot, i)) + else: + entity_tracker.clear_mesh_data() + + if label_list: + entity_tracker.plane_label = label_list.get_plane_semantic_label(i) + else: + entity_tracker.invalidate_pose("default") + + # We don't register our tracker until after we've set our initial data. + if register_with_xr_server: + XRServer.add_tracker(entity_tracker) + + # Any entities we've got left over, we can remove + for entity_id in org_entities: + var entity_tracker : OpenXRPlaneTracker = entities[entity_id] + entity_tracker.spatial_tracking_state = OpenXRSpatialEntityTracker.OPENXR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED + XRServer.remove_tracker(entity_tracker) + entities.erase(entity_id) + + +Marker tracking capability +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Marker tracking is handled by the +:ref:`OpenXRSpatialMarkerTrackingCapability` +singleton class. + +Marker tracking works similar to plane tracking however we're now tracking specific entities in +the real world based on some code printed on an object like a piece of paper. + +There are various different marker tracking options. OpenXR supports 4 out of the box, the following +table provides more information and the function name with which to check if your headset supports +a given option: + +.. list-table:: Marker tracking options + :header-rows: 1 + + * - Option + - Check for support + - Configuration object + * - April tag + - ``april_tag_is_supported`` + - :ref:`OpenXRSpatialCapabilityConfigurationAprilTag` + * - Aruco + - ``aruco_is_supported`` + - :ref:`OpenXRSpatialCapabilityConfigurationAruco` + * - QR code + - ``qrcode_is_supported`` + - :ref:`OpenXRSpatialCapabilityConfigurationQrCode` + * - Micro QR code + - ``micro_qrcode_is_supported`` + - :ref:`OpenXRSpatialCapabilityConfigurationMicroQrCode` + +Each option has its own configuration object that you can use when creating a spatial entity. + +QR codes allow you to encode a string which is decoded by the XR runtime and accessible when a marker is found. +With April tags and Aruco markers binary data is encoded based which you again can access when a marker is found, +however you need to configure the detection with the correct decoding format. + +As an example we'll create a spatial context that will find QR codes adn Aruco markers. + +.. code-block:: gdscript + + extends Node + + var qrcode_config : OpenXRSpatialCapabilityConfigurationQrCode + var aruco_config : OpenXRSpatialCapabilityConfigurationAruco + var spatial_context : RID + + func _setup_spatial_context(): + # Already setup? + if spatial_context: + return + + var configurations : Array + + # Add our QR code configuration + if not OpenXRSpatialMarkerTrackingCapability.qrcode_is_supported(): + qrcode_config = OpenXRSpatialCapabilityConfigurationQrCode.new() + configurations.push_back(qrcode_config) + + # Add our Aruco marker configuration + if not OpenXRSpatialMarkerTrackingCapability.aruco_is_supported(): + aruco_config = OpenXRSpatialCapabilityConfigurationAruco.new() + aruco_config.aruco_dict = OpenXRSpatialCapabilityConfigurationAruco.OPENXR_SPATIAL_MARKER_ARUCO_DICT_7X7_1000 + configurations.push_back(aruco_config) + + # Nothing supported? + if configurations.is_empty(): + return + + var future_result : OpenXRFutureResult = OpenXRSpatialEntityExtension.create_spatial_context(configurations) + + # Wait for async completion. + await future_result.completed + + # Obtain our result. + spatial_context = future_result.get_spatial_context() + if spatial_context: + # connect to our discovery signal. + OpenXRSpatialEntityExtension.spatial_discovery_recommended.connect(_on_perform_discovery) + + # Perform our initial discovery. + _on_perform_discovery(spatial_context) + + func _enter_tree(): + var openxr_interface : OpenXRInterface = XRServer.find_interface("OpenXR") + if openxr_interface and openxr_interface.is_initialized(): + # Just in case our session hasn't started yet, + # call our spatial context creation on start. + openxr_interface.session_begun.connect(_setup_spatial_context) + + # And in case it is already up and running, call it already, + # it will exit if we've called it too early. + _setup_spatial_context() + + func _exit_tree(): + if spatial_context: + # disconnect from our discovery signal. + OpenXRSpatialEntityExtension.spatial_discovery_recommended.disconnect(_on_perform_discovery) + + # Free our spatial context, this will clean it up. + OpenXRSpatialEntityExtension.free_spatial_context(spatial_context) + spatial_context = RID() + + var openxr_interface : OpenXRInterface = XRServer.find_interface("OpenXR") + if openxr_interface and openxr_interface.is_initialized(): + openxr_interface.session_begun.disconnect(_setup_spatial_context) + + +Every marker regardless of the type of marker will consist of two components: + +.. list-table:: Marker tracking components + :header-rows: 1 + + * - Component + - Data class + - Description + * - OPENXR_SPATIAL_COMPONENT_TYPE_MARKER + - :ref:`OpenXRSpatialComponentMarkerList` + - Provides us with the type, id (Aruco and April Tag) and/or data (QR Code) for each marker. + * - OPENXR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D + - :ref:`OpenXRSpatialComponentBounded2DList` + - Provides us with the center pose and bounding rectangle for each plane + +Finally we add our discovery implementation: + +.. code-block:: gdscript + + ... + + var discovery_result : OpenXRFutureResult + var entities : Dictionary[int, OpenXRMarkerTracker] + + func _on_perform_discovery(p_spatial_context): + # We get this signal for all spatial contexts, so exit if this is not for us + if p_spatial_context != spatial_context: + return + + # If we currently have an ongoing discovery result, cancel it. + if discovery_result: + discovery_result.cancel_discovery() + + # Perform our discovery + discovery_result = OpenXRSpatialEntityExtension.discover_spatial_entities(spatial_context, [\ + OpenXRSpatialEntityExtension.OPENXR_SPATIAL_COMPONENT_TYPE_MARKER, \ + OpenXRSpatialEntityExtension.OPENXR_SPATIAL_COMPONENT_TYPE_BOUNDED_2D \ + ]) + + # Wait for async completion. + await discovery_result.completed + + var snapshot : RID = discovery_result.get_spatial_snapshot() + if snapshot: + # Process our snapshot result. + _process_snapshot(snapshot) + + # And cleanup our snapshot + OpenXRSpatialEntityExtension.free_spatial_snapshot(snapshot) + + func _process_snapshot(p_snapshot): + var result_data : Array + + # Make a copy of the entities we've currently found + var org_entities : PackedInt64Array + for entity_id in entities: + org_entities.push_back(entity_id) + + # Always include our query result data + var query_result_data : OpenXRSpatialQueryResultData = OpenXRSpatialQueryResultData.new() + result_data.push_back(query_result_data) + + # And our marker component data. + var marker_list : OpenXRSpatialComponentMarkerList = OpenXRSpatialComponentMarkerList.new() + result_data.push_back(marker_list) + + # Add in our bounded 2d component data. + var bounded2d_list : OpenXRSpatialComponentBounded2DList = OpenXRSpatialComponentBounded2DList.new() + result_data.push_back(bounded2d_list) + + if OpenXRSpatialEntityExtension.query_snapshot(p_snapshot, result_data): + for i in query_result_data.get_entity_id_size(): + var entity_id = query_result_data.get_entity_id(i) + var entity_state = query_result_data.get_entity_state(i) + + # Remove the entity from our original list + if org_entities.has(entity_id): + org_entities.erase(entity_id) + + if entity_state == OpenXRSpatialEntityTracker.OPENXR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED: + # We're not doing update snapshots so we shouldn't get this, + # but just to future proof: + if entities.has(entity_id): + var entity_tracker : OpenXRMarkerTracker = entities[entity_id] + entity_tracker.spatial_tracking_state = entity_state + XRServer.remove_tracker(entity_tracker) + entities.erase(entity_id) + else: + var entity_tracker : OpenXRMarkerTracker + var register_with_xr_server : bool = false + if entities.has(entity_id): + entity_tracker = entities[entity_id] + else: + entity_tracker = OpenXRMarkerTracker.new() + entity_tracker.entity = OpenXRSpatialEntityExtension.make_spatial_entity(spatial_context, entity_id) + entities[entity_id] = entity_tracker + register_with_xr_server = true + + # Copy the state + entity_tracker.spatial_tracking_state = entity_state + + # Only if we're tracking, we should query the rest of our components. + if entity_state == OpenXRSpatialEntityTracker.OPENXR_SPATIAL_ENTITY_TRACKING_STATE_TRACKING: + var center_pose : Transform3D = bounded2d_list.get_center_pose(i) + entity_tracker.set_pose("default", center_pose, Vector3(), Vector3(), XRPose.XR_TRACKING_CONFIDENCE_HIGH) + + entity_tracker.bounds_size = bounded2d_list.get_size(i) + + entity_tracker.marker_type = marker_list.get_marker_type(i) + entity_tracker.marker_id = marker_list.get_marker_id(i) + entity_tracker.marker_data = marker_list.get_marker_data(p_snapshot, i) + else: + entity_tracker.invalidate_pose("default") + + # We don't register our tracker until after we've set our initial data. + if register_with_xr_server: + XRServer.add_tracker(entity_tracker) + + # Any entities we've got left over, we can remove + for entity_id in org_entities: + var entity_tracker : OpenXRMarkerTracker = entities[entity_id] + entity_tracker.spatial_tracking_state = OpenXRSpatialEntityTracker.OPENXR_SPATIAL_ENTITY_TRACKING_STATE_STOPPED + XRServer.remove_tracker(entity_tracker) + entities.erase(entity_id) + + + + +**Q : Should we be doing update queries here to get position changes for markers??**