Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

Пространственные сущности OpenXR

Для любого приложения дополненной реальности вам необходим доступ к информации о реальном мире и возможность отслеживать реальные местоположения. API пространственных сущностей OpenXR был создан именно для этой цели.

Он имеет очень модульную архитектуру. Ядро API определяет, как структурированы сущности реального мира, как они находятся, а также как хранится и осуществляется доступ к информации о них.

Сверху добавляются различные расширения, реализующие конкретные системы, такие как отслеживание маркеров, отслеживание плоскостей и якорей. Они называются пространственными возможностями.

Каждая сущность, которую может обрабатывать система, разбивается на более мелкие компоненты, что упрощает расширение системы и добавление новых возможностей.

Поставщики имеют возможность реализовывать и предоставлять дополнительные возможности и типы компонентов, которые можно использовать с основным API. Для Godot их можно реализовать в расширениях. Однако эти реализации выходят за рамки данного руководства.

Наконец, важно отметить, что система пространственных сущностей использует асинхронные функции. Это означает, что вы можете запустить процесс, а затем получить уведомление о его завершении позже.

Настройка

Для использования пространственных сущностей необходимо включить соответствующие настройки проекта. Вы можете найти их в разделе OpenXR:

../../_images/openxr_spatial_entities_project_settings.webp
Настройки пространственных сущностей

Настройка

Описание

Enabled

Включает ядро системы пространственных сущностей. Должно быть включено для работы любой из систем пространственных сущностей.

Включить пространственные якоря

Включает возможность пространственных якорей, которая позволяет создавать и отслеживать пространственные якоря.

Включить постоянные якоря

Включает возможность сделать пространственные якоря постоянными. Это означает, что их местоположение сохраняется и может быть получено в последующих сеансах.

Включить встроенное обнаружение якорей

Включает нашу встроенную логику обнаружения якорей, которая автоматически извлекает постоянные якоря и корректирует их позиционирование при обновлении отслеживания.

Включить отслеживание плоскостей

Включает возможность отслеживания плоскостей, которая позволяет обнаруживать поверхности, такие как полы, стены, потолки и столы.

Включить встроенное обнаружение плоскостей

Включает нашу встроенную логику обнаружения плоскостей, которая автоматически реагирует на появление новых данных о плоскостях.

Включить отслеживание маркеров

Включает возможность отслеживания маркеров, которая позволяет обнаруживать маркеры, такие как QR-коды, маркеры Aruco и теги April.

Включить встроенное отслеживание маркеров

Включает нашу встроенную логику обнаружения маркеров, которая автоматически реагирует на обнаружение новых маркеров или их перемещение в пространстве игрока.

Примечание

Обратите внимание, что различные XR-устройства также требуют установки флагов разрешений. Их необходимо включить в настройках пресета экспорта.

Включение различных возможностей активирует соответствующие API OpenXR, но для взаимодействия с этими данными требуется дополнительная логика. Для каждой основной системы у нас есть встроенная логика, которую можно включить, и она сделает это за вас.

Мы будем обсуждать систему пространственных сущностей в предположении, что сначала включена встроенная логика. Затем мы рассмотрим базовые API и то, как вы можете реализовать это самостоятельно, однако следует отметить, что это часто излишне и базовые API в основном предоставляются для того, чтобы плагины GDExtension могли реализовывать дополнительные возможности.

Создание нашего менеджера пространственных сущностей

При обнаружении или создании пространственных сущностей создаётся объект OpenXRSpatialEntityTracker и регистрируется в XRServer.

Каждый тип пространственной сущности будет реализовывать свой собственный подкласс, и мы можем по-разному реагировать на каждый тип сущности.

Generally speaking we will instantiate different subscenes for each type of entity. As the tracker objects can be used with XRAnchor3D nodes, these subscenes should have such a node as their root node.

Все трекеры сущностей будут предоставлять своё местоположение через позу default.

Мы можем автоматизировать создание этих под-сцен и добавление их в дерево сцены, создав объект-менеджер. Поскольку все местоположения локальны относительно узла XROrigin3D, мы должны создать наш менеджер как дочерний узел узла начала координат.

Ниже приведена основа скрипта, реализующего нашу логику менеджера:

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[XRTracker, 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 instantiated 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)

Пространственные якоря

