DirectX11 With Windows SDK--04 變換


前言

這一章涉及的是常用的矩陣變換,絕大部分內容節選自龍書,以幫助大家構建矩陣與2D/3D空間的數學聯系。

學習目標:

  1. 了解Direct3D的一些內在規定
  2. 掌握矩陣變換與2D/3D空間的聯系
  3. 熟悉3D變換與投影成像的過程
  4. 熟悉2D變換與投影成像的過程

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。

Direct3D的一些規定

3D坐標系

Direct3D使用的是左手坐標系,而OpenGL與我們平日接觸到的數學使用的則是右手坐標系:

我們可以用左手擺出上圖中的左手坐標系,其中拇指朝向+X,食指朝向+Y,中指朝向+Z

紋理坐標系和屏幕坐標系

為了避免混淆,這里直說Direct3D的。由於Direct3D支持3D紋理,紋理坐標系實際上是可以有三個維度的,如下圖所示。只不過我們絕大多數情況使用的僅僅是2D紋理,故只需要考慮X軸和Y軸的部分。

屏幕坐標系(2D)與紋理坐標系的X軸、Y軸朝向是一致的。

矩陣計算

Direct3D中,矩陣通常被表示為行矩陣,即你將要使用到的DirectXMath數學庫中生成的矩陣都是行矩陣。這也意味着矩陣乘法通常被表示為行向量乘以行矩陣的形式。這不僅在編寫C++的代碼中有所體現,在HLSL中我們也將習慣寫成上述形式。

矩陣變換

基、基向量

由於上面我們提到DirectX使用的是行矩陣,因此我們的基向量也為行向量。基向量是構成坐標系的包含大小的坐標軸,而3D坐標系的是由3個基向量組成的。而3個模為1,且相互正交的基向量構成的基叫做標准正交基,其中(1,0,0), (0,1,0)和(0,0,1)則作為標准正交基的3個標准基向量

變換矩陣

變換矩陣是通過與頂點坐標(或向量)進行矩陣乘法來實現對頂點的變換,變換得到的頂點坐標(或向量)為在原坐標系下對應的坐標。

例如有下圖的這樣一個變換,y軸縮小為原來的1/2,而z軸加上1個單位的x軸向量得到新的z軸。

現有一坐標(2,4,1),上述變換過程可以用下面的矩陣乘法表示:

\[\begin{bmatrix} 2 & 4 & 1\end{bmatrix}\begin{bmatrix} 1 & 0 & 0 \\ 0 & \frac{1}{2} & 0 \\ 1 & 0 & 1 \\ \end{bmatrix}=\begin{bmatrix} 3 & 2 & 1\end{bmatrix} \]

其中3x3矩陣的3個行向量從上往下依次為變換后的坐標基的x'軸(或向量i')、y'軸(或向量j')和z'軸(或向量k')。這種變換的一般形式為:

\[\begin{bmatrix} x & y & z\end{bmatrix}\begin{bmatrix} \longleftarrow\vec{\mathbf{i'}}\longrightarrow \\ \longleftarrow\vec{\mathbf{j'}}\longrightarrow \\ \longleftarrow\vec{\mathbf{k'}}\longrightarrow \\ \end{bmatrix}=\begin{bmatrix} x' & y' & z'\end{bmatrix} \]

線性變換

縮放

縮放(scaling)是指改變物體的大小。通過分別改變x軸、y軸、z軸的比例我們可以得到想要的物體大小,以及寬窄、高低、厚扁程度。

縮放矩陣的表示為:

\[S=\begin{bmatrix} S_x & 0 & 0 \\ 0 & S_y & 0 \\ 0 & 0 & S_z \\ \end{bmatrix} \]

在DirectXMath中,縮放矩陣對應的函數為XMMatrixScale

旋轉

旋轉(rotation)是為了改變物體的朝向。在初學階段我們常用的是繞x軸、y軸、z軸旋轉的變換。需要注意的是,DirectXMath中的旋轉變換都是順時針旋轉(theta > 0表示順時針旋轉)。

但現在暫時先讓我們回到一般數學上(右手坐標系)。現在我們需要讓向量或坐標點(x, y)繞原點逆時針旋轉β度:

變換前的向量用極坐標表示為:

\[x=rcos\alpha \\ y=rsin\alpha \]

逆時針旋轉β度后的向量用極坐標表示為:

\[x'=rcos(\alpha + \beta) = rcos\alpha cos\beta - rsin\alpha sin\beta = xcos\beta - ysin\beta \\ y'=rsin(\alpha + \beta) = rsin\alpha cos\beta + rsin\beta cos\alpha = xsin\beta + ycos\beta \]

上述變換過程我們可以用矩陣表示為:

\[\begin{bmatrix} x & y \end{bmatrix}\begin{bmatrix} cos\beta & -sin\beta \\ sin\beta & cos\beta \\ \end{bmatrix}=\begin{bmatrix} x' & y' \end{bmatrix} \]

這里省略3D空間下分別繞X,Y,Z軸逆時針旋轉的矩陣推導過程,我們可以得到下面3個對應的矩陣:

\[\mathbf{R_x}=\begin{bmatrix} 1 & 0 & 0 \\ 0 & cos\theta & sin\theta \\ 0 & -sin\theta & cos\theta \\ \end{bmatrix}, \mathbf{R_y}=\begin{bmatrix} cos\theta & 0 & -sin\theta \\ 0 & 1 & 0 \\ sin\theta & 0 & cos\theta \\ \end{bmatrix}, \mathbf{R_z}=\begin{bmatrix} cos\theta & sin\theta & 0 \\ -sin\theta & cos\theta & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} \]

需要注意的是,因為左手坐標系跟右手坐標系是鏡像關系,因此右手坐標系下的繞某一軸逆時針旋轉(在軸的朝向處向原點看)所用的矩陣,和左手坐標系下的繞同一軸順時針旋轉(在軸的朝向處向原點看)所用的矩陣是相同的。

例如,在左手坐標系下,對向量[sqrt(2), sqrt(2), 1, 0]繞Z軸順時針旋轉45°(從Z軸正方向往原點看)的運算過程如下:

\[\begin{bmatrix} \frac{\sqrt{2}}{2} & \frac{\sqrt{2}}{2} & 1 & 0 \end{bmatrix}\begin{bmatrix} cos45° & sin45° & 0 & 0 \\ -sin45° & cos45° & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix}=\begin{bmatrix} 0 & 1 & 1 & 0 \end{bmatrix} \]

