Используйте глубину в своем приложении для Android

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

Информация о глубине рассчитывается на основе движения и может быть объединена с информацией от аппаратного датчика глубины, такого как датчик времени полета (ToF), если таковой имеется. Устройству не требуется датчик ToF для поддержки Depth API .

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

Прежде чем продолжить, убедитесь, что вы понимаете фундаментальные концепции AR и то, как настроить сеанс ARCore .

Ограничить доступ к устройствам с поддержкой Depth

Если вашему приложению требуется поддержка Depth API, либо потому, что основная часть AR-интерфейса зависит от глубины, либо потому, что для частей приложения, использующих глубину, не существует корректного резервного варианта, вы можете ограничить распространение своего приложения в Google Play. Сохраните на устройствах, поддерживающих Depth API, добавив следующую строку в AndroidManifest.xml в дополнение к изменениям AndroidManifest.xml , описанным в руководстве по включению ARCore :

<uses-feature android:name="com.google.ar.core.depth" />

Включить глубину

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

Ява

Config config = session.getConfig();

// Check whether the user's device supports the Depth API.
boolean isDepthSupported = session.isDepthModeSupported(Config.DepthMode.AUTOMATIC);
if (isDepthSupported) {
  config.setDepthMode(Config.DepthMode.AUTOMATIC);
}
session.configure(config);

Котлин

val config = session.config

// Check whether the user's device supports the Depth API.
val isDepthSupported = session.isDepthModeSupported(Config.DepthMode.AUTOMATIC)
if (isDepthSupported) {
  config.depthMode = Config.DepthMode.AUTOMATIC
}
session.configure(config)

Получение изображений глубины

Вызовите Frame.acquireDepthImage16Bits() чтобы получить изображение глубины для текущего кадра.

Ява

// Retrieve the depth image for the current frame, if available.
Image depthImage = null;
try {
  depthImage = frame.acquireDepthImage16Bits();
  // Use the depth image here.
} catch (NotYetAvailableException e) {
  // This means that depth data is not available yet.
  // Depth data will not be available if there are no tracked
  // feature points. This can happen when there is no motion, or when the
  // camera loses its ability to track objects in the surrounding
  // environment.
} finally {
  if (depthImage != null) {
    depthImage.close();
  }
}

Котлин

// Retrieve the depth image for the current frame, if available.
try {
  frame.acquireDepthImage16Bits().use { depthImage ->
    // Use the depth image here.
  }
} catch (e: NotYetAvailableException) {
  // This means that depth data is not available yet.
  // Depth data will not be available if there are no tracked
  // feature points. This can happen when there is no motion, or when the
  // camera loses its ability to track objects in the surrounding
  // environment.
}

Возвращенное изображение предоставляет буфер необработанного изображения, который можно передать во фрагментный шейдер для использования на графическом процессоре для каждого визуализированного объекта, который необходимо перекрыть. Он ориентирован на OPENGL_NORMALIZED_DEVICE_COORDINATES и может быть изменен на TEXTURE_NORMALIZED путем вызова Frame.transformCoordinates2d() . Как только изображение глубины становится доступным в объектном шейдере, к этим измерениям глубины можно получить прямой доступ для обработки окклюзии.

Понимание значений глубины

Учитывая точку A на наблюдаемой реальной геометрии и двумерную точку a представляющую ту же точку на изображении глубины, значение, заданное Depth API в a , равно длине CA , проецируемой на главную ось. Это также можно назвать координатой Z A относительно начала координат камеры C . При работе с Depth API важно понимать, что значения глубины — это не длина самого CA луча, а его проекция .

Используйте глубину в шейдерах

Анализ информации о глубине для текущего кадра

Используйте вспомогательные функции DepthGetMillimeters() и DepthGetVisibility() во фрагментном шейдере, чтобы получить доступ к информации о глубине для текущей позиции экрана. Затем используйте эту информацию для выборочного закрытия частей визуализируемого объекта.

// Use DepthGetMillimeters() and DepthGetVisibility() to parse the depth image
// for a given pixel, and compare against the depth of the object to render.
float DepthGetMillimeters(in sampler2D depth_texture, 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(depth_texture, depth_uv).xyz;
  return dot(packedDepthAndVisibility.xy, vec2(255.0, 256.0 * 255.0));
}

// Return a value representing how visible or occluded a pixel is relative
// to the depth image. The range is 0.0 (not visible) to 1.0 (completely
// visible).
float DepthGetVisibility(in sampler2D depth_texture, in vec2 depth_uv,
                         in float asset_depth_mm) {
  float depth_mm = DepthGetMillimeters(depth_texture, depth_uv);

  // Instead of a hard Z-buffer test, allow the asset to fade into the
  // background along a 2 * kDepthTolerancePerMm * asset_depth_mm
  // range centered on the background depth.
  const float kDepthTolerancePerMm = 0.015f;
  float visibility_occlusion = clamp(0.5 * (depth_mm - asset_depth_mm) /
    (kDepthTolerancePerMm * asset_depth_mm) + 0.5, 0.0, 1.0);

 // Use visibility_depth_near to set the minimum depth value. If using
 // this value for occlusion, avoid setting it too close to zero. A depth value
 // of zero signifies that there is no depth data to be found.
  float visibility_depth_near = 1.0 - InverseLerp(
      depth_mm, /*min_depth_mm=*/150.0, /*max_depth_mm=*/200.0);

  // Use visibility_depth_far to set the maximum depth value. If the depth
  // value is too high (outside the range specified by visibility_depth_far),
  // the virtual object may get inaccurately occluded at further distances
  // due to too much noise.
  float visibility_depth_far = InverseLerp(
      depth_mm, /*min_depth_mm=*/7500.0, /*max_depth_mm=*/8000.0);

  const float kOcclusionAlpha = 0.0f;
  float visibility =
      max(max(visibility_occlusion, kOcclusionAlpha),
          max(visibility_depth_near, visibility_depth_far));

  return visibility;
}

Закрывать виртуальные объекты

Окклюдируйте виртуальные объекты в теле фрагментного шейдера. Обновите альфа-канал объекта в зависимости от его глубины. Это отобразит закрытый объект.

// Occlude virtual objects by updating the object’s alpha channel based on its depth.
const float kMetersToMillimeters = 1000.0;

float asset_depth_mm = v_ViewPosition.z * kMetersToMillimeters * -1.;

// Compute the texture coordinates to sample from the depth image.
vec2 depth_uvs = (u_DepthUvTransform * vec3(v_ScreenSpacePosition.xy, 1)).xy;

gl_FragColor.a *= DepthGetVisibility(u_DepthTexture, depth_uvs, asset_depth_mm);

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

Пообъектный, прямой рендеринг

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

Двухпроходный рендеринг

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

Извлечь расстояние из изображения глубины

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

Ява

/** Obtain the depth in millimeters for depthImage at coordinates (x, y). */
public int getMillimetersDepth(Image depthImage, int x, int y) {
  // The depth image has a single plane, which stores depth for each
  // pixel as 16-bit unsigned integers.
  Image.Plane plane = depthImage.getPlanes()[0];
  int byteIndex = x * plane.getPixelStride() + y * plane.getRowStride();
  ByteBuffer buffer = plane.getBuffer().order(ByteOrder.nativeOrder());
  return Short.toUnsignedInt(buffer.getShort(byteIndex));
}

Котлин

/** Obtain the depth in millimeters for [depthImage] at coordinates ([x], [y]). */
fun getMillimetersDepth(depthImage: Image, x: Int, y: Int): UInt {
  // The depth image has a single plane, which stores depth for each
  // pixel as 16-bit unsigned integers.
  val plane = depthImage.planes[0]
  val byteIndex = x * plane.pixelStride + y * plane.rowStride
  val buffer = plane.buffer.order(ByteOrder.nativeOrder())
  val depthSample = buffer.getShort(byteIndex)
  return depthSample.toUInt()
}

Преобразование координат между изображениями камеры и изображениями глубины

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

Чтобы получить координаты изображения глубины для координат на изображении ЦП:

Ява

