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

Hit Test API cho phép bạn đặt các mục ảo 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 Chrome triển khai API này vẫn đang trong quá trình hoàn thiện. Chrome rất vui mừng thông báo rằng một số công việc đã hoàn tất. Trong Chrome 81, có 2 tính năng mới:

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 trong chế độ xem camera thực tế.

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 một vòng lặp khung hình. Nếu chưa quen với những 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 hệt) mã có trong mẫu Kiểm tra lượt truy cập của Nhóm công tác về 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 lê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 ả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 thẳng này sẽ xuất hiện để gắn vào các bề mặt như sàn nhà, mặt bàn và tường. Điều này là do tính năng kiểm tra lượt truy cập cung cấp vị trí và hướng của điểm giao nhau, nhưng không cung cấp thông tin gì về chính các bề mặt.

Vòng tròn này được gọi là tâm ngắm, là một hình ảnh tạm thời giúp đặt một đối tượng vào 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í tâm ngắm và hướng của điểm tâm ngắm, bất kể bạn nhấn vào màn hình ở đâu. Tâm ngắm sẽ 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
Tâm ngắm là một hình ảnh tạm thời giúp đặt một vật thể vào thực tế tăng cường.

Tạo tâm ngắm

Bạn phải tự tạo hình ảnh tâm ngắm 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ẽ đối tượng này là phương thức dành riêng cho 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 cách vẽ tâm ngắm trong mẫu. Tôi chỉ cho thấy một dòng của mã này vì một lý do duy nhất: để trong các mẫu mã 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
});

Tham gia 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 phiên bản này bên dưới cùng với 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 vào khung hình 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ó hiểu. Tôi hỏi tại sao mã kiểm thử lượt nhấn không yêu cầu một khung hì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 các lượt kiểm thử lượt nhấn. Nguyên nhân gây ra 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 một điểm gốc và thế giới.

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

Trong mẫu này, người xem và bộ điều khiển là cùng một thiết bị. Nhưng tôi gặp phải 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ẽ bằng lại di chuyển.

Đối với việc 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ôi trường. Sau khi nhận được thông tin này, tôi sẽ bắt đầu vòng lặp khung bằng cách gọi requestAnimationFrame().

Để kiểm thử lượt 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 kiểm thử lượt nhấn. Nhãn "viewer" (người xem) có phần gây nhầm lẫn trong ngữ cảnh này vì tôi đang nói về một bộ điều khiển. Điều này có lý nếu bạn coi bộ điều khiển là một thiết bị xem điện tử. Sau khi nhận được thông tin này, tôi sẽ gọi xrSession.requestHitTestSource(). Lệnh này sẽ tạo nguồn dữ liệu kiểm tra lượt truy cập mà tôi 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ý kiểm thử lượt truy cập.

Khi bạn di chuyển thiết bị, tâm ngắm cũng cần di chuyển theo khi thiết bị cố gắng tìm các bề mặt. Để tạo ảo giác về chuyển động, hãy vẽ lại tâm ngắm trong mỗi khung hình. Nhưng đừng hiện tâm ngắm nếu kiểm thử lượt 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 rằng hitTestSourcexrViewerPose 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 cho getHitTestResults(). Phương thức này lấy hitTestSource làm đối số và trả về một mảng gồm các thực thể HitTestResult. Thử nghiệm lượt truy cập 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 camera nhất. Hầu hết thời gian bạn sẽ sử dụng nó, nhưng một mảng sẽ đượ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 đặt trên sàn nhà. Có thể kiểm thử lượt truy cập sẽ trả về cả 3 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, tức là nếu không có lượt kiểm thử nào được trả về, hãy tiếp tục. Hãy thử lại ở 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ả kiểm tra lượt nhấn. Quy trình cơ bản như sau. Lấy một tư thế từ kết quả kiểm tra lượt nhấn, biến đổi (di chuyển) hình ảnh tâm ngắm đến vị trí kiểm tra lượt nhấn, sau đó đặt thuộc tính visible thành true. Tư thế này biểu thị 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 một đối tượng

Một đối tượng sẽ được đặt trong AR 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 phần trên.)

Điều quan trọng trong bước này là biết vị trí đặt. Vì tâm ngắm di chuyển cung cấp cho bạn một nguồn kiểm tra lượt 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 ở lượt kiểm tra lượt nhấn cuối cùng.

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 bắt được điều này là xem qua mã mẫu hoặc dùng thử lớp học mã. Hy vọng tôi đã cung cấp cho bạn đủ thông tin cơ bản để hiểu được cả hai.

Chúng tôi vẫn đang tiếp tục xây dựng các API web sống động. Chúng tôi sẽ xuất bản các bài viết mới tại đây khi có tiến triển.

Ảnh của Daniel Frank trên Unsplash