OpenGL學習筆記《二》繪製簡單的圖形

  在開始繪圖以前,簡單的瞭解一下opengl的繪圖流程。在opengl裏面,全部的事物都是處於3D空間中,而咱們的屏幕及像素是以2D表現的,因此就須要將3D轉換爲2D。opengl內部管理這個流程的叫作渲染管線,主要分爲兩個部分:3D座標轉換爲2D座標,2D座標轉換爲帶顏色的像素數據。細分來看主要分爲6個流程/步驟,每一個流程相互獨立,以流程數據的輸入、輸出做爲數據傳遞方式。每一個流程/步驟,由一小段程序/代碼組成,因此又能夠稱之爲shader/着色器程序。而gpu對於這些片斷程序的執行效率很是高,而且因爲gpu的核心數多,能夠併發執行的量也很是大。git

  這六個步驟以下所示:github

 

   上圖中藍色標識的步驟,就是通常咱們作shader編程的步驟。不過目前咱們主要是作第一步(Vertex Shader頂點着色器)和第五步(Fragment Shader片斷着色器)這兩個步驟的編程。頂點着色器處理輸入的頂點數據,轉換好座標,片斷着色器處理進過光柵化後的像素數據,生成最終顯示的顏色信息。編程

  咱們要在屏幕上繪製出簡單的圖形,那麼就必需要提供頂點着色器和片斷着色器,才能最終顯示出樣式、顏色正常的圖形。數組

一、簡單的着色器程序併發

  首先是Vertex Shader,頂點着色器程序代碼函數

const char* vertexShaderSource = "#version 330 core\n"
        "layout (location = 0) in vec3 aPos;\n"
        "void main()\n{"
        " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
        "}\0";

  代碼中指定了opengl的版本,而後指定了 in vec3類型的輸入變量,這個是咱們輸入的頂點數據,注意到這裏咱們額外用到了layout(location = 0),這個在後面會提到。而後就是着色器程序的核心main函數了,在這裏咱們給opengl的內建變量gl_Position賦值,即咱們傳過來的頂點位置屬性值。oop

  其次是Fragment Shader,片斷着色器程序代碼ui

const char* fragmentShaderSource = "#version 330 core\n"
        "out vec4 FragColor;\n"
        "void main()\n{"
        " FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
        "}\0";

  一樣的須要指定opengl的版本,而後聲明一個out vec4類型的變量,這個表示是着色器的輸出數據,也就是片斷着色器處理好的像素顏色值的數據,沒有這個返回,咱們的繪圖出來的將會是空白或者黑的。spa

  而後咱們須要根據程序代碼編譯連接成着色器程序翻譯

   // build and compile shader // -------------------------------- // create vertex shader
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); // attach shader source
    glShaderSource(vertexShader, 1, &vSource, nullptr); // compile shader
 glCompileShader(vertexShader); // check error
    int success; char infoLog[512]; glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; } // create fragment shader
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fSource, nullptr); glCompileShader(fragmentShader); glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog); std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl; } // build and link shaderprogram // -------------------------------- // create shader program
    unsigned int shaderProgram = glCreateProgram(); // attach shader and link program
 glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); // check error
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success); if (!success) { glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog); std::cout << "ERROR::PROGRAM::ATTACH_LINK_FAILED\n" << infoLog << std::endl; } // delete shader 
 glDeleteShader(vertexShader); glDeleteShader(fragmentShader);

  建立和編譯着色器的流程都差很少:

  glCreateShader根據參數類型,返回咱們須要建立的着色器類型;

  glShaderSource將咱們提供的着色器代碼綁定到着色器上;

  glCompileShader編譯咱們的着色器;

  glGetShaderiv能夠檢查咱們的編譯結果;

   編譯好着色器以後,咱們須要建立着色器程序:

  glCreateProgram建立着色器程序,返回一個unsigned int類型的索引ID,在後面咱們須要用到;

  glAttachShader將咱們編譯好的着色器附加到着色器程序上;

  glLinkProgram連接好着色器程序;

  glGetProgramiv能夠檢查咱們的連接結果。

  最後,咱們須要刪除編譯的着色器,經過glDeleteShader方法。

  咱們最終須要用到的是glCreateProgram方法返回的着色器程序的索引ID。

二、提供頂點數據

  上面的流程圖中能夠看到,第一步就是對輸入的頂點數據進行處理,因此咱們須要提供頂點數據。頂點數據包含頂點的位置、顏色、紋理座標等多種類型的數據,在這裏咱們先簡單的提供位置數據,顏色的話直接在片斷着色器中固定一種顏色。

  咱們能夠一次提供一個頂點數據,可是這個方式效率過低。之前的opengl也有一個Display list 的概念,一次打包提供一組數據,這樣效率高,可是因爲數據是直接存儲在了GPU端,數據一旦提供了就沒法再調整了。後來又提供了VBO(vertex buffer object)的概念,也是一次收集/打包好一組數據,但相較於Display List,數據是收集在CPU端,每次渲染會再傳遞一次。因此這種方式相較於一次提供一個頂點數據,效率更高,可是相較於Display List方式,效率稍微低一點,不過靈活性提升了。

  因此咱們首先能夠聲明一個頂點數據數組,而後綁定到VBO對象上:

float vertices[] = { -1.0f, -0.5f, 0.0f, // left
        0.0f, -0.5f, 0.0f, // right
        -0.5f, 0.5f, 0.0f // top
 }; unsigned int VBO; glGenBuffers(1, &VBO); ... loop 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); 
