reckfellENGINE

Пространственные объекты OpenXR: якоря, плоскости и маркеры

Рабочая привязка трекеров, потеря tracking, UUID, геометрия плоскостей и владение snapshots.

На этой странице

Пространственные объекты OpenXR связывают приложение с реальным помещением: якорь хранит привязку к месту, плоскость описывает поверхность, маркер — обнаруженный код. Это данные XR runtime, а не Entity игрового дерева и не ECS. Для отображения создайте обычную Entity, связанную с трекером.

Что включить

Сначала настройте OpenXR. Нужны работающая сессия, поддержка соответствующих spatial extensions и разрешения целевого устройства. Настройки проекта запрашивают возможности, но не добавляют их устройству. Получение плоскостей не включает автоматически passthrough, физические столкновения или сканирование комнаты.

Ключи ниже находятся под xr/openxr/extensions/spatial_entity/. Все выключены по умолчанию; параметры расширений задавайте до старта OpenXR.

ЗадачаКлючи
Общая spatial APIenabled=true
Якоряenable_spatial_anchors=true
Сохранение якорейenable_persistent_anchors=true
Встроенное обнаружение/обновление якорейenable_builtin_anchor_detection=true
Плоскостиenable_plane_tracking=true, enable_builtin_plane_detection=true
Маркерыmarker_tracking/enable=true, marker_tracking/enable_builtin_for_types

Последний параметр — битовая маска: QR=1, Micro QR=2, ArUco=4, AprilTag=8; например 1 запрашивает встроенную обработку QR, 15 — всех четырёх типов. Это не гарантия поддержки всех типов runtime. Для ArUco/AprilTag также выберите соответствующий словарь в настройках marker tracking. Отдельного актуального переключателя enable_builtin_marker_tracking не используйте.

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

Подключение трекеров к дереву

Runtime регистрирует OpenXRAnchorTracker, OpenXRPlaneTracker и OpenXRMarkerTracker в XRServer. Все относятся к категории Anchor и предоставляют pose default. Обычный XRPositionalTracker категории Anchor при этом не обязательно является spatial tracker.

Добавьте Entity со скриптом ниже непосредственно под XROrigin3D. Скрипт создаёт пустые XRAnchor3D непосредственно под тем же origin. В OnChanged после создания view добавьте подходящее содержимое из своего .rprefab: отдельное для якоря, плоскости или маркера. Сам пример не создаёт видимую геометрию и не запускает обнаружение.

Текст
XROrigin3D
├── XRCamera3D
├── SpatialVisuals
└── XRAnchor3D (created at runtime)
    └── Your content
C#
using System;
using System.Collections.Generic;
using Reckfell;

public partial class SpatialVisuals : Entity
{
    private XROrigin3D _origin = null!;
    private readonly Dictionary<StringName, (OpenXRSpatialEntityTracker Tracker, XRAnchor3D View)> _views = new();
    public int ViewCount => _views.Count;

    public static bool IsTracked(OpenXRSpatialEntityTracker tracker)
    {
        var pose = tracker.GetPose("default");
        return tracker.SpatialTrackingState == OpenXRSpatialEntityTracker.EntityTrackingState.Tracking
            && pose != null && pose.HasTrackingData;
    }

    public override void _EnterTree()
    {
        _origin = GetParent() as XROrigin3D
            ?? throw new InvalidOperationException("SpatialVisuals must be a direct child of XROrigin3D.");
        XRServer.TrackerAdded += OnChanged;
        XRServer.TrackerUpdated += OnChanged;
        XRServer.TrackerRemoved += OnRemoved;
        using var trackers = XRServer.GetTrackers((int)XRServer.TrackerType.Anchor);
        foreach (var key in trackers.Keys) OnChanged(key.AsStringName(), (long)XRServer.TrackerType.Anchor);
    }

    private void OnChanged(StringName name, long type)
    {
        OnRemoved(name, type);
        if (XRServer.GetTracker(name) is not OpenXRSpatialEntityTracker tracker) return;
        var view = new XRAnchor3D { Tracker = name, Pose = "default", Visible = false };
        _views.Add(name, (tracker, view));
        _origin.AddChild(view);
    }

    private void OnRemoved(StringName name, long type)
    {
        if (_views.Remove(name, out var item)) { item.View.Visible = false; item.View.QueueFree(); }
    }

    public override void _Process(double delta)
    {
        foreach (var item in _views.Values) item.View.Visible = IsTracked(item.Tracker);
    }

