ARCore Depth

ARCore は、モバイル デバイスで拡張現実(AR)アプリを作成するためのプラットフォームです。Google の ARCore Depth API を使うと、ARCore のセッションから得られた各フレームに対応する深度画像にアクセスできます。各ピクセルからはカメラから環境までの測距値が得られます。今回の Codelab では、これを利用して AR アプリケーションで拡張現実を実現します。

Depth API は ARCore 対応デバイスの一部でのみサポートされます。深度呼び出しに対応しているスマートフォンについては、こちらのリストをご覧ください。Depth API を利用できるのは Android だけです。

この Codelab では、深度画像を使って実世界のサーフェスの背後にある仮想アセットのオクルージョンを行い、そうして検出された世界のジオメトリを視覚化する単純な AR 対応アプリを作成する過程を説明します。

作成するアプリの概要

d9bd5136c54ce47a.gif

この Codelab では、各フレームの深度画像を使ってシーンのジオメトリを視覚化し、配置された仮想アセットでオクルージョンを行うアプリを作成します。この Codelab では以下の内容を説明します。

  1. スマートフォンの Depth API サポートを確認する
  2. 各フレームの深度画像を取得する
  3. 深度情報を視覚化する複数の方法(上のアニメーションを参照)
  4. 深度情報を使いオクルージョンでアプリの現実感を向上させる方法
  5. Depth API に対応していないスマートフォンを適切に取り扱う方法

注: 途中で問題が発生した場合は、最後のセクションに移動して、トラブルシューティングのヒントを確認してください。

この Codelab を完了するには、特定のハードウェアとソフトウェアが必要になります。

ハードウェア要件

ソフトウェア要件

開発マシンのセットアップ

ARCore デバイスを USB ケーブルでパソコンに接続します。開発用デバイスでは USB デバッグを許可してください。ターミナルを開いて、次のように adb devices を実行します。

adb devices

List of devices attached
<DEVICE_SERIAL_NUMBER>    device

<DEVICE_SERIAL_NUMBER> は開発用デバイスに固有の文字列です。デバイスが 1 台だけ表示されていることを確認してから、次へ進んでください。

コードのダウンロードとインストール

次のようにしてリポジトリのクローンを作成するか、

git clone https://github.com/googlecodelabs/arcore-depth

または、次のボタンで ZIP ファイルをダウンロードして展開します。

ZIP をダウンロード

Android Studio を起動します。[Open an existing Android Studio project] をクリックします。次に、先ほどダウンロードした ZIP ファイルを展開したディレクトリに移動し、depth_codelab_io2020 ディレクトリをダブルクリックします。

これは複数のモジュールがある単一の Gradle プロジェクトです。Android Studio の左上に [Project] ペインがまだ表示されていない場合は、プルダウン メニューから [Projects] をクリックします。結果は次のようになります。

このプロジェクトに含まれるモジュール:

  • part0_work: スターター アプリです。この Codelab を実施する際には、このモジュールを編集する必要があります。
  • part1: パート 1 を完了する際の編集内容を示す参照コードです。
  • part2: パート 2 を完了する際の参照コードです。
  • part3: パート 3 を完了する際の参照コードです。
  • part4_completed: アプリの最終バージョンです。パート 4 とこの Codelab を完了する際の参照コードです。

part0_work モジュールで作業します。また、Codelab の各パートに完全な解答が用意されています。各モジュールはビルド可能なアプリになっています。

