OPenGL 學習筆記之 VAO VBO EBO 以及SHADER 並使用其繪製三角形

譯註

在學習此節之前,建議將這三個單詞先記下來:

  • 頂點數組對象:Vertex Array Object,VAO
  • 頂點緩衝對象:Vertex Buffer Object,VBO
  • 索引緩衝對象:Element Buffer Object,EBO或Index Buffer Object,IBO

當指代這三個東西的時候,可能使用的是全稱,也可能用的是英文縮寫,翻譯的時候和原文保持的一致。由於沒有英文那樣的分詞間隔,中文全稱的部分可能不太容易注意。但請記住,縮寫和中文全稱指代的是一個東西。

在OpenGL中,任何事物都在3D空間中,而屏幕和窗口卻是2D像素數組,這導致OpenGL的大部分工作都是關於把3D座標轉變爲適應你屏幕的2D像素。3D座標轉爲2D座標的處理過程是由OpenGL的圖形渲染管線(Graphics Pipeline,大多譯爲管線,實際上指的是一堆原始圖形數據途經一個輸送管道,期間經過各種變化處理最終出現在屏幕的過程)管理的。圖形渲染管線可以被劃分爲兩個主要部分:第一部分把你的3D座標轉換爲2D座標,第二部分是把2D座標轉變爲實際的有顏色的像素。這個教程裏,我們會簡單地討論一下圖形渲染管線,以及如何利用它創建一些漂亮的像素。

2D座標和像素也是不同的,2D座標精確表示一個點在2D空間中的位置,而2D像素是這個點的近似值,2D像素受到你的屏幕/窗口分辨率的限制。

圖形渲染管線接受一組3D座標,然後把它們轉變爲你屏幕上的有色2D像素輸出。圖形渲染管線可以被劃分爲幾個階段,每個階段將會把前一個階段的輸出作爲輸入。所有這些階段都是高度專門化的(它們都有一個特定的函數),並且很容易並行執行。正是由於它們具有並行執行的特性,當今大多數顯卡都有成千上萬的小處理核心,它們在GPU上爲每一個(渲染管線)階段運行各自的小程序,從而在圖形渲染管線中快速處理你的數據。這些小程序叫做着色器(Shader)。

有些着色器允許開發者自己配置,這就允許我們用自己寫的着色器來替換默認的。這樣我們就可以更細緻地控制圖形渲染管線中的特定部分了,而且因爲它們運行在GPU上,所以它們可以給我們節約寶貴的CPU時間。OpenGL着色器是用OpenGL着色器語言(OpenGL Shading Language, GLSL)寫成的,在下一節中我們再花更多時間研究它。

下面,你會看到一個圖形渲染管線的每個階段的抽象展示。要注意藍色部分代表的是我們可以注入自定義的着色器的部分。

如你所見,圖形渲染管線包含很多部分,每個部分都將在轉換頂點數據到最終像素這一過程中處理各自特定的階段。我們會概括性地解釋一下渲染管線的每個部分,讓你對圖形渲染管線的工作方式有個大概瞭解。

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

譯註1

當我們談論一個“位置”的時候,它代表在一個“空間”中所處地點的這個特殊屬性;同時“空間”代表着任何一種座標系,比如x、y、z三維座標系,x、y二維座標系,或者一條直線上的x和y的線性關係,只不過二維座標系是一個扁扁的平面空間,而一條直線是一個很瘦的長長的空間。

爲了讓OpenGL知道我們的座標和顏色值構成的到底是什麼,OpenGL需要你去指定這些數據所表示的渲染類型。我們是希望把這些數據渲染成一系列的點?一系列的三角形?還是僅僅是一個長長的線?做出的這些提示叫做圖元(Primitive),任何一個繪製指令的調用都將把圖元傳遞給OpenGL。這是其中的幾個:GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP。

圖形渲染管線的第一個部分是頂點着色器(Vertex Shader),它把一個單獨的頂點作爲輸入。頂點着色器主要的目的是把3D座標轉爲另一種3D座標(後面會解釋),同時頂點着色器允許我們對頂點屬性進行一些基本處理。

