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

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

Joe Medley
Joe Medley

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

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

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

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

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

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

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

เป้าเล็งที่แสดงผลบนผนัง โดยจะแสดงแบบ Lax หรือ Strict ขึ้นอยู่กับบริบท
เป้าเล็งคือรูปภาพชั่วคราวที่ช่วยในการวางวัตถุในเทคโนโลยีความจริงเสริม (AR)

สร้างเป้าเล็ง

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

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

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

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

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

กำลังเรียกใช้การวนซ้ำเฟรม

Callback 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
}

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

การวางวัตถุ

ระบบจะวางวัตถุใน 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 หวังว่าข้อมูลที่ให้ไปจะเพียงพอที่จะทำให้คุณเข้าใจทั้ง 2 อย่างนี้

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