Google Cardboard 的 Android NDK 快速入門導覽課程

本指南將說明如何使用 Android 版的 Cardboard SDK 打造自己的虛擬實境 (VR) 體驗。

您可以使用 Cardboard SDK 將智慧型手機變成 VR 平台。智慧型手機可以偵測使用者按下檢視器按鈕的時機,來顯示 3D 場景以立體聲音轉譯、追蹤頭部動作並做出回應,以及與應用程式互動。

如要開始使用,您將使用 HelloCardboard 這個示範遊戲,示範 Cardboard SDK 的核心功能。在遊戲中,玩家可在虛擬世界中四處尋找並收集物件。內容說明如何:

  • 設定開發環境
  • 下載並建構試用版應用程式
  • 掃描 Cardboard 觀影盒的 QR code,即可儲存對應參數
  • 追蹤使用者的頭部動作
  • 為每隻眼睛設定正確的檢視畫面投影矩陣,即可算繪立體視覺圖像

HelloCardboard 使用 Android NDK。每種原生方法都具有以下特性:

  • 限定於 HelloCardboardApp 類別方法的專屬範圍,或
  • 建立或刪除該類別的例項

設定開發環境

硬體需求:

  • 搭載 Android 8.0「Oreo」(API 級別 26) 以上版本的 Android 裝置
  • Cardboard 觀影盒

軟體需求:

  • Android Studio 2022.1.1「Electric Eel」或以上版本
  • Android SDK 13.0「Tiramisu」(API 級別 33) 以上版本
  • 最新版本的 Android NDK 架構

    如要查看或更新已安裝的 SDK,請依序前往「偏好設定」 >「外觀和行為」

    Android Studio 中的「System Settings」 >「Android SDK」

下載並建構試用版應用程式

Cardboard SDK 是根據每個著色器的預先編譯 Vulkan 標頭檔案建構而成。請參閱這篇文章,瞭解從頭開始建構標頭檔案的步驟。

  1. 執行下列指令,從 GitHub 複製 Cardboard SDK 和 HelloCardboard 試用版應用程式:

    git clone https://github.com/googlevr/cardboard.git
  2. 在 Android Studio 中,選取「Open an existing Android Studio Project」,然後選取要複製 Cardboard SDK 和 HelloCardboard 試用版應用程式的目錄。

    您的程式碼會顯示在 Android Studio 的「Project」視窗中。

  3. 如要組裝 Cardboard SDK,請在「Gradle」分頁中,按兩下「cardboard/:sdk/Tasks/build」資料夾內的「join」選項 (依序點選「View」>「Tool Windows」>「Gradle」)。

  4. 如要在手機上執行 HelloCardboard 試用版應用程式,請依序選取「Run」 >「Run...」,然後選取 hellocardboard-android 目標。

掃描 QR code

如要儲存裝置參數,請掃描 Cardboard 觀影盒上的 QR code:

如果使用者按下「SKIP」,且先前沒有儲存任何參數,Cardboard 會儲存 Google Cardboard v1 (於 2014 年 Google I/O 大會推出) 的參數。

立即試用

在 HelloCardboard 中,你需要在 3D 空間中尋找並收集測地球體;

如何尋找及收集球體:

  1. 四處移動頭部,直到你看到漂浮的形狀。

  2. 直接看著球體。這會導致色彩改變。

  3. 按下 Cardboard 觀影盒按鈕即可「收集」球體。

設定裝置

使用者輕觸齒輪圖示切換 Cardboard 觀影盒時,系統會呼叫 nativeSwitchViewer 方法。nativeSwitchViewer 會呼叫 CardboardQrCode_scanQrCodeAndSaveDeviceParams,系統隨即會開啟視窗來掃描檢視器的 QR code。掃描 QR code 後,系統會更新檢視器的鏡頭變形和其他參數。

// Called by JNI method
void HelloCardboardApp::SwitchViewer() {
  CardboardQrCode_scanQrCodeAndSaveDeviceParams();
}

啟用 Android Studio x86 模擬器

如要建構 Android Studio x86 模擬器,請從 SDK範例build.gradle 檔案中移除下列程式碼:

abiFilters 'armeabi-v7a', 'arm64-v8a'

這樣即可啟用所有 ABI,並大幅增加產生的 .aar 檔案大小。詳情請參閱 Android ABI

頭部追蹤

建立頭部追蹤器

主要追蹤器會在 HelloCardboardApp 的建構函式中建立一次:

HelloCardboardApp::HelloCardboardApp(JavaVM* vm, jobject obj, jobject asset_mgr_obj) {
  Cardboard_initializeAndroid(vm, obj); // Must be called in constructor
  head_tracker_ = CardboardHeadTracker_create();
}

建立 VrActivity 時,系統會透過呼叫 nativeOnCreate 方法產生 HelloCardboardApp 類別的執行個體:

public void onCreate(Bundle savedInstance) {
  super.onCreate(savedInstance);
  nativeApp = nativeOnCreate(getAssets());
  //...
}

暫停及繼續抬頭追蹤器

如要暫停、繼續及刪除頭戴式追蹤器,必須分別呼叫 CardboardHeadTracker_pause(head_tracker_)CardboardHeadTracker_resume(head_tracker_)CardboardHeadTracker_destroy(head_tracker_)。在「HelloCardboard」應用程式中,我們會在 nativeOnPausenativeOnResumenativeOnDestroy 中呼叫這些函式:

// Code to pause head tracker in hello_cardboard_app.cc

void HelloCardboardApp::OnPause() { CardboardHeadTracker_pause(head_tracker_); }

// Call nativeOnPause in VrActivity
@Override
protected void onPause() {
  super.onPause();
  nativeOnPause(nativeApp);
  //...
}

