واقعیت مجازی به وب می آید، قسمت دوم

همه چیز درباره حلقه قاب

جو مدلی
Joe Medley

اخیراً، مقاله‌ای با عنوان «واقعیت مجازی به وب می‌آید» منتشر کردم که مفاهیم اولیه‌ی پشت رابط برنامه‌نویسی کاربردی دستگاه WebXR را معرفی می‌کرد. همچنین دستورالعمل‌هایی برای درخواست، ورود و پایان دادن به یک جلسه‌ی XR ارائه دادم.

این مقاله حلقه فریم را شرح می‌دهد، که یک حلقه بی‌نهایت کنترل‌شده توسط عامل کاربر است که در آن محتوا به طور مکرر روی صفحه نمایش داده می‌شود. محتوا در بلوک‌های گسسته‌ای به نام فریم رسم می‌شود. توالی فریم‌ها باعث ایجاد توهم حرکت می‌شود.

این مقاله چه چیزی نیست؟

WebGL و WebGL2 تنها ابزارهای رندر محتوا در طول یک حلقه فریم در یک برنامه WebXR هستند. خوشبختانه بسیاری از چارچوب‌ها لایه‌ای از انتزاع را بر روی WebGL و WebGL2 ارائه می‌دهند. چنین چارچوب‌هایی شامل three.js ، babylonjs و PlayCanvas هستند، در حالی که A-Frame و React 360 برای تعامل با WebXR طراحی شده‌اند.

این مقاله اصول اولیه یک حلقه فریم را با استفاده از نمونه Immersive VR Session گروه کاری Immersive Web ( دمو ، منبع ) توضیح می‌دهد. اگر می‌خواهید به WebGL یا یکی از چارچوب‌ها بپردازید، فهرست رو به رشدی از منابع آنلاین وجود دارد.

بازیکنان و بازی

وقتی سعی می‌کردم حلقه فریم را بفهمم، مدام در جزئیات گم می‌شدم. اشیاء زیادی در بازی وجود دارند و برخی از آنها فقط با توجه به ویژگی‌های مرجع اشیاء دیگر نامگذاری شده‌اند. برای اینکه بتوانید موضوع را سرراست نگه دارید، اشیاء را که من آنها را «بازیکن» می‌نامم، توصیف می‌کنم. سپس نحوه تعامل آنها را که من آن را «بازی» می‌نامم، شرح خواهم داد.

بازیکنان

XRViewerPose

یک حالت، موقعیت و جهت چیزی در فضای سه‌بعدی است. هم نمایشگرها و هم دستگاه‌های ورودی دارای یک حالت هستند، اما در اینجا منظور ما حالت نمایشگر است. هم حالت‌های نمایشگر و هم دستگاه ورودی دارای یک ویژگی transform هستند که موقعیت آن را به عنوان یک بردار و جهت آن را به عنوان یک چهارگانه نسبت به مبدأ توصیف می‌کند. مبدأ بر اساس نوع فضای مرجع درخواستی هنگام فراخوانی XRSession.requestReferenceSpace() مشخص می‌شود.

توضیح فضاهای مرجع کمی طول می‌کشد. من آنها را در واقعیت افزوده به طور مفصل پوشش می‌دهم. نمونه‌ای که من به عنوان مبنای این مقاله استفاده می‌کنم از یک فضای مرجع 'local' استفاده می‌کند، به این معنی که مبدا در موقعیت بیننده در زمان ایجاد جلسه بدون یک طبقه مشخص است و موقعیت دقیق آن ممکن است بسته به پلتفرم متفاوت باشد.

ایکس آر ویو

یک نما (view) معادل دوربینی است که صحنه مجازی را مشاهده می‌کند. یک نما همچنین دارای یک ویژگی transform است که موقعیت آن را به عنوان یک بردار و جهت آن را توصیف می‌کند. این ویژگی‌ها هم به صورت یک جفت بردار/چهارگانه و هم به صورت یک ماتریس معادل ارائه می‌شوند. می‌توانید بسته به اینکه کدام یک به بهترین شکل با کد شما مطابقت دارد، از هر دو نمایش استفاده کنید. هر نما معادل یک نمایشگر یا بخشی از نمایشگر است که توسط یک دستگاه برای ارائه تصاویر به بیننده استفاده می‌شود. اشیاء XRView در یک آرایه از شیء XRViewerPose بازگردانده می‌شوند. تعداد نماها در آرایه متفاوت است. در دستگاه‌های تلفن همراه، یک صحنه AR دارای یک نما است که ممکن است صفحه نمایش دستگاه را پوشش دهد یا ندهد. هدست‌ها معمولاً دو نما دارند، یکی برای هر چشم.

لایه XRWebGL

لایه‌ها منبعی از تصاویر بیت‌مپ و توضیحاتی در مورد نحوه رندر شدن این تصاویر در دستگاه ارائه می‌دهند. این توضیحات کاملاً گویای عملکرد این پخش‌کننده نیست. من به این نتیجه رسیده‌ام که این پخش‌کننده واسطه‌ای بین یک دستگاه و یک WebGLRenderingContext است. MDN نیز تقریباً همین دیدگاه را دارد و می‌گوید که بین این دو «ارتباطی» برقرار می‌کند. به این ترتیب، دسترسی به سایر پخش‌کننده‌ها را فراهم می‌کند.

به طور کلی، اشیاء WebGL اطلاعات حالت را برای رندر کردن گرافیک‌های دوبعدی و سه‌بعدی ذخیره می‌کنند.

بافر وب جی‌ال‌فریم

یک framebuffer داده‌های تصویر را به WebGLRenderingContext ارائه می‌دهد. پس از بازیابی آن از XRWebGLLayer ، آن را به WebGLRenderingContext فعلی منتقل می‌کنید. به غیر از فراخوانی bindFramebuffer() (که بعداً در مورد آن بیشتر صحبت خواهیم کرد) هرگز مستقیماً به این شیء دسترسی نخواهید داشت. شما صرفاً آن را از XRWebGLLayer به WebGLRenderingContext منتقل خواهید کرد.

ایکس‌آر ویوپورت

یک viewport مختصات و ابعاد یک ناحیه مستطیلی را در WebGLFramebuffer ارائه می‌دهد.

WebGLRenderingContext

یک زمینه رندرینگ، یک نقطه دسترسی برنامه‌نویسی‌شده برای یک بوم (فضایی که روی آن طراحی می‌کنیم) است. برای انجام این کار، به یک WebGLFramebuffer و یک XRViewport نیاز دارد.

به رابطه بین XRWebGLLayer و WebGLRenderingContext توجه کنید. یکی مربوط به دستگاه بیننده و دیگری مربوط به صفحه وب است. WebGLFramebuffer و XRViewport از اولی به دومی منتقل می‌شوند.

رابطه بین XRWebGLLayer و WebGLRenderingContext
رابطه بین XRWebGLLayer و WebGLRenderingContext

بازی

حالا که می‌دانیم بازیکنان چه کسانی هستند، بیایید به بازی‌ای که انجام می‌دهند نگاهی بیندازیم. این یک بازی است که با هر فریم از نو شروع می‌شود. به یاد بیاورید که فریم‌ها بخشی از یک حلقه فریم هستند که با سرعتی وابسته به سخت‌افزار اصلی اتفاق می‌افتد. برای برنامه‌های VR، فریم در ثانیه می‌تواند از ۶۰ تا ۱۴۴ باشد. AR برای اندروید با سرعت ۳۰ فریم در ثانیه اجرا می‌شود. کد شما نباید هیچ نرخ فریم خاصی را در نظر بگیرد.

فرآیند اساسی برای حلقه فریم به این شکل است:

  1. فراخوانی XRSession.requestAnimationFrame() . در پاسخ، عامل کاربر XRFrameRequestCallback را که توسط شما تعریف شده است، فراخوانی می‌کند.
  2. درون تابع فراخوانی شما:
    1. دوباره XRSession.requestAnimationFrame() را فراخوانی کنید.
    2. ژست بیننده را بگیرید.
    3. WebGLFramebuffer را از XRWebGLLayer به WebGLRenderingContext ارسال ('bind') کنید.
    4. روی هر شیء XRView تکرار کنید، XRViewport آن را از XRWebGLLayer بازیابی کنید و آن را به WebGLRenderingContext ارسال کنید.
    5. چیزی را در فریم‌بافر رسم کن.

