Custom Pose Tracker 구현
이 튜토리얼에서는 3.CustomPoseTracker 예제 프로젝트를 직접 구현하여 Custom Pose Tracker를 직접 개발하고 적용하는 방법을 익힙니다. Custom Pose Tracker는 사용자가 직접 개발한 PoseTracker를 의미합니다.
대부분의 경우는 VLSDK에서 제공해주는 기본 Pose Tracker를 이용해서 앱을 개발할 수 있습니다. 하지만 로봇, AR 글래스 등 VLSDK에서 기본적으로 제공하는 PoseTracker를 사용하기 어려운 환경인 경우 PoseTracker를 직접 개발해서 사용해야 합니다.
이 튜토리얼을 시작하기 전에 Simple 앱 구현을 완료해야 합니다.
1. CustomPoseTracker 사용 플로우
전체 VLSDK의 동작 프로세스에서 PoseTracker는 ARFrame을 생성하고 VLSDK의 코어 로직에게 전달하는 역할을 수행합니다. 자세한 내용은 문서를 참고하시길 바랍니다.
2. CustomPoseTracker 생성 (기본)
2.1 PoseTracker 상속 스크립트 생성
- 원하는 위치에
MyPoseTracker.cs파일을 생성합니다. CustomPoseTrackerAdaptor는 임의의 위치에 생성한 PoseTracker 파일도 인식할 수 있습니다.

- 다음과 같이
MyPoseTracker클래스를 구현합니다.
using UnityEngine;
using ARCeye;
public class MyPoseTracker : PoseTracker // <- PoseTracker를 상속받는 클래스.
{
// 매 프레임마다 호출되는 메서드. ARFrame을 생성하고 리턴합니다.
protected override ARFrame CreateARFrame()
{
// 이미지를 로드합니다. 빠른 테스트를 위해 매 프레임마다 Resources 디렉토리의 이미지를 사용합니다.
var texture = Resources.Load<Texture2D>("001");
// ARFrame을 생성합니다.
ARFrame frame = new ARFrame();
frame.texture = texture;
return frame;
}
}
- 테스트에 사용된 이미지는 다음과 같은 경로에 배치 되어 있습니다.

3. CustomPoseTracker 적용
3.1 CustomPoseTrackerAdaptor
VLSDKManager를 선택한 뒤 CustomPoseTrackerAdaptor 컴포넌트를 추가합니다. Use Custom Editor Pose Tracker 항목에 체크를 하면 Editor 환경에서 Play Mode 진입 시 사용되는 PoseTracker를 선택할 수 있습니다. Use Custom Device Pose Tracker 항목에 체크하면 실제 기기 환경에서 사용되는 PoseTracker를 선택할 수 있습니다. 여기에서는 Use Custom Editor Pose Tracker에 체크한 뒤 앞서 생성한 MyPoseTracker를 선택합니다.

Play Mode에 진입하면 MyPoseTracker를 통해 ARFrame을 생성하고 VL 요청을 보내는 모습을 확인할 수 있습니다.

