Используйте ARCore Depth API для погружения в дополненную реальность.

1. Прежде чем начать

ARCore — это платформа для создания приложений дополненной реальности (AR) на мобильных устройствах. Используя различные API, ARCore позволяет устройству пользователя наблюдать и получать информацию об окружающей среде, а также взаимодействовать с этой информацией.

В этом практическом занятии вы пройдете весь процесс создания простого приложения с поддержкой дополненной реальности, использующего API ARCore Depth.

Предварительные требования

Данный практический урок предназначен для разработчиков, обладающих знаниями основных концепций дополненной реальности.

Что вы построите

1a0236e93212210c.gif

Вы разработаете приложение, которое будет использовать изображение глубины для каждого кадра, чтобы визуализировать геометрию сцены и выполнять окклюзию для размещенных виртуальных объектов. Вы пройдете следующие этапы:

  • Проверка поддержки API глубины на телефоне
  • Получение изображения глубины для каждого кадра
  • Визуализация информации о глубине различными способами (см. анимацию выше)
  • Использование глубины для повышения реализма приложений с окклюзией.
  • Изучение способов корректной работы с телефонами, не поддерживающими API глубины.

Что вам понадобится

Требования к оборудованию

Требования к программному обеспечению

2. ARCore и API глубины

API глубины использует RGB-камеру поддерживаемого устройства для создания карт глубины (также называемых изображениями глубины). Вы можете использовать информацию, предоставляемую картой глубины, чтобы виртуальные объекты точно отображались перед или за реальными объектами, обеспечивая захватывающий и реалистичный пользовательский опыт.

API ARCore Depth предоставляет доступ к изображениям глубины, соответствующим каждому кадру, предоставляемому сессией ARCore. Каждый пиксель содержит измерение расстояния от камеры до окружающей среды, что повышает реализм вашего приложения дополненной реальности.

Ключевой особенностью API Depth является окклюзия : способность цифровых объектов точно отображаться относительно объектов реального мира. Это создает ощущение, что объекты действительно находятся в окружающей среде вместе с пользователем.

В этом практическом занятии вы узнаете, как создать простое приложение с поддержкой дополненной реальности, которое использует изображения глубины для затенения виртуальных объектов за реальными поверхностями и визуализации обнаруженной геометрии пространства.

3. Подготовка к работе

Настройте машину для разработки.

  1. Подключите устройство ARCore к компьютеру с помощью USB-кабеля. Убедитесь, что ваше устройство поддерживает отладку по USB .
  2. Откройте терминал и выполните команду adb devices , как показано ниже:
adb devices

List of devices attached
<DEVICE_SERIAL_NUMBER>    device

<DEVICE_SERIAL_NUMBER> — это строка, уникальная для вашего устройства. Убедитесь, что вы видите ровно одно устройство, прежде чем продолжить.

Скачайте и установите Code e

  1. Вы можете либо клонировать репозиторий:
git clone https://github.com/googlecodelabs/arcore-depth

Или скачайте ZIP-файл и распакуйте его:

  1. Запустите Android Studio и нажмите «Открыть существующий проект Android Studio» .
  2. Найдите папку, куда вы распаковали загруженный выше ZIP-файл, и откройте папку depth_codelab_io2020 .

Это единый проект Gradle, содержащий несколько модулей. Если панель «Проекты» в левом верхнем углу Android Studio еще не отображается, выберите «Проекты» в выпадающем меню.

В результате должно получиться примерно так:

Данный проект включает следующие модули:

  • part0_work : Стартовое приложение. Вам следует внести изменения в этот модуль при выполнении данного практического задания.
  • part1 : Справочный код, показывающий, как должны выглядеть ваши изменения после завершения Части 1.
  • part2 : При выполнении части 2 используйте предоставленный вами справочный код.
  • part3 : При выполнении Части 3 используйте предоставленный вами справочный код.
  • part4_completed : Финальная версия приложения. Используйте этот код в качестве справочного материала после завершения части 4 и данного практического задания.

Вы будете работать в модуле part0_work . Для каждой части практического занятия также доступны готовые решения. Каждый модуль представляет собой собираемое приложение.

4. Запустите стартовое приложение.

  1. Нажмите «Выполнить» > «Выполнить...» > «part0_work» . В появившемся диалоговом окне «Выбор целевого объекта развертывания» ваше устройство должно отображаться в списке подключенных устройств .
  2. Выберите своё устройство и нажмите ОК . Android Studio создаст начальный вариант приложения и запустит его на вашем устройстве.
  3. Приложение запросит разрешение на использование камеры. Нажмите « Разрешить» , чтобы продолжить.

c5ef65f7a1da0d9.png

