การวางตำแหน่งวัตถุเสมือนจริงในมุมมองสถานที่จริง

Hit Test API ช่วยให้คุณสามารถวางตำแหน่งรายการเสมือนจริงในมุมของจริง

โจ เมดเลย์
โจ เมดเลย์

WebXR Device API จัดส่งในช่วงฤดูใบไม้ร่วงที่ผ่านมาใน Chrome 79 ดังที่ได้กล่าวไปแล้ว การใช้งาน API ของ Chrome ยังอยู่ระหว่างการพัฒนา Chrome ยินดีที่จะประกาศให้ ทราบว่างานบางส่วนเสร็จสมบูรณ์แล้ว Chrome 81 มีฟีเจอร์ใหม่ 2 รายการดังนี้

บทความนี้กล่าวถึง WebXR Hit Test API ซึ่งเป็นการวางวัตถุเสมือนจริงในมุมมองกล้องของจริง

ในบทความนี้ เราถือว่าคุณทราบวิธีสร้างเซสชัน Augmented Reality และรู้วิธีเรียกใช้เฟรมลูปแล้ว หากไม่คุ้นเคยกับแนวคิดเหล่านี้ คุณควรอ่านบทความก่อนหน้าในชุดนี้

ตัวอย่างเซสชัน AR ที่สมจริง

โค้ดในบทความนี้อิงตามตัวอย่างการทดสอบ Hit ของ Immersive Web Working Group (รุ่นสาธิต แหล่งที่มา) แต่ไม่เหมือนกับโค้ดที่พบในตัวอย่างการทดสอบ Hit ของ Immersive Web Working Group ตัวอย่างนี้ให้คุณวางทานตะวันแบบเสมือนจริงบนพื้นผิวต่างๆ ในชีวิตจริง

เมื่อเปิดแอปครั้งแรก คุณจะเห็นวงกลมสีน้ำเงินพร้อมจุดที่อยู่ตรงกลาง จุดคือจุดร่วมระหว่างเส้นจินตภาพจากอุปกรณ์ไปยังจุดในสภาพแวดล้อม โดยขยับตามการขยับอุปกรณ์ เมื่อพบจุดสี่แยก ก็ดูเหมือนว่าจะยึดกับพื้นผิวต่างๆ เช่น พื้น พื้นโต๊ะ และผนัง ซึ่งทำได้ด้วยการทดสอบ Hit ที่จะระบุตำแหน่งและการวางแนวของจุดตัด แต่ไม่มีอะไรเกี่ยวกับพื้นผิวเลย

วงกลมนี้เรียกว่าเส้นโครง ซึ่งเป็นรูปภาพชั่วคราวที่ช่วยวางวัตถุในแบบ Augmented Reality ถ้าคุณแตะหน้าจอ ดอกทานตะวันจะวางบนพื้นผิวตรงตำแหน่งจุดยึดและการวางแนวของจุดยึด ไม่ว่าคุณจะแตะหน้าจอตรงไหนก็ตาม จุดยึดจะเคลื่อนที่ต่อไป กับอุปกรณ์ของคุณ

เส้นวัดที่แสดงผลบนผนัง, Lax หรือแบบเข้มงวด โดยขึ้นอยู่กับบริบท
เส้นวัดคือรูปภาพชั่วคราวที่ช่วยในการวางวัตถุในแบบ Augmented Reality

สร้างเส้นโครง

คุณต้องสร้างอิมเมจ Reticle ด้วยตัวเองเนื่องจากเบราว์เซอร์หรือ 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 เมื่อผู้ใช้แตะหน้าจอ ระบบจะวางดอกไม้ไว้ในมุมมองกล้องโดยอิงตามท่าทางของกรอบสี่เหลี่ยม ฉันจะอธิบาย Listener เหตุการณ์นั้นในภายหลัง

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() 2 ครั้ง ในตอนแรกฉันสับสน ผมถามว่าทำไมโค้ดทดสอบ Hit จึงไม่ขอเฟรมภาพเคลื่อนไหว (เริ่มลูปเฟรม) และทำไมเฟรมลูปจึงไม่เกี่ยวข้องกับการทดสอบ Hit สาเหตุของความสับสนคือ การเข้าใจผิดเกี่ยวกับพื้นที่อ้างอิง พื้นที่อ้างอิงแสดงความสัมพันธ์ ระหว่างต้นทางกับโลก

เพื่อทำความเข้าใจว่าโค้ดนี้ใช้ทำอะไร ให้สมมติว่าคุณกำลังดูตัวอย่างนี้โดยใช้อุปกรณ์แบบสแตนด์อโลน และคุณมีทั้งชุดหูฟังและตัวควบคุม หากต้องการวัดระยะทางจากตัวควบคุม คุณจะใช้เฟรมอ้างอิงที่มีตัวควบคุมเป็นศูนย์กลาง แต่การจะวาดบางสิ่งบนหน้าจอได้ใช้พิกัด ที่เน้นผู้ใช้เป็นหลัก

ในตัวอย่างนี้ ผู้มีสิทธิ์อ่านและตัวควบคุมเป็นอุปกรณ์เดียวกัน แต่ผมมีปัญหา สิ่งที่ฉันวาดต้องมีความเสถียรเมื่อพูดถึงสภาพแวดล้อม แต่ "ตัวควบคุม" ที่ฉันใช้วาดมีการเคลื่อนไหว

สำหรับการวาดภาพ ฉันใช้พื้นที่อ้างอิง local ซึ่งทำให้มีความเสถียร ในสภาพแวดล้อม หลังดำเนินการนี้แล้ว ฉันจะเริ่มลูปเฟรมโดยเรียก requestAnimationFrame()

สำหรับการทดสอบ Hit ฉันใช้พื้นที่อ้างอิง viewer ซึ่งอิงตามท่าทางของอุปกรณ์ในขณะที่มีการทดสอบ Hit ป้ายกำกับ "ผู้ดู" ค่อนข้างสับสน ในบริบทนี้ เพราะฉันกำลังพูดถึงตัวควบคุม ตัวควบคุมเป็นอุปกรณ์รับชมอิเล็กทรอนิกส์ ก็น่าจะเหมาะสมเช่นกัน หลังจากได้ข้อมูลนี้ ผมจึงเรียกใช้ xrSession.requestHitTestSource() ซึ่งจะสร้างแหล่งที่มาของข้อมูลทดสอบ Hit ที่ผมจะใช้เมื่อวาด

การเรียกใช้ลูปเฟรม

นอกจากนี้ โค้ดเรียกกลับ requestAnimationFrame() ยังจะได้รับโค้ดใหม่เพื่อจัดการการทดสอบ Hit อีกด้วย

เมื่อคุณย้ายอุปกรณ์ เส้นวัดจะต้องเคลื่อนที่ไปพร้อมกับพยายามค้นหาพื้นผิว หากต้องการสร้างภาพลวงตาของการเคลื่อนไหว ให้วาดเส้นจุดซ้ำในทุกเฟรม แต่อย่าแสดงเส้นเป้าหากการทดสอบการชนล้มเหลว ดังนั้น สำหรับเส้นตรงที่ผมสร้างไว้ก่อนหน้านี้ ผมตั้งค่าพร็อพเพอร์ตี้ 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
}

ถ้าอยากวาดภาพอะไรใน AR ฉันต้องรู้เลยว่าผู้ชมอยู่ที่ไหน และมองอะไรอยู่ ดังนั้นฉันจึงทดสอบว่า 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 การทดสอบ Hit อาจพบหลายแพลตฟอร์ม รายการแรกในอาร์เรย์คืออันที่ใกล้กับกล้องที่สุด โดยส่วนใหญ่คุณจะใช้งาน แต่ระบบจะส่งคืนอาร์เรย์สำหรับกรณีการใช้งานขั้นสูง เช่น สมมติว่ากล้องเล็งไปที่กล่องบนโต๊ะบนพื้น การทดสอบ Hit อาจแสดงผลทั้ง 3 แพลตฟอร์มในอาร์เรย์ ในกรณีส่วนใหญ่ มันจะเป็นกล่องที่ฉันสนใจ หากความยาวของอาร์เรย์ที่จะแสดงผลเป็น 0 กล่าวคือ หากไม่มีการส่งคืนการทดสอบ Hit ให้ดำเนินการต่อต่อไป โปรดลองอีกครั้งในเฟรมถัดไป

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
}

สุดท้าย เราต้องประมวลผลผลการทดสอบ Hit ขั้นตอนพื้นฐานคือ โพสท่าจากผลการทดสอบ Hit เปลี่ยนรูปแบบ (ย้าย) ภาพเส้นตรงไปยังตำแหน่งทดสอบ Hit จากนั้นตั้งค่าคุณสมบัติ 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
}

การวางวัตถุ

วัตถุจะวางอยู่ใน AR เมื่อผู้ใช้แตะหน้าจอ ฉันได้เพิ่มเครื่องจัดการเหตุการณ์ select ในเซสชันแล้ว (ดูด้านบน)

สิ่งสำคัญในขั้นตอนนี้คือการทราบว่าจะวางไว้ที่ใด เนื่องจากเส้นเป้าที่เคลื่อนที่จะเป็นแหล่งที่มาของการทดสอบการ Hit อย่างสม่ำเสมอ วิธีที่ง่ายที่สุดในการวางวัตถุคือการวาดวัตถุที่ตำแหน่งเส้นสะท้อนในการทดสอบ Hit ครั้งสุดท้าย

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 ผมหวังว่าคุณจะให้ภูมิหลัง เพียงพอสำหรับการทำความเข้าใจทั้ง 2 อย่าง

เราไม่ได้สร้าง API เว็บที่สมจริงด้วยการทดสอบในระยะยาว เราจะเผยแพร่บทความใหม่ๆ ที่นี่ในขณะที่เราพัฒนา

รูปภาพโดย Daniel Frank ใน Unsplash