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;

若在 session 組裝時無法判斷 IsAvailable,可覆寫 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());
}

後續步驟

相關主題