在 Android 應用程式中使用深度

深度 API 可協助裝置的相機瞭解場景中實際物體的大小和形狀。這項功能會使用相機建立深度圖片或深度地圖,進而在應用程式中加入 AR 寫實程度層。您可以運用深度圖片提供的資訊,精準地將虛擬物件顯示在真實世界中或實際物件後方,為使用者帶來身歷其境的沉浸式體驗。

深度資訊是根據動作計算而得,可能會與硬體深度感應器 (例如飛行時間 (ToF) 感應器) 的資訊 (如果有的話) 合併計算。裝置不需要 ToF 感應器來支援 Depth API

必要條件

請務必先瞭解基本 AR 概念,以及如何設定 ARCore 工作階段,然後再繼續操作。

限制支援深度功能的裝置存取權

如果應用程式需要 Depth API 支援 (因為 AR 體驗的核心部分需要仰賴深度),或者因為使用深度的應用程式部分沒有優雅的備用方案,您可以選擇在 Google Play 商店中將應用程式限制為支援 DepCore API 的裝置,方法是將以下這行程式碼加到 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 代表深度圖片中的相同點,因此 a 的深度 API 提供的值等於投影至主體軸的 CA 長度。也稱為相對於相機來源 CA Z 座標。使用 Depth API 時,請務必瞭解深度值不是光線 CA 本身的長度,而是其「投影」

使用深度著色器

剖析目前影格的深度資訊

在片段著色器中使用輔助函式 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;
}

Occlude 虛擬物件

在片段著色器的主體中隱藏虛擬物件。根據物件的深度更新物件的 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 混合) 裁剪,在使用者裝置上模擬遮蔽。

兩段式轉譯

採用兩段轉譯機制時,第一個傳遞會將所有虛擬內容算繪到中介緩衝區中。第二通道會根據實際景深與虛擬場景深度之間的差異,將虛擬場景融入背景。這個方法不需要額外的物件專用著色器運作,而且通常能產生比正向傳遞方法更一致的結果。

擷取深度圖片的距離

如要將深度 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)

後續步驟