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

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

جو ميدلي
جو ميدلي

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

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

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

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

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

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

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

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

إنشاء الشبكة

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

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);
  }
}

الخلاصة

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

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

تصوير دانيال فرانك على UnLaunch