OpenGL座標變換

基礎概述

衆所周知,OpenGL是一個3D圖形庫,在終端設備上普遍使用。可是咱們的顯示設備都是2D平面,那麼OpenGL怎麼把3D圖形映射到2D屏幕那?這就是OpenGL座標變換所要完成的工做。 通常狀況下,咱們老是經過一個2D屏幕,觀察3D世界。所以,咱們實際看到的是3D世界在2D屏幕上的一個投影。經過OpenGL座標變換,咱們能夠在一個給定的觀察視角下,把3D物體投影到2D屏幕上,再通過後面的光柵化和片元着色,整個3D物體就映射成了2D屏幕上的像素。 OpenGL的座標變換流程以下所示: html

OpenGL座標變換過程

  • 第一行和第二行的模型變換、視變換和投影變換是頂點着色器負責完成的,它決定了一個圖元在3D空間中的位置。
  • 第三行的透視除法和視口變換是圖元裝配階段完成的,它決定了一個圖元在屏幕上的位置。

咱們先簡單看下整個流程:c++

  1. 首先,輸入頂點通常是以本地座標表示的3D模型。本地座標是爲了研究孤立的3D模型,座標原點通常都是模型的中心。每一個3D模型都有本身的本地座標系(Local Coordinate),互相之間沒有關聯。
  2. 當咱們須要同時渲染多個3D物體時,須要把不一樣的3D模型,變換到一個統一的座標系,這就是世界座標系(World Coordinate)。把物體從本地座標系變換到世界座標系,是經過一個Model矩陣完成的。模型矩陣能夠實現多種變換:平移(translation)、縮放(scale)、旋轉(rotation)、鏡像(reflection)、錯切(shear)等。例如:經過平移操做,咱們能夠在世界座標系的不一樣位置繪製同一個3D模型;
  3. 世界座標系中的多個物體共同構成了一個3D場景。從不一樣的角度觀察這個3D場景,咱們能夠看到不一樣的屏幕投影。OpenGL提出了攝像頭座標系的概念,即從攝像頭位置來觀察整個3D場景。把物體從世界座標系變換到攝像頭座標系,是經過一個View矩陣完成的。視圖矩陣定義了攝像頭的位置、方向向量和上向量等構成攝像頭座標系的基礎信息。View矩陣左乘世界座標系中頂點A的座標,就把頂點A變換到了攝像頭座標系。同一個3D物體,在世界座標系中,擁有一個世界座標;在攝像頭座標系中,擁有一個攝像頭座標,View變換就是負責把物體的座標從世界座標系變換到攝像頭座標系。
  4. 由於咱們是從一個2D屏幕觀察3D場景,而屏幕自己不是無限大的。因此當從攝像頭的角度觀察3D場景時,可能沒法看到整個場景,這時候就須要把看不到的場景裁減掉。投影變換就是負責裁剪工做,投影矩陣指定了一個視見體(View Frustum),在視見體內部的物體會出如今投影平面上,而在視見體以外的物體會被裁減掉。投影包括不少類型,OpenGL中主要考慮透視投影(Perspective Projection)和正交投影(Orthographic Projection),二者的區別在後面會詳細介紹。除此以外,經過Projection矩陣,能夠把物體從攝像頭座標系變換到裁剪座標系。在裁剪座標下,X、Y、Z各個座標軸上會指定一個可見範圍,超過可見範圍的頂點(vertex)都會被裁剪掉。
  5. 每一個裁剪座標系指定的可見範圍多是不一樣的,爲了獲得一個統一的座標系,須要對裁剪座標進行透視除法(Perspective Division),獲得NDC座標(Normalized Device Coordinates - 標準化設備座標系)。透視除法就是將裁剪座標除以齊次份量W,獲得NDC座標:
    獲得NDC座標
    在NDC座標系中,X、Y、Z各個座標軸的區間是[-1,1]。所以,能夠把NDC座標系看作做一個邊長爲2的立方體,全部的可見物體都在這個立方體內部。
  6. NDC座標系的範圍是[-1,1],可是咱們的屏幕尺寸是變幻無窮的,那麼OpenGL是如何把NDC座標映射到屏幕座標的那?視口變換(Viewport Transform)就是負責這塊工做的。在OpenGL中,咱們只須要經過glViewport指定繪製區域的座標和寬高,系統會幫咱們自動完成視口變換。通過視口變換,咱們就獲得了2D屏幕上的屏幕座標。須要注意的是:屏幕座標與屏幕的像素位置是不同的,屏幕座標是屏幕上任意一個頂點的精確位置,能夠是任意小數。可是像素位置只能是整數(具體的某個像素)。這裏的視口變換是從NDC座標變換到屏幕座標,尚未生成最終的像素位置。從屏幕座標映射到對應的像素位置,是後面光柵化完成的。

