Định vị đối tượng ảo trong chế độ xem thực tế

API Thử nghiệm nhấn cho phép bạn đặt các hàng hoá kỹ thuật số trong chế độ xem thực tế.

Joe Medley
Joe Medley

WebXR Device API đã được phát hành vào mùa thu năm ngoái trong Chrome 79. Như đã nêu, việc triển khai API của Chrome vẫn đang trong quá trình hoàn thiện. Chrome rất vui được thông báo rằng một số công việc đã hoàn tất. Trong Chrome 81, hai tính năng mới đã ra mắt:

Bài viết này đề cập đến WebXR Hit Test API, một phương tiện để đặt các đối tượng ảo vào chế độ xem camera trong thế giới thực.

Trong bài viết này, tôi giả định rằng bạn đã biết cách tạo một phiên thực tế tăng cường và biết cách chạy vòng lặp khung. Nếu chưa quen với các khái niệm này, bạn nên đọc các bài viết trước đó trong loạt bài này.

Mẫu phiên thực tế tăng cường sống động

Mã trong bài viết này dựa trên nhưng không giống với mã trong mẫu Thử nghiệm nhấn của Nhóm công tác web sống động (bản minh hoạ, nguồn). Ví dụ này cho phép bạn đặt hoa hướng dương ảo trên các bề mặt trong thế giới thực.

Khi mở ứng dụng lần đầu tiên, bạn sẽ thấy một vòng tròn màu xanh dương có một dấu chấm ở giữa. Dấu chấm là giao điểm giữa một đường thẳng tưởng tượng từ thiết bị của bạn đến điểm trong môi trường. Hình này sẽ di chuyển khi bạn di chuyển thiết bị. Khi tìm thấy các điểm giao nhau, ứng dụng sẽ bám vào các bề mặt như sàn nhà, mặt bàn và tường. Lý do là vì thử nghiệm nhấn cung cấp vị trí và hướng của điểm giao nhau, nhưng không cung cấp thông tin về các bề mặt.

Vòng tròn này được gọi là mạng lưới, là một hình ảnh tạm thời giúp đặt một đối tượng trong thực tế tăng cường. Nếu bạn nhấn vào màn hình, một bông hoa hướng dương sẽ được đặt trên bề mặt tại vị trí của tâm ngắm và hướng của điểm tâm ngắm, bất kể bạn nhấn vào vị trí nào trên màn hình. Hình chữ thập tiếp tục di chuyển theo thiết bị của bạn.

Một tâm ngắm được kết xuất trên tường, Lax hoặc Strict tuỳ thuộc vào bối cảnh của chúng
Mạng lưới là một hình ảnh tạm thời giúp đặt một đối tượng trong thực tế tăng cường.

Tạo tâm ngắm

Bạn phải tự tạo hình ảnh lưới chữ thập vì trình duyệt hoặc API không cung cấp hình ảnh này. Phương thức tải và vẽ hình ảnh này tuỳ thuộc vào khung. Nếu bạn không vẽ trực tiếp bằng WebGL hoặc WebGL2, hãy tham khảo tài liệu về khung của bạn. Vì lý do này, tôi sẽ không đi sâu vào chi tiết về cách vẽ lưới ngắm trong mẫu. Dưới đây, tôi chỉ trình bày một dòng của tệp này vì một lý do duy nhất: để trong các mã mẫu sau này, bạn sẽ biết tôi đang đề cập đến điều gì khi sử dụng biến reticle.

let reticle = new Gltf2Node({url: 'media/gltf/reticle/reticle.gltf'});

Yêu cầu một phiên

Khi yêu cầu một phiên, bạn phải yêu cầu 'hit-test' trong mảng requiredFeatures như minh hoạ dưới đây.

navigator.xr.requestSession('immersive-ar', {
  requiredFeatures: ['local', 'hit-test']
})
.then((session) => {
  // Do something with the session
});

Vào một phiên

Trong các bài viết trước, tôi đã trình bày mã để tham gia một phiên XR. Tôi đã trình bày một phiên bản của quy trình này ở bên dưới, kèm theo một số nội dung bổ sung. Trước tiên, tôi đã thêm trình nghe sự kiện select. Khi người dùng nhấn vào màn hình, một bông hoa sẽ được đặt trong chế độ xem camera dựa trên tư thế của tâm ngắm. Tôi sẽ mô tả trình nghe sự kiện đó sau.

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

Nhiều không gian tham chiếu

Lưu ý rằng mã được đánh dấu gọi XRSession.requestReferenceSpace() hai lần. Ban đầu, tôi thấy điều này khá khó hiểu. Tôi đã hỏi tại sao mã thử nghiệm nhấn không yêu cầu khung ảnh động (bắt đầu vòng lặp khung hình) và tại sao vòng lặp khung hình dường như không liên quan đến thử nghiệm nhấn. Nguồn gốc của sự nhầm lẫn là do hiểu sai về không gian tham chiếu. Không gian tham chiếu thể hiện mối quan hệ giữa điểm gốc và thế giới.

