OpenGL詳解


OpenGL學習視頻下載https://pan.baidu.com/s/1muWuuuo1_89AijQRNOcJmg
提取碼:xcwn

原文出處:http://blog.csdn.net/candycat1992/article/details/39676669

支持大家去看原文,真是一片好的博文。關於OpenGL寫點很詳細也很透徹,

 

OpenGL能做的事情太多了!很多程序也看起來很復雜。很多人感覺OpenGL晦澀難懂,原因大多是被OpenGL里面各種語句搞得頭大,一會gen一下,一會bind一下,一會又active一下。搞到最后都不知道自己在干嘛,更有可能因為某一步的順序錯誤導致最后渲染出錯,又或者覺得記下這些操作的順序是非常煩人的一件事。那么,OpenGL為什么會長成這個樣子呢?這篇文章旨在通過一個最簡單的OpenGL程序開始,讓我們能夠“看懂”它,“記住”這些操作順序。

 

我們先來解釋一下OpenGL為什么會涉及這么多操作順序。這是因為,和我們現在使用的C++、C#這種面向對象的語言不同,OpenGL中的大多數函數使用了一種基於狀態的方法,大多數OpenGL對象都需要在使用前把該對象綁定到context上。這里有兩個新名詞——OpenGL對象和Context。

 

Context

Context是一個非常抽象的概念,我們姑且把它理解成一個包含了所有OpenGL狀態的對象。如果我們把一個Context銷毀了,那么OpenGL也不復存在。

 

OpenGL對象

我們可以把OpenGL對象理解成一個狀態的集合,它負責管理它下屬的所有狀態。當然,除了狀態,OpenGL對象還會存儲其他數據。注意。這些狀態和上述context中的狀態並不重合,只有在把一個OpenGL對象綁定到context上時,OpenGL對象的各種狀態才會映射到context的狀態。因此,這時如果我們改變了context的狀態,那么也會影響這個對象,而相反地,依賴這些context狀態的函數也會使用存儲在這個對象上的數據。

 

因此,OpenGL對象的綁定既可能是為了修改該對象的狀態(大多數對象需要綁定到context上才可以改變它的狀態),也可能是為了讓context渲染時使用它的狀態。

 

畫了一個圖,僅供理解。圖中灰色的方塊代表各種狀態,箭頭表示當把一個OpenGL對象綁定到context上后,對應狀態的映射。

 

前面提到過,OpenGL就是一個“狀態機”。那些各種各樣的API調用會改變這些狀態,或者根據這些狀態進行操作。但我們要注意的是,這只是說明了OpenGL是怎樣被定義的,但硬件是否是按狀態機實現的就是另一回事了。不過,這不是我們需要擔心的地方。

 

OpenGL對象包含了下面一些類型:Buffer Objects,Vertex Array Objects,Textures,Framebuffer Objects等等。我們下面會講到Vertex Array Objects這個對象。

 

這些對象都有三個相關的重要函數:

void glGen*(GLsizei n​, GLuint *objects​);  

負責生成一個對象的name。而name就是這個對象的引用。

void glDelete*(GLsizei n​, const GLuint *objects​);  


負責銷毀一個對象。

void glBind*(GLenum target​, GLuint object​);  


將對象綁定到context上。

 

關於OpenGL對象還有很多內容,這里就不講了。可以參見官方wiki
 

 

 

在開始第一個程序之前,我們還要了解一些圖形名詞。

 

  • 渲染(Rendering):計算機從模型到創建一張圖像的過程。OpenGL僅僅是其中一個渲染系統。它是一個基於光柵化的系統,其他的系統還有光線追蹤(但有時也會用到OpenGL)等。
     
  • 模型(Models)或者對象(Objects):這里兩者的含義是一樣的。指從幾何圖元——點、線、三角形中創建的東西,由頂點指定。
     
  • Shaders:這是一類特殊的函數,是在圖形硬件上執行的。我們可以理解成,Shader是一些為圖形處理單元(GPU)編譯的小程序。OpenGL包含了編譯工具來把我們編寫的Shader源代碼編譯成可以在GPU上運行的代碼。在OpenGL中,我們可以使用四種shader階段。最常見的就是vertex shaders——它們可以處理頂點數據;以及fragment shaders,它們處理光柵化后生成的fragments。vertex shaders和fragment shaders是每個OpenGL程序必不可少的部分。
     
  • 像素(pixel):像素是我們顯示器上的最小可見元素。我們系統中的像素被存儲在一個幀緩存(framebuffer)中。幀緩存是一塊由圖形硬件管理的內存空間,用於供給給我們的顯示設備。

 

 

