Utilizzare l'API ARCore Depth per esperienze di realtà aumentata immersive

1. Prima di iniziare

ARCore è una piattaforma per la creazione di app di realtà aumentata (AR) su dispositivi mobili. Utilizzando diverse API, ARCore consente al dispositivo di un utente di osservare e ricevere informazioni sul suo ambiente e di interagire con queste informazioni.

In questo codelab, seguirai la procedura di creazione di una semplice app abilitata alla realtà aumentata che utilizza l'API ARCore Depth.

Prerequisiti

Questo codelab è stato scritto per sviluppatori con conoscenze dei concetti fondamentali di AR.

Cosa creerai

1a0236e93212210c.gif

Creerai un'app che utilizza l'immagine di profondità per ogni frame per visualizzare la geometria della scena ed eseguire l'occlusione sugli asset virtuali posizionati. Vedrai i passaggi specifici di:

  • Controllo del supporto dell'API Depth sullo smartphone
  • Recupero dell'immagine di profondità per ogni frame
  • Visualizzazione delle informazioni sulla profondità in vari modi (vedi l'animazione sopra)
  • Utilizzare la profondità per aumentare il realismo delle app con l'occlusione
  • Imparare a gestire correttamente gli smartphone che non supportano l'API Depth

Che cosa ti serve

Requisiti hardware

Requisiti software

2. ARCore e l'API Depth

L'API Depth utilizza la fotocamera RGB di un dispositivo supportato per creare mappe di profondità (chiamate anche immagini di profondità). Puoi utilizzare le informazioni fornite da una mappa di profondità per far apparire con precisione gli oggetti virtuali davanti o dietro gli oggetti del mondo reale, consentendo esperienze utente immersive e realistiche.

L'API ARCore Depth fornisce l'accesso a immagini di profondità corrispondenti a ogni frame fornito dalla sessione di ARCore. Ogni pixel fornisce una misurazione della distanza dalla videocamera all'ambiente, il che offre un realismo migliorato per la tua app AR.

Una funzionalità chiave dell'API Depth è l'occlusione: la capacità degli oggetti digitali di apparire con precisione rispetto agli oggetti del mondo reale. In questo modo, gli oggetti sembrano trovarsi effettivamente nell'ambiente con l'utente.

Questo codelab ti guiderà nel processo di creazione di una semplice app abilitata all'AR che utilizza immagini di profondità per eseguire l'occlusione di oggetti virtuali dietro superfici del mondo reale e visualizzare la geometria rilevata dello spazio.

3. Configurazione

Configura la macchina di sviluppo

  1. Collega il dispositivo ARCore al computer tramite il cavo USB. Assicurati che il dispositivo consenta il debug USB.
  2. Apri un terminale ed esegui adb devices, come mostrato di seguito:
adb devices

List of devices attached
<DEVICE_SERIAL_NUMBER>    device

<DEVICE_SERIAL_NUMBER> sarà una stringa univoca per il tuo dispositivo. Prima di continuare, assicurati di visualizzare un solo dispositivo.

Scarica e installa Code

  1. Puoi clonare il repository:
git clone https://github.com/googlecodelabs/arcore-depth

In alternativa, scarica un file ZIP ed estrailo:

  1. Avvia Android Studio e fai clic su Apri un progetto Android Studio esistente.
  2. Trova la directory in cui hai estratto il file ZIP scaricato in precedenza e apri la directory depth_codelab_io2020.

Si tratta di un singolo progetto Gradle con più moduli. Se il riquadro Progetto in alto a sinistra di Android Studio non è già visualizzato, fai clic su Progetti dal menu a discesa.

Il risultato dovrebbe essere simile a questo:

Questo progetto contiene i seguenti moduli:

  • part0_work: l'app iniziale. Devi apportare modifiche a questo modulo quando svolgi questo codelab.
  • part1: codice di riferimento di come dovrebbero apparire le modifiche al termine della parte 1.
  • part2: il codice di riferimento quando completi la parte 2.
  • part3: Codice di riferimento quando completi la parte 3.
  • part4_completed: La versione finale dell'app. Codice di riferimento quando completi la parte 4 e questo codelab.

Lavorerai nel modulo part0_work. Sono disponibili anche soluzioni complete per ogni parte del codelab. Ogni modulo è un'app compilabile.

4. Esegui l'app di base

  1. Fai clic su Esegui > Esegui… > 'part0_work'. Nella finestra di dialogo Seleziona destinazione di distribuzione visualizzata, il tuo dispositivo dovrebbe essere elencato nella sezione Dispositivi connessi.
  2. Seleziona il tuo dispositivo e fai clic su Ok. Android Studio creerà l'app iniziale e la eseguirà sul tuo dispositivo.
  3. L'app richiederà le autorizzazioni della fotocamera. Tocca Consenti per continuare.

c5ef65f7a1da0d9.png

Come utilizzare l'app

  1. Sposta il dispositivo per aiutare l'app a trovare un aereo. Il messaggio in basso indica quando continuare a muoverti.
  2. Tocca un punto qualsiasi della superficie piana per posizionare un ancoraggio. Verrà disegnato un robot Android nel punto in cui è stato posizionato l'ancoraggio. Questa app ti consente di posizionare un solo ancoraggio alla volta.
  3. Sposta il dispositivo. La figura dovrebbe rimanere nello stesso punto, anche se il dispositivo si sposta.

Al momento, la tua app è molto semplice e non conosce molto la geometria della scena del mondo reale.

Se posizioni una figura di Android dietro una sedia, ad esempio, il rendering sembrerà fluttuare davanti, poiché l'applicazione non sa che la sedia è lì e dovrebbe nascondere Android.

6182cf62be13cd97.png beb0d327205f80ee.png e4497751c6fad9a7.png

Per risolvere questo problema, utilizzeremo l'API Depth per migliorare l'immersività e il realismo in questa app.

5. Verificare se l'API Depth è supportata (parte 1)

L'API ARCore Depth viene eseguita solo su un sottoinsieme di dispositivi supportati. Prima di integrare funzionalità in un'app utilizzando queste immagini di profondità, devi prima assicurarti che l'app sia in esecuzione su un dispositivo supportato.

Aggiungi un nuovo membro privato a DepthCodelabActivity che funge da flag che memorizza se il dispositivo attuale supporta la profondità:

private boolean isDepthSupported;

Possiamo compilare questo flag dall'interno della funzione onResume(), in cui viene creata una nuova sessione.

Trova il codice esistente:

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

Aggiorna il codice a:

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

Ora la sessione AR è configurata correttamente e la tua app sa se può utilizzare o meno le funzionalità basate sulla profondità.

Devi anche comunicare all'utente se la profondità viene utilizzata o meno per questa sessione.

Aggiungi un altro messaggio alla snackbar. Verrà visualizzato nella parte inferiore dello schermo:

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

All'interno di onDrawFrame(), puoi presentare questo messaggio in base alle esigenze:

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

Se la tua app viene eseguita su un dispositivo che non supporta la profondità, il messaggio che hai appena aggiunto viene visualizzato in basso:

5c878a7c27833cb2.png

Successivamente, aggiornerai l'app per chiamare l'API Depth e recuperare le immagini di profondità per ogni frame.

6. Recuperare le immagini di profondità (parte 2)

L'API Depth acquisisce osservazioni 3D dell'ambiente del dispositivo e restituisce un'immagine di profondità con questi dati alla tua app. Ogni pixel nell'immagine di profondità rappresenta una misurazione della distanza dalla fotocamera del dispositivo al suo ambiente reale.

Ora utilizzerai queste immagini di profondità per migliorare il rendering e la visualizzazione nell'app. Il primo passaggio consiste nel recuperare l'immagine di profondità per ogni frame e associare la texture da utilizzare alla GPU.

Innanzitutto, aggiungi una nuova classe al tuo progetto.
DepthTextureHandler è responsabile del recupero dell'immagine di profondità per un determinato frame ARCore.
Aggiungi questo file:

be8d14dfe9656551.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 acquireDepthImage16Bits().
   * This method needs to be called on a thread with an EGL context attached.
   */
  public void update(final Frame frame) {
    try {
      Image depthImage = frame.acquireDepthImage16Bits();
      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;
  }
}

Ora aggiungerai un'istanza di questa classe a DepthCodelabActivity, assicurandoti di avere una copia facilmente accessibile dell'immagine di profondità per ogni frame.

In DepthCodelabActivity.java, aggiungi un'istanza della nuova classe come variabile membro privata:

private final DepthTextureHandler depthTexture = new DepthTextureHandler();

Successivamente, aggiorna il metodo onSurfaceCreated() per inizializzare questa texture, in modo che possa essere utilizzata dagli shader della GPU:

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

Infine, devi compilare questa texture in ogni frame con l'ultima immagine di profondità, cosa che puoi fare chiamando il metodo update() che hai creato sopra sull'ultimo frame recuperato da session.
Poiché il supporto della profondità è facoltativo per questa app, utilizza questa chiamata solo se utilizzi la profondità.

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

Ora hai un'immagine di profondità che viene aggiornata a ogni frame. È pronto per essere utilizzato dagli shader.

Tuttavia, il comportamento dell'app non è ancora cambiato. Ora utilizzerai l'immagine di profondità per migliorare la tua app.

7. Esegui il rendering dell'immagine di profondità (parte 3)

Ora che hai un'immagine di profondità con cui giocare, vorrai vedere che aspetto ha. In questa sezione aggiungerai un pulsante all'app per eseguire il rendering della profondità per ogni frame.

Aggiungere nuovi shader

Esistono molti modi per visualizzare un'immagine di profondità. I seguenti shader forniscono una semplice visualizzazione della mappatura dei colori.

Aggiungere un nuovo shader .vert

In Android Studio:

  1. Innanzitutto, aggiungi i nuovi shader .vert e .frag alla directory src/main/assets/shaders/.
  2. Fai clic con il tasto destro del mouse sulla directory degli shader.
  3. Seleziona Nuovo -> File.
  4. Assegna un nome background_show_depth_map.vert
  5. Impostalo come file di testo.

Nel nuovo file, aggiungi il seguente codice:

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

Ripeti i passaggi precedenti per creare lo shader di framenti nella stessa directory e chiamalo background_show_depth_map.frag.

Aggiungi il seguente codice al nuovo file:

src/main/assets/shaders/background_show_depth_map.frag

precision mediump float;
uniform sampler2D u_Depth;
varying vec2 v_TexCoord;
const highp float kMaxDepth = 20000.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;
}

