Usar profundidade no app Android

A API Depth ajuda a câmera de um dispositivo a entender o tamanho e a forma dos objetos reais em uma cena. Ele usa a câmera para criar imagens ou mapas de profundidade, adicionando uma camada de realismo de RA aos seus apps. É possível usar as informações fornecidas por uma imagem de profundidade para que objetos virtuais apareçam com precisão na frente ou atrás de objetos do mundo real, permitindo experiências do usuário imersivas e realistas.

As informações de profundidade são calculadas com base no movimento e podem ser combinadas com as informações de um sensor de profundidade de hardware, como um sensor de tempo de voo (ToF), se disponível. Um dispositivo não precisa de um sensor ToF para oferecer suporte à API Depth.

Pré-requisitos

Verifique se você entendeu os conceitos fundamentais de RA e como configurar uma sessão do ARCore antes de continuar.

Restringir o acesso a dispositivos compatíveis com a profundidade

Se o app exigir suporte à API Depth, seja porque uma parte importante da experiência de RA depende da profundidade ou porque não há um substituto otimizado para as partes do app que usam a profundidade, você pode optar por restringir a distribuição do app na Google Play Store a dispositivos com suporte à API Depth adicionando a seguinte linha ao AndroidManifest.xml, além das mudanças no AndroidManifest.xml descritas no guia Ativar o ARCore:

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

Ativar profundidade

Em uma nova sessão do ARCore, verifique se o dispositivo do usuário é compatível com a profundidade. Nem todos os dispositivos compatíveis com o ARCore oferecem suporte à API Depth devido a restrições de capacidade de processamento. Para economizar recursos, a profundidade é desativada por padrão no ARCore. Ative o modo de profundidade para que o app use a 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)

Conseguir imagens de profundidade

Chame Frame.acquireDepthImage16Bits() para ver a imagem de profundidade do frame atual.

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

A imagem retornada fornece o buffer de imagem bruta, que pode ser transmitido a um sombreador de fragmento para uso na GPU para cada objeto renderizado a ser ocultado. Ela é orientada em OPENGL_NORMALIZED_DEVICE_COORDINATES e pode ser mudada para TEXTURE_NORMALIZED chamando Frame.transformCoordinates2d(). Quando a imagem de profundidade está acessível em um sombreador de objeto, essas medidas de profundidade podem ser acessadas diretamente para lidar com a oclusão.

Entender os valores de profundidade

Considerando o ponto A na geometria real observada e um ponto 2D a que representa o mesmo ponto na imagem de profundidade, o valor fornecido pela API Depth em a é igual ao comprimento de CA projetado no eixo principal. Ela também pode ser chamada de coordenada z de A em relação à origem C da câmera. Ao trabalhar com a API Depth, é importante entender que os valores de profundidade não são o comprimento da CA de raio em si, mas a projeção dela.

Usar profundidade em sombreadores

Analisar informações de profundidade do frame atual

Use as funções auxiliares DepthGetMillimeters() e DepthGetVisibility() em um sombreador de fragmento para acessar as informações de profundidade da posição atual da tela. Em seguida, use essas informações para ocultar seletivamente partes do 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;
}

Ocultar objetos virtuais

Oculte objetos virtuais no corpo do sombreador de fragmentos. Atualizar o canal Alfa do objeto com base na profundidade dele. Isso renderizará um 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);

É possível renderizar a oclusão usando a renderização em duas passagens ou a renderização de passagem direta por objeto. A eficiência de cada abordagem depende da complexidade do cenário e de outras considerações específicas do app.

Renderização por objeto, passagem para frente

A renderização de passagem direta por objeto determina a oclusão de cada pixel do objeto no sombreador do material. Se os pixels não estiverem visíveis, eles serão cortados, geralmente usando a mistura alfa, simulando a oclusão no dispositivo do usuário.

Renderização em duas etapas

Com a renderização em duas etapas, a primeira passagem renderiza todo o conteúdo virtual em um buffer intermediário. O segundo passe combina a cena virtual com o segundo plano com base na diferença entre a profundidade do mundo real e a profundidade da cena virtual. Essa abordagem não exige outro trabalho de sombreador específico do objeto e geralmente produz resultados de aparência mais uniforme do que o método de passagem direta.

Extrair distância de uma imagem de profundidade

Para usar a API Depth para fins que não sejam a ocultação de objetos virtuais ou a visualização de dados de profundidade, extraia informações da imagem de profundidade.

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

Converter coordenadas entre imagens de câmera e imagens de profundidade

As imagens geradas com getCameraImage() podem ter uma proporção diferente em comparação com as imagens de profundidade. Nesse caso, a imagem de profundidade é um corte da imagem da câmera, e nem todos os pixels dela têm uma estimativa de profundidade válida correspondente.

Para obter coordenadas de imagem de profundidade para coordenadas na imagem da 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()

Para obter coordenadas de imagem da CPU para coordenadas de imagem de profundidade:

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

Teste de hit

Os testes de hit permitem que os usuários coloquem objetos em um local do mundo real na cena. Antes, os testes de hit só podiam ser realizados em aviões detectados, limitando os locais a superfícies grandes e planas, como os resultados mostrados pelos Androids verdes. Os testes de hit de profundidade aproveitam as informações de profundidade suaves e brutas para fornecer resultados de hit mais precisos, mesmo em superfícies não planas e de baixa textura. É mostrado com os Androids vermelhos.

Para usar testes de hit com profundidade, chame hitTest() e verifique se DepthPoints está na lista de retorno.

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)

O que vem em seguida?