圖元裝配(Primitive Assembly)階段將頂點着色器輸出的所有頂點作爲輸入(如果是GL_POINTS,那麼就是一個頂點),並所有的點裝配成指定圖元的形狀;本節例子中是一個三角形。

圖元裝配階段的輸出會傳遞給幾何着色器(Geometry Shader)。幾何着色器把圖元形式的一系列頂點的集合作爲輸入,它可以通過產生新頂點構造出新的(或是其它的)圖元來生成其他形狀。例子中,它生成了另一個三角形。

幾何着色器的輸出會被傳入光柵化階段(Rasterization Stage),這裏它會把圖元映射爲最終屏幕上相應的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器運行之前會執行裁切(Clipping)。裁切會丟棄超出你的視圖以外的所有像素,用來提升執行效率。

OpenGL中的一個片段是OpenGL渲染一個像素所需的所有數據。

片段着色器的主要目的是計算一個像素的最終顏色,這也是所有OpenGL高級效果產生的地方。通常,片段着色器包含3D場景的數據(比如光照、陰影、光的顏色等等),這些數據可以被用來計算最終像素的顏色。

在所有對應顏色值確定以後,最終的對象將會被傳到最後一個階段,我們叫做Alpha測試和混合(Blending)階段。這個階段檢測片段的對應的深度(和模板(Stencil))值(後面會講),用它們來判斷這個像素是其它物體的前面還是後面,決定是否應該丟棄。這個階段也會檢查alpha值(alpha值定義了一個物體的透明度)並對物體進行混合(Blend)。所以,即使在片段着色器中計算出來了一個像素輸出的顏色,在渲染多個三角形的時候最後的像素顏色也可能完全不同。

可以看到,圖形渲染管線非常複雜,它包含很多可配置的部分。然而,對於大多數場合,我們只需要配置頂點和片段着色器就行了。幾何着色器是可選的,通常使用它默認的着色器就行了。

在現代OpenGL中,我們必須定義至少一個頂點着色器和一個片段着色器(因爲GPU中沒有默認的頂點/片段着色器)。出於這個原因,剛開始學習現代OpenGL的時候可能會非常困難,因爲在你能夠渲染自己的第一個三角形之前已經需要了解一大堆知識了。在本節結束你最終渲染出你的三角形的時候,你也會瞭解到非常多的圖形編程知識。

頂點輸入


開始繪製圖形之前,我們必須先給OpenGL輸入一些頂點數據。OpenGL是一個3D圖形庫,所以我們在OpenGL中指定的所有座標都是3D座標(x、y和z)。OpenGL不是簡單地把所有的3D座標變換爲屏幕上的2D像素;OpenGL僅當3D座標在3個軸(x、y和z)上都爲-1.0到1.0的範圍內時才處理它。所有在所謂的標準化設備座標(Normalized Device Coordinates)範圍內的座標纔會最終呈現在屏幕上(在這個範圍以外的座標都不會顯示)。

由於我們希望渲染一個三角形,我們一共要指定三個頂點,每個頂點都有一個3D位置。我們會將它們以標準化設備座標的形式(OpenGL的可見區域)定義爲一個float數組。

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

由於OpenGL是在3D空間中工作的,而我們渲染的是一個2D三角形,我們將它頂點的z座標設置爲0.0。這樣子的話三角形每一點的深度(Depth,譯註2)都是一樣的,從而使它看上去像是2D的。

譯註2

通常深度可以理解爲z座標,它代表一個像素在空間中和你的距離,如果離你遠就可能被別的像素遮擋,你就看不到它了,它會被丟棄,以節省資源。

標準化設備座標(Normalized Device Coordinates, NDC)

一旦你的頂點座標已經在頂點着色器中處理過,它們就應該是標準化設備座標了,標準化設備座標是一個x、y和z值在-1.0到1.0的一小段空間。任何落在範圍外的座標都會被丟棄/裁剪,不會顯示在你的屏幕上。下面你會看到我們定義的在標準化設備座標中的三角形(忽略z軸):