Successivamente, aggiorna la classe BackgroundRenderer per utilizzare questi nuovi shader, che si trovano in src/main/java/com/google/ar/core/codelab/common/rendering/BackgroundRenderer.java.

Aggiungi i percorsi dei file agli shader nella parte superiore della 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";

Aggiungi altre variabili membro alla classe BackgroundRenderer, poiché verranno eseguiti due shader:

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

Aggiungi un nuovo metodo per compilare questi campi:

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

Aggiungi questo metodo, utilizzato per disegnare con questi shader su ogni frame:

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

Aggiungere un pulsante di attivazione/disattivazione

Ora che hai la possibilità di eseguire il rendering della mappa di profondità, usala. Aggiungi un pulsante che attiva e disattiva questo rendering.

Nella parte superiore del file DepthCodelabActivity, aggiungi un'importazione per il pulsante da utilizzare:

import android.widget.Button;

Aggiorna la classe per aggiungere un membro booleano che indica se il rendering della profondità è attivato (è disattivato per impostazione predefinita):

private boolean showDepthMap = false;

Poi, aggiungi il pulsante che controlla il valore booleano showDepthMap alla fine del metodo 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);
          }
        });

Aggiungi queste stringhe 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>

Aggiungi questo pulsante alla parte inferiore del layout dell'app in 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"/>

Ora il pulsante controlla il valore del booleano showDepthMap. Utilizza questo flag per controllare se la mappa di profondità viene sottoposta a rendering.

Nel metodo onDrawFrame() in DepthCodelabActivity, aggiungi:

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

Passa la texture di profondità a backgroundRenderer aggiungendo la seguente riga in onSurfaceCreated():

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

Ora puoi vedere l'immagine di profondità di ogni frame premendo il pulsante in alto a destra dello schermo.

Esecuzione senza supporto dell'API Depth

Esecuzione con il supporto dell'API Depth

[Facoltativo] Animazione profondità avanzata

Attualmente l'app mostra direttamente la mappa di profondità. I pixel rossi rappresentano le aree vicine. I pixel blu rappresentano le aree lontane.

Esistono molti modi per trasmettere informazioni sulla profondità. In questa sezione, modificherai lo shader per pulsare periodicamente la profondità, modificando lo shader in modo che mostri solo la profondità all'interno di bande che si allontanano ripetutamente dalla videocamera.

Inizia aggiungendo queste variabili all'inizio di background_show_depth_map.frag:

uniform float u_DepthRangeToRenderMm;
const float kDepthWidthToRenderMm = 350.0;
  • Quindi, utilizza questi valori per filtrare i pixel da coprire con i valori di profondità nella funzione main() dello shader:
