ARCore 与 ARKit 平面检测与锚点

本文系统讲解 ARCore 与 ARKit 的平面检测与锚点机制,回答平面为什么飘、锚点姿态怎么读、射线放置物体失败怎么办、跨平台该不该用 AR Foundation 等实战问题。覆盖会话生命周期、运动追踪与 VIO、平面检测与语义分类、射线检测与放置、锚点与姿态解算、深度遮挡、光照估计、图像识别与性能功耗优化,并给出代码、对比表、权衡与常见坑。

引言

手机 AR 是绝大多数团队接触空间计算的第一站:不需要专用硬件,ARCore(Android)与 ARKit(iOS)已经把 SLAM、平面检测、光照估计封装成了几十行代码就能调用的 API。但「跑起来」和「跑得住」之间差距极大。

工程上真正的问题集中在三处:平面为什么会飘(追踪漂移与环境变化)、锚点为什么对不上(坐标系与姿态语义被误读)、放置为什么会失败(射线打不到平面或平面还没稳定)。这三个问题几乎占了移动 AR 线上反馈的八成。

本文按「技术栈 → 会话 → 追踪 → 平面 → 锚点 → 深度与光照 → 跨平台 → 性能」的顺序展开,代码以 ARCore/ARKit 原生与 AR Foundation 两条线对照给出,并强调那些文档里不写但线上必踩的细节。整体定位见 空间计算与 XR 技术全景 。

目录

  1. 手机 AR 的技术栈
  2. 会话与生命周期
  3. 运动追踪与位姿
  4. 平面检测机制
  5. 平面类型与语义
  6. 射线检测与物体放置
  7. 锚点与姿态解算
  8. 深度与遮挡
  9. 光照估计与阴影
  10. 图像与物体识别
  11. AR Foundation 跨平台
  12. 性能与功耗
  13. 权衡取舍
  14. 常见坑清单
  15. 小结

1. 手机 AR 的技术栈

两大平台的能力对照:

能力ARCoreARKit备注
运动追踪支持支持均为 VIO
平面检测水平+垂直水平+垂直ARKit 收敛更快
深度 APIDepth APILiDAR / 场景深度iPhone Pro 有 LiDAR
光照估计支持支持含方向光
图像识别Augmented ImagesARImageTracking数量上限不同
物体识别无原生ARObjectScanningARKit 独有
持久化Cloud AnchorsARWorldMap机制差异大
语义分割无ARFrame segmentationARKit 独有

关键差异在深度: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 会话配置项速查

配置项取值影响
平面检测模式关闭/水平/垂直/全部全开会增加算力消耗
光照估计关闭/环境光/HDRHDR 更准但更耗 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 msDrawCall 与合批
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 交互体系 。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「AR 与 VR」更多文章

  1. XR 培训与仿真应用
  2. XR 控制器与输入设备
  3. XR 内容分发与商店上架