每一個小步驟的源碼都放在了Github
的內容為插入注釋,可以先跳過
前言
我們已經學習了如何利用矩陣變換來對所有頂點進行變換
OpenGL希望在每次頂點着色器運行后,我們可見的所有頂點都為標准化設備坐標(Normalized Device Coordinate, NDC),也就是說,每個頂點的x,y,z坐標都應該在-1.0到1.0之間,超出這個坐標范圍的頂點都將不可見
我們通常會自己設定一個坐標的范圍,之后再在頂點着色器中將這些坐標變換為標准化設備坐標,然后將這些標准化設備坐標傳入光柵器(Rasterizer),將它們變換為屏幕上的二維坐標或像素
將坐標變換為標准化設備坐標,接着再轉化為屏幕坐標的過程通常是類似於流水線分步進行的
在流水線中,物體的頂點還會被變換到多個過渡坐標系(Intermediate Coordinate System),在這些特定的坐標系統中,一些操作或運算更加方便和容易
對我們來說比較重要的總共有5個不同的坐標系統:
- 局部空間(Local Space,或者稱為物體空間(Object Space))
- 世界空間(World Space)
- 觀察空間(View Space,或者稱為視覺空間(Eye Space))
- 裁剪空間(Clip Space)
- 屏幕空間(Screen Space)
這就是一個頂點在最終被轉化為片段之前需要經歷的所有不同狀態
坐標系統概述
為了將坐標從一個坐標系變換到另一個坐標系,我們需要用到幾個變換矩陣,最重要的幾個分別是模型(Model)、觀察(View)、投影(Projection)三個矩陣
我們的頂點坐標起始於局部空間(Local Space),在這里它稱為局部坐標(Local Coordinate),它在之后會變為世界坐標(World Coordinate),觀察坐標(View Coordinate),裁剪坐標(Clip Coordinate),並最后以屏幕坐標(Screen Coordinate)的形式結束
- 局部坐標是對象相對於局部原點的坐標,也是物體起始的坐標
- 世界空間坐標是處於一個更大的空間范圍的,這些坐標相對於世界的全局原點,它們會和其它物體一起相對於世界的原點進行擺放
- 接下來我們將世界坐標變換為觀察空間坐標,使得每個坐標都是從攝像機或者說觀察者的角度進行觀察的
- 坐標到達觀察空間之后,我們需要將其投影到裁剪坐標,裁剪坐標會被處理至-1.0到1.0的范圍內,並判斷哪些頂點將會出現在屏幕上
- 最后,我們將裁剪坐標變換為屏幕坐標,我們將使用一個叫做視口變換(Viewport Transform)的過程。視口變換將位於-1.0到1.0范圍的坐標變換到由glViewport函數所定義的坐標范圍內。最后變換出來的坐標將會送到光柵器,將其轉化為片段。
我們之所以將頂點變換到各個不同的空間的,是因為有些操作在特定的坐標系統中才有意義且更方便
例如,當需要對物體進行修改的時候,在局部空間中來操作會更說得通;如果要對一個物體做出一個相對於其它物體位置的操作時,在世界坐標系中來做這個才更說得通,當然如果你願意,我們也可以定義一個直接從局部空間變換到裁剪空間的變換矩陣,但那樣會失去很多靈活性
接下來我們將更仔細地討論各個坐標系統
局部坐標
局部坐標是指相對於物體自身位置的坐標空間
比如一個3D人物模型,相對該模型的中心點(0,0,0)來說,頭頂的坐標為(0,0,20)
當整個模型向前移動了10個單位,其中心點依舊是(0,0,0),頭頂依舊是(0,0,20)
你的模型的所有頂點都是在局部坐標中——它們相對於你的物體來說都是局部的
世界坐標
如果我們將我們所有的物體導入到游戲程序當中,它們有可能會全擠在世界的原點(0, 0, 0)上,世界空間中的坐標正如其名:是指頂點相對於整個(游戲)世界的坐標
上面的人物模型在游戲世界沿Z軸正方向移動了10個單位后,中心點在世界坐標系中變成了(0,0,10),頭頂在世界坐標系里變成了(0,0,25)(但是在局部坐標系中,中心點和頭頂的局部坐標沒有改變)
從局部坐標系到世界坐標系的變換由模型矩陣(Model Matrix)實現
模型矩陣也是一種變換矩陣,變換矩陣能通過對物體進行位移、縮放、旋轉來將它置於它本應該在的位置或朝向,你可以將它想像為變換一個房子,你需要先將它縮小(它在局部空間中太大了),並將其位移至郊區的一個小鎮,然后在y軸上往左旋轉一點以搭配附近的房子
觀察坐標
觀察坐標是以攝像機為坐標原點,攝像機的朝向為Z軸方向的坐標系
我們在窗口里看到的東西都是攝像機幫我們看的(因此也稱為攝像機空間(Camera Space)或視覺空間(Eye Space)),所以觀察空間就是從攝像機的視角所觀察到的空間
在局部,世界,觀察空間下,X,Y,Z的范圍都是無窮大,只是坐標系的基准不一樣而已
觀察坐標也是由一系列的位移和旋轉的組合來完成——平移/旋轉場景從而使得特定的對象被變換到攝像機的前方,這些組合在一起的變換存儲在另一個變換矩陣——觀察矩陣(View Matrix)里,它用來將世界坐標變換到觀察空間
裁剪空間
在一個頂點着色器運行的最后,OpenGL期望所有的坐標都能落在一個特定的范圍內,且任何在這個范圍之外的點都應該被裁剪掉(Clipped),被裁剪掉的坐標就會被忽略,所以剩下的坐標就將變為屏幕上可見的片段
因為將所有可見的坐標都指定在-1.0到1.0的范圍內不是很直觀,所以我們會指定自己的坐標集(Coordinate Set)並將它變換回標准化設備坐標系,就像OpenGL期望的那樣
為了將頂點坐標從觀察變換到裁剪空間,我們需要定義一個投影矩陣(Projection Matrix),它指定了一個范圍的坐標,比如在每個維度上的-1000到1000,投影矩陣接着會將在這個指定的范圍內的坐標變換為標准化設備坐標的范圍(-1.0, 1.0),所有在范圍外的坐標不會被映射到在-1.0到1.0的范圍之間,所以會被裁剪掉
在上面這個投影矩陣所指定的范圍內,坐標(1250, 500, 750)將是不可見的,這是由於它的x坐標超出了范圍,它被轉化為一個大於1.0的標准化設備坐標,所以被裁剪掉了(如果只是圖元(Primitive),例如三角形,的一部分超出了裁剪體積(Clipping Volume),則OpenGL會重新構建這個三角形為一個或多個三角形讓其能夠適合這個裁剪范圍)
由投影矩陣創建的觀察箱(Viewing Box)被稱為平截頭體(Frustum),每個出現在平截頭體范圍內的坐標都會最終出現在用戶的屏幕上,將特定范圍內的坐標轉化到標准化設備坐標系的過程被稱之為投影(Projection),因為使用投影矩陣能將3D坐標投影(Project)到很容易映射到2D的標准化設備坐標系中
一旦所有頂點被變換到裁剪空間,最終的操作——透視除法(Perspective Division)將會執行,在這個過程中我們將位置向量的x,y,z分量分別除以向量的齊次w分量;透視除法是將4D裁剪空間坐標變換為3D標准化設備坐標的過程,這一步會在每一個頂點着色器運行的最后被自動執行
在這一階段之后,最終的坐標將會被映射到屏幕空間中(使用glViewport中的設定),並被變換成片段
將觀察坐標變換為裁剪坐標的投影矩陣可以為兩種不同的形式,每種形式都定義了不同的平截頭體。我們可以選擇創建一個正射投影矩陣(Orthographic Projection Matrix)或一個透視投影矩陣(Perspective Projection Matrix)
正射投影
正射投影矩陣定義了一個類似立方體的平截頭箱,它定義了一個裁剪空間,在這空間之外的頂點都會被裁剪掉
創建一個正射投影矩陣需要指定可見平截頭體的寬、高和長度,在使用正射投影矩陣變換至裁剪空間之后處於這個平截頭體內的所有坐標將不會被裁剪掉,它的平截頭體看起來像一個容器:
上面的平截頭體定義了可見的坐標,它由由寬、高、近(Near)平面和遠(Far)平面所指定(任何出現在近平面之前或遠平面之后的坐標都會被裁剪掉),正射平截頭體直接將平截頭體內部的所有坐標映射為標准化設備坐標,因為每個向量的w分量都沒有進行改變;如果w分量等於1.0,透視除法則不會改變這個坐標
要創建一個正射投影矩陣,我們可以使用GLM的內置函數glm::ortho
:
// 左右坐標 底部和頂部 近平面和遠平面的距離
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
前兩個參數指定了平截頭體的左右坐標,第三和第四參數指定了平截頭體的底部和頂部,通過這四個參數我們定義了近平面和遠平面的大小,然后第五和第六個參數則定義了近平面和遠平面的距離,這個投影矩陣會將處於這些x,y,z值范圍內的坐標變換為標准化設備坐標
正視投影效果如下(Blender):
正射投影矩陣直接將坐標映射到2D平面中,即你的屏幕,但實際上一個直接的投影矩陣會產生不真實的結果,因為這個投影沒有將透視(Perspective)考慮進去,所以我們需要透視投影矩陣來解決這個問題
透視投影
實際生活的經驗告訴我們,離你越遠的東西看起來越小,這個效果就是透視(Perspective):
由於透視,這兩條線在很遠的地方看起來會相交,這正是透視投影想要模仿的效果,它是使用透視投影矩陣來完成的
這個投影矩陣將給定的平截頭體范圍映射到裁剪空間,除此之外還修改了每個頂點坐標的w值,從而使得離觀察者越遠的頂點坐標w分量越大,被變換到裁剪空間的坐標都會在-w到w的范圍之間(任何大於這個范圍的坐標都會被裁剪掉),OpenGL要求所有可見的坐標都落在-1.0到1.0范圍內,作為頂點着色器最后的輸出,因此,一旦坐標在裁剪空間內之后,透視除法就會被應用到裁剪空間坐標上:
頂點坐標的每個分量都會除以它的w分量,距離觀察者越遠頂點坐標就會越小,這是也是w分量非常重要的另一個原因,它能夠幫助我們進行透視投影,最后的結果坐標就是處於標准化設備空間中的
在GLM中可以這樣創建一個透視投影矩陣:
glm::mat4 proj = glm::perspective(
glm::radians(45.0f), //FOV
(float)width/(float)height, //寬高比
0.1f, //平截頭體的近面
100.0f //平截頭體的遠面
);
同樣,glm::perspective
所做的其實就是創建了一個定義了可視空間的大平截頭體
- 第一個參數定義了fov的值,它表示的是視野(Field of View),並且設置了觀察空間的大小(如果想要一個真實的觀察效果,它的值通常設置為45.0f,但想要一個末日風格的結果你可以將其設置一個更大的值)
- 第二個參數設置了寬高比,由視口的寬除以高所得
- 第三和第四個參數設置了平截頭體的近和遠平面,我們通常設置近距離為0.1f,而遠距離設為100.0f
一個透視平截頭體可以被看作一個不均勻形狀的箱子,在這個箱子內部的每個坐標都會被映射到裁剪空間上的一個點:
透視投影效果如下(Blender):
當你把透視矩陣的 near 值設置太大時(如10.0f),OpenGL會將靠近攝像機的坐標(在0.0f和10.0f之間)都裁剪掉,這會產生一個你在游戲中可能遇見過的視覺效果:太過靠近一個物體時,你的視線會直接穿透過去
當使用正射投影時,每一個頂點坐標都會直接映射到裁剪空間中而不經過任何精細的透視除法(其實它仍然會進行透視除法,只是w分量沒有被改變(它保持為1),因此沒有起作用),正射投影主要用於二維渲染以及一些建築或工程的程序,在這些場景中我們更希望頂點不會被透視所干擾,例如我使用 Unity 也經常會使用到正射投影,因為它在各個維度下都更准確地描繪了每個物體
你能夠看到在Blender里面使用兩種投影方式的對比:
很顯然,使用透視投影的話,遠處的頂點看起來比較小,而在正射投影中每個頂點距離觀察者的距離都是一樣的
空間組合
我們為上述的每一個步驟都創建了一個變換矩陣:模型矩陣、觀察矩陣和投影矩陣,一個頂點坐標將會根據以下過程被變換到裁剪坐標:
但是你要注意:矩陣運算的順序是相反的!,上面的式子應該從右往左讀,最后的頂點應該被賦值到頂點着色器中的gl_Position,OpenGL將會自動進行透視除法和裁剪
頂點着色器的輸出要求所有的頂點都在裁剪空間內,這正是我們剛才使用變換矩陣所做的,OpenGL然后對裁剪坐標執行透視除法從而將它們變換到標准化設備坐標,OpenGL會使用glViewPort內部的參數來將標准化設備坐標映射到屏幕坐標,每個坐標都關聯了一個屏幕上的點(在我們的例子中是一個800x600的屏幕),這個過程就是視口變換
應用到3D
既然我們知道了如何將3D坐標變換為2D坐標,我們可以開始使用真正的3D物體,而不是枯燥的2D平面了
模型矩陣
在開始進行3D繪圖時,我們首先創建一個模型矩陣,這個模型矩陣包含了位移、縮放與旋轉操作,它們會被應用到所有物體的頂點上,以變換它們到全局的世界空間
讓我們變換一下我們的平面,將其繞着x軸旋轉,使它看起來像放在地上一樣,這個模型矩陣看起來是這樣的:
glm::mat4 model;
model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));
通過將頂點坐標乘以這個模型矩陣,我們將該頂點坐標變換到世界坐標,我們的平面看起來就是在地板上,代表全局世界里的平面
觀察矩陣
接下來我們需要創建一個觀察矩陣:我們想要在場景里面稍微往后移動,以使得物體變成可見的(當在世界空間時,我們也位於原點(0,0,0)),要想在場景里面移動,你先要清楚:
- 將攝像機向后移動,和將整個場景向前移動是一樣的
這正是觀察矩陣所做的,我們以相反於攝像機移動的方向移動整個場景。因為我們想要往后移動,並且OpenGL是一個右手坐標系(Right-handed System),所以我們需要沿着z軸的正方向移動,我們會通過將場景沿着z軸負方向平移來實現,它會給我們一種我們在往后移動的感覺
OpenGL是一個右手坐標系,簡單來說,就是正x軸在你的右手邊,正y軸朝上,而正z軸是朝向后方的。想象你的屏幕處於三個軸的中心,則正z軸穿過你的屏幕朝向你。坐標系畫起來如下:
注意在標准化設備坐標系中OpenGL實際上使用的是左手坐標系(投影矩陣交換了左右手)
glm::mat4 view;
// 注意,我們將矩陣向我們要進行移動場景的反方向移動。
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
投影矩陣
我們希望在場景中使用透視投影,所以像這樣聲明一個投影矩陣:
glm::mat4 projection;
projection = glm::perspective(glm::radians(45.0f), screenWidth / screenHeight, 0.1f, 100.0f);
我們已經創建了變換矩陣,我們應該將它們傳入着色器
首先,讓我們在頂點着色器中聲明一個uniform變換矩陣然后將它乘以頂點坐標:
// vertex shader
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
// 注意乘法要從右向左讀
gl_Position = projection * view * model * vec4(aPos, 1.0);
TexCoord = vec2(aTexCoord.x, aTexCoord.y);
}
我們還應該將矩陣傳入着色器(這通常在每次的渲染迭代中進行,因為變換矩陣會經常變動):
int modelLoc = glGetUniformLocation(ourShader.ID, "model"));
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
// 觀察矩陣和投影矩陣與之類似
我們也可以在我們的shader類里加入setMat4的方法一步到位:
void setMat4(const std::string &name, const glm::mat4 &mat) const
{
glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]);
}
寫在一起就是
// 創建矩陣
glm::mat4 model = glm::mat4(1.0f);
glm::mat4 view = glm::mat4(1.0f);
glm::mat4 projection = glm::mat4(1.0f);
model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
// 將矩陣傳入着色器
ourShader.setMat4("model", model);
ourShader.setMat4("view", view);
ourShader.setMat4("projection", projection);
我們的頂點坐標已經使用模型、觀察和投影矩陣進行變換了,最終的物體應該會:
- 稍微向后傾斜至地板方向
- 離我們有一些距離
- 有透視效果(頂點越遠,變得越小)
讓我們檢查一下結果是否滿足這些要求:
3D立方體
到目前為止,我們一直都在使用一個2D平面,現在讓我們大膽地拓展我們的2D平面為一個3D立方體
要想渲染一個立方體,我們一共需要36個頂點(6個面 x 每個面有2個三角形組成 x 每個三角形有3個頂點),這36個頂點的位置不難弄出來:
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
};
為了有趣一點,我們將讓立方體隨着時間旋轉:
model = glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f));
然后我們使用glDrawArrays來繪制立方體,但這一次總共有36個頂點。
glDrawArrays(GL_TRIANGLES, 0, 36);
如果一切順利的話你應該能得到下面這樣的效果:
這的確有點像是一個立方體,但又有種說不出的奇怪,立方體的某些本應被遮擋住的面被繪制在了這個立方體其他面之上——之所以這樣是因為OpenGL是一個三角形一個三角形地來繪制你的立方體的,所以即便之前那里有東西它也會覆蓋之前的像素,所以有些三角形會被繪制在其它三角形上面,雖然它們本不應該是被覆蓋的
幸運的是,OpenGL存儲深度信息在一個叫做Z緩沖(Z-buffer)的緩沖中,它允許OpenGL決定何時覆蓋一個像素而何時不覆蓋,通過使用Z緩沖,我們可以配置OpenGL來進行深度測試
Z緩沖
OpenGL存儲它的所有深度信息於一個Z緩沖(Z-buffer)中,也被稱為深度緩沖(Depth Buffer),GLFW會自動為你生成這樣一個緩沖(就像它也有一個顏色緩沖來存儲輸出圖像的顏色),深度值存儲在每個片段里面(作為片段的z值),當片段想要輸出它的顏色時,OpenGL會將它的深度值和z緩沖進行比較,如果當前的片段在其它片段之后,它將會被丟棄,否則將會覆蓋——這個過程稱為深度測試(Depth Testing),它是由OpenGL自動完成的
然而,如果我們想要確定OpenGL真的執行了深度測試,首先我們要告訴OpenGL我們想要啟用深度測試;它默認是關閉的,我們可以通過glEnable函數來開啟深度測試
glEnable(GL_DEPTH_TEST);
glEnable和glDisable函數允許我們啟用或禁用某個OpenGL功能,這個功能會一直保持啟用/禁用狀態,直到另一個調用來禁用/啟用它
因為我們使用了深度測試,我們也想要在每次渲染迭代之前清除深度緩沖(否則前一幀的深度信息仍然保存在緩沖中),就像清除顏色緩沖一樣,我們可以通過在glClear函數中指定DEPTH_BUFFER_BIT位來清除深度緩沖:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
重新運行:
awesome
參考源代碼:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include "shader.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
// callback
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
int main()
{
#pragma region 窗口
// 實例化GLFW窗口
glfwInit();//glfw初始化
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本號
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本號
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
//(寬,高,窗口名)返回一個GLFWwindow類的實例:window
if (window == NULL)
{
// 生成錯誤則輸出錯誤信息
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
// 告訴GLFW我們希望每當窗口調整大小的時候調用改變窗口大小的函數
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
#pragma endregion GLFW
#pragma region 函數指針
// glad管理opengl函數指針,初始化glad
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
// 生成錯誤則輸出錯誤信息
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
#pragma endregion GLAD
Shader ourShader("1.6.0vertex.txt", "1.6.0fragment.txt");
glEnable(GL_DEPTH_TEST);//啟用深度測試
#pragma region 頂點數據
//頂點數據
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
};
#pragma endregion vertices[],indices[]
#pragma region 緩存對象
// 初始化緩存對象
unsigned int VBO;
glGenBuffers(1, &VBO);
unsigned int VAO;
glGenVertexArrays(1, &VAO);
//unsigned int EBO;
//glGenBuffers(1, &EBO);
// 1. 綁定頂點數組對象
glBindVertexArray(VAO);
// 2. 把我們的頂點數組復制到一個頂點緩沖中,供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//// 3. 復制我們的索引數組到一個索引緩沖中,供OpenGL使用
//glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
//glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 設定頂點屬性指針
// 位置屬性
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);
#pragma endregion VAO,VBO,EBO
#pragma region 材質
// 加載材質
unsigned int texture1;//紋理也是使用ID引用的
glGenTextures(1, &texture1);//glGenTextures先輸入要生成紋理的數量,然后把它們儲存在第二個參數的`unsigned int`數組中
glBindTexture(GL_TEXTURE_2D, texture1);
// 為當前綁定的紋理對象設置環繞、過濾方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加載並生成紋理
int width, height, nrChannels;
unsigned char* data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
#pragma endregion 加載材質
#pragma region 渲染
ourShader.use();
ourShader.setInt("texture1", 0);
// 渲染循環
while (!glfwWindowShouldClose(window))
{
// 輸入
processInput(window);
// 渲染指令
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 綁定材質
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
// 創建矩陣
glm::mat4 model = glm::mat4(1.0f);
glm::mat4 view = glm::mat4(1.0f);
glm::mat4 projection = glm::mat4(1.0f);
model = glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f));
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
// 將矩陣傳入着色器
ourShader.setMat4("model", model);
ourShader.setMat4("view", view);
ourShader.setMat4("projection", projection);
// 渲染箱子
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
// 檢查並調用事件,交換緩沖
glfwSwapBuffers(window);
// 檢查觸發什么事件,更新窗口狀態
glfwPollEvents();
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
//glDeleteBuffers(1, &EBO);
// 釋放之前的分配的所有資源
glfwTerminate();
#pragma endregion Rendering
return 0;
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// 每當窗口改變大小,GLFW會調用這個函數並填充相應的參數供你處理
glViewport(0, 0, width, height);
}
void processInput(GLFWwindow* window)
{
// 返回這個按鍵是否正在被按下
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//是否按下了返回鍵
glfwSetWindowShouldClose(window, true);
}
更多的立方體
現在我們想在屏幕上顯示10個立方體。每個立方體看起來都是一樣的,區別在於它們在世界的位置及旋轉角度不同。立方體的圖形布局已經定義好了,所以當渲染更多物體的時候我們不需要改變我們的緩沖數組和屬性數組,我們唯一需要做的只是改變每個對象的模型矩陣來將立方體變換到世界坐標系中。
首先,我們手動為每個立方體定義一個位移向量來指定它在世界空間的位置,我們將在一個glm::vec3
數組中定義10個立方體位置(寫在頂點數組之后):
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)
};
現在,在游戲循環中,我們調用glDrawArrays 10次,但這次在我們渲染之前每次傳入一個不同的模型矩陣到頂點着色器中。我們將會在游戲循環中創建一個小的循環用不同的模型矩陣渲染我們的物體10次,注意我們也對每個箱子加了一點旋轉:
glBindVertexArray(VAO);
for(unsigned int i = 0; i < 10; i++)
{
glm::mat4 model;
model = glm::translate(model, cubePositions[i]);
float angle = 20.0f * i;
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
ourShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
這段代碼將會在每次新立方體繪制出來的時候更新模型矩陣,如此總共重復10次:
我們甚至可以使 編號是3倍數的箱子以及第1個箱子 旋轉,而讓剩下的箱子保持靜止
for (unsigned 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) // every 3rd iteration (including the first) we set the angle using GLFW's time function.
angle = glfwGetTime() * 25.0f;
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
ourShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
舒服了,可以睡覺了
參考源代碼:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include "shader.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
// callback
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
int main()
{
#pragma region 窗口
// 實例化GLFW窗口
glfwInit();//glfw初始化
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本號
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本號
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
//(寬,高,窗口名)返回一個GLFWwindow類的實例:window
if (window == NULL)
{
// 生成錯誤則輸出錯誤信息
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
// 告訴GLFW我們希望每當窗口調整大小的時候調用改變窗口大小的函數
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
#pragma endregion GLFW
#pragma region 函數指針
// glad管理opengl函數指針,初始化glad
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
// 生成錯誤則輸出錯誤信息
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
#pragma endregion GLAD
Shader ourShader("1.6.0vertex.txt", "1.6.0fragment.txt");
glEnable(GL_DEPTH_TEST);//啟用深度測試
#pragma region 頂點數據
//頂點數據
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
};
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)
};
#pragma endregion vertices[],indices[]
#pragma region 緩存對象
// 初始化緩存對象
unsigned int VBO;
glGenBuffers(1, &VBO);
unsigned int VAO;
glGenVertexArrays(1, &VAO);
//unsigned int EBO;
//glGenBuffers(1, &EBO);
// 1. 綁定頂點數組對象
glBindVertexArray(VAO);
// 2. 把我們的頂點數組復制到一個頂點緩沖中,供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//// 3. 復制我們的索引數組到一個索引緩沖中,供OpenGL使用
//glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
//glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 設定頂點屬性指針
// 位置屬性
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);
#pragma endregion VAO,VBO,EBO
#pragma region 材質
// 加載材質
unsigned int texture1;//紋理也是使用ID引用的
glGenTextures(1, &texture1);//glGenTextures先輸入要生成紋理的數量,然后把它們儲存在第二個參數的`unsigned int`數組中
glBindTexture(GL_TEXTURE_2D, texture1);
// 為當前綁定的紋理對象設置環繞、過濾方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加載並生成紋理
int width, height, nrChannels;
unsigned char* data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
#pragma endregion 加載材質
#pragma region 渲染
ourShader.use();
ourShader.setInt("texture1", 0);
// 渲染循環
while (!glfwWindowShouldClose(window))
{
// 輸入
processInput(window);
// 渲染指令
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 綁定材質
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
// 創建矩陣
//glm::mat4 model = glm::mat4(1.0f);
//model = glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f));
glm::mat4 view = glm::mat4(1.0f);
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
glm::mat4 projection = glm::mat4(1.0f);
projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
// 將矩陣傳入着色器
//ourShader.setMat4("model", model);
ourShader.setMat4("view", view);
ourShader.setMat4("projection", projection);
// 渲染箱子
glBindVertexArray(VAO);
//glDrawArrays(GL_TRIANGLES, 0, 36);
for (unsigned 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) // every 3rd iteration (including the first) we set the angle using GLFW's time function.
angle = glfwGetTime() * 25.0f;
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
ourShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
// 檢查並調用事件,交換緩沖
glfwSwapBuffers(window);
// 檢查觸發什么事件,更新窗口狀態
glfwPollEvents();
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
//glDeleteBuffers(1, &EBO);
// 釋放之前的分配的所有資源
glfwTerminate();
#pragma endregion Rendering
return 0;
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// 每當窗口改變大小,GLFW會調用這個函數並填充相應的參數供你處理
glViewport(0, 0, width, height);
}
void processInput(GLFWwindow* window)
{
// 返回這個按鍵是否正在被按下
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//是否按下了返回鍵
glfwSetWindowShouldClose(window, true);
}