وضع العناصر الافتراضية في عرض العالم الحقيقي

تتيح لك Hit Test API تحديد موضع العناصر الافتراضية في عرض واقعي.

Joe Medley
Joe Medley

تم طرح WebXR Device API في الإصدار 79 من Chrome في الخريف الماضي. وكما ذكرنا آنذاك، لا يزال تنفيذ واجهة برمجة التطبيقات في Chrome قيد التطوير. يسرّ فريق Chrome الإعلان عن اكتمال بعض الأعمال. في الإصدار 81 من Chrome، ظهرت ميزتان جديدتان:

تتناول هذه المقالة WebXR Hit Test API، وهو وسيلة لوضع أجسام افتراضية في عرض الكاميرا في العالم الواقعي.

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

عيّنة من جلسة الواقع المعزّز

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

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

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

شبكي معروض على حائط أو لاكس أو صارم حسب سياقه
شبكة الاستهداف هي صورة مؤقتة تساعد في وضع عنصر في الواقع المعزّز.

إنشاء شبكة الاستهداف

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

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 التي تستند إلى وضع الجهاز في وقت اختبار التأثير. إنّ التصنيف "المُشاهد" قد يسبب بعض confusion في هذا السياق لأنّني أتحدث عن جهاز تحكّم. ويُعتبر ذلك منطقيًا إذا اعتبرت وحدة التحكّم كمشاهد إلكتروني. بعد الحصول على هذا، أُجري دعوة xrSession.requestHitTestSource()، التي تنشئ مصدر بيانات اختبار التصادم التي سأستخدمها عند الرسم.

تشغيل حلقة إطارات

يحصل الإجراء المُعاد الاتصال به 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
}

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

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
}

وضع عنصر

يتم وضع كائن في الواقع المعزّز عندما ينقر المستخدم على الشاشة. سبق أن أضفت معالج أحداث 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);
  }
}

الخاتمة

إنّ أفضل طريقة للتعامل مع هذا الأمر هي من خلال الانتقال إلى نموذج الرمز أو تجربة الدرس التطبيقي حول الترميز. آمل أن أكون قد أعطيتك خلفية كافية لفهم كليهما.

لم ننتهي بعد من إنشاء واجهات برمجة تطبيقات غامرة على الويب. سننشر مقالات جديدة هنا كلما أحرزنا تقدمًا في هذا الصدد.

صورة دانيال فرانك على Unsplash