想识别桌面和地面?AR Engine 平面检测和 Mesh 识别详解

你有没有想过,AR 家具 APP 是怎么把虚拟沙发稳稳地放在地面上的?它怎么知道哪里是地面、哪里是桌面、哪里是墙壁?

答案就是平面检测。AR Engine 能从摄像头画面中自动识别出各种平面——水平的地面和桌面、垂直的墙壁,甚至天花板。识别出来之后,你就能在这些平面上放置虚拟物体了。

Mesh 识别比平面检测更进一步。它不是只给你一个平面,而是把整个环境的表面用三角形网格重建出来。有了网格数据,你就能做更精细的 AR 效果,比如让虚拟物体沿着不规则表面贴合。

平面检测整体流程

下面是平面检测的完整工作流程:

仅水平面

仅垂直面

全部

地面

桌面

墙壁

创建ARViewContext

配置planeFindingMode

选择检测模式

HORIZONTAL

VERTICAL

HORIZONTAL_AND_VERTICAL

初始化AR会话

后台识别平面

触发onAnchorAdd回调

获取ARPlane对象

读取平面类型和语义标签

判断平面用途

放置虚拟家具

放置虚拟物品

挂虚拟画框

平面检测怎么开启?

ARConfig 里设置 planeFindingMode 就行了:

import { arEngine, arViewController } from '@kit.AREngine';

let context: arViewController.ARViewContext = new arViewController.ARViewContext();
context.config = {
  type: arEngine.ARType.WORLD,
  planeFindingMode: arEngine.ARPlaneFindingMode.HORIZONTAL_AND_VERTICAL,
  poseMode: arEngine.ARPoseMode.GRAVITY_AND_HEADING,
  powerMode: arEngine.ARPowerMode.POWER_SAVING,
  depthMode: arEngine.ARDepthMode.AUTOMATIC
};

planeFindingMode: arEngine.ARPlaneFindingMode.HORIZONTAL_AND_VERTICAL 告诉 AR Engine:“水平面和竖直面我都要”。

ARPlaneFindingMode 枚举有 4 个值:

  • DISABLED(0):禁用平面检测。如果你不需要平面检测,可以关掉来节省性能。
  • HORIZONTAL(1):只检测水平面,比如地板和桌子。如果你只做家具摆放,用这个就够了。
  • VERTICAL(2):只检测竖直平面,比如墙壁。如果你想在墙上挂虚拟画框,用这个。
  • HORIZONTAL_AND_VERTICAL(3):同时检测水平面和竖直平面。这是默认值,最全面的选项。

注意,planeFindingMode 默认就是 HORIZONTAL_AND_VERTICAL,所以如果你不设置这个字段,平面检测也是开着的。但如果你想关闭它来节省性能,需要显式设成 DISABLED

配置好之后,初始化 AR 会话:

await context.init();

初始化完成后,AR Engine 就开始在后台默默识别平面了。随着用户移动手机扫描更多环境,检测到的平面会越来越多、越来越精确。

平面有哪几种类型?

AR Engine 检测到的平面有 4 种类型,用 ARPlaneType 枚举来表示:

  • FACING_HORIZONTAL_UPWARD(0):朝上的水平面,比如地面和桌面。这是最常见的平面类型,大部分虚拟物体都是放在这种平面上的。
  • FACING_HORIZONTAL_DOWNWARD(1):朝下的水平面,比如天花板。你可以在天花板上挂虚拟吊灯。
  • FACING_VERTICAL(2):垂直平面,比如墙壁。适合在墙上挂画、放电视。
  • FACING_INVALID(3):无效或不支持的平面类型。可能是环境变化、光线条件不好等原因导致的。

你拿到一个平面对象后,可以通过 planeType 属性来判断它是哪种类型的平面,然后根据类型来做不同的处理。比如地面可以放家具,墙壁可以挂画,天花板可以挂灯。

怎么获取检测到的平面?

平面检测的结果需要通过 ARFrame 来获取。有两种方式:

方式一:获取所有已检测到的平面:

import { arEngine } from '@kit.AREngine';

// arSession创建参考ARSession.getFrame接口示例代码
let trackables: Array<arEngine.ARTrackable> = arSession.getAllTrackables(arEngine.ARTrackableType.PLANE);
let plane = trackables[0] as arEngine.ARPlane;
plane.getPolygonXZ();

getAllTrackables(ARTrackableType.PLANE) 返回当前所有已检测到的平面对象数组。然后你可以通过类型转换得到 ARPlane 对象。

方式二:通过回调自动通知:

还记得前面说的 ARViewCallback 吗?当 AR Engine 检测到新平面时,会自动触发 onAnchorAdd 回调:

import { arEngine, arViewController } from '@kit.AREngine';
import { Node } from '@kit.ArkGraphics3D';

class ARViewCallbackImpl extends arViewController.ARViewCallback {

  onAnchorAdd(ctx: arViewController.ARViewContext, node: Node, anchor: arEngine.ARAnchor): void {
    console.info('onAnchorAdd');
    console.info(`add anchor id = ${String(anchor.id)}`);
    console.info(`add anchor translation = ${anchor.getPose().translation}`);
    console.info(`add node pose = ${node.position}`);
  }

  onAnchorUpdate(ctx: arViewController.ARViewContext, node: Node, anchor: arEngine.ARAnchor): void {
    console.info('onAnchorUpdate');
    console.info(`update anchor id = ${String(anchor.id)}`);
    console.info(`update anchor translation = ${anchor.getPose().translation}`);
    console.info(`update node pose = ${node.position}`);
  }

  async onFrameUpdate(ctx: arViewController.ARViewContext, sysBootTs: number): Promise<void> {
    let arSession: arEngine.ARSession | undefined = ctx.session;
    if (arSession) {
      let frame: arEngine.ARFrame = arSession.getFrame();
      if (!frame) {
        console.error('Failed to get arSession.frame, it is undefined or null');
      } else {
        console.info(`Succeeded in getting arSession.frame = ${frame.timestamp}`);
        await frame.release();
      }
    } else {
      console.error('Failed to get arSession, arSession is undefined');
    }
  }
}

let context: arViewController.ARViewContext = new arViewController.ARViewContext();
context.callback = new ARViewCallbackImpl();

这段代码继承了 arViewController.ARViewCallback,重写了三个回调方法:

onAnchorAdd:当检测到新平面时触发。AR Engine 会自动为新平面创建一个锚点(Anchor)和对应的场景节点(Node),然后通过这个回调通知你。你可以在回调里拿到锚点的位姿信息,用来在平面上放置虚拟物体。

onAnchorUpdate:当已有平面的范围扩大或位置更新时触发。平面检测是一个持续的过程——随着用户移动手机,AR Engine 会不断优化平面的大小和位置。每次更新都会触发这个回调。

onFrameUpdate:每一帧都会触发。你可以在这里做实时的渲染和逻辑处理。

ARPlane 对象包含哪些信息?

ARPlane 继承自 ARTrackable,包含了检测到的平面的详细信息:

属性 类型 说明
planeType ARPlaneType 平面类型(朝上水平面、朝下水平面、垂直面等)
extendX number 平面边界矩形沿 X 轴的长度(米)
extendZ number 平面边界矩形沿 Z 轴的长度(米)
label ARSemanticPlaneLabel 平面的语义类型(地面、墙面、桌子等)

extendXextendZ 描述了平面的大小。打个比方,如果 AR Engine 检测到一张桌子,extendX 可能是 1.2 米(桌子的宽度),extendZ 可能是 0.8 米(桌子的深度)。这两个值是边界矩形的尺寸,不是实际的多边形形状。

label 是平面的语义标签,用 ARSemanticPlaneLabel 枚举表示。这个比 planeType 更具体——planeType 只告诉你"这是个朝上的水平面",而 label 能告诉你"这是一张桌子"或者"这是地面"。语义标签有以下几种:

  • UNKNOWN(0):未知平面类型
  • WALL(1):墙面
  • FLOOR(2):地面
  • SEAT(3):座位,如椅子或凳子
  • TABLE(4):桌子

有了语义标签,你就能做更智能的交互。比如检测到是 TABLE,就在上面放虚拟杯子;检测到是 FLOOR,就在上面放虚拟家具;检测到是 WALL,就在上面挂虚拟画。

怎么获取平面的实际形状?

前面说的 extendXextendZ 只是边界矩形的尺寸,但实际的平面可能不是矩形的——比如一张 L 形的桌子,或者一个不规则的地面区域。

要获取平面的实际形状,需要用 getPolygonXZ() 方法:

import { arEngine } from '@kit.AREngine';

// arSession创建参考ARSession.getFrame接口示例代码
let trackables: Array<arEngine.ARTrackable> = arSession.getAllTrackables(arEngine.ARTrackableType.PLANE);
let plane = trackables[0] as arEngine.ARPlane;
plane.getPolygonXZ();

这个方法返回一个 ArrayBuffer,里面包含了平面 2D 顶点的坐标数组。这些坐标是在平面局部坐标系的 X-Z 平面上的。

使用的时候,需要把 ArrayBuffer 转换成 float32 类型的数组。每两个连续的值代表一个顶点的 (x, z) 坐标。把所有顶点按顺序连起来,就得到了平面的实际边界形状。

打个比方,如果检测到的桌面是一个矩形,那这个数组里可能有 8 个值(4 个顶点,每个顶点 2 个坐标):[x1, z1, x2, z2, x3, z3, x4, z4]。你可以用这些坐标来绘制平面的轮廓,或者判断某个点是否在平面范围内。

怎么判断一个点在不在平面内?

AR Engine 提供了两个方法来判断一个点是否在平面范围内:

isPoseInExtents(pose):判断一个位姿点是否在平面的边界矩形内。这个判断比较粗略,但速度快。

isPoseInPolygon(pose):判断一个位姿点是否在平面的实际多边形内。这个判断更精确,但计算量稍大。

import { arEngine } from '@kit.AREngine';

// arSession创建参考ARSession.getFrame接口示例代码
let trackables: Array<arEngine.ARTrackable> = arSession.getAllTrackables(arEngine.ARTrackableType.PLANE);
let pose: arEngine.ARPose = trackables[0].getPose();
let plane: arEngine.ARPlane = trackables[0] as arEngine.ARPlane;
plane.isPoseInExtents(pose);

这段代码先获取了第一个平面的位姿,然后判断这个位姿是否在平面的边界矩形内。注意 getPose()ARTrackable 基类的方法,返回的是可追踪对象的位姿信息。

import { arEngine } from '@kit.AREngine';

// arSession创建参考ARSession.getFrame接口示例代码
let trackables: Array<arEngine.ARTrackable> = arSession.getAllTrackables(arEngine.ARTrackableType.PLANE);
let pose: arEngine.ARPose = trackables[0].getPose();
let plane: arEngine.ARPlane = trackables[0] as arEngine.ARPlane;
plane.isPoseInPolygon(pose);

这个用法类似,只是换成了 isPoseInPolygon,判断的是是否在实际多边形内。

在实际使用中,你通常会在命中检测(hitTest)之后用这两个方法。比如用户点击屏幕,你通过 hitTest 找到一个交点,然后用 isPoseInPolygon 判断这个交点是否真的在某个平面内,如果在,就在这个位置放虚拟物体。

平面合并

有时候 AR Engine 会把两个相邻的平面合并成一个。比如你先检测到桌子的一部分,然后又检测到另一部分,AR Engine 会把它们合并成一个完整的桌面。

合并之后,原来的平面会被标记为"被合并"状态,新的合并后的平面成为"父平面"。你可以通过 getSubsumedBy() 方法来获取父平面:

import { arEngine } from '@kit.AREngine';

// arSession创建参考ARSession.getFrame接口示例代码
let trackables: Array<arEngine.ARTrackable> = arSession.getAllTrackables(arEngine.ARTrackableType.PLANE);
let plane: arEngine.ARPlane = trackables[0] as arEngine.ARPlane;
plane.getSubsumedBy();

如果这个平面没有被合并,调用这个方法可能会返回空值。如果被合并了,返回的就是合并后的父平面对象。

Mesh 识别怎么开启?

平面检测只能识别出平面,但现实世界有很多东西不是平面的——比如沙发、椅子、不规则的物体表面。这时候就需要 Mesh 识别了。

Mesh 识别会把环境的表面用三角形网格重建出来,给你更完整的 3D 几何信息。

ARConfig 里设置 meshMode 就行了:

import { arEngine, arViewController } from '@kit.AREngine';

let context: arViewController.ARViewContext = new arViewController.ARViewContext();
context.config = {
  type: arEngine.ARType.WORLD,
  meshMode: arEngine.ARMeshMode.ENABLE,
  depthMode: arEngine.ARDepthMode.AUTOMATIC
};

meshMode: arEngine.ARMeshMode.ENABLE 告诉 AR Engine:“开启网格识别”。

ARMeshMode 枚举有两个值:

  • DISABLED(0):网格模式关闭,AR Engine 不会处理或显示网格数据。这是默认值。
  • ENABLE(1):网格模式开启,AR Engine 会尝试处理和显示网格数据。

注意,Mesh 识别默认是关闭的,你需要显式设成 ENABLE 才能用。

怎么获取 Mesh 数据?

Mesh 数据通过 ARFrame.acquireSceneMesh() 来获取:

import { arEngine } from '@kit.AREngine';

// arSession创建参考ARSession.getFrame接口示例代码
let frame: arEngine.ARFrame = arSession.getFrame();
frame.acquireSceneMesh();