NDC

與通常的屏幕座標不同,y軸正方向爲向上,(0, 0)座標是這個圖像的中心,而不是左上角。最終你希望所有(變換過的)座標都在這個座標空間中,否則它們就不可見了。

你的標準化設備座標接着會變換爲屏幕空間座標(Screen-space Coordinates),這是使用你通過glViewport函數提供的數據,進行視口變換(Viewport Transform)完成的。所得的屏幕空間座標又會被變換爲片段輸入到片段着色器中。

定義這樣的頂點數據以後,我們會把它作爲輸入發送給圖形渲染管線的第一個處理階段:頂點着色器。它會在GPU上創建內存用於儲存我們的頂點數據,還要配置OpenGL如何解釋這些內存,並且指定其如何發送給顯卡。頂點着色器接着會處理我們在內存中指定數量的頂點。

我們通過頂點緩衝對象(Vertex Buffer Objects, VBO)管理這個內存,它會在GPU內存(通常被稱爲顯存)中儲存大量頂點。使用這些緩衝對象的好處是我們可以一次性的發送一大批數據到顯卡上,而不是每個頂點發送一次。從CPU把數據發送到顯卡相對較慢,所以只要可能我們都要嘗試儘量一次性發送儘可能多的數據。當數據發送至顯卡的內存中後,頂點着色器幾乎能立即訪問頂點,這是個非常快的過程。

頂點緩衝對象是我們在OpenGL教程中第一個出現的OpenGL對象。就像OpenGL中的其它對象一樣,這個緩衝有一個獨一無二的ID,所以我們可以使用glGenBuffers函數和一個緩衝ID生成一個VBO對象:

unsigned int 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 :數據每次繪製時都會改變。

三角形的位置數據不會改變,每次渲染調用時都保持原樣,所以它的使用類型最好是GL_STATIC_DRAW。如果,比如說一個緩衝中的數據將頻繁被改變,那麼使用的類型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,這樣就能確保顯卡把數據放在能夠高速寫入的內存部分。

現在我們已經把頂點數據儲存在顯卡的內存中,用VBO這個頂點緩衝對象管理。下面我們會創建一個頂點和片段着色器來真正處理這些數據。現在我們開始着手創建它們吧。

頂點着色器


頂點着色器(Vertex Shader)是幾個可編程着色器中的一個。如果我們打算做渲染的話,現代OpenGL需要我們至少設置一個頂點和一個片段着色器。我們會簡要介紹一下着色器以及配置兩個非常簡單的着色器來繪製我們第一個三角形。下一節中我們會更詳細的討論着色器。

我們需要做的第一件事是用着色器語言GLSL(OpenGL Shading Language)編寫頂點着色器,然後編譯這個着色器,這樣我們就可以在程序中使用它了。下面你會看到一個非常基礎的GLSL頂點着色器的源代碼:

#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

可以看到,GLSL看起來很像C語言。每個着色器都起始於一個版本聲明。OpenGL 3.3以及和更高版本中,GLSL版本號和OpenGL的版本是匹配的(比如說GLSL 420版本對應於OpenGL 4.2)。我們同樣明確表示我們會使用核心模式。

下一步,使用in關鍵字,在頂點着色器中聲明所有的輸入頂點屬性(Input Vertex Attribute)。現在我們只關心位置(Position)數據,所以我們只需要一個頂點屬性。GLSL有一個向量數據類型,它包含1到4個float分量,包含的數量可以從它的後綴數字看出來。由於每個頂點都有一個3D座標,我們就創建一個vec3輸入變量aPos。我們同樣也通過layout (location = 0)設定了輸入變量的位置值(Location)你後面會看到爲什麼我們會需要這個位置值。

向量(Vector)

在圖形編程中我們經常會使用向量這個數學概念,因爲它簡明地表達了任意空間中的位置和方向,並且它有非常有用的數學屬性。在GLSL中一個向量有最多4個分量,每個分量值都代表空間中的一個座標,它們可以通過vec.xvec.yvec.zvec.w來獲取。注意vec.w分量不是用作表達空間中的位置的(我們處理的是3D不是4D),而是用在所謂透視除法(Perspective Division)上。我們會在後面的教程中更詳細地討論向量。

