Learn OpenGL (二):三角形
知識補充:
- 頂點數組對象:Vertex Array Object,VAO
- 頂點緩沖對象:Vertex Buffer Object,VBO
- 索引緩沖對象:Element Buffer Object,EBO或Index Buffer Object,IBO
藍色部分代表的是我們可以注入自定義的著色器的部分。
圖形渲染管線的第一個部分是頂點著色器(Vertex Shader),它把一個單獨的頂點作為輸入。頂點著色器主要的目的是把3D坐標轉為另一種3D坐標(后面會解釋),同時頂點著色器允許我們對頂點屬性進行一些基本處理。
圖元裝配(Primitive Assembly)階段將頂點著色器輸出的所有頂點作為輸入(如果是GL_POINTS,那么就是一個頂點),并所有的點裝配成指定圖元的形狀;本節例子中是一個三角形。
圖元裝配階段的輸出會傳遞給幾何著色器(Geometry Shader)。幾何著色器把圖元形式的一系列頂點的集合作為輸入,它可以通過產生新頂點構造出新的(或是其它的)圖元來生成其他形狀。例子中,它生成了另一個三角形。
幾何著色器的輸出會被傳入光柵化階段(Rasterization Stage),這里它會把圖元映射為最終屏幕上相應的像素,生成供片段著色器(Fragment Shader)使用的片段(Fragment)。在片段著色器運行之前會執行裁切(Clipping)。裁切會丟棄超出你的視圖以外的所有像素,用來提升執行效率。
?
頂點輸入
由于我們希望渲染一個三角形,我們一共要指定三個頂點,每個頂點都有一個3D位置。我們會將它們以標準化設備坐標的形式(OpenGL的可見區域)定義為一個float數組。
float vertices[] = {-0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f,0.0f, 0.5f, 0.0f
};
開始繪制圖形之前,我們必須先給OpenGL輸入一些頂點數據。OpenGL是一個3D圖形庫,所以我們在OpenGL中指定的所有坐標都是3D坐標(x、y和z)。OpenGL不是簡單地把所有的3D坐標變換為屏幕上的2D像素;OpenGL僅當3D坐標在3個軸(x、y和z)上都為-1.0到1.0的范圍內時才處理它。所有在所謂的標準化設備坐標(Normalized Device Coordinates)范圍內的坐標才會最終呈現在屏幕上(在這個范圍以外的坐標都不會顯示)。
標準化設備坐標(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對象。就像OpenGL中的其它對象一樣,這個緩沖有一個獨一無二的ID,所以我們可以使用glGenBuffers函數和一個緩沖ID生成一個VBO對象:
unsigned int 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這個頂點緩沖對象管理。下面我們會創建一個頂點和片段著色器來真正處理這些數據。現在我們開始著手創建它們吧。
頂點著色器
#version 330 core
layout (location = 0) in vec3 aPos;void main()
{gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
layout (location = 0),將緩沖區中的屬性名和屬性綁定在一起,當我們的頂點有多個屬性(位置,法向量,紋理坐標等等)的時候這個是必須要指明的。我們必須要讓編譯器知道緩沖區中頂點的哪個屬性要和shader中聲明的哪個屬性進行映射匹配,
使用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
編譯著色器
我們首先要做的是創建一個著色器對象,注意還是用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。
片段著色器
在計算機圖形中顏色被表示為有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)中。
我們可以調用glUseProgram函數,用剛創建的程序對象作為它的參數,以激活這個程序對象:
glUseProgram(shaderProgram);
在glUseProgram函數調用之后,每個著色器調用和渲染調用都會使用這個程序對象(也就是之前寫的著色器)了。
對了,在把著色器對象鏈接到程序對象以后,記得刪除著色器對象,我們不再需要它們了:
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
現在,我們已經把輸入頂點數據發送給了GPU,并指示了GPU如何在頂點和片段著色器中處理它。就快要完成了,但還沒結束,OpenGL還不知道它該如何解釋內存中的頂點數據,以及它該如何將頂點數據鏈接到頂點著色器的屬性上。我們需要告訴OpenGL怎么做。
鏈接頂點屬性
頂點著色器允許我們指定任何以頂點屬性為形式的輸入。這使其具有很強的靈活性的同時,它還的確意味著我們必須手動指定輸入數據的哪一個部分對應頂點著色器的哪一個頂點屬性。所以,我們必須在渲染前指定OpenGL該如何解釋頂點數據。
我們的頂點緩沖數據會被解析為下面這樣子:
- 位置數據被儲存為32位(4字節)浮點值。
- 每個位置包含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之間。我們把它設置為GL_FALSE。
- 第五個參數叫做步長(Stride),它告訴我們在連續的頂點屬性組之間的間隔。由于下個組位置數據在3個
float之后,我們把步長設置為3 * sizeof(float)。要注意的是由于我們知道這個數組是緊密排列的(在兩個頂點屬性之間沒有空隙)我們也可以設置為0來讓OpenGL決定具體步長是多少(只有當數值是緊密排列時才可用)。一旦我們有更多的頂點屬性,我們就必須更小心地定義每個頂點屬性之間的間隔,我們在后面會看到更多的例子(譯注: 這個參數的意思簡單說就是從這個屬性第二次出現的地方到整個數組0位置之間有多少字節)。 - 最后一個參數的類型是
void*,所以需要我們進行這個奇怪的強制類型轉換。它表示位置數據在緩沖中起始位置的偏移量(Offset)。由于位置數據在數組的開頭,所以這里是0。我們會在后面詳細解釋這個參數。 -
每個頂點屬性從一個VBO管理的內存中獲得它的數據,而具體是從哪個VBO(程序中可以有多個VBO)獲取則是通過在調用glVetexAttribPointer時綁定到GL_ARRAY_BUFFER的VBO決定的。由于在調用glVetexAttribPointer之前綁定的是先前定義的VBO對象,頂點屬性
0現在會鏈接到它的頂點數據。現在我們已經定義了OpenGL該如何解釋頂點數據,我們現在應該使用glEnableVertexAttribArray,以頂點屬性位置值作為參數,啟用頂點屬性;頂點屬性默認是禁用的。自此,所有東西都已經設置好了:我們使用一個頂點緩沖對象將頂點數據初始化至緩沖中,建立了一個頂點和一個片段著色器,并告訴了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(float), (void*)0); glEnableVertexAttribArray(0); // 2. 當我們渲染一個物體時要使用著色器程序 glUseProgram(shaderProgram); // 3. 繪制物體 someOpenGLFunctionThatDrawsOurTriangle();每當我們繪制一個物體的時候都必須重復這一過程。這看起來可能不多,但是如果有超過5個頂點屬性,上百個不同物體呢(這其實并不罕見)。綁定正確的緩沖對象,為每個物體配置所有頂點屬性很快就變成一件麻煩事。有沒有一些方法可以使我們把所有這些狀態配置儲存在一個對象中,并且可以通過綁定這個對象來恢復狀態呢?
-
頂點數組對象
頂點數組對象(Vertex Array Object,?VAO)可以像頂點緩沖對象那樣被綁定,任何隨后的頂點屬性調用都會儲存在這個VAO中。這樣的好處就是,當配置頂點屬性指針時,你只需要將那些調用執行一次,之后再繪制物體的時候只需要綁定相應的VAO就行了。這使在不同頂點數據和屬性配置之間切換變得非常簡單,只需要綁定不同的VAO就行了。剛剛設置的所有狀態都將存儲在VAO中
OpenGL的核心模式要求我們使用VAO,所以它知道該如何處理我們的頂點輸入。如果我們綁定VAO失敗,OpenGL會拒絕繪制任何東西。
一個頂點數組對象會儲存以下這些內容:
- glEnableVertexAttribArray和glDisableVertexAttribArray的調用。
- 通過glVertexAttribPointer設置的頂點屬性配置。
- 通過glVertexAttribPointer調用與頂點屬性關聯的頂點緩沖對象。
-
// ..:: 初始化代碼(只運行一次 (除非你的物體頻繁改變)) :: .. // 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(float), (void*)0); glEnableVertexAttribArray(0);[...]// ..:: 繪制代碼(渲染循環中) :: .. // 4. 繪制物體 glUseProgram(shaderProgram); glBindVertexArray(VAO); someOpenGLFunctionThatDrawsOurTriangle();就這么多了!前面做的一切都是等待這一刻,一個儲存了我們頂點屬性配置和應使用的VBO的頂點數組對象。一般當你打算繪制多個物體時,你首先要生成/配置所有的VAO(和必須的VBO及屬性指針),然后儲存它們供后面使用。當我們打算繪制物體的時候就拿出相應的VAO,綁定它,繪制完物體后,再解綁VAO。
創建一個VAO和創建一個VBO很類似:
unsigned int 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(float), (void*)0); glEnableVertexAttribArray(0);[...]// ..:: 繪制代碼(渲染循環中) :: .. // 4. 繪制物體 glUseProgram(shaderProgram); glBindVertexArray(VAO); someOpenGLFunctionThatDrawsOurTriangle();一個儲存了我們頂點屬性配置和應使用的VBO的頂點數組對象。一般當你打算繪制多個物體時,你首先要生成/配置所有的VAO(和必須的VBO及屬性指針),然后儲存它們供后面使用。當我們打算繪制物體的時候就拿出相應的VAO,綁定它,繪制完物體后,再刪除VBO VAO。
放代碼:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>// 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";// 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);
}int main()
{// glfw: initialize and configureglfwInit();glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);// glfw window creationGLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);glfwMakeContextCurrent(window);glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);// glad: load all OpenGL function pointersif (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;return -1;}// build and compile our shader program// vertex shaderint vertexShader = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);glCompileShader(vertexShader);// fragment shaderint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);glCompileShader(fragmentShader);// check for shader compile errors// link shadersint shaderProgram = glCreateProgram();glAttachShader(shaderProgram, vertexShader);glAttachShader(shaderProgram, fragmentShader);glLinkProgram(shaderProgram);glDeleteShader(vertexShader);glDeleteShader(fragmentShader);// set up vertex data (and buffer(s)) and configure vertex attributesfloat vertices[] = {-0.5f, -0.5f, 0.0f, // left 0.5f, -0.5f, 0.0f, // right 0.0f, 0.5f, 0.0f // top };unsigned int VBO, VAO;glGenVertexArrays(1, &VAO);glGenBuffers(1, &VBO);// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).glBindVertexArray(VAO);glEnableVertexAttribArray(0);glBindBuffer(GL_ARRAY_BUFFER, VBO);glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);//解綁VBO VAO// 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 unbindglBindBuffer(GL_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 loopwhile (!glfwWindowShouldClose(window)){// inputprocessInput(window);// renderglClearColor(0.2f, 0.3f, 0.3f, 1.0f);glClear(GL_COLOR_BUFFER_BIT);// draw our first triangleglUseProgram(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 organizedglDrawArrays(GL_TRIANGLES, 0, 3);// 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);// glfw: terminate, clearing all previously allocated GLFW resources.glfwTerminate();return 0;
}
索引緩沖對象
在渲染頂點這一話題上我們還有最有一個需要討論的東西——索引緩沖對象(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個頂點就能繪制矩形了,之后只要指定繪制的順序就行了。如果OpenGL提供這個功能就好了,對吧?
很幸運,索引緩沖對象的工作方式正是這樣的。和頂點緩沖對象一樣,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,否則它就沒有這個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>// 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";// 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);
}int main()
{// glfw: initialize and configureglfwInit();glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);glfwMakeContextCurrent(window);glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);// glad: load all OpenGL function pointersif (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;return -1;}// build and compile our shader program// vertex shaderint vertexShader = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);glCompileShader(vertexShader);// fragment shaderint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);glCompileShader(fragmentShader);// link shadersint shaderProgram = glCreateProgram();glAttachShader(shaderProgram, vertexShader);glAttachShader(shaderProgram, fragmentShader);glLinkProgram(shaderProgram);glDeleteShader(vertexShader);glDeleteShader(fragmentShader);// set up vertex data (and buffer(s)) and configure vertex attributesfloat vertices[] = {0.5f, 0.5f, 0.0f, // top right0.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 Triangle1, 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);glBindBuffer(GL_ARRAY_BUFFER, VBO);glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);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 unbindglBindBuffer(GL_ARRAY_BUFFER, 0);glBindVertexArray(0);// render loopwhile (!glfwWindowShouldClose(window)){// inputprocessInput(window);// renderglClearColor(0.2f, 0.3f, 0.3f, 1.0f);glClear(GL_COLOR_BUFFER_BIT);// draw our first triangleglUseProgram(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 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;
}
線框模式(Wireframe Mode)
要想用線框模式繪制你的三角形,你可以通過glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)函數配置OpenGL如何繪制圖元。第一個參數表示我們打算將其應用到所有的三角形的正面和背面,第二個參數告訴我們用線來繪制。之后的繪制調用會一直以線框模式繪制三角形,直到我們用glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)將其設置回默認模式。
?
總結
以上是生活随笔為你收集整理的Learn OpenGL (二):三角形的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Learn OpenGL (一):打开窗
- 下一篇: Learn OpenGL (三):着色器