Android NDK 版 Google Cardboard 快速入门

本指南介绍了如何使用 Android 版 Cardboard SDK 打造您自己的虚拟实境 (VR) 体验。

您可以使用 Cardboard SDK 将智能手机转变为 VR 平台。智能手机可以通过立体呈现来显示 3D 场景,跟踪头部移动并做出响应,并通过检测用户何时按下观看器按钮来与应用进行交互。

首先,您将使用 HelloCardboard,这是一款演示游戏,演示了 Cardboard SDK 的核心功能。在游戏中,用户环顾虚拟世界来查找和收集物品。该指南介绍了如何执行以下操作:

  • 设置开发环境
  • 下载并构建演示版应用
  • 扫描 Cardboard 观看器的二维码以保存其参数
  • 跟踪用户头部活动
  • 通过为每只眼睛设置正确的视图投影矩阵渲染立体图像

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,请依次转到 Preferences > Appearance and Behavior

    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 文件夹(视图 > 工具窗口 > Gradle)中的 compose 选项。

  4. 在手机上运行 HelloCardboard 演示应用,方法是依次选择运行 > 运行...,然后选择 hellocardboard-android 目标。

扫描二维码

若要保存设备参数,请扫描 Cardboard 观看器上的二维码:

如果用户按“跳过”,且之前未保存任何参数,Cardboard 会保存 Google Cardboard v1(在 2014 年 Google I/O 大会上发布)参数。

试用演示版

在 HelloCardboard 中,你可以在 3D 空间中查找并收集测地球体。

要找到并收集球体,请执行以下操作:

  1. 朝任意方向移动头部,直至您看到一个浮动的形状。

  2. 直视球体。这会导致其更改颜色。

  3. 按下 Cardboard 眼镜按钮以“收集”球体。

配置设备

当用户点按齿轮图标切换 Cardboard 眼镜时,系统会调用 nativeSwitchViewer 方法。nativeSwitchViewer 会调用 CardboardQrCode_scanQrCodeAndSaveDeviceParams,这将打开用于扫描查看器的二维码的窗口。扫描二维码后,观看者的镜头失真及其他参数即会更新。

// 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;
}

镜头失真

每次 Cardboard 扫描新的二维码时,以下代码都会读取已保存的参数,并使用这些参数来创建镜头失真对象,从而对呈现的内容应用适当的镜头失真:

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_);