HarmonyOS AR Engine 运动跟踪实操:实时读取设备位姿
HarmonyOS AR Engine 运动跟踪实操:实时读取设备位姿

本次实验做什么
第一篇已经完成设备能力检测、相机权限申请和 AR 会话管理。本次在同一个工程中增加“实验 02”,从 AR 会话的每一帧中读取设备位姿,并把以下数据直接显示在相机画面上:
- 设备在 AR 世界坐标系中的 X、Y、Z 位置;
- 表示设备朝向的四元数 x、y、z、w;
- 设备相对当前原点移动的距离;
- 设备相对当前原点旋转的角度;
- 当前跟踪状态和已经处理的采样数。
页面同时保留“重置原点、暂停/恢复、销毁退出”三个操作,用于验证位姿计算和 AR 会话生命周期。
本次最终在 Mate 60 Pro 真机上取得连续位姿数据。重置原点后,相对位移和转角回到 0.000 m / 0.0°;移动并旋转手机后,画面记录到 0.727 m / 21.2°;暂停后采样数保持在 802,恢复后继续增长。
实验环境与准备
| 项目 | 本次使用值 |
|---|---|
| 开发工具 | DevEco Studio |
| 工程类型 | HarmonyOS Phone,ArkTS |
| compileSdk / targetSdk | API 26 |
| 真机 | HUAWEI Mate 60 Pro(ALN-AL80) |
| 真机系统 | HarmonyOS 7.0.0.100,API 26 |
| 相机权限 | PERMISSION_GRANTED |
| 运动跟踪能力 | SLAM 支持 |
本次直接沿用第一篇已经完成的权限和能力检测。首页只有在相机权限已授予、SLAM 能力受支持时,才开放“实验 02:进入运动跟踪”入口。

设备位姿来自 AR Engine 建立的局部世界坐标系,单位是米;它不是经纬度,也不是 GPS 位置。数据链路如下:

实现过程
1. 增加运动跟踪实验页
工程保留第一篇的 AR 会话页面,另外新增:
entry/src/main/ets/features/arengine/MotionTrackingPage.ets
首页点击实验 02 后,先复用相机权限与 SLAM 检查,再显示 MotionTrackingPage。退出运动跟踪页时恢复首页。
if (this.showMotionTracking) {
MotionTrackingPage({
onExit: () => {
this.showMotionTracking = false;
}
})
} else {
// 首页内容
}
2. 接收 AR 帧回调
位姿数据随 AR 帧更新。这里实现 ARViewCallback,把 onFrameUpdate() 收到的上下文交给页面处理。
class MotionTrackingCallback extends arViewController.ARViewCallback {
private readonly handler:
(context: arViewController.ARViewContext, timestamp: number) => void;
constructor(handler:
(context: arViewController.ARViewContext, timestamp: number) => void) {
super();
this.handler = handler;
}
onFrameUpdate(context: arViewController.ARViewContext,
timestamp: number): void {
this.handler(context, timestamp);
}
onAnchorAdd(context: arViewController.ARViewContext,
node: Node, anchor: arEngine.ARAnchor): void {
}
onAnchorUpdate(context: arViewController.ARViewContext,
node: Node, anchor: arEngine.ARAnchor): void {
}
}
本篇只验证设备位姿,不创建锚点,所以两个锚点回调保持空实现。
3. 创建 WORLD 类型 AR 会话
页面出现时加载 ArkGraphics 3D 场景,创建 ARViewContext,设置回调和 WORLD 会话参数,然后调用 init()。
private async initARView(): Promise<void> {
const startTime: number = Date.now();
const scene: Scene = await Scene.load();
const context = new arViewController.ARViewContext();
this.callback = new MotionTrackingCallback(
(viewContext: arViewController.ARViewContext,
timestamp: number) => {
this.handleFrameUpdate(viewContext, timestamp);
});
context.scene = scene;
context.callback = this.callback;
context.config = {
type: arEngine.ARType.WORLD,
planeFindingMode:
arEngine.ARPlaneFindingMode.HORIZONTAL_AND_VERTICAL,
powerMode: arEngine.ARPowerMode.NORMAL,
semanticMode: arEngine.ARSemanticMode.NONE,
poseMode: arEngine.ARPoseMode.GRAVITY,
depthMode: arEngine.ARDepthMode.DISABLED,
meshMode: arEngine.ARMeshMode.DISABLED,
focusMode: arEngine.ARFocusMode.AUTO
};
await context.init();
this.arContext = context;
hilog.info(DOMAIN, TAG,
'MOTION_SESSION_INIT success costMs=%{public}d',
Date.now() - startTime);
}
平面识别模式保留在会话配置中,但本篇不读取平面、命中结果和锚点;深度、Mesh、语义能力保持关闭。
4. 从每个有效帧中读取位姿
回调触发后,先从 ARViewContext.session 取得当前会话,再依次获取 ARFrame、ARCamera 和 ARPose。
只有相机状态为 TRACKING 时才读取位姿。初始化阶段或暂时失去跟踪时,页面只更新状态,不把无效数据写入统计结果。
private handleFrameUpdate(
context: arViewController.ARViewContext,
timestamp: number): void {
const now: number = Date.now();
if (this.isDestroyed || this.isPaused ||
now - this.lastFrameTimestamp < 100) {
return;
}
this.lastFrameTimestamp = now;
const session: arEngine.ARSession | undefined = context.session;
if (!session) {
return;
}
let frame: arEngine.ARFrame | undefined = undefined;
let pose: arEngine.ARPose | undefined = undefined;
try {
frame = session.getFrame();
const camera: arEngine.ARCamera = frame.getCamera();
this.updateTrackingStatus(camera.state, camera.stateReason);
if (camera.state !== arEngine.ARTrackingState.TRACKING) {
return;
}
pose = camera.getPose();
this.updatePose(pose.translation, pose.rotation);
} finally {
if (pose) {
pose.release();
}
if (frame) {
frame.release();
}
}
}
页面按约 100 ms 一次,也就是约 10 Hz,更新文字和日志。ARView 相机预览仍按自身帧率运行,节流只用于本实验的数据展示。
每次处理完成后释放 ARPose 和 ARFrame,避免持续采样时累积原生资源。
5. 显示平移和旋转数据
ARPose.translation 提供 X、Y、Z 三个位置分量;ARPose.rotation 提供四元数 x、y、z、w。页面保留三位小数显示位置和四元数,转角保留一位小数。
首个有效位姿自动保存为原点:
private setOrigin(translation: Vec3, rotation: Quaternion): void {
this.originTranslation = {
x: translation.x,
y: translation.y,
z: translation.z
};
this.originRotation = {
x: rotation.x,
y: rotation.y,
z: rotation.z,
w: rotation.w
};
this.originReady = true;
}
相对位移使用当前位置与原点之间的三维欧氏距离:
private distanceBetween(current: Vec3, origin: Vec3): number {
const dx: number = current.x - origin.x;
const dy: number = current.y - origin.y;
const dz: number = current.z - origin.z;
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
相对转角通过两个单位四元数的点积计算。四元数 q 和 -q 表示同一姿态,因此点积先取绝对值。
private rotationBetween(
current: Quaternion,
origin: Quaternion): number {
const dot: number = Math.abs(
current.x * origin.x +
current.y * origin.y +
current.z * origin.z +
current.w * origin.w);
const normalizedDot: number =
Math.min(1, Math.max(0, dot));
return 2 * Math.acos(normalizedDot) * 180 / Math.PI;
}
“重置原点”不会重建 AR 会话,只把当前有效位置和朝向保存为新的比较基准,同时把相对位移和转角清零。
6. 处理跟踪状态
实验页区分三种状态:
| AR 状态 | 页面显示 | 数据处理 |
|---|---|---|
TRACKING |
跟踪中 | 读取并更新位姿 |
PAUSED |
跟踪暂停 | 不读取位姿,显示暂停原因 |
STOPPED |
跟踪停止 | 不读取位姿,等待会话退出 |
EXCESSIVE_MOTION 显示为“设备移动过快”,INSUFFICIENT_FEATURES 显示为“环境纹理不足”。没有更具体原因时显示“暂时无法建立稳定跟踪”。
7. 暂停、恢复和销毁
暂停和恢复直接调用 ARViewContext.pause() 与 resume()。暂停标记同时阻止帧处理;恢复后清空节流时间,下一次有效帧可以立即进入采样。
private pauseARView(): void {
if (!this.arContext || this.isDestroyed || this.isPaused) {
return;
}
this.arContext.pause();
this.isPaused = true;
}
private resumeARView(): void {
if (!this.arContext || this.isDestroyed || !this.isPaused) {
return;
}
this.arContext.resume();
this.isPaused = false;
this.lastFrameTimestamp = 0;
}
点击“销毁退出”或页面消失时调用 destroy(),再清空页面持有的上下文和回调。
构建与安装
工程使用已配置好的签名执行 clean 构建:
$env:JAVA_HOME='D:\Program Files\Huawei\DevEco Studio\jbr'
$env:DEVECO_SDK_HOME='D:\Program Files\Huawei\DevEco Studio\sdk'
$env:Path="$env:JAVA_HOME\bin;$env:Path"
$hvigor='D:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat'
& $hvigor clean --no-daemon
& $hvigor assembleHap --mode module `
-p product=default `
-p module=entry@default `
-p buildMode=debug `
--no-daemon
本次 CompileArkTS、PackageHap、SignHap 全部成功,构建结果为零告警。签名包位置:
entry/build/default/outputs/default/entry-default-signed.hap
最终 HAP 大小为 312812 bytes,SHA-256 为:
B1EB789CD168F17E65F67E40E778747C5B7D3C226AC95C8734027E28D251A146
安装到当前连接的真机:
$hdc='D:\Program Files\Huawei\DevEco Studio\sdk\default\openharmony\toolchains\hdc.exe'
& $hdc install `
'entry\build\default\outputs\default\entry-default-signed.hap'
安装结果为 install bundle successfully。
真机实操与结果
1. 进入运动跟踪
首页确认相机权限为 PERMISSION_GRANTED,设备能力矩阵中 SLAM 为“支持”,点击“实验 02:进入运动跟踪”。
ARView 初始化后先短暂处于“跟踪暂停”,相机看到有纹理的地毯后切换为“跟踪中”。最终回归日志中,会话初始化耗时 17 ms,从暂停切换到跟踪约 1.8 秒。
2. 确认位姿持续更新
进入跟踪状态后,X/Y/Z、四元数和采样数开始变化。第一次真机测试记录到第 45 个采样:位置为 X=0.097 m、Y=0.641 m、Z=-0.051 m,相对自动原点的位移为 0.650 m。

这三个坐标描述手机在本次 AR 会话局部坐标系中的位置,坐标正负方向随会话建立的坐标系确定。图中“重置原点”仍是灰色,这是第一次测试时发现的问题,修正过程记录在后文“情况四”;最终重置结果使用修正后的页面重新采集。
3. 重置当前原点
手机保持当前姿态,点击“重置原点”。页面继续显示绝对位姿,同时把相对位移和相对转角改为 0.000 m / 0.0°。

日志同步记录重置时的采样数和位置:
MOTION_ORIGIN_RESET sample=17 position=(-0.047,0.016,-0.064)
4. 暂停后检查采样数
点击“暂停”,状态标签变为“会话已暂停”,按钮变为“恢复”。采样数停在 802;间隔 5 秒再次读取页面,仍为 802。

对应日志:
MOTION_SESSION_PAUSE success sample=802
5. 恢复后移动并旋转手机
点击“恢复”,跟踪重新建立,采样数继续增长。随后平移并旋转手机,画面记录到第 825 个采样:
- X/Y/Z:
-0.050 m / 0.076 m / -0.126 m; - 相对位移:
0.727 m; - 相对转角:
21.2°; - 四元数:
x=-0.045, y=0.705, z=0.706, w=0.039。

本轮销毁会话时,日志汇总的最大相对位移为 0.791 m,最大相对转角为 21.2°。
6. 销毁会话并返回首页
点击“销毁退出”后,ARViewContext.destroy() 成功,页面返回实验工作台,相机占用指示消失。

实操中遇到的情况与处理
情况一:初始化后暂时没有位姿
真机刚进入 ARView 时,相机状态先是 PAUSED,原因显示“暂时无法建立稳定跟踪”;镜头对准有纹理的地毯后才切换为 TRACKING。
处理方式是在帧回调中先判断 camera.state。只有 TRACKING 才调用 getPose();其他状态只显示原因,不参与位移计算。
情况二:数据在日志中变化,页面数字没有刷新
第一次实现把 X/Y/Z 数值作为参数传给带参数的 @Builder。Hilog 中的位置持续变化,但构建器中的文字停留在首次值。

处理后,构建器只接收坐标轴名称,内部直接读取 @State:
@Builder
private positionItem(axis: string, color: string): void {
Text(this.positionValue(axis))
}
修改后,页面数值与每 10 次采样输出的 Hilog 一致更新。
情况三:半透明深色卡片显示成黄色
最初把八位颜色写成了 RRGGBBAA 顺序。ArkUI 按 AARRGGBB 解释八位颜色,导致透明度和颜色通道错位。
颜色改为 #E812233D、#12FFFFFF 等 AARRGGBB 格式后,叠加卡恢复为半透明深蓝色,相机画面仍可见。
情况四:已经有位姿,“重置原点”仍不可点击
按钮最初绑定 latestTranslation !== undefined,但 latestTranslation 不是 @State,值改变后没有触发按钮重新构建。
增加 @State originReady,首个有效位姿保存为原点时设为 true,按钮改为:
.enabled(this.originReady)
修正后,位姿建立时按钮立即可用,重置原点结果可以稳定复现。
情况五:按回调时间戳节流后,真机仍接近每帧刷新
SDK 回调提供了 timestamp 参数。第一版直接用它做 100 ms 差值判断,但真机观察到采样仍接近相机帧率,界面更新约 70 次/秒。
本实验只需要稳定观察数据,节流基准改为 Date.now()。最终连续采样约 10 次/秒,暂停和恢复的采样数也更容易核对。回调参数仍保留,后续需要与相机帧时间对齐时可以单独处理其时间单位。
最终验证结果
| 验证项 | 真机结果 |
|---|---|
| WORLD 会话初始化 | 成功,最终回归耗时 17 ms |
| 跟踪状态 | 从 PAUSED 切换为 TRACKING |
| X/Y/Z 平移 | 持续更新,单位为米 |
| 四元数 | x/y/z/w 持续更新 |
| 重置原点 | 0.000 m / 0.0° |
| 移动和旋转 | 截图 0.727 m / 21.2° |
| 暂停 | 采样数 802,等待 5 秒不变 |
| 恢复 | 采样继续增长,截图达到 825 |
| 销毁 | 成功返回首页,相机资源释放 |
| clean 签名构建 | 成功,零告警 |
真机验证日志
下面是最终短回归中保留的完整关键日志。从会话初始化、跟踪状态变化、自动原点、手动重置,到暂停、恢复和销毁,整个流程均有对应记录。
08-19 16:59:27.359 MOTION_SESSION_INIT success costMs=17
08-19 16:59:27.413 MOTION_TRACKING state=跟踪暂停 reason=暂时无法建立稳定跟踪
08-19 16:59:29.178 MOTION_TRACKING state=跟踪中 reason=暂时无法建立稳定跟踪
08-19 16:59:29.179 MOTION_ORIGIN_AUTO position=(-0.025,-0.005,0.002)
08-19 16:59:30.082 MOTION_POSE sample=10 position=(-0.033,0.004,-0.024) distance=0.028 rotation=0.0
08-19 16:59:30.856 MOTION_ORIGIN_RESET sample=17 position=(-0.047,0.016,-0.064)
08-19 16:59:31.134 MOTION_POSE sample=20 position=(-0.056,0.022,-0.085) distance=0.023 rotation=0.0
08-19 16:59:32.172 MOTION_POSE sample=30 position=(-0.091,0.034,-0.178) distance=0.123 rotation=0.1
08-19 16:59:32.955 MOTION_SESSION_PAUSE success sample=31
08-19 16:59:34.935 MOTION_SESSION_RESUME success sample=31
08-19 16:59:35.027 MOTION_TRACKING state=跟踪暂停 reason=暂时无法建立稳定跟踪
08-19 16:59:38.134 MOTION_SESSION_DESTROY success samples=31 maxDistance=0.135 maxRotation=0.1
长时间移动、旋转和生命周期验证日志如下:
MOTION_SESSION_PAUSE success sample=802
暂停后等待 5 秒,页面采样数仍为 802
MOTION_SESSION_RESUME success sample=802
MOTION_POSE sample=810 position=(-0.031,0.037,-0.042) distance=0.775 rotation=21.2
MOTION_POSE sample=820 distance=0.746 rotation=21.2
恢复后画面:sample=825,distance=0.727 m,rotation=21.2°
MOTION_SESSION_DESTROY success samples=832 maxDistance=0.791 maxRotation=21.2
本篇完成的是“从 AR 帧取得设备位姿并验证数据变化”的链路,不包含平面命中、锚点和 3D 模型放置。
官方资料
更多推荐


所有评论(0)