چون مراحل ۱ و ۲ الف در مقاله قبلی پوشش داده شدند، من از مرحله ۲ ب شروع می‌کنم.

ژست بیننده را بگیرید

احتمالاً نیازی به گفتن نیست. برای ترسیم هر چیزی در 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 را فراخوانی می‌کند.

یک مسیر انحرافی کوتاه

مرحله بعدی نیاز به اشیاء ایجاد شده در طول تنظیم جلسه دارد. به یاد بیاورید که من یک بوم ایجاد کردم و به آن دستور دادم که یک زمینه رندر Web GL سازگار با XR ایجاد کند، که با فراخوانی canvas.getContext() به دست آوردم. تمام ترسیم با استفاده از API WebGL، API WebGL2 یا یک چارچوب مبتنی بر WebGL مانند Three.js انجام می‌شود. این زمینه علاوه بر یک نمونه جدید از XRWebGLLayer ، با updateRenderState() به شیء جلسه منتقل شد.

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 را ارسال ('bind') کنید

XRWebGLLayer یک فریم‌بافر برای WebGLRenderingContext فراهم می‌کند که به‌طور خاص برای استفاده با WebXR ارائه شده و جایگزین فریم‌بافر پیش‌فرض زمینه‌های رندرینگ می‌شود. این عمل در زبان WebGL «اتصال» نامیده می‌شود.

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 تکرار کنید

پس از دریافت پوز و اتصال فریم‌بافر، زمان دریافت نماها (viewports) فرا می‌رسد. XRViewerPose شامل آرایه‌ای از رابط‌های XRView است که هر کدام یک نمایشگر یا بخشی از یک نمایشگر را نشان می‌دهند. آن‌ها حاوی اطلاعاتی هستند که برای رندر کردن محتوایی که به درستی برای دستگاه و بیننده قرار گرفته است، مانند میدان دید، انحراف چشم و سایر ویژگی‌های نوری، مورد نیاز است. از آنجایی که من برای دو چشم طراحی می‌کنم، دو نما دارم که آن‌ها را مرور می‌کنم و برای هر کدام یک تصویر جداگانه ترسیم می‌کنم.

هنگام پیاده‌سازی واقعیت افزوده مبتنی بر تلفن، من فقط یک نما خواهم داشت اما همچنان از یک حلقه استفاده خواهم کرد. اگرچه ممکن است تکرار از طریق یک نما بی‌معنی به نظر برسد، اما انجام این کار به شما امکان می‌دهد یک مسیر رندر واحد برای طیف وسیعی از تجربیات فراگیر داشته باشید. این یک تفاوت مهم بین WebXR و سایر سیستم‌های فراگیر است.

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 بحثی داشتم. اسکریپت‌های نمونه و بیشتر کدهای WebXR این متغیر را gl می‌نامند. وقتی داشتم روی فهمیدن نمونه‌ها کار می‌کردم، مدام فراموش می‌کردم که gl به چه چیزی اشاره دارد. من آن را webGLRenContext نامیده‌ام تا در حین یادگیری به شما یادآوری کنم که این یک نمونه از WebGLRenderingContext است.

دلیل این امر این است که استفاده از gl باعث می‌شود نام متدها شبیه به معادل‌های خود در API مربوط به OpenGL ES 2.0 که برای ایجاد واقعیت مجازی در زبان‌های کامپایل‌شده استفاده می‌شود، به نظر برسند. اگر با استفاده از OpenGL برنامه‌های واقعیت مجازی نوشته باشید، این واقعیت واضح است، اما اگر کاملاً با این فناوری تازه‌کار باشید، گیج‌کننده خواهد بود.

چیزی را در فریم بافر رسم کن

اگر خیلی جاه‌طلب هستید، می‌توانید مستقیماً از WebGL استفاده کنید، اما من این را توصیه نمی‌کنم. استفاده از یکی از چارچوب‌های ذکر شده در بالا بسیار ساده‌تر است.

نتیجه‌گیری

این پایان به‌روزرسانی‌ها یا مقالات WebXR نیست. می‌توانید مرجعی برای تمام رابط‌ها و اعضای WebXR در MDN پیدا کنید. برای پیشرفت‌های آتی خود رابط‌ها، ویژگی‌های جداگانه را در Chrome Status دنبال کنید.