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

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

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

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

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

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

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

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

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

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

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

רינדור בשני מעברים

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

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

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

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

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, שמדגים דרכים שונות לגשת לנתוני עומק.