現代3D圖形編程學習-你好,三角形(譯)

你好,三角形

傳統的入門教程在介紹編程語言的時候,一般從「Hello,World!」的程序開始。這樣的程序擁有最簡單的可以直接輸出「Hello, World!」的代碼。這是一種熟悉編譯系統以及代碼執行的很好的一種方式。ios

使用opengl來寫實際的文本的具備必定難度的。在第一章中,咱們採用將三角形繪製到屏幕上還取代文本的輸出。c++

本章例子中的代碼

#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <algorithm>
#include <sstream>
#include <GL/glew.h>
#include <GL/glut.h>
#include <GL/gl.h>

GLuint positionBufferObject;
GLuint theProgram;
const float vertexPositions[] = {
    0.75f, 0.75f, 0.0f, 1.0f,
    0.75f, -0.75f, 0.0f, 1.0f,
    -0.75f, -0.75f, 0.0f, 1.0f,
};

void InitializeVertexBuffer()
{
    glGenBuffers(1, &positionBufferObject);

    glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STATIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

GLuint CreateShader(GLenum eShaderType, const std::string &strShaderFilename)
{
    GLuint shader = glCreateShader(eShaderType);
    std::ifstream ifile(strShaderFilename.c_str());
    std::stringstream buffer;
    buffer << ifile.rdbuf();
    std::string contents(buffer.str());
    const char* pContents = contents.c_str();

    glShaderSource(shader, 1, &pContents, NULL);

    glCompileShader(shader);
    GLint status;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
    if (status == GL_FALSE)
    {
        GLint infoLogLength;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLogLength);
        GLchar *strInfoLog = new GLchar[infoLogLength+1];
        glGetShaderInfoLog(shader, infoLogLength, NULL, strInfoLog);
        fprintf(stderr, "Compile failure in %d shader: \n%s\n", eShaderType, strInfoLog);
        delete[] strInfoLog;
    }
    return shader;
}

GLuint CreateProgram(const std::vector<GLuint>& vecShader)
{
    GLuint program = glCreateProgram();
    for (size_t i=0; i<vecShader.size(); ++i)
    {
        glAttachShader(program, vecShader[i]);
    }

    glLinkProgram(program);

    GLint status;
    glGetProgramiv(program, GL_LINK_STATUS, &status);
    if (status == GL_FALSE)
    {
        GLint infoLogLength;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLength);
        printf("Log length: %d\n", infoLogLength);
        GLchar *strInfoLog = new GLchar[infoLogLength+1];
        glGetProgramInfoLog(program, infoLogLength, NULL, strInfoLog);
        fprintf(stderr, "Linker failure: %s\n", strInfoLog);
        delete[] strInfoLog;
    }

    for (size_t i=0; i<vecShader.size(); i++)
    {
        glDetachShader(program, vecShader[i]);
    }
    return program;
}


void InitializeProgram()
{
    std::vector<GLuint> shaderList;
    shaderList.push_back(CreateShader(GL_VERTEX_SHADER, "./triangle.vert"));
    shaderList.push_back(CreateShader(GL_FRAGMENT_SHADER, "./triangle.frag"));

    theProgram = CreateProgram(shaderList);
    std::for_each(shaderList.begin(), shaderList.end(), glDeleteShader);
}

void init()
{
    InitializeProgram();
    InitializeVertexBuffer();
}

void reshape(int w, int h)
{
    glViewport(0, 0, (GLsizei)w, (GLsizei)h);
}

void display()
{
    glClearColor(0.0f, 0.0f, 0.0f,0.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    glUseProgram(theProgram);
    glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0,4,GL_FLOAT,GL_FALSE,0,0);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    glDisableVertexAttribArray(0);
    glUseProgram(0);
    glutSwapBuffers();
}