Пространственные якоря позволяют нам сопоставлять реальные местоположения в нашем виртуальном мире таким образом, чтобы среда выполнения XR отслеживала эти местоположения и корректировала их по мере необходимости. При поддержке якоря могут быть сделаны постоянными, что означает, что они будут воссозданы в правильном местоположении при следующем запуске вашего приложения.

Вы можете представить такие случаи использования: - размещение виртуальных окон вокруг вашего пространства, которые воссоздаются при перезапуске приложения - размещение виртуальных объектов на вашем столе или стенах с их воссозданием

Пространственные якоря отслеживаются с помощью объектов OpenXRAnchorTracker, зарегистрированных в XRServer.

При необходимости местоположение пространственного якоря будет обновляться автоматически; поза в соответствующем трекере будет обновлена, и узел XRAnchor3D будет перемещён.

Когда пространственный якорь становится постоянным, ему присваивается универсальный уникальный идентификатор (UUID). Вам необходимо сохранить его вместе с любой информацией, необходимой для восстановления сцены. В нашем примере ниже мы просто будем вызывать set_scene_path и get_scene_path, но вам нужно будет предоставить свои собственные реализации для этих функций.

Для создания постоянного якоря необходимо выполнить следующий порядок действий: - Создать пространственный якорь - Дождаться изменения статуса отслеживания на ENTITY_TRACKING_STATE_TRACKING - Сделать якорь постоянным - Получить UUID и сохранить его

Когда существующий постоянный якорь обнаружен, добавляется новый трекер, у которого уже установлен UUID. Именно это различие в рабочем процессе позволяет нам правильно реагировать на новые и существующие постоянные якоря.

Примечание

Если вы отменяете постоянство якоря, UUID уничтожается, но якорь не удаляется автоматически. Вам нужно будет отреагировать на завершение отмены постоянства и затем очистить его. Также вы получите ошибку, если попытаетесь уничтожить якорь, который всё ещё является постоянным.

Чтобы завершить нашу систему якорей, мы начнём с создания сцены, которую мы назначим как сцену для создания экземпляров якорей на нашем узле менеджера пространственных сущностей.

Эта сцена должна иметь узел XRAnchor3D в качестве корневого и больше ничего. Мы добавим к ней скрипт, который будет загружать под-сцену, содержащую фактическое визуальное представление нашего якоря, чтобы мы могли создавать различные якоря в нашей сцене. Предполагается, что мы намерены сделать эти якоря постоянными и сохранить путь к этой под-сцене в качестве метаданных для UUID.

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)

После того как наша сцена якоря готова, мы можем добавить пару функций в наш скрипт менеджера пространственных сущностей для создания или удаления якорей:

...

## 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

Примечание

В коде выше происходит некоторая магия. Всякий раз, когда пространственный якорь создаётся или удаляется в нашей возможности якорей, соответствующий объект трекера создаётся или уничтожается. Это приводит к тому, что менеджер пространственных сущностей добавляет или удаляет дочернюю сцену для этого якоря. Поэтому мы можем положиться на это здесь.

Отслеживание плоскостей

Отслеживание плоскостей позволяет нам обнаруживать поверхности, такие как стены, полы, потолки и столы в окрестностях игрока. Эти данные могут поступать из захвата комнаты, выполненного пользователем в прошлом, или обнаруживаться в реальном времени оптическими датчиками. Расширение отслеживания плоскостей не делает здесь различий.

Примечание

Некоторые среды выполнения XR требуют расширений поставщиков для включения и/или настройки этого процесса, но данные будут предоставлены через это расширение.

Код, который мы написали выше для менеджера пространственных сущностей, уже будет обнаруживать наши новые плоскости. Нам нужно настроить новую сцену и назначить её менеджеру пространственных сущностей.

Корневой узел этой сцены должен быть узлом XRAnchor3D. Мы добавим узел StaticBody3D в качестве дочернего и добавим узлы CollisionShape3D и MeshInstance3D как дочерние узлы статического тела.

../../_images/openxr_plane_anchor.webp

Статическое тело и форма столкновения позволят нам сделать плоскость интерактивной.

Узел экземпляра меша позволяет нам применить материал «дырокола» к плоскости; в сочетании со сквозным режимом это превращает нашу плоскость в визуальный окклюдер. Альтернативно мы можем назначить материал, который визуализирует плоскость для отладки.

