שימוש ב'עומק' באפליקציית Android NDK

ה-Depth API עוזר למצלמה של המכשיר להבין את הגודל והצורה של האובייקטים האמיתיים בסצנה. היא משתמשת במצלמה כדי ליצור תמונות עומק או מפות עומק, וכך להוסיף לאפליקציות שלכם שכבה של ריאליזם ב-AR. אפשר להשתמש במידע שמסופק על ידי תמונת עומק כדי לגרום לאובייקטים וירטואליים להופיע במדויק לפני אובייקטים בעולם האמיתי או מאחוריהם. כך ניתן לספק חוויית משתמש סוחפת ומציאותית.

נתוני העומק מחושבים מתנועה ועשויים להיות משולבים עם מידע מחיישן עומק של חומרה, כמו חיישן זמן טיסה (ToF), אם קיים. במכשיר אין צורך בחיישן ToF כדי לתמוך ב-Depth API.

דרישות מוקדמות

לפני שממשיכים, חשוב לוודא שאתם מבינים את המושגים הבסיסיים של AR ואת האופן שבו מגדירים סשן של ARCore.

הגבלת הגישה למכשירים שתומכים בעומק

אם נדרשת תמיכה ב-Depth API באפליקציה, כי חלק מרכזי מחוויית ה-AR מסתמך על העומק או כי אין חלופה חיננית לחלקים באפליקציה שמשתמשים בעומק, אפשר להגביל את הפצת האפליקציה בחנות Google Play למכשירים שתומכים ב-Depth API. לשם כך מוסיפים את השורה הבאה ל-AndroidManifest.xml, בנוסף לשינויי ה-ARC שמתוארים ב-AndroidManifest.xml:

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

הפעלת העומק

בסשן חדש של ARCore, צריך לבדוק אם מכשיר של משתמש תומך בעומק. לא כל המכשירים שתואמים ל-ARCore תומכים ב-Depth API בגלל אילוצי כוח עיבוד. כברירת מחדל, כדי לחסוך במשאבים, העומק מושבת ב-ARCore. כדי שהאפליקציה תשתמש ב-Depth API, צריך להפעיל את מצב העומק.

// Check whether the user's device supports the Depth API.
int32_t is_depth_supported = 0;
ArSession_isDepthModeSupported(ar_session, AR_DEPTH_MODE_AUTOMATIC,
                               &is_depth_supported);

// Configure the session for AR_DEPTH_MODE_AUTOMATIC.
ArConfig* ar_config = NULL;
ArConfig_create(ar_session, &ar_config);
if (is_depth_supported) {
  ArConfig_setDepthMode(ar_session, ar_config, AR_DEPTH_MODE_AUTOMATIC);
}
CHECK(ArSession_configure(ar_session, ar_config) == AR_SUCCESS);
ArConfig_destroy(ar_config);

משיגים תמונות עומק

אפשר להתקשר אל ArFrame_acquireDepthImage16Bits() כדי לקבל את תמונת העומק של הפריים הנוכחי.

// Retrieve the depth image for the current frame, if available.
ArImage* depth_image = NULL;
// If a depth image is available, use it here.
if (ArFrame_acquireDepthImage16Bits(ar_session, ar_frame, &depth_image) !=
    AR_SUCCESS) {
  // No depth image received for this frame.
  // This normally 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.
  return;
}

התמונה שמוחזרת מספקת את מאגר התמונות הגולמיות. ניתן להעביר את הנתונים האלה לתוכנת הצללה של הקטעים לצורך שימוש ב-GPU לכל אובייקט שעבר רינדור. המיקום הנוכחי הוא AR_COORDINATES_2D_OPENGL_NORMALIZED_DEVICE_COORDINATES. אפשר לשנות אותו לAR_COORDINATES_2D_TEXTURE_NORMALIZED באמצעות שיחה אל ArFrame_transformCoordinates2d(). לאחר שניתן לגשת לתמונת העומק מתוך תוכנת הצללה של אובייקט, ניתן לגשת ישירות למדידות העומק האלה לצורך טיפול בחסימות.

הסבר על ערכי העומק

בהינתן הנקודה A על הגיאומטריה בעולם האמיתי ונקודה דו-ממדית a שמייצגת את אותה נקודה בתמונת העומק, הערך שניתן על ידי ממשק ה-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);

ניתן לעבד חסימות באמצעות עיבוד בשני מעברים או רינדור לפי אובייקט העברה קדימה. היעילות של כל גישה תלויה במורכבות הסצנה ובשיקולים אחרים, ספציפיים לאפליקציה.

רינדור לפי אובייקט, העברה קדימה

רינדור של מעבר קדימה לפי אובייקט קובע את ההסתרה של כל פיקסל של העצם בהצללה של החומר. אם הפיקסלים לא גלויים, הם נחתכים, בדרך כלל באמצעות מיזוג אלפא, ומתבצעת הדמיה של הסתרה במכשיר של המשתמש.

עיבוד בשני שלבים

עם עיבוד בשני שלבים, האישור הראשון מעבד את כל התוכן הווירטואלי למאגר מתווך. הכרטיס השני משלב את הסצנה הווירטואלית עם הרקע, בהתאם להבדל בין העומק בעולם האמיתי לבין העומק של הסצנה הווירטואלית. גישה זו לא דורשת עבודת הצללה ספציפית לאובייקט נוסף, ובדרך כלל היא מניבה תוצאות שנראות אחידות יותר משיטת המעבר הלאה.

