Utiliser l'API ARCore Depth pour des expériences de réalité augmentée immersives

1. Avant de commencer

ARCore est une plate-forme qui permet de créer des applications de réalité augmentée (RA) sur des appareils mobiles. Grâce aux différentes API, ARCore permet à l'appareil d'un utilisateur d'observer et de recevoir des informations sur son environnement, et d'interagir avec ces informations.

Dans cet atelier de programmation, vous allez créer une application de RA simple qui utilise l'API ARCore Depth.

Prérequis

Cet atelier de programmation a été élaboré pour les développeurs qui maîtrisent les concepts fondamentaux de la RA.

Objectifs de l'atelier

d9bd5136c54ce47a.gif

Vous allez créer une application qui utilise la représentation de profondeur pour chaque image afin de visualiser les volumes géométriques d'une scène et d'appliquer une occlusion à différents éléments virtuels ajoutés. Vous allez réaliser les étapes spécifiques suivantes :

  • Vérifier la compatibilité d'un téléphone avec l'API Depth
  • Récupérer la représentation de profondeur pour chaque image
  • Visualiser les informations de profondeur de plusieurs manières (voir l'animation ci-dessus)
  • Utiliser la profondeur pour accroître le réalisme des applications grâce à l'occlusion
  • Faire en sorte que l'application fonctionne de façon fluide sur les téléphones non compatibles avec l'API Depth

Prérequis

Matériel requis

  • Un appareil compatible ARCore, connecté via un câble USB à votre ordinateur de développement. Cet appareil doit aussi être compatible avec l'API Depth. Consultez la liste des appareils compatibles. L'API Depth n'est disponible que sur Android.
  • Activez le débogage USB pour cet appareil.

Logiciels requis

2. ARCore et l'API Depth

L'API Depth utilise l'appareil photo RVB d'un appareil compatible pour créer des cartes de profondeur. Vous pouvez utiliser les informations fournies par une carte de profondeur pour faire apparaître avec précision des objets virtuels, devant ou derrière des objets réels, ce qui permet de proposer des expériences utilisateur immersives et réalistes.

L'API ARCore Depth permet d'accéder à des représentations de profondeur correspondant à chaque image fournie par la session ARCore. Chaque pixel fournit une mesure de la distance entre l'appareil photo et l'environnement, ce qui améliore le réalisme de votre application de RA.

L'occlusion est une fonctionnalité essentielle de l'API Depth : elle permet aux objets numériques d'apparaître précisément par rapport aux objets réels. Ceux-ci donnent l'impression de se trouver réellement dans l'environnement avec l'utilisateur.

Cet atelier de programmation va vous aider à créer une application de RA simple qui utilise des représentations de profondeur pour permettre l'occlusion d'objets virtuels derrière des surfaces réelles et la visualisation des volumes géométriques détectés dans l'espace.

3. Configuration

Configurer l'ordinateur de développement

  1. Connectez votre appareil ARCore à votre ordinateur avec le câble USB. Assurez-vous que votre appareil autorise le débogage USB.
  2. Ouvrez un terminal et exécutez adb devices, comme illustré ci-dessous :
adb devices

List of devices attached
<DEVICE_SERIAL_NUMBER>    device

Le <DEVICE_SERIAL_NUMBER> sera une chaîne propre à votre appareil. Assurez-vous qu'un seul appareil s'affiche avant de passer à la suite.

Télécharger et installer le code

  1. Vous pouvez cloner le dépôt :
git clone https://github.com/googlecodelabs/arcore-depth

Il est également possible de télécharger un fichier ZIP et de l'extraire :

  1. Lancez Android Studio, puis cliquez sur Open an existing Android Studio project (Ouvrir un projet Android Studio existant).
  2. Recherchez le répertoire dans lequel vous avez extrait le fichier ZIP téléchargé ci-dessus, puis ouvrez le répertoire depth_codelab_io2020.

Il s'agit d'un projet Gradle unique, comportant plusieurs modules. Si l'onglet "Project", en haut à gauche d'Android Studio, ne s'affiche pas déjà, cliquez sur Projects (Projets) dans le menu déroulant pour afficher ce volet.

Vous devriez obtenir le résultat suivant :

Ce projet contient les modules suivants :

  • part0_work : application de départ. Vous apporterez des modifications à ce module lors de cet atelier de programmation.
  • part1 : code de référence pour vérifier vos modifications en fin de 1ʳᵉ partie.
  • part2 : code de référence à consulter en fin de 2ᵉ partie.
  • part3 : code de référence à consulter en fin de 3ᵉ partie.
  • part4_completed : version finale de l'application. Code de référence à consulter en fin de 4ᵉ partie et de cet atelier.

Vous allez travailler dans le module part0_work. Il existe également des solutions complètes pour chaque partie de l'atelier de programmation. Chaque module correspond à une application pouvant être créée.

4. Exécuter l'application de départ

  1. Cliquez sur Run > Run… (Exécuter > Exécuter…) > 'part0_work'. Dans la boîte de dialogue Select Deployment Target (Sélectionner une cible de déploiement) qui s'affiche, votre appareil doit s'afficher sous Connected Devices (Appareils connectés).
  2. Sélectionnez votre appareil, puis cliquez sur OK. Android Studio génère l'application de départ, puis l'exécute sur votre appareil.
  3. L'application demande l'autorisation d'accéder à l'appareil photo. Appuyez sur Allow (Autoriser) pour continuer.

c5ef65f7a1da0d9.png

Utiliser l'application

  1. Déplacez l'appareil pour aider l'application à trouver une surface plane. Le message s'affichant en bas de l'écran vous indique si vous pouvez vous arrêter.
  2. Appuyez n'importe où sur la surface pour placer une ancre. Un personnage Android est dessiné à cet emplacement. Cette application vous permet de placer une ancre à la fois.
  3. Déplacez l'appareil. Le personnage doit rester au même endroit, même si l'appareil est en mouvement.

Dans son état actuel, l'application est très simple et dispose de peu d'informations sur les volumes géométriques de la scène réelle.

Par exemple, si vous placez un personnage Android derrière un fauteuil, son rendu s'affiche au premier plan, car l'application ne détecte pas la présence du fauteuil qui devrait cacher ce personnage.

6182cf62be13cd97.png beb0d327205f80ee.png e4497751c6fad9a7.png

Pour résoudre ce problème, nous allons utiliser l'API Depth pour améliorer l'immersion et le réalisme de cette application.

5. Vérifier si l'API Depth est compatible (1ʳᵉ partie)

L'API ARCore Depth n'est compatible qu'avec certains appareils. Avant d'intégrer la fonctionnalité dans une application à l'aide de ces représentations de profondeur, vous devez d'abord vous assurer que l'application s'exécute sur un appareil compatible.

Ajoutez un nouveau membre privé à DepthCodelabActivity. Il servira d'indicateur permettant d'enregistrer la compatibilité éventuelle de l'appareil avec les représentations de profondeur :

private boolean isDepthSupported;

Nous pouvons insérer cette option à partir de la fonction onResume(), où une session est créée.

Recherchez le code existant :

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

Remplacez ce code par le code suivant :

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

À présent, la session de RA est configurée correctement et l'application sait si elle peut utiliser des fonctionnalités basées sur la profondeur.

Vous devez également indiquer à l'utilisateur si ces fonctionnalités sont activées pour cette session.

Ajoutez un autre message à la barre de notification. Il apparaîtra en bas de l'écran :

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

Vous pouvez présenter ce message si nécessaire dans onDrawFrame() :

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

Si cette application est exécutée sur un appareil non compatible avec la mesure de profondeur, le message que nous venons d'ajouter s'affiche en bas de l'écran :

5c878a7c27833cb2.png

Vous allez ensuite mettre à jour l'application pour appeler l'API Depth et récupérer les représentations de profondeur pour chaque image.

6. Récupérer les représentations de profondeur (2e partie)

L'API Depth capture les observations en 3D de l'environnement de l'appareil et renvoie une représentation de profondeur contenant ces données à votre application. Chaque pixel de la représentation de profondeur représente une mesure de distance entre l'objectif de l'appareil et son environnement réel.

Vous allez maintenant utiliser ces représentations de profondeur pour améliorer les rendus et la visualisation dans l'application. La première étape consiste à récupérer la représentation de profondeur pour chaque image et à associer cette texture qui sera utilisée par le GPU.

Tout d'abord, ajoutez une nouvelle classe à votre projet.
DepthTextureHandler se charge de récupérer la représentation de profondeur pour une image ARCore donnée.
Ajoutez ce fichier :

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

Vous allez à présent ajouter une instance de cette classe à DepthCodelabActivity, afin de disposer d'une copie facile d'accès de la représentation de profondeur pour chaque image.

Dans DepthCodelabActivity.java, ajoutez une instance de notre nouvelle classe en tant que variable de membre privé :

private final DepthTextureHandler depthTexture = new DepthTextureHandler();

Ensuite, mettez à jour la méthode onSurfaceCreated() pour initialiser cette texture, de sorte qu'elle puisse être utilisée par les nuanceurs GPU :

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

Enfin, vous allez insérer cette texture dans chaque image avec la dernière représentation de profondeur. Vous allez pour cela appeler la méthode update() que vous avez créée ci-dessus sur la dernière image récupérée de la session.
La compatibilité avec la mesure de profondeur étant facultative pour cette application, cet appel ne doit être effectué que si vous utilisez cette fonctionnalité.

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

Vous disposez désormais d'une représentation de profondeur qui est mise à jour à chaque image. Elle est prête à être utilisée par les nuanceurs.

Cependant, le comportement de l'application n'a pas changé. Vous allez à présent utiliser la représentation de profondeur pour améliorer votre application.

7. Effectuer le rendu de la représentation de profondeur (3ᵉ partie)

Maintenant que vous disposez d'une représentation de profondeur, vous allez pouvoir la visualiser. Dans cette section, vous allez ajouter un bouton à l'application pour afficher la profondeur de chaque image.

Ajouter des nuanceurs

Il existe de nombreuses façons d'afficher une représentation de profondeur. Les nuanceurs suivants fournissent une visualisation simple basée sur une carte de couleurs.

Ajouter un nuanceur ".vert"

Dans Android Studio :

  1. Ajoutez d'abord les nuanceurs .vert et .frag dans le répertoire src/main/assets/shaders/.
  2. Effectuez un clic droit sur le répertoire des nuanceurs.
  3. Sélectionnez New > File (Nouveau > Fichier)
  4. Appelez-le background_show_depth_map.vert
  5. Définissez-le en tant que fichier texte.

Dans ce nouveau fichier, ajoutez le code suivant :

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

Répétez les étapes ci-dessus pour créer le nuanceur de fragment dans le même répertoire et nommez-le background_show_depth_map.frag.

Ajoutez ensuite le code suivant à ce nouveau fichier :

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

Ensuite, mettez à jour la classe BackgroundRenderer pour utiliser ces nouveaux nuanceurs, situés dans le répertoire src/main/java/com/google/ar/core/codelab/common/rendering/BackgroundRenderer.java.

Ajoutez les chemins d'accès aux nuanceurs en tête de la 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";

Ajoutez d'autres variables de membre à la classe BackgroundRenderer, car elle exécutera deux nuanceurs :

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

Ajoutez une méthode pour insérer ces champs :

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

Ajoutez la méthode suivante, qui est utilisée pour dessiner sur chaque image à l'aide de ces nuanceurs :

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

Ajouter un bouton d'activation

Maintenant que vous avez la possibilité d'afficher la carte de profondeur, utilisez-la. Ajoutez un bouton pour activer ou désactiver ce rendu.

En haut du fichier DepthCodelabActivity, ajoutez une importation que le bouton doit utiliser :

import android.widget.Button;

Mettez à jour la classe pour ajouter un membre booléen indiquant si le rendu de la profondeur est activé (il est désactivé par défaut) :

private boolean showDepthMap = false;

Ensuite, ajoutez le bouton qui contrôle la valeur booléenne showDepthMap à la fin de la méthode 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);
          }
        });

