1. 程式人生 > >LearnOpenGL學習筆記2:繪製基礎圖形

LearnOpenGL學習筆記2:繪製基礎圖形

一、 基礎

1. 渲染流程

3D座標轉為2D座標的處理過程是由OpenGL的圖形渲染管線管理的。

管線:Graphics Pipeline,大多譯為管線,實際上指的是一堆原始圖形資料途經一個輸送管道,期間經過各種變化處理最終出現在螢幕的過程。它可以被劃分為兩個主要部分:第一部分把你的3D座標轉換為2D座標,第二部分是把2D座標轉變為實際的有顏色的畫素。

畫素和座標:2D座標和畫素也是不同的,2D座標精確表示一個點在2D空間中的位置,而2D畫素是這個點的近似值,2D畫素受到你的螢幕/視窗解析度的限制。

圖形渲染管線接受一組3D座標,然後把它們轉變為你螢幕上的有色2D畫素輸出。圖形渲染管線可以被劃分為幾個階段,每個階段將會把前一個階段的輸出作為輸入。由於它們具有並行執行的特性,當今大多數顯示卡都有成千上萬的小處理核心,它們在GPU上為每一個(渲染管線)階段執行各自的小程式,從而在圖形渲染管線中快速處理你的資料。這些小程式叫做著色器(Shader)。有些著色器允許開發者自己配置,這就允許我們用自己寫的著色器來替換預設的,OpenGL著色器是用OpenGL著色器語言(OpenGL Shading Language, GLSL)寫成的。
下圖是一個圖形渲染管線的每個階段的抽象展示。要注意藍色部分代表的是我們可以注入自定義的著色器的部分。在現代OpenGL中,我們必須定義至少一個頂點著色器和一個片段著色器(因為GPU中沒有預設的頂點/片段著色器)。
這裡寫圖片描述


我們以陣列的形式傳遞3個3D座標作為圖形渲染管線的輸入,用來表示一個三角形,這個陣列叫做頂點資料(Vertex Data);頂點資料是一系列頂點的集合。一個頂點(Vertex)是一個3D座標的資料的集合。而頂點資料是用頂點屬性(Vertex Attribute)表示的,它可以包含任何我們想用的資料,但是簡單起見,我們假定每個頂點只由一個3D位置和一些顏色值組成的。

頂點著色器(Vertex Shader):它把一個單獨的頂點作為輸入。頂點著色器主要的目的是把3D座標轉為另一種3D座標(後面會解釋),同時頂點著色器允許我們對頂點屬性進行一些基本處理。
幾何著色器(Geometry Shader):幾何著色器把圖元形式的一系列頂點的集合作為輸入,它可以通過產生新頂點構造出新的(或是其它的)圖元來生成其他形狀。例子中,它生成了另一個三角形。
光柵化階段(Rasterization Stage):這裡它會把圖元對映為最終螢幕上相應的畫素,生成供片段著色器(Fragment Shader)使用的片段(Fragment)。在片段著色器執行之前會執行裁切(Clipping)。裁切會丟棄超出你的檢視以外的所有畫素,用來提升執行效率。
片段著色器:主要目的是計算一個畫素的最終顏色,這也是所有OpenGL高階效果產生的地方。通常,片段著色器包含3D場景的資料(比如光照、陰影、光的顏色等等),這些資料可以被用來計算最終畫素的顏色。
Alpha測試和混合(Blending):這個階段檢測片段的對應的深度(和模板(Stencil))值(後面會講),用它們來判斷這個畫素是其它物體的前面還是後面,決定是否應該丟棄。這個階段也會檢查alpha值(alpha值定義了一個物體的透明度)並對物體進行混合(Blend)。

2. 標準化裝置座標(Normalized Device Coordinates)

