渲染和 WebXR 幀動畫回撥
一旦你的 WebXR 環境已經設定好,並且建立了一個 XRSession 來表示一個正在進行的 XR 環境會話,你需要向 XR 裝置提供場景的幀進行渲染。本文將介紹在渲染迴圈中將 XR 場景幀驅動到裝置的過程,使用 XRSession 獲取表示每一幀的 XRFrame 物件,然後用它來準備幀緩衝區,以便交付給 XR 裝置。
在渲染虛擬環境之前,你需要透過使用 navigator.xr.requestSession() 方法建立一個 XRSession 來建立 WebXR 會話;你還需要將會話與幀緩衝區關聯並執行其他設定任務。這些設定任務在文章 啟動和關閉 WebXR 會話 中有描述。
準備渲染器
一旦 XR 會話設定完畢,WebGL 幀緩衝區連線好,並且 WebGL 填充了渲染場景所需的資料,你就可以設定渲染器開始運行了。這首先要獲取你想要繪製的參考空間,其原點和方向設定在觀察者的起始位置和視角。一旦準備就緒,你就可以請求瀏覽器在下次需要幀緩衝區來渲染你的場景時呼叫你的渲染函式。這是透過呼叫 XRSession 方法 requestAnimationFrame() 來完成的。
啟動渲染器看起來像這樣
let worldRefSpace;
async function runXR(xrSession) {
worldRefSpace = await xrSession.requestReferenceSpace("local");
if (worldRefSpace) {
viewerRefSpace = worldRefSpace.getOffsetReferenceSpace(
new XRRigidTransform(viewerStartPosition, viewerStartOrientation),
);
animationFrameRequestID = xrSession.requestAnimationFrame(myDrawFrame);
}
}
在獲取沉浸式世界的參考空間後,它透過建立一個表示該位置和方向的 XRRigidTransform,然後呼叫 XRReferenceSpace 方法 getOffsetReferenceSpace(),從而建立一個表示觀察者位置和方向的偏移參考空間。
然後透過呼叫 XRSession 方法 requestAnimationFrame() 來安排第一個動畫幀,提供一個回撥函式 myDrawFrame(),其任務是渲染幀。
請注意,這段程式碼沒有迴圈!相反,幀渲染程式碼(在本例中是一個名為 myDrawFrame() 的函式)負責透過再次呼叫 requestAnimationFrame() 來排程繪製下一幀的時間。
重新整理率和幀速率
假設自上次螢幕重新整理以來你已經呼叫了 XRSession 方法 requestAnimationFrame(),那麼瀏覽器將在每次準備重繪你的應用程式或網站視窗時呼叫你的幀渲染回撥。在這種情況下,“重繪”指的是確保螢幕顯示的內容與 DOM 及其內部元素此刻試圖呈現的內容相匹配的過程。
硬體垂直重新整理率
當瀏覽器準備重新整理顯示 WebXR 內容的 <canvas> 時,它會呼叫你的幀渲染回撥,該回調使用指定的時間戳和任何其他相關資料(如模型和紋理)以及應用程式狀態,將場景(在指定時間應顯示的樣子)渲染到 WebGL 後緩衝區。當你的回撥返回時,瀏覽器會將該後緩衝區連同自上次螢幕重新整理以來發生變化的任何其他內容一起傳輸到顯示器或 XR 裝置。
歷史上,顯示器每秒重新整理 60 次。這是由於早期的顯示器利用交流電網的電流波形進行計時,在美國每秒迴圈 60 次(歐洲為 50 次)。這個數字有許多不同的名稱,但它們都等同或幾乎等同:
- 重新整理率
- 垂直重新整理率
- 垂直消隱率 (VBL)
- 垂直同步率
還有其他類似的術語,但無論如何稱呼,應用的測量單位都是赫茲(Hz)。一個每秒重新整理 60 次的顯示器具有 60 Hz 的重新整理率。這意味著它每秒最多可以顯示 60 幀。無論你每秒渲染多少幀超出這個數量,一秒鐘內只有 60 幀能顯示到螢幕上。
但並非所有顯示器都以 60 Hz 執行;如今,高效能顯示器開始使用更高的重新整理率。例如,120 Hz(或每秒 120 幀)的顯示器越來越普遍。瀏覽器總是嘗試以與顯示器相同的速率重新整理,這意味著在某些計算機上,你的回撥函式每秒最多執行 60 次,而在其他計算機上,根據幀率的不同,它可能會每秒呼叫 90 或 120 次甚至更多。
每幀可用的渲染時間
這使得充分利用幀之間可用的時間變得至關重要。如果使用者的裝置使用 60 Hz 顯示器,你的回撥函式每秒將被呼叫最多 60 次,你的目標是盡力確保它不會被呼叫得更少。你透過儘可能多地在主執行緒之外完成工作,並使你的幀渲染回撥儘可能高效來達到這個目的。下圖顯示了時間如何分成 60 Hz 的塊,每個塊至少部分用於渲染場景。
這很重要,因為隨著計算機變得越來越繁忙,它可能無法準確地每幀呼叫你的回撥,並且可能不得不跳過幀。這被稱為**丟幀**。當渲染一幀所需的時間超過幀之間可用時間時,就會發生這種情況,無論是由於渲染延遲還是渲染本身花費的時間超過了可用時間。
在上圖中,幀 3 被丟棄了,因為幀 2 直到幀 3 應該繪製之後才完成渲染。下一幀將繪製幀 4。這是傳遞給你的渲染回撥的時間戳有用的另一個原因。透過基於時間而不是幀號配置場景,你可以確保渲染的幀與預期匹配,而不是落後。
當幀被丟棄時,受影響的顯示區域內容在該幀迴圈中不會發生變化。因此,偶爾的丟幀通常不會特別明顯,但如果它開始頻繁發生——尤其是短時間內連續丟棄多幀——它可能會變得刺耳,甚至使你的顯示器無法使用。
幸運的是,你可以輕鬆計算每幀之間允許使用的時間,即 `1/refreshRate` 秒。也就是說,將 1 除以顯示器的重新整理率。所得值是渲染每幀所需的時間,以便不丟幀。例如,一個 60 Hz 的顯示器有 1/60 秒的時間來渲染一幀,即 0.0166667 秒。如果裝置的重新整理率是 120 Hz,如果你想避免丟幀,你只有 0.00883333 秒的時間來渲染每一幀。
即使硬體實際上是 120 Hz,你仍然可以以每秒 60 次的速度重新整理,並且將其作為目標通常是一個很好的基準。60 FPS 已經超出了大多數人能夠輕易察覺動畫不是一系列快速播放的靜止影像的程度。換句話說,當不確定時,你可以假設顯示器以 60 Hz 重新整理。只要你的程式碼編寫正確,一切都會正常執行。
渲染器效能考量
顯然,每幀你渲染場景的時間非常少。不僅如此,如果你的渲染器本身執行時間超過該時間,不僅可能導致幀被丟棄,而且該時間會完全浪費,阻止其他程式碼在該幀中執行。
不僅如此,如果你的渲染跨越了垂直重新整理邊界,你可能會遇到**撕裂**效應。當顯示硬體開始下一個重新整理週期而上一幀仍在螢幕上繪製時,就會發生撕裂。結果,你會看到螢幕上半部分顯示新幀,而下半部分顯示上一幀甚至可能是更早一幀的組合的視覺效果。
那麼,你的任務就是讓你的程式碼足夠緊湊和輕量,以至於你不會超出可用時間,也不會導致丟幀或過度濫用主執行緒。
出於這些原因,除非你的渲染器相當小巧輕量,工作量不大,否則你應該考慮將所有能解除安裝的工作都解除安裝到 worker 中,這樣你就可以在瀏覽器處理其他事情時計算下一幀。透過在實際需要幀之前準備好計算和資料,你可以使你的網站或應用程式渲染效率更高,從而提高主執行緒效能並普遍改善使用者體驗。
幸運的是,如果你的渲染需求特別繁重,可以使用一些技巧來進一步減少影響並最佳化效能。請參閱 WebXR 效能指南,瞭解可幫助你確保效能達到最佳水平的建議和技巧。
WebXR 幀
你的幀渲染回撥函式接收兩個引數作為輸入:幀對應的時間,以及一個描述該時間場景狀態的 XRFrame 物件。
3D 的光學原理
我們有兩隻眼睛是有原因的:透過兩隻眼睛,每隻眼睛都能從稍微不同的角度看世界。由於它們之間存在一個已知且固定的距離,我們的大腦可以進行基本的幾何和三角學計算,並從這些資訊中找出現實的 3D 本質。我們還利用透視、大小差異,甚至我們對事物通常外觀的理解來找出第三維度的細節。這些因素,以及其他因素,是我們深度知覺的來源。
為了在渲染圖形時建立三維錯覺,我們需要儘可能多地模擬這些因素。我們模擬的越多——以及我們模擬得越準確——我們就越能更好地欺騙人腦,使其以 3D 方式感知我們的影像。XR 的優勢在於,我們不僅可以使用經典的單眼技術模擬 3D 圖形(透視、大小和模擬視差),還可以透過為動畫的每一幀渲染兩次場景(每隻眼睛一次)來模擬雙眼視覺——即使用兩隻眼睛的視覺。
典型人類的瞳距(瞳孔中心之間的距離)在 54 到 74 毫米(0.054 到 0.074 米)之間。所以,如果觀察者的頭部中心位於 `[0.0, 2.0, 0.0]`(在水平空間中心地面以上約兩米),我們首先需要從,比如說,`[-0.032, 2.0, 0.0]`(中心左側 32 毫米)渲染場景,然後再從 `[0.032, 2.0, 0.0]`(中心右側 32 毫米)渲染一次。這樣,我們就將觀察者的眼睛位置放置在平均人類瞳距 64 毫米處。
這個距離(或 XR 系統配置使用的任何瞳距)足以讓我們的思維透過視網膜差異(兩隻視網膜所見差異)和視差效應看到足夠多的差異,從而讓我們的大腦計算物體的距離和深度,從而使我們能夠感知三維空間,儘管我們的視網膜只是二維表面。
下圖對此進行了說明,其中我們看到每隻眼睛如何感知位於觀察者正前方的一個骰子。儘管此圖在某些方面為說明目的誇大了效果,但概念是相同的。每隻眼睛都看到一個區域,其邊界在眼睛前方形成一個弧形。因為每隻眼睛都偏向頭部中心線的一側或另一側,並且每隻眼睛看到的視野大致相同,所以結果是每隻眼睛都看到了前方世界略微不同的部分,並且從略微不同的角度。
左眼從中心偏左一點點看骰子,右眼從中心偏右一點點看。結果,左眼看到物體左側多一點點,右側少一點點,反之亦然。這兩個影像聚焦在視網膜上,產生的訊號透過視神經傳輸到位於枕葉後部的腦部視覺皮層。
大腦接收來自左眼和右眼的訊號,並在觀察者大腦中構建一個單一、統一的 3D 世界影像,這就是所看到的一切。正是由於左眼和右眼所見之間的差異,大腦能夠推斷出關於物體深度、大小等大量資訊。透過將推斷出的深度資訊與其他線索(例如透視、陰影、對這些關係含義的記憶等)結合起來,我們可以對我們周圍的世界瞭解很多。
幀、姿勢、檢視和幀緩衝區
一旦你有一個 XRFrame 表示場景在某一時刻的狀態,你需要確定場景中物體相對於觀察者的位置,以便你可以渲染它們。觀察者相對於參考空間的位置和方向由一個 XRViewerPose 表示,該物件透過呼叫 XRFrame 方法 getViewerPose() 獲取。
XRFrame 不直接跟蹤你世界中物件的位置或方向。相反,它提供了一種將位置和方向轉換為場景座標系的方法,並從 XR 硬體收集觀察者的位置和方向資料,將其轉換為你配置的參考空間,並透過時間戳將其傳遞給你的幀渲染程式碼。你使用該時間戳和你自己的資料來決定如何渲染場景。
在將場景渲染兩次——一次渲染到幀緩衝區的左半部分,一次渲染到右半部分——之後,幀緩衝區被髮送到 XR 硬體,XR 硬體將幀緩衝區的每一半顯示給相應的眼睛。這通常(但並非總是)透過將影像繪製到單個螢幕並使用透鏡將該影像的正確一半傳輸到每隻眼睛來完成。
你可以在 用 WebXR 表示 3D 中瞭解更多關於 WebXR 如何表示 3D 的資訊。
繪製場景
當需要準備幀緩衝區以便瀏覽器可以繪製場景的下一幀時,將呼叫你提供給 requestAnimationFrame() 的函式。它接收兩個輸入:繪製幀的時間和一個 XRFrame 物件,該物件提供你需要渲染幀的場景狀態的詳細資訊。
理想情況下,你希望這段程式碼足夠快,以保持 60 FPS 的幀率,或者儘可能接近這個幀率,同時記住在這個函式中,除了你的程式碼之外,還有更多事情在發生。你需要確保主執行緒每幀執行的時間不超過幀本身的時長。
一個基本的渲染器
在這個版本的 WebXR 渲染回撥中,我們使用一種非常直接的方法,它非常適用於相對簡單的專案。此虛擬碼概述了該過程:
for each view in the pose's views list:
get the WebXR GL layer's viewport
set the WebGL viewport to match
for each object in the scene
bindProgram()
bindVertices()
bindMatrices()
bindUniforms()
bindBuffers()
bindTextures()
drawMyObject()
簡而言之,這種形式的渲染器使用的是**檢視優先順序**。構成 XR 裝置顯示的兩個檢視是背靠背渲染的,其中在渲染其他檢視上的同一組物件之前,所有物件都會在一個檢視上繪製。因此,存在大量的重複工作,因為繪製物件所需的大部分資料最終會在每幀傳送到 GPU 兩次。然而,它簡化了現有 WebGL 程式碼的移植,並且通常足以完成任務,所以我們首先來看這種方法。
請參閱透過以物件優先順序渲染進行最佳化,瞭解另一種方法,該方法在渲染構成該幀場景的下一個物件之前,對每個物件連續渲染兩次,每隻眼睛一次;也就是說,以**物件優先順序**進行渲染。
示例渲染回撥
讓我們看一些遵循此基本模式的實際程式碼。由於在上面的示例中我們將此函式命名為 myDrawFrame(),因此我們在此處將繼續使用該名稱。
let lastFrameTime = 0;
function myDrawFrame(currentFrameTime, frame) {
const session = frame.session;
let viewerPose;
// Schedule the next frame to be painted when the time comes.
animationFrameRequestID = session.requestAnimationFrame(myDrawFrame);
// Get an XRViewerPose representing the position and
// orientation of the viewer. If successful, render the
// frame.
viewerPose = frame.getViewerPose(viewerRefSpace);
if (viewerPose) {
const glLayer = session.renderState.baseLayer;
gl.bindFrameBuffer(gl.FRAMEBUFFER, glLayer.framebuffer);
// Start by erasing the color and depth framebuffers.
gl.clearColor(0, 0, 0, 1.0);
gl.clearDepth(1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Compute the time elapsed since the last frame was rendered.
// Use this value to ensure your animation runs at the exact
// rate you intend.
const deltaTime = currentFrameTime - lastFrameTime;
lastFrameTime = currentFrameTime;
// Now call the scene rendering code once for each of
// the session's views.
for (const view of viewerPose.views) {
const viewport = glLayer.getViewport(view);
gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
myDrawSceneIntoView(view, deltaTime);
}
}
}
myDrawFrame() 函式從 frame 引數指定的 XRFrame 物件中獲取 XRSession,然後呼叫會話的 requestAnimationFrame() 方法,立即安排下一幀的渲染。這確保我們立即進入佇列,從而允許 myDrawFrame() 函式的這次迭代中剩餘的時間計入繪製下一幀的時間。
然後,我們使用幀的 getViewerPose() 方法獲取描述觀察者姿勢(他們的位置和方向)的 XRViewerPose 物件,傳入之前在設定 WebXR 會話時獲得的 viewerRefSpace 中的觀察者參考空間。
有了觀察者的姿態,我們就可以開始渲染幀了。第一步是獲取 WebXR 裝置希望繪製幀的幀緩衝區的訪問許可權;這是透過從會話的 renderState 物件的 baseLayer 屬性中獲取目標 WebGL 層,然後從該 XRWebGLLayer 物件中獲取 framebuffer 來完成的。然後我們呼叫 gl.bindFrameBuffer() 將該幀緩衝區繫結為所有即將進行的繪圖命令的目標。
下一步是擦除幀緩衝區。儘管理論上你可以跳過這一步——*僅當你的渲染程式碼保證寫入幀緩衝區中的每個畫素時*——但通常最安全的方法是在開始繪製之前清除它,除非你需要榨取每一盎司的效能並且知道無論如何都會觸及所有畫素。背景顏色使用 gl.clearColor() 設定為完全不透明的黑色;透過呼叫 gl.clearDepth() 將清除深度設定為 1.0,以便清除所有畫素,無論它們所屬的物件有多遠;最後,透過呼叫 gl.clear() 擦除幀的畫素緩衝區和深度緩衝區,傳入一個位掩碼,其中 COLOR_BUFFER_BIT 和 DEPTH_BUFFER_BIT 都已設定。
由於 WebXR 對每個檢視都使用單個幀緩衝區,並且檢視上的視口用於在幀緩衝區內分離每個眼睛的視點,我們只需清除單個幀緩衝區,而無需單獨為每隻眼睛(或任何其他視點)清除。
接下來,透過從 currentFrameTime 引數指定當前時間中減去上次渲染幀的儲存時間 lastFrameTime 來計算自上次渲染幀以來經過的時間。結果是一個 DOMHighResTimeStamp 值,表示自上次渲染幀以來經過的毫秒數。我們可以在繪製場景時使用此值,以確保在給定真實經過時間的情況下,我們以適當的距離移動所有內容,而不是假設回撥將以一致的幀率觸發。此經過的時間儲存在變數 deltaTime 中,並且 lastFrameTime 的值將替換為該幀的時間,準備計算下一幀的差值。
現在是時候為每隻眼睛實際渲染場景了。我們遍歷觀察者姿勢的 views 陣列中的檢視。對於每個代表眼睛對場景視角的 XRView 物件,我們需要首先將繪製限制在幀緩衝區中代表當前眼睛可見影像的區域。
我們首先透過呼叫 XRWebGLLayer 方法 getViewport() 獲取將繪圖限制在幀緩衝區內為當前眼睛影像保留的區域的視口,從而準備 WebGL 渲染眼睛的內容。然後,我們透過將視口的 X 和 Y 原點及其寬度和高度傳入 gl.viewport() 來設定 WebGL 視口以匹配。
最後,我們呼叫方法 myDrawSceneIntoView() 來實際使用 WebGL 渲染場景。我們將代表我們正在繪製的眼睛的 XRView(為了執行透視對映等)和 deltaTime 傳入此方法,以便場景繪製程式碼在確定隨時間移動的物體位置時能夠準確表示經過的時間。
當遍歷檢視的迴圈結束時,表示場景給觀察者所需的所有影像都已渲染,並且返回時,幀緩衝區將透過 GPU 並最終到達 XR 裝置的顯示器。由於我們在函式頂部呼叫了 requestAnimationFrame(),因此當需要渲染場景動畫的下一幀時,我們的回撥將再次被呼叫。
這種方法的缺點
由於儘可能減少在此函式中花費的時間很重要,因此花在處理狀態更改上的時間越多,實際繪製事物的時間就越少。這種技術對於少量物件非常有效,但由於它必須為每個物件重新繫結所有資料兩次(左眼一次,右眼一次),因此你會花費大量時間調整狀態、上傳緩衝區和紋理等等。在下一節中,我們研究了一種修改後的方法,該方法大大減少了這些狀態更改,提供了一種可能快得多的渲染方法,尤其是在物件數量增加時。
透過以物件優先順序渲染進行最佳化
WebXR 方法的優勢在於使用單個 WebGL 幀緩衝區來包含左右眼的檢視,這使得透過重新安排操作順序可以顯著提高渲染效能。而不是為給定檢視(例如左眼)設定視口,然後逐一渲染左眼可見的每個物件,並在操作過程中為每個物件重新配置緩衝區,你可以改為連續渲染每個物件兩次,每隻眼睛一次,從而只需為兩隻眼睛設定一次緩衝區、uniforms 等。
生成的虛擬碼看起來像這樣
for each object in the scene
bindProgram()
bindUniforms()
bindBuffers()
bindTextures()
for each view in the pose's views list
get the XRWebGLLayer's viewport
set the WebGL viewport to match
bindVertices()
bindMatrices()
drawMyObject()
透過這種方式改變,我們每幀只繫結程式、uniform、緩衝區、紋理以及可能其他東西一次,而不是場景中每個物件兩次。這極大地減少了開銷。
限制幀速率
如果你需要有意限制幀速率,以建立一個基線幀速率來嘗試保持,同時為其他程式碼留出更多執行時間,你可以透過有意識地、定時地跳過幀來實現。
例如,要將幀速率降低 50%,只需跳過每隔一幀
let tick = 0;
function drawFrame(time, frame) {
animationFrameRequestID = frame.session.requestAnimationFrame(drawFrame);
if (!(tick % 2)) {
/* Draw the scene */
}
tick++;
}
此版本的渲染回撥維護一個 `tick` 計數器。只有當 `tick` 是偶數值時,幀才會被渲染。這樣,每隔一幀才會被渲染。
你可以類似地使用 !(tick % 4) 渲染每第四幀,依此類推。
讓你的動畫與經過時間匹配
渲染回撥接收一個 time 引數是有充分理由的。這個 DOMHighResTimeStamp 值是一個浮點值,表示幀被安排渲染的時間。由於你的回撥的執行不會以精確的 1/60 秒間隔發生——事實上,如果使用者的顯示器有不同的幀速率,它可能會以其他速率發生——你不能僅僅依靠你的程式碼正在執行這一事實來假設自上一幀以來已經過去了 1/60 秒。
因此,你需要使用提供的時間戳來確保你的動畫以精確的所需速度渲染。為此,你需要做的第一件事是計算自上一幀渲染以來經過的時間
let lastFrameTime = 0;
function drawFrame(time, frame) {
// schedule next frame, prepare the buffer, etc.
const deltaTime = (time - lastFrameTime) * 0.001;
lastFrameTime = time;
for (const view of pose.views) {
/* render each view */
}
}
這會維護一個名為 lastFrameTime 的全域性變數(或物件屬性),其中包含上一幀的渲染時間。在本例中,由於時間值以毫秒儲存,我們乘以 0.001 將時間轉換為秒。在某些情況下,這會節省以後的時間。在其他情況下,你需要以毫秒為單位的時間,因此你不需要更改任何內容。
有了經過的時間,你的渲染程式碼就能夠計算出每個移動物體在經過的時間內移動了多少。例如,如果一個物體正在旋轉,你可能會像這樣應用旋轉:
const xDeltaRotation =
xRotationDegreesPerSecond * RADIANS_PER_DEGREE * deltaTime;
const yDeltaRotation =
yRotationDegreesPerSecond * RADIANS_PER_DEGREE * deltaTime;
const zDeltaRotation =
zRotationDegreesPerSecond * RADIANS_PER_DEGREE * deltaTime;
這計算了自上次繪製幀以來,物體圍繞三個軸中的每一個旋轉的量。如果沒有這一點,無論經過多少時間,形狀都會在每一幀中按給定的量旋轉。這在許多情況下可能會導致明顯的卡頓。
同樣的概念也適用於移動而非旋轉的物體
const xDistanceMoved = xSpeedPerSecond * deltaTime;
const yDistanceMoved = ySpeedPerSecond * deltaTime;
const ZDistanceMoved = zSpeedPerSecond * deltaTime;
xSpeedPerSecond、ySpeedPerSecond 和 zSpeedPerSecond 各自包含該軸上的物體速度分量。換句話說,[xDistanceMoved, yDistanceMoved, zDistanceMoved] 是一個表示物體速度的向量。
與場景動畫相關的額外任務
當然,每次透過渲染器時,可能還需要發生其他事情。其中最常見的兩項是處理使用者輸入以及根據已知因素(例如使用者控制狀態或場景中物體的已知動畫路徑)更新物體(或觀察者)的位置。
處理使用者控制輸入
使用者在使用 WebXR 應用程式時可以透過三種方式提供輸入。首先,WebXR 支援直接處理來自與 XR 硬體本身整合的控制器的輸入。這些輸入源可能包括手持控制器、光學跟蹤系統、加速度計和磁力計以及其他此類裝置。
第二種輸入型別是透過 XR 系統連線的遊戲手柄。這使用了從 遊戲手柄 API 繼承的介面,但你透過 WebXR 與它們進行互動。
第三種也是最後一種輸入型別是傳統的非 XR 輸入裝置,例如鍵盤、滑鼠、觸控板、觸控式螢幕以及非 XR 遊戲手柄和操縱桿。
可以直接從 XR 硬體收集的方位和位置資訊會自動應用。因此,你需要自己處理其他型別的輸入
- 指向裝置目標和按鈕按下
- 遊戲手柄輸入
- 非 XR 輸入裝置輸入
要了解更多關於如何在使用 WebXR 呈現場景時處理使用者輸入的資訊,請參閱文章 輸入和輸入源。
更新物件位置
大多數(但並非所有)場景都包含某種形式的動畫,其中事物以適當的方式移動並相互反應。
例如,一個虛擬現實或增強現實遊戲可能由電腦控制敵方非玩家角色,並在場景中移動。他們的位置不僅隨時間變化,而且每個 NPC 可能都有相互移動的身體部位或元件。當生物行走時,手臂和腿會擺動,頭部會晃動和轉動,頭髮會彈跳和搖擺,軀幹會隨著角色的呼吸而擴張和收縮。
此外,還可能存在移動的物體和結構。在體育遊戲中,可能有一個球在空中劃過,需要模擬它的運動。在賽車遊戲中,可能有汽車或其他車輛,其移動部件(包括車輪)需要動畫化。如果場景中有水,它需要波紋或波浪才能看起來逼真。結構的部分可能會移動,例如門、牆壁和地板(對於某些型別的遊戲),等等。
另一個常見的運動來源是玩家自身。在解釋了來自控制器(包括 XR 相關的和非 XR 相關的)的輸入後,你需要將這些更改應用到場景中,以模擬使用者的運動。請參閱文章 移動、方向和運動,瞭解詳細資訊和有關其工作原理的全面示例。
後續步驟
一旦你寫好了渲染器——或者至少寫出了一個能用的東西,即使它還沒完成——你就可以開始處理攝像機及其在場景中的移動了。這在我們的關於 WebXR 中的視點和觀察者的文章中有所涉及。