http://huangwei.pro/2015-08/modern-opengl3/

本文中,我會(huì)將不會(huì)動(dòng)的2D三角形替換為旋轉(zhuǎn)的3D立方體。你會(huì)看到這樣的效果:

現(xiàn)在我們終于能在屏幕上搞點(diǎn)有趣的東西了,我放了更多的動(dòng)圖在這里:http://imgur.com/a/x8q7R
為了生成旋轉(zhuǎn)立方體,我們需要學(xué)些關(guān)于矩陣的數(shù)學(xué),用于創(chuàng)建透視投影,旋轉(zhuǎn),平移和“相機(jī)”概念。我們還有必要學(xué)習(xí)些深度緩沖,和典型的隨時(shí)間改變的3D應(yīng)用,比如動(dòng)畫。
獲取代碼
所有例子代碼的zip打包可以從這里獲取:https://github.com/tomdalling/opengl-series/archive/master.zip。
這一系列文章中所使用的代碼都存放在:https://github.com/tomdalling/opengl-series。你可以在頁面中下載zip,加入你會(huì)git的話,也可以復(fù)制該倉庫。
本文代碼你可以在source/02_textures
目錄里找到。使用OS X系統(tǒng)的,可以打開根目錄里的opengl-series.xcodeproj
,選擇本文工程。使用Windows系統(tǒng)的,可以在Visual Studio 2013里打開opengl-series.sln
,選擇相應(yīng)工程。
工程里已包含所有依賴,所以你不需要再安裝或者配置額外的東西。如果有任何編譯或運(yùn)行上的問題,請(qǐng)聯(lián)系我。
矩陣原理
本文講的最多的就是關(guān)于3D中的矩陣,所以讓我們?cè)趯懘a前先了解下矩陣原理。我不會(huì)過多關(guān)注數(shù)學(xué),網(wǎng)上有很多好的這類資源。我們只需要使用GLM來實(shí)現(xiàn)相關(guān)運(yùn)算。我會(huì)注重于那些應(yīng)用在我們3D程序里的矩陣。
矩陣是用來進(jìn)行3D變換。可能的變換包括(點(diǎn)擊可以看動(dòng)畫):
一個(gè)矩陣是一個(gè)數(shù)字表格,像這樣:

矩陣英文matrix的復(fù)數(shù)形式是matrices。
不同的數(shù)值的能產(chǎn)生不同類型的變換。上面的那個(gè)矩陣會(huì)繞著Z軸旋轉(zhuǎn)90°。我們會(huì)使用GLM來創(chuàng)建矩陣,所以我們不用理解如何計(jì)算出這些數(shù)值。
矩陣可以有任意行和列,但3D變換使用4×4矩陣,就像上面看到的那樣。無論我在那說到“矩陣”,指的就是4×4矩陣。
當(dāng)用代碼實(shí)現(xiàn)矩陣時(shí),一般會(huì)用一個(gè)浮點(diǎn)數(shù)組來表示。我們使用glm::mat4
類來表示4×4矩陣。
兩個(gè)最重要的矩陣操作是:
- matrix × matrix = combined matrix
- matrix × coordinate = transformed coordinate
矩陣 × 矩陣
當(dāng)你要對(duì)兩個(gè)矩陣進(jìn)行相乘時(shí),它們的乘積是一個(gè)包含兩者變換的新矩陣。
比如,你將一個(gè)旋轉(zhuǎn)矩陣乘以一個(gè)平移矩陣,得到的結(jié)果就是“組合”矩陣,即先旋轉(zhuǎn)然后平移。下面的例子展示這類矩陣相乘。

不像普通的乘法,矩陣乘法中順序很重要。 比如,A
和B
是矩陣,A*B
不一定等于B*A
。下面我們會(huì)使用相同的矩陣,但改變下乘法順序:

注意不同的順序,結(jié)果也不同。下面動(dòng)畫說明順序有多重要。相同的矩陣,不同的順序。兩個(gè)變換分別是沿Y軸上移,和旋轉(zhuǎn)45°。


當(dāng)你編碼的時(shí)候,假如看到變換出錯(cuò),請(qǐng)回頭檢查下你的矩陣運(yùn)算是否是正確的順序。
矩陣 × 坐標(biāo)
當(dāng)你用矩陣乘以一個(gè)坐標(biāo)時(shí),它們的乘積就是一個(gè)變換后的新坐標(biāo)。
比如,你有上面提到的旋轉(zhuǎn)矩陣,乘上坐標(biāo)(1,1,0),它的結(jié)果就是(-1,1,0)。變換后的坐標(biāo)就是原始坐標(biāo)繞著Z周旋轉(zhuǎn)90°。下面是該乘法的圖例:

為何我們會(huì)使用4D坐標(biāo)
你可能注意到了上面的坐標(biāo)是4D的,而非3D。它的格式是這樣的:

為何我們會(huì)使用4D坐標(biāo)?因?yàn)槲覀冃枰?x4的矩陣完成所有我們需要的3D變換。不管怎樣,矩陣乘法需要左邊的列數(shù)等于右邊的行數(shù)。這就意味著4x4矩陣無法與3D坐標(biāo)相乘,因?yàn)榫仃囉?列,但坐標(biāo)只有3行。我們需要使用4D坐標(biāo),因?yàn)?x4的矩陣需要用它們來完成矩陣運(yùn)算。
一些變換,比如旋轉(zhuǎn),縮放,只需要3x3矩陣。對(duì)于這些變換,我們不需要4D坐標(biāo),因?yàn)?D坐標(biāo)就能運(yùn)算。但無論如何,變換需要至少是4x3的矩陣,而透視投影矩陣需要4x4矩陣,而我們兩者都會(huì)用到,所以我們強(qiáng)制使用4D。
這些被稱為齊次坐標(biāo)。在后續(xù)的教程里,我們會(huì)講到有向光照,那里我們會(huì)學(xué)到有關(guān)“W”維度的表示。在這里,我們只需要將3D轉(zhuǎn)換為4D。3D轉(zhuǎn)換為4D只要將第四維坐標(biāo)“W”設(shè)為1即可。比如,坐標(biāo)(22,33,44)轉(zhuǎn)換為:

當(dāng)需要將4D坐標(biāo)變?yōu)?D時(shí),假如“W”維度是1,你可以直接忽略它,使用X,Y,Z的值即可。如果你發(fā)現(xiàn)“W”的值不為1,好吧,你就需要做些額外處理,或者這里出了個(gè)bug。
構(gòu)造一個(gè)立方體
代碼上第一個(gè)變動(dòng)就是用立方體替換之前的三角形。
我們用三角形來構(gòu)造立方體,用兩個(gè)三角形表示6個(gè)面的每個(gè)面。在舊版本的OpengGL中,我們可以使用1個(gè)正方形(GL_QUADS
)來替代2個(gè)三角表示每個(gè)面,但GL_QUADS
已經(jīng)被現(xiàn)代版本的OpenGL給移除了。X,Y,Z坐標(biāo)值域?yàn)?1到1,這意味著立方體是兩個(gè)單位寬,立方體中心點(diǎn)在原點(diǎn)(原點(diǎn)坐標(biāo)(0,0,0))。我們將使用256×256的貼圖給立方體每個(gè)面貼上。后序文章中都會(huì)使用這個(gè)數(shù)據(jù),我們不需要改變太多。這里有立方體數(shù)據(jù):
GLfloat vertexData[] = { // X Y Z U V // bottom -1.0f,-1.0f,-1.0f, 0.0f, 0.0f, 1.0f,-1.0f,-1.0f, 1.0f, 0.0f, -1.0f,-1.0f, 1.0f, 0.0f, 1.0f, 1.0f,-1.0f,-1.0f, 1.0f, 0.0f, 1.0f,-1.0f, 1.0f, 1.0f, 1.0f, -1.0f,-1.0f, 1.0f, 0.0f, 1.0f, // top -1.0f, 1.0f,-1.0f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f,-1.0f, 1.0f, 0.0f, 1.0f, 1.0f,-1.0f, 1.0f, 0.0f, -1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, // front -1.0f,-1.0f, 1.0f, 1.0f, 0.0f, 1.0f,-1.0f, 1.0f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,-1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, // back -1.0f,-1.0f,-1.0f, 0.0f, 0.0f, -1.0f, 1.0f,-1.0f, 0.0f, 1.0f, 1.0f,-1.0f,-1.0f, 1.0f, 0.0f, 1.0f,-1.0f,-1.0f, 1.0f, 0.0f, -1.0f, 1.0f,-1.0f, 0.0f, 1.0f, 1.0f, 1.0f,-1.0f, 1.0f, 1.0f, // left -1.0f,-1.0f, 1.0f, 0.0f, 1.0f, -1.0f, 1.0f,-1.0f, 1.0f, 0.0f, -1.0f,-1.0f,-1.0f, 0.0f, 0.0f, -1.0f,-1.0f, 1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f,-1.0f, 1.0f, 0.0f, // right 1.0f,-1.0f, 1.0f, 1.0f, 1.0f, 1.0f,-1.0f,-1.0f, 1.0f, 0.0f, 1.0f, 1.0f,-1.0f, 0.0f, 0.0f, 1.0f,-1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,-1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f };
我們需要更改下Render
函數(shù)中glDrawArrays
調(diào)用,之前是用來繪制三角形的。立方體6個(gè)面,每個(gè)面有2個(gè)三角形,每個(gè)三角形有3個(gè)頂點(diǎn),所以需要繪制的頂點(diǎn)數(shù)是:6 × 2 × 3 = 36。新的glDrawArrays
調(diào)用像這樣:
glDrawArrays(GL_TRIANGLES, 0, 6*2*3);
最后,我們使用新的貼圖“wooden-crate.jpg”,我們更改LoadTexture
中的文件名,如下:
tdogl::Bitmap bmp = tdogl::Bitmap::bitmapFromFile(ResourcePath("wooden-crate.jpg"));
就是這樣!我們已經(jīng)提供了所有繪制帶貼圖立方體的需要用到的數(shù)據(jù)。假如你運(yùn)行程序,你可以看到這樣的:

此時(shí)此刻,我們有兩個(gè)問題。第一,這個(gè)立方體看上去非常2D,因?yàn)槲覀冎豢吹搅艘粋€(gè)面。我們需要“移動(dòng)相機(jī)”,以不同角度觀察這個(gè)立方體。第二,上面有些問題,因?yàn)榱⒎襟w寬和高應(yīng)該相等,但從截圖看上去寬度明顯比高度大。為了修復(fù)這兩個(gè)問題,我們需要學(xué)習(xí)更多的矩陣知識(shí),和如何應(yīng)用到3D程序中。
裁剪體 - 默認(rèn)相機(jī)
為了理解3D中的“相機(jī)”,我們首先得理解裁剪體。
裁剪體是一個(gè)立方體。無論什么東西在裁剪體中的都會(huì)顯示在屏幕上,任何在裁剪體之外的都不會(huì)顯示。裁剪體跟我們上面的立方體是相同大小,它的X,Y,Z坐標(biāo)值域也是從-1到+1。-X表示左邊,+X表示右邊,-Y是底部,+Y是頂部,+Z是遠(yuǎn)離相機(jī),-Z是朝著相機(jī)。
因?yàn)槲覀兊牧⒎襟w和裁剪體一樣大,所以我們只能看到立方體的正面。
這也解釋了為何我們的立方體看起來比較寬。窗口顯示了裁剪體里的所有東西。窗口的左右邊緣是X軸的-1和+1,窗口的底部和頂部邊緣是Y軸的-1和+1。裁剪體被拉伸了,用來跟窗口的可視大小相適應(yīng),所以我們的立方體看上去不是正方形的。
固定住相機(jī),讓世界移動(dòng)起來
我們需要移動(dòng)相機(jī),使得可以從不同角度進(jìn)行觀察,或放大縮小。但不管怎樣,裁剪體不會(huì)更改。它永遠(yuǎn)是一樣的大小和位置。所以我們換種方式來替代移動(dòng)相機(jī),我們可以移動(dòng)3D場(chǎng)景讓它正確得出現(xiàn)在裁剪體中。比如,我們想要讓相機(jī)往右旋轉(zhuǎn),我們可以把整個(gè)世界往左旋轉(zhuǎn)。假如我們想要讓相機(jī)離玩家近些,我們可以把玩家挪到相機(jī)前。這就是“相機(jī)”在3D中的工作方式,變換整個(gè)世界使得它出現(xiàn)在裁剪體中并且看上去是正確的。
無論你走到哪里,都會(huì)覺得是世界沒動(dòng),是你在移動(dòng)。但你也能想象出當(dāng)你不動(dòng),而世界在你腳下滾動(dòng),就像你在跑步機(jī)上一樣。這就是“移動(dòng)相機(jī)”和“移動(dòng)世界”的區(qū)別,這兩種方式,對(duì)于觀察者而言,看上去都是一樣的。
我們?nèi)绾螌?duì)3D場(chǎng)景進(jìn)行變換來適應(yīng)裁剪體呢?這里我們需要用到矩陣。
實(shí)現(xiàn)相機(jī)矩陣
讓我們先來實(shí)現(xiàn)相機(jī)矩陣。3D中“相機(jī)”的解釋可認(rèn)為是對(duì)3D場(chǎng)景的一系列變換。因?yàn)橄鄼C(jī)就是一個(gè)變換,所以我們可以用矩陣來表示。
首先,我們需要包含GLM頭文件,用來創(chuàng)建不同類型的矩陣。
#include <glm/gtc/matrix_transform.hpp>
接著,我們需要更新頂點(diǎn)著色器。我們創(chuàng)建一個(gè)相機(jī)矩陣變量叫做camera
,并且每個(gè)頂點(diǎn)都會(huì)乘上這個(gè)相機(jī)矩陣。這樣我們就將整個(gè)3D場(chǎng)景進(jìn)行了變換。每個(gè)頂點(diǎn)都會(huì)被相機(jī)矩陣所變換。新的頂點(diǎn)著色器看上去應(yīng)該是這樣的:
#version 150 uniform mat4 camera; //this is the new variable in vec3 vert; in vec2 vertTexCoord; out vec2 fragTexCoord; void main() { // Pass the tex coord straight through to the fragment shader fragTexCoord = vertTexCoord; // Transform the input vertex with the camera matrix gl_Position = camera * vec4(vert, 1); }
現(xiàn)在我們需要在C++代碼中設(shè)置camera
著色器變量。在LoadShaders
函數(shù)的地步,我們添加這樣的代碼:
gProgram->use(); glm::mat4 camera = glm::lookAt(glm::vec3(3,3,3), glm::vec3(0,0,0), glm::vec3(0,1,0)); gProgram->setUniform("camera", camera); gProgram->stopUsing();
這個(gè)相機(jī)矩陣在本文中不會(huì)再被改變,當(dāng)所有著色器被創(chuàng)建后,我們只需這樣設(shè)置一次。
你無法在設(shè)置著色器變量,除非著色器在使用中,這就是為何我們用到了gProgram->use()
和gProgram->stopUsing()
。
我們使用glm::lookAt
函數(shù)為我們創(chuàng)建相機(jī)矩陣。假如你使用的是舊版本的OpenGL,那你應(yīng)該使用gluLookAt
函數(shù)來達(dá)到相同目的,但gluLookAt
已經(jīng)在最近的OpenGL版本中被移除了。第一個(gè)參數(shù)glm::vec3(3,3,3)
是相機(jī)的位置。第二個(gè)參數(shù)glm::vec3(0,0,0)
是相機(jī)觀察的點(diǎn)。立方體中心是(0,0,0),相機(jī)就朝著這個(gè)點(diǎn)觀察。最后一個(gè)參數(shù)glm::vec3(0,1,0)
是“向上”的方向。我們需要垂直擺放相機(jī),所以我們?cè)O(shè)置“向上”是沿著Y軸的正方向。假如相機(jī)是顛倒或者傾斜的,這里就是其它值了。
在我們生成了相機(jī)矩陣后,我們用gProgram->setUniform("camera", camera);
來設(shè)置camera
著色器變量,setUniform
方法屬于tdogl::Program
類,它會(huì)調(diào)用glUniformMatrix4fv
來設(shè)置變量。
就是這樣!我們現(xiàn)在有了一個(gè)可運(yùn)行的相機(jī)。
不幸的是,假如你現(xiàn)在運(yùn)行程序,你會(huì)看到整個(gè)都是黑屏。因?yàn)槲覀兊牧⒎襟w頂點(diǎn)經(jīng)過相機(jī)矩陣變換后,飛出了裁剪體。這就是上面我提到的,在裁剪體之外的它是不會(huì)被顯示。為了能再次看到它,我們需要設(shè)置投影矩陣。
實(shí)現(xiàn)投影矩陣
記住裁剪體只有2個(gè)單元寬、高和深。假設(shè)1個(gè)單元等于我們3D場(chǎng)景中的1米。這就意味著我們?cè)谙鄼C(jī)中能看到正前方2米,這樣不是很方便。
我們需要擴(kuò)大裁剪體使得能看到3D場(chǎng)景中的更多東西,可憐我們又不能改變裁剪體的大小,但,我們能縮小整個(gè)場(chǎng)景。縮小是一個(gè)變換,所以我們用矩陣來表示,基本上說,投影矩陣就是用來干這個(gè)的。
讓我們?cè)陧旤c(diǎn)著色器中加入投影矩陣變量。更新后的代碼看上去是這樣的:
#version 150 uniform mat4 projection; //this is the new variable uniform mat4 camera; in vec3 vert; in vec2 vertTexCoord; out vec2 fragTexCoord; void main() { // Pass the tex coord straight through to the fragment shader fragTexCoord = vertTexCoord; // Apply camera and projection transformations to the vertex gl_Position = projection * camera * vec4(vert, 1); }
注意矩陣相乘的順序:projection * camera * vert
。相機(jī)變換是放在首位的,投影矩陣是第二位。矩陣乘法中,變換從右往左,從頂點(diǎn)角度說是從最近的變換到更早前的變換。
現(xiàn)在讓我們?cè)贑++代碼中設(shè)置projection
著色器變量,方式和我們?cè)O(shè)置camera
變量相同。在LoadShaders
函數(shù)中,添加如下代碼:
glm::mat4 projection = glm::perspective(glm::radians(50.0f), SCREEN_SIZE.x/SCREEN_SIZE.y, 0.1f, 10.0f); gProgram->setUniform("projection", projection);
假如你使用的是舊版本OpenGL,你可以使用gluPerspective
來設(shè)置投影矩陣,同樣gluPerspective
函數(shù)在最近版本的OpenGL中也被移除了。幸運(yùn)的是你可以使用glm::perspective
來替代。
glm::perspective
第一個(gè)參數(shù)是“可視區(qū)域”參數(shù)。這個(gè)參數(shù)是個(gè)弧度,用來說明相機(jī)視野有多寬。弧度換算我們可以用glm::radians
函數(shù)來將50度轉(zhuǎn)換為弧度。大的可視區(qū)域意味著我們的相機(jī)可以看到更多場(chǎng)景,看上去就像是縮小了。小的可視區(qū)域意味著相機(jī)只能看到場(chǎng)景的一小部分,看上去像是放大了。第二個(gè)參數(shù)是“縱橫比”,該參數(shù)表示可視區(qū)域的縱橫比率。一般該參數(shù)設(shè)置為窗口的width/height
,倒數(shù)第二個(gè)參數(shù)是“近平面”,近平面是裁剪體的前面,0.1
表示近平面離相機(jī)是0.1單位遠(yuǎn)。任何離相機(jī)小于0.1
單位的物體均不可見。近平面的值必須大于0。最后一個(gè)參數(shù)是“遠(yuǎn)平面”,遠(yuǎn)平面是裁剪體的后面。10.0
表示相機(jī)所顯示的物體均離相機(jī)10個(gè)單位之內(nèi)。任何大于10單位的物體均不可見。我們的立方體是3單位遠(yuǎn),所以它能被看見。
glm::perspective
對(duì)將可視錐體對(duì)應(yīng)到裁剪體中非常有用。一個(gè)錐體像是一個(gè)金字塔被砍掉了頂端。金字塔的底部就是遠(yuǎn)平面,頂部就是近平面。可視區(qū)域就是該錐體胖瘦。任何在錐體里的物體都會(huì)被顯示,而不再內(nèi)的就隱藏。

