Usar a API Depth do ARCore para experiências imersivas de realidade aumentada

1. Antes de começar

O ARCore é uma plataforma para criar apps de realidade aumentada (RA) para dispositivos móveis. Usando APIs diferentes, o ARCore permite que o dispositivo de um usuário observe e receba informações sobre o ambiente e interaja com essas informações.

Neste codelab, você verá o processo de criação de um app simples compatível com RA que utiliza a API Depth do ARCore.

Pré-requisitos

Este codelab foi escrito para desenvolvedores com conhecimento sobre os conceitos fundamentais de RA.

O que você vai criar

d9bd5136c54ce47a.gif

Você vai 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. Você vai seguir estas etapas específicas:

  • Conferir se há compatibilidade com a API Depth no smartphone
  • Extrair a imagem de profundidade de cada frame
  • Visualizar informações de profundidade de várias maneiras (veja a animação acima)
  • Usar a profundidade para aumentar o realismo de apps com oclusão
  • Gerenciar corretamente smartphones sem compatibilidade com a API Depth

Pré-requisitos

Requisitos de hardware

Requisitos de software

2. ARCore e API Depth

A API Depth usa a câmera RGB de um dispositivo compatível para criar mapas de profundidade (também chamados de imagens de profundidade). Você pode utilizar as informações disponibilizadas por um mapa de profundidade para que os objetos virtuais apareçam com precisão em frente ou atrás de objetos do mundo real, possibilitando experiências do usuário imersivas e realistas.

A API Depth do ARCore oferece acesso a imagens de profundidade correspondentes a cada frame exibido pela sessão do ARCore. Cada pixel tem uma medida da distância da câmera em relação ao ambiente, o que aumenta o realismo do seu app de RA.

Um dos principais recursos da API Depth é a oclusão: a capacidade de objetos digitais aparecerem com precisão em relação a objetos reais. Isso faz os objetos parecerem estar no ambiente com o usuário.

Este codelab orientará você no processo de criação de um app compatível com RA que usa imagens de profundidade para realizar a oclusão de objetos virtuais atrás de superfícies do mundo real e visualizar a geometria detectada do espaço.

3. Começar a configuração

Configurar a máquina de desenvolvimento

  1. Conecte o dispositivo ARCore ao computador usando o cabo USB. Verifique se a depuração USB está ativada no dispositivo.
  2. 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.

Fazer o download e instalar o código

  1. 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:

  1. Inicie o Android Studio e clique em Open an existing Android Studio project.
  2. Encontre o diretório em que você extraiu o arquivo ZIP depois de ter feito o download acima e abra o 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.

4. Executar o app inicial

  1. Clique em Run > Run… > ‘part0_work'. Na caixa de diálogo Select Deployment Target exibida, o dispositivo estará listado em Connected Devices.
  2. Selecione seu dispositivo e clique em OK. O Android Studio criará o app inicial e o executará no dispositivo.
  3. O app solicitará permissões de uso da câmera. Toque em Permitir para continuar.

c5ef65f7a1da0d9.png

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.

No momento, seu app é 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.

6182cf62be13cd97.png beb0d327205f80ee.png e4497751c6fad9a7.png

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

5. Conferir se há compatibilidade com a API Depth (Parte 1)

A API Depth do ARCore pode ser executada em um subconjunto de dispositivos compatíveis. Antes de integrar a funcionalidade em um app usando essas imagens de profundidade, é preciso 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.

Adicione outra mensagem ao snackbar. Ela aparecerá 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]";

Dentro de onDrawFrame(), você pode 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:

5c878a7c27833cb2.png

Em seguida, você atualizará o app para chamar a API Depth e extrair imagens de profundidade para cada frame.

6. Recuperar as imagens de profundidade (Parte 2)

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

Agora você usará essas imagens de profundidade para melhorar a renderização e a visualização no app. A primeira etapa é extrair a imagem de cada frame e vincular a textura para que ela seja utilizada pela GPU.

Primeiro, adicione uma nova classe ao seu projeto.
DepthTextureHandler é responsável pela recuperação da imagem de profundidade de 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;
  }
}

Agora, você vai adicionar uma instância dessa classe à DepthCodelabActivity, garantindo que terá 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, atualize o método onSurfaceCreated() para inicializar essa textura de forma que seja usada pelos sombreadores da GPU:

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

Por fim, preencha essa textura em todos os frames com a imagem de profundidade mais recente, chamando o método update() criado acima no último frame extraído da session.
Como a compatibilidade com profundidade é opcional para esse app, só use essa chamada se você estiver utilizando a profundidade.

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

Agora você tem uma imagem de profundidade que será atualizada a cada frame. Ela está pronta para ser usada pelos seus sombreadores.

No entanto, nada mudou ainda no comportamento do app. Agora, você usará a imagem de profundidade para melhorar seu app.

7. Renderizar a imagem de profundidade (Parte 3)

Agora que você tem uma imagem de profundidade, pode ver como ela é. Nesta seção, você adicionará 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.

Adicionar um novo sombreador .vert

No Android Studio:

  1. Primeiro, adicione novos sombreadores .vert e .frag ao diretório src/main/assets/shaders/.
  2. Clique com o botão direito no diretório de sombreadores.
  3. Selecione New -> File.
  4. Nomeie o arquivo como background_show_depth_map.vert.
  5. 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 etapas acima para criar o sombreador de fragmento no mesmo diretório e o chame de background_show_depth_map.frag.

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 você pode renderizar o mapa de profundidade, use-o. Adicione um botão que ativa e desativa essa renderização.

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 (ela fica 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() na DepthCodelabActivity, adicione:

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

Transmita a textura de profundidade para o backgroundRenderer adicionando esta linha a onSurfaceCreated():

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

Agora, você pode 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. Os pixels azuis representam áreas distantes.

Há muitas maneiras de expressar informações de profundidade. Nesta seção, você modificará o sombreador para que ele possa pulsar periodicamente, modificando o sombreador para mostrar a profundidade apenas nas faixas que se afastam da câmera repetidamente.

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, você pode 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.

8. Usar a API Depth para oclusão (Parte 4)

Agora, você resolverá a oclusão de objetos no seu app.

Oclusão é o que acontece quando o objeto virtual não pode ser completamente renderizado, porque há objetos reais entre o objeto virtual e a câmera. Resolver a oclusão é essencial para que as experiências de RA sejam imersivas.

A renderização adequada de objetos virtuais em tempo real 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, você atualizará seu app para incluir objetos virtuais somente se a profundidade estiver disponível.

Como adicionar novos sombreadores de objeto

Como nas seções anteriores, você vai adicionar novos sombreadores para oferecer compatibilidade com informações de profundidade. Dessa vez, é possível copiar os sombreadores de objetos já existentes e incluir a funcionalidade de oclusão.

É importante manter as duas versões dos sombreadores de objeto. Assim, o app poderá decidir aceitar a profundidade durante a execução.

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

  • Copie object.vert para o arquivo de destino src/main/assets/shaders/occlusion_object.vert.
  • 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;
  • Inclua essas funções auxiliares acima de main() no sombreador para facilitar o gerenciamento das 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 você tem uma nova versão dos sombreadores de objeto, modifique 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 > Copiar.
  • Selecione a pasta rendering.
  • Clique com o botão direito do mouse > Colar.

6c87dcb87da558c1.png

  • Renomeie a classe como OcclusionObjectRenderer.

f2ffe488c81ad404.png

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

e5bf1c158e26c322.png

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. As variáveis vão ajustar a nitidez da borda de oclusão.
// 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;

Crie 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 que esses valores sejam atualizados sempre que exibidos, atualize o método draw():
// 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 de OcclusionObjectRenderer possam exibir 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 você tem um novo OcclusionObjectRenderer, é possível adicioná-lo à DepthCodelabActivity e escolher quando e como implantar a renderização de oclusão.

Ative essa lógica incluindo uma instância de OcclusionObjectRenderer à atividade, de modo que ObjectRenderer e OcclusionObjectRenderer sejam membros de 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();
  • É possível 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 de 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 existente:

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

Substitua-o 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 o 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 também 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, você pode executar o app com oclusão de objetos virtuais.

Agora, ele vai funcionar corretamente em todos os smartphones e vai 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

9. [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 essa linha existente em main():

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

pela linha:

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.

10. Build-Run-Test

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.

Na primeira vez que você tentar implantar o app no seu dispositivo:

  • Será necessário permitir a depuração USB no dispositivo. Selecione "OK" para continuar.
  • O app perguntará se você tem permissão para usar a câmera do dispositivo. Permita 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.

Solução de problemas

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:
  3. Abra o app Configurações.
  4. Se o dispositivo usa o Android v8.0 ou versão posterior, selecione System. Caso contrário, avance para a próxima etapa.
  5. Navegue até a parte inferior da tela e selecione Sobre o dispositivo.
  6. Navegue até a parte inferior da tela e toque em Número da versão sete vezes.
  7. Volte à tela anterior, navegue até a parte inferior e toque em Opções do desenvolvedor.
  8. 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

11. Parabéns

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

Perguntas frequentes