Как пользоваться приложением

  1. Подвигайте устройство, чтобы помочь приложению найти самолет . Сообщение внизу укажет, когда следует продолжать движение.
  2. Коснитесь любой точки на плоскости, чтобы установить якорь . В месте установки якоря будет нарисована фигурка Android. Это приложение позволяет устанавливать только один якорь за раз.
  3. Перемещайте устройство . Изображение должно оставаться на одном месте, даже если устройство перемещается.

В настоящее время ваше приложение очень простое и мало что знает о геометрии реальной сцены.

Если, например, разместить фигурку Android за стулом, изображение будет выглядеть так, будто оно парит перед ним, поскольку приложение не знает о существовании стула и должно скрывать фигурку Android.

6182cf62be13cd97.pngbeb0d327205f80ee.pnge4497751c6fad9a7.png

Для решения этой проблемы мы будем использовать API глубины, чтобы повысить погружение и реализм в этом приложении.

5. Проверьте, поддерживается ли API глубины (Часть 1)

API ARCore Depth работает только на ограниченном наборе поддерживаемых устройств. Прежде чем интегрировать функциональность приложения, использующего эти изображения глубины, необходимо убедиться, что приложение работает на поддерживаемом устройстве.

Добавьте в класс 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);

Теперь сессия дополненной реальности настроена должным образом, и ваше приложение знает, может ли оно использовать функции, основанные на глубине.

Также следует сообщить пользователю, используется ли глубина в данной сессии.

Добавьте ещё одно сообщение в Snackbar. Оно появится внизу экрана:

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

Далее вам нужно будет обновить приложение, чтобы оно вызывало API глубины и получало изображения глубины для каждого кадра.

6. Получение изображений глубины (Часть 2)

API глубины захватывает трехмерные данные об окружающей среде устройства и возвращает в ваше приложение изображение глубины с этими данными. Каждый пиксель на изображении глубины представляет собой измерение расстояния от камеры устройства до окружающей его среды в реальном мире.

Теперь вы будете использовать эти изображения глубины для улучшения рендеринга и визуализации в приложении. Первый шаг — получить изображение глубины для каждого кадра и привязать эту текстуру для использования графическим процессором.

Сначала добавьте в свой проект новый класс.
DepthTextureHandler отвечает за получение изображения глубины для заданного кадра ARCore.
Добавьте этот файл:

be8d14dfe9656551.png

src/main/java/com/google/ar/core/codelab/depth/DepthTextureHandler.java

package com.google.ar.core.codelab.depth;

import static android.opengl.GLES20.GL_CLAMP_TO_EDGE;
import static android.opengl.GLES20.GL_TEXTURE_2D;
import static android.opengl.GLES20.GL_TEXTURE_MAG_FILTER;
import static android.opengl.GLES20.GL_TEXTURE_MIN_FILTER;
import static android.opengl.GLES20.GL_TEXTURE_WRAP_S;
import static android.opengl.GLES20.GL_TEXTURE_WRAP_T;
import static android.opengl.GLES20.GL_UNSIGNED_BYTE;
import static android.opengl.GLES20.glBindTexture;
import static android.opengl.GLES20.glGenTextures;
import static android.opengl.GLES20.glTexImage2D;
import static android.opengl.GLES20.glTexParameteri;
import static android.opengl.GLES30.GL_LINEAR;
import static android.opengl.GLES30.GL_RG;
import static android.opengl.GLES30.GL_RG8;

import android.media.Image;
import com.google.ar.core.Frame;
import com.google.ar.core.exceptions.NotYetAvailableException;

/** Handle RG8 GPU texture containing a DEPTH16 depth image. */
public final class DepthTextureHandler {

  private int depthTextureId = -1;
  private int depthTextureWidth = -1;
  private int depthTextureHeight = -1;

  /**
   * Creates and initializes the depth texture. This method needs to be called on a
   * thread with a EGL context attached.
   */
  public void createOnGlThread() {
    int[] textureId = new int[1];
    glGenTextures(1, textureId, 0);
    depthTextureId = textureId[0];
    glBindTexture(GL_TEXTURE_2D, depthTextureId);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  }

  /**
   * Updates the depth texture with the content from acquireDepthImage16Bits().
   * This method needs to be called on a thread with an EGL context attached.
   */
  public void update(final Frame frame) {
    try {
      Image depthImage = frame.acquireDepthImage16Bits();
      depthTextureWidth = depthImage.getWidth();
      depthTextureHeight = depthImage.getHeight();
      glBindTexture(GL_TEXTURE_2D, depthTextureId);
      glTexImage2D(
          GL_TEXTURE_2D,
          0,
          GL_RG8,
          depthTextureWidth,
          depthTextureHeight,
          0,
          GL_RG,
          GL_UNSIGNED_BYTE,
          depthImage.getPlanes()[0].getBuffer());
      depthImage.close();
    } catch (NotYetAvailableException e) {
      // This normally means that depth data is not available yet.
    }
  }

