在 Android NDK 应用中使用 Depth

Depth API 可帮助设备的相机了解场景中真实对象的大小和形状。它使用相机来创建深度图像或深度图,从而为您的应用增添一层 AR 真实感。您可以使用深度图像提供的信息让虚拟对象准确显示在真实对象的前方或后方,从而实现沉浸式真实用户体验。

深度信息通过运动计算,可与来自硬件深度传感器(例如飞行时间 (ToF) 传感器(如有))的信息结合使用。设备无需 ToF 传感器即可支持 Depth API

前提条件

确保您了解 AR 基础概念 以及如何在继续之前配置 ARCore 现场录像

仅限支持 Depth 的设备访问

如果您的应用需要 Depth API 支持(因为增强现实体验的核心部分依赖于深度,或者因为应用中使用深度的部分没有妥善的回退方式),您可以选择将应用在 Google Play 商店中的分发范围限制为支持 Depth API 的设备,方法是除了启用 ARCore 指南中所述的 AndroidManifest.xml 更改之外,还将以下代码行添加到 AndroidManifest.xml

<uses-feature android:name="com.google.ar.core.depth" />

启用深度

新的 ARCore 会话中,检查用户的设备是否支持深度。由于处理能力限制,并非所有与 ARCore 兼容的设备都支持 Depth API。为节省资源,ARCore 默认停用深度。启用深度模式,让应用使用 Depth API。

// Check whether the user's device supports the Depth API.
int32_t is_depth_supported = 0;
ArSession_isDepthModeSupported(ar_session, AR_DEPTH_MODE_AUTOMATIC,
                               &is_depth_supported);

// Configure the session for AR_DEPTH_MODE_AUTOMATIC.
ArConfig* ar_config = NULL;
ArConfig_create(ar_session, &ar_config);
if (is_depth_supported) {
  ArConfig_setDepthMode(ar_session, ar_config, AR_DEPTH_MODE_AUTOMATIC);
}
CHECK(ArSession_configure(ar_session, ar_config) == AR_SUCCESS);
ArConfig_destroy(ar_config);

获取深度图像

调用 ArFrame_acquireDepthImage16Bits() 以获取当前帧的深度图像。

// Retrieve the depth image for the current frame, if available.
ArImage* depth_image = NULL;
// If a depth image is available, use it here.
if (ArFrame_acquireDepthImage16Bits(ar_session, ar_frame, &depth_image) !=
    AR_SUCCESS) {
  // No depth image received for this frame.
  // This normally means that depth data is not available yet.
  // Depth data will not be available if there are no tracked
  // feature points. This can happen when there is no motion, or when the
  // camera loses its ability to track objects in the surrounding
  // environment.
  return;
}

返回的图片提供原始图片缓冲区,该缓冲区可传递给片段着色器,以便在 GPU 上用于遮挡每个要遮挡的渲染对象。它面向 AR_COORDINATES_2D_OPENGL_NORMALIZED_DEVICE_COORDINATES,可通过调用 ArFrame_transformCoordinates2d() 更改为 AR_COORDINATES_2D_TEXTURE_NORMALIZED。一旦可在对象着色器中访问深度图像,即可直接访问这些深度测量结果,以进行遮挡处理。

了解深度值

在观测到的真实几何图形上给定点 A 和 2D 点 a 深度图像中的相同点,深度值 a 处的 API 等于投影到主轴上的 CA 的长度。 这也可称为 A 相对于镜头的 z 坐标 来源:C。使用 Depth API 时,请务必了解 深度值不是光线 CA 本身的长度,而是投影

在着色器中使用深度

解析当前帧的深度信息

在 fragment 着色器中使用辅助函数 DepthGetMillimeters()DepthGetVisibility() 来获取当前屏幕位置的深度信息。然后,使用此信息有选择地遮盖所渲染对象的某些部分。