...draw
...loop

  glBindBuffer 和 glBufferData 兩個方法,及將頂點數據綁定VBO及賦值VBO對象的方法。而後GPU須要知道如何使用這些數據,glVertexAttribPointer方法負責讓GPU知道如何使用這些數據。

  glVertexAttribPointer的參數1對應咱們在頂點着色器中提到的(layout = 0)的屬性,參數2表示這個屬性值用到的數據數量(咱們這裏用到的是位置屬性,每一個屬性由三個浮點數據構成),參數3表示數據類型(咱們這裏用到的是浮點型),參數4表示是否須要對數據類型進行轉換,參數5表示兩兩數據屬性間的間隔(由於咱們這裏每一個屬性值之間是緊挨着的,因此間隔就是一個屬性的數據量),參數6表示屬性數據的起始索引(在這裏咱們填0,在後面涉及到配置多個頂點屬性的時候,這個地方的值就會有變化);

  glEnableVertexAttribArray 方法的參數,指定激活咱們配置的哪一個屬性,在上面我提到了配置的是(layout = 0) 的屬性,因此這裏填0;

  在上面的代碼中咱們能夠看到,在渲染的循環中,咱們一直要調用綁定VBO、賦值VBO、設置數據使用方法等接口,有點複雜。這個時候就須要引入另一個概念vertex array object(VAO),來簡化咱們的操做。VAO概念是用來記錄咱們在綁定、賦值VBO對象,設置數據使用方法的,在引入VAO以後,咱們的渲染流程能夠調整爲:    unsigned int VBO, VAO;    glGenVertexArrays(1, &VAO);

 glGenBuffers(1, &VBO); // bind the vertex array object  glBindVertexArray(VAO); // bind the vertex buffer object  glBindBuffer(GL_ARRAY_BUFFER, VBO); // fill data uage:GL_STATIC_DRAW, GL_DYNAMIC_DRAW, GL_STREAM_DRAW glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // tell opengl how it should interpret the vertex data glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0);
   ...loop
   glUseProgram(shaderProgram);
   glBindVertexArray(VAO);
   ...draw
   ...loop
   glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);

  能夠看到,咱們的渲染流程變得簡單了,數據的初始化和配置,放到了渲染流程以外,在渲染的時候,只須要綁定VAO對象,就能夠直接執行繪圖操做了。固然在循環退出以後,咱們也要釋放掉VAO和VBO對象。

三、繪製第一個三角形

  在上面咱們提供了一個包含3個頂點位置的頂點數據,建立好了着色器程序,同時也引入了VAO概念,簡化了咱們的渲染流程。opengl提供了簡單的繪圖方法供咱們使用,在這裏咱們須要繪製的是三角形,咱們能夠用以下的代碼來繪圖:

  glDrawArrays(GL_TRIANGLES, 0, 3);

  咱們畫的是三角形,因此咱們第一個參數類型填的就是三角形;第二個參數指定頂點數據在數組中的起始位置,在這裏咱們填0;第三個參數表示要繪製的點數量,咱們這裏有三個頂點,咱們傳3就能夠了。編譯執行咱們的項目,能夠獲得一個簡單的三角形:

 

 四、繪製一個矩形

  在上面咱們繪製了一個三角形,如今咱們要繪製一個矩形,該如何操做?

  咱們知道一個矩形能夠經過畫兩個三角形實現,因此咱們能夠改一下咱們的頂點數據數組,提供兩個三角形的頂點數據

float vertices[] = { // first triangle
     0.5f,  0.5f, 0.0f,  // top right
     0.5f, -0.5f, 0.0f,  // bottom right
    -0.5f,  0.5f, 0.0f,  // top left // second triangle
     0.5f, -0.5f, 0.0f,  // bottom right
    -0.5f, -0.5f, 0.0f,  // bottom left
    -0.5f,  0.5f, 0.0f   // top left
};

  而後修改咱們的繪圖函數,將頂點數量改成6,咱們就能夠獲得一個矩形了。

  可是再看一下數組的數據,發現其中實際上是有重複的,好比第一個三角形的右下角和第二個三角形的右下角。若是要繪製的矩形數量較少影響不大,若是矩形數量多了,那麼咱們就會有好多重複的數據,將會佔用額外的內存,同時也形成項目數據複雜化了。因此此時咱們須要引入另一個概念element buffer object(EBO)對象,來簡化咱們的操做,在中文中這個對象也翻譯爲頂點索引對象。顧名思義,這個是對頂點進行索引編號,告訴GPU該如何使用咱們提供的頂點數據。

  

   // indice data
    unsigned int indices[] = {  // note that we start from 0!
        0, 1, 3,   // first triangle
        1, 2, 3    // second triangle
 };    unsigned int VAO, VBO, EBO; // gen
    glGenBuffers(1, &EBO); // bind VAO // bind VBO // bind EBO
 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // set pointer
 ... render loop ... render loop glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &EBO);

  簡化一下流程。咱們須要提供一份索引列表,對應的就是頂點數據中的頂點索引。而後綁定並賦值EBO對象,其他的操做跟以前相似,編譯執行,咱們就獲得了一個簡單的矩形。

 

  在這裏咱們須要注意的是,在最後刪除VAO、VBO、EBO的時候,必定不能在刪除VAO對象以前先刪除EBO,由於在VAO對象內部其實維護了一份EBO對象的數據,若是先刪除了EBO,會致使刪除出現異常。

  以上就是利用opoengl提供的接口,繪製的簡單圖形。

  對應的代碼在這裏

相關文章
相關標籤/搜索