Depth API 深度数据获取与配置
Depth API 能做什么
Depth API 为每一帧摄像头画面生成一张深度图(Depth Map)——一张与 RGB 画面同分辨率(或较低分辨率)的灰度图,每个像素的值表示该像素点到手机相机的距离。
有了深度图,ARCore 应用就能做到:
- 遮挡渲染(Occlusion):虚拟物体自动出现在真实物体的前面或后面
- 精确 Hit Test:在没有平面或特征点的光滑表面也能精确命中
- 物理模拟:让虚拟物体"感知"周围环境的形状(比如虚拟球滚下真实斜坡)
- 尺寸测量:知道两个点的距离就能算出真实尺寸
Depth API 的三种模式
| 模式 | Session.Config.DepthMode | 说明 | 设备要求 |
|---|---|---|---|
| DISABLED | DISABLED | 不启用 Depth API(默认) | 无 |
| HARDWARE | AUTOMATIC(自动选择 HARDWARE 或 SOFTWARE) | 优先用硬件深度传感器(ToF) | 有 ToF 或双摄的设备 |
| SOFTWARE | 自动降级 | 没有硬件时用被动深度(RGB 双目立体匹配) | 所有 ARCore 设备 |
ToF vs 被动深度的区别
| 对比项 | ToF 硬件深度 | 被动深度 |
|---|---|---|
| 工作原理 | 发射红外光,测量飞行时间 | 对比多帧 RGB 画面的视差 |
| 精度 | 高(厘米级) | 较低(尤其远距离) |
| 最小测量距离 | 近 | 远(近景容易失败) |
| 刷新率 | 高(可达 30fps+) | 取决于摄像头帧率 |
| 适用设备 | Pixel 4+、三星 S 系列旗舰等 | 所有 ARCore 设备 |
| 能耗 | 较高(需要发射红外) | 较高(需要大量计算) |
Session 配置启用 Depth API
Android 原生
val session = Session(context)
// 检查设备是否支持
if (session.isDepthSupported()) {
session.configure(
Session.Config(context).apply {
setDepthMode(Session.Config.DepthMode.AUTOMATIC)
// 可选:设置深度图质量
// setDepthTextureSize() 控制分辨率
}
)
} else {
// 设备不支持,降级到平面检测
}
Unity AR Foundation
// 在 Unity 的 ARCoreLoaderSettings 中配置
// 或运行时:
var sessionSubsystem = XRGeneralSettings.Instance.Manager.activeLoader.GetSubsystem<XRSessionSubsystem>();
if (sessionSubsystem != null && sessionSubsystem is ARCoreSessionSubsystem arcoreSessionSubsystem)
{
arcoreSessionSubsystem.SetDepthMode(ARCoreDepthMode.Automatic);
}
Unity 的 ARFoundation 在 4.0+ 版本原生支持 Depth API,AROcclusionManager 组件可以直接启用遮挡渲染。
获取深度图数据
Depth API 有两种数据访问方式,取决于你的场景:
方式一:Texture(推荐渲染用)
Depth API 把深度数据直接渲染成一个 OpenGL 纹理(Android)或 Metal 纹理(iOS),你可以直接传给着色器做遮挡渲染:
// 在每一帧 session.update() 之后
val frame = session.update()
// 深度纹理的句柄(OpenGL 纹理 ID)
val depthTextureId = frame.depthTexture
// 深度纹理的尺寸
val depthWidth = frame.depthWidth
val depthHeight = frame.depthHeight
// 深度图格式:通常是 GL_R32F(单通道 32 位浮点数,值 = 距离米数)
方式二:Image(计算/测量用)
如果你需要在 CPU 端读取深度值(比如做尺寸测量),用 Frame.acquireDepthImage():
// 获取深度 Image
val depthImage: Image = frame.acquireDepthImage()
// 深度 Image 的 format 通常是 YUV_420_888,Y 平面存储深度值
val planes = depthImage.planes
val plane0 = planes[0] // Y 平面
// 读取某个像素的深度值
// 注意:深度图可能低于 RGB 分辨率,需要缩放坐标
val depthAtPixel = readDepthFromPlane(plane0, x, y)
// depthAtPixel 就是该像素到相机的距离(米)
// 用完必须释放(否则内存泄漏)
depthImage.close()
深度值的单位与约定
- 单位:米
- 有效范围:通常 0.1 米 ~ 5 米(ToF),更远精度快速下降
- 无效值:某些像素无法计算深度时(比如纯白墙面、过曝区域),值为 0 或特定常量(用
isValidDepth()判断)
坐标系转换:深度图坐标 → 世界坐标
深度图的像素坐标 (u, v) 和 RGB 画面是对齐的(同一个像素对应同一个物理点),但分辨率可能不同(深度图分辨率通常比 RGB 低)。
从深度图像素获取对应世界坐标的流程:
// 1. 在 RGB 画面上有一个屏幕像素坐标 (screenX, screenY)
// 先映射到深度图坐标
val depthX = screenX * (depthWidth / rgbWidth.toFloat())
val depthY = screenY * (depthHeight / rgbHeight.toFloat())
// 2. 从深度图读取该点的距离
val depth = readDepthAt(depthX, depthY)
if (depth <= 0) return // 无效深度
// 3. 用相机内参把 (depthX, depthY, depth) 反投影到相机坐标系
val cx = frame.camera.textureIntrinsics[0] // principal point X
val cy = frame.camera.textureIntrinsics[1] // principal point Y
val fx = frame.camera.textureIntrinsics[2] // focal length X
val fy = frame.camera.textureIntrinsics[3] // focal length Y
val cameraX = (depthX - cx) / fx * depth
val cameraY = (depthY - cy) / fy * depth
val cameraZ = depth
// 4. 相机坐标系 → 世界坐标系
val worldPoint = frame.camera.pose.transformPoint(Vector3(cameraX, cameraY, cameraZ))
Unity 中读取深度值
Unity AR Foundation 封装得更简单:
public class DepthReader : MonoBehaviour
{
public AROcclusionManager occlusionManager;
void Update()
{
if (occlusionManager.TryAcquireEnvironmentDepthCpuImage(out var cpuImage))
{
// cpuImage 就是深度图的 CPU 端访问
// 每个像素值 = 距离(米)
// 用完记得 Release
cpuImage.Dispose();
}
// 或获取 GPU 端的纹理
if (occlusionManager.TryAcquireEnvironmentDepthTexture(out var texture))
{
// texture 可以直接传给材质做遮挡
}
}
}
设备支持查询
// 在创建 Session 之前或之后都可以查
val arSession = Session(context)
if (!arSession.isDepthSupported()) {
// 设备完全不支持 Depth API(极少数情况)
// 只能用平面检测做遮挡的近似
}
// 或者查询具体哪种模式可用
val hardwareSupported = arSession.isDepthModeSupported(Session.Config.DepthMode.AUTOMATIC)
Depth API 并不是所有 ARCore 设备都支持——低端手机可能只支持基础的平面检测而没有 Depth。在设计功能时要考虑降级方案。