HarmonyOS AR 引擎:用 C++ 实现空间感知能力

什么是 AR 引擎

AR(增强现实)技术让你可以在真实世界上叠加虚拟物体。比如你用手机摄像头对着桌子,屏幕上可以看到一个虚拟的杯子放在桌子上,就像真的一样。

HarmonyOS 的 AR Engine 提供了以下核心能力:

  • 运动跟踪:实时获取设备在空间中的位置和姿态
  • 环境跟踪:识别周围的平面(比如桌面、地面)
  • 深度估计:获取场景中物体的距离信息
  • 图像跟踪:识别现实中的图片或物体
  • 命中检测:通过点击屏幕与虚拟物体交互

这篇我们用 C++ 来实现这些能力。

环境搭建

硬件要求

  • 设备类型:支持 AR Engine 的华为设备

软件要求

  • DevEco Studio 版本:DevEco Studio 6.0.0 Release 及以上
  • HarmonyOS SDK 版本:HarmonyOS 6.0.0 Release SDK 及以上

搭建步骤

  1. 安装 DevEco Studio:去华为开发者官网下载安装
  2. 配置开发环境:确保网络环境正常
  3. 设备调试:使用真机进行调试

项目结构

├── entry/src/main
│  ├── cpp
│  │  ├── ar_world                 // AR世界感知
│  │  │  ├── ar_world_app.h/cpp    // AR世界应用
│  │  │  ├── world_render_manager.h/cpp  // 渲染管理
│  │  │  └── ...
│  │  ├── ar_depth                 // 深度估计
│  │  │  ├── depth_render_manager.h/cpp
│  │  │  └── ...
│  │  ├── ar_image                 // 图像跟踪
│  │  │  ├── ar_image_app.h/cpp
│  │  │  └── ...
│  │  ├── common                   // 通用代码
│  │  │  ├── background_renderer.h/cpp  // 背景渲染
│  │  │  └── ...
│  │  └── napi_manager.h/cpp       // NAPI管理
│  ├── ets
│  │  ├── entryability
│  │  │  └── EntryAbility.ts
│  │  └── pages
│  │     └── index.ets

这个项目结构比较复杂,分为几个模块:

  • ar_world:世界感知,包括运动跟踪和平面检测
  • ar_depth:深度估计
  • ar_image:图像跟踪
  • common:通用代码,比如背景渲染

第一步:创建 AR 会话

使用 AR Engine 的第一步是创建一个 AR 会话。会话就像一个"连接",把你和 AR 引擎连接起来。

// 创建一个新的AREngine_ARSession会话。
CHECK(HMS_AREngine_ARSession_Create(nullptr, nullptr, &mArSession));

HMS_AREngine_ARSession_Create 创建一个新的 AR 会话。第一个参数是配置,第二个参数是回调,都传 nullptr 表示使用默认值。

// 配置AREngine_ARSession。
AREngine_ARConfig *arConfig = nullptr;
CHECK(HMS_AREngine_ARConfig_Create(mArSession, &arConfig));
// 设置预览画面尺寸。
CHECK(HMS_AREngine_ARConfig_SetPreviewSize(mArSession, arConfig, 1440, 1080));
// 设置画面更新模式。
CHECK(HMS_AREngine_ARConfig_SetUpdateMode(mArSession, arConfig, ARENGINE_UPDATE_MODE_LATEST));
CHECK(HMS_AREngine_ARSession_Configure(mArSession, arConfig));
HMS_AREngine_ARConfig_Destroy(arConfig);

配置 AR 会话:

  • SetPreviewSize:设置预览画面的尺寸
  • SetUpdateMode:设置画面更新模式,ARENGINE_UPDATE_MODE_LATEST 表示总是使用最新的画面
