OpenGL入門1.5:矩陣與變換


每一個小步驟的源碼都放在了Github

的內容為插入注釋,可以先跳過

前言

在閱讀本篇博客之前,你必須對向量和矩陣有基本的認識,並且能熟練進行向量和矩陣的運算

我們已經知道了如何創建一個物體、着色、加入紋理,但它們都還是靜態的物體
我們可以嘗試着在每一幀改變物體的頂點並且重配置緩沖區從而使它們移動
但是這樣的操作太過復雜,而且消耗性能也很大

我們現在有一個更好的解決方案,使用多個矩陣(Matrix)對象變換(Transform)一個物體

如果你具備了我說到的向量和矩陣的數學基礎,接下來的操作都很簡單

使用矩陣變換向量

縮放(Scale)

對向量的長度進行縮放,而保持它的方向不變

由於我們進行的是2維或3維操作,我們可以分別定義一個有2或3個縮放變量的向量,每個變量縮放一個軸(x、y或z)

記住,OpenGL通常是在3D空間進行操作的,對於2D的情況我們可以把z軸縮放1倍,只操作x和y軸,這樣z軸的值就不變了,每個軸的縮放因子(Scaling Factor)都不一樣,就是不均勻(Non-uniform)縮放,都一樣那么就叫均勻縮放(Uniform Scale)

我們下面會構造一個變換矩陣來為我們提供縮放功能:

從單位矩陣了解到,每個對角線元素會分別與向量的對應元素相乘,如果我們把1變為3會怎樣?這樣子的話,我們就把向量的每個元素乘以3了,這事實上就把向量縮放3倍

如果我們把縮放變量表示為(S1,S2,S3)我們可以為任意向量(x,y,z)定義一個縮放矩陣:

\[\begin{vmatrix} S_{1}& 0 & 0 & 0\\ 0 & S_{2} & 0 & 0\\ 0 & 0 & S_{3} & 0\\ 0 & 0 & 0 & 1\end{vmatrix}\cdot \begin{pmatrix}x\\ y\\ z\\ 1\end{pmatrix}=\begin{pmatrix}S_{1}\cdot x\\ S_{2}\cdot y\\ S_{3}\cdot z\\ 1\end{pmatrix} \]

第四個縮放向量w仍然是1,因為在3D空間中縮放w分量是無意義的(w分量另有其他用途)

位移(Translation)

在原始向量的基礎上加上另一個向量從而獲得一個在不同位置的新向量的過程,從而在位移向量基礎上移動了原始向量,你肯定學過了向量加法,所以應該不會太陌生

和縮放矩陣一樣,在4×4矩陣上有幾個特別的位置用來執行特定的操作,對於位移來說它們是第四列最上面的3個值。如果我們把位移向量表示為(Tx,Ty,Tz),我們就能把位移矩陣定義為:

\[\begin{vmatrix} 1 & 0 & 0 & T_{x}\\ 0 & 1 & 0 & T_{y}\\ 0 & 0 & 1 & T_{z}\\ 0 & 0 & 0 & 1\end{vmatrix}\cdot \begin{pmatrix}x\\ y\\ z\\ 1\end{pmatrix}=\begin{pmatrix}x+T_{x}\\ y+T_{y}\\ z+T_{z}\\ 1\end{pmatrix} \]

有了位移矩陣我們就可以在3個方向(x、y、z)上移動物體,它是我們的變換工具箱中非常有用的一個變換矩陣

旋轉(Rotate)

首先我們來定義一個向量的旋轉到底是什么。2D或3D空間中的旋轉用角(Angle)來表示,角可以是角度制或弧度制的,周角是360角度或2pi弧度

大多數旋轉函數需要用弧度制的角,但幸運的是角度制的角也可以很容易地轉化為弧度制的:

  • 弧度轉角度:角度 = 弧度 * (180.0f / PI)
  • 角度轉弧度:弧度 = 角度 * (PI / 180.0f)

PI約等於3.14159265359

轉半圈會旋轉360/2 = 180度,向右旋轉1/5圈表示向右旋轉360/5 = 72度。下圖中展示的2D向量 是由 向右旋轉72度所得的:

在3D空間中旋轉需要定義一個角一個旋轉軸(Rotation Axis)

使用三角學,給定一個角度,可以把一個向量變換為一個經過旋轉的新向量,這通常是使用一系列正弦和余弦函數(一般簡稱sin和cos)各種巧妙的組合得到的

旋轉矩陣在3D空間中每個單位軸都有不同定義,旋轉角度用θ表示:

沿x軸旋轉:

\[\begin{vmatrix} 1 & 0 & 0 & 0\\ 0 & cos\theta & -sin\theta & 0\\ 0 & sin\theta & cos\theta & 0\\ 0 & 0 & 0 & 1\end{vmatrix}\cdot \begin{pmatrix}x\\ y\\ z\\ 1\end{pmatrix}=\begin{pmatrix}x\\ cos\theta\cdot y-sin\theta\cdot z\\ sin\theta\cdot y+cos\theta\cdot z\\ 1\end{pmatrix} \]

沿y軸旋轉:

\[\begin{vmatrix}cos\theta & 0 & sin\theta & 0\\ 0 & 1 & 0 & 0\\ -sin\theta & 0 & cos\theta & 0\\ 0 & 0 & 0 & 1\end{vmatrix}\cdot \begin{pmatrix}x\\ y\\ z\\ 1\end{pmatrix}=\begin{pmatrix}cos\theta\cdot x+sin\theta\cdot z\\ y\\ -sin\theta\cdot x+cos\theta\cdot z\\ 1\end{pmatrix} \]

沿z軸旋轉:

\[\begin{vmatrix}cos\theta & -sin\theta & 0 & 0\\ sin\theta & cos\theta & 0 & 0\\ 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 1\end{vmatrix}\cdot \begin{pmatrix}x\\ y\\ z\\ 1\end{pmatrix}=\begin{pmatrix}cos\theta\cdot x-sin\theta\cdot y\\ sin\theta\cdot x+cos\theta\cdot y\\ z\\ 1\end{pmatrix} \]

利用旋轉矩陣我們可以把任意位置向量沿一個單位旋轉軸進行旋轉,也可以將多個矩陣復合,比如先沿着x軸旋轉再沿着y軸旋轉。但是這會很快導致一個問題——萬向節死鎖(Gimbal Lock)

在這里我們不會討論它的細節,但是對於3D空間中的旋轉,一個更好的模型是沿着任意的一個軸,比如單位向量\((0.662, 0.2, 0.7222)\)旋轉,而不是對一系列旋轉矩陣進行復合。這樣的一個(超級麻煩的)矩陣是存在的,見下面這個公式,其中(Rx,Ry,Rz)代表任意旋轉軸:

但是,即使這樣一個矩陣也不能完全解決萬向節死鎖問題(雖然能盡量避免),避免萬向節死鎖的終極解決方案是使用四元數(Quaternion),它不僅更安全,而且計算會更有效率,這里暫不介紹四元數

矩陣的組合

使用矩陣進行變換的真正力量在於,根據矩陣之間的乘法,我們可以把多個變換組合到一個矩陣中

讓我們看看我們是否能生成一個變換矩陣,讓它組合多個變換:假設我們有一個頂點(x, y, z),我們希望將其縮放2倍,然后位移(1, 2, 3)個單位,我們需要一個位移和縮放矩陣來完成這些變換:

\[Trans.Scale = \begin{vmatrix}1 & 0 & 0 & 1\\ 0 & 1 & 0 & 2\\ 0 & 0 & 1 & 3\\ 0 & 0 & 0 & 1\end{vmatrix}\cdot \begin{vmatrix}2 & 0 & 0 & 0\\ 0 & 2 & 0 & 0\\ 0 & 0 & 2 & 0\\ 0 & 0 & 0 & 1\end{vmatrix}=\begin{vmatrix}2 & 0 & 0 & 1\\ 0 & 2 & 0 & 2\\ 0 & 0 & 2 & 3\\ 0 & 0 & 0 & 1\end{vmatrix} \]

注意,當矩陣相乘時我們先寫位移再寫縮放變換的,矩陣乘法不遵守交換律,這意味着它們的順序很重要。當矩陣相乘時,在最右邊的矩陣是第一個與向量相乘的,所以你應該從右向左讀這個乘法,建議在組合矩陣時,先進行縮放操作,然后是旋轉,最后才是位移,否則會互相影響

用最終的變換矩陣左乘我們的向量會得到以下結果:

\[\begin{vmatrix}2 & 0 & 0 & 1\\ 0 & 2 & 0 & 2\\ 0 & 0 & 2 & 3\\ 0 & 0 & 0 & 1\end{vmatrix}\cdot\begin{pmatrix}x\\ y\\ z\\ 1\end{pmatrix}=\begin{pmatrix}2x+1\\ 2y+2\\ 2z+3\\ 1\end{pmatrix} \]

向量先縮放2倍,然后位移了(1, 2, 3)個單位

實踐

OpenGL沒有自帶任何的矩陣和向量的東西,所以我們必須定義自己的數學類和函數,在教程中我們更希望抽象所有的數學細節,使用已經做好了的數學庫

