- 頂點數組對象:Vertex Array Object,VAO
- 頂點緩沖對象:Vertex Buffer Object,VBO
- 索引緩沖對象:Element Buffer Object,EBO或Index Buffer Object,IBO
渲染管線
在OpenGL中,任何事物都在3D空間中,而屏幕和窗口卻是2D像素數組,這導致OpenGL的大部分工作都是關於把3D坐標轉變為適應你屏幕的2D像素。3D坐標轉為2D坐標的處理過程是由OpenGL的圖形渲染管線管理的。圖形渲染管線可以被划分為兩個主要部分:第一部分把你的3D坐標轉換為2D坐標,第二部分是把2D坐標轉變為實際的有顏色的像素。
2D坐標和像素也是不同的,2D坐標精確表示一個點在2D空間中的位置,而2D像素是這個點的近似值,2D像素受到你的屏幕/窗口分辨率的限制。
圖形渲染管線接受一組3D坐標,然后把它們轉變為你屏幕上的有色2D像素輸出。圖形渲染管線可以被划分為幾個階段,每個階段將會把前一個階段的輸出作為輸入。所有這些階段都是高度專門化的(它們都有一個特定的函數),並且並行執行。GPU上為每一個(渲染管線)階段運行各自的小程序,從而在圖形渲染管線中快速處理你的數據。這些小程序叫做着色器(Shader)。
OpenGL着色器是用OpenGL着色器語言(OpenGL Shading Language, GLSL)寫成的。
下面,你會看到一個圖形渲染管線的每個階段的抽象展示。藍色部分代表的是可以注入自定義的着色器的部分。

圖形渲染管線包含很多部分,每個部分都將在轉換頂點數據到最終像素這一過程中處理各自特定的階段。概括性地解釋一下渲染管線的每個部分。
頂點數據
首先,我們以數組的形式傳遞3個3D坐標作為圖形渲染管線的輸入,用來表示一個三角形,這個數組叫做頂點數據(Vertex Data);頂點數據是一系列頂點的集合。一個頂點(Vertex)是一個3D坐標的數據的集合。而頂點數據是用頂點屬性(Vertex Attribute)表示的,它可以包含任何我們想用的數據,但是簡單起見,我們還是假定每個頂點只由一個3D位置和一些顏色值組成的吧。
為了讓OpenGL知道我們的坐標和顏色值構成的到底是什么,OpenGL需要你去指定這些數據所表示的渲染類型。我們是希望把這些數據渲染成一系列的點?一系列的三角形?還是僅僅是一個長長的線?做出的這些提示叫做圖元(Primitive),任何一個繪制指令的調用都將把圖元傳遞給OpenGL。這是其中的幾個:GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP。
頂點着色器
圖形渲染管線的第一個部分是頂點着色器(Vertex Shader),它把一個單獨的頂點作為輸入。頂點着色器主要的目的是把3D坐標轉為另一種3D坐標,同時頂點着色器允許我們對頂點屬性進行一些基本處理。
圖元組裝
圖元裝配(Primitive Assembly)階段將頂點着色器輸出的所有頂點作為輸入(如果是GL_POINTS,那么就是一個頂點),並所有的點裝配成指定圖元的形狀。
幾何着色器
圖元裝配階段的輸出會傳遞給幾何着色器(Geometry Shader)。幾何着色器把圖元形式的一系列頂點的集合作為輸入,它可以通過產生新頂點構造出新的(或是其它的)圖元來生成其他形狀。例子中,它生成了另一個三角形。
光柵化
幾何着色器的輸出會被傳入光柵化階段(Rasterization Stage),這里它會把圖元映射為最終屏幕上相應的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器運行之前會執行裁切(Clipping)。裁切會丟棄超出你的視圖以外的所有像素,用來提升執行效率。
片段着色器
OpenGL中的一個片段是OpenGL渲染一個像素所需的所有數據。片段着色器的主要目的是計算一個像素的最終顏色,這也是所有OpenGL高級效果產生的地方。通常,片段着色器包含3D場景的數據(比如光照、陰影、光的顏色等等),這些數據可以被用來計算最終像素的顏色。
測試混合
在所有對應顏色值確定以后,最終的對象將會被傳到最后一個階段,我們叫做Alpha測試和混合(Blending)階段。這個階段檢測片段的對應的深度(和模板(Stencil))值(后面會講),用它們來判斷這個像素是其它物體的前面還是后面,決定是否應該丟棄。這個階段也會檢查alpha值(alpha值定義了一個物體的透明度)並對物體進行混合(Blend)。所以,即使在片段着色器中計算出來了一個像素輸出的顏色,在渲染多個三角形的時候最后的像素顏色也可能完全不同。
頂點輸入
開始繪制圖形之前,我們必須先給OpenGL輸入一些頂點數據。OpenGL是一個3D圖形庫,所以我們在OpenGL中指定的所有坐標都是3D坐標(x、y和z)。OpenGL不是簡單地把所有的3D坐標變換為屏幕上的2D像素;OpenGL僅當3D坐標在3個軸(x、y和z)上都為-1.0到1.0的范圍內時才處理它。所有在所謂的標准化設備坐標(Normalized Device Coordinates)范圍內的坐標才會最終呈現在屏幕上(在這個范圍以外的坐標都不會顯示)。
由於我們希望渲染一個三角形,我們一共要指定三個頂點,每個頂點都有一個3D位置。我們會將它們以標准化設備坐標的形式(OpenGL的可見區域)定義為一個float數組。
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
由於OpenGL是在3D空間中工作的,而我們渲染的是一個2D三角形,我們將它頂點的z坐標設置為0.0。
標准化設備坐標(Normalized Device Coordinates, NDC)
一旦你的頂點坐標已經在頂點着色器中處理過,它們就應該是標准化設備坐標了,標准化設備坐標是一個x、y和z值在-1.0到1.0的一小段空間。任何落在范圍外的坐標都會被丟棄/裁剪,不會顯示在你的屏幕上。下面你會看到我們定義的在標准化設備坐標中的三角形(忽略z軸):