// 创建一个新的AREngine_ARFrame对象。
CHECK(HMS_AREngine_ARFrame_Create(mArSession, &mArFrame));
// 设置显示的高和宽(以像素为单位)。
CHECK(HMS_AREngine_ARSession_SetDisplayGeometry(mArSession, mDisplayRotation, mWidth, mHeight));

创建 AR 帧对象,并设置显示几何信息。

第二步:运动跟踪

运动跟踪是 AR 的基础。它让你知道设备在空间中的位置和朝向。

// 获取当前帧的相机参数。
AREngine_ARCamera *arCamera = nullptr;
CHECK(HMS_AREngine_ARFrame_AcquireCamera(arSession, arFrame, &arCamera));

先获取当前帧的相机对象。相机对象包含了设备的位置、朝向等信息。

// 获取最新帧中相机的视图矩阵。
CHECK(HMS_AREngine_ARCamera_GetViewMatrix(arSession, arCamera, glm::value_ptr(*viewMat), 16));

获取视图矩阵。视图矩阵描述了相机的位置和朝向,用它来确定虚拟物体应该画在哪里。

// 获取投影矩阵。
CHECK(HMS_AREngine_ARCamera_GetProjectionMatrix(arSession, arCamera, {0.1f, 100.f}, glm::value_ptr(*projectionMat), 16));

获取投影矩阵。投影矩阵用来把 3D 坐标转换成 2D 屏幕坐标。{0.1f, 100.f} 是近裁剪面和远裁剪面的距离。

// 获取摄像机的当前跟踪状态。
AREngine_ARTrackingState cameraTrackingState = ARENGINE_TRACKING_STATE_STOPPED;
CHECK(HMS_AREngine_ARCamera_GetTrackingState(arSession, arCamera, &cameraTrackingState));

获取跟踪状态。如果状态不是 ARENGINE_TRACKING_STATE_TRACKING,说明设备还没有成功跟踪,不应该渲染虚拟物体。

第三步:平面检测

平面检测可以识别现实中的平面,比如桌面、地面。有了平面信息,你就可以把虚拟物体"放"在这些平面上。

// 创建一个可跟踪对象列表。
AREngine_ARTrackableList *planeList = nullptr;
CHECK(HMS_AREngine_ARTrackableList_Create(arSession, &planeList));
// 获取所有指定类型的可跟踪对象集合。
AREngine_ARTrackableType planeTrackedType = ARENGINE_TRACKABLE_PLANE;
CHECK(HMS_AREngine_ARSession_GetAllTrackables(arSession, planeTrackedType, planeList));

获取所有检测到的平面。ARENGINE_TRACKABLE_PLANE 表示只获取平面类型的可跟踪对象。

// 获取此列表中的可跟踪对象的数量。
int32_t planeListSize = 0;
CHECK(HMS_AREngine_ARTrackableList_GetSize(arSession, planeList, &planeListSize));

获取平面数量。

for (int i = 0; i < planeListSize; ++i) {
    AREngine_ARTrackable *arTrackable = nullptr;
    // 从可跟踪列表中获取指定index的对象。
    CHECK(HMS_AREngine_ARTrackableList_AcquireItem(arSession, planeList, i, &arTrackable));
    AREngine_ARPlane *arPlane = reinterpret_cast<AREngine_ARPlane *>(arTrackable);
    
    // 获取当前可跟踪对象的跟踪状态。
    AREngine_ARTrackingState outTrackingState;
    CHECK(HMS_AREngine_ARTrackable_GetTrackingState(arSession, arTrackable, &outTrackingState));
    
    // 只有跟踪状态为TRACKING时才进行绘制。
    if (AREngine_ARTrackingState::ARENGINE_TRACKING_STATE_TRACKING != outTrackingState) {
        continue;
    }
    
    // 渲染平面
    mPlaneRenderer.Draw(projectionMat, viewMat, arSession, arPlane, color);
}

遍历所有平面,只渲染跟踪状态正常的平面。

第四步:深度估计

深度估计可以获取场景中每个像素离相机有多远。这个信息可以用于遮挡、碰撞检测等场景。