[Run] > [Run...] > [‘part0_work'] をクリックします。表示された [Select Deployment Target] ダイアログの [Connected Devices] の下に開発用デバイスが表示されるはずです。開発用デバイスを選択して、[OK] をクリックします。Android Studio によって初期アプリがビルドされ、開発用デバイスで実行されます。

初めてアプリを実行したとき、CAMERA パーミッションがリクエストされます。[許可] をタップして続行します。

アプリの使用方法

  1. デバイスを動かして平面が検出されるようにします。動かすタイミングを示すメッセージが下部に表示されます。
  2. 平面上のどこかをタップしてアンカーを置きます。アンカーを置いた場所に Android のキャラクターが描画されます。このアプリで一度に置けるアンカーは 1 個だけです。
  3. デバイスを動かします。デバイスを動かしてもキャラクターは同じ場所に表示されたままです。

現在のところ、このアプリケーションは非常にシンプルで、実世界のシーンのジオメトリについての認識度は高くありません。たとえば、Android のキャラクターを椅子の背後に置いた場合、キャラクターは椅子の前で浮いているようにレンダリングされます。これは椅子がそこにあって Android を隠す必要があることがアプリで認識されていないためです。

5e8f5fe9098d316e.png 76f41b692224801b.png 3f320c851d1903d.png

この問題を解決するため、Depth API の使用によりアプリケーションの没入感と現実感を高めます。

ARCore Depth API は対応デバイスの一部で動作します。上記の深度画像を使用する機能をアプリに組み込む前に、まずアプリが対応デバイスで動作していることを確認する必要があります。

現在のデバイスが深度に対応しているかどうかを保存するフラグとなる非公開メンバーを DepthCodelabActivity に追加します。

private boolean isDepthSupported;

このフラグは新しいセッションが作成される onResume() 関数の中で設定できます。次の既存のコードを見つけます。

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

次のコードに更新します。

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

これで AR セッションが適切に構成され、深度ベースの機能を使用できるかどうかをアプリで判別できるようになります。

ユーザーにも、このセッションに深度が利用できるかどうかを知らせる必要があります。画面下部に表示されるスナックバーにメッセージを追加します。

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

そして、このメッセージを必要に応じて onDrawFrame() 内で表示します。

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

このアプリが深度に対応していないデバイスで実行されている場合、さきほど追加したメッセージが下部に表示されます。

feb1c9f42f3cf396.png

次に、Depth API を呼び出してフレームごとに深度画像を取得するようにアプリを更新します。

Depth API は、環境の 3D 計測結果を取り込んで、そのデータを含んだ深度画像をアプリケーションに与えます。この画像の各ピクセルはカメラから実世界の環境までの測距値を表しています。

今回の Codelab では、この深度画像を使ってアプリでのレンダリングと視覚化を改善します。最初のステップでは、フレームごとに深度画像を取得し、GPU で使用されるテクスチャにバインドします。

最初に、新しいクラスをプロジェクトに追加します。DepthTextureHandler の役割は、指定された ARCore フレームの深度画像を取得することです。次のファイルを追加します。

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

次に、このクラスのインスタンスを DepthCodelabActivity に追加して、各フレームで深度画像のコピーに簡単にアクセスできるようにします。

DepthCodelabActivity.java に新しいクラスのインスタンスを非公開メンバー変数として追加します。

private final DepthTextureHandler depthTexture = new DepthTextureHandler();

次に、GPU シェーダーで利用できるように、onSurfaceCreated() メソッドを更新してこのテクスチャを初期化します。

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

最後に、フレームごとにこのテクスチャに最新の深度画像を設定するには、上で作成した update() メソッドを session から取得した最新のフレームで呼び出します。このアプリの深度対応はオプションなので、呼び出しを行うのは深度を使う場合のみです。

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

これで、フレームごとに更新される深度画像ができました。また、シェーダーで使用できるようになりました。しかし、まだアプリの動作は変わっていません。深度画像を使用してアプリを改良しましょう。

深度画像が用意できたので、それを表示しましょう。このセクションでは、各フレームの深度をレンダリングするボタンをアプリに追加します。

新しいシェーダーを追加する

深度画像には多くの表示方法があります。次のシェーダーはシンプルなカラー マッピングによる視覚化を行います。

まず、新しい .vert シェーダーと .frag シェーダーを src/main/assets/shaders/ ディレクトリに追加します。

新しい .vert シェーダーの追加

Android Studio で行う手順:

  1. shaders ディレクトリを右クリック
  2. [New] -> [File] を選択
  3. background_show_depth_map.vert という名前を付ける
  4. テキスト ファイルに設定

新しいファイルに次のコードを追加します。

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

上記の手順を繰り返して、同じディレクトリに background_show_depth_map.frag という名前のフラグメント シェーダーを作成します。この新しいファイルに次のコードを追加します。

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

次に、上記の新しいシェーダーを使用するように src/main/java/com/google/ar/core/codelab/common/rendering/BackgroundRenderer.java にある BackgroundRenderer クラスを更新します。

このクラスの先頭にシェーダーのファイルパスを追加します。

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

2 つのシェーダーを実行するので、BackgroundRenderer クラスにさらにメンバー変数を追加します。

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

上記のフィールドを設定する新しいメソッドを追加します。

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

フレームごとに上記のシェーダーで描画するためのメソッドを追加します。

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

切り替えボタンを追加する

深度マップをレンダリングする機能ができたので使ってみましょう。DepthCodelabActivity に、このレンダリングのオンとオフを切り替えるボタンを追加します。

DepthCodelabActivity ファイルの先頭に、使用するボタンの import を追加します。

import android.widget.Button;

クラスを更新して、深度レンダリングの切り替え(デフォルトではオフ)を示すブール値のメンバーを追加します。

private boolean showDepthMap = false;

次に、onCreate() メソッドの最後に showDepthMap ブール値を制御するボタンを追加します。

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

次の文字列を 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>

このボタンを 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"/>

これでボタンがブール値 showDepthMap を制御するようになりました。このフラグを使用して、深度マップをレンダリングするかどうかを制御します。DepthCodelabActivity のメソッド onDrawFrame() に戻り、次を追加します。

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

onSurfaceCreated() に次の行を追加して、深度テクスチャを backgroundRenderer に渡します。

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

これで、画面右上のボタンを押すと各フレームの深度画像が表示されるようになりました。

Depth API サポートなしで実行

Depth API サポートありで実行

(省略可)幻想的な深度アニメーション

現在、このアプリでは深度マップをそのまま表示しています。赤いピクセルが近いエリアを示し、青いピクセルが遠いエリアを示しています。

深度情報の表現には、さまざまな方法があります。このサブセクションでは、シェーダーをカメラから繰り返し遠ざかる帯の中だけで深度を表示するように変更して、周期的に深度が明滅するようにします。

まず、以下の変数を background_show_depth_map.frag の先頭に追加します。

uniform float u_DepthRangeToRenderMm;
const float kDepthWidthToRenderMm = 350.0;

次に、シェーダーの main() 関数で、ピクセルを深度値の範囲でフィルタリングするために上記の値を使用します。

// Add this line at the end of main().
gl_FragColor.a = clamp(1.0 - abs((depth_mm - u_DepthRangeToRenderMm) / kDepthWidthToRenderMm), 0.0, 1.0);

次に、これらのシェーダー パラメータを保持するよう BackgroundRenderer.java を更新します。次のフィールドをクラスの先頭に追加します。

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

createDepthShaders() メソッドに次の行を追加して、パラメータがシェーダー プログラムと一致するようにします。

depthRangeToRenderMmParam = GLES20.glGetUniformLocation(depthProgram, "u_DepthRangeToRenderMm");

最後に、drawDepth() メソッド内でこの範囲を時間の経過とともに制御します。次のコードを追加して、この範囲がフレームの描画のたびにインクリメントされるようにします。

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

これで深度がシーン内を移動しながら明滅するアニメーションで視覚化されました。

37e2a86b833150f8.gif

ここで指定した値を変更すると、明滅を遅くしたり、速くしたり、細くしたり、太くしたりなどできます。また、深度情報を表示するシェーダーの新しい変更方法を試すこともできます。

オクルージョンとは、仮想オブジェクトとカメラの間に実世界のサーフェスがあるために仮想オブジェクトの一部がレンダリングされていない場合のことを指します。

仮想オブジェクトをその場に正しくレンダリングできれば、拡張シーンの現実性や真実性が向上します。その他の例については、Depth API を使用した混合現実に関する動画をご覧ください。

このセクションでは、深度が利用できる場合は仮想オブジェクトを取り込むようにアプリを更新します。

新しいオブジェクト シェーダーの追加

前のセクションと同じように、深度情報に対応した新しいシェーダーを追加します。今回は、既存のオブジェクト シェーダーをコピーし、オクルージョン機能を追加します。深度に対応するかどうかをアプリケーションで実行時に判断できるように、両方のバージョンのオブジェクト シェーダーを持っておくことが重要です。

object.vertobject.frag のシェーダー ファイルを src/main/assets/shaders ディレクトリにコピーします。

  1. object.vert をファイル src/main/assets/shaders/occlusion_object.vert にコピーします。
  2. object.frag をファイル src/main/assets/shaders/occlusion_object.frag にコピーします。

occlusion_object.vert 内の main() の上に次の変数を追加します。

varying vec3 v_ScreenSpacePosition;

main() の最後でこの変数を設定します。

v_ScreenSpacePosition = gl_Position.xyz / gl_Position.w;

occlusion_object.frag を更新して、次の変数をファイルの先頭で main() の上に追加します。

varying vec3 v_ScreenSpacePosition;

uniform sampler2D u_Depth;
uniform mat3 u_UvTransform;
uniform float u_DepthTolerancePerMm;
uniform float u_OcclusionAlpha;
uniform float u_DepthAspectRatio;

次のヘルパー関数をシェーダー内の main() の上に追加して、深度情報を取り扱いやすくします。

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

ここで occlusion_object.fragmain() を更新し、深度が考慮されてオクルージョンが適用されるようにします。ファイルの最後に次の行を追加します。

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

オブジェクト シェーダーの新しいバージョンを作成したので、レンダラのコードを変更できるようになりました。

オブジェクト オクルージョンのレンダリング

今度は src/main/java/com/google/ar/core/codelab/common/rendering/ObjectRenderer.java にある ObjectRenderer クラスのコピーを作成します。

ObjectRenderer クラスを右クリックして [Copy] を選択し、rendering フォルダを右クリックして [Paste] を選択します。

クラスの名前を OcclusionObjectRenderer に変更します。

名前が変更された新しいクラスが同じフォルダに現れます。

新しく作成した OcclusionObjectRenderer.java を開き、ファイルの先頭にあるシェーダーのパスを変更します。

private static final String VERTEX_SHADER_NAME = "shaders/occlusion_object.vert";
private static final String FRAGMENT_SHADER_NAME = "shaders/occlusion_object.frag";

深度関連などのメンバー変数をクラスの先頭に追加します。

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

上記の変数でオクルージョン境界の明瞭度を調整できます。まず、クラスの先頭にデフォルト値を持ったメンバー変数を作成します。

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

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

これらの値が描画されるたびに更新されるように、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);

