僅需6步,實現虛擬物體在現實世界的精準放置

HarmonyOS_SDK發表於2024-09-26

擴增實境(AR)技術作為一種將數字資訊和現實場景融合的創新技術,近年來得到了快速發展,並在多個應用領域展現出其獨特的魅力。比如在教育行業,老師可以透過虛擬現實場景生動直觀地幫助學生理解抽象概念;在旅遊行業,AR技術還能虛擬歷史文化場景、虛擬導航等,為遊客提供更加沉浸的互動體驗。

然而,對於應用來說,AR技術的開發使用絕非易事,這需要高昂的開發成本和專業的技術人才。基於此,HarmonyOS SDKAR引擎服務(AR Engine)為廣大應用開發者提供了先進的AR技術,解決了開發成本和技術門檻的難題。

image

在整合AR Engine能力後,開發者只需6個開發步驟,就可以實現將虛擬物體擺放於現實世界的平面上,實現虛擬和現實的融合,該功能可應用於虛擬傢俱放置、數字展廳布展等場景,為使用者提供虛實結合的新體驗。

業務流程

image

AR擺放實現的業務流程主要分為開啟應用、識別平面並展示和放置虛擬物體三個部分。

第一部分是使用者開啟應用,應用需要向使用者申請相機許可權。如果使用者未同意授權,則無法使用該功能。

第二部分中,AR Engine識別平面並展示。包括完成AR Engine初始化,更新ARFrame物件、獲取平面、繪製平面並顯示預覽畫面等步驟。

第三部分為放置虛擬物體。即使用者點選螢幕,透過碰撞檢測獲取現實環境中的興趣點,並在興趣點上建立錨點,最終實現在錨點位置繪製虛擬物體,並將虛擬物體顯示在預覽畫面上。

開發步驟

在實現AR物體擺放的具體開發步驟之前,開發者需要先建立Native C++工程,宣告ArkTs介面,並申請以下許可權授權。

image

1.建立UI介面

在做好準備工作後,需要建立一個UI介面,用於顯示相機預覽畫面,並定時觸發每一幀繪製。

import { Logger } from '../utils/Logger';
import arEngineDemo from 'libentry.so';
import { resourceManager } from '@kit.LocalizationKit';
import { display } from '@kit.ArkUI';

[@Entry](https://my.oschina.net/u/4127701)
[@Component](https://my.oschina.net/u/3907912)
struct ArWorld {
  private xcomponentId = 'ArWorld';
  private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All });
  private resMgr: resourceManager.ResourceManager = getContext(this).resourceManager;
  private interval: number = -1;
  private isUpdate: boolean = true;

  aboutToAppear() {
    Logger.debug('aboutToAppear ' + this.xcomponentId);
    arEngineDemo.init(this.resMgr);
    arEngineDemo.start(this.xcomponentId);
    display.on("foldStatusChange", (foldStatus: display.FoldStatus) => {
      Logger.info('foldStatusChange display on ' + foldStatus);
      if (foldStatus === display.FoldStatus.FOLD_STATUS_EXPANDED
        || foldStatus === display.FoldStatus.FOLD_STATUS_FOLDED) {
        arEngineDemo.stop(this.xcomponentId);
        arEngineDemo.init(this.resMgr);
        // 呼叫Native的start介面,建立ARSession。
        arEngineDemo.start(this.xcomponentId);
        arEngineDemo.show(this.xcomponentId);
      }
    })
  }

  aboutToDisappear() {
    Logger.debug('aboutToDisappear ' + this.xcomponentId);
    arEngineDemo.stop(this.xcomponentId);
  }

  onPageShow() {
   this.isUpdate = true;
    Logger.debug('onPageShow ' + this.xcomponentId);
    arEngineDemo.show(this.xcomponentId);
  }

  onPageHide() {
    Logger.debug('onPageHide ' + this.xcomponentId);
    this.isUpdate = false;
    arEngineDemo.hide(this.xcomponentId);
  }

  build() {
    Column() {
      XComponent({ id: this.xcomponentId, type: 'surface', libraryname: 'entry' })
        .onLoad(() => {
          Logger.debug('XComponent onLoad ' + this.xcomponentId);
          this.interval = setInterval(() => {
            if (this.isUpdate) {
              // 呼叫Native的update,更新AR Engine每一幀的計算結果
              arEngineDemo.update(this.xcomponentId);
            }
          }, 33); // 控制幀率為30fps(每33毫秒重新整理一幀)。
        })
        .width('100%');
        .height('100%');
        .onDestroy(() => {
          Logger.debug('XComponent onDestroy ' + this.xcomponentId);
          clearInterval(this.interval);
        })
        .backgroundColor(Color.White);
    }
    .justifyContent(FlexAlign.SpaceAround);
    .alignItems(HorizontalAlign.Center);
    .backgroundColor(Color.White);
    .borderRadius(24);
    .width('100%');
    .height('100%');
  }
}