在OpenGL中,本地座標系、世界座標系和攝像頭座標系都屬於右手座標系,而最終的裁剪座標系和標準化設備座標系屬於左手座標系。 左右手座標系的示意圖以下所示,其中大拇指、食指、其他手指分別指向x,y,z軸的正方向。 git

左右手座標系

下面咱們分別來看下模型變換、視圖變換、投影變換和視口變換的推導和使用。github

模型變換

模型變換經過對3D模型執行平移、縮放、旋轉、鏡像、錯切等操做,來調整模型在世界座標系中的位置。模型變換是經過模型矩陣來完成的,咱們看下每種模型矩陣的推導過程。app

平移變換

平移就是將一個頂點A = (x,y,z),移動到另外一個位置A^* =(x^*,y^*,z^*),移動距離D = A^* - A = (x^* - x , y^*- y , z^*- z) = (d_x , d_y , d_z),因此A^*能夠用頂點A來表示:ide

A^* =(x^*,y^*,z^*)= (x+d_x,y+d_y,z+d_z)

經過平移矩陣來表示以下所示:wordpress

A^* = M_{translation} * A = \begin{bmatrix} 1 & 0 & 0 & d_x \\ 0 & 1 & 0 & d_y \\ 0 & 0 & 1 & d_z \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} * \begin{bmatrix} x \\ y \\ z \\ 1 \\ \end{bmatrix} = \begin{bmatrix} x+d_x \\ y+d_y \\ z+d_z \\ 1 \end{bmatrix}

其中M_{translation}就是平移變換矩陣,d_x表示X軸上的位移,d_y表示Y軸上的位移,d_z表示Z軸上的位移。 雖然看上去很繁瑣,可是在OpenGL中,咱們能夠經過GLM庫來實現平移變換。函數

glm::mat4 model; // 定義單位矩陣
model = glm::translate(model, glm::vec3(1.0f, 1.0f, 1.0f));
複製代碼

上述代碼定義了平移模型矩陣,表示在X、Y、Z軸上同時位移1。學習

縮放變換

能夠在X、Y和Z軸上對物體進行縮放,3個座標軸相互獨立。對於以原點爲中心的縮放,假設頂點A(x,y,z)在X、Y和Z軸上分別放大s_xs_ys_z倍,那麼能夠獲得放大後的頂點A^* =(s_x * x ,s_y * y , s_z * z),經過縮放矩陣來表示以下所示:spa

A^* = M_{scale} * A = 
\begin{bmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} * \begin{bmatrix} x \\ y \\ z \\ 1 \\ \end{bmatrix} = \begin{bmatrix} x * s_x \\ y * s_y \\ z * s_z \\ 1 \end{bmatrix}

其中M_{scale}就是縮放變換矩陣。 默認狀況下,縮放的中心點是座標原點,若是咱們要以指定頂點P(x_p , y_p , z_p)爲中心對物體進行縮放。那麼能夠按照以下步驟操做:

  1. 把頂點P移動到座標原點
  2. 以座標原點爲中心,旋轉指定角度
  3. 把頂點P移動回原來的位置 整個過程能夠簡化成一個矩陣:
M_{scale} = Translation(P) * Scale(\theta) * Translation(-P)

在OpenGL中,咱們能夠經過GLM庫來實現縮放變換:

glm::mat4 model; // 定義單位矩陣
model = glm::scale(model, glm::vec3(2.0f, 0.5f, 1.0f);
複製代碼

上述代碼定義了縮放模型矩陣,表示在X軸上fa2倍,Y軸上縮小0.5倍、Z軸上保持不變。

旋轉變換

在3D空間中,旋轉須要定義一個旋轉軸和一個角度。物體會沿着給定的旋轉軸旋轉指定角度。 咱們首先看下,沿着Z軸旋轉的旋轉矩陣是怎樣的? 假設有一個頂點P,原始座標爲 (x_o , y_o , z_o),離原點的距離是r,沿着Z軸順時針旋轉\theta度,新的座標爲(x , y , z),由於旋轉先後,z座標不變,因此暫時忽略,那麼能夠獲得:

x_0 = r * \cos(\alpha)
y_0 = r * \sin(\alpha)
x = r * \cos(\alpha + \theta) = r * \cos(\alpha) * \cos(\theta) - r * \sin(\alpha) * \sin(\theta) = x_o * \cos(\theta) - y_o * \sin(\theta)
y = r * \sin(\alpha + \theta) = r * \sin(\alpha) * \cos(\theta) + r * \cos(\alpha) * \sin(\theta) = x_o * \sin(\theta) + y_o * \cos(\theta)

根據上述公式,能夠獲得圍繞Z軸的旋轉矩陣:

\begin{bmatrix} 
\cos(\theta) & -\sin(\theta) & 0 & 0
\\ 
\sin(\theta) & \cos(\theta) & 0 & 0
\\ 
0 & 0 & 1 & 0
\\ 
0 & 0 & 0 & 1
\\ 
\end{bmatrix}

同理,能夠獲得圍繞X軸的旋轉矩陣:

\begin{bmatrix} 
1 & 0 & 0 & 0
\\
0 & \cos(\theta) & -\sin(\theta)  & 0
\\ 
0 & \sin(\theta) & \cos(\theta) & 0
\\ 
0 & 0 & 0 & 1
\\ 
\end{bmatrix}

同理,能夠獲得圍繞Y軸的旋轉矩陣:

\begin{bmatrix} 
\cos(\theta) & 0 & \sin(\theta) & 0
\\
0 & 1 & 0  & 0
\\ 
-\sin(\theta) & 0 & \cos(\theta) & 0
\\ 
0 & 0 & 0 & 1
\\ 
\end{bmatrix}

在OpenGL中,咱們能夠經過GLM庫來實現旋轉變換:

glm::mat4 model; // 定義單位矩陣
model = glm::rotate(model, glm::radians(-45.0f), glm::vec3(0.4f, 0.6f, 0.8f));
複製代碼

上述代碼表示:圍繞向量(0.4f, 0.6f, 0.8f),順時針旋轉45度。

在進行旋轉操做時,常常有一個困惑:順時針是正方向,仍是逆時針是正方向? 其實,存在一個左手規則和右手規則,能夠用於判斷物體繞軸旋轉時的正方向。

左手規則和右手規則
在OpenGl中,咱們使用右手規則,大拇指指向旋轉軸的正方向,其他手指的彎曲方向即爲旋轉正方向。因此上面的-45度是順時針旋轉。

模型變換的順序問題

由於矩陣不知足交換律,因此平移、旋轉和縮放的順序十分重要, 通常是先縮放、再旋轉、最後平移。固然最終仍是要考慮實際狀況。 還有一點須要注意,GLM操做矩陣的順序和實際效果是相反的。以下所示,雖然書寫順序是:平移、旋轉和縮放,可是實際最終的模型矩陣是:先縮放、再旋轉、最後平移。

glm::mat4 model; // 定義單位矩陣
model = glm::translate(model, glm::vec3(1.0f, 1.0f, 1.0f));
model = glm::rotate(model, glm::radians(-45.0f), glm::vec3(0.4f, 0.6f, 0.8f));
model = glm::scale(model, glm::vec3(2.0f, 0.5f, 1.0f);
複製代碼

視圖變換

通過模型變換,都有的座標都處於世界座標系中,本節就是以攝像頭的角度觀察整個世界空間。首先須要定義一個攝像頭座標系。 通常狀況下,定義一個座標系須要如下參數:

  1. 指定座標系的維度:2D、3D、4D等。
  2. 定義座標空間的軸向量,例如:X軸、Y軸、Z軸,這些向量稱爲基向量,基向量通常都是正交的。座標系中的全部頂點都是經過基向量表示的。
  3. 座標系的原點O,原點是座標系中全部其餘點的參考點。 簡單來講,座標系=(基向量,原點O)

同一個頂點,在不一樣的座標系中擁有不一樣的座標,那怎麼才能把世界座標系中的頂點座標,變換到攝像頭座標系那? 要實現不一樣座標系之間的座標轉換,須要計算一個變換矩陣。這個矩陣就是座標系A中的原點和基向量在另外一個座標系B下的座標表示。假設存在A座標系B座標系以及頂點V,那麼頂點V在A和B座標系下的座標變換公式以下所示:

[V]_A = [B]_A * [V]_B
[V]_B = [A]_B * [V]_A

簡單解釋一下:

頂點V在A座標系的座標 = B座標系的基向量和原點在A座標系下的座標表示構成的變換矩陣 * 頂點V在B座標系的座標;

頂點V在B座標系的座標 = A座標系的基向量和原點在B座標系下的座標表示構成的變換矩陣 * 頂點V在A座標系的座標
複製代碼

其中,[B]_A[A]_B互爲逆矩陣。因此座標系之間的切換,關鍵就是求出座標系之間互相表示的變換矩陣。那麼[A]_B矩陣應該怎麼計算那?假設座標系A的三個基向量和原點在B座標空間的單位座標向量分別是\vec{X^A_B}\vec{Y^A_B}\vec{Z^A_B}\vec{O^A_B},那麼[A]_B矩陣以下所示:

[A]_B = \begin{bmatrix} 
\vec{X^A_B[0]} & \vec{Y^A_B[0]} & \vec{Z^A_B[0]} & \vec{O^A_B[0]} 
\\ 
\vec{X^A_B[1]} & \vec{Y^A_B[1]} & \vec{Z^A_B[1]} & \vec{O^A_B[1]}
\\ 
\vec{X^A_B[2]} & \vec{Y^A_B[2]} & \vec{Z^A_B[2]} & \vec{O^A_B[2]} 
\\ 
0 & 0 & 0 & 1 
\\ 
\end{bmatrix}

[B]_A矩陣的計算方式也相似,此處再也不贅述。

下面咱們看下OpenGL的視圖變換矩陣是怎麼計算出來的? 如今存在兩個座標系:世界座標系W和攝像頭座標系E,還有一個頂點V,而且知道頂點V在世界座標系的座標 = (x_w,y_w,z_w),那麼頂點V在攝像頭座標系下的座標是多少那?根據上面的公式可知,咱們首先須要計算出[W]_E矩陣。

衆所周知,世界座標系的原點O = (0,0,0),三個基向量分別是,X軸:(1,0,0)、Y軸:(0,1,0)、Z軸:(0,0,1)。 理論上,定義一個攝像頭座標系,須要4個參數:

  1. 攝像頭在世界座標系中的位置(攝像頭座標系的原點)
  2. 攝像頭的觀察方向(攝像頭座標系的Z基向量)
  3. 一個指向攝像頭右側的向量(攝像頭座標系的X基向量)
  4. 一個指向攝像頭上方的向量(攝像頭座標系的Y基向量)。

經過上述4個參數,咱們實際上建立了一個三個單位軸相互垂直的,以攝像機位置爲原點的座標系。

攝像頭座標系

在使用過程當中,咱們只須要指定3個參數:

  1. 攝像機位置向量(\vec{eye})
  2. 攝像機指向的目標位置向量(\vec{target})
  3. 指向攝像頭上方的向量(\vec{up}

接下來是根據上面3個參數,推導出攝像頭座標系單位基向量的步驟:

  1. 首先計算攝像頭的方向向量\vec{forwrad}(方向向量是攝像頭座標系的Z軸正方向,和實際的觀察方向是相反的)。
\vec{forwrad}=(\vec{eye} - \vec{target})

而後計算出單位方向向量

\vec{forwrad_{norm}} = \frac {\vec{forwrad}}{|\vec{forwrad}|}
  1. 根據上向量\vec{up}和單位方向向量\vec{forwrad_{norm}}肯定攝像頭的右向量\vec{side}
\vec{side} = cross(\vec{forwrad_{norm}},\vec{up})

而後計算出單位右向量

\vec{side_{norm}} = \frac {\vec{side}}{|\vec{side}|}
  1. 根據單位右向量\vec{side_{norm}}和單位方向向量\vec{forwrad_{norm}}肯定單位上向量\vec{up_{norm}}
\vec{up_{norm}} = cross(\vec{side_{norm}},\vec{forwrad_{norm}})

這樣,就肯定了攝像頭座標系的三個單位基向量:\vec{side_{norm}}\vec{up_{norm}}\vec{forwrad_{norm}}以及攝像頭的位置向量\vec{eye}。這四個參數一塊兒肯定了攝像頭座標系:攝像頭位置是座標原點,單位右向量指向正X軸,單位上向量指向正Y軸,單位方向向量指向正Z軸。

如今咱們已經定義了一個攝像頭座標系,下一步就是把世界座標系中的頂點V = (x_w,y_w,z_w),變換到這個攝像頭座標系。根據上文可知,頂點V在攝像頭座標系E的座標計算過程以下所示:

[V]_E = [W]_E * [V]_W = [E]^{-1}_W * [V]_W

因此關鍵是計算變換矩陣[E]^{-1}_W,而根據攝像頭座標系的基向量和原點在世界空間中的座標表示,咱們能夠獲得[E]_W

[E]_W = \begin{bmatrix} 
\vec{side_{norm}}[0] & \vec{up_{norm}}[0] & \vec{forward_{norm}}[0] & \vec{eye}[0] 
\\ 
\vec{side_{norm}}[1] & \vec{up_{norm}}[1] & \vec{forward_{norm}}[1] & \vec{eye}[1] 
\\ 
\vec{side_{norm}}[2] & \vec{up_{norm}}[2] & \vec{forward_{norm}}[2] & \vec{eye}[2] 
\\
0 & 0 & 0 & 1 
\\ 
\end{bmatrix}

那麼最終的變換矩陣[E]^{-1}_W以下所示:

[E]^{-1}_W = \begin{bmatrix} 
\vec{side_{norm}}[0] & \vec{side_{norm}}[1] & \vec{side_{norm}}[2] & -dot(\vec{side_{norm}},\vec{eye})
\\ 
\vec{up_{norm}}[0] & \vec{up_{norm}}[1] & \vec{up_{norm}}[2] & -dot(\vec{up_{norm}},\vec{eye}) 
\\ 
\vec{forward_{norm}}[0] & \vec{forward_{norm}}[1] & \vec{forward_{norm}}[2] & dot(\vec{forward_{norm}},\vec{eye}) 
\\ 
0 & 0 & 0 & 1 
\\ 
\end{bmatrix}

其中,dot函數表示向量的點積,是一個標量。最終,頂點V在攝像頭座標系下的座標[V]_E以下所示:

[V]_E = 
\begin{bmatrix} 
\vec{side_{norm}}[0] & \vec{side_{norm}}[1] & \vec{side_{norm}}[2] & -dot(\vec{side_{norm}},\vec{eye})
\\ 
\vec{up_{norm}}[0] & \vec{up_{norm}}[1] & \vec{up_{norm}}[2] & -dot(\vec{up_{norm}},\vec{eye}) 
\\ 
\vec{forward_{norm}}[0] & \vec{forward_{norm}}[1] & \vec{forward_{norm}}[2] & dot(\vec{forward_{norm}},\vec{eye}) 
\\ 
0 & 0 & 0 & 1 
\\ 
\end{bmatrix}
* 
\begin{bmatrix} 
x_w
\\ 
y_w
\\ 
z_w
\\ 
1 
\\ 
\end{bmatrix}

上面的[E]^{-1}_W矩陣就是View變換矩陣。

下面看一個案例:假設攝像頭的座標是(0, 0, 3),攝像頭的觀察方向是世界座標系的原點(0,0,0),上向量是(0,1,0),頂點V在世界座標系的座標爲(1,1,0),那麼能夠計算出攝像頭座標系的基向量和原點以下所示:

  1. \vec{side_{norm}} = \begin{bmatrix} 
1 \\ 0 \\ 0 \\ \end{bmatrix}
  2. \vec{up_{norm}} = \begin{bmatrix} 
0 \\ 1 \\ 0 \\ \end{bmatrix}
  3. \vec{forward_{norm}} = \begin{bmatrix} 
0 \\ 0 \\ 1 \\ \end{bmatrix}
  4. \vec{eye} = \begin{bmatrix} 
0 \\ 0 \\ 3 \\ \end{bmatrix} 因此對應的View變換矩陣就是:
View = \begin{bmatrix} 
1 & 0 & 0 & 0
\\ 
0 & 1 & 0 & 0
\\ 
0 & 0 & 1 & -3
\\ 
0 & 0 & 1 & 1
\\ 
\end{bmatrix}

最後,頂點V在攝像頭座標系的座標就是:

[V]_E = \begin{bmatrix} 
1 & 0 & 0 & 0
\\ 
0 & 1 & 0 & 0
\\ 
0 & 0 & 1 & -3
\\ 
0 & 0 & 0 & 1
\\ 
\end{bmatrix}
* 
\begin{bmatrix} 
1 \\ 1 \\ 0 \\ 1 \\ 
\end{bmatrix}
.=
\begin{bmatrix} 
1 \\ 1 \\ -3 \\ 1 \\ 
\end{bmatrix}

雖然上述流程很複雜,但在OpenGL中,咱們能夠經過GLM庫定義View矩陣。針對上述案例,經過lookAt函數就能夠獲得View矩陣。

glm::mat4 view;
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f),glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
複製代碼

通過驗證,經過lookAt函數獲得View矩陣爲:

\begin{bmatrix} 
1 & 0 & 0 & 0
\\ 
0 & 1 & 0 & 0
\\ 
0 & 0 & 1 & -3
\\ 
0 & 0 & 0 & 1
\\ 
\end{bmatrix}

很顯然,經過lookAt函數獲得View矩陣和上面咱們推導的View矩陣是一致的。

投影變換

前面通過模型變換和視圖變換後,3D模型已經處於攝像頭座標系中。本節的投影變換將物體從攝像頭座標系變換到裁剪座標系,爲下一步的視口變換作好準備。 投影變換經過指定視見體來決定場景中哪些物體能夠呈如今屏幕上。在視見體中的物體會出如今投影平面上,而在視見體以外的物體不會出如今投影平面上。在OpenGL中,咱們主要考慮透視投影和正交投影,二者的區別以下所示:

透視投影和正交投影
上圖中,紅色和黃色球在視見體內,於是呈如今投影平面上;綠色球在視見體外,因此沒有投影到近平面上。除此以外,透視投影會根據物體的Z座標,決定物體在投影平面的大小,原則是:遠小近大,符合生活常識。而正交投影不考慮物體Z座標,全部物體在投影平面上保持原來的大小。

無論透視投影,仍是正交投影,均可以經過指定(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble near, GLdouble far)6個參數來指定視見體。(left,bottom)指定了近裁剪面左下角的座標,(right,top)指定了近裁剪面右上角的座標,-near表示近裁剪面,−far表示遠裁剪面。下面須要利用這6個參數,推導投影矩陣。

在攝像頭座標系下,攝像頭指向-z軸,因此近裁剪面z=−near,遠裁剪面z=−far。而且OpenGL是在近平面上成像的。

經過上述6個參數指定的透視投影變換以下所示:

透視投影變換

經過上述6個參數指定的正交投影變換以下所示:

正交投影變換

投影變換和透視除法後,攝像頭座標系中的頂點被映射到一個標準立方體中,即NDC座標系。其中X軸上:[left,right]映射到[−1,1],Y軸上:[bottom,top]映射到[-1,1]中,Z軸上:[near,far]映射到[−1,1],下面的矩陣推導會利用這裏的映射關係。下面咱們分別看下兩種投影矩陣的推導過程。

透視投影

透視投影和透視除法的座標映射以下所示:

投影映射關係

上圖中,攝像頭座標系是右手座標系,NDC是左手座標系,NDC座標系的Z軸指向攝像頭座標系的-Z軸方向。

假設頂點V在攝像頭座標系的座標 = (x_e , y_e , z_e , w_e),變換到裁剪座標系的座標 = (x_c , y_c , z_c , w_c),透視除法到NDC座標系的座標 = (x_n , y_n , z_n , w_n)。咱們的目標是計算出投影矩陣M_{projection},使得:

\begin{bmatrix} 
x_c \\ y_c \\ z_c \\ w_c \\ 
\end{bmatrix}
= M_{projection} * 
\begin{bmatrix} 
x_e \\ y_e \\ z_e \\ w_e \\ 
\end{bmatrix}

同時,可獲得透視除法的變換:

\begin{bmatrix} 
x_n \\ y_n \\ z_n \\ 
\end{bmatrix}
.= 
\begin{bmatrix} 
x_c/w_c \\ y_c/w_c \\ z_c/w_c \\ 
\end{bmatrix}

首先,咱們看下投影矩陣M_{projection}對X軸和Y軸的變換。頂點P投影到近平面後,獲得頂點P_{near} = (x^p , y^p , −near)。具體示意圖以下所示:

X軸映射
Y軸映射
利用三角形的類似性,經過左圖可知:

\frac {-near} {z_e} = \frac {x^p} {x_e}

因此,能夠獲得X軸上的投影值:

x^p = \frac {near * x_e} {-z_e} 
\tag{1}

同理,經過右圖,能夠獲得Y軸上的投影值:

y^p = \frac {near * y_e} {-z_e} 
\tag{2}

由(1)(2)公式能夠發現,他們都除以了{-z_e}份量,而且與之成反比。這能夠做爲透視除法的一個線索,所以咱們的矩陣M_{projection}以下所示:

\begin{bmatrix} 
* & * & * & * \\ 
* & * & * & * \\ 
* & * & * & * \\ 
0 & 0 & -1 & 0 \\
\end{bmatrix}

也就是說w_c = -z_e

接下來,咱們根據x^py^p與NDC座標的映射關係,推導出M_{projection}的前兩行。 x^p知足[left,right]映射到[-1,1],以下所示:

Mapping from $x_p$ to $x_n$
由於是線性映射關係,因此能夠設置線性方程,求出係數 K和常量 P

x_n = K * x_p + P

經過代入[left,right]到[-1,1]的映射關係,能夠獲得線性方程:

x_n = \frac {2}{right - left} * x_p - \frac {right + left}{right - left}
\tag{3}

將上面的公式(1)代入公式(3),可得:

x_n = 
\frac {2 * x_e * near}{right - left} * \frac {1}{-z_e} - \frac {right + left}{right - left} 
= \frac {\frac{2 * x_e * near}{right - left} + \frac {right + left}{right - left} * z_e}{-z_e}
\tag{4}

又由於w_c = -z_e,因此能夠進一步簡化公式:

x_c = \frac {2 * near}{right - left} * x_e + \frac {right + left}{right - left} * z_e
\tag{5}

根據公式(5),能夠進一步獲得矩陣M_{projection}

\begin{bmatrix} 
\frac {2 * near}{right - left} & 0 & \frac {right + left}{right - left} & 0 \\ 
* & * & * & * \\ 
* & * & * & * \\ 
0 & 0 & -1 & 0 \\
\end{bmatrix}

OK,繼續看下y^{p}的映射關係:知足[bottom,top]映射到[-1,1],以下所示:

Mapping from $y_p$ to $y_n$
同理,根據 y^{p}線性映射關係,能夠獲得以下公式:

y_n = 
\frac {2 * y_e * near}{top - bottom} * \frac {1}{-z_e} - \frac {top + bottom}{top - bottom} 
= \frac {\frac{2 * y_e * near}{top - bottom} + \frac {top + bottom}{top - bottom} * z_e}{-z_e}
\tag{6}

又由於w_c = -z_e,因此能夠進一步簡化公式:

y_c = \frac {2 * near}{top - bottom} * y_e + \frac {top + bottom}{top - bottom} * z_e
\tag{7}

根據公式(7),能夠進一步獲得矩陣M_{projection}

\begin{bmatrix} 
\frac {2 * near}{right - left} & 0 & \frac {right + left}{right - left} & 0 \\ 
0 & \frac {2 * near}{top - bottom} & \frac {top + bottom}{top - bottom} & 0 \\ 
* & * & * & * \\ 
0 & 0 & -1 & 0 \\
\end{bmatrix}

接下來須要計算z_n的係數,這和x_ny_n的計算方式不一樣,由於攝像頭座標系的座標z_e投影到近平面後老是-near。同時咱們知道z_n與x和y份量無關,所以,可進一步獲得矩陣M_{projection}

\begin{bmatrix} 
x_c \\ y_c \\ z_c \\ w_c 
\end{bmatrix}
.=
\begin{bmatrix} 
\frac {2 * near}{right - left} & 0 & \frac {right + left}{right - left} & 0 \\ 
0 & \frac {2 * near}{top - bottom} & \frac {top + bottom}{top - bottom} & 0 \\ 
0 & 0 & A & B \\ 
0 & 0 & -1 & 0 \\
\end{bmatrix}
* 
\begin{bmatrix} 
x_e \\ y_e \\ z_e \\ w_e 
\end{bmatrix}

由於w_c = -z_e,因此能夠獲得:

z_n = \frac {A * z_e + B * w_e}{-z_e}

又由於攝像頭座標系中w_e = 1,因此進一步獲得:

z_n = \frac {A * z_e + B }{-z_e}

一樣的,代入z_ez_n的映射關係:[-near,-far]映射到[-1,1],可獲得:

z_n = \frac {-\frac{far + near}{far - near} * z_e - \frac {2 * far * near}{far - near}}{-z_e}
\tag{8}

又由於w_c = -z_e,能夠進一步簡化獲得z_cz_e的關係:

z_c = -\frac{far + near}{far - near} * z_e - \frac {2 * far * near}{far - near}
\tag{9}

由公式(9)就能夠知道A和B了,所以,最終的矩陣M_{projection}

\begin{bmatrix} 
\frac {2 * near}{right - left} & 0 & \frac {right + left}{right - left} & 0 \\ 
0 & \frac {2 * near}{top - bottom} & \frac {top + bottom}{top - bottom} & 0 \\ 
0 & 0 & -\frac{(far + near)}{far - near} & -\frac {2 * far * near}{far - near} \\ 
0 & 0 & -1 & 0 \\
\end{bmatrix}

通常狀況下,投影的視見體都是對稱的,即知足left=−right,bottom=−top,那麼能夠獲得:

\begin
{cases} right + left = 0 
\\ 
right - left = 2 * right = width 
\end{cases}
\begin
{cases} top + bottom = 0 
\\ 
top - bottom = 2 * top = height 
\end{cases}

則矩陣M_{projection}能夠簡化爲:

\begin{bmatrix} 
\frac {near}{right} & 0 & 0 & 0 \\ 
0 & \frac {near}{top} & 0 & 0 \\ 
0 & 0 & -\frac{(far + near)}{far - near} & -\frac {2 * far * near}{far - near} \\ 
0 & 0 & -1 & 0 \\
\end{bmatrix}

除了能夠經過(left,right,bottom,top,near,far)指定透視投影矩陣外,還能夠經過函數glm::perspective指定視角(Fov)、寬高比(Aspect)、近平面(Near)、遠平面(Far)來生成透視投影矩陣,以下所示,指定了45度視角,近平面和遠平面分別是0.1f和100.0f:

glm::mat4 proj = glm::perspective(glm::radians(45.0f), width/height, 0.1f, 100.0f);
複製代碼

觀察視角的示意圖以下所示:

觀察視角
經過視角指定的透視投影變換以下所示:
經過視角指定的透視投影變換
經過視角指定的透視投影矩陣的視見體是對稱的:
透視投影矩陣的對稱視見體
由上圖可知,近平面的寬和高以下所示:

Height = 2 * near * \tan(\frac{\theta}{2}) \tag{10}
Width = height * Aspect  \tag{11}

由於視見體是對稱的,因此把公式(10)(11)代入已有的M_{projection}矩陣,能夠獲得由視角Fov表示的M_{projection}矩陣,以下所示:

M_{projection} = 
\begin{bmatrix} 
\frac {\cot(\frac{\theta}{2})}{Aspect} & 0 & 0 & 0 \\ 
0 & \cot(\frac{\theta}{2}) & 0 & 0 \\ 
0 & 0 & -\frac{(far + near)}{far - near} & -\frac {2 * far * near}{far - near} \\ 
0 & 0 & -1 & 0 \\
\end{bmatrix}

經過M_{projection}矩陣左乘攝像頭座標系中的頂點,就把這些頂點變換到了裁剪座標系。而後再通過透視除法,就變換到了NDC座標系。

正交投影

相比於透視投影矩陣,正交投影矩陣要簡單一些,以下所示:

正交投影
由於正交投影不考慮遠小近大的狀況,因此正交投影矩陣 M_{orthographic}的第4行始終爲 [0 , 0 , 0 , 1]

對於正交投影變換,投影到近平面的座標(x_p , y_p) = (x_e , y_e),所以能夠直接利用x_ex_ny_ey_nz_ez_n的線性映射關係,求出線性方程係數。X、Y、Z軸的映射關係以下所示:

映射關係 映射值 示意圖
x_ex_n的映射關係 [left , right] \iff [-1 , 1]
$x_e$與$x_n$的映射關係
y_ey_n的映射關係 [bottom , top] \iff [-1 , 1]
$y_e$與$y_n$的映射關係
z_ez_n的映射關係 [near , far] \iff [-1 , 1]
$z_e$與$z_n$的映射關係

根據上述的映射關係,同時攝像頭座標系的w_e = 1,能夠獲得三個線性方程,以下所示:

x_c = x_n = \frac{2}{right - left} * x_e - \frac{right + left}{right - left}
\tag{$x_n$和$x_e$的映射關係}
y_c = y_n = \frac{2}{top - bottom} * y_e - \frac{top + bottom}{top - bottom}
\tag{$y_n$和$y_e$的映射關係}
z_c = z_n = \frac{-2}{far - near} * z_e - \frac{far + near}{far - near}
\tag{$z_n$和$z_e$的映射關係}

根據上述3個線性方程,能夠獲得正交投影矩陣M_{orthographic}

M_{orthographic} =
\begin{bmatrix} 
\frac{2}{right - left} & 0 & 0 & - \frac{right + left}{right - left} \\ 
0 & \frac{2}{top - bottom} & 0 & - \frac{top + bottom}{top - bottom} \\ 
0 & 0 & \frac{-2}{far - near} & - \frac{far + near}{far - near} \\ 
0 & 0 & 0 & 1 \\
\end{bmatrix}

若是視見體是對稱的,即知足left=−right,bottom=−top,那麼能夠獲得:

\begin
{cases} right + left = 0 
\\ 
right - left = 2 * right = width 
\end{cases}
\begin
{cases} top + bottom = 0 
\\ 
top - bottom = 2 * top = height 
\end{cases}

則正交投影矩陣M_{orthographic}能夠進一步簡化爲:

M_{orthographic} =
\begin{bmatrix} 
\frac{1}{right} & 0 & 0 & 0 \\ 
0 & \frac{1}{top} & 0 & 0 \\ 
0 & 0 & \frac{-2}{far - near} & - \frac{far + near}{far - near} \\ 
0 & 0 & 0 & 1 \\
\end{bmatrix}

視口變換

通過投影變換和透視除法後,咱們裁減掉了不可見物體,獲得了NDC座標。最後一步是把NDC座標映射到屏幕座標(x_sy_s , z_s)。以下所示:

NDC座標變換到屏幕座標
在映射到屏幕座標時,咱們須要指定窗口的位置、寬高和深度。以下所示:

//指定窗口的位置和寬高
glViewport(GLint x , GLint y , GLsizei width , GLsizei height); 
//指定窗口的深度
glDepthRangef(GLclampf near , GLclampf far);
複製代碼

那麼能夠NDC座標和屏幕座標的線性映射關係:

映射關係 映射值
x_nx_s的映射關係 [-1 , 1] \iff [x , x + width]
y_ny_s的映射關係 [-1 , 1] \iff [y , y + height]
z_nz_s的映射關係 [-1 , 1] \iff [near , far]

所以,能夠設置線性方程,求出係數K和常量P

Y = K * X + P

把上述映射關係代入線性方程,能夠獲得各個份量的參數值。

座標份量 線性方程的係數K 線性方程的常量P
X份量線性方程 \frac {width} {2} x + \frac {width} {2}
Y份量線性方程 \frac {height} {2} y + \frac {height} {2}
Z份量線性方程 \frac {far - near} {2} \frac {far + near} {2}

經過上述各個座標份量值,能夠獲得視口變換矩陣:

ViewPort = 
\begin{bmatrix} 
\frac {width} {2} & 0 & 0 & x + \frac {width} {2}
\\ 
0 & \frac {height} {2} & 0 & y + \frac {height} {2}
\\ 
0 & 0 & \frac {far - near} {2} &  \frac {far + near} {2} 
\\ 
0 & 0 & 0 & 1
\\ 
\end{bmatrix}

所以,經過ViewPort矩陣左乘NDC座標,就獲得了屏幕座標。

對於2D屏幕,nearfar通常爲0。所以ViewPort矩陣的第三行都是0。因此通過視口變換後,屏幕座標的Z值都是0。

至此,OpenGL的整個座標變換過程都介紹完了,關鍵仍是要多實踐、實踐、實踐!!!

參考文檔

  1. Cmd Markdown 公式指導手冊
  2. 齊次座標系入門級思考
  3. 仿射變換與齊次座標
  4. 座標和變換的數學基礎
  5. OpenGL學習腳印
相關文章
相關標籤/搜索