draw() 内に次の行を追加して、レンダリング時にブレンドモードを有効にし、仮想オブジェクトが隠れたときに透明度が適用されるようにします。

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

OcclusionObjectRenderer の呼び出し元が深度情報を提供できるように、次のメソッドを追加します。

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

オブジェクト オクルージョンの制御

新しい OcclusionObjectRenderer ができたので、それを DepthCodelabActivity に追加してオクルージョン レンダリングを行うタイミングと方法を選択します。

このロジックを有効にするには、アクティビティに OcclusionObjectRenderer のインスタンスを追加し、ObjectRendererOcclusionObjectRenderer の両方を 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();

今度は、現在のデバイスが Depth API に対応しているかどうかに応じて、この occludedVirtualObject が使用されるタイミングを制御します。次の行を onSurfaceCreated メソッド内で 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);
}

深度に対応していないデバイスでは、occludedVirtualObject インスタンスが作成されますが、使用はされません。深度に対応しているデバイスでは、どちらのバージョンも初期化され、レンダラが描画時にどちらを使用するかは実行時に判断されます。

onDrawFrame() メソッド内で、以下の既存コードを探します

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

それを次のコードに置き換えます

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

最後に、深度画像が出力レンダリングに正しくマッピングされるようにします。深度画像は解像度が画面とは異なり、アスペクト比も異なる場合があるので、テクスチャの座標はカメラ画像と異なる場合があります。

