在 Android 应用中使用“深度”功能

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

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

前提条件

确保您了解基本 AR 概念以及如何配置 ARCore 会话,然后再继续。

限制访问支持深度的设备

如果您的应用需要 Depth API 支持(无论是因为 AR 体验的核心部分依赖于深度,还是由于应用中使用深度的部分没有优雅回退机制),您可以选择限制在 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。

Java

Config config = session.getConfig();

// Check whether the user's device supports the Depth API.
boolean isDepthSupported = session.isDepthModeSupported(Config.DepthMode.AUTOMATIC);
if (isDepthSupported) {
  config.setDepthMode(Config.DepthMode.AUTOMATIC);
}
session.configure(config);

Kotlin

val config = session.config

// Check whether the user's device supports the Depth API.
val isDepthSupported = session.isDepthModeSupported(Config.DepthMode.AUTOMATIC)
if (isDepthSupported) {
  config.depthMode = Config.DepthMode.AUTOMATIC
}
session.configure(config)

获取深度图像

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

Java

// Retrieve the depth image for the current frame, if available.
Image depthImage = null;
try {
  depthImage = frame.acquireDepthImage16Bits();
  // Use the depth image here.
} catch (NotYetAvailableException e) {
  // This 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.
} finally {
  if (depthImage != null) {
    depthImage.close();
  }
}

Kotlin

// Retrieve the depth image for the current frame, if available.
try {
  frame.acquireDepthImage16Bits().use { depthImage ->
    // Use the depth image here.
  }
} catch (e: NotYetAvailableException) {
  // This 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.
}

返回的图像提供原始图像缓冲区,该缓冲区可以传递给片段着色器,以在 GPU 上使用要遮挡的每个渲染对象。它面向 OPENGL_NORMALIZED_DEVICE_COORDINATES,可通过调用 Frame.transformCoordinates2d() 更改为 TEXTURE_NORMALIZED。一旦可以在对象着色器中访问深度图像,就可以直接访问这些深度测量值来进行遮挡处理。

了解深度值

给定实测几何图形上的点 A 和代表深度图像中同一点的 2D 点 a,Depth API 在 a 处给出的值等于投影到主轴上的 CA 的长度。这也可称为 A 相对于相机原点 C 的 z 坐标。使用 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);

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

每个对象的前向传递渲染

每个对象的前向渲染决定了对象在其 Material 着色器中的遮挡效果。如果像素不可见,系统会对其进行裁剪(通常通过 alpha 混合),从而在用户设备上模拟遮挡。

双通道渲染

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

从深度图像中提取距离

如需将 Depth API 用于遮蔽虚拟对象或直观呈现深度数据之外的用途,请从深度图像中提取信息。

Java

/** Obtain the depth in millimeters for depthImage at coordinates (x, y). */
public int getMillimetersDepth(Image depthImage, int x, int y) {
  // The depth image has a single plane, which stores depth for each
  // pixel as 16-bit unsigned integers.
  Image.Plane plane = depthImage.getPlanes()[0];
  int byteIndex = x * plane.getPixelStride() + y * plane.getRowStride();
  ByteBuffer buffer = plane.getBuffer().order(ByteOrder.nativeOrder());
  return Short.toUnsignedInt(buffer.getShort(byteIndex));
}

Kotlin

/** Obtain the depth in millimeters for [depthImage] at coordinates ([x], [y]). */
fun getMillimetersDepth(depthImage: Image, x: Int, y: Int): UInt {
  // The depth image has a single plane, which stores depth for each
  // pixel as 16-bit unsigned integers.
  val plane = depthImage.planes[0]
  val byteIndex = x * plane.pixelStride + y * plane.rowStride
  val buffer = plane.buffer.order(ByteOrder.nativeOrder())
  val depthSample = buffer.getShort(byteIndex)
  return depthSample.toUInt()
}

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

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

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

Java

float[] cpuCoordinates = new float[] {cpuCoordinateX, cpuCoordinateY};
float[] textureCoordinates = new float[2];
frame.transformCoordinates2d(
    Coordinates2d.IMAGE_PIXELS,
    cpuCoordinates,
    Coordinates2d.TEXTURE_NORMALIZED,
    textureCoordinates);
if (textureCoordinates[0] < 0 || textureCoordinates[1] < 0) {
  // There are no valid depth coordinates, because the coordinates in the CPU image are in the
  // cropped area of the depth image.
  return null;
}
return new Pair<>(
    (int) (textureCoordinates[0] * depthImage.getWidth()),
    (int) (textureCoordinates[1] * depthImage.getHeight()));

Kotlin

val cpuCoordinates = floatArrayOf(cpuCoordinateX.toFloat(), cpuCoordinateY.toFloat())
val textureCoordinates = FloatArray(2)
frame.transformCoordinates2d(
  Coordinates2d.IMAGE_PIXELS,
  cpuCoordinates,
  Coordinates2d.TEXTURE_NORMALIZED,
  textureCoordinates,
)
if (textureCoordinates[0] < 0 || textureCoordinates[1] < 0) {
  // There are no valid depth coordinates, because the coordinates in the CPU image are in the
  // cropped area of the depth image.
  return null
}
return (textureCoordinates[0] * depthImage.width).toInt() to
  (textureCoordinates[1] * depthImage.height).toInt()

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

Java

float[] textureCoordinates =
    new float[] {
      (float) depthCoordinateX / (float) depthImage.getWidth(),
      (float) depthCoordinateY / (float) depthImage.getHeight()
    };
float[] cpuCoordinates = new float[2];
frame.transformCoordinates2d(
    Coordinates2d.TEXTURE_NORMALIZED,
    textureCoordinates,
    Coordinates2d.IMAGE_PIXELS,
    cpuCoordinates);
return new Pair<>((int) cpuCoordinates[0], (int) cpuCoordinates[1]);

Kotlin

val textureCoordinates =
  floatArrayOf(
    depthCoordinatesX.toFloat() / depthImage.width.toFloat(),
    depthCoordinatesY.toFloat() / depthImage.height.toFloat(),
  )
val cpuCoordinates = FloatArray(2)
frame.transformCoordinates2d(
  Coordinates2d.TEXTURE_NORMALIZED,
  textureCoordinates,
  Coordinates2d.IMAGE_PIXELS,
  cpuCoordinates,
)
return cpuCoordinates[0].toInt() to cpuCoordinates[1].toInt()

深度点击测试

借助命中测试,用户可以将对象放置在场景中的实际位置。以前,点击测试只能在检测到的平面上进行,因此测试位置仅限于大的平坦表面,例如绿色 Android 机器人显示的结果。深度点击测试会同时利用平滑和原始深度信息来提供更准确的命中结果,即使在非平面和低纹理表面上也是如此。这种情况与红色的 Android 一起显示。

如需使用支持深度的命中测试,请调用 hitTest() 并检查返回列表中是否有 DepthPoints

Java

// Create a hit test using the Depth API.
List<HitResult> hitResultList = frame.hitTest(tap);
for (HitResult hit : hitResultList) {
  Trackable trackable = hit.getTrackable();
  if (trackable instanceof Plane
      || trackable instanceof Point
      || trackable instanceof DepthPoint) {
    useHitResult(hit);
    break;
  }
}

Kotlin

// Create a hit test using the Depth API.
val hitResult =
  frame
    .hitTest(tap)
    .filter {
      val trackable = it.trackable
      trackable is Plane || trackable is Point || trackable is DepthPoint
    }
    .firstOrNull()
useHitResult(hitResult)

后续步骤