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

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

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

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

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

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

Что ты построишь

1a0236e93212210c.gif

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

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

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

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

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

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

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

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

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

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

3. Настройте

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

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

List of devices attached
<DEVICE_SERIAL_NUMBER>    device

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

Загрузите и установите код 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

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

5. Проверьте, поддерживается ли Depth 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);

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

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

Добавьте еще одно сообщение в 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

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

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

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

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

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

be8d14dfe9656551.png

src/main/java/com/google/ar/core/codelab/глубина/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-студии:

  1. Сначала добавьте новые шейдеры .vert и .frag в каталог src/main/assets/shaders/ .
  2. Щелкните правой кнопкой мыши каталог шейдеров.
  3. Выберите Создать -> Файл.
  4. Назовите его background_show_depth_map.vert .
  5. Установите его как текстовый файл.

В новый файл добавьте следующий код:

src/main/assets/shaders/background_show_length_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_length_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());

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

Работа без поддержки 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 = 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. Используйте Depth API для окклюзии (часть 4)

Теперь вы будете обрабатывать окклюзию объектов в своем приложении.

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

Правильный рендеринг виртуальных объектов в реальном времени повышает реалистичность и правдоподобность дополненной сцены. Дополнительные примеры можно найти в нашем видеоролике о смешивании реалий с помощью 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;

Инициализируйте универсальные параметры шейдера в методе 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 в зависимости от того, поддерживает ли текущее устройство Depth 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);
}

После внесения этих изменений вы теперь можете запускать приложение с окклюзией виртуальных объектов!

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

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

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

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

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

Запустив приложение, вы можете проверить его базовое поведение, удерживая устройство, перемещаясь по пространству и медленно сканируя область. Постарайтесь собрать данные хотя бы за 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 от Google!

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