ARCore Depth API を使用して臨場感のある拡張現実を体験する

1. 始める前に

ARCore は、モバイル デバイスで拡張現実(AR)アプリを作成するためのプラットフォームです。ARCore は、さまざまな API を使用して、ユーザーのデバイスが環境に関する情報を観測および受信し、情報を操作できるようにします。

この Codelab では、ARCore Depth API を使用するシンプルな AR 対応アプリを作成するプロセスについて説明します。

前提条件

この Codelab は、AR の基本的なコンセプトを理解しているデベロッパーを対象としています。

作成するアプリの概要

d9bd5136c54ce47a.gif

各フレームの深度画像を使ってシーンのジオメトリを可視化し、配置された仮想アセットでオクルージョンを行うアプリを作成します。以下の具体的な手順を説明します。

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

必要なもの

ハードウェア要件

ソフトウェア要件

2. ARCore と Depth API

Depth API は、サポートされているデバイスの RGB カメラを使用して深度マップ(深度画像)を作成します。深度マップで提供される情報を使用して、仮想オブジェクトを現実世界のオブジェクトの前または後ろに正確に表示し、臨場感のあるリアルなユーザー エクスペリエンスを実現します。

ARCore Depth API を使用すると、ARCore のセッションによって提供される各フレームに一致する深度画像へのアクセスが可能になります。各ピクセルによりカメラから環境までの距離の測定値が得られます。これにより、AR アプリの臨場感を高めることができます。

Depth API の背後にある主な機能は、オクルージョン、つまりデジタル オブジェクトが実世界のオブジェクトに対して正確に表示されることです。そのため、オブジェクトが実際にユーザーの環境に存在するかのように感じられます。

この Codelab では、深度画像を使用して実世界のサーフェスの背後にある仮想オブジェクトのオクルージョンを行い、検出された空間のジオメトリを可視化する、シンプルな AR 対応アプリを作成するプロセスについて説明します。

3. 設定する

開発マシンをセットアップする

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

List of devices attached
<DEVICE_SERIAL_NUMBER>    device

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

コードをダウンロードしてインストールする

  1. 次のようにしてリポジトリのクローンを作成できます。
git clone https://github.com/googlecodelabs/arcore-depth

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

  1. Android Studio を起動して、[Open an existing Android Studio project] をクリックします。
  2. 先ほどダウンロードした 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 の各パートに完全な解答が用意されています。各モジュールはビルド可能なアプリになっています。

4.スターター アプリを実行する

  1. [Run] > [Run...] > [‘part0_work'] をクリックします。表示された [Select Deployment Target] ダイアログの [Connected Devices] の下に、ご利用のデバイスが表示されます。
  2. デバイスを選択して、[OK] をクリックします。Android Studio によって初期アプリがビルドされ、開発用デバイスで実行されます。
  3. アプリからカメラへのアクセス許可がリクエストされます。[許可] をタップして続行します。

c5ef65f7a1da0d9.png

アプリの使用方法

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

現在のところ、アプリは非常にシンプルで、実世界のシーンのジオメトリについての認識度は高くありません。

たとえば、Android のキャラクターを椅子の背後に置いた場合、キャラクターは椅子の前で浮いているようにレンダリングされます。これは椅子がそこにあって Android を隠す必要があることがアプリで認識されていないためです。

6182cf62be13cd97.png beb0d327205f80ee.png e4497751c6fad9a7.png

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

5. Depth API に対応しているかどうかを確認する(パート 1)

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

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

5c878a7c27833cb2.png

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

6. 深度画像を取得する(パート 2)

Depth API は、デバイスの環境の 3D 観測結果を取り込んで、そのデータを含んだ深度画像をアプリに返します。深度画像の各ピクセルにより、デバイスのカメラから実世界の環境までの距離の測定値が得られます。

ここでは、アプリでのレンダリングと可視化を高めるために、これらの深度画像を使用します。最初の手順として、各フレームの深度画像を取得し、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);
}

これで、フレームごとに更新される深度画像ができました。シェーダーで使用する準備が整いました。

しかし、まだアプリの動作は変わっていません。深度画像を使用してアプリを改善します。

7. 深度画像をレンダリングする(パート 3)

使用する深度画像が用意できたので、画像がどのように見えるかを確認します。このセクションでは、各フレームの深度をレンダリングするボタンをアプリに追加します。

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

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

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

Android Studio での手順:

  1. まず、新しい .vert シェーダーと .frag シェーダーを src/main/assets/shaders/ ディレクトリに追加します。
  2. shaders ディレクトリを右クリック
  3. [New] -> [File] を選択
  4. background_show_depth_map.vert という名前を付ける
  5. テキスト ファイルに設定

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

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 ファイルの先頭に、使用するボタンの 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

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

8. オクルージョンに Depth API を使用する(パート 4)

ここでは、アプリでオブジェクト オクルージョンを処理します。

オクルージョンとは、仮想オブジェクトとカメラの間に現実のオブジェクトが存在するため、仮想オブジェクトを完全にレンダリングできない場合のことを指します。オクルージョンの管理は、AR 体験を臨場感あるものにするためには不可欠です。

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

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

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

前のセクションと同じように、深度情報に対応した新しいシェーダーを追加します。今回は、既存のオブジェクト シェーダーをコピーし、オクルージョン機能を追加します。

深度に対応するかどうかをアプリで実行時に判断できるように、両方のバージョンのオブジェクト シェーダーを保持することが重要です。

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

  • object.vert をファイル src/main/assets/shaders/occlusion_object.vert にコピーします。
  • 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] を選択します

6c87dcb87da558c1.png

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

f2ffe488c81ad404.png

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

e5bf1c158e26c322.png

新しく作成した 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 サポートなしでのアプリの実行

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

上で実装した深度ベースのオクルージョンでは、はっきりした境界線のオクルージョンとなります。カメラがオブジェクトから離れると、深度の測定値は不正確になり、表示に乱れが生じる場合があります。

オクルージョン テストにぼかしを加え、隠された仮想オブジェクトの輪郭を滑らかにすることで、この問題を軽減できます。

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

見え方の比較

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

10. ビルド、実行、テスト

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

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

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

  • デバイスで USB デバッグを許可する必要があります。[OK] を選択して続行します。
  • アプリがデバイスのカメラを使用する権限を持っているかどうかを尋ねられます。AR 機能を引き続き使用するには、アクセスを許可してください。

アプリをテストする

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

トラブルシューティング

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

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

11. 完了

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

よくある質問