返回的是一个 ARSceneMesh 对象,它包含两个只读属性:

  • verticesSize:顶点数量,最小为 0,无上限
  • triangleIndicesSize:三角形索引数量,最小为 0,无上限

然后你可以通过三个方法来获取具体的数据:

获取顶点坐标:

import { arEngine } from '@kit.AREngine';

// arSession创建参考ARSession.getFrame接口示例代码
let frame: arEngine.ARFrame = arSession.getFrame();
let sceneMesh: arEngine.ARSceneMesh = frame.acquireSceneMesh();
sceneMesh.getVertices();

getVertices() 返回一个 ArrayBuffer,转换成 float32 后,每 3 个连续的值代表一个顶点的 (x, y, z) 坐标。

获取顶点法线:

import { arEngine } from '@kit.AREngine';

// arSession创建参考ARSession.getFrame接口示例代码
let frame: arEngine.ARFrame = arSession.getFrame();
let sceneMesh: arEngine.ARSceneMesh = frame.acquireSceneMesh();
sceneMesh.getVertexNormals();

getVertexNormals() 返回顶点法线数据。法线向量垂直于表面,用于光照计算。有了法线,你才能正确地渲染光照效果——比如阳光照在墙上,墙面朝向太阳的那一面应该更亮。

获取三角形索引:

import { arEngine } from '@kit.AREngine';

// arSession创建参考ARSession.getFrame接口示例代码
let frame: arEngine.ARFrame = arSession.getFrame();
let sceneMesh: arEngine.ARSceneMesh = frame.acquireSceneMesh();
sceneMesh.getTriangleIndices();

getTriangleIndices() 返回三角形索引数据,转换成 int32 后,每 3 个连续的索引组成一个三角形,指向 getVertices() 返回的顶点数组中的位置。

打个比方,如果索引数组是 [0, 1, 2, 1, 3, 2],那表示有两个三角形:第一个三角形由顶点 0、1、2 组成,第二个三角形由顶点 1、3、2 组成。

平面检测与Mesh识别对比

两种能力各有特点,可根据需求选择:

简单平面交互

复杂3D几何

两者都要

AR环境感知需求

需要什么能力?

平面检测

Mesh识别

同时开启

识别地面/桌面/墙壁

获取平面边界和语义

适合家具摆放/墙面装饰

三角形网格重建

获取顶点/法线/索引

适合遮挡/碰撞效果

消耗更多计算资源

建议使用省电模式

平面检测和 Mesh 识别能一起用吗?

当然可以!平面检测和 Mesh 识别是两个独立的能力,可以同时开启。

平面检测适合做"把虚拟物体放在桌面上"这类场景——它给你的是干净的平面信息,方便做交互。

Mesh 识别适合做遮挡、碰撞等效果——它给你的是完整的环境几何,更真实但数据量也更大。

如果你两个都需要,就同时开启:

import { arEngine, arViewController } from '@kit.AREngine';

let context: arViewController.ARViewContext = new arViewController.ARViewContext();
context.config = {
  type: arEngine.ARType.WORLD,
  planeFindingMode: arEngine.ARPlaneFindingMode.HORIZONTAL_AND_VERTICAL,
  meshMode: arEngine.ARMeshMode.ENABLE,
  depthMode: arEngine.ARDepthMode.AUTOMATIC
};

不过要注意,同时开启多个能力会消耗更多的计算资源,手机可能会更热、更耗电。如果你的应用只需要其中一个,就只开那个,用 POWER_SAVING 省电模式可以让手机轻松一点。

实际应用场景

平面检测和Mesh识别

AR家具摆放

AR墙面装饰

AR遮挡效果

AR测量

AR游戏

识别地面和桌面

在平面上放置虚拟家具

识别墙壁

挂虚拟画框/照片墙

Mesh提供完整几何

虚拟物体被真实物体遮挡

结合命中检测

测量两点距离

在平面上创建游戏场景

AR 家具摆放:平面检测识别出地面和桌面,用户在上面放虚拟家具。这是最经典的 AR 应用场景。

AR 墙面装饰:平面检测识别出墙壁,用户在墙上挂虚拟画框、照片墙或电视。

AR 遮挡效果:Mesh 识别提供环境的完整几何信息,让虚拟物体能被真实物体正确遮挡。比如虚拟小人走到沙发后面,会被沙发挡住。

AR 测量:结合平面检测和命中检测,可以测量现实世界中两个点之间的距离。

AR 游戏:在检测到的平面上做虚拟棋盘、虚拟弹球台等游戏场景。

平面检测和 Mesh 识别是 AR Engine 的基础环境感知能力。有了它们,虚拟物体才能"站"在现实世界的表面上,而不是飘在空中。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