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。

硬件要求

软件要求

设置开发机器

使用 USB 线将 ARCore 设备连接到计算机。确保您的设备允许执行 USB 调试。打开一个终端并运行 adb devices,如下所示:

adb devices

List of devices attached
<DEVICE_SERIAL_NUMBER>    device

<DEVICE_SERIAL_NUMBER> 将是与您的设备对应的独一无二的字符串。请在确保设备的序列号准确无误后再继续。

下载并安装代码

您可以克隆代码库:

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

或者下载 ZIP 文件并解压缩:

下载 ZIP 文件

启动 Android Studio。点击 Open an existing Android Studio project。然后,导航到解压缩先前下载的 ZIP 文件的目录,双击 depth_codelab_io2020 目录。

这是一个包含多个模块的 Gradle 项目。如果 Project 窗格尚未显示在 Android Studio 左上角的相应位置,请点击下拉菜单中的 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 权限。点按 Allow 以继续。

如何使用该应用

  1. 移动设备以帮助应用找一个平面。底部的消息会指示是否需要继续移动。
  2. 点按平面上的某个位置以放置锚点。系统将在放置锚点的位置绘制 Android 小人。此应用一次仅允许放置一个锚点。
  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();

接下来,我们需要更新 onSurfaceCreated() 方法以初始化此纹理,以供 GPU 着色器使用:

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

最后,我们需要使用最新深度图像为每一帧填充此纹理,为此,只需对从 session 检索的最新帧调用我们先前创建的 update() 方法即可。由于深度支持对于此应用是可选的,因此只有在使用深度时才应发出此调用。

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

现在,我们有了会伴随每一帧更新的深度图像。接下来可以将其提供给着色器使用。不过,应用的行为并没有发生任何变化。让我们使用该深度图像来改进应用。

我们使用深度图像进行尝试,一起来看看改进效果如何。在本部分中,我们将向应用添加一个按钮,以渲染每一帧的深度。

添加新的着色器

有多种方法可以查看深度图像。以下着色器提供了简单的色表视觉呈现。

首先,在 src/main/assets/shaders/ 目录中添加新的 .vert.frag 着色器。

添加新的 .vert 着色器

在 Android Studio 中:

  1. 右键点击着色器目录
  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;
}

重复上述步骤,在同一目录中添加相应的 fragment 着色器,并将文件命名为 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;
}

接下来,更新 BackgroundRenderer 类,以使用位于 src/main/java/com/google/ar/core/codelab/common/rendering/BackgroundRenderer.java 中的这些新着色器。

在类的顶部添加这些着色器的文件路径。

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

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 android.widget.Button;

更新相应类以添加一个布尔值成员,用于指示是否已开启深度渲染(默认处于关闭状态):

private boolean showDepthMap = false;

接下来,将控制 showDepthMap 布尔值的按钮添加到 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);
          }
        });

将以下字符串添加到 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 实现增强现实的视频。

在本部分中,我们将更新应用,以便在支持深度时添加虚拟对象。

添加新的对象着色器

如上面几部分所述,我们将添加新的着色器来支持深度信息。这一次,我们将复制现有的对象着色器,并添加遮挡功能。请务必保留两个版本的对象着色器,以便应用在运行时可以决定是否支持深度。

src/main/assets/shaders 目录中为 object.vertobject.frag 着色器文件创建副本。

  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;

在文件顶部 main() 上方添加以下变量,以更新 occlusion_object.frag

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.frag 中的 main() 更新为深度感知型,并应用遮挡。在文件底部添加以下几行代码:

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() 方法中,初始化着色器的 uniform 参数:

// 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 中,并选择何时以及如何应用遮挡渲染。

实现此逻辑的方法是向 activity 添加 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 变量:

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 v8.0 或更高版本,请选择系统。否则,请继续执行下一步。
  • 滚动到底部,选择关于手机
  • 滚动到底部,点按版本号七次。
  • 返回上一屏幕,滚动到底部,然后点按开发者选项
  • 开发者选项窗口中,向下滚动以找到并启用 USB 调试

如需详细了解此流程,请访问 Google 的 Android 开发者网站

1480e83e227b94f1.png

如果您遇到与许可相关的构建失败(由于尚未接受某些许可,导致安装以下 Android SDK 软件包失败),可以使用以下命令来查看和接受这些许可:

cd <path to Android SDK>

tools/bin/sdkmanager --licenses

常见问题解答