موقعیت‌یابی اشیای مجازی در نماهای دنیای واقعی

Hit Test API به شما امکان می‌دهد موارد مجازی را در نمای دنیای واقعی قرار دهید.

Joe Medley
Joe Medley

WebXR Device API پاییز گذشته در Chrome 79 عرضه شد. همان‌طور که در آن زمان اعلام شد، پیاده‌سازی API در Chrome هنوز درحال توسعه است. Chrome خوشحال است که اعلام کند بخشی از کار تمام شده است. در Chrome 81، دو ویژگی جدید اضافه شده است:

این مقاله WebXR Hit Test API را پوشش می‌دهد، روشی برای قرار دادن اشیای مجازی در نمای دوربین دنیای واقعی.

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

نمونه جلسه واقعیت افزوده همه‌جانبه

کد موجود در این مقاله براساس نمونه «آزمایش برخورد» Immersive Web Working Group است، اما با آن یکسان نیست (نسخه نمایشی، منبع). این نمونه به شما امکان می‌دهد گل‌های آفتابگردان مجازی را روی سطوح در دنیای واقعی قرار دهید.

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

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

شبکه تار مویی که روی دیوار، Lax، یا Strict بسته به بافت آن‌ها ارائه شده است
شبکه هدف تصویر موقتی است که به قرار دادن شیء در واقعیت افزوده کمک می‌کند.

ایجاد تارک

باید تصویر تار را خودتان ایجاد کنید زیرا توسط مرورگر یا 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
});

ورود به جلسه

در مقاله‌های قبلی، کدی را برای ورود به جلسه واقعیت گسترش‌یافته ارائه کرده‌ام. نسخه‌ای از این را با چند مورد اضافه در زیر نشان داده‌ام. ابتدا شنونده رویداد 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 را برمی‌گرداند. آزمایش ضربه ممکن است چند سطح را پیدا کند. اولین مورد در آرایه نزدیک‌ترین مورد به دوربین است. بیشتر اوقات از آن استفاده خواهید کرد، اما آرایه‌ای برای موارد استفاده پیشرفته برگردانده می‌شود. برای مثال، تصور کنید دوربینتان به‌سمت جعبه‌ای روی میز روی زمین گرفته شده است. ممکن است آزمایش برخورد هر سه سطح را در آرایه برگرداند. در بیشتر موارد، جعبه برایم مهم است. اگر طول آرایه برگردانده‌شده ۰ باشد، به‌عبارت دیگر، اگر هیچ تست ضربه‌ای برگردانده نشود، ادامه دهید. در قاب بعدی دوباره امتحان کنید.

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

نتیجه‌گیری

بهترین راه برای درک این موضوع، بررسی گام‌به‌گام کد نمونه یا امتحان کردن codelab است. امیدوارم پیش‌زمینه کافی برای درک هر دو به شما داده باشم.

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