視點和觀察者:在 WebXR 中模擬相機

在考慮管理應用程式中視角和攝像機的程式碼時,首先要理解最重要的一點是:WebXR 沒有攝像機。WebGL 或 WebXR API 都沒有提供一個神奇的物件來表示觀看者,你可以旋轉和移動它來自動改變螢幕上看到的內容。在本指南中,我們將展示如何在沒有攝像機可移動的情況下,使用 WebGL 模擬攝像機運動。這些技術可以在任何 WebGL(或 WebXR)專案中使用。

動畫 3D 圖形是軟體開發的一個領域,它彙集了計算機科學、數學、藝術、圖形設計、運動學、解剖學、生理學、物理學和電影攝影學等多個學科。由於我們沒有真實的攝像機,我們想象一個,重現擁有攝像機的效果,而實際上並沒有能力在場景中移動使用者。

在閱讀本文之前或閱讀本文時,有一些關於 WebGL 和 WebXR 背後的基本數學、幾何和其他概念的文章可能很有用,包括:

編者注:本文中用於展示攝像機在執行標準運動時如何移動的大多數圖表都取自FilmmakerIQ 網站上的一篇文章;即,來自這張圖片,這張圖片在網路上隨處可見。我們假設由於它們頻繁被重複使用,它們是在寬鬆許可下可用的,所有權不確定。我們希望它可以免費使用;如果不是,並且您是所有者,請告知我們,我們將尋找或製作新的圖表。或者,如果您樂意讓我們繼續使用這些圖片,請告知我們,以便我們能正確註明您的功勞!

攝像機與相對運動

當拍攝一部經典的真人電影時,演員在佈景上表演,隨著他們的表演在佈景中移動,有一臺或多臺攝像機監視著他們的動作。攝像機可能固定在原地,但也可能設定成隨之移動,跟蹤表演者的動作,前後推拉以達到情感效果等等。

虛擬攝像機

在 WebGL (以及擴充套件到 WebXR) 中,沒有我們可以移動和旋轉的攝像機物件,所以我們必須找到一種方法來偽造這些運動。既然沒有攝像機,我們必須找到一種方法來模擬它。幸運的是,像伽利略、牛頓、洛倫茲和愛因斯坦這樣的物理學家給了我們相對性原理,該原理指出物理定律在所有參考系中具有相同的形式。也就是說,無論你站在哪裡,物理定律都以相同的方式運作。

推而廣之,如果你和另一個人站在一個空曠的、一望無際的堅實石地上,如果你向另一個人移動三米,結果看起來就像另一個人向你移動了三米一樣。你們倆都無法看出區別。第三方可以看出區別,但你們倆不能。如果你是一臺攝像機,你可以透過移動攝像機或移動攝像機周圍的一切來獲得相同的視覺效果。

這就是我們的解決方案。由於我們無法移動攝像機,我們移動它周圍的世界。我們的渲染器需要知道我們想象中的攝像機位置,然後改變每個可見物件的位置以模擬該位置和方向。因此,在 WebGL 和 WebXR 程式設計中,攝像機一詞不是指實際的攝像機物件,而是指描述場景中假設觀看者的位置和觀看方向的物件,無論 3D 空間中是否存在實際物件。

視角

由於攝像機是一個虛擬物件,它不一定代表虛擬世界中的物理物件,而是代表觀看者的位置和觀看方向,因此思考需要使用攝像機的情況很有用。與遊戲相關的情況單獨列出,因為它們通常是特定於遊戲的特殊情況,但這些視角中的任何一個都可能適用於任何 3D 圖形場景。

通用攝像機

一般來說,虛擬攝像機可能與場景中的物理物件結合,也可能不結合。事實上,在 3D 遊戲範圍之外,攝像機根本不對應場景中出現的物件的可能性要大得多。以下是 3D 攝像機的一些使用示例:

  • 在渲染動畫時——無論是用於電影製作還是用於演示或遊戲——虛擬攝像機就像現實世界的電影攝像機一樣使用。儘可能地使用標準電影攝影技術,因為觀眾很可能從小就看著使用這些技術的電影長大,並且潛意識裡期望電影或動畫會遵循這些方法。偏離這些方法可能會讓觀眾出戲。
  • 在商業應用中,3D 攝像機用於在渲染圖表等內容時設定表觀大小和透視。
  • 在地圖應用中,攝像機可以直接放置在場景上方,也可以使用各種角度來展示透視效果。對於 3D GPS 解決方案,攝像機的位置會顯示使用者周圍的區域,顯示屏的大部分割槽域顯示使用者移動路徑前方的區域。
  • 當使用 WebGL 加速 2D 圖形繪製時,攝像機通常直接放置在場景中心上方,並設定距離和視野以允許顯示整個場景。
  • 在加速點陣圖圖形時,渲染器會將 2D 影像繪製到 WebGL 紋理的緩衝區中,然後重新繪製紋理以重新整理螢幕。這實質上是使用紋理作為執行 2D 圖形應用程式中的多重緩衝的後臺緩衝區。

遊戲中的攝像機

遊戲種類繁多,因此,攝像機在遊戲中的使用方式也有多種。一些常見情況包括:

  • 在第一人稱遊戲中,攝像機位於玩家角色的頭部內,面向與角色眼睛相同的方向。這樣,玩家螢幕或頭戴裝置上呈現的檢視就是他們角色所看到的。
  • 在某些第三人稱遊戲中,攝像機位於玩家角色或載具後方不遠處,顯示他們在遊戲世界中移動時的背影。這在許多多人線上角色扮演遊戲、某些射擊遊戲等中都有使用。流行的例子包括《魔獸世界》、《古墓麗影》和《堡壘之夜》。此類別還包括攝像機放置在玩家肩上方的遊戲。
  • 一些 3D 遊戲提供改變視點的能力,例如在飛行模擬器中檢視飛機各個窗戶的景色,或者檢視遊戲關卡中所有安全攝像頭的檢視(間諜和潛行類遊戲中的常見功能)。此功能也用於提供帶瞄準鏡武器的遊戲,此時檢視不再完全基於頭部位置。
  • 3D 遊戲還可能為非玩家提供觀察動作的能力,可以透過放置一種隱形虛擬形象,或者選擇一個固定的虛擬攝像機進行觀察。
  • 在高階 3D 遊戲中,攝像機或類似攝像機的物件可以用來確定非玩家角色能看到什麼,這依賴於玩家角色使用的相同渲染和物理引擎。
  • 在單屏 2D 遊戲中,攝像機不直接與玩家或遊戲中的任何其他角色關聯,而是固定在遊戲區域上方或旁邊,或者隨著動作在滾動遊戲世界中移動而跟隨動作。例如,像《吃豆人》這樣的經典街機遊戲發生在一個固定的遊戲地圖上,因此攝像機保持固定在地圖上方一定距離處,始終垂直向下指向遊戲世界。
  • 在像《超級馬里奧兄弟》這樣的橫向卷軸或縱向卷軸遊戲中,攝像機左右(或上下,或兩者)移動,以確保即使遊戲關卡比視口大得多,動作也始終可見。

定位攝像機

由於 WebGL 或 WebXR 中沒有標準的攝像機物件,我們需要自己模擬攝像機。在此之前,以及我們模擬攝像機運動之前,讓我們先來看看虛擬攝像機及其在最基本層面上如何移動。像所有事物一樣,空間中物件(即使是虛擬空間)的位置可以使用三個數字來表示,這些數字表示其相對於原點(其位置定義為 (0, 0, 0))的位置。

空間中物體與原點的空間關係還有一個方面需要考慮:透視。透視,如果正確應用於場景中的物體,可以將一個看起來像典型的 2D 螢幕一樣平坦的場景,使其看起來像真實的 3D。透視有幾種型別;這些型別及其數學解釋在文章WebGL 模型檢視投影中定義和解釋。重要的是,透視對向量的影響可以透過向向量新增第四個分量來表示:透視分量,稱為w

w 的值透過將其他三個分量除以它來獲得最終位置或向量;也就是說,對於給定為 (x, y, z, w) 的座標,3D 空間中的點實際上是 (x/w, y/w, z/w, 1) 或 (x/w, y/w, z/w)。如果你不使用透視,w 總是 1。在這種情況下,位於 (1, 0, 3) 的物件的完整座標是 (1, 0, 3, 1)。

但位置不足以描述 3D 空間中的物件,因為物件的空間狀態不僅與其位置有關,還與其旋轉或朝向方向(也稱為其方向)有關。方向可以使用 3D 向量表示,該向量通常歸一化,使其長度為 1.0。例如,如果物件面向位於 (3, 1, -2) 的物件——即向右三米,向上​​一米,距離原點兩米——結果是