ヘルパー メソッド getTextureTransformMatrix() をファイルの最後に追加します。このメソッドは、適用すると画面空間の UV がカメラフィードのレンダリングに使用するクワッド テクスチャ座標と一致するようになる変換行列を返します。これにはデバイスの向きが考慮されます。

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() を使用するには、ファイルの先頭に次のインポートが必要です。

import com.google.ar.core.Coordinates2d;

画面テクスチャが変更されたとき(画面が回転したときなど)には、これらのテクスチャ座標間の変換を計算する必要があります。この機能はゲート制御されます。次に、ファイルの先頭に以下のフラグを追加します。

// Add this member at the top of the file.
private boolean calculateUVTransform = true;

onDrawFrame() の中で、フレームとカメラの作成後に保存された変換を再計算する必要があるかどうかをチェックします。

// Add these lines inside onDrawFrame() after frame.getCamera().
if (frame.hasDisplayGeometryChanged() || calculateUVTransform) {
  calculateUVTransform = false;
  float[] transform = getTextureTransformMatrix(frame);
  occludedVirtualObject.setUvTransformMatrix(transform);
}

以上の変更が完了したら、仮想オブジェクトのオクルージョンが行われた状態でアプリを実行できます。これで、すべてのスマートフォンで正常に動作し、対応していれば自動的にオクルージョン用深度が使われるようになります。