// 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 questo punto, aggiorna BackgroundRenderer.java per mantenere questi parametri dello shader. Aggiungi i seguenti campi all'inizio della classe:

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

All'interno del metodo createDepthShaders(), aggiungi quanto segue per abbinare questi parametri al programma shader:

depthRangeToRenderMmParam = GLES20.glGetUniformLocation(depthProgram, "u_DepthRangeToRenderMm");
  • Infine, puoi controllare questo intervallo nel tempo all'interno del metodo drawDepth(). Aggiungi il seguente codice, che incrementa questo intervallo ogni volta che viene disegnato un frame:
// 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);

Ora la profondità è visualizzata come un impulso animato che scorre nella scena.

b846e4365d7b69b1.gif

Puoi modificare i valori forniti qui per rendere l'impulso più lento, più veloce, più ampio, più stretto e così via. Puoi anche provare a esplorare nuovi modi per modificare lo shader per mostrare le informazioni sulla profondità.

8. Utilizzare l'API Depth per l'occlusione (parte 4)

Ora gestirai l'occlusione degli oggetti nella tua app.

L'occlusione si verifica quando l'oggetto virtuale non può essere visualizzato completamente perché ci sono oggetti reali tra l'oggetto virtuale e la videocamera. La gestione dell'occlusione è essenziale per rendere coinvolgenti le esperienze AR.

Il rendering corretto degli oggetti virtuali in tempo reale migliora il realismo e la credibilità della scena aumentata. Per altri esempi, guarda il nostro video sulla combinazione di realtà con l'API Depth.

In questa sezione, aggiornerai la tua app in modo da includere oggetti virtuali solo se è disponibile la profondità.

Aggiunta di nuovi shader per oggetti

Come nelle sezioni precedenti, aggiungerai nuovi shader per supportare le informazioni sulla profondità. Questa volta puoi copiare gli shader degli oggetti esistenti e aggiungere la funzionalità di occlusione.

È importante conservare entrambe le versioni degli shader degli oggetti, in modo che l'app possa decidere in fase di runtime se supportare la profondità.

Crea copie dei file shader object.vert e object.frag nella directory src/main/assets/shaders.

  • Copia object.vert nel file di destinazione src/main/assets/shaders/occlusion_object.vert
  • Copia object.frag nel file di destinazione src/main/assets/shaders/occlusion_object.frag

All'interno di occlusion_object.vert, aggiungi la seguente variabile sopra main():

varying vec3 v_ScreenSpacePosition;

Imposta questa variabile in fondo a main():

v_ScreenSpacePosition = gl_Position.xyz / gl_Position.w;

Aggiorna occlusion_object.frag aggiungendo queste variabili sopra main() nella parte superiore del file:

varying vec3 v_ScreenSpacePosition;

uniform sampler2D u_Depth;
uniform mat3 u_UvTransform;
uniform float u_DepthTolerancePerMm;
uniform float u_OcclusionAlpha;
uniform float u_DepthAspectRatio;
  • Aggiungi queste funzioni di supporto sopra main() nello shader per semplificare la gestione delle informazioni sulla profondità:
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=*/17500.0, /*max_depth_mm=*/20000.0);

  float visibility =
    max(max(visibility_occlusion, u_OcclusionAlpha),
      max(visibility_depth_near, visibility_depth_far));

  return visibility;
}

Ora aggiorna main() in occlusion_object.frag in modo che sia consapevole della profondità e applichi l'occlusione. Aggiungi le seguenti righe in fondo al file:

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

Ora che hai una nuova versione degli shader degli oggetti, puoi modificare il codice del renderer.

Rendering dell'occlusione dell'oggetto

Crea una copia del corso ObjectRenderer successivo, disponibile in src/main/java/com/google/ar/core/codelab/common/rendering/ObjectRenderer.java.

  • Seleziona il corso ObjectRenderer.
  • Fai clic con il tasto destro del mouse > Copia.
  • Seleziona la cartella rendering
  • Fai clic con il tasto destro del mouse > Incolla.

7487ece853690c31.png

  • Rinomina il corso in OcclusionObjectRenderer

760a4c80429170c2.png

La nuova classe rinominata dovrebbe ora essere visualizzata nella stessa cartella:

9335c373dc60cd17.png

Apri il file OcclusionObjectRenderer.java appena creato e modifica i percorsi degli shader nella parte superiore del file:

private static final String VERTEX_SHADER_NAME = "shaders/occlusion_object.vert";
private static final String FRAGMENT_SHADER_NAME = "shaders/occlusion_object.frag";
  • Aggiungi queste variabili membro relative alla profondità insieme alle altre nella parte superiore della classe. Le variabili regoleranno la nitidezza del bordo dell'occlusione.
// 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 queste variabili membro con valori predefiniti nella parte superiore della 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;

Inizializza i parametri uniformi per lo shader nel metodo 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");
  • Assicurati che questi valori vengano aggiornati ogni volta che vengono disegnati aggiornando il metodo 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);

Aggiungi le seguenti righe all'interno di draw() per attivare la modalità di fusione nel rendering, in modo che la trasparenza possa essere applicata agli oggetti virtuali quando sono occlusi:

// 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);
  • Aggiungi i seguenti metodi in modo che i chiamanti di OcclusionObjectRenderer possano fornire le informazioni sulla profondità:
// 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;
}

Controllare l'occlusione degli oggetti

Ora che hai un nuovo OcclusionObjectRenderer, puoi aggiungerlo al tuo DepthCodelabActivity e scegliere quando e come utilizzare il rendering dell'occlusione.

Attiva questa logica aggiungendo un'istanza di OcclusionObjectRenderer all'attività, in modo che sia ObjectRenderer che OcclusionObjectRenderer siano membri di 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();
  • Puoi quindi controllare quando viene utilizzato questo occludedVirtualObject in base al fatto che il dispositivo corrente supporti l'API Depth. Aggiungi queste righe all'interno del metodo onSurfaceCreated, sotto la configurazione di 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);
}

Sui dispositivi in cui la profondità non è supportata, viene creata l'istanza occludedVirtualObject, ma non viene utilizzata. Sugli smartphone con profondità, vengono inizializzate entrambe le versioni e viene presa una decisione in fase di runtime su quale versione del renderer utilizzare durante il disegno.

All'interno del metodo onDrawFrame(), trova il codice esistente:

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

Sostituisci questo codice con il seguente:

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

Infine, assicurati che l'immagine di profondità sia mappata correttamente nel rendering di output. Poiché l'immagine di profondità ha una risoluzione diversa e potenzialmente un formato diverso rispetto allo schermo, le coordinate della texture potrebbero essere diverse tra l'immagine di profondità e l'immagine della videocamera.

  • Aggiungi il metodo helper getTextureTransformMatrix() alla fine del file. Questo metodo restituisce una matrice di trasformazione che, se applicata, fa corrispondere correttamente le coordinate UV dello spazio schermo con le coordinate della texture del quadrilatero utilizzate per il rendering del feed della videocamera. Inoltre, tiene conto dell'orientamento 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() richiede la seguente importazione all'inizio del file:

import com.google.ar.core.Coordinates2d;

Vuoi calcolare la trasformazione tra queste coordinate della texture ogni volta che la texture dello schermo cambia (ad esempio se lo schermo ruota). Questa funzionalità è controllata.

Aggiungi il seguente flag all'inizio del file:

// Add this member at the top of the file.
private boolean calculateUVTransform = true;
  • All'interno di onDrawFrame(), controlla se la trasformazione memorizzata deve essere ricalcolata dopo la creazione del frame e della videocamera:
// Add these lines inside onDrawFrame() after frame.getCamera().
if (frame.hasDisplayGeometryChanged() || calculateUVTransform) {
  calculateUVTransform = false;
  float[] transform = getTextureTransformMatrix(frame);
  occludedVirtualObject.setUvTransformMatrix(transform);
}

