Cómo usar Depth en tu app para Android

La API de Depth ayuda a la cámara de un dispositivo a comprender el tamaño y la forma de los objetos reales de una escena. Usa la cámara para crear imágenes o mapas de profundidad, lo que agrega una capa de realismo de RA a tus apps. Puedes usar la información que proporciona una imagen de profundidad para hacer que los objetos virtuales aparezcan con precisión delante o detrás de objetos del mundo real, lo que permite experiencias del usuario inmersivas y realistas.

La información de profundidad se calcula a partir del movimiento y se puede combinar con la información de un sensor de profundidad de hardware, como un sensor de tiempo de vuelo (ToF), si está disponible. Un dispositivo no necesita un sensor ToF para admitir la API de Depth.

Requisitos previos

Asegúrate de comprender los conceptos fundamentales de RA y cómo configurar una sesión de ARCore antes de continuar.

Cómo restringir el acceso a los dispositivos compatibles con Depth

Si tu app requiere compatibilidad con la API de Depth, ya sea porque una parte La experiencia de RA se basa en la profundidad o porque no hay resguardo de la app que usan profundidad, puedes restringir la distribución de tu de Google Play Store para dispositivos compatibles con la API de Depth agregando la siguiente línea a tu AndroidManifest.xml, además del AndroidManifest.xml cambios descritos en el Guía para habilitar ARCore:

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

Habilitar profundidad

En una nueva sesión de ARCore, verifica si el dispositivo de un usuario es compatible con Depth. No todos los dispositivos compatibles con ARCore admiten la API de Depth debido a limitaciones de la capacidad de procesamiento. Para ahorrar recursos, la profundidad está inhabilitada de forma predeterminada en ARCore. Habilita el modo de profundidad para que tu app use la API de 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)

Cómo adquirir imágenes de profundidad

Llama a Frame.acquireDepthImage16Bits() para obtener la imagen de profundidad del fotograma actual.

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

La imagen que se muestra proporciona el búfer de imagen sin procesar, que se puede pasar a un sombreador de fragmentos para usarlo en la GPU y ocultar cada objeto renderizado. Está orientada en OPENGL_NORMALIZED_DEVICE_COORDINATES y se puede cambiar a TEXTURE_NORMALIZED llamando a Frame.transformCoordinates2d(). Una vez que se puede acceder a la imagen de profundidad dentro de un sombreador de objetos, se puede acceder directamente a estas mediciones de profundidad para controlar la oclusión.

Cómo interpretar los valores de profundidad

Dado el punto A en la geometría del mundo real observada y un punto 2D a que representan el mismo punto en la imagen de profundidad, el valor que otorga la profundidad La API en a es igual a la longitud de CA proyectada en el eje principal. También se puede denominar la coordenada z de A en relación con la cámara. origen C. Cuando trabajes con la API de Depth, es importante que comprendas lo siguiente: Los valores de profundidad no son la longitud del rayo CA, sino la proyección de sus aspectos más emocionantes.

Cómo usar profundidad en sombreadores

Cómo analizar la información de profundidad del fotograma actual

Usa las funciones auxiliares DepthGetMillimeters() y DepthGetVisibility() en un sombreador de fragmentos para acceder a la información de profundidad de la posición actual de la pantalla. Luego, usa esta información para ocluir de manera selectiva partes del objeto renderizado.

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

Ocluye objetos virtuales

Ocluye objetos virtuales en el cuerpo del sombreador de fragmentos. Actualiza el canal alfa del objeto en función de su profundidad. Esto renderizará un objeto oculto.

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

Puedes renderizar la oclusión con renderización de dos pasos o por objeto, renderizado de pase hacia delante. La eficiencia de cada enfoque depende de la complejidad de la escena y otras consideraciones específicas de la app.

Renderización de pase hacia delante por objeto

La renderización de pase hacia delante por objeto determina la oclusión de cada píxel del objeto en el sombreador de material. Si los píxeles no son visibles, se recortan, generalmente, mediante una combinación alfa, con lo que se simula la oclusión en el dispositivo del usuario.

Renderización de dos pasos

Con la renderización de dos pasos, el primer pase renderiza todo el contenido virtual en un búfer intermedio. El segundo pase combina la escena virtual con el fondo en función de la diferencia entre la profundidad del mundo real y la profundidad de la escena virtual. Este enfoque no requiere trabajo adicional de sombreador específico del objeto y, por lo general, produce resultados más uniformes que el método de pase hacia delante.

Cómo extraer la distancia de una imagen de profundidad

Si quieres usar la API de Depth con otros fines que no sean para ocultar objetos virtuales ni visualizar datos de profundidad, extrae información de la imagen de profundidad.

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

Cómo convertir coordenadas entre imágenes de la cámara y de profundidad

Las imágenes obtenidas con getCameraImage() pueden tener una relación de aspecto diferente en comparación con las imágenes de profundidad. En este caso, la imagen de profundidad es un recorte de la imagen de la cámara y no todos los píxeles de la imagen de la cámara tienen una estimación de profundidad válida correspondiente.

Para obtener coordenadas de imágenes de profundidad en la imagen de la CPU, haz lo siguiente:

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

Para obtener las coordenadas de la imagen de la CPU correspondientes a las coordenadas de la imagen de profundidad, haz lo siguiente:

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

Prueba de posicionamiento de profundidad

Las pruebas de posicionamiento permiten a los usuarios colocar objetos en una ubicación real de la escena. Anteriormente, las pruebas de posicionamiento solo se podían realizar en planos detectados, lo que limitaba las ubicaciones a superficies grandes y planas, como los resultados que mostraban los Android verdes. Las pruebas de posicionamiento de profundidad aprovechan la información de profundidad tanto suave como sin procesar para proporcionar resultados de hits más precisos, incluso en superficies no planas y con poca textura. Esto se muestra con los Androides rojos.

Para usar pruebas de posicionamiento habilitadas para la profundidad, llama a hitTest() y verifica si hay DepthPoints en la lista de resultados.

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)

¿Qué sigue?