幸運的是,有個易於使用,專門為OpenGL量身定做的數學庫,那就是GLM

GLM是 OpenGL Mathematics 的縮寫,它是一個只有頭文件的庫,也就是說我們只需包含對應的頭文件就行了,不用鏈接和編譯。GLM可以在它們的網站上下載,把頭文件的根目錄復制到你的includes文件夾,然后你就可以使用這個庫了

GLM庫從0.9.9版本起,默認會將矩陣類型初始化為一個零矩陣(所有元素均為0),而不是單位矩陣(對角元素為1,其它元素為0)。如果你使用的是0.9.9或0.9.9以上的版本,你需要將所有的矩陣初始化改為 glm::mat4 mat = glm::mat4(1.0f)。如果你想與本教程的代碼保持一致,請使用低於0.9.9版本的GLM,或者改用上述代碼初始化所有的矩陣。

我們需要的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); // 向量(1, 0, 0)
// 如果使用的是0.9.9及以上版本,下面這行代碼就需要改為:
// glm::mat4 trans = glm::mat4(1.0f)
glm::mat4 trans; // 單位矩陣
// 傳遞單位矩陣和一個位移向量
trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f)); 
vec = trans * vec; // trans為位移矩陣
std::cout << vec.x << vec.y << vec.z << std::endl; // 210

我們先用GLM內建的向量類定義一個叫做vec的向量,接下來定義一個mat4類型的trans(4×4單位矩陣),下一步是創建一個變換矩陣,我們是把單位矩陣和一個位移向量傳遞給glm::translate函數來完成這個工作的(然后用給定的矩陣乘以位移矩陣就能獲得最后需要的矩陣)
之后我們把向量乘以位移矩陣並且輸出最后的結果,得到的向量應該是(1 + 1, 0 + 1, 0 + 0),也就是(2, 1, 0)
這個代碼片段將會輸出210,所以這個位移矩陣是正確的

我們來做些更有意思的事情,讓我們來旋轉和縮放之前教程中的那個箱子:首先我們把箱子逆時針旋轉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乘以位置向量:

// vertex shader
#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);
}

GLSL也有mat2mat3類型從而允許了像向量一樣的混合運算,前面提到的所有數學運算(像是標量-矩陣相乘,矩陣-向量相乘和矩陣-矩陣相乘)在矩陣類型里都可以使用(出現特殊的矩陣運算的時候我們會特別說明)

在把位置向量傳給gl_Position之前,我們先添加一個uniform,並且將其與變換矩陣相乘,我們的箱子現在應該是原來的二分之一大小並(向左)旋轉了90度,當然,我們仍需要把變換矩陣傳遞給着色器:

unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));

我們首先查詢uniform變量的地址,然后用有Matrix4fv后綴的glUniform函數把矩陣數據發送給着色器

  1. 第一個參數你現在應該很熟悉了,它是uniform的位置值
  2. 第二個參數告訴OpenGL我們將要發送多少個矩陣,這里是1
  3. 第三個參數詢問我們我們是否希望對我們的矩陣進行置換(Transpose),也就是說交換我們矩陣的行和列,OpenGL開發者通常使用一種內部矩陣布局,叫做列主序(Column-major Ordering)布局,GLM的默認布局就是列主序,所以並不需要置換矩陣,我們填GL_FALSE
  4. 最后一個參數是真正的矩陣數據,但是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));

要記住的是前面的例子中我們可以在任何地方聲明變換矩陣,但是現在我們必須在每一次迭代中創建它,從而保證我們能夠不斷更新旋轉角度,這意味着我們不得不在每次游戲循環的迭代中重新創建變換矩陣,通常在渲染場景的時候,我們也會有多個需要在每次渲染迭代中都用新值重新創建的變換矩陣

在這里我們先把箱子圍繞原點(0, 0, 0)旋轉,之后,我們把旋轉過后的箱子位移到屏幕的右下角,記住,實際的變換順序應該與閱讀順序相反:在代碼中我們先位移再旋轉實際的變換卻是先應用旋轉再是位移的

如果你做對了,你將看到下面的結果:

vs7blfFLVk

awesome

現在你可以明白為什么矩陣在圖形領域是一個如此重要的工具了,我們可以定義無限數量的變換,而把它們組合為僅僅一個矩陣,如果願意的話我們還可以重復使用它

在着色器中使用矩陣可以省去重新定義頂點數據的功夫,它也能夠節省處理時間,因為我們沒有一直重新發送我們的數據(這是個非常慢的過程)


免責聲明!

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



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