// Use DepthGetMillimeters() and DepthGetVisibility() to parse the depth image
// for a given pixel, and compare against the depth of the object to render.
float DepthGetMillimeters(in sampler2D depth_texture, in vec2 depth_uv) {
  // Depth is packed into the red and green components of its texture.
  // The texture is a normalized format, storing millimeters.
  vec3 packedDepthAndVisibility = texture2D(depth_texture, depth_uv).xyz;
  return dot(packedDepthAndVisibility.xy, vec2(255.0, 256.0 * 255.0));
}

// Return a value representing how visible or occluded a pixel is relative
// to the depth image. The range is 0.0 (not visible) to 1.0 (completely
// visible).
float DepthGetVisibility(in sampler2D depth_texture, in vec2 depth_uv,
                         in float asset_depth_mm) {
  float depth_mm = DepthGetMillimeters(depth_texture, depth_uv);

  // Instead of a hard Z-buffer test, allow the asset to fade into the
  // background along a 2 * kDepthTolerancePerMm * asset_depth_mm
  // range centered on the background depth.
  const float kDepthTolerancePerMm = 0.015f;
  float visibility_occlusion = clamp(0.5 * (depth_mm - asset_depth_mm) /
    (kDepthTolerancePerMm * asset_depth_mm) + 0.5, 0.0, 1.0);

 // Use visibility_depth_near to set the minimum depth value. If using
 // this value for occlusion, avoid setting it too close to zero. A depth value
 // of zero signifies that there is no depth data to be found.
  float visibility_depth_near = 1.0 - InverseLerp(
      depth_mm, /*min_depth_mm=*/150.0, /*max_depth_mm=*/200.0);

  // Use visibility_depth_far to set the maximum depth value. If the depth
  // value is too high (outside the range specified by visibility_depth_far),
  // the virtual object may get inaccurately occluded at further distances
  // due to too much noise.
  float visibility_depth_far = InverseLerp(
      depth_mm, /*min_depth_mm=*/7500.0, /*max_depth_mm=*/8000.0);

  const float kOcclusionAlpha = 0.0f;
  float visibility =
      max(max(visibility_occlusion, kOcclusionAlpha),
          max(visibility_depth_near, visibility_depth_far));

  return visibility;
}

遮挡虚拟对象

遮盖 fragment 着色器正文中的虚拟对象。根据对象的深度更新其 Alpha 通道。这样会渲染一个被遮挡的对象。

// Occlude virtual objects by updating the object’s alpha channel based on its depth.
const float kMetersToMillimeters = 1000.0;

float asset_depth_mm = v_ViewPosition.z * kMetersToMillimeters * -1.;

// Compute the texture coordinates to sample from the depth image.
vec2 depth_uvs = (u_DepthUvTransform * vec3(v_ScreenSpacePosition.xy, 1)).xy;

gl_FragColor.a *= DepthGetVisibility(u_DepthTexture, depth_uvs, asset_depth_mm);

您可以使用双通道渲染或按对象正向渲染来渲染遮挡。每种方法的效率取决于场景的复杂性和其他特定于应用的注意事项。

按对象正向渲染

按对象正向渲染可确定对象在其材料着色器中每个像素的遮挡。如果像素不可见,则会被剪裁(通常是通过 Alpha 混合),从而模拟用户设备上的遮挡。

两步渲染

使用双通道渲染时,第一通道会将所有虚拟内容渲染到中间缓冲区。第二步则根据现实世界深度与虚拟场景深度之间的差异,将虚拟场景与背景混合。此方法不需要额外的特定于对象的着色器工作,并且通常比前向传播方法生成的结果更一致。

转换相机图像和深度图像之间的坐标

使用 ArFrame_acquireCameraImage() 获取的图片与深度图片可能具有不同的宽高比。 在这种情况下,深度图像是相机图像的剪裁,并非所有相机图像都有相应的有效深度估算值。