與通常的屏幕坐標不同,y軸正方向為向上,(0, 0)坐標是這個圖像的中心,而不是左上角。最終你希望所有(變換過的)坐標都在這個坐標空間中,否則它們就不可見了。
你的標准化設備坐標接着會變換為屏幕空間坐標(Screen-space Coordinates),這是使用你通過glViewport函數提供的數據,進行視口變換(Viewport Transform)完成的。所得的屏幕空間坐標又會被變換為片段輸入到片段着色器中。
定義這樣的頂點數據以后,我們會把它作為輸入發送給圖形渲染管線的第一個處理階段:頂點着色器。它會在GPU上創建內存用於儲存我們的頂點數據,還要配置OpenGL如何解釋這些內存,並且指定其如何發送給顯卡。頂點着色器接着會處理我們在內存中指定數量的頂點。
通過頂點緩沖對象(Vertex Buffer Objects, VBO)管理這個內存,它會在GPU內存(通常被稱為顯存)中儲存大量頂點。使用這些緩沖對象的好處是我們可以一次性的發送一大批數據到顯卡上,而不是每個頂點發送一次。從CPU把數據發送到顯卡相對較慢,所以只要可能我們都要嘗試盡量一次性發送盡可能多的數據。當數據發送至顯卡的內存中后,頂點着色器幾乎能立即訪問頂點,這是個非常快的過程。
頂點緩沖對象是OpenGL對象。就像OpenGL中的其它對象一樣,這個緩沖有一個獨一無二的ID,所以我們可以使用glGenBuffers函數和一個緩沖ID生成一個VBO對象:
unsigned int VBO;
glGenBuffers(1, &VBO);
OpenGL有很多緩沖對象類型,頂點緩沖對象的緩沖類型是GL_ARRAY_BUFFER。OpenGL允許我們同時綁定多個緩沖,只要它們是不同的緩沖類型。我們可以使用glBindBuffer函數把新創建的緩沖(VBO)綁定到GL_ARRAY_BUFFER目標上:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
從這一刻起,我們使用的任何(在GL_ARRAY_BUFFER目標上的)緩沖調用都會用來配置當前綁定的緩沖(VBO)。然后我們可以調用glBufferData函數,它會把之前定義的頂點數據復制到緩沖的內存中:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData是一個專門用來把用戶定義的數據復制到當前綁定緩沖的函數。它的第一個參數是目標緩沖的類型:頂點緩沖對象當前綁定到GL_ARRAY_BUFFER目標上。第二個參數指定傳輸數據的大小(以字節為單位);用一個簡單的sizeof計算出頂點數據大小就行。第三個參數是我們希望發送的實際數據。
第四個參數指定了我們希望顯卡如何管理給定的數據。它有三種形式:
- GL_STATIC_DRAW :數據不會或幾乎不會改變。
- GL_DYNAMIC_DRAW:數據會被改變很多。
- GL_STREAM_DRAW :數據每次繪制時都會改變。
三角形的位置數據不會改變,每次渲染調用時都保持原樣,所以它的使用類型最好是GL_STATIC_DRAW。如果,比如說一個緩沖中的數據將頻繁被改變,那么使用的類型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,這樣就能確保顯卡把數據放在能夠高速寫入的內存部分。
現在我們已經把頂點數據儲存在顯卡的內存中,用VBO這個頂點緩沖對象管理。下面我們會創建一個頂點和片段着色器來真正處理這些數據。
頂點着色器
頂點着色器(Vertex Shader)是幾個可編程着色器中的一個。我們使用着色器以及配置兩個非常簡單的着色器來繪制我們第一個三角形。
我們需要做的第一件事是用着色器語言GLSL(OpenGL Shading Language)編寫頂點着色器,然后編譯這個着色器,這樣我們就可以在程序中使用它了。下面你會看到一個非常基礎的GLSL頂點着色器的源代碼:
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
可以看到,GLSL看起來很像C語言。每個着色器都起始於一個版本聲明。
下一步,使用in關鍵字,在頂點着色器中聲明所有的輸入頂點屬性(Input Vertex Attribute)。現在我們只關心位置(Position)數據,所以我們只需要一個頂點屬性。GLSL有一個向量數據類型,它包含1到4個float分量,包含的數量可以從它的后綴數字看出來。由於每個頂點都有一個3D坐標,我們就創建一個vec3輸入變量aPos。我們同樣也通過layout (location = 0)設定了輸入變量的位置值(Location)你后面會看到為什么我們會需要這個位置值。
為了設置頂點着色器的輸出,我們必須把位置數據賦值給預定義的gl_Position變量,它在幕后是vec4類型的。在main函數的最后,我們將gl_Position設置的值會成為該頂點着色器的輸出。由於我們的輸入是一個3分量的向量,我們必須把它轉換為4分量的。我們可以把vec3的數據作為vec4構造器的參數,同時把w分量設置為1.0f(我們會在后面解釋為什么)來完成這一任務。
當前這個頂點着色器可能是我們能想到的最簡單的頂點着色器了,因為我們對輸入數據什么都沒有處理就把它傳到着色器的輸出了。在真實的程序里輸入數據通常都不是標准化設備坐標,所以我們首先必須先把它們轉換至OpenGL的可視區域內。
編譯着色器
我們已經寫了一個頂點着色器源碼(儲存在一個C的字符串中),但是為了能夠讓OpenGL使用它,我們必須在運行時動態編譯它的源碼。
我們首先要做的是創建一個着色器對象,注意還是用ID來引用的。所以我們儲存這個頂點着色器為unsigned int,然后用glCreateShader創建這個着色器:
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
我們把需要創建的着色器類型以參數形式提供給glCreateShader。由於我們正在創建一個頂點着色器,傳遞的參數是GL_VERTEX_SHADER。
下一步我們把這個着色器源碼附加到着色器對象上,然后編譯它:
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
glShaderSource函數把要編譯的着色器對象作為第一個參數。第二參數指定了傳遞的源碼字符串數量,這里只有一個。第三個參數是頂點着色器真正的源碼,第四個參數我們先設置為NULL。
你可能會希望檢測在調用glCompileShader后編譯是否成功了,如果沒成功的話,你還會希望知道錯誤是什么,這樣你才能修復它們。檢測編譯時錯誤可以通過以下代碼來實現:
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
首先我們定義一個整型變量來表示是否成功編譯,還定義了一個儲存錯誤消息(如果有的話)的容器。然后我們用glGetShaderiv檢查是否編譯成功。如果編譯失敗,我們會用glGetShaderInfoLog獲取錯誤消息,然后打印它。
if(!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
如果編譯的時候沒有檢測到任何錯誤,頂點着色器就被編譯成功了。
片段着色器
片段着色器所做的是計算像素最后的顏色輸出。
在計算機圖形中顏色被表示為有4個元素的數組:紅色、綠色、藍色和alpha(透明度)分量,通常縮寫為RGBA。當在OpenGL或GLSL中定義一個顏色的時候,我們把顏色每個分量的強度設置在0.0到1.0之間。比如說我們設置紅為1.0f,綠為1.0f,我們會得到兩個顏色的混合色,即黃色。這三種顏色分量的不同調配可以生成超過1600萬種不同的顏色!
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
片段着色器只需要一個輸出變量,這個變量是一個4分量向量,它表示的是最終的輸出顏色,我們應該自己將其計算出來。我們可以用out關鍵字聲明輸出變量,這里我們命名為FragColor。下面,我們將一個alpha值為1.0(1.0代表完全不透明)的橘黃色的vec4賦值給顏色輸出。
編譯片段着色器的過程與頂點着色器類似,只不過我們使用GL_FRAGMENT_SHADER常量作為着色器類型:
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
兩個着色器現在都編譯了,剩下的事情是把兩個着色器對象鏈接到一個用來渲染的着色器程序(Shader Program)中。
着色器程序
着色器程序對象(Shader Program Object)是多個着色器合並之后並最終鏈接完成的版本。如果要使用剛才編譯的着色器我們必須把它們鏈接(Link)為一個着色器程序對象,然后在渲染對象的時候激活這個着色器程序。已激活着色器程序的着色器將在我們發送渲染調用的時候被使用。
當鏈接着色器至一個程序的時候,它會把每個着色器的輸出鏈接到下個着色器的輸入。當輸出和輸入不匹配的時候,你會得到一個連接錯誤。
創建一個程序對象很簡單:
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glCreateProgram函數創建一個程序,並返回新創建程序對象的ID引用。現在我們需要把之前編譯的着色器附加到程序對象上,然后用glLinkProgram鏈接它們:
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
代碼應該很清楚,我們把着色器附加到了程序上,然后用glLinkProgram鏈接。
就像着色器的編譯一樣,我們也可以檢測鏈接着色器程序是否失敗,並獲取相應的日志。與上面不同,我們不會調用glGetShaderiv和glGetShaderInfoLog,現在我們使用:
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
...
}
得到的結果就是一個程序對象,我們可以調用glUseProgram函數,用剛創建的程序對象作為它的參數,以激活這個程序對象:
glUseProgram(shaderProgram);
在glUseProgram函數調用之后,每個着色器調用和渲染調用都會使用這個程序對象(也就是之前寫的着色器)了。
對了,在把着色器對象鏈接到程序對象以后,記得刪除着色器對象,我們不再需要它們了:
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
現在,我們已經把輸入頂點數據發送給了GPU,並指示了GPU如何在頂點和片段着色器中處理它。OpenGL還不知道它該如何解釋內存中的頂點數據,以及它該如何將頂點數據鏈接到頂點着色器的屬性上。
鏈接頂點屬性
頂點着色器允許我們指定任何以頂點屬性為形式的輸入。我們必須手動指定輸入數據的哪一個部分對應頂點着色器的哪一個頂點屬性。所以,我們必須在渲染前指定OpenGL該如何解釋頂點數據。
我們的頂點緩沖數據會被解析為下面這樣子:

- 位置數據被儲存為32位浮點值。
- 每個位置包含3個這樣的值。
- 在這3個值之間沒有空隙。這幾個值在數組中緊密排列(Tightly Packed)。
- 數據中第一個值在緩沖開始的位置。
有了這些信息我們就可以使用glVertexAttribPointer函數告訴OpenGL該如何解析頂點數據(應用到逐個頂點屬性上)了:
//解析頂點數據即解析頂點數據給着色器中的頂點屬性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
//啟用頂點屬性
glEnableVertexAttribArray(0);
glVertexAttribPointer函數的參數非常多,所以我會逐一介紹它們:
- 第一個參數指定我們要配置的頂點屬性。還記得我們在頂點着色器中使用
layout(location = 0)定義了position頂點屬性的位置值(Location)嗎?它可以把頂點屬性的位置值設置為0。因為我們希望把數據傳遞到這一個頂點屬性中,所以這里我們傳入0。在着色器中的位置索引。 - 第二個參數指定頂點屬性的大小。頂點屬性是一個
vec3,它由3個值組成,所以大小是3。 - 第三個參數指定數據的類型,這里是GL_FLOAT(GLSL中
vec*都是由浮點數值組成的)。 - 第四個參數是否希望數據被標准化(Normalize)。如果我們設置為GL_TRUE,所有數據都會被映射到0(對於有符號型signed數據是-1)到1之間。
- 第五個參數叫做步長(Stride),它告訴我們在連續的頂點屬性組之間的間隔。由於下個組位置數據在3個
float之后,我們把步長設置為3 * sizeof(float)。 - 最后一個參數的類型是
void*,需進行這個奇怪的強制類型轉換。它表示位置數據在緩沖(vbo)中起始位置的偏移量(Offset)。由於位置數據在數組的開頭,所以這里是0。
每個頂點屬性從一個VBO管理的內存中獲得它的數據,而具體是從哪個VBO獲取則是通過在調用glVetexAttribPointer時綁定到GL_ARRAY_BUFFER的VBO決定的。由於在調用glVetexAttribPointer之前綁定的是先前定義的VBO對象,頂點屬性0現在會鏈接到它的頂點數據。
現在我們已經定義了OpenGL該如何解釋頂點數據,我們現在應該使用glEnableVertexAttribArray,以頂點屬性位置值作為參數,啟用頂點屬性;頂點屬性默認是禁用的。我們使用一個頂點緩沖對象將頂點數據初始化至緩沖中,建立了一個頂點和一個片段着色器,並告訴了OpenGL如何把頂點數據鏈接到頂點着色器的頂點屬性上。在OpenGL中繪制一個物體,代碼會像是這樣:
//創建頂點緩沖區對象(vbo)
unsigned int VBO;
glGenBuffers(1, &VBO);
//綁定vbo到GL_ARRAY_BUFFER目標(GL_ARRAY_BUFFER是頂點緩沖區的類型)
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//復制頂點數組到緩沖中供OpenGL使用
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//設置頂點屬性指針,告訴opengl如何解析頂點數據到頂點屬性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
//啟用頂點屬性
glEnableVertexAttribArray(0);
//當我們渲染一個物體時要使用着色器程序
glUseProgram(shaderProgram);
//繪制物體
someOpenGLFunctionThatDrawsOurTriangle();
當多存多個物體,每個物體有多個頂點的時候,綁定正確的緩沖對象,為每個物體配置所有頂點屬性很快就變成一件麻煩事。有沒有一些方法可以使我們把所有這些狀態配置儲存在一個對象中,並且可以通過綁定這個對象來恢復狀態呢?
頂點數組對象
頂點數組對象(Vertex Array Object, VAO)可以像頂點緩沖對象那樣被綁定,任何隨后的頂點屬性調用都會儲存在這個VAO中。這樣的好處就是,當配置頂點屬性指針時,你只需要將那些調用執行一次,之后再繪制物體的時候只需要綁定相應的VAO就行了。這使在不同頂點數據和屬性配置之間切換變得非常簡單,只需要綁定不同的VAO就行了。剛剛設置的所有狀態都將存儲在VAO中
一個頂點數組對象會儲存以下這些內容:
- glEnableVertexAttribArray和glDisableVertexAttribArray的調用。
- 通過glVertexAttribPointer設置的頂點屬性配置。
- 通過glVertexAttribPointer調用與頂點屬性關聯的頂點緩沖對象。