float[] cpuCoordinates = new float[] {cpuCoordinateX, cpuCoordinateY};
float[] textureCoordinates = new float[2];
frame.transformCoordinates2d(
    Coordinates2d.IMAGE_PIXELS,
    cpuCoordinates,
    Coordinates2d.TEXTURE_NORMALIZED,
    textureCoordinates);
if (textureCoordinates[0] < 0 || textureCoordinates[1] < 0) {
  // There are no valid depth coordinates, because the coordinates in the CPU image are in the
  // cropped area of the depth image.
  return null;
}
return new Pair<>(
    (int) (textureCoordinates[0] * depthImage.getWidth()),
    (int) (textureCoordinates[1] * depthImage.getHeight()));

Котлин

val cpuCoordinates = floatArrayOf(cpuCoordinateX.toFloat(), cpuCoordinateY.toFloat())
val textureCoordinates = FloatArray(2)
frame.transformCoordinates2d(
  Coordinates2d.IMAGE_PIXELS,
  cpuCoordinates,
  Coordinates2d.TEXTURE_NORMALIZED,
  textureCoordinates,
)
if (textureCoordinates[0] < 0 || textureCoordinates[1] < 0) {
  // There are no valid depth coordinates, because the coordinates in the CPU image are in the
  // cropped area of the depth image.
  return null
}
return (textureCoordinates[0] * depthImage.width).toInt() to
  (textureCoordinates[1] * depthImage.height).toInt()

Чтобы получить координаты изображения ЦП для координат изображения глубины:

Ява

float[] textureCoordinates =
    new float[] {
      (float) depthCoordinateX / (float) depthImage.getWidth(),
      (float) depthCoordinateY / (float) depthImage.getHeight()
    };
float[] cpuCoordinates = new float[2];
frame.transformCoordinates2d(
    Coordinates2d.TEXTURE_NORMALIZED,
    textureCoordinates,
    Coordinates2d.IMAGE_PIXELS,
    cpuCoordinates);
return new Pair<>((int) cpuCoordinates[0], (int) cpuCoordinates[1]);

Котлин

val textureCoordinates =
  floatArrayOf(
    depthCoordinatesX.toFloat() / depthImage.width.toFloat(),
    depthCoordinatesY.toFloat() / depthImage.height.toFloat(),
  )
val cpuCoordinates = FloatArray(2)
frame.transformCoordinates2d(
  Coordinates2d.TEXTURE_NORMALIZED,
  textureCoordinates,
  Coordinates2d.IMAGE_PIXELS,
  cpuCoordinates,
)
return cpuCoordinates[0].toInt() to cpuCoordinates[1].toInt()

Проверка глубины

Хит-тесты позволяют пользователям размещать объекты в реальных местах сцены. Раньше тесты на попадание можно было проводить только на обнаруженных самолетах, ограничивая локации большими плоскими поверхностями, как, например, результаты, показанные зелеными андроидами. При тестировании глубины удара используется как гладкая, так и необработанная информация о глубине, чтобы обеспечить более точные результаты попадания даже на неплоские поверхности и поверхности с низкой текстурой. Это показано красными Андроидами.

Чтобы использовать тесты попадания с поддержкой глубины, вызовите hitTest() и проверьте наличие DepthPoints в возвращаемом списке.

Ява

// Create a hit test using the Depth API.
List<HitResult> hitResultList = frame.hitTest(tap);
for (HitResult hit : hitResultList) {
  Trackable trackable = hit.getTrackable();
  if (trackable instanceof Plane
      || trackable instanceof Point
      || trackable instanceof DepthPoint) {
    useHitResult(hit);
    break;
  }
}

Котлин

// Create a hit test using the Depth API.
val hitResult =
  frame
    .hitTest(tap)
    .filter {
      val trackable = it.trackable
      trackable is Plane || trackable is Point || trackable is DepthPoint
    }
    .firstOrNull()
useHitResult(hitResult)

Что дальше

  • Обеспечьте более точное измерение с помощью API Raw Depth .
  • Посетите лабораторию ARCore Depth Lab , где демонстрируются различные способы доступа к данным о глубине.