Мы настраиваем этот материал как материал material_override на нашем MeshInstance3D. Для нашего материала «дырокола» создайте ShaderMaterial и используйте следующий код в качестве кода шейдера:

shader_type spatial;
render_mode unshaded, shadow_to_opacity;

void fragment() {
    ALBEDO = vec3(0.0, 0.0, 0.0);
}

Нам также нужно добавить скрипт в нашу сцену, чтобы обеспечить применение наших столкновений и меша.

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)

Если это поддерживается средой выполнения XR, вы можете запросить дополнительные метаданные у объекта трекера плоскостей. Особого внимания заслуживает свойство plane_label, которое, если доступно, идентифицирует тип поверхности. Для получения дополнительной информации обратитесь к документации класса OpenXRPlaneTracker.

Отслеживание маркеров

Отслеживание маркеров обнаруживает определённые маркеры в реальном мире. Обычно это печатные изображения, например QR-коды.

API предоставляет поддержку 4 различных кодов: QR-коды, Micro QR-коды, коды Aruco и теги April, однако среды выполнения XR не обязаны поддерживать их все.

При обнаружении маркеров создаются объекты OpenXRMarkerTracker и регистрируются в XRServer.

Наш существующий код менеджера пространственных сущностей уже обнаруживает их, всё, что нам нужно сделать, — это создать сцену с узлом XRAnchor3D в корне, сохранить её и назначить её менеджеру пространственных сущностей как сцену для создания экземпляров маркеров.

Трекер маркеров должен быть полностью настроен при назначении, поэтому всё, что требуется, — это функция _ready, которая реагирует на данные маркера. Ниже приведён шаблон необходимого кода:

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 is String:
                    # Data is a QR code as a string, usually a URL.
                    pass
                elif data is PackedByteArray:
                    # Data is binary, can be anything.
                    pass
            OpenXRSpatialComponentMarkerList.MARKER_TYPE_MICRO_QRCODE:
                var data = marker_tracker.get_marker_data()
                if data is String:
                    # Data is a QR code as a string, usually a URL.
                    pass
                elif data is PackedByteArray:
                    # 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

Как мы видим, QR-коды предоставляют блок данных, который является либо строкой, либо массивом байтов. Маркеры Aruco и April предоставляют ID, который считывается из кода.

Вам решать, как лучше связать данные маркера со сценой, которую необходимо загрузить, в зависимости от вашего случая использования. Примером может быть кодирование имени ресурса, который вы хотите отобразить, в QR-код.

Внутренний доступ

Для большинства целей основная система вместе с любыми расширениями поставщиков должна быть тем, что большинство пользователей будут использовать в том виде, в котором она предоставлена.

Для тех, кто реализует расширения поставщиков, или для тех, кому встроенной логики недостаточно, предоставляется внутренний доступ через набор объектов-синглтонов.

Эти объекты также можно использовать для запроса того, какие возможности поддерживаются используемым шлемом. Мы уже добавили код, проверяющий это, в наш менеджер пространственных сущностей и код пространственных якорей в разделах выше.

Примечание

Система пространственных сущностей будет инкапсулировать многие сущности OpenXR в ресурсы, которые возвращаются в виде RID.

Ядро пространственных сущностей

Основная функциональность пространственных сущностей предоставляется через синглтон OpenXRSpatialEntityExtension.

Specific logic is exposed through capabilities that introduce specialized 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.

Начнём с рассмотрения отдельных компонентов, из которых состоит ядро системы.

Пространственные контексты

Пространственный контекст — это основной объект, через который мы запрашиваем систему пространственных сущностей. Пространственные контексты позволяют настраивать взаимодействие с одной или несколькими возможностями.

Рекомендуется создавать пространственный контекст для каждой возможности, с которой вы хотите взаимодействовать; собственно, это и делает Godot для своей встроенной логики.

Мы начинаем с установки объектов конфигурации возможностей для тех возможностей, к которым мы хотим получить доступ. Каждая возможность будет включать компоненты, которые мы поддерживаем для этой возможности. Настройки могут определять, какие компоненты будут включены. Мы рассмотрим эти объекты конфигурации более подробно, когда будем рассматривать каждую поддерживаемую возможность.

Создание пространственного контекста — это асинхронное действие. Это означает, что мы просим среду выполнения XR создать пространственный контекст, и в некоторый момент в будущем среда выполнения XR предоставит нам результат.

