前言
在前兩章,總結(jié)有頂點坐標,紋理坐標。實際上在這之上還有更多的坐標。作者經(jīng)過學習后,在本文總結(jié)一番。
上一篇:OpenGL矩陣
正文
OpenGL希望每一次運行頂點著色器之后,我們所見到的頂點坐標都轉(zhuǎn)化為標準化設(shè)備坐標(NDC)。
也就是說,每個頂點的x,y,z都應(yīng)該在[-1,1]之間,超出這個范圍都應(yīng)該看不見。我們通常會自己設(shè)定一個坐標范圍,之后頂點著色器中所有的坐標都會轉(zhuǎn)化為標準化設(shè)備坐標,然后這些坐標經(jīng)歷光柵器,轉(zhuǎn)化為屏幕上的二維坐標或者像素。
將坐標轉(zhuǎn)化標準化坐標,在轉(zhuǎn)化為屏幕上的坐標都是分步驟來的,中間經(jīng)歷多個坐標系。把物體的坐標變換到幾個過渡坐標系,優(yōu)點在于一些操作就會就會很簡單,大致分為如下5個坐標系統(tǒng):
- 1.局部空間
- 2.世界空間
- 3.觀察空間
- 4.裁剪空間
- 5.屏幕空間
為了將坐標從一個坐標系變換到另一個坐標系,我們需要幾個變換矩陣來完成這個過程。
分別是模型,觀察和投影。我們的頂點坐標起始于局部空間,也稱為局部坐標。經(jīng)過模型變換之后,就變成了世界空間。經(jīng)過觀察變換之后就變成了觀察空間,經(jīng)過投影變換之后就變化為裁剪坐標,最后輸出到設(shè)備,變成屏幕坐標。
過程如下圖:
稍微說明一下,對各個變換的理解。
局部坐標就是沒有處理,最初的我們傳入頂點著色器的坐標。在這個過程我們,可能會存在兩種坐標一種叫做頂點坐標,一種是紋理坐標。這個過程我理解為把一個物體構(gòu)造起來。
世界坐標是經(jīng)過模型矩陣變化之后的坐標。這過程我是這么理解的,假如有這么一個3D的世界,我們物體已經(jīng)構(gòu)建好了,那么接下來就是把這個物體要擺在這個世界的哪里,需要從原點位移多少,旋轉(zhuǎn)的角度等等。
觀察坐標,是上面的坐標再一次經(jīng)過觀察矩陣進行變換得來。實際上這個過程,我們可以形象的比喻成,我們的眼睛從哪個方向去看這個世界中的物體。
最后一個裁剪坐標,是經(jīng)過投影矩陣變換得來。我們可以形象比喻為如下這種狀況,我們通過攝像機或者眼睛去觀察,往往投影在我們的樣子是經(jīng)過類似投影機生成的圖像,生成一個用2d圖像代表的3d圖像。
如下圖,一個無限延伸的鐵路,在現(xiàn)實世界中是一個平行的鐵軌,在我們的眼睛中是一個在地平線初相交的兩條直線。
由投影矩陣創(chuàng)建的觀察箱(Viewing Box)被稱為平截頭體(Frustum),每個出現(xiàn)在平截頭體范圍內(nèi)的坐標都會最終出現(xiàn)在用戶的屏幕上。將特定范圍內(nèi)的坐標轉(zhuǎn)化到標準化設(shè)備坐標系的過程(而且它很容易被映射到2D觀察空間坐標)被稱之為投影(Projection),因為使用投影矩陣能將3D坐標投影(Project)到很容易映射到2D的標準化設(shè)備坐標系中。
一旦所有頂點被變換到裁剪空間,最終的操作——透視除法(Perspective Division)將會執(zhí)行,在這個過程中我們將位置向量的x,y,z分量分別除以向量的齊次w分量;透視除法是將4D裁剪空間坐標變換為3D標準化設(shè)備坐標的過程。這一步會在每一個頂點著色器運行的最后被自動執(zhí)行。
在這一階段之后,最終的坐標將會被映射到屏幕空間中(使用glViewport中的設(shè)定),并被變換成片段。
將觀察坐標變換為裁剪坐標的投影矩陣可以為兩種不同的形式,每種形式都定義了不同的平截頭體。我們可以選擇創(chuàng)建一個正射投影矩陣(Orthographic Projection Matrix)或一個透視投影矩陣(Perspective Projection Matrix)。
投影矩陣
從上面的小結(jié),我們能夠輕松的理解到,世界空間,觀察坐標,實際上就是對原來的頂點坐標做一次矩陣左乘操作。但是這個投影矩陣比較特殊,分為 正射投影矩陣以及透視矩陣,這些有研究的價值。
正射投影矩陣
正射投影矩陣構(gòu)成一個立方體平截頭,它定義了一個裁剪空間,在這個空間之外的所有頂點都會被裁剪掉。
我們定義這個裁剪的空間的時候,能夠定義寬高和遠平面以及近平面。任何超出近平面和遠平面的坐標都會被裁剪掉。正射平截頭把平截頭體內(nèi)部的坐標最后都轉(zhuǎn)化為屏幕坐標,因為每個向量的w分量都沒有進行改變;如果w分量等于1.0,透視除法則不會改變這個坐標。
舉個形象的例子,我們通過一個筆直的鏡頭,通過正方形隧道看到遠方的物體的正視圖,左視圖,俯視圖等情況。
當初我看到這里的第一反應(yīng)是,正射投影矩陣是這樣的,但是像素點究竟經(jīng)過什么變換,最后變到哪里,投射到哪里去,對著一切的一切都是蒙蔽。為此,為了弄懂它,實際上我需要對正射投影矩陣證明有一定的了解。
正射投影矩陣證明
我們從上面的定義能夠理解到,正射投影矩陣的作用就是把一個物體能夠在一個[-1,1]范圍內(nèi)顯示出來,我們可以把這個過程看成一次縮放和位移的過程。
如果灰色的物體就是就是原來的世界坐標中的物體。我們要經(jīng)過一定的變換的得到把視圖容納到下面這個x,y,z坐標系。
假如我們?nèi)×⒎襟w其中一面,如果面中的邊緣點都能投影到[-1,1]之間。那么這個立方體就能容納到我們的NDC坐標中。假設(shè)立方體一面中如上圖有三個如此的點:(l,t,-n),(r,t,-n),(r,b,-n).
為什么我們倒著z軸過來設(shè)置點呢?因為OpenGL和DirectX不一樣,是一個右手坐標系。
按照慣例,OpenGL是一個右手坐標系。簡單來說,就是正x軸在你的右手邊,正y軸朝上,而正z軸是朝向后方的。想象你的屏幕處于三個軸的中心,則正z軸穿過你的屏幕朝向你。
定義這個立方體中的某個點投影到到未知的NDC坐標系的
我們以X軸上的為例子進行推導(dǎo)。
對于x軸上的點,已知:
我們稍微做一下變化,為了讓立方體內(nèi)x的點位于[-1,1]:
=
但是這樣還是不夠符合我們的范圍,大致上只有一半,因此我們還進一步對這個范圍做一次變換,先乘以2,再減1:
=
=
這樣就能通過乘法和減法,簡單的把這個壓縮到[-1,1]之間,也就是說對應(yīng)到了
我們可以得到第一個點:
同理我們可以得到y(tǒng)軸上的點關(guān)系:
假如立方體,z軸方向,最近是n的距離,最遠處是一個f的距離。
根據(jù)OpenGL這個右手坐標,我們需要把物體的z軸投影到NDC的z軸的[-1,1].因此我們可以按照上面的z軸一樣做變換。
=
=
=
同理可以得到下面一個等式:
我們得到幾個對應(yīng)關(guān)系之后,我們就能寫出如下的正射投影矩陣
但是實際上,如果我們的視體是對稱的(r = -l,t = -b),那么我們可以通過這條件,得到一個更加簡單的矩陣。
這就是正射投影矩陣來源。這是十分簡單的證明過程,接下來透視矩陣會稍微復(fù)雜一點點,需要適當?shù)氖褂靡稽c三角函數(shù)。
透視投影矩陣
透視投影,實際上就是模擬我們的人眼,能夠把3d的映像轉(zhuǎn)化為了2d像素投影在屏幕上,就像上面我舉的鐵軌例子。
這個投影矩陣將給定的平截頭體范圍映射到裁剪空間,除此之外還修改了每個頂點坐標的w值,從而使得離觀察者越遠的頂點坐標w分量越大。被變換到裁剪空間的坐標都會在-w到w的范圍之間(任何大于這個范圍的坐標都會被裁剪掉)。
OpenGL要求所有可見的坐標都落在-1.0到1.0范圍內(nèi),作為頂點著色器最后的輸出,因此,一旦坐標在裁剪空間內(nèi)之后,透視除法就會被應(yīng)用到裁剪空間坐標上:
頂點坐標的每個分量都會除以它的w分量,距離觀察者越遠頂點坐標就會越小。這是也是w分量非常重要的另一個原因,它能夠幫助我們進行透視投影。最后的結(jié)果坐標就是處于標準化設(shè)備空間中的。
齊次坐標系
這里面涉及到一個新的概念,齊次坐標系。
我們先來理解以下齊次性:
一般地,在數(shù)學里面,如果一個函數(shù)的自變量乘以一個系數(shù),那么這個函數(shù)將乘以這個系數(shù)的k次方,我們稱這個函數(shù)為k次齊次函數(shù),也就是:
如果函數(shù) f(v)滿足
f(ax)=a^k f(x),
其中,x是輸入變量,k是整數(shù),a是非零的實數(shù),則稱f(x)是k次齊次函數(shù)。
比如:一次齊次函數(shù)就是線性函數(shù)2.多項式函數(shù) f(x,y)=x2+y2
因為f(ax,ay)=a^2f(x,y),所以f(x,y)是2次齊次函數(shù)。
齊次性在數(shù)學中描述的是函數(shù)的一個倍數(shù)的性質(zhì)。
在理解了齊次性之后,我們就能比較好的理解齊次坐標系。
齊次坐標
在數(shù)學里,齊次坐標(homogeneous coordinates),或投影坐標(projective coordinates)是指一個用于投影幾何里的坐標系統(tǒng),如同用于歐氏幾何里的笛卡兒坐標一般。
實投影平面可以看作是一個具有額外點的歐氏平面,這些點稱之為無窮遠點,并被認為是位于一條新的線上(該線稱之為無窮遠線)。每一個無窮遠點對應(yīng)至一個方向(由一條線之斜率給出),可非正式地定義為一個點自原點朝該方向移動之極限。在歐氏平面里的平行線可看成會在對應(yīng)其共同方向之無窮遠點上相交。給定歐氏平面上的一點 (x, y),對任意非零實數(shù) Z,三元組 (xZ, yZ, Z) 即稱之為該點的齊次坐標。依據(jù)定義,將齊次坐標內(nèi)的數(shù)值乘上同一個非零實數(shù),可得到同一點的另一組齊次坐標。例如,笛卡兒坐標上的點 (1,2) 在齊次坐標中即可標示成 (1,2,1) 或 (2,4,2)。原來的笛卡兒坐標可透過將前兩個數(shù)值除以第三個數(shù)值取回。因此,與笛卡兒坐標不同,一個點可以有無限多個齊次坐標表示法。
一條通過原點 (0, 0) 的線之方程可寫作 nx + my = 0,其中 n 及 m 不能同時為 0。以參數(shù)表示,則能寫成 x = mt, y = ? nt。令 Z=1/t,則線上的點之笛卡兒坐標可寫作 (m/Z, ? n/Z)。在齊次坐標下,則寫成 (m, ? n, Z)。當 t 趨向無限大,亦即點遠離原點時,Z 會趨近于 0,而該點的齊次坐標則會變成 (m, ?n, 0)。因此,可定義 (m, ?n, 0) 為對應(yīng) nx + my = 0 這條線之方向的無窮遠點之齊次坐標。因為歐氏平面上的每條線都會與透過原點的某一條線平行,且因為平行線會有相同的無窮遠點,歐氏平面每條線上的無窮遠點都有其齊次坐標。
概括來說:
- 投影平面上的任何點都可以表示成一三元組 (X, Y, Z),稱之為該點的'齊次坐標或投影坐標,其中 X、Y 及 Z 不全為 0。
- 以齊次坐標表表示的點,若該坐標內(nèi)的數(shù)值全乘上一相同非零實數(shù),仍會表示該點。
- 相反地,兩個齊次坐標表示同一點,當且僅當其中一個齊次坐標可由另一個齊次坐標乘上一相同非零常數(shù)得取得。
- 當 Z 不為 0,則該點表示歐氏平面上的該 (X/Z, Y/Z)。
- 當 Z 為 0,則該點表示一無窮遠點。
- 注意,三元組 (0, 0, 0) 不表示任何點。原點表示為 (0, 0, 1)[3]。
齊次坐標系為什么在計算機視覺中運用廣泛?
主要有兩點:
- 1.區(qū)分向量還是點
- 2.更加易于仿射(線性)變換
1.區(qū)分向量還是點
(1) 從普通坐標轉(zhuǎn)成齊次坐標時:
如果(x,y,z)是向量,那么齊次坐標為(x,y,z,0)
如果(x,y,z)是 3D 點,那么齊次坐標為 (x,y,z,1)
(2) 從齊次坐標轉(zhuǎn)成普通坐標時:
如果 (x,y,z,1)(3D點),在普通坐標系下為(x,y,z)
如果 (x,y,z,0)(向量),在普通坐標系下為(x,y,z)
這樣就能通過w的分量來察覺到是點還是向量
2.更加易于仿射(線性)變換
對于平移,旋轉(zhuǎn)和縮放,都通過矩陣的乘法完成的。在之前的文章我都是默認使用齊次坐標系進行計算。假如我們放棄了齊次坐標系,使用傳統(tǒng)的坐標系會如何?
就以最簡單的平移為例子。
首先一個矢量來表示空間中的一個點:r=[rx,ry,rz]r=[rx,ry,rz]
如果我們要將其平移, 平移的矢量為:t=[tx,ty,tz]t=[tx,ty,tz]
那么正常的做法就是:r+t=[rx+tx,ry+ty,rz+tz]
假如我們不使用齊次坐標系需要一個圖像平移,那么我們就需要一個移動矩陣m乘以原來坐標像素矩陣r,辦到以下的情況
很可惜,我們無法找到這么一個矩陣。
但是如果我們加多一個維度w,就能找到這么一個矩陣
這就是齊次坐標系的便利。我們就在這個基礎(chǔ)上做投影矩陣的推導(dǎo)。
透視投影矩陣的推導(dǎo)
能看到此時的平截頭像一個梯形。小的那一面叫做近平面,大的那一面叫做遠平面。這個梯形是怎么來的?實際上如下圖,就像一個攝像機以一定大小的視角對這個空間的觀察:
能看到整個梯形的平截頭,是由一個攝像機,以fov的角度觀察世界,攝像機距離外面世界的距離就是攝像機到近平面的距離。遠平面就是指我們能夠看到最遠的地方是哪里,物體擺放的位置超出了遠平面,就看不到了。
我們能夠根據(jù)上面的圖,如果原點是我們的視角,從這個視角看出去,就能構(gòu)成一個三角形。
我們能夠輕松的看到,此時剛好以fov的角度構(gòu)成一個直角三角形。不妨把整個透視投影過程看成:把平截頭內(nèi)的點,投射到近平面上。我們要求的透視投影點,就是上面黑色的點。
我們拆開了x,y軸拆開觀察,就如下情況
我們就根據(jù)這兩個圖分別對x,y軸的平截頭投影到NDC的證明。
老規(guī)矩,我們收集一下圖中有用的信息。
- 視角原點到近平面的距離為n,距離遠平面為f
- 已知平截頭內(nèi)一點
- 已知視角角度
- 求投影點
因為是來自同一個角度,同一個原點的兩條射線。因此原點,z軸相交的點(0,0,-n),構(gòu)成的三角形A,和原點,
,
構(gòu)成的三角形B是相似三角形。
因此,我們能夠得到如下的比例:
稍微轉(zhuǎn)化一下:
同理:
能看到,此時的投影后的x和y都依賴于.
我們根據(jù)齊次坐標系知道如下的等式:
裁剪矩陣 = 投影矩陣 * 空間矩陣
NDC 矩陣 = 裁剪矩陣 / w分量
因此,我們?yōu)榱俗尶臻g矩陣可以正確的除以。因此我們需要構(gòu)造如下的矩陣:
我們已經(jīng)推導(dǎo)出了投影矩陣的w分量的參數(shù)。讓我們繼續(xù)推導(dǎo)x,y,z上的分量。
根據(jù)我們在正射投影推導(dǎo)出來的結(jié)果,在透視投影也同樣適用。我們同樣要把平截頭內(nèi)所有點,都壓縮到NDC的[-1,1]之內(nèi)。因此,正射投影的結(jié)論一樣能夠放到這里來使用。
正射投影矩陣中:
不過這只是簡單的做縮放和位移。在透視投影中,平截頭內(nèi)和NDC兩個點之間還需要依賴
我們可以把它化簡為除以w的分量
這樣我們就能獲得透視投影矩陣x,y的分量
這樣,我們就還差z軸的分量還沒有求出來。但是我們知道,z軸不依賴x,y軸。因此,我們可以把矩陣寫出如下形式:
就可以寫出如下等式:
但是我們根據(jù)定義,能夠知道這個坐標將會壓縮在[-1,1]的區(qū)間。換句話說我們帶入
必定對應(yīng)上-1和1之間。
2個未知數(shù),兩個連立方程式,必定有解。
求解A,B:
此時我們就完成了透視投影矩陣的推導(dǎo):
同樣的,如果視體是對稱的,我們一樣可以獲得一個簡化的矩陣
這樣就把正射投影矩陣和透視投影矩陣證明完畢。
實戰(zhàn)演練
從世界空間某個角度觀察圖片
那么我們開發(fā)中是不是要這么麻煩的處理編寫投影矩陣呢?實際上glm已經(jīng)已經(jīng)提供了相關(guān)的函數(shù)了:
正射投影:
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
前兩個參數(shù)指定了平截頭體的左右坐標,第三和第四參數(shù)指定了平截頭體的底部和頂部。通過這四個參數(shù)我們定義了近平面和遠平面的大小,然后第五和第六個參數(shù)則定義了近平面和遠平面的距離。這個投影矩陣會將處于這些x,y,z值范圍內(nèi)的坐標變換為標準化設(shè)備坐標。
透視投影矩陣:
glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
它的第一個參數(shù)定義了fov的值,它表示的是視野(Field of View),并且設(shè)置了觀察空間的大小。如果想要一個真實的觀察效果,它的值通常設(shè)置為45.0f,但想要一個末日風格的結(jié)果你可以將其設(shè)置一個更大的值。第二個參數(shù)設(shè)置了寬高比,由視口的寬除以高所得。第三和第四個參數(shù)設(shè)置了平截頭體的近和遠平面。我們通常設(shè)置近距離為0.1f,而遠距離設(shè)為100.0f。所有在近平面和遠平面內(nèi)且處于平截頭體內(nèi)的頂點都會被渲染。
如果我們把攝像機放到z軸上方,看一個在世界坐標上,沿著x軸做向上反轉(zhuǎn)-55度的笑臉箱子,該如何處理?
我們繼續(xù)沿用上一期的代碼,做一定的修改
首先編寫對應(yīng)的頂點著色器
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
void main(){
gl_Position = projection * view * model * vec4(aPos,1.0);
TexCoord = vec2(aTexCoord.x,aTexCoord.y);
}
設(shè)定好之后,我們?nèi)ヌ幚硎澜缈臻g,觀察者空間,投影空間的矩陣:
shader->use();
//設(shè)置的是紋理單元
glUniform1i(glGetUniformLocation(shader->ID,"ourTexture"),0);
shader->setInt("ourTexture2", 1);
//模型矩陣
//物體坐標變換到世界坐標
//把它從局部坐標,擺到世界中,是沿著x軸旋轉(zhuǎn)-55度
glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f,0.0f,0.0f));
//觀察矩陣
//觀察的位置的移動
//從世界坐標到觀察空間
//模擬我們攝像機,沿著z軸向后移3個單位
glm::mat4 view = glm::mat4(1.0f);
view = glm::translate(view, glm::vec3(0.0,0.0,-3.0f));
//投影矩陣
//觀察到裁剪空間
glm::mat4 projection = glm::mat4(1.0f);
projection = glm::perspective(glm::radians(45.0f),
((float)engine->screenWidth)/((float)engine->screenHeight), 0.1f, 100.0f);
GLuint modelLoc = glGetUniformLocation(shader->ID,"model");
glUniformMatrix4fv(modelLoc,1,GL_FALSE,&model[0][0]);
GLuint viewLoc = glGetUniformLocation(shader->ID,"view");
glUniformMatrix4fv(viewLoc,1,GL_FALSE,&view[0][0]);
GLuint projectionLoc = glGetUniformLocation(shader->ID,"projection");
glUniformMatrix4fv(projectionLoc,1,GL_FALSE,&projection[0][0]);
engine->loop(VAO, VBO, texture, 1,shader, [](Shader* shader,GLuint VAO,
GLuint* texture,GLFWwindow *window){
//箱子
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,texture[0]);
//笑臉
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D,texture[1]);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);
});
}
從世界空間觀察某個3d物體。
我們先通過36點頂點坐標構(gòu)造一個立方體:
void create3D(GLuint& VAO,GLuint& VBO){
float vertices[] = {
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f
};
glGenVertexArrays(1,&VAO);
glBindVertexArray(VAO);
glGenBuffers(1,&VBO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1,2,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(3*sizeof(float)));
glEnableVertexAttribArray(1);
//glBindVertexArray(0);
}
緊接著繪制:
void showOneCube(Shader *shader){
//模型矩陣
//物體坐標變換到世界坐標
//把它從局部坐標,擺到世界中,是沿著x軸旋轉(zhuǎn)90度
glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, glm::radians(90.0f), glm::vec3(0.5f,1.0f,0.0f));
//觀察矩陣
//觀察的位置的移動
//從世界坐標到觀察空間
//模擬我們攝像機,沿著z軸向后移3個單位
glm::mat4 view = glm::mat4(1.0f);
view = glm::translate(view, glm::vec3(0.0,0.0,-3.0f));
//投影矩陣
//觀察到裁剪空間
glm::mat4 projection = glm::mat4(1.0f);
projection = glm::perspective(glm::radians(45.0f),
(width/height), 0.1f, 100.0f);
// GLuint modelLoc = glGetUniformLocation(shader->ID,"model");
// glUniformMatrix4fv(modelLoc,1,GL_FALSE,&model[0][0]);
shader->setMat4("model", &model[0][0]);
// GLuint viewLoc = glGetUniformLocation(shader->ID,"view");
// glUniformMatrix4fv(viewLoc,1,GL_FALSE,&view[0][0]);
shader->setMat4("view", &view[0][0]);
shader->setMat4("projection", &projection[0][0]);
// GLuint projectionLoc = glGetUniformLocation(shader->ID,"projection");
// glUniformMatrix4fv(projectionLoc,1,GL_FALSE,&projection[0][0]);
}
if(shader&&shader->isCompileSuccess()){
shader->use();
//設(shè)置的是紋理單元
glUniform1i(glGetUniformLocation(shader->ID,"ourTexture"),0);
shader->setInt("ourTexture2", 1);
width = (float)engine->screenWidth;
height = (float)engine->screenHeight;
glEnable(GL_DEPTH_TEST);
engine->loop(VAO, VBO, texture, 1,shader, [](Shader* shader,GLuint VAO,
GLuint* texture,GLFWwindow *window){
//箱子
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,texture[0]);
//笑臉
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D,texture[1]);
showOneCube(shader);
//showMoreCube(shader);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
});
}
繪制更多的立方體并且轉(zhuǎn)動起來
如果我們需要讓3的倍數(shù)的立方體旋轉(zhuǎn)起來又如何?
我們需要更多的世界坐標來設(shè)置立方體:
float width = 0;
float height = 0;
//世界坐標
glm::vec3 cubePositions[] = {
glm::vec3( 0.0f, 0.0f, 0.0f),
glm::vec3( 2.0f, 5.0f, -15.0f),
glm::vec3(-1.5f, -2.2f, -2.5f),
glm::vec3(-3.8f, -2.0f, -12.3f),
glm::vec3( 2.4f, -0.4f, -3.5f),
glm::vec3(-1.7f, 3.0f, -7.5f),
glm::vec3( 1.3f, -2.0f, -2.5f),
glm::vec3( 1.5f, 2.0f, -2.5f),
glm::vec3( 1.5f, 0.2f, -1.5f),
glm::vec3(-1.3f, 1.0f, -1.5f)
};
循環(huán)繪制:
void showMoreCube(Shader *shader){
glm::mat4 view = glm::mat4(1.0f);
glm::mat4 projection = glm::mat4(1.0f);
view = glm::translate(view, glm::vec3(0.0,0.0,-3.0f));
projection = glm::perspective(glm::radians(45.0f),
(width/height), 0.1f, 100.0f);
shader->setMat4("view", glm::value_ptr(view));
shader->setMat4("projection", glm::value_ptr(projection));
for(int i = 0;i<10;i++){
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, cubePositions[i]);
float angle = 20.0f * i;
if(i % 3 == 0){
model = glm::rotate(model, (float)glfwGetTime(), glm::vec3(1.0f,3.0f,0.5f));
}else{
model = glm::rotate(model, angle, glm::vec3(1.0f,3.0f,0.5f));
}
shader->setMat4("model", glm::value_ptr(model));
glDrawArrays(GL_TRIANGLES,0,36);
}
}
總結(jié)
OpenGL的有5個坐標系,局部空間,世界空間,觀察空間,裁剪空間,屏幕空間。
我們能夠通過如下的公式來對一個在局部空間中用頂點坐標構(gòu)建的物體做變換到一個從攝像機角度觀察的世界中的物體:
當我們要從裁剪坐標換算到NDC坐標,要符合如下公式:
實際上這就是所有坐標之間的關(guān)系。