- 頂點數組對象:Vertex Array Object,VAO,用於存儲頂點狀態配置信息,每當界面刷新時,則通過VAO進行繪制.
- 頂點緩沖對象:Vertex Buffer Object,VBO,通過VBO將大量頂點存儲在GPU內存(通常被稱為顯存)中
1.渲染步驟
下面,你會看到一個圖形渲染管線的每個階段的抽象展示。要注意藍色部分代表的是我們可以注入自定義的着色器的部分。

- 注意:片段着色器也稱為片元着色器

頂點着色器(Vertex Shader)
頂點着色器主要的目的是把3D坐標轉為另一種3D坐標(后面會解釋),同時頂點着色器允許我們對頂點屬性進行一些基本處理。
圖元裝配(Primitive Assembly)
將頂點着色器輸出的所有頂點作為輸入(如果是GL_POINTS,那么就是一個頂點),並所有的點裝配成指定圖元的形狀;本節例子中是一個三角形。
幾何着色器和光柵化階段
幾何着色器的輸出會被傳入光柵化階段(Rasterization Stage),這里它會把圖元映射為最終屏幕上相應的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器運行之前會執行裁切(Clipping)。裁切會丟棄超出你的視圖以外的所有像素,用來提升執行效率。
片元着色器
主要目的是計算一個像素的最終顏色,這也是所有OpenGL高級效果產生的地方。通常,片段着色器包含3D場景的數據(比如光照、陰影、光的顏色等等),這些數據可以被用來計算最終像素的顏色。
測試和混合(Blending)階段
這個階段檢測片段的對應的深度(和模板(Stencil))值(后面會講),用它們來判斷這個像素是其它物體的前面還是后面,決定是否應該丟棄。這個階段也會檢查alpha值(alpha值定義了一個物體的透明度)並對物體進行混合(Blend)。所以,即使在片段着色器中計算出來了一個像素輸出的顏色,在渲染多個三角形的時候最后的像素顏色也可能完全不同。
在現代OpenGL中,我們必須定義至少一個頂點着色器和一個片段着色器(因為GPU中沒有默認的頂點/片段着色器)。出於這個原因,剛開始學習現代OpenGL的時候可能會非常困難,因為在你能夠渲染自己的第一個三角形之前已經需要了解一大堆知識了。在本節結束你最終渲染出你的三角形的時候,你也會了解到非常多的圖形編程知識。
而幾何着色器是可選的,通常使用它默認的着色器就行了。
2.通過代碼實現每步驟
2.1 頂點數據(Vertex Data)
我們希望渲染一個三角形,所以創建三個頂點,我們將它頂點的z坐標設置為0.0。從而使它看上去像是2D的。
float vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f };
然后我們通過glViewport函數進行視口變換(Viewport Transform),變換后的坐標x、y和z值在-1.0到1.0的一小段空間。任何落在范圍外的坐標都會被丟棄/裁剪,不會顯示在你的屏幕上。

glViewport函數聲明如下所示:
glViewport(GLint x, GLint y, GLsizei width, GLsizei height); //x y:定義視口的左下角開始位置 //width,height :定義這個視口矩形的寬度和高度
如果設置全屏,一般都是 glViewport(0, 0, width(), height()),比如設置為glViewport(100, 50, 100,100)時,那么對應300x200窗口,是這樣的:

2.2 通過VBO將頂點存儲到GPU內存中
接下來我們還要通過頂點緩沖對象(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); //把用戶定義的數據復制到當前綁定緩沖對象上 //參數1:目標緩沖的類型 //參數2:傳輸數據的大小(以字節為單位) //參數3:數據指針 //參數4:指定我們希望顯卡如何管理給定的數據
它有三種形式:
- GL_STATIC_DRAW :數據不會或幾乎不會改變(一次修改,多次使用)
- GL_DYNAMIC_DRAW:數據會頻繁修改(多次修改,多次使用)
- GL_STREAM_DRAW :數據每次繪制時都會改變(每幀都不同,一次修改,一次使用)
現在我們已經把頂點數據儲存在顯卡的內存中,用VBO這個頂點緩沖對象管理。下面我們會創建一個頂點和片段着色器來真正處理這些數據。
2.3 頂點着色器源碼
做的第一件事是用着色器語言GLSL(OpenGL Shading Language)編寫頂點着色器源碼,下面你會看到一個非常基礎的GLSL頂點着色器的源代碼:
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";
- #version 330 core : 定義版本號,需要OpenGL 3.3或者更高版本
- layout (location = 0) in vec3 aPos : 使用in關鍵字來聲明頂點屬性輸入,這里創建一個輸入變量aPos(3分量),通過layout (location = 0)設定了輸入變量的頂點屬性的位置值(Location)為0,后面將會通過glVertexAttribPointer()函數來設置它.
- gl_Position : 設置頂點着色器的輸出,這里gl_Position之所以為vec4類型,是因為3d圖形演算要用到 4x4的矩陣(4行4列),而矩陣乘法要求n行m列 和 m行p列才能相乘,所以是vec4而不是vec3,由於position 是位置所以應該是 (x,y,z,1.0f),如果是方向向量,則就是 (x,y,z,0.0f).
在真實的程序里輸入數據通常都不是標准化設備坐標,所以我們首先必須先把它們轉換至OpenGL的可視區域內。
2.4 編譯頂點着色器
我們已經寫了一個頂點着色器源碼,但為了能夠讓OpenGL使用它,我們必須在運行時動態編譯它的源碼。
我們首先要做的是創建一個頂點着色器對象,注意還是用ID來引用的。所以我們儲存這個頂點着色器為unsigned int,然后用glCreateShader創建這個着色器:
unsigned int vertexShader; vertexShader = glCreateShader(GL_VERTEX_SHADER); //創建一個着色器,參數GL_VERTEX_SHADER 或者GL_FRAGMENT_SHADER,由於我們創建的是頂點shader,所以填入GL_VERTEX_SHADER ,否則就是片元shader
下一步我們把這個着色器源碼附加到着色器對象上,然后編譯它:
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); //設置頂點源碼 glCompileShader(vertexShader);//編譯源碼 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; }
glGetShaderiv函數不僅僅用於檢測着色器編譯成功,還可以判斷類型,狀態,源碼等,函數定義如下所示:
void glGetShaderiv(GLuint shader,GLenum pname,GLint *params); //shader:要查詢的着色器對象 //pname : 查詢類型,可以設置的有GL_SHADER_TYPE、GL_DELETE_STATUS、GL_COMPILE_STATUS、GL_INFO_LOG_LENGTH. //GL_SHADER_TYPE: 用來判斷並返回着色器類型,若是頂點着色器則success=GL_VERTEX_SHADER,若是片元着色器success=GL_FRAGMENT_SHADER //GL_DELETE_STATUS: 判斷着色器是否被刪除,success=GL_TRUE,否則success=GL_FALSE, //GL_COMPILE_STATUS: 用於檢測編譯是否成功,success=GL_TRUE,否則success=GL_FALSE, //GL_INFO_LOG_LENGTH: 獲取着色器的信息日志的長度(information log length), 如果着色器沒有信息日志,則success=0。 //GL_SHADER_SOURCE_LENGTH: 獲取着色器源碼長度,不存在則success=0; //params:查詢的內容
2.5 片元着色器(也稱為片段着色器)
我們渲染三角形最后一步就是片元着色器,用來計算出每個像素的最終顏色。這里我們設置為輸出橘黃色。
在OpenGL或GLSL中,顏色每個分量的強度設置在0.0到1.0之間。比如說我們設置紅為1.0f,綠為1.0f,我們會得到兩個顏色的混合色,即黃色。
所以片元着色器源碼如下所示:
const char *fragmentShaderSource = "#version 330 core\n" "out vec4 FragColor;\n" "void main()\n" "{\n" " FragColor = vec4(1.0f, 1.0f, 0.0f, 1.0f);\n" "}\n\0";
- FragColor : 定義類型為out vec4 類型,表明是個要輸出的變量,該變量值為 vec4(1.0f, 1.0f, 0.0f, 1.0f),表示的是RGBA為(1,1,0,1),所以為黃色,而alpha值為1.0,表示完全不透明
2.6 編譯頂點着色器
編譯片段着色器的過程與頂點着色器類似,代碼如下所示:
unsigned int fragmentShader; fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); //創建一個片元着色器 glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);//設置片元源碼 glCompileShader(fragmentShader); //編譯
2.7 着色器Program對象
兩個着色器現在都編譯了,接下來就是把兩個着色器對象鏈接到一個用來渲染(調用頂點shader和片元shader數據)的着色器Program對象中。
創建一個Program對象,並鏈接shader
unsigned int shaderProgram; shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); //將附加vertexShader到的shaderProgram對象中 glAttachShader(shaderProgram, fragmentShader);//將附加fragmentShader到的shaderProgram對象中 glLinkProgram(shaderProgram); //將附加的shader鏈接到program對象中 glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success); ////檢測鏈接是否成功 if (!success) { glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog); std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl; } glDeleteShader(vertexShader); //鏈接后不再需要它們,需要刪除shader glDeleteShader(fragmentShader);
glGetProgramiv函數不僅僅用於檢測鏈接成功,還可以獲取着色器對象數量、激活的屬性變量數等,比如:
glGetProgramiv(shaderProgram, GL_ATTACHED_SHADERS, &cnt); //獲取着色器對象的數量
現在,我們已經把輸入頂點數據發送給了GPU,但是OpenGL還不知道它該如何解釋內存中的頂點數據(組件數量,數據類型,頂點個數),比如xyz坐標數據類型是GL_BYTE型,還是GL_SHORT型,還是GL_FLOAT型等
所以我們需要通過glVertexAttribPointer()設置頂點屬性,然后使能屬性,並激活shaderProgram
2.8 鏈接頂點屬性
我們的頂點緩沖數據會被解析為下面這樣子:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);//設置頂點屬性 glEnableVertexAttribArray(0);//使能頂點屬性(默認是禁止的) glUseProgram(shaderProgram); //激活Program對象 someOpenGLFunctionThatDrawsOurTriangle();// 繪制物體
其中glVertexAttribPointer()函數聲明如下所示:
void glVertexAttribPointer(GLuint index , GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *ptr); //指定渲染時索引值為 index 的頂點屬性數組的數據格式和位置。 //index:指定要修改的頂點位置的索引值,之前使用layout(location = 0)設置了頂點位置為0, //size:指定每個頂點屬性的組件數量。必須為1、2、3、4之一。(如我們這里頂點是由3個(x,y,z)組成,而顏色是4個(r,g,b,a)) //type:指定數組中每個組件的數據類型。可用的符號常量有GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT,GL_UNSIGNED_SHORT, GL_FIXED, 和 GL_FLOAT //normalized:是否希望數據被標准化。設置為GL_TRUE,所有數據都會被映射到0(對於有符號型signed數據是-1)到1之間。我們把它設置為GL_FALSEM,不需要。 //stride:步長,指定頂點之間的偏移量。我們這里是每3個float為一個頂點,所以偏移量為3 * sizeof(float) //ptr:可以指向需要綁定的VBO,如果已經綁定VBO,並且位置數據在緩沖中起始位置為0,那么此項為0,否則填入開頭的偏移量
當我們繪制好物體后,每當最大化,尺寸變化界面后,openGL就會進入刷新狀態,所以我們需要把所有這些狀態配置儲存在一個頂點數組對象(Vertex Array Object, VAO)中,每次刷新時,就可以通過VAO來恢復狀態.
2.9 頂點數組對象VAO實現
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //設置清除顏色(背景色)為rgba(0.2f, 0.3f, 0.3f, 1.0f) // 初始化代碼,初始化頂點shader、片元shader、program、vbo // ... .. // ... ... //1.初始化vao unsigned int VAO; glGenVertexArrays(1, &VAO); // 注冊VAO glBindVertexArray(VAO); // 綁定VAO //2. 把頂點數組復制到緩沖中供OpenGL使用 glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);//把用戶定義的頂點數據復制到vao上
//3. 設置頂點屬性指針 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0); glEnableVertexAttribArray(0);//使能頂點屬性(默認是禁止的) //4. 解綁VAO glBindVertexArray(0);
然后每次繪制物體時,只需要:
glClear(GL_COLOR_BUFFER_BIT); //開始清除,設置背景色 glUseProgram(shaderProgram); ////激活Program對象 glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); //繪制三角形 someOpenGLFunctionThatDrawsOurTriangle();// 繪制物體 glBindVertexArray(0); //繪制完成,便解綁,用來綁定下一個要繪制的物體
這里,我們一直再調用glBindVertexArray()綁定和解綁,是為了方便繪制有多個VAO的時候,避免出錯(如果只繪制一個VAO,也可以無需多次解綁).
其中glDrawArrays函數聲明如下所示:
glDrawArrays(GLenum mode, GLint first, GLsizei count); //函數根據頂點數組中的坐標數據和指定的模式,進行繪制。 //mode,繪制方式,如下圖所示,提供以下參數: //GL_POINTS(畫點)、GL_LINES(每兩個頂點為一條直線)、GL_LINE_LOOP(是個環狀)、 //GL_LINE_STRIP(第一個頂點和最后一個頂點不相連)、GL_TRIANGLES(每三個頂點組成一個三角形)、 //GL_TRIANGLE_STRIP(共用多個頂點的一個三角形)、GL_TRIANGLE_FAN(共用一個原點為中心的一個三角形)。 //first,從數組緩存中的哪一位開始繪制,一般為0。 //count,數組中頂點的數量。
如下圖所示:
2.10 最終代碼如下所示:
//hello_triangle.cpp #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: initialize and configure // ------------------------------ glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X #endif // glfw window creation // -------------------- 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 // ------------------------------------ // vertex shader 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; } // fragment shader 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; } // link shaders 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; } glDeleteShader(vertexShader); glDeleteShader(fragmentShader); // set up vertex data (and buffer(s)) and configure vertex attributes // ------------------------------------------------------------------ float 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); 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); 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); // 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, 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; } // 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); }
未完待續 ,下章學習: