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

ה-API של בדיקת ההיטים מאפשר להציב פריטים וירטואליים בתצוגה של בעולם האמיתי.

ג'ו מדלי
ג'ו מדלי

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

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

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

טעימה של סשנים של מציאות רבודה (AR)

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

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

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

רשת שמעובדת על קיר, Lax או Strict בהתאם להקשר שלהם
הרשת היא תמונה זמנית שעוזרת למקם אובייקט במציאות רבודה.

יוצרים את הרשת

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

תמונה מאת דניאל פרנק ב-UnFlood