Để hiểu rõ chức năng của mã này, hãy giả vờ rằng bạn đang xem mẫu này bằng một thiết bị độc lập và bạn có cả tai nghe lẫn tay điều khiển. Để đo khoảng cách từ bộ điều khiển, bạn sẽ sử dụng hệ quy chiếu lấy bộ điều khiển làm trung tâm. Nhưng để vẽ nội dung nào đó lên màn hình, bạn sẽ sử dụng toạ độ lấy người dùng làm trung tâm.

Trong mẫu này, thiết bị xem và thiết bị điều khiển là cùng một thiết bị. Nhưng tôi gặp một vấn đề. Những gì tôi vẽ phải ổn định so với môi trường, nhưng "bộ điều khiển" mà tôi đang vẽ lại di chuyển.

Để vẽ hình ảnh, tôi sử dụng không gian tham chiếu local, giúp tôi có được sự ổn định về mặt môi trường. Sau khi nhận được thông tin này, tôi bắt đầu vòng lặp khung bằng cách gọi requestAnimationFrame().

Để thử nghiệm nhấn, tôi sử dụng không gian tham chiếu viewer, dựa trên tư thế của thiết bị tại thời điểm thử nghiệm nhấn. Nhãn "viewer" (trình xem) có phần gây nhầm lẫn trong bối cảnh này vì tôi đang nói về một bộ điều khiển. Điều này là hợp lý nếu bạn coi bộ điều khiển là một trình xem điện tử. Sau khi nhận được thông tin này, tôi gọi xrSession.requestHitTestSource() để tạo nguồn dữ liệu thử nghiệm nhấn mà tôi sẽ sử dụng khi vẽ.

Chạy vòng lặp khung

Lệnh gọi lại requestAnimationFrame() cũng nhận được mã mới để xử lý thử nghiệm nhấn.

Khi bạn di chuyển thiết bị, tâm ngắm cần di chuyển theo thiết bị khi thiết bị cố gắng tìm bề mặt. Để tạo ảo giác về chuyển động, hãy vẽ lại lưới ngắm trong mỗi khung hình. Nhưng đừng hiện tâm ngắm nếu thử nghiệm nhấn không thành công. Vì vậy, đối với tâm ngắm mà tôi đã tạo trước đó, tôi đặt thuộc tính visible của tâm ngắm thành 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
}

Để vẽ bất cứ thứ gì trong thực tế tăng cường, tôi cần biết người xem đang ở đâu và họ đang nhìn vào đâu. Vì vậy, tôi kiểm tra để đảm bảo hitTestSource và xrViewerPose vẫn hợp lệ.

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
}

Bây giờ, tôi gọi getHitTestResults(). Hàm này lấy hitTestSource làm đối số và trả về một mảng các thực thể HitTestResult. Thử nghiệm nhấn có thể tìm thấy nhiều bề mặt. Phần tử đầu tiên trong mảng là phần tử gần máy ảnh nhất. Hầu hết các trường hợp bạn sẽ sử dụng, nhưng một mảng được trả về cho các trường hợp sử dụng nâng cao. Ví dụ: hãy tưởng tượng camera của bạn đang hướng vào một chiếc hộp trên bàn trên sàn nhà. Có thể thử nghiệm nhấn sẽ trả về cả ba bề mặt trong mảng. Trong hầu hết các trường hợp, đó sẽ là hộp mà tôi quan tâm. Nếu độ dài của mảng được trả về là 0, nói cách khác, nếu không có thử nghiệm nhấn nào được trả về, hãy tiếp tục. Hãy thử lại trong khung hình tiếp theo.

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
}

Cuối cùng, tôi cần xử lý kết quả thử nghiệm nhấn. Quy trình cơ bản như sau. Lấy tư thế từ kết quả thử nghiệm nhấn, biến đổi (di chuyển) hình ảnh lưới chữ thập đến vị trí thử nghiệm nhấn, sau đó đặt thuộc tính visible thành true. Tư thế thể hiện tư thế của một điểm trên bề mặt.

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
}

Đặt đối tượng

Một đối tượng được đặt trong thực tế tăng cường khi người dùng nhấn vào màn hình. Tôi đã thêm một trình xử lý sự kiện select vào phiên. (Xem ở trên.)

Điều quan trọng trong bước này là biết vị trí đặt quảng cáo. Vì tâm ngắm di chuyển cung cấp cho bạn nguồn thử nghiệm nhấn liên tục, nên cách đơn giản nhất để đặt một đối tượng là vẽ đối tượng đó tại vị trí của tâm ngắm trong lần thử nghiệm nhấn gần đây nhất.

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

Kết luận

Cách tốt nhất để nắm được điều này là thực hiện từng bước theo mã mẫu hoặc thử lớp học lập trình. Tôi hy vọng mình đã cung cấp đủ thông tin cơ bản để bạn hiểu được cả hai.

Chúng tôi vẫn chưa hoàn tất việc xây dựng các API web sống động, còn lâu mới xong. Chúng tôi sẽ đăng các bài viết mới tại đây khi có tiến triển.