Con queste modifiche, ora puoi eseguire l'app con l'occlusione degli oggetti virtuali.

Ora la tua app dovrebbe funzionare correttamente su tutti gli smartphone e utilizzare automaticamente la profondità per l'occlusione quando è supportata.

Esecuzione dell'app con il supporto dell'API Depth

Esecuzione dell'app senza supporto dell'API Depth

9. [Facoltativo] Migliora la qualità dell'occlusione

Il metodo per l'occlusione basata sulla profondità, implementato sopra, fornisce un'occlusione con confini netti. Man mano che la videocamera si allontana dall'oggetto, le misurazioni della profondità possono diventare meno accurate, il che può comportare artefatti visivi.

Possiamo attenuare questo problema aggiungendo un ulteriore sfocatura al test di occlusione, che produce un bordo più uniforme per gli oggetti virtuali nascosti.

occlusion_object.frag

Aggiungi la seguente variabile uniforme nella parte superiore di occlusion_object.frag:

uniform float u_OcclusionBlurAmount;

Aggiungi questa funzione helper appena sopra main() nello shader, che applica una sfocatura del kernel al campionamento dell'occlusione:

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

Sostituisci questa riga esistente in main():

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

con questa riga:

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

Aggiorna il renderer per sfruttare questa nuova funzionalità dello shader.

OcclusionObjectRenderer.java

Aggiungi le seguenti variabili membro nella parte superiore della classe:

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

Aggiungi quanto segue all'interno del metodo createOnGlThread:

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

Aggiungi quanto segue all'interno del metodo draw:

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

Confronto visivo

Con queste modifiche, il confine di occlusione dovrebbe ora essere più uniforme.

10. Build-Run-Test

Crea ed esegui la tua app

  1. Collega un dispositivo Android tramite USB.
  2. Scegli File > Build and Run.
  3. Salva come: ARCodeLab.apk.
  4. Attendi che l'app venga creata e implementata sul dispositivo.

La prima volta che tenti di eseguire il deployment dell'app sul tuo dispositivo:

  • Devi consentire il debug USB sul dispositivo. Seleziona OK per continuare.
  • Ti verrà chiesto se l'app ha l'autorizzazione a utilizzare la fotocamera del dispositivo. Consenti l'accesso per continuare a utilizzare la funzionalità AR.

Testare l'app

Quando esegui l'app, puoi testarne il comportamento di base tenendo in mano il dispositivo, spostandoti nello spazio e scansionando lentamente un'area. Prova a raccogliere almeno 10 secondi di dati e a scansionare l'area da diverse direzioni prima di passare al passaggio successivo.

Risoluzione dei problemi

Configurare il dispositivo Android per lo sviluppo

  1. Collega il dispositivo alla macchina di sviluppo con un cavo USB. Se sviluppi utilizzando Windows, potresti dover installare il driver USB appropriato per il tuo dispositivo.
  2. Per attivare il debug USB nella finestra Opzioni sviluppatore, procedi nel seguente modo:
  3. Apri l'app Impostazioni.
  4. Se il dispositivo utilizza Android 8.0 o versioni successive, seleziona Sistema. Altrimenti, procedi al passaggio successivo.
  5. Scorri fino in fondo e seleziona Informazioni sullo smartphone.
  6. Scorri fino in fondo e tocca Numero build 7 volte.
  7. Torna alla schermata precedente, scorri verso il basso e tocca Opzioni sviluppatore.
  8. Nella finestra Opzioni sviluppatore, scorri verso il basso per trovare e attivare Debug USB.

Puoi trovare informazioni più dettagliate su questa procedura sul sito web di Google per gli sviluppatori Android.

cfa20a722a68f54f.png

Se si verifica un errore di build relativo alle licenze (Failed to install the following Android SDK packages as some licences have not been accepted), puoi utilizzare i seguenti comandi per esaminare e accettare queste licenze:

cd <path to Android SDK>

tools/bin/sdkmanager --licenses

11. Complimenti

Congratulazioni, hai creato ed eseguito correttamente la tua prima app di realtà aumentata basata sulla profondità utilizzando l'API ARCore Depth di Google.

Domande frequenti