2.引入AR Engine

建立完UI介面後,引入AR Engine標頭檔案,並編寫CMakeLists.txt。

#include "ar/ar_engine_core.h" 

find_library(
    # Sets the name of the path variable.
    arengine-lib
    # Specifies the name of the NDK library that
    # you want CMake to locate.
    libarengine_ndk.z.so
)

target_link_libraries(entry PUBLIC
    ${arengine-lib}
)

3.建立AR場景

首先,配置AR會話及預覽尺寸。

// 【可選】建立一個擁有合理預設配置的配置物件。
AREngine_ARConfig *arConfig = nullptr;
HMS_AREngine_ARConfig_Create(arSession, &arConfig);
// 【可選】配置AREngine_ARSession會話。
HMS_AREngine_ARSession_Configure(arSession, arConfig);
// 【可選】釋放指定的配置物件的記憶體空間。
HMS_AREngine_ARConfig_Destroy(arConfig);

// 建立一個新的AREngine_ARFrame物件。
HMS_AREngine_ARFrame_Create(arSession, &arFrame);
// 預覽區域的實際寬高,如使用xcomponent元件顯示,則該寬和高是xcomponent的寬和高,如果不一致,會導致顯示相機預覽出錯。
int32_t width = 1440;
int32_t height = 1080;
// 設定顯示的寬和高(以畫素為單位)。
HMS_AREngine_ARSession_SetDisplayGeometry(arSession, displayRotation, width, height);

透過openGL介面獲取紋理ID。

//透過openGL介面獲取紋理ID.
GLuint textureId = 0;
glGenTextures(1, &textureId);

設定openGL紋理,儲存相機預覽流資料。

// 設定可用於儲存相機預覽流資料的openGL紋理。
HMS_AREngine_ARSession_SetCameraGLTexture(arSession, textureId );

4.獲取平面

呼叫HMS_AREngine_ARSession_Update函式更新當前AREngine_ARFrame物件。

// 獲取幀資料AREngine_ARFrame。
HMS_AREngine_ARSession_Update(arSession, arFrame);

獲取相機的檢視矩陣和相機的投影矩陣,用於後續繪製。

// 根據AREngine_ARFrame物件可以獲取相機物件AREngine_ARCamera。
AREngine_ARCamera *arCamera = nullptr;
HMS_AREngine_ARFrame_AcquireCamera(arSession, arFrame, &arCamera);
// 獲取最新幀中相機的檢視矩陣。
HMS_AREngine_ARCamera_GetViewMatrix(arSession, arCamera, glm::value_ptr(*viewMat), 16);
// 獲取用於在相機影像上層渲染虛擬內容的投影矩陣,可用於相機座標系到裁剪座標系轉換。Near (0.1) Far (100)。
HMS_AREngine_ARCamera_GetProjectionMatrix(arSession, arCamera, {0.1f, 100.f}, glm::value_ptr(*projectionMat), 16);

呼叫HMS_AREngine_ARSession_GetAllTrackables函式獲取平面列表。

