Android アプリで Depth を使用する

Depth API により、デバイスのカメラはシーン内の実際のオブジェクトのサイズと形状を認識できます。カメラを使用して奥行き画像(奥行きマップ)を作成し、アプリに AR のリアルなレイヤを追加します。奥行きのある画像から得られる情報を使用して、仮想オブジェクトを現実世界のオブジェクトの前または背後に正確に表示し、没入感のあるリアルなユーザー エクスペリエンスを実現できます。

奥行き情報は動きから計算され、利用可能であれば、Time of Flight(ToF)センサーなどのハードウェア深度センサーからの情報と組み合わせることができます。Depth API をサポートするため ToF センサーは必要ありません

前提条件

続行する前に、AR の基本的なコンセプトARCore セッションを構成する方法を理解しておいてください。

Depth 対応デバイスへのアクセスを制限する

アプリで Depth API のサポートが必要な場合(AR エクスペリエンスのコア部分が奥行きに依存している場合、またはアプリの奥行きを使用する部分にグレースフル フォールバックがないため)、Google Play ストアでのアプリの配信を Depth API をサポートするデバイスに制限できます。そのためには、AndroidManifest.xml に「AndroidManifest.xml の有効化」のガイドに加えて、AndroidManifest.xml に次の行を追加します。

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

Depth を有効にする

新しい ARCore セッションで、ユーザーのデバイスが Depth に対応しているかどうかを確認します。処理能力の制約により、すべての 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.
}

返された画像に含まれる RAW 画像バッファがフラグメント シェーダーに渡され、レンダリングされた各オブジェクトでオクルージョンされるオブジェクトごとに GPU で使用できるようになります。向きは OPENGL_NORMALIZED_DEVICE_COORDINATES で、Frame.transformCoordinates2d() を呼び出すことで TEXTURE_NORMALIZED に変更できます。オブジェクト シェーダー内で深度画像にアクセスできるようになると、オクルージョン処理のためにこれらの深度測定値に直接アクセスできます。

奥行きの値を理解する

観測された実世界のジオメトリ上の点 A と、奥行きのある画像内の同じ点を表す 2D 点 a から、a の Depth API によって指定される値は、主軸に投影された CA の長さと等しくなります。これは、カメラの原点 C を基準とする A の 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 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);

オクルージョンは、2 パス レンダリングまたはオブジェクトごとのフォワードパス レンダリングを使用してレンダリングできます。それぞれの方法の効率は、シーンの複雑さと、その他のアプリ固有の考慮事項によって決まります。

オブジェクトごとのフォワードパス レンダリング

オブジェクトごとのフォワードパス レンダリングでは、マテリアル シェーダー内のオブジェクトの各ピクセルのオクルージョンを決定します。見えないピクセルは、通常はアルファ ブレンディングによってクリップされ、ユーザーのデバイスでオクルージョンがシミュレートされます。

2 パス レンダリング

2 パス レンダリングでは、最初のパスがすべての仮想コンテンツを中間バッファにレンダリングします。2 つ目のパスでは、現実世界の奥行きと仮想シーンの奥行きの差に基づいて、仮想シーンが背景にブレンドされます。この方法では、オブジェクト固有のシェーダー作業を追加で必要なく、一般的に、フォワードパス方式よりも均一な結果が得られます。

奥行きのある画像から距離を抽出する

仮想オブジェクトのオクルージョンや深度データの可視化以外の目的で 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」で示されています。

深度対応のヒットテストを使用するには、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)

次のステップ

  • Raw Depth API を使用すると、より正確なセンシングが可能になります。
  • ARCore Depth Lab をご確認ください。深度データにアクセスするさまざまな方法を解説しています。