衆所周知,OpenGL是一個3D圖形庫,在終端設備上普遍使用。可是咱們的顯示設備都是2D平面,那麼OpenGL怎麼把3D圖形映射到2D屏幕那?這就是OpenGL座標變換所要完成的工做。 通常狀況下,咱們老是經過一個2D屏幕,觀察3D世界。所以,咱們實際看到的是3D世界在2D屏幕上的一個投影。經過OpenGL座標變換,咱們能夠在一個給定的觀察視角下,把3D物體投影到2D屏幕上,再通過後面的光柵化和片元着色,整個3D物體就映射成了2D屏幕上的像素。 OpenGL的座標變換流程以下所示: html
- 第一行和第二行的模型變換、視變換和投影變換是頂點着色器負責完成的,它決定了一個圖元在3D空間中的位置。
- 第三行的透視除法和視口變換是圖元裝配階段完成的,它決定了一個圖元在屏幕上的位置。
咱們先簡單看下整個流程:c++
Model
矩陣完成的。模型矩陣能夠實現多種變換:平移(translation)、縮放(scale)、旋轉(rotation)、鏡像(reflection)、錯切(shear)等。例如:經過平移操做,咱們能夠在世界座標系的不一樣位置繪製同一個3D模型;View
矩陣完成的。視圖矩陣定義了攝像頭的位置、方向向量和上向量等構成攝像頭座標系的基礎信息。View
矩陣左乘世界座標系中頂點A的座標,就把頂點A變換到了攝像頭座標系。同一個3D物體,在世界座標系中,擁有一個世界座標;在攝像頭座標系中,擁有一個攝像頭座標,View
變換就是負責把物體的座標從世界座標系變換到攝像頭座標系。Projection
矩陣,能夠把物體從攝像頭座標系變換到裁剪座標系。在裁剪座標下,X、Y、Z各個座標軸上會指定一個可見範圍,超過可見範圍的頂點(vertex)都會被裁剪掉。W
,獲得NDC座標:
glViewport
指定繪製區域的座標和寬高,系統會幫咱們自動完成視口變換。通過視口變換,咱們就獲得了2D屏幕上的屏幕座標。須要注意的是:屏幕座標與屏幕的像素位置是不同的,屏幕座標是屏幕上任意一個頂點的精確位置,能夠是任意小數。可是像素位置只能是整數(具體的某個像素)。這裏的視口變換是從NDC座標變換到屏幕座標,尚未生成最終的像素位置。從屏幕座標映射到對應的像素位置,是後面光柵化完成的。在OpenGL中,本地座標系、世界座標系和攝像頭座標系都屬於右手座標系,而最終的裁剪座標系和標準化設備座標系屬於左手座標系。 左右手座標系的示意圖以下所示,其中大拇指、食指、其他手指分別指向x,y,z軸的正方向。 git
![]()
下面咱們分別來看下模型變換、視圖變換、投影變換和視口變換的推導和使用。github
模型變換經過對3D模型執行平移、縮放、旋轉、鏡像、錯切等操做,來調整模型在世界座標系中的位置。模型變換是經過模型矩陣來完成的,咱們看下每種模型矩陣的推導過程。app
平移就是將一個頂點A = (x,y,z),移動到另外一個位置 =(
,
,
),移動距離D =
- A = (
- x ,
- y ,
- z) = (
,
,
),因此
能夠用頂點A來表示:ide
經過平移矩陣來表示以下所示:wordpress
其中就是平移變換矩陣,
表示X軸上的位移,
表示Y軸上的位移,
表示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軸上分別放大、
、
倍,那麼能夠獲得放大後的頂點
=(
* x ,
* y ,
* z),經過縮放矩陣來表示以下所示:spa
其中就是縮放變換矩陣。 默認狀況下,縮放的中心點是座標原點,若是咱們要以指定頂點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,原始座標爲 ( ,
,
),離原點的距離是
,沿着Z軸順時針旋轉
度,新的座標爲(
,
,
),由於旋轉先後,z座標不變,因此暫時忽略,那麼能夠獲得:
根據上述公式,能夠獲得圍繞Z軸的旋轉矩陣:
同理,能夠獲得圍繞X軸的旋轉矩陣:
同理,能夠獲得圍繞Y軸的旋轉矩陣:
在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);
複製代碼
通過模型變換,都有的座標都處於世界座標系中,本節就是以攝像頭的角度觀察整個世界空間。首先須要定義一個攝像頭座標系。 通常狀況下,定義一個座標系須要如下參數:
基向量
,基向量通常都是正交的。座標系中的全部頂點都是經過基向量表示的。座標系=(基向量,原點O)
同一個頂點,在不一樣的座標系中擁有不一樣的座標,那怎麼才能把世界座標系中的頂點座標,變換到攝像頭座標系那? 要實現不一樣座標系之間的座標轉換,須要計算一個變換矩陣。這個矩陣就是座標系A中的原點和基向量在另外一個座標系B下的座標表示。假設存在A座標系和B座標系以及頂點V,那麼頂點V在A和B座標系下的座標變換公式以下所示:
簡單解釋一下:
頂點V在A座標系的座標 = B座標系的基向量和原點在A座標系下的座標表示構成的變換矩陣 * 頂點V在B座標系的座標;
頂點V在B座標系的座標 = A座標系的基向量和原點在B座標系下的座標表示構成的變換矩陣 * 頂點V在A座標系的座標
複製代碼
其中,和
互爲逆矩陣。因此座標系之間的切換,關鍵就是求出座標系之間互相表示的變換矩陣。那麼
矩陣應該怎麼計算那?假設座標系A的三個基向量和原點在B座標空間的單位座標向量分別是
、
、
和
,那麼
矩陣以下所示:
矩陣的計算方式也相似,此處再也不贅述。
下面咱們看下OpenGL的視圖變換矩陣是怎麼計算出來的? 如今存在兩個座標系:世界座標系W
和攝像頭座標系E
,還有一個頂點V,而且知道頂點V在世界座標系的座標 = (,
,
),那麼頂點V在攝像頭座標系下的座標是多少那?根據上面的公式可知,咱們首先須要計算出
矩陣。
衆所周知,世界座標系的原點O = (0,0,0),三個基向量分別是,X軸:(1,0,0)、Y軸:(0,1,0)、Z軸:(0,0,1)。 理論上,定義一個攝像頭座標系,須要4個參數:
經過上述4個參數,咱們實際上建立了一個三個單位軸相互垂直的,以攝像機位置爲原點的座標系。
在使用過程當中,咱們只須要指定3個參數:
接下來是根據上面3個參數,推導出攝像頭座標系單位基向量的步驟:
而後計算出單位方向向量
而後計算出單位右向量
這樣,就肯定了攝像頭座標系的三個單位基向量:、
和
以及攝像頭的位置向量
。這四個參數一塊兒肯定了攝像頭座標系:攝像頭位置是座標原點,單位右向量指向正X軸,單位上向量指向正Y軸,單位方向向量指向正Z軸。
如今咱們已經定義了一個攝像頭座標系,下一步就是把世界座標系中的頂點V = (,
,
),變換到這個攝像頭座標系。根據上文可知,頂點V在攝像頭座標系
E
的座標計算過程以下所示:
因此關鍵是計算變換矩陣,而根據攝像頭座標系的基向量和原點在世界空間中的座標表示,咱們能夠獲得
:
那麼最終的變換矩陣以下所示:
其中,dot
函數表示向量的點積,是一個標量。最終,頂點V在攝像頭座標系下的座標以下所示:
上面的矩陣就是
View
變換矩陣。
下面看一個案例:假設攝像頭的座標是(0, 0, 3),攝像頭的觀察方向是世界座標系的原點(0,0,0),上向量是(0,1,0),頂點V在世界座標系的座標爲(1,1,0),那麼能夠計算出攝像頭座標系的基向量和原點以下所示:
View
變換矩陣就是:最後,頂點V在攝像頭座標系的座標就是:
雖然上述流程很複雜,但在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
矩陣爲:
很顯然,經過lookAt
函數獲得View
矩陣和上面咱們推導的View
矩陣是一致的。
前面通過模型變換和視圖變換後,3D模型已經處於攝像頭座標系中。本節的投影變換將物體從攝像頭座標系變換到裁剪座標系,爲下一步的視口變換作好準備。 投影變換經過指定視見體來決定場景中哪些物體能夠呈如今屏幕上。在視見體中的物體會出如今投影平面上,而在視見體以外的物體不會出如今投影平面上。在OpenGL中,咱們主要考慮透視投影和正交投影,二者的區別以下所示:
無論透視投影,仍是正交投影,均可以經過指定(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在攝像頭座標系的座標 = ( ,
,
,
),變換到裁剪座標系的座標 = (
,
,
,
),透視除法到NDC座標系的座標 = (
,
,
,
)。咱們的目標是計算出投影矩陣
,使得:
同時,可獲得透視除法的變換:
首先,咱們看下投影矩陣對X軸和Y軸的變換。頂點P投影到近平面後,獲得頂點
= (
,
, −near)。具體示意圖以下所示:
因此,能夠獲得X軸上的投影值:
同理,經過右圖,能夠獲得Y軸上的投影值:
由(1)(2)公式能夠發現,他們都除以了份量,而且與之成反比。這能夠做爲透視除法的一個線索,所以咱們的矩陣
以下所示:
也就是說。
接下來,咱們根據、
與NDC座標的映射關係,推導出
的前兩行。
知足[left,right]映射到[-1,1],以下所示:
K
和常量
P
。
經過代入[left,right]到[-1,1]的映射關係,能夠獲得線性方程:
將上面的公式(1)代入公式(3),可得:
又由於,因此能夠進一步簡化公式:
根據公式(5),能夠進一步獲得矩陣:
OK,繼續看下的映射關係:知足[bottom,top]映射到[-1,1],以下所示:
又由於,因此能夠進一步簡化公式:
根據公式(7),能夠進一步獲得矩陣:
接下來須要計算的係數,這和
、
的計算方式不一樣,由於攝像頭座標系的座標
投影到近平面後老是-near。同時咱們知道
與x和y份量無關,所以,可進一步獲得矩陣
:
由於,因此能夠獲得:
又由於攝像頭座標系中 = 1,因此進一步獲得:
一樣的,代入與
的映射關係:[-near,-far]映射到[-1,1],可獲得:
又由於,能夠進一步簡化獲得
和
的關係:
由公式(9)就能夠知道A和B了,所以,最終的矩陣:
通常狀況下,投影的視見體都是對稱的,即知足left=−right,bottom=−top,那麼能夠獲得:
則矩陣能夠簡化爲:
除了能夠經過(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);
複製代碼
觀察視角的示意圖以下所示:
由於視見體是對稱的,因此把公式(10)(11)代入已有的矩陣,能夠獲得由視角Fov表示的
矩陣,以下所示:
經過矩陣左乘攝像頭座標系中的頂點,就把這些頂點變換到了裁剪座標系。而後再通過透視除法,就變換到了NDC座標系。
相比於透視投影矩陣,正交投影矩陣要簡單一些,以下所示:
對於正交投影變換,投影到近平面的座標( ,
) = (
,
),所以能夠直接利用
與
、
與
、
與
的線性映射關係,求出線性方程係數。X、Y、Z軸的映射關係以下所示:
映射關係 | 映射值 | 示意圖 |
---|---|---|
![]() ![]() |
[left , right] ![]() |
![]() |
![]() ![]() |
[bottom , top] ![]() |
![]() |
![]() ![]() |
[near , far] ![]() |
![]() |
根據上述的映射關係,同時攝像頭座標系的 = 1,能夠獲得三個線性方程,以下所示:
根據上述3個線性方程,能夠獲得正交投影矩陣:
若是視見體是對稱的,即知足left=−right,bottom=−top,那麼能夠獲得:
則正交投影矩陣能夠進一步簡化爲:
通過投影變換和透視除法後,咱們裁減掉了不可見物體,獲得了NDC座標。最後一步是把NDC座標映射到屏幕座標( ,
,
)。以下所示:
//指定窗口的位置和寬高
glViewport(GLint x , GLint y , GLsizei width , GLsizei height);
//指定窗口的深度
glDepthRangef(GLclampf near , GLclampf far);
複製代碼
那麼能夠NDC座標和屏幕座標的線性映射關係:
映射關係 | 映射值 |
---|---|
![]() ![]() |
[-1 , 1] ![]() |
![]() ![]() |
[-1 , 1] ![]() |
![]() ![]() |
[-1 , 1] ![]() |
所以,能夠設置線性方程,求出係數K
和常量P
。
把上述映射關係代入線性方程,能夠獲得各個份量的參數值。
座標份量 | 線性方程的係數K |
線性方程的常量P |
---|---|---|
X份量線性方程 | ![]() |
x + ![]() |
Y份量線性方程 | ![]() |
y + ![]() |
Z份量線性方程 | ![]() |
![]() |
經過上述各個座標份量值,能夠獲得視口變換矩陣:
所以,經過ViewPort矩陣左乘NDC座標,就獲得了屏幕座標。
對於2D屏幕,
near
和far
通常爲0。所以ViewPort
矩陣的第三行都是0。因此通過視口變換後,屏幕座標的Z值都是0。
至此,OpenGL的整個座標變換過程都介紹完了,關鍵仍是要多實踐、實踐、實踐!!!