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

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

Joe Medley
Joe Medley

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

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

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

דוגמה לסשן AR immersive

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

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

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