如需获取 CPU 图片上坐标的深度图片坐标,请执行以下命令:

const float cpu_image_coordinates[] = {(float)cpu_coordinate_x,
                                 (float)cpu_coordinate_y};
float texture_coordinates[2];
ArFrame_transformCoordinates2d(
    ar_session, ar_frame, AR_COORDINATES_2D_IMAGE_PIXELS, 1,
    cpu_image_coordinates, AR_COORDINATES_2D_TEXTURE_NORMALIZED,
    texture_coordinates);
if (texture_coordinates[0] < 0 || texture_coordinates[1] < 0) {
  // There are no valid depth coordinates, because the coordinates in the CPU
  // image are in the cropped area of the depth image.
} else {
  int depth_image_width = 0;
  ArImage_getWidth(ar_session, depth_image, &depth_image_width);
  int depth_image_height = 0;
  ArImage_getHeight(ar_session, depth_image, &depth_image_height);

  int depth_coordinate_x = (int)(texture_coordinates[0] * depth_image_width);
  int depth_coordinate_y = (int)(texture_coordinates[1] * depth_image_height);
}

如需获取深度图像坐标的 CPU 图像坐标,请执行以下操作:

int depth_image_width = 0;
ArImage_getWidth(ar_session, depth_image, &depth_image_width);
int depth_image_height = 0;
ArImage_getHeight(ar_session, depth_image, &depth_image_height);

float texture_coordinates[] = {
    (float)depth_coordinate_x / (float)depth_image_width,
    (float)depth_coordinate_y / (float)depth_image_height};
float cpu_image_coordinates[2];
ArFrame_transformCoordinates2d(
    ar_session, ar_frame, AR_COORDINATES_2D_TEXTURE_NORMALIZED, 1,
    texture_coordinates, AR_COORDINATES_2D_IMAGE_PIXELS,
    cpu_image_coordinates);

int cpu_image_coordinate_x = (int)cpu_image_coordinates[0];
int cpu_image_coordinate_y = (int)cpu_image_coordinates[1];

深度命中测试

点击测试可让用户将对象放置在场景中的实际位置。以前,点击测试只能在检测到的平面上执行,将位置限制在较大的平坦表面,例如绿色 Android 显示的结果。深度点击测试利用平滑深度和原始深度信息来提供更准确的点击结果,即使在非平面和低纹理表面上也是如此。这与红色 Android 设备一起显示。

如需使用支持深度的点击测试,请调用 ArFrame_hitTest() 并检查返回列表中是否存在 AR_TRACKABLE_DEPTH_POINT

// Create a hit test using the Depth API.
ArHitResultList* hit_result_list = NULL;
ArHitResultList_create(ar_session, &hit_result_list);
ArFrame_hitTest(ar_session, ar_frame, hit_x, hit_y, hit_result_list);

int32_t hit_result_list_size = 0;
ArHitResultList_getSize(ar_session, hit_result_list, &hit_result_list_size);

ArHitResult* ar_hit_result = NULL;
for (int32_t i = 0; i < hit_result_list_size; ++i) {
  ArHitResult* ar_hit = NULL;
  ArHitResult_create(ar_session, &ar_hit);
  ArHitResultList_getItem(ar_session, hit_result_list, i, ar_hit);

  ArTrackable* ar_trackable = NULL;
  ArHitResult_acquireTrackable(ar_session, ar_hit, &ar_trackable);
  ArTrackableType ar_trackable_type = AR_TRACKABLE_NOT_VALID;
  ArTrackable_getType(ar_session, ar_trackable, &ar_trackable_type);
  // Creates an anchor if a plane or an oriented point was hit.
  if (AR_TRACKABLE_DEPTH_POINT == ar_trackable_type) {
    // Do something with the hit result.
  }
  ArTrackable_release(ar_trackable);
  ArHitResult_destroy(ar_hit);
}

ArHitResultList_destroy(hit_result_list);

后续步骤