המרת קואורדינטות בין תמונות מצלמה לתמונות עומק

יחס הגובה-רוחב של תמונות שהתקבלו באמצעות ArFrame_acquireCameraImage() עשוי להיות שונה בהשוואה לתמונות עומק. במקרה זה, תמונת העומק היא חיתוך של תמונת המצלמה, ולא לכל הפיקסלים בתמונת המצלמה יש הערכת עומק חוקית תואמת.

כדי לקבל קואורדינטות תמונה של עומק עבור קואורדינטות בתמונה של המעבד:

const float cpu_image_coordinates[] = {(float)cpu_coordinate_x,
                                 (float)cpu_coordinate_y};
float texture_coordinates[2];
ArFrame_transformCoordinates2d(
    ar_session, ar_frame, AR_COORDINATES_2D_IMAGE_PIXELS, 1,
    cpu_image_coordinates, AR_COORDINATES_2D_TEXTURE_NORMALIZED,
    texture_coordinates);
if (texture_coordinates[0] < 0 || texture_coordinates[1] < 0) {
  // There are no valid depth coordinates, because the coordinates in the CPU
  // image are in the cropped area of the depth image.
} else {
  int depth_image_width = 0;
  ArImage_getWidth(ar_session, depth_image, &depth_image_width);
  int depth_image_height = 0;
  ArImage_getHeight(ar_session, depth_image, &depth_image_height);

  int depth_coordinate_x = (int)(texture_coordinates[0] * depth_image_width);
  int depth_coordinate_y = (int)(texture_coordinates[1] * depth_image_height);
}

כדי לקבל קואורדינטות תמונה של המעבד (CPU) עבור קואורדינטות של תמונת עומק:

int depth_image_width = 0;
ArImage_getWidth(ar_session, depth_image, &depth_image_width);
int depth_image_height = 0;
ArImage_getHeight(ar_session, depth_image, &depth_image_height);

float texture_coordinates[] = {
    (float)depth_coordinate_x / (float)depth_image_width,
    (float)depth_coordinate_y / (float)depth_image_height};
float cpu_image_coordinates[2];
ArFrame_transformCoordinates2d(
    ar_session, ar_frame, AR_COORDINATES_2D_TEXTURE_NORMALIZED, 1,
    texture_coordinates, AR_COORDINATES_2D_IMAGE_PIXELS,
    cpu_image_coordinates);

int cpu_image_coordinate_x = (int)cpu_image_coordinates[0];
int cpu_image_coordinate_y = (int)cpu_image_coordinates[1];

בדיקת התאמת עומק

בדיקות היט מאפשרות למשתמשים להציב אובייקטים במיקום בעולם האמיתי בתוך הסצנה. בעבר, ניתן היה לבצע בדיקות של היטים רק במישורים שזוהו, שהגבילו את המיקומים שלהם למשטחים גדולים ושטוחים, כמו התוצאות שמוצגות על ידי מכשירי Android הירוקים. בדיקות של התאמות עומק משתמשות במידע על עומק חלק ובמידע על עומק גולמי, כדי לספק תוצאות מדויקות יותר של היטים, גם על פני משטחים שאינם מישוריים או בעלי מרקם נמוך. השם הזה מוצג במכשירי Android האדומים.

כדי להשתמש בבדיקות היטים התומכות בעומק, אפשר להתקשר אל ArFrame_hitTest() ולבדוק אם יש AR_TRACKABLE_DEPTH_POINT ברשימת החזרה.

// Create a hit test using the Depth API.
ArHitResultList* hit_result_list = NULL;
ArHitResultList_create(ar_session, &hit_result_list);
ArFrame_hitTest(ar_session, ar_frame, hit_x, hit_y, hit_result_list);

int32_t hit_result_list_size = 0;
ArHitResultList_getSize(ar_session, hit_result_list, &hit_result_list_size);

ArHitResult* ar_hit_result = NULL;
for (int32_t i = 0; i < hit_result_list_size; ++i) {
  ArHitResult* ar_hit = NULL;
  ArHitResult_create(ar_session, &ar_hit);
  ArHitResultList_getItem(ar_session, hit_result_list, i, ar_hit);

  ArTrackable* ar_trackable = NULL;
  ArHitResult_acquireTrackable(ar_session, ar_hit, &ar_trackable);
  ArTrackableType ar_trackable_type = AR_TRACKABLE_NOT_VALID;
  ArTrackable_getType(ar_session, ar_trackable, &ar_trackable_type);
  // Creates an anchor if a plane or an oriented point was hit.
  if (AR_TRACKABLE_DEPTH_POINT == ar_trackable_type) {
    // Do something with the hit result.
  }
  ArTrackable_release(ar_trackable);
  ArHitResult_destroy(ar_hit);
}

ArHitResultList_destroy(hit_result_list);

מה השלב הבא?

  • מפעילים חישה מדויקת יותר באמצעות Raw Depth API.
  • כדאי לנסות את ARCore Depth Lab, שמדגים דרכים שונות לגשת לנתוני עומק.