ARCore Depth

ARCore est une plate-forme qui permet de créer des applications de réalité augmentée (RA) sur des appareils mobiles. L'API ARCore Depth de Google permet d'obtenir une représentation de la profondeur pour chaque image fournie lors d'une session ARCore. Chaque pixel fournit une mesure de la distance entre l'appareil photo et un point de l'environnement. Cette fonctionnalité est utilisée dans cet atelier de programmation pour améliorer le réalisme d'une application de RA.

L'API Depth ne fonctionne qu'avec certains appareils compatibles ARCore. Veuillez consulter cette liste pour découvrir les téléphones compatibles avec les mesures de profondeur. L'API Depth n'est disponible que sur Android.

Cet atelier de programmation vous guide tout au long du processus de création d'une application de RA basique utilisant des représentations de profondeur pour permettre l'occlusion d'éléments virtuels derrière des surfaces réelles et la visualisation des volumes géométriques détectés dans l'espace réel.

Objectifs de l'atelier

d9bd5136c54ce47a.gif

Dans cet atelier de programmation, 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. Cet atelier de programmation présente les étapes spécifiques suivantes :

  1. Vérifier la compatibilité d'un téléphone avec l'API Depth
  2. Récupérer la représentation de profondeur pour chaque image
  3. Découvrir différentes méthodes pour visualiser des informations sur la profondeur (voir l'animation ci-dessus)
  4. Utiliser la profondeur pour accroître le réalisme des applications grâce à l'occlusion
  5. Faire en sorte que l'application fonctionne de façon fluide sur les téléphones non compatibles avec l'API Depth

Remarque : Si vous rencontrez des problèmes, consultez la dernière section pour obtenir des conseils de dépannage.

Vous aurez besoin de matériel et de logiciels spécifiques pour cet atelier de programmation.

Matériel requis

Logiciels requis

Configurer l'ordinateur de développement

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

adb devices

List of devices attached
<DEVICE_SERIAL_NUMBER>    device

Le numéro de série <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

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 :

Télécharger le fichier ZIP

Lancez Android Studio. Cliquez sur Open an existing Android Studio project (Ouvrir un projet Android Studio existant). Accédez ensuite au répertoire dans lequel vous avez extrait le fichier ZIP téléchargé ci-dessus, puis double-cliquez sur 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.

Cliquez 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). 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.

Lorsque vous exécutez l'application pour la première fois, vous devez l'autoriser à utiliser l'appareil photo. Appuyez sur Allow (Autoriser) pour continuer.

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 d'une 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.

5e8f5fe9098d316e.png 76f41b692224801b.png 3f320c851d1903d.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.

L'API ARCore Depth n'est compatible qu'avec certains d'appareils. Avant d'intégrer de nouvelles fonctionnalités utilisant les représentations de profondeur dans l'application, nous devons d'abord nous assurer que celle-ci 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.

Nous devons également indiquer à l'utilisateur si ces fonctionnalités sont activées pour cette session. Nous pouvons ajouter un autre message à la barre de notification snackbar, qui s'affiche 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]";

Et à l'intérieur de onDrawFrame(), nous pouvons présenter ce message, si nécessaire :

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

feb1c9f42f3cf396.png

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

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

Cet atelier de programmation utilise 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 de chaque image et à associer la texture qui sera utilisée par le GPU.

Ajoutons d'abord une nouvelle classe à notre projet. L'élément DepthTextureHandler est responsable de la récupération de 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;
  }
}

Ensuite, nous pouvons ajouter une instance de cette classe à DepthCodelabActivity, afin de disposer d'une copie facile d'accès de 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();

Mettons à jour la méthode onSurfaceCreated() afin d'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, nous souhaitons insérer cette texture dans chaque image avec la dernière représentation de profondeur, ce qui peut être fait en appelant la méthode update() que nous avons créée ci-dessus sur la dernière image récupérée de cette session. La compatibilité avec la mesure de profondeur étant facultative pour cette application, cet appel ne doit être effectué que si nous utilisons cette fonctionnalité.

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

Nous disposons maintenant d'une représentation de profondeur qui est mise à jour à chaque image. Elle est prête à être utilisée par nos nuanceurs. Cependant, le comportement de l'application n'a pas changé. Utilisons une représentation de profondeur pour l'améliorer.