    public override void _ExitTree()
    {
        XRServer.TrackerAdded -= OnChanged;
        XRServer.TrackerUpdated -= OnChanged;
        XRServer.TrackerRemoved -= OnRemoved;
        foreach (var item in _views.Values) { item.View.Visible = false; item.View.QueueFree(); }
        _views.Clear();
    }
}

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

XRServer, трекеры и возвращённые ими pose здесь заимствованные: не вызывайте Dispose/Free. Менеджер владеет только созданными XRAnchor3D. QueueFree безопасно откладывает их удаление; после запроса старое представление сразу скрывается. Не заменяйте его на Free в _ExitTree: родитель может ещё изменять список детей. Скрипт рассчитан на основной поток.

Показ разрешён только при SpatialTrackingState=Tracking и HasTrackingData у pose. Paused/Stopped или невалидная pose скрывают представление, а не оставляют его видимым на последнем месте. Visible не выключает физические формы, звук, обработку ввода или игровой процесс: отключайте их отдельно при потере tracking. Для диагностики показывайте число ViewCount и состояние трекера; ноль не означает автоматически отсутствие поддержки.

Создание и сохранение якоря

Используйте зарегистрированный OpenXRSpatialAnchorCapability, проверяя его наличие и IsSpatialAnchorSupported после запуска сессии. Не создавайте новый capability через конструктор ради проверки поддержки. Полученный объект движка не освобождайте.

  1. Вызовите CreateNewAnchor и проверьте результат на null. Передавайте pose в пространстве OpenXR play space. В текущих исходниках обратное применение WorldScale/reference frame оставлено TODO: не подставляйте произвольный GlobalTransform. Простейший случай ограничьте WorldScale=1, единичным reference frame/world origin; остальные преобразования требуют отдельной проверки.
  2. Дождитесь SpatialTrackingState=Tracking и валидной pose. Возвращённый tracker сам по себе не подтверждает устойчивую локализацию.
  3. Для сохранения проверьте IsSpatialPersistenceSupported и готовность persistence context. С включённой встроенной обработкой используется её default context; собственный context создаётся асинхронно.
  4. Вызовите PersistAnchor, сохраните возвращённый future и дождитесь завершения. Null означает, что запрос не запущен. У завершённого запроса проверьте GetResultValue и HasUuid; только после этого сохраняйте UUID вместе с идентификатором своего содержимого и его данными.
  5. При следующем обнаружении сопоставляйте UUID с записью приложения. Не сохраняйте tracker Name или RID вместо UUID: это идентификаторы текущего сеанса. Наличие UUID в своём JSON не означает, что устройство восстановило якорь.

Persistence scope LocalAnchors предназначен для того же приложения, пользователя и устройства; не обещает перенос между устройствами. SystemManaged даёт доступ к системно сохранённым объектам и не является изменяемым хранилищем приложения. Удаление приложения, сброс данных, изменение помещения и потеря локализации требуют обработки как отдельных случаев.

Для удаления сохранённого якоря сначала UnpersistAnchor; дождитесь успешного результата и исчезновения UUID, затем RemoveAnchor. Unpersist не удаляет tracker автоматически. RemoveAnchor отвергает всё ещё persistent anchor. Освобождение графической Entity само по себе не удаляет сохранённую привязку.

Future — не Task и не результат операции. GetStatus отличает Running, Finished и Cancelled; завершение также бывает ошибочным. В текущем C# SDK Completed описан с параметром OpenXRFutureResult, тогда как native emission передаёт result_value (например bool или RID): не предполагайте корректную доставку такого результата через типизированное событие. В интеграции проверяйте GetStatus/GetResultValue и состояние операции; реальная C# async-доставка на устройстве здесь не проверена. Не запускайте повторное сохранение каждый кадр и не объявляйте тайм-аут доказательством отсутствия поддержки.

Плоскости и маркеры

У OpenXRPlaneTracker слушайте MeshChanged и обновляйте MeshInstance3D.Mesh и CollisionShape3D.Shape из GetMesh/GetShape. Применяйте GetMeshOffset к общему дочернему Entity3D, содержащему визуальную и физическую геометрию: offset относительно pose якоря может быть ненулевым. Встроенная геометрия лежит в локальной XY-плоскости; не добавляйте безусловный поворот на 90°. Если обе границы положительные, возможен прямоугольный fallback; при отсутствии геометрии/границ возвращается null — уберите старые mesh/collider. Mesh и shape кешируются трекером, не освобождайте заимствованные обёртки. Для разных параметров физической формы не рассчитывайте на изменение уже кешированной GetShape простым повторным вызовом.

