Oriented Points 检测斜面与有向特征点
问题场景:斜面 Hit Test 不准
当用户用 Hit Test 在一个倾斜表面(比如楼梯台阶、书架斜角、弯曲的物体表面)上点一下,ARCore 需要返回这个表面的正确朝向——而不是默认的水平朝上。
在 Depth API 启用前,Hit Test 命中单个特征点时返回的 Pose 通常假设表面是水平朝上的(y轴方向 = (0, -1, 0) 表示表面朝下,而不是按实际表面方向)。这导致虚拟物体放置在斜面上时,朝向会歪。
Oriented Points(有向点) 解决的就是这个问题:它让 ARCore 为场景中的单个特征点估计出该位置的表面法线方向,返回一个带正确朝向的 Pose。
Oriented Points 与普通 Trackable Point 的区别
| 对比项 | 普通 Point | Oriented Point |
|---|---|---|
| 法线方向 | 固定(通常水平朝下) | 由周围特征点聚类估计 |
| Hit Test 返回值 | Pose 的旋转不对 | Pose 的旋转匹配表面实际方向 |
| 适用场景 | Hit Test 命中点位置即可 | 需要精确表面朝向的交互 |
启用 Oriented Points
Oriented Points 是 ARCore 的默认行为之一,不需要显式启用。当 Hit Test 命中一个有足够邻近特征点的 Point 时,ARCore 会自动估计该位置的表面法线。
但你可以在 Session 配置中显式开启(某些旧版本需要):
session.configure(
Session.Config(context).apply {
// 确保启用有向点检测(某些版本默认关闭)
setPlaneFindingMode(Session.Config.PlaneFindingMode.HORIZONTAL_AND_VERTICAL)
// 同时检测水平和垂直平面,有利于有向点法线估计
}
)
如何利用 Oriented Points
Hit Test 返回的正确旋转
// 假设用户点在了一个斜面上
val hitResult = frame.hitTest(tapX, tapY).first()
// hitResult.pose 的旋转现在包含了斜面的正确朝向
val surfaceNormal = hitResult.pose.rotation.transformVector(Vector3(0f, 1f, 0f))
// surfaceNormal 就是该点表面的法线方向
// 把虚拟物体的底部对齐这个表面
// 而不是硬设为水平
virtualObject.transform.rotation = hitResult.pose.rotation
virtualObject.transform.position = hitResult.pose.translation
直接查询 Oriented Points
// 从 Session 中获取所有被跟踪的有向点
val orientedPoints = session.getAllTrackables<Point>()
orientedPoints.forEach { point ->
if (point.trackingState == TrackingState.TRACKING) {
// point.pose.translation = 位置
// point.pose.rotation = 表面法线方向(Z 轴方向 = 法线)
val normal = point.pose.rotation.transformVector(Vector3(0f, 0f, 1f))
// normal 指向表面法线方向
}
}
估计质量的影响因素
ARCore 估计有向点表面法线的质量依赖于周围特征点的密度和分布:
| 条件 | 好 | 坏 | 原因 |
|---|---|---|---|
| 邻近特征点 | 密集且分布在不同方向 | 稀疏或集中在同一侧 | 法线估计需要多方向参考 |
| 纹理丰富度 | 有图案的斜面(木纹、贴纸) | 光滑平面(玻璃、金属) | 没有特征点就无法估计 |
| 平面已检测 | 该区域已被识别为 Plane | 该区域还是散落的 Point | Plane 的法线已经由更多特征点共同决定 |
如果周围特征点太少,ARCore 会退回到"默认水平"的假设——此时有向点的 Pose 旋转不会反映斜面方向。
与 Depth API 结合
Depth API 启用后,Hit Test 返回的 DEPTH_POINT 类型命中自带深度值,ARCore 可以更精确地估计表面方向:
// Depth 数据的法线估计更准确
val depthHits = frame.hitTest(tapX, tapY)
val bestHit = depthHits.firstOrNull {
it.trackable == HitTest.TrackableType.DEPTH_POINT
}
if (bestHit != null) {
// bestHit.pose 的旋转包含了从深度图计算的精确表面法线
// 质量通常优于纯特征点估计
}
Unity 中的 Surface Placement
Unity AR Foundation 中,Raycast 返回的 ARRaycastHit.pose 已经包含了正确的表面朝向:
var hit = raycastManager.Raycast(touch.position, hits, TrackableType.FeaturePoint)
if (hit)
{
// hit.pose.rotation 已经是正确的表面朝向
// Oriented Points 是 Unity 底层自动处理的
var rotation = hit.pose.rotation;
var position = hit.pose.position;
}
实际场景:家具摆放对齐
一个很实际的场景:用户想把虚拟画挂在倾斜的墙面上(比如斜天花板)。
// 传统做法:假设墙面是垂直的,手动计算旋转
val wallNormal = Vector3(0f, 0f, 1f) // 默认法线
// 这种做法在斜天花板上会让画歪
// 用 Oriented Points:
val hitResult = frame.hitTest(tapX, tapY).first()
// hitResult.pose 已经包含了精确的墙面法线
// 直接用它,画自然就能对齐任何角度的表面
virtualPicture.transform.pose = hitResult.pose
与平面检测的关系
有向点和平面检测是互补关系:
- 平面检测在特征点足够密集且呈现共面趋势时生效,返回大的 Plane 对象
- 有向点在单个或少量特征点但有方向信息时生效,返回小的带方向的 Point
两者不会冲突——同一个物理区域如果被检测为 Plane,Hit Test 会优先返回 Plane 而不是 Point;如果区域还没完成平面检测,就返回带方向的 Point。