驚鴻一瞥

 

 

我們的第一個程序(不完整)的運行結果如下:

 

 

 

代碼如下(提示:這里可以粗略地看下中文注釋,后面會更詳細講述的):

  1.  
    ///  
  2.  
    //  
  3.  
    // triangles.cpp  
  4.  
    //  
  5.  
    ///  
  6.  
      
  7.  
      
  8.  
    //--------------------------------------------------------------------  
  9.  
    //  
  10.  
    // 在程序一開頭,我們包含了所需的頭文件,  
  11.  
    // 聲明了一些全局變量(但通常是不用全局變量在做的,這里只是為了說明一些基本問題)  
  12.  
    // 以及其他一些有用的程序結構  
  13.  
    //  
  14.  
      
  15.  
    #include <iostream>  
  16.  
    using namespace std;  
  17.  
      
  18.  
    #include "vgl.h"  
  19.  
    #include "LoadShaders.h"  
  20.  
      
  21.  
    enum VAO_IDs { Triangles, NumVAOs };  
  22.  
    enum Buffer_IDs { ArrayBuffer, NumBuffers };  
  23.  
    enum Attrib_IDs { vPosition = 0 };  
  24.  
      
  25.  
    GLuint  VAOs[NumVAOs];  
  26.  
    GLuint  Buffers[NumBuffers];  
  27.  
      
  28.  
    const GLuint NumVertices = 6;  
  29.  
      
  30.  
    //---------------------------------------------------------------------  
  31.  
    //  
  32.  
    // init  
  33.  
    //  
  34.  
    // init()函數用於設置我們后面會用到的一些數據.例如頂點信息,紋理等  
  35.  
    //  
  36.  
      
  37.  
    void init(void) {  
  38.  
        glGenVertexArrays(NumVAOs, VAOs);  
  39.  
        glBindVertexArray(VAOs[Triangles]);  
  40.  
      
  41.  
         // 我們首先指定了要渲染的兩個三角形的位置信息.  
  42.  
        GLfloat  vertices[NumVertices][ 2] = {  
  43.  
            {  -0.90, -0.90 },  // Triangle 1  
  44.  
            {   0.85, -0.90 },  
  45.  
            {  -0.90,  0.85 },  
  46.  
            {   0.90, -0.85 },  // Triangle 2  
  47.  
            {   0.90,  0.90 },  
  48.  
            {  -0.85,  0.90 }  
  49.  
        };  
  50.  
      
  51.  
        glGenBuffers(NumBuffers, Buffers);  
  52.  
        glBindBuffer(GL_ARRAY_BUFFER, Buffers[ArrayBuffer]);  
  53.  
        glBufferData(GL_ARRAY_BUFFER,  sizeof(vertices),  
  54.  
                         vertices, GL_STATIC_DRAW);  
  55.  
      
  56.  
         // 然后使用了必需的vertex和fragment shaders  
  57.  
        ShaderInfo  shaders[] = {  
  58.  
                { GL_VERTEX_SHADER,  "triangles.vert" },  
  59.  
                { GL_FRAGMENT_SHADER,  "triangles.frag" },  
  60.  
                { GL_NONE, NULL }  
  61.  
        };  
  62.  
      
  63.  
         // LoadShaders()是我們自定義(這里沒有給出)的一個函數,  
  64.  
         // 用於簡化為GPU准備shaders的過程,后面會詳細講述  
  65.  
        GLuint program = LoadShaders(shaders);  
  66.  
        glUseProgram(program);  
  67.  
         // 最后這部分我們成為shader plumbing,  
  68.  
         // 我們把需要的數據和shader程序中的變量關聯在一起,后面會詳細講述  
  69.  
        glVertexAttribPointer(vPosition,  2, GL_FLOAT,  
  70.  
                              GL_FALSE,  0, BUFFER_OFFSET(0));  
  71.  
        glEnableVertexAttribArray(vPosition);  
  72.  
    }  
  73.  
      
  74.  
    //---------------------------------------------------------------------  
  75.  
    //  
  76.  
    // display  
  77.  
    //  
  78.  
    // 這個函數是真正進行渲染的地方.它調用OpenGL的函數來請求數據進行渲染.  
  79.  
    // 幾乎所有的display函數都會進行下面的三個步驟.  
  80.  
    //  
  81.  
      
  82.  
    void display(void) {  
  83.  
         // 1. 調用glClear()清空窗口  
  84.  
        glClear(GL_COLOR_BUFFER_BIT);  
  85.  
      
  86.  
         // 2. 發起OpenGL調用來請求渲染你的對象  
  87.  
        glBindVertexArray(VAOs[Triangles]);  
  88.  
        glDrawArrays(GL_TRIANGLES,  0, NumVertices);  
  89.  
      
  90.  
         // 3. 請求將圖像繪制到窗口  
  91.  
        glFlush();  
  92.  
    }  
  93.  
      
  94.  
    //---------------------------------------------------------------------  
  95.  
    //  
  96.  
    // main  
  97.  
    //  
  98.  
    // main()函數用於創建窗口,調用init()函數,最后進入到事件循環(event loop).  
  99.  
    // 這里仍會看到一些以gl開頭的函數,但和上面的有所不同.  
  100.  
    // 這些函數來自第三方庫,以便我們可以在不同的系統中更方便地使用OpenGL.  
  101.  
    // 這里我們使用的是GLUT和GLEW.  
  102.  
    //  
  103.  
      
  104.  
    int main(int argc, char** argv) {  
  105.  
        glutInit(&argc, argv);  
  106.  
        glutInitDisplayMode(GLUT_RGBA);  
  107.  
        glutInitWindowSize( 512, 512);  
  108.  
        glutInitContextVersion( 4, 3);  
  109.  
        glutInitContextProfile(GLUT_CORE_PROFILE);  
  110.  
        glutCreateWindow(argv[ 0]);  
  111.  
      
  112.  
         if (glewInit()) {  
  113.  
            cerr <<  "Unable to initialize GLEW ... exiting" << endl; exit(EXIT_FAILURE);  
  114.  
        }  
  115.  
        init();  
  116.  
      
  117.  
        glutDisplayFunc(display);  
  118.  
      
  119.  
        glutMainLoop();  
  120.  
    }  