[31-2]\left [ \begin{matrix} 3 \\ 1 \\ -2 \end{matrix} \right ]

這也可以表示為陣列

js
let directionVector = [3, 1, -2];

為了執行涉及座標和朝向方向向量的操作,向量需要包含w分量。對於向量,w的值始終為0,因此上述向量也可以表示為[3, 1, -2, 0]

[31-20]\left [ \begin{matrix} 3 \\ 1 \\ -2 \\ 0 \end{matrix} \right ]

WebXR 會自動將向量歸一化為 1 米的長度;但是,你可能會發現出於各種原因自己進行歸一化是有意義的,例如透過不必重複執行歸一化來提高計算效能。

一旦你確定了表示你希望攝像機執行的組合運動的矩陣,你需要將其反轉,因為你沒有移動攝像機。由於你實際上正在移動除了攝像機之外的所有東西,所以取變換矩陣的逆矩陣以獲得逆變換矩陣。然後可以將此逆矩陣應用於世界中的物件,以改變它們的位置和方向,從而模擬所需的攝像機位置。

這就是為什麼 WebXR 用於表示變換的 XRRigidTransform 物件包含 inverse 屬性的原因。inverse 屬性是另一個 XRRigidTransform 物件,它是父變換的逆變換。由於表示檢視的 XRView 具有一個 transform 屬性,它是一個提供攝像機檢視的 XRRigidTransform,你可以像這樣獲取模型檢視矩陣——模擬所需攝像機位置所需的世界移動變換矩陣:

js
let viewMatrix = view.transform.inverse.matrix;

如果您使用的庫直接接受 XRRigidTransform 物件,則可以直接獲取 view.transform.inverse,而不是隻提取表示檢視矩陣的陣列。

組合多個變換

如果你的攝像機需要同時執行多個變換,例如同時縮放和平移,你可以將變換矩陣相乘,將它們組合成一個單一的矩陣,一次性應用所有更改。請參閱文章Web 的矩陣數學中的兩個矩陣相乘,瞭解執行此操作的清晰易讀的函式,或者使用你喜歡的矩陣數學庫(例如glMatrix)來完成工作。

重要的是要記住,與典型的算術不同,乘法是可交換的(也就是說,無論是從左到右還是從右到左相乘,結果都相同),矩陣乘法是不可交換的!這是因為每個變換都會影響物件的位置,甚至可能影響座標系本身,這會極大地改變下一個操作的結果。因此,在構建複合變換(或直接按順序應用變換)時,需要注意應用變換的順序。

應用變換

要應用變換,你需要將點或向量乘以變換或變換組合。

這是對物理位置、方向或朝向方向以及透視概念的非常快速的概述。有關該主題的更多詳細資訊,請參閱文章幾何和參考空間WebGL 模型檢視投影Web 的矩陣數學

模擬經典電影攝影

電影攝影是一門設計、規劃和執行攝像機運動的藝術,旨在為動畫或電影中的場景創造所需的視覺效果和情感。有許多術語有助於理解,主要圍繞攝像機運動,因為這些術語用於描述虛擬攝像機設計的視點變化。同時執行多個這些運動也是完全可能的;例如,您可以在平移攝像機的同時放大場景。

請記住,大多數攝像機運動是相對於攝像機的參考空間來描述的。

矩陣的儲存格式通常是列主序的平面陣列;也就是說,矩陣的值從左上角開始,向下移動到底部,然後向右移動一行並重復,直到所有值都在陣列中。

因此,一個看起來像這樣的矩陣:

[a1a5a9a13a2a6a10a14a3a7a11a15a4a8a12a16]\left [ \begin{matrix} a_{1} & a_{5} & a_{9} & a_{13} \\ a_{2} & a_{6} & a_{10} & a_{14} \\ a_{3} & a_{7} & a_{11} & a_{15} \\ a_{4} & a_{8} & a_{12} & a_{16} \end{matrix} \right ]

在陣列形式中表示如下:

js
let matrixArray = [
  a1, a2, a3, a4,
  a5, a6, a7, a8,
  a9, a10, a11, a12,
  a13, a14, a15, a16,
];

在此陣列中,最左邊的列包含條目 a1a2a3a4。最上面的行包含條目 a1a5a9a13

請記住,大多數 WebGL 和 WebXR 程式設計是使用第三方庫完成的,這些庫透過新增例程來擴充套件 WebGL 的基本功能,這些例程不僅更容易執行核心矩陣和其他操作,而且通常還更容易模擬這些標準電影攝影技術。您應該強烈考慮使用其中之一,而不是直接使用 WebGL。本指南直接使用 WebGL,因為它有助於在一定程度上了解其內部工作原理,並有助於庫的開發或幫助您最佳化程式碼。

注意:儘管我們使用“移動攝像機”之類的短語,但我們真正做的是圍繞攝像機移動整個世界。這會影響某些值的運作方式,下面會進行說明。

縮放

最著名的攝像機效果之一是變焦。變焦是在物理攝像機中透過改變鏡頭的焦距來完成的;這是鏡頭中心本身與攝像機感光元件之間的距離。因此,變焦實際上根本不涉及移動攝像機。相反,變焦鏡頭會隨著時間的推移改變攝像機的放大倍數,使焦點區域看起來離觀看者更近或更遠,而無需實際物理移動攝像機。緩慢的移動可以給場景帶來運動感、輕鬆感或焦點感,而快速的變焦可以製造焦慮、驚喜或緊張感。

由於變焦不移動攝像機位置,因此產生的效果是不自然的。人眼沒有變焦鏡頭。我們透過遠離或靠近物體來使它們變小或變大。在電影攝影中,這被稱為推拉鏡頭

3D 圖形中有兩種技術可以建立相似但不相同的結果,並且它們的方法在不同情況下更容易應用。

透過調整視場進行縮放

您可以透過改變攝像機的視場 (FOV) 來實現更接近真實“縮放”的效果。視場是一個角度,定義了攝像機周圍整個可見區域中應該同時可見的弧線長度。這是物理攝像機中焦距的一種效果,因此,由於沒有真實的攝像機,改變 FOV 是一個可行的替代方案。

回想一下,圓的周長是 2π⋅r 弧度 (360°);因此,這是理論上的最大 FOV。然而,實際上,人類不僅看不到那麼多,而且顯示器和 VR 眼鏡等觀看裝置往往會進一步縮小視場。人眼通常具有約 135°(約 2.356 弧度)的水平視場和約 180°(π 或約 3.142 弧度)的垂直 FOV。

縮小攝像機的 FOV 會減少視口中包含的弧線,從而在渲染到檢視時放大該內容。這與光學變焦效果存在差異,但結果通常足夠接近以完成工作。

以下函式返回一個投影透視矩陣,該矩陣集成了指定的視場角以及給定的近裁剪面和遠裁剪面距離

js
function createPerspectiveMatrix(viewport, fovDegrees, nearClip, farClip) {
  const fovRadians = fovDegrees * (Math.PI / 180.0);
  const aspectRatio = viewport.width / viewport.height;

  const transform = mat4.create();
  mat4.perspective(transform, fovRadians, aspectRatio, nearClip, farClip);
  return transform;
}

在將 FOV 角度 fovDegrees 從度轉換為弧度並計算由 viewport 引數指定的 XRViewport 的縱橫比之後,此函式使用 glMatrix 庫的 mat4.perspective() 函式來計算透視矩陣。

透視矩陣將視野(技術上是垂直視野)、縱橫比以及近遠裁剪平面封裝在 4x4 矩陣 transform 中,然後將其返回給呼叫者。

近裁剪面是到與顯示錶面平行的平面的距離(以米為單位),比該平面更近的任何東西都不會被繪製。位於該平面與攝像機同一側的任何頂點都不會被繪製。相反,遠裁剪面是到某個平面的距離(以米為單位),超出該平面後沒有頂點被繪製。

要使用縮放因子或百分比進行縮放,您可以將 1 倍(正常大小的 100%)對映到您允許的最大 FOV 值(這會導致可見內容最多),然後將您的最大放大倍數對映到您支援的最大 FOV 值,並對映介於兩者之間的相應值。

如果你在每一幀的渲染過程中都計算透視矩陣,那麼你可以將所有其他需要應用的變換乘入該矩陣,以得到該幀所需的幾何體。例如:

js
const transform = createPerspectiveMatrix(viewport, 130, 1, 100);
const translateVec = vec3.fromValues(
  -trackDistance,
  -craneDistance,
  pushDistance,
);
mat4.translate(transform, transform, translateVec);

這從一個表示 130° 垂直視場的透視矩陣開始,然後應用一個平移,以一種包含橫向移動升降推入運動的方式移動攝像機。

縮放變換

與真正的“縮放”不同,縮放涉及將位置或頂點中的每個 xyz 座標值乘以該軸的縮放因子。這些因子可能不一定對每個軸都相同,儘管最接近縮放效果的結果將涉及對每個軸使用相同的值。這需要應用於場景中的每個頂點——理想情況下是在頂點著色器中。

如果你想放大 2 倍,你需要將每個分量乘以 2.0。要縮小相同倍數,將它們乘以 -2.0。在矩陣術語中,這是透過將縮放因子考慮在內的變換矩陣來執行的,如下所示:

js
let scaleTransform = [
  Sx, 0, 0, 0,
  0, Sy, 0, 0,
  0, 0, Sz, 0,
  0, 0, 0, 1
];

此矩陣表示一個變換,其縮放因子由 (Sx, Sy, Sz) 表示,其中 Sx 表示沿 X 軸的縮放因子,Sy 表示沿 Y 軸的縮放因子,Sz 表示沿 Z 軸的縮放因子。如果這些值中的任何一個與其他值不同,則結果將是拉伸或收縮,其在某些維度上與其他維度不同。

如果每個方向都要應用相同的縮放因子,您可以建立一個簡單的函式來為您生成縮放變換矩陣:

js
function createScalingMatrix(f) {
  return [f, 0, 0, 0, 0, f, 0, 0, 0, 0, f, 0, 0, 0, 0, 1];
}

有了變換矩陣,我們將變換 scaleTransform 應用於向量(或頂點)myVector

js
let myVector = [2, 1, -3];
let scaleTransform = [2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1];
vec4.transformMat4(myVector, myVector, scaleTransform);

或者,使用上面所示的 createScalingMatrix() 函式,沿每個軸以相同的因子進行縮放

js
let myVector = [2, 1, -3];
vec4.transformMat4(myVector, myVector, createScalingMatrix(2.0));

平移(向左或向右偏航)

平移偏航是攝像機左右旋轉,其底座保持固定。攝像機在空間中的位置不變,只改變其看向的方向。而且該方向除了水平方向外不會改變。平移非常適合建立背景或在廣闊的空間或巨大的物體上提供範圍感。或者只是左右看,就像模擬玩家在沉浸式或 VR 場景中轉動頭部一樣。

A diagram showing a camera panning left or right

要做到這一點,我們需要圍繞 Y 軸旋轉,以模擬攝像機的左右旋轉。使用我們之前使用過的 glMatrix 庫,這可以透過 mat4 類上的 rotateY() 方法來完成,該類表示一個標準的 4x4 矩陣。要將由矩陣 viewMatrix 定義的視點旋轉 panAngle 弧度:

js
mat4.rotateY(viewMatrix, viewMatrix, panAngle);

如果 panAngle 為正,此變換將使攝像機向右平移;如果 panAngle 為負,則向左平移。

傾斜(向上或向下俯仰)

當你傾斜俯仰攝像機時,你將它固定在相同的空間座標中,同時垂直改變它朝向的方向,而根本不改變其朝向的水平部分。它調整了它向上和向下指向的方向。傾斜對於捕捉高大物體或場景的範圍很有用,例如森林或山脈,但也是引入重要或令人敬畏的人物或地點的一種流行方式。當然,它也有助於實現玩家上下看的支援。

A diagram showing a camera tilting up and down

因此,傾斜攝像機可以透過圍繞 X 軸旋轉攝像機來實現,使其向上和向下旋轉。這可以使用矩陣數學庫中的適當方法完成,例如 glMatrix 的 mat4 類中的 rotateX() 方法:

js
mat4.rotateX(viewMatrix, viewMatrix, angle);

angle 的正值將使攝像機向下傾斜,而 angle 的負值將使其向上傾斜。

推拉(向內或向外移動)

推拉鏡頭是指整個攝像機向前和向後移動的鏡頭。在經典電影製作中,這通常透過將攝像機安裝在軌道上或移動車輛上來完成。由此產生的運動可以產生令人印象深刻的平滑效果,特別是當與您拍攝焦點的人物或物體一起移動時。

A diagram showing how a camera moves for a dolly shot

