如何创建EasyAR头显扩展¶
这篇文章将说明如何在一个尚未受到EasyAR支持的头显设备上支持EasyAR的功能。EasyAR已经支持的头显及常规使用说明请参考 EasyAR头显扩展 。
在该文档成文之时(2023年),AR/VR/MR/XR行业内还没有形成非常统一的接口方案,虽然OpenXR是个很好的候选,但规范演化和行业实现还需要时间。所以通常来说市贩设备直接运行EasyAR并不那么容易,很有可能存在数据接口缺失的情况(大概率)。如果你是应用或内容开发者,请联系硬件制造方或EasyAR商务。2024年苹果开放了Vision Pro的部分接口,接口比较完善,建议硬件厂商参考。
因此这份文档主要的阅读者是硬件供应商,而非普通消费者。通常来说,本文档在提供规范的同时并不限定所有实现细节,任何实现方式或接口定义都可以讨论,欢迎通过商务渠道联系沟通。
本文档覆盖的硬件自身需要有运动跟踪(VIO)能力。通常我们假定EasyAR的功能运行在良好的设备跟踪能力之上,通常不建议靠EasyAR优化设备的跟踪从而产生循环依赖(站在最上层架构上考虑,不排除误差被正反馈放大从而导致系统趋于不稳)。如果设备本身没有运动跟踪能力,那么支持方案并不在本文档覆盖范围之内,如有需要可通过商务渠道进行沟通。
目标与合理预期¶
不要误解即将完成的目标是极其重要的。对即将投入的资源以及最终会获得的结果要有合理预期。
什么是EasyAR头显扩展¶
你在写的扩展是,
使用 EasyAR Sense 的自定义相机接口,从你的设备API抓取数据并发送进 EasyAR Sense 的一堆代码。
在Unity中,头显扩展会使用可继承的frame source API和 EasyAR Sense Unity Plugin 定义的一套 EasyAR Sense 数据流来简化 EasyAR Sense 自定义相机开发。
在Unity中,头显扩展是一个 Unity package,包含运行时脚本,编辑器脚本和扩展的sample,你或EasyAR可以将它分发给下游用户。
你在写扩展的时候,可能会,
修改你的SDK接口设计和内部实现。
与你的团队一起讨论确认数据获取和使用方案。
花大量时间进行数据正确性验证而不是写代码。
写完扩展后,你将会有,
大多数 EasyAR Sense 功能(比如图像跟踪,稠密空间地图)在你的设备上可以使用,这些功能会利用你设备的运动跟踪(VIO)能力。
EasyAR Sense 内支持的EasyAR云服务在你的设备上可以使用。
使用自定义相机时的所有 EasyAR license 限制以相同方式适用于你的设备。
只能使用xr license,个人版、专业版以及经典版的license无法使用。
什么不是EasyAR头显扩展¶
这个扩展不能脱离 EasyAR Sense 使用,
这个头显扩展不会独立运行,作为依赖, EasyAR Sense 也是需要的。在Unity中则必须使用 EasyAR Sense Unity Plugin 。
它不会直接调用EasyAR云服务API,比如EasyAR Mega定位服务,它们在 EasyAR Sense 中完成。
在Unity中,不会直接调用比如图像跟踪API的方法,它们在 EasyAR Sense Unity Plugin 中完成。
在Unity中,头显扩展不会修改场景中物体或跟踪目标的 transform,它们在 EasyAR Sense Unity Plugin 中完成。
这个扩展不能脱离你的设备SDK使用,
在Unity中,头显扩展或 EasyAR Sense Unity Plugin 不会修改场景中相机的 transform,这必须在你的设备SDK中完成。
通过头显扩展有一些EasyAR功能仍是无法使用的,
表面跟踪功能将无法使用。
EasyAR运动跟踪将无法使用。
平面检测(EasyAR运动跟踪的一部分)将无法使用。
通常来说,写扩展不能将资源限制在使用Unity做开发上面,
由于缺少标准,通常它无法只在3D引擎上面实现,所以建议从第一天起就让系统工程师和SDK工程师等底层开发工程师参与进来。
所以我该如何在我的设备上使用Mega呢?¶
在你的设备上运行验证 EasyAR Sense ,然后EasyAR Mega将会自然被支持,不需要其它多余工作。请确保不要一开始就直接使用Mega sample来在你设备上运行验证EasyAR,你很可能会失败。
背景知识¶
打造 AR/VR 设备需要一些领域知识,相似地,在设备上运行验证 EasyAR Sense 将需要你或你的团队是如下领域的专家,
你设备的物理结构和渲染系统
相机系统几何
SDK开发
常规Android debug技能,比如adb (https://developer.android.google.cn/tools/adb)
如果你工作在Unity上,你还需要知道这些,
Unity开发基础和package使用 (https://docs.unity3d.com/Manual/Packages.html)
Unity package开发 (https://docs.unity3d.com/Manual/CustomPackages.html)
C# 语言基础,包括 IDisposable Interface 等。
另外,有一点在这些领域的知识将帮助你更好地理解系统,尤其是如何发送正确的数据到EasyAR,
Android 开发 (https://developer.android.google.cn)
几何视觉,尤其是图像匹配和3D重建
数据需求¶
为使EasyAR在你的设备上工作,最重要的工作同时也是最棘手的部分是确保数据正确性。
EasyAR Sense 通常需要两组数据,我们根据接口调用时间和数据特征,将这两组数据称为,
相机帧数据(Camera frame data)
渲染帧数据(Rendering frame data)
相机帧数据¶
数据需求:
时间戳(Timestamp)
跟踪状态(Tracking status)
设备位姿(Device pose)
内参(Intrinsics,包括图像大小、焦距、主点。如果有畸变还需要畸变模型和畸变参数)
外参(Extrinsics,T_head_camera)
原始相机图像数据
数据时间:
曝光中点
数据使用:
API调用时间:可根据你的设计改变。一个大多数设备使用的常规方法是在3D引擎的渲染更新中查询,然后根据frame中的时间戳来判断是否进一步进行数据处理
API调用线程:3D引擎的game thread或任何其它线程(如果你的所有API都是线程安全的)
Unity中API调用示例如下,
void TryInputCameraFrameData()
{
double timestamp;
if (timestamp == curTimestamp) { return; }
curTimestamp = timestamp;
PixelFormat format;
Vector2Int size;
Vector2Int pixelSize;
int bufferSize;
var bufferO = TryAcquireBuffer(bufferSize);
if (bufferO.OnNone) { return; }
var buffer = bufferO.Value;
IntPtr imageData;
buffer.tryCopyFrom(imageData, 0, 0, bufferSize);
Pose devicePose;
MotionTrackingStatus trackingStatus;
using (buffer)
using (var image = Image.create(buffer, format, size.x, size.y, pixelSize.x, pixelSize.y))
{
HandleCameraFrameData(timestamp, image, cameraParameters, extrinsics, devicePose, trackingStatus);
}
}
上面的代码不会通过编译,它只是一个在Unity中的简化的API调用示例。请通过 com.easyar.sense.ext.hmdtemplate
模板获取可以使用的代码样例。
渲染帧数据¶
数据需求:
时间戳(Timestamp)
跟踪状态(Tracking status)
设备位姿(Device pose)
数据时间:
上屏时刻。Timewrap不计算在内。相同时刻的device pose数据会被用来设置3D相机的transform以渲染当前帧。
数据使用:
API调用时间:3D引擎的每个渲染帧
API调用线程:3D引擎的game thread
Unity中API调用示例如下,
void InputRenderingFrameData()
{
double timestamp;
Pose devicePose;
MotionTrackingStatus trackingStatus;
HandleRenderFrameData(timestamp, devicePose, trackingStatus);
}
上面的代码不会通过编译,它只是一个在Unity中的简化的API调用示例。请通过 com.easyar.sense.ext.hmdtemplate
模板获取可以使用的代码样例。
额外细节¶
相机图像数据,
图像坐标系:在传感器水平时获取的数据也应是水平的。数据应该以左上角为原点,行优先存储。图像不应翻转或颠倒。
图像FPS:正常 30或60 fps 的数据都可以。如果高fps有特殊影响,为达到合理的算法效果,最小可接受帧率为2。建议使用高于2的fps,通常情况下使用原始数据帧率即可。
图像大小:为获取更好的计算结果,最大边应为960或更大。正常不鼓励在数据链路中进行耗时的图像缩放,建议直接使用原始数据,除非完整大小的数据拷贝时间已经长得无法接受。
像素格式:优先跟踪效果并综合考虑性能的话,通常格式优先顺序为 YUV > RGB > RGBA > Gray (YUV中的Y分量)。在使用YUV数据时,需要完整的数据定义,包括数据封装和填充细节。相较单通道图像而言,使用彩色图像EasyAR Mega的效果会更好,但其它功能影响不大。
数据访问:数据指针或等价实现。最好在数据链路中消除所有可能的非必须拷贝。EasyAR会在数据复制后异步使用。注意数据所有权。
时间戳,
所有时间戳都应时钟同步,最好是硬件同步。
跟踪状态,
跟踪状态由设备定义,需要包含跟踪丢失(VIO不可用)的状态。如有更多等级则更好。
设备位姿,
所有pose(包括3D引擎中的相机transform)都应使用同一个原点。
在Unity中,pose数据应以Unity坐标系定义方式给出。如果头显扩展由EasyAR实现且pose使用了其它坐标系定义方式,你应提供清晰的坐标系定义或给出转换到Unity坐标系的方法。
在Unity中,如果使用Unity XR 框架,只需要兼容 device模式即可。
内参,
所有数值都应与图像数据匹配。如有需要请在发送给EasyAR之前对内参进行缩放。
如果头显扩展由EasyAR实现,你应说明内参是否会在每一帧变化(区别是对应API应该调用一次还是每帧调用)。
外参,
必须提供,它应该是一个标定矩阵 T_head_camera,用于从head坐标系变换到 camera 坐标系。如果你的设备pose和相机pose相等,它应该是单位阵。
Apple Vision Pro 对应接口为: CameraFrame.Sample.Parameters.extrinsics
性能,
数据应以最优效率提供。在大多数实现中,API调用会发生在渲染过程,所以建议即使在底层需要进行耗时操作的情况下,也不要阻塞API调用,或者以合理的方式来使用这些API。
如果头显扩展由EasyAR实现,你需要对所有耗时API调用进行说明。
多相机,
至少一个相机的数据是需要的。这个相机可以是RGB相机、VST相机、定位相机等中的任意一个。如果只有一个相机,我们通常推荐使用在中央或在眼睛附近的RGB相机或VST相机。
使用多相机可提升EasyAR算法效果。所有可用相机的
Camera frame data
的数据应在同一个时间点同时提供。多相机目前尚未完全支持,请联系我们获取更多细节。
准备工作¶
有一些工作需要在开始写头显扩展之前完成。
为AR/MR准备你的设备¶
准备你的 SLAM/VIO 系统
确保设备跟踪误差受控。一些 EasyAR 功能比如Mega可以在某种程度上降低设备累积误差,但大的局部误差也会让EasyAR的算法变得不稳定。通常来说,我们期望VIO漂移小于1‰。有意图地使用EasyAR来降低VIO误差是错误的。
准备你的显示系统
确保当一个与现实中某个物体大小和轮廓相同的虚拟物体被放置在虚拟世界中,且它与相机的相对变换关系与真实世界中对应物体与设备的变换关系相同,在这样的情况下,虚拟物体可以覆盖显示在真实物体上,且移动设备不会打破显示效果。
建议参考Vision Pro的效果。
准备你的设备SDK
确保你已经有API可以提供 数据需求 段落中所描述的数据需求。这些数据应该由你系统中的两个且只有两个时间点产生,请确保设计上不会出现因考虑不充分从而导致数据无法对齐的情况。
从应用开发角度学习 EasyAR Sense¶
先学习一下 EasyAR Sense 是很重要的。如果你在使用Unity,则还需要学习 EasyAR Sense Unity Plugin 。你必须能够在Android系统下运行样例,EasyAR需要一些基础配置,同时你也会学到如何使用 EasyAR Sense 的许可证。
建议先在手机上运行Unity中的这些样例,学习这些功能的行为是什么样的。在你的设备上运行这些功能的时候并不会有根本性的不同。
Workflow_ARSession 。它解释了AR Session的工作流和基础控件逻辑。
ImageTracking_Targets 。它解释了 EasyAR 平面图像跟踪 。你会在运行验证时用到。
SpatialMap_Dense_BallGame 。它解释了 EasyAR 稠密空间地图 。你会在运行验证时用到。
ImageTracking_MotionFusion 。它解释了 EasyAR 运动融合。你应该理解运动融合开或关对图像跟踪功能的影响,你会在不同场景下使用对应的功能。
注意:即使你工作在其它3D引擎上,也请确保运行一下Unity样例。
为开发Unity的package做好准备¶
你可以通过Unity的 Package Manager window 来 使用本地tarball文件安装插件 导入 EasyAR Sense Unity Plugin (package com.easyar.sense),或者也可以使用任何Unity允许的方式来导入它。
你应该解压头显扩展模板 (package com.easyar.sense.ext.hmdtemplate)到你可以开发package的位置,或者根据 Unity创建自定义package的指南 来创建一个新的package。
注意:如果你的设备SDK未使用Unity的package来组织,你需要解压头显扩展模板到Unity的Assets文件夹,然后从解压的文件中删除package.json以及任何以 .asmdef 为后缀名的文件。请注意在这种使用方式下,同时使用两个SDK的用户将无法获得合理的版本依赖。
熟悉头显模板¶
com.easyar.sense.ext.hmdtemplate
package 是为你准备的样例以及模板。它是一个SDK的实现,并且包含了给你用户的样例。你应该先熟悉一下所有文件。在你知道那些文件尤其是所有脚本文件的用途之前不要急于做修改,你应该完全掌控这个package。
这个头显扩展是一个SDK,它的上游是 EasyAR Sense (在Unity中是 EasyAR Sense Unity Plugin )以及你的设备SDK,下游是用户app。为了开发一个SDK,你不仅需要从app开发的视角看这个package,还需要以SDK供应商的视角来思考。因此,你需要比正常app开发者学习更多的EasyAR细节。
这个package的包结构遵循了 Unity推荐的文件布局 ,
.
├── CHANGELOG.md
├── Documentation~
├── Editor
├── LICENSE.md
├── package.json
├── Runtime
└── Samples~
└── Combination_BasedOn_HMD
请确保通过 Unity 文档 来学习package开发。我们在此列出一些要点如下,
Runtime
:存放运行时平台资产的文件夹。这是模板中最重要的文件夹 ,你将主要修改它里面的脚本。Samples~
:存放package中所有样例的文件夹。它包含给下游使用的样例,你可以将它用作测试扩展的demo。为了原地开发这个样例,请确保修改文件夹名为Samples
。Unity的 Client.Pack 方法会在你打包一个新的发布时将其自动重命名为Samples~
。Editor
:存放编辑时平台资产的文件夹。这个文件夹的脚本主要用于创建菜单项,通常你需要修改其中的一部分文字用以代表你的设备。package.json
:package的清单文件,请确保在发布前修改。
深入了解 EasyAR Sense Unity Plugin : ARSession 工作流¶
请参考 Session验证工具 及 Workflow_ARSession sample。
在使用头显时,相机的transform不会被修改。
请阅读 ARSession 的API文档或源码来学习更多细节。
深入了解 EasyAR Sense Unity Plugin : Frame Source¶
写头显扩展时最重要的部分就是写一个自定义相机设备(或者在Unity插件中,写一个external frame source)。
Frame source 是在 EasyAR Sense Unity Plugin 中设计的一个组件,用于抽象所有相机设备以及其它提供图像(和pose)的组件。在 EasyAR Sense Unity Plugin 中, CameraDeviceFrameSource 、 MotionTrackerFrameSource 、 ARKitFrameSource 、 ARCoreFrameSource 都表达了frame source。而在 EasyAR Sense 中,同功能的组件为 CameraDevice 、 MotionTrackerCameraDevice 、 ARKitCameraDevice 和 ARCoreCameraDevice 。请确保阅读 EasyAR Sense 的 API 概览 来学习 EasyAR Sense 数据流和自定义相机。
在Unity中,你可以在创建你的头显扩展时使用一些预定义的抽象 frame source作为基类。
上图显示了这些frame source的相互关系,以及EasyAR提供的一些头显设备支持的frame source。
在写一个新的头显扩展的时候,你需要知道一些重要的细节,请 注意阅读下面链接指向的接口文档。
FrameSource.IsAvailable :
可用性(Availability)
。用于判断frame source是否可以使用。FrameSource.Camera :
渲染相机(Rendering Camera)
。frame source中定义的渲染相机会用来在你的眼前显示 ARSession 运行时的一些信息。FrameSource.Display :
显示设备信息
。用于提供display旋转等信息。通常在头显上可以使用 Display.DefaultHMDDisplayFrameSource.OnSessionStart :
Session工作流:启动
FrameSource.OnSessionStop :
Session工作流:停止
FrameSource.IsCameraUnderControl : (会自动处理)。在使用头显时 ExternalDeviceFrameSource 中会设置为false。
FrameSource.IsHMD :
定义头显设备
。在使用头显时,IsHMD应被设为true。
ExternalFrameSource.TryAcquireBuffer : 你需要使用它来获取可用buffer
ExternalFrameSource.ReceivedFrameCount : 插件会用它来检查设备帧输入的健康情况
ExternalDeviceFrameSource.Origin :
会话原点(Session Origin)
ExternalDeviceFrameSource.OriginType :
会话原点类型
ExternalDeviceFrameSource.DeviceOriginType
ExternalDeviceMotionFrameSource:
“External”设备运动的意思是,SLAM由非EasyAR组件提供,并且3D相机的transform已经由你的设备SDK控制。
ExternalDeviceRotationFrameSource:
“External”设备旋转的意思是,设备不提供6DOF跟踪能力,提供3DOF旋转跟踪,并且3D相机的transform已经由你的设备SDK控制。
请阅读每个frame source的API文档或源代码来学习更多细节。
如果你需要定义Unity消息,比如Awake或OnEnable,请确保检查你的基类是否已经使用这些方法,并在你的实现中调用基类中的方法。
为 EasyAR Sense Unity Plugin 写头显扩展¶
在这个段落中,我们会使用package com.easyar.sense.ext.hmdtemplate
来演示,但可能不会覆盖模板中的所有细节。在写你的头显扩展时,请确保阅读package中的所有细节。源码中已经有所有接口和数据需求的解释。
注意:不同版本的实现细节可能会有所区别。
写头显扩展:定义头显¶
Override IsHMD
并设为true。
public override bool IsHMD { get => true; }
Override Display
并设置合理数据。
protected override IDisplay Display => easyar.Display.DefaultHMDDisplay;
写头显扩展:可用性¶
所有EasyAR功能(包括设备支持)都会定义可用性接口,以便让用户知道对应功能在运行时在session的某个状态下以及在特定设备上是否可以使用。可用性接口会在 ARSession.Assemble 时使用,不可用的组件将不会被选择且它的方法在 ARSession 运行(running)时不会被调用。
你需要override属性 IsAvailable
。如果 IsAvailable
session启动之前有值,则 CheckAvailability
不会被调用。 CheckAvailability
是一个协程。有时在可用性可被决定之前你可能需要等待设备准备好或等待数据更新。如果不需要等待可以直接返回null。
protected override Optional<bool> IsAvailable => throw new NotImplementedException("Please finish this method using your device SDK API");
举例, NrealFrameSource 中的实现方式如下,
protected override Optional<bool> IsAvailable => Application.platform == RuntimePlatform.Android && SetupCameraRig();
写头显扩展:渲染相机¶
frame source中定义的渲染相机会用来在你的眼前显示 ARSession 运行时的一些信息。
你需要override属性 Camera
,它会在 ARSession.Assemble 时使用。如果 ExternalDeviceFrameSource.OriginType 是 DeviceOriginType.XROrigin
,则不需要override,这样会使用Unity XR框架中定义的相机。
protected override Camera Camera
{
get
{
if (OriginType == DeviceOriginType.XROrigin) { return base.Camera; }
// NOTE: Return the rendering camera. It is used to display messages in front of your eyes.
// If OriginType is XROrigin, just remove the following line.
throw new NotImplementedException("Please finish this method using your device SDK API");
}
}
举例, NrealFrameSource 中的实现方式如下,
protected override Camera Camera => SetupCameraRig() ? CameraRigCandidate.centerCamera : null;
写头显扩展:Session原点¶
定义原点可以获得更多灵活性,而不定义原点你会损失一些灵活性,尤其是只能支持有限的中心模式,物体的移动方式也会随之受限。应用开发者必须对于他们如何摆放虚拟物体十分小心,因为在使用这个类的时候EasyAR节点和物体永远都会动。所有放在Unity世界坐标系下的物体在任何配置下都永远不可能显示在正确的位置。
你需要override属性 Origin
和 OriginType
来返回你的设备SDK定义的原点,它会在 ARSession.Assemble 时使用。如果 ExternalDeviceFrameSource.OriginType 不是 DeviceOriginType.Custom
,则不需要override,这样 Origin
会自动使用 XR.CoreUtils.XROrigin 。
protected override DeviceOriginType OriginType => throw new NotImplementedException("Please set your origin type");
protected override GameObject Origin
{
get
{
if (OriginType != DeviceOriginType.Custom) { return base.Origin; }
// NOTE: The Origin is used to setup transform base in SessionOrigin center mode and to transform camera-origin pair together in other center modes.
// If OriginType is not Custom, just remove the following line.
throw new NotImplementedException("Please finish this method using your device SDK API");
}
}
写头显扩展:Session启动和停止¶
OnSessionStart
会在 ARSession 启动的时候在每个EasyAR组件上调用。 该frame source的 OnSessionStart
会在 ARSession.Assemble 结束选择组件且你的frame source已经被选择之后才会发生。这个方法设计上是用来做延迟初始化的。
你需要override方法 OnSessionStart
然后做AR独有的初始化工作。需要确保先调用base.OnSessionStart。
protected override void OnSessionStart(ARSession session)
{
base.OnSessionStart(session);
StartCoroutine(InitializeCamera()); // NOTE: Start to do initialization for acquiring camera data, and / or wait for device ready.
}
这里是打开设备相机(比如RGB相机或VST相机等)的好地方,尤其是如果这些相机没有被设计成要一直打开时。同时这里也是获取整个生命周期内不会变化的标定数据的好地方。有时在这些数据可以被获取前你可能需要等待设备准备好或等待数据更新。
private IEnumerator InitializeCamera()
{
yield return new WaitUntil(() => false); // NOTE: Wait until device initialized, so don't forget to change this endless waiting sample.
var cameraModel = (CameraModelType)(-1); // NOTE: Replace with device calibration data. Use CameraModelType.Pinhole if camera model is pinhole.
var imageSize = new Vec2I(); // NOTE: Replace with device calibration data.
var cameraParamList = new List<float>(); // NOTE: Replace with device calibration data. When using Mega, CLS v3 or later is required to support non-pinhole.
var cameraDeviceType = CameraDeviceType.Back; // NOTE: No need to change in most cases.
var cameraOrientation = 0; // NOTE: Replace with device calibration data. Acceptable value: 0, 90, 180, 270.
var parameters = CameraParameters.tryCreateWithCustomIntrinsics(imageSize, cameraParamList, cameraModel, cameraDeviceType, cameraOrientation); // NOTE: If online optimize exists, generate in TryInputCameraFrameData instead.
if (parameters.OnNone)
{
throw new InvalidOperationException("Invalid intrinsics in CameraParameters.tryCreateWithCustomIntrinsics");
}
cameraParameters = parameters.Value;
extrinsics = new Extrinsics(new Pose(), Extrinsics.CoordinateSystem.Unity); // NOTE: Replace with device calibration data.
StartCoroutine(InputDeviceData()); // NOTE: Start to input data into EasyAR.
throw new NotImplementedException("Please finish this method using your device SDK API");
}
同时,这里也是一个启动数据输入循环的好地方。你也可以在 Unity 脚本 Update
或其它方法中写这个循环,尤其是当你的数据需要在Unity执行顺序的某个特殊时间点获取的时候。在session准备好(ready)之前不要输入数据。
private IEnumerator InputDeviceData()
{
while (true)
{
InputRenderFrameMotionData();
TryInputCameraFrameData();
yield return null;
}
}
如果你希望,你也可以忽略启动过程并在每次更新时做数据检查,这完全取决于你。
举例, QiyuFrameSource 中的实现方式如下,
private IEnumerator InitializeCamera()
{
QiyuARCore.InitAR(500);
if (ControlSeeThrough)
{
QiyuARCore.EnableSeeThrough(true);
}
var frameData = new QiyuARPlugin.VstFrameDataNative();
QiyuARPlugin.QVR_GetVstFrame(ref frameData);
while (frameData.headTimeStamp <= 0 || frameData.dataLength <= 0)
{
QiyuARPlugin.QVR_GetVstFrame(ref frameData);
yield return null;
}
QiyuARPlugin.QVR_GetVstFrame(ref frameData);
var radialDisortion = new float[4];
System.Runtime.InteropServices.Marshal.Copy(frameData.radialDisortion, radialDisortion, 0, radialDisortion.Length);
var cameraParamList = new List<float> { frameData.focal.x, frameData.focal.y, frameData.center.x, frameData.center.y }.Concat(radialDisortion).ToList();
cameraParameters = CameraParameters.tryCreateWithCustomIntrinsics(new Vec2I((int)frameData.size.x, (int)frameData.size.y), cameraParamList, CameraModelType.OpenCV_Fisheye, CameraDeviceType.Back, 0).Value;
extrinsics = new Extrinsics(frameData.cameraOffset.ToPose(), Extrinsics.CoordinateSystem.Unity);
}
写头显扩展:相机帧¶
这里是你发送 Camera frame data
到 EasyAR Sense 内部的地方。请参考 数据需求 来了解细节。
没有必要每帧调用。最小可接受帧率 = 2。它可以在任何线程调用,只要你的API都是线程安全的即可。这些数据需要与相机传感器曝光时的数据一致。只要可以获取,建议输入色彩数据到EasyAR Sense,这对EasyAR Mega的效果是有帮助的。为实现最佳效率,你可以设计整个数据链条让原始YUV数据直接通过共享内存透传,并直接使用数据指针传入EasyAR Sense。请注意数据所有权。
private void TryInputCameraFrameData()
{
...
if (timestamp == curTimestamp) { return; } // NOTE: Do not waste time sending the same data again. And if possible, do not copy memory or do any time-consuming tasks in your own API getting camera data.
curTimestamp = timestamp;
...
// NOTE: Make sure dispose is called. There will be serious memory leak otherwise.
using (buffer)
using (var image = Image.create(buffer, format, size.x, size.y, pixelSize.x, pixelSize.y))
{
HandleCameraFrameData(timestamp, image, cameraParameters, extrinsics, historicalHeadPose, trackingStatus);
}
throw new NotImplementedException("Please finish this method using your device SDK API");
}
重要 :不要忘记在使用后dispose EasyAR Sense的数据。否则会出现严重内存泄漏,buffer pool获取buffer也可能会失败。
举例, QiyuFrameSource 中的实现方式如下,
void Update()
{
if (extrinsics.OnNone) { return; }
var frameData = new QiyuARPlugin.VstFrameDataNative();
QiyuARPlugin.QVR_GetVstFrame(ref frameData);
...
OnCameraFrameReceived(frameData);
}
private void OnCameraFrameReceived(QiyuARPlugin.VstFrameDataNative frameData)
{
if (frameData.cameraTimeStamp == curTimestamp) { return; }
curTimestamp = frameData.cameraTimeStamp;
var size = new Vector2Int((int)frameData.size.x, (int)frameData.size.y);
var pixelSize = size;
var yLen = pixelSize.x * pixelSize.y;
var bufferBlockSize = yLen;
var bufferO = TryAcquireBuffer(bufferBlockSize);
if (bufferO.OnNone) { return; }
var bufferO = TryAcquireBuffer(bufferBlockSize);
if (bufferO.OnNone) { return; }
var buffer = bufferO.Value;
buffer.tryCopyFrom(frameData.data, 0, 0, bufferBlockSize);
var pose = frameData.cameraPose.ToPose();
// NOTE: Qiyu did not give a reasonable tracking status. Request submitted. We are wating for update...
var trackingStatus = MotionTrackingStatus.Tracking;
using (buffer)
using (var image = Image.create(buffer, PixelFormat.Gray, size.x, size.y, pixelSize.x, pixelSize.y))
{
HandleCameraFrameData(frameData.cameraTimeStamp * 1e-9, image, cameraParameters, extrinsics.Value, pose, trackingStatus);
}
}
写头显扩展:渲染帧¶
这里是你发送 Rendering frame data
到 EasyAR Sense 内部的地方。请参考 数据需求 来了解细节。
请确保在设备数据准备好之后每帧调用,不能跳帧。这些数据需要与驱动同一帧内当前Unity渲染相机的数据一致。
private void InputRenderFrameMotionData()
{
...
HandleRenderFrameData(timestamp, headPose, trackingStatus);
throw new NotImplementedException("Please finish this method using your device SDK API");
}
举例, QiyuFrameSource 中的实现方式如下,
void Update()
{
if (extrinsics.OnNone) { return; }
var frameData = new QiyuARPlugin.VstFrameDataNative();
QiyuARPlugin.QVR_GetVstFrame(ref frameData);
if (frameData.headTimeStamp <= 0) { return; }
// NOTE: Qiyu did not give a reasonable tracking status. Request submitted. We are wating for update...
HandleRenderFrameData(frameData.headTimeStamp * 1e-9, frameData.headPose.ToPose(), MotionTrackingStatus.Tracking);
...
}
运行验证(bring-up)头显扩展¶
准备用于运行验证的样例¶
样例位于 Samples~/Combination_BasedOn_HMD。为了在原地开发样例,请确保将文件夹名从 Samples~
重命名为 Samples
。简单起见,sample中没有代码,全部由场景中配置实现。
添加你的设备支持物体到场景中。
你也可以反过来做,使用一个可以在你设备上运行的场景,然后添加EasyAR组件和sample场景中的其它物体到你的场景中。
如果场景中定义了任何原点,移动 "Cube" 和 "UI" 到原点节点下。
"Cube"方块会提供一个设备SLAM行为的参照,这会帮助判断跟踪不稳定时候的原因。
设置"UI"的constraint source为你的渲染相机,以确保 "HUD" 按钮可以按预期工作。
注意"UI" 节点下的"Canvas",确保raycast可以工作,以确保所有按钮和开关可以按预期工作。
如果你当前没有使用 EasyAR Mega ,确保关闭 ARSession 下面的
Mega Tracker
物体,否则会出现错误。
如果要了解sample场景中的细节以及正常sample是如何创建的,请参考 完成包:样例 。
构建和运行样例¶
请确保通过 样例使用说明 学习如何使用样例。对于Android的一些特殊配置,请阅读 Android 工程配置 。
你可以参考 EasyAR头显扩展样例说明 来了解如何使用样例。
为了运行图像跟踪,你需要打印namecard.jpg到A4纸,并确保图像宽度与纸的长边一致。
第一次在你的设备上运行验证EasyAR时,请确保先顺序运行这些功能,尤其是不要急于运行Mega,因为EasyAR Mega有一些容错性,在短时间运行或单一现实场景中运行的时候难以发觉。
阅读眼前显示的 session dump信息,确保没有意外情况发送,并确保frame count在持续增长。
运行
Image
,即 EasyAR 平面图像跟踪 功能,与手机运行效果对比。运行
Dense
,即 EasyAR 稠密空间地图 功能,与手机运行效果对比。
注意
头显的支持是通过EasyAR Sense的自定义相机实现的。使用个人版的EasyAR Sense license或使用试用版本的Mega服务时,EasyAR每次启动将只能使用100秒。使用付费版本的EasyAR Sense和付费的EasyAR Mega服务没有这个限制。默认情况下,Unity插件会在100s后主动崩溃,你可以通过 消息显示及错误围栏 的 SenseError
和 SessionError
选项来改变这个 "主动崩溃" 的行为。
运行验证过程中的问题分解¶
为使EasyAR在你的设备上工作,最重要的工作同时也是最棘手的部分是确保数据正确性。在一个新设备上首次运行验证EasyAR时,超过90%的问题都是由错误的数据造成的。强烈建议在没有EasyAR存在的时候,仅通过你的设备使用一些方法来直接验证数据的正确性。我们下面会提供一些使用EasyAR功能来验证你的数据的经验方法,它能帮助你理解 数据需求 ,但使用如此耦合的系统绝不是确保数据正确性的最佳方法。
如果 Image
(跟踪和目标覆盖显示)以及 Dense
(mesh位置、生成速度和质量)都和手机上的效果表现一致或更好(最好使用iPhone做对比)那么大部分EasyAR的功能都可以在你的设备上正常工作,你可以开始测试Mega(如果这是你的目标的话)。需要注意 EasyAR 稠密空间地图 可能无法在部分Android设备上运行,mesh质量也会根据设备而变化。
如果你无法重现与手机上相同的结果,那接下来是一个详细的问题分解过程,你可以参考它来寻找根因。
请确保始终关注adb的日志输出。 Dense spatial map存在已知问题,会在运行一段时间之后持续输出mesh组件相关的错误日志。这不会影响已经显示出来的mesh的质量,会在今后的版本中修复。
你的系统误差
还记得准备阶段所描述的的SLAM和显示需求吗?
SLAM/VIO误差会始终以不同方式影响EasyAR算法的稳定性。请始终记住这一点。
显示系统误差可能会导致虚拟物体和现实物体无法完美对齐。在一些误差比较大的情形下,虚拟物体会看起来悬浮于真实物体上面或下面,然后(看起来)一直在漂移。这个现象可以在Pico 4E上观察到,即使不使用EasyAR只打开它自己的VST也有同样的现象。
会话状态显示及转储 显示
接收到的Camera frame Count
必需的健康功能或数据:
EasyAR Sense Unity Plugin 中 Frame source
Camera frame data
数据通路(不包含数据正确性以及到 EasyAR Sense 的数据通路)
这个数据应该随时间增长,否则会在几秒之后弹出显示警告信息。
如果你发现这个数值不增长,你应该debug以寻找原因。
在设备上录制EIF,然后在Unity编辑器中回放
必需的健康功能或数据:
Frame source
Camera frame data
到 EasyAR Sense 的数据通路(不包含数据正确性)
点击
EIF
来启动录制,再次点击停止。你必须停止录制才能获取到可以使用的EIF文件,否则录制的文件将不可使用。在Unity编辑器中运行EIF数据时最好使用纯净的EasyAR场景或使用EasyAR的样例以避免场景中不正确的配置。使用EIF你可以做很多事情,你可以在Unity编辑器中使用EIF运行 EasyAR 平面图像跟踪 和 EasyAR 稠密空间地图 。但是需要记住的是,在设备上运行的显示效果有可能是不一样的。
EasyAR会在计算中使用畸变参数但不会对图像做反畸变。所以如果你输入了这些数据,当你在Unity中回放EIF文件时,你会观察到没有反畸变的数据,这是符合预期的。
必需的健康功能或数据:
Camera frame data
中的Raw camera image data
Camera frame data
中的Timestamp
(不包括时间点和数据同步)
你可以在Unity编辑器中看到相机帧的回放。图像数据并不是字节相等的,整个流程中有有损编解码。修改Unity game窗口的比例与输入相同,否则数据会被裁剪显示。
如果数据播放偏快或偏慢,你需要检查你的
Timestamp
输入。使用EIF运行 EasyAR 平面图像跟踪
必需的健康功能或数据:
Camera frame data
中的Raw camera image data
Camera frame data
中的Intrinsics
(数据正确性不能完全保证,因为算法对误差存在容忍度)
在Unity编辑器中使用EIF运行 ImageTracking_Targets 样例,你需要录制一个图像可以被跟踪到的EIF。
请注意, EasyAR 平面图像跟踪 需要跟踪目标占据整个图像的一定比例,如果你无法跟踪到图像,尝试移动头到更加接近图像的位置。
如果跟踪持续失败或虚拟物体显示在图像中远离目标的位置,则很有可能
Intrinsics
是错的。如果你的图像数据有畸变,你可能会看到虚拟物体不会完美的覆盖图像上的跟踪目标,这是符合预期的。
在设备上运行 EasyAR 平面图像跟踪
必需的健康功能或数据:
你的显示系统
Camera frame data
中的Raw camera image data
Camera frame data
中的Intrinsics
(数据正确性不能完全保证,因为算法对误差存在容忍度)Camera frame data
中的Extrinsics
Camera frame data
和Rendering frame data
中Device pose
的坐标一致性Camera frame data
和Rendering frame data
中Device pose
的时间差
请注意, EasyAR 平面图像跟踪 需要跟踪目标占据整个图像的一定比例,如果你无法跟踪到图像,尝试移动头到更加接近图像的位置。
请注意, EasyAR 平面图像跟踪 需要目标的scale与真实世界中物体的大小一致,在样例中需要你跟踪一个宽度上撑满A4纸的图像,因此不要跟踪显示在电脑屏幕上的图像,除非你使用一把尺子并参照尺子将图像调整到A4大小。
如果在使用EIF时图像跟踪很完美但在设备上却不同,请在继续其它测试前解决它。在后续步骤中解决问题要困难得多。
如果虚拟物体悬浮显示在某个远离真实物体的地方,而且即使人不动也是如此,那很有可能
Intrinsics
或Extrinsics
不正确或Camera frame data
和Rendering frame data
中Device pose
不在同一个坐标系,或者你的显示系统产生了这个误差。如果虚拟物体在你移动头部的时候持续移动并且看起来就像是有延迟一样,那有很大可能性
Device pose
不够像要求中描述的那样健康。这经常发生于几种情况,比如Device pose
与Raw camera image data
的数据时间不同步,或者Camera frame data
和Rendering frame data
中使用了相同的pose等等。使用EIF或在设备上运行 EasyAR 稠密空间地图
必需的健康功能或数据:
你的显示系统
Camera frame data
中的Raw camera image data
Camera frame data
中的Intrinsics
(数据正确性不能完全保证,因为算法对误差存在容忍度)Camera frame data
中的Extrinsics
Camera frame data
中的Device pose
如果mesh生成速度非常慢和/或地面重建坑坑洼洼,那非常有可能
Device pose
有问题。也有可能pose的坐标系不正确或pose的时间点不对。通常分辨精确的mesh位置不是非常容易,所以在使用 EasyAR 稠密空间地图 时你的显示系统误差可能不一定能观察出来。
EasyAR Mega¶
准备工作¶
在使用EasyAR Mega之前你需要首先阅读 EasyAR Mega 入门指南 。然后跟随 EasyAR Mega Unity 开发手册 学习使用EasyAR Mega时应该如何做Unity侧的开发。
请确保先在手机上运行Mega并了解流程。
如果你之前关闭了ARSession下的 Mega Tracker
物体,不要忘记启用它。
运行验证过程中的问题分解¶
必需的健康功能或数据:
你的显示系统
Camera frame data
和rendering frame data
中的所有数据
如果你跟随这篇文章成功运行验证了 EasyAR 平面图像跟踪 以及 EasyAR 稠密空间地图 两个功能,那么EasyAR Mega应该已经被支持了。如果在头显上运行的表现明显比手机上差,请重点关注 Camera frame data
和 rendering frame data
中的pose数据和timestamp。另外还需要关注你的SLAM/VIO系统输出。 Origin
下面的方块会是一个好的参考。
如果你还没有尝试过 EasyAR 平面图像跟踪 和 EasyAR 稠密空间地图 就在测试EasyAR Mega,那请返回上一段落,耐心地跟着前面的问题分解指引一条一条查看。
打包和发布¶
完成包:编辑器¶
修改MenuItems类中的 "HMD Template" 字符串以代表你的设备。如果你需要其它自定义编辑器功能,也可以添加其它脚本。
完成包:样例¶
样例位于 Samples~/Combination_BasedOn_HMD。为了在原地开发样例,请确保将文件夹名从 Samples~
重命名为 Samples
。Unity的 Client.Pack 方法会在你打包一个新的发布时将其自动重命名为 Samples~
。
模板中的样例主要为了两个目的而提供,在运行验证过程中验证设备以及为下游用户提供使用参考。因此在发布给应用开发者之前需要先完成和完善这个样例。
首先,让我们看一下在我们发布整个模板之前是如何创建这个样例的。
创建 EasyAR ARSession
你可以参考 从零创建可运行的工程 来在场景中创建EasyAR组件。
创建 ARSession,
往session中添加一些filter。下图显示了如何往session中添加image tracker。
创建sample中需要使用的image target和sparse spatial map
举例来讲,使用这个菜单来创建image target,
配置image target,
请注意,在完成上述配置之后,Unity Scene窗口中显示的图像是gizmo。这个sample中通过一个quad来显示同一图像的虚拟物体。
添加显示在target上面的虚拟物体,
添加一个立方体作为SLAM的参考
这个立方体对你们、我们以及下游用户都很重要,它用于解耦设备SLAM和EasyAR算法。
添加功能选择的UI
关闭启动中启用的EasyAR功能,并通过UI开关来打开它们
例如,图像跟踪的功能在启动时可以关闭,只需要设置对应组件的enable为false即可,
然后添加UI开关处理,
你需要使用你的设备SDK完成这个sample。你已经在 运行验证(bring-up)头显扩展 这个段落中做过,因此我们在这里略过详细步骤。请确保在场景中添加你的设备支持,然后关注 "Cube"、"UI" 以及 "UI" 下的 "Canvas" 节点的相关配置。
如果之前关闭过,不要忘记打开ARSession下的 Mega Tracker
物体,
别忘了清理场景中的一些字符串,比如“(Move into Origin if there is any, set constraint source to your rendering camera)”或“Cube (Move into Origin if there is any)”,这些字符串是写给你而非app开发者的。
完成包:包定义¶
包本身的定义在package.json中,你可以根据 Unity创建自定义package的指南 来修改这个文件或创建一个新的包。请确保修改 package的 "name" 和 "displayName",否则它会与模板本身或其它供应商的扩展发生冲突。
完成包:重新生成meta¶
请重新生成package中所有文件的 .meta 文件。否则它们会与模板本身或其它供应商的扩展发生冲突。
发布¶
你可能还希望修改package中的其它一些文件,请确保在发布前仔细审查整个package。
检查扩展与你的设备SDK以及EasyAR Sense Unity Plugin的版本兼容性。请注意EasyAR 不使用Unity所要求的 semantic versioning。主要区别是,minor版本号的变化也可能会引入不兼容的变化,虽然并不总是如此。如果你在使用Mega预发布版本,每次更新都可能会有API修改。
建议使用Unity package来打包你的文件。但是如果你的设备SDK并没有准备好以package形式发布,你也可以选择通过asset package来发布。
你需要提醒你的用户,EasyAR license key的所有限制(尤其是针对自定义相机的限制)都适用于你发布的包。
为 EasyAR Sense 写头显扩展¶
你需要有一个用于替代Unity的3D引擎。这并没有很多人想的那么容易,所以正常来说你需要使用Unity并按上面几段文档的描述来操作。如果你在尝试在这样的平台上运行验证 EasyAR Sense ,那下面列出了一些你需要重点关注的事情。
我们在与一些厂商紧密合作,比如微信上的XR-Frame,支付宝上的Paladin,以及粒界。如果你在尝试让你的设备工作在这些平台上,请先与我们联系了解最新进展。
额外的背景知识和准备工作¶
精通你在使用的3D引擎。
知道如何处理c++/java/jni开发,以及(根据你使用的3D引擎提供的脚本语言)其它跨语言开发。
把 EasyAR Sense Unity Plugin 看作 EasyAR Sense 的一个样例,你需要通过 EasyAR Sense Unity Plugin 的源码学习很多细节。
推荐步骤¶
运行验证(bring up) EasyAR Sense Unity Plugin + HMD:强烈建议先完成上面的 Unity端测试。 EasyAR Sense Unity Plugin 做了很多相关工作,这会利于整体的验证。
运行验证(bring up) EasyAR Sense + 3D engine:参考 EasyAR Sense Unity Plugin 来在你的3D引擎中和手机上运行验证 EasyAR Sense 。确保验证 EasyAR 平面图像跟踪 功能(如果可能,也要验证 EasyAR 稠密空间地图 功能)。
运行验证(bring up) EasyAR Sense + 3D engine + HMD:参考 EasyAR Sense Unity Plugin 和头显模板来你的3D引擎中和头显设备上运行验证 EasyAR Sense 。
关键点和与手机的差异¶
EasyAR Sense 初始化
android.app.Activity
是必须的。请阅读 Engine API 文档 , EasyARController.Initialize 源码也可以作为参考。注意初始化必须使用xr license。
获取同步数据
重点关注 MegaTracker.setResultAsyncMode 和 MegaTracker.getSyncResult 。确保在创建后调用 setResultAsyncMode(false) ,然后每个渲染帧使用 getSyncResult 替换 OutputFrame 的对应输出。你仍需要使用整个 OutputFrame 路径来完成其它任务,因此不要删除它。其它功能组件类似。
EasyAR Mega的特殊代码路径
不要连接Accelerometer的输出到MegaTracker.accelerometerResultSink ( EasyAR Sense Unity Plugin 中的
IsHMD
控制选项)。如果CLS内有多个block,使用一个父节点组织它们,并通过父节点进行整体的节点移动。
3D相机 ( EasyAR Sense Unity Plugin 中的
IsCameraUnderControl
控制选项)不要使用EasyAR Sense的数据渲染相机图像。
不要使用EasyAR Sense的数据设置3D相机的投影矩阵。
不要使用EasyAR Sense的数据设置相机的transform。
中心模式设计,原点设计和节点/相机控制
如果可能,设计类似 ARSession.ARCenterMode 的中心模式。
强烈建议设计类似 XR.CoreUtils.XROrigin 的原点。
最佳方式是做类似Unity XR框架的 XR.CoreUtils.XROrigin 的设计,使用设备数据设置3D相机的local transform,然后通过 EasyAR Sense 中返回的数据移动 origin。如果在整体设计中没有
Origin
,则需要使用设备数据设置3D相机的world transform,然后通过 EasyAR Sense 中返回的数据移动 EasyAR Sense 的 target 或 block (这就是 EasyAR Sense Unity Plugin 中的 SessionOrigin 中心模式)。永远不要通过 EasyAR Sense 中计算的target pose来设置3D相机的 transform,如果这么做显示将是错的。
你可以参考 EasyAR Sense Unity Plugin 的源代码来做 pose 计算,可以从 ARSession.OnFrameUpdate 开始理解代码。
额外的问题分解方法¶
与 EasyAR Sense Unity Plugin 对比并寻找差异
对Mega来说,可以对比 Mega Toolbox
在
Origin
下放置方块或其它物体来解耦设备SLAM和EasyAR算法录制EIF然后在Unity编辑器(或你的3D编辑器)中播放
录制rendering frame data用来分析(rendering frame data没有记录在EIF文件中)
使用类似 运行验证(bring-up)头显扩展 中描述的问题分解步骤