有了相機(jī)矩陣和投影矩陣的組合,我們就可以看到立方體了。運(yùn)行程序你會(huì)看到:

這看上去。。。幾乎是對(duì)的。
這個(gè)立方體看上去已經(jīng)是正方形了,不再是矩形。這是因?yàn)?code style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; font-size: 14px; padding: 2px 4px; color: #c7254e; white-space: nowrap; border-radius: 4px; background-color: #f9f2f4;">glm::perspective中的“縱橫比”參數(shù),能夠基于窗口的寬和高進(jìn)行正確的調(diào)整比例。
不幸的是,截圖看上去立方體的背面渲染并覆蓋到前面來了。我們當(dāng)然不希望發(fā)生這樣的事,我們需要開啟深度緩沖來解決。
深度緩沖
OpenGL默認(rèn)會(huì)將最新的繪制覆蓋到之前的繪制上。假如一個(gè)物體的背面在前面之后繪制,就會(huì)發(fā)生背面擋住前面。深度緩沖就是為了防止背景層覆蓋到前景層的東西。
假如深度緩沖被開啟,每個(gè)被繪制的像素到相機(jī)的距離都是可知的。這個(gè)距離會(huì)以一個(gè)數(shù)值保存在深度緩沖里。當(dāng)你繪制一個(gè)像素在另外一個(gè)已存在的像素上時(shí),OpenGL會(huì)查找深度緩沖來決定哪個(gè)像素應(yīng)該離相機(jī)更近。假如新的像素離相機(jī)更近,那該像素點(diǎn)就會(huì)被重寫。假如之前的像素離相機(jī)更近,那新像素就會(huì)被拋棄。所以,一個(gè)之前已存在的像素只會(huì)當(dāng)新像素離相機(jī)更近時(shí)才會(huì)被重寫。這就叫做“深度測(cè)試”。
實(shí)現(xiàn)深度緩沖
在AppMain
函數(shù)中,調(diào)用了glewInit
之后,我們添加如下代碼:
glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS);
這告訴OpenGL開啟深度測(cè)試。調(diào)用glDepthFunc
是表明假如像素離相機(jī)的距離小于之前的像素距離時(shí)應(yīng)該被重寫。
最后一步我們需要在渲染每幀之后清理深度緩沖。假如我們不清理,舊的像素距離會(huì)保存在緩沖中,這樣會(huì)影響到繪制新的一幀。在Render
函數(shù)里,我們改變glClear
來實(shí)現(xiàn)它:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

