Korzystanie z głębi w aplikacji na Androida

Depth API pomaga aparatowi urządzenia rozpoznać rozmiar i kształt rzeczywistych obiektów w scenie. Używa aparatu do tworzenia zdjęć głębi, czyli map głębi, dzięki czemu Twoje aplikacje zyskują dodatkową warstwę realizmu AR. Na podstawie informacji uzyskanych dzięki obrazowi głębi możesz precyzyjnie wyświetlać wirtualne obiekty przed lub za rzeczywistymi obiektami, co zapewnia użytkownikom realistyczne i ciekawe wrażenia.

Informacje o głębi są obliczane na podstawie ruchu i mogą być łączone z informacjami ze sprzętowego czujnika głębokości, np. z czujnika czasu lotu (ToF), jeśli jest dostępny. Urządzenie nie potrzebuje czujnika ToF do obsługi interfejsu Depth API.

Wymagania wstępne

Upewnij się, że znasz podstawowe pojęcia związane z AR. i dowiedz się, jak skonfigurować sesję ARCore, zanim przejdziesz dalej.

Ogranicz dostęp do urządzeń z obsługą głębi

Jeśli Twoja aplikacja wymaga obsługi interfejsu Depth API, ponieważ kluczowa część Jakość AR zależy od głębi lub dlatego, które wymagają głębszej reakcji, możesz ograniczyć rozpowszechnianie aplikację w Sklepie Google Play, aby urządzeniach obsługujących Depth API, dodając następujący wiersz do: AndroidManifest.xml, oprócz Zmiany (AndroidManifest.xml) opisane w Włączanie przewodnika po ARCore:

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

Włącz głębię

W nowej sesji ARCore sprawdź, czy urządzenie użytkownika obsługuje Depth. Nie wszystkie urządzenia zgodne z ARCore obsługują Depth API ze względu na ograniczenia mocy obliczeniowej. Aby można było oszczędzać zasoby, głębokość jest domyślnie wyłączona w ARCore. Włącz tryb głębi, aby aplikacja używała interfejsu Depth API.

Java

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

Kotlin

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)

Uzyskiwanie zdjęć głębi

Wywołaj Frame.acquireDepthImage16Bits(), aby uzyskać obraz głębi bieżącej klatki.

Java

// 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();
  }
}

Kotlin

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

Zwrócony obraz zawiera bufor nieprzetworzonego obrazu, który można przekazać do cieniowania fragmentów, aby użyć go w GPU w celu pominięcia każdego renderowanego obiektu. Ma ona język OPENGL_NORMALIZED_DEVICE_COORDINATES i można ją zmienić na TEXTURE_NORMALIZED, wywołując metodę Frame.transformCoordinates2d(). Po uzyskaniu dostępu do obrazu głębi w cieniowaniu obiektów można uzyskać dostęp do pomiarów głębokości i wyeliminować przesłonięcie.

Poznawanie wartości głębokości

Podany punkt A na zaobserwowanej geometrii rzeczywistej i punkt 2D: a reprezentujący ten sam punkt na zdjęciu głębi. Wartość podana jako Głębia Interfejs API w miejscu a ma długość CA przewidywanej na oś podmiotu zabezpieczeń. Może też być nazywana współrzędną Z zmiennej A względem kamery. miejsce wylotu: C. Podczas pracy z Depth API należy pamiętać, że wartości głębi to nie długość promienia CA, ale odwzorowanie .

Używaj głębi w cieniowaniu

Przeanalizuj informacje o głębi w bieżącej ramce

Aby uzyskać dostęp do informacji o głębi w bieżącej pozycji na ekranie, użyj funkcji pomocniczych DepthGetMillimeters() i DepthGetVisibility() w cieniowaniu fragmentów. Następnie na podstawie tych informacji możesz selektywnie ukrywać fragmenty renderowanego obiektu.

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

Blokuj wirtualne obiekty

Blokuj obiekty wirtualne w treści modułu cieniowania fragmentów. Zaktualizuj kanał alfa obiektu na podstawie jego głębokości. Spowoduje to wyrenderowanie wykluczonego obiektu.

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

Możesz wyrenderować przesłanianie za pomocą renderowania dwuetapowego lub renderowania z przebiegiem do przodu dla poszczególnych obiektów. Skuteczność każdego podejścia zależy od złożoności sceny i innych czynników związanych z aplikacją.

Na obiekt, renderowanie do przodu

Renderowanie z przebiegiem do przodu określa przesłonięcie każdego piksela obiektu w trybie cieniowania materiału. Jeśli piksele nie są widoczne, są przycinane, zwykle przez mieszanie alfa, w celu symulowania przesłaniania na urządzeniu użytkownika.

Renderowanie dwuprzebiegowe

W przypadku renderowania dwuprzebiegowego pierwsze przejście renderuje całą zawartość wirtualną w buforze pośrednim. Drugi etap polega na połączeniu sceny wirtualnej z tłem na podstawie różnicy między głębią świata a głębią sceny wirtualnej. Ta metoda nie wymaga dodatkowej pracy z programem cieniowania dla konkretnego obiektu i zwykle daje bardziej jednolite wyniki niż metoda oparta na przebiegu do przodu.

Wyodrębnij odległość z obrazu głębi

Aby używać interfejsu Depth API do celów innych niż zakrywanie wirtualnych obiektów lub wizualizowanie danych głębi, wyodrębnij informacje z obrazu głębi.

Java

/** 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));
}

Kotlin

/** 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()
}

Konwersja współrzędnych między zdjęciami aparatu a zdjęciami głębi

Obrazy uzyskane za pomocą getCameraImage() mogą mieć inny format obrazu niż obrazy głębi. W tym przypadku obraz głębi to przycięcie obrazu z aparatu i nie wszystkie piksele na zdjęciu mają odpowiednią przybliżoną głębię obrazu.

Aby uzyskać współrzędne głębi obrazu dla współrzędnych obrazu procesora:

Java

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

Kotlin

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

Aby uzyskać współrzędne obrazu CPU dla współrzędnych obrazu głębi:

Java

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

Kotlin

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

Test trafienia głębokości

Dzięki testom działań użytkownicy mogą umieszczać obiekty w rzeczywistych lokalizacjach sceny. Wcześniej testy trafień można było przeprowadzać tylko na wykrytych płaszczyznach, ograniczając lokalizacje do dużych, płaskich powierzchni, jak w przypadku wyników wyświetlanych przez zielone Androida. Przeprowadzanie testów głębokich korzysta z gładkich i nieprzetworzonych informacji o głębi, by zapewnić dokładniejsze wyniki trafień, nawet na powierzchniach niepłaskich i o niskiej tekstur. Ta informacja jest widoczna przy czerwonych urządzeniach z Androidem.

Aby użyć testów działań z uwzględnieniem głębokości, wywołaj hitTest() i sprawdź, czy na liście zwrotów występuje DepthPoints.

Java

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

Kotlin

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

Co dalej?

  • Włącz dokładniejsze wykrywanie za pomocą interfejsu Raw Depth API.
  • Wypróbuj ARCore Depth Lab, aby dowiedzieć się, jak uzyskać dostęp do szczegółowych danych.