Телефонный VR: MobileVRInterface
Проверенный lifecycle helper, параметры линз, единицы и ограничения телефонного stereo.
Телефонный VR: MobileVRInterface
MobileVRInterface — встроенный интерфейс для stereo-вывода на экран телефона в держателе. Имя в XRServer — Native mobile. Это отдельный путь от OpenXR и ARCore. Он моделирует поворот головы по доступным данным датчиков, а положение задаёт фиксированной высотой EyeHeight: ходьбу по комнате не отслеживает.
Если целевое устройство работает через OpenXR, используйте руководство запуска OpenXR. Этот раздел нужен для простого телефонного stereo-пути и диагностики его жизненного цикла.
Что означает успешный запуск
Initialize создаёт tracker head, делает интерфейс primary и возвращает true. В текущей реализации это происходит и без физического телефона. Поэтому сообщение «гарнитура подключена» после одного Initialize неверно. Два вида, наличие head pose и её фиксированная высота также не доказывают наличие датчиков или точность tracking.
Для вывода нужен Viewport с UseXR=true и XR-риг. Успешный запуск интерфейса не включает этот флаг автоматически. Не запускайте одновременно два компонента, управляющих primary interface и tracker head.
Пример с одним владельцем
Helper использует зарегистрированный интерфейс, отказывается перезаписывать существующий head/primary, сохраняет UseXR и освобождает только собственный запуск:
using System;
using Reckfell;
public sealed class MobileVrPreview
{
private MobileVRInterface? current;
private XRTracker? head;
private Viewport? viewport;
private bool previousUseXr;
// One owner, main thread. Call TryStop before leaving the preview.
public bool TryStart(Viewport target)
{
ArgumentNullException.ThrowIfNull(target);
if (current != null) return false;
var candidate = XRServer.FindInterface("Native mobile") as MobileVRInterface;
if (candidate == null || candidate.IsInitialized()
|| XRServer.PrimaryInterface != null || XRServer.GetTracker("head") != null)
return false;
bool oldUseXr = target.UseXR;
if (!candidate.Initialize()) return false;
var tracker = XRServer.GetTracker("head");
if (tracker == null)
{
candidate.Uninitialize();
return false;
}
current = candidate;
head = tracker;
viewport = target;
previousUseXr = oldUseXr;
target.UseXR = true;
return true;
}
public bool TryStop()
{
var active = current;
if (active == null) return true;
// Do not unregister another provider's head tracker after a conflict.
if (XRServer.PrimaryInterface != active || XRServer.GetTracker("head") != head)
return false;
if (viewport != null && ReckfellObject.IsInstanceValid(viewport))
viewport.UseXR = previousUseXr;
active.Uninitialize();
current = null;
head = null;
viewport = null;
return true;
}
}Сохраните один экземпляр MobileVrPreview в управляющем Entity. В _Ready передайте TryStart свой живой GetViewport(); проверяйте bool. При выходе из preview, до удаления владельца, вызовите TryStop. Для сообщений используйте ReckfellApi.Print, а не старый GD.Print.
TryStop=false означает конфликт владения: другой компонент изменил primary или head. Helper в этом случае не удаляет чужой tracker. Разрешите конфликт в управляющей логике; не продолжайте запуск второго XR-провайдера поверх первого. Повторный TryStop после успешной остановки безопасен.
Интерфейс и tracker заимствованы у XRServer. Не вызывайте у них Dispose/Free. Helper также не освобождает Viewport. Он не меняет параметры линз, глобальный WorldScale или reference frame: настройте их отдельно до старта.
Параметры и единицы
| Параметр C# | По умолчанию | Смысл |
|---|---|---|
| EyeHeight | 1.85 | Предполагаемая высота в метрах до применения WorldScale, не измерение положения телефона. |
| Iod | 6.0 | Межзрачковое расстояние в сантиметрах. При WorldScale=1 смещения глаз равны −0.03 и +0.03. |
| DisplayWidth | 14.5 | Физическая ширина экрана в сантиметрах, не разрешение в пикселях. |
| DisplayToLens | 4.0 | Расстояние от дисплея до линз в сантиметрах. |
| Oversample | 1.5 | Множитель размеров render target, влияющий также на расчёт frustum; не MSAA. |
| K1, K2 | 0.215, 0.215 | Безразмерные коэффициенты lens distortion, не универсальная калибровка всех держателей. |
| OffsetRect | (0, 0, 1, 1) | Область вывода относительно полного экранного прямоугольника. |
| VrsMinRadius | 20 | Центральная область полной плотности в процентах размера, если используется VRS. Допустимый диапазон setter: 1–100. |
| VrsStrength | 1 | Сила построения VRS-карты, диапазон 0.1–10. Не гарантия экономии GPU. |
Измеряйте реальные параметры экрана и держателя. Не передавайте метры в DisplayToLens/Iod и сантиметры в EyeHeight. Для оптической модели нужны осмысленные положительные размеры. Setter многих параметров сохраняет число без проверки физической корректности.
Автоматическое чтение QR-профиля держателя в этом пути не реализовано: коэффициенты и размеры не подбираются по одному Initialize. Увеличение Oversample не исправляет ошибочную геометрию линз. В коде размер target на глаз рассчитывается как половина ширины окна × Oversample и высота окна × Oversample; большее значение увеличивает объём рендеринга.
Масштаб и кэш глаз
Задавайте WorldScale до запуска. В текущей реализации GetCameraOffsets кэширует смещения глаз. В выполненной проверке после смены WorldScale с 1 на 2 уже вычисленные ±0.03 остались прежними. После контролируемого TryStop/TryStart они стали ±0.06. Это воспроизведённое ограничение текущего движка, а не исправленная ошибка.
Изменение Iod на другое значение очищает кэш offsets и projections; DisplayWidth, DisplayToLens и Oversample очищают projections. Повторная запись того же значения не является универсальным сбросом. Getter массивов может возвращать общий кэш: не редактируйте его элементы для изменения настроек.
Датчики и вывод
Для Android исходник описывает настройки input_devices/sensors/enable_accelerometer, enable_gravity, enable_gyroscope и enable_magnetometer. Это включение источников ввода, а не доказательство наличия оборудования. Проверьте реальные значения и поведение на целевом устройстве. Состояние GetTrackingStatus и качество sensor fusion этим прогоном не приняты.
Основной путь вывода делит экран на левую и правую половины с lens distortion. Для SubViewport без экранного прямоугольника интерфейс не выводит изображение на экран и предупреждает об этом: отдельный render target сам по себе не становится изображением в держателе. VRS требует подходящего RD-рендерера и Viewport.VrsMode=Xr; свойства VRS не означают, что карта реально используется.
Пример C# проверен на Windows/SDK 1.0.0 как lifecycle-код. Это не подтверждение C#-экспорта на телефон. Поддержку платформы, языкового runtime и модулей нужно подтверждать используемой сборкой и тестом на устройстве.
Диагностика
| Симптом | Проверить |
|---|---|
| Native mobile не найден | Наличие mobile_vr в сборке; модуль отключается при disable_xr. |
| TryStart возвращает false | Уже запущенный интерфейс, занятые primary/head, повторный старт или отсутствие модуля. |
| Initialize успешен, голова неподвижна | Датчики и их настройки; успех инициализации не проверяет телефон. |
| Неверная высота | EyeHeight в метрах и WorldScale; это фиксированная модель высоты. |
| Изображение не сходится | Iod/DisplayWidth/DisplayToLens в сантиметрах, параметры держателя и distortion. |
| Смещения глаз не меняются с масштабом | Остановите собственный preview и запустите после настройки масштаба; не перезаписывайте чужой head. |
| TryStop возвращает false | Конкурирующего владельца primary/head; helper специально не удаляет чужие данные. |
Что проверено
Установленный Reckfell 1.0.0, Windows headless с XR off: 22 наблюдения по параметрам, старту/остановке helper, конфликтам владения, восстановлению UseXR, фиксированной высоте и расчётам двух видов. Для датчиков и изображения аппаратная приёмка не проводилась. Исходная проблема live-WorldScale кэша сохранена в отчёте как неуспешная приёмка.
Не проверены телефон, C#-экспорт на него, GetTrackingStatus initialization, точность датчиков, реальные линзы, stereo pixels, VRS output, комфорт и FPS. Исходники движка, установленный Runtime и SDK не менялись.