Vertex Shader如下:

  1.  
    #version 430 core  
  2.  
    layout(location =  0) in vec4 vPosition;  
  3.  
    void  
  4.  
    main()  
  5.  
     {  
  6.  
         gl_Position = vPosition;  
  7.  
    }  

Fragment Shader如下:

  1.  
    #version 430 core  
  2.  
    out vec4 fColor;  
  3.  
    void  
  4.  
    main()  
  5.  
    {  
  6.  
    fColor = vec4( 0.0, 0.0, 1.0, 1.0);  
  7.  
    }  

 

OpenGL的語法

 

這里插播一段語法解釋。從上面可以看出,OpenGL里面的函數長得都有一個特點,都是由“gl”開頭的,然后緊跟一個或多個大寫字母(例如,glBindVertexArray())。而且可以告訴,所有的OpenGL函數都長這樣。在上面的程序里面還有一些函數是“glut”開頭的,這是來自OpenGL實用工具(OpenGL Utility Toolkit)——GLUT。這是一個非常流行的跨平台工具,可以用於打開窗口、管理輸入等操作。龍書用的GLUT版本是Freeglut,是原始GLUT的一個變種。GLUT已經不再更新了。。。Sad。。。同樣,還有一個函數,glewInit(),它來自GLEW庫。GLUT和GLEW就是龍書所用的兩個庫了。

 

和OpenGL函數的命名規范類似,在display()函數里見到的GL_COLOR_BUFFER_BIT這樣的常量,也是OpenGL定義的。它們由GL_開頭,實用下划線來分割字符。它們的定義就是通過OpenGL頭文件(glcorearb.h和glewt.h)里面的#define指令定義的。

 

OpenGL為了跨平台還自己定義了一系列數據類型,如GLfloat。而且,因為OpenGL是一個“C”語言庫,它不使用函數重載來解決不同類型的數據問題,而是使用函數命名規范來組織不同的函數。例如,后面我們會碰到一個函數叫glUniform*(),這個函數有很多形式,例如,glUniform2f()和glUniform3fv。這些函數名字后面的后綴——2f和3fv,提供了函數的參數信息。例如,2f中的2表示有兩個數據將會傳遞給函數,f表示這兩個參數的類型是GLfloat。而3fv中最后的v,則是vector的簡寫,表明這三個GLfloat將以vector的形式傳遞給函數,而不是三個獨立的參數。

 

一些例子中沒有使用OpenGL定義的數據類型,直接使用了float這樣的變量。這可能會造成在不同平台上不兼容的問題。

 

 

 

在三維的世界里,所有的故事都是從頂點開始的。雖然題目是“詳解第一個程序”,但目的是為了讓大家理解最基礎的頂點是怎么一步步傳遞到GLSL中的。

 

 

重點內容開始!

 

 

傳遞頂點數據:你會怎么做

 

 

那么,現在的問題是,如果是你,你會怎么把頂點和它相關的信息,例如紋理坐標、法線等,傳遞給GLSL呢?一般人都會想到多維數組。我們下面把它稱為頂點流(Vertex Stream)。(什么?!你不是這么想的?!沒關系,OpenGL是這么想的就好。。。)

 

我們負責創建這個頂點流,然后只需要告訴OpenGL怎樣解讀它就可以了。

 

為了渲染一個對象,我們必須使用一個shader program。而這個program會定義一系列頂點屬性,例如上述Vertex Shader中的vPosition一行。這些屬性決定了我們需要傳遞哪些頂點數據。每一個屬性對應了一個數組,並且這些數據的維度都必須相等,即是一一對應的關系。

 

比如我們想要渲染3個頂點,我們會定義下面的數據:

{ {1, 1, 1}, {0, 0, 0}, {0, 0, 1} }  

這些頂點的順序是非常重要的,OpenGL將會根據這些順序渲染網格。我們可以直接使用上述這種數據來直接渲染,也可以使用索引(indices)來指定順序,這樣可以重復使用同一個頂點。

 

例如,我們使用下面的索引列表:

{2, 1, 0, 2, 1, 2}  
那么OpenGL將會渲染6個頂點:

{ {0, 0, 1}, {0, 0, 0}, {1, 1, 1}, {0, 0, 1}, {0, 0, 0}, {0, 0, 1} }  

 

現在,我們還想傳遞一個新的頂點屬性,即每個頂點的紋理坐標,那么新的紋理數組可能長這樣:

[plain] view plain copy print?在CODE上查看代碼片派生到我的代碼片

 
 

 

  1. { {0, 0}, {0.5, 0}, {0, 1} }  


注意,紋理數據的維度大小一定要和上面的坐標數組大小一致,而其他頂點屬性數組的維度也要滿足這個條件。這是非常容易理解的。

 

那么,合並后的頂點屬性列表就是:

[{0, 0, 1}, {0, 1}], [{0, 0, 0}, {0.5, 0}], [{1, 1, 1}, {0, 0}], [{0, 0, 1}, {0, 1}], [{0, 0, 0}, {0.5, 0}], [{0, 0, 1}, {0, 1}] }  

 

 

 

OpenGL的做法:VAO和VBO

 

 

OpenGL使用了VAO來實現上述管理頂點數據的數據作用,以及VBO來存放真正的頂點屬性數據。

 

VAO(Vertex Array Object)

 

 

我們這里遇到了第一種OpenGL對象——VAO(Vertex Array Object)。前面說到OpenGL對象是狀態的集合,那么VAO就是所有頂點數據的狀態集合。它存儲了頂點數據的格式以及頂點數據數據所需的緩存對象的引用。前面提過,OpenGL對象都有三個非常重要的函數,而VAO對應的就是glGenVertexArrays​、glDeleteVertexArraysglBindVertexArray​。

 

VAO負責管理頂點屬性,而這些頂點屬性從0到GL_MAX_VERTEX_ATTRIBS​ - 1被編號。這些屬性在Vertex Shader里的表現就是類似下面的語句:

layout(location = 0) in vec4 vPosition;  

 

上述頂點屬性vPosition被編號為0。

 

每個屬性可以被enable或者disable,被disable的屬性是不會傳遞給shader的,即便在shader里定義了這些屬性,它們讀出的值也會是一個常量,而非真正的數據。一個新建的VAO的所有屬性訪問都是disable的。而開啟一個屬性是通過下面的函數:

void glEnableVertexAttribArray​(GLuint index​);  


與其對應的是glDisableVertexAttribArray​ 函數。

 

而為了使用上述函數來改變VAO的狀態,我們首先需要把VAO綁定到當前的context上。

 

 

 

VBO(Vertex Buffer Object)

 

 

VBO是一種Buffer Object,即它也是一個OpenGl對象。VBO是頂點數組數據真正所在的地方。

 

為了指定一個屬性數據的格式和來源,我們需要告訴OpenGL,編號為0的屬性使用哪個VBO,編號為1的屬性使用哪個VBO等等。為了實現它,我們可以這么做。

 

首先,我們要知道,任何VBO都需要先綁定到GL_ARRAY_BUFFER​才可以對它進行操作。綁定后,我們可以調用下面的函數之一:

  1.  
    void glVertexAttribPointer​( GLuint index​, GLint size​, GLenum type​,  
  2.  
       GLboolean normalized​, GLsizei stride​,  const void *offset​);  
  3.  
      void glVertexAttribIPointer​( GLuint index​, GLint size​, GLenum type​,  
  4.  
       GLsizei stride​,  const void *offset​ );  
  5.  
      void glVertexAttribLPointer​( GLuint index​, GLint size​, GLenum type​,  
  6.  
       GLsizei stride​,  const void *offset​ );  


它們的作用大同小異,就是告訴OpenGl,編號為index的屬性使用當前綁定在GL_ARRAY_BUFFER​的VBO。為了更好理解,我們舉例:

  1.  
    glBindBuffer(GL_ARRAY_BUFFER, buf1);  
  2.  
    glVertexAttribPointer( 0, 4, GL_FLOAT, GL_FALSE, 0, 0);  
  3.  
    glBindBuffer(GL_ARRAY_BUFFER,  0);  


上面第一行代碼將buf1綁定到了GL_ARRAY_BUFFER​上。第二行意味着,編號為0的屬性將使用buf1的數據,因為當前綁定到GL_ARRAY_BUFFER​上的是buf1。第三行將緩存對象0綁定到了GL_ARRAY_BUFFER​上,這不會對頂點屬性有任何影響,只有glVertexAttribPointer​函數可以影響它們!

 

這個過程就像一個中介人的作用,而中介人就是GL_ARRAY_BUFFER​。我們可以這么想,glBindBuffer​ 設置了一個全局變量,然后glVertexAttribPointer讀取了這個全局變量並把它存儲在VAO中,這個全局變量就是GL_ARRAY_BUFFER。當調用完glVertexAttribPointer后,頂點屬性已經知道了數據來源就是buf1,它們之間就會直接聯系,而不需要在通過GL_ARRAY_BUFFER。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM