你好,三角形
圖形渲染管線(Pipeline)
3D坐標(biāo)轉(zhuǎn)為2D坐標(biāo)的處理過程是由OpenGL的圖形渲染管線(Pipeline,大多譯為管線,實際上指的是一堆原始圖形數(shù)據(jù)途經(jīng)一個輸送管道,期間經(jīng)過各種變化處理最終出現(xiàn)在屏幕的過程)管理的。圖形渲染管線可以被劃分為兩個主要部分:第一個部分把你的3D坐標(biāo)轉(zhuǎn)換為2D坐標(biāo),第二部分是把2D坐標(biāo)轉(zhuǎn)變?yōu)閷嶋H的有顏色的像素。
圖形渲染管線可以被劃分為幾個階段,每個階段需要把前一個階段的輸出作為輸入。所有這些階段都是高度專門化的(它們有一個特定的函數(shù)),它們能簡單地并行執(zhí)行。由于它們的并行執(zhí)行特性,當(dāng)今大多數(shù)顯卡都有成千上萬的小處理核心,在GPU上為每一個(渲染管線)階段運(yùn)行各自的小程序,從而在圖形渲染管線中快速處理你的數(shù)據(jù)。這些小程序叫做 著色器(Shader)。因為它們運(yùn)行在GPU上,所以它們會節(jié)約寶貴的CPU時間。
下圖是一個圖形渲染管線的每個階段的抽象表達(dá),藍(lán)色部分代表的是我們可以自定義的著色器。

我們以數(shù)組的形式傳遞3個3D坐標(biāo)作為圖形渲染管線的輸入,它用來表示一個三角形,這個數(shù)組叫做頂點(diǎn)數(shù)據(jù)(Vertex Data);這里頂點(diǎn)數(shù)據(jù)是一些頂點(diǎn)的集合。一個頂點(diǎn)是一個3D坐標(biāo)的集合(也就是x、y、z數(shù)據(jù))。而頂點(diǎn)數(shù)據(jù)是用頂點(diǎn)屬性(Vertex Attributes)表示的,它可以包含任何我們希望用的數(shù)據(jù),但是簡單起見,我們還是假定每個頂點(diǎn)只由一個3D位置(譯注1)和幾個顏色值組成的吧。
為了讓OpenGL知道我們的坐標(biāo)和顏色值構(gòu)成的到底是什么,OpenGL需要你去提示你希望這些數(shù)據(jù)所表示的是什么類型。我們是希望把這些數(shù)據(jù)渲染成一系列的點(diǎn)?一系列的三角形?還是僅僅是一個長長的線?做出的這些提示叫做圖元(Primitives)****,任何一個繪制命令的調(diào)用都必須把基本圖形類型傳遞給OpenGL。這是其中的幾個:GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP。
- 圖形渲染管線的第一個部分是頂點(diǎn)著色器(Vertex Shader),它把一個單獨(dú)的頂點(diǎn)作為輸入。頂點(diǎn)著色器主要的目的是把3D坐標(biāo)轉(zhuǎn)為另一種3D坐標(biāo)(后面會解釋),同時頂點(diǎn)著色器允許我們對頂點(diǎn)屬性進(jìn)行一些基本處理。
- 基本圖元裝配(Primitive Assembly)階段把頂點(diǎn)著色器的表示為基本圖形的所有頂點(diǎn)作為輸入(如果選擇的是GL_POINTS,那么就是一個單獨(dú)頂點(diǎn)),把所有點(diǎn)組裝為特定的基本圖形的形狀;本節(jié)例子是一個三角形。
- 基本圖形裝配階段的輸出會傳遞給幾何著色器(Geometry Shader)。幾何著色器把基本圖形形式的一系列頂點(diǎn)的集合作為輸入,它可以通過產(chǎn)生新頂點(diǎn)構(gòu)造出新的(或是其他的)基本圖形來生成其他形狀。例子中,它生成了另一個三角形。
- 細(xì)分著色器(Tessellation Shaders)擁有把給定基本圖形細(xì)分為更多小基本圖形的能力。這樣我們就能在物體更接近玩家的時候通過創(chuàng)建更多的三角形的方式創(chuàng)建出更加平滑的視覺效果。
- 細(xì)分著色器的輸出會進(jìn)入光柵化(Rasterization也譯為像素化)階段,這里它會把基本圖形映射為屏幕上相應(yīng)的像素,生成供片段著色器(Fragment Shader)使用的片段(Fragment)。在片段著色器運(yùn)行之前,會執(zhí)行裁切(Clipping)。裁切會丟棄超出你的視圖以外的那些像素,來提升執(zhí)行效率。(OpenGL中的一個fragment是OpenGL渲染一個獨(dú)立像素所需的所有數(shù)據(jù)。)
- 片段著色器(Fragrament Shader)的主要目的是計算一個像素的最終顏色,這也是OpenGL高級效果產(chǎn)生的地方。通常,片段著色器包含用來計算像素最終顏色的3D場景的一些數(shù)據(jù)(比如光照、陰影、光的顏色等等)。
- 在所有相應(yīng)顏色值確定以后,最終它會傳到另一個階段,我們叫做alpha測試和混合(Blending)階段。這個階段檢測像素的相應(yīng)的深度(和Stencil)值(后面會講),使用這些,來檢查這個像素是否在另一個物體的前面或后面,如此做到相應(yīng)取舍。這個階段也會檢查alpha值(alpha值是一個物體的透明度值)和物體之間的混合(Blend)。所以,即使在片段著色器中計算出來了一個像素所輸出的顏色,最后的像素顏色在渲染多個三角形的時候也可能完全不同。
對于大多數(shù)場合,我們必須做的只是頂點(diǎn)和片段著色器。幾何著色器和細(xì)分著色器是可選的,通常使用默認(rèn)的著色器就行了。
在現(xiàn)代OpenGL中,我們必須定義至少一個頂點(diǎn)著色器和一個片段著色器(因為GPU中沒有默認(rèn)的頂點(diǎn)/片段著色器)。出于這個原因,開始學(xué)習(xí)現(xiàn)代OpenGL的時候非常困難,因為在你能夠渲染自己的第一個三角形之前需要一大堆知識。本節(jié)結(jié)束就是你可以最終渲染出你的三角形的時候,你也會了解到很多圖形編程知識。
頂點(diǎn)輸入(Vertex Input)
我們在OpenGL中指定的所有坐標(biāo)都是在3D坐標(biāo)里(x、y和z)。OpenGL只是在當(dāng)它們的3個軸(x、y和z)在特定的-1.0到1.0的范圍內(nèi)時才處理3D坐標(biāo)。所有在這個范圍內(nèi)的坐標(biāo)叫做標(biāo)準(zhǔn)化設(shè)備坐標(biāo)(Normalized Device Coordinates,NDC)會最終顯示在你的屏幕上(所有出了這個范圍的都不會顯示)。
我們希望渲染一個三角形,我們把它們以GLfloat數(shù)組的方式定義為標(biāo)準(zhǔn)化設(shè)備坐標(biāo)(也就是在OpenGL的可見區(qū)域)中。
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
由于OpenGL是在3D空間中工作的,我們渲染一個2D三角形,它的每個頂點(diǎn)都要有同一個z坐標(biāo)0.0。在這樣的方式中,三角形的每一處的深度都一樣,從而使它看上去就像2D的。
你的標(biāo)準(zhǔn)化設(shè)備坐標(biāo)接著會變換為屏幕空間坐標(biāo)(Screen-space Coordinates),這是使用你通過glViewport函數(shù)提供的數(shù)據(jù),進(jìn)行視口變換(Viewport Transform)完成的。
有了這樣的頂點(diǎn)數(shù)據(jù),我們會把它作為輸入數(shù)據(jù)發(fā)送給圖形渲染管線的第一個處理階段:頂點(diǎn)著色器。它會在GPU上創(chuàng)建儲存空間用于儲存我們的頂點(diǎn)數(shù)據(jù),還要配置OpenGL如何解釋這些內(nèi)存,并且指定如何發(fā)送給顯卡。頂點(diǎn)著色器接著會處理我們告訴它要處理內(nèi)存中的頂點(diǎn)的數(shù)量。
我們通過頂點(diǎn)緩沖對象(Vertex Buffer Objects, VBO)管理這個內(nèi)存,它會在GPU內(nèi)存(通常被稱為顯存)中儲存大批頂點(diǎn)。使用這些緩沖對象的好處是我們可以一次性的發(fā)送一大批數(shù)據(jù)到顯卡上,而不是每個頂點(diǎn)發(fā)送一次。
就像OpenGL中的其他對象一樣,這個緩沖有一個獨(dú)一無二的ID,所以我們可以使用glGenBuffers函數(shù)生成一個緩沖ID:
GLuint VBO;
glGenBuffers(1, &VBO);
OpenGL有很多緩沖對象類型,GL_ARRAY_BUFFER是其中一個頂點(diǎn)緩沖對象的緩沖類型。OpenGL允許我們同時綁定多個緩沖,只要它們是不同的緩沖類型。我們可以使用glBindBuffer函數(shù)把新創(chuàng)建的緩沖綁定到GL_ARRAY_BUFFER上:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
從這一刻起,我們使用的任何緩沖函數(shù)(在GL_ARRAY_BUFFER目標(biāo)上)都會用來配置當(dāng)前綁定的緩沖(VBO)。然后我們可以調(diào)用glBufferData函數(shù),它會把之前定義的頂點(diǎn)數(shù)據(jù)復(fù)制到緩沖的內(nèi)存中:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData是一個用來把用戶定的義數(shù)據(jù)復(fù)制到當(dāng)前綁定緩沖的函數(shù)。它的第一個參數(shù)是我們希望把數(shù)據(jù)復(fù)制到上面的緩沖類型:頂點(diǎn)緩沖對象當(dāng)前綁定到GL_ARRAY_BUFFER目標(biāo)上。第二個參數(shù)指定我們希望傳遞給緩沖的數(shù)據(jù)的大小(以字節(jié)為單位);用一個簡單的sizeof計算出頂點(diǎn)數(shù)據(jù)就行。第三個參數(shù)是我們希望發(fā)送的真實數(shù)據(jù)(的指針)。
第四個參數(shù)指定了我們希望顯卡如何管理給定的數(shù)據(jù)。有三種形式:
GL_STATIC_DRAW :數(shù)據(jù)不會或幾乎不會改變。
GL_DYNAMIC_DRAW:數(shù)據(jù)會被改變很多。
GL_STREAM_DRAW :數(shù)據(jù)每次繪制時都會改變。
三角形的位置數(shù)據(jù)不會改變,每次渲染調(diào)用時都保持原樣,所以它使用的類型最好是GL_STATIC_DRAW。如果,比如,一個緩沖中的數(shù)據(jù)將頻繁被改變,那么使用的類型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW。這樣就能確保圖形卡把數(shù)據(jù)放在高速寫入的內(nèi)存部分。
到現(xiàn)在我們已經(jīng)把頂點(diǎn)數(shù)據(jù)儲存在顯卡的內(nèi)存中,并且用VBO頂點(diǎn)緩沖對象來管理。下面我們會創(chuàng)建一個頂點(diǎn)和片段著色器,來處理這些數(shù)據(jù)。
頂點(diǎn)著色器(Vertex Shader)
我們需要做的第一件事是用著色器語言GLSL寫頂點(diǎn)著色器,然后編譯這個著色器,
#version 330 core
layout (location = 0) in vec3 position;
void main()
{
gl_Position = vec4(position.x, position.y, position.z, 1.0);
}
每個著色器都起始于一個版本聲明。我們同樣顯式地表示我們會用核心模式(Core-profile)。
下一步,我們在頂點(diǎn)著色器中聲明所有的輸入頂點(diǎn)屬性,使用in關(guān)鍵字。現(xiàn)在我們只關(guān)心位置(Position)數(shù)據(jù),所以我們只需要一個頂點(diǎn)屬性(Attribute)。
GLSL有一個向量數(shù)據(jù)類型(vecn),它包含1到4個float元素,包含的數(shù)量可以從它的后綴看出來。由于每個頂點(diǎn)都有一個3D坐標(biāo),我們就創(chuàng)建一個vec3輸入變量來表示位置(Position)。
我們同樣也指定輸入變量的位置值(Location),這是用layout (location = 0)來完成的。
在GLSL中一個向量有最多4個元素,每個元素值都可以從各自代表一個空間坐標(biāo)的vec.x、vec.y、vec.z和vec.w來獲取到。
為了設(shè)置頂點(diǎn)著色器的輸出,我們必須把位置數(shù)據(jù)賦值給預(yù)定義的gl_Position變量,這個位置數(shù)據(jù)是一個vec4類型的。在main函數(shù)的最后,無論我們給gl_Position設(shè)置成什么,它都會成為著色器的輸出。
這個頂點(diǎn)著色器可能是能想到的最簡單的了,因為我們什么都沒有處理就把輸入數(shù)據(jù)輸出了。在真實的應(yīng)用里輸入數(shù)據(jù)通常都沒有在標(biāo)準(zhǔn)化設(shè)備坐標(biāo)中,所以我們首先就必須把它們放進(jìn)OpenGL的可視區(qū)域內(nèi)。
編譯一個著色器
我們已經(jīng)寫了一個頂點(diǎn)著色器源碼,但是為了OpenGL能夠使用它,我們必須在運(yùn)行時動態(tài)編譯它的源碼。
- 我們要做的第一件事是創(chuàng)建一個著色器對象,再次引用它的ID。所以我們儲存這個頂點(diǎn)著色器為GLuint,然后用glCreateShader創(chuàng)建著色器:
GLuint vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
我們把著色器的類型提供glCreateShader作為它的參數(shù)。這里我們傳遞的參數(shù)是GL_VERTEX_SHADER這樣就創(chuàng)建了一個頂點(diǎn)著色器。
- 下一步我們把這個著色器源碼附加到著色器對象,然后編譯它:
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
glShaderSource函數(shù)把著色器對象作為第一個參數(shù)來編譯它。第二參數(shù)指定了源碼中有多少個字符串,這里只有一個。第三個參數(shù)是頂點(diǎn)著色器真正的源碼,我們可以把第四個參數(shù)先設(shè)置為NULL。
你可能會希望檢測調(diào)用glCompileShader后是否編譯成功了,是否要去修正錯誤。檢測編譯時錯誤的方法是:
GLint success;
GLchar infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if(!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
首先我們定義一個整型來表示是否成功編譯,還需要一個儲存錯誤消息的容器(如果有的話)。然后我們用glGetShaderiv檢查是否編譯成功了。如果編譯失敗,我們應(yīng)該用glGetShaderInfoLog獲取錯誤消息,然后打印它。
如果編譯的時候沒有任何錯誤,頂點(diǎn)著色器就被編譯成功了。
片段著色器(Fragment Shader)
片段著色器的全部,都是用來計算你的像素的最后顏色輸出。為了讓事情比較簡單,我們的片段著色器只輸出橘黃色。
#version 330 core
out vec4 color;
void main()
{
color = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
片段著色器只需要一個輸出變量,這個變量是一個4元素表示的最終輸出顏色的向量。用out關(guān)鍵字聲明輸出變量,這里我們命名為color。下面,我們簡單的把一個帶有alpha值為1.0(1.0代表完全不透明)的橘黃的vec4賦值給color作為輸出。
編譯片段著色器的過程與頂點(diǎn)著色器相似,盡管這次我們使用GL_FRAGMENT_SHADER作為著色器類型:
GLuint fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, null);
glCompileShader(fragmentShader);
每個著色器現(xiàn)在都編譯了,剩下的事情是把兩個著色器對象鏈接到一個著色器程序中(Shader Program),它是用來渲染的。
著色器程序(Shader program)
著色器程序?qū)ο?Shader Program Object)是多個著色器最后鏈接的版本。如果要使用剛才編譯的著色器我們必須把它們鏈接為一個著色器程序?qū)ο?,然后?dāng)渲染物體的時候激活這個著色器程序。激活了的著色器程序的著色器,在調(diào)用渲染函數(shù)時才可用。
把著色器鏈接為一個程序就等于把每個著色器的輸出鏈接到下一個著色器的輸入。如果你的輸出和輸入不匹配那么就會得到一個鏈接錯誤。
創(chuàng)建一個程序?qū)ο蠛芎唵危?/p>
GLuint shaderProgram;
shaderProgram = glCreateProgram();
glCreateProgram函數(shù)創(chuàng)建一個程序,返回新創(chuàng)建的程序?qū)ο蟮腎D引用?,F(xiàn)在我們需要把前面編譯的著色器附加到程序?qū)ο笊?,然后用glLinkProgram鏈接它們:
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
就像著色器的編譯一樣,我們也可以檢驗鏈接著色器程序是否失敗,獲得相應(yīng)的日志。與glGetShaderiv和glGetShaderInfoLog不同,現(xiàn)在我們使用:
GLint success;
GLchar infoLog[512];
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
...
}
我們可以調(diào)用glUseProgram函數(shù),用新創(chuàng)建的程序?qū)ο笞鳛樗膮?shù),這樣就能激活這個程序?qū)ο螅?/p>
glUseProgram(shaderProgram);
現(xiàn)在在glUseProgram函數(shù)調(diào)用之后的每個著色器和渲染函數(shù)都會用到這個程序?qū)ο?當(dāng)然還有這些鏈接的著色器)了。
在我們把著色器對象鏈接到程序?qū)ο笠院螅灰泟h除著色器對象;我們不再需要它們了:
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
現(xiàn)在,我們把輸入頂點(diǎn)數(shù)據(jù)發(fā)送給GPU,指示GPU如何在頂點(diǎn)和片段著色器中處理它。還沒結(jié)束,OpenGL還不知道如何解釋內(nèi)存中的頂點(diǎn)數(shù)據(jù),以及怎樣把頂點(diǎn)數(shù)據(jù)鏈接到頂點(diǎn)著色器的屬性上。我們需要告訴OpenGL怎么做。
鏈接頂點(diǎn)屬性
頂點(diǎn)著色器允許我們以任何我們想要的形式作為頂點(diǎn)屬性(Vertex Attribute)的輸入,同樣它也具有很強(qiáng)的靈活性,這意味著我們必須手動指定我們的輸入數(shù)據(jù)的哪一個部分對應(yīng)頂點(diǎn)著色器的哪一個頂點(diǎn)屬性。這意味著我們必須在渲染前指定OpenGL如何解釋頂點(diǎn)數(shù)據(jù)。
我們的頂點(diǎn)緩沖數(shù)據(jù)被格式化為下面的形式:

- 位置數(shù)據(jù)被儲存為32-bit(4 byte)浮點(diǎn)值。
- 每個位置包含3個這樣的值。
- 在這3個值之間沒有空隙(或其他值)。這幾個值緊密排列為一個數(shù)組。
- 數(shù)據(jù)中第一個值是緩沖的開始位置。
有了這些信息我們就可以告訴OpenGL如何解釋頂點(diǎn)數(shù)據(jù)了(每一個頂點(diǎn)屬性),我們使用glVertexAttribPointer這個函數(shù)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);:
glVertexAttribPointer函數(shù)有很多參數(shù),所以我們仔細(xì)來了解它們:
- 第一個參數(shù)指定我們要配置哪一個頂點(diǎn)屬性。記住,我們在頂點(diǎn)著色器中使用layout(location = 0)定義了頂點(diǎn)屬性——位置(position)的位置值(Location)。這樣要把頂點(diǎn)屬性的位置值(Location)設(shè)置為0,因為我們希望把數(shù)據(jù)傳遞到這個頂點(diǎn)屬性中,所以我們在這里填0。
- 第二個參數(shù)指定頂點(diǎn)屬性的大小。頂點(diǎn)屬性是vec3類型,它由3個數(shù)值組成。
- 第三個參數(shù)指定數(shù)據(jù)的類型,這里是GL_FLOAT(GLSL中vec*是由浮點(diǎn)數(shù)組成的)。
- 下個參數(shù)定義我們是否希望數(shù)據(jù)被標(biāo)準(zhǔn)化。如果我們設(shè)置為GL_TRUE,所有數(shù)據(jù)都會被映射到0(對于有符號型signed數(shù)據(jù)是-1)到1之間。我們把它設(shè)置為GL_FALSE。
- 第五個參數(shù)叫做步長(Stride),它告訴我們在連續(xù)的頂點(diǎn)屬性之間間隔有多少。由于下個位置數(shù)據(jù)在3個GLfloat后面的位置,我們把步長設(shè)置為3 * sizeof(GLfloat)。要注意的是由于我們知道這個數(shù)組是緊密排列的(在兩個頂點(diǎn)屬性之間沒有空隙)我們也可以設(shè)置為0來讓OpenGL決定具體步長是多少(只有當(dāng)數(shù)值是緊密排列時才可用)。每當(dāng)我們有更多的頂點(diǎn)屬性,我們就必須小心地定義每個頂點(diǎn)屬性之間的空間,我們在后面會看到更多的例子(譯注: 這個參數(shù)的意思簡單說就是從這個屬性第二次出現(xiàn)的地方到整個數(shù)組0位置之間有多少字節(jié))。
- 最后一個參數(shù)有奇怪的GLvoid*的強(qiáng)制類型轉(zhuǎn)換。它表示我們的位置數(shù)據(jù)在緩沖中起始位置的偏移量。由于位置數(shù)據(jù)是數(shù)組的開始,所以這里是0。我們會在后面詳細(xì)解釋這個參數(shù)。
每個頂點(diǎn)屬性從VBO管理的內(nèi)存中獲得它的數(shù)據(jù),它所獲取數(shù)據(jù)的那個VBO,就是當(dāng)調(diào)用glVetexAttribPointer的時候,最近綁定到GL_ARRAY_BUFFER的那個VBO。由于在調(diào)用glVertexAttribPointer之前綁定了VBO,頂點(diǎn)屬性0(position屬性)現(xiàn)在鏈接到了它的頂點(diǎn)數(shù)據(jù)。
現(xiàn)在我們定義了OpenGL如何解釋頂點(diǎn)數(shù)據(jù),我們也要開啟頂點(diǎn)屬性,使用glEnableVertexAttribArray,把頂點(diǎn)屬性位置值作為它的參數(shù);頂點(diǎn)屬性默認(rèn)是關(guān)閉的。
glEnableVertexAttribArray (0);
自此,我們把每件事都做好了:我們使用一個頂點(diǎn)緩沖對象初始化了一個緩沖中的頂點(diǎn)數(shù)據(jù),設(shè)置了一個頂點(diǎn)和片段著色器,告訴了OpenGL如何把頂點(diǎn)數(shù)據(jù)鏈接到頂點(diǎn)著色器的頂點(diǎn)屬性上。在OpenGL繪制一個物體,看起來會像是這樣:
// 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(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 2. 當(dāng)我們打算渲染一個物體時要使用著色器程序
glUseProgram(shaderProgram);
// 3. 繪制物體
someOpenGLFunctionThatDrawsOurTriangle();
我們繪制一個物體的時候必須重復(fù)這件事。這看起來也不多,但是如果有超過5個頂點(diǎn)屬性,100多個不同物體呢(這其實并不罕見)。綁定合適的緩沖對象,為每個物體配置所有頂點(diǎn)屬性很快就變成一件麻煩事。有沒有一些方法可以使我們把所有的配置儲存在一個對象中,并且可以通過綁定這個對象來恢復(fù)狀態(tài)?
頂點(diǎn)數(shù)組對象(Vertex Array Object, VAO)
頂點(diǎn)數(shù)組對象(Vertex Array Object, VAO)可以像頂點(diǎn)緩沖對象一樣綁定,任何隨后的頂點(diǎn)屬性調(diào)用都會儲存在這個VAO中。這有一個好處,當(dāng)配置頂點(diǎn)屬性指針時,你只用做一次,每次繪制一個物體的時候,我們綁定相應(yīng)VAO就行了。切換不同頂點(diǎn)數(shù)據(jù)和屬性配置就像綁定一個不同的VAO一樣簡單。所有狀態(tài)我們都放到了VAO里。
OpenGL核心模式版要求我們使用VAO,這樣它就能知道對我們的頂點(diǎn)輸入做些什么。如果我們綁定VAO失敗,OpenGL會拒絕繪制任何東西。
一個頂點(diǎn)數(shù)組對象儲存下面的內(nèi)容:
- 調(diào)用glEnableVertexAttribArray和glDisableVertexAttribArray。
- 使用glVertexAttribPointer的頂點(diǎn)屬性配置。
-
使用glVertexAttribPointer進(jìn)行的頂點(diǎn)緩沖對象與頂點(diǎn)屬性鏈接。
生成一個VAO和生成VBO類似:
GLuint VAO;
glGenVertexArrays(1, &VAO);
使用VAO要做的全部就是使用glBindVertexArray綁定VAO。自此我們就應(yīng)該綁定/配置相應(yīng)的VBO和屬性指針,然后解綁VAO以備后用。當(dāng)我們打算繪制一個物體的時候,我們只要在繪制物體前簡單地把VAO綁定到希望用到的配置就行了。這段代碼應(yīng)該看起來像這樣:
// ..:: 初始化代碼 (一次完成 (除非你的物體頻繁改變)) :: ..
// 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(GLfloat), (GLvoid * )0);
glEnableVertexAttribArray(0);
//4. 解綁VAO
glBindVertexArray(0);
[...]
// ..:: 繪制代碼 (in Game loop) :: ..
// 5. 繪制物體
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
glBindVertexArray(0);
通常情況下當(dāng)我們配置好它們以后要解綁OpenGL對象,這樣我們才不會在某處錯誤地配置它們。
前面做的一切都是等待這一刻,我們已經(jīng)把我們的頂點(diǎn)屬性配置和打算使用的VBO儲存到一個VAO中。一般當(dāng)你有多個物體打算繪制時,你首先要生成/配置所有的VAO(它需要VBO和屬性指針),然后儲存它們準(zhǔn)備后面使用。當(dāng)我們打算繪制物體的時候就拿出相應(yīng)的VAO,綁定它,繪制完物體后,再解綁VAO。
我們一直期待的三角形
OpenGL的glDrawArrays函數(shù)為我們提供了繪制物體的能力,它使用當(dāng)前激活的著色器、前面定義的頂點(diǎn)屬性配置和VBO的頂點(diǎn)數(shù)據(jù)(通過VAO間接綁定)來繪制基本圖形。
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glDrawArrays函數(shù)第一個參數(shù)是我們打算繪制的OpenGL基本圖形的類型。由于我們在一開始時說過,我們希望繪制三角形,我們傳遞GL_TRIANGLES給它。第二個參數(shù)定義了我們打算繪制的那個頂點(diǎn)數(shù)組的起始位置的索引;我們這里填0。最后一個參數(shù)指定我們打算繪制多少個頂點(diǎn),這里是3(我們只從我們的數(shù)據(jù)渲染一個三角形,它只有3個頂點(diǎn))。
如果你編譯通過了,你應(yīng)該看到下面的結(jié)果:

完整的程序源碼可以在這里找到。
索引緩沖對象(Element Buffer Objects,EBO)
假設(shè)我們不再繪制一個三角形而是矩形。我們就可以繪制兩個三角形來組成一個矩形(OpenGL主要就是繪制三角形)。這會生成下面的頂點(diǎn)的集合:
GLfloat 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, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
有幾個頂點(diǎn)疊加了。我們指定右下角和左上角兩次!一個矩形只有4個而不是6個頂點(diǎn),這樣就產(chǎn)生50%的額外開銷。最好的解決方案就是每個頂點(diǎn)只儲存一次。一個EBO是一個像頂點(diǎn)緩沖對象(VBO)一樣的緩沖,它專門儲存索引,OpenGL調(diào)用這些頂點(diǎn)的索引來繪制。這樣每個頂點(diǎn)只儲存一次,當(dāng)我們打算繪制這些頂點(diǎn)的時候只調(diào)用頂點(diǎn)的索引。
我們先要定義(獨(dú)一無二的)頂點(diǎn),和繪制出矩形的索引:
GLfloat 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 // 左上角
};
GLuint indices[] = { // 起始于0!
0, 1, 3, // 第一個三角形
1, 2, 3 // 第二個三角形
};
下一步我們需要創(chuàng)建索引緩沖對象:
GLuint EBO;
glGenBuffers(1, &EBO);
我們綁定EBO然后用glBufferData把索引復(fù)制到緩沖里。這次我們把緩沖的類型定義為GL_ELEMENT_ARRAY_BUFFER。
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
要注意的是,我們現(xiàn)在用GL_ELEMENT_ARRAY_BUFFER當(dāng)作緩沖目標(biāo)。最后一件要做的事是用glDrawElements來替換glDrawArrays函數(shù),來指明我們從索引緩沖渲染。當(dāng)使用glDrawElements的時候,我們就會用當(dāng)前綁定的索引緩沖進(jìn)行繪制:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
第一個參數(shù)指定了我們繪制的模式,這個和glDrawArrays的一樣。第二個參數(shù)是我們打算繪制頂點(diǎn)的次數(shù)。我們填6,說明我們總共想繪制6個頂點(diǎn)。第三個參數(shù)是索引的類型,這里是GL_UNSIGNED_INT。最后一個參數(shù)里我們可以指定EBO中的偏移量(或者傳遞一個索引數(shù)組,但是這只是當(dāng)你不是在使用索引緩沖對象的時候),但是我們只打算在這里填寫0。
glDrawElements函數(shù)從當(dāng)前綁定到GL_ELEMENT_ARRAY_BUFFER目標(biāo)的EBO獲取索引。這意味著我們必須在每次要用索引渲染一個物體時綁定相應(yīng)的EBO,這還是有點(diǎn)麻煩。不過頂點(diǎn)數(shù)組對象仍可以保存索引緩沖對象的綁定狀態(tài)。VAO綁定之后可以索引緩沖對象,EBO就成為了VAO的索引緩沖對象。再次綁定VAO的同時也會自動綁定EBO。

