Profundidade com o ARCore

O ARCore é uma plataforma para criar apps de realidade aumentada (RA) para dispositivos móveis. A API Depth do ARCore do Google oferece acesso a uma imagem de profundidade correspondente para cada frame fornecido pela Sessão do ARCore. Cada pixel tem uma medida da distância da câmera em relação ao ambiente, que é usada neste codelab para deixar aplicativos de RA mais realistas.

A API Depth só é compatível com um subconjunto de dispositivos compatíveis com o ARCore. Consulte esta lista para ver quais smartphones são compatíveis com chamadas de profundidade. A API Depth está disponível apenas no Android.

Este codelab ensinará você a criar um app compatível com RA que usa imagens de profundidade para realizar a oclusão de recursos virtuais atrás de superfícies do mundo real e visualiza essa geometria detectada do mundo.

O que você criará

d9bd5136c54ce47a.gif

Neste codelab, você criará um app que usa a imagem de profundidade de cada frame para visualizar a geometria da cena e realizar a oclusão de recursos virtuais. Este codelab seguirá estas etapas específicas:

  1. Conferir se há compatibilidade com a API Depth no smartphone.
  2. Extrair a imagem de profundidade de cada frame.
  3. Várias maneiras de visualizar informações de profundidade (veja a animação acima).
  4. Como usar a profundidade para que apps com oclusão sejam mais realistas.
  5. Como lidar corretamente com smartphones sem compatibilidade com a API Depth.

Observação: se você tiver problemas durante o processo, vá para a última seção e veja algumas dicas de solução de problemas.

Você precisará de hardware e software específicos para concluir este codelab.

Requisitos de hardware

Requisitos de software

Como configurar a máquina de desenvolvimento

Conecte o dispositivo ARCore ao computador usando o cabo USB. Verifique se a depuração USB está ativada no dispositivo. Abra um terminal e execute adb devices, como mostrado abaixo:

adb devices

List of devices attached
<DEVICE_SERIAL_NUMBER>    device

O <DEVICE_SERIAL_NUMBER> será uma string exclusiva do dispositivo. Antes de continuar, verifique se apenas um dispositivo é exibido.

Como fazer o download e instalar o código

Você pode clonar o repositório:

git clone https://github.com/googlecodelabs/arcore-depth

Ou fazer o download de um arquivo ZIP e extraí-lo:

Fazer o download do ZIP

Inicie o Android Studio. Clique em Open an existing Android Studio project. Em seguida, navegue até o diretório em que você extraiu o arquivo ZIP depois de ter feito o download acima e clique duas vezes no diretório depth_codelab_io2020.

Esse é um projeto individual do Gradle com vários módulos. Se ele não ainda não for exibido no painel "Project" no canto superior esquerdo do Android Studio, clique em Projects no menu suspenso. O resultado ficará assim:

Este projeto contém os seguintes módulos:

  • part0_work: o app inicial. Ao fazer este codelab, você fará editará esse arquivo.
  • part1: o código de referência para saber como o arquivo editado ficará após concluir a Parte 1.
  • part2: o código de referência após concluir a Parte 2.
  • part3: o código de referência após concluir a Parte 3.
  • part4_completed: a versão final do app, que é o código de referência após concluir a Parte 4 e este codelab.

Você trabalhará com o módulo part0_work. Também fornecemos soluções completas para cada parte do codelab. Cada módulo é um app que pode ser compilado.

Clique em Run > Run… > ‘part0_work'. Na caixa de diálogo Select Deployment Target que será exibida, o dispositivo estará listado em Connected Devices. Selecione o dispositivo e clique em OK. O Android Studio criará o app inicial e o executará no dispositivo.

Quando você executar o app pela primeira vez, ele solicitará a permissão para usar a CÂMERA. Toque em Permitir para continuar.

Como usar o app

  1. Mova o dispositivo para ajudar o app a encontrar uma superfície plana. A mensagem na parte inferior indica quando você precisa continuar a movimentar o dispositivo.
  2. Toque em algum lugar plano para colocar uma âncora. Um ícone do Android será exibido onde a âncora for colocada. Este app permite colocar apenas uma âncora de cada vez.
  3. Mova o dispositivo pelo ambiente. A figura precisa dar a impressão de permanecer no mesmo lugar, embora o dispositivo esteja se movendo.

Atualmente, o aplicativo é muito simples e não sabe muito sobre a geometria do mundo real. Se você colocar um ícone Android atrás de uma cadeira, por exemplo, a renderização parecerá estar sempre na frente dela, porque o app não sabe que a cadeira está lá e que ela deveria estar escondendo o ícone.

5e8f5fe9098d316e.png 76f41b692224801b.png 3f320c851d1903d.png

Para corrigir o problema, usaremos a API Depth para melhorar a imersão e o realismo do app.

A API Depth do ARCore pode ser executada em um subconjunto de dispositivos compatíveis. Antes de integrar a funcionalidade no app que usará essas imagens de profundidade, precisamos garantir que ele seja executado em um dispositivo compatível.

Adicione um novo membro particular à DepthCodelabActivity que servirá como uma sinalização e informará se o dispositivo atual é compatível com a profundidade ou não:

private boolean isDepthSupported;

É possível preencher essa sinalização na função onResume(), em que uma nova Sessão é criada. Veja o código existente:

// Creates the ARCore session.
session = new Session(/* context= */ this);

Atualize o código para:

// Creates the ARCore session.
session = new Session(/* context= */ this);
Config config = session.getConfig();
isDepthSupported = session.isDepthModeSupported(Config.DepthMode.AUTOMATIC);
if (isDepthSupported) {
  config.setDepthMode(Config.DepthMode.AUTOMATIC);
} else {
  config.setDepthMode(Config.DepthMode.DISABLED);
}
session.configure(config);

Agora, a sessão de RA está configurada corretamente e o app sabe se é possível ou não usar recursos com base em profundidade.

Também precisamos informar ao usuário se a profundidade será usada na sessão. É possível adicionar outra mensagem à Snackbar exibida na parte inferior da tela:

// Add this line at the top of the file, with the other messages.
private static final String DEPTH_NOT_AVAILABLE_MESSAGE = "[Depth not supported on this device]";

No método onDrawFrame(), podemos apresentar esta mensagem conforme necessário:

// Add this if-statement above messageSnackbarHelper.showMessage(this, messageToShow).
if (!isDepthSupported) {
  messageToShow += "\n" + DEPTH_NOT_AVAILABLE_MESSAGE;
}

Se o app for executado em um dispositivo que não é compatível com a profundidade, a mensagem que acabamos de adicionar será exibida na parte inferior:

feb1c9f42f3cf396.png

Em seguida, atualizaremos o app para chamar a API Depth e extrair imagens de profundidade para cada frame.

A API Depth captura observações em 3D do ambiente e fornece uma imagem de profundidade com esses dados ao app. Cada pixel dessa imagem representa uma medida da distância da câmera até o ambiente do mundo real.

Esse codelab usará essas imagens de profundidade para melhorar a renderização e a visualização no app. O primeiro passo é extrair a imagem de profundidade de cada frame e vincular a textura para que ela seja usada pela GPU.

Primeiro, adicionaremos uma nova classe ao nosso projeto. DepthTextureHandler é responsável por extrair a imagem de profundidade para um frame específico do ARCore. Adicione este arquivo:

41c3889f2bbc8345.png

src/main/java/com/google/ar/core/codelab/depth/DepthTextureHandler.java

package com.google.ar.core.codelab.depth;

import static android.opengl.GLES20.GL_CLAMP_TO_EDGE;
import static android.opengl.GLES20.GL_TEXTURE_2D;
import static android.opengl.GLES20.GL_TEXTURE_MAG_FILTER;
import static android.opengl.GLES20.GL_TEXTURE_MIN_FILTER;
import static android.opengl.GLES20.GL_TEXTURE_WRAP_S;
import static android.opengl.GLES20.GL_TEXTURE_WRAP_T;
import static android.opengl.GLES20.GL_UNSIGNED_BYTE;
import static android.opengl.GLES20.glBindTexture;
import static android.opengl.GLES20.glGenTextures;
import static android.opengl.GLES20.glTexImage2D;
import static android.opengl.GLES20.glTexParameteri;
import static android.opengl.GLES30.GL_LINEAR;
import static android.opengl.GLES30.GL_RG;
import static android.opengl.GLES30.GL_RG8;

import android.media.Image;
import com.google.ar.core.Frame;
import com.google.ar.core.exceptions.NotYetAvailableException;

/** Handle RG8 GPU texture containing a DEPTH16 depth image. */
public final class DepthTextureHandler {

  private int depthTextureId = -1;
  private int depthTextureWidth = -1;
  private int depthTextureHeight = -1;

  /**
   * Creates and initializes the depth texture. This method needs to be called on a
   * thread with a EGL context attached.
   */
  public void createOnGlThread() {
    int[] textureId = new int[1];
    glGenTextures(1, textureId, 0);
    depthTextureId = textureId[0];
    glBindTexture(GL_TEXTURE_2D, depthTextureId);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  }

  /**
   * Updates the depth texture with the content from acquireDepthImage().
   * This method needs to be called on a thread with an EGL context attached.
   */
  public void update(final Frame frame) {
    try {
      Image depthImage = frame.acquireDepthImage();
      depthTextureWidth = depthImage.getWidth();
      depthTextureHeight = depthImage.getHeight();
      glBindTexture(GL_TEXTURE_2D, depthTextureId);
      glTexImage2D(
          GL_TEXTURE_2D,
          0,
          GL_RG8,
          depthTextureWidth,
          depthTextureHeight,
          0,
          GL_RG,
          GL_UNSIGNED_BYTE,
          depthImage.getPlanes()[0].getBuffer());
      depthImage.close();
    } catch (NotYetAvailableException e) {
      // This normally means that depth data is not available yet.
    }
  }

  public int getDepthTexture() {
    return depthTextureId;
  }

  public int getDepthWidth() {
    return depthTextureWidth;
  }

  public int getDepthHeight() {
    return depthTextureHeight;
  }
}

Em seguida, podemos adicionar uma instância dessa classe à DepthCodelabActivity, garantindo que teremos uma cópia fácil de acessar da imagem de profundidade de cada frame.

Em DepthCodelabActivity.java, adicione uma instância da nossa nova classe como uma variável de membro particular:

private final DepthTextureHandler depthTexture = new DepthTextureHandler();

Em seguida, vamos atualizar o método onSurfaceCreated() para inicializar essa textura para que ela seja usada pelos sombreadores da GPU:

// Put this at the top of the "try" block in onSurfaceCreated().
depthTexture.createOnGlThread();

Por fim, queremos preencher essa textura em todos os frames com a imagem de profundidade mais recente, o que pode ser feito chamando o método update(), que criamos acima, no último frame extraído da session. A chamada só será feita se estivermos usando a profundidade, porque a compatibilidade com ela é opcional.

// Add this just after "frame" is created inside onDrawFrame().
if (isDepthSupported) {
  depthTexture.update(frame);
}

Agora temos uma imagem de profundidade que será atualizada a cada frame. Ela está pronta para ser usada pelos nossos sombreadores. No entanto, nada mudou ainda no comportamento do app. Vamos usar a imagem de profundidade para melhorar nosso app.

Agora que temos essa imagem para brincar, vamos ver como ela é. Nesta seção, adicionaremos um botão ao app para renderizar a profundidade de cada frame.

Adicionar novos sombreadores

Há muitas maneiras de ver uma imagem de profundidade. Os sombreadores a seguir fornecem uma visualização simples do mapeamento de cores.

Primeiro, adicione novos sombreadores .vert e .frag ao diretório src/main/assets/shaders/.

Como adicionar um novo sombreador .vert

No Android Studio:

  1. Clique com o botão direito no diretório de sombreadores.
  2. Selecione New -> File.
  3. Nomeie o arquivo como background_show_depth_map.vert.
  4. Defina-o como um arquivo de texto.

No novo arquivo, adicione este código:

src/main/assets/shaders/background_show_depth_map.vert

attribute vec4 a_Position;
attribute vec2 a_TexCoord;

varying vec2 v_TexCoord;

void main() {
   v_TexCoord = a_TexCoord;
   gl_Position = a_Position;
}

Repita as mesmas etapas acima para criar o sombreador de fragmento com o nome background_show_depth_map.frag no mesmo diretório. Em seguida, adicione este código ao novo arquivo:

src/main/assets/shaders/background_show_depth_map.frag

precision mediump float;
uniform sampler2D u_Depth;
varying vec2 v_TexCoord;
const highp float kMaxDepth = 8000.0; // In millimeters.

float GetDepthMillimeters(vec4 depth_pixel_value) {
  return 255.0 * (depth_pixel_value.r + depth_pixel_value.g * 256.0);
}

// Returns an interpolated color in a 6 degree polynomial interpolation.
vec3 GetPolynomialColor(in float x,
  in vec4 kRedVec4, in vec4 kGreenVec4, in vec4 kBlueVec4,
  in vec2 kRedVec2, in vec2 kGreenVec2, in vec2 kBlueVec2) {
  // Moves the color space a little bit to avoid pure red.
  // Removes this line for more contrast.
  x = clamp(x * 0.9 + 0.03, 0.0, 1.0);
  vec4 v4 = vec4(1.0, x, x * x, x * x * x);
  vec2 v2 = v4.zw * v4.z;
  return vec3(
    dot(v4, kRedVec4) + dot(v2, kRedVec2),
    dot(v4, kGreenVec4) + dot(v2, kGreenVec2),
    dot(v4, kBlueVec4) + dot(v2, kBlueVec2)
  );
}

// Returns a smooth Percept colormap based upon the Turbo colormap.
vec3 PerceptColormap(in float x) {
  const vec4 kRedVec4 = vec4(0.55305649, 3.00913185, -5.46192616, -11.11819092);
  const vec4 kGreenVec4 = vec4(0.16207513, 0.17712472, 15.24091500, -36.50657960);
  const vec4 kBlueVec4 = vec4(-0.05195877, 5.18000081, -30.94853351, 81.96403246);
  const vec2 kRedVec2 = vec2(27.81927491, -14.87899417);
  const vec2 kGreenVec2 = vec2(25.95549545, -5.02738237);
  const vec2 kBlueVec2 = vec2(-86.53476570, 30.23299484);
  const float kInvalidDepthThreshold = 0.01;
  return step(kInvalidDepthThreshold, x) *
         GetPolynomialColor(x, kRedVec4, kGreenVec4, kBlueVec4,
                            kRedVec2, kGreenVec2, kBlueVec2);
}

void main() {
  vec4 packed_depth = texture2D(u_Depth, v_TexCoord.xy);
  highp float depth_mm = GetDepthMillimeters(packed_depth);
  highp float normalized_depth = depth_mm / kMaxDepth;
  vec4 depth_color = vec4(PerceptColormap(normalized_depth), 1.0);
  gl_FragColor = depth_color;
}

Em seguida, atualize a classe BackgroundRenderer para usar esses novos sombreadores, localizados em src/main/java/com/google/ar/core/codelab/common/rendering/BackgroundRenderer.java.

Adicione os caminhos de arquivos aos sombreadores na parte superior da classe.

// Add these under the other shader names at the top of the class.
private static final String DEPTH_VERTEX_SHADER_NAME = "shaders/background_show_depth_map.vert";
private static final String DEPTH_FRAGMENT_SHADER_NAME = "shaders/background_show_depth_map.frag";

Adicione mais variáveis de membro à classe BackgroundRenderer, porque ela executará dois sombreadores.

// Add to the top of file with the rest of the member variables.
private int depthProgram;
private int depthTextureParam;
private int depthTextureId = -1;
private int depthQuadPositionParam;
private int depthQuadTexCoordParam;

Adicione um novo método para preencher os campos:

// Add this method below createOnGlThread().
public void createDepthShaders(Context context, int depthTextureId) throws IOException {
  int vertexShader =
      ShaderUtil.loadGLShader(
          TAG, context, GLES20.GL_VERTEX_SHADER, DEPTH_VERTEX_SHADER_NAME);
  int fragmentShader =
      ShaderUtil.loadGLShader(
          TAG, context, GLES20.GL_FRAGMENT_SHADER, DEPTH_FRAGMENT_SHADER_NAME);

  depthProgram = GLES20.glCreateProgram();
  GLES20.glAttachShader(depthProgram, vertexShader);
  GLES20.glAttachShader(depthProgram, fragmentShader);
  GLES20.glLinkProgram(depthProgram);
  GLES20.glUseProgram(depthProgram);
  ShaderUtil.checkGLError(TAG, "Program creation");

  depthTextureParam = GLES20.glGetUniformLocation(depthProgram, "u_Depth");
  ShaderUtil.checkGLError(TAG, "Program parameters");

  depthQuadPositionParam = GLES20.glGetAttribLocation(depthProgram, "a_Position");
  depthQuadTexCoordParam = GLES20.glGetAttribLocation(depthProgram, "a_TexCoord");

  this.depthTextureId = depthTextureId;
}

Adicione este método, que será usado para aplicar esses sombreadores em todos os frames:

// Put this at the bottom of the file.
public void drawDepth(@NonNull Frame frame) {
  if (frame.hasDisplayGeometryChanged()) {
    frame.transformCoordinates2d(
        Coordinates2d.OPENGL_NORMALIZED_DEVICE_COORDINATES,
        quadCoords,
        Coordinates2d.TEXTURE_NORMALIZED,
        quadTexCoords);
  }

  if (frame.getTimestamp() == 0 || depthTextureId == -1) {
    return;
  }

  // Ensure position is rewound before use.
  quadTexCoords.position(0);

  // No need to test or write depth, the screen quad has arbitrary depth, and is expected
  // to be drawn first.
  GLES20.glDisable(GLES20.GL_DEPTH_TEST);
  GLES20.glDepthMask(false);

  GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
  GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, depthTextureId);
  GLES20.glUseProgram(depthProgram);
  GLES20.glUniform1i(depthTextureParam, 0);

  // Set the vertex positions and texture coordinates.
  GLES20.glVertexAttribPointer(
        depthQuadPositionParam, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, quadCoords);
  GLES20.glVertexAttribPointer(
        depthQuadTexCoordParam, TEXCOORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, quadTexCoords);

  // Draws the quad.
  GLES20.glEnableVertexAttribArray(depthQuadPositionParam);
  GLES20.glEnableVertexAttribArray(depthQuadTexCoordParam);
  GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
  GLES20.glDisableVertexAttribArray(depthQuadPositionParam);
  GLES20.glDisableVertexAttribArray(depthQuadTexCoordParam);

  // Restore the depth state for further drawing.
  GLES20.glDepthMask(true);
  GLES20.glEnable(GLES20.GL_DEPTH_TEST);

  ShaderUtil.checkGLError(TAG, "BackgroundRendererDraw");
}

Adicionar um botão ativar

Agora que podemos renderizar o mapa de profundidade, vamos usá-lo. Podemos adicionar um botão que ativa e desativa a renderização na DepthCodelabActivity.

Na parte superior do arquivo DepthCodelabActivity, adicione uma importação que será usada pelo botão:

import android.widget.Button;

Atualize a classe para adicionar um membro booleano indicando se a renderização de profundidade está ativada ou não (desativada por padrão):

private boolean showDepthMap = false;

Em seguida, adicione o botão que controla o booleano showDepthMap no final do método onCreate():

final Button toggleDepthButton = (Button) findViewById(R.id.toggle_depth_button);
    toggleDepthButton.setOnClickListener(
        view -> {
          if (isDepthSupported) {
            showDepthMap = !showDepthMap;
            toggleDepthButton.setText(showDepthMap ? R.string.hide_depth : R.string.show_depth);
          } else {
            showDepthMap = false;
            toggleDepthButton.setText(R.string.depth_not_available);
          }
        });

Adicione estas strings ao arquivo res/values/strings.xml:

<string translatable="false" name="show_depth">Show Depth</string>
<string translatable="false" name="hide_depth">Hide Depth</string>
<string translatable="false" name="depth_not_available">Depth Not Available</string>

Adicione este botão à parte inferior do layout do app em res/layout/activity_main.xml:

<Button
    android:id="@+id/toggle_depth_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="20dp"
    android:gravity="center"
    android:text="@string/show_depth"
    android:layout_alignParentRight="true"
    android:layout_alignParentTop="true"/>

Agora, o botão controlará o valor do booleano showDepthMap. Use esta sinalização para controlar se o mapa de profundidade será renderizado. De volta ao método onDrawFrame() em DepthCodelabActivity, adicione este código:

// Add this snippet just under backgroundRenderer.draw(frame);
if (showDepthMap) {
  backgroundRenderer.drawDepth(frame);
}

Transmita a textura de profundidade para o backgroundRenderer adicionando esta linha ao método onSurfaceCreated():

// Add to onSurfaceCreated() after backgroundRenderer.createonGlThread(/*context=*/ this);
backgroundRenderer.createDepthShaders(/*context=*/ this, depthTexture.getDepthTexture());

Agora, podemos ver a imagem de profundidade de cada frame ao pressionar o botão no canto superior direito da tela.

App em execução sem compatibilidade com a API Depth

App em execução com compatibilidade com a API Depth

[Opcional] Animação com profundidade sofisticada

No momento, o app mostra diretamente o mapa de profundidade. Os pixels vermelhos representam áreas próximas e os pixels azuis representam áreas distantes.

Há muitas maneiras de expressar informações de profundidade. Nesta subseção, modificaremos o sombreador para que ele exiba a profundidade apenas nas faixas que se afastam da câmera repetidas vezes como um "pulso" periódico.

Comece adicionando essas variáveis à parte superior de background_show_depth_map.frag:

uniform float u_DepthRangeToRenderMm;
const float kDepthWidthToRenderMm = 350.0;

Em seguida, use esses valores para filtrar quais pixels serão tratados com valores de profundidade na função main() do sombreador.

// Add this line at the end of main().
gl_FragColor.a = clamp(1.0 - abs((depth_mm - u_DepthRangeToRenderMm) / kDepthWidthToRenderMm), 0.0, 1.0);

Depois, atualize BackgroundRenderer.java para armazenar esses parâmetros de sombreador. Adicione os campos a seguir na parte superior da classe:

private static final float MAX_DEPTH_RANGE_TO_RENDER_MM = 10000.0f;
private float depthRangeToRenderMm = 0.0f;
private int depthRangeToRenderMmParam;

No método createDepthShaders(), adicione o código a linha a seguir para que esses parâmetros correspondam ao programa do sombreador:

depthRangeToRenderMmParam = GLES20.glGetUniformLocation(depthProgram, "u_DepthRangeToRenderMm");

Por fim, poderemos controlar esse intervalo ao longo do tempo no método drawDepth(). Adicione o código a seguir, que incrementa esse intervalo sempre que um frame é exibido.

// Enables alpha blending.
GLES20.glEnable(GLES20.GL_BLEND);
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);

// Updates range each time draw() is called.
depthRangeToRenderMm += 50.0f;
if (depthRangeToRenderMm > MAX_DEPTH_RANGE_TO_RENDER_MM) {
  depthRangeToRenderMm = 0.0f;
}

// Passes latest value to the shader.
GLES20.glUniform1f(depthRangeToRenderMmParam, depthRangeToRenderMm);

Agora, a profundidade pode ser visualizada como uma pulsação animada que flui pela cena.

37e2a86b833150f8.gif

Você pode mudar os valores fornecidos aqui para deixar o pulso mais lento, mais rápido, mais largo, mais estreito e muito mais. Também é possível experimentar novas formas de mudar o sombreador para mostrar as informações de profundidade.

A oclusão ocorre quando um objeto virtual não é totalmente renderizado porque há superfícies reais entre ele e a câmera.

A capacidade de renderizar objetos virtuais corretamente in situ melhora o realismo e a credibilidade da cena de realidade aumentada. Para ver mais exemplos, assista ao nosso vídeo sobre como mesclar realidades usando a API Depth.

Nesta seção, vamos atualizar nosso app para incluir objetos virtuais se a profundidade estiver disponível.

Como adicionar novos sombreadores de objeto

Como nas seções anteriores, vamos adicionar novos sombreadores para oferecer compatibilidade com informações de profundidade. Desta vez, copiaremos os sombreadores já existentes de objeto e adicionaremos a funcionalidade de oclusão. É importante manter as duas versões dos sombreadores de objeto. Assim, o app poderá decidir oferecer compatibilidade com a profundidade durante a execução.

Copie os arquivos de sombreador object.vert e object.frag no diretório src/main/assets/shaders.

  1. Copie object.vert para o arquivo de destino src/main/assets/shaders/occlusion_object.vert.
  2. Copie object.frag para o arquivo de destino src/main/assets/shaders/occlusion_object.frag.

Em occlusion_object.vert, adicione esta variável acima de main():

varying vec3 v_ScreenSpacePosition;

Defina esta variável na parte inferior de main():

v_ScreenSpacePosition = gl_Position.xyz / gl_Position.w;

Adicione estas variáveis acima de main() na parte superior do arquivo para atualizar o occlusion_object.frag:

varying vec3 v_ScreenSpacePosition;

uniform sampler2D u_Depth;
uniform mat3 u_UvTransform;
uniform float u_DepthTolerancePerMm;
uniform float u_OcclusionAlpha;
uniform float u_DepthAspectRatio;

Adicione essas funções auxiliares acima de main() no sombreador para facilitar o gerenciamento de informações de profundidade.

float GetDepthMillimeters(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(u_Depth, depth_uv).xyz;
  return dot(packedDepthAndVisibility.xy, vec2(255.0, 256.0 * 255.0));
}

// Returns linear interpolation position of value between min and max bounds.
// E.g., InverseLerp(1100, 1000, 2000) returns 0.1.
float InverseLerp(in float value, in float min_bound, in float max_bound) {
  return clamp((value - min_bound) / (max_bound - min_bound), 0.0, 1.0);
}

// Returns a value between 0.0 (not visible) and 1.0 (completely visible)
// Which represents how visible or occluded is the pixel in relation to the
// depth map.
float GetVisibility(in vec2 depth_uv, in float asset_depth_mm) {
  float depth_mm = GetDepthMillimeters(depth_uv);

  // Instead of a hard z-buffer test, allow the asset to fade into the
  // background along a 2 * u_DepthTolerancePerMm * asset_depth_mm
  // range centered on the background depth.
  float visibility_occlusion = clamp(0.5 * (depth_mm - asset_depth_mm) /
    (u_DepthTolerancePerMm * asset_depth_mm) + 0.5, 0.0, 1.0);

  // Depth close to zero is most likely invalid, do not use it for occlusions.
  float visibility_depth_near = 1.0 - InverseLerp(
      depth_mm, /*min_depth_mm=*/150.0, /*max_depth_mm=*/200.0);

  // Same for very high depth values.
  float visibility_depth_far = InverseLerp(
      depth_mm, /*min_depth_mm=*/7500.0, /*max_depth_mm=*/8000.0);

  float visibility =
    max(max(visibility_occlusion, u_OcclusionAlpha),
      max(visibility_depth_near, visibility_depth_far));

  return visibility;
}

Em seguida, atualize a função main() em occlusion_object.frag para facilitar o reconhecimento de profundidade e aplicar a oclusão. Adicione as linhas a seguir na parte inferior do arquivo:

const float kMToMm = 1000.0;
float asset_depth_mm = v_ViewPosition.z * kMToMm * -1.;
vec2 depth_uvs = (u_UvTransform * vec3(v_ScreenSpacePosition.xy, 1)).xy;
gl_FragColor.a *= GetVisibility(depth_uvs, asset_depth_mm);

Agora que temos uma nova versão dos sombreadores de objeto, podemos modificar o código do renderizador.

Como renderizar a oclusão de objetos

Em seguida, copie a classe ObjectRenderer, localizada em src/main/java/com/google/ar/core/codelab/common/rendering/ObjectRenderer.java.

Selecione a classe ObjectRenderer. Clique com o botão direito do mouse em "Copy". Selecione a pasta rendering. Clique com o botão direito do mouse em "Paste".

Renomeie a classe como OcclusionObjectRenderer.

Agora, a nova classe renomeada será exibida na mesma pasta.

Abra o OcclusionObjectRenderer.java recém-criado e mude os caminhos do sombreador na parte superior do arquivo:

private static final String VERTEX_SHADER_NAME = "shaders/occlusion_object.vert";
private static final String FRAGMENT_SHADER_NAME = "shaders/occlusion_object.frag";

Adicione essas variáveis de membro relacionadas à profundidade na parte superior da classe:

// Shader location: depth texture
private int depthTextureUniform;

// Shader location: transform to depth uvs
private int depthUvTransformUniform;

// Shader location: depth tolerance property
private int depthToleranceUniform;

// Shader location: maximum transparency for the occluded part.
private int occlusionAlphaUniform;

private int depthAspectRatioUniform;

private float[] uvTransform = null;
private int depthTextureId;

As variáveis acima ajustam a nitidez da borda de oclusão. Comece criando essas variáveis de membro com valores padrão na parte superior da classe:

// These values will be changed each frame based on the distance to the object.
private float depthAspectRatio = 0.0f;
private final float depthTolerancePerMm = 0.015f;
private final float occlusionsAlpha = 0.0f;

Inicialize os parâmetros uniformes para o sombreador no método createOnGlThread():

// Occlusions Uniforms.  Add these lines before the first call to ShaderUtil.checkGLError
// inside the createOnGlThread() method.
depthTextureUniform = GLES20.glGetUniformLocation(program, "u_Depth");
depthUvTransformUniform = GLES20.glGetUniformLocation(program, "u_UvTransform");
depthToleranceUniform = GLES20.glGetUniformLocation(program, "u_DepthTolerancePerMm");
occlusionAlphaUniform = GLES20.glGetUniformLocation(program, "u_OcclusionAlpha");
depthAspectRatioUniform = GLES20.glGetUniformLocation(program, "u_DepthAspectRatio");

Para garantir que esses valores serão atualizados sempre que forem exibidos, atualize o método draw() para incluir:

// Add after other GLES20.glUniform calls inside draw().
GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, depthTextureId);
GLES20.glUniform1i(depthTextureUniform, 1);
GLES20.glUniformMatrix3fv(depthUvTransformUniform, 1, false, uvTransform, 0);
GLES20.glUniform1f(depthToleranceUniform, depthTolerancePerMm);
GLES20.glUniform1f(occlusionAlphaUniform, occlusionsAlpha);
GLES20.glUniform1f(depthAspectRatioUniform, depthAspectRatio);

Adicione as linhas a seguir em draw() para ativar o modo de mesclagem na renderização, para que a transparência possa ser aplicada em objetos virtuais quando durante a oclusão:

// Add these lines just below the code-block labeled "Enable vertex arrays"
GLES20.glEnable(GLES20.GL_BLEND);
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
// Add these lines just above the code-block labeled "Disable vertex arrays"
GLES20.glDisable(GLES20.GL_BLEND);
GLES20.glDepthMask(true);

Adicione os métodos a seguir para que os autores das chamadas do OcclusionObjectRenderer possam fornecer informações de profundidade.

// Add these methods at the bottom of the OcclusionObjectRenderer class.
public void setUvTransformMatrix(float[] transform) {
  uvTransform = transform;
}

public void setDepthTexture(int textureId, int width, int height) {
  depthTextureId = textureId;
  depthAspectRatio = (float) width / (float) height;
}

Como controlar a oclusão de objetos

Agora que temos um novo OcclusionObjectRenderer, podemos adicioná-lo à DepthCodelabActivity e escolher quando e como implantar a renderização de oclusão.

Essa lógica é ativada ao adicionar uma instância do OcclusionObjectRenderer à atividade, de modo que o ObjectRenderer e o OcclusionObjectRenderer sejam membros da DepthCodelabActivity:

// Add this include at the top of the file.
import com.google.ar.core.codelab.common.rendering.OcclusionObjectRenderer;
// Add this member just below the existing "virtualObject", so both are present.
private final OcclusionObjectRenderer occludedVirtualObject = new OcclusionObjectRenderer();

Podemos controlar quando esse occludedVirtualObject será usado com base na compatibilidade do dispositivo atual com a API Depth. Adicione as linhas a seguir ao método onSurfaceCreated, abaixo da configuração do virtualObject:

if (isDepthSupported) {
  occludedVirtualObject.createOnGlThread(/*context=*/ this, "models/andy.obj", "models/andy.png");
  occludedVirtualObject.setDepthTexture(
     depthTexture.getDepthTexture(),
     depthTexture.getDepthWidth(),
     depthTexture.getDepthHeight());
  occludedVirtualObject.setMaterialProperties(0.0f, 2.0f, 0.5f, 6.0f);
}

Em dispositivos sem compatibilidade com a profundidade, a instância occludedVirtualObject é criada, mas não será usada. Em smartphones compatíveis com a profundidade, as duas versões serão inicializadas e o renderizador será usado para a exibição será definido durante a execução.

No método onDrawFrame(), localize o código já existente:

virtualObject.updateModelMatrix(anchorMatrix, scaleFactor);
virtualObject.draw(viewmtx, projmtx, colorCorrectionRgba, OBJECT_COLOR);

Substitua esse código por este:

if (isDepthSupported) {
  occludedVirtualObject.updateModelMatrix(anchorMatrix, scaleFactor);
  occludedVirtualObject.draw(viewmtx, projmtx, colorCorrectionRgba, OBJECT_COLOR);
} else {
  virtualObject.updateModelMatrix(anchorMatrix, scaleFactor);
  virtualObject.draw(viewmtx, projmtx, colorCorrectionRgba, OBJECT_COLOR);
}

Por fim, confira se a imagem de profundidade está mapeada corretamente para a renderização de saída. Como a imagem de profundidade tem uma resolução diferente e, possivelmente, uma proporção diferente da tela, as coordenadas de textura podem ser diferentes da imagem da câmera.

Adicione um método auxiliar getTextureTransformMatrix() à parte inferior do arquivo. Esse método retorna uma matriz de transformação que, quando aplicada, faz os UVs do espaço de tela corresponderem corretamente às coordenadas de textura quad usadas para renderizar o feed da câmera. Ele leva a orientação do dispositivo em consideração.

private static float[] getTextureTransformMatrix(Frame frame) {
  float[] frameTransform = new float[6];
  float[] uvTransform = new float[9];
  // XY pairs of coordinates in NDC space that constitute the origin and points along the two
  // principal axes.
  float[] ndcBasis = {0, 0, 1, 0, 0, 1};

  // Temporarily store the transformed points into outputTransform.
  frame.transformCoordinates2d(
      Coordinates2d.OPENGL_NORMALIZED_DEVICE_COORDINATES,
      ndcBasis,
      Coordinates2d.TEXTURE_NORMALIZED,
      frameTransform);

  // Convert the transformed points into an affine transform and transpose it.
  float ndcOriginX = frameTransform[0];
  float ndcOriginY = frameTransform[1];
  uvTransform[0] = frameTransform[2] - ndcOriginX;
  uvTransform[1] = frameTransform[3] - ndcOriginY;
  uvTransform[2] = 0;
  uvTransform[3] = frameTransform[4] - ndcOriginX;
  uvTransform[4] = frameTransform[5] - ndcOriginY;
  uvTransform[5] = 0;
  uvTransform[6] = ndcOriginX;
  uvTransform[7] = ndcOriginY;
  uvTransform[8] = 1;

  return uvTransform;
}

O método getTextureTransformMatrix() exige esta importação na parte superior do arquivo:

import com.google.ar.core.Coordinates2d;

Queremos calcular a transformação entre essas coordenadas de textura sempre que a textura da tela mudar, por exemplo, se a tela girar. Essa funcionalidade é controlada. Adicione a sinalização a seguir na parte superior do arquivo:

// Add this member at the top of the file.
private boolean calculateUVTransform = true;

Em onDrawFrame(), verifique se a transformação armazenada precisa ser recalculada após a criação do frame e da câmera:

// Add these lines inside onDrawFrame() after frame.getCamera().
if (frame.hasDisplayGeometryChanged() || calculateUVTransform) {
  calculateUVTransform = false;
  float[] transform = getTextureTransformMatrix(frame);
  occludedVirtualObject.setUvTransformMatrix(transform);
}

Com essas mudanças em vigor, podemos executar o app com oclusão de objetos virtuais. Agora, o app funcionará corretamente em todos os smartphones e usará a profundidade para oclusão automaticamente, quando o dispositivo for compatível.

App em execução com compatibilidade com a API Depth

App em execução sem compatibilidade com a API Depth

[Opcional] Melhorar a qualidade da oclusão

O método de oclusão com base em profundidade, implementado acima, oferece a oclusão com limites nítidos. Conforme a câmera se afasta mais do objeto, as medidas de profundidade podem ser menos precisas, o que pode resultar em artefatos visuais. Podemos atenuar esse problema ao adicionar mais desfoque no teste de oclusão, resultando em uma borda mais suave ao ocultar objetos virtuais.

occlusion_object.frag

Adicione a variável uniforme a seguir na parte superior de occlusion_object.frag:

uniform float u_OcclusionBlurAmount;

Adicione esta função auxiliar logo acima de main() no sombreador, que aplica um desfoque de kernel à amostragem de oclusão:

float GetBlurredVisibilityAroundUV(in vec2 uv, in float asset_depth_mm) {
  // Kernel used:
  // 0   4   7   4   0
  // 4   16  26  16  4
  // 7   26  41  26  7
  // 4   16  26  16  4
  // 0   4   7   4   0
  const float kKernelTotalWeights = 269.0;
  float sum = 0.0;

  vec2 blurriness = vec2(u_OcclusionBlurAmount,
                         u_OcclusionBlurAmount * u_DepthAspectRatio);

  float current = 0.0;

  current += GetVisibility(uv + vec2(-1.0, -2.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(+1.0, -2.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(-1.0, +2.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(+1.0, +2.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(-2.0, +1.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(+2.0, +1.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(-2.0, -1.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(+2.0, -1.0) * blurriness, asset_depth_mm);
  sum += current * 4.0;

  current = 0.0;
  current += GetVisibility(uv + vec2(-2.0, -0.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(+2.0, +0.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(+0.0, +2.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(-0.0, -2.0) * blurriness, asset_depth_mm);
  sum += current * 7.0;

  current = 0.0;
  current += GetVisibility(uv + vec2(-1.0, -1.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(+1.0, -1.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(-1.0, +1.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(+1.0, +1.0) * blurriness, asset_depth_mm);
  sum += current * 16.0;

  current = 0.0;
  current += GetVisibility(uv + vec2(+0.0, +1.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(-0.0, -1.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(-1.0, -0.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(+1.0, +0.0) * blurriness, asset_depth_mm);
  sum += current * 26.0;

  sum += GetVisibility(uv , asset_depth_mm) * 41.0;

  return sum / kKernelTotalWeights;
}

Substitua a linha já existente em main():

gl_FragColor.a *= GetVisibility(depth_uvs, asset_depth_mm);

por:

gl_FragColor.a *= GetBlurredVisibilityAroundUV(depth_uvs, asset_depth_mm);

Atualize o renderizador para aproveitar essa nova funcionalidade de sombreador.

OcclusionObjectRenderer.java

Adicione as variáveis de membro a seguir à parte superior da classe:

private int occlusionBlurUniform;
private final float occlusionsBlur = 0.01f;

Adicione o código a seguir ao método createOnGlThread:

// Add alongside the other calls to GLES20.glGetUniformLocation.
occlusionBlurUniform = GLES20.glGetUniformLocation(program, "u_OcclusionBlurAmount");

Adicione o código a seguir ao método draw:

// Add alongside the other calls to GLES20.glUniform1f.
GLES20.glUniform1f(occlusionBlurUniform, occlusionsBlur);

Comparação visual

O limite de oclusão agora será mais suave com essas mudanças.

Criar e executar o app

Siga estas etapas para criar e executar o app:

  1. Conecte um dispositivo Android via USB.
  2. Selecione File > Build and Run.
  3. Salve como: ARCodeLab.apk.
  4. Espere o app ser criado e implantado no dispositivo.

A primeira vez que você tentar implantar o app no seu dispositivo, precisará

Ativar a depuração USB

no dispositivo. Selecione "OK" para continuar.

Na primeira vez que você executar o app no dispositivo, ele pedirá permissão para usar a câmera. É necessário autorizar o acesso para continuar usando a funcionalidade de RA.

Como testar seu app

Ao executar o app, você pode testar o comportamento básico dele segurando o dispositivo, movendo-o pelo espaço e digitalizando lentamente uma área. Tente coletar pelo menos 10 segundos de dados e digitalizar a área de várias direções antes de continuar para a próxima etapa.

Parabéns! Você criou e executou seu primeiro app de realidade aumentada com base em profundidade usando a API Depth do ARCore do Google.

Como configurar o dispositivo Android para desenvolvimento

  1. Conecte o dispositivo à máquina de desenvolvimento usando um cabo USB. Se você usa o Windows para desenvolver, pode ser necessário instalar o driver USB adequado para o dispositivo.
  2. Siga estas etapas para ativar a depuração USB na janela Opções do desenvolvedor:
  • Abra o app Configurações.
  • Se o dispositivo usa o Android v8.0 ou versão posterior, selecione System. Caso contrário, avance para a próxima etapa.
  • Navegue até a parte inferior da tela e selecione Sobre o dispositivo.
  • Navegue até a parte inferior da tela e toque em Número da versão sete vezes.
  • Volte à tela anterior, navegue até a parte inferior e toque em Opções do desenvolvedor.
  • Na janela Opções do desenvolvedor, role para baixo para encontrar e ativar a depuração USB.

Veja informações mais detalhadas sobre esse processo no site para desenvolvedores Android do Google.

1480e83e227b94f1.png

Se você encontrar um erro de compilação relacionado a licenças, comoFailed to install the following Android SDK packages as some licences have not been accepted, use os comandos a seguir para revisar e aceitar essas licenças:

cd <path to Android SDK>

tools/bin/sdkmanager --licenses

Perguntas frequentes