// 獲取當前檢測到的平面列表。
AREngine_ARTrackableList *planeList = nullptr;
// 建立一個可跟蹤物件列表。
HMS_AREngine_ARTrackableList_Create(arSession, &planeList);
// 獲取所有指定型別為ARENGINE_TRACKABLE_PLANE的可跟蹤對像集合。
AREngine_ARTrackableType planeTrackedType = ARENGINE_TRACKABLE_PLANE;
HMS_AREngine_ARSession_GetAllTrackables(arSession, planeTrackedType, planeList);
int32_t planeListSize = 0;
// 獲取此列表中的可跟蹤物件的數量。
HMS_AREngine_ARTrackableList_GetSize(arSession, planeList, &planeListSize);
mPlaneCount = planeListSize;
for (int i = 0; i < planeListSize; ++i) {
    AREngine_ARTrackable *arTrackable = nullptr;
    // 從可跟蹤列表中獲取指定index的物件。
    HMS_AREngine_ARTrackableList_AcquireItem(arSession, planeList, i, &arTrackable);
    AREngine_ARPlane *arPlane = reinterpret_cast<AREngine_ARPlane*>(arTrackable);
    // 獲取當前可跟蹤物件的跟蹤狀態。如果狀態為:ARENGINE_TRACKING_STATE_TRACKING(可跟蹤狀態)才進行繪製。
    AREngine_ARTrackingState outTrackingState;
    HMS_AREngine_ARTrackable_GetTrackingState(arSession, arTrackable, &outTrackingState);
    AREngine_ARPlane *subsumePlane = nullptr;
    // 獲取平面的父平面(一個平面被另一個平面合併時,會產生父平面),如果無父平面返回為NULL。
     HMS_AREngine_ARPlane_AcquireSubsumedBy(arSession, arPlane, &subsumePlane);
if (subsumePlane != nullptr) {
HMS_AREngine_ARTrackable_Release(reinterpret_cast<AREngine_ARTrackable*>(subsumePlane));
        // 如果當前平面有父平面,則當前平面不進行展示。否則會出現雙平面。
        continue;
    }
    // 跟蹤狀態為:ARENGINE_TRACKING_STATE_TRACKING時才進行繪製。
    if (AREngine_ARTrackingState::ARENGINE_TRACKING_STATE_TRACKING != outTrackingState) {
        continue;
    }
    // 進行平面繪製。
}
HMS_AREngine_ARTrackableList_Destroy(planeList);
planeList = nullptr;

呼叫HMS_AREngine_ARPlane_GetPolygon函式獲取平面的二維頂點座標陣列,用於繪製平面邊界。

// 獲取檢測到平面的二維頂點陣列大小。
int32_t polygonLength = 0;
HMS_AREngine_ARPlane_GetPolygonSize(session, plane, &polygonLength);

// 獲取檢測到平面的二維頂點陣列,格式為[x1,z1,x2,z2,...]。
const int32_t verticesSize = polygonLength / 2;
std::vector<glm::vec2> raw_vertices(verticesSize);
HMS_AREngine_ARPlane_GetPolygon(session, plane, glm::value_ptr(raw_vertices.front()), polygonLength);

// 區域性座標系頂點座標。
for (int32_t i = 0; i < verticesSize; ++i) {
    vertices.emplace_back(raw_vertices[i].x, raw_vertices[i].y, 0.75f);
}

將平面的二維頂點座標轉換到世界座標系,並繪製平面。

// 獲取從平面的區域性座標系到世界座標系轉換的位姿資訊。
AREngine_ARPose *scopedArPose = nullptr;
HMS_AREngine_ARPose_Create(session, nullptr, 0, &scopedArPose);
HMS_AREngine_ARPlane_GetCenterPose(session, plane, scopedArPose);

// 將位姿資料轉換成4X4的矩陣,outMatrixColMajor4x4為存放陣列,其中的資料按照列優先儲存.
// 該矩陣與區域性座標系的座標點做乘法,可以得到區域性座標系到世界座標系的轉換。
HMS_AREngine_ARPose_GetMatrix(session, scopedArPose, glm::value_ptr(modelMat), 16);
HMS_AREngine_ARPose_Destroy(scopedArPose);

// 構築繪製渲染平面所需的資料。
// 生成三角形。
for (int i = 1; i < verticesSize - 1; ++i) {
    triangles.push_back(0);
    triangles.push_back(i);
    triangles.push_back(i + 1);
}
// 生成平面包圍線。
for (int i = 0; i < verticesSize; ++i) {
    lines.push_back(i);
}

5.點選螢幕

使用者點選螢幕後,基於點選事件獲取螢幕座標。

// 新增標頭檔案:native_interface_xcomponent.h
#include <ace/xcomponent/native_interface_xcomponent.h>

float pixeLX= 0.0f;
float pixeLY= 0.0f;
int32_t ret = OH_NativeXComponent_GetTouchEvent(component, window, &mTouchEvent);

