你好,三角形
圖形渲染管線(Pipeline)
3D坐標轉為2D坐標的處理過程是由OpenGL的圖形渲染管線(Pipeline,大多譯為管線,實際上指的是一堆原始圖形數據途經一個輸送管道,期間經過各種變化處理最終出現在屏幕的過程)管理的。圖形渲染管線可以被劃分為兩個主要部分:第一個部分把你的3D坐標轉換為2D坐標,第二部分是把2D坐標轉變為實際的有顏色的像素。
圖形渲染管線可以被劃分為幾個階段,每個階段需要把前一個階段的輸出作為輸入。所有這些階段都是高度專門化的(它們有一個特定的函數),它們能簡單地并行執行。由于它們的并行執行特性,當今大多數顯卡都有成千上萬的小處理核心,在GPU上為每一個(渲染管線)階段運行各自的小程序,從而在圖形渲染管線中快速處理你的數據。這些小程序叫做 著色器(Shader)。因為它們運行在GPU上,所以它們會節約寶貴的CPU時間。
下圖是一個圖形渲染管線的每個階段的抽象表達,藍色部分代表的是我們可以自定義的著色器。
我們以數組的形式傳遞3個3D坐標作為圖形渲染管線的輸入,它用來表示一個三角形,這個數組叫做頂點數據(Vertex Data);這里頂點數據是一些頂點的集合。一個頂點是一個3D坐標的集合(也就是x、y、z數據)。而頂點數據是用頂點屬性(Vertex Attributes)表示的,它可以包含任何我們希望用的數據,但是簡單起見,我們還是假定每個頂點只由一個3D位置(譯注1)和幾個顏色值組成的吧。
為了讓OpenGL知道我們的坐標和顏色值構成的到底是什么,OpenGL需要你去提示你希望這些數據所表示的是什么類型。我們是希望把這些數據渲染成一系列的點?一系列的三角形?還是僅僅是一個長長的線?做出的這些提示叫做圖元(Primitives)****,任何一個繪制命令的調用都必須把基本圖形類型傳遞給OpenGL。這是其中的幾個:GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP。
- 圖形渲染管線的第一個部分是頂點著色器(Vertex Shader),它把一個單獨的頂點作為輸入。頂點著色器主要的目的是把3D坐標轉為另一種3D坐標(后面會解釋),同時頂點著色器允許我們對頂點屬性進行一些基本處理。
- 基本圖元裝配(Primitive Assembly)階段把頂點著色器的表示為基本圖形的所有頂點作為輸入(如果選擇的是GL_POINTS,那么就是一個單獨頂點),把所有點組裝為特定的基本圖形的形狀;本節例子是一個三角形。
- 基本圖形裝配階段的輸出會傳遞給幾何著色器(Geometry Shader)。幾何著色器把基本圖形形式的一系列頂點的集合作為輸入,它可以通過產生新頂點構造出新的(或是其他的)基本圖形來生成其他形狀。例子中,它生成了另一個三角形。
- 細分著色器(Tessellation Shaders)擁有把給定基本圖形細分為更多小基本圖形的能力。這樣我們就能在物體更接近玩家的時候通過創建更多的三角形的方式創建出更加平滑的視覺效果。
- 細分著色器的輸出會進入光柵化(Rasterization也譯為像素化)階段,這里它會把基本圖形映射為屏幕上相應的像素,生成供片段著色器(Fragment Shader)使用的片段(Fragment)。在片段著色器運行之前,會執行裁切(Clipping)。裁切會丟棄超出你的視圖以外的那些像素,來提升執行效率。(OpenGL中的一個fragment是OpenGL渲染一個獨立像素所需的所有數據。)
- 片段著色器(Fragrament Shader)的主要目的是計算一個像素的最終顏色,這也是OpenGL高級效果產生的地方。通常,片段著色器包含用來計算像素最終顏色的3D場景的一些數據(比如光照、陰影、光的顏色等等)。
- 在所有相應顏色值確定以后,最終它會傳到另一個階段,我們叫做alpha測試和混合(Blending)階段。這個階段檢測像素的相應的深度(和Stencil)值(后面會講),使用這些,來檢查這個像素是否在另一個物體的前面或后面,如此做到相應取舍。這個階段也會檢查alpha值(alpha值是一個物體的透明度值)和物體之間的混合(Blend)。所以,即使在片段著色器中計算出來了一個像素所輸出的顏色,最后的像素顏色在渲染多個三角形的時候也可能完全不同。
對于大多數場合,我們必須做的只是頂點和片段著色器。幾何著色器和細分著色器是可選的,通常使用默認的著色器就行了。
在現代OpenGL中,我們必須定義至少一個頂點著色器和一個片段著色器(因為GPU中沒有默認的頂點/片段著色器)。出于這個原因,開始學習現代OpenGL的時候非常困難,因為在你能夠渲染自己的第一個三角形之前需要一大堆知識。本節結束就是你可以最終渲染出你的三角形的時候,你也會了解到很多圖形編程知識。
頂點輸入(Vertex Input)
我們在OpenGL中指定的所有坐標都是在3D坐標里(x、y和z)。OpenGL只是在當它們的3個軸(x、y和z)在特定的-1.0到1.0的范圍內時才處理3D坐標。所有在這個范圍內的坐標叫做標準化設備坐標(Normalized Device Coordinates,NDC)會最終顯示在你的屏幕上(所有出了這個范圍的都不會顯示)。
我們希望渲染一個三角形,我們把它們以GLfloat數組的方式定義為標準化設備坐標(也就是在OpenGL的可見區域)中。
GLfloat 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。在這樣的方式中,三角形的每一處的深度都一樣,從而使它看上去就像2D的。
你的標準化設備坐標接著會變換為屏幕空間坐標(Screen-space Coordinates),這是使用你通過glViewport函數提供的數據,進行視口變換(Viewport Transform)完成的。
有了這樣的頂點數據,我們會把它作為輸入數據發送給圖形渲染管線的第一個處理階段:頂點著色器。它會在GPU上創建儲存空間用于儲存我們的頂點數據,還要配置OpenGL如何解釋這些內存,并且指定如何發送給顯卡。頂點著色器接著會處理我們告訴它要處理內存中的頂點的數量。
我們通過頂點緩沖對象(Vertex Buffer Objects, VBO)管理這個內存,它會在GPU內存(通常被稱為顯存)中儲存大批頂點。使用這些緩沖對象的好處是我們可以一次性的發送一大批數據到顯卡上,而不是每個頂點發送一次。
就像OpenGL中的其他對象一樣,這個緩沖有一個獨一無二的ID,所以我們可以使用glGenBuffers函數生成一個緩沖ID:
GLuint VBO;
glGenBuffers(1, &VBO);
OpenGL有很多緩沖對象類型,GL_ARRAY_BUFFER是其中一個頂點緩沖對象的緩沖類型。OpenGL允許我們同時綁定多個緩沖,只要它們是不同的緩沖類型。我們可以使用glBindBuffer函數把新創建的緩沖綁定到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寫頂點著色器,然后編譯這個著色器,
#version 330 core
layout (location = 0) in vec3 position;
void main()
{
gl_Position = vec4(position.x, position.y, position.z, 1.0);
}
每個著色器都起始于一個版本聲明。我們同樣顯式地表示我們會用核心模式(Core-profile)。
下一步,我們在頂點著色器中聲明所有的輸入頂點屬性,使用in關鍵字。現在我們只關心位置(Position)數據,所以我們只需要一個頂點屬性(Attribute)。
GLSL有一個向量數據類型(vecn),它包含1到4個float元素,包含的數量可以從它的后綴看出來。由于每個頂點都有一個3D坐標,我們就創建一個vec3輸入變量來表示位置(Position)。
我們同樣也指定輸入變量的位置值(Location),這是用layout (location = 0)來完成的。
在GLSL中一個向量有最多4個元素,每個元素值都可以從各自代表一個空間坐標的vec.x、vec.y、vec.z和vec.w來獲取到。
為了設置頂點著色器的輸出,我們必須把位置數據賦值給預定義的gl_Position變量,這個位置數據是一個vec4類型的。在main函數的最后,無論我們給gl_Position設置成什么,它都會成為著色器的輸出。
這個頂點著色器可能是能想到的最簡單的了,因為我們什么都沒有處理就把輸入數據輸出了。在真實的應用里輸入數據通常都沒有在標準化設備坐標中,所以我們首先就必須把它們放進OpenGL的可視區域內。
編譯一個著色器
我們已經寫了一個頂點著色器源碼,但是為了OpenGL能夠使用它,我們必須在運行時動態編譯它的源碼。
- 我們要做的第一件事是創建一個著色器對象,再次引用它的ID。所以我們儲存這個頂點著色器為GLuint,然后用glCreateShader創建著色器:
GLuint vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
我們把著色器的類型提供glCreateShader作為它的參數。這里我們傳遞的參數是GL_VERTEX_SHADER這樣就創建了一個頂點著色器。
- 下一步我們把這個著色器源碼附加到著色器對象,然后編譯它:
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
glShaderSource函數把著色器對象作為第一個參數來編譯它。第二參數指定了源碼中有多少個字符串,這里只有一個。第三個參數是頂點著色器真正的源碼,我們可以把第四個參數先設置為NULL。
你可能會希望檢測調用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檢查是否編譯成功了。如果編譯失敗,我們應該用glGetShaderInfoLog獲取錯誤消息,然后打印它。
如果編譯的時候沒有任何錯誤,頂點著色器就被編譯成功了。
片段著色器(Fragment Shader)
片段著色器的全部,都是用來計算你的像素的最后顏色輸出。為了讓事情比較簡單,我們的片段著色器只輸出橘黃色。
#version 330 core
out vec4 color;
void main()
{
color = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
片段著色器只需要一個輸出變量,這個變量是一個4元素表示的最終輸出顏色的向量。用out關鍵字聲明輸出變量,這里我們命名為color。下面,我們簡單的把一個帶有alpha值為1.0(1.0代表完全不透明)的橘黃的vec4賦值給color作為輸出。
編譯片段著色器的過程與頂點著色器相似,盡管這次我們使用GL_FRAGMENT_SHADER作為著色器類型:
GLuint fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, null);
glCompileShader(fragmentShader);
每個著色器現在都編譯了,剩下的事情是把兩個著色器對象鏈接到一個著色器程序中(Shader Program),它是用來渲染的。
著色器程序(Shader program)
著色器程序對象(Shader Program Object)是多個著色器最后鏈接的版本。如果要使用剛才編譯的著色器我們必須把它們鏈接為一個著色器程序對象,然后當渲染物體的時候激活這個著色器程序。激活了的著色器程序的著色器,在調用渲染函數時才可用。
把著色器鏈接為一個程序就等于把每個著色器的輸出鏈接到下一個著色器的輸入。如果你的輸出和輸入不匹配那么就會得到一個鏈接錯誤。
創建一個程序對象很簡單:
GLuint shaderProgram;
shaderProgram = glCreateProgram();
glCreateProgram函數創建一個程序,返回新創建的程序對象的ID引用。現在我們需要把前面編譯的著色器附加到程序對象上,然后用glLinkProgram鏈接它們:
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
就像著色器的編譯一樣,我們也可以檢驗鏈接著色器程序是否失敗,獲得相應的日志。與glGetShaderiv和glGetShaderInfoLog不同,現在我們使用:
GLint success;
GLchar infoLog[512];
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怎么做。
鏈接頂點屬性
頂點著色器允許我們以任何我們想要的形式作為頂點屬性(Vertex Attribute)的輸入,同樣它也具有很強的靈活性,這意味著我們必須手動指定我們的輸入數據的哪一個部分對應頂點著色器的哪一個頂點屬性。這意味著我們必須在渲染前指定OpenGL如何解釋頂點數據。
我們的頂點緩沖數據被格式化為下面的形式:
- 位置數據被儲存為32-bit(4 byte)浮點值。
- 每個位置包含3個這樣的值。
- 在這3個值之間沒有空隙(或其他值)。這幾個值緊密排列為一個數組。
- 數據中第一個值是緩沖的開始位置。
有了這些信息我們就可以告訴OpenGL如何解釋頂點數據了(每一個頂點屬性),我們使用glVertexAttribPointer這個函數
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);:
glVertexAttribPointer函數有很多參數,所以我們仔細來了解它們:
- 第一個參數指定我們要配置哪一個頂點屬性。記住,我們在頂點著色器中使用layout(location = 0)定義了頂點屬性——位置(position)的位置值(Location)。這樣要把頂點屬性的位置值(Location)設置為0,因為我們希望把數據傳遞到這個頂點屬性中,所以我們在這里填0。
- 第二個參數指定頂點屬性的大小。頂點屬性是vec3類型,它由3個數值組成。
- 第三個參數指定數據的類型,這里是GL_FLOAT(GLSL中vec*是由浮點數組成的)。
- 下個參數定義我們是否希望數據被標準化。如果我們設置為GL_TRUE,所有數據都會被映射到0(對于有符號型signed數據是-1)到1之間。我們把它設置為GL_FALSE。
- 第五個參數叫做步長(Stride),它告訴我們在連續的頂點屬性之間間隔有多少。由于下個位置數據在3個GLfloat后面的位置,我們把步長設置為3 * sizeof(GLfloat)。要注意的是由于我們知道這個數組是緊密排列的(在兩個頂點屬性之間沒有空隙)我們也可以設置為0來讓OpenGL決定具體步長是多少(只有當數值是緊密排列時才可用)。每當我們有更多的頂點屬性,我們就必須小心地定義每個頂點屬性之間的空間,我們在后面會看到更多的例子(譯注: 這個參數的意思簡單說就是從這個屬性第二次出現的地方到整個數組0位置之間有多少字節)。
- 最后一個參數有奇怪的GLvoid*的強制類型轉換。它表示我們的位置數據在緩沖中起始位置的偏移量。由于位置數據是數組的開始,所以這里是0。我們會在后面詳細解釋這個參數。
每個頂點屬性從VBO管理的內存中獲得它的數據,它所獲取數據的那個VBO,就是當調用glVetexAttribPointer的時候,最近綁定到GL_ARRAY_BUFFER的那個VBO。由于在調用glVertexAttribPointer之前綁定了VBO,頂點屬性0(position屬性)現在鏈接到了它的頂點數據。
現在我們定義了OpenGL如何解釋頂點數據,我們也要開啟頂點屬性,使用glEnableVertexAttribArray,把頂點屬性位置值作為它的參數;頂點屬性默認是關閉的。
glEnableVertexAttribArray (0);
自此,我們把每件事都做好了:我們使用一個頂點緩沖對象初始化了一個緩沖中的頂點數據,設置了一個頂點和片段著色器,告訴了OpenGL如何把頂點數據鏈接到頂點著色器的頂點屬性上。在OpenGL繪制一個物體,看起來會像是這樣:
// 0. 復制頂點數組到緩沖中提供給OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. 設置頂點屬性指針
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 2. 當我們打算渲染一個物體時要使用著色器程序
glUseProgram(shaderProgram);
// 3. 繪制物體
someOpenGLFunctionThatDrawsOurTriangle();
我們繪制一個物體的時候必須重復這件事。這看起來也不多,但是如果有超過5個頂點屬性,100多個不同物體呢(這其實并不罕見)。綁定合適的緩沖對象,為每個物體配置所有頂點屬性很快就變成一件麻煩事。有沒有一些方法可以使我們把所有的配置儲存在一個對象中,并且可以通過綁定這個對象來恢復狀態?
頂點數組對象(Vertex Array Object, VAO)
頂點數組對象(Vertex Array Object, VAO)可以像頂點緩沖對象一樣綁定,任何隨后的頂點屬性調用都會儲存在這個VAO中。這有一個好處,當配置頂點屬性指針時,你只用做一次,每次繪制一個物體的時候,我們綁定相應VAO就行了。切換不同頂點數據和屬性配置就像綁定一個不同的VAO一樣簡單。所有狀態我們都放到了VAO里。
OpenGL核心模式版要求我們使用VAO,這樣它就能知道對我們的頂點輸入做些什么。如果我們綁定VAO失敗,OpenGL會拒絕繪制任何東西。
一個頂點數組對象儲存下面的內容:
- 調用glEnableVertexAttribArray和glDisableVertexAttribArray。
- 使用glVertexAttribPointer的頂點屬性配置。
-
使用glVertexAttribPointer進行的頂點緩沖對象與頂點屬性鏈接。
生成一個VAO和生成VBO類似:
GLuint VAO;
glGenVertexArrays(1, &VAO);
使用VAO要做的全部就是使用glBindVertexArray綁定VAO。自此我們就應該綁定/配置相應的VBO和屬性指針,然后解綁VAO以備后用。當我們打算繪制一個物體的時候,我們只要在繪制物體前簡單地把VAO綁定到希望用到的配置就行了。這段代碼應該看起來像這樣:
// ..:: 初始化代碼 (一次完成 (除非你的物體頻繁改變)) :: ..
// 1. 綁定VAO
glBindVertexArray(VAO);
// 2. 把頂點數組復制到緩沖中提供給OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 設置頂點屬性指針
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);
通常情況下當我們配置好它們以后要解綁OpenGL對象,這樣我們才不會在某處錯誤地配置它們。
前面做的一切都是等待這一刻,我們已經把我們的頂點屬性配置和打算使用的VBO儲存到一個VAO中。一般當你有多個物體打算繪制時,你首先要生成/配置所有的VAO(它需要VBO和屬性指針),然后儲存它們準備后面使用。當我們打算繪制物體的時候就拿出相應的VAO,綁定它,繪制完物體后,再解綁VAO。
我們一直期待的三角形
OpenGL的glDrawArrays函數為我們提供了繪制物體的能力,它使用當前激活的著色器、前面定義的頂點屬性配置和VBO的頂點數據(通過VAO間接綁定)來繪制基本圖形。
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glDrawArrays函數第一個參數是我們打算繪制的OpenGL基本圖形的類型。由于我們在一開始時說過,我們希望繪制三角形,我們傳遞GL_TRIANGLES給它。第二個參數定義了我們打算繪制的那個頂點數組的起始位置的索引;我們這里填0。最后一個參數指定我們打算繪制多少個頂點,這里是3(我們只從我們的數據渲染一個三角形,它只有3個頂點)。
如果你編譯通過了,你應該看到下面的結果:
完整的程序源碼可以在這里找到。
索引緩沖對象(Element Buffer Objects,EBO)
假設我們不再繪制一個三角形而是矩形。我們就可以繪制兩個三角形來組成一個矩形(OpenGL主要就是繪制三角形)。這會生成下面的頂點的集合:
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 // 左上角
};
有幾個頂點疊加了。我們指定右下角和左上角兩次!一個矩形只有4個而不是6個頂點,這樣就產生50%的額外開銷。最好的解決方案就是每個頂點只儲存一次。一個EBO是一個像頂點緩沖對象(VBO)一樣的緩沖,它專門儲存索引,OpenGL調用這些頂點的索引來繪制。這樣每個頂點只儲存一次,當我們打算繪制這些頂點的時候只調用頂點的索引。
我們先要定義(獨一無二的)頂點,和繪制出矩形的索引:
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 // 第二個三角形
};
下一步我們需要創建索引緩沖對象:
GLuint EBO;
glGenBuffers(1, &EBO);
我們綁定EBO然后用glBufferData把索引復制到緩沖里。這次我們把緩沖的類型定義為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綁定之后可以索引緩沖對象,EBO就成為了VAO的索引緩沖對象。再次綁定VAO的同時也會自動綁定EBO。
當目標是GL_ELEMENT_ARRAY_BUFFER的時候,VAO儲存了glBindBuffer的函數調用。這也意味著它也會儲存解綁調用,所以確保你沒有在解綁VAO之前解綁索引數組緩沖,否則就沒有這個EBO配置了。(WHY???????????????)
最后的初始化和繪制代碼現在看起來像這樣:
// ..:: 初始化代碼 :: ..
// 1. 綁定VAO
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);
// 3. 設置頂點屬性指針
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid * )0);
glEnableVertexAttribArray(0);
// 4. 解綁VAO,不解綁EBO(譯注:解綁緩沖相當于沒有綁定緩沖,可以在解綁VAO之后解綁緩沖)
glBindVertexArray(0);
[...]
// ..:: 繪制代碼(在游戲循環中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);
運行程序會獲得下面這樣的圖片的結果。上面的圖片看起來很熟悉,而下面的則是使用線框模式(Wireframe Mode)繪制的。線框矩形可以顯示出矩形的確是由兩個三角形組成的。
線框模式(Wireframe Mode)
如果用線框模式繪制你的三角,你可以通過調用glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)來配置OpenGL繪制用的基本圖形。第一個參數說:我們打算應用到所有的三角形的前面和背面,第二個參數告訴我們用線來繪制。在隨后的繪制函數調用后會一直以線框模式繪制三角形,直到我們用glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)設置回了默認模式。
本例代碼在這。
附加資源
- antongerdelan.net/vertexbuffers: 頂點緩沖對象的一些深入探討。
該附加資源的代碼在這。值得一看的講解,關于VBO,以及Using Multiple Vertex Buffers for One Object。
練習
- 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.
- 論壇上別人提的問題