ARCore Depth

ARCore es una plataforma que permite crear apps de realidad aumentada (RA) en dispositivos móviles. La API de Depth de ARCore de Google brinda acceso a una imagen de profundidad que corresponde a cada fotograma proporcionado por la sesión de ARCore. Cada píxel proporciona una medida de distancia entre la cámara y el entorno que usaremos en este codelab a fin de perfeccionar el realismo en una app de RA.

La API de Depth solo es compatible con un subconjunto de dispositivos que admiten ARCore. Consulta esta lista para conocer qué teléfonos son compatibles con las llamadas de profundidad. La API de Depth solo está disponible en Android.

En este codelab, se explica el proceso para crear una app simple que admite RA, usa imágenes de profundidad para ocultar los recursos virtuales detrás de superficies reales y visualiza la geometría que detecta en el mundo.

Qué compilarás

d9bd5136c54ce47a.gif

Con este codelab, compilarás una app que usa imágenes de profundidad de cada fotograma para visualizar la geometría de la escena y ocultar los recursos virtuales agregados. En este codelab, se explican los pasos específicos para realizar las siguientes acciones:

  1. Cómo comprobar si un teléfono es compatible con la API de Depth
  2. Cómo recuperar imágenes de profundidad de cada fotograma
  3. Distintas formas de visualizar la información de profundidad (consulta la animación anterior)
  4. Cómo usar profundidad para aumentar el realismo de las apps con oclusión
  5. Cómo controlar de forma óptima los teléfonos que no admiten la API de Depth

Nota: Si tienes algún problema durante el codelab, ve a la última sección para obtener algunas sugerencias.

Necesitarás hardware y software específicos para completar este codelab.

Requisitos de hardware

Requisitos de software

Cómo configurar la máquina de desarrollo

Conecta el dispositivo ARCore a la computadora mediante el cable USB. Asegúrate de habilitar la depuración por USB en el dispositivo. Abre una terminal y ejecuta adb devices, como se muestra a continuación:

adb devices

List of devices attached
<DEVICE_SERIAL_NUMBER>    device

El <DEVICE_SERIAL_NUMBER> será una string única para tu dispositivo. Antes de continuar, asegúrate de ver únicamente un dispositivo.

Cómo descargar e instalar el código

Puedes optar por clonar el repositorio de la siguiente manera:

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

O bien, puedes descargar este archivo ZIP y extraerlo:

Descargar ZIP

Inicia Android Studio. Haz clic en Open an existing Android Studio project. Luego, navega hasta el directorio en el que extrajiste el archivo ZIP que descargaste antes y haz doble clic en el directorio depth_codelab_io2020.

Este es un solo proyecto de Gradle que contiene varios módulos. Si el panel Project no se muestra en la parte superior izquierda de Android Studio, haz clic en Projects desde el menú desplegable. El resultado debería verse como el siguiente:

Este proyecto contiene los siguientes módulos:

  • part0_work: Es la app de inicio. Las modificaciones que se detallan en el codelab deben realizarse en este módulo.
  • part1: Es un código de referencia de cómo se deberían ver tus modificaciones cuando completes la Parte 1.
  • part2: Es un código de referencia que obtienes cuando completas la Parte 2.
  • part3: Es un código de referencia que obtienes cuando completas la Parte 3.
  • part4_completed: Es la versión final de la app. Es un código de referencia que obtienes cuando la Parte 4 y este codelab están completos.

Trabajarás en el módulo part0_work. También te brindamos soluciones completas para cada parte del codelab. Cada módulo es una app que se puede compilar.

Haz clic en Run > Run… > 'part0_work'. En el diálogo Select Deployment Target, tu dispositivo debería aparecer en la sección Connected Devices. Elige tu dispositivo y haz clic en OK. Android Studio compilará la app inicial y la ejecutará en tu dispositivo.

Cuando ejecutes la app por primera vez, se te solicitará permiso para acceder a la cámara. Presiona Allow para continuar.

Cómo utilizar la app

  1. Mueve el dispositivo para que la app detecte un plano. El mensaje que se muestra en la parte inferior indica cuándo debes moverlo.
  2. Presiona algún punto del plano para fijar un ancla. Allí se dibujará una figura de Android. Esta app solo te permite colocar un ancla a la vez.
  3. Mueve el dispositivo. La figura debe permanecer en el mismo lugar, aunque el dispositivo se mueva.

Por ahora, la app es muy simple y no puede reconocer la geometría del mundo real. Por ejemplo, si colocas una figura de Android detrás de una silla, la renderización aparecerá delante de ella, dado que la app no reconoce que la silla está allí y que debería ocultar la figura.

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

Para solucionar este problema, usaremos la API de Depth que nos permitirá mejorar la interactividad y el realismo de esta app.

La API de Depth de ARCore se ejecuta en un subconjunto de dispositivos compatibles. Antes de agregar funcionalidad a la app que usa estas imágenes de profundidad, primero debemos asegurarnos de que la app se ejecute en un dispositivo compatible.

Agrega un miembro privado a DepthCodelabActivity que funcionará como marca y que indicará si el dispositivo actual admite profundidad:

private boolean isDepthSupported;

Se puede propagar la marca desde la función onResume(), en la que se crea una nueva sesión. Busca el siguiente código existente:

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

Actualiza el código de la siguiente manera:

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

Ahora la sesión de RA se configuró correctamente y la app sabe si puede usar funciones basadas en profundidad.

También debemos informar al usuario si se usará profundidad en esta sesión. Podemos agregar otro mensaje a la barra de notificaciones que aparece en la parte inferior de la pantalla.

// 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(), podemos presentar este mensaje según sea necesario.

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

Si la app se ejecuta en un dispositivo que no admite profundidad, el mensaje que agregamos aparecerá en la parte inferior, como se muestra a continuación:

feb1c9f42f3cf396.png

Ahora actualizaremos la app para llamar a la API de Depth y recuperar imágenes de profundidad de cada fotograma.

La API de Depth captura imágenes en 3D de tu entorno y, con esos datos, le proporciona una imagen de profundidad a la app. Cada píxel de esta imagen representa una medida de distancia entre la cámara y el entorno real.

Este codelab usa las imágenes de profundidad para mejorar la renderización y la visualización de la app. El primer paso consiste en recuperar la imagen de profundidad de cada fotograma y enlazar esa textura de modo que la GPU pueda usarla.

Primero, agregaremos una clase nueva a nuestro proyecto. DepthTextureHandler es responsable de recuperar la imagen de profundidad de un determinado fotograma de ARCore. Agrega el siguiente archivo:

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

Luego, podemos agregar una instancia de esta clase a DepthCodelabActivity y asegurarnos de tener una copia de fácil acceso de la imagen de profundidad de cada fotograma.

En DepthCodelabActivity.java, agrega una instancia de la clase nueva como una variable de miembro privado.

private final DepthTextureHandler depthTexture = new DepthTextureHandler();

A continuación, verás cómo actualizar el método onSurfaceCreated() a fin de inicializar la textura para poder usarla con los sombreadores de la GPU:

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

Por último, vamos a propagar esta textura en cada fotograma con la última imagen de profundidad. Para ello, llamaremos al método update() creado anteriormente en el último fotograma que recuperamos a partir de session. Debido a que la compatibilidad con la profundidad es opcional en esta app, solo debes realizar esta llamada si usarás profundidad.

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

Ahora tenemos una imagen de profundidad que se actualiza en cada fotograma y que ya puede usarse con nuestros sombreadores. Sin embargo, aún no cambiamos el comportamiento de la app. Usemos la imagen de profundidad a fin de mejorar la app.

Tenemos una imagen de profundidad con la que podemos experimentar. Veamos cómo luce. En esta sección, agregaremos un botón a la app para renderizar la profundidad de cada fotograma.

Cómo agregar sombreadores nuevos

Hay muchas formas de ver una imagen de profundidad. Los siguientes sombreadores ofrecen una visualización simple del mapa de colores.

Primero, agrega nuevos sombreadores .vert y .frag en el directorio src/main/assets/shaders/.

Cómo agregar un sombreador .vert nuevo

En Android Studio:

  1. Haz clic con el botón derecho en el directorio de los sombreadores.
  2. Selecciona New -> File
  3. Asígnale el nombre background_show_depth_map.vert.
  4. Configúralo como un archivo de texto.

En el archivo nuevo, agrega el siguiente 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;
}

Repite los pasos anteriores para crear el sombreador de fragmentos en el mismo directorio y asígnale el nombre background_show_depth_map.frag. Luego, agrega el siguiente código a este archivo nuevo:

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

