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

Hit Test API ช่วยให้คุณจัดวางไอเทมเสมือนจริงในมุมมองของชีวิตจริงได้

Joe Medley
Joe Medley

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

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

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

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

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

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

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

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

สร้างรูรับแสง

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

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

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

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

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

การเรียกใช้เฟรมวน

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

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

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 รายการ

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

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