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

您可以使用命中測試 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 中繪製任何內容,我必須知道觀看者的位置和他們正在觀看的位置。因此,我測試 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);
  }
}

結論

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

因此,我們尚未建構沉浸式網路 API,而不是長遠的景象。我們會在進展順利時發布新文章。

相片來源:Daniel FrankUnsplash 網站上提供