A continuación, actualiza la clase BackgroundRenderer para usar los sombreadores nuevos ubicados en src/main/java/com/google/ar/core/codelab/common/rendering/BackgroundRenderer.java.

En la parte superior de la clase, agrega las rutas de acceso del archivo a los sombreadores.

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

Agrega más variables de miembro a la clase BackgroundRenderer, ya que ejecutará dos 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;

Agrega un método nuevo para propagar estos campos de la siguiente manera:

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

Agrega el siguiente método, que se usa para dibujar con los sombreadores en cada fotograma:

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

Agrega un botón de activación

Ahora que podemos renderizar el mapa de profundidad, vamos a usarlo. Dentro de DepthCodelabActivity, podemos agregar un botón que active y desactive la renderización.

En la parte superior del archivo DepthCodelabActivity, agrega la importación que usará el botón.

import android.widget.Button;

Actualiza la clase para agregar un miembro booleano que indique si la renderización de profundidad está desactivada (de forma predeterminada) o activada.

private boolean showDepthMap = false;

Luego, agrega el botón que controla el valor booleano showDepthMap al final del 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);
          }
        });

Agrega las siguientes strings a 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>

En res/layout/activity_main.xml, agrega el siguiente botón en la parte inferior del diseño de la app:

<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"/>

El botón ahora controla el valor del showDepthMap booleano. Usa esta marca para controlar si se renderizará el mapa de profundidad. Vuelve al método onDrawFrame() en DepthCodelabActivity y agrega lo siguiente:

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

Para pasar la textura de profundidad a backgroundRenderer, agrega la siguiente línea en onSurfaceCreated():

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

Ahora podemos ver la imagen de profundidad de cada fotograma presionando el botón ubicado en la parte superior derecha de la pantalla.

Sin compatibilidad con la API de Depth

Con compatibilidad con la API de Depth

(Opcional) Animación de profundidad sofisticada

Por el momento, la app muestra directamente el mapa de profundidad. Los píxeles rojos representan las áreas cercanas, mientras que los azules representan las áreas que están más lejos.

Existen varias formas de transmitir información de profundidad. En esta subsección, modificamos el sombreador a fin de "emitir" la profundidad de manera periódica y solo mostrar la profundidad dentro de las bandas que se alejan continuamente de la cámara.

Para comenzar, agrega estas variables en la parte superior de background_show_depth_map.frag:

uniform float u_DepthRangeToRenderMm;
const float kDepthWidthToRenderMm = 350.0;

Luego, usa estos valores para filtrar los píxeles que se deben cubrir con los valores de profundidad en la función main() del 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);

A continuación, actualiza BackgroundRenderer.java para conservar estos parámetros del sombreador. Agrega los siguientes campos a la parte superior de la clase:

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

Dentro del método createDepthShaders(), agrega lo siguiente para hacer coincidir los parámetros con el programa de sombreador:

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

Por último, podemos controlar este rango con el tiempo dentro del método drawDepth(). Agrega el código que se muestra a continuación, que aumentará este rango con cada fotograma que se dibuje.

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

Ahora, la profundidad se visualiza como una animación de pulsación que recorre la escena.

37e2a86b833150f8.gif

Puedes cambiar los valores proporcionados para hacer que la pulsación sea más lenta, más rápida, más amplia, más angosta, etc. También puedes explorar otras maneras de cambiar la forma en que el sombreador muestra la información de profundidad.

La oclusión ocurre cuando no se renderiza por completo un objeto virtual, ya que hay superficies reales entre el objeto virtual y la cámara.

Renderizar de forma correcta los objetos virtuales en el entorno hace que la escena aumentada luzca más real y creíble. Si deseas obtener más ejemplos, mira nuestro video sobre cómo combinar realidades con la API de Depth.

En esta sección, actualizamos nuestra app para incluir objetos virtuales si la profundidad está disponible.

Cómo agregar nuevos sombreadores de objetos

Al igual que en las secciones anteriores, agregaremos sombreadores nuevos para brindar compatibilidad con la información de profundidad. Esta vez, copiaremos los sombreadores de objetos existentes y agregaremos la funcionalidad de oclusión. Es importante mantener ambas versiones de los sombreadores de objetos para que la app pueda decidir si admite profundidad en el entorno de ejecución.

