מיקום אובייקטים וירטואליים בתצוגות בעולם האמיתי

‫Hit Test API מאפשר למקם פריטים וירטואליים בתצוגה של העולם האמיתי.

Joe Medley
Joe Medley

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

במאמר הזה נסביר על WebXR Hit Test API, שמאפשר להציב אובייקטים וירטואליים בתצוגת מצלמה של העולם האמיתי.

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

הדוגמה של סשן AR עשיר

הקוד במאמר הזה מבוסס על הקוד שנמצא בדוגמה לבדיקה של מיקום המצביע של Immersive Web Working Group (הדגמה, מקור), אבל הוא לא זהה לו. הדוגמה הזו מאפשרת לכם להציב חמניות וירטואליות על משטחים בעולם האמיתי.

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

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

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

יצירת הכוונת

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

let reticle = new Gltf2Node({url: 'media/gltf/reticle/reticle.gltf'});

בקשה לפגישה

כשמבקשים סשן, צריך לבקש את 'hit-test' במערך requiredFeatures, כמו שמוצג בהמשך.

navigator.xr.requestSession('immersive-ar', {
  requiredFeatures: ['local', 'hit-test']
})
.then((session) => {
  // Do something with the session
});

כניסה לסשן

במאמרים קודמים הצגתי קוד לכניסה לסשן XR. הוספתי כמה דברים לגרסה שלמטה. קודם הוספתי את רכיב ה-event listener של select. כשהמשתמש ילחץ על המסך, פרח יופיע בתצוגת המצלמה בהתאם למיקום של הכוונת. אתאר את רכיב ה-event listener הזה בהמשך.

function onSessionStarted(xrSession) {
  xrSession.addEventListener('end', onSessionEnded);
  xrSession.addEventListener('select', onSelect);

  let canvas = document.createElement('canvas');
  gl = canvas.getContext('webgl', { xrCompatible: true });

  xrSession.updateRenderState({
    baseLayer: new XRWebGLLayer(session, gl)
  });

  xrSession.requestReferenceSpace('viewer').then((refSpace) => {
    xrViewerSpace = refSpace;
    xrSession.requestHitTestSource({ space: xrViewerSpace })
    .then((hitTestSource) => {
      xrHitTestSource = hitTestSource;
    });
  });

  xrSession.requestReferenceSpace('local').then((refSpace) => {
    xrRefSpace = refSpace;
    xrSession.requestAnimationFrame(onXRFrame);
  });
}

מספר מרחבי עזר

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

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

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

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

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

הפעלת לולאת פריימים

הקריאה החוזרת (callback) של requestAnimationFrame() מקבלת גם קוד חדש לטיפול בבדיקה של מיקום המצביע.

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

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);

  reticle.visible = false;

  // Reminder: the hitTestSource was acquired during onSessionStart()
  if (xrHitTestSource && xrViewerPose) {
    let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
    if (hitTestResults.length > 0) {
      let pose = hitTestResults[0].getPose(xrRefSpace);
      reticle.visible = true;
      reticle.matrix = pose.transform.matrix;
    }
  }

  // Draw to the screen
}

כדי לצייר משהו ב-AR, אני צריך לדעת איפה הצופה נמצא ולאן הוא מסתכל. אז אני בודק שhitTestSource וxrViewerPose עדיין תקפים.

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);

  reticle.visible = false;

  // Reminder: the hitTestSource was acquired during onSessionStart()
  if (xrHitTestSource && xrViewerPose) {
    let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
    if (hitTestResults.length > 0) {
      let pose = hitTestResults[0].getPose(xrRefSpace);
      reticle.visible = true;
      reticle.matrix = pose.transform.matrix;
    }
  }

  // Draw to the screen
}

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

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);

  reticle.visible = false;

  // Reminder: the hitTestSource was acquired during onSessionStart()
  if (xrHitTestSource && xrViewerPose) {
    let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
    if (hitTestResults.length > 0) {
      let pose = hitTestResults[0].getPose(xrRefSpace);
      reticle.visible = true;
      reticle.matrix = pose.transform.matrix;
    }
  }

  // Draw to the screen
}

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

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);

  reticle.visible = false;

  // Reminder: the hitTestSource was acquired during onSessionStart()
  if (xrHitTestSource && xrViewerPose) {
    let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
    if (hitTestResults.length > 0) {
      let pose = hitTestResults[0].getPose(xrRefSpace);
      reticle.matrix = pose.transform.matrix;
      reticle.visible = true;

    }
  }

  // Draw to the screen
}

הצבת אובייקט

אובייקט ממוקם ב-AR כשהמשתמש מקיש על המסך. כבר הוספתי לסשן גורם מטפל באירועים מסוג select. (כמו שמופיע למעלה).

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

function onSelect(event) {
  if (reticle.visible) {
    // The reticle should already be positioned at the latest hit point,
    // so we can just use its matrix to save an unnecessary call to
    // event.frame.getHitTestResults.
    addARObjectAt(reticle.matrix);
  }
}

סיכום

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

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