  public int getDepthTexture() {
    return depthTextureId;
  }

  public int getDepthWidth() {
    return depthTextureWidth;
  }

  public int getDepthHeight() {
    return depthTextureHeight;
  }
}

Теперь добавьте экземпляр этого класса в DepthCodelabActivity , чтобы обеспечить себе легкодоступную копию изображения глубины для каждого кадра.

В DepthCodelabActivity.java добавьте экземпляр нашего нового класса в качестве закрытой переменной-члена:

private final DepthTextureHandler depthTexture = new DepthTextureHandler();

Далее обновите метод 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. Щелкните правой кнопкой мыши на папке с шейдерами.
  3. Выберите Создать -> Файл
  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 = 20000.0; // In millimeters.

float GetDepthMillimeters(vec4 depth_pixel_value) {
  return 255.0 * (depth_pixel_value.r + depth_pixel_value.g * 256.0);
}

// Returns an interpolated color in a 6 degree polynomial interpolation.
vec3 GetPolynomialColor(in float x,
  in vec4 kRedVec4, in vec4 kGreenVec4, in vec4 kBlueVec4,
  in vec2 kRedVec2, in vec2 kGreenVec2, in vec2 kBlueVec2) {
  // Moves the color space a little bit to avoid pure red.
  // Removes this line for more contrast.
  x = clamp(x * 0.9 + 0.03, 0.0, 1.0);
  vec4 v4 = vec4(1.0, x, x * x, x * x * x);
  vec2 v2 = v4.zw * v4.z;
  return vec3(
    dot(v4, kRedVec4) + dot(v2, kRedVec2),
    dot(v4, kGreenVec4) + dot(v2, kGreenVec2),
    dot(v4, kBlueVec4) + dot(v2, kBlueVec2)
  );
}

// Returns a smooth Percept colormap based upon the Turbo colormap.
vec3 PerceptColormap(in float x) {
  const vec4 kRedVec4 = vec4(0.55305649, 3.00913185, -5.46192616, -11.11819092);
  const vec4 kGreenVec4 = vec4(0.16207513, 0.17712472, 15.24091500, -36.50657960);
  const vec4 kBlueVec4 = vec4(-0.05195877, 5.18000081, -30.94853351, 81.96403246);
  const vec2 kRedVec2 = vec2(27.81927491, -14.87899417);
  const vec2 kGreenVec2 = vec2(25.95549545, -5.02738237);
  const vec2 kBlueVec2 = vec2(-86.53476570, 30.23299484);
  const float kInvalidDepthThreshold = 0.01;
  return step(kInvalidDepthThreshold, x) *
         GetPolynomialColor(x, kRedVec4, kGreenVec4, kBlueVec4,
                            kRedVec2, kGreenVec2, kBlueVec2);
}

void main() {
  vec4 packed_depth = texture2D(u_Depth, v_TexCoord.xy);
  highp float depth_mm = GetDepthMillimeters(packed_depth);
  highp float normalized_depth = depth_mm / kMaxDepth;
  vec4 depth_color = vec4(PerceptColormap(normalized_depth), 1.0);
  gl_FragColor = depth_color;
}

Далее обновите класс 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 добавьте импорт для кнопки:

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 . Используйте этот флаг, чтобы контролировать, будет ли отображаться карта глубины.

В методе onDrawFrame() в DepthCodelabActivity добавьте:

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

Передайте текстуру глубины в backgroundRenderer , добавив следующую строку в onSurfaceCreated() :

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

Теперь вы можете просмотреть изображение глубины каждого кадра, нажав кнопку в правом верхнем углу экрана.

Работает без поддержки API глубины.

Работает с поддержкой 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 = 20000.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);

Теперь глубина визуализируется в виде анимированного пульса, протекающего через вашу сцену.

b846e4365d7b69b1.gif

Вы можете свободно изменять указанные здесь значения, чтобы сделать импульс медленнее, быстрее, шире, уже и т.д. Также вы можете попробовать изучить совершенно новые способы изменения шейдера для отображения информации о глубине!

8. Использование API глубины для окклюзии (Часть 4)

Теперь вы займетесь обработкой перекрытия объектов в вашем приложении.

Окклюзия — это явление, когда виртуальный объект не может быть полностью отображен, поскольку между виртуальным объектом и камерой находятся реальные объекты. Управление окклюзией имеет важное значение для создания эффекта полного погружения в дополненную реальность.

Правильное отображение виртуальных объектов в реальном времени повышает реализм и правдоподобность дополненной сцены. Больше примеров смотрите в нашем видео о смешивании реальностей с помощью Depth API .

В этом разделе вы обновите свое приложение, чтобы оно включало виртуальные объекты только в том случае, если доступна глубина.

Добавление новых шейдеров объектов

Как и в предыдущих разделах, вы добавите новые шейдеры для поддержки информации о глубине. На этот раз вы можете скопировать существующие шейдеры объектов и добавить функциональность окклюзии.

Важно сохранять обе версии шейдеров объектов, чтобы ваше приложение могло в режиме реального времени принимать решение о поддержке глубины.

Создайте копии файлов шейдеров object.vert и object.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=*/17500.0, /*max_depth_mm=*/20000.0);

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

  return visibility;
}

Теперь обновите main() в файле occlusion_object.frag , чтобы она учитывала глубину и применяла окклюзию. Добавьте следующие строки в конец файла:

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

Теперь, когда у вас есть новая версия шейдеров объектов, вы можете изменить код рендерера.

Рендеринг окклюзии объекта

Затем создайте копию класса ObjectRenderer , расположенного в файле src/main/java/com/google/ar/core/codelab/common/rendering/ObjectRenderer.java .

  • Выберите класс ObjectRenderer
  • Щелкните правой кнопкой мыши > Копировать
  • Выберите папку для рендеринга .
  • Щелкните правой кнопкой мыши > Вставить

7487ece853690c31.png

  • Переименуйте класс в OcclusionObjectRenderer

760a4c80429170c2.png

Новый, переименованный класс теперь должен находиться в той же папке:

9335c373dc60cd17.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;

Инициализируйте параметры uniform-переменных для шейдера в методе 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 , так чтобы ObjectRenderer и OcclusionObjectRenderer были членами класса 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();
  • Далее вы можете управлять использованием этого occludedVirtualObject в зависимости от того, поддерживает ли текущее устройство API глубины. Добавьте следующие строки в метод 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);
}

Благодаря этим изменениям, теперь вы можете запускать приложение с виртуальным перекрытием объектов!

Теперь ваше приложение должно корректно работать на всех телефонах и автоматически использовать технологию определения глубины для окклюзии, если она поддерживается.

Запуск приложения с поддержкой API глубины.

Запуск приложения без поддержки 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. Подключите устройство Android через USB.
  2. Выберите Файл > Сборка и запуск .
  3. Сохранить как: ARCodeLab.apk .
  4. Дождитесь сборки и развертывания приложения на вашем устройстве.

При первой попытке развернуть приложение на вашем устройстве:

  • Вам потребуется разрешить отладку по USB на устройстве. Нажмите «ОК», чтобы продолжить.
  • Вам будет предложено разрешить приложению использовать камеру устройства. Разрешите доступ, чтобы продолжить использование функций дополненной реальности.

Тестирование вашего приложения

При запуске приложения вы можете проверить его базовое поведение, держа устройство в руках, перемещаясь по пространству и медленно сканируя область. Постарайтесь собрать данные в течение как минимум 10 секунд и просканировать область с нескольких направлений, прежде чем переходить к следующему шагу.

Поиск неисправностей

Настройка вашего Android-устройства для разработки.

  1. Подключите ваше устройство к компьютеру разработчика с помощью USB-кабеля. Если вы разрабатываете под управлением Windows, вам может потребоваться установить соответствующий USB-драйвер для вашего устройства.
  2. Чтобы включить отладку по USB в окне «Параметры разработчика», выполните следующие действия:
  3. Откройте приложение «Настройки» .
  4. Если на вашем устройстве установлена ​​версия Android 8.0 или выше, выберите «Система» . В противном случае перейдите к следующему шагу.
  5. Прокрутите страницу вниз и выберите «О телефоне» .
  6. Прокрутите страницу вниз и нажмите на номер сборки 7 раз.
  7. Вернитесь на предыдущий экран, прокрутите вниз и нажмите «Параметры разработчика» .
  8. В окне «Параметры разработчика» прокрутите вниз, чтобы найти и включить отладку по USB .

Более подробную информацию об этом процессе можно найти на веб-сайте Google для разработчиков Android .

cfa20a722a68f54f.png

Если вы столкнулись с ошибкой сборки, связанной с лицензиями ( Не удалось установить следующие пакеты Android SDK, поскольку некоторые лицензии не были приняты ), вы можете использовать следующие команды для просмотра и принятия этих лицензий:

cd <path to Android SDK>

tools/bin/sdkmanager --licenses

11. Поздравляем!

Поздравляем, вы успешно создали и запустили свое первое приложение дополненной реальности на основе глубины, используя API ARCore Depth от Google!

Часто задаваемые вопросы