Table of Contents

創建圖像和設備運動數據輸入擴展

通過創建圖像和設備運動數據輸入擴展,開發者可以爲 EasyAR Sense 擴展自定義的相機實現,從而支持特定的頭顯設備或其它輸入設備。以下內容介紹了創建圖像和設備運動數據輸入擴展的步驟和注意事項。

開始之前

創建外部幀數據源類

它們都是 MonoBehaviour 的子類,文件名應與類名相同。

例如,創建一個 6DoF 設備輸入擴展:

public class MyFrameSource : ExternalDeviceMotionFrameSource
{
}

在創建頭顯擴展時,可以使用 com.easyar.sense.ext.hmdtemplate 模板,在模板基礎上進行修改。這個模板在從 EasyAR 網站下載獲得的 Unity 插件壓縮包內。

設備定義

重寫 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 在 session 組裝時無法判斷,可以重寫 CheckAvailability() 協程來阻塞組裝過程,直到確定是否可用爲止。

session 原點

重寫 OriginType 來定義設備 SDK 定義的原點類型。

如果 OriginTypeCustom,還需要重寫 Origin

例如, RokidFrameSource 中的實現方式如下:

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

虛擬攝像機

如果 OriginTypeCustomNone,需要重寫 Camera 來提供虛擬攝像機。

例如, RokidFrameSource 中的實現方式如下:

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

物理相機

使用 DeviceFrameSourceCamera 類型重寫 DeviceCameras 以提供設備物理相機信息。這個數據會在輸入相機幀數據時使用。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;

session 啓動和停止

重寫 OnSessionStart(ARSession) 然後做 AR 獨有的初始化工作。需要確保先調用 base.OnSessionStart。

例如:

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

這裏是適合打開設備相機(比如 RGB 相機或 VST 相機等)的位置,尤其是如果這些相機沒有被設計成要一直打開時。同時這裏也是適合獲取整個生命週期內不會變化的標定數據的位置。有時在這些數據可以被獲取前可能需要等待設備準備好或等待數據更新。

同時,這裏也是一個適合啓動數據輸入循環的位置。也可以在 Update() 或其它方法中寫這個循環,尤其是當數據需要在 Unity 執行順序的某個特殊時間點獲取的時候。在 session 準備好(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]),
    };
    // NOTE: Use real tracking status when camera exposure if possible when writing your own device frame source.
    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() 或通過 using 等機制釋放 ImageBuffer 以及其它相關數據。否則會出現嚴重內存泄漏,buffer pool 獲取 buffer 也可能會失敗。

輸入渲染幀數據

在設備數據準備好之後,每個渲染幀調用 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());
}

後續步驟

相關主題