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

您可以使用 Hit Test API,在真實世界檢視畫面中放置虛擬項目。

Joe Medley
Joe Medley

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

本文將介紹 WebXR Hit Test API,說明如何在真實世界的攝影機畫面中放置虛擬物件。

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

沉浸式 AR 工作階段範例

本文中的程式碼是以 Immersive Web 工作群組的 Hit Test 範例為基礎,但並非完全相同 (示範來源)。這個範例可讓您在現實世界的表面上放置虛擬向日葵。

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

這個圓圈稱為「十字線」,是暫時性的圖像,可協助在擴增實境中放置物件。輕觸螢幕後,無論輕觸螢幕的哪個位置,系統都會在十字線位置的表面上放置向日葵,並以十字線點的方向為準。十字線會繼續隨著裝置移動。

根據情境在牆上顯示十字線,可選擇寬鬆或嚴格模式
十字線是暫時性圖像,可協助在擴增實境中放置物件。

建立十字線

由於瀏覽器或 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 中繪製任何內容,我需要知道檢視者的位置和視線方向。因此我測試 hitTestSourcexrViewerPose 是否仍然有效。

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

結論

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

我們還在建構沉浸式 Web API,而且還有很長的路要走。我們會在這裡發布新文章,說明最新進展。

相片來源:Daniel Frank 發表於 Unsplash 網站上