Table of Contents

Создание расширения для ввода данных изображений и движения устройств

Разработчики могут расширить возможности EasyAR Sense, создав пользовательскую реализацию камеры через расширение для ввода данных изображений и движения устройств. Это позволяет поддерживать специфические head-mounted устройства или другие устройства ввода. Ниже описаны шаги и важные аспекты создания такого расширения.

Перед началом

Создание класса источника данных внешнего кадра

Оба являются подклассами MonoBehaviour, имя файла должно совпадать с именем класса.

Например, создание расширения ввода устройства 6DoF:

public class MyFrameSource : ExternalDeviceMotionFrameSource
{
}

При создании расширения гарнитуры можно использовать шаблон com.easyar.sense.ext.hmdtemplate, модифицируя его на основе этого шаблона. Этот шаблон находится внутри архива плагина Unity, загруженного с сайта EasyAR.

Определение устройства

Переопределите IsHMD, чтобы определить, является ли устройство гарнитурой.

Например, для гарнитуры установите значение true.

public override bool IsHMD { get => true; }

Переопределите Display, чтобы определить дисплей устройства.

Например, для гарнитуры используется информация дисплея по умолчанию Display.DefaultHMDDisplay, что определяет поворот дисплея как 0.

protected override IDisplay Display => easyar.Display.DefaultHMDDisplay;

Доступность

Переопределите IsAvailable, чтобы определить, доступно ли устройство.

Например, в RokidFrameSource реализация выглядит так:

protected override Optional<bool> IsAvailable => Application.platform == RuntimePlatform.Android;

Если доступность (IsAvailable) невозможно определить во время сборки сессии, переопределите корутину CheckAvailability(), чтобы приостановить процесс сборки до тех пор, пока не станет ясно, доступно ли устройство.

Сессия точка отсчёта

Переопределите OriginType, чтобы задать тип точки отсчёта, определённый SDK устройства.

Если OriginType имеет значение Custom, также необходимо переопределить Origin.

Например, в RokidFrameSource это реализовано так:

protected override DeviceOriginType OriginType =>
#if EASYAR_HAVE_ROKID_UXR
    hasUXRComponents ? DeviceOriginType.None :
#endif
    DeviceOriginType.XROrigin;

Виртуальная камера

Если OriginType имеет значение Custom или None, необходимо переопределить Camera, чтобы предоставить виртуальную камеру.

Например, в RokidFrameSource это реализовано следующим образом:

protected override Camera Camera => hasUXRComponents ? (cameraCandidate ? cameraCandidate : Camera.main) : base.Camera;

Физическая камера

Переопределите DeviceCameras с типом DeviceFrameSourceCamera, чтобы предоставить информацию о физических камерах устройства. Эти данные используются при вводе кадров с камеры. Создание должно быть завершено, когда CameraFrameStarted имеет значение true.

Например, реализация в RokidFrameSource выглядит следующим образом:

private DeviceFrameSourceCamera deviceCamera;
protected override List<FrameSourceCamera> DeviceCameras => new List<FrameSourceCamera> { deviceCamera };

{
    var imageDimensions = new int[2];
    RokidExtensionAPI.RokidOpenXR_API_GetImageDimensions(imageDimensions);
    size = new Vector2Int(imageDimensions[0], imageDimensions[1]);
    deviceCamera = new DeviceFrameSourceCamera(CameraDeviceType.Back, 0, size, new Vector2(50, 50), new DeviceFrameSourceCamera.CameraExtrinsics(Pose.identity, true), AxisSystemType.Unity);
    started = true;
}

Переопределите CameraFrameStarted, чтобы предоставить индикатор начала ввода кадров с камеры.

Например:

protected override bool CameraFrameStarted => started;

Запуск и остановка сессии

Переопределите OnSessionStart(ARSession) и выполните инициализацию, специфичную для AR. Убедитесь, что сначала вызывается base.OnSessionStart.

Например:

protected override void OnSessionStart(ARSession session)
{
    base.OnSessionStart(session);
    StartCoroutine(InitializeCamera());
}

Это подходящее место для включения камеры устройства (например, RGB-камеры или камеры VST), особенно если эти камеры не предназначены для постоянной работы. Также здесь можно получить калибровочные данные, которые не изменяются в течение всего жизненного цикла. Иногда может потребоваться подождать, пока устройство будет готово или данные обновятся, прежде чем эти данные станут доступны.

Также это подходящее место для запуска цикла ввода данных. Этот цикл также можно написать в Update() или других методах, особенно если данные необходимо получать в определенный момент порядка выполнения Unity. Не вводите данные, пока сессия не будет готова (ready).

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

Например, реализация в RokidFrameSource выглядит так:

private IEnumerator InitializeCamera()
{
    yield return new WaitUntil(() => (RokidTrackingStatus)RokidExtensionAPI.RokidOpenXR_API_GetHeadTrackingStatus() >= RokidTrackingStatus.Detecting && (RokidTrackingStatus)RokidExtensionAPI.RokidOpenXR_API_GetHeadTrackingStatus() < RokidTrackingStatus.Tracking_Paused);

    var focalLength = new float[2];
    RokidExtensionAPI.RokidOpenXR_API_GetFocalLength(focalLength);
    var principalPoint = new float[2];
    RokidExtensionAPI.RokidOpenXR_API_GetPrincipalPoint(principalPoint);
    var distortion = new float[5];
    RokidExtensionAPI.RokidOpenXR_API_GetDistortion(distortion);
    var imageDimensions = new int[2];
    RokidExtensionAPI.RokidOpenXR_API_GetImageDimensions(imageDimensions);

    size = new Vector2Int(imageDimensions[0], imageDimensions[1]);
    var cameraParamList = new List<float> { focalLength[0], focalLength[1], principalPoint[0], principalPoint[1] }.Concat(distortion.ToList().GetRange(1, 4)).ToList();
    cameraParameters = CameraParameters.tryCreateWithCustomIntrinsics(size.ToEasyARVector(), cameraParamList, CameraModelType.OpenCV_Fisheye, CameraDeviceType.Back, 0).Value;
    deviceCamera = new DeviceFrameSourceCamera(CameraDeviceType.Back, 0, size, new Vector2(50, 50), new DeviceFrameSourceCamera.CameraExtrinsics(Pose.identity, true), AxisSystemType.Unity);

    RokidExtensionAPI.RokidOpenXR_API_OpenCameraPreview(OnCameraDataUpdate);
    started = true;
}

Переопределите OnSessionStop() и освободите ресурсы. Убедитесь, что вызывается base.OnSessionStop.

Например, реализация в RokidFrameSource выглядит так:

protected override void OnSessionStop()
{
    base.OnSessionStop();
    RokidExtensionAPI.RokidOpenXR_API_CloseCameraPreview();
    started = false;
    StopAllCoroutines();
    cameraParameters?.Dispose();
    cameraParameters = null;
    deviceCamera?.Dispose();
    deviceCamera = null;
}

Ввод данных кадров камеры

После получения обновления данных кадров камеры вызовите HandleCameraFrameData(DeviceFrameSourceCamera, double, Image, CameraParameters, Pose, MotionTrackingStatus) / HandleCameraFrameData(DeviceFrameSourceCamera, double, Image, CameraParameters, Quaternion) для ввода данных кадров камеры.

Например, реализация в RokidFrameSource выглядит следующим образом:

private static void OnCameraDataUpdate(IntPtr ptr, int dataSize, ushort width, ushort height, long timestamp)
{
    if (!instance) { return; }
    if (ptr == IntPtr.Zero || dataSize == 0 || timestamp == 0) { return; }
    if (timestamp == instance.curTimestamp) { return; }

    instance.curTimestamp = timestamp;

    RokidExtensionAPI.RokidOpenXR_API_GetHistoryCameraPhysicsPose(timestamp, positionCache, rotationCache);
    var pose = new Pose
    {
        position = new Vector3(positionCache[0], positionCache[1], -positionCache[2]),
        rotation = new Quaternion(-rotationCache[0], -rotationCache[1], rotationCache[2], rotationCache[3]),
    };
    // ПРИМЕЧАНИЕ: При написании собственного источника кадров устройства по возможности используйте реальный статус отслеживания во время экспозиции камеры.
    var trackingStatus = ((RokidTrackingStatus)RokidExtensionAPI.RokidOpenXR_API_GetHeadTrackingStatus()).ToEasyARStatus();

    var size = instance.size;
    var pixelSize = instance.size;
    var pixelFormat = PixelFormat.Gray;
    var yLen = pixelSize.x * pixelSize.y;
    var bufferBlockSize = yLen;

    var bufferO = instance.TryAcquireBuffer(bufferBlockSize);
    if (bufferO.OnNone) { return; }

    var buffer = bufferO.Value;
    buffer.tryCopyFrom(ptr, 0, 0, bufferBlockSize);

    using (buffer)
    using (var image = Image.create(buffer, pixelFormat, size.x, size.y, pixelSize.x, pixelSize.y))
    {
        instance.HandleCameraFrameData(instance.deviceCamera, timestamp * 1e-9, image, instance.cameraParameters, pose, trackingStatus);
    }
}
Осторожно

Не забудьте выполнить Dispose() или освободить Image, Buffer и другие связанные данные с помощью механизмов вроде using после использования. В противном случае произойдет серьезная утечка памяти, а получение буфера из пула буферов может завершиться неудачей.

Ввод данных рендеринга кадров

После подготовки данных устройства, каждый рендеринговый кадр вызывает HandleRenderFrameData(double, Pose, MotionTrackingStatus) / HandleRenderFrameData(double, Quaternion) для ввода данных рендеринга кадров.

Например, реализация в RokidFrameSource выглядит следующим образом:

protected void LateUpdate()
{
    if (!started) { return; }
    if ((RokidTrackingStatus)RokidExtensionAPI.RokidOpenXR_API_GetHeadTrackingStatus() < RokidTrackingStatus.Detecting) { return; }
    if ((RokidTrackingStatus)RokidExtensionAPI.RokidOpenXR_API_GetHeadTrackingStatus() >= RokidTrackingStatus.Tracking_Paused) { return; }

    InputRenderFrameMotionData();
}

private void InputRenderFrameMotionData()
{
    var timestamp = RokidExtensionAPI.RokidOpenXR_API_GetCameraPhysicsPose(positionCache, rotationCache);
    var pose = new Pose
    {
        position = new Vector3(positionCache[0], positionCache[1], -positionCache[2]),
        rotation = new Quaternion(-rotationCache[0], -rotationCache[1], rotationCache[2], rotationCache[3]),
    };
    if (timestamp == 0) { return; }
    HandleRenderFrameData(timestamp * 1e-9, pose, ((RokidTrackingStatus)RokidExtensionAPI.RokidOpenXR_API_GetHeadTrackingStatus()).ToEasyARStatus());
}

Последующие шаги

Связанные темы