Depth API サポート付きでのアプリの実行

Depth API サポートなしでのアプリの実行

(省略可)オクルージョン品質の向上

上で実装した深度ベースのオクルージョンでは、はっきりした境界線のオクルージョンとなります。カメラがオブジェクトから離れると、深度の測定値は不正確になり、表示に乱れが生じる可能性があります。オクルージョン テストにぼかしを加え、隠された仮想オブジェクトの輪郭を滑らかにすることで、この問題を軽減できます。

occlusion_object.frag

occlusion_object.frag の先頭に次のユニフォーム変数を追加します。

uniform float u_OcclusionBlurAmount;

次のヘルパー関数を、シェーダーの main() のすぐ上に追加します。これにより、オクルージョンのサンプリングにカーネルぼかしが適用されます。

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

main() の次の既存の行を置き換えます。

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

置き換えた後は次のようになります。

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

レンダラを更新して、この新しいシェーダー機能を利用するようにします。

OcclusionObjectRenderer.java

クラスの先頭に次のメンバー変数を追加します。

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

createOnGlThread メソッド内に次の行を追加します。

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

draw メソッド内に次の行を追加します。

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

見え方の比較

この変更によりオクルージョンの境界が滑らかになります。

アプリをビルドして実行する

以下の手順に沿ってアプリをビルドして実行します。

  1. USB 経由で Android デバイスに接続します。
  2. [File] > [Build and Run] を選択します。
  3. ARCodeLab.apk として保存します。
  4. アプリがビルドされ、デバイスにデプロイされるのを待ちます。

デバイスに初めてアプリをデプロイする場合は、

[USB デバッグを許可しますか?]

とデバイスで尋ねられるので、[OK] を選択して続行します。

デバイスで初めてアプリを実行する場合には、アプリにデバイスのカメラを使用する権限があるか尋ねられます。AR 機能を引き続き使用するには、アクセスを許可する必要があります。

アプリのテスト

アプリを実行するとき、デバイスをつかんで、空間内で動かし、領域をゆっくりスキャンすることで、基本的な動作をテストできます。次のステップに進む前に、少なくとも 10 秒間データを収集して、複数の方向から領域をスキャンします。

これで、Google の ARCore Depth API を使用して初めての深度ベースの拡張現実アプリを作成して実行できました。

開発用 Android デバイスのセットアップ

  1. USB ケーブルで、デバイスと開発マシンを接続します。Windows を使用して開発する場合、デバイスに適切な USB ドライバのインストールが必要になる場合があります。
  2. 次の手順に沿って、[開発者向けオプション] ウィンドウで、[USB デバッグ] を有効にします。
  • 設定アプリを開きます。
  • Android 8.0 以降を搭載しているデバイスの場合は、[システム] を選択します。それ以外の場合は、次の手順に進みます。
  • 下にスクロールして、[デバイス情報] を選択します。
  • 一番下までスクロールして [ビルド番号] を 7 回タップします。
  • 前の画面に戻り、一番下までスクロールして [開発者向けオプション] をタップします。
  • [開発者向けオプション] ウィンドウで、下にスクロールして [USB デバッグ] を有効にします。

この手順の詳細については、Google の Android デベロッパー ウェブサイトをご覧ください。

1480e83e227b94f1.png

ライセンス関連のビルドエラーが発生した場合(Failed to install the following Android SDK packages as some licences have not been accepted)、次のコマンドでライセンスを確認して承認できます。

cd <path to Android SDK>

tools/bin/sdkmanager --licenses

よくある質問