一旦你的頂點座標已經在頂點著色器中處理過,它們就應該是標準化裝置座標了,標準化裝置座標是一個x、y和z值在-1.0到1.0的一小段空間。任何落在範圍外的座標都會被丟棄/裁剪,不會顯示在你的螢幕上。下面你會看到我們定義的在標準化裝置座標中的三角形(忽略z軸):
這裡寫圖片描述
與通常的螢幕座標不同,y軸正方向為向上,(0, 0)座標是這個影象的中心,而不是左上角。最終你希望所有(變換過的)座標都在這個座標空間中,否則它們就不可見了。
你的標準化裝置座標接著會變換為螢幕空間座標(Screen-space Coordinates),這是使用你通過glViewport函式提供的資料,進行視口變換(Viewport Transform)完成的。所得的螢幕空間座標又會被變換為片段輸入到片段著色器中。

二、 著色器示例

1. 頂點著色器

#version 330 core
layout(location = 0) in vec3 position;
void main()
{
    gl_Position = vec4(position.x, position.y, position.z, 1.0);
}

每個著色器都起始於一個版本宣告。OpenGL 3.3以及和更高版本中,GLSL版本號和OpenGL的版本是匹配的(比如說GLSL 420版本對應於OpenGL 4.2)。
下一步,使用in關鍵字,在頂點著色器中宣告所有的輸入頂點屬性(Input Vertex Attribute)。現在我們只關心位置(Position)資料,所以我們只需要一個頂點屬性。GLSL有一個向量資料型別,它包含1到4個float分量,包含的數量可以從它的字尾數字看出來。由於每個頂點都有一個3D座標,我們就建立一個vec3輸入變數position。我們同樣也通過layout (location = 0)設定了輸入變數的位置值(Location)你後面會看到為什麼我們會需要這個位置值。
為了設定頂點著色器的輸出,我們必須把位置資料賦值給預定義的gl_Position變數,它在幕後是vec4型別的。在main函式的最後,我們將gl_Position設定的值會成為該頂點著色器的輸出。由於我們的輸入是一個3分量的向量,我們必須把它轉換為4分量的。我們可以把vec3的資料作為vec4構造器的引數,同時把w分量設定為1.0f(我們會在後面解釋為什麼)來完成這一任務。

2. 片段著色器

#version 330 core
out vec4 color;
void main()
{
    color = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}

片段著色器只需要一個輸出變數,這個變數是一個4分量向量,它表示的是最終的輸出顏色,我們應該自己將其計算出來。我們可以用out關鍵字宣告輸出變數,這裡我們命名為color。下面,我們將一個alpha值為1.0(1.0代表完全不透明)的橘黃色的vec4賦值給顏色輸出。

三、 繪製圖形

1. 頂點輸入

OpenGL是一個3D圖形庫,所以我們在OpenGL中指定的所有座標都是3D座標(x、y和z)。例如我們希望渲染一個三角形,我們一共要指定三個頂點,每個頂點都有一個3D位置,我們會將它們以標準化裝置座標的形式(OpenGL的可見區域)定義為一個GLfloat陣列:

GLfloat vertices[] = {
    -0.5f, -0.5f, 0.0f,
    0.5f, -0.5f, 0.0f,
    0.0f,  0.5f, 0.0f
};

我們會把它作為輸入傳送給圖形渲染管線的第一個處理階段:頂點著色器。它會在GPU上建立記憶體用於儲存我們的頂點資料,還要配置OpenGL如何解釋這些記憶體,並且指定其如何傳送給顯示卡。頂點著色器接著會處理我們在記憶體中指定數量的頂點。
我們通過頂點緩衝物件(Vertex Buffer Objects, VBO)管理這個記憶體,它會在GPU記憶體(通常被稱為視訊記憶體)中儲存大量頂點。使用這些緩衝物件的好處是我們可以一次性的傳送一大批資料到顯示卡上,而不是每個頂點發送一次。
就像OpenGL中的其它物件一樣,這個緩衝有一個獨一無二的ID,所以我們可以使用glGenBuffers函式和一個緩衝ID生成一個VBO物件:

    GLuint 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);
