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

API تست ضربه (Hit Test API) به شما امکان می‌دهد آیتم‌های مجازی را در نمای دنیای واقعی قرار دهید.

جو مدلی
Joe Medley

رابط برنامه‌نویسی کاربردی دستگاه WebXR پاییز گذشته در کروم ۷۹ منتشر شد. همانطور که در آن زمان گفته شد، پیاده‌سازی این رابط برنامه‌نویسی در کروم در حال انجام است. کروم با کمال میل اعلام می‌کند که بخشی از کار به پایان رسیده است. در کروم ۸۱، دو ویژگی جدید اضافه شده است:

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

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

نمونه جلسه واقعیت افزوده فراگیر

کد موجود در این مقاله بر اساس، اما نه دقیقاً مشابه، کدی است که در نمونه تست ضربه (Hit Test) گروه کاری Immersive Web ( دمو ، منبع ) یافت می‌شود. این مثال به شما امکان می‌دهد گل‌های آفتابگردان مجازی را روی سطوح دنیای واقعی قرار دهید.

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

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

یک رتیکل که بسته به زمینه آنها، روی دیوار، 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
});

ورود به یک جلسه

در مقالات قبلی کدی برای ورود به یک جلسه 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
}

قرار دادن یک شیء

وقتی کاربر روی صفحه ضربه می‌زند، یک شیء در AR قرار می‌گیرد. من قبلاً یک کنترل‌کننده رویداد 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 است. امیدوارم پیش‌زمینه کافی برای درک هر دو مورد به شما داده باشم.

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

عکس از دنیل فرانک در Unsplash