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
Налаштування просторових сутностей

Налаштування

Опис

Увімкнено

Вмикає ядро системи просторових сутностей. Це має бути ввімкнено для роботи будь-якої з систем просторових сутностей.

Увімкнути просторові прив'язки

Вмикає функцію просторових прив'язок, яка дозволяє створювати та відстежувати просторові прив'язки.

Увімкнути постійні прив'язки

Дозволяє зробити просторові прив'язки постійними. Це означає, що їхнє розташування зберігається та може бути отримано в наступних сеансах.

Увімкнути вбудоване виявлення якорів

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

Увімкнути відстеження літака

Забезпечує функцію відстеження площини, яка дозволяє виявляти такі поверхні, як підлога, стіни, стелі та столи.

Увімкнути вбудоване виявлення площин

Вмикає нашу вбудовану логіку виявлення площин, яка автоматично реагуватиме на появу нових даних про площини.

Увімкнути відстеження маркерів

Активує нашу функцію відстеження маркерів, яка дозволяє виявляти такі маркери, як QR-коди, маркери Aruco та мітки April.

Увімкнути вбудоване відстеження маркерів

Вмикає нашу вбудовану логіку виявлення маркерів, яка автоматично реагуватиме на виявлення нових маркерів або їх переміщення по простору гравця.

Примітка

Зверніть увагу, що для різних пристроїв XR також потрібно встановити прапорці дозволів. Їх потрібно буде ввімкнути в налаштуваннях стилів експорту.

Увімкнення різних можливостей активує пов’язані API OpenXR, але для взаємодії з цими даними потрібна додаткова логіка. Для кожної основної системи у нас є вбудована логіка, яку можна ввімкнути, щоб зробити це за вас.

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

Створення нашого просторового менеджера

Коли просторові об'єкти виявляються або створюються, створюється екземпляр об'єкта OpenXRSpatialEntityTracker та реєструється за допомогою XRServer.

Кожен тип просторової сутності реалізовуватиме свій власний підклас, і таким чином ми можемо по-різному реагувати на кожен тип сутності.

Загалом кажучи, ми будемо створювати різні підсцени для кожного типу сутності. Оскільки об'єкти трекера можна використовувати з вузлами XRAnchor3D, ці підсцени повинні мати такий вузол як кореневий вузол.

Усі трекери сутностей показуватимуть своє місцезнаходження через позу 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 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)

Просторові якорі

Просторові прив'язки дозволяють нам відображати реальні місця у нашому віртуальному світі таким чином, що середовище виконання 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-кодів, мікро-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 надають ідентифікатор, який зчитується з коду.

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

Доступ до серверної частини

Для більшості цілей основна система разом з будь-якими розширеннями постачальників повинна бути такою, якою більшість користувачів використовуватимуть її у наданому вигляді.

Для тих, хто впроваджує розширення постачальників, або для тих, кому вбудованої логіки недостатньо, доступ до серверної частини забезпечується через набір об'єктів-синглтонів.

Ці об'єкти також можна використовувати для запиту можливостей, що підтримуються використовуваною гарнітурою. Ми вже додали код, який перевіряє їх наявність, у нашому просторовому менеджері та коді просторового прив'язування у розділах вище.

Примітка

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

Ядро просторової сутності

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

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

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

Просторові контексти

Просторовий контекст – це основний об'єкт, через який ми запитуємо систему просторових сутностей. Просторові контексти дозволяють нам налаштувати, як ми взаємодіємо з однією або кількома можливостями.

Рекомендується створити просторовий контекст для кожної можливості, з якою ви хочете взаємодіяти, власне, саме це робить 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. Цей об'єкт міститиме запис для кожної повернутої сутності з її унікальним ідентифікатором та поточним станом сутності.

Завершуючи нашу логіку виявлення, ми додаємо наступне:

...

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, де ми створюємо просторовий об'єкт сутності з нашого ідентифікатора сутності. Поки цей об'єкт існує, середовище виконання 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 підтримуються два типи областей видимості персистенції:

Області дії стійкості

Enum

Опис

PERSISTENCE_SCOPE_SYSTEM_MANAGED

Надає програмі доступ лише для читання (тобто програми не можуть змінювати це сховище) до просторових сутностей, що зберігаються та керуються системою. Програма може використовувати UUID у компоненті persistent для цього сховища, щоб співвідносити сутності в різних просторових контекстах та перезавантаженнях пристроїв.

PERSISTENCE_SCOPE_LOCAL_ANCHORS

Операції збереження та доступ до даних обмежені просторовими якорями, на одному пристрої, для одного користувача та програми (за допомогою функцій persist_anchor та unpersist_anchor)

Ми почнемо з нового скрипта, який обробляє наші просторові прив'язки. Він буде схожим на скрипт, представлений раніше, але з кількома відмінностями.

Перше — це створення нашої області видимості (persistence scope).

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

Надає нам двовимірну сітку, яка формує кожну площину

COMPONENT_TYPE_POLYGON_2D

OpenXRSpatialComponentPolygon2DList

Надає нам двовимірний полігон, який формує кожну площину

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_tag_is_supported

OpenXRSpatialCapabilityConfigurationAprilTag

Аруко

aruco_is_supported

OpenXRSpatialCapabilityConfigurationAruco

QR-код

qrcode_is_supported

OpenXRSpatialCapabilityConfigurationQrCode

Мікро 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

Надає нам тип, ідентифікатор (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)