爲了設置頂點着色器的輸出,我們必須把位置數據賦值給預定義的gl_Position變量,它在幕後是vec4類型的。在main函數的最後,我們將gl_Position設置的值會成爲該頂點着色器的輸出。由於我們的輸入是一個3分量的向量,我們必須把它轉換爲4分量的。我們可以把vec3的數據作爲vec4構造器的參數,同時把w分量設置爲1.0f(我們會在後面解釋爲什麼)來完成這一任務。

當前這個頂點着色器可能是我們能想到的最簡單的頂點着色器了,因爲我們對輸入數據什麼都沒有處理就把它傳到着色器的輸出了。在真實的程序裏輸入數據通常都不是標準化設備座標,所以我們首先必須先把它們轉換至OpenGL的可視區域內。

編譯着色器


我們已經寫了一個頂點着色器源碼(儲存在一個C的字符串中),但是爲了能夠讓OpenGL使用它,我們必須在運行時動態編譯它的源碼。

我們首先要做的是創建一個着色器對象,注意還是用ID來引用的。所以我們儲存這個頂點着色器爲unsigned int,然後用glCreateShader創建這個着色器:

unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

我們把需要創建的着色器類型以參數形式提供給glCreateShader。由於我們正在創建一個頂點着色器,傳遞的參數是GL_VERTEX_SHADER。

下一步我們把這個着色器源碼附加到着色器對象上,然後編譯它:

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

glShaderSource函數把要編譯的着色器對象作爲第一個參數。第二參數指定了傳遞的源碼字符串數量,這裏只有一個。第三個參數是頂點着色器真正的源碼,第四個參數我們先設置爲NULL

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

int  success;
char 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;
}

 

如果編譯的時候沒有檢測到任何錯誤,頂點着色器就被編譯成功了。

片段着色器


片段着色器(Fragment Shader)是第二個也是最後一個我們打算創建的用於渲染三角形的着色器。片段着色器所做的是計算像素最後的顏色輸出。爲了讓事情更簡單,我們的片段着色器將會一直輸出橘黃色。

在計算機圖形中顏色被表示爲有4個元素的數組:紅色、綠色、藍色和alpha(透明度)分量,通常縮寫爲RGBA。當在OpenGL或GLSL中定義一個顏色的時候,我們把顏色每個分量的強度設置在0.0到1.0之間。比如說我們設置紅爲1.0f,綠爲1.0f,我們會得到兩個顏色的混合色,即黃色。這三種顏色分量的不同調配可以生成超過1600萬種不同的顏色!

#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
} 

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

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

unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

兩個着色器現在都編譯了,剩下的事情是把兩個着色器對象鏈接到一個用來渲染的着色器程序(Shader Program)中。

着色器程序

着色器程序對象(Shader Program Object)是多個着色器合併之後並最終鏈接完成的版本。如果要使用剛纔編譯的着色器我們必須把它們鏈接(Link)爲一個着色器程序對象,然後在渲染對象的時候激活這個着色器程序。已激活着色器程序的着色器將在我們發送渲染調用的時候被使用。

當鏈接着色器至一個程序的時候,它會把每個着色器的輸出鏈接到下個着色器的輸入。當輸出和輸入不匹配的時候,你會得到一個連接錯誤。

創建一個程序對象很簡單:

unsigned int 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);

在glUseProgram函數調用之後,每個着色器調用和渲染調用都會使用這個程序對象(也就是之前寫的着色器)了。

對了,在把着色器對象鏈接到程序對象以後,記得刪除着色器對象,我們不再需要它們了:

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

現在,我們已經把輸入頂點數據發送給了GPU,並指示了GPU如何在頂點和片段着色器中處理它。就快要完成了,但還沒結束,OpenGL還不知道它該如何解釋內存中的頂點數據,以及它該如何將頂點數據鏈接到頂點着色器的屬性上。我們需要告訴OpenGL怎麼做。

