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

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

Joe Medley
Joe Medley

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

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

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

דוגמה לסשן ב-AR סוחף

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

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

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

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

יצירת הרשת

אתם צריכים ליצור את התמונה של רשת הראייה בעצמכם, כי היא לא מסופקת על ידי הדפדפן או ה-API. שיטת הטעינה והציור שלו ספציפית למסגרת. אם אתם לא מציירים אותו ישירות באמצעות WebGL או WebGL2, כדאי לעיין במסמכי העזרה של המסגרת. לכן לא אפרט לגבי אופן השרטוט של הרשת בדוגמה. בהמשך מוצגת שורה אחת ממנו, מסיבה אחת בלבד: כדי שבדוגמאות הקוד שבהמשך תוכלו לדעת למה אני מתכוון כשאני משתמש במשתנה 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() מתווסף קוד חדש לטיפול בבדיקת היטים.

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

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 של אינטרנט עשיר, ולא סיימנו. נמשיך לפרסם כאן מאמרים חדשים בהתאם להתקדמות שלנו.

תמונה של Daniel Frank ב-Unsplash