استخدام الواقع الافتراضي على الويب، الجزء الثاني

كل شيء عن حلقة الإطار

Joe Medley
Joe Medley

نشرتُ مؤخرًا مقالة الواقع الافتراضي يظهر على الويب، وهي مقالة طرحت فيها المفاهيم الأساسية عن واجهة برمجة التطبيقات WebXR Device API. لقد قدمت أيضًا تعليمات لطلب جلسة XR وإدخالها وإنهائها.

تصف هذه المقالة حلقة الإطار، وهي عبارة عن حلقة لانهائية يتحكّم فيها وكيل المستخدم ويتم فيها سحب المحتوى بشكل متكرّر إلى الشاشة. يتم رسم المحتوى في كتل منفصلة تسمى الإطارات. يخلق تتابع الإطارات وهمًا بالحركة.

المحتوى الذي لا تتناوله هذه المقالة

إن WebGL وWebX2 هما الوسيلة الوحيدة لعرض المحتوى أثناء حلقة الإطار في تطبيق WebXR. لحسن الحظ، فإن العديد من الأطر العمل تقدم طبقة من التجريد على WebGL وWebX2. وتشمل أُطر العمل هذه كلاً من three.js وbabylonjs وPlayCanvas، في حين تم تصميم A-Frame وReact 360 للتفاعل مع WebXR.

هذه المقالة ليست WebGL أو برنامجًا تعليميًا لإطار العمل. وهو يشرح أساسيات تكرار الإطارات باستخدام نموذج جلسة الواقع الافتراضي الواقعي ضمن مجموعة Immersive Web Working Group (العرض التوضيحي، المصدر). إذا كنت تريد التعمّق في WebGL أو أحد أطر العمل، توفّر الإنترنت قائمة متزايدة من المقالات.

اللاعبون واللعبة

وعند محاولة فهم حلقة الإطار، كنت أتوق إلى التفاصيل باستمرار. هناك الكثير من الكائنات قيد التشغيل، وتتم تسمية بعضها فقط باستخدام خصائص مرجعية في كائنات أخرى. لمساعدتك في الحفاظ على الوضع مستقيمًا، سأصف الكائنات التي أسميها "اللاعبون". ثم سأصف كيفية تفاعلهم، والتي أطلق عليها "اللعبة".

اللاعبون

XRViewerPose

الوضع هو وضع واتجاه شيء ما في مساحة ثلاثية الأبعاد. لدى كل من المشاهدين وأجهزة الإدخال وضعية، ولكنها هي وضعية المشاهد التي نهتم بها هنا. يتضمّن كلّ من وضعَي العارض وجهاز الإدخال سمة transform تصف موضعه كمتجه واتجاهه كرباعي نسبةً إلى المصدر. يتم تحديد المصدر بناءً على نوع المساحة المرجعية المطلوب عند استدعاء XRSession.requestReferenceSpace().

تحتاج المساحات المرجعية إلى القليل للشرح. وأتناولها بالتفصيل في الواقع المعزز. إنّ النموذج الذي أستخدمه كأساس لهذه المقالة يستخدم مساحة مرجعية 'local'، ما يعني أنّ المصدر يكون في موضع المشاهد وقت إنشاء الجلسة بدون حد أدنى محدد، وقد يختلف موضعه الدقيق حسب النظام الأساسي.

XRView

تتوافق طريقة العرض مع كاميرا تعرض المشهد الافتراضي. تحتوي طريقة العرض أيضًا على السمة transform التي تصف موضعها كمتجه واتجاهه. يتم توفير هذه كزوج متجه/رباعي وكمصفوفة مكافئة، يمكنك استخدام أي من التمثيلين اعتمادًا على ما يناسب الكود الخاص بك بشكل أفضل. تتوافق كل طريقة عرض مع شاشة عرض أو جزء من شاشة يستخدمها أحد الأجهزة لتقديم الصور للمشاهد. يتم عرض كائنات XRView في صفيف من الكائن XRViewerPose. يختلف عدد المشاهدات في المصفوفة. على الأجهزة الجوّالة، يكون لمشهد الواقع المعزّز طريقة عرض واحدة قد تغطي شاشة الجهاز أو لا تغطيها. عادةً ما يكون لسماعات الرأس مشاهدتان، واحدة لكل عين.

XRWebGLLayer

توفر الطبقات مصدرًا للصور النقطية وأوصافًا لكيفية عرض هذه الصور في الجهاز. هذا الوصف لا يعبر تمامًا عن ما يفعله هذا اللاعب. لقد أعتبرتُ ذلك وسيطًا بين جهاز وWebGLRenderingContext. يتبنى MDN نفس وجهة النظر، إذ يشير إلى أنه "يوفر ارتباطًا" بين الاثنين. وبناءً على ذلك، فإنه يتيح الوصول إلى اللاعبين الآخرين.

بوجه عام، تخزن كائنات WebGL معلومات الحالة لعرض الرسومات ثنائية وثلاثية الأبعاد.

WebGLFramebuffer

يوفّر المخزن المؤقت للإطارات بيانات الصورة لـ WebGLRenderingContext. بعد استرداده من XRWebGLLayer، ما عليك سوى تمريره إلى WebGLRenderingContext الحالي. بخلاف استدعاء bindFramebuffer() (المزيد عن ذلك لاحقًا)، لن تتمكن أبدًا من الوصول إلى هذا الكائن مباشرةً. ما عليك سوى تمريرها من XRWebGLLayer إلى WebGLRenderingContext.

XRViewport

يوفّر إطار العرض إحداثيات وأبعاد المنطقة المستطيلة في WebGLFramebuffer.

WebGLRenderingContext

سياق العرض هو نقطة وصول آلي إلى اللوحة (المساحة التي نعتمد عليها). ولإجراء ذلك، يحتاج إلى كل من WebGLFramebuffer وXRViewport.

لاحِظ العلاقة بين XRWebGLLayer وWebGLRenderingContext. يتجاوب أحدهما مع جهاز المشاهد والآخر مع صفحة الويب. يتم تمرير WebGLFramebuffer وXRViewport من الأول إلى الأخير.

العلاقة بين XRWebGLLayer وWebRenderingContext
العلاقة بين XRWebGLLayer وWebGLRenderingContext

اللعبة

الآن بعد أن عرفنا من هم، لنلقِ نظرة على اللعبة التي يلعبونها. إنها لعبة تبدأ من جديد مع كل إطار. تذكر أن الإطارات هي جزء من تكرار الإطارات الذي يحدث بمعدل يعتمد على الجهاز الأساسي. بالنسبة إلى تطبيقات الواقع الافتراضي، يمكن أن تتراوح الإطارات في الثانية بين 60 و144 إطارًا. يعمل الواقع المعزّز لأجهزة Android بمعدّل 30 لقطة في الثانية. يجب ألا يفترض الرمز البرمجي أي عدد لقطات في الثانية معيّن.

تبدو العملية الأساسية لتكرار الإطار على النحو التالي:

  1. تواصل هاتفيًا مع "XRSession.requestAnimationFrame()". استجابةً لذلك، يستدعي وكيل المستخدم XRFrameRequestCallback الذي حدّدته.
  2. داخل دالة رد الاتصال:
    1. يمكنك الاتصال بـ "XRSession.requestAnimationFrame()" مرة أخرى.
    2. اكتسب الوضعية التي اتخذها المشاهد.
    3. مرِّر ("ربط") WebGLFramebuffer من XRWebGLLayer إلى WebGLRenderingContext.
    4. التكرار فوق كل عنصر XRView، استرداد XRViewport من XRWebGLLayer وتمريره إلى WebGLRenderingContext.
    5. ارسم شيئًا للمخزن المؤقت للإطارات.

نظرًا لأن المقالة السابقة قد تم تناول الخطوتين 1 و2 (أ)، سأبدأ من الخطوة 2 (ب).

حسّن وضعية المشاهد

من المحتمل أن يعبر عن ذلك. لرسم أي شيء في الواقع المعزّز أو الواقع الافتراضي، أحتاج إلى معرفة مكان المشاهد وأين ينظر. يتم تحديد موضع المشاهد واتجاهه بواسطة كائن 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(). يتم تنفيذ جميع الرسومات باستخدام واجهة برمجة تطبيقات WebGL أو واجهة برمجة تطبيقات WebGL2 أو إطار عمل يستند إلى 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 مخزنًا مؤقتًا للإطارات لـ 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

بعد الحصول على الوضعية وربط المخزن المؤقت للإطارات، حان الوقت للحصول على إطارات العرض. يحتوي 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 إلى ما يمكن ملاحظته على الشاشة. لكن للرسم إلى هذا العرض، أحتاج إلى إحداثيات وأبعاد خاصة بجهازي. وكما هي الحال في المخزن المؤقت للإطارات، أطلبها من 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 المستخدَمة لإنشاء واقع افتراضي في لغات مجمّعة. هذه الحقيقة واضحة إذا كنت قد كتبت تطبيقات الواقع الافتراضي باستخدام OpenGL، ولكنها مُربكة إذا كنت جديدًا تمامًا في هذه التكنولوجيا.

رسم عنصر للمخزن المؤقت للإطارات

أمّا إذا كنت طموحًا للغاية، فيمكنك استخدام WebGL مباشرةً، ولكنني لا أنصح بذلك. من الأسهل استخدام أحد أُطر العمل المدرجة في الأعلى.

الخلاصة

هذه ليست نهاية تحديثات WebXR أو مقالاته. يمكنك العثور على مرجع لجميع واجهات وأعضائها في WebXR في MDN. للحصول على التحسينات القادمة على الواجهات نفسها، اتّبِع الميزات الفردية حول حالة Chrome.

صورة من JESHOOTS.COM على Un تحقق