glBufferData是一個專門用來把使用者定義的資料複製到當前繫結緩衝的函式。它的第一個引數是目標緩衝的型別:頂點緩衝物件當前繫結到GL_ARRAY_BUFFER目標上。第二個引數指定傳輸資料的大小(以位元組為單位);用一個簡單的sizeof計算出頂點資料大小就行。第三個引數是我們希望傳送的實際資料。
第四個引數指定了我們希望顯示卡如何管理給定的資料。它有三種形式:

  • GL_STATIC_DRAW :資料不會或幾乎不會改變。
  • GL_DYNAMIC_DRAW:資料會被改變很多。
  • GL_STREAM_DRAW :資料每次繪製時都會改變。

2. 編譯著色器

我們首先要做的是建立一個著色器物件,注意還是用ID來引用的。所以我們儲存這個頂點著色器為GLuint,然後用glCreateShader建立這個著色器,我們把需要建立的著色器型別以引數形式提供給glCreateShader。

    GLuint vertexShader;
    vertexShader = glCreateShader(GL_VERTEX_SHADER);

下一步我們把這個著色器原始碼附加到著色器物件上,然後編譯它:

    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);

glShaderSource函式把要編譯的著色器物件作為第一個引數。第二引數指定了傳遞的原始碼字串數量,這裡只有一個。第三個引數是頂點著色器真正的原始碼,第四個引數我們先設定為NULL。
同樣的,我們編譯片段著色器:

    GLuint fragmentShader;
    fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, null);
    glCompileShader(fragmentShader);

你可能會希望檢測在呼叫glCompileShader後編譯是否成功了,如果沒成功的話,你還會希望知道錯誤是什麼,這樣你才能修復它們。檢測編譯時錯誤可以通過以下程式碼來實現:

    GLint success;
    GLchar infoLog[512];
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);

首先我們定義一個整型變數來表示是否成功編譯,還定義了一個儲存錯誤訊息(如果有的話)的容器。然後我們用glGetShaderiv檢查是否編譯成功。如果編譯失敗,我們會用glGetShaderInfoLog獲取錯誤訊息,然後列印它。

if (!success)
{
    glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}

編譯片段著色器的過程與頂點著色器類似,只不過我們使用GL_FRAGMENT_SHADER常量作為著色器型別:

3. 著色器程式

當連結著色器至一個程式的時候,它會把每個著色器的輸出連結到下個著色器的輸入。當輸出和輸入不匹配的時候,你會得到一個連線錯誤。
建立一個程式物件很簡單:

    GLuint shaderProgram;
    shaderProgram = glCreateProgram();

glCreateProgram函式建立一個程式,並返回新建立程式物件的ID引用。現在我們需要把之前編譯的著色器附加到程式物件上,然後用glLinkProgram連結它們:

    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

我們把著色器附加到了程式上,然後用glLinkProgram連結。
就像著色器的編譯一樣,我們也可以檢測連結著色器程式是否失敗,並獲取相應的日誌。與上面不同,我們不會呼叫glGetShaderiv和glGetShaderInfoLog,現在我們使用:

    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        ...
    }

得到的結果就是一個程式物件,我們可以呼叫glUseProgram函式,用剛建立的程式物件作為它的引數,以啟用這個程式物件,之後每個著色器呼叫和渲染呼叫都會使用這個程式物件(也就是之前寫的著色器)了:

    glUseProgram(shaderProgram);

在把著色器物件連結到程式物件以後,記得刪除著色器物件,我們不再需要它們了:

    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

4. 連結頂點屬性

我們的頂點緩衝資料會被解析為下面這樣子:
這裡寫圖片描述
- 位置資料被儲存為32-bit(4位元組)浮點值。
- 每個位置包含3個這樣的值。
- 在這3個值之間沒有空隙(或其他值)。這幾個值在陣列中緊密排列。
- 資料中第一個值在緩衝開始的位置。