此外,旋轉矩陣具有正交性,即滿足\(\mathbf{R^T} = \mathbf{R^{-1}}\)

在DirectXMath中,縮放矩陣對應的函數為XMMatrixRotationXXMMatrixRotationYXMMatrixRotationZ(參數為弧度)

注意:除了旋轉和平移,只要三個基向量線性無關(即不共面),就都可以稱之為線性變換。就如同變換矩陣那一小節所用到的矩陣那樣。

仿射變換

齊次坐標

仿射變換是由一個線性變換與一個平移變換組合而成的。對於向量而言,平移操作是沒有意義的,因為向量只描述方向與大小、卻與位置無關,即平移操作不應作用於向量。因此,平移變換只能應用於點(位置向量)。齊次坐標(homogeneous coordinate)所提供的表示機制,使我們可以方便地對點和向量進行統一的處理。在采用其次坐標表示法時,我們將坐標擴充為四元組,第四個坐標w的取值將根據被描述對象是點還是向量而定:

  1. (x, y, z, 0)表示向量
  2. (x, y, z, 1)表示點

注意:這種表示法可以很方便地表示兩個坐標點之差即為一個向量(w分量為0),以及表示一個點與一個向量之和為一個點(w分量為1)

  1. (x, y, z, w)和(x/w, y/w, z/w, 1)都表示同一個點(w≠0),這對於后續做透視投影會用到這個性質

仿射變換的定義及矩陣表示

線性變換並不能表示出我們需要的所有變換,因此,現將其擴充為一種稱作仿射變換的映射范圍更廣的函數類。仿射變換的矩陣表示法為:

\[\begin{bmatrix} x & y & z\end{bmatrix}\begin{bmatrix} a_{11} & a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \\ a_{31} & a_{32} & a_{33} \\ \end{bmatrix}+\begin{bmatrix} t_x & t_y & t_z\end{bmatrix}=\begin{bmatrix} x' & y' & z'\end{bmatrix} \]

如果用w = 1把坐標擴充為齊次坐標,那么就可以將上式更加簡潔地寫作:

\[\begin{bmatrix} x & y & z & 1 \end{bmatrix}\begin{bmatrix} a_{11} & a_{12} & a_{13} & 0 \\ a_{21} & a_{22} & a_{23} & 0 \\ a_{31} & a_{32} & a_{33} & 0 \\ t_x & t_y & t_z & 1 \\ \end{bmatrix}=\begin{bmatrix} x' & y' & z' & 1 \end{bmatrix} \]

若把w改成0,它就不會受到向量t平移的影響。

平移

現在將平移變換定義為仿射變換。若要利用向量t對坐標點u進行平移,則這種變換矩陣可以表示為:

\[\mathbf{T}=\begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ t_x & t_y & t_z & 1 \\ \end{bmatrix} \]

平移矩陣的逆矩陣表示平移的逆操作,即為利用向量-t對坐標點u進行平移:

\[\mathbf{T^{-1}}=\begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ -t_x & -t_y & -t_z & 1 \\ \end{bmatrix} \]

變換的復合

假設S是一個縮放矩陣,R是一個旋轉矩陣,T是一個平移矩陣。對幾何體的變換順序通常為先縮放,后旋轉,再平移。對幾何體的每個頂點,都有:

\[((v_i S)R)T={v_i}^{'} \]

因為矩陣乘法滿足結合律,故可以先計算SRT,再計算viSRT的乘積。又或者將C=SRT看作一個矩陣,即提前將3種變換封裝為一個凈變換矩陣。對於一個包含20000頂點組成的3D物體,如果按照上式進行計算,則需要執行60000次的向量與矩陣的乘法運算;而預先將C算出來的話,則只需要執行2次矩陣乘法運算和20000次向量與矩陣的乘法運算。

但是,矩陣乘法並不滿足交換律!這意味着諸如SRRSRTTR的變換結果很可能是不同的。

下圖展示了立方體先按X軸放大為原來的兩倍,再繞Y軸順時針旋轉30°(圖上半部分),以及先繞Y軸順時針旋轉30°,再按X軸放大為原來的兩倍的結果(圖下半部分):

可以看到,先旋轉后縮放會導致物體的畸變。

坐標變換

坐標變換的矩陣為:

\[\begin{bmatrix} x & y & z & w \end{bmatrix}\begin{bmatrix} \longleftarrow\vec{\mathbf{u}}\longrightarrow & 0 \\ \longleftarrow\vec{\mathbf{v}}\longrightarrow & 0 \\ \longleftarrow\vec{\mathbf{w}}\longrightarrow & 0 \\ \longleftarrow\vec{\mathbf{Q}}\longrightarrow & 1 \\ \end{bmatrix}= \begin{bmatrix} x & y & z & w \end{bmatrix}\begin{bmatrix} u_x & u_y & u_z & 0 \\ v_x & v_y & v_z & 0 \\ w_x & w_y & w_z & 0 \\ Q_x & Q_y & Q_z & 1 \\ \end{bmatrix}= \begin{bmatrix} x' & y' & z' & w \end{bmatrix} \]

坐標變換矩陣其實就是仿射變換矩陣的一種,即本質上坐標變換矩陣和仿射變換矩陣是相同的,但是在看待這種變換的過程會有所不同。在講變換矩陣的時候,我們是讓坐標系不變,然后讓物體在當前坐標系下進行縮放、旋轉和平移來到最終的位置;但在講坐標變換的時候,我們是讓整個坐標系縮放、旋轉(帶動物體的縮放和旋轉),然后再讓坐標系遠離物體以完成所謂的平移操作。它們的差別僅僅在於選擇的參考系不同而已。如果能理解這一點,對於接下來的世界變換、觀察變換和投影變換理解都有所幫助。這樣我們就能夠清楚,既然坐標變換可以讓坐標軸遠離物體(以物體為參考系),那么坐標變換的逆變換可以讓坐標軸靠近物體(以物體為參考系)。

3D的變換與投影成像

3D的部分包含了四大變換:世界變換、觀察變換、投影變換和視口變換。其中前面三種變換需要在頂點着色器完成,必要時需要提供變換矩陣。而視口變換是在光柵化階段完成的,通過第一章傳給光柵化階段的D3D11_VIEWPORT來完成。

局部空間與世界空間

每個物體都有其自己的局部空間(局部坐標系),但是世界空間只有一個。

世界變換囊括了縮放、旋轉和平移變換。通過世界變換,我們可以將物體模型從自身的局部坐標系轉換到世界坐標系中。當然,我們也可以理解為將物體從世界原點開始縮放、旋轉,然后平移到目標位置。這樣每一個物體都能在同一個世界空間中表示。

注意:對於不同的物體,需要使用不同的世界變換矩陣;而對於同一個頂點的所有頂點,都要使用一致的世界變換矩陣來變換。

從局部空間到世界空間的坐標變換矩陣即為前面提到的:

\[W=\begin{bmatrix} u_x & u_y & u_z & 0 \\ v_x & v_y & v_z & 0 \\ w_x & w_y & w_z & 0 \\ Q_x & Q_y & Q_z & 1 \\ \end{bmatrix} \]

其中,[ux, uy, uz, 0][vx, vy, vz, 0][wx, wy, wz, 0] 表示局部坐標系三個相互垂直的軸向量(這三個向量的模不一定相等),而[Qx, Qy, Qz, 1]則表示上述坐標軸原點在世界坐標系的位置坐標。這種表示形式的世界變換矩陣也可以理解為局部坐標系在世界坐標系的位置,以及三個軸向量在世界坐標系的表現形式。現在對一個物體進行縮放、順時針旋轉、平移:

變換結果如下:

觀察空間

為了獲取一個2D圖像,我們必須引入虛擬攝像機的概念。一個虛擬攝像機可以看作一個觀察坐標系,它也是一個局部坐標系,原點為攝像機的位置,Z軸為攝像機的觀察方向,Y軸為攝像機的上方向,而X軸則為攝像機的右方向。

若已知[ux, uy, uz, 0][vx, vy, vz, 0][wx, wy, wz, 0]分別對應觀察坐標系的三個相互垂直的單位坐標軸,以及[Qx, Qy, Qz, 1]表示攝像機在世界坐標系的位置,那么從觀察坐標系到世界坐標系的世界變換矩陣如下:

\[\mathbf{W}=\mathbf{RT}=\begin{bmatrix} u_x & u_y & u_z & 0 \\ v_x & v_y & v_z & 0 \\ w_x & w_y & w_z & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix}\begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ Q_x & Q_y & Q_z & 1 \\ \end{bmatrix}=\begin{bmatrix} u_x & u_y & u_z & 0 \\ v_x & v_y & v_z & 0 \\ w_x & w_y & w_z & 0 \\ Q_x & Q_y & Q_z & 1 \\ \end{bmatrix} \]

然而,我們想做的並不是這樣,現在我們想要的是從世界坐標系轉換到觀察(局部)坐標系。逆變換可以由變換矩陣的逆求得,所以從世界空間到觀察空間的坐標變換矩陣為W^-1

\[\mathbf{V}=\mathbf{W^{-1}}=\mathbf{(RT)^{-1}}=\mathbf{T^{-1}R^{-1}}=\mathbf{T^{-1}R^T}= \\ \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ -Q_x & -Q_y & -Q_z & 1 \\ \end{bmatrix}\begin{bmatrix} u_x & v_x & w_x & 0 \\ u_y & v_y & w_y & 0 \\ u_z & v_z & w_z & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix}=\begin{bmatrix} u_x & v_x & w_x & 0 \\ u_y & v_y & w_y & 0 \\ u_z & v_z & w_z & 0 \\ -Q_x\cdot \mathbf{u} & Q_y\cdot \mathbf{v} & Q_z\cdot \mathbf{w} & 1 \\ \end{bmatrix} \]

現在我們來展示一種用於構建觀察矩陣中所有向量的直觀方法。若已知Q為攝像機的位置,T為攝像機對准的觀察目標點,j為世界空間“向上”方向的單位向量。(下圖以平面xOz作為場景中的“地平面”,並以世界空間的y軸作為攝像機“向上”的方向。因此,j=(0,1,0)僅是平行於世界空間中y軸的一個單位向量)對於下圖來說,虛擬攝像機的觀察方向為:

\[\mathbf{w}=\frac{\mathbf{T-Q}}{\parallel\mathbf{T-Q}\parallel} \]

該向量表示虛擬攝像機局部空間的z軸。指向w“右側”的單位向量為:

\[\mathbf{u}=\frac{\mathbf{j}\times\mathbf{w}}{\parallel\mathbf{j}\times\mathbf{w}\parallel} \]

它表示的是虛擬攝像機局部空間的x軸。最后,該攝像機局部空間的y軸為:

\[\mathbf{v}=\mathbf{w}\times\mathbf{u} \]

因為wu是互相正交的單位向量,所以v也必為單位向量。因此我們也無須對向量v進行規范化處理了。

DirectXMath庫針對上述計算觀察矩陣的處理流程提供了以下函數:

XMMATRIX XMMatrixLookAtLH(  // 輸出視圖變換矩陣V
    FXMVECTOR EyePosition,      // 輸入攝影機坐標
    FXMVECTOR FocusPosition,    // 輸入攝影機焦點坐標
    FXMVECTOR UpDirection);     // 輸入攝影機上朝向坐標

投影變換

最近在看GAMES101: 現代計算機圖形學入門,對投影矩陣這邊有了新的見解。故在這里翻新一下內容。

投影變換的目的就是要將3D的場景投影到一個2D的平面上。包含透視投影正交投影兩種。其中由透視投影產生的圖片中的物體會表現為近大遠小,而正交投影產生的圖片中的物體只與他本身的大小有關,與距離無關。

正交投影變換與規格化設備坐標(NDC)

由於正交投影變換相對簡單,放在這里先講,也為了引出后面的內容。

在經過觀察變換后,此時我們來到了攝像機的局部空間,即攝像機位於原點,朝着+Z方向觀察,上方向為+Y。我們要在這個基礎上定義投影范圍。觀察上圖,可以看到正交投影的可視范圍是一個長方體,而且它是與坐標軸對齊的。我們可以對這個立方體進行平移,或者是縮放。只要物體落在立方體內且沒有被遮擋,則最終投影出來的2D圖片上我們是可以看到它的。

而對於硬件來說,使用統一的規格化設備坐標,可以無需提前知道屏幕的寬高比來簡化像素的映射操作。在DirectX中,規格化設備坐標的可視取值范圍為:

\[-1\leq x\leq 1\\ -1\leq y\leq 1\\ 0\leq z\leq 1 \]

這個范圍正好也是表示一個立方體。有了這個區間范圍,我們可以將它再變換到任意大小的2D矩形屏幕上。這個變換過程叫視口變換

現在我們嘗試求出以原點為中心的正交投影矩陣。定義可視立方體的寬度為w,高度為h,近平面位於z=n,遠平面位於z=f.

\[\begin{bmatrix} \frac{w}{2} & \frac{h}{2} & n & 1 \end{bmatrix} P_{ortho} = \begin{bmatrix} 1 & 1 & 0 & 1 \end{bmatrix} \\ \begin{bmatrix} 0 & 0 & f & 1 \end{bmatrix} P_{ortho} = \begin{bmatrix} 0 & 0 & 1 & 1 \end{bmatrix} \\ \]

由於x和y只受縮放影響,而w始終都未發生變化,故正交投影矩陣的這些參數都可以確定下來:

\[P_{ortho}=\begin{bmatrix} \frac{2}{w} & 0 & ? & 0 \\ 0 & \frac{2}{h} & ? & 0 \\ 0 & 0 & ? & 0 \\ 0 & 0 & ? & 1 \\ \end{bmatrix} \]

現在觀察這兩個式子:

\[\begin{bmatrix} \frac{w}{2} & \frac{h}{2} & n & 1 \end{bmatrix} \begin{bmatrix} ? \\ ? \\ ? \\ ? \end{bmatrix} = 0\\ \begin{bmatrix} 0 & 0 & f & 1 \end{bmatrix} \begin{bmatrix} ? \\ ? \\ ? \\ ? \end{bmatrix} = 1\\ \]

坐標點與第三行相乘的結果是向量的z值,而上面的方程得到的z值都是常數,顯然與w和h無關。故第一行和第二行都是0,然后我們設第三行和第四行的分別為A和B

\[P_{ortho}=\begin{bmatrix} \frac{2}{w} & 0 & 0 & 0 \\ 0 & \frac{2}{h} & 0 & 0 \\ 0 & 0 & A & 0 \\ 0 & 0 & B & 1 \\ \end{bmatrix} \]

由:

\[nA+B=0\\ fA+B=1 \]

解得:

\[A=\frac{1}{f-n}\\ B=\frac{-n}{f-n} \]

故以原點為中心的正交投影矩陣構造如下:

\[P_{ortho}=\begin{bmatrix} \frac{2}{w} & 0 & 0 & 0 \\ 0 & \frac{2}{h} & 0 & 0 \\ 0 & 0 & \frac{1}{f - n} & 0 \\ 0 & 0 & \frac{-n}{f - n} & 1 \\ \end{bmatrix} \]

經過投影矩陣變換后的深度值依然保持線性關系:

\[G(z)=\frac{z}{f-n}+\frac{-n}{f-n} \]

在DirectXMath中,對應的函數為XMMatrixOrthographicLH

XMMATRIX XMMatrixOrthographicLH(
    float ViewWidth,	// [In]待投影區域的寬度 
    float ViewHeight, 	// [In]待投影區域的高度
    float NearZ, 		// [In]近平面
    float FarZ);		// [In]遠平面

而對於離心的正交投影矩陣,無非就是在上面的正交投影的基礎上再將其平移回中心位置上。若規定投影立方體為[left, right] x [bottom, top] x [n, f],則有:

\[P_{ortho}=\begin{bmatrix} \frac{2}{right-left} & 0 & 0 & 0 \\ 0 & \frac{2}{top-bottom} & 0 & 0 \\ 0 & 0 & \frac{1}{f - n} & 0 \\ -\frac{right+left}{right-left} & -\frac{top+bottom}{top-bottom} & \frac{-n}{f - n} & 1 \\ \end{bmatrix} \]

left和right指定投影區域的左右邊界,top和bottom指定投影區域的上下邊界。

在DirectXMath中,對應的函數為XMMatrixOrthographicOffCenterLH

XMMATRIX XMMatrixOrthographicOffCenterLH(
    float ViewLeft,		// [In]待投影區域的左邊界 
    float ViewRight, 	// [In]待投影區域的右邊界
    float ViewBottom, 	// [In]待投影區域的下邊界
    float ViewTop, 		// [In]待投影區域的上邊界
    float NearZ, 		// [In]近平面
    float FarZ);		// [In]遠平面

按像素定義世界空間的優點是能夠做到按像素繪制到屏幕上,但缺點是不允許出現像素的拉伸,如果屏幕分辨率被改變,那投影區域也應當隨之改變。

透視投影變換

講完正交投影,接下來是投影變換。由下圖可知,在不限制遠近范圍的情況下,攝像機的視野就像是一個無限延伸的四棱錐。

和正交投影一樣,我們也需要定義近平面和遠平面來限制范圍,那定義出來的可視范圍實際上就是一個平截頭體(或叫視錐體更方便一些)。

不過這個視錐體對我們來說並不好處理,最好是想辦法把這個視錐體給擠壓成一個正交立方體,然后再進行正交投影變換

將視錐體擠壓成正交立方體

現在我們以yOz平面的視角來觀察這個視錐體。在視錐體上有一點(x, y, z),由於透視投影相當於從攝像機發射出一系列射線,射線上的任意一點都會投影到屏幕的同一高度位置上,故同樣高度的物體在近處顯得高,在遠處則顯得較矮。根據相似三角形的性質,我們可以求出(x,y,z)投影到平面z=n的位置為(x', y', n)。其中:

\[x'=\frac{xn}{z}\\ y'=\frac{yn}{z}\\ \]

然而我們用矩陣乘法並不能幫我們進行除z這一操作。我們需要利用前面提到的齊次坐標的性質進行擴維,把(x, y, z)變成(x, y, z, 1),那么(xz, yz, z^2, z)和前者也表示同一個頂點。即便是這樣,我們也不好用一個四維矩陣變換得到,因為我們需要的這個投影矩陣是一個與x, y, z無關的矩陣。

因此我們可以這樣做。首先令z=n,然后取近平面上一點(x, y, n, 1),那么(xn, yn, n^2, n)同樣也表示這個點,且近平面上的點投影到近平面依然是它本身(即沒有產生擠壓)。因此有:

\[\begin{bmatrix} x & y & n & 1 \end{bmatrix} P_{persp\rightarrow ortho} = \begin{bmatrix} xn & yn & n^2 & n \end{bmatrix} \]

由於x和y都是被縮放的,故只有它們對應的主元是n。對於w分量,我們這里讓第四列的第三行為1,而不是讓第四行的為n,原因在下面會提到。

\[P_{persp\rightarrow ortho} = \begin{bmatrix} n & 0 & ? & 0 \\ 0 & n & ? & 0 \\ 0 & 0 & ? & 1 \\ 0 & 0 & ? & 0 \\ \end{bmatrix} \]

現在再令z=f,點(x, y, f)會被擠壓成(xn/f, yn/f, f) (只有x和y才會被擠壓),而在齊次坐標系中(xn/f, yn/f, f, 1)和(xn, yn, f^2, f)又是同一個點。因此可以有:

\[\begin{bmatrix} x & y & f & 1 \end{bmatrix} P_{persp\rightarrow ortho} = \begin{bmatrix} xn & yn & f^2 & f \end{bmatrix} \]

結合這兩個方程,我們就可以知道這里只能讓第四列的第三行為1。

現在觀察z分量的變換:

\[\begin{bmatrix} x & y & n & 1 \end{bmatrix} \begin{bmatrix} ? \\ ? \\ ? \\ ? \end{bmatrix} = n^2\\ \begin{bmatrix} x & y & f & 1 \end{bmatrix} \begin{bmatrix} ? \\ ? \\ ? \\ ? \end{bmatrix} = f^2\\ \]

顯然x和y對結果並不會有影響,故可以確定第一行和第二行為0。此時可以令第三行和第四行分別設為A和B,則有:

\[P_{persp\rightarrow ortho} = \begin{bmatrix} n & 0 & 0 & 0 \\ 0 & n & 0 & 0 \\ 0 & 0 & A & 1 \\ 0 & 0 & B & 0 \\ \end{bmatrix} \]

由:

\[nA+B=n^2\\fA+B=f^2 \]

解得:

\[A=n+f\\B=-fn \]

故將視錐體擠壓成正交立方體(由透視到正交的變換)的矩陣如下:

\[P_{persp\rightarrow ortho}= \begin{bmatrix} n & 0 & 0 & 0 \\ 0 & n & 0 & 0 \\ 0 & 0 & n + f & 1 \\ 0 & 0 & -fn & 0 \\ \end{bmatrix} \]

這個矩陣用圖表現起來就像這樣:

但經過該矩陣變換后得到的深度值顯然不是線性的。

\[G(z)=A+\frac{B}{z} \]

關於深度值的變換函數,等最終的透視投影矩陣求出來后再討論。

垂直視場角(FOV)和寬高比(Aspect Ratio)

前面那個矩陣只是幫助我們將分散的射線擠壓成平行的射線(前提是n > 0),但我們還沒確定這個正交投影的范圍。雖然用[l, r]x[b, t]x[n, f]可以得到投影矩陣,但仍然不是很方便。我們需要引入兩個新的變量來間接進行描述。

垂直視場角(FOV)是指視錐體垂直方向的最大夾角。這就變相定義了這個視錐體近平面的高度了:

近平面高度為:

\[h=2n\cdot tan(\alpha/2) \]

然后通過寬高比(AspectRatio)把視錐體近平面寬度(同時也是正交立方體的寬高)也確定出來。令寬高比為r,則:

\[r=w/h \\ w = 2rn\cdot tan(\alpha / 2) \]

代入到正交投影矩陣有:

\[P_{ortho}=\begin{bmatrix} \frac{1}{rn\cdot tan(\alpha/2)} & 0 & 0 & 0 \\ 0 & \frac{1}{n\cdot tan(\alpha/2)} & 0 & 0 \\ 0 & 0 & \frac{1}{f - n} & 0 \\ 0 & 0 & \frac{-n}{f - n} & 1 \\ \end{bmatrix} \]

故最終的透視投影矩陣為:

\[\begin{align} P_{persp}&=P_{persp\rightarrow ortho} P_{ortho} \\ &= \begin{bmatrix} n & 0 & 0 & 0 \\ 0 & n & 0 & 0 \\ 0 & 0 & n + f & 1 \\ 0 & 0 & -fn & 0 \\ \end{bmatrix}\begin{bmatrix} \frac{1}{rn\cdot tan(\alpha/2)} & 0 & 0 & 0 \\ 0 & \frac{1}{n\cdot tan(\alpha/2)} & 0 & 0 \\ 0 & 0 & \frac{1}{f - n} & 0 \\ 0 & 0 & \frac{-n}{f - n} & 1 \\ \end{bmatrix} \\ &= \begin{bmatrix} \frac{1}{rtan(\alpha/2)} & 0 & 0 & 0 \\ 0 & \frac{1}{tan(\alpha/2)} & 0 & 0 \\ 0 & 0 & \frac{f}{f-n} & 1 \\ 0 & 0 & -\frac{nf}{f-n} & 0 \\ \end{bmatrix} \end{align} \]

歸一化深度值

待投影操作完畢后,所有的投影點都會位於2D投影窗口上,從而構成視覺上可見的2D圖像。看起來,我們似乎在此時就可以丟棄原始的3D z坐標了。然而,為了實現深度緩沖算法,我們仍需要保留這些3D深度信息。就像Direct3D希望將x、y坐標映射到歸一化范圍一樣,深度坐標也要被映射到歸一化區間[0, 1]以內。因此,我們必須構建一個保序函數g(z),用來把z坐標從[n, f]映射到區間[0, 1]。由於該函數具有保序性,即如果\(z_1, z_2 \in [n, f]\)\(z_1<z_2\),那么\(g(z_1)<g(z_2)\)。也就是說,對深度值進行歸一化處理后,深度關系保持不變。所以,在實現深度緩沖算法的過程中,我們仍能在歸一化區間內正確地比較出不同點之間的深度關系。

雖然通過一次縮放和平移操作,便能將z坐標從[n, f]區間映射到[0, 1]區間。但是此方法卻不能整合到我們當前的投影方案中去。在上式中,z坐標經過了以下變換的處理:

\[g(z)=\frac{f}{f-n}-\frac{nf}{(f-n)z} \]

根據函數g(z)的圖像可以看出,它是嚴格遞增(保序性)的非線性函數。同時,這也反映了g(z)大部分取值是由近平面附近的深度值所計算得出的。換言之,大多數的深度值被集中地映射到了取值區間中的一段較小的區域內。這將引發深度緩沖區的精度問題(由於計算機表示的數值范圍有限,使計算機不足以區分歸一化深度值之間的微小差異)。對此,我們一般建議令近平面與遠平面盡可能地接近,以改善深度值的問題。

在頂點乘以投影矩陣后但還未進行透視除法之前,幾何體會處於所謂的齊次裁減空間投影空間之中。待完成透視除法之后,使用規格化設備坐標(NDC)來表示幾何體了。

在DirectXMath中,我們使用下面的函數來獲得一個投影矩陣:

XMMATRIX XMMatrixPerspectiveFovLH( // 返回投影矩陣
    FLOAT FovAngleY,                   // 中心垂直弧度
    FLOAT AspectRatio,                 // 寬高比
    FLOAT NearZ,                       // 近平面距離
    FLOAT FarZ);                       // 遠平面距離

到了光柵化階段需要對上面算出的NDC坐標進行插值運算,由於g(z)函數是非線性的,使用線性插值法對深度的插值會出現問題。這就需要使用透視校正插值法來計算出正確的深度值,有興趣的話作為練習題自行了解。

2D的變換與投影成像

Direct3D不僅能夠繪制3D物體,還可以在后備緩沖區直接繪制2D平面物體。當然,也可以使用Direct2D來繪制2D物體(后續章節會涉及到Direct2D與Direct3D的交互)。

我們也可以借助3D繪制的思想,划分為世界變換、可脫離中心的正交投影變換(也可以細分成攝像機變換、以原點為中心的正交投影變換)、視口變換。

考慮有人可能會跳過3D部分來看2D,這里在內容上和3D會有些重復。

2D局部空間與2D世界空間

由於是2D世界空間,通常只有x軸和y軸會利用到,絕大部分情況下我們可以讓z值等於0。

和3D物體一樣,每個2D物體都有其自己的2D局部空間(局部坐標系),但是2D世界空間只有一個。

2D世界變換也囊括了縮放、旋轉和平移變換。通過世界變換,我們可以將物體模型從自身的局部坐標系轉換到世界坐標系中。當然,我們也可以理解為將物體從世界原點開始縮放、旋轉,然后平移到目標位置。這樣每一個物體都能在同一個世界空間中表示。

注意:對於不同的物體,需要使用不同的世界變換矩陣;而對於同一個頂點的所有頂點,都要使用一致的世界變換矩陣來變換。

從局部空間到世界空間的坐標變換矩陣即為前面提到的:

\[W=\begin{bmatrix} u_x & u_y & 0 & 0 \\ v_x & v_y & 0 & 0 \\ 0 & 0 & 1 & 0 \\ Q_x & Q_y & 0 & 1 \\ \end{bmatrix} \]

對於旋轉矩陣,我們只使用XMMatrixRotationZ

正交投影變換與規格化設備坐標(NDC)

對於硬件來說,使用統一的規格化設備坐標,可以無需提前知道屏幕的寬高比來簡化像素的映射操作。規格化設備坐標的可視取值范圍為:

\[-1\leq x\leq 1\\ -1\leq y\leq 1\\ 0\leq z\leq 1 \]

其中z值被用於深度測試,我們可以利用z值給2D繪制划分出更多的層次,比如說把場景分成16層,這樣z根據層級從最優先到最后可以依次設置為0, 1/15, ... , 1

在知道了最終的可視范圍后,我們該如何定義世界空間的長度呢?

一種做法是按像素來定義,即我們可以定義1個像素的寬高作為1個單位的x值和y值。這樣當觀察中心位於原點時,在分辨率為800x600的情況下四個可視邊界點分別為(-400, -300), (-400, 300), (400, 300), (400, -300)。然后我們需要使用正交投影變換(由函數XMMatrixOrthographicLH獲取),將x∈[-400, 400], y∈[-300, 300]的區間映射到NDC的可視范圍內。

如果你的游戲世界很大,超出一個屏幕,想要獲取離開屏幕中心區域的投影的話,可以使用下面帶平移的正交投影矩陣(由函數XMMatrixOrthographicOffCenterLH獲取):left和right指定投影區域的左右邊界,top和bottom指定投影區域的上下邊界。

按像素定義世界空間的優點是能夠做到按像素繪制到屏幕上,但缺點是不允許出現像素的拉伸,如果屏幕分辨率被改變,那投影區域也應當隨之改變。

如果你不打算按像素定義,也可以自己定義游戲內的世界尺度,比如可視游戲區域的范圍為寬200個單位,高150個單位。那么投影矩陣的寬需要固定在200個單位,高需要固定在150個單位。這種做法如果允許窗口被拉伸的話,那么游戲場景也將被拉伸,若要拉伸最好是等比例的拉伸。

注意:基於NDC的x軸朝右,y軸朝上,而你自己定義的世界坐標系可能是x軸朝右,y軸朝下。這需要進行一個負向的投影變換。

HLSL變量命名的一些約定

在閱讀HLSL代碼的時候你將會遇到諸如PosWPosH這樣帶字母后綴的變量名,那么它們有什么含義呢?其實該字母表達了當前點或向量所處的空間:

字母后綴 含義
L 處於物體局部空間(Local Space)
W 處於世界空間(World Space)
V 處於觀察空間(View Space)
H 處於齊次裁減空間(Homogeneous space)

練習題

粗體字為自定義題目

  1. 若有興趣,可以去了解一下繞任意軸旋轉的矩陣推導過程。本章不列出。
  2. 若有興趣,可以去了解一下透視校正插值法。本章不列出。
  3. 修改項目03,在窗口被拉伸的情況下修改透視投影矩陣的寬高比保證物體正常顯示。
  4. 修改項目03,觀察透視投影矩陣下不同FOV值的效果,以及將遠平面設置為5.0f的效果
  5. 修改項目03,讓攝像機位於點(1, 0, -2)觀察原點,並使用正交投影觀察效果
  6. 修改項目03,讓立方體中心位於點(3, 0, 0),然后立方體按Z軸繞原點逆時針旋轉,同時它也按Z軸繞立方體中心順時針旋轉,速度自擬。

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM