המציאות הווירטואלית מגיעה לאינטרנט, חלק 2

כל מה שצריך לדעת על לולאת הפריימים

Joe Medley
Joe Medley

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

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

מה לא במאמר הזה

WebGL ו-WebGL2 הם הדרכים היחידות לעיבוד תוכן במהלך לולאת פריים באפליקציית WebXR. למרבה המזל, יש הרבה מסגרות שמספקות שכבת הפשטה מעל WebGL ו-WebGL2. מסגרות כאלה כוללות את three.js,‏ babylonjs ו-PlayCanvas, ואילו A-Frame ו-React 360 תוכננו ליצירת אינטראקציה עם WebXR.

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

השחקנים והמשחק

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

השחקנים

XRViewerPose

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

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

XRView

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

XRWebGLLayer

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

באופן כללי, אובייקטים של WebGL שומרים מידע על מצב לצורך עיבוד גרפיקה דו-ממדית ותלת-ממדית.

WebGLFramebuffer

מאגר framebuffer מספק נתוני תמונה ל-WebGLRenderingContext. אחרי שמאחזרים אותו מה-XRWebGLLayer, מעבירים אותו ל-WebGLRenderingContext הנוכחי. מלבד קריאה ל-bindFramebuffer() (מידע נוסף על כך בהמשך), אף פעם לא תהיה לכם גישה ישירה לאובייקט הזה. פשוט מעבירים אותו מה-XRWebGLLayer ל-WebGLRenderingContext.

XRViewport

אזור התצוגה מספק את הקואורדינטות והמידות של אזור מלבני ב-WebGLFramebuffer.

WebGLRenderingContext

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

שימו לב לקשר בין XRWebGLLayer ל-WebGLRenderingContext. אחד מהם תואם למכשיר של הצופה והשני תואם לדף האינטרנט. הערכים WebGLFramebuffer ו-XRViewport מועברים מהקודם לשני.

הקשר בין XRWebGLLayer לבין WebGLRenderingContext
הקשר בין XRWebGLLayer ל-WebGLRenderingContext

המשחק

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

התהליך הבסיסי של לולאת הפריימים נראה כך:

  1. התקשרו אל XRSession.requestAnimationFrame(). בתגובה, סוכן המשתמש מפעיל את XRFrameRequestCallback, שהגדרתם.
  2. בתוך פונקציית הקריאה החוזרת:
    1. התקשרו שוב למספר XRSession.requestAnimationFrame().
    2. אחזור של תנוחת הצופה.
    3. מעבירים ('מקשרים') את WebGLFramebuffer מ-XRWebGLLayer אל WebGLRenderingContext.
    4. מבצעים איטרציה מעל כל אובייקט XRView, מאחזרים את ה-XRViewport שלו מה-XRWebGLLayer ומעבירים אותו אל WebGLRenderingContext.
    5. משרטטים משהו למאגר הנתונים הזמני.

מאחר ששלבים 1 ו-2א צוינו במאמר הקודם, אתחיל בשלב 2ב.

התמונה של הצופה

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

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
  if (xrViewerPose) {
    // Render based on the pose.
  }
}

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

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

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

סטייה קצרה

בשלב הבא נדרשים אובייקטים שנוצרו במהלך הגדרת הסשן. נזכרים שיצרתי לוח ציור והנחיתי אותו ליצור הקשר עיבוד (render) של Web GL תואם-XR, שקיבלתי באמצעות קריאה ל-canvas.getContext(). כל הציור מתבצע באמצעות WebGL API,‏ WebGL2 API או מסגרת מבוססת-WebGL כמו Three.js. ההקשר הזה הועבר לאובייקט הסשן דרך updateRenderState(), יחד עם מופע חדש של XRWebGLLayer.

let canvas = document.createElement('canvas');
// The rendering context must be based on WebGL or WebGL2
let webGLRenContext = canvas.getContext('webgl', { xrCompatible: true });
xrSession.updateRenderState({
    baseLayer: new XRWebGLLayer(xrSession, webGLRenContext)
  });

העברה ('קישור') של WebGLFramebuffer

XRWebGLLayer מספק framebuffer ל-WebGLRenderingContext שמיועד במיוחד לשימוש עם WebXR, ומחליף את framebuffer ברירת המחדל של הקשרי הרינדור. שפה זו נקראת 'קישור'.

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
  if (xrViewerPose) {
    let glLayer = xrSession.renderState.baseLayer;
    webGLRenContext.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
    // Iterate over the views
  }
}

מבצעים איטרציה מעל כל אובייקט XRView

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

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

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
  if (xrViewerPose) {
    let glLayer = xrSession.renderState.baseLayer;
    webGLRenContext.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
    for (let xrView of xrViewerPose.views) {
      // Pass viewports to the context
    }
  }
}

העברת האובייקט XRViewport אל WebGLRenderingContext

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

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
  if (xrViewerPose) {
    let glLayer = xrSession.renderState.baseLayer;
    webGLRenContext.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
    for (let xrView of xrViewerPose.views) {
      let viewport = glLayer.getViewport(xrView);
      webGLRenContext.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
      // Draw something to the framebuffer
    }
  }
}

ה-webGLRenContext

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

הסיבה לכך היא שהשימוש ב-gl מאפשר לשמות השיטות להיראות כמו עמיתיהם ב-OpenGL ES 2.0 API, שמשמש ליצירת VR בשפות הידור. העובדה הזו ברורה אם כבר כתבתם אפליקציות VR באמצעות OpenGL, אבל היא מבלבלת אם אתם חדשים לגמרי בטכנולוגיה הזו.

שרטטו משהו במאגר הנתונים הזמני

אם אתם ממש שאפתניים, אפשר להשתמש ישירות ב-WebGL, אבל לא מומלץ לעשות זאת. הרבה יותר קל להשתמש באחת מהמסגרות המפורטות למעלה.

סיכום

זה לא סוף העדכונים או המאמרים בנושא WebXR. ב-MDN תוכלו למצוא מסמך עזרה עם כל הממשקים והחברים של WebXR. כדי לקבל עדכונים על שיפורים עתידיים בממשקים עצמם, תוכלו לעקוב אחרי תכונות ספציפיות ב-Chrome Status.

תמונה של JESHOOTS.COM ב-Unsplash