有了這些資訊我們就可以使用glVertexAttribPointer函式告訴OpenGL該如何解析頂點資料(應用到逐個頂點屬性上)了:

  • glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat),
    (GLvoid*)0); glEnableVertexAttribArray(0);

glVertexAttribPointer函式的引數非常多,所以我會逐一介紹它們:

  • 第一個引數指定我們要配置的頂點屬性。我們在頂點著色器中使用layout(location =
    0)定義了position頂點屬性的位置值(Location),它可以把頂點屬性的位置值設定為0。因為我們希望把資料傳遞到這一個頂點屬性中,所以這裡我們傳入0。
  • 第二個引數指定頂點屬性的大小。頂點屬性是一個vec3,它由3個值組成,所以大小是3。
  • 第三個引數指定資料的型別,這裡是GL_FLOAT(GLSL中vec*都是由浮點數值組成的)。
  • 下個引數定義我們是否希望資料被標準化(Normalize)。如果我們設定為GL_TRUE,所有資料都會被對映到0(對於有符號型signed資料是-1)到1之間。我們把它設定為GL_FALSE。
  • 第五個引數叫做步長(Stride),它告訴我們在連續的頂點屬性組之間的間隔。由於下個組位置資料在3個GLfloat之後,我們把步長設定為3
  • sizeof(GLfloat)。要注意的是由於我們知道這個陣列是緊密排列的(在兩個頂點屬性之間沒有空隙)我們也可以設定為0來讓OpenGL決定具體步長是多少(只有當數值是緊密排列時才可用)。一旦我們有更多的頂點屬性,我們就必須更小心地定義每個頂點屬性之間的間隔,我們在後面會看到更多的例子(譯註:
    這個引數的意思簡單說就是從這個屬性第二次出現的地方到整個陣列0位置之間有多少位元組)。
  • 最後一個引數的型別是GLvoid*,所以需要我們進行這個奇怪的強制型別轉換。它表示位置資料在緩衝中起始位置的偏移量(Offset)。由於位置資料在陣列的開頭,所以這裡是0。我們會在後面詳細解釋這個引數。
    每個頂點屬性從一個VBO管理的記憶體中獲得它的資料,而具體是從哪個VBO(程式中可以有多個VBO)獲取則是通過在呼叫glVetexAttribPointer時繫結到GL_ARRAY_BUFFER的VBO決定的。由於在呼叫glVetexAttribPointer之前繫結的是先前定義的VBO物件,頂點屬性0現在會連結到它的頂點資料。

現在我們已經定義了OpenGL該如何解釋頂點資料,我們現在應該使用glEnableVertexAttribArray,以頂點屬性位置值作為引數,啟用頂點屬性;頂點屬性預設是禁用的。

5. 頂點陣列物件

頂點陣列物件(Vertex Array Object, VAO)可以像頂點緩衝物件那樣被繫結,任何隨後的頂點屬性呼叫都會儲存在這個VAO中。這樣的好處就是,當配置頂點屬性指標時,你只需要將那些呼叫執行一次,之後再繪製物體的時候只需要繫結相應的VAO就行了。
一個頂點陣列物件會儲存以下這些內容:

  • glEnableVertexAttribArray和glDisableVertexAttribArray的呼叫。
  • 通過glVertexAttribPointer設定的頂點屬性配置。
  • 通過glVertexAttribPointer呼叫進行的頂點緩衝物件與頂點屬性連結。
    這裡寫圖片描述

建立一個VAO和建立一個VBO很類似:

GLuint VAO;
glGenVertexArrays(1, &VAO);

要想使用VAO,要做的只是使用glBindVertexArray繫結VAO。從繫結之後起,我們應該繫結和配置對應的VBO和屬性指標,之後解綁VAO供之後使用。當我們打算繪製一個物體的時候,我們只要在繪製物體前簡單地把VAO繫結到希望使用的設定上就行了。這段程式碼應該看起來像這樣:

// ..:: 初始化程式碼(只執行一次 (除非你的物體頻繁改變)) :: ..
// 1. 繫結VAO
glBindVertexArray(VAO);
// 2. 把頂點陣列複製到緩衝中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 設定頂點屬性指標
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
//4. 解綁VAO
glBindVertexArray(0);

[...]
// ..:: 繪製代(遊戲迴圈中) :: ..
// 5. 繪製物體
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
glBindVertexArray(0);

通常情況下當我們配置好OpenGL物件以後要解綁它們,這樣我們才不會在其它地方錯誤地配置它們。

6. 繪製三角形

要想繪製我們想要的物體,OpenGL給我們提供了glDrawArrays函式,它使用當前啟用的著色器,之前定義的頂點屬性配置,和VBO的頂點資料(通過VAO間接繫結)來繪製圖元。

glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);

glDrawArrays函式第一個引數是我們打算繪製的OpenGL圖元的型別。由於我們在一開始時說過,我們希望繪製的是一個三角形,這裡傳遞GL_TRIANGLES給它。第二個引數指定了頂點陣列的起始索引,我們這裡填0。最後一個引數指定我們打算繪製多少個頂點,這裡是3(我們只從我們的資料中渲染一個三角形,它只有3個頂點長)。

7. 索引緩衝物件

索引繪製(Indexed Drawing)允許我們在已經儲存的頂點中選取不同的頂點來繪製不同的圖形,而不需要對對同一個頂點進行重複定義。進行索引繪製需要藉助索引緩衝物件(Element Buffer Object,EBO,也叫Index Buffer Object,IBO)。
和頂點緩衝物件一樣,EBO也是一個緩衝,它專門儲存索引,OpenGL呼叫這些頂點的索引來決定該繪製哪個頂點。首先,我們先要定義(獨一無二的)頂點,和繪製出矩形所需的索引:

GLfloat 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   // 左上角
};

GLuint indices[] = { // 注意索引從0開始! 
    0, 1, 3, // 第一個三角形
    1, 2, 3  // 第二個三角形
};

下一步我們需要建立索引緩衝物件:

GLuint EBO;
glGenBuffers(1, &EBO);

與VBO類似,我們先繫結EBO然後用glBufferData把索引複製到緩衝裡。同樣,和VBO類似,我們會把這些函式呼叫放在繫結和解綁函式呼叫之間,只不過這次我們把緩衝的型別定義為GL_ELEMENT_ARRAY_BUFFER。

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

要注意的是,我們傳遞了GL_ELEMENT_ARRAY_BUFFER當作緩衝目標。最後一件要做的事是用glDrawElements來替換glDrawArrays函式,來指明我們從索引緩衝渲染。使用glDrawElements時,我們會使用當前繫結的索引緩衝物件中的索引進行繪製:

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

第一個引數指定了我們繪製的模式,這個和glDrawArrays的一樣。第二個引數是我們打算繪製頂點的個數,這裡填6,也就是說我們一共需要繪製6個頂點。第三個引數是索引的型別,這裡是GL_UNSIGNED_INT。最後一個引數裡我們可以指定EBO中的偏移量(或者傳遞一個索引陣列,但是這是當你不在使用索引緩衝物件的時候),但是我們會在這裡填寫0。
glDrawElements函式從當前繫結到GL_ELEMENT_ARRAY_BUFFER目標的EBO中獲取索引。這意味著我們必須在每次要用索引渲染一個物體時繫結相應的EBO,這還是有點麻煩。不過頂點陣列物件同樣可以儲存索引緩衝物件的繫結狀態。VAO繫結時正在繫結的索引緩衝物件會被儲存為VAO的元素緩衝物件。繫結VAO的同時也會自動繫結EBO。
這裡寫圖片描述
當目標是GL_ELEMENT_ARRAY_BUFFER的時候,VAO會儲存glBindBuffer的函式呼叫。這也意味著它也會儲存解綁呼叫,所以確保你沒有在解綁VAO之前解綁索引陣列緩衝,否則它就沒有這個EBO配置了。
最後的初始化和繪製程式碼現在看起來像這樣:

// ..:: 初始化程式碼 :: ..
// 1. 繫結頂點陣列物件
glBindVertexArray(VAO);
// 2. 把我們的頂點陣列複製到一個頂點緩衝中,供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 複製我們的索引陣列到一個索引緩衝中,供OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 3. 設定頂點屬性指標
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 4. 解綁VAO(不是EBO!)
glBindVertexArray(0);

[...]

// ..:: 繪製程式碼(遊戲迴圈中) :: ..

glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);

四、 示例

本示例分別使用頂點陣列物件和索引緩衝物件兩種方式繪製了一個黃色梯形房頂和橙色長方形牆體,具體見程式碼註釋

程式碼:

#include <iostream>
using namespace std;
// GLEW
#define GLEW_STATIC
#include <GL/glew.h>
// GLFW
#include <GLFW/glfw3.h>

const GLuint WIDTH = 800, HEIGHT = 600;

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
void checkError(GLuint shader, GLbyte status);

// Shaders
const GLchar* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(position.x, position.y, position.z, 1.0);\n"
"}\0";
const GLchar* fragmentShaderSourceOrange = "#version 330 core\n"
"out vec4 color;\n"
"void main()\n"
"{\n"
"color = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";

const GLchar* fragmentShaderSourceYellow = "#version 330 core\n"
"out vec4 color;\n"
"void main()\n"
"{\n"
"color = vec4(1.0f, 1.0f, 0.0f, 1.0f);\n"
"}\n\0";

GLuint vertexShader, fragmentShaderOrange, fragmentShaderYellow, shaderProgramOrange, shaderProgramYellow;
GLuint VBO1, VAO1;
GLuint VBO2, VAO2, EBO2;

void shaderInit() {
    // 頂點 shader
    vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);
    // 檢查編譯錯誤
    checkError(vertexShader, GL_COMPILE_STATUS);

    // 初始化橙色片段 shader
    fragmentShaderOrange = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShaderOrange, 1, &fragmentShaderSourceOrange, NULL);
    glCompileShader(fragmentShaderOrange);
    // 檢查事件錯誤
    checkError(fragmentShaderOrange, GL_COMPILE_STATUS);

    //初始化黃色片段shader
    fragmentShaderYellow = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShaderYellow, 1, &fragmentShaderSourceYellow, NULL);
    glCompileShader(fragmentShaderYellow);
    // 檢查事件錯誤
    checkError(fragmentShaderYellow, GL_COMPILE_STATUS);

    // 連結橙色 shader
    shaderProgramOrange = glCreateProgram();
    glAttachShader(shaderProgramOrange, vertexShader);
    glAttachShader(shaderProgramOrange, fragmentShaderOrange);
    glLinkProgram(shaderProgramOrange);
    // 檢查連結錯誤
    checkError(shaderProgramOrange, GL_LINK_STATUS);

    //連結黃色 shader
    shaderProgramYellow = glCreateProgram();
    glAttachShader(shaderProgramYellow, vertexShader);
    glAttachShader(shaderProgramYellow, fragmentShaderYellow);
    glLinkProgram(shaderProgramYellow);
    // 檢查連結錯誤
    checkError(shaderProgramYellow, GL_LINK_STATUS);

    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShaderOrange);
    glDeleteShader(fragmentShaderYellow);
}

