- 基本概念
頂點(diǎn)數(shù)組對(duì)象:Vertex Array Object计螺,VAO
頂點(diǎn)緩沖對(duì)象:Vertex Buffer Object蕾各,VBO
索引緩沖對(duì)象:Element Buffer Object蕊温,EBO或Index Buffer Object袱箱,IBO - 圖形渲染管線(管線):3D空間坐標(biāo)-->2D屏幕坐標(biāo)處理,一堆原始圖形數(shù)據(jù)途徑一個(gè)輸送管道义矛,期間經(jīng)過各種變化處理最終出現(xiàn)在屏幕的過程。圖形渲染管線可以被劃分為兩個(gè)主要部分:1)把你的3D坐標(biāo)轉(zhuǎn)換為2D坐標(biāo)筐咧;2)把2D坐標(biāo)轉(zhuǎn)變?yōu)閷?shí)際有顏色的像素量蕊。每個(gè)階段會(huì)把前一個(gè)階段的輸出作為輸入残炮,每個(gè)階段運(yùn)行各自的小程序泉瞻,從而在圖形渲染管線中快速處理數(shù)據(jù)袖牙,這些小程序叫做著色器鞭达,GPU上運(yùn)行畴蹭。
- 圖形渲染管線階段介紹叨襟,以制作三角形為例:
1)以數(shù)組形式傳遞3個(gè)3D坐標(biāo)糊闽,用來表示一個(gè)三角形墓怀,這個(gè)數(shù)組叫做頂點(diǎn)數(shù)據(jù)傀履;圖元:指出的渲染類型
2)頂點(diǎn)著色器階段:把一個(gè)單獨(dú)的頂點(diǎn)作為輸入钓账,每個(gè)輸入變量也叫頂點(diǎn)屬性,目的:把3D坐標(biāo)轉(zhuǎn)為另一種3D坐標(biāo)絮宁;頂點(diǎn)著色器允許我們對(duì)頂點(diǎn)屬性進(jìn)行一些基本處理
3)圖元裝配階段:將頂點(diǎn)著色器輸出的所有頂點(diǎn)作為輸入梆暮,所有的點(diǎn)裝配成指定圖元的形狀;例:三角形
4)幾何著色器:圖元裝配階段的輸出會(huì)傳遞給幾何著色器绍昂,把圖元形式的一系列頂點(diǎn)幾何作為輸入啦粹,它可以通過產(chǎn)生新頂點(diǎn)構(gòu)造出新的圖元來生成其他形狀偿荷。例:生成了另一個(gè)三角形
5)光柵化階段:幾何著色器的輸出會(huì)被傳入光柵化階段,把圖元映射為最終屏幕上相應(yīng)的像素唠椭,生成供片段著色器使用的片段(OpenGL中的一個(gè)片段是OpenGL渲染一個(gè)像素所需的所有數(shù)據(jù))。在片段著色器運(yùn)行之前會(huì)執(zhí)行裁剪:丟棄超出視圖以外的所有像素贪嫂,用來提升執(zhí)行效率。
6)片段著色器:計(jì)算一個(gè)像素的最終顏色斗塘,所有OpenGL高級(jí)效果產(chǎn)生的部分。通常馍盟,片段著色器包含3D場景的數(shù)據(jù)(光照、陰影台猴、光的顏色等等)朽合,這些數(shù)據(jù)可以被用來計(jì)算最終像素的顏色宪彩。
7)Alpha測試和混合階段:檢測片段對(duì)應(yīng)的深度值,用來判斷這個(gè)像素是其他物體前面還是后面,決定是否應(yīng)該丟棄。這個(gè)階段也會(huì)檢查alpha值(alpha值定義了一個(gè)物體的透明度)并對(duì)物體進(jìn)行混合错维。
在現(xiàn)代OpenGL中仰楚,我們必須定義至少一個(gè)頂點(diǎn)著色器和一個(gè)片段著色器(因?yàn)镚PU中沒有默認(rèn)的頂點(diǎn)/片段著色器)
圖形渲染管線階段.png
頂點(diǎn)輸入
OpenGL僅當(dāng)3D坐標(biāo)在3個(gè)軸(x飒炎、y、z)上都為-1.0到1.0的范圍內(nèi)時(shí)才處理它。深度通常為Z坐標(biāo)的輸入,它代表一個(gè)像素空間中和你的距離。以三角形為例:三個(gè)float型頂點(diǎn)的3D位置數(shù)組如下:
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
標(biāo)準(zhǔn)化設(shè)備坐標(biāo)<(-1.0懦铺,1.0)區(qū)間>接著會(huì)變換為屏幕空間坐標(biāo)(Screen-space Coordinates),這是使用你通過glViewport函數(shù)提供的數(shù)據(jù)支鸡,進(jìn)行視口變換(Viewport Transform)完成的冬念。所得的屏幕空間坐標(biāo)又會(huì)被變換為片段輸入到片段著色器中。
定義這樣的頂點(diǎn)數(shù)據(jù)以后牧挣,我們會(huì)把它作為輸入發(fā)送給渲染管線的第一個(gè)處理階段:頂點(diǎn)著色器急前。
它會(huì)在GPU上創(chuàng)建內(nèi)存用于儲(chǔ)存我們的頂點(diǎn)數(shù)據(jù),還要配置OpenGL如何解釋這些內(nèi)存瀑构,并且制定其如何發(fā)送給顯卡裆针。頂點(diǎn)著色器接著會(huì)處理我們在內(nèi)存中指定數(shù)量的頂點(diǎn)。
我們通過頂點(diǎn)緩沖對(duì)象(VBO)管理這個(gè)內(nèi)存检碗,它會(huì)在GPU內(nèi)存(顯存)中儲(chǔ)存大量頂點(diǎn)据块。使用這些緩沖對(duì)象的好處是我們可以一次性的發(fā)送一大批數(shù)據(jù)到顯卡上,而不是每個(gè)頂點(diǎn)發(fā)送一次折剃,從CPU把數(shù)據(jù)發(fā)送到顯卡相對(duì)較慢另假,所以盡量一次性發(fā)送盡可能多的數(shù)據(jù)。
頂點(diǎn)緩沖對(duì)象有一個(gè)獨(dú)一無二的ID怕犁,可以使用glGenBuffers函數(shù)和一個(gè)緩沖ID生成一個(gè)VBO對(duì)象边篮。
unsigned int VBO;
glGenBuffers(1, &VBO);
OpenGL有很多緩沖對(duì)象類型己莺,頂點(diǎn)緩沖對(duì)象的緩沖類型是GL_ARRAY_BUFFER。OpenGL允許我們同事綁定多個(gè)緩沖戈轿,只要他們是不同的緩沖類型凌受。我們可以使用glbindbuffer函數(shù)把新創(chuàng)建的緩沖綁定到GL_ARRAY_BUFFER上:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
可以調(diào)用glBufferData函數(shù),它會(huì)把之前定義的頂點(diǎn)數(shù)據(jù)復(fù)制到緩沖的內(nèi)存中:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData是一個(gè)專門用來把用戶定義的數(shù)據(jù)復(fù)制到當(dāng)前綁定緩沖的函數(shù)思杯。它的第一個(gè)參數(shù)是目標(biāo)緩沖的類型:頂點(diǎn)緩沖對(duì)象當(dāng)前綁定到GL_ARRAY_BUFFER目標(biāo)上胜蛉。第二個(gè)參數(shù)指定傳輸數(shù)據(jù)的大小(以字節(jié)為單位);用一個(gè)簡單的sizeof計(jì)算出頂點(diǎn)數(shù)據(jù)大小就行色乾。第三個(gè)參數(shù)是我們希望發(fā)送的實(shí)際數(shù)據(jù)誊册。
第四個(gè)參數(shù)指定了我們希望顯卡如何管理給定的數(shù)據(jù)。它有三種形式:
GL_STATIC_DRAW :數(shù)據(jù)不會(huì)或幾乎不會(huì)改變暖璧。
GL_DYNAMIC_DRAW:數(shù)據(jù)會(huì)被改變很多案怯。
GL_STREAM_DRAW :數(shù)據(jù)每次繪制時(shí)都會(huì)改變。
頂點(diǎn)著色器
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
gl_Position設(shè)置的值會(huì)成為該頂點(diǎn)著色器的輸出,必須是將三分量轉(zhuǎn)換為四分量澎办,w分量設(shè)置為1.0嘲碱。
編譯著色器
現(xiàn)在,我們暫時(shí)將頂點(diǎn)著色器的源代碼硬編碼在代碼文件頂部的C風(fēng)格字符串中:
const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
為了能夠讓OpenGL使用它局蚀,我們必須在運(yùn)行時(shí)動(dòng)態(tài)編譯它的源代碼麦锯。
我們首先要做的是創(chuàng)建一個(gè)著色器對(duì)象,注意還是用ID來引用的至会。所以我們儲(chǔ)存這個(gè)頂點(diǎn)著色器為unsigned int离咐,然后用glCreateShader創(chuàng)建這個(gè)著色器:
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
/* glShaderSource把要編譯的著色器作為第一個(gè)參數(shù)谱俭,第二參數(shù)指定了傳遞的源碼字符串?dāng)?shù)量奉件,
這里只有一個(gè)。第三個(gè)參數(shù)是頂點(diǎn)著色器真正的源碼昆著,第四個(gè)參數(shù)我們先設(shè)置為NULL县貌。*/
glCompileShader(vertexShader);
片段著色器
計(jì)算像素最后的顏色輸出,在計(jì)算機(jī)圖形中顏色被表示為有4個(gè)元素的數(shù)組:紅色凑懂、綠色煤痕、藍(lán)色和alpha(透明度)分量,通辰咏鳎縮寫為RGBA摆碉。當(dāng)在OpenGL或GLSL中定義一個(gè)顏色的時(shí)候,我們把顏色每個(gè)分量的強(qiáng)度設(shè)置在0.0到1.0之間脓豪。
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
編譯片段著色器的過程與頂點(diǎn)著色器類似巷帝,只不過我們使用GL_FRAGMENT_SHADER常量作為著色器類型:
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
著色器程序
多個(gè)著色器合并之后并最終鏈接完成的版本,然后在渲染對(duì)象的時(shí)候激活這個(gè)著色器程序扫夜,已激活著色器程序的著色器將在我們發(fā)送渲染調(diào)用的時(shí)候被使用楞泼。
創(chuàng)建一個(gè)程序?qū)ο?/p>
unsigned int shaderProgram;
shaderProgram = glCreateProgram();//創(chuàng)建一個(gè)程序驰徊,并返回新創(chuàng)建程序?qū)ο蟮腎D引用。
//現(xiàn)在我們需要把之前編譯的著色器附加到程序?qū)ο笊隙槔缓笥胓lLinkProgram鏈接它們:
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
得到的結(jié)果就是一個(gè)程序?qū)ο蠊鞒В覀兛梢哉{(diào)用glUseProgram函數(shù),用剛創(chuàng)建的程序?qū)ο笞鳛樗膮?shù)超陆,以激活這個(gè)程序?qū)ο螅?/p>
glUseProgram(shaderProgram);
在glUseProgram函數(shù)調(diào)用之后牺弹,每個(gè)著色器調(diào)用和渲染調(diào)用都會(huì)使用這個(gè)程序?qū)ο螅ㄒ簿褪侵皩懙闹?了。
//刪除著色器對(duì)象
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
鏈接頂點(diǎn)屬性
我們必須在渲染前指定OpenGL該如何解釋頂點(diǎn)數(shù)據(jù)时呀。
說明:
1)位置數(shù)據(jù)被儲(chǔ)存為32位(4字節(jié))浮點(diǎn)值例驹。
2)每個(gè)位置包含3個(gè)這樣的值。
3)在這3個(gè)值之間沒有空隙(或其他值)退唠。這幾個(gè)值在數(shù)組中緊密排列(Tightly Packed)鹃锈。
4)數(shù)據(jù)中第一個(gè)值在緩沖開始的位置。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer函數(shù)的參數(shù)非常多瞧预,所以我會(huì)逐一介紹它們:
1)第一個(gè)參數(shù)指定我們要配置的頂點(diǎn)屬性屎债。還記得我們在頂點(diǎn)著色器中使用layout(location = 0)定義了position頂點(diǎn)屬性的位置值(Location)嗎?它可以把頂點(diǎn)屬性的位置值設(shè)置為0垢油。因?yàn)槲覀兿M褦?shù)據(jù)傳遞到這一個(gè)頂點(diǎn)屬性中盆驹,所以這里我們傳入0。
2)第二個(gè)參數(shù)指定頂點(diǎn)屬性的大小滩愁。頂點(diǎn)屬性是一個(gè)vec3躯喇,它由3個(gè)值組成,所以大小是3硝枉。
3)第三個(gè)參數(shù)指定數(shù)據(jù)的類型廉丽,這里是GL_FLOAT(GLSL中vec*都是由浮點(diǎn)數(shù)值組成的)。
4)下個(gè)參數(shù)定義我們是否希望數(shù)據(jù)被標(biāo)準(zhǔn)化(Normalize)妻味。如果我們設(shè)置為GL_TRUE正压,所有數(shù)據(jù)都會(huì)被映射到0(對(duì)于有符號(hào)型signed數(shù)據(jù)是-1)到1之間。我們把它設(shè)置為GL_FALSE责球。
5)第五個(gè)參數(shù)叫做步長(Stride)焦履,它告訴我們在連續(xù)的頂點(diǎn)屬性組之間的間隔。由于下個(gè)組位置數(shù)據(jù)在3個(gè)float之后雏逾,我們把步長設(shè)置為3 * sizeof(float)嘉裤。要注意的是由于我們知道這個(gè)數(shù)組是緊密排列的(在兩個(gè)頂點(diǎn)屬性之間沒有空隙)我們也可以設(shè)置為0來讓OpenGL決定具體步長是多少(只有當(dāng)數(shù)值是緊密排列時(shí)才可用)。一旦我們有更多的頂點(diǎn)屬性栖博,我們就必須更小心地定義每個(gè)頂點(diǎn)屬性之間的間隔屑宠,我們在后面會(huì)看到更多的例子(譯注: 這個(gè)參數(shù)的意思簡單說就是從這個(gè)屬性第二次出現(xiàn)的地方到整個(gè)數(shù)組0位置之間有多少字節(jié))。
6)最后一個(gè)參數(shù)的類型是void *笛匙,所以需要我們進(jìn)行這個(gè)奇怪的強(qiáng)制類型轉(zhuǎn)換侨把。它表示位置數(shù)據(jù)在緩沖中起始位置的偏移量(Offset)犀变。由于位置數(shù)據(jù)在數(shù)組的開頭,所以這里是0秋柄。我們會(huì)在后面詳細(xì)解釋這個(gè)參數(shù)获枝。
在OpenGL中繪制一個(gè)物體,代碼會(huì)像是這樣:
// 0. 復(fù)制頂點(diǎn)數(shù)組到緩沖中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. 設(shè)置頂點(diǎn)屬性指針
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2. 當(dāng)我們渲染一個(gè)物體時(shí)要使用著色器程序
glUseProgram(shaderProgram);
// 3. 繪制物體
someOpenGLFunctionThatDrawsOurTriangle();
頂點(diǎn)數(shù)組對(duì)象(VAO)
頂點(diǎn)數(shù)組對(duì)象(Vertex Array Object, VAO)可以像頂點(diǎn)緩沖對(duì)象那樣被綁定骇笔,任何隨后的頂點(diǎn)屬性調(diào)用都會(huì)儲(chǔ)存在這個(gè)VAO中省店。這樣的好處就是,當(dāng)配置頂點(diǎn)屬性指針時(shí)笨触,你只需要將那些調(diào)用執(zhí)行一次懦傍,之后再繪制物體的時(shí)候只需要綁定相應(yīng)的VAO就行了。這使在不同頂點(diǎn)數(shù)據(jù)和屬性配置之間切換變得非常簡單芦劣,只需要綁定不同的VAO就行了粗俱。剛剛設(shè)置的所有狀態(tài)都將存儲(chǔ)在VAO中。
一個(gè)頂點(diǎn)數(shù)組對(duì)象會(huì)儲(chǔ)存以下這些內(nèi)容:
1)glEnableVertexAttribArray和glDisableVertexAttribArray的調(diào)用虚吟。
2)通過glVertexAttribPointer設(shè)置的頂點(diǎn)屬性配置寸认。
3)通過glVertexAttribPointer調(diào)用與頂點(diǎn)屬性關(guān)聯(lián)的頂點(diǎn)緩沖對(duì)象。
創(chuàng)建一個(gè)VAO和創(chuàng)建一個(gè)VBO很類似:
unsigned int VAO;
glGenVertexArrays(1, &VAO);
要想使用VAO串慰,要做的只是使用glBindVertexArray綁定VAO偏塞。從綁定之后起,我們應(yīng)該綁定和配置對(duì)應(yīng)的VBO和屬性指針邦鲫,之后解綁VAO供之后使用灸叼。當(dāng)我們打算繪制一個(gè)物體的時(shí)候,我們只要在繪制物體前簡單地把VAO綁定到希望使用的設(shè)定上就行了庆捺。這段代碼應(yīng)該看起來像這樣:
// ..:: 初始化代碼(只運(yùn)行一次 (除非你的物體頻繁改變)) :: ..
// 1. 綁定VAO
glBindVertexArray(VAO);
// 2. 把頂點(diǎn)數(shù)組復(fù)制到緩沖中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 設(shè)置頂點(diǎn)屬性指針
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 繪制代碼(渲染循環(huán)中) :: ..
// 4. 繪制物體
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
索引緩沖對(duì)象EBO:專門存儲(chǔ)索引
索引繪制:先要定義(不重復(fù)的)頂點(diǎn)古今,和繪制出矩形所需的索引。例:
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
unsigned int indices[] = { // 注意索引從0開始!
0, 1, 3, // 第一個(gè)三角形
1, 2, 3 // 第二個(gè)三角形
}
最后的初始化和繪制代碼現(xiàn)在看起來像這樣:
// ..:: 初始化代碼 :: ..
// 1. 綁定頂點(diǎn)數(shù)組對(duì)象
glBindVertexArray(VAO);
// 2. 把我們的頂點(diǎn)數(shù)組復(fù)制到一個(gè)頂點(diǎn)緩沖中疼燥,供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 復(fù)制我們的索引數(shù)組到一個(gè)索引緩沖中沧卢,供OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 設(shè)定頂點(diǎn)屬性指針
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 繪制代碼(渲染循環(huán)中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);