创建图像和设备运动数据输入扩展
通过创建图像和设备运动数据输入扩展,开发者可以为 EasyAR Sense 扩展自定义的相机实现,从而支持特定的头显设备或其它输入设备。以下内容介绍了创建图像和设备运动数据输入扩展的步骤和注意事项。
开始之前
创建外部帧数据源类
- 如果需要创建 6DoF 设备输入扩展,继承 ExternalDeviceMotionFrameSource
- 如果需要创建 3DoF 设备输入扩展,继承 ExternalDeviceRotationFrameSource
它们都是 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 定义的原点类型。
如果 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 相机等)的位置,尤其是如果这些相机没有被设计成要一直打开时。同时这里也是适合获取整个生命周期内不会变化的标定数据的位置。有时在这些数据可以被获取前可能需要等待设备准备好或等待数据更新。
同时,这里也是一个适合启动数据输入循环的位置。也可以在 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 等机制释放 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());
}
后续步骤
- 创建 头显扩展包