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) sui dispositivi mobili. Utilizzando API diverse, ARCore consente al dispositivo di un utente di osservare e ricevere informazioni sul suo ambiente e di interagire con queste informazioni.

In questo codelab, analizzerai la procedura di creazione di una semplice app compatibile con AR che utilizza l'API ARCore Depth.

Prerequisiti

Questo codelab è stato scritto per gli sviluppatori con una conoscenza dei concetti fondamentali della realtà aumentata.

Cosa creerai

1a0236e93212210c.gif

Creerai un'app che utilizza l'immagine di profondità per ogni fotogramma per visualizzare la geometria della scena e occlusione gli asset virtuali posizionati. Dovrai seguire i passaggi specifici di:

  • Controllo del supporto dell'API Depth sullo smartphone in corso...
  • Recupero dell'immagine di profondità per ogni frame
  • Visualizzare informazioni approfondite in vari modi (vedi l'animazione sopra).
  • Uso della profondità per aumentare il realismo delle app con occlusione.
  • Imparare a gestire agevolmente 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à (dette anche immagini profondità). Puoi utilizzare le informazioni fornite da una mappa di profondità per mostrare con precisione gli oggetti virtuali davanti o dietro a oggetti del mondo reale, in modo da offrire esperienze utente immersive e realistiche.

L'API ARCore Depth consente di accedere alle immagini di profondità corrispondenti a ogni frame fornito dalla sessione di ARCore. Ogni pixel misura la distanza tra la fotocamera e l'ambiente, il che migliora il realismo della tua app AR.

Una funzionalità chiave alla base dell'API Depth è l'occlusione, ovvero la capacità degli oggetti digitali di apparire con precisione rispetto agli oggetti del mondo reale. In questo modo gli oggetti hanno la sensazione di trovarsi effettivamente nell'ambiente circostante.

Questo codelab ti guiderà nella procedura di creazione di una semplice app per l'AR che utilizza immagini di profondità per occlusione di oggetti virtuali dietro superfici reali 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 dispositivo. Prima di continuare, assicurati di vedere esattamente un solo dispositivo.

Scarica e installa il Code

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

Oppure 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 e apri la directory depth_codelab_io2020.

Questo è un singolo progetto Gradle con più moduli. Se il riquadro Progetto nella parte superiore sinistra di Android Studio non è già visualizzato nel riquadro Progetto, fai clic su Progetti dal menu a discesa.

Il risultato dovrebbe essere simile al seguente:

Questo progetto contiene i seguenti moduli:

  • part0_work: l'app iniziale. Dovresti apportare modifiche a questo modulo durante questo codelab.
  • part1: fai riferimento al codice di come dovrebbero essere le modifiche quando completi la Parte 1.
  • part2: 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. Ci sono anche soluzioni complete per ogni parte del codelab. Ogni modulo è un'app generabile.

4. Esegui l'app Starter

  1. Fai clic su Esegui > Esegui... &gt; "part0_work". Nella finestra di dialogo Seleziona la destinazione del deployment visualizzata, il tuo dispositivo dovrebbe essere elencato in 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 di accesso alla 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 dell'aereo per posizionare un ancoraggio. Nel punto in cui è stato posizionato l'ancoraggio verrà disegnata una figura Android. Quest'app ti consente di posizionare un solo ancoraggio alla volta.
  3. Sposta il dispositivo. La figura dovrebbe apparire nella stessa posizione, anche se il dispositivo si muove.

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

Ad esempio, se posizioni una figura Android dietro una sedia, il rendering sembrerà sospeso in primo piano, dato che l'applicazione non sa che la sedia è lì e dovrebbe nascondere Android.

6182cf62be13cd97.png beb0d327205f80ee.png e4497751c6fad9a7.png

Per risolvere il problema, useremo l'API Depth per migliorare la 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 la funzionalità in un'app utilizzando queste immagini di profondità, devi assicurarti che l'app sia in esecuzione su un dispositivo supportato.

Aggiungi a DepthCodelabActivity un nuovo membro privato che funga da flag per memorizzare se il dispositivo attuale supporta la profondità:

private boolean isDepthSupported;

Possiamo completare questo flag dall'interno della funzione onResume(), dove viene creata una nuova sessione.

Individua il codice esistente:

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

Aggiorna il codice in:

// 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 funzionalità basate sulla profondità.

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

Aggiungi un altro messaggio allo Snackbar. Il pulsante viene 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 tue 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 in modo che chiami l'API Depth e recupererai le immagini depth 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 tra la fotocamera del dispositivo e l'ambiente reale.

Ora userai 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 per l'utilizzo da parte della GPU.

Per prima cosa, aggiungi una nuova classe al progetto.
DepthTextureHandler si occupa 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 di facile accesso dell'immagine di profondità per ogni frame.

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

private final DepthTextureHandler depthTexture = new DepthTextureHandler();

Dopodiché aggiorna il metodo onSurfaceCreated() per inizializzare questa texture, in modo che possa essere utilizzata dai nostri shaker GPU:

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

Infine, ti conviene compilare questa texture in ogni frame con l'ultima immagine di profondità, operazione che può essere eseguita richiamando il metodo update() che hai creato in precedenza sull'ultimo frame recuperato da session.
Poiché il supporto della profondità è facoltativo per questa app, usa questa chiamata soltanto se usi 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 con ogni frame. È pronta per essere usata dai tuoi ombreggiatori.

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

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

Ora che hai un'immagine di profondità con cui sperimentare, dovrai vedere che aspetto ha. In questa sezione, aggiungerai un pulsante all'app per visualizzare la profondità di ogni frame.

Aggiungere nuovi ombreggiatori

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

Aggiungi un nuovo streamr .vert

In Android Studio:

  1. Innanzitutto, aggiungi nuovi Shar .vert e .frag alla directory src/main/assets/shaders/.
  2. Fai clic con il tasto destro del mouse sulla directory Shars
  3. Seleziona Nuovo -> File
  4. Assegna il nome background_show_depth_map.vert
  5. Impostala 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 shaker dei frammenti nella stessa directory e assegnargli il nome background_show_depth_map.frag.

Aggiungi il seguente codice a questo 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;
}