雖然推拉鏡頭和變焦鏡頭看起來應該差不多,但它們並不一樣。變焦改變了攝像機的焦距,這意味著目標與其周圍環境的空間關係不會改變,即使目標在畫面中變大或變小。另一方面,推拉鏡頭透過實際移動攝像機,複製了物理運動的感覺,使場景中物體之間的關係在你朝著或遠離拍攝目標移動時,按照你的預期發生變化。

要執行推拉操作,請沿 Z 軸向前和向後平移攝像機檢視

js
mat4.translate(viewMatrix, viewMatrix, [0, 0, dollyDistance]);

此處,[0, 0, dollyDistance] 是一個向量,其中 dollyDistance 是攝像機推拉的距離。由於這是透過圍繞攝像機移動整個世界來工作的,因此這裡真正發生的是整個世界沿 Z 軸相對於攝像機移動 dollyDistance 米。如果 dollyDistance 為正,世界將向用戶移動該量,導致攝像機更接近場景。相反,dollyDistance 的負值將世界移離使用者,導致攝像機似乎向後移動遠離目標。

跟拍(左右移動)

使用物理攝像機的跟拍與推拉使用相同的裝置,但它不是向前和向後移動攝像機,而是左右移動或反之。攝像機完全不旋轉,因此拍攝焦點會慢慢滑出畫面。這可以在嘗試在場景中建立情感時暗示專注、時間流逝或沉思。它也經常用於“邊走邊聊”的場景中,其中攝像機沿著角色滑動,角色穿過場景。

A diagram showing how a camera trucks left and right

要左右移動攝像機,請沿 X 軸平移檢視矩陣,方向與所需的攝像機移動方向相反

js
mat4.translate(viewMatrix, viewMatrix, [-truckDistance, 0, 0]);

請注意向量 [-truckDistance, 0, 0]。這補償了跟拍操作是透過移動世界而不是攝像機來工作的事實。透過將整個世界移動到與 truckDistance 指示的方向相反的方向,我們實現了攝像機沿預期方向移動的效果。這樣,truckDistance 的正值將使攝像機向右移動(透過向左移動世界),而 truckDistance 的負值將使攝像機向左移動(透過向右移動世界)。

升降(上下移動)

升降鏡頭是指攝像機相對於地面水平固定,但垂直上下移動的鏡頭。想象一下攝像機在一個基座(或杆)上,基座變得更高或更矮。這對於跟蹤一個正在變高或變矮、或者從椅子上站起來或坐下、或者垂直上下移動的主體很有用。

A diagram showing a camera moving up and down using a pedestal motion

這類似於搖臂鏡頭,搖臂鏡頭涉及移動連線在搖臂上的攝像機上下。要執行升降或搖臂運動,請沿 Y 軸平移檢視,方向與您想要移動攝像機的方向相反

js
mat4.translate(viewMatrix, viewMatrix, [0, -pedestalDistance, 0]);

透過對 pedestalDistance 的值取反,我們補償了我們實際上正在移動世界而不是攝像機的事實。因此,pedestalDistance 的正值將使攝像機向上移動,而負值將使其向下移動。

傾斜(左右滾動)

傾斜(或滾動)是攝像機圍繞其滾動軸的旋轉;也就是說,攝像機固定在空間中,並保持指向同一位置,但旋轉,使攝像機的頂部指向不同的方向。

A diagram showing a camera rolling left and right

你可以這樣想象:伸出你的手臂,手掌向下。想象你的手是攝像機,手背代表攝像機頂部。現在旋轉你的手,使“攝像機”倒置。你剛剛將你的手圍繞滾動軸傾斜了。在電影攝影中,傾斜可以用來模擬各種不穩定的運動,如波浪或湍流,但也可以用於戲劇效果。

使用 glMatrix 實現繞 Z 軸的旋轉

js
mat4.rotateZ(viewMatrix, viewMatrix, cantAngle);

組合運動

您可以同時執行多種運動,例如在平移的同時縮放,或同時傾斜和傾斜。

沿多軸平移

沿多個軸進行平移非常簡單。之前,我們是這樣進行平移的:

js
mat4.translate(viewMatrix, viewMatrix, [-truckDistance, 0, 0]);
mat4.translate(viewMatrix, viewMatrix, [0, -pedestalDistance, 0]);
mat4.translate(viewMatrix, viewMatrix, [0, 0, dollyDistance]);