創建一個VAO和創建一個VBO很類似:
unsigned int VAO;
glGenVertexArrays(1, &VAO);
要想使用VAO,要做的只是使用glBindVertexArray綁定VAO。從綁定之后起,我們應該綁定和配置對應的VBO和屬性指針,之后解綁VAO供之后使用。當我們打算繪制一個物體的時候,我們只要在繪制物體前簡單地把VAO綁定到希望使用的設定上就行了。這段代碼應該看起來像這樣:
//初始化代碼(只運行一次 (除非你的物體頻繁改變)) :: ..
//綁定VAO
glBindVertexArray(VAO);
//把頂點數組復制到緩沖中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VAO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//設置頂點屬性指針
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 繪制代碼(渲染循環中) :: ..
// 4. 繪制物體
glUseProgram(shaderProgram);
glBindVertexArray(VAO); 設置的頂點屬性配置
someOpenGLFunctionThatDrawsOurTriangle();
一個儲存了頂點屬性配置和應使用的VBO的頂點數組對象。一般當你打算繪制多個物體時,你首先要生成/配置所有的VAO(和必須的VBO及屬性指針),然后儲存它們供后面使用。當我們打算繪制物體的時候就拿出相應的VAO,綁定它,繪制完物體后,再解綁VAO。
繪制三角形
要想繪制我們想要的物體,OpenGL給我們提供了glDrawArrays函數,它使用當前激活的着色器,之前定義的頂點屬性配置,和VBO的頂點數據(通過VAO間接綁定)來繪制圖元。
glUseProgram(shaderProgram); //使用着色器
glBindVertexArray(VAO); //設置的頂點屬性配置
glDrawArrays(GL_TRIANGLES, 0, 3);
glDrawArrays函數第一個參數是我們打算繪制的OpenGL圖元的類型。由於我們在一開始時說過,我們希望繪制的是一個三角形,這里傳遞GL_TRIANGLES給它。第二個參數指定了頂點數組的起始索引,我們這里填0。最后一個參數指定我們打算繪制多少個頂點,這里是3