Ниже приведён скрипт, с которого начинается наш пример; его можно добавить в качестве узла в вашу сцену. Он показывает создание пространственного контекста для отслеживания плоскостей и настраивает обнаружение сущностей.

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

Снимки обнаружения

После создания нашего пространственного контекста среда выполнения XR начнёт управлять пространственными сущностями в соответствии с конфигурацией указанных возможностей.

Чтобы найти новые сущности или получить информацию о наших текущих сущностях, мы можем создать снимок обнаружения. Это укажет среде выполнения XR собрать конкретные данные, относящиеся ко всем пространственным сущностям, управляемым в данный момент пространственным контекстом.

Эта функция является асинхронной, так как может потребоваться некоторое время для сбора данных и предоставления результатов. Как правило, вы захотите выполнять снимок обнаружения при обнаружении новых сущностей. OpenXR генерирует событие, когда появляются новые сущности для обработки, в результате чего нашим синглтоном OpenXRSpatialEntityExtension генерируется сигнал spatial_discovery_recommended.

В приведённом выше примере кода мы уже подключаемся к этому сигналу и вызываем метод _on_perform_discovery на нашем узле. Давайте реализуем это:

...

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

Обратите внимание, что при вызове discover_spatial_entities мы указываем список компонентов. Запрос обнаружения найдет любую сущность, управляемую пространственным контекстом и имеющую хотя бы один из указанных компонентов.

Снимки обновления

Выполнение снимка обновления позволяет нам получать обновлённую информацию о сущностях, которые мы уже обнаружили ранее с помощью снимка обнаружения. Эта функция является синхронной и в основном предназначена для получения статуса и данных о позиционировании; её можно запускать каждый кадр.

Как правило, вы будете выполнять снимки обновления только тогда, когда сущности, вероятно, изменяются или имеют процесс жизненного цикла. Хорошим примером являются постоянные якоря и маркеры. Обратитесь к документации по конкретной возможности, чтобы определить, необходимо ли это.

Это не требуется для отслеживания плоскостей, однако для полноты нашего примера приведём пример того, как выглядел бы снимок обновления для отслеживания плоскостей, если бы он нам понадобился:

...

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)

Обратите внимание, что в нашем примере мы используем одну и ту же функцию _process_snapshot для обработки снимка. Это имеет смысл в большинстве ситуаций. Однако если компоненты, указанные вами при создании снимка, различаются для снимка обнаружения и снимка обновления, вам необходимо учитывать эти различия.

Запрос снимков

После того как у нас есть снимок, мы можем выполнять запросы к этому снимку, чтобы получить содержащиеся в нём данные. Снимок гарантированно остаётся неизменным до тех пор, пока вы его не освободите.

Для каждого компонента, добавленного в наш снимок, у нас есть соответствующий объект данных. Этот объект данных выполняет двойную функцию: добавление его в запрос гарантирует, что мы запрашиваем этот тип компонента, и он является объектом, в который загружаются запрашиваемые данные.

Существует один специальный объект данных, который всегда должен добавляться в наш список запросов самым первым, — это OpenXRSpatialQueryResultData. Этот объект будет содержать запись для каждой возвращённой сущности с её уникальным ID и текущим состоянием сущности.

Завершая нашу логику обнаружения, мы добавляем следующее:

...

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)

Примечание

В приведённом выше примере мы полагаемся на ENTITY_TRACKING_STATE_STOPPED для очистки пространственных сущностей, которые больше не отслеживаются. Это доступно только при использовании снимков обновления.

Для возможностей, которые полагаются только на снимки обнаружения, вы можете выполнить очистку на основе сущностей, которые больше не являются частью снимка, вместо того чтобы полагаться на изменение состояния.

Пространственные сущности

С приведённой выше информацией мы теперь знаем, как запрашивать наши пространственные сущности и получать информацию о них, но есть ещё кое-что, что нам нужно рассмотреть в отношении самих сущностей.

Теоретически мы получаем все наши данные из снимков, однако OpenXR имеет дополнительный API, в котором мы создаём объект пространственной сущности из ID сущности. Пока этот объект существует, среда выполнения XR знает, что мы используем эту сущность и что сущность не будет очищена досрочно. Это необходимое условие для выполнения запроса обновления для этой сущности.

