|
@@ -0,0 +1,1771 @@
|
|
|
|
|
+.. _doc_openxr_spatial_entities:
|
|
|
|
|
+
|
|
|
|
|
+OpenXR spatial entities
|
|
|
|
|
+=======================
|
|
|
|
|
+
|
|
|
|
|
+For any sort of augmented reality application you need to access real world information, and be able to
|
|
|
|
|
+track real world locations. OpenXR's 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 on top, which implement specific systems such as marker tracking,
|
|
|
|
|
+plane tracking, and anchors. These are referred 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 systems to work.
|
|
|
|
|
+ * - Enable spatial anchors
|
|
|
|
|
+ - Enables the spatial anchors capability that allow creating and tracking 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.
|
|
|
|
|
+ * - Enable 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.
|
|
|
|
|
+ * - Enable built-in marker tracking
|
|
|
|
|
+ - Enables our built-in marker detection logic, this will automatically react to new markers being
|
|
|
|
|
+ found or markers being moved around the player's 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 that 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 an
|
|
|
|
|
+:ref:`OpenXRSpatialEntityTracker<class_OpenXRSpatialEntityTracker>`
|
|
|
|
|
+object is instantiated and registered with the :ref:`XRServer<class_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<class_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<class_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 marker 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 to 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)
|
|
|
|
|
+
|
|
|
|
|
+ # Set up 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 our 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 completeness 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
|
|
|
|
|
+
|
|
|
|
|
+ # Set up 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 any 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 locations 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<class_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<class_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 ``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 workflow that allows us to correctly react to new and existing
|
|
|
|
|
+persistent anchors.
|
|
|
|
|
+
|
|
|
|
|
+.. note::
|
|
|
|
|
+
|
|
|
|
|
+ If you unpersist an anchor, the UUID is destroyed but the anchor is not
|
|
|
|
|
+ removed automatically.
|
|
|
|
|
+ You will need to react to the completion of unpersisting an anchor and then clean it up.
|
|
|
|
|
+ Also you will get an error if you try to destroy an anchor that is still persistent.
|
|
|
|
|
+
|
|
|
|
|
+To complete our anchor system we start by 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<class_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 metadata 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.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 notify that the UUID changed on the anchor,
|
|
|
|
|
+ # we can then store our scene path which we've already applied to our
|
|
|
|
|
+ # tracked scene.
|
|
|
|
|
+ OpenXRSpatialAnchorCapability.persist_anchor(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, look up 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 at least 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 functions to our spatial manager script
|
|
|
|
|
+to create or remove anchors:
|
|
|
|
|
+
|
|
|
|
|
+.. code-block:: gdscript
|
|
|
|
|
+
|
|
|
|
|
+ ...
|
|
|
|
|
+
|
|
|
|
|
+ ## Create a new spatial 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 anchor's UUID for future recreation.
|
|
|
|
|
+ func create_spatial_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 an 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 persistent, the associated UUID will be cleared.
|
|
|
|
|
+ func remove_spatial_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.unpersist_anchor(anchor_tracker, RID(), _on_unpersist_complete)
|
|
|
|
|
+ else:
|
|
|
|
|
+ # Otherwise 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_unpersist_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 player's 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 wrote 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 an :ref:`XRAnchor3D<class_XRAnchor3D>` node.
|
|
|
|
|
+We'll add a :ref:`StaticBody3D<class_StaticBody3D>` node as a child and add a
|
|
|
|
|
+:ref:`CollisionShape3D<class_CollisionShape3D>` and :ref:`MeshInstance3D<class_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 visualize 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<class_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 metadata 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_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<class_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 an :ref:`XRAnchor3D<class_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 needed 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.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.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.MARKER_TYPE_ARUCO:
|
|
|
|
|
+ # Use marker_tracker.marker_id to identify the marker.
|
|
|
|
|
+ pass
|
|
|
|
|
+ OpenXRSpatialComponentMarkerList.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 a 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.
|
|
|
|
|
+
|
|
|
|
|
+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 whom 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<class_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 or 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 the 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 capability.
|
|
|
|
|
+
|
|
|
|
|
+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 _set_up_spatial_context():
|
|
|
|
|
+ # Already set up?
|
|
|
|
|
+ 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(_set_up_spatial_context)
|
|
|
|
|
+
|
|
|
|
|
+ # And in case it is already up and running, call it already,
|
|
|
|
|
+ # it will exit if we've called it too early.
|
|
|
|
|
+ _set_up_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(_set_up_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 emits an event when there are new entities to be processed, this results in the
|
|
|
|
|
+``spatial_discovery_recommended`` signal being emitted by our
|
|
|
|
|
+:ref:`OpenXRSpatialEntityExtension<class_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.COMPONENT_TYPE_BOUNDED_2D, \
|
|
|
|
|
+ OpenXRSpatialEntityExtension.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 clean up 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
|
|
|
|
|
+at least 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 it's likely entities
|
|
|
|
|
+change or have a lifetime process. A good example of this are persistent anchors and
|
|
|
|
|
+markers. Consult the documentation about 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
|
|
|
|
|
+
|
|
|
|
|
+ if entities.is_empty():
|
|
|
|
|
+ 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.COMPONENT_TYPE_BOUNDED_2D, \
|
|
|
|
|
+ OpenXRSpatialEntityExtension.COMPONENT_TYPE_PLANE_ALIGNMENT \
|
|
|
|
|
+ ])
|
|
|
|
|
+ if snapshot:
|
|
|
|
|
+ # Process our snapshot.
|
|
|
|
|
+ _process_snapshot(snapshot)
|
|
|
|
|
+
|
|
|
|
|
+ # And clean up 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 your update snapshot,
|
|
|
|
|
+you have to take the different components into account.
|
|
|
|
|
+
|
|
|
|
|
+Querying snapshots
|
|
|
|
|
+""""""""""""""""""
|
|
|
|
|
+
|
|
|
|
|
+Once we have a snapshot we can run queries over that snapshot 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<class_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 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.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
|
|
|
|
|
+
|
|
|
|
|
+ # If we're tracking, we should query the rest of our components.
|
|
|
|
|
+ if entity_state == OpenXRSpatialEntityTracker.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 extend 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 ``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 prerequisite 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 an 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<class_OpenXRSpatialEntityTracker>` instance is destroyed.
|
|
|
|
|
+
|
|
|
|
|
+Spatial anchor capability
|
|
|
|
|
+~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
+
|
|
|
|
|
+Spatial anchors are managed by our :ref:`OpenXRSpatialAnchorCapability<class_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 showed in our example before we always start with creating a spatial context but now using the
|
|
|
|
|
+:ref:`OpenXRSpatialCapabilityConfigurationAnchor<class_OpenXRSpatialCapabilityConfigurationAnchor>`
|
|
|
|
|
+configuration object.
|
|
|
|
|
+We'll show an example of this code after we discuss persistence 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 set up a persistence scope.
|
|
|
|
|
+In the core of OpenXR two types of persistence scopes are supported:
|
|
|
|
|
+
|
|
|
|
|
+.. list-table:: Persistence scopes
|
|
|
|
|
+ :header-rows: 1
|
|
|
|
|
+
|
|
|
|
|
+ * - Enum
|
|
|
|
|
+ - Description
|
|
|
|
|
+ * - PERSISTENCE_SCOPE_SYSTEM_MANAGED
|
|
|
|
|
+ - Provides the application with read-only access (i.e. applications 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.
|
|
|
|
|
+ * - PERSISTENCE_SCOPE_LOCAL_ANCHORS
|
|
|
|
|
+ - Persistence operations and data access is limited to spatial anchors, on the same device,
|
|
|
|
|
+ for the same user and app (using `persist_anchor` and
|
|
|
|
|
+ `unpersist_anchor` 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 _set_up_persistence_context():
|
|
|
|
|
+ # Already set up?
|
|
|
|
|
+ if persistence_context:
|
|
|
|
|
+ # Check our spatial context.
|
|
|
|
|
+ _set_up_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():
|
|
|
|
|
+ _set_up_spatial_context()
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ var scope : int = 0
|
|
|
|
|
+ if OpenXRSpatialAnchorCapability.is_persistence_scope_supported(OpenXRSpatialAnchorCapability.PERSISTENCE_SCOPE_LOCAL_ANCHORS):
|
|
|
|
|
+ scope = OpenXRSpatialAnchorCapability.PERSISTENCE_SCOPE_LOCAL_ANCHORS
|
|
|
|
|
+ elif OpenXRSpatialAnchorCapability.is_persistence_scope_supported(OpenXRSpatialAnchorCapability.PERSISTENCE_SCOPE_SYSTEM_MANAGED):
|
|
|
|
|
+ scope = OpenXRSpatialAnchorCapability.PERSISTENCE_SCOPE_SYSTEM_MANAGED
|
|
|
|
|
+ else:
|
|
|
|
|
+ # Don't have a known persistence scope, report and just set up without it.
|
|
|
|
|
+ push_error("No known persistence scope is supported.")
|
|
|
|
|
+ _set_up_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 set up without it.
|
|
|
|
|
+ _set_up_spatial_context()
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # Now wait for our process to complete.
|
|
|
|
|
+ await future_result.completed
|
|
|
|
|
+
|
|
|
|
|
+ # Get our result.
|
|
|
|
|
+ persistence_context = future_result.get_result()
|
|
|
|
|
+ if persistence_context:
|
|
|
|
|
+ # Now set up our spatial context.
|
|
|
|
|
+ _set_up_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(_set_up_persistence_context)
|
|
|
|
|
+
|
|
|
|
|
+ # And in case it is already up and running, call it already,
|
|
|
|
|
+ # it will exit if we've called it too early.
|
|
|
|
|
+ _set_up_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(_set_up_persistence_context)
|
|
|
|
|
+
|
|
|
|
|
+With our persistence scope created, we can now create our spatial context.
|
|
|
|
|
+
|
|
|
|
|
+.. code-block:: gdscript
|
|
|
|
|
+
|
|
|
|
|
+ ...
|
|
|
|
|
+
|
|
|
|
|
+ var spatial_context: RID
|
|
|
|
|
+
|
|
|
|
|
+ func _set_up_spatial_context():
|
|
|
|
|
+ # Already set up?
|
|
|
|
|
+ if spatial_context:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # Not supported or we're not yet set up.
|
|
|
|
|
+ if not OpenXRSpatialAnchorCapability.is_spatial_anchor_supported():
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # Create our anchor capability.
|
|
|
|
|
+ var anchor_capability : OpenXRSpatialCapabilityConfigurationAnchor = OpenXRSpatialCapabilityConfigurationAnchor.new()
|
|
|
|
|
+
|
|
|
|
|
+ # And set up 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 the state so we do want to
|
|
|
|
|
+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
|
|
|
|
|
+ * - COMPONENT_TYPE_ANCHOR
|
|
|
|
|
+ - :ref:`OpenXRSpatialComponentAnchorList<class_OpenXRSpatialComponentAnchorList>`
|
|
|
|
|
+ - Provides us with the pose (location + orientation) of each anchor
|
|
|
|
|
+ * - COMPONENT_TYPE_PERSISTENCE
|
|
|
|
|
+ - :ref:`OpenXRSpatialComponentPersistenceList<class_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.COMPONENT_TYPE_ANCHOR, \
|
|
|
|
|
+ OpenXRSpatialEntityExtension.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 clean up our snapshot.
|
|
|
|
|
+ OpenXRSpatialEntityExtension.free_spatial_snapshot(snapshot)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ func _process(_delta):
|
|
|
|
|
+ if not spatial_context:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ if entities.is_empty():
|
|
|
|
|
+ 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.COMPONENT_TYPE_ANCHOR, \
|
|
|
|
|
+ ])
|
|
|
|
|
+ if snapshot:
|
|
|
|
|
+ # Process our snapshot.
|
|
|
|
|
+ _process_snapshot(snapshot)
|
|
|
|
|
+
|
|
|
|
|
+ # And clean up 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<class_OpenXRAnchorTracker>`
|
|
|
|
|
+as our tracker class as this already has all the support for anchors built 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 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.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
|
|
|
|
|
+
|
|
|
|
|
+ # If we're tracking, we update our position.
|
|
|
|
|
+ if entity_state == OpenXRSpatialEntityTracker.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 a 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<class_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 to 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
|
|
|
|
|
+ * - COMPONENT_TYPE_BOUNDED_2D
|
|
|
|
|
+ - :ref:`OpenXRSpatialComponentBounded2DList<class_OpenXRSpatialComponentBounded2DList>`
|
|
|
|
|
+ - Provides us with the center pose and bounding rectangle for each plane.
|
|
|
|
|
+ * - COMPONENT_TYPE_PLANE_ALIGNMENT
|
|
|
|
|
+ - :ref:`OpenXRSpatialComponentPlaneAlignmentList<class_OpenXRSpatialComponentPlaneAlignmentList>`
|
|
|
|
|
+ - Provides us with the alignment of each plane
|
|
|
|
|
+ * - COMPONENT_TYPE_MESH_2D
|
|
|
|
|
+ - :ref:`OpenXRSpatialComponentMesh2DList<class_OpenXRSpatialComponentMesh2DList>`
|
|
|
|
|
+ - Provides us with a 2D mesh that shapes each plane
|
|
|
|
|
+ * - COMPONENT_TYPE_POLYGON_2D
|
|
|
|
|
+ - :ref:`OpenXRSpatialComponentPolygon2DList<class_OpenXRSpatialComponentPolygon2DList>`
|
|
|
|
|
+ - Provides us with a 2D polygon that shapes each plane
|
|
|
|
|
+ * - COMPONENT_TYPE_PLANE_SEMANTIC_LABEL
|
|
|
|
|
+ - :ref:`OpenXRSpatialComponentPlaneSemanticLabelList<class_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 interrogate
|
|
|
|
|
+it so we'll store our instance in a member variable.
|
|
|
|
|
+We can use our :ref:`OpenXRPlaneTracker<class_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 _set_up_spatial_context():
|
|
|
|
|
+ # Already set up?
|
|
|
|
|
+ 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(_set_up_spatial_context)
|
|
|
|
|
+
|
|
|
|
|
+ # And in case it is already up and running, call it already,
|
|
|
|
|
+ # it will exit if we've called it too early.
|
|
|
|
|
+ _set_up_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(_set_up_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 clean up 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 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 a Mesh2D or a 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.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
|
|
|
|
|
+
|
|
|
|
|
+ # If we're tracking, we should query the rest of our components.
|
|
|
|
|
+ if entity_state == OpenXRSpatialEntityTracker.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:
|
|
|
|
|
+ # The logic in our tracker will convert the polygon to a 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.ENTITY_TRACKING_STATE_STOPPED
|
|
|
|
|
+ XRServer.remove_tracker(entity_tracker)
|
|
|
|
|
+ entities.erase(entity_id)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+Marker tracking capability
|
|
|
|
|
+~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
+
|
|
|
|
|
+Marker tracking is handled by the
|
|
|
|
|
+:ref:`OpenXRSpatialMarkerTrackingCapability<class_OpenXRSpatialMarkerTrackingCapability>`
|
|
|
|
|
+singleton class.
|
|
|
|
|
+
|
|
|
|
|
+Marker tracking works similarly 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<class_OpenXRSpatialCapabilityConfigurationAprilTag>`
|
|
|
|
|
+ * - Aruco
|
|
|
|
|
+ - ``aruco_is_supported``
|
|
|
|
|
+ - :ref:`OpenXRSpatialCapabilityConfigurationAruco<class_OpenXRSpatialCapabilityConfigurationAruco>`
|
|
|
|
|
+ * - QR code
|
|
|
|
|
+ - ``qrcode_is_supported``
|
|
|
|
|
+ - :ref:`OpenXRSpatialCapabilityConfigurationQrCode<class_OpenXRSpatialCapabilityConfigurationQrCode>`
|
|
|
|
|
+ * - Micro QR code
|
|
|
|
|
+ - ``micro_qrcode_is_supported``
|
|
|
|
|
+ - :ref:`OpenXRSpatialCapabilityConfigurationMicroQrCode<class_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 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 and Aruco markers.
|
|
|
|
|
+
|
|
|
|
|
+.. code-block:: gdscript
|
|
|
|
|
+
|
|
|
|
|
+ extends Node
|
|
|
|
|
+
|
|
|
|
|
+ var qrcode_config : OpenXRSpatialCapabilityConfigurationQrCode
|
|
|
|
|
+ var aruco_config : OpenXRSpatialCapabilityConfigurationAruco
|
|
|
|
|
+ var spatial_context: RID
|
|
|
|
|
+
|
|
|
|
|
+ func _set_up_spatial_context():
|
|
|
|
|
+ # Already set up?
|
|
|
|
|
+ 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.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(_set_up_spatial_context)
|
|
|
|
|
+
|
|
|
|
|
+ # And in case it is already up and running, call it already,
|
|
|
|
|
+ # it will exit if we've called it too early.
|
|
|
|
|
+ _set_up_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(_set_up_spatial_context)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+Every marker regardless of typer will consist of two components:
|
|
|
|
|
+
|
|
|
|
|
+.. list-table:: Marker tracking components
|
|
|
|
|
+ :header-rows: 1
|
|
|
|
|
+
|
|
|
|
|
+ * - Component
|
|
|
|
|
+ - Data class
|
|
|
|
|
+ - Description
|
|
|
|
|
+ * - COMPONENT_TYPE_MARKER
|
|
|
|
|
+ - :ref:`OpenXRSpatialComponentMarkerList<class_OpenXRSpatialComponentMarkerList>`
|
|
|
|
|
+ - Provides us with the type, ID (Aruco and April Tag), and/or data (QR Code) for each marker.
|
|
|
|
|
+ * - COMPONENT_TYPE_BOUNDED_2D
|
|
|
|
|
+ - :ref:`OpenXRSpatialComponentBounded2DList<class_OpenXRSpatialComponentBounded2DList>`
|
|
|
|
|
+ - Provides us with the center pose and bounding rectangle for each plane.
|
|
|
|
|
+
|
|
|
|
|
+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.COMPONENT_TYPE_MARKER, \
|
|
|
|
|
+ OpenXRSpatialEntityExtension.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, true)
|
|
|
|
|
+
|
|
|
|
|
+ # And clean up our snapshot.
|
|
|
|
|
+ OpenXRSpatialEntityExtension.free_spatial_snapshot(snapshot)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ func _process_snapshot(p_snapshot, bool p_is_discovery):
|
|
|
|
|
+ var result_data : Array
|
|
|
|
|
+
|
|
|
|
|
+ # Make a copy of the entities we've currently found.
|
|
|
|
|
+ var org_entities : PackedInt64Array
|
|
|
|
|
+ if p_is_discovery:
|
|
|
|
|
+ # Only on discovery will we check if we have untracked entities to clean up.
|
|
|
|
|
+ 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
|
|
|
|
|
+ if p_is_discovery:
|
|
|
|
|
+ # Only on discovery do we check our marker data
|
|
|
|
|
+ marker_list = OpenXRSpatialComponentMarkerList.new()
|
|
|
|
|
+ result_data.push_back(marker_list)
|
|
|
|
|
+
|
|
|
|
|
+ # Add 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.ENTITY_TRACKING_STATE_STOPPED:
|
|
|
|
|
+ # We should only get this when doing an update,
|
|
|
|
|
+ # and we'll remove our marker in that case.
|
|
|
|
|
+ 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
|
|
|
|
|
+
|
|
|
|
|
+ # If we're tracking, we should query the rest of our components.
|
|
|
|
|
+ if entity_state == OpenXRSpatialEntityTracker.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)
|
|
|
|
|
+
|
|
|
|
|
+ if p_is_discovery:
|
|
|
|
|
+ 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)
|
|
|
|
|
+
|
|
|
|
|
+ if p_is_discovery:
|
|
|
|
|
+ # 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.ENTITY_TRACKING_STATE_STOPPED
|
|
|
|
|
+ XRServer.remove_tracker(entity_tracker)
|
|
|
|
|
+ entities.erase(entity_id)
|
|
|
|
|
+
|
|
|
|
|
+And we add our update functionality:
|
|
|
|
|
+
|
|
|
|
|
+.. code-block:: gdscript
|
|
|
|
|
+
|
|
|
|
|
+ ...
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ func _process(_delta):
|
|
|
|
|
+ if not spatial_context:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ if entities.is_empty():
|
|
|
|
|
+ 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.COMPONENT_TYPE_BOUNDED_2D, \
|
|
|
|
|
+ ])
|
|
|
|
|
+ if snapshot:
|
|
|
|
|
+ # Process our snapshot.
|
|
|
|
|
+ _process_snapshot(snapshot, false)
|
|
|
|
|
+
|
|
|
|
|
+ # And clean up our snapshot.
|
|
|
|
|
+ OpenXRSpatialEntityExtension.free_spatial_snapshot(snapshot)
|
|
|
|
|
+
|