Ajoutez les chaînes suivantes à 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>

Ajoutez ce bouton dans la partie inférieure de la mise en page de l'application dans le fichier 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"/>

Le bouton contrôle désormais la valeur de l'opérateur booléen showDepthMap. Utilisez cet indicateur pour vérifier si le rendu de la carte de profondeur s'effectue correctement.

Revenez à la méthode onDrawFrame() dans DepthCodelabActivity pour ajouter ceci :

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

Transmettez la texture de profondeur à l'élément backgroundRenderer en ajoutant la ligne suivante dans onSurfaceCreated() :

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

Vous pouvez maintenant afficher la représentation de profondeur pour chaque image en appuyant sur le bouton dans l'angle supérieur droit de l'écran.

Exécuter l'application sans compatibilité de l'API Depth

Exécuter l'application avec compatibilité de l'API Depth

[Facultatif] Animation de profondeur sophistiquée

Actuellement, l'application affiche directement la carte de profondeur. Les pixels rouges représentent les zones proches. Les pixels bleus représentent les zones éloignées.

Il existe de nombreuses façons de représenter des informations de profondeur. Dans cette section, vous allez modifier le nuanceur pour représenter la profondeur sous la forme d'une impulsion animée qui n'apparaît que dans les bandes qui s'éloignent de l'appareil photo de façon répétée.

