本章主要解決這個問題:
如何對物體進行位置變換?
想要操作物體的位置,我們就要使用數學工具對其位置進行計算。先來看看回顧一下需要用到的基本數學知識:
向量
在最初的定義中,向量就是用來表示方向的。向量包括大小和方向兩個要素。你可以把向量想象成在藏寶圖上的箭頭指示:向左走10米,然后,往北走3米,再然后,往右走5米。這個左右南北就是方向,10米就是向量的大小。理論上,向量可以是任意維數的,不過我們不關心這個,我們關心的是我們最最常用的2到4維向量。2維向量表示平面上的方向,3維向量表示3D世界里的方向。
用一張直觀的圖來表示一下。
因為向量只有大小和方向兩個要素,起點并不算在內,所以我們可以認為向量w和向量v是相同的。本文中向量用粗斜體字來表示,比如之前的向量w和向量v。為了顯示的明顯一點,我們用垂直行列式的方式來表示一個向量,例如:
因為向量含有位置的信息,不知從什么時候開始,大家都用向量來表示位置了。這種向量被稱作位置向量,不過不用擔心,我們很熟悉這種表示方式,就是把它看做是在三維空間中的一個坐標而已。
向量運算
標量
和一個標量進行計算是最簡單的一種運算了,加減乘除都可以(除數不能是0哦)。計算方式就是用向量的每個分量和標量進行一次運算,得出的一個新向量就是計算的結果。
向量取反
向量取反操作直接把向量的方向變反了,對向量的大小并沒有什么影響。運算方式非常簡單,直接看圖:
向量加減
向量相加操作就是把兩個向量的相應分量相加,得到一個新的向量。
向量相減操作就是把兩個向量的對應分量相減,得到一個新的向量。
長度
長度計算通過最普通的勾股定理就能算出來
向量乘法
普通的乘法(對應分量相乘)并沒有什么實際意義,所以也沒有必要去研究。不過,向量乘法兩種特殊的乘法運算,點乘(w · v)和叉乘(w x v)。我們分別研究一下這兩種乘法有啥作用:
點乘
兩個向量的點乘結果是:兩個向量長度的乘積再乘上夾角的cos值。像這樣:
特別地,如果兩個向量為單位向量(單位向量是指長度為1的向量),那么這個公式就退化成:
看到沒,就只是兩向量夾角的cos值,這個功能在進行光照計算的時候非常有用,我們計算光照的時候優勢不需要知道它的大小,只需要知道其法向量和光照方向的夾角就好了。
從公式上來看,兩個向量的點乘每個分量相乘的結果在求和。
叉乘
叉乘只在3D空間中有意義,因為叉乘的結果向量方向是與兩個相乘的向量都垂直的。在2D空間里,你無法找到一個能與兩條相交的直線都垂直的直線,但在3D空間中易如反掌!
如果你是在左手坐標系中,那么伸出左手,四指從v彎曲到k,大拇指的方向就是叉乘結果向量的方向。等等,好像不太對啊,圖上的結果向量是朝外的!哈哈,這說明圖上的坐標系是右手坐標系,這時候就要用右手做彎曲動作確定方向了。
不過,不管是左手坐標系還是右手坐標系,向量乘積的結果都是一樣的!向量叉乘公式:
驗證一下我們的公式:A = (1, 0, 0), B = (0, 1, 0) , A x B = (0 * 0 - 0 * 1, 0 * 0 - 1 * 0, 1 * 1 - 0 * 0) = (0, 0, 1)。公式正確!
矩陣
關于向量我們已經了解的差不多了,除了最后的向量叉乘有點難理解之外,其余的幾乎都是小菜一碟。是時候來點挑戰性了!歡迎各位勇士來到矩陣的世界!先說好,矩陣就不會像向量那樣“溫馨”了。
所謂矩陣,基本上就是一個矩形數組,這個數組中可能有數字、符號或者表達式。矩陣中的每一項被稱為矩陣的一個元素。舉一個2x3的矩陣例子來看:
矩陣可以通過索引獲取其某個位置的元素,例如 :索引(i,j)表示第i行第j列的元素。這也就是為什么上面的矩陣被稱為2 x 3的矩陣,因為它是2行3列。引用矩陣元素可能會和表示坐標上的點相混淆(筆者就經常弄混),一個(2,1)的位置表示x=2,y=1,而一個(2,1)的矩陣索引表示row = 2, column = 1,剛好和位置坐標相反。
光有矩陣并沒啥意思,我們最看中地是它的運算方式。它也和向量一樣有很多有趣的運算方法。
加法和減法
矩陣可以和一個簡單的標量相加減,也可以和一個矩陣加減,運算的方式不太一樣,我們分別來看!
1、和標量
矩陣可以和一個標量相加或相減。方式是用矩陣的每一個分量去加或者減這個標量:
2、和矩陣
當一個矩陣和另一個矩陣相加或者相減的時候,情況也很簡單,只要將兩個矩陣對應的分量相加或者相減就行了。
你可能會有疑問,如果兩個矩陣的維數(所謂維數,就是矩陣的行數和列數)不同,那該怎么加減呢?沒錯,不同維數的矩陣的加減操作沒有意義,所以在數學上,我們就禁止不同維數的矩陣進行加減操作!
乘法
和加減操作一樣,矩陣乘法也有和標量、和矩陣之分,運算方式大不相同,我們仔細來看!
和標量
和標量的乘法非常簡單,只需要把標量和矩陣的每個元素相乘,得到一個新的矩陣就行了。
和矩陣
和矩陣相乘就不是那么令人愉快了。在矩陣相乘之前,有兩條規則我們要來看看,這是兩個最基本的原則:
- 相乘的兩個矩陣,第一個矩陣的列數必須要等于第二個矩陣的行數!
- 矩陣相乘不滿足交換律,也就是說A · B != B · A !
滿足這兩個條件后,我們再來看兩個矩陣是如何相乘的。
我們可以看到,矩陣的乘法是第一個矩陣的行,乘以第二個矩陣的列,對應元素相乘然后求和(這就是為什么有第一條原則的原因了!)。畫個圈圈可以看得更清楚
提示:自己在紙上算一遍更好理解哦!
這里我們就給出了行列數相同的矩陣乘法示例,而根據規則,矩陣行列數可以不同,但也能進行運算。我們只稍微說說,兩個可以相乘的矩陣(注意哦,前提是可以相乘!)的運算結果也是一個矩陣,這個結果矩陣的行數等于第一個矩陣,列數等于第二個矩陣。(想象一下用一個矩陣乘以一個向量,得到的結果也必定是一個向量。)
矩陣和向量相乘
嚴格來說,矩陣和向量相乘并不能單獨作為一節來說,因為就像前面說的那樣,把向量當成一個列數為1的矩陣,就可以根據矩陣運算規則算出來了。但是,矩陣與向量的運算太重要了,以至于它完全值得我們單獨列出來對他大肆捯飭一番。
單位矩陣
所謂單位矩陣就是除了從左上角到右下角對角線上的元素都是1,其余元素都是0的矩陣。一個單位矩陣和一個向量相乘,結果還是那個向量,就像任何數乘以1都不會對其有啥改變。
你可能會想知道,既然單位矩陣不會改變向量的值,那還有個卵用啊?別急,雖然用處不大,但還是有點用滴,不然誰會發明這個東西啊。單位矩陣通常都是其他矩陣的“起點”,很多矩陣都是從它開始算出來的。另外,如果我們對線性代數研究地更深一點,就會發現,它對于提供理論證明,解決線性相等問題有很大的幫助。
當然這些都是題外話,光啃干貨不舒服,扯皮用的。
比例變化
我們可以構造一個矩陣來對向量進行縮放,除了對各個坐標進行統一縮放,我們還能通過給不同的坐標設置不同縮放因子這種方法對各個坐標進行不統一的縮放。是不是聽上去很神奇?我們來看看就知道了
如果S1,S2,S3不相同,那就是不統一縮放(改變方向),如果相同,那就是統一縮放(不改變方向)。注意,第四個縮放因子必須是1,因為在3D空間中,對w分量進行縮放的操作不知道會出啥問題!
平移
根據我們的已有知識,要想平移一個向量,只要在這個4x4的矩陣的最后一列放上我們需要平移的量就行了,當然最后一個必須還是1.像這樣:
很簡單吧!
旋轉
旋轉的內容有點復雜。首先我們要了解的是,如何定義一個旋轉?有人可能就不明白了,旋轉還要定義嗎?直接往左或者往右轉個90度不就完了嗎?這個還真的得說兩句,因為在數學世界中,角度有兩種表示方法:角度或者弧度。我們熟悉的都是用角度來表示,一圈有360度。而在數學世界里,弧度也是非常常用的,一圈是2PI。為了便于理解,我們用角度來說明。
角度和弧度可以相互轉換,具體的公式是:
角度= 弧度 * (180.0f / PI)
弧度= 角度 * (PI / 180.0f)
PI的精度最好高一點,以免出現誤差,通常把它設置為:3.14159265359。
在3D世界中,當我們需要將一個向量進行旋轉,我們就需要確定三樣東西:
- 繞著什么旋轉
- 往哪個方向旋轉
- 旋轉多少度
理論上,我們可以繞任意軸旋轉(實際上也是一樣_),不過在計算的時候,我們通常把繞任意軸旋轉的操作分解成繞三條主軸旋轉的操作。通過一些三角變換函數,計算出繞某一個主軸的變換結果,然后將這些操作結合起來,組成繞任意軸旋轉的操作。下面直接給出繞3個軸旋轉的變換公式:
首先是繞X軸旋轉的公式:
然后是繞Y軸旋轉的公式:
最后是繞Z軸旋轉的公式:
這些公式不需要記,你可以非常快地在網上查到,或者把這篇文章收藏一下,直接就能看到。在這個互聯網時代,筆者的主張是不需要記那么多的知識點,但是你必須要記住在哪能查到這些知識!明白了嗎?知道在哪比單純的記住更加重要!
言歸正傳,當我們把這些變換組合起來的時候,很快就會遇到一個問題,那就是萬向鎖(Gimbal lock)。
簡單講講萬向鎖:
先不要被鎖這個字給嚇唬住了,出現萬象鎖現象并不是說你不能再旋轉了,而是這種情況下,某些旋轉不是按照我們想要的方式來。筆者看了許多文字描述的萬向鎖,但是都沒搞明白,所以不打算用文字解釋,直接推薦一個視頻。仔細看萬向鎖的部分,就能明白了。
先來說一個解決方法,繞任意一個單位軸進行旋轉,例如(0.662, 0.2, 0.722),不過記住一定要是單位向量。轉換公式像這樣(Rx, Ry, Rz是坐標值):
跟其他的轉換矩陣相比,是不是頓時有種鶴立雞群的感覺!What the f**k?幸好我們有網絡這個東西,不用死記硬背實在是太幸福了。順便一提,解決萬向鎖還可以用一種四元數的東西,以后我們會涉及到。
實戰演練
講了這么多基礎知識,終于可以動手操作了,是不是等不及了?先別急,我們是要學OpenGL的,花太多的時間在實現數學庫上顯然和我們的初衷背道而馳,所以,我們可以采用“拿來主義”,直接找一個數學庫用。幸好,OpenGL的“周邊”就有一個好的數學庫,叫做:GLM。
到GLM的網站去下載0.9.8版本的數學庫(不要下最新的0.9.9版本,和我們的代碼不兼容)。沒法FQ的可以到我的網盤里去下載。下載解壓后,把頭文件根目錄(glm目錄,不是解壓縮后的glm文件夾,而是在里面的glm文件夾)復制到你的includes目錄下面就可以了。
設置完后,我們需要在代碼中包含需要的頭文件。
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
我們先來試試這個庫有沒有效。
//Test
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)); //從這里就能看出單位矩陣的作用了。初始化的trans是一個單位矩陣,讓它平移到(1.0f, 1.0f, 0.0f)的位置產生了一個平移矩陣。
vec = trans * vec;
std::cout << "(" << vec.x << "," << vec.y << "," << vec.z << ")" << std::endl;
嗯,輸出正確。
讓我們袖子干吧!把前面章節中顯示的圖片縮小成原來的一半,然后再繞著z軸逆時針旋轉90度。
先來生成矩陣:
glm::mat4 trans;
trans = glm::scale(trans, glm::vec3(0.5f, 0.5f, 0.5f));
trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));
矩陣生成完后,我們如何讓它在頂點著色器中生效呢?想來你很快就能得到答案,沒錯,就是用uniform關鍵字。先聲明一個變量uniform mat4 transform
,然后在主函數中調用gl_Position = transform * vec4(aPos, 1.0f)
。
接下來,我們要在程序里設置這個值。但原有的shader類中沒有設置mat4類型的接口,所以我們要添加一個:
void Shader::setMat4(const std::string& name, float value[]) const {
glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, value);
}
介紹一下glUniformMatrix4fv
函數的參數:
- 參數一:變量位置
- 參數二:我們想傳的矩陣個數,這里我們只設置一個,所以是1
- 參數三:我們是否想轉換矩陣,把行和列交換。OpenGL中的矩陣是列主序的矩陣(和DX中的不同),不過GLM中生成的矩陣也是列主序的,所以我們設置成GL_FALSE,表示不用轉換。
- 參數四:矩陣數組。這里我們要把矩陣轉換成數組的格式傳遞。
接口寫好后,我們就能在主循環中使用了:
shader.setMat4("transform", glm::value_ptr(trans));
完成后,編譯運行:
跟我們想象的一樣!
慢著,這樣就滿足了嗎?NO!我們還要讓它動起來。方法也很簡單,我們傳入一個glfwGetTime()作為旋轉的弧度就可以了。像這樣:
trans = glm::rotate(trans, /*glm::radians(90.0f)*/(float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));
如果之前你是在循環外面生成的轉換矩陣,那么你就要把它放到循環里面去了,這樣隨著每次運行,旋轉的角度也不一樣。嗯,編譯運行。
效果不錯!如果你的程序不這樣的顯示,可以點擊這里下載代碼進行比對。
總結
好了,艱苦的數學旅程告一段落,我們來回憶一下都學了些什么。首先是向量,以及向量能做的一些運算;然后是矩陣,以及矩陣的一些運算;接著,我們看到了實際有用的一些運算矩陣;最后,我們使用了一個現成的庫GLM來實現變換坐標的效果。呼~休息!
參考資料:
www.learningopengl.com(很好的學習網站,建議多去看看)