画像とデバイスモーションデータ入力拡張の作成
画像とデバイスモーションデータ入力拡張を作成すると、開発者は EasyAR Sense に対してカスタムのカメラ実装を拡張でき、特定のヘッドマウントデバイスやその他の入力デバイスをサポートできます。以下では、その手順と注意点を説明します。
開始前
- カメラ、入力フレーム などの基本概念を理解します。
- 外部フレームデータソース を読み、外部フレームデータソース作成に必要な詳細なインターフェースを確認します。
- 外部入力フレームデータ を読み、カメラフレームデータとレンダーフレームデータを理解します。
外部フレームデータソースクラスの作成
- 6DoF デバイス入力拡張を作成する場合は、ExternalDeviceMotionFrameSource を継承します。
- 3DoF デバイス入力拡張を作成する場合は、ExternalDeviceRotationFrameSource を継承します。
どちらも MonoBehaviour の子クラスで、ファイル名はクラス名と同じにします。
たとえば、6DoF デバイス入力拡張を作成する場合は次のようになります。
public class MyFrameSource : ExternalDeviceMotionFrameSource
{
}
HMD 拡張を作成する場合は、com.easyar.sense.ext.hmdtemplate テンプレートを使用し、これをベースに修正できます。このテンプレートは、EasyAR Web サイトからダウンロードした Unity プラグインの圧縮パッケージに含まれています。
デバイス定義
IsHMD をオーバーライドして、デバイスがヘッドマウントディスプレイかどうかを定義します。
たとえば、HMD では true に設定します。
public override bool IsHMD { get => true; }
Display をオーバーライドして、デバイスの表示を定義します。
たとえば、HMD では既定の HMD 表示情報 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 が定義する原点タイプを定義します。
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;
物理カメラ
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 カメラなど)を開くのに適した場所です。特に、これらのカメラが常時開いたままになるよう設計されていない場合に向いています。また、ライフサイクル全体を通して変わらないキャリブレーションデータを取得する場所としても適しています。こうしたデータを取得できるようになる前に、デバイスの準備やデータ更新を待つ必要がある場合もあります。
同時に、ここはデータ入力ループを開始するのに適した場所でもあります。Unity の実行順序の特定のタイミングでデータを取得する必要がある場合は、Update() や他のメソッドでこのループを書くこともできます。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 などの仕組みで 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());
}
後続手順
- ヘッドセット拡張パック を作成します。