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

كل ما يخصّ حلقة اللقطات

Joe Medley
Joe Medley

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

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

ما لا تتناوله هذه المقالة

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

توضّح هذه المقالة أساسيات حلقة الإطارات، وذلك باستخدام نموذج جلسة الواقع الافتراضي الغامر من فريق عمل 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 وWebGLRenderingContext
العلاقة بين 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، وسأشرحها بعد قليل.

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

من الأمثلة على حظر الوضعية لأسباب تتعلّق بالخصوصية أن يعرض سماعة الرأس مربّع حوار أمان، مثل طلب الحصول على إذن، وفي هذه الحالة قد يتوقف المتصفّح عن تقديم الوضعيات إلى التطبيق. ولكنني اتصلتُ بالفعل بـ XRSession.requestAnimationFrame() حتى إذا تمكّن النظام من استعادة البيانات، ستستمر حلقة الصور. وفي حال عدم توفّرها، سيوقف وكيل المستخدم الجلسة ويستدعي معالج الأحداث end.

تحويلة قصيرة

تتطلّب الخطوة التالية عناصر تم إنشاؤها أثناء إعداد الجلسة. تذكَّر أنّني أنشأت لوحة عرض وطلبت منها إنشاء سياق عرض WebGL متوافق مع الواقع الممتد، وقد حصلت عليه من خلال استدعاء 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)
  });

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

رسم شيء ما في إطار المخزن المؤقت

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

الخاتمة

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