Модели XR-контроллеров: загрузка и fallback
Проверка доступности, порядок событий, фильтры рук и управление геометрией устройства.
Модели устройств OpenXR
Render models — визуальные модели оборудования, предоставленные XR runtime. Текущая реализация Reckfell использует XR_EXT_render_model и XR_EXT_interaction_render_model. Наличие interaction profile не подтверждает доступность модели конкретного устройства.
Сначала настройте запуск XR и action map. Включите xr/openxr/extensions/render_model до запуска приложения. Для этих классов нужна сборка с OpenXR и glTF. Сохранённая галочка и наличие классов в SDK не означают, что runtime активировал оба расширения или загрузил геометрию.
Начать с общего менеджера
Для первого варианта используйте один OpenXRRenderModelManager с Tracker=Any непосредственно под XROrigin3D. Оставьте MakeLocalToPose пустым и сохраняйте собственный fallback-визуал, пока реальная модель недоступна.
XROrigin3D
├── XRCamera3D
├── OpenXRRenderModelManager (Tracker=Any)
└── FallbackVisualsСоздавайте менеджер только после проверки runtime-синглтона и IsActive. В текущем коде менеджер обращается к native-синглтону даже при уведомлениях создания; создание при отсутствующем wrapper может дать native error. Не создавайте OpenXRRenderModelExtension через new в игровом коде: wrapper управляется системой OpenXR.
using System;
using Reckfell;
public static class RenderModelGate
{
// Returns a new child owned by origin, or null so the caller can retain fallback visuals.
public static OpenXRRenderModelManager? CreateIfActive(XROrigin3D origin,
Action<OpenXRRenderModel> added, Action<OpenXRRenderModel> removed)
{
ArgumentNullException.ThrowIfNull(origin);
ArgumentNullException.ThrowIfNull(added);
ArgumentNullException.ThrowIfNull(removed);
if (!origin.IsInsideTree()) return null;
if (!Engine.HasSingleton("OpenXRRenderModelExtension")) return null;
var extension = Engine.GetSingleton("OpenXRRenderModelExtension") as OpenXRRenderModelExtension;
if (extension == null || !extension.IsActive()) return null;
var manager = new OpenXRRenderModelManager
{
Tracker = OpenXRRenderModelManager.RenderModelTracker.Any
};
// Subscribe before AddChild: existing runtime models can be reported during EnterTree.
manager.RenderModelAdded += model => added(model);
manager.RenderModelRemoved += model => removed(model);
origin.AddChild(manager);
return manager;
}
}Вызовите CreateIfActive после запуска XR, передав живой origin и обработчики. null оставляет fallback под управлением вашего компонента. Возвращённый менеджер уже добавлен к origin; повторно AddChild не нужен. Он и его модели освобождаются с деревом. Синглтон extension заимствован: не вызывайте у него Free/Dispose.
Подписка внутри helper происходит до AddChild: менеджер может сообщить об уже известных моделях прямо при EnterTree. Если подписываться после добавления, начальные события можно пропустить. Положительная ветка с реальным runtime этим прогоном не проверена; проверены отказ без синглтона и аргументы.
Событие добавления не равно готовой картинке
RenderModelAdded менеджера передаёт OpenXRRenderModel Entity. Оно означает добавление записи в дерево менеджера. Если render_model_data отсутствует, создание дочерней геометрии может вернуть null, но событие Entity всё равно возможно. Не скрывайте fallback только по счётчику этих событий.
Для переключения визуалов проверьте фактическую геометрию выбранной модели и текущую tracking confidence через extension. При Confidence=None встроенный OpenXRRenderModel перестаёт обновлять transform/анимируемые части, но не скрывает предыдущую геометрию. Политику скрытия устаревшей модели и возврата fallback реализуйте отдельно. IsActive тоже не является флагом «контроллер загружен».
RenderModelRemoved передаётся перед remove_child/queue_free. Объект ещё доступен в обработчике, но сохранять его как живую модель на следующий кадр нельзя. У менеджера с фильтром обработка может быть отложена: backend RID к этому моменту уже может быть отозван, поэтому не запрашивайте по нему данные extension. Не освобождайте manager-owned Entity внутри этого события: удаление выполнит менеджер. Обновите собственное состояние fallback и отсоедините свои ссылки.
Отдельно для левой и правой руки
| Tracker | Размещение |
|---|---|
| Any | Все модели; менеджер непосредственно под XROrigin3D. |
| NoneSet | Модели без определённого top-level path; также непосредственно под origin. |
| LeftHand / RightHand | Фильтр по пути руки; origin должен быть предком, допускается промежуточный Entity. |
Для размещения внутри XRController3D используйте одинаковое имя pose у контроллера и MakeLocalToPose менеджера, например grip:
XROrigin3D
└── XRController3D (Tracker=left_hand, Pose=grip)
└── OpenXRRenderModelManager (Tracker=LeftHand, MakeLocalToPose=grip)MakeLocalToPose применяет обратный adjusted transform pose к внутреннему контейнеру. Это компенсация уже применённой pose, а не имя файла и не смена действия ввода. Включайте её только для конкретной руки и соответствующего родителя; Any/NoneSet с этой настройкой дают configuration warning.
Tracker связывается с positional tracker и XR path в setter при изменении значения. Если тогда API/tracker ещё отсутствовали, повтор той же настройки не гарантирует повторную привязку. Настраивайте hand-менеджер после готовности provider/trackers; при смене setup пересоздавайте принадлежащий вам менеджер контролируемо. Source также проверяет существование pose, а не её HasTrackingData, поэтому потерю tracking нужно обрабатывать явно.
Top-level path /user/hand/left или /user/hand/right — связь модели с рукой, а не доказательство, что контроллер физически удерживается. Система моделей не создаёт коллизии, ограничения движения руки или игровую логику кнопок. Эти задачи используйте отдельно от визуальной геометрии.
Прямая работа с OpenXRRenderModel
RenderModel принимает RID именно этого OpenXR extension. Это не mesh RID RenderingServer и не текстура RenderingDevice. Получайте актуальные идентификаторы у provider; не придумывайте числа и не освобождайте чужие RID через FreeRid/RenderModelDestroy.
Setter RenderModel сохраняет RID и при нахождении в дереве пытается добавить геометрию. В текущем исходнике он не удаляет прежние children: повторная установка ненулевого RID может добавить ещё одну сцену, а default RID сам по себе не очищает уже загруженную. Для замены собственного прямого экземпляра создайте новый OpenXRRenderModel и удалите старый Entity; для manager-owned экземпляров оставьте lifecycle менеджеру. Эти пути с реальной геометрией сверены по исходнику, не выполнены на гарнитуре.
GetTopLevelPath возвращает строку пути или пустую строку. RenderModelTopLevelPathChanged уведомляет о смене связи для конкретной модели; это не событие загрузки mesh. Root transform и анимируемые части обновляются из provider. Не добавляйте origin/reference frame второй раз поверх уже настроенной иерархии.
Диагностика
| Симптом | Проверить |
|---|---|
| Классы есть, менеджер даёт native error | Реальный runtime-синглтон, IsActive и момент создания; не создавайте wrapper вручную. |
| IsActive=true, но геометрии нет | Runtime-модели, данные mesh, action map и связь устройства; это отдельные состояния. |
| Дубли контроллеров | Несколько менеджеров с пересекающимися фильтрами, свой fallback и повторные SetRenderModel. |
| Модель смещена вдвое | Родитель XRController3D, его Pose, MakeLocalToPose и отсутствие второго origin/pose transform. |
| После потери tracking остался старый контроллер | Confidence=None не скрывает Entity автоматически; включите свою политику fallback. |
| Фильтр руки не обновился после позднего старта | Порядок создания trackers и настройки Tracker; одинаковый setter не является rebind. |
Что проверено
На установленном Reckfell 1.0.0, Windows headless с XR off выполнено 19 проверок: реальное отсутствие зарегистрированного runtime-синглтона, отказ точного helper, null-аргументы, параметры/пустой lifecycle Entity и сигнатуры C# events. Для metadata-проверок тест временно создал собственный неактивный диагностический wrapper, не зарегистрированный в Engine; это приём теста, не игровой пример. Его освобождение исправлено в QA-коде через Free, предупреждений об утечке в итоговом прогоне нет.
События в тесте вызывались вручную: это не обнаружение контроллеров. Сигналы provider, загрузка реальной геометрии, tracking loss visuals, hand filtering, pose rebasing, анимация, изображение и экспорт не проверены. Их ограничения описаны по текущему исходнику. Исходники движка, Runtime и SDK не менялись.