Ajoutez d'abord les variables suivantes en tête de l'élément background_show_depth_map.frag :

uniform float u_DepthRangeToRenderMm;
const float kDepthWidthToRenderMm = 350.0;
  • Utilisez ensuite ces valeurs pour filtrer les pixels à couvrir avec les valeurs de profondeur dans la fonction main() du nuanceur :
// Add this line at the end of main().
gl_FragColor.a = clamp(1.0 - abs((depth_mm - u_DepthRangeToRenderMm) / kDepthWidthToRenderMm), 0.0, 1.0);

Ensuite, mettez à jour BackgroundRenderer.java pour conserver ces paramètres de nuanceur. Ajoutez les champs suivants en tête de la classe :

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

Dans la méthode createDepthShaders(), ajoutez les éléments suivants pour ajuster ces paramètres au programme du nuanceur :

depthRangeToRenderMmParam = GLES20.glGetUniformLocation(depthProgram, "u_DepthRangeToRenderMm");
  • Enfin, vous pouvez contrôler la façon dont cette plage se modifie progressivement dans la méthode drawDepth(). Ajoutez le code suivant, qui incrémente cette plage chaque fois qu'une image est affichée :
// 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);

La profondeur est désormais représentée sous la forme d'une impulsion animée qui traverse la scène.