int main(int argc, char** argv)
{
    glutInit(&argc, argv);

    int width = 500;
    int height = 500;

    glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH);
    glutInitWindowSize(width, height);
    glutInitWindowPosition(300,200);
    int window = glutCreateWindow(argv[0]);

    GLenum err = glewInit();
    if (err != GLEW_OK)
    {
        std::cout << "glewInit failed: " << glewGetErrorString(err) << std::endl;
    }

    init();
    glutDisplayFunc(display);
    glutReshapeFunc(reshape);
    /* glutKeyboardFunc(keyboard); */
    glutMainLoop();
    return 0;
}

display函數詳解

display函數從表面上來看是很是簡單的。可是,它的功能是很是複雜的,而且和init中的初始化過程有關係。在閱讀過程當中對於暫時不理解的地方不用糾結,略過繼續看便可。編程

// Example 1.1. The display Function
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);

glUseProgram(theProgram);

glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);

glDrawArrays(GL_TRIANGLES, 0, 3);

glDisableVertexAttribArray(0);
glUseProgram(0);

glutSwapBuffers();

前兩行用來清空屏幕。glClearColor是其中一個狀態設置函數,它用來設置清空屏幕的時候使用的顏色。此處將清空顏色設置成了黑色。glClear並無設置opengl的狀態,它用來觸發屏幕清空操做。其中GL_COLOR_BUFFER_BIT參數表示此次清空操做會影響到顏色緩存區,將顏色緩存區中的顏色設置成上述設定的黑色。數組

接下來的一行代碼是用來設置當前的紋理程序,這個紋理程序會被接下去的渲染命令用到。在後面的內容中咱們會詳細闡述紋理程序是怎麼工做的。緩存

再接下去的三行指令都使用來設置opengl上下文中的狀態的。這些命令設置了將被渲染的三角形的座標。他們告訴opengl三角形座標在內存中的位置。詳細內容後續會深刻介紹。編程語言

glDrawArrays從命名上能夠發現端倪,這是一個渲染相關的函數。根據當前設定的上下文中的狀態,來繪製圖形,參數中GL_TRIANGLES說明當前繪製的是三角形。函數

再接下去的兩行是簡單的清理工做,將渲染完後原先設置的一些參數清空。oop

最後一行,glutSwapBuffers,是FreeGLUT的命令,並非opengl的命令。咱們在程序中將opengl的幀緩存設定成了雙緩存。這意味着當前顯示圖像的緩存和當前用於渲染的緩存是兩個獨立的緩存。所以,咱們全部的渲染過程只有在渲染玩以後纔會呈現給用戶。也就是說,用戶不會看到渲染通常的圖像。glutSwapBuffers是用來將渲染獲得的圖像呈現給用戶。佈局

跟着數據走

基礎簡介的章節中,咱們介紹了opengl管線的功能。此次咱們將會依據代碼再次對管線的內容進行介紹。這裏將會更深次的理解到opengl渲染數據相關的細節。性能

頂點傳輸

光柵化管線的第一個步驟是將頂點轉換到裁剪空間。在opengl執行轉換以前,必須接受到頂點的列表。所以該管線更初始的步驟是將三角形的頂點數據傳輸給opengl。

下面是將要被傳輸的頂點:

const float vertexPositions[] = {
    0.75f, 0.75f, 0.0f, 1.0f,
    0.75f, -0.75f, 0.0f, 1.0f,
    -0.75f, -0.75f, 0.0f, 1.0f,
}

每一行都表示一個頂點的四維座標。這些頂點是四維的緣由是裁剪空間中的頂點是四維的。這些頂點已是裁剪座標系中的點了。如今咱們想要讓opengl根據這些點(三角形的三個定點)來渲染三角形。

儘管咱們已經有了數據,可是opengl並不能直接使用他們。opengl對於能夠讀取的內存區域是有必定限制的。你能夠本身給頂點分配內存空間,可是opengl並不能直接看到這些內存。所以,咱們須要作的第一步事情就是分配一個opengl可見的內存空間,而後將咱們的數據填充到裏面。這個建立的對象叫作「緩存對象(buffer object)」。

緩存的對象是一個線性的存儲空間,受opengl管理和分配。這個內存空間中的內容是受用戶控制的,可是用戶只有間接的訪問能力。能夠將這些緩存對象想象成GPU的內存。GPU能夠很快的從這個內存中讀取內容,所以將數據存儲到這裏面,是有性能優點的。

本章中的緩存對象是在初始話的時候建立的。以下:

void InitializeVertexBuffer()
{
    glGenBuffers(1, &positionBufferObject);

    glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STATIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

第一行用來建立緩存對象,並將句柄存儲到全局變量positionBufferObject中。這個對象被建立出來了,可是尚未被分配任何的內存空間。

glBindBuffer函數將新建立的緩存對象綁定到GL_ARRAY_BUFFER。正如在前面簡介中介紹的,opengl中的對象一般必須被綁定到上下文,這樣opengl在渲染的時候纔會用到這些對象。

glBufferData函數進行了兩個操做,內存分配和內容拷貝。它爲當前綁定到GL_ARRAY_BUFFER對象分配內存。經過sizeof(vertexPositions)獲取分配內存的大小,而後將vertexPosition對應大小的內存內容拷貝到glBufferData爲對象分配的內存中。簡單的來說,能夠理解爲咱們已經擁有了內存中存儲的特定大小的數組,而後須要在GPU中分配相應大小的內存空間,最後,將內存拷貝到GPU中的內存中。

glBufferData第四個參數的含義將會在後面的內容中給出解釋。

第二個綁定操做是起到清理做用。將上下文中GL_ARRAY_BUFFER綁定到0,也就是說先前綁定的對象將會被解綁。這個操做從嚴格意義上來說並非必須的。由於,後續的綁定操做,會對當前的對象解綁。可是,進行解綁操做仍是一個很好的習慣,除非你對渲染過程有個嚴格的控制。

經過上述的步驟,咱們實現了頂點數據到GPU內存的傳輸。可是,咱們僅僅分配了必定大小的內存,而後用二進制進行填充,opengl並不知道內存中數據的格式。所以,咱們須要告訴opengl怎麼處理這些存儲的數據。

在渲染的代碼裏咱們作了以下操做:

glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);

第一個函數咱們以前已經說明過了,就是讓opengl上下文使用buffer對象。第二個函數,glEnableVertexAttribArray將在下一節進行介紹,若是沒有這個函數那麼第三個函數就不重要了。

第三個函數是最爲關鍵的函數。glVertexAttribPointer這個函數裏面雖而後Pointer單詞,可是它並非用來處理指針的。而是用來處理緩存對象的。

當渲染髮生的時候,opengl會將緩存對象中存儲的數據取出來。接着,咱們須要告訴opengl的是,這些頂點的數據存儲的格式是怎麼樣的。也就是說,咱們須要告訴opengl怎麼去識別buffer中存儲的數據。

在上面的例子中,咱們的數據格式是這樣的:

  • 咱們的位置數據是以32位浮點型數據(c/c++中的float)
  • 頂點的每個位置都有4個數字組成
  • 每一個頂點數據之間沒有空餘空間。是在數組中緊密排列的。
  • 數組中的第一個值是緩存對象中的起點位置。

glVertexAttribPointer將上述四個信息都告訴了opengl。函數的第三個參數是數據中每一個值的基本類型,在這個例子中是GL_FLOAT,表示32位浮點型。第二個參數表示了有多少個基本類型的數據能夠組成一個小的單元,這裏是4個。第五個參數用來表示每組數值之間的間隔。在這個例子中,每組數值之間沒有間隔,所以這個值是0。第六個參數表示緩存對象的起始的字節偏移量,0表示在緩存對象的起端。

第四個參數會在接下去的章節中介紹。第一個參數會在下面一節中介紹。

這裏有一點看上去被忽略的事情是,數據來自於哪一個緩存對象。這一點是隱含的。glVertexAttribPointer老是綁定到以前綁定到GL_ARRAY_BUFFER緩存對象上。所以,函數中沒有說明緩存對象的句柄,直接使用了先前綁定的句柄。

這個函數在接下去的章節中會給出詳細介紹。

如今opengl知道從哪裏獲取頂點數據,因而可使用下面的函數進行渲染。

glDrawArrays(GL_TRIANGLES, 0, 3);

這個函數表面上看起來很是簡單,可是它幹了不少活。第二個和第三個參數分別表示從頂點數據中讀取的初始位置以及個數。0,3就表示被處理的頂點爲0,1,2。

第一個參數用來告訴opengl,從頂點中讀取的三個數值是用來繪製三角形的。

頂點處理和着色器

如今咱們已經告訴opengl頂點數據有哪些了,渲染管線的下一個步驟是頂點的處理。這是本章中介紹的兩個可編程階段中的一個,這涉及到着色器的使用。

着色器是運行在gpu上的一個程序。在渲染管線中存在幾個可能的着色器階段,而且沒一個都有本身的輸入和輸出。着色器的目的是對輸入進行必定的處理,得到想要的輸出。

每一個着色器都是在一系列的輸入下執行的。這裏須要強調的是任何階段的着色器之間是相互獨立的。在單獨執行着色器的時候不會出現串擾。着色器對於輸入和輸出是有嚴格的定義的。着色器在執行完成以後沒有對全部的輸出賦值的行爲在大多數狀況下是不合法的。

頂點着色器正如名稱所示,是對一系列頂點進行操做的。嚴格的來說,每個頂點着色器的調用都是針對一個頂點的。這些着色器必須輸出用戶定義的輸出和頂點在裁剪座標系下對應的位置。這個裁剪座標系的計算邏輯徹底由頂點着色器控制。

opengl中的着色器由opengl着色器語言編寫的(GLSL)。這個語言看上去很想C語言,可是和C仍是有很大區別的。相對於C語言而言,GLSL有不少的限制,如不能有遞歸的邏輯等。下面咱們給出了頂點着色器的示例:

#version 330

layout(location = 0) in vec4 position;
void main()
{
    gl_Position = position;
}

上面的代碼看上去是很是簡單的。第一行表示了這個着色器使用的GLSL的版本是3.30。全部的GLSL着色器都須要聲明版本號。

接下去的一行定義了頂點着色器的輸入。輸入變量的名字爲position,類型是vec4:一個四維的浮點型向量。這一句話也說明了變量的佈局位置是0,這個在下面會給出更多的解釋。

和C語言相似的是,着色器的執行也都是從main函數的調用開始的。這個着色器很是簡單,僅僅實現了將輸入position的值拷貝給gl_Position的變量。這個變量沒有在着色器中定義;由於它是一個在每一個着色器中都事先定義好了的標準變量。若是在GLSL代碼中發現一個變量是以gl_開始的,那麼這個變量必定是內置的變量。在本身對變量生命定義的時候,是不容許使用gl_做爲開頭的。

gl_Position的定義相似以下:

out vec4 gl_Position;

頂點着色器至少須要作的事情是產生頂點對應的裁剪座標系下的位置。gl_Position正是頂點在裁剪座標系中的位置。所以,此處輸入的頂點已是裁剪座標系下的點了,這個着色器就直接將輸入的值複製到輸出便可。

頂點屬性。着色器有輸入輸出。能夠將這想象成函數的入參和返回值。若是一個着色器是一個函數,那麼它被調用的時候會有一系列的入參,而且指望獲得一系列的輸出。

着色器的輸入和輸出都分別有本身的來源和去處。所以,入參position必須在一開始附上來自某個地方的數值。那麼這些數值從什麼地方來的呢?裝載這些數值的地方叫作頂點屬性。

你也許會發現「vertex attribute」這兩個詞好像在哪裏出現過。不錯,就在glEnableVertexAttribArrayglVertexAttribPointer兩個函數中帶有這兩個詞。

這說明了數據是怎麼在opengl管線中傳輸的。當渲染開始的時候,緩存對象中的頂點數據的讀取和設定由函數glVertexAttribPointer函數完成。這個函數描述了數據從什麼地方來的。經過glVertexAttribPointer函數將數據和頂點着色器中的變量名字聯繫在一塊兒的具體實現是比較複雜的。

頂點着色器中的每一個輸入參數都有一個對應的位置,叫作index屬性。正如上文中提到的,入參的定義以下:

layout(location = 0) in vec4 position;

佈局的位置(lcoation)就是變量position的index屬性。該屬性值必須大於等於0,它的取值範圍是和硬件相關的。

在代碼裏,當指向一個屬性的時候,都是經過指向index屬性實現的。函數glEnableVertexAttribArrayglDisableVertexAttribArrayglVertexAttribPointer的第一個參數都是index屬性。在着色器代碼裏,咱們將position變量的index屬性設置爲0。所以在調用glEnableVertexAttribArray(0)函數的時候,就是使得position的index屬性有效。

下面給除了數據如何傳輸到頂點着色器的示意圖:

沒有調用glEnableVertexAttribArray,僅僅調用glVertexAttribPointer到那個index是沒有意義的。enable函數並不必定要放在glVertexAttribPointer函數調用以前,可是在渲染命令調用前,enable函數必需要被調用。若是這個屬性沒被enable,那麼渲染的時候將不會用到這個屬性。

光柵化

到目前已經完成了將3個頂點傳遞給opengl,並經過頂點着色器將其轉換成了裁剪座標系中的3個位置。接下來,頂點的位置經過將xyz三個份量,分別除以w份量獲得標準化設備座標系中點。在咱們的例子中,3個頂點的w份量是1,所以獲得的位置已是標準化設備座標系中的位置了。

在這以後,頂點位置還須要轉換成窗口座標系中。這個轉換過程被成爲視圖變換。在opengl中對應的函數爲glviewport。本章中,窗口大小的變化都會觸發該函數的執行。所以本章中reshape函數的實現爲:

void reshape(int w, int h)
{
    glViewport(0, 0, (GLsizei)w, (GLsizei)h);
}

這個告訴opengl窗口的哪一個區域是能夠用於渲染的。在這個例子中,咱們將整個窗口的可視區域做爲渲染區域。若是不調用這個函數,窗口大小的變化就不會對渲染產生影響。同時須要注意的是,上述的代碼並無讓渲染保持原來的比例。窗口的收縮和延展會一樣影響到三角形的收縮和延展。

glviewport函數調用時,對應的原點位於窗口的左下方。一旦位於窗口座標系中,opengl能夠將三角形的三個頂點經過掃描轉換的過程,將三角形轉換成一系列的片斷。爲了完成這樣的操做,opengl必須肯定這些點表述的是什麼。

opengl能夠將一系列的點以不一樣的方式表述。讓opengl將點理解成三角形的命令是

glDrawArrays(GL_TRIANGLES, 0, 3);

枚舉GL_TRIANGLES告訴opengl一系列輸入點中的每三個點用來構建一個三角形。因爲咱們僅僅傳入了3個點,咱們只獲得了一個三角形。

Figure 1.2. Data Flow to Rasterizer

若是咱們傳入的是6個點,那麼咱們就可以獲得2個三角形。

片斷處理

片斷着色器是用來計算片斷的輸出顏色。片斷着色器的輸入包括片斷在窗口空間中的xyz座標位置。輸入中也能夠包含一些用戶定義的數據。

這裏咱們使用的片斷着色器相似以下:

Example 1.5. Fragment Shader

#version 330

out vec4 outputColor;
void main()
{
    outputColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);
}

第一行與頂點着色器相同,都是用來表示着色器使用的GLSL語言的版本。第二行代碼表示的是片斷着色器的輸出。輸出參數的類型是vec4。

main函數中說明了這個片斷着色器的輸出是一個份量都爲1的四維向量。這樣建立獲得的是一個白色的三角形。第四個維度表示的是透明度的意思。因爲全部的片斷着色器輸出的點都是對應到窗口座標系中的,所以這裏不須要在對其進行賦值等操做。

Note

在頂點着色器一節中,咱們使用了layout (location=#)的表達式的目的是用來提供頂點着色器輸入和頂點index屬性值之間的關聯。這個爲了將頂點數組和頂點着色器中的輸入關聯起來。所以,你接下來會對片斷着色器的輸出和屏幕中的點之間的關聯產生必定的疑惑。
opengl可以本身進行處理。由於,對於片斷着色器輸出的位置是惟一肯定的,如當前圖像要被渲染到屏幕。正由於這樣,若是你定義一個片斷着色器的輸出,那麼這個輸出值會自動的輸出到目標位置的圖像中。對於不一樣的片斷着色器的結果輸出到不一樣的圖像上是可能的,這樣會增長一些複雜度,他的實現也會和輸入的處理方式相似。但這裏不會對此進行更詳細的說明。

生成着色器

這裏咱們將會說明,着色器的相關代碼如何傳遞給opengl,並讓它識別的。

着色器語言是用類c語言編寫的。所以,opengl對着色器語言的處理方式也相似與對c語言的處理方式。在c語言中,單獨的c文件被編譯成目標文件,而後,一個或多個目標文件經過連接的方式,生成單個項目,或是動態,靜態庫。opengl的處理方式很是相似。

一個着色器語言首先被編譯成着色器目標對象,而後一個或多個着色器目標對象經過連接的方式生成項目對象。

opengl項目對象包含了用於渲染操做的全部着色器。在本章的示例中,咱們有頂點着色器和片斷着色器;他們二者經過連接的方式共同組成了項目對象。具體實現的方式相似以下:

void InitializeProgram()
{
    std::vector<GLuint> shaderList;

    shaderList.push_back(CreateShader(GL_VERTEX_SHADER, strVertexShader));
    shaderList.push_back(CreateShader(GL_FRAGMENT_SHADER, strFragmengShader));

    theProgram = CreateProgram(shaderList);

    std::for_each(shaderList.begin(), shaderList.end(), glDeleteShader);
}

第一個語句建立的對象用於存儲將要被連接的着色器目標。接下去的兩條語句將兩條着色器代碼進行了編譯。最後經過CreateProgram將兩個着色器目標進行連接,生成項目對象。CreateShader代碼以下:

GLuint CreateShader(GLenum eShaderType, const std::string& strShader)
{
    GLuint shader = glCreateShader(eShaderType);
    const char *pStrShader = strShader.c_str();
    glShaderSource(shader, 1, &pStrShader, NULL);

    glCompileShader(shader);

    GLint status;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
    if (status == GL_FALSE)
    {
        GLint infoLogLength;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLogLength);

        GLchar *strInfoLog = new GLchar[infoLogLength+1];
        glGetShaderInfoLog(shader, infoLogLength, NULL, strInfoLog);
        
        const char *strShaderType = NULL;
        switch(eShaderType)
        {
        case GL_VERTEX_SHADER: strShaderType = "vertex"; break;
        case GL_GEOMETRY_SHADER: strShaderType = "geometry"; break;
        case GL_FRAGMENT_SHADER: strShaderType = "fragment"; break;

        }
        fprintf(stderr, "Compile failure in %s shader:\n%s\n", strShaderType, strInfoLog);
        delete[] strInfoLog;
    }
    return shader;
}

首先告訴opengl,須要建立什麼類型的對象,由glCreateShader完成。這個函數能夠建立特定類型的紋理對象,如頂點或是片斷紋理,所以該函數的入參是紋理對象的類型。因爲不一樣版本的着色器語言會有不一樣的格式規則,以及對應的預先定義的變量和常量,所以在着色器語言的字符串中首先必需要有說明對應的版本。這樣編譯器纔會知道採用怎樣的方式對其進行編譯。

Note

着色器和程序對象都是opengl中的對象。可是,他們的建立方式等和其餘的opengl對象有顯著的區別。好比,對於緩存對象,它的建立是經過相似與「glGenBuffer」的形式建立的。除此以外還有其餘不少的不一樣之處。

上述代碼中的下一個階段是經過glShaderSource將等待編譯的着色器語句導入到建立的着色器對象中。該函數的第一個參數是着色器對象,第二個參數是有多少個字符串會被載入到着色器對象。將多個字符串載入到一個紋理對象中,相似於c語言中,將頭文件合併到c文件中。第三個參數是將要被載入的字符串數組,最後一個參數是字符串長度的數組,這裏傳入NULL,表示咱們傳入的字符串都是以null結尾的。一般狀況下,最後一個參數傳入NULL就能夠了,除非你須要在字符串中使用null字符,才須要傳入字符串的長度。

一旦這些字符串在對象中時,他們就能夠經過glCompileShader對着色器進行編譯。

當編譯之後,須要查看編譯是否成功。這裏,咱們使用了glGetShaderiv來獲取GL_COMPILE_STATUS。若是返回的是GL_FALSE,那麼編譯就是失敗了;不然編譯就是成功了。

當編譯失敗的時候,咱們會將錯誤打印出來。它將消息輸出到stderr,解釋編譯失敗的緣由。這個錯誤就類比於c編譯器報的錯誤。

當建立的着色器對象都編譯成功後,咱們將其傳入到CreateProgram函數中。

GLuint CreateProgram(const std::vector<GLuint> &shaderList)
{
    GLuint program = glCreateProgram();

    for (size_t iLoop=0; iLoop<shaderList.size(); ++iLoop)
        glAttachShader(program, shaderList[iLoop]);

    glLinkProgram(program);

    GLint status;
    glGetProgramiv(program, GL_LINK_STATUS, &status);
    if (status == GL_FALSE)
    {
        GLint infoLogLength;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLength);
        
        GLchar *strInfoLog = new GLchar[infoLogLength+1];
        glGetProgramInfoLog(program, infoLogLength, NULL, strInfoLog);
        fprintf(stderr, "Linker failure: %s\n", strInfoLog);
        delete[] strInfoLog;
    }

    for (size_t iLoop=0; iLoop<shaderList.size(); ++iLoop)
        glDetachShader(program, shaderList[iLoop]);

    return program;
}

這個函數是至關簡單的。首先用glCreateProgram建立空的項目對象。這個函數沒有入參。項目對象是全部着色器集合而成的。其次,將全部的建立的着色器經過glAttachShader對象掛載到項目對象中。在掛載的時候,並不須要指出各個着色器對象的類型。

當全部的着色器對象都掛載到項目對象後,經過glLinkProgram將代碼進行連接。與先前的處理方式相似。咱們必須經過調用glGetProgramiv獲取對象連接是否成功。若是連接失敗,則輸出錯誤日誌,不然返回建立了的項目對象。

Note

在上述着色器對象中,頂點着色器中的輸入position的屬性位置是着色器自動賦值的。其它的屬性須要經過layout(location=#)的方式賦值。若是,你忘了對屬性的index進行賦值,opengl會自動進行賦值。所以,你可能並不知道一個屬性的index。若是你須要查詢屬性的位置,你能夠經過glGetAttribLocation獲取。

當整個項目成功連接以後,你能夠經過glDeatchShader將掛載的着色器對象從項目對喜好那個中刪除。項目對象的狀態和功能並不會收到影響。這樣作的做用是告訴opengl這些着色器對象對於項目對象而言沒有做用了。

在項目對象連接以後,還須要將其餘的着色器對象完全刪除,使用的函數是glDeleteShader

項目對象的使用。在opengl開始渲染的時候,須要告訴opengl執行哪一個項目對象,這是須要調用glUseProgram函數。在本章的例子中,咱們在display函數中調用了兩次該函數。第一次傳入的參數是theProgram,第二次入參是0,告訴opengl沒有項目對象用於渲染了。

清理工做

本章的例子中建立了不少opengl資源。它分配了緩存對象(佔用顯存資源)。建立了兩個着色器對象,和一個項目對象,這些對象都是存儲在opengl的內存中。可是,咱們到目前爲止僅僅刪除了着色器對象。

在這個例子中,並不須要進行額外的清理工做。由於當opengl工做的窗口被關閉的時候,這些資源會被opengl釋放。

可是,一般意義上而言,在關掉opengl以前,將這些對象進行釋放,是一個比較好的習慣。若是,你將對象放在c++對象中,那麼在析夠函數中能夠將這些資源進行釋放。

回顧

  • 緩存對象是opengl分配的現行內存空間,用來存儲頂點數值
  • glsl語言編寫的着色器被編譯成着色器對象,而後經過連接產生項目對象,項目對象將在渲染過程當中執行。
  • glDrawArrays函數用來繪製三角形,使用當前綁定到buffer對象中的點進行渲染。

opengl函數

glClearColor,glClear:設置清空屏幕的顏色,當glClear調用GL_COLOR_BUFFER_BIT的時候,使用設定的顏色進行清屏。

glGenBuffers,glBindBuffer,glBufferData:這些函數用來建立和操做緩存對象。glGenBuffers建立一個或多個緩存對象,glBindBuffer將建立的對象綁定到上下文特定的位置,glBufferData用來分配內存,並將特定的數據存入到分配的內存中。

glEnableVertexAttribArray,glDisableVertexAttribArray,glVertexAttribPointer:這些函數用來控制頂點屬性數組。glEnableVertexAttribArray用來激活屬性index,glDisableVertexAttribArray用來disable激活的屬性index,glVertexAttribPointer用來定義頂點數據格式,和具體位置,告訴opengl如何解析緩存對想中的定點數據。

glDrawArrays:用來執行渲染,使用當前激活的頂點屬性,以及當前的項目對象。這個函數致使一系列的定點按照必定的順序進行繪製。

glViewPort:這個函數定義了當前的視口變換。定義了窗口的顯示區域。

glCreateShader, glShaderSource, glCompileShader, glDeleteShader:這些函數是用來建立紋理項目對象。glCreateShader用來建立特定類型的空的紋理對象。glShaderSource將特定的紋理語言字符串設定到特定紋理對象;屢次調用該函數會將原先設定的紋理內容覆蓋。glCompileShader用來對剛設定的紋理對象進行編譯。glDeleteShader將原先的紋理對象刪除。

glCreateProgram, glAttachShader, glLinkProgram, glDetachShader:這些函數用來處理項目對象。glCreateProgram建立一個空的項目對象。glAttachShader用來將紋理對象綁定到項目對象上。屢次調用該函數能夠將多個紋理對象綁定到項目對象。glLinkProgram將全部綁定的紋理對象進行連接操做。glDetachShader用來將項目對象中綁定的紋理對象刪除。

glUseProgram:這個函數輸入的項目對象設爲當前使用的項目。全部發生的渲染操做都是對當前設定的項目所綁定的紋理對象進行操做。若是這個函數的入參設爲0,就表示當前沒有項目被綁定。

glGetAttribLocation:這個函數使用來獲取特定名字的屬性的index。

相關文章
相關標籤/搜索