鏈接頂點屬性


頂點着色器允許我們指定任何以頂點屬性爲形式的輸入。這使其具有很強的靈活性的同時,它還的確意味着我們必須手動指定輸入數據的哪一個部分對應頂點着色器的哪一個頂點屬性。所以,我們必須在渲染前指定OpenGL該如何解釋頂點數據。

我們的頂點緩衝數據會被解析爲下面這樣子:

  • 位置數據被儲存爲32位(4字節)浮點值。
  • 每個位置包含3個這樣的值。
  • 在這3個值之間沒有空隙(或其他值)。這幾個值在數組中緊密排列(Tightly Packed)。
  • 數據中第一個值在緩衝開始的位置。

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

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)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個float之後,我們把步長設置爲3 * sizeof(float)。要注意的是由於我們知道這個數組是緊密排列的(在兩個頂點屬性之間沒有空隙)我們也可以設置爲0來讓OpenGL決定具體步長是多少(只有當數值是緊密排列時纔可用)。一旦我們有更多的頂點屬性,我們就必須更小心地定義每個頂點屬性之間的間隔,我們在後面會看到更多的例子(譯註: 這個參數的意思簡單說就是從這個屬性第二次出現的地方到整個數組0位置之間有多少字節)。
  • 最後一個參數的類型是void*,所以需要我們進行這個奇怪的強制類型轉換。它表示位置數據在緩衝中起始位置的偏移量(Offset)。由於位置數據在數組的開頭,所以這裏是0。我們會在後面詳細解釋這個參數。

每個頂點屬性從一個VBO管理的內存中獲得它的數據,而具體是從哪個VBO(程序中可以有多個VBO)獲取則是通過在調用glVertexAttribPointer時綁定到GL_ARRAY_BUFFER的VBO決定的。由於在調用glVertexAttribPointer之前綁定的是先前定義的VBO對象,頂點屬性0現在會鏈接到它的頂點數據。

現在我們已經定義了OpenGL該如何解釋頂點數據,我們現在應該使用glEnableVertexAttribArray,以頂點屬性位置值作爲參數,啓用頂點屬性;頂點屬性默認是禁用的。自此,所有東西都已經設置好了:我們使用一個頂點緩衝對象將頂點數據初始化至緩衝中,建立了一個頂點和一個片段着色器,並告訴了OpenGL如何把頂點數據鏈接到頂點着色器的頂點屬性上。在OpenGL中繪製一個物體,代碼會像是這樣:

// 0. 複製頂點數組到緩衝中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. 設置頂點屬性指針
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2. 當我們渲染一個物體時要使用着色器程序
glUseProgram(shaderProgram);
// 3. 繪製物體
someOpenGLFunctionThatDrawsOurTriangle();

每當我們繪製一個物體的時候都必須重複這一過程。這看起來可能不多,但是如果有超過5個頂點屬性,上百個不同物體呢(這其實並不罕見)。綁定正確的緩衝對象,爲每個物體配置所有頂點屬性很快就變成一件麻煩事。有沒有一些方法可以使我們把所有這些狀態配置儲存在一個對象中,並且可以通過綁定這個對象來恢復狀態呢?

頂點數組對象

頂點數組對象(Vertex Array Object, VAO)可以像頂點緩衝對象那樣被綁定,任何隨後的頂點屬性調用都會儲存在這個VAO中。這樣的好處就是,當配置頂點屬性指針時,你只需要將那些調用執行一次,之後再繪製物體的時候只需要綁定相應的VAO就行了。這使在不同頂點數據和屬性配置之間切換變得非常簡單,只需要綁定不同的VAO就行了。剛剛設置的所有狀態都將存儲在VAO中

OpenGL的核心模式要求我們使用VAO,所以它知道該如何處理我們的頂點輸入。如果我們綁定VAO失敗,OpenGL會拒絕繪製任何東西。

一個頂點數組對象會儲存以下這些內容:

  • glEnableVertexAttribArray和glDisableVertexAttribArray的調用。
  • 通過glVertexAttribPointer設置的頂點屬性配置。
  • 通過glVertexAttribPointer調用與頂點屬性關聯的頂點緩衝對象。

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