// 获取深度图像。
AREngine_ARImage *depthImage = nullptr;
AREngine_ARStatus status = HMS_AREngine_ARFrame_AcquireDepthImage(arSession, arFrame, &depthImage);

获取当前帧的深度图像。深度图像中每个像素的值表示该点离相机的距离。

if (depthImage != nullptr) {
    uint32_t depthWidth = 0;
    uint32_t depthHeight = 0;
    // 获取深度图像的宽度和高度。
    HMS_AREngine_ARImage_GetWidth(arSession, depthImage, &depthWidth);
    HMS_AREngine_ARImage_GetHeight(arSession, depthImage, &depthHeight);
    
    // 获取深度图像数据。
    uint8_t *depthData = nullptr;
    HMS_AREngine_ARImage_GetPlaneData(arSession, depthImage, 0, &depthData, &dataSize);
    
    // 获取中心点的深度值。
    uint16_t *data = reinterpret_cast<uint16_t *>(depthData);
    auto midDistance = (data[depthHeight * depthWidth / 2 + depthWidth / 2]) / 1000.f;
    
    // 释放深度图像。
    HMS_AREngine_ARImage_Release(depthImage);
}

获取深度图像的详细信息,包括宽度、高度和数据。这里还计算了画面中心点的深度值。

第五步:图像跟踪

图像跟踪可以识别现实中的图片。比如你用手机扫描一张海报,AR 应用可以在海报上叠加虚拟内容。

添加跟踪图像

首先需要告诉 AR Engine 你要跟踪哪些图片。

// 创建图像数据库。
AREngine_ARAugmentedImageDatabase *database = nullptr;
CHECK(HMS_AREngine_ARAugmentedImageDatabase_Create(arSession, &database));

// 添加图像到数据库。
AREngine_ARImage *arImage = nullptr;
// ... 加载图片数据
CHECK(HMS_AREngine_ARAugmentedImageDatabase_AddImage(arSession, database, "image_name", arImage, &imageIndex));

// 配置AR会话使用图像数据库。
AREngine_ARConfig *arConfig = nullptr;
CHECK(HMS_AREngine_ARConfig_Create(arSession, &arConfig));
CHECK(HMS_AREngine_ARConfig_SetAugmentedImageDatabase(arSession, arConfig, database));
CHECK(HMS_AREngine_ARSession_Configure(arSession, arConfig));

创建图像数据库,添加要跟踪的图片,然后配置 AR 会话使用这个数据库。

获取跟踪结果

// 获取所有增强图像可跟踪对象。
AREngine_ARTrackableList *imageTrackableList = nullptr;
CHECK(HMS_AREngine_ARTrackableList_Create(arSession, &imageTrackableList));
CHECK(HMS_AREngine_ARSession_GetAllTrackables(arSession, ARENGINE_TRACKABLE_AUGMENTED_IMAGE, imageTrackableList));

int32_t listSize = 0;
CHECK(HMS_AREngine_ARTrackableList_GetSize(arSession, imageTrackableList, &listSize));

for (int i = 0; i < listSize; ++i) {
    AREngine_ARTrackable *trackable = nullptr;
    CHECK(HMS_AREngine_ARTrackableList_AcquireItem(arSession, imageTrackableList, i, &trackable));
    AREngine_ARAugmentedImage *augmentedImage = reinterpret_cast<AREngine_ARAugmentedImage *>(trackable);
    
    // 获取图像的跟踪状态。
    AREngine_ARTrackingState trackingState;
    CHECK(HMS_AREngine_ARTrackable_GetTrackingState(arSession, trackable, &trackingState));
    
    if (trackingState == ARENGINE_TRACKING_STATE_TRACKING) {
        // 获取图像的中心位姿。
        AREngine_ARPose *pose = nullptr;
        CHECK(HMS_AREngine_ARPose_Create(arSession, nullptr, &pose));
        CHECK(HMS_AREngine_ARAugmentedImage_GetCenterPose(arSession, augmentedImage, pose));
        
        // 获取图像的四个角点坐标。
        float extentX = 0.0f;
        float extentZ = 0.0f;
        CHECK(HMS_AREngine_ARAugmentedImage_GetExtentX(arSession, augmentedImage, &extentX));
        CHECK(HMS_AREngine_ARAugmentedImage_GetExtentZ(arSession, augmentedImage, &extentZ));
    }
}