Dopodiché, aggiorna la classe BackgroundRenderer per usare questi nuovi ombreggiatori, che si trovano in src/main/java/com/google/ar/core/codelab/common/rendering/BackgroundRenderer.java.

Aggiungi i percorsi dei file agli Shar 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 dei membri 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, che viene utilizzato per disegnare con questi Shaper 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 per attivare e disattivare 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 profondo è 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 nella parte inferiore del layout delle 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"/>

Il pulsante ora controlla il valore del valore booleano showDepthMap. Utilizza questo flag per controllare se il rendering della mappa di profondità viene eseguito.

Torna al metodo onDrawFrame() in DepthCodelabActivity e 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 in profondità di ogni fotogramma premendo il pulsante in alto a destra dello schermo.

Esecuzione senza supporto dell'API Depth

Esecuzione con il supporto dell'API Depth

[Facoltativo] Animazione di profondità fantasia

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

Esistono molti modi per trasmettere informazioni approfondite. In questa sezione, modificherai periodicamente la profondità del impulso, modificandola in modo che mostri solo la profondità all'interno dei cinturini che si allontanano ripetutamente dalla videocamera.

Per iniziare, aggiungi queste variabili all'inizio di background_show_depth_map.frag:

uniform float u_DepthRangeToRenderMm;
const float kDepthWidthToRenderMm = 350.0;
  • Dopodiché usa questi valori per filtrare i pixel da coprire con i valori di profondità nella funzione main() dello shaker:
// Add this line at the end of main().
gl_FragColor.a = clamp(1.0 - abs((depth_mm - u_DepthRangeToRenderMm) / kDepthWidthToRenderMm), 0.0, 1.0);

Dopodiché, aggiorna BackgroundRenderer.java per mantenere questi parametri shaker. Aggiungi i seguenti campi all'inizio del corso:

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 associare questi parametri al programma shaker:

depthRangeToRenderMmParam = GLES20.glGetUniformLocation(depthProgram, "u_DepthRangeToRenderMm");
  • Infine, puoi controllare questo intervallo nel tempo con il metodo drawDepth(). Aggiungi il seguente codice, che incrementa questo intervallo ogni volta che viene tracciato 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 battito animato che scorre attraverso la scena.

b846e4365d7b69b1.gif

Se vuoi, puoi modificare i valori forniti qui per aumentare la velocità, la velocità e la distanza del battito. Puoi anche provare nuovi modi per cambiare lo streamr in modo da mostrare informazioni più approfondite.

8. Utilizzo dell'API Depth per la copertura (parte 4)

Ora potrai gestire la copertura degli oggetti nell'app.

Con occlusione si intende ciò che accade quando non è possibile eseguire il rendering completo dell'oggetto virtuale, perché ci sono oggetti reali tra l'oggetto virtuale e la videocamera. La gestione dell'occlusione è essenziale per rendere le esperienze AR immersive.

Rappresentare correttamente oggetti virtuali in tempo reale migliora il realismo e la credibilità della scena aumentata. Per ulteriori esempi, guarda il nostro video su come combinare le realtà con l'API Depth.

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

Aggiunta di nuovi Shar per oggetti

Come nelle sezioni precedenti, aggiungerai nuovi oscuratori per supportare informazioni di approfondimento. Questa volta puoi copiare gli streamr di oggetti esistenti e aggiungere la funzionalità di occlusione.

È importante mantenere entrambe le versioni degli oggetti Shaper, in modo che la tua app possa decidere in fase di runtime se supportare la profondità.

Crea copie dei file Shar 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 alla fine di main():

