Korzystanie z głębi w aplikacji na Androida

Interfejs Depth API ułatwia aparatowi urządzenia określenie rozmiaru i kształtu rzeczywistych obiektów na ekranie. Aparat wykorzystuje aparat do tworzenia zdjęć głębi (map głębi), dzięki czemu aplikacja nabiera warstwy realizmu AR. Za pomocą informacji dostarczanych przez zdjęcia w trybie głębokim możesz tworzyć wirtualne obiekty dokładnie przed lub za rzeczywistymi obiektami, co sprawia, że wrażenia użytkownika są wciągające i realistyczne.

Informacje o głębokości są obliczane na podstawie ruchu i mogą być łączone z informacjami pochodzącymi ze sprzętowego czujnika głębokości, takiego jak czujnik czasu lotu (ToF), jeśli jest dostępny. Urządzenie nie potrzebuje czujnika ToF, aby obsługiwać interfejs Depth API.

Wymagania wstępne

Zanim przejdziesz dalej, upewnij się, że znasz podstawowe pojęcia związane z AR i wiesz, jak skonfigurować sesję ARCore.

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

Jeśli Twoja aplikacja wymaga obsługi interfejsu Depth API ze względu na fakt, że jej główna część zależy od głębi lub z powodu braku efektów zastępczych dla tych części aplikacji, które korzystają z głębokości, możesz ograniczyć jej rozpowszechnianie w Sklepie Google Play tylko do urządzeń obsługujących Depth API. W tym celu oprócz instrukcji dotyczących AndroidManifest.xml możesz dodać do interfejsu AndroidManifest.xml ten wiersz.

<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. Z powodu ograniczeń mocy obliczeniowej nie wszystkie urządzenia zgodne z ARCore obsługują interfejs Depth API. Aby oszczędzać zasoby, głębia 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)

Pobierz zdjęcia głębi

Wywołaj Frame.acquireDepthImage16Bits(), aby uzyskać zdjęcie głębi w bieżącej klatce.

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 zapewnia bufor nieprzetworzonego obrazu, który można przekazać do cieniowania fragmentów, aby wykorzystać go przez GPU, aby zasłaniać każdy wyrenderowany obiekt. Język jest ukierunkowany na OPENGL_NORMALIZED_DEVICE_COORDINATES i można go zmienić na TEXTURE_NORMALIZED, wywołując Frame.transformCoordinates2d(). Gdy obraz głębi będzie dostępny w programie cieniowania obiektów, pomiary głębi można wykonywać bezpośrednio w celu obsługi przesłonięcia.

Interpretowanie wartości głębokości

Biorąc pod uwagę punkt A na obserwowanej geometrii w świecie rzeczywistym i punkt 2D a reprezentujący ten sam punkt na obrazie głębi, wartość podana przez interfejs Depth API w miejscu a jest równa długości obiektu CA umieszczonego na głównej osi. Nazywa się to też współrzędną z obiektu A względem źródła kamery C. Podczas pracy z interfejsem Depth API należy pamiętać, że wartości głębi to nie długość promienia CA, ale jego odwzorowanie.

Używaj głębi w cieniowaniu

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

Użyj funkcji pomocniczych DepthGetMillimeters() i DepthGetVisibility() w cieniowaniu fragmentów, aby uzyskać szczegółowe informacje o bieżącym położeniu ekranu. Następnie użyj tych informacji do selektywnego zasłonięcia części 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;
}

Wycisz obiekty wirtualne

Ukrywanie obiektów wirtualnych w treści cienia fragmentów. Zaktualizuj kanał alfa obiektu na podstawie jego głębokości. Spowoduje to wyrenderowanie zasłoniętego 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);

Stosując renderowanie dwuprzebiegowe lub renderowanie z przebiegiem w przód na obiekt, możesz zastosować przesłonięcie. Efektywność każdego podejścia zależy od złożoności sceny i innych czynników związanych z aplikacją.

Renderowanie według obiektu, przekazywanie do przodu

Renderowanie z przebiegiem w przód na poziomie obiektu decyduje o przesłonięciu każdego piksela obiektu w jego cieniowaniu. Jeśli piksele nie są widoczne, są przycinane, zwykle w ramach mieszania alfa, co powoduje symulowanie przesłonięcia na urządzeniu użytkownika.

Renderowanie dwuprzebiegowe

W przypadku renderowania dwuprzebiegowego pierwszy przebieg renderuje wszystkie wirtualne treści w buforze pośrednim. Druga opcja polega na połączeniu sceny wirtualnej z tłem w oparciu o różnicę między głębią świata rzeczywistego a wirtualną głębią sceny. To podejście nie wymaga dodatkowego cieniowania specyficznego dla obiektów i zwykle daje bardziej jednolite wyniki niż metoda przekazywania do przodu.

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

Aby używać interfejsu Depth API do innych celów niż zasłanianie obiektów wirtualnych lub wizualizowanie danych o 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()
}

Konwertowanie współrzędnych między zdjęciami z aparatu i obrazami głębi

Obrazy uzyskane za pomocą getCameraImage() mogą mieć inny format obrazu niż zdjęcia głębi. W tym przypadku obraz głębi to przycięcie obrazu z aparatu, a nie wszystkie piksele na zdjęciu z aparatu mają odpowiadające im oszacowanie głębi.

Aby uzyskać współrzędne obrazu głębi na obrazie 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 procesora 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 zgodności z głębokością

Testy działań pozwalają użytkownikom umieszczać obiekty w rzeczywistej lokalizacji na scenie. Wcześniej testy trafień można było przeprowadzać tylko na wykrytych samolotach, co ograniczało możliwości lokalizacji do dużych, płaskich powierzchni, takich jak wyniki wyświetlane przez zielone Androida. W teście uderzenia w głębokości wykorzystywane są informacje zarówno płynne, jak i bezpośrednie, pozwalające uzyskać dokładniejsze wyniki trafień, nawet na powierzchniach niepłaskich i o niskiej gęstości tekstu. Jest to oznaczone czerwonymi Androidem.

Aby użyć szczegółowych testów trafień, wywołaj hitTest() i sprawdź wartość DepthPoints na liście zwracanych.

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 dzięki Raw Depth API.
  • Sprawdź ARCore Depth Lab, w którym przedstawiamy różne sposoby uzyskiwania dostępu do szczegółowych danych.