你有没有想过一个问题:AR 应用是怎么知道手机在现实世界里移动了多少、转了多少度的?
比如说你在玩一个 AR 游戏,把一个虚拟小人放在桌面上,然后你拿着手机绕桌子走一圈。小人始终稳稳地站在那个位置,不会乱跑。这背后靠的就是SLAM 运动跟踪。
简单说,SLAM(Simultaneous Localization and Mapping)就是让手机一边"画地图"(识别周围环境),一边"定位自己"(知道自己在哪)。这两个事情是同时做的,所以叫"同时定位与建图"。
SLAM 运动跟踪工作原理
SLAM(同时定位与建图)的工作流程可以概括为以下几个阶段:
怎么开启 SLAM 跟踪?
要让 AR Engine 开启 SLAM 能力,你需要做两件事:
- 配置 AR 类型为
WORLD(环境追踪) - 通过
ARViewContext初始化并启动 AR 会话
先看配置部分。你需要创建一个ARConfig对象,告诉 AR Engine 你要用什么能力:
import{arEngine,arViewController}from'@kit.AREngine';letcontext:arViewController.ARViewContext=newarViewController.ARViewContext();context.config={type:arEngine.ARType.WORLD,poseMode:arEngine.ARPoseMode.GRAVITY_AND_HEADING,powerMode:arEngine.ARPowerMode.POWER_SAVING,depthMode:arEngine.ARDepthMode.AUTOMATIC};这里每一行配置都值得说说:
type: arEngine.ARType.WORLD:这是最关键的,告诉 AR Engine “我要做环境追踪”。如果你写成FACE或BODY,那就变成人脸追踪或人体追踪了,跟 SLAM 没关系。poseMode: arEngine.ARPoseMode.GRAVITY_AND_HEADING:这个决定世界坐标系怎么建立。GRAVITY_AND_HEADING表示 Y 轴跟重力方向一致,X 轴指向指南针北向。打个比方,就像你在地上画了一个有"东南西北"的坐标系,手机在这个坐标系里移动。powerMode: arEngine.ARPowerMode.POWER_SAVING:省电模式。AR 运算很吃性能,省电模式会让手机不那么烫,但精度可能会稍低。如果你做的是对精度要求高的 AR 测量工具,可以换成PERFORMANCE_FIRST。depthMode: arEngine.ARDepthMode.AUTOMATIC:开启深度估计。AR Engine 会自动尝试获取场景的深度信息,这对放置虚拟物体很有帮助。
配置好之后,初始化 AR 会话:
awaitcontext.init();这一步会启动摄像头、初始化传感器,开始 SLAM 追踪。调用之后,AR Engine 就开始默默地在后台做两件事:跟踪手机的姿态(位置和朝向),以及检测环境中的平面。
怎么获取手机的实时位置?
SLAM 跟踪的核心产出就是手机的位姿(Pose)——也就是手机在 3D 空间中的位置和朝向。
要拿到这个信息,你需要在每一帧的回调里去读取。AR Engine 提供了onFrameUpdate回调,每一帧渲染前都会触发:
import{arEngine,arViewController}from'@kit.AREngine';import{Node}from'@kit.ArkGraphics3D';classARViewCallbackImplextendsarViewController.ARViewCallback{onAnchorAdd(ctx:arViewController.ARViewContext,node:Node,anchor:arEngine.ARAnchor):void{console.info('onAnchorAdd');console.info(`add anchor id =${String(anchor.id)}`);console.info(`add anchor translation =${anchor.getPose().translation}`);console.info(`add node pose =${node.position}`);}onAnchorUpdate(ctx:arViewController.ARViewContext,node:Node,anchor:arEngine.ARAnchor):void{console.info('onAnchorUpdate');console.info(`update anchor id =${String(anchor.id)}`);console.info(`update anchor translation =${anchor.getPose().translation}`);console.info(`update node pose =${node.position}`);}asynconFrameUpdate(ctx:arViewController.ARViewContext,sysBootTs:number):Promise<void>{letarSession:arEngine.ARSession|undefined=ctx.session;if(arSession){letframe:arEngine.ARFrame=arSession.getFrame();if(!frame){console.error('Failed to get arSession.frame, it is undefined or null');}else{console.info(`Succeeded in getting arSession.frame =${frame.timestamp}`);awaitframe.release();}}else{console.error('Failed to get arSession, arSession is undefined');}}}letcontext:arViewController.ARViewContext=newarViewController.ARViewContext();context.callback=newARViewCallbackImpl();这段代码看起来挺长,但其实做的事情很清晰。我们继承了arViewController.ARViewCallback,然后重写了三个回调方法:
onAnchorAdd:当 AR Engine 检测到一个新的平面时,会自动创建一个锚点(Anchor)和对应的场景节点(Node),然后触发这个回调。你在回调里可以拿到锚点的位姿信息,比如anchor.getPose().translation就是这个锚点在 3D 空间中的坐标。这对 AR 导航很有用——当用户走到一个新的位置,你能知道那个位置在哪。
onAnchorUpdate:锚点的位置会随着手机的移动不断更新。每更新一次就触发这个回调。你可以在这里拿到最新的位姿,用来更新虚拟物体的位置。
onFrameUpdate:每一帧都会触发,这是你做实时渲染的地方。你可以在这一帧里获取ARFrame,里面有当前帧的时间戳、相机位姿等信息。注意用完之后要调用frame.release()释放资源,不然内存会一直涨。
锚点是怎么回事?
你可能会问:我只想知道手机在哪,为什么要搞锚点?
打个比方。你站在一个空旷的房间里,四面都是白墙,没有参照物。这时候你闭上眼睛原地转一圈,再睁开眼,你很难判断自己到底转了多少度。但如果墙上有个标记,你就能清楚地知道自己转了多少。
锚点就是那个"标记"。它是 AR Engine 在现实世界中识别到的一个固定参考点。有了锚点,虚拟物体才能"钉"在现实世界中不动。
你可以手动创建锚点。首先拿到当前相机的位姿,然后在那个位置创建一个锚点:
import{Quaternion,Vec3}from'@kit.ArkGraphics3D';import{arEngine}from'@kit.AREngine';letr:Quaternion={x:0,y:0,z:0,w:0}lett:Vec3={x:0,y:0,z:0};letpose:arEngine.ARPose=arEngine.createARPose(r,t);// arSession创建参考ARSession.getFrame接口示例代码arSession.createAnchor(pose);这里arEngine.createARPose(r, t)创建了一个位姿对象,r是旋转(用四元数表示),t是平移(用 3D 向量表示)。然后arSession.createAnchor(pose)在这个位姿处创建一个锚点。
创建好的锚点会被 AR Engine 持续追踪。即使你移开手机再回来,AR Engine 也能认出这个锚点还在原来的位置。
SLAM 跟踪失败了怎么办?
SLAM 跟踪不是万能的。有时候会失败,主要有两个原因:
- 手机移动太快(
EXCESSIVE_MOTION):你拿着手机乱甩,摄像头看到的画面模糊了,SLAM 算法就懵了。解决办法是让用户慢慢移动手机。 - 环境特征太少(
INSUFFICIENT_FEATURES):你对着一面纯白的墙拍,或者在很暗的环境里,SLAM 找不到足够的特征点来判断位置。解决办法是换个有纹理的地方,或者开灯。
你可以通过ARFrame的getCamera()方法拿到相机对象,然后检查跟踪状态。如果状态是PAUSED,说明跟踪暂停了,可能需要提示用户调整手机的位置。
坐标系转换流程
AR Engine 和 3D 渲染引擎使用不同的坐标系,需要进行转换:
世界坐标系的理解
SLAM 建立的世界坐标系有两种模式:
GRAVITY:Y 轴跟重力方向一致,原点在手机启动时的位置。这是最常用的模式,适合大多数 AR 应用。GRAVITY_AND_HEADING:在GRAVITY的基础上,X 轴指向指南针北向。适合 AR 导航这类需要方向感的场景。
需要注意的是,GRAVITY_AND_HEADING目前只支持省电模式。所以如果你用了这个模式,记得把powerMode也设成POWER_SAVING。
另外,AR Engine 的世界坐标系跟 3D 渲染引擎(AGP)的坐标系不一样。AR Engine 用的是重力对齐坐标系,AGP 用的是自己的世界坐标系。如果你要在 3D 场景里放虚拟物体,需要做一次坐标转换。ARViewContext提供了transformPose方法来帮你做这个事情:
import{arViewController}from'@kit.AREngine';import{Vec3,Quaternion}from'@kit.ArkGraphics3D';letcontext:arViewController.ARViewContext=newarViewController.ARViewContext();letpose:Vec3={x:1.0,y:-1.0,z:-0.5};letrot:Quaternion={x:-0.1,y:0.2,z:-0.3,w:0.5};context.transformPose(pose,rot);把 AR 坐标系的位姿传进去,它会返回转换后在 AGP 渲染坐标系中的位姿。这样你就能正确地在 3D 场景里放置虚拟物体了。
点云数据怎么看?
SLAM 在跟踪的同时,还会提取环境中的特征点。这些特征点组成一个点云(Point Cloud)。
你可以通过ARFrame获取当前帧的点云数据。点云里的每个点都有 3D 坐标和一个置信度值。置信度越高,说明这个特征点越可靠。
点云数据在很多场景下很有用。比如你想做一个 AR 标注工具,让用户在空中画线,你就需要知道用户手指指向的 3D 位置,这时候点云就能帮上忙。或者你想在 AR 场景里做遮挡效果(虚拟物体被真实物体挡住),也需要用到深度信息,而点云就是深度信息的一种来源。
平面检测是 SLAM 的延伸
SLAM 追踪还有一个很重要的"副产品"——平面检测。
AR Engine 在做 SLAM 的时候,会从点云中识别出哪些点在一个平面上(比如地面、桌面、墙面),然后把这些点拟合成一个平面。这就是平面检测。
你可以在配置里指定要检测哪种平面:
HORIZONTAL:只检测水平面,比如地面和桌面VERTICAL:只检测竖直面,比如墙壁HORIZONTAL_AND_VERTICAL:两个都检测
默认是HORIZONTAL_AND_VERTICAL,对大多数 AR 应用来说够用了。
当检测到平面后,AR Engine 会自动创建锚点,然后通过onAnchorAdd回调告诉你。你就可以在这些平面上放置虚拟物体了。想象一下,用户打开 AR 家具 APP,把手机对着地面扫一圈,AR Engine 就能识别出地面,然后你就可以在地面上摆一个虚拟沙发。
实际应用场景
说了这么多 API,来看看 SLAM 运动跟踪能做什么:
AR 室内导航:在商场里,用户打开 APP,手机摄像头对着周围环境扫一圈。SLAM 会建立一个环境地图,然后持续跟踪用户的位置。你可以在地图上标注路线,用箭头引导用户走到目的地。
AR 家具摆放:用户打开 APP,扫描房间,SLAM 识别出地面和桌面,用户就可以在这些平面上放虚拟家具,看看摆在家里好不好看。
AR 游戏:SLAM 识别出桌面后,你可以在桌面上做一个虚拟棋盘,用户围着桌子走,棋盘始终在桌面上。或者在地面上做一个虚拟跑步机,用户在 AR 世界里跑来跑去。
SLAM 运动跟踪是 AR 的基础能力。几乎所有 AR 应用都离不开它——不管是 AR 导航、AR 游戏还是 AR 测量工具,都需要先知道手机在 3D 空间中的位置,才能把虚拟内容放到正确的地方。