Haz copias de los archivos de sombreador object.vert y object.frag en el directorio src/main/assets/shaders.

  1. Copia object.vert en el archivo de destino src/main/assets/shaders/occlusion_object.vert.
  2. Copia object.frag en el archivo de destino src/main/assets/shaders/occlusion_object.frag.

Dentro de occlusion_object.vert, agrega la siguiente variable arriba de main():

varying vec3 v_ScreenSpacePosition;

Configura esta variable en la parte inferior de main().

v_ScreenSpacePosition = gl_Position.xyz / gl_Position.w;

Agrega estas variables arriba de main() en la parte superior del archivo para actualizar 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;

En el sombreador, agrega las siguientes funciones auxiliares por encima de main() para facilitar el manejo de la información de profundidad.

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

Ahora actualiza main() en occlusion_object.frag para que reconozca la profundidad y aplique la oclusión. Agrega las siguientes líneas en la parte inferior del archivo:

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

Ahora que tenemos una versión nueva de nuestros sombreadores de objetos, podemos modificar el código del procesador.

Cómo renderizar la oclusión de objetos

Haz una copia de la clase ObjectRenderer, que se encuentra en src/main/java/com/google/ar/core/codelab/common/rendering/ObjectRenderer.java.

Selecciona la clase ObjectRenderer. Haz clic con el botón derecho en > "Copy". Selecciona la carpeta rendering. Haz clic con el botón derecho en > "Paste".

Cambia el nombre de la clase a OcclusionObjectRenderer.

La clase nueva que renombramos aparecerá en la misma carpeta.

Abre el objeto OcclusionObjectRenderer.java recién creado y cambia las rutas del sombreador en la parte superior del archivo.

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

En la parte superior de la clase, agrega las siguientes variables de miembro relacionadas con la profundidad con las demás:

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

Esas variables ajustan la nitidez del borde de la oclusión. Para comenzar, crea las siguientes variables de miembro con valores predeterminados en la parte superior de la clase:

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

Inicializa los parámetros uniformes del sombreador en el 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");

Asegúrate de que estos valores se actualicen cada vez que se dibujen. Para ello, actualiza el método draw() a fin de incluir lo siguiente:

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

Habilita el modo de combinación en la renderización con las siguientes líneas dentro de draw() para que la transparencia pueda aplicarse a la oclusión de los objetos virtuales:

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

Agrega los siguientes métodos, de manera que los llamadores de OcclusionObjectRenderer puedan proporcionar información de profundidad:

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

Cómo controlar la oclusión de objetos

Ahora que tenemos un nuevo objeto OcclusionObjectRenderer, podemos agregarlo a nuestra DepthCodelabActivity y elegir cuándo y cómo renderizar con oclusión.

Para habilitar esta lógica, agrega una instancia de OcclusionObjectRenderer a la actividad, de modo que ObjectRenderer y OcclusionObjectRenderer sean miembros 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();

Ahora podemos controlar cuándo se usa occludedVirtualObject en función de si el dispositivo actual es compatible con la API de Depth o no. Incluye las siguientes líneas dentro del método onSurfaceCreated en la parte inferior donde se configura 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);
}

Si el dispositivo no admite profundidad, se crea la instancia occludedVirtualObject, pero no se usa. En los teléfonos que sí admiten profundidad, se inicializan ambas versiones y se decide en el tiempo de ejecución qué versión del procesador se usará para el dibujo.

Dentro del método onDrawFrame(), busca el siguiente código existente:

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

Reemplaza ese código por lo siguiente:

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 último, asegúrate de que la imagen de profundidad esté mapeada de forma correcta a la renderización de salida. Debido a que la imagen de profundidad tiene una resolución y, posiblemente, una relación de aspecto diferentes a las de la pantalla, es posible que las coordenadas de la textura difieran entre sí y a las de la imagen de la cámara.

Agrega un método auxiliar getTextureTransformMatrix() al final del archivo. Este método muestra una matriz de transformación que, cuando se aplica, hace coincidir de manera adecuada los UV del espacio de la pantalla con las cuatro coordenadas de texturas que sirven para renderizar el feed de la cámara. Tiene en cuenta la orientación del dispositivo.

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

getTextureTransformMatrix() requiere la siguiente importación en la parte superior del archivo:

import com.google.ar.core.Coordinates2d;

