在實際檢視畫面中放置虛擬物件

點擊測試 API 可讓您在真實世界檢視畫面中放置虛擬商品。

Joe Medley
Joe Medley

WebXR Device API 於去年秋季在 Chrome 79 中推出。如當時所述,Chrome 的 API 實作仍在開發階段。Chrome 很高興宣布,部分工作已完成。Chrome 81 推出兩項新功能:

本文將介紹 WebXR Hit Test API,這是一種在真實世界相機檢視畫面中放置虛擬物件的方法。

本文假設您已瞭解如何建立擴增實境工作階段,以及如何執行影格迴圈。如果您不熟悉這些概念,建議您閱讀本系列的前幾篇文章。

沉浸式 AR 工作階段範例

本文中的程式碼是以 Immersive Web Working Group 的點擊測試範例 (示範、來源) 為基礎,但並不完全相同。這個範例可讓您在真實世界的表面上放置虛擬向日葵。

初次開啟應用程式時,你會看到一個藍色圓圈,中間有一個圓點。圓點是從裝置到環境中某個點的假想線交點。會隨著裝置移動。找到交點後,就會貼齊地板、桌面和牆壁等表面。這是因為點擊測試會提供交點的位置和方向,但不會提供表面本身的資訊。

這個圓圈稱為「標線」,是暫時性圖片,可協助在擴增實境中放置物件。輕觸螢幕後,無論輕觸螢幕的位置為何,向日葵都會放置在十字游標位置和十字游標點方向的表面上。標線會隨著裝置移動。

視情境而定,在牆上顯示寬鬆或嚴格的十字準星
標線是暫時性圖片,可協助在擴增實境中放置物件。

建立十字準星

您必須自行建立十字線圖片,因為瀏覽器或 API 不會提供。載入和繪製的方法則因框架而異。如果您不是直接使用 WebGL 或 WebGL2 繪製,請參閱架構說明文件。因此,我不會詳細說明範例中如何繪製十字準星。以下我只顯示一行,原因只有一個:在後續的程式碼範例中,您會知道我使用 reticle 變數時所指的內容。

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

要求會談

要求工作階段時,您必須在 requiredFeatures 陣列中要求 'hit-test',如下所示。

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

進入工作階段

在先前的文章中,我介紹了進入 XR 工作階段的程式碼。以下是這個版本的範例,並加入了一些內容。首先,我新增了 select 事件監聽器。使用者輕觸螢幕時,系統會根據十字線的姿勢,在相機檢視畫面中放置一朵花。稍後我會說明該事件監聽器。

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() 兩次。我一開始覺得很困惑。我詢問為什麼點擊測試程式碼沒有要求動畫影格 (啟動影格迴圈),以及為什麼影格迴圈似乎不涉及點擊測試。造成混淆的原因是誤解了參考空間。參考空間可表示原點與世界之間的關係。

如要瞭解這段程式碼的作用,請假裝您正在使用獨立式裝置查看這個範例,且同時擁有頭戴式裝置和控制器。如要測量與控制器的距離,請使用以控制器為中心的參照框架。但如要在螢幕上繪製內容,則應使用以使用者為中心的座標。

在這個範例中,檢視器和控制器是同一個裝置。但我遇到一個問題。我繪製的內容必須與環境保持穩定,但我繪製的「控制器」正在移動。

繪製圖片時,我會使用 local 參考空間,這樣環境就會保持穩定。取得後,我會呼叫 requestAnimationFrame() 來啟動影格迴圈。

在點擊測試中,我使用 viewer 參考空間,該空間是根據點擊測試時裝置的姿勢而定。在這個情境中,「檢視器」這個標籤有點令人困惑,因為我談論的是控制器。如果將控制器視為電子檢視器,就很容易理解。取得後,我會呼叫 xrSession.requestHitTestSource(),建立繪圖時要使用的點擊測試資料來源。

執行影格迴圈

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 執行個體的陣列。點擊測試可能會找到多個表面。陣列中的第一個元素最靠近相機。在多數情況下,您會使用這個方法,但如果是進階用途,則會傳回陣列。舉例來說,假設攝影機對準地板上桌子上的盒子。點擊測試可能會傳回陣列中的所有三個表面。在大多數情況下,這會是我關心的方塊。如果傳回的陣列長度為 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 屬性設為 true。姿勢代表表面上某個點的姿勢。

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

結論

如要掌握這項功能,最好的方法是逐步查看程式碼範例,或試用程式碼研究室。希望我提供的背景資訊,能幫助您理解這兩者。

我們尚未完成沉浸式網頁 API 的建構作業,還有很長的路要走。如有進展,我們會在這裡發布新文章。