Menggunakan Depth di aplikasi Android Anda

Depth API membantu kamera perangkat memahami ukuran dan bentuk objek nyata dalam adegan. Fitur ini menggunakan kamera untuk membuat gambar kedalaman, atau peta kedalaman, sehingga menambahkan lapisan realisme AR ke aplikasi Anda. Anda dapat menggunakan informasi yang diberikan oleh gambar kedalaman untuk membuat objek virtual secara akurat muncul di depan atau di belakang objek dunia nyata, yang memungkinkan pengalaman pengguna yang imersif dan realistis.

Informasi kedalaman dihitung dari gerakan dan dapat digabungkan dengan informasi dari sensor kedalaman hardware, seperti sensor waktu terbang (ToF), jika tersedia. Perangkat tidak memerlukan sensor ToF untuk mendukung Depth API.

Prasyarat

Pastikan Anda memahami konsep AR dasar dan cara mengonfigurasi sesi ARCore sebelum melanjutkan.

Membatasi akses ke perangkat yang didukung Depth

Jika aplikasi Anda memerlukan dukungan Depth API, baik karena bagian inti dari pengalaman AR bergantung pada kedalaman, atau karena tidak ada penggantian halus untuk bagian aplikasi yang menggunakan kedalaman, Anda dapat memilih untuk membatasi distribusi aplikasi Anda di Google Play Store ke perangkat yang mendukung Depth API dengan menambahkan baris berikut ke AndroidManifest.xml, selain perubahan AndroidManifest.xml yang dijelaskan di Aktifkan AR:

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

Aktifkan Kedalaman

Di sesi ARCore baru, periksa apakah perangkat pengguna mendukung Depth. Tidak semua perangkat yang kompatibel dengan ARCore mendukung Depth API karena keterbatasan daya pemrosesan. Untuk menghemat resource, depth dinonaktifkan secara default di ARCore. Aktifkan mode kedalaman agar aplikasi Anda menggunakan 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)

Mendapatkan gambar kedalaman

Panggil Frame.acquireDepthImage16Bits() guna mendapatkan gambar kedalaman untuk bingkai saat ini.

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.
}

Gambar yang ditampilkan menyediakan buffering gambar mentah, yang dapat diteruskan ke shader fragmen untuk digunakan di GPU bagi setiap objek yang dirender yang akan dihalangi. Kode ini berorientasi dalam OPENGL_NORMALIZED_DEVICE_COORDINATES dan dapat diubah menjadi TEXTURE_NORMALIZED dengan memanggil Frame.transformCoordinates2d(). Setelah gambar kedalaman dapat diakses dalam shader objek, pengukuran kedalaman ini dapat diakses secara langsung untuk penanganan oklusi.

Memahami nilai kedalaman

Titik A yang ditentukan pada geometri dunia nyata yang diamati dan titik 2D a mewakili titik yang sama pada gambar kedalaman, nilai yang diberikan oleh Depth API pada a sama dengan panjang CA yang diproyeksikan ke sumbu utama. Ini juga dapat disebut sebagai koordinat z A relatif terhadap C asal kamera. Saat menggunakan Depth API, pahami bahwa nilai kedalaman bukanlah panjang CA sinar itu sendiri, tetapi proyeksi sinarnya.

Menggunakan kedalaman dalam shader

Mengurai informasi kedalaman untuk frame saat ini

Gunakan fungsi bantuan DepthGetMillimeters() dan DepthGetVisibility() dalam shader fragmen untuk mengakses informasi kedalaman posisi layar saat ini. Kemudian gunakan informasi ini untuk secara selektif menutupi bagian objek yang dirender.

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

Menutup objek virtual

Menutup objek virtual dalam isi shader fragmen. Mengupdate saluran alfa objek berdasarkan kedalamannya. Tindakan ini akan merender objek yang tersumbat.

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

Anda dapat merender oklusi menggunakan rendering dua jalur atau rendering maju per objek. Efisiensi setiap pendekatan bergantung pada kompleksitas adegan dan pertimbangan khusus aplikasi lainnya.

Rendering maju per objek

Rendering maju per objek menentukan oklusi setiap piksel objek dalam shader materialnya. Jika piksel tidak terlihat, piksel akan dipotong, biasanya melalui pencampuran alfa, sehingga menyimulasikan oklusi pada perangkat pengguna.

Rendering dua tahap

Dengan rendering dua-pass, pass pertama merender semua konten virtual ke buffer perantara. Tahap kedua memadukan adegan virtual dengan latar belakang berdasarkan perbedaan antara kedalaman dunia nyata dengan kedalaman adegan virtual. Pendekatan ini tidak memerlukan pekerjaan shader khusus objek tambahan dan umumnya menghasilkan hasil yang tampak lebih seragam daripada metode forward-pass.

Mengekstrak jarak dari gambar kedalaman

Agar dapat menggunakan Depth API untuk tujuan selain menutupi objek virtual atau memvisualisasikan data kedalaman, ekstrak informasi dari gambar kedalaman.

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()
}

Mengonversi koordinat antara gambar kamera dan gambar kedalaman

Gambar yang diperoleh menggunakan getCameraImage() mungkin memiliki rasio aspek yang berbeda dibandingkan dengan gambar kedalaman. Dalam hal ini, gambar kedalaman adalah potongan gambar kamera, dan tidak semua piksel dalam gambar kamera memiliki perkiraan kedalaman valid yang sesuai.

Untuk mendapatkan koordinat gambar kedalaman untuk koordinat pada image 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()

Untuk mendapatkan koordinat gambar CPU untuk koordinat gambar kedalaman:

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()

Hit-test kedalaman

Hit-test memungkinkan pengguna untuk menempatkan objek di lokasi dunia nyata dalam adegan. Sebelumnya, hit-test hanya dapat dilakukan pada bidang yang terdeteksi, sehingga membatasi lokasi ke permukaan yang besar dan datar, seperti hasil yang ditampilkan oleh Android berwarna hijau. Hit-test kedalaman memanfaatkan informasi kedalaman yang halus dan mentah untuk memberikan hasil hit yang lebih akurat, bahkan pada permukaan yang tidak datar dan bertekstur rendah. Hal ini ditampilkan dengan Android berwarna merah.

Untuk menggunakan hit-test yang diaktifkan kedalaman, panggil hitTest() dan periksa DepthPoints di daftar yang ditampilkan.

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)

Langkah selanjutnya

  • Aktifkan deteksi yang lebih akurat dengan Raw Depth API.
  • Lihat ARCore Depth Lab, yang menunjukkan berbagai cara untuk mengakses data kedalaman.