if (ret == OH_NATIVEXCOMPONENT_RESULT_SUCCESS) {
    if (mTouchEvent.type == OH_NATIVEXCOMPONENT_DOWN) {
        pixeLX= mTouchEvent.touchPoints[0].x;
    pixeLY= mTouchEvent.touchPoints[0].y;
    } else {
    return;
    }
}

呼叫HMS_AREngine_ARFrame_HitTest函式進行碰撞檢測,結果存放在碰撞檢測結果列表中。

// 建立一個命中檢測結果物件列表,arSession為建立AR場景步驟中建立的會話物件。
AREngine_ARHitResultList *hitResultList = nullptr;
HMS_AREngine_ARHitResultList_Create(arSession, &hitResultList);

// 獲取命中檢測結果物件列表,arFrame為建立AR場景步驟中建立的幀物件,pixeLX/pixeLY為螢幕點座標。
HMS_AREngine_ARFrame_HitTest(arSession, arFrame, pixeLX, pixeLY, hitResultList);

6.放置虛擬物體

呼叫HMS_AREngine_ARHitResultList_GetItem函式遍歷碰撞檢測結果列表,獲取命中的可跟蹤物件。

// 建立命中檢測結果物件。
AREngine_ARHitResult *arHit = nullptr;
HMS_AREngine_ARHitResult_Create(arSession, &arHit);

// 獲取第一個命中檢測結果物件。
HMS_AREngine_ARHitResultList_GetItem(arSession, hitResultList, 0, arHit);

// 獲取被命中的可追蹤物件。
AREngine_ARTrackable *arHitTrackable = nullptr;
HMS_AREngine_ARHitResult_AcquireTrackable(arSession, arHit, &arHitTrackable);

判斷碰撞結果是否存在於平面內部。

AREngine_ARTrackableType ar_trackable_type = ARENGINE_TRACKABLE_INVALID;
HMS_AREngine_ARTrackable_GetType(arSession, arTrackable, &ar_trackable_type)
if (ARENGINE_TRACKABLE_PLANE == ar_trackable_type) {
    AREngine_ARPose *arPose = nullptr;
    HMS_AREngine_ARPose_Create(arSession, nullptr, 0, &arPose);
    HMS_AREngine_ARHitResult_GetHitPose(arSession, arHit, arPose);
    // 判斷位姿是否位於平面的多邊形範圍內。0表示不在範圍內,非0表示在範圍內。
    HMS_AREngine_ARPlane_IsPoseInPolygon(mArSession, arPlane, arPose, &inPolygon)
    HMS_AREngine_ARPose_Destroy(arPose);
    if (!inPolygon) {
    // 不在平面內,就跳過當前平面。
    continue;
    }
}

在碰撞結果位置建立一個新的錨點,並基於此錨點放置虛擬模型。

// 在碰撞命中位置建立一個新的錨點。
AREngine_ARAnchor *anchor = nullptr;
HMS_AREngine_ARHitResult_AcquireNewAnchor(arSession, arHitResult, &anchor)

// 判斷錨點的可跟蹤狀態
AREngine_ARTrackingState trackingState = ARENGINE_TRACKING_STATE_STOPPED;
HMS_AREngine_ARAnchor_GetTrackingState(arSession, anchor, &trackingState)
if (trackingState != ARENGINE_TRACKING_STATE_TRACKING) {
    HMS_AREngine_ARAnchor_Release(anchor);
    return;
}

呼叫HMS_AREngine_ARAnchor_GetPose函式獲取錨點位姿,並基於該位姿繪製虛擬模型。

// 獲取錨點的位姿。
AREngine_ARPose *pose = nullptr;
HMS_AREngine_ARPose_Create(arSession, nullptr, 0, &pose);
HMS_AREngine_ARAnchor_GetPose(arSession, anchor, pose);
// 將位姿資料轉換成4X4的矩陣modelMat。
HMS_AREngine_ARPose_GetMatrix(arSession, pose, glm::value_ptr(modelMat), 16);
HMS_AREngine_ARPose_Destroy(pose);
// 繪製虛擬模型。

瞭解更多詳情>>

訪問AR Engine聯盟官網

獲取AR Engine開發指導文件

相關文章