從0開(kāi)始的OpenGL學(xué)習(xí)(七)-轉(zhuǎn)換

本章主要解決這個(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)表示一下。

向量圖(來(lái)自:www.learningopengl.com)

因?yàn)橄蛄恐挥写笮『头较騼蓚€(gè)要素,起點(diǎn)并不算在內(nèi),所以我們可以認(rèn)為向量w和向量v是相同的。本文中向量用粗斜體字來(lái)表示,比如之前的向量w和向量v。為了顯示的明顯一點(diǎn),我們用垂直行列式的方式來(lái)表示一個(gè)向量,例如:

v向量

因?yàn)橄蛄亢形恢玫男畔?,不知從什么時(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é)果。

標(biāo)量加法運(yùn)算

向量取反

向量取反操作直接把向量的方向變反了,對(duì)向量的大小并沒(méi)有什么影響。運(yùn)算方式非常簡(jiǎn)單,直接看圖:

向量取反

向量加減

向量相加操作就是把兩個(gè)向量的相應(yīng)分量相加,得到一個(gè)新的向量。

向量相加

向量相減操作就是把兩個(gè)向量的對(duì)應(yīng)分量相減,得到一個(gè)新的向量。

向量相減

長(zhǎng)度

向量長(zhǎng)度

長(zhǎng)度計(jì)算通過(guò)最普通的勾股定理就能算出來(lái)

計(jì)算長(zhǎng)度

向量乘法

普通的乘法(對(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值。像這樣:

點(diǎn)乘公式

特別地,如果兩個(gè)向量為單位向量(單位向量是指長(zhǎng)度為1的向量),那么這個(gè)公式就退化成:

單位向量的點(diǎn)乘公式

看到?jīng)],就只是兩向量夾角的cos值,這個(gè)功能在進(jìn)行光照計(jì)算的時(shí)候非常有用,我們計(jì)算光照的時(shí)候優(yōu)勢(shì)不需要知道它的大小,只需要知道其法向量和光照方向的夾角就好了。

從公式上來(lái)看,兩個(gè)向量的點(diǎn)乘每個(gè)分量相乘的結(jié)果在求和。

點(diǎn)乘的向量公式
叉乘

叉乘只在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)看:

2 x 3矩陣

矩陣可以通過(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)量:

標(biāo)量加法
標(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è)新的矩陣就行了。

標(biāo)量乘法
和矩陣

和矩陣相乘就不是那么令人愉快了。在矩陣相乘之前,有兩條規(guī)則我們要來(lái)看看,這是兩個(gè)最基本的原則:

  1. 相乘的兩個(gè)矩陣,第一個(gè)矩陣的列數(shù)必須要等于第二個(gè)矩陣的行數(shù)!
  2. 矩陣相乘不滿足交換律,也就是說(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ì)其有啥改變。

單位矩陣運(yùn)算結(jié)果

你可能會(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)|西:

  1. 繞著什么旋轉(zhuǎn)
  2. 往哪個(gè)方向旋轉(zhuǎn)
  3. 旋轉(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)的公式:

繞X軸旋轉(zhuǎn)的公式

然后是繞Y軸旋轉(zhuǎn)的公式:

繞Y軸旋轉(zhuǎn)的公式

最后是繞Z軸旋轉(zhuǎn)的公式:

繞Z軸旋轉(zhuǎn)的公式

這些公式不需要記,你可以非??斓卦诰W(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)的公式

跟其他的轉(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)行:

運(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)行。

旋轉(zhuǎ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)站,建議多去看看)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容