遍历所有跟踪到的图像,获取它们的位置和大小。

第六步:命中检测

命中检测让你可以通过点击屏幕与虚拟物体交互。

// 获取屏幕点击坐标。
float pixeLX = mTouchEvent.touchPoints[0].x;
float pixeLY = mTouchEvent.touchPoints[0].y;

// 获取命中检测结果对象列表。
AREngine_ARHitResultList *hitResultList = nullptr;
CHECK(HMS_AREngine_ARHitResultList_Create(mArSession, &hitResultList));

// 射线与系统跟踪的平面产生交点。
CHECK(HMS_AREngine_ARFrame_HitTest(mArSession, mArFrame, pixeLX, pixeLY, hitResultList));

HMS_AREngine_ARFrame_HitTest 从屏幕坐标发射一条射线,检测它与哪些平面或物体相交。结果按照距离从近到远排序。

// 获取命中检测结果数量。
int32_t hitResultListSize = 0;
CHECK(HMS_AREngine_ARHitResultList_GetSize(mArSession, hitResultList, &hitResultListSize));

for (int32_t i = 0; i < hitResultListSize; ++i) {
    AREngine_ARHitResult *hitResult = nullptr;
    CHECK(HMS_AREngine_ARHitResultList_AcquireItem(mArSession, hitResultList, i, &hitResult));
    
    // 获取命中点的位姿。
    AREngine_ARPose *pose = nullptr;
    CHECK(HMS_AREngine_ARPose_Create(mArSession, nullptr, &pose));
    CHECK(HMS_AREngine_ARHitResult_GetHitPose(mArSession, hitResult, pose));
    
    // 获取命中的可跟踪对象。
    AREngine_ARTrackable *trackable = nullptr;
    CHECK(HMS_AREngine_ARHitResult_AcquireTrackable(mArSession, hitResult, &trackable));
    
    // 如果命中的是平面,可以在该位置放置虚拟物体。
    AREngine_ARTrackableType trackableType;
    CHECK(HMS_AREngine_ARTrackable_GetType(mArSession, trackable, &trackableType));
    if (trackableType == ARENGINE_TRACKABLE_PLANE) {
        // 在命中点放置虚拟物体
    }
}

遍历所有命中结果,找到第一个平面命中点,在那里放置虚拟物体。

第七步:渲染虚拟物体

获取到平面和命中点信息后,就可以在上面渲染虚拟物体了。

// 渲染物体。
void WorldRenderManager::RenderObject(AREngine_ARSession *arSession,
                                       const glm::mat4 &viewMat,
                                       const glm::mat4 &projectionMat,
                                       const std::vector<ColoredAnchor> &anchors) {
    for (const auto &coloredAnchor : anchors) {
        AREngine_ARAnchor *anchor = coloredAnchor.anchor;
        AREngine_ARTrackingState trackingState;
        CHECK(HMS_AREngine_ARTrackable_GetTrackingState(arSession,
            reinterpret_cast<AREngine_ARTrackable *>(anchor), &trackingState));
        
        if (trackingState == ARENGINE_TRACKING_STATE_TRACKING) {
            // 获取锚点的位姿矩阵。
            glm::mat4 modelMat;
            CHECK(HMS_AREngine_ARAnchor_GetPose(arSession, anchor, modelMat));
            
            // 使用位姿矩阵渲染虚拟物体。
            mVirtualObject.Draw(projectionMat, viewMat, modelMat, coloredAnchor.color);
        }
    }
}