// Code to resume head tracker in hello_cardboard_app.cc
void HelloCardboardApp::onResume() {
  CardboardHeadTracker_resume(head_tracker_);
  //...
}

// Call nativeOnResume in VrActivity
@Override
protected void onResume() {
  super.onResume();
  //...
  nativeOnResume(nativeApp);
}

// Code to destroy head tracker in hello_cardboard_app.cc
HelloCardboardApp::~HelloCardboardApp() {
  CardboardHeadTracker_destroy(head_tracker_);
  //...
}

// Call nativeOnDestroy in VrActivity
@Override
protected void onDestroy() {
  super.onDestroy();
  nativeOnDestroy(nativeApp);
  nativeApp = 0;
}

鏡頭變形

每次掃描新的 QR code 時,以下程式碼都會讀取已儲存的參數,並使用這些參數建立鏡頭變形物件,為轉譯的內容套用適當的鏡頭變形效果:

CardboardQrCode_getSavedDeviceParams(&buffer, &size);

CardboardLensDistortion_destroy(lens_distortion_);
lens_distortion_ = CardboardLensDistortion_create(
    buffer, size, screen_width_, screen_height_);

CardboardQrCode_destroy(buffer);

轉譯

在 Cardboard 中算繪內容的程序如下:

  • 建立紋理
  • 取得左右眼睛的視野及投影矩陣
  • 建立轉譯器並設定變形網格
  • 轉譯每個影格

建立紋理

所有內容都會繪製在紋理上,然後分為左右眼睛的多個部分。這些部分分別在 _leftEyeTexture_rightEyeTexture 中初始化。

void HelloCardboardApp::GlSetup() {
  LOGD("GL SETUP");

  if (framebuffer_ != 0) {
    GlTeardown();
  }

  // Create render texture.
  glGenTextures(1, &texture_);
  glBindTexture(GL_TEXTURE_2D, texture_);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, screen_width_, screen_height_, 0,
               GL_RGB, GL_UNSIGNED_BYTE, 0);

  left_eye_texture_description_.texture = texture_;
  left_eye_texture_description_.left_u = 0;
  left_eye_texture_description_.right_u = 0.5;
  left_eye_texture_description_.top_v = 1;
  left_eye_texture_description_.bottom_v = 0;

  right_eye_texture_description_.texture = texture_;
  right_eye_texture_description_.left_u = 0.5;
  right_eye_texture_description_.right_u = 1;
  right_eye_texture_description_.top_v = 1;
  right_eye_texture_description_.bottom_v = 0;

  //...
  CHECKGLERROR("GlSetup");
}

這些紋理會以參數的形式傳入 CardboardDistortionRenderer_renderEyeToDisplay

取得左右眼睛的視野及投影矩陣

首先,請擷取左右眼睛的眼部矩陣:

CardboardLensDistortion_getEyeFromHeadMatrix(
    lens_distortion_, kLeft, eye_matrices_[0]);
CardboardLensDistortion_getEyeFromHeadMatrix(
    lens_distortion_, kRight, eye_matrices_[1]);
CardboardLensDistortion_getProjectionMatrix(
    lens_distortion_, kLeft, kZNear, kZFar, projection_matrices_[0]);
CardboardLensDistortion_getProjectionMatrix(
    lens_distortion_, kRight, kZNear, kZFar, projection_matrices_[1]);

接下來,取得每個眼睛的變形網格,並傳遞至變形轉譯器:

CardboardLensDistortion_getDistortionMesh(lens_distortion_, kLeft, &left_mesh);
CardboardLensDistortion_getDistortionMesh(lens_distortion_, kRight, &right_mesh);

建立轉譯器並設定正確的變形網格

轉譯器只需初始化一次。建立轉譯器後,請根據 CardboardLensDistortion_getDistortionMesh 函式傳回的網格值,設定左右眼睛的新變形網格。

distortion_renderer_ = CardboardOpenGlEs2DistortionRenderer_create();
CardboardDistortionRenderer_setMesh(distortion_renderer_, &left_mesh, kLeft);
CardboardDistortionRenderer_setMesh(distortion_renderer_, &right_mesh, kRight);

轉譯內容

CardboardHeadTracker_getPose 擷取每個影格目前的頭向方向:

CardboardHeadTracker_getPose(head_tracker_, monotonic_time_nano, &out_position[0], &out_orientation[0]);

使用目前的頭部方向與檢視畫面和投影矩陣,為每種眼睛編寫檢視畫面投影矩陣,並將內容算繪至螢幕:

// Draw eyes views
for (int eye = 0; eye < 2; ++eye) {
  glViewport(eye == kLeft ? 0 : screen_width_ / 2, 0, screen_width_ / 2,
             screen_height_);

  Matrix4x4 eye_matrix = GetMatrixFromGlArray(eye_matrices_[eye]);
  Matrix4x4 eye_view = eye_matrix * head_view_;

  Matrix4x4 projection_matrix =
      GetMatrixFromGlArray(projection_matrices_[eye]);
  Matrix4x4 modelview_target = eye_view * model_target_;
  modelview_projection_target_ = projection_matrix * modelview_target;
  modelview_projection_room_ = projection_matrix * eye_view;

  // Draw room and target. Replace this to render your own content.
  DrawWorld();
}

使用 CardboardDistortionRenderer_renderEyeToDisplay 對內容套用變形校正,並將內容算繪至螢幕。

// Render
CardboardDistortionRenderer_renderEyeToDisplay(
    distortion_renderer_, /* target_display = */ 0, /* x = */ 0, /* y = */ 0,
    screen_width_, screen_height_, &left_eye_texture_description_,
    &right_eye_texture_description_);