В нашем примере кода мы делаем это, вызывая OpenXRSpatialEntityExtension.make_spatial_entity.

Некоторые API пространственных сущностей автоматически создают объект за нас. В этом случае нам нужно вызвать OpenXRSpatialEntityExtension.add_spatial_entity, чтобы зарегистрировать созданный объект в нашей реализации.

Обе функции возвращают RID, который мы можем использовать в дальнейших функциях, требующих наш объект сущности.

Когда мы закончим, мы можем вызвать OpenXRSpatialEntityExtension.free_spatial_entity.

Обратите внимание, что мы не сделали этого в нашем примере кода. Это обрабатывается автоматически, когда экземпляр OpenXRSpatialEntityTracker уничтожается.

Возможность пространственных якорей

Пространственные якоря управляются нашим объектом-синглтоном OpenXRSpatialAnchorCapability. После создания сеанса OpenXR вы можете вызвать OpenXRSpatialAnchorCapability.is_spatial_anchor_supported, чтобы проверить, поддерживается ли функция пространственных якорей на вашем оборудовании.

Возможность пространственных якорей немного выбивается из того, что мы показали выше.

Система пространственных якорей позволяет нам идентифицировать, отслеживать, сохранять и делиться физическим местоположением. Отличие заключается в том, что мы создаём и уничтожаем якорь и, таким образом, управляем его жизненным циклом.

Таким образом, мы используем систему обнаружения только для обнаружения якорей, созданных и сохранённых в предыдущих сеансах, или якорей, которыми с нами поделились.

Примечание

Обмен якорями в настоящее время не поддерживается в спецификации пространственных сущностей.

Как мы показали в нашем примере, мы всегда начинаем с создания пространственного контекста, но теперь используем объект конфигурации OpenXRSpatialCapabilityConfigurationAnchor. Мы покажем пример этого кода после того, как обсудим области действия постоянства. Сначала мы рассмотрим управление локальными якорями.

Создание пространственных якорей ничем не отличается от того, что мы обсуждали в связи со встроенной логикой. Единственное важное — передать свой собственный пространственный контекст в качестве параметра в OpenXRSpatialAnchorCapability.create_new_anchor.

Чтобы сделать якорь постоянным, необходимо дождаться, пока якорь будет отслеживаться; это означает, что вы должны выполнять запросы обновления для любого создаваемого вами якоря, чтобы обрабатывать изменения состояния.

Чтобы включить возможность создания постоянных якорей, необходимо также настроить область действия постоянства. В ядре OpenXR поддерживаются два типа областей действия постоянства:

Области действия постоянства

Перечисление

Описание

PERSISTENCE_SCOPE_SYSTEM_MANAGED

Предоставляет приложению доступ только для чтения (т.е. приложения не могут изменять это хранилище) к пространственным сущностям, сохранённым и управляемым системой. Приложение может использовать UUID в компоненте постоянства для этого хранилища, чтобы связывать сущности между пространственными контекстами и перезагрузками устройства.

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)

Мы начнём с нового скрипта, который обрабатывает наши пространственные якоря. Он будет похож на скрипт, представленный ранее, но с некоторыми отличиями.

Первое — создание нашей области действия постоянства.

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)

После создания области действия постоянства мы теперь можем создать наш пространственный контекст.

...

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)

Создание снимка обнаружения для наших якорей почти такое же, как и раньше, однако создавать снимок имеет смысл только для постоянных якорей. Мы уже знаем якоря, созданные во время нашего сеанса, мы просто хотим получить доступ к тем, которые поступают из среды выполнения XR.

Мы также хотим выполнять регулярные запросы обновления; здесь нас интересует только состояние, поэтому мы хотим обрабатывать наш снимок немного иначе.

Система якорей предоставляет нам доступ к двум компонентам:

Компоненты якорей

Компонент

Класс данных

Описание

COMPONENT_TYPE_ANCHOR

OpenXRSpatialComponentAnchorList

Предоставляет нам позу (местоположение + ориентацию) каждого якоря

COMPONENT_TYPE_PERSISTENCE

OpenXRSpatialComponentPersistenceList

Предоставляет нам состояние постоянства и UUID каждого якоря

...

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

Наконец, мы можем обработать наш снимок. Обратите внимание, что мы используем OpenXRAnchorTracker в качестве класса трекера, поскольку он уже имеет всю встроенную поддержку якорей.