旋轉(zhuǎn)立方體
假如你完成了上述例子,祝賀你走了這么遠(yuǎn)!最后我們來實(shí)現(xiàn)會(huì)旋轉(zhuǎn)的立方體動(dòng)畫。
如何實(shí)現(xiàn)旋轉(zhuǎn)?你會(huì)猜到:另外一個(gè)矩陣。這與之前的矩陣不同的是,這個(gè)矩陣是每幀都在改變,之前的矩陣都是常量。
我需要新建一個(gè)“模型”矩陣。在常見的3D引擎中,每個(gè)物體都有一個(gè)模型矩陣。相機(jī)和投影矩陣對(duì)整個(gè)場(chǎng)景來說是一樣的,但模型矩陣是每個(gè)物體都不同。模型矩陣用來擺放每個(gè)物體在正確的位置上(平移),設(shè)置正確的面向(旋轉(zhuǎn)),或者改變物體大小(縮放)。我們只有一個(gè)物體在當(dāng)前3D場(chǎng)景上,所以,我們只需要一個(gè)模型矩陣。
讓我們添加一個(gè)model
矩陣變量到頂點(diǎn)著色器,就像我們添加相機(jī)和投影一樣。最終版本的頂點(diǎn)著色器應(yīng)該是這樣的:
#version 150 uniform mat4 projection; uniform mat4 camera; uniform mat4 model; //this is the new variable in vec3 vert; in vec2 vertTexCoord; out vec2 fragTexCoord; void main() { // Pass the tex coord straight through to the fragment shader fragTexCoord = vertTexCoord; // Apply all matrix transformations to vert gl_Position = projection * camera * model * vec4(vert, 1); }
還是要注意矩陣相乘的順序。模型矩陣是vert
變量最近的一次變換,意味著模型矩陣應(yīng)該第一個(gè)被使用,其次是相機(jī),最后是投影。
現(xiàn)在我們需要設(shè)置新的model
著色器變量。不像相機(jī)和投影變量,模型變量需要每幀都被設(shè)置,所以我們把它放在Render
函數(shù)里。在gProgram->use()
之后添加這樣的代碼:
gProgram->setUniform("model", glm::rotate(glm::mat4(), glm::radians(45.0f), glm::vec3(0,1,0)));
我們使用glm::rotate
函數(shù)創(chuàng)建一個(gè)旋轉(zhuǎn)矩陣。第一個(gè)參數(shù)是一個(gè)已存在的需要進(jìn)行旋轉(zhuǎn)的矩陣。在這我們不需要對(duì)已存在的矩陣進(jìn)行旋轉(zhuǎn),所以我們傳個(gè)新的glm::mat4
對(duì)象就可以了。下一個(gè)參數(shù)是旋轉(zhuǎn)的角度,或者說是要旋轉(zhuǎn)多少度。現(xiàn)在讓我給它設(shè)置個(gè)45°。最后一個(gè)參數(shù)是旋轉(zhuǎn)的軸。想象下旋轉(zhuǎn)像是將物體插在叉子上,然后轉(zhuǎn)動(dòng)叉子。叉子就是軸,角度就是你的轉(zhuǎn)動(dòng)。在我們的例子中,我們使用垂直的叉子,所以立方體像在一個(gè)平臺(tái)上旋轉(zhuǎn)。
運(yùn)行程序,你們看到立方體被旋轉(zhuǎn):

