引言
手机 AR 是绝大多数团队接触空间计算的第一站:不需要专用硬件,ARCore(Android)与 ARKit(iOS)已经把 SLAM、平面检测、光照估计封装成了几十行代码就能调用的 API。但「跑起来」和「跑得住」之间差距极大。
工程上真正的问题集中在三处:平面为什么会飘(追踪漂移与环境变化)、锚点为什么对不上(坐标系与姿态语义被误读)、放置为什么会失败(射线打不到平面或平面还没稳定)。这三个问题几乎占了移动 AR 线上反馈的八成。
本文按「技术栈 → 会话 → 追踪 → 平面 → 锚点 → 深度与光照 → 跨平台 → 性能」的顺序展开,代码以 ARCore/ARKit 原生与 AR Foundation 两条线对照给出,并强调那些文档里不写但线上必踩的细节。整体定位见 空间计算与 XR 技术全景 。
目录
- 手机 AR 的技术栈
- 会话与生命周期
- 运动追踪与位姿
- 平面检测机制
- 平面类型与语义
- 射线检测与物体放置
- 锚点与姿态解算
- 深度与遮挡
- 光照估计与阴影
- 图像与物体识别
- AR Foundation 跨平台
- 性能与功耗
- 权衡取舍
- 常见坑清单
- 小结
1. 手机 AR 的技术栈
两大平台的能力对照:
| 能力 | ARCore | ARKit | 备注 |
|---|---|---|---|
| 运动追踪 | 支持 | 支持 | 均为 VIO |
| 平面检测 | 水平+垂直 | 水平+垂直 | ARKit 收敛更快 |
| 深度 API | Depth API | LiDAR / 场景深度 | iPhone Pro 有 LiDAR |
| 光照估计 | 支持 | 支持 | 含方向光 |
| 图像识别 | Augmented Images | ARImageTracking | 数量上限不同 |
| 物体识别 | 无原生 | ARObjectScanning | ARKit 独有 |
| 持久化 | Cloud Anchors | ARWorldMap | 机制差异大 |
| 语义分割 | 无 | ARFrame segmentation | ARKit 独有 |
关键差异在深度:iPhone Pro 与 iPad Pro 有 LiDAR,能直接给出稠密深度;Android 侧主要靠 Depth API 用运动视差估计深度,精度与速度都弱一档。这直接影响遮挡效果与放置稳定性。
2. 会话与生命周期
AR 会话是有状态的,必须严格管理生命周期:
// ARCore 会话配置(Kotlin)
val session = Session(context)
val config = Config(session).apply {
planeFindingMode = Config.PlaneFindingMode.HORIZONTAL_AND_VERTICAL
lightEstimationMode = Config.LightEstimationMode.ENVIRONMENTAL_HDR
depthMode = if (session.isDepthModeSupported(
Config.DepthMode.AUTOMATIC)) {
Config.DepthMode.AUTOMATIC
} else Config.DepthMode.DISABLED
focusMode = Config.FocusMode.AUTO
updateMode = Config.UpdateMode.LATEST_CAMERA_IMAGE
}
session.configure(config)
生命周期状态机:
CREATED ──resume──► RESUMED ──pause──► PAUSED
│ │ │
└─── 未配置 └── 每帧 update └── 释放相机
异常状态:ERROR_CAMERA_PERMISSION / ERROR_CAMERA_NOT_AVAILABLE
工程要点:
- 暂停必须真的暂停:切后台不调用 pause 会持续占用相机与 GPU,被系统杀掉。
- 恢复后位姿不连续:从后台回来追踪会重置,锚点可能全部失效,必须重新放置或重定位。
- 权限拒绝路径:用户拒绝相机权限要有明确引导,不能黑屏。
2.1 会话配置项速查
| 配置项 | 取值 | 影响 |
|---|---|---|
| 平面检测模式 | 关闭/水平/垂直/全部 | 全开会增加算力消耗 |
| 光照估计 | 关闭/环境光/HDR | HDR 更准但更耗 GPU |
| 深度模式 | 关闭/自动 | 自动按设备能力启用 |
| 对焦模式 | 固定/自动 | 固定适合近距离 AR |
| 更新模式 | 阻塞/最新帧 | 最新帧降低延迟但可能丢帧 |
| 相机分辨率 | 按设备 | 追踪 640×480 通常足够 |
配置的原则是按需开启:展示类应用不需要深度与 HDR 光照,全开会白耗 20% 以上的电。
3. 运动追踪与位姿
ARCore/ARKit 的位姿是设备在世界坐标系中的位置与朝向,世界坐标系在会话开始时以设备初始位置为原点。
世界坐标系约定:
右手坐标系
Y 轴向上(与重力对齐,靠 IMU 重力方向估计)
原点:会话开始时设备所在位置
单位:米
位姿表示:位置 vec3 + 旋转四元数
ARKit: simd_float4x4(列主序,第 4 列是位置)
ARCore: Pose(translation + rotationQuaternion)
三个必须理解的限制:
- 世界原点不固定:不同会话原点不同,跨会话的绝对坐标没有意义。
- Y 轴靠重力估计:水平面可靠,但设备的 Yaw(绕垂直轴旋转)是相对初始朝向,没有绝对方向。
- 漂移不可避免:VIO 累积误差,长时间使用或走过大空间后会明显偏移。
因此「把物体放在世界坐标 (1, 0, 2)」这种写法只在单次会话内有效,要跨会话必须用 空间锚点与跨会话持久化 的机制。
3.1 追踪质量状态
两平台都暴露追踪质量枚举,必须据此做 UI 提示与降级:
ARCore TrackingState:
TRACKING 正常
PAUSED 追踪暂停(遮挡/快速运动/弱光),位姿不可用
STOPPED 已停止
ARKit ARCamera.TrackingState:
.normal
.limited(.initializing / .excessiveMotion / .insufficientFeatures
/ .relocalizing)
.notAvailable
工程上应把 limited 的四种原因分别提示:初始化中提示「缓慢移动手机」、特征不足提示「换个角度」、运动过快提示「放慢速度」、重定位中提示「回到之前的位置」。统一的「请调整」提示对用户毫无帮助。
4. 平面检测机制
平面检测不是「一次扫描出结果」,而是逐步生长的过程:
1. 从 VIO 特征点中提取共面点簇
2. 用 RANSAC 拟合平面方程
3. 平面边界多边形随新观测扩张(extent 增长)
4. 持续更新中心、法向与边界顶点
5. 长时间无观测则标记为 subsumed(被合并)或移除
这意味着平面是动态对象,同一逻辑平面在 API 中可能出现多次、被合并、被替换。常见错误是缓存了 Plane 引用然后长期使用,结果平面已被移除。
// AR Foundation:正确处理平面增删
void OnPlanesChanged(ARPlanesChangedEventArgs args) {
foreach (var p in args.added) RegisterPlane(p);
foreach (var p in args.updated) UpdatePlane(p); // extent 会变
foreach (var p in args.removed) UnregisterPlane(p); // 必须处理
}
4.1 收敛速度的影响因素
| 因素 | 影响 | 对策 |
|---|---|---|
| 环境纹理 | 纹理少则特征少,收敛慢 | 引导用户扫视,避免白墙 |
| 光照 | 弱光噪点大,特征不稳 | 提示开灯,避免逆光 |
| 运动方式 | 纯旋转无三角化,必须平移 | 提示左右移动手机 |
| 平面反光 | 镜面/玻璃误检 | 语义过滤,降低置信度阈值 |
| 遮挡 | 前景物体破坏共面性 | 允许平面被分割 |
5. 平面类型与语义
平面有两种分类维度:
几何朝向:
- 水平朝上(HORIZONTAL_UPWARD_FACING):地面、桌面。
- 水平朝下(HORIZONTAL_DOWNWARD_FACING):天花板。
- 垂直(VERTICAL):墙面、门。
语义标签(ARKit 支持,需开启 planeDetection 的 .horizontal + sceneReconstruction 或 ARPlaneClassification):
| 标签 | 含义 | 典型用途 |
|---|---|---|
| FLOOR | 地板 | 放置大件、角色行走 |
| TABLE | 桌面 | 放置小物件 |
| WALL | 墙 | 贴海报、虚拟窗 |
| CEILING | 天花板 | 灯光、吊饰 |
| SEAT | 座椅 | 交互锚点 |
| DOOR / WINDOW | 门窗 | 空间理解 |
语义分类基于特征与几何启发式,准确率有限,不要用它做安全或计费决策。工程上更稳的做法是结合平面尺寸与高度做规则过滤:例如「高度 0.7 到 0.8 米、面积大于 0.2 平米」判为桌面。
6. 射线检测与物体放置
放置物体的标准流程是「屏幕点 → 射线 → 与平面求交 → 得到世界坐标」:
// AR Foundation:点击放置
bool TryPlace(Vector2 screenPos, GameObject prefab, out GameObject go) {
go = null;
var ray = arCamera.ScreenPointToRay(screenPos);
// 只与平面求交,避免打中已有物体
if (!planeManager.Raycast(ray, out ARRaycastHit hit,
TrackableType.PlaneWithinPolygon))
return false; // 没打中平面,走兜底逻辑
go = Instantiate(prefab, hit.pose.position, hit.pose.rotation);
var anchor = go.AddComponent<ARAnchor>();
return true;
}
关键细节:
- 用
PlaneWithinPolygon而非PlaneWithinBounds:前者严格限制在已识别的多边形内,后者用包围盒,会把物体放到平面上方空气里。 - 必须有兜底:射线打不到时,退化到「距相机 1.5 米、朝向相机」的放置,否则用户点半天没反应。
- 放置后立刻挂锚点:直接放 GameObject 会在追踪更新时抖动,挂 ARAnchor 让系统接管位姿。
- 用 hit.pose 而不是 hit.point:姿态包含平面法向,物体才能贴合平面。
6.1 放置稳定性技巧
1. 等待平面稳定:ARKit 的 ARPlaneAlignment 或 extent 不再变化
2. 检查平面面积:小于 0.1 m² 的平面不要放
3. 检查相机俯角:俯角过小(贴地平视)时射线与平面交角小,误差放大
4. 显示放置指示器:先显示幽灵模型,点击后实体化
6.2 放置后的交互
物体放下之后,用户通常还要移动、旋转、缩放:
单指拖动:沿平面平移(保持 y 不变,只改 x/z)
双指旋转:取两指连线角度变化量作为 Yaw 增量
双指捏合:取两指距离比值作为缩放系数
约束:
限制缩放上下限,防止物体缩到看不见或大到穿墙
拖动时用射线与平面重新求交,而不是直接加屏幕位移
旋转只绕 Y 轴,避免物体倾斜出平面
拖动时最常见的错误是把屏幕像素位移直接加到世界坐标,导致移动速度与距离不成比例(远看快、近看慢)。正确做法是每帧用射线与平面重新求交。
7. 锚点与姿态解算
锚点是「让系统持续追踪某个世界位置」的句柄。区别在于:
| 类型 | 说明 | 生命周期 |
|---|---|---|
| 会话内锚点 | 仅在当前会话有效 | 会话结束即失效 |
| 云锚点(ARCore) | 上传到云端可跨设备共享 | 云端管理,有时效 |
| 世界地图(ARKit) | 保存特征地图供重定位 | 本地保存,可跨会话 |
| 持久锚点(ARCore) | 本地保存锚点 | 本地持久化 |
姿态读取时要注意坐标系的列主序与右手系约定:
// ARKit:从锚点矩阵取位置与朝向
let t = anchor.transform
let pos = SIMD3<Float>(t.columns.3.x, t.columns.3.y, t.columns.3.z)
// 注意:位置在第 4 列,不是第 4 行
最常见的错误是把矩阵当作行主序读取,结果是位置正确但旋转全错,表现为物体「位置对但躺倒」。
8. 深度与遮挡
遮挡是移动 AR 观感的分水岭:没有遮挡时,虚拟物体永远「浮」在真实物体前面。
三种深度来源:
| 来源 | 精度 | 延迟 | 设备要求 |
|---|---|---|---|
| Depth API(运动视差) | 低,边缘糊 | 高 | 大部分 ARCore 设备 |
| LiDAR | 高,稠密 | 低 | iPhone/iPad Pro |
| 场景几何(ARKit) | 中高 | 中 | 需扫描 |
ARCore 的 Depth API 提供的是低分辨率平滑深度图,用它做硬遮挡会出现明显的「果冻边缘」。推荐做法是用软遮挡(Soft Occlusion):把深度转成透明度,让虚拟物体边缘渐隐。
软遮挡思路:
depthDiff = virtualDepth - realDepth
若 depthDiff < 0 → 虚拟在前,完全不透明
若 0 < depthDiff < 0.1 m → 线性插值 alpha
若 depthDiff > 0.1 m → 完全被遮挡
9. 光照估计与阴影
光照估计提供两个关键值:
- 环境强度(Intensity):用于调整虚拟物体的亮度。
- 环境色温(Color Correction):让虚拟物体融入真实光照。
// AR Foundation:把光照估计应用到虚拟物体
void UpdateLighting(ARLightEstimationData d) {
if (d.averageBrightness.HasValue)
light.intensity = d.averageBrightness.Value;
if (d.averageColorTemperature.HasValue)
light.colorTemperature = d.averageColorTemperature.Value;
}
方向光估计(ARKit 支持)能给出主光方向,让虚拟物体投出与真实一致方向的阴影。没有方向光时,常见做法是投一个「软椭圆阴影」贴到平面上,观感远好于没有阴影。
阴影的实现要点:用平面上的一块半透明贴图(blob shadow)而不是实时光照阴影,成本低且稳定,因为移动端实时阴影贴图代价高。
10. 图像与物体识别
图像识别(ARCore Augmented Images / ARKit ARImageTracking)用于把内容绑定到已知图片上:
ARCore Augmented Images:
最多同时追踪 20 张(配置不同),需预先提供特征库
离线:把图片加入 imgdb 数据库并打包进 APK
在线:运行时用 AugmentedImageDatabase 动态加载
ARKit ARImageTracking:
运行时提供参考图片,最多追踪 100 张
对图片纹理要求高,纯色图几乎无法识别
选图经验:纹理丰富、非对称、无重复图案的图片识别率高;纯色 Logo、渐变、重复花纹识别率极低。工程上应提供「识别失败」的降级路径,例如退回到平面放置。
11. AR Foundation 跨平台
AR Foundation 是 Unity 的跨平台抽象层,一套 API 覆盖 ARCore/ARKit:
| 场景 | 建议 |
|---|---|
| 双平台都要,功能是交集 | 用 AR Foundation |
| 只需单平台 | 用原生 SDK,能力更全 |
| 依赖平台独有能力 | AR Foundation + 原生插件 |
| 团队无 Unity 经验 | 原生更直接 |
AR Foundation 的坑在于它只暴露交集:ARKit 的物体扫描、场景分割等独有能力在 AR Foundation 里没有,必须写原生插件。因此选型时要先列能力清单,确认交集是否够用。
// 检查运行时能力,避免在不支持的设备上黑屏
if (!ARSession.state == ARSessionState.SessionTracking) { /* 提示 */ }
if (LoaderUtility.GetLoaderCount() == 0) { /* 无可用 XR 插件 */ }
12. 性能与功耗
移动 AR 的性能约束比普通应用严格得多,因为相机、SLAM、渲染三件事同时在跑:
| 优化项 | 手段 | 收益 |
|---|---|---|
| 相机分辨率 | 降到 640×480(追踪够用) | GPU 与功耗显著下降 |
| 渲染分辨率 | 动态降采样 | 直接降 GPU 负载 |
| 平面更新频率 | 降低 updateMode 频率 | 省 CPU |
| 光照估计 | 关闭 HDR 模式 | 省 GPU |
| 模型复杂度 | 单模型 < 5 万面 | 移动 GPU 友好 |
| 发热控制 | 降帧率到 30 fps | 避免降频与关机 |
发热是移动 AR 的头号杀手:持续 60 fps 加相机与 SLAM,10 到 15 分钟后手机开始降频,帧率掉到 30 以下,追踪质量下降。工程上必须做热管理策略:检测到温度升高主动降帧率与分辨率,而不是等系统强制降频。
12.1 移动 AR 的帧预算
以 60 fps(16.7 ms)为例的参考分配:
| 环节 | 预算 | 说明 |
|---|---|---|
| 相机采集 | 2 ms | 与渲染并行 |
| SLAM 与平面更新 | 3 ms | 平台内部,不可控 |
| 应用逻辑 | 2 ms | 交互、动画、状态 |
| 渲染提交 | 3 ms | DrawCall 与合批 |
| GPU 渲染 | 6 ms | 含相机背景 |
| 合成 | 1 ms | 平台内部 |
余量只有 1 到 2 ms,因此移动 AR 应用不应在同帧做网络请求解析或大文件 IO,必须异步化。
13. 权衡取舍
- 原生 SDK 与 AR Foundation:前者能力全、开发快(单平台),后者跨平台但只有交集。
- 硬遮挡与软遮挡:硬遮挡更「真实」但边缘果冻,软遮挡观感更稳但不够精确。
- 高分辨率相机与功耗:追踪用低分辨率就够,渲染用高分辨率才好看,二者应分开设置。
- 实时阴影与贴图阴影:前者真实但昂贵,后者廉价且稳定,移动端优先后者。
- 云锚点与本地持久化:云锚点可跨设备但依赖网络与配额,本地简单但有设备边界。
- 持续追踪与按需追踪:持续追踪体验好但耗电,展示类应用可按需启停。
14. 常见坑清单
- 缓存平面对象:平面会被合并或移除,必须处理 removed 事件。
- 用 PlaneWithinBounds:物体被放到平面外空气里,应用 PlaneWithinPolygon。
- 直接放 GameObject 不挂锚点:追踪更新时物体抖动,必须挂 ARAnchor。
- 世界坐标硬编码:跨会话无效,持久化要用锚点或世界地图。
- 矩阵行列主序读反:位置对但旋转错,物体躺倒,注意列主序。
- 切后台不 pause 会话:相机与 GPU 持续占用,被系统杀进程。
- 不做放置兜底:射线打不到平面时用户点击无响应,体验崩塌。
- 忽略热管理:持续满帧导致降频,后期帧率与追踪双降。
- 用语义标签做业务判断:分类准确率有限,不能用于关键决策。
- 只在良好光照下测试:弱光与白墙场景必须在真机压力测试覆盖。
15. 小结
移动 AR 的工程主线是:配置会话(平面+深度+光照)→ 管理生命周期 → 理解世界坐标系的限制 → 监听平面增删改 → 用射线加锚点放置 → 用软遮挡与光照融入真实场景。最容易出错的两处是「把平面当静态对象」和「把世界坐标当持久坐标」。
判断一个移动 AR 应用是否工程合格,看三件事:切后台能正确恢复、放置有兜底、长时运行不烫手。这三点比画面好看更能决定留存。
下一步如果要做跨会话与跨设备的内容共享,读 空间锚点与跨会话持久化 ;如果要做 XR 头显上的完整交互体系,读 Unity XR Interaction Toolkit 交互体系 。
延伸阅读
- 空间计算与 XR 技术全景 — XR 全景与选型
- 空间锚点与跨会话持久化 — 云锚点与世界地图
- XR 显示光学与头部眼动追踪 — VIO 的硬件基础
- 计算机视觉技术全景 — 特征与深度估计基础
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。