37e2a86b833150f8.gif

N'hésitez pas à modifier les valeurs fournies ici pour ralentir, accélérer, élargir ou affiner l'impulsion. Vous pouvez également tester de nouvelles façons de modifier le nuanceur pour afficher des informations de profondeur.

8. Utiliser l'API Depth pour l'occlusion (4ᵉ partie)

Vous allez maintenant gérer l'occlusion des objets dans votre application.

L'occlusion désigne ce qui se produit lorsqu'un objet virtuel ne peut pas être entièrement affiché, en raison de la présence d'objets réels entre l'objet virtuel et l'appareil photo. La gestion de l'occlusion est essentielle pour que les expériences de RA soient immersives.

Le rendu correct d'objets virtuels en temps réel améliore le réalisme et la crédibilité de la scène augmentée. Pour voir d'autres exemples, regardez notre vidéo sur l'utilisation de l'API Depth pour fusionner différentes réalités.

Dans cette section, vous allez modifier votre application pour n'inclure des objets virtuels que si la profondeur est disponible.

Ajouter des nuanceurs d'objets

Comme dans les sections précédentes, vous allez ajouter de nouveaux nuanceurs pour fournir des informations de profondeur. Cette fois, vous pouvez copier les nuanceurs d'objets existants et ajouter une fonctionnalité d'occlusion.

Il est important de conserver les deux versions des nuanceurs d'objet afin que l'application puisse en choisir une lors de son exécution, en fonction de sa compatibilité avec les mesures de profondeur.

Créez des copies des fichiers de nuanceurs object.vert et object.frag dans le répertoire src/main/assets/shaders.

  • Copiez object.vert dans le fichier de destination src/main/assets/shaders/occlusion_object.vert.
  • Copiez object.frag dans le fichier de destination src/main/assets/shaders/occlusion_object.frag.

Dans occlusion_object.vert, ajoutez la variable suivante au-dessus de main() :

varying vec3 v_ScreenSpacePosition;

Définissez cette variable à la fin de l'élémentmain() :

v_ScreenSpacePosition = gl_Position.xyz / gl_Position.w;

Mettez à jour occlusion_object.frag en ajoutant ces variables au-dessus de main() en tête du fichier :

varying vec3 v_ScreenSpacePosition;

uniform sampler2D u_Depth;
uniform mat3 u_UvTransform;
uniform float u_DepthTolerancePerMm;
uniform float u_OcclusionAlpha;
uniform float u_DepthAspectRatio;
  • Ajoutez ces fonctions d'assistance au-dessus de main() dans le nuanceur afin de faciliter l'exploitation des informations de profondeur :
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;
}

Mettez à jour main() dans le fichier occlusion_object.frag pour qu'il tienne compte de la profondeur et applique l'occlusion. Ajoutez les lignes suivantes en fin de fichier :

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

Maintenant que vous disposez d'une nouvelle version de vos nuanceurs d'objets, vous pouvez modifier le code du moteur de rendu.

Effectuer le rendu de l'occlusion des objets