4. CustomPoseTracker 생성 (확장)
다음은 CustomPoseTracker를 사용하는 고급 예제입니다. 사용자의 시스템이 자체 루프를 가지고 있을 경우 아래와 같은 형식으로 구현할 수 있습니다. 아래의 예시는 ARFoundation을 기반으로 하고 있지만 다양한 시스템으로 응용 가능합니다.
using System;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
using Unity.Collections.LowLevel.Unsafe;
using ARCeye;
public class MyPoseTracker : PoseTracker
{
private ARCameraManager m_CameraManager;
private ARCameraFrameEventArgs m_LastFrameEventArgs;
private Texture2D m_CameraTexture;
private RawImage m_RequestedTexture;
// MyPoseTracker 최소 생성 시 호출되는 이벤트.
public override void OnCreate(Config config)
{
config.tracker.useFaceBlurring = false;
// ARFoundation 사용.
m_CameraManager = GameObject.FindObjectOfType<ARCameraManager>();
// 디버깅을 위한 카메라 preview 출력용 이미지.
m_RequestedTexture = GameObject.Find("Canvas/RawImage_Test").GetComponent<RawImage>();
}
// 루프 이벤트 등록.
public override void RegisterFrameLoop()
{
m_CameraManager.frameReceived += OnCameraFrameReceived;
}
// 루프 이벤트 해제.
public override void UnregisterFrameLoop()
{
m_CameraManager.frameReceived -= OnCameraFrameReceived;
}
private void OnCameraFrameReceived(ARCameraFrameEventArgs eventArgs)
{
if (!m_IsInitialized)
{
return;
}
m_LastFrameEventArgs = eventArgs;
OnFrameLoop();
}
// ARFrame 생성 이벤트.
protected override ARFrame CreateARFrame()
{
return CreateARFrameFromEventArgs(m_LastFrameEventArgs);
}
private ARFrame CreateARFrameFromEventArgs(ARCameraFrameEventArgs eventArgs)
{
ARFrame frame = new ARFrame();
// Camera texture.
frame.texture = GetCameraTexture();
m_RequestedTexture.texture = frame.texture;
// Camera model matrix.
frame.localPosition = m_ARCamera.transform.localPosition;
frame.localRotation = m_ARCamera.transform.localRotation;
// Intrinsic matrix.
frame.intrinsic = AquireCameraIntrinsic();
// Projection matrix.
frame.projMatrix = eventArgs.projectionMatrix ?? Camera.main.projectionMatrix;
// Display matrix.
frame.displayMatrix = eventArgs.displayMatrix ?? Matrix4x4.identity;
return frame;
}
unsafe private Texture2D GetCameraTexture()
{
if (!m_CameraManager.TryAcquireLatestCpuImage(out XRCpuImage cpuImage))
{
Debug.LogError("Failed to acquire latest CPU image.");
return null;
}
TryUpdateCameraTexture(cpuImage);
var rawTextureData = m_CameraTexture.GetRawTextureData<byte>();
var rawTexturePtr = new IntPtr(rawTextureData.GetUnsafePtr());
var conversionParams = new XRCpuImage.ConversionParams(cpuImage, TextureFormat.RGBA32);
try
{
conversionParams.inputRect = new RectInt(0, 0, cpuImage.width, cpuImage.height);
conversionParams.outputDimensions = cpuImage.dimensions;
cpuImage.Convert(conversionParams, rawTexturePtr, rawTextureData.Length);
}
finally
{
cpuImage.Dispose();
}
m_CameraTexture.Apply();
return m_CameraTexture;
}
private void TryUpdateCameraTexture(XRCpuImage cpuImage)
{
var outputDimensions = cpuImage.dimensions;
if (IsCameraTextureUpdated(outputDimensions))
{
m_CameraTexture = new Texture2D(cpuImage.width, cpuImage.height, TextureFormat.RGBA32, false);
}
}
private bool IsCameraTextureUpdated(Vector2Int outputDimensions)
{
return m_CameraTexture == null || m_CameraTexture.width != outputDimensions.x || m_CameraTexture.height != outputDimensions.y;
}
public ARIntrinsic AquireCameraIntrinsic()
{
float fx, fy, cx, cy;
if (m_CameraManager.TryGetIntrinsics(out XRCameraIntrinsics cameraIntrinsics))
{
fx = cameraIntrinsics.focalLength.x;
fy = cameraIntrinsics.focalLength.y;
cx = cameraIntrinsics.principalPoint.x;
cy = cameraIntrinsics.principalPoint.y;
}
else
{
fx = 0;
fy = 0;
cx = 0;
cy = 0;
}
return new ARIntrinsic(fx, fy, cx, cy);
}
}
5. 마치며
PoseTracker를 확장한 CustomPoseTracker를 사용하여 다양한 환경에서 VLSDK를 사용하는 방법을 익혔습니다. 이를 바탕으로 ARFoundation을 지원하지 않는 개발 환경에서도 VLSDK를 연동한 서비스를 개발할 수 있습니다.