Nous disposons désormais d'une représentation de profondeur. Voyons comment elle se présente. Dans cette section, nous allons 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.

Ajoutez d'abord les nuanceurs .vert et .frag dans le répertoire src/main/assets/shaders/.

Ajouter un nouveau nuanceur ".vert"

Dans Android Studio :

  1. Effectuez un clic droit sur le répertoire des nuanceurs.
  2. Sélectionnez New > File (Nouveau > Fichier)
  3. Appelez-le background_show_depth_map.vert
  4. Définissez-le en tant que fichier texte.

Dans ce nouveau fichier, ajoutez le code suivant :

src/main/assets/nuanceurs/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 appelez-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 nous avons réussi à afficher la carte de profondeur, utilisons-la. En examinant DepthCodelabActivity, nous pouvons ajouter un bouton qui active ou désactive 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());

Nous pouvons désormais 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 bleus les zones éloignées.

Il existe de nombreuses façons de représenter des informations de profondeur. Dans cette sous-section, nous modifions le nuanceur pour envoyer des "impulsions radar" régulières représentant la profondeur sous forme de bandes s'éloignant de l'appareil photo.

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, nous pouvons 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.

Une occlusion se produit lorsqu'un objet virtuel n'est pas entièrement affiché, car des surfaces réelles se trouvent entre l'objet virtuel et l'appareil photo.

La capacité à afficher correctement les objets virtuels in situ permet d'améliorer 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, nous mettons à jour notre application afin d'inclure des objets virtuels lorsque la mesure de profondeur est disponible.

Ajouter des nuanceurs d'objets

Comme dans les sections précédentes, nous allons ajouter de nouveaux nuanceurs pour fournir des informations de profondeur. Cette fois, nous allons copier les nuanceurs d'objets existants et ajouterons 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.

  1. Copiez object.vert dans le fichier de destination src/main/assets/shaders/occlusion_object.vert.
  2. 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 nous disposons d'une nouvelle version de nos nuanceurs d'objets, nous pouvons 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 "Copy" (Copier). Sélectionnez le dossier rendering. Faites un clic droit, puis sélectionnez "Paste" (Coller).

Donnez ce nouveau nom à la classe : OcclusionObjectRenderer.

La nouvelle classe renommée doit désormais apparaître dans le même dossier.

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 :

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

Les variables ci-dessus ajustent la netteté des contours de l'occlusion. Créez d'abord 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 à mettre à jour ces valeurs à chaque affichage en mettant à jour la méthode draw() pour inclure les éléments suivants :

// 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 nous disposons d'un nouvel élément OcclusionObjectRenderer, nous pouvons l'ajouter à notre activité DepthCodelabActivity et choisir quand et comment appliquer le rendu d'occlusion.

Cette logique est activée en ajoutant une instance de OcclusionObjectRenderer à l'activité, de sorte que ObjectRenderer et OcclusionObjectRenderer soient membres de l'activité 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();

Nous pouvons 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 l'objet "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;

Nous devons 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, nous pouvons 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

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

Trouvez cette ligne dans main() :

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

Puis remplacez-la par :

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é.

Générer et exécuter votre application

Procédez comme suit pour 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.

Au premier déploiement de l'application sur votre appareil, vous devez :

Autoriser le débogage USB

sur l'appareil. Sélectionnez "OK" pour continuer.

Lorsque vous exécutez votre application pour la première fois sur votre appareil, il vous est demandé si elle est autorisée à utiliser l'appareil photo de celui-ci. Vous devez accorder cette autorisation 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.

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.

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 :
  • Ouvrez l'application Paramètres.
  • Si votre appareil est équipé d'Android 8.0 ou version ultérieure, sélectionnez Système. Sinon, passez à l'étape suivante.
  • Faites défiler l'écran jusqu'en bas, puis sélectionnez À propos de.
  • Faites défiler la page jusqu'en bas, puis appuyez sept fois sur Numéro de build.
  • Revenez à l'écran précédent, faites défiler la page jusqu'en bas, puis appuyez sur Options pour les développeurs.
  • 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

Questions fréquentes