虛擬實境可說是網路,第 2 部分

關於影格迴圈的所有資訊

Joe Medley
Joe Medley

我最近發布了網路遊行,一篇文章介紹 WebXR Device API 背後的基本概念。我還提供要求、進入及結束 XR 工作階段的操作說明。

本文說明影格迴圈,這是一個由使用者代理程式控制的無限迴圈,內容會重複繪製至螢幕畫面。內容會以稱為影格的獨立區塊繪製。畫格的連續顯示會產生移動的錯覺。

本文不適用於以下情況:

WebGL 和 WebGL2 是 WebXR 應用程式在影格迴圈期間算繪內容的唯一方式。幸運的是,許多架構在 WebGL 和 WebGL2 之上提供一層抽象層。這類架構包含 three.jsbabylonjsPlayCanvas,而 A-FrameReact 360 則是專為與 WebXR 互動而設計。

本文並非 WebGL 或架構教學課程。本文件會說明使用沉浸式 Web 工作群組沉浸式 VR 工作階段範例 (demosource) 的框架迴圈基本概念。如果您想深入瞭解 WebGL 或其中一個架構,網際網路會提供一系列不斷增長的文章清單。

玩家和遊戲

在嘗試瞭解影格迴圈時,我一直無法掌握細節。有許多物件正在運作,其中部分物件只以其他物件的參照屬性命名。為了方便您保持直觀,我會說明物件 也就是「玩家」然後描述它們的互動方式,也就是所謂的「遊戲」

玩家

XRViewerPose

姿勢是指 3D 空間中某物體的位置和方向。觀眾和輸入裝置都有一個姿勢,但我們擔心這個情況。檢視者和輸入裝置姿勢都含有 transform 屬性,說明其位置為向量,方向為相對於起點的四元數。呼叫 XRSession.requestReferenceSpace() 時,系統會根據要求的參照空間類型指定來源。

參照空間需要花點時間說明。我們會在擴增實境中詳細說明這些概念。我用於本文章的範例使用 'local' 參考空間,也就是說,起點位於建立工作階段時的觀看者位置,沒有明確的底部,其精確位置可能因平台而異。

XRView

檢視畫面對應於觀看虛擬場景的相機。檢視畫面也具有 transform 屬性,可說明其位置為向量,以及其方向。這些值會以向量/四元數組合和等效矩陣的形式提供,您可以根據程式碼的最佳需求使用任一表示法。每個檢視畫面都對應至裝置用來向觀眾呈現圖像的螢幕或螢幕部分。XRView 物件會以 XRViewerPose 物件的陣列形式傳回。陣列中的觀看次數會有所不同。在行動裝置上,AR 情境有一個檢視畫面,可能會或不一定會覆蓋裝置螢幕。頭戴式裝置通常有兩個檢視畫面,每個眼睛各一個。

XRWebGLLayer

圖層提供點陣圖圖片來源,並說明這些圖片在裝置中如何算繪。這項說明無法完整呈現這個播放器的功能。我認為它是裝置和 WebGLRenderingContext 之間的中介。MDN 也持有類似的觀點,指出它「提供兩者之間的連結」。因此可讓您存取其他玩家。

一般來說,WebGL 物件會儲存 2D 和 3D 圖形的狀態資訊。

WebGLFramebuffer

Framebuffer 會將圖片資料提供給 WebGLRenderingContext。從 XRWebGLLayer 擷取後,您只需將其傳遞至目前的 WebGLRenderingContext。除了呼叫 bindFramebuffer() (稍後會進一步說明) 之外,您絕不會直接存取這個物件。您只需將其從 XRWebGLLayer 傳遞至 WebGLRenderingContext。

XRViewport

可視區域會提供 WebGLFramebuffer 中矩形區域的座標和尺寸。

WebGLRenderingContext

轉譯背景資訊是畫布 (我們繪製的空間) 的程式輔助存取點。為此,您需要同時使用 WebGLFramebuffer 和 XRViewport。

請注意 XRWebGLLayerWebGLRenderingContext 之間的關係。一個對應觀眾的裝置,另一個對應網頁。WebGLFramebufferXRViewport 會從先前的內容傳遞至後者。

XRWebGLLayer 和 WebGLRenderingContext 之間的關係
XRWebGLLayerWebGLRenderingContext 的關係

遊戲

既然我們知道玩家是誰,現在就來看看他們玩的遊戲。這是一款在每個影格開始重複的遊戲。請注意,影格是影格迴圈的一部分,其發生的速率取決於基礎硬體。對於 VR 應用程式,每秒影格數可介於 60 到 144 之間。AR for Android 的執行速度為每秒 30 張影格。程式碼不應假設任何特定的幀率。

影格迴圈的基本流程如下所示:

  1. 呼叫 XRSession.requestAnimationFrame()。在回應中,使用者代理程式會叫用您定義的 XRFrameRequestCallback
  2. 在回呼函式中:
    1. 再次呼叫 XRSession.requestAnimationFrame()
    2. 取得檢視器的姿勢。
    3. WebGLFramebufferXRWebGLLayer 傳遞 (「繫結」) 至 WebGLRenderingContext
    4. 針對每個 XRView 物件執行疊代,從 XRWebGLLayer 擷取其 XRViewport,並將其傳遞至 WebGLRenderingContext
    5. 將內容繪製至 Framebuffer。

由於步驟 1 和 2a 已在上一篇文章中介紹,因此我會從步驟 2b 開始。

取得觀眾的姿勢

這點應該不必多說。如要在 AR 或 VR 中繪製任何內容,我必須知道觀看者的位置和他們正在觀看的位置。觀看者的方向和位置由 XRViewerPose 物件提供。我會在目前的動畫影格上呼叫 XRFrame.getViewerPose(),藉此取得觀看者的姿勢。我會將在設定工作階段時取得的參照空間傳遞給它。這個物件傳回的值一律會相對於我進入目前工作階段時要求的參照空間。您可能還記得,我必須在要求姿勢時傳遞目前的參考空間。

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
  if (xrViewerPose) {
    // Render based on the pose.
  }
}

觀眾姿勢代表使用者的整體位置,也就是智慧型手機的觀眾頭部或手機相機。姿勢會告訴應用程式觀看者的位置。實際圖像算繪會使用 XRView 物件,稍後我會介紹這項技術。

在繼續之前,我會先測試系統是否會傳回觀眾姿勢,以防系統失去追蹤方向或基於隱私權考量而封鎖姿勢。追蹤是 XR 裝置能夠瞭解其位置和/或輸入裝置與環境相對的能力。追蹤資料可能會因多種原因而遺失,具體原因取決於追蹤方法。舉例來說,如果耳機或手機上的攝影機是用於追蹤裝置,可能會導致該裝置無法判斷裝置在低光源或無光源環境,或鏡頭是否在遮蔽物上。

舉例來說,如果頭戴式裝置顯示安全性對話方塊 (例如權限提示),瀏覽器可能會在這種情況下停止向應用程式提供姿勢。不過,我已呼叫 XRSession.requestAnimationFrame(),因此如果系統能夠復原,影格迴圈就會繼續執行。如果不是,使用者代理程式會結束工作階段,並呼叫 end 事件處理常式。

繞路一小段

下一個步驟需要在會話設定期間建立的物件。請回想,我建立了畫布並指示畫布建立與 XR 相容的 WebGL 算繪情境,而我取得這項情境的方式是呼叫 canvas.getContext()。所有繪圖作業都會使用 WebGL API、WebGL2 API 或以 WebGL 為基礎的架構 (例如 Three.js) 完成。這個結構定義與 XRWebGLLayer 的新例項一併透過 updateRenderState() 傳遞至工作階段物件。

let canvas = document.createElement('canvas');
// The rendering context must be based on WebGL or WebGL2
let webGLRenContext = canvas.getContext('webgl', { xrCompatible: true });
xrSession.updateRenderState({
    baseLayer: new XRWebGLLayer(xrSession, webGLRenContext)
  });

傳遞 ('bind') WebGLFramebuffer

XRWebGLLayer 會為 WebGLRenderingContext 提供框架緩衝區,這項功能專門用於與 WebXR 搭配使用,並取代算繪環境的預設框架緩衝區。這在 WebGL 的語言稱為「繫結」。

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
  if (xrViewerPose) {
    let glLayer = xrSession.renderState.baseLayer;
    webGLRenContext.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
    // Iterate over the views
  }
}

對每個 XRView 物件進行疊代

取得姿勢並繫結 framebuffer 後,即可取得視區範圍。XRViewerPose 包含 XRView 介面的陣列,每個介面代表一個螢幕或部分畫面。這些屬性包含了必要資訊,可為裝置和觀看者正確顯示內容,例如視野範圍、眼睛偏移和其他光學屬性。由於我要為兩隻眼睛繪製圖形,因此我有兩個檢視畫面,我會循環處理並為每個檢視畫面繪製個別圖片。

實作手機式擴增實境時,我只會有一個檢視畫面,但仍會使用迴圈。雖然重複顯示單一檢視畫面似乎毫無意義,但這麼做可讓您為各種沉浸式體驗提供單一轉譯路徑。這是 WebXR 與其他沉浸式系統的重要差異。

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
  if (xrViewerPose) {
    let glLayer = xrSession.renderState.baseLayer;
    webGLRenContext.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
    for (let xrView of xrViewerPose.views) {
      // Pass viewports to the context
    }
  }
}

將 XRViewport 物件傳遞至 WebGLRenderingContext

XRView 物件是指螢幕上可觀測的內容。不過,為了在該檢視畫面中繪製圖形,我需要裝置專屬的座標和尺寸。如同 framebuffer,我會從 XRWebGLLayer 要求這些內容,並將其傳遞至 WebGLRenderingContext

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);
  if (xrViewerPose) {
    let glLayer = xrSession.renderState.baseLayer;
    webGLRenContext.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
    for (let xrView of xrViewerPose.views) {
      let viewport = glLayer.getViewport(xrView);
      webGLRenContext.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
      // Draw something to the framebuffer
    }
  }
}

webGLRenContext

撰寫本文時,我與幾位同事對 webGLRenContext 物件的命名有辯論。範例指令碼和大多數 WebXR 程式碼都會呼叫這個變數 gl。在瞭解範例時,我一直忘記 gl 指的是什麼。我將其命名為 webGLRenContext,提醒您在學習過程中,這是 WebGLRenderingContext 的例項。

原因是使用 gl 可讓方法名稱看起來像 OpenGL ES 2.0 API 中的對應項目,用於在編譯語言中建立 VR。如果您曾使用 OpenGL 編寫 VR 應用程式,這一點就很明顯,但如果您完全不熟悉這項技術,就會感到困惑。

在 framebuffer 中繪製

如果您有雄心壯志,可以直接使用 WebGL,但我不建議這麼做。使用頂端所列的架構會更加簡單。

結論

這不是 WebXR 更新或文章的結束。您可以在 MDN 中找到 所有 WebXR 介面和成員的參考資料。如要瞭解介面本身的近期增強功能,請在 Chrome 狀態中追蹤個別功能。

相片來源:JESHOOTS.COM 發布於 Unsplash