对于每个锚点(虚拟物体的放置点),获取它的位姿矩阵,然后用这个矩阵来渲染虚拟物体。这样虚拟物体就会"固定"在现实世界中的某个位置。

NAPI 封装

为了让 ArkTS 层能调用 C++ 的 AR 功能,需要做 NAPI 封装。

// 注册NAPI接口。
static napi_value Init(napi_env env, napi_value exports) {
    napi_property_descriptor desc[] = {
        {"init", nullptr, Init, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"start", nullptr, Start, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"stop", nullptr, Stop, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"show", nullptr, Show, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"hide", nullptr, Hide, nullptr, nullptr, nullptr, napi_default, nullptr},
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}

封装了 initstartstopshowhide 等接口,供 ArkTS 层调用。

ArkTS 层调用

在 ArkTS 层,通过 XComponent 来显示 AR 画面,并调用 NAPI 接口控制 AR 服务。

XComponent({ id: this.xComponentId, type: XComponentType.SURFACE, libraryname: 'entry' })
  .width('100%')
  .height('100%')
  .onAppear(() => {
    arEngineDemo.init(this.resMgr);
    let config: Int32Array = new Int32Array([1, this.rotation]);
    arEngineDemo.start(this.xComponentId, config);
  })
  .onWillDisappear(() => {
    arEngineDemo.stop(this.xComponentId);
  })
  .onShown(() => {
    arEngineDemo.show(this.xComponentId);
  })
  .onHidden(() => {
    arEngineDemo.hide(this.xComponentId);
  })

XComponent 是 HarmonyOS 提供的原生渲染组件,AR 画面通过它来显示。在不同的生命周期调用对应的 AR 接口。

适用场景

AR Engine 适合以下场景:

  • AR 游戏:在现实世界中放置虚拟游戏角色
  • AR 购物:在房间里预览家具摆放效果
  • AR 导航:在现实场景中叠加导航箭头
  • AR 教育:在现实物体上叠加说明信息
  • AR 测量:测量现实物体的尺寸

注意事项

  1. 设备支持:不是所有设备都支持 AR Engine,要先确认设备兼容性
  2. 光线条件:AR 跟踪需要足够的光线,太暗的环境会影响效果
  3. 特征点:环境需要有足够的特征点(纹理、边缘),纯色墙面很难跟踪
  4. 性能:AR 应用比较耗资源,要注意性能优化
  5. 权限:需要相机权限才能使用 AR 功能
  6. 初始化时间:AR 服务初始化需要时间,要给用户适当的提示

核心流程图

AR Engine C++ 版本的整体开发流程:

创建 AR 会话

配置 AR 会话参数

创建 AR 帧对象

获取相机参数

运动跟踪: 获取视图矩阵和投影矩阵

平面检测: 遍历所有平面

深度估计: 获取深度图像

图像跟踪: 识别现实图片

命中检测: 屏幕点击交互

渲染虚拟物体到锚点位置

命中检测与虚拟物体放置的流程:

用户点击屏幕

获取屏幕坐标

发射射线检测交点

获取命中结果列表

是否有命中结果?

无操作

遍历命中结果

命中的是平面?

获取命中点位姿

在该位置放置虚拟物体

总结

AR Engine 是一个功能强大的 AR 开发框架,核心流程:

  1. 创建 AR 会话并配置
  2. 实现运动跟踪,获取相机位姿
  3. 实现平面检测,识别现实中的平面
  4. 实现深度估计,获取场景深度信息
  5. 实现图像跟踪,识别现实中的图片
  6. 实现命中检测,支持屏幕点击交互
  7. 在检测到的平面上渲染虚拟物体

掌握了这些,你就能在 HarmonyOS 应用中实现各种 AR 功能,打造沉浸式的增强现实体验。

Logo

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

更多推荐