void vertexObjectInit() {
    //不使用索引緩衝物件用兩個三角形繪製一個梯形
    // 設定頂點快取和屬性指標
    GLfloat vertices1[] = {
        -0.5f, 0.2f, 0.0f, // BottomLeft  
        0.5f, 0.2f, 0.0f, // BottomRight 
        -0.2f,  0.5f, 0.0f,  // TopLeft 

        0.5f, 0.2f, 0.0f, // BottomRight 
        -0.2f,  0.5f, 0.0f,  // TopLeft
        0.2f,  0.5f, 0.0f  // TopRight
    };
    //建立索引緩衝物件
    glGenBuffers(1, &VBO1);
    glGenVertexArrays(1, &VAO1);
    glBindVertexArray(VAO1);
    // 把頂點陣列複製到緩衝中供OpenGL使用
    glBindBuffer(GL_ARRAY_BUFFER, VBO1);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices1), vertices1, GL_STATIC_DRAW);
    // 設定頂點屬性指標
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, 0);// 這個方法將頂點屬性指標註冊到VBO作為當前繫結頂點物件,然後我們就可以安全的解綁
    glBindVertexArray(0);// 解綁 VAO (這通常是一個很好的用來解綁任何快取/陣列並防止奇怪錯誤的方法)

    // 使用索引緩衝物件用兩個三角形繪製一個長方形
    GLfloat vertices[] = {
        -0.5f, 0.2f, 0.0f, // TopLeft  
        0.5f, 0.2f, 0.0f, // TopRight 
        -0.5f,  -0.5f, 0.0f,  // BottomLeft   
        0.5f,  -0.5f, 0.0f  // BottomRight 
    };
    GLuint indices[] = {
        0,1,2,
        1,2,3
    };
    glGenBuffers(1, &VBO2);
    glGenBuffers(1, &EBO2);
    glGenVertexArrays(1, &VAO2);
    glBindVertexArray(VAO2);

    glBindBuffer(GL_ARRAY_BUFFER, VBO2);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO2);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, 0); 

    glBindVertexArray(0); 
}

int main()
{
    //初始化GLFW
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);

    //建立視窗物件
    GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "LearnOpenGL", nullptr, nullptr);
    if (window == nullptr)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    //註冊鍵盤迴調
    glfwSetKeyCallback(window, key_callback);

    //初始化GLEW
    glewExperimental = GL_TRUE;
    if (glewInit() != GLEW_OK)
    {
        std::cout << "Failed to initialize GLEW" << std::endl;
        return -1;
    }
    //告訴OpenGL渲染視窗尺寸大小
    int width, height;
    glfwGetFramebufferSize(window, &width, &height);

    glViewport(0, 0, width, height);

    //初始化並繫結shaders
    shaderInit();

    //初始化頂點物件資料
    vertexObjectInit();

    //讓視窗接受輸入並保持執行
    while (!glfwWindowShouldClose(window))
    {
        //檢查事件
        glfwPollEvents();

        //渲染指令
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        //繪製梯形
        glUseProgram(shaderProgramYellow);
        glBindVertexArray(VAO1);
        glDrawArrays(GL_TRIANGLES, 0, 6);
        glBindVertexArray(0);

        //繪製長方形
        glUseProgram(shaderProgramOrange);
        glBindVertexArray(VAO2);
        glDrawElements(GL_TRIANGLES, 6,GL_UNSIGNED_INT,0);
        glBindVertexArray(0);

        //交換緩衝
        glfwSwapBuffers(window);
    }
    // Properly de-allocate all resources once they've outlived their purpose
    glDeleteVertexArrays(1, &VAO1);
    glDeleteBuffers(1, &VBO1);
    glDeleteVertexArrays(1, &VAO2);
    glDeleteBuffers(1, &VBO2);

    //釋放資源
    glfwTerminate();
    return 0;
}

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
    // 當用戶按下ESC鍵,我們設定window視窗的WindowShouldClose屬性為true
    // 關閉應用程式
    if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
        glfwSetWindowShouldClose(window, GL_TRUE);
}
void checkError(GLuint shader,GLbyte status) {
    GLint success;
    GLchar infoLog[512];
    glGetShaderiv(shader, status, &success);
    if (!success)
    {
        glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
}

結果:

這裡寫圖片描述