索引緩沖對象
索引緩沖對象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO)。要解釋索引緩沖對象的工作方式最好還是舉個例子:假設我們不再繪制一個三角形而是繪制一個矩形。我們可以繪制兩個三角形來組成一個矩形(OpenGL主要處理三角形)。這會生成下面的頂點的集合:
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, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
可以看到,有幾個頂點疊加了。我們指定了右下角和左上角兩次!一個矩形只有4個而不是6個頂點,這樣就產生50%的額外開銷。當我們有包括上千個三角形的模型之后這個問題會更糟糕,這會產生一大堆浪費。更好的解決方案是只儲存不同的頂點,並設定繪制這些頂點的順序。這樣子我們只要儲存4個頂點就能繪制矩形了,之后只要指定繪制的順序就行了。
索引緩沖對象的工作方式正是這樣的。和頂點緩沖對象一樣,EBO也是一個緩沖,它專門儲存索引,OpenGL調用這些頂點的索引來決定該繪制哪個頂點。所謂的索引繪制(Indexed Drawing)正是我們問題的解決方案。
- 首先,我們先要定義(不重復的)頂點,和繪制出矩形所需的索引:
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, // 第一個三角形
1, 2, 3 // 第二個三角形
};
你可以看到,當時用索引的時候,我們只定義了4個頂點,而不是6個。
- 下一步我們需要創建索引緩沖對象:
unsigned int EBO;
glGenBuffers(1, &EBO);
與VBO類似,我們先綁定EBO然后用glBufferData把索引復制到緩沖里。同樣,和VBO類似,我們會把這些函數調用放在綁定和解綁函數調用之間,只不過這次我們把緩沖的類型定義為GL_ELEMENT_ARRAY_BUFFER。
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
要注意的是,我們傳遞了GL_ELEMENT_ARRAY_BUFFER當作緩沖目標。
-
最后一件要做的事是用glDrawElements來替換glDrawArrays函數,來指明我們從索引緩沖渲染。
使用glDrawElements時,我們會使用當前綁定的索引緩沖對象中的索引進行繪制:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
第一個參數指定了我們繪制的模式,這個和glDrawArrays的一樣。第二個參數是我們打算繪制頂點的個數,這里填6,也就是說我們一共需要繪制6個頂點。第三個參數是索引的類型,這里是GL_UNSIGNED_INT。最后一個參數里我們可以指定EBO中的偏移量(或者傳遞一個索引數組,但是這是當你不在使用索引緩沖對象的時候),但是我們會在這里填寫0。
glDrawElements函數從當前綁定到GL_ELEMENT_ARRAY_BUFFER目標的EBO中獲取索引。這意味着我們必須在每次要用索引渲染一個物體時綁定相應的EBO,這還是有點麻煩。不過頂點數組對象同樣可以保存索引緩沖對象的綁定狀態。VAO綁定時正在綁定的索引緩沖對象會被保存為VAO的元素緩沖對象。****綁定VAO的同時也會自動綁定EBO。

