ARCore 中文文档 下载 App

Oriented Points 检测斜面与有向特征点

问题场景:斜面 Hit Test 不准

当用户用 Hit Test 在一个倾斜表面(比如楼梯台阶、书架斜角、弯曲的物体表面)上点一下,ARCore 需要返回这个表面的正确朝向——而不是默认的水平朝上。

在 Depth API 启用前,Hit Test 命中单个特征点时返回的 Pose 通常假设表面是水平朝上的(y轴方向 = (0, -1, 0) 表示表面朝下,而不是按实际表面方向)。这导致虚拟物体放置在斜面上时,朝向会歪。

Oriented Points(有向点) 解决的就是这个问题:它让 ARCore 为场景中的单个特征点估计出该位置的表面法线方向,返回一个带正确朝向的 Pose。

Oriented Points 与普通 Trackable Point 的区别

对比项普通 PointOriented 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该区域还是散落的 PointPlane 的法线已经由更多特征点共同决定

如果周围特征点太少,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,Hit Test 会优先返回 Plane 而不是 Point;如果区域还没完成平面检测,就返回带方向的 Point。