Utiliser la fonctionnalité Depth dans votre application Android

L'API Depth aide l'appareil photo d'un appareil à comprendre la taille et la forme des objets réels d'une scène. Elle utilise l'appareil photo pour créer des images de profondeur, ou cartes de profondeur, et ajoute ainsi un niveau de réalisme de la RA à vos applications. Vous pouvez utiliser les informations fournies par une représentation de profondeur pour faire apparaître avec précision des objets virtuels devant ou derrière des objets du monde réel, offrant ainsi des expériences utilisateur immersives et réalistes.

Les informations de profondeur sont calculées à partir des mouvements et peuvent être combinées à celles d'un capteur matériel de profondeur, tel qu'un capteur de durée de vol, si disponible. Un appareil n'a pas besoin d'un capteur ToF pour être compatible avec l'API Depth.

Prérequis

Assurez-vous de bien maîtriser les concepts fondamentaux de la RA. et comment configurer une session ARCore avant de continuer.

Limiter l'accès aux appareils compatibles avec la fonctionnalité de profondeur

Si votre application nécessite la prise en charge de l'API Depth, soit parce qu'une partie essentielle de L'expérience de RA repose sur la profondeur, ou parce qu'il n'existe pas de solution de remplacement élégante pour les parties de l'application qui utilisent la profondeur, vous pouvez choisir de limiter la distribution de votre sur le Google Play Store pour appareils compatibles avec l'API Depth en ajoutant la ligne suivante à votre AndroidManifest.xml, en plus de AndroidManifest.xml modifications décrites dans le Guide Activer ARCore:

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

Activer la profondeur

Dans une nouvelle session ARCore, vérifiez si l'appareil de l'utilisateur est compatible avec la fonctionnalité de profondeur. Les appareils compatibles ARCore ne sont pas tous compatibles avec l'API Depth en raison de contraintes de puissance de traitement. Pour économiser des ressources, la profondeur est désactivée par défaut sur ARCore. Activez le mode Profondeur pour que votre application utilise l'API Depth.

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)

Obtenir des représentations de profondeur

Appelez Frame.acquireDepthImage16Bits() pour obtenir la représentation de profondeur pour l'image actuelle.

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

L'image renvoyée fournit le tampon d'image brut, qui peut être transmis à un nuanceur de fragments afin qu'il soit utilisé sur le GPU pour masquer chaque objet affiché. Il est orienté dans OPENGL_NORMALIZED_DEVICE_COORDINATES et peut être remplacé par TEXTURE_NORMALIZED en appelant Frame.transformCoordinates2d(). Une fois que la représentation de profondeur est accessible dans un nuanceur d'objets, vous pouvez accéder directement aux mesures de profondeur pour gérer l'occlusion.

Comprendre les valeurs de profondeur

Point A donné sur la géométrie réelle observée et un point 2D a. représentant le même point dans la représentation de profondeur, la valeur donnée par le paramètre L'API à a est égale à la longueur de CA projetée sur l'axe principal. Également appelée coordonnée Z de A par rapport à la caméra origine C. Lorsque vous utilisez l'API Depth, il est important de comprendre que Les valeurs de profondeur ne correspondent pas à la longueur du rayon CA lui-même, mais à la projection du projet.

Utiliser la profondeur dans les nuanceurs

Analyser les informations de profondeur pour l'image actuelle

Utilisez les fonctions d'assistance DepthGetMillimeters() et DepthGetVisibility() dans un nuanceur de fragments pour accéder aux informations de profondeur correspondant à la position actuelle de l'écran. Utilisez ensuite ces informations pour masquer de manière sélective certaines parties de l'objet rendu.

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

Masquer les objets virtuels

Les objets virtuels sont masqués dans le corps du nuanceur de fragments. Mettez à jour le canal alpha de l'objet en fonction de sa profondeur. Un objet masqué est alors affiché.

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

Vous pouvez effectuer le rendu d'une occlusion à l'aide d'un rendu en deux passes ou d'un rendu par objet en effectuant une passe avant. L'efficacité de chaque approche dépend de la complexité de la scène et d'autres considérations propres à l'application.

Rendu par objet, passage avant

Le rendu avant transmission détermine l'occlusion de chaque pixel de l'objet dans son nuanceur Material. Si les pixels ne sont pas visibles, ils sont rognés, généralement via le mélange alpha, ce qui simule une occlusion sur l'appareil de l'utilisateur.

Rendu en deux passes

Avec le rendu en deux passes, la première passe affiche tout le contenu virtuel dans un tampon intermédiaire. La seconde passe associe la scène virtuelle à l'arrière-plan en fonction de la différence entre la profondeur réelle et la profondeur virtuelle. Cette approche ne nécessite aucun travail supplémentaire du nuanceur spécifique aux objets et produit généralement des résultats d'apparence plus uniformes que la méthode de transmission avant.

Extraire la distance d'une représentation de profondeur

Si vous souhaitez utiliser l'API Depth à d'autres fins que d'occlure des objets virtuels ou de visualiser des données de profondeur, extrayez les informations de la représentation de profondeur.

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

Convertir des coordonnées entre des images d'appareil photo et des représentations de profondeur

Les images obtenues à l'aide de getCameraImage() peuvent avoir un format différent de celui des représentations de profondeur. Dans ce cas, la représentation de profondeur est un recadrage de l'image de l'appareil photo, et tous les pixels de l'image de l'appareil photo n'ont pas une estimation de profondeur valide correspondante.

Pour obtenir les coordonnées de profondeur pour les coordonnées de l'image de processeur:

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

Pour obtenir les coordonnées de l'image de processeur pour la profondeur de l'image:

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

Test de positionnement de profondeur

Les tests de positionnement permettent aux utilisateurs de placer des objets à un emplacement réel de la scène. Auparavant, les tests de positionnement ne pouvaient être effectués que sur des avions détectés, en limitant les emplacements aux grandes surfaces plates, comme les résultats des Android verts. Les tests de positionnement de profondeur exploitent les informations de profondeur à la fois fluides et brutes pour fournir des résultats de positionnement plus précis, même sur les surfaces non planes et à faible texture. C'est ce qu'illustrent les personnages Android rouges.

<ph type="x-smartling-placeholder">

Pour utiliser des tests de positionnement avec fonctionnalité de profondeur, appelez hitTest() et recherchez DepthPoints dans la liste renvoyée.

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)

Étapes suivantes

  • Améliorez la précision de la détection avec l'API Raw Depth.
  • Suivez l'atelier ARCore Depth qui présente différentes façons d'accéder aux données de profondeur.