unsigned int 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(float), (void*)0);
glEnableVertexAttribArray(0);

[...]

// ..:: 繪製代碼(渲染循環中) :: ..
// 4. 繪製物體
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();

就這麼多了!前面做的一切都是等待這一刻,一個儲存了我們頂點屬性配置和應使用的VBO的頂點數組對象。一般當你打算繪製多個物體時,你首先要生成/配置所有的VAO(和必須的VBO及屬性指針),然後儲存它們供後面使用。當我們打算繪製物體的時候就拿出相應的VAO,綁定它,繪製完物體後,再解綁VAO。

我們一直期待的三角形

要想繪製我們想要的物體,OpenGL給我們提供了glDrawArrays函數,它使用當前激活的着色器,之前定義的頂點屬性配置,和VBO的頂點數據(通過VAO間接綁定)來繪製圖元。

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

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

現在嘗試編譯代碼,如果彈出了任何錯誤,回頭檢查你的代碼。如果你編譯通過了,你應該看到下面的結果:

完整的程序源碼

//hello_triangle.cpp
#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

const char *vertexShaderSource = "#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 *fragmentShaderSource = "#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()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }


    // build and compile our shader program
    // ------------------------------------
    // vertex shader
    int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);
    // check for shader compile errors
    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;
    }
    // fragment shader
    int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);
    // check for shader compile errors
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
    // link shaders
    int shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
    // check for linking errors
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
    }
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    // set up vertex data (and buffer(s)) and configure vertex attributes
    // ------------------------------------------------------------------
    float vertices[] = {
        -0.5f, -0.5f, 0.0f, // left  
         0.5f, -0.5f, 0.0f, // right 
         0.0f,  0.5f, 0.0f  // top   
    }; 

    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    // bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
    glBindVertexArray(VAO);

    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);

    // note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
    glBindBuffer(GL_ARRAY_BUFFER, 0); 

    // You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
    // VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
    glBindVertexArray(0); 


    // uncomment this call to draw in wireframe polygons.
    //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // input
        // -----
        processInput(window);

        // render
        // ------
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // draw our first triangle
        glUseProgram(shaderProgram);
        glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
        glDrawArrays(GL_TRIANGLES, 0, 3);
        // glBindVertexArray(0); // no need to unbind it every time 
 
        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // optional: de-allocate all resources once they've outlived their purpose:
    // ------------------------------------------------------------------------
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);

    // glfw: terminate, clearing all previously allocated GLFW resources.
    // ------------------------------------------------------------------
    glfwTerminate();
    return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and 
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

如果你的輸出和這個看起來不一樣,你可能做錯了什麼。去查看一下源碼,檢查你是否遺漏了什麼東西,或者你也可以在評論區提問。

索引緩衝對象

在渲染頂點這一話題上我們還有最後一個需要討論的東西——索引緩衝對象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO)。要解釋索引緩衝對象的工作方式最好還是舉個例子:假設我們不再繪製一個三角形而是繪製一個矩形。我們可以繪製兩個三角形來組成一個矩形(OpenGL主要處理三角形)。這會生成下面的頂點的集合:

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

可以看到,有幾個頂點疊加了。我們指定了右下角左上角兩次!一個矩形只有4個而不是6個頂點,這樣就產生50%的額外開銷。當我們有包括上千個三角形的模型之後這個問題會更糟糕,這會產生一大堆浪費。更好的解決方案是隻儲存不同的頂點,並設定繪製這些頂點的順序。這樣子我們只要儲存4個頂點就能繪製矩形了,之後只要指定繪製的順序就行了。如果OpenGL提供這個功能就好了,對吧?

很幸運,索引緩衝對象的工作方式正是這樣的。和頂點緩衝對象一樣,EBO也是一個緩衝,它專門儲存索引,OpenGL調用這些頂點的索引來決定該繪製哪個頂點。所謂的索引繪製(Indexed Drawing)正是我們問題的解決方案。首先,我們先要定義(不重複的)頂點,和繪製出矩形所需的索引:

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  // 第二個三角形
};