這裡的解決方案很明顯。由於平移表示為一個向量,提供了沿每個軸移動的距離,我們可以將它們組合起來,如下所示:

js
mat4.translate(viewMatrix, viewMatrix, [
  -truckDistance,
  -pedestalDistance,
  dollyDistance,
]);

這將使矩陣 viewMatrix 的原點沿每個軸移動指定的量。

圍繞多個軸旋轉

您還可以將圍繞多個軸的旋轉組合成圍繞一個代表旋轉的共享軸的四元數進行單一旋轉。要單獨執行旋轉,您可以使用尤拉角(圍繞每個軸的單獨角度)來應用俯仰、偏航和滾動,如下所示:

js
mat4.rotateX(viewMatrix, viewMatrix, pitchAngle);
mat4.rotateY(viewMatrix, viewMatrix, yawAngle);
mat4.rotateZ(viewMatrix, viewMatrix, rollAngle);

您可以轉而從尤拉角構造一個表示組合旋轉軸的四元數,然後使用乘法旋轉矩陣,如下所示:

js
const axisQuat = quat.create();
const rotateMatrix = mat4.create();
quat.fromEuler(axisQuat, pitchAngle, yawAngle, rollAngle);
mat4.fromQuat(rotateMatrix, axisQuat);
mat4.multiply(viewMatrix, viewMatrix, rotateMatrix);

這會將俯仰、偏航和滾動的尤拉角轉換為表示所有三個旋轉的四元數。然後將其轉換為旋轉變換矩陣;最後,檢視矩陣乘以旋轉變換以完成旋轉。

用 WebXR 表示 3D

WebXR 將 3D 圖形更進一步,允許使用特殊的視覺硬體(如護目鏡或頭戴裝置)來呈現 3D 圖形,這些圖形似乎真實存在於三維空間中,可能在現實世界的背景下(在增強現實的情況下)。

為了感知深度,需要對場景有兩個視角。透過比較這兩個檢視,可以識別物體的深度,並進一步識別觀看者與所見物體之間的距離。這就是我們有兩隻眼睛,略微分開的原因。你可以透過一次閉上一隻眼睛,然後交替切換兩隻眼睛來提醒自己這個事實。注意你的左眼如何能看到鼻子的左側而不是右側,而你的右眼能看到鼻子的右側而不是左側。這只是你的每隻眼睛所看到內容之間眾多差異之一。

我們的大腦接收到兩組關於我們視野內光照水平和波長的資料——每隻眼睛一組。大腦利用這些資料在我們的腦海中構建場景,利用兩個視角之間的微小差異來計算深度和距離。

渲染場景

XR——涵蓋虛擬現實 (VR) 和增強現實 (AR) 的簡稱——頭戴裝置透過繪製場景的兩個檢視向我們呈現 3D 影像,這兩個檢視略微偏移,就像我們兩隻眼睛獲得的檢視一樣。然後將這些檢視分別提供給每隻眼睛,以便它們收集我們大腦構建 3D 影像所需的資料。

為此,WebXR 要求您的渲染器為每一幀影片繪製場景兩次——每隻眼睛一次。這兩個檢視渲染到同一個幀緩衝區中,一個在左側,一個在右側。然後 XR 裝置使用螢幕和透鏡將生成的影像的左半部分呈現給我們的左眼,將右半部分呈現給我們的右眼。

例如,考慮一個使用 2560x1440 畫素幀緩衝區的裝置。將其分為兩部分——每隻眼睛一半——導致每隻眼睛的檢視以 1280x1440 畫素的解析度繪製。從概念上講,它看起來是這樣的:

Diagram showing how a framebuffer is divided between two eyes' viewpoints

您的程式碼透過呼叫 XRSession 方法 requestAnimationFrame() 來告知 WebXR 引擎您想要提供下一幀動畫,並提供一個渲染動畫幀的回撥函式。當瀏覽器需要您渲染場景時,它會呼叫回撥,將當前時間和一個 XRFrame(封裝渲染正確幀所需的資料)作為輸入引數提供。

此資訊包括描述觀看者在場景中位置和朝向方向的 XRViewerPose,以及 XRView 物件的列表,每個物件代表場景的一個視角。在當前的 WebXR 實現中,此列表中永遠不會有超過兩個條目:一個描述左眼的位置和視角,另一個描述右眼的位置和視角。您可以透過檢查給定 XRVieweye 屬性的值來判斷它代表哪隻眼睛,該屬性是一個字串,其值為 leftright(第三個可能的值 none 理論上可用於表示另一個視角,但當前 API 中對此的支援並不完全可用)。

幀回撥示例

一個相當基本(但典型)的幀渲染回撥可能看起來像這樣:

js
function myAnimationFrameCallback(time, frame) {
  const adjustedRefSpace = applyPositionOffsets(xrReferenceSpace);
  const pose = frame.getViewerPose(adjustedRefSpace);

  animationFrameRequestID = frame.session.requestAnimationFrame(
    myAnimationFrameCallback,
  );

  if (pose) {
    const glLayer = frame.session.renderState.baseLayer;
    gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer);
    CheckGLError("Binding the framebuffer");

    gl.clearColor(0, 0, 0, 1.0);
    gl.clearDepth(1.0);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    CheckGLError("Clearing the framebuffer");

    const deltaTime = (time - lastFrameTime) * 0.001;
    lastFrameTime = time;

    for (const view of pose.views) {
      const viewport = glLayer.getViewport(view);
      gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
      CheckGLError(`Setting viewport for eye: ${view.eye}`);

      myRenderScene(gl, view, sceneData, deltaTime);
    }
  }
}

回撥開始時呼叫一個自定義函式 applyPositionOffsets(),該函式接收一個參考空間,並對其變換矩陣應用任何需要進行的更改,以考慮諸如 WebXR 未控制的裝置(如鍵盤和滑鼠)的使用者輸入等因素。此函式返回的調整後的 XRReferenceSpace 然後傳遞給 XRFrame 方法 getViewerPose() 以獲取表示觀看者位置和視角的 XRViewerPose

接下來,我們繼續排隊請求渲染下一幀影片,這樣我們就不必擔心稍後再做,透過再次呼叫 requestAnimationFrame()

現在是渲染場景的時候了。如果我們成功獲取到姿態,我們就會從會話的 renderState 物件的 baseLayer 屬性中獲取渲染所需的 XRWebGLLayer。我們使用 WebGLRenderingContext 方法 gl.bindFrameBuffer() 將其繫結到 WebGL 的 gl.FRAMEBUFFER 目標。

然後我們清空幀緩衝區,以確保我們從已知狀態開始,因為我們的渲染器不會觸及每個畫素。我們使用 gl.clearColor() 將清除顏色設定為不透明黑色,並透過呼叫 WebGLRenderingContext 方法 gl.clearDepth() 將深度緩衝區清除的值設定為 1.0。然後我們呼叫 WebGLRenderingContext 方法 gl.clear(),它會清除幀緩衝區(因為我們在掩碼引數中包含了 gl.COLOR_BUFFER_BIT)和深度緩衝區(因為我們包含了 gl.DEPTH_BUFFER_BIT)。

然後,我們透過比較幀的所需渲染時間與上一幀的繪製時間,來確定自上一幀渲染以來經過了多少時間。由於此值以毫秒為單位,我們透過乘以 0.001(或除以 1000)將其轉換為秒。

現在我們迴圈遍歷姿態的檢視,這些檢視可以在 XRViewerPose 陣列 views 中找到。對於每個檢視,我們向 XRWebGLLayer 請求要使用的適當視口,透過將位置和大小資訊傳遞給 gl.viewport() 來配置 WebGL 視口以匹配。這限制了渲染,使我們只能繪製到幀緩衝區中代表由 view.eye 標識的眼睛所看到的影像的部分。

在建立這些約束條件並準備好所有其他所需內容後,我們呼叫一個自定義函式 myRenderScene() 來實際執行計算和 WebGL 渲染以渲染幀。在這種情況下,我們傳入 WebGL 上下文 glXRView view、一個 sceneData 物件(其中包含頂點和片段著色器、頂點列表、紋理等內容),以及 deltaTime,它指示自上一幀以來經過的時間,以便我們知道動畫應該前進多遠。

當此函式返回時,WebXR 正在使用的 WebGL 幀緩衝區中現在包含場景的兩個副本,每個副本佔據幀的一半:一個用於左眼,一個用於右眼。這些副本透過 XR 軟體和驅動程式進入頭戴裝置,其中每一半都顯示給相應的眼睛。

另見