當(dāng)目標(biāo)是GL_ELEMENT_ARRAY_BUFFER的時候,VAO儲存了glBindBuffer的函數(shù)調(diào)用。這也意味著它也會儲存解綁調(diào)用,所以確保你沒有在解綁VAO之前解綁索引數(shù)組緩沖,否則就沒有這個EBO配置了。(WHY???????????????)
最后的初始化和繪制代碼現(xiàn)在看起來像這樣:
// ..:: 初始化代碼 :: ..
// 1. 綁定VAO
glBindVertexArray(VAO);
// 2. 把我們的頂點(diǎn)數(shù)組復(fù)制到一個頂點(diǎn)緩沖中,提供給OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 復(fù)制我們的索引數(shù)組到一個索引緩沖中,提供給OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices),indices, GL_STATIC_DRAW);
// 3. 設(shè)置頂點(diǎn)屬性指針
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid * )0);
glEnableVertexAttribArray(0);
// 4. 解綁VAO,不解綁EBO(譯注:解綁緩沖相當(dāng)于沒有綁定緩沖,可以在解綁VAO之后解綁緩沖)
glBindVertexArray(0);
[...]
// ..:: 繪制代碼(在游戲循環(huán)中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);
運(yùn)行程序會獲得下面這樣的圖片的結(jié)果。上面的圖片看起來很熟悉,而下面的則是使用線框模式(Wireframe Mode)繪制的。線框矩形可以顯示出矩形的確是由兩個三角形組成的。


線框模式(Wireframe Mode)
如果用線框模式繪制你的三角,你可以通過調(diào)用glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)來配置OpenGL繪制用的基本圖形。第一個參數(shù)說:我們打算應(yīng)用到所有的三角形的前面和背面,第二個參數(shù)告訴我們用線來繪制。在隨后的繪制函數(shù)調(diào)用后會一直以線框模式繪制三角形,直到我們用glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)設(shè)置回了默認(rèn)模式。
本例代碼在這。
附加資源
- antongerdelan.net/vertexbuffers: 頂點(diǎn)緩沖對象的一些深入探討。

該附加資源的代碼在這。值得一看的講解,關(guān)于VBO,以及Using Multiple Vertex Buffers for One Object。
練習(xí)
- Try to draw 2 triangles next to each other using glDrawArrays by adding more vertices to your data:solution.

- Now create the same 2 triangles using two different VAOs and VBOs for their data: solution.

- Create two shader programs where the second program uses a different fragment shader that outputs the color yellow; draw both triangles again where one outputs the color yellow: solution.

- 論壇上別人提的問題

