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 de profundidad (o mapas de profundidad), lo que agrega una capa de realismo de RA a tus apps. Puedes usar la información proporcionada por 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 envolventes y realistas.

La información de profundidad se calcula a partir del movimiento y se puede combinar con 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 central de la experiencia de RA se basa en la profundidad o porque no hay un resguardo correcto para las partes de la app que la usan, puedes restringir la distribución de tu app en Google Play Store a dispositivos compatibles con la API de Depth agregando la siguiente línea a tu AndroidManifest.xml, además de la guía para habilitar ARCore:AndroidManifest.xml

<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 potencia de procesamiento. Para guardar 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)

Adquiere 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 su uso en la GPU para cada objeto representado que se ocluya. Se orienta 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 en un sombreador de objetos, se puede acceder a estas mediciones de profundidad directamente para controlar la oclusión.

Comprende los valores de profundidad

Dado el punto A de la geometría real observada y un punto a en 2D que representa el mismo punto en la imagen de profundidad, el valor dado por la API de Depth en a es igual a la longitud de CA proyectada en el eje principal. Esto también puede denominarse coordenada z de A en relación con el origen de la cámara C. Cuando trabajas con la API de Depth, es importante comprender que los valores de profundidad no son la longitud del CA del rayo, sino la proyección de este.

Cómo usar profundidad en sombreadores

Analiza 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 forma 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 según su profundidad. Esto renderizará un objeto ocluido.

// 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 mediante la renderización de dos pases o la renderización de pase hacia delante por objeto. 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 avance por objeto determina la oclusión de cada píxel del objeto en su sombreador de material. Si los píxeles no son visibles, se recortan, por lo general, mediante la combinación alfa, simulando así la oclusión en el dispositivo del usuario.

Renderización de dos pases

Con la renderización de dos pases, el primer pase renderiza todo el contenido virtual en un búfer intermedio. En la segunda pasada, se combina la escena virtual con el fondo en función de la diferencia entre la profundidad del mundo real y la 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 avance.

Extrae la distancia de una imagen de profundidad

Si quieres usar la API de Depth para otros fines que no sean ocluir objetos virtuales o 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()
}

Conversión de coordenadas entre imágenes de cámara e imágenes 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 esta tienen una estimación de profundidad válida correspondiente.

Para obtener las coordenadas de una imagen de profundidad para las coordenadas en la imagen de la CPU, sigue estos pasos:

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

Sigue estos pasos a fin de obtener coordenadas de imágenes de la CPU para coordenadas de imágenes de profundidad:

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 alcance de posicionamiento

Las pruebas de posicionamiento permiten a los usuarios colocar objetos en una ubicación del mundo real dentro de la escena. Anteriormente, las pruebas de posicionamiento solo se podían realizar en los 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 suave y sin procesar para brindar resultados más precisos, incluso en superficies no planas y de textura baja. Se muestra con los androides rojos.

Para usar pruebas de posicionamiento con alcance habilitado, llama a hitTest() y busca DepthPoints en la lista que se muestra.

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?