影格迴圈簡介
最近我發布了「虛擬實境來到網頁」一文,介紹 WebXR 裝置 API 背後的基礎概念。我也提供要求、輸入及結束 XR 會話的操作說明。
本文說明影格迴圈,這是使用者代理程式控制的無限迴圈,可重複將內容繪製到畫面上。內容會繪製在稱為影格的獨立區塊中。連續的影格會產生動作的錯覺。
本文不適用於:
在 WebXR 應用程式的影格迴圈中,只能透過 WebGL 和 WebGL2 算繪內容。幸好,許多架構都在 WebGL 和 WebGL2 的頂端提供抽象層。這類架構包括 three.js、babylonjs 和 PlayCanvas,而 A-Frame 和 React 360 則專為與 WebXR 互動而設計。
本文將說明影格迴圈的基本概念,並使用 Immersive Web Working Group 的 Immersive VR Session 範例 (示範、來源)。如要深入瞭解 WebGL 或其中一個架構,網路上有越來越多的資源可供參考。
球員和比賽
嘗試瞭解影格迴圈時,我一直迷失在細節中。 遊戲中有很多物件,其中有些物件只會透過其他物件的參照屬性命名。為了方便說明,我會描述這些物件,並稱之為「玩家」。接著我會說明這些角色如何互動, 也就是我所謂的「遊戲」。
球員
XRViewerPose
姿態是指某個物體在 3D 空間中的位置和方向。檢視器和輸入裝置都有姿勢,但我們這裡關注的是檢視器的姿勢。檢視器和輸入裝置的姿勢都有 transform 屬性,可描述其位置 (以向量表示) 和方向 (以四元數表示,相對於原點)。呼叫 XRSession.requestReferenceSpace() 時,系統會根據要求的參考空間類型指定原點。
參考空間需要一些時間才能說明清楚。我會在「擴增實境」中深入探討這些主題。我用來撰寫本文的範例是以 'local' 參照空間為基礎,這表示原點位於工作階段建立時的檢視者位置,且沒有明確定義的地板,因此確切位置可能會因平台而異。
XRView
檢視畫面會對應到攝影機觀看的虛擬場景。檢視區塊也有 transform 屬性,可說明檢視區塊的位置 (以向量表示) 和方向。這些資料會以向量/四元組和對等矩陣的形式提供,您可以根據最適合程式碼的形式使用。每個檢視區塊都對應到裝置用來向觀眾呈現圖像的螢幕或部分螢幕。XRView 物件會以陣列形式從 XRViewerPose 物件傳回。陣列中的檢視畫面數量不一。在行動裝置上,AR 場景只有一個檢視畫面,可能涵蓋裝置螢幕,也可能不會。頭戴式裝置通常有兩個畫面,分別對應左右眼。
XRWebGLLayer
圖層提供點陣圖來源,以及這些圖片在裝置中的算繪方式說明。這項說明無法充分呈現這個播放器的功能。我認為這是裝置和 WebGLRenderingContext 之間的仲介者。MDN 的看法也大致相同,指出這兩者「提供連結」。因此,其他玩家可以存取該檔案。
一般來說,WebGL 物件會儲存狀態資訊,用於算繪 2D 和 3D 圖像。
WebGLFramebuffer
訊框緩衝區會將圖片資料提供給 WebGLRenderingContext。從 XRWebGLLayer 擷取後,您會將其傳遞至目前的 WebGLRenderingContext。除了呼叫 bindFramebuffer() (稍後會詳細說明),您永遠不會直接存取這個物件。您只需要將其從 XRWebGLLayer 傳遞至 WebGLRenderingContext 即可。
XRViewport
可視區域提供 WebGLFramebuffer 中矩形區域的座標和尺寸。
WebGLRenderingContext
算繪環境是畫布 (我們繪製的空間) 的程式輔助存取點。為此,它需要 WebGLFramebuffer 和 XRViewport。
請注意 XRWebGLLayer 和 WebGLRenderingContext 之間的關係。一個對應檢視者的裝置,另一個則對應網頁。WebGLFramebuffer 和 XRViewport 會從前者傳遞至後者。
XRWebGLLayer 和 WebGLRenderingContext 之間的關係
遊戲
現在我們知道玩家是誰了,接著來看看他們玩的遊戲。這款遊戲會在每個影格重新開始。請注意,影格是影格迴圈的一部分,該迴圈會以取決於基礎硬體的速率發生。VR 應用程式的每秒影格數可介於 60 到 144 之間。Android 適用的 AR 執行速度為每秒 30 個影格。程式碼不應假設任何特定影格率。
影格迴圈的基本流程如下:
- 呼叫
XRSession.requestAnimationFrame()。使用者代理程式會呼叫您定義的XRFrameRequestCallback。 - 在回呼函式中:
- 再次撥打
XRSession.requestAnimationFrame()。 - 取得觀眾的姿勢。
- 將
XRWebGLLayer中的WebGLFramebuffer傳遞 (「繫結」) 至WebGLRenderingContext。 - 疊代每個
XRView物件,從XRWebGLLayer擷取XRViewport,並傳遞至WebGLRenderingContext。 - 將內容繪製到訊框緩衝區。
- 再次撥打
由於前一篇文章已說明步驟 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 相容的 Web GL 算繪情境,而我是透過呼叫 canvas.getContext() 取得這個情境。所有繪圖作業都是使用 WebGL API、WebGL2 API 或 Three.js 等以 WebGL 為基礎的架構完成。這個內容已透過 updateRenderState() 傳遞至工作階段物件,此外還有 XRWebGLLayer 的新例項。
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)
});
傳遞 (「繫結」) WebGLFramebuffer
XRWebGLLayer 會提供專為 WebXR 使用而提供的 WebGLRenderingContext,並取代預設的算繪內容訊框緩衝區。在 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 物件是指畫面上可觀察到的內容。但如要繪製到該檢視區塊,我需要裝置專屬的座標和尺寸。與訊框緩衝區相同,我會從 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 應用程式,這項事實顯而易見,但如果您是這項技術的新手,可能會感到困惑。
將內容繪製到訊框緩衝區
如果想挑戰高難度,可以直接使用 WebGL,但我不建議這麼做。使用頂端列出的其中一個架構會簡單許多。
結論
這並非 WebXR 更新或文章的終點。如需所有 WebXR 介面和成員的參考資料,請前往 MDN 。如要瞭解介面本身的近期強化功能,請在 Chrome 狀態追蹤個別功能。