Queremos calcular la transformación de las coordenadas de estas texturas cada vez que cambie la textura de la pantalla (por ejemplo, si se gira la pantalla). Esta funcionalidad se encuentra restringida. Incluye la siguiente marca en la parte superior del archivo:

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

Dentro de onDrawFrame(), comprueba si la transformación almacenada debe recalcularse después de crear el fotograma y la cámara.

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

Con estos cambios implementados, ahora podemos ejecutar la app con oclusión de objetos virtuales. La app debería funcionar bien en todos los teléfonos y usar automáticamente la profundidad para ocluir los objetos siempre que sea compatible.

App que admite la API de Depth

App que no admite la API de Depth

(Opcional) Cómo mejorar la calidad de oclusión

El método que implementamos antes proporciona una oclusión basada en profundidad con bordes nítidos. A medida que la cámara se aleja del objeto, las mediciones de profundidad pueden perder precisión, lo que podría generar anomalías visuales. A fin de mitigar este problema, agregaremos un mayor desenfoque a la prueba de oclusión, lo que aporta contornos más suaves a los objetos virtuales ocultos.

occlusion_object.frag

Incluye la siguiente variable uniforme en la parte superior de occlusion_object.frag:

uniform float u_OcclusionBlurAmount;

Agrega la siguiente función auxiliar arriba de main() en el sombreador, que aplica un desenfoque de kernel a la muestra de oclusión:

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

Reemplaza esta línea existente en main():

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

por la siguiente línea:

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

Actualiza el procesador para aprovechar esta funcionalidad nueva del sombreador.

OcclusionObjectRenderer.java

En la parte superior de la clase, agrega las siguientes variables de miembro:

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

Agrega lo siguiente al método createOnGlThread:

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

Agrega lo siguiente al método draw:

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

Comparación visual

Después de realizar estos cambios, el contorno de la oclusión debería verse más suave.

Cómo compilar y ejecutar la app

Para compilar y ejecutar tu app, sigue estos pasos:

  1. Conecta un dispositivo Android mediante USB.
  2. Selecciona File > Build and Run.
  3. Guárdala como: ARCodeLab.apk.
  4. Espera a que la app se compile y se implemente en el dispositivo.

La primera vez que intentes implementar la app en tu dispositivo, deberás seleccionar

Permitir depuración por USB

en el dispositivo. Selecciona Aceptar para continuar.

La primera vez que ejecutes la app en el dispositivo, se te preguntará si tiene permiso para usar la cámara. Debes permitir ese acceso para seguir usando la funcionalidad de RA.

Cómo probar la app

Si ejecutas la app, puedes probar su comportamiento básico. Para ello, sostén el dispositivo, muévete por el lugar y escanea lentamente el área. Procura obtener al menos 10 segundos de datos y examina el área desde distintas direcciones antes de avanzar al siguiente paso.

¡Felicitaciones! Creaste y ejecutaste con éxito tu primera app de realidad aumentada basada en profundidad con la API de Depth de ARCore de Google.

Cómo configurar un dispositivo Android para el desarrollo

  1. Conecta el dispositivo a la máquina de desarrollo con un cable USB. Si desarrollas la app en Windows, es posible que debas instalar el controlador USB adecuado para tu dispositivo.
  2. Completa los siguientes pasos a fin de habilitar la depuración por USB en la ventana Opciones para desarrolladores:
  • Abre la app de Configuración.
  • Si tu dispositivo usa Android 8.0 o una versión posterior, selecciona Sistema. De lo contrario, continúa con el paso siguiente.
  • Desplázate hasta la parte inferior y selecciona Acerca del teléfono.
  • Desplázate hasta la parte inferior y presiona Número de compilación siete veces.
  • Regresa a la pantalla anterior, desplázate hasta la parte inferior y presiona Opciones para desarrolladores.
  • En la ventana Opciones para desarrolladores, desplázate hacia abajo para buscar y habilitar la depuración por USB.

Obtén información más detallada sobre este proceso en el sitio web para desarrolladores de Android de Google.

1480e83e227b94f1.png

Si se produce un error de compilación relacionado con licencias (Failed to install the following Android SDK packages as some licences have not been accepted), usa los siguientes comandos a fin de revisar y aceptar las licencias:

cd <path to Android SDK>

tools/bin/sdkmanager --licenses

Preguntas frecuentes