Créez une copie de la classe ObjectRenderer, disponible dans src/main/java/com/google/ar/core/codelab/common/rendering/ObjectRenderer.java.

  • Sélectionnez la classe ObjectRenderer.
  • Faites un clic droit, puis sélectionnez Copier.
  • Sélectionnez le dossier du rendu.
  • Faites un clic droit, puis sélectionnez Coller.

6c87dcb87da558c1.png

  • Donnez ce nouveau nom à la classe : OcclusionObjectRenderer.

f2ffe488c81ad404.png

La nouvelle classe renommée doit maintenant apparaître dans le même dossier :

e5bf1c158e26c322.png

Ouvrez le fichier OcclusionObjectRenderer.java que vous venez de créer, puis modifiez les chemins d'accès du nuanceur en tête du fichier :

private static final String VERTEX_SHADER_NAME = "shaders/occlusion_object.vert";
private static final String FRAGMENT_SHADER_NAME = "shaders/occlusion_object.frag";
  • Ajoutez ces variables de membre associées aux mesures de profondeur. Les variables ci-dessus ajustent la netteté des contours de l'occlusion.
// 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;

Créez ces variables de membre avec des valeurs par défaut en tête de la 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;

Initialisez les paramètres uniformes du nuanceur dans la méthode 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");
  • Veillez à modifier ces valeurs à chaque affichage en mettant à jour la méthode 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);

Ajoutez les lignes suivantes dans draw() pour activer le mode de fusion dans le rendu afin que la transparence puisse être appliquée aux objets virtuels en cas d'occlusion :

// 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);
  • Ajoutez les méthodes suivantes pour que les appelants de OcclusionObjectRenderer puissent fournir les informations sur la profondeur :
// 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;
}

Contrôler l'occlusion des objets

Maintenant que vous disposez d'un nouveau OcclusionObjectRenderer, vous pouvez l'ajouter à votre DepthCodelabActivity et choisir quand et comment appliquer le rendu d'occlusion.

Activez cette logique en ajoutant une instance de OcclusionObjectRenderer à l'activité, de sorte que ObjectRenderer et OcclusionObjectRenderer soient membres 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();
  • Vous pouvez ensuite contrôler l'utilisation de l'instance occludedVirtualObject en fonction de la compatibilité de l'appareil avec l'API Depth. Ajoutez les lignes suivantes dans la méthode onSurfaceCreated, après la configuration 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);
}

Sur les appareils où la mesure de profondeur n'est pas disponible, l'instance occludedVirtualObject est créée, mais n'est pas utilisée. Sur les téléphones équipés de cette fonctionnalité, les deux versions sont initialisées et un choix a lieu lors de l'exécution en fonction de la version du moteur de rendu à utiliser pour l'affichage.

Dans la méthode onDrawFrame(), recherchez ce code existant :

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

Remplacez-le par le code suivant :

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

Pour terminer, assurez-vous que la représentation de profondeur est correctement mappée au rendu de sortie. Comme la résolution de la représentation de profondeur n'est pas la même que celle de votre écran et peut présenter un format différent, il peut y avoir un écart entre les coordonnées de la texture et celle de l'image de l'appareil photo.

  • Ajoutez une méthode d'assistance getTextureTransformMatrix() en fin de fichier. Cette méthode renvoie une matrice de transformation qui calcule les valeurs correspondant aux coordonnées UV de l'espace d'écran pour les coordonnées du rectangle de texture utilisées pour afficher le flux de l'appareil photo. Elle tient compte de l'orientation de l'appareil.
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;
}

La méthode d'assistance getTextureTransformMatrix() requiert l'importation suivante en tête de fichier :

import com.google.ar.core.Coordinates2d;

Vous devez calculer la transformation entre ces coordonnées de texture pour chaque modification de la texture d'écran (par exemple, si l'écran pivote). Cette fonctionnalité est contrôlée.

Ajoutez l'indicateur suivant en tête du fichier :

// Add this member at the top of the file.
private boolean calculateUVTransform = true;
  • Dans onDrawFrame(), vérifiez si la transformation stockée doit être recalculée après la création de l'image et de l'appareil photo :
// Add these lines inside onDrawFrame() after frame.getCamera().
if (frame.hasDisplayGeometryChanged() || calculateUVTransform) {
  calculateUVTransform = false;
  float[] transform = getTextureTransformMatrix(frame);
  occludedVirtualObject.setUvTransformMatrix(transform);
}

Une fois ces changements en place, vous pouvez exécuter l'application avec occlusion d'objet virtuel.

L'application devrait désormais fonctionner de façon fluide sur tous les téléphones et exploiter automatiquement la mesure de profondeur pour l'occlusion quand cette fonctionnalité est disponible.

Exécuter une application compatible avec l'API Depth

Exécuter une application non compatible avec l'API Depth

9. [Facultatif] Améliorer la qualité de l'occlusion

La méthode d'occlusion basée sur la profondeur, mise en œuvre ci-dessus, offre une occlusion aux contours nets. Lorsque l'appareil photo se trouve plus loin de l'objet, les mesures de profondeur peuvent devenir moins précises, ce qui peut entraîner des artefacts visuels.

Pour résoudre ce problème, nous allons ajouter un flou supplémentaire au test d'occlusion afin de générer un contour moins tranché pour des objets virtuels masqués.

Élément "occlusion_object.frag"

Ajoutez la variable uniforme suivante en tête de occlusion_object.frag :

uniform float u_OcclusionBlurAmount;

Ajoutez cette fonction d'assistance juste au-dessus de main() dans le nuanceur, qui applique un masque de floutage à l'échantillonnage d'occlusion :

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

Remplacez la ligne existante dans main() :

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

par ce qui suit :

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

Mettez à jour le moteur de rendu pour activer cette nouvelle fonctionnalité du nuanceur.

Élément "OcclusionObjectRenderer.java"

Ajoutez les variables de membre suivantes en tête de la classe :

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

Ajoutez ce qui suit dans la méthode createOnGlThread :

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

Ajoutez ce qui suit dans la méthode draw :

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

Comparaison visuelle

Grâce à ces modifications, le contour d'occlusion devrait désormais être moins tranché.

10. Test de génération et d'exécution

Générer et exécuter votre application

  1. Branchez un appareil Android à l'aide d'un câble USB.
  2. Sélectionnez File > Build and Run (Fichier > Générer et exécuter).
  3. Enregistrez-le sous le nom : ARCodeLab.apk.
  4. Attendez que l'application soit générée et se déploie sur votre appareil.

La première fois que vous tentez de déployer l'application sur votre appareil, procédez comme suit :

  • Vous devez autoriser le débogage USB sur l'appareil. Sélectionnez "OK" pour continuer.
  • Il vous est demandé si l'application est autorisée à utiliser l'appareil photo. Autorisez l'accès pour continuer à utiliser la fonctionnalité de RA.

Tester votre application

Lorsque vous exécutez votre application, vous pouvez tester son comportement de base en manipulant votre appareil, en vous déplaçant dans l'espace où vous vous trouvez et en scannant lentement une zone. Essayez de collecter au moins 10 secondes de données et de scanner cette zone selon plusieurs axes avant de passer à l'étape suivante.

Dépannage

Configurer votre appareil Android pour le développement

  1. Connectez votre appareil à l'ordinateur de développement à l'aide d'un câble USB. Si vous développez avec Windows, vous devrez peut-être installer le pilote USB correspondant à votre appareil.
  2. Pour activer le débogage USB dans la fenêtre Options pour les développeurs, procédez comme suit :
  3. Ouvrez l'application Paramètres.
  4. Si votre appareil est équipé d'Android 8.0 ou version ultérieure, sélectionnez Système. Sinon, passez à l'étape suivante.
  5. Faites défiler l'écran jusqu'en bas, puis sélectionnez À propos du téléphone.
  6. Faites défiler l'écran jusqu'en bas, puis appuyez sept fois sur Numéro de build.
  7. Revenez à l'écran précédent, faites défiler la page jusqu'en bas, puis appuyez sur Options pour les développeurs.
  8. Dans la fenêtre Options pour les développeurs, faites défiler l'écran vers le bas pour trouver et activer l'option Débogage USB.

Pour en savoir plus sur cette procédure, consultez le site Web des développeurs Android de Google.

1480e83e227b94f1.png

Si vous rencontrez un problème de génération dû à des licences (Failed to install the following Android SDK packages as some licenses have not been accepted [Échec de l'installation des packages SDK Android suivants, car certaines licences n'ont pas été acceptées]), vous pouvez utiliser les commandes suivantes pour vérifier et accepter ces licences :

cd <path to Android SDK>

tools/bin/sdkmanager --licenses

11. Félicitations

Félicitations, vous venez de créer et d'exécuter votre première application de réalité augmentée basée sur les mesures de la profondeur grâce à l'API ARCore Depth de Google.

Questions fréquentes