版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2017.07.26 |
前言
OpenGL 圖形庫項目中一直也沒用過,最近也想學著使用這個圖形庫,感覺還是很有意思,也就自然想著好好的總結一下,希望對大家能有所幫助。
1. OpenGL 圖形庫使用(一) —— 概念基礎
2. OpenGL 圖形庫使用(二) —— 渲染模式、對象、擴展和狀態機
3. OpenGL 圖形庫使用(三) —— 著色器、數據類型與輸入輸出
4. OpenGL 圖形庫使用(四) —— Uniform及更多屬性
5. OpenGL 圖形庫使用(五) —— 紋理
變換
??前面我們知道了如何創建一個物體、著色以及加入紋理,但是他們都是靜態的物體,如果想要物體發生運動,需要另外一個方案,那就是使用多個矩陣Matrix
對象可以更好的變換Transform
一個物體。下面我們繼續深入的了解下向量。
向量
??向量最基本的定義就是一個方向。或者更正式的說,向量有一個方向(Direction)
和大小(Magnitude,也叫做強度或長度)
。
1. 向量與標量運算
標量(Scalar)
只是一個數字(或者說是僅有一個分量的向量)。當把一個向量加/減/乘/除一個標量,我們可以簡單的把向量的每個分量分別進行該運算
2. 向量取反
對一個向量取反(Negate)
會將其方向逆轉。一個指向東北的向量取反后就指向西南方向了。我們在一個向量的每個分量前加負號就可以實現取反了(或者說用-1數乘該向量)。
3. 向量加減
向量的加法可以被定義為是分量的(Component-wise)
相加,即將一個向量中的每一個分量加上另一個向量的對應分量。
4. 長度
我們使用勾股定理(Pythagoras Theorem)
來獲取向量的長度(Length)/大小
(Magnitude)。還有一個特殊類型的向量叫做單位向量
(Unit Vector)`,單位向量有一個特別的性質——它的長度是1。
5. 向量相乘
兩個向量相乘是一種很奇怪的情況。普通的乘法在向量上是沒有定義的,因為它在視覺上是沒有意義的。但是在相乘的時候我們有兩種特定情況可以選擇:一個是點乘(Dot Product)
,記作vˉ? kˉ
,另一個是叉乘(Cross Product)
,記作vˉ× kˉ
。
1. 點乘
兩個向量的點乘等于它們的數乘結果乘以兩個向量之間夾角的余弦值。
2. 叉乘
叉乘只在3D空間中有定義,它需要兩個不平行向量作為輸入,生成一個正交于兩個輸入向量的第三個向量。如果輸入的兩個向量也是正交的,那么叉乘之后將會產生3個互相正交的向量。接下來的教程中這會非常有用。下面的圖片展示了3D空間中叉乘的樣子。下面看一個圖。
下面你會看到兩個正交向量A
和B
叉積。
矩陣
簡單來說矩陣就是一個矩形的數字、符號或表達式數組。矩陣中每一項叫做矩陣的元素(Element)
。
1. 矩陣加減
矩陣與矩陣之間的加減就是兩個矩陣對應元素的加減運算,所以總體的規則和與標量運算是差不多的,只不過在相同索引下的元素才能進行運算。這也就是說加法和減法只對同維度的矩陣才是有定義的。
2. 矩陣的數乘
和矩陣與標量的加減一樣,矩陣與標量之間的乘法也是矩陣的每一個元素分別乘以該標量。
3. 矩陣相乘
矩陣之間的乘法不見得有多復雜,但的確很難讓人適應。矩陣乘法基本上意味著遵照規定好的法則進行相乘。當然,相乘還有一些限制:
- 只有當左側矩陣的列數與右側矩陣的行數相等,兩個矩陣才能相乘。
- 矩陣相乘不遵守交換律
(Commutative)
,也就是說A?B ≠ B?A
。
矩陣與向量相乘
讓我們更深入了解一下向量,它其實就是一個N×1矩陣,N表示向量分量的個數(也叫N維(N-dimensional)向量)
。但是為什么我們會關心矩陣能否乘以一個向量?好吧,正巧,很多有趣的2D/3D
變換都可以放在一個矩陣中,用這個矩陣乘以我們的向量將變換(Transform)
這個向量。
1. 單位矩陣
在OpenGL
中,由于某些原因我們通常使用4×4的變換矩陣,而其中最重要的原因就是大部分的向量都是4分量的。我們能想到的最簡單的變換矩陣就是單位矩陣(Identity Matrix)。單位矩陣是一個除了對角線以外都是0的N×N矩陣。
2. 縮放
對一個向量進行縮放(Scaling)
就是對向量的長度進行縮放,而保持它的方向不變。由于我們進行的是2維或3維操作,我們可以分別定義一個有2或3個縮放變量的向量,每個變量縮放一個軸(x、y或z)。
OpenGL通常是在3D空間進行操作的,對于2D的情況我們可以把z軸縮放1倍,這樣z軸的值就不變了。我們剛剛的縮放操作是不均勻(Non-uniform)縮放
,因為每個軸的縮放因子(Scaling Factor)都不一樣。如果每個軸的縮放因子都一樣那么就叫均勻縮放(Uniform Scale)
。下面就是縮放的例子。
3. 位移
位移(Translation)是在原始向量的基礎上加上另一個向量從而獲得一個在不同位置的新向量的過程,從而在位移向量基礎上移動了原始向量。我們已經討論了向量加法,所以這應該不會太陌生。和縮放矩陣一樣,在4×4矩陣上有幾個特別的位置用來執行特定的操作,對于位移來說它們是第四列最上面的3個值。
齊次坐標(Homogeneous Coordinates)
向量的w分量也叫齊次坐標。想要從齊次向量得到3D向量,我們可以把x、y和z坐標分別除以w坐標。我們通常不會注意這個問題,因為w分量通常是1.0。使用齊次坐標有幾點好處:它允許我們在3D向量上進行位移(如果沒有w分量我們是不能位移向量的),而且下一章我們會用w值創建3D視覺效果。
如果一個向量的齊次坐標是0,這個坐標就是方向向量(Direction Vector),因為w坐標是0,這個向量就不能位移
4. 旋轉
如果你想知道旋轉矩陣是如何構造出來的,我推薦你去看可汗學院線性代數的視頻。
首先我們來定義一個向量的旋轉到底是什么。2D或3D空間中的旋轉用角(Angle)
來表示。在3D空間中旋轉需要定義一個角和一個旋轉軸(Rotation Axis)
。物體會沿著給定的旋轉軸旋轉特定角度。
旋轉矩陣在3D
空間中每個單位軸都有不同定義,旋轉角度用θ
表示:
利用旋轉矩陣我們可以把我們的位置向量沿一個單位軸進行旋轉。也可以把多個矩陣結合起來,比如先沿著x軸旋轉再沿著y軸旋轉。但是這會很快導致一個問題——萬向節死鎖
(Gimbal Lock,可以看看這個視頻(優酷)來了解)。
下面是繞任意軸旋轉的公式,見下面這個公式,(Rx,Ry,Rz)
代表任意旋轉軸:
5. 矩陣組合
使用矩陣進行變換的真正力量在于,根據矩陣之間的乘法,我們可以把多個變換組合到一個矩陣中。讓我們看看我們是否能生成一個變換矩陣,讓它組合多個變換。建議您在組合矩陣時,先進行縮放操作,然后是旋轉,最后才是位移。
實踐
OpenGL
沒有自帶任何的矩陣和向量知識,所以我們必須定義自己的數學類和函數。在教程中我們更希望抽象所有的數學細節,使用已經做好了的數學庫。幸運的是,有個易于使用,專門為OpenGL量身定做的數學庫,那就是GLM
。
1. GLM
GLM
是OpenGL Mathematics
的縮寫,它是一個只有頭文件的庫,也就是說我們只需包含對應的頭文件就行了,不用鏈接和編譯。GLM可以在它們的網站上下載。把頭文件的根目錄復制到你的includes文件夾,然后你就可以使用這個庫了。
我們需要的GLM的大多數功能都可以從下面這3個頭文件中找到:
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
我們來看看是否可以利用我們剛學的變換知識把一個向量(1, 0, 0)位移(1, 1, 0)個單位(注意,我們把它定義為一個glm::vec4類型的值,齊次坐標設定為1.0)。
glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f);
glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f));
vec = trans * vec;
std::cout << vec.x << vec.y << vec.z << std::endl;
我們先用GLM
內建的向量類定義一個叫做vec
的向量。接下來定義一個mat4
類型的trans
,默認是一個4×4
單位矩陣。下一步是創建一個變換矩陣,我們是把單位矩陣和一個位移向量傳遞給glm::translate
函數來完成這個工作的(然后用給定的矩陣乘以位移矩陣就能獲得最后需要的矩陣)。
我們來做些更有意思的事情,讓我們來旋轉和縮放之前教程中的那個箱子。首先我們把箱子逆時針旋轉90
度。然后縮放0.5
倍,使它變成原來的一半大。我們先來創建變換矩陣:
glm::mat4 trans;
trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0));
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));
首先,我們把箱子在每個軸都縮放到0.5倍,然后沿z軸旋轉90度。
GLM
希望它的角度是弧度制的(Radian),所以我們使用glm::radians
將角度轉化為弧度。注意有紋理的那面矩形是在XY平面上的,所以我們需要把它繞著z軸旋轉。因為我們把這個矩陣傳遞給了GLM的每個函數,GLM會自動將矩陣相乘,返回的結果是一個包括了多個變換的變換矩陣。下一個大問題是:如何把矩陣傳遞給著色器?我們在前面簡單提到過
GLSL
里也有一個mat4
類型。所以我們將修改頂點著色器讓其接收一個mat4
的uniform
變量,然后再用矩陣uniform
乘以位置向量:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 transform;
void main()
{
gl_Position = transform * vec4(aPos, 1.0f);
TexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
}
- 在把位置向量傳給
gl_Position
之前,我們先添加一個uniform
,并且將其與變換矩陣相乘。我們的箱子現在應該是原來的二分之一大小并(向左)旋轉了90度。當然,我們仍需要把變換矩陣傳遞給著色器。
unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
我們首先查詢uniform
變量的地址,然后用有Matrix4fv
后綴的glUniform
函數把矩陣數據發送給著色器。第一個參數你現在應該很熟悉了,它是uniform
的位置值。第二個參數告訴OpenGL
我們將要發送多少個矩陣,這里是1。第三個參數詢問我們我們是否希望對我們的矩陣進行置換(Transpose)
,也就是說交換我們矩陣的行和列。OpenGL
開發者通常使用一種內部矩陣布局,叫做列主序(Column-major Ordering)
布局。GLM
的默認布局就是列主序,所以并不需要置換矩陣,我們填GL_FALSE
。最后一個參數是真正的矩陣數據,但是GLM并不是把它們的矩陣儲存為OpenGL所希望接受的那種,因此我們要先用GLM
的自帶的函數value_ptr
來變換這些數據。
我們創建了一個變換矩陣,在頂點著色器中聲明了一個uniform
,并把矩陣發送給了著色器,著色器會變換我們的頂點坐標。
下面我們看一下效果圖。
我們現在做些更有意思的,看看我們是否可以讓箱子隨著時間旋轉,我們還會重新把箱子放在窗口的右下角。要讓箱子隨著時間推移旋轉,我們必須在游戲循環中更新變換矩陣,因為它在每一次渲染迭代中都要更新。我們使用GLFW
的時間函數來獲取不同時間的角度。
glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(0.5f, -0.5f, 0.0f));
trans = glm::rotate(trans, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));
要記住的是前面的例子中我們可以在任何地方聲明變換矩陣,但是現在我們必須在每一次迭代中創建它,從而保證我們能夠不斷更新旋轉角度。這也就意味著我們不得不在每次游戲循環的迭代中重新創建變換矩陣。通常在渲染場景的時候,我們也會有多個需要在每次渲染迭代中都用新值重新創建的變換矩陣。
盡管在代碼中我們先位移再旋轉,實際的變換卻是先應用旋轉再是位移的。明白所有這些變換的組合,并且知道它們是如何應用到物體上是一件非常困難的事情。只有不斷地嘗試和實驗這些變換你才能快速地掌握它們。
后記
未完,待續~~