啟動和關閉 WebXR 會話

假設你已經熟悉 3D 圖形(尤其是 WebGL),那麼邁出混合現實的下一步(即在現實世界中或取代現實世界呈現人造場景或物體)並不太複雜。在開始渲染增強或虛擬現實場景之前,你需要建立並設定 WebXR 會話,並且也應該知道如何正確關閉它。本文將教你如何完成這些操作。

訪問 WebXR API

你的應用訪問 WebXR API 是從 XRSystem 物件開始的。此物件代表透過使用者裝置上的硬體和驅動程式可用的整個 WebXR 裝置套件。你的文件可以透過 Navigator 屬性 xr 使用全域性 XRSystem 物件,如果根據可用硬體和文件環境有合適的 XR 硬體可用,該屬性將返回 XRSystem 物件。

因此,獲取 XRSystem 物件的最簡單程式碼是

js
const xr = navigator.xr;

如果 WebXR 不可用,xr 的值將為 nullundefined

WebXR 可用性

作為一種新的且仍在開發中的 API,WebXR 的支援僅限於特定的裝置和瀏覽器;即使在這些裝置和瀏覽器上,它也可能預設未啟用。但是,即使你沒有相容的系統,也可能有可用的選項允許你試驗 WebXR。

WebXR Polyfill

WebXR 規範的設計團隊釋出了一個 WebXR polyfill,你可以用它來在不支援 WebXR API 的瀏覽器上模擬 WebXR。如果瀏覽器支援舊的 WebVR API,則使用它。否則,polyfill 會回退到使用 Google Cardboard VR API 的實現。

polyfill 與規範同步維護,並與規範保持同步。此外,它還會更新以保持與瀏覽器的相容性,因為它們對 WebXR 以及與 WebXR 和 polyfill 實現相關的其他技術的支援會隨著時間的推移而改變。

請務必仔細閱讀自述檔案;polyfill 有幾個版本,具體取決於你的目標瀏覽器包含新 JavaScript 功能的程度。

模擬器使用

雖然與使用實際頭戴裝置相比有些笨拙,但這使得在桌面計算機上試驗和開發 WebXR 程式碼成為可能,而 WebXR 通常在桌面計算機上不可用。它還允許你在將程式碼部署到真實裝置之前執行一些基本測試。但請注意,模擬器尚未完全模擬所有 WebXR API,因此你可能會遇到意想不到的問題。同樣,在開始之前請仔細閱讀自述檔案,並確保你瞭解其侷限性。

重要提示:在釋出或交付產品之前,你始終應該在實際的 AR 和/或 VR 硬體上測試你的程式碼!模擬、模擬或 polyfill 環境不能充分替代在物理裝置上的實際測試。

獲取擴充套件

請從下方下載你的受支援瀏覽器的 WebXR API 模擬器

擴充套件的原始碼 也可在 GitHub 上獲取。

模擬器問題和注意事項

雖然這不是一篇關於該擴充套件的完整文章,但有一些具體事項值得一提。

該擴充套件的 0.4.0 版本於 2020 年 3 月 26 日釋出。它透過 WebXR AR Module 引入了對增強現實 (AR) 的支援,該模組正接近穩定狀態。AR 的文件很快將在 MDN 上釋出。

其他改進包括更新模擬器以將 XR 介面重新命名為 XRSystem,引入對擠壓(握持)輸入源的支援,並新增對 XRInputSource 屬性 profiles 的支援。

上下文要求

WebXR 相容環境始於安全載入的文件。你的文件需要從本地驅動器載入(例如使用 https:///… 等 URL),或者在載入頁面時使用 HTTPS。JavaScript 程式碼也必須安全載入。

如果文件未安全載入,你將無法走得很遠。navigator.xr 屬性甚至在文件未安全載入時也不存在。如果不存在相容的 XR 硬體,也可能是這種情況。無論哪種情況,你都需要為缺少 xr 屬性做好準備,並優雅地處理錯誤或提供某種形式的備用方案。

回退到 WebXR polyfill

一種備用選項是 WebXR polyfill,由負責 WebXR 標準化過程的 沉浸式 Web 工作組 提供。polyfill 為沒有原生 WebXR 支援的瀏覽器帶來了 WebXR 支援,並解決了支援 WebXR 的瀏覽器之間實現不一致的問題,因此即使原生 WebXR 可用,它有時也可能有用。

在這裡,我們定義了一個 getXR() 函式,它在可選安裝 polyfill 後返回 XRSystem 物件,假設 polyfill 已透過之前的 <script> 標籤包含或載入。

js
let webxrPolyfill = null;

function getXR(usePolyfill) {
  let tempXR;

  switch (usePolyfill) {
    case "if-needed":
      tempXR = navigator.xr;
      if (!tempXR) {
        webxrPolyfill = new WebXRPolyfill();
        tempXR = webxrPolyfill;
      }
      break;
    case "yes":
      webxrPolyfill = new WebXRPolyfill();
      tempXR = webxrPolyfill;
      break;
    case "no":
    default:
      tempXR = navigator.xr;
      break;
  }

  return tempXR;
}

const nativeXr = getXR("no"); // Get the native XRSystem object
const polyfilledXr = getXR("yes"); // Always returns an XRSystem from the polyfill
const xr = getXR("if-needed"); // Use the polyfill only if navigator.xr missing

然後可以根據 MDN 上提供的文件使用返回的 XRSystem 物件。全域性變數 webxrPolyfill 僅用於保留對 polyfill 的引用,以確保它在不再需要之前保持可用。將其設定為 null 表示當不再有依賴它的物件使用它時,可以對其進行垃圾回收。

當然,你可以根據需要簡化它;由於你的應用程式可能不會在是否使用 polyfill 之間頻繁切換,你可以將其簡化為只針對你需要的特定情況。

許可權和安全

WebXR 圍繞著許多安全措施。其中最重要的一點是,使用 immersive-vr 模式(它完全取代了使用者對世界的看法)要求 xr-spatial-tracking 許可權策略 到位。除此之外,文件需要是安全的並且當前處於焦點狀態。最後,你必須從使用者事件處理程式(例如 click 事件的處理程式)中呼叫 requestSession()

有關保護 WebXR 活動和使用的更多具體資訊,請參閱文章 WebXR 的許可權和安全

確認所需的會話型別可用

在嘗試建立新的 WebXR 會話之前,通常明智的做法是首先檢查使用者的硬體和軟體是否支援你希望使用的演示模式。例如,這也可以用於確定是使用沉浸式還是內聯演示。

要查明是否支援給定模式,請呼叫 XRSystem 方法 isSessionSupported()。它返回一個 promise,如果給定型別的會話可用,則解析為 true,否則解析為 false

js
const immersiveOK = await navigator.xr.isSessionSupported("immersive-vr");
if (immersiveOK) {
  // Create and use an immersive VR session
} else {
  // Create an inline session instead, or tell the user about the
  // incompatibility if inline is required
}

建立並啟動會話

WebXR 會話由一個 XRSession 物件表示。要獲取 XRSession,你可以呼叫你的 XRSystemrequestSession() 方法,該方法返回一個 promise,如果能夠成功建立會話,則解析為 XRSession。從根本上說,它看起來像這樣

js
xr.requestSession("immersive-vr").then((session) => {
  xrSession = session;
  /* continue to set up the session */
});

請注意此程式碼片段中傳遞給 requestSession() 的引數:immersive-vr。此字串指定了你想要建立的 WebXR 會話型別——在本例中,是完全沉浸式的虛擬現實體驗。有三個選項

immersive-vr

使用頭戴裝置或類似裝置進行完全沉浸式虛擬現實會話,該裝置將使用者周圍的世界完全替換為你呈現的影像。

immersive-ar

一種增強現實會話,其中使用頭戴裝置或類似裝置將影像新增到現實世界中。此選項尚未廣泛支援,因為 AR 規範仍在變化。

inline

在文件視窗的上下文中顯示 XR 影像。

如果由於某種原因無法建立會話(例如功能策略不允許使用或使用者拒絕授予使用頭戴裝置的許可權),則 promise 會被拒絕。因此,一個更完整的啟動並返回 WebXR 會話的函式可能如下所示

js
async function createImmersiveSession(xr) {
  session = await xr.requestSession("immersive-vr");
  return session;
}

此函式返回新的 XRSession,或者在建立會話時發生錯誤時丟擲異常。

自定義會話

除了顯示模式,requestSession() 方法還可以接受一個可選的帶有初始化引數的物件,用於自定義會話。目前,會話唯一可配置的方面是應該使用哪個參考空間來表示世界座標系。你可以指定所需或可選的參考空間,以獲取與你所需或偏好使用的參考空間相容的會話。

例如,如果你需要一個 unbounded 參考空間,你可以將其指定為必需功能,以確保你獲得的會話可以使用無界空間

js
async function createImmersiveSession(xr) {
  session = await xr.requestSession("immersive-vr", {
    requiredFeatures: ["unbounded"],
  });
  return session;
}

另一方面,如果你需要一個內聯會話並且更喜歡 local 參考空間,你可以這樣做

js
async function createInlineSession(xr) {
  session = await xr.requestSession("inline", {
    optionalFeatures: ["local"],
  });
  return session;
}

createInlineSession() 函式將嘗試建立一個與 local 參考空間相容的內聯會話。當你準備建立參考空間時,可以嘗試區域性空間,如果失敗,則回退到 viewer 參考空間,所有裝置都要求支援該空間。

準備新會話以供使用

一旦 requestSession() 方法返回的 promise 成功解析,你就知道你手上有一個可用的 WebXR 會話。然後你可以繼續準備會話以供使用並開始你的動畫。

為了完成會話配置,你需要(或可能需要)執行的關鍵事項包括

  • 新增你需要監視的事件處理程式。這最可能至少包括 end,以便你可以檢測會話何時結束。
  • 如果你使用 XR 輸入控制器,請監視 inputsourceschange 事件以檢測 XR 輸入控制器的新增或移除,以及各種 選擇和擠壓動作事件
  • 你可能希望監視 XRSystem 事件 devicechange,以便在可用沉浸式裝置集發生變化時收到通知。
  • 透過在目標上下文上呼叫 HTMLCanvasElement 方法 getContext() 來獲取你打算渲染幀的 canvas 的 WebGL 上下文。
  • 設定你的 WebGL 資料和模型,並準備渲染場景。
  • 透過建立 XRWebGLLayer 並設定會話 renderState 屬性 baseLayer 的值,將 WebGL 上下文設定為 XR 系統的源。
  • 根據需要計算物件的初始位置和比例。
  • 開始幀渲染週期

以基本形式,完成此最終設定的程式碼可能如下所示

js
async function runSession(session) {
  session.addEventListener("end", onSessionEnd);

  const canvas = document.querySelector("canvas");
  const gl = canvas.getContext("webgl", { xrCompatible: true });

  // Set up WebGL data and such

  const worldData = loadGLPrograms(session, "world-data.xml");
  if (!worldData) {
    return null;
  }

  // Finish configuring WebGL

  worldData.session.updateRenderState({
    baseLayer: new XRWebGLLayer(worldData.session, gl),
  });

  // Start rendering the scene

  referenceSpace = await worldData.session.requestReferenceSpace("unbounded");
  worldData.referenceSpace = referenceSpace.getOffsetReferenceSpace(
    new XRRigidTransform(
      worldData.playerSpawnPosition,
      worldData.playerSpawnOrientation,
    ),
  );
  worldData.animationFrameRequestID =
    worldData.session.requestAnimationFrame(onDrawFrame);

  return worldData;
}

為了此示例的目的,建立了一個名為 worldData 的物件來封裝關於世界和渲染環境的資料。這包括 XRSession 本身、用於在 WebGL 中渲染場景的所有資料、世界參考空間以及由 requestAnimationFrame() 返回的 ID。

首先,設定 end 事件的處理程式。然後獲取渲染 canvas 並檢索對其 WebGL 上下文的引用,在呼叫 getContext() 時指定 xrCompatible 選項。

接下來,在執行 WebGL 渲染器所需的任何資料和設定之後,然後將 WebGL 配置為使用 WebGL 上下文的幀緩衝區作為其自身的幀緩衝區。這是透過使用 XRSession 方法 updateRenderState() 將渲染狀態的 baseLayer 設定為新建立的封裝 WebGL 上下文的 XRWebGLLayer 來完成的。

準備渲染場景

此時,XRSession 本身已完全配置,因此我們可以開始渲染。首先,我們需要一個參考空間,在該空間中宣告世界的座標。我們可以透過呼叫 XRSessionrequestReferenceSpace() 方法來獲取會話的初始參考空間。在呼叫 requestReferenceSpace() 時,我們指定我們想要的參考空間的型別名稱;在本例中是 unbounded。你可以根據需要輕鬆指定 localviewer

注意:要了解如何根據你的需求選擇正確的參考空間,請參閱 選擇參考空間型別

requestReferenceSpace() 返回的參考空間將原點 (0, 0, 0) 放置在空間的中心。這很好——如果你的玩家的視角從世界的正中心開始。但很可能並非如此。如果是這樣,你可以在初始參考空間上呼叫 getOffsetReferenceSpace() 來建立一個新的參考空間,該空間抵消了座標系,以便 (0, 0, 0) 位於觀看者的位置,方向也相應地移動以面向所需方向。getOffsetReferenceSpace() 的輸入值是一個 XRRigidTransform,它封裝了玩家在預設世界座標中指定的位置和方向。

手頭有新的參考空間並存儲在 worldData 物件中以供安全儲存後,我們呼叫會話的 requestAnimationFrame() 方法來安排一個回撥,該回調將在 WebXR 會話的下一幀動畫渲染時執行。返回的值是一個 ID,我們可以稍後使用它來取消請求(如果需要),所以我們也將其儲存到 worldData 中。

最後,worldData 物件返回給呼叫者,允許主程式碼稍後引用它需要的資料。此時,設定過程完成,我們已進入應用程式的渲染階段。要了解有關渲染的更多資訊,請參閱文章 渲染和 WebXR 幀動畫回撥

關於操作細節

顯然,這只是一個例子。你不需要一個 worldData 物件來儲存所有內容;你可以以任何你想要的方式維護你需要的資訊。你可能需要不同的資訊,或者有不同的特定要求,導致你以不同的方式或以不同的順序做事。

同樣,你用於載入模型和其他資訊以及設定 WebGL 資料(紋理、頂點緩衝區、著色器等)的具體方法將根據你的需求、你正在使用的任何框架等而有很大差異。

重要的會話維護事件

在 WebXR 會話期間,你可能會收到多個事件,這些事件指示會話狀態的變化,或者讓你瞭解為了使會話正常執行你需要做的事情。

檢測會話可見性狀態的變化

XRSession 的可見性狀態發生變化時——例如會話被隱藏或顯示,或者使用者已將焦點切換到另一個上下文——會話會收到一個 visibilitychange 事件。

js
session.onvisibilitychange = (event) => {
  switch (event.session.visibilityState) {
    case "hidden":
      myFrameRate = 10;
      break;
    case "blurred-visible":
      myFrameRate = 30;
      break;
    case "visible":
    default:
      myFrameRate = 60;
      break;
  }
};

此示例根據可見性狀態的變化來更改變數 myFrameRate。渲染器大概使用此值來計算動畫迴圈進行時渲染新幀的頻率,從而使場景越“模糊”,渲染頻率越低。

檢測參考空間重置

在追蹤使用者在世界中的位置時,有時可能會出現 原生原點 的不連續或跳躍。最常見的情況是使用者請求重新校準其 XR 裝置,或者從 XR 硬體接收的追蹤資料流中出現中斷或故障。這些情況會導致原生原點突然跳躍,跳躍距離和方向角是使原生原點與使用者位置和朝向方向重新對齊所需的。

當發生這種情況時,一個 reset 事件會發送到會話的 XRReferenceSpace。事件的 transform 屬性是一個 XRRigidTransform,詳細說明了重新對齊原生原點所需的變換。

注意:reset 事件是在 XRReferenceSpace 而不是 XRSession 上觸發的!

reset 事件的另一個常見原因是當邊界參考空間(bounded-floor)的幾何形狀(由 XRBoundedReferenceSpace 的屬性 boundsGeometry 指定)發生變化時。

有關參考空間重置的更多常見原因以及更多詳細資訊和示例程式碼,請參閱 reset 事件的文件。

檢測可用 WebXR 輸入控制元件集何時更改

WebXR 維護一個特定於 WebXR 系統的輸入控制元件列表。這些裝置包括手持控制器、運動感應攝像頭、運動感應手套和其他反饋裝置。當用戶連線或斷開 WebXR 控制器裝置時,inputsourceschange 事件會分派給 XRSession。這是一個機會,可以通知使用者裝置的可用性,開始監視它以獲取輸入,提供配置選項,或者你可能需要對其執行的任何操作。

結束 WebXR 會話

當用戶的 VR 或 AR 會話接近尾聲時,會話結束。XRSession 的關閉可能由於會話本身決定是時候關閉(例如使用者關閉其 XR 裝置),或者使用者點選按鈕結束會話,或者根據你的應用程式的需要發生其他情況。

這裡我們討論如何請求關閉 WebXR 會話以及如何檢測會話何時結束,無論是透過你的請求還是其他方式。

關閉會話

為了在你完成 WebXR 會話後 cleanly 關閉它,你應該呼叫會話的 end() 方法。這會返回一個 promise,你可以用它來知道關閉何時完成。

js
async function shutdownXR(session) {
  if (session) {
    await session.end();

    /* At this point, WebXR is fully shut down */
  }
}

shutdownXR() 返回給其呼叫者時,WebXR 會話已完全安全關閉。

如果會話結束時必須完成工作,例如釋放資源等,你應該在你的 end 事件處理程式中完成該工作,而不是在主程式碼體中。這樣,無論關閉是自動觸發還是手動觸發,你都將處理清理。

檢測會話何時結束

如前所述,你可以透過監視 XRSession 接收到的 end 事件來檢測 WebXR 會話何時結束——無論是你呼叫了其 end() 方法,使用者關閉了他們的頭戴裝置,還是 XR 系統中發生了某種無法解決的錯誤。

js
session.onend = (event) => {
  /* the session has shut down */

  freeResources();
};

在這裡,當會話結束並收到 end 事件時,會呼叫 freeResources() 函式來釋放之前為處理 XR 展示而分配和/或載入的資源。透過在 end 事件處理程式中呼叫 freeResources(),我們可以在使用者點選觸發關閉的按鈕(例如透過呼叫上面顯示的 shutdownXR() 函式)時以及會話自動結束(無論是由於錯誤還是其他原因)時呼叫它。

另見