你可以看到,當時用索引的時候,我們只定義了4個頂點,而不是6個。下一步我們需要創建索引緩衝對象:

unsigned int 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);
// 4. 設定頂點屬性指針
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

[...]

// ..:: 繪製代碼(渲染循環中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);

運行程序會獲得下面這樣的圖片的結果。左側圖片看應該起來很熟悉,而右側的則是使用線框模式(Wireframe Mode)繪製的。線框矩形可以顯示出矩形的確是由兩個三角形組成的。

線框模式(Wireframe Mode)

要想用線框模式繪製你的三角形,你可以通過glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)函數配置OpenGL如何繪製圖元。第一個參數表示我們打算將其應用到所有的三角形的正面和背面,第二個參數告訴我們用線來繪製。之後的繪製調用會一直以線框模式繪製三角形,直到我們用glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)將其設置回默認模式。

如果你遇到任何錯誤,回頭檢查代碼,看看是否遺漏了什麼。同時,全部源碼如下:

//hello_triangle_indexed.cpp
#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

const char *vertexShaderSource = "#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 *fragmentShaderSource = "#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()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }


    // build and compile our shader program
    // ------------------------------------
    // vertex shader
    int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);
    // check for shader compile errors
    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;
    }
    // fragment shader
    int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);
    // check for shader compile errors
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
    // link shaders
    int shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
    // check for linking errors
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
    }
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    // set up vertex data (and buffer(s)) and configure vertex attributes
    // ------------------------------------------------------------------
    float vertices[] = {
         0.5f,  0.5f, 0.0f,  // top right
         0.5f, -0.5f, 0.0f,  // bottom right
        -0.5f, -0.5f, 0.0f,  // bottom left
        -0.5f,  0.5f, 0.0f   // top left 
    };
    unsigned int indices[] = {  // note that we start from 0!
        0, 1, 3,  // first Triangle
        1, 2, 3   // second Triangle
    };
    unsigned int VBO, VAO, EBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);
    // bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
    glBindVertexArray(VAO);

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

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

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    // note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
    glBindBuffer(GL_ARRAY_BUFFER, 0); 

    // remember: do NOT unbind the EBO while a VAO is active as the bound element buffer object IS stored in the VAO; keep the EBO bound.
    //glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

    // You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
    // VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
    glBindVertexArray(0); 


    // uncomment this call to draw in wireframe polygons.
    //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // input
        // -----
        processInput(window);

        // render
        // ------
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // draw our first triangle
        glUseProgram(shaderProgram);
        glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
        //glDrawArrays(GL_TRIANGLES, 0, 6);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
        // glBindVertexArray(0); // no need to unbind it every time 
 
        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // optional: de-allocate all resources once they've outlived their purpose:
    // ------------------------------------------------------------------------
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);

    // glfw: terminate, clearing all previously allocated GLFW resources.
    // ------------------------------------------------------------------
    glfwTerminate();
    return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and 
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

如果你像我這樣成功繪製出了這個三角形或矩形,那麼恭喜你,你成功地通過了現代OpenGL最難部分之一:繪製你自己的第一個三角形。這部分很難,因爲在可以繪製第一個三角形之前你需要了解很多知識。幸運的是我們現在已經越過了這個障礙,接下來的教程會比較容易理解一些。

附加資源

練習

爲了更好的掌握上述概念,我準備了一些練習。建議在繼續下一節的學習之前先做完這些練習,確保你對這些知識有比較好的理解。

  1. 添加更多頂點到數據中,使用glDrawArrays,嘗試繪製兩個彼此相連的三角形:參考解答
  2. 創建相同的兩個三角形,但對它們的數據使用不同的VAO和VBO:參考解答
  3. 創建兩個着色器程序,第二個程序使用一個不同的片段着色器,輸出黃色;再次繪製這兩個三角形,讓其中一個輸出爲黃色:參考解答
發佈了98 篇原創文章 · 獲贊 146 · 訪問量 24萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章