...

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)

Возможность отслеживания плоскостей

Отслеживание плоскостей обрабатывается классом-синглтоном OpenXRSpatialPlaneTrackingCapability.

После создания сеанса OpenXR вы можете вызвать OpenXRSpatialPlaneTrackingCapability.is_supported, чтобы проверить, поддерживается ли функция отслеживания плоскостей на вашем оборудовании.

Хотя мы уже предоставили большую часть кода для отслеживания плоскостей выше, ниже мы представим полную реализацию, так как она содержит несколько небольших изменений. Здесь нет необходимости обновлять снимки, мы просто делаем снимок обнаружения и реализуем функцию обработки.

Отслеживание плоскостей предоставляет доступ к двум компонентам, поддержка которых гарантирована, и трём необязательным.

Компоненты отслеживания плоскостей

Компонент

Класс данных

Описание

COMPONENT_TYPE_BOUNDED_2D

OpenXRSpatialComponentBounded2DList

Предоставляет нам центральную позу и ограничивающий прямоугольник для каждой плоскости.

COMPONENT_TYPE_PLANE_ALIGNMENT

OpenXRSpatialComponentPlaneAlignmentList

Предоставляет нам выравнивание каждой плоскости

COMPONENT_TYPE_MESH_2D

OpenXRSpatialComponentMesh2DList

Предоставляет нам 2D-меш, формирующий каждую плоскость

COMPONENT_TYPE_POLYGON_2D

OpenXRSpatialComponentPolygon2DList

Предоставляет нам 2D-многоугольник, формирующий каждую плоскость

COMPONENT_TYPE_PLANE_SEMANTIC_LABEL

OpenXRSpatialComponentPlaneSemanticLabelList

Предоставляет нам идентификацию типа каждой плоскости

Наш объект конфигурации отслеживания плоскостей уже включает все поддерживаемые компоненты, но нам нужно его проверить, поэтому мы сохраним наш экземпляр в переменной-члене. Мы можем использовать наш объект трекера OpenXRPlaneTracker для хранения данных наших компонентов.

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)

Возможность отслеживания маркеров

Отслеживание маркеров обрабатывается классом-синглтоном OpenXRSpatialMarkerTrackingCapability.

Отслеживание маркеров работает аналогично отслеживанию плоскостей, однако теперь мы отслеживаем конкретные сущности в реальном мире на основе кода, напечатанного на объекте, например на листе бумаги.

Существует несколько вариантов отслеживания маркеров. OpenXR поддерживает 4 из коробки; в следующей таблице приведена дополнительная информация и имя функции, с помощью которой можно проверить, поддерживает ли ваш шлем конкретный вариант:

Параметры отслеживания маркеров

Опция

Проверка поддержки

Объект конфигурации

Тег April

april_tag_is_supported

OpenXRSpatialCapabilityConfigurationAprilTag

Aruco

aruco_is_supported

OpenXRSpatialCapabilityConfigurationAruco

QR-код

qrcode_is_supported

OpenXRSpatialCapabilityConfigurationQrCode

Micro QR-код

micro_qrcode_is_supported

OpenXRSpatialCapabilityConfigurationMicroQrCode

Каждый вариант имеет свой собственный объект конфигурации, который можно использовать при создании пространственной сущности.

QR-коды позволяют закодировать строку, которая декодируется средой выполнения XR и становится доступной при обнаружении маркера. В тегах April и маркерах Aruco кодируются двоичные данные, к которым вы также можете получить доступ при обнаружении маркера, однако вам необходимо настроить обнаружение с правильным форматом декодирования.

В качестве примера мы создадим пространственный контекст, который будет находить QR-коды и маркеры Aruco.

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)

Каждый маркер, независимо от типа, будет состоять из двух компонентов:

Компоненты отслеживания маркеров

Компонент

Класс данных

Описание

COMPONENT_TYPE_MARKER

OpenXRSpatialComponentMarkerList

Предоставляет нам тип, ID (Aruco и April Tag) и/или данные (QR-код) для каждого маркера.

COMPONENT_TYPE_BOUNDED_2D

OpenXRSpatialComponentBounded2DList

Предоставляет нам центральную позу и ограничивающий прямоугольник для каждой плоскости.

Мы добавляем нашу реализацию обнаружения:

...

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)

И мы добавляем нашу функцию обновления:

...


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)