在OpenGL中全部的事物都是在3D空間中,可是咱們所看到的屏幕成像倒是2D的像素數組。這致使OpenGL的大部分工做就是把獲得的3D座標轉換爲適應屏幕的2D圖像。轉換的整個處理過程是由OpenGL的圖形渲染管線管理的。ios
OpenGL圖形渲染管線:數組
1> 指的是一堆原始圖形數據途經一個輸送管道,期間通過各類變化處理最終出如今屏幕的過程
2> 圖形渲染管線能夠被劃分爲兩個主要部分:第一部分把你的3D座標轉換爲2D座標,第二部分是把2D座標轉變爲實際的有顏色的像素。
3> 圖形渲染管線能夠劃分爲幾個階段,每一個階段都是把上個階段的輸出當作輸入來處理的。因爲每隔階段的處理都是高度專門化的,因此很容易並行執行,因此在GPU中可用成千上萬的小處理核心能夠爲每一個管線階段的着色器並行處理。
4> OpenGL的渲染管線大體可分爲如下幾個階段:
定點數據輸入——> >圖元(片元)裝配——>幾何着色器——>光柵化——>片斷着色器——>Alpha測試與顏色混合
5> 有些着色器語序能夠經過開發者本身實現,從而達到更細緻的控制圖像的渲染結果和速度。其中頂點着色器,幾何着色器,片斷着色器是咱們能夠從新實現的部分。函數
簡略歸納:
頂點數據輸入: 首先將頂點數據以數組的形式將n個3D座標做爲整個圖形渲染管線的輸入。頂點數據是一系列頂點的集合。一個頂點是一個3D座標的數據的集合。在OpenGL中頂點數據是用頂點屬性表示的。他能夠包含任何可能呢會用到的數據。性能
頂點着色器: 接受一個單獨的頂點進行輸入,主要工做是將3D座標變爲另外一種3D座標,他接受開發者本身實現。測試
圖元(片元)裝配: ui
1> OpenGL須要去指定咱們想要繪製的圖像的座標和顏色構成,是一系列的點,或是一系列的三角形,仍是一系列的線。這些最基礎的構成被稱爲圖元(片元)。 // TODO eg:
2> 圖元裝配將頂點着色器的全部輸出做爲輸入,(注意,若是圖元類型爲GL_PIONTS,則只接受一個頂點)。而後將全部的點裝配成指定好的圖元形狀。spa
幾何着色器: 接受圖元形式的一系列頂點做爲輸入,經過產生新頂點構造出新的圖元生成其餘圖元。指針
光柵化: 將圖元映射爲最終屏幕上相應的像素,生成供片斷着色器使用的片斷,並進行裁切,將超出你的視圖之外的全部像素丟棄,用來提高執行效率。code
片斷着色器: 對象
1> 這裏的片斷是指OpenGL渲染一個像素所需的全部數據。
2> 片斷着色器將計算一個像素的最終顏色,這也是全部OpenGL高級效果產生的地方。一般,片斷着色器包含3D場景的數據(如光照、陰影、光的顏色等),這些數據能夠被用來計算該片斷最終像素的顏色。
Alpha測試與顏色混合: 該階段會檢測對應片斷對應的深度值,用來判斷這個片斷是否存在於其餘的片斷以前或者以後,從而判斷該片斷是否要被丟棄。同時也會檢查Alpha(透明度)值並對片斷進行顏色混合
詳細過程及代碼實現:
定義頂點數據
float vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f };
(OpenGL僅當3D座標在3個軸(x、y和z)上都爲-1.0到1.0的範圍內時才處理它。)
將定義的定點數據做爲輸入發送給頂點着色器,頂點着色器在GPU上建立內存用於儲存頂點數據,還要配置OpenGL如何解釋這些內存,而且指定其如何發送給顯卡,頂點着色器接着會處理咱們在內存中指定好數量的頂點。
這個時候若是一個頂點一個頂點的發送,其缺點是顯而易見的,將頂點數據從CPU發送至GPU的速度相對較慢。使用頂點緩衝對象來管理這些頂點的內存則能夠一次性發送儘量多的數據。當頂點數據發送至顯存中後,頂點着色器幾乎能當即訪問頂點。
建立一個頂點緩衝對象的過程以下:
首先生成一個緩衝區對象
// generate buffer object names void glGenBuffers(GLsizei n, GLuint * buffers);
第一個參數是要生成的緩衝對象的數量,第二個是要輸入用來存儲緩衝對象名稱的長度爲n的id數組,若是 n == 1, 則只須要一個id。
該函數僅僅生成一個緩衝對象的名稱,這個緩衝對象並不具有任何意義,即它僅僅是個緩衝對象,還不是頂點緩衝對象。他就像C中的一個指針變量,能夠給他分配內存對象而且用它的名稱來引用這個內存對象。
unsigned int VBO; glGenBuffers(1, &VBO);
頂點緩衝對象的緩衝區類型(緩衝區ID)爲 GL_ARRAY_BUFFER,
要指定緩衝對象的類型則須要用到下面的函數
// bind a named buffer object void glBindBuffer(GLenum target, GLuint buffer);
一個參數是緩衝對象的類型,第二個參數是已經建立的緩衝對象的名稱。使用該函數將建立好的VBO對象綁定到OpenGL的上下文環境中,若是綁定的buffer值爲零,那麼OpenGL將再也不對當前target使用任何對象。
須要注意的是函數的第二個參數雖然是Gluint型的。
OpenGL容許咱們同一同時綁定多個不一樣類型的緩衝對象,可是不能綁定兩個相同的緩衝對象,這就是上下文之因此用上下文來理解它的意義。
緩衝對象只是OpenGL對象的一種,使用其餘對象的思路跟緩衝對象是差很少的,都是建立對象——>綁定上下文——>設置數據
// 建立並初始化緩衝區對象的數據存儲 void glBufferData (GLenum target, GLsizeiptr size, const GLvoid * data, GLenum usage);
該函數將刪除以前存在於指定類型緩衝區的數據,爲當前已綁定的指定類型的緩衝區對象建立一個新的數據存儲,第一個參數是指定目標緩衝區類型,有GL_ARRAY_BUFFER(VBO)、GL_ELEMENT_ARRAY_BUFFER(EBO)兩種,第二個參數用來指定緩衝區對象的新數據須要的內存大小,第三個參數是咱們但願發送的實際數據的指針,第四個參數用來指定給GPU數據的預期使用方式,這使得GL的實現可以作出更明智的選擇,可能會影響緩衝區對象的性能,它不會限制數據存儲的實際使用。usage可分解爲兩個部分理解。第一,訪問的頻率,第二,訪問的性質:
訪問頻率:
STREAM // 數據存儲內容將被修改一次並最多使用幾回。
STATIC // 數據存儲內容將被修改一次並屢次使用。
DYNAMIC // 數據存儲內容將被重複修改並屢次使用。
訪問的性質
DRAW // 數據存儲內容由應用程序修改,並用做GL繪圖和圖像規範命令的源。
和在一塊兒則是如下三個
GL_STATIC_DRAW // 數據不會或幾乎不會改變。
GL_DYNAMIC_DRAW // 數據會被改變不少。
GL_STREAM_DRAW // 數據每次繪製時都會改變。
到此頂點數據已經被存在GPU內存中了。
頂點着色器:
頂點着色器就要使用着色器語言GLES,用字符串存儲,發送給GL,而後編譯這個着色器。eg:
#version 330 core layout (lcoation = 0) in vec3 aPos; void main() { //注意,若是圖元類型爲GL_PIONTS,則只接受一個頂點 sition = vec4(aPos.x, aPos.y, aPos.z, 1.0); }
每一個着色器都起始與一個版本聲明,同時說明咱們使用的渲染模式是核心模式。
in 關鍵字用來頂點着色器中聲明的全部頂點屬性,(上面的只有一個頂點位置屬性),layout (lcoation = 0) 表明輸入變量的位置值。
在主函數中設置着色器的雙輸出值,這裏將gl_Position頂點位置屬性進行更改。
而後是編譯着色器:
// 建立一個着色器對象 unsigned int vertexShader; vertexShader = glCreateShader(GL_VERTEX_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; }
片斷着色器:
片斷着色器的編寫,編譯和頂點着色器的過程大同小異,只是類型變成了GL_FRAGMENT_SHADER。
#version 330 core out vec4 FragColor; void main() { FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); } // 片斷着色器只須要一個輸出變量,這個變量是一個vec4類型,他表明的是最終的 輸出顏色,使用out關鍵字聲明輸出變量。 unsigned int fragmentShader; fragmentShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader);
兩個着色器寫完以後,將兩個着色器對象連接到一個用來渲染的着色器程序中。
着色器程序對象是多個着色器對象連接在一塊兒的成果。在渲染對象的時候激活須要的着色器程序。在發送渲染調用的時候GL會使用當前激活的着色器程序。
當連接着色器至一個程序的時候,它會把每一個着色器的輸出連接到下個着色器的輸入。當輸出和輸入不匹配的時候,你會獲得一個鏈接錯誤。
// 建立一個着色器程序對象
// 建立一個着色器程序對象 unsigned int shaderProgram; shaderProgram = glCreateProgram(); // 將着色器對象加入着色器程序對象並連接 glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); // 檢查連接成功: int successs; char infoLog[512]; glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success); if(!success) { glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog); } // 在連接成功後刪除以前建立的着色器對象,釋放內存 glDeleteShader(vertexShader); glDeleteShader(fragmentShader); // 在須要的時候,使用着色器 glUseProgram(shaderProgram);
在glUseProgram函數調用以後,每一個着色器調用和渲染調用都會使用這個着色器程序對象。
頂點數據已經發送給了GPU,也指示了GPU在頂點着色器和片斷着色器中如何處理它,可是GPU還不知道如何處理已經存在GPU內存中的的頂點數據。即如何將已經在GPU中的頂點是數據以頂點着色器能夠接受的方式發送給頂點着色器。頂點着色器能夠接受任何以頂點屬性形式的輸入,因此頂點着色器有着很強的靈活性,可是這樣咱們必須手動指定輸入數據的哪個部分對應頂點着色器須要的的哪個頂點屬性。因此在開始渲染以前,必選要告訴頂點着色器如何解釋頂點數據。
上文在傳入的時候,vertices是一個float類型的一維數組,即一段連續的浮點型內存,在GPU中頂點緩衝數據則會被解析爲如下特徵的連續內存:
1> 每一個數據會被存儲32位(4字節)的float類型值;
2> 每一個位置包含三個這樣的值;
3> 存儲的內存和傳入的時候同樣是連續的;
4> 數據的第一個值在緩衝開始的位置,即數組的第0個元素。
經過既定的這個信息,就可使用glVertexAttribPointer函數來告訴GL如何去解釋頂點緩衝對象。eg:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0);
// 參數含義
1> 第一個參數指定配置的頂點屬性,在編寫頂點着色器的時候,第二句中有個 layout(location = 0),該語句設置position頂點的屬性位置值(location)爲0,若是但願把頂點數據傳遞到這一個頂點屬性中,那麼這裏參數傳入0;
2> 第二個參數用來指定頂點屬性的大小,由於用到的頂點位置數據爲vec3,由三個值組成,大小爲3;
3> 第三個參數指定數據的類型;
4> 第四個參數指定是否但願數據被標準化,若是爲GL_TRUE,全部傳入的數據會被映射到0到1(有符號輸爲-1到1)之間。
5> 第五個參數能夠用「步長」來解釋,他用來告訴GL整個頂點屬性第二次出現的地方到整個數組0位置之間有多少字節,也能夠傳入0讓GL本身檢測,可是隻能在傳入連續內存(即數組)的狀況下使用。
6> 第六個參數用來表示位置數據在緩衝中起始位置的偏移量(Offset),但要注意,改參數的類型爲void*,因此要講類型強制轉換爲void*。
而 glEnableVertexAttribArray 則以頂點屬性位置值做爲參數,啓用頂點屬性,由於頂點屬性默認是禁用的。 整套流程下來大概會是這樣:
// 建立並綁定頂點緩衝對象 float vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f }; 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); // 渲染時,使用着色器程序 glUseProgram(shaderProgram); // 繪製物體 someOpenGLFunctionThatDrawsOurTriangle();
在繪製時,每繪製一個物體的時候都必須重複這一過程,這個時候綁定正確的緩衝對象,爲每次繪製配置正確的頂點屬性就變的比較麻煩,爲了解決這個麻煩,可使用頂點數組對象,將全部繪製須要的狀態信息儲存在一個對象中。
頂點數組對象能夠像頂點緩衝對象同樣被綁定,任何隨後的頂點屬性調用都會被儲存着這個VAO對象中,這樣就等於說是在配置頂點屬性指針時,只須要將以前的調用執行一次,在繪製不一樣的物體時,只須要綁定不一樣的VAO就能夠了。(OPGL的核心渲染模式指定使用VAO對象,若是綁定VAO對象失敗,OPGL會拒絕渲染)
一個頂點數組對象將會存儲如下內容:
1> glEnableVertexAttribArray和glDisableVertexAttribArray的調用。
2> 經過glVertexAttribPointer設置的頂點屬性配置。(也就是數據放在哪裏,和如何讀取數據)
3> 經過glVertexAttribPointer調用與頂點屬性關聯的頂點緩衝對象。
在內存中大概就是這個意思
VBO1 = {pos[0], pos[1], pos[2], ...} VBO2 = {pos2[0], pos2[1], pos2[2], ...} VAO1 = {*pos[0]} VBO3 = {pos[0], color[0], pos[1], color[1], pos[2], color[2], ...} VAO2 = {*pos[0], *color[0]}
建立一個VAO對象跟建立一個VBO對象類似:
unsigned int VAO; glGenVertexArrays(1, &VAO);
在使用glBindVertexArray綁定VAO以後,就能夠對VAO對象進行操做。在綁定以後,可把綁定和配置對應的VBO和屬性指針而後解綁,供以後使用。在繪製物體的時候只須要把VAO對象綁定到但願使用的設定上,新的使用VAO繪製的流程應該以下:
// 綁定VAO glBindVertexArray(VAO); // 將BO複製到GL緩衝區中供GL使用 glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 設置VBO對象 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 繪製 glUseProgram(shaderProgram); glBindVertexArray(VAO); someOpenGLFunctionThatDrawsOurTriangle(); // 實際繪製時(在循環中)使用的代碼應以下 glUseProgram(shaderProgram); glBindVertexArray(VAO); //綁定咱們須要的VAO,會致使上面全部VAO保存的設置自動設置完成 glDrawArrays(); // 最後記得解綁不會再使用的VAO對象 glBindVertexArray(0); //解綁VAO
索引緩衝對象(EBO):
若是是繪製一個矩形,可使用繪製兩個三角形的辦法繪製。這會生成下面兩個索引緩衝對象:
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 // 左上角 }
能夠看到,有兩對如出一轍的頂點,但實際上,一個矩形只有四個頂點這就會產生50%的額外開銷。若是模型越來愈大,這個問題會更加糟糕,會產生數不清的浪費。好的解決方案是存儲不一樣的頂點,並設定繪製這些頂點的順序。這就是索引緩衝對象的(EBO)工做方式和頂點緩衝對象同樣EBO是一個緩衝,他專門儲存索引,OpenGL調用這些頂點的索引來決定繪製哪一個點。
1> 首先,咱們要定義不懂位置的頂點,和繪製矩形所須要啊的索引:
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 // 第二個三角形 };
2> 建立索引緩衝對象:
ussigned int EBO; glGenBuffers(1, &EBO);
3> 綁定EBO而後用將索引複製到緩衝中
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
4> 用glDrawElements來替換glDrawArrays函數,來指明咱們從索引緩衝渲染。
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
第一個參數指定了咱們繪製的模式;
第二個參數是咱們打算繪製頂點的個數,這裏填6;
第三個參數是索引的類型;
最後一個參數裏咱們指定EBO中的偏移量。
若是整套流程下來(使用VAO、VBO、EBO)整個三角形的位置代碼會是這樣:
#include <iostream> #include <glad/glad.h> #include <GLFW/glfw3.h> #include "config.h" using namespace std; const char *strvertexShaderSource = "#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 *strfragmentShaderSource = "#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() { glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); GLFWwindow *window = glfwCreateWindow(WIND_WIGHT, WIND_HEIGHT, "window", NULL, NULL); if (window == NULL) { std::cout << "create window failed!" << endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "init glad failed!" << endl; return -1; } // 建立一個着色器,着色器類型爲 GL_VERTEX_SHADER(頂點着色器) unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); // 把着色器源碼附加到着色器對象 glShaderSource(vertexShader, 1, &strvertexShaderSource, NULL); // 編譯着色器 glCompileShader(vertexShader); int success; char infoLog[512]; // 判斷編譯結果並打印出錯誤 glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std:cout << "VERTEX SHADER COMPILATION_FAILED" << infoLog << std::endl; } // 建立一個着色器,着色器類型爲 GL_FRAGMENT_SHADER (片斷着色器) unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &strfragmentShaderSource, NULL); glCompileShader(fragmentShader); // 判斷編譯結果並打印出錯誤 glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog); std::cout << "FRAGMENT SHADER COMPLATION_FAILED" << infoLog << std::endl; } // 連接着色器對象 int shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); // 判斷連接結果並打印出錯誤 if (!success) { glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog); std::cout << "LINK SHADER FAILED" << infoLog << std::endl; } // 連接成功後就能夠將shader刪除 glDeleteShader(vertexShader); glDeleteShader(fragmentShader); // 將頂點數據傳入頂點着色器 // 定義一個頂點數組 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, 1, 3, 1, 2, 3 }; // 建立一個VAO對象,一個VBO對象,一個EBO對象 unsigned int VAO, VBO, EBO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); // 綁定VAO對象 glBindVertexArray(VAO); // 將VBO複製到GL緩衝區中供GL使用 glBindBuffer(GL_ARRAY_BUFFER, VBO); // 複製頂點數組到緩衝中供OpenGL使用 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 告訴GL如何去解釋頂點緩衝對象(VBO) glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); // 將EBO複製到GL緩衝區中供GL使用 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); // 將頂點索引複製到緩衝中 glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 告訴GL啓用頂點屬性 glEnableVertexAttribArray(0); // 解除上下文綁定VAO,這樣其餘VAO調用不會意外地修改此VAO(一般狀況下是沒必要要的) // glBindVertexArray(0); while (!glfwWindowShouldClose(window)) { glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // 使用連接好的shader glUseProgram(shaderProgram); // 綁定VAO對象(若是有操做解綁了VAO對象) // glBindVertexArray(VAO); // 指明從索引緩衝渲染 glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); glfwSwapBuffers(window); glfwPollEvents(); } // GC glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &EBO); glDeleteProgram(shaderProgram); glfwTerminate(); return 0; }