Hit Test 射线检测与用户交互实现
Hit Test 是什么
Hit Test(射线检测)是 ARCore 中连接用户屏幕输入和 3D 世界坐标的桥梁。它解决的问题非常具体:
用户在手机屏幕上点了一下 (x, y)——这个屏幕位置对应现实世界中的哪个点?
ARCore 的做法是:从手机相机的光心向屏幕上的触点投射一条射线(Ray),然后计算这条射线和所有已知 Trackable(平面、特征点)的交点,返回交点的世界坐标 Pose。
Hit Test 完整流程
步骤一:拿到屏幕触点坐标
// 在你的手势检测回调中(比如 GestureDetector onSingleTapConfirmed)
fun onTap(x: Float, y: Float) {
// x, y 是屏幕像素坐标
performHitTest(x, y)
}
步骤二:从 Frame 获取 Hit Test 接口
Hit Test 必须在 session.update() 之后、从最新 Frame 获取:
session.update(frame)
// Frame 对象提供了 hitTest() 方法
// 不同版本的 API 签名略有不同,以下是典型用法
val hitResults = frame.hitTest(x, y) // 旧版本 API
// 或
val hitResults = frame.hitTestInstantPlacement(x, y, 1.0f) // 新版本支持 Instant Placement
步骤三:解析 Hit Test 结果
Hit Test 返回命中列表(按距离从近到远排序):
hitResults.forEach { result ->
// 命中的类型:PLANE / POINT / INSTANT_PLACEMENT / DEPTH_POINT / SESSION / CAMERA
val type = result.trackableType
// 命中点的世界坐标 Pose(位置 + 朝向)
val pose = result.pose
// 命中的 Trackable(如果命中的是平面或特征点)
val trackable = result.trackable
when (type) {
HitTest.TrackableType.PLANE -> {
// 命中了平面,最理想的情况
// result.trackable 是 Plane 对象
val plane = result.trackable as Plane
}
HitTest.TrackableType.POINT -> {
// 命中了特征点(没有 plane,只有点)
// result.trackable 是 Point 对象
}
HitTest.TrackableType.INSTANT_PLACEMENT -> {
// 即时放置,可能还不是精确位置
// 详见 Instant Placement 页面
}
HitTest.TrackableType.SESSION -> {
// 没有命中任何 Trackable,但 ARCore 返回了基于深度图的估计位置
}
HitTest.TrackableType.DEPTH_POINT -> {
// Depth API 启用时,基于深度图的精确命中
}
}
}
步骤四:在命中点放置虚拟物体
// 取第一个命中(通常是最近的)
val firstHit = hitResults.firstOrNull { it.trackable == HitTest.TrackableType.PLANE }
?: hitResults.firstOrNull { it.trackable == HitTest.TrackableType.POINT }
?: return
// 创建锚点
val anchor = session.createAnchor(firstHit.pose)
// 在锚点位置放置虚拟物体
placeVirtualObjectAtAnchor(anchor)
Unity 中的 Hit Test
Unity AR Foundation 中 Hit Test 的用法封装得更优雅:
using UnityEngine.XR.ARFoundation;
public class HitTestExample : MonoBehaviour
{
public ARRaycastManager raycastManager;
private List<ARRaycastHit> hits = new();
void Update()
{
if (Input.touchCount < 1) return;
var touch = Input.GetTouch(0);
if (touch.phase != TouchPhase.Began) return;
// 从屏幕位置向世界投射
if (raycastManager.Raycast(touch.position, hits, TrackableType.PlaneWithinPolygon))
{
var hit = hits[0];
// hit.pose 是命中点的世界坐标 + 朝向
// hit.trackable 是命中的 Plane 或 Point
Debug.Log($"命中位置: {hit.pose.position}, 命中类型: {hit.trackable.GetType().Name}");
}
}
}
RaycastManager 可以配置要检测的 TrackableType 过滤条件——PlaneWithinPolygon 是只检测在已检测平面 polygon 范围内的命中。
射线不命中任何东西怎么办
以下场景 Hit Test 可能返回空列表:
- 用户点在纯纹理空白的墙壁上(没有 Plane 也没有 Point)
- 点在平面的 polygon 范围外(ARCore 已经检测到一个大桌子,但只完成了 3/4 的检测)
- Depth API 未启用且距离估计不准
处理方式:
- 降级到 Instant Placement:任何时候都能返回一个近似位置(但精度较低)
- 提示用户:Toast 显示"请对准一张桌子或墙壁"
- 用 Depth API 补充:如果设备支持 Depth,即使没有 Plane 也能返回基于深度图的精确命中
Depth API 增强 Hit Test
启用 Depth API 后,Hit Test 多了 DEPTH_POINT 类型的命中——这是基于深度图逐像素计算的,比只依赖特征点更精确:
// 启用 Depth API 后,Hit Test 返回的 DEPTH_POINT 类型
// 包含深度估计的精确命中位置
val depthHits = hitResults.filter { it.trackableType == HitTest.TrackableType.DEPTH_POINT }
Depth API Hit Test 的优势:
- 可以命中没有特征点的光滑表面(白色墙壁、玻璃桌面)
- 命中精度更高(直接读每个像素的深度值)
- 即使 ARCore 还没检测到 Plane,也能返回有效命中
但要注意 Depth API 对设备有要求(见故障排查页面)。