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를 재정의하여 장치의 디스플레이를 정의합니다.

예를 들어, 헤드셋에서는 기본 HMD 디스플레이 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가 정의한 원점 유형을 지정합니다.

OriginTypeCustom이라면 Origin도 재정의해야 합니다.

예를 들어, RokidFrameSource의 구현은 다음과 같습니다.

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

가상 카메라

OriginTypeCustom 또는 None이라면, 가상 카메라를 제공하기 위해 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;

세션 시작과 중지

AR 전용 초기화 작업을 수행하기 위해 OnSessionStart(ARSession)를 재정의합니다. 먼저 base.OnSessionStart를 호출해야 합니다.

예를 들어:

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

이 위치는 디바이스 카메라(RGB 카메라나 VST 카메라 등)를 여는 데 적합하며, 특히 이런 카메라가 항상 열려 있도록 설계되지 않은 경우에 그렇습니다. 또한 수명 전체에서 변하지 않는 보정 데이터를 가져오기에 적합한 위치이기도 합니다. 때로는 이 데이터가 사용 가능해지기 전에 디바이스가 준비되거나 데이터가 갱신되기를 기다려야 할 수 있습니다.

이곳은 데이터 입력 루프를 시작하기에도 적합한 위치입니다. Unity 실행 순서의 특정 시점에 데이터를 가져와야 하는 경우에는 Update()나 다른 메서드에 루프를 작성할 수도 있습니다. 세션이 준비되기 전에는 데이터를 입력하지 마세요.

필요하다면 시작 과정을 건너뛰고 매 업데이트마다 데이터 검사를 수행할 수도 있으며, 이는 전적으로 구체적인 요구 사항에 달려 있습니다.

예를 들어, 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 같은 메커니즘으로 Image, Buffer 및 기타 관련 데이터를 해제하는 것을 잊지 마세요. 그렇지 않으면 심각한 메모리 누수가 발생할 수 있고, 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());
}

후속 단계

관련 주제