Usa la API de Depth de ARCore para desarrollar experiencias de realidad aumentada envolvente

1. Antes de comenzar

ARCore es una plataforma que permite crear apps de realidad aumentada (RA) en dispositivos móviles. Con diferentes API, ARCore permite que el dispositivo de un usuario observe y reciba información sobre su entorno y, luego, interactúe con esa información.

En este codelab, aprenderás a crear una app sencilla compatible con RA que use la API de Depth de ARCore.

Requisitos

Este codelab está dirigido a desarrolladores que tienen conocimientos básicos de RA.

Qué crearás

d9bd5136c54ce47a.gif

Creará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. Para ello, deberás seguir estos pasos:

  • Comprobar si el teléfono es compatible con la API de Depth
  • Recuperar imágenes de profundidad de cada fotograma
  • Visualizar información de profundidad de varias maneras (consulta la animación anterior)
  • Usar la profundidad para aumentar el realismo de las apps con oclusión
  • Controlar de forma óptima los teléfonos que no admiten la API de Depth

Requisitos

Requisitos de hardware

Requisitos de software

2. ARCore y la API de Depth

La API de Depth usa la cámara RGB de un dispositivo compatible para crear mapas de profundidad (también llamados imágenes de profundidad). Puedes usar la información que proporcionan esos mapas para que los objetos virtuales aparezcan de manera precisa delante o detrás de objetos del mundo real, lo que proporciona experiencias del usuario envolventes y realistas.

La API de Depth de ARCore proporciona acceso a imágenes de profundidad que coinciden con cada fotograma que entrega la sesión de ARCore. Cada píxel entrega una medida de distancia entre la cámara y el entorno, lo que incrementa el realismo en tu app de RA.

Una funcionalidad clave de la API de Depth es la oclusión, es decir, la capacidad de que los objetos digitales aparezcan de forma precisa en relación con objetos del mundo real. Esto da la ilusión de que los objetos virtuales están en el entorno del usuario.

En este codelab, se te guiará en el proceso de creación de una app simple compatible con RA, que usa imágenes de profundidad para ocultar los objetos virtuales detrás de superficies reales y visualiza la geometría detectada del espacio.

3. Prepárate

Configura la máquina de desarrollo

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

List of devices attached
<DEVICE_SERIAL_NUMBER>    device

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

Descarga e instala el código

  1. Puedes clonar el repositorio de la siguiente manera:
git clone https://github.com/googlecodelabs/arcore-depth

O bien, puedes descargar este archivo ZIP y extraerlo:

  1. Inicia Android Studio y haz clic en Open an existing Android Studio project.
  2. Busca el directorio del que extrajiste el archivo ZIP que descargaste antes y abre 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.

4. Ejecuta la app de inicio

  1. 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.
  2. Selecciona tu dispositivo y haz clic en OK. Android Studio creará la app inicial y la ejecutará en tu dispositivo.
  3. La app solicitará permisos de cámara. Presiona Allow para continuar.

c5ef65f7a1da0d9.png

Cómo usar 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.

Actualmente, tu 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.

6182cf62be13cd97.png beb0d327205f80ee.png e4497751c6fad9a7.png

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

5. Comprueba la compatibilidad con la API de Depth (parte 1)

La API de Depth de ARCore solo se ejecuta en un subconjunto de dispositivos compatibles. Antes de agregar funcionalidad a la app que usa estas imágenes de profundidad, primero debes asegurarte 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.

Además, debes informar al usuario si se usará profundidad en esta sesión.

Agrega otro mensaje a la barra de notificaciones para que aparezca 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(), puedes 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 agregaste aparecerá en la parte inferior, como se muestra a continuación:

5c878a7c27833cb2.png

A continuación, actualizarás la app para llamar a la API de Depth y recuperar imágenes de profundidad de cada fotograma.

6. Recupera las imágenes de profundidad (parte 2)

La API de Depth capta imágenes en 3D del entorno del dispositivo y muestra una imagen de profundidad con esos datos en tu app. Cada píxel de la imagen de profundidad representa una medida de distancia entre la cámara del dispositivo y su entorno real.

Ahora usarás estas imágenes de profundidad para mejorar la renderización y 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, agrega una nueva clase a tu 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;
  }
}

Ahora agregarás una instancia de esta clase a DepthCodelabActivity. Asegúrate 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, actualiza el método onSurfaceCreated() a fin de inicializar la textura para que los sombreadores de la GPU puedan usarla:

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

Por último, debes propagar esta textura en cada marco con la imagen de profundidad más reciente. Para ello, llama al método update() que antes en el marco más reciente que se recuperó de session.
Dado que la compatibilidad con la profundidad es opcional en esta app, solo debes utilizar esta llamada si usas profundidad.

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

Ahora tienes una imagen de profundidad que se actualiza con cada fotograma y que puede usarse con los sombreadores.

Sin embargo, aún no hemos cambiado el comportamiento de la app. Ahora usarás la imagen de profundidad para mejorar tu app.

7. Renderiza la imagen de profundidad (parte 3)

Ahora que tienes una imagen de profundidad con la que puedes experimentar, seguramente quieres saber cómo es. En esta sección, agregarás un botón a la app para renderizar la profundidad de cada fotograma.

Agrega sombreadores nuevos

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

Agrega un nuevo sombreador .vert

En Android Studio:

  1. Primero, agrega nuevos sombreadores .vert y .frag en el directorio src/main/assets/shaders/.
  2. Haz clic con el botón derecho en el directorio de los sombreadores.
  3. Selecciona New -> File
  4. Asígnale el nombre background_show_depth_map.vert.
  5. 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.

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, ingresa 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 puedes renderizar el mapa de profundidad, úsalo. Agrega 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 está activada la renderización de profundidad (está desactivada de forma predeterminada):

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 puedes 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 y los azules las áreas que están más lejos.

Existen varias formas de transmitir información de profundidad. En esta sección, modificarás el sombreador a fin de pulsar la profundidad de forma periódica. Para ello, ajustarás el sombreador a fin de que solo muestre 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, puedes 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.

8. Usa la API de Depth para la oclusión (parte 4)

Ahora controlarás la oclusión de objetos en tu app.

La oclusión hace referencia a lo que sucede cuando no se puede renderizar por completo el objeto virtual, ya que hay objetos reales entre el objeto virtual y la cámara. Administrar la oclusión es esencial para que las experiencias de RA sean interactivas.

La renderización adecuada de objetos virtuales en tiempo real mejora el realismo y la credibilidad de la escena aumentada. Si deseas ver más ejemplos, mira nuestro video para combinar realidades con la API de Depth.

En esta sección, actualizarás tu app para que incluya objetos virtuales solo si la profundidad está disponible.

Agrega nuevos sombreadores de objetos

Al igual que en las secciones anteriores, agregarás sombreadores nuevos para proporcionar compatibilidad con la información de profundidad. Esta vez, puedes copiar los sombreadores de objetos existentes y agregar 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.

  • Copia object.vert en el archivo de destino src/main/assets/shaders/occlusion_object.vert.
  • 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 tienes una versión nueva de los sombreadores de objetos, puedes modificar el código del renderizador.

Renderiza la oclusión de objetos

Crea 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 > Copiar
  • Selecciona la carpeta rendering.
  • Haz clic con el botón derecho > Pegar.

6c87dcb87da558c1.png

  • Cambia el nombre de la clase a OcclusionObjectRenderer.

f2ffe488c81ad404.png

La clase nueva a la que le cambiaste el nombre aparecerá en la misma carpeta:

e5bf1c158e26c322.png

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. Esas variables ajustan la nitidez del borde de la oclusión.
// 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;

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

Controla la oclusión de objetos

Ahora que tienes un nuevo objeto OcclusionObjectRenderer, puedes agregarlo a 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 puedes controlar cuándo se usa occludedVirtualObject en función de si el dispositivo actual es compatible con la API de Depth o no. Agrega estas 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:

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é asignada 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 el 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;

Quieres 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 puedes 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

9. Mejora la calidad de la oclusión (opcional)

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 aportará 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 esta 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.

10. Crea, ejecuta y realiza pruebas

Cómo crear y ejecutar la app

  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 cree y se implemente en el dispositivo.

La primera vez que intentes implementar la app en tu dispositivo, haz lo siguiente:

  • Permite la depuración por USB en el dispositivo. Selecciona OK para continuar.
  • Se te preguntará si la app tiene permiso para usar la cámara del dispositivo. Permite el acceso para seguir usando la funcionalidad de RA.

Prueba tu 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.

Solución de problemas

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:
  3. Abre la app de Configuración.
  4. Si tu dispositivo usa Android 8.0 o una versión posterior, selecciona Sistema. De lo contrario, continúa con el paso siguiente.
  5. Desplázate hasta la parte inferior y selecciona Acerca del teléfono.
  6. Desplázate hasta la parte inferior y presiona Número de compilación 7 veces.
  7. Regresa a la pantalla anterior, desplázate hasta la parte inferior y presiona Opciones para desarrolladores.
  8. 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

11. Felicitaciones

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

Preguntas frecuentes