當目標是GL_ELEMENT_ARRAY_BUFFER的時候,VAO會儲存glBindBuffer的函數調用。這也意味着它也會儲存解綁調用,所以確保你沒有在解綁VAO之前解綁索引數組緩沖,否則它就沒有這個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, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 繪制代碼(渲染循環中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);
運行程序會獲得下面這樣的圖片的結果。左側圖片看應該起來很熟悉,而右側的則是使用線框模式(Wireframe Mode)繪制的。線框矩形可以顯示出矩形的確是由兩個三角形組成的。

繪制三角形的代碼
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
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;
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";
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";
int main()
{
//glfw初始化和設置
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
//創建glfw窗口設置當前上下文設置窗口大小變化回調
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// glad: load all OpenGL function pointers
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// build and compile our shader program
// 創建編譯頂點着色器
int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// check for shader compile errors
int success;
char 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;
}
//創建編譯片段着色器
int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
// check for shader compile errors
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}
//創建shader程序 連接頂點片段着色器 連接shader程序
int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// check for linking errors
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
//鏈接完成着shader程序之后刪除着色器程序
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
// set up vertex data (and buffer(s)) and configure vertex attributes
//頂點數組
float vertices[] = {
0.5f, 0.5f, 0.0f, // top right
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f // top left
};
//索引數組
unsigned int indices[] = { // note that we start from 0!
0, 1, 3, // first Triangle
1, 2, 3 // second Triangle
};
//創建頂點緩沖對象 頂點數組對象 索引緩沖對象
unsigned int VBO, VAO, EBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
glBindVertexArray(VAO);
//綁定頂點緩沖區 copy頂點數據到頂點緩沖區對象
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//綁定頂點索引緩沖區 copy頂點索引數據到頂點索引緩沖區
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//設置頂點屬性指針 解釋頂點數據 頂點索引獲取頂點數據
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
glBindBuffer(GL_ARRAY_BUFFER, 0);
// remember: do NOT unbind the EBO while a VAO is active as the bound element buffer object IS stored in the VAO; keep the EBO bound.
//glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
// You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
// VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
glBindVertexArray(0);
// uncomment this call to draw in wireframe polygons.
//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// draw our first triangle
glUseProgram(shaderProgram);
glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
//glDrawArrays(GL_TRIANGLES, 0, 6); //需要綁定頂點數據
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); //需要綁定頂點索引數據
// glBindVertexArray(0); // no need to unbind it every time
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}
// optional: de-allocate all resources once they've outlived their purpose:
// ------------------------------------------------------------------------
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height);
}