v_ScreenSpacePosition = gl_Position.xyz / gl_Position.w;

Aggiorna occlusion_object.frag aggiungendo queste variabili sopra main() all'inizio 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 helper sopra main()nello streamr per gestire più facilmente le informazioni più approfondite:
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 per rilevare la profondità e applicare 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 streamr di oggetti, puoi modificare il codice del renderer.

Copertura degli oggetti di rendering

Crea una copia del corso ObjectRenderer successivo, che si trova 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 di rendering
  • Fai clic con il tasto destro del mouse > Incolla

7487ece853690c31.png

  • Rinomina il corso in OcclusionObjectRenderer

760a4c80429170c2.png

Il nuovo corso rinominato dovrebbe ora apparire nella stessa cartella:

9335c373dc60cd17.png

Apri il OcclusionObjectRenderer.java appena creato e modifica i percorsi dello shaker 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 dei membri relative alla profondità con le altre all'inizio della classe. Le variabili regolano la nitidezza del bordo di 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 all'inizio 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 shaker 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 tracciati 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 occulti:

// 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 per consentire ai chiamanti di OcclusionObjectRenderer di fornire informazioni dettagliate:
// 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;
}

Controllo dell'occlusione degli oggetti

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

Abilita questa logica aggiungendo un'istanza di OcclusionObjectRenderer all'attività, in modo che ObjectRenderer e 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 controllare in seguito quando questo occludedVirtualObject viene utilizzato a seconda che il dispositivo attuale supporti l'API Depth. Aggiungi queste righe all'interno del metodo onSurfaceCreated, sotto dove è configurato 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, l'istanza occludedVirtualObject viene creata, ma non viene utilizzata. Sui telefoni con profondità, entrambe le versioni vengono inizializzate e viene presa una decisione in fase di runtime per stabilire quale versione del renderer utilizzare per il disegno.

Nel metodo onDrawFrame(), individua 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, proporzioni diverse rispetto allo schermo, le coordinate della trama potrebbero essere diverse tra l'immagine stessa e l'immagine della fotocamera.

  • Aggiungi il metodo helper getTextureTransformMatrix() in fondo al file. Questo metodo restituisce una matrice di trasformazione che, se applicata, consente agli UV dello spazio sullo schermo di abbinare correttamente le coordinate della trama quadrupla utilizzate per il rendering del feed della videocamera. Prende anche in considerazione l'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 nella parte superiore del file:

import com.google.ar.core.Coordinates2d;

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

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

Una volta apportate queste modifiche, puoi eseguire l'app con l'occlusione di oggetti virtuali.

Ora l'app dovrebbe funzionare perfettamente su tutti gli smartphone e utilizzare automaticamente la profondità per occlusione, se supportata.

App in esecuzione con supporto dell'API Depth

App in esecuzione senza supporto dell'API Depth

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

Il metodo per l'occlusione basata sulla profondità, implementato in precedenza, fornisce un'occlusione con confini netti. Man mano che la fotocamera si allontana dall'oggetto, le misurazioni della profondità possono diventare meno precise, generando artefatti visivi.

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

occlusion_object.frag

Aggiungi la seguente variabile uniforme all'inizio di occlusion_object.frag:

uniform float u_OcclusionBlurAmount;

Aggiungi questa funzione helper appena sopra main() nello streamr, che applica una sfocatura del kernel al campionamento di 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à Shar.

OcclusionObjectRenderer.java

Aggiungi le seguenti variabili membro all'inizio 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

Il confine di occlusione dovrebbe ora essere più uniforme con queste modifiche.

10. Build-Run-Test

Crea ed esegui la tua app

  1. Collega un dispositivo Android tramite USB.
  2. Scegli File > Build ed esegui.
  3. Salva con nome: ARCodeLab.apk.
  4. Attendi la creazione e il deployment dell'app sul dispositivo.

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

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

Testare l'app

Quando esegui la tua app, puoi testarne il comportamento di base tenendo il dispositivo, muovendoti nello spazio e scansionando lentamente un'area. Prova a raccogliere almeno 10 secondi di dati e scansiona l'area da diverse direzioni prima di andare al passaggio successivo.

Risoluzione dei problemi

Configurazione del dispositivo Android per lo sviluppo

  1. Collega il dispositivo alla macchina di sviluppo con un cavo USB. Se sviluppi utilizzando Windows, potrebbe essere necessario 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 sul tuo dispositivo è installato Android 8.0 o versioni successive, seleziona Sistema. In caso contrario, vai 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 fino in fondo 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 per sviluppatori Android di Google.

cfa20a722a68f54f.png

Se si verifica un errore della build relativo alle licenze (Impossibile installare i seguenti pacchetti SDK Android perché alcune licenze non sono state accettate), puoi usare i comandi seguenti per esaminare e accettare queste licenze:

cd <path to Android SDK>

tools/bin/sdkmanager --licenses

11. Complimenti

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

Domande frequenti