MeshChanged не сообщает обо всех изменениях offset: в текущей реализации SetMeshData с прежними vertices/indices и новым origin меняет GetMeshOffset без события. Пока tracker валиден, обновляйте transform дочерней геометрии из GetMeshOffset каждый кадр; перестроение mesh/shape оставьте обработчику MeshChanged. Этот случай воспроизведён отдельно; исходники движка не исправлялись.

Результат детектора может быть старым сканом комнаты, а не текущим полным мешем. PlaneLabel и alignment заполняются только при наличии данных; неизвестная метка не равна полу. Обнаруженная плоскость не становится стеной физического мира, пока вы не назначите форму своему StaticBody3D. Passthrough occlusion требует отдельного материала и проверки прозрачности/композиции на шлеме; обычный чёрный материал не считается доказательством корректного скрытия виртуальных объектов.

У OpenXRMarkerTracker сначала проверьте MarkerType. QR/Micro QR могут содержать строку или byte[] в GetMarkerData; неизвестный тип данных обрабатывайте явно. ArUco/AprilTag сопоставляйте по типу, выбранному словарю и MarkerId, не по предполагаемой строке. Не интерпретируйте содержимое QR как произвольный путь к ресурсу: используйте таблицу разрешённых идентификаторов своего приложения.

Когда нужен собственный backend

OpenXRSpatialEntityExtension предоставляет SupportsCapability/SupportsComponentType, создание contexts, discovery/update snapshots и чтение компонентов. Нужна поддержка каждой используемой возможности; наличия типа C# недостаточно.

  • CreateSpatialContext асинхронно возвращает context RID через результат; проверьте успех и GetSpatialContextReady перед запросами.
  • В массивах component data первым должен быть OpenXRSpatialQueryResultData; остальные компоненты выбирайте по реальной поддержке runtime.
  • Прямой DiscoverSpatialEntities возвращает принадлежащий вызывающему snapshot RID. Завершите чтение/копирование буферов, затем FreeSpatialSnapshot; для update snapshot действует то же правило.
  • Capability StartEntityDiscovery сам читает snapshot и освобождает его после пользовательского callback. Не сохраняйте этот RID на потом и не освобождайте повторно; копируйте необходимые данные внутри callback.
  • Tracker владеет назначенным ему spatial entity RID: SetEntity может освободить предыдущий. Не вызывайте FreeSpatialEntity для RID встроенного трекера. Свои contexts освобождайте только после остановки запросов и освобождения зависимых объектов; default contexts движка не освобождайте.

Диагностика

СимптомЧто проверить
Нет трекеровСессия OpenXR, permissions, конкретная capability, включённый встроенный detector, наличие room/marker data
Объект застыл или пропалSpatialTrackingState, HasTrackingData, pose default, правильный XROrigin3D и обработка Paused
Якорь появился не тамPlay-space transform, WorldScale/reference frame, отсутствие повторного применения world origin
После перезапуска нет содержимогоУспешное сохранение, UUID, локальное сопоставление UUID→контент, тот же пользователь/приложение/устройство
Плоскость смещена или не сталкиваетсяGetMeshOffset, локальная XY-геометрия, обновлённая Shape и CollisionLayer/Mask
Маркер не распознанПоддержка именно этого типа, словарь, качество/размер изображения, permissions и условия камеры

Границы проверки

На установленном Windows Runtime 1.0.0 выполнены 20 headless проверок точного скрипта: synthetic tracker resources через настоящий XRServer, существующие/новые записи, замена без дублей, pose/state gating, скрытие и отложенная очистка, локальная геометрия ограниченной плоскости и данные маркера. Это проверка клиентской логики, не обнаружение реальной комнаты. Реальный шлем, создание/восстановление якорей, async persistence, permissions, сканирование, физические столкновения, passthrough pixels и export не проверены. Исходники движка, Runtime и SDK не менялись.

Документация Reckfell EngineНаверх ↑
БЫСТРЫЙ ПЕРЕХОДНачало работыEntity и иерархияC# & .NET 10Сцены и префабы
Руководства и полный API Reckfell K
reckfellENGINE
RECKFELL ACCOUNT

Вход в аккаунт

Войдите в аккаунт Reckfell.

Один аккаунт для всей платформы Reckfell.