„Tiefe“ in Ihrer Android-App verwenden

Mithilfe der Depth API kann die Kamera eines Geräts die Größe und Form der realen Objekte in einer Szene erkennen. Dabei werden mithilfe der Kamera Tiefenbilder bzw. Tiefenkarten erstellt und deine Apps um AR-Realismus ergänzt. Mithilfe der Informationen aus einem Tiefenbild können Sie virtuelle Objekte vor oder hinter realen Objekten genau darstellen lassen und so eine immersive und realistische User Experience schaffen.

Tiefeninformationen werden aus Bewegungen berechnet und können mit Daten von einem Hardware-Tiefensensor wie einem Flugzeitsensor (ToF) kombiniert werden, sofern verfügbar. Geräte benötigen keinen ToF-Sensor, um die Depth API zu unterstützen.

Voraussetzungen

Machen Sie sich mit den grundlegenden AR-Konzepten und zur Konfiguration einer ARCore-Sitzung vertraut, bevor Sie fortfahren.

Zugriff auf Geräte mit Tiefendaten beschränken

Wenn für deine App Unterstützung für die Depth API erforderlich ist, kannst du die Bereitstellung deiner App im Google Play Store auf Geräte beschränken, die die Depth API unterstützen. Dazu musst du deinem AndroidManifest.xml zusätzlich zu den Änderungen für ARCore die folgende Zeile hinzufügen: ARCore aktivieren.AndroidManifest.xml

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

Tiefe aktivieren

Prüfen Sie in einer neuen ARCore-Sitzung, ob das Gerät eines Nutzers die Funktion „Tiefe“ unterstützt. Die Depth API wird aufgrund von Einschränkungen bei der Verarbeitungsleistung nicht von allen ARCore-kompatiblen Geräten unterstützt. Die Tiefe ist bei ARCore standardmäßig deaktiviert, um Ressourcen zu sparen. Aktivieren Sie den Tiefenmodus, damit Ihre Anwendung die Depth API verwendet.

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)

Tiefenbilder aufnehmen

Rufen Sie Frame.acquireDepthImage16Bits() auf, um das Tiefenbild für den aktuellen Frame abzurufen.

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

Das zurückgegebene Bild stellt den Zwischenspeicher für Rohbilder bereit, der an einen Fragment-Shader übergeben werden kann, damit er auf der GPU verwendet werden kann, damit jedes gerenderte Objekt verdeckt werden kann. Sie ist auf OPENGL_NORMALIZED_DEVICE_COORDINATES ausgerichtet und kann durch Aufrufen von Frame.transformCoordinates2d() in TEXTURE_NORMALIZED geändert werden. Sobald das Tiefenbild innerhalb eines Objekt-Shaders zugänglich ist, kann direkt auf diese Tiefenmessungen zugegriffen werden, um Verdeckungen zu verarbeiten.

Tiefenwerte verstehen

Bei einem Punkt A auf der beobachteten realen Geometrie und einem 2D-Punkt a, der denselben Punkt im Tiefenbild darstellt, entspricht der von der Tiefen-API bei a angegebene Wert der Länge von CA, die auf die Hauptachse projiziert wird. Dies kann auch als Z-Koordinate von A relativ zum Kameraursprung C bezeichnet werden. Wenn Sie mit der Depth API arbeiten, ist es wichtig zu verstehen, dass die Tiefenwerte nicht der Länge des CAstrahls selbst, sondern der Projektion des Strahls entsprechen.

Tiefe in Shadern verwenden

Tiefeninformationen für den aktuellen Frame parsen

Verwenden Sie die Hilfsfunktionen DepthGetMillimeters() und DepthGetVisibility() in einem Fragment-Shader, um auf die Tiefeninformationen für die aktuelle Bildschirmposition zuzugreifen. Verwenden Sie diese Informationen dann, um Teile des gerenderten Objekts selektiv zu verdecken.

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

Virtuelle Objekte verdecken

Verdecken Sie virtuelle Objekte im Text des Fragment-Shaders. Aktualisieren Sie den Alphakanal des Objekts anhand seiner Tiefe. Dadurch wird ein verdecktes Objekt gerendert.

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

Sie können Verdeckungen mit Zwei-Durchlauf-Rendering oder Vorwärtsdurchlauf pro Objekt rendern. Die Effizienz der einzelnen Ansätze hängt von der Komplexität der Szene und anderen anwendungsspezifischen Überlegungen ab.

Rendering pro Objekt, Vorwärtsdurchlauf

Beim Vorwärtsdurchlauf-Rendering pro Objekt wird die Verdeckung jedes Pixels des Objekts in seinem Material-Shader bestimmt. Wenn die Pixel nicht sichtbar sind, werden sie abgeschnitten, in der Regel durch Alpha-Mischung, wodurch eine Verdeckung auf dem Gerät des Nutzers simuliert wird.

Rendering mit zwei Durchgängen

Beim Rendern mit zwei Durchgängen werden mit der ersten Übergabe alle virtuellen Inhalte in einem Zwischenpuffer gerendert. Beim zweiten Durchlauf wird die virtuelle Szene basierend auf dem Unterschied zwischen der tatsächlichen Tiefe und der virtuellen Szene mit dem Hintergrund verschmelzen. Dieser Ansatz erfordert keine zusätzliche objektspezifische Shader-Arbeit und erzeugt im Allgemeinen einheitlichere Ergebnisse als die Forward-Pass-Methode.

Entfernung aus einem Tiefenbild extrahieren

Wenn Sie die Depth API nicht zum Verdecken virtueller Objekte oder zur Visualisierung von Tiefendaten verwenden möchten, extrahieren Sie Informationen aus dem Tiefenbild.

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

Koordinaten zwischen Kamerabildern und Tiefenbildern konvertieren

Mit getCameraImage() aufgenommene Bilder können ein anderes Seitenverhältnis als Bilder mit Tiefe haben. In diesem Fall ist das Tiefenbild ein Ausschnitt des Kamerabilds und nicht für alle Pixel im Kamerabild ist eine gültige Tiefenschätzung vorhanden.

So rufen Sie Tiefenbildkoordinaten für Koordinaten im CPU-Bild ab:

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

So rufen Sie CPU-Bildkoordinaten für Tiefenbildkoordinaten ab:

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

Testtiefe der Treffer

Treffertests ermöglichen es Nutzern, Objekte an einem realen Ort in der Szene zu platzieren. Bisher konnten Treffertests nur auf erkannten Ebenen durchgeführt werden, wobei Standorte auf große, ebene Flächen beschränkt wurden, wie z. B. die von den grünen Android-Geräten angezeigten Ergebnisse. Bei Tiefentests werden sowohl glatte als auch rohe Tiefendaten genutzt, um genauere Trefferergebnisse zu liefern – selbst auf nicht planaren Oberflächen und Oberflächen mit niedriger Textur. Dies wird mit den roten Android-Geräten dargestellt.

Wenn Sie Treffertests mit aktivierter Tiefe verwenden möchten, rufen Sie hitTest() auf und suchen Sie in der Rückgabeliste nach 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)

Nächste Schritte

  • Verwenden Sie die Raw Depth API, um eine genauere Erkennung zu ermöglichen.
  • Im ARCore Depth Lab werden verschiedene Möglichkeiten für den Zugriff auf Tiefendaten vorgestellt.