它還沒有轉(zhuǎn)動(dòng),因?yàn)榫仃嚊]有被更改-它永遠(yuǎn)是旋轉(zhuǎn)了45°。最后一步就是讓它每幀都旋轉(zhuǎn)一下。
動(dòng)畫
首先,添加一個(gè)新的全局變量叫gDegreesRotated
。
GLfloat gDegreesRotated = 0.0f;
每幀,我們會(huì)輕微的增加gDegreesRotated
,并且我們用它來計(jì)算新的旋轉(zhuǎn)矩陣。這樣就能達(dá)到動(dòng)畫效果。我們需要做的就是更新,繪制,更新,繪制,更新,繪制,這樣一個(gè)模式。
讓我們創(chuàng)建一個(gè)Update
函數(shù),用來每次增加gDegreesRotated
:
void Update() { //rotate by 1 degree gDegreesRotated += 1.0f; //don't go over 360 degrees while(gDegreesRotated > 360.0f) gDegreesRotated -= 360.0f; }
我們需要每幀都調(diào)用一次Update
函數(shù)。讓我們把它加入到AppMain
的循環(huán)中,在調(diào)用Render
之前。
while(glfwGetWindowParam(GLFW_OPENED)){ // process pending events glfwPollEvents(); // update the rotation animation Update(); // draw one frame Render(); }
現(xiàn)在我們需要基于gDegreesRotated
變量來重新計(jì)算模型矩陣。在Render
函數(shù)中我們修改相關(guān)代碼來設(shè)置模型矩陣:
gProgram->setUniform("model", glm::rotate(glm::mat4(), glm::radians(gDegreesRotated), glm::vec3(0,1,0)));
與之前唯一不同的是我們使用了gDegreesRotated
來替換45°常量。
你現(xiàn)在運(yùn)行程序能看到一個(gè)漂亮,平滑轉(zhuǎn)動(dòng)的立方體動(dòng)畫。唯一的問題就是轉(zhuǎn)動(dòng)的速度很你的FPS幀率有關(guān)。假如FPS高,你的立方體旋轉(zhuǎn)的就快。假如FPS降低,那立方體旋轉(zhuǎn)的就慢些。這不夠理想。一個(gè)程序應(yīng)該能正確更新,而不在乎于運(yùn)行的幀率。
基于時(shí)間的動(dòng)畫
為了使程序跑起來更正確,不依賴于FPS,動(dòng)畫應(yīng)該每秒更新,而非每幀更新。最簡(jiǎn)單得方式就是對(duì)時(shí)間進(jìn)行計(jì)數(shù),并相對(duì)上次更新時(shí)間來正確更新。讓我們改下Update
函數(shù),增加個(gè)變量secondsElapsed
:
void Update(float secondsElapsed) { const GLfloat degreesPerSecond = 180.0f; gDegreesRotated += secondsElapsed * degreesPerSecond; while(gDegreesRotated > 360.0f) gDegreesRotated -= 360.0f; }
這段代碼使得立方體每秒旋轉(zhuǎn)180°,而無關(guān)多少幀率。
在AppMain
循環(huán)中,我們需要計(jì)算離上次更新過去了多少秒。新的循環(huán)應(yīng)該是這樣:
double lastTime = glfwGetTime(); while(glfwGetWindowParam(GLFW_OPENED)){ // process pending events glfwPollEvents(); // update the scene based on the time elapsed since last update double thisTime = glfwGetTime(); Update((float)(thisTime - lastTime)); lastTime = thisTime; // draw one frame Render(); }
glfwGetTime
返回從程序啟動(dòng)開始到現(xiàn)在所逝去的時(shí)間。
我們使用lastTime
變量來記錄上次更新時(shí)間。每次迭代,我們獲取最新的時(shí)間存入變量thisTime
。從上次更新到現(xiàn)在的差值就是thisTime - lastTime
。當(dāng)更新結(jié)束,我們?cè)O(shè)置lastTime = thisTime
以便下次循環(huán)迭代的時(shí)候很正常工作。
這是基于時(shí)間更新的最簡(jiǎn)單方法。這里還有更好的更新方法,但我們還不需要搞得這么復(fù)雜。
下篇預(yù)告
下一篇,我們會(huì)使用tdogl::Camera
類來實(shí)現(xiàn)用鍵盤操作第一人稱射擊類型的相機(jī)移動(dòng),可以用鼠標(biāo)觀察不同方向,或者用鼠標(biāo)滾輪來放大縮小。
更多資源