本章主要解決這個(gè)問(wèn)題:
如何對(duì)物體進(jìn)行位置變換奢讨?
想要操作物體的位置蔚鸥,我們就要使用數(shù)學(xué)工具對(duì)其位置進(jìn)行計(jì)算。先來(lái)看看回顧一下需要用到的基本數(shù)學(xué)知識(shí):
向量
在最初的定義中捧弃,向量就是用來(lái)表示方向的赠叼。向量包括大小和方向兩個(gè)要素。你可以把向量想象成在藏寶圖上的箭頭指示:向左走10米塔橡,然后梅割,往北走3米,再然后葛家,往右走5米户辞。這個(gè)左右南北就是方向,10米就是向量的大小癞谒。理論上底燎,向量可以是任意維數(shù)的,不過(guò)我們不關(guān)心這個(gè)弹砚,我們關(guān)心的是我們最最常用的2到4維向量双仍。2維向量表示平面上的方向,3維向量表示3D世界里的方向桌吃。
用一張直觀的圖來(lái)表示一下朱沃。
因?yàn)橄蛄恐挥写笮『头较騼蓚€(gè)要素,起點(diǎn)并不算在內(nèi),所以我們可以認(rèn)為向量w和向量v是相同的逗物。本文中向量用粗斜體字來(lái)表示搬卒,比如之前的向量w和向量v。為了顯示的明顯一點(diǎn)翎卓,我們用垂直行列式的方式來(lái)表示一個(gè)向量契邀,例如:
因?yàn)橄蛄亢形恢玫男畔ⅲ恢獜氖裁磿r(shí)候開(kāi)始失暴,大家都用向量來(lái)表示位置了坯门。這種向量被稱作位置向量,不過(guò)不用擔(dān)心逗扒,我們很熟悉這種表示方式古戴,就是把它看做是在三維空間中的一個(gè)坐標(biāo)而已。
向量運(yùn)算
標(biāo)量
和一個(gè)標(biāo)量進(jìn)行計(jì)算是最簡(jiǎn)單的一種運(yùn)算了缴阎,加減乘除都可以(除數(shù)不能是0哦)允瞧。計(jì)算方式就是用向量的每個(gè)分量和標(biāo)量進(jìn)行一次運(yùn)算,得出的一個(gè)新向量就是計(jì)算的結(jié)果蛮拔。
向量取反
向量取反操作直接把向量的方向變反了述暂,對(duì)向量的大小并沒(méi)有什么影響。運(yùn)算方式非常簡(jiǎn)單建炫,直接看圖:
向量加減
向量相加操作就是把兩個(gè)向量的相應(yīng)分量相加畦韭,得到一個(gè)新的向量。
向量相減操作就是把兩個(gè)向量的對(duì)應(yīng)分量相減肛跌,得到一個(gè)新的向量艺配。
長(zhǎng)度
長(zhǎng)度計(jì)算通過(guò)最普通的勾股定理就能算出來(lái)
向量乘法
普通的乘法(對(duì)應(yīng)分量相乘)并沒(méi)有什么實(shí)際意義,所以也沒(méi)有必要去研究衍慎。不過(guò)转唉,向量乘法兩種特殊的乘法運(yùn)算,點(diǎn)乘(w · v)和叉乘(w x v)稳捆。我們分別研究一下這兩種乘法有啥作用:
點(diǎn)乘
兩個(gè)向量的點(diǎn)乘結(jié)果是:兩個(gè)向量長(zhǎng)度的乘積再乘上夾角的cos值赠法。像這樣:
特別地,如果兩個(gè)向量為單位向量(單位向量是指長(zhǎng)度為1的向量)乔夯,那么這個(gè)公式就退化成:
看到?jīng)]砖织,就只是兩向量夾角的cos值,這個(gè)功能在進(jìn)行光照計(jì)算的時(shí)候非常有用末荐,我們計(jì)算光照的時(shí)候優(yōu)勢(shì)不需要知道它的大小侧纯,只需要知道其法向量和光照方向的夾角就好了。
從公式上來(lái)看甲脏,兩個(gè)向量的點(diǎn)乘每個(gè)分量相乘的結(jié)果在求和眶熬。
叉乘
叉乘只在3D空間中有意義妹笆,因?yàn)椴娉说慕Y(jié)果向量方向是與兩個(gè)相乘的向量都垂直的。在2D空間里聋涨,你無(wú)法找到一個(gè)能與兩條相交的直線都垂直的直線晾浴,但在3D空間中易如反掌!
如果你是在左手坐標(biāo)系中牍白,那么伸出左手,四指從v彎曲到k抖棘,大拇指的方向就是叉乘結(jié)果向量的方向茂腥。等等综芥,好像不太對(duì)啊袍榆,圖上的結(jié)果向量是朝外的!哈哈荆永,這說(shuō)明圖上的坐標(biāo)系是右手坐標(biāo)系朝捆,這時(shí)候就要用右手做彎曲動(dòng)作確定方向了般渡。
不過(guò),不管是左手坐標(biāo)系還是右手坐標(biāo)系芙盘,向量乘積的結(jié)果都是一樣的驯用!向量叉乘公式:
驗(yàn)證一下我們的公式: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)儒老。公式正確蝴乔!
矩陣
關(guān)于向量我們已經(jīng)了解的差不多了,除了最后的向量叉乘有點(diǎn)難理解之外驮樊,其余的幾乎都是小菜一碟薇正。是時(shí)候來(lái)點(diǎn)挑戰(zhàn)性了!歡迎各位勇士來(lái)到矩陣的世界囚衔!先說(shuō)好挖腰,矩陣就不會(huì)像向量那樣“溫馨”了。
所謂矩陣练湿,基本上就是一個(gè)矩形數(shù)組猴仑,這個(gè)數(shù)組中可能有數(shù)字、符號(hào)或者表達(dá)式鞠鲜。矩陣中的每一項(xiàng)被稱為矩陣的一個(gè)元素宁脊。舉一個(gè)2x3的矩陣?yán)觼?lái)看:
矩陣可以通過(guò)索引獲取其某個(gè)位置的元素,例如 :索引(i,j)表示第i行第j列的元素贤姆。這也就是為什么上面的矩陣被稱為2 x 3的矩陣榆苞,因?yàn)樗?行3列。引用矩陣元素可能會(huì)和表示坐標(biāo)上的點(diǎn)相混淆(筆者就經(jīng)常弄混)霞捡,一個(gè)(2,1)的位置表示x=2,y=1坐漏,而一個(gè)(2,1)的矩陣索引表示row = 2, column = 1,剛好和位置坐標(biāo)相反。
光有矩陣并沒(méi)啥意思赊琳,我們最看中地是它的運(yùn)算方式街夭。它也和向量一樣有很多有趣的運(yùn)算方法。
加法和減法
矩陣可以和一個(gè)簡(jiǎn)單的標(biāo)量相加減躏筏,也可以和一個(gè)矩陣加減板丽,運(yùn)算的方式不太一樣,我們分別來(lái)看趁尼!
1埃碱、和標(biāo)量
矩陣可以和一個(gè)標(biāo)量相加或相減。方式是用矩陣的每一個(gè)分量去加或者減這個(gè)標(biāo)量:
2酥泞、和矩陣
當(dāng)一個(gè)矩陣和另一個(gè)矩陣相加或者相減的時(shí)候砚殿,情況也很簡(jiǎn)單,只要將兩個(gè)矩陣對(duì)應(yīng)的分量相加或者相減就行了芝囤。
你可能會(huì)有疑問(wèn)似炎,如果兩個(gè)矩陣的維數(shù)(所謂維數(shù),就是矩陣的行數(shù)和列數(shù))不同悯姊,那該怎么加減呢羡藐?沒(méi)錯(cuò),不同維數(shù)的矩陣的加減操作沒(méi)有意義挠轴,所以在數(shù)學(xué)上传睹,我們就禁止不同維數(shù)的矩陣進(jìn)行加減操作!
乘法
和加減操作一樣岸晦,矩陣乘法也有和標(biāo)量欧啤、和矩陣之分,運(yùn)算方式大不相同启上,我們仔細(xì)來(lái)看邢隧!
和標(biāo)量
和標(biāo)量的乘法非常簡(jiǎn)單,只需要把標(biāo)量和矩陣的每個(gè)元素相乘冈在,得到一個(gè)新的矩陣就行了倒慧。
和矩陣
和矩陣相乘就不是那么令人愉快了。在矩陣相乘之前包券,有兩條規(guī)則我們要來(lái)看看纫谅,這是兩個(gè)最基本的原則:
- 相乘的兩個(gè)矩陣,第一個(gè)矩陣的列數(shù)必須要等于第二個(gè)矩陣的行數(shù)溅固!
- 矩陣相乘不滿足交換律付秕,也就是說(shuō)A · B != B · A !
滿足這兩個(gè)條件后侍郭,我們?cè)賮?lái)看兩個(gè)矩陣是如何相乘的询吴。
我們可以看到掠河,矩陣的乘法是第一個(gè)矩陣的行,乘以第二個(gè)矩陣的列猛计,對(duì)應(yīng)元素相乘然后求和(這就是為什么有第一條原則的原因了_肽 )。畫個(gè)圈圈可以看得更清楚
提示:自己在紙上算一遍更好理解哦奉瘤!
這里我們就給出了行列數(shù)相同的矩陣乘法示例勾拉,而根據(jù)規(guī)則,矩陣行列數(shù)可以不同盗温,但也能進(jìn)行運(yùn)算望艺。我們只稍微說(shuō)說(shuō),兩個(gè)可以相乘的矩陣(注意哦肌访,前提是可以相乘!)的運(yùn)算結(jié)果也是一個(gè)矩陣艇劫,這個(gè)結(jié)果矩陣的行數(shù)等于第一個(gè)矩陣吼驶,列數(shù)等于第二個(gè)矩陣。(想象一下用一個(gè)矩陣乘以一個(gè)向量店煞,得到的結(jié)果也必定是一個(gè)向量蟹演。)
矩陣和向量相乘
嚴(yán)格來(lái)說(shuō),矩陣和向量相乘并不能單獨(dú)作為一節(jié)來(lái)說(shuō)顷蟀,因?yàn)榫拖袂懊嬲f(shuō)的那樣酒请,把向量當(dāng)成一個(gè)列數(shù)為1的矩陣,就可以根據(jù)矩陣運(yùn)算規(guī)則算出來(lái)了鸣个。但是羞反,矩陣與向量的運(yùn)算太重要了,以至于它完全值得我們單獨(dú)列出來(lái)對(duì)他大肆捯飭一番囤萤。
單位矩陣
所謂單位矩陣就是除了從左上角到右下角對(duì)角線上的元素都是1昼窗,其余元素都是0的矩陣。一個(gè)單位矩陣和一個(gè)向量相乘涛舍,結(jié)果還是那個(gè)向量澄惊,就像任何數(shù)乘以1都不會(huì)對(duì)其有啥改變。
你可能會(huì)想知道富雅,既然單位矩陣不會(huì)改變向量的值掸驱,那還有個(gè)卵用啊没佑?別急毕贼,雖然用處不大,但還是有點(diǎn)用滴图筹,不然誰(shuí)會(huì)發(fā)明這個(gè)東西啊帅刀。單位矩陣通常都是其他矩陣的“起點(diǎn)”让腹,很多矩陣都是從它開(kāi)始算出來(lái)的。另外扣溺,如果我們對(duì)線性代數(shù)研究地更深一點(diǎn)骇窍,就會(huì)發(fā)現(xiàn),它對(duì)于提供理論證明锥余,解決線性相等問(wèn)題有很大的幫助腹纳。
當(dāng)然這些都是題外話,光啃干貨不舒服驱犹,扯皮用的嘲恍。
比例變化
我們可以構(gòu)造一個(gè)矩陣來(lái)對(duì)向量進(jìn)行縮放,除了對(duì)各個(gè)坐標(biāo)進(jìn)行統(tǒng)一縮放雄驹,我們還能通過(guò)給不同的坐標(biāo)設(shè)置不同縮放因子這種方法對(duì)各個(gè)坐標(biāo)進(jìn)行不統(tǒng)一的縮放佃牛。是不是聽(tīng)上去很神奇?我們來(lái)看看就知道了
如果S1医舆,S2俘侠,S3不相同,那就是不統(tǒng)一縮放(改變方向)蔬将,如果相同爷速,那就是統(tǒng)一縮放(不改變方向)。注意霞怀,第四個(gè)縮放因子必須是1惫东,因?yàn)樵?D空間中,對(duì)w分量進(jìn)行縮放的操作不知道會(huì)出啥問(wèn)題毙石!
平移
根據(jù)我們的已有知識(shí)廉沮,要想平移一個(gè)向量,只要在這個(gè)4x4的矩陣的最后一列放上我們需要平移的量就行了胁黑,當(dāng)然最后一個(gè)必須還是1.像這樣:
很簡(jiǎn)單吧废封!
旋轉(zhuǎn)
旋轉(zhuǎn)的內(nèi)容有點(diǎn)復(fù)雜。首先我們要了解的是丧蘸,如何定義一個(gè)旋轉(zhuǎn)漂洋?有人可能就不明白了,旋轉(zhuǎn)還要定義嗎力喷?直接往左或者往右轉(zhuǎn)個(gè)90度不就完了嗎刽漂?這個(gè)還真的得說(shuō)兩句,因?yàn)樵跀?shù)學(xué)世界中弟孟,角度有兩種表示方法:角度或者弧度贝咙。我們熟悉的都是用角度來(lái)表示,一圈有360度拂募。而在數(shù)學(xué)世界里庭猩,弧度也是非常常用的窟她,一圈是2PI。為了便于理解蔼水,我們用角度來(lái)說(shuō)明震糖。
角度和弧度可以相互轉(zhuǎn)換,具體的公式是:
角度= 弧度 * (180.0f / PI)
弧度= 角度 * (PI / 180.0f)
PI的精度最好高一點(diǎn)趴腋,以免出現(xiàn)誤差吊说,通常把它設(shè)置為:3.14159265359。
在3D世界中优炬,當(dāng)我們需要將一個(gè)向量進(jìn)行旋轉(zhuǎn)颁井,我們就需要確定三樣?xùn)|西:
- 繞著什么旋轉(zhuǎn)
- 往哪個(gè)方向旋轉(zhuǎn)
- 旋轉(zhuǎn)多少度
理論上,我們可以繞任意軸旋轉(zhuǎn)(實(shí)際上也是一樣_)蠢护,不過(guò)在計(jì)算的時(shí)候雅宾,我們通常把繞任意軸旋轉(zhuǎn)的操作分解成繞三條主軸旋轉(zhuǎn)的操作。通過(guò)一些三角變換函數(shù)葵硕,計(jì)算出繞某一個(gè)主軸的變換結(jié)果秀又,然后將這些操作結(jié)合起來(lái),組成繞任意軸旋轉(zhuǎn)的操作贬芥。下面直接給出繞3個(gè)軸旋轉(zhuǎn)的變換公式:
首先是繞X軸旋轉(zhuǎn)的公式:
然后是繞Y軸旋轉(zhuǎn)的公式:
最后是繞Z軸旋轉(zhuǎn)的公式:
這些公式不需要記,你可以非承觯快地在網(wǎng)上查到蘸劈,或者把這篇文章收藏一下,直接就能看到尊沸。在這個(gè)互聯(lián)網(wǎng)時(shí)代威沫,筆者的主張是不需要記那么多的知識(shí)點(diǎn),但是你必須要記住在哪能查到這些知識(shí)洼专!明白了嗎棒掠?知道在哪比單純的記住更加重要!
言歸正傳屁商,當(dāng)我們把這些變換組合起來(lái)的時(shí)候烟很,很快就會(huì)遇到一個(gè)問(wèn)題,那就是萬(wàn)向鎖(Gimbal lock)蜡镶。
簡(jiǎn)單講講萬(wàn)向鎖:
先不要被鎖這個(gè)字給嚇唬住了雾袱,出現(xiàn)萬(wàn)象鎖現(xiàn)象并不是說(shuō)你不能再旋轉(zhuǎn)了,而是這種情況下官还,某些旋轉(zhuǎn)不是按照我們想要的方式來(lái)芹橡。筆者看了許多文字描述的萬(wàn)向鎖,但是都沒(méi)搞明白望伦,所以不打算用文字解釋林说,直接推薦一個(gè)視頻煎殷。仔細(xì)看萬(wàn)向鎖的部分,就能明白了腿箩。
先來(lái)說(shuō)一個(gè)解決方法豪直,繞任意一個(gè)單位軸進(jìn)行旋轉(zhuǎn),例如(0.662, 0.2, 0.722)度秘,不過(guò)記住一定要是單位向量顶伞。轉(zhuǎn)換公式像這樣(Rx, Ry, Rz是坐標(biāo)值):
跟其他的轉(zhuǎn)換矩陣相比,是不是頓時(shí)有種鶴立雞群的感覺(jué)剑梳!What the f**k?幸好我們有網(wǎng)絡(luò)這個(gè)東西唆貌,不用死記硬背實(shí)在是太幸福了。順便一提垢乙,解決萬(wàn)向鎖還可以用一種四元數(shù)的東西锨咙,以后我們會(huì)涉及到。
實(shí)戰(zhàn)演練
講了這么多基礎(chǔ)知識(shí)追逮,終于可以動(dòng)手操作了酪刀,是不是等不及了?先別急钮孵,我們是要學(xué)OpenGL的骂倘,花太多的時(shí)間在實(shí)現(xiàn)數(shù)學(xué)庫(kù)上顯然和我們的初衷背道而馳,所以巴席,我們可以采用“拿來(lái)主義”历涝,直接找一個(gè)數(shù)學(xué)庫(kù)用。幸好漾唉,OpenGL的“周邊”就有一個(gè)好的數(shù)學(xué)庫(kù)荧库,叫做:GLM。
到GLM的網(wǎng)站去下載0.9.8版本的數(shù)學(xué)庫(kù)(不要下最新的0.9.9版本赵刑,和我們的代碼不兼容)分衫。沒(méi)法FQ的可以到我的網(wǎng)盤里去下載。下載解壓后般此,把頭文件根目錄(glm目錄蚪战,不是解壓縮后的glm文件夾,而是在里面的glm文件夾)復(fù)制到你的includes目錄下面就可以了铐懊。
設(shè)置完后屎勘,我們需要在代碼中包含需要的頭文件。
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
我們先來(lái)試試這個(gè)庫(kù)有沒(méi)有效居扒。
//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是一個(gè)單位矩陣,讓它平移到(1.0f, 1.0f, 0.0f)的位置產(chǎn)生了一個(gè)平移矩陣喜喂。
vec = trans * vec;
std::cout << "(" << vec.x << "," << vec.y << "," << vec.z << ")" << std::endl;
嗯瓤摧,輸出正確竿裂。
讓我們袖子干吧!把前面章節(jié)中顯示的圖片縮小成原來(lái)的一半照弥,然后再繞著z軸逆時(shí)針旋轉(zhuǎn)90度腻异。
先來(lái)生成矩陣:
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));
矩陣生成完后,我們?nèi)绾巫屗陧旤c(diǎn)著色器中生效呢这揣?想來(lái)你很快就能得到答案悔常,沒(méi)錯(cuò),就是用uniform關(guān)鍵字给赞。先聲明一個(gè)變量uniform mat4 transform
机打,然后在主函數(shù)中調(diào)用gl_Position = transform * vec4(aPos, 1.0f)
。
接下來(lái)片迅,我們要在程序里設(shè)置這個(gè)值残邀。但原有的shader類中沒(méi)有設(shè)置mat4類型的接口,所以我們要添加一個(gè):
void Shader::setMat4(const std::string& name, float value[]) const {
glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, value);
}
介紹一下glUniformMatrix4fv
函數(shù)的參數(shù):
- 參數(shù)一:變量位置
- 參數(shù)二:我們想傳的矩陣個(gè)數(shù)柑蛇,這里我們只設(shè)置一個(gè)芥挣,所以是1
- 參數(shù)三:我們是否想轉(zhuǎn)換矩陣,把行和列交換耻台。OpenGL中的矩陣是列主序的矩陣(和DX中的不同)空免,不過(guò)GLM中生成的矩陣也是列主序的,所以我們?cè)O(shè)置成GL_FALSE盆耽,表示不用轉(zhuǎn)換鼓蜒。
- 參數(shù)四:矩陣數(shù)組。這里我們要把矩陣轉(zhuǎn)換成數(shù)組的格式傳遞征字。
接口寫好后,我們就能在主循環(huán)中使用了:
shader.setMat4("transform", glm::value_ptr(trans));
完成后娇豫,編譯運(yùn)行:
跟我們想象的一樣匙姜!
慢著,這樣就滿足了嗎冯痢?NO氮昧!我們還要讓它動(dòng)起來(lái)。方法也很簡(jiǎn)單浦楣,我們傳入一個(gè)glfwGetTime()作為旋轉(zhuǎn)的弧度就可以了袖肥。像這樣:
trans = glm::rotate(trans, /*glm::radians(90.0f)*/(float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));
如果之前你是在循環(huán)外面生成的轉(zhuǎn)換矩陣,那么你就要把它放到循環(huán)里面去了振劳,這樣隨著每次運(yùn)行椎组,旋轉(zhuǎn)的角度也不一樣。嗯历恐,編譯運(yùn)行寸癌。
效果不錯(cuò)专筷!如果你的程序不這樣的顯示,可以點(diǎn)擊這里下載代碼進(jìn)行比對(duì)蒸苇。
總結(jié)
好了磷蛹,艱苦的數(shù)學(xué)旅程告一段落,我們來(lái)回憶一下都學(xué)了些什么溪烤。首先是向量味咳,以及向量能做的一些運(yùn)算;然后是矩陣檬嘀,以及矩陣的一些運(yùn)算槽驶;接著,我們看到了實(shí)際有用的一些運(yùn)算矩陣枪眉;最后捺檬,我們使用了一個(gè)現(xiàn)成的庫(kù)GLM來(lái)實(shí)現(xiàn)變換坐標(biāo)的效果。呼~休息贸铜!
參考資料:
www.learningopengl.com(很好的學(xué)習(xí)網(wǎng)站堡纬,建議多去看看)