我們已經學會了創建窗口,這一講,我們將學習如何使用現代OpenGL畫一個三角形。在開始寫代碼之前,我們需要先了解一些OpenGL概念。本文會很長,請大家做好心理准備~
注:以下OpenGL概念翻譯自https://learnopengl.com/#!Getting-started/Hello-Triangle,有刪減。(實際上LearnOpenGL的教程有中文翻譯,但是我還是自己翻譯了。)代碼則是原創。
圖形管線(graphics pipeline)和着色器(shader)
在OpenGL中所有的東西都在3D空間中,而屏幕和窗口是一個2D像素數組,因此將3D坐標轉換成屏幕上的2D像素就成了OpenGL的很大一部分的工作。而這一過程是由OpenGL的圖形管線(graphics pipeline)進行管理的。圖形管線可以被分成兩個部分,第一部分是把3D坐標變換成2D坐標,第二部分是把2D坐標變換成塗了顏色的像素。注意2D坐標和像素的區別:2D坐標是一個點在2D空間中的精確表示,而像素則是受限於屏幕分辨率時,該2D坐標的近似值。
圖形管線接受一組坐標作為輸入,並將該坐標變換成屏幕上的上了色的2D像素。圖形管線可以被分成幾步,每一步都需要用上一部的輸出作為輸入。這些步驟都是高度特化的(原文是highly specialized)(它們有一個具體的功能),可以被輕易地並發執行。因為它們的平行特點,今天的顯卡基本都有幾千個小的處理內核,可以在每一步時,通過在GPU上運行小程序,迅速在圖形管線中處理你的數據。這些小程序被稱為着色器(shader)。
一些着色器允許用戶自己去設置,這樣我們就可以自己寫着色器去替代默認的着色器。着色器是用OpenGL着色語言(GLSL)編寫的。下圖描述了整個圖形管線,藍色的框所代表的階段我們可以自己添加着色器(圖片來自LearnOpenGL)。
如你所見,圖形管線包含很多部分,每個部分都有特定的工作。下面我們將簡要解釋一下圖形管線的每個部分。
頂點數據(vertex data):作為輸入,我們會給圖形管線傳入一組數據,叫做頂點數據。頂點數據描述了一組頂點的信息,這些頂點構成一個或多個圖元(primitive)(關於圖元將在后面解釋)。頂點數據用頂點屬性(vertex attribute)表示,頂點屬性可以包含任何我們喜歡的數據,但通常包含的是頂點位置、顏色、貼圖坐標(texture coordinates)等信息。
這里還有一個圖元的概念:提供了頂點數據后,OpenGL是將這些頂點解釋成一個三角形,還是一條線段,還是其它圖形呢?因此,調用OpenGL繪制命令時,你需要告訴OpenGL要繪制的圖形,叫做圖元。
頂點着色器(vertex shader):圖形管線的第一個階段,接受一個頂點作為輸入,將這個頂點進行相應的變換(以后會更詳細地講到)。頂點着色器允許我們對頂點屬性做些基本處理。
圖元裝配(primitive assembly):將頂點着色器輸出的所有組成一個圖元的頂點作為輸入(如果畫點,則只有一個頂點),將所有的點按照所給的圖元類型進行裝配(這里是三角形)。
幾何着色器(geometry shader):可選項,這里不做介紹。
光柵化(rasterization):將圖元轉換成最終屏幕上的像素,得到許多片元(fragment)給片元着色器(fragment shader)使用。片元指渲染一個像素所需的全部數據。這一步還會有剪切(clipping),將不可見的片元全部丟棄。
片元着色器(fragment shader):計算一個像素的最終顏色。通常高級OpenGL效果都會應用在這里(例如光照、陰影效果)。
測試與混合(test and blending):圖形管線的最后一步,檢查片元的深度,例如如果發現有片元位於其它片元的后面,就會被丟棄。這一步還會檢查片元的alpha值(代表透明度),並將對象進行混合。(所以即使片元着色器計算出了顏色,最終顏色還可能不同。)
可以看出,圖形管線是一個復雜的整體,含有很多可設置的部分。但我們一般只會與頂點着色器和片元着色器打交道。幾何着色器一般會使用默認的。
在現代OpenGL中我們需要定義至少一個頂點着色器和片元着色器。因此,學習現代OpenGL比學習舊版OpenGL要困難很多,因為在開始渲染之前需要知道大量的知識。在本講最后您渲染出三角形時,您將會學到更多的圖形學知識。
NDC坐標
頂點坐標被頂點着色器處理完畢后,頂點的x、y、z值應位於-1.0~1.0這一范圍之內,否則就不會被渲染。具有這種范圍限制的系統被稱為規格化設備坐標系統(normalized device coordinate,NDC)。x、y、z位於-1.0~1.0這一范圍內的坐標叫做NDC坐標(這種解釋不是很好,但是為了新手好理解,就先這樣說吧)。
對於NDC坐標,原點(0, 0)位於窗口中央;點(-1, -1)位於窗口左下角;點(1, -1)位於窗口右下角;點(-1, 1)位於窗口左上角;點(1, 1)位於窗口右上角。
開始編寫代碼
我們先從着色器開始。這里我們把頂點着色器和片元着色器分別寫到兩個文本文件里,分別命名為shader.vert和shader.frag。.vert和.frag分別表示vertex shader和fragment shader。(如果願意,你也可以使用其它擴展名,或者直接使用.txt。)在后面我們將讀取這兩個文件,動態加載兩個着色器。OpenGL的着色器使用OpenGL着色語言(OpenGL Shading Language,GLSL)編寫。
頂點着色器(vertex shader)
文件名:shader.vert
#version 330 core
layout (location = 0) in vec4 position;
void main()
{
gl_Position = position;
}
頂點着色器用於計算一個頂點的最終位置(NDC坐標)。可以看到頂點着色器非常簡單。從這里也可以看出,GLSL的語法和C/C++很相似。
先來看第一行:
#version 330 core
這是GLSL的#version預處理器指令,用於指定着色器的版本。“330”表示我們使用OpenGL 3.3對應的GLSL(在OpenGL 3.3以前,這個數字和OpenGL版本號完全不同,這里不做詳細討論),與之前用glfwWindowHint()設置的OpenGL版本一致。而“core”表示我們要使用OpenGL的核心模式(core profile)。“core”可以省略,但這個#version指令不能省略。
下一行:
layout (location = 0) in vec4 position;
創建了一個着色器變量。為方便理解,這里從右往左依次解釋。這個變量叫“position”,表示頂點的位置。“vec4”是position的類型,表示一個含有4個float分量的向量,4個分量分別是x、y、z、w。“in”表示position是輸入變量,如果是頂點着色器,“in”聲明的變量將從頂點數據獲得相應的值。“layout (location = 0)”是布局限定符(layout qualifier),將position變量的location值指定為0,它的用處將在后面的章節討論。
前面說過,OpenGL中所有東西都在3D空間中。你可能會問:我們要畫的不是2D三角形嗎?是的,但是2D可以被看作3D的一部分,2D三角形可以被看作每個點的z值都為0的三角形(先忽略w)。
然后是main()函數:
void main()
{
gl_Position = position;
}
與ANSI C/C++不同,main()返回void,即沒有返回值。gl_Position是GLSL的內置變量(類型為vec4),代表頂點的NDC位置(也就是x、y、z應位於-1.0~1.0的范圍內)。這里只是簡單地將position賦給gl_Position。(以后還會有頂點變換,就不是直接將position賦給gl_Position了。)
片元着色器(fragment shader)
文件名:shader.frag
#version 330 core
out vec4 color;
void main()
{
color = vec4(0.0, 0.5, 0.5, 1.0);
}
第一行不解釋了,和前面是一樣的。
out vec4 color;
與前面相反,這里使用了out關鍵字,聲明了一個輸出變量。變量名為color,類型為vec4。所有的片元着色器都需要輸出一個vec4變量(一個有4個float元素的向量),該變量代表了一個像素的最終顏色(不像頂點着色器,position也是一個vec4,但因為我們將它賦給了gl_Position,因此它表示的是一個位置)。這里所有像素都是一個顏色。
然后是main()函數:
void main()
{
color = vec4(0.0, 0.5, 0.5, 1.0);
}
在main()中,我們把color設置為一個4個元素分別為0.0、0.5、0.5、1.0的vec4向量。當用一個vec4來表示顏色時,它的4個分量分別表示該顏色的R、G、B、A值。(如果你還不知道RGB顏色,請自己先百度或Google。)在OpenGL中,R、G、B分量的范圍是0.0~1.0(在畫圖中該范圍是0~255)。(0.0, 0.5, 0.5)這一RGB值代表的是一種藍綠色。
除了R、G、B,A分量是什么意思呢?A是alpha值的意思,表示透明度,范圍也是0.0~1.0。這里我們直接將A分量設為1.0,表示完全不透明。很長一段時間我們都會這么做,直到學到混合。
加載着色器
寫完了着色器,我們還需要在我們的程序中,加入對着色器的支持,也就是在運行程序時動態加載着色器。這里我們創建了新的源代碼文件。
文件名:shader.h
#ifndef SHADER_H_ #define SHADER_H_ #include <GL/glew.h> GLuint loadShader(const char * vFilename, const char * fFilename); #endif
這就是整個shader.h的內容。函數只有一個,用於讀取着色器源代碼文件,並創建相應的着色器程序(shader program)。
文件名:shader.cpp
#include "shader.h" #include <iostream> #include <fstream> using std::cout; using std::endl;
shader.cpp包含了3個頭文件。第一個是shader.h,其余的是C++標准頭文件<iostream>和<fstream>。包含<fstream>是因為需要讀取着色器文件。
const int PROGRAM = 0;
一個常量,后面會使用到。這里先不作說明。
GLuint loadShader(const char * filename, GLenum type); char * loadShaderFromFile(const char * filename); GLuint makeProgram(GLuint vShader, GLuint fShader); bool getCompileStatus(GLuint id, bool isProgram); void printInfoLog(GLuint id, GLenum type); const char * getShaderName(GLenum type);
一些會使用到的函數的原型。這里簡要地解釋它們的用處(看不懂也沒關系,有些概念后面會講到)。
loadShader():讀取filename文件,加載類型為type的着色器,並返回該着色器對象。
loadShaderFromFile():讀取filename文件,返回讀取的文件內容。
makeProgram():將頂點着色器、片元着色器vShader、fShader鏈接成一個着色器程序,並返回該着色器程序對象。
getCompileStatus():獲取着色器編譯情況或着色器程序鏈接情況。id為一個OpenGL對象ID,isProgram表示該ID是否是着色器程序(isProgram是false時,該ID是着色器對象)。
printInfoLog():打印着色器/着色器程序的編譯/鏈接日志。type為OpenGL表示着色器的常量或PROGRAM。
getShaderName():獲取type表示的着色器類型的名字。
GLuint loadProgram(const char * vFilename, const char * fFilename) { GLuint vShader = loadShader(vFilename, GL_VERTEX_SHADER); GLuint fShader = loadShader(fFilename, GL_FRAGMENT_SHADER); GLuint program = makeProgram(vShader, fShader); return program; }
在講解這段代碼前,需要了解OpenGL的對象(object)概念。在OpenGL中,對象的意思和C++不太一樣。OpenGL中,對象指表示OpenGL狀態的一個子集的一組選項(a collection of options that represents a subset of OpenGL's state)。例如這里就有着色器對象、着色器程序對象。每個類型相同的OpenGL對象,都具有一個獨一無二的ID(不同類型則可能重復)。ID的類型是GLuint,這是OpenGL定義的一個類型(一個簡單的typedef),代表32位無符號整數。我們不能直接訪問OpenGL對象,只能通過對象的ID進行間接訪問。這一點和上一課所講的窗口句柄(GLFWwindow指針)類似。
這里的vShader、fShader和program都是OpenGL對象ID。為了方便,我們會將OpenGL對象ID說成OpenGL對象。
loadProgram()函數有兩個const char *參數,分別表示頂點着色器和片元着色器的文件名。loadShader()將讀取相應的着色器並編譯。makeProgram接受兩個GLuint參數表示兩個着色器,並把兩個着色器鏈接成相應的着色器程序。loadProgram將返回該着色器程序對象。
loadShader的第一個參數是文件名,第二個是着色器類型。GL_VERTEX_SHADER和GL_FRAGMENT_SHADER是OpenGL的常量,分別表示頂點着色器和片元着色器。
GLuint loadShader(const char * filename, GLenum type) {
loadShader函數從文件中加載着色器並編譯。它有兩個參數,一個是着色器文件名filename,另一個是着色器類型type。type的類型是GLenum,也是32位無符號整形,這里type只應該是兩個值:GL_VERTEX_SHADER和GL_FRAGMENT_SHADER,表示頂點着色器和片元着色器。
char * source; GLuint shader;
這里聲明了兩個變量。source是着色器的源代碼,shader是着色器對象。
source = loadShaderFromFile(filename); if (source == nullptr) return 0;
因為filename是着色器文件的文件名,所以這里使用loadShaderFromFile()讀取該文件的內容。文件內容被保存在了char指針source里,loadShaderFromFile()將會使用new動態分配一個char數組。如果打開文件失敗,loadShaderFromFile()會返回nullptr。如果source為nullptr,說明加載失敗,loadShader()將會返回0表示加載失敗。
shader = glCreateShader(type); glShaderSource(shader, 1, &source, nullptr); glCompileShader(shader);
glCreateShader()創建一個着色器對象(shader object),並返回其ID。glCreateShader()接受一個參數表示着色器類型,在這個程序里,應該是GL_VERTEX_SHADER和GL_FRAGMENT_SHADER(實際上還可以是更多的值,例如GL_GEOMETRY_SHADER)。glCreateShader()的返回值保存在GLuint變量shader里,表示該着色器對象。着色器對象在后面有時被簡稱為着色器。
從這里開始我們需要注意區分着色器(shader)和着色器程序(shader program)。后者將前者組合起來,這個將在后面討論。
shader雖然已經創建完畢,但它還是空的。使用glShaderSource()給它提供源代碼。glShaderSource()在GLEW中原型如下:
void glShaderSource(GLuint shader, GLsizei count, const GLchar *const *string, const GLint *length);
shader:着色器對象。這里將傳入shader。
count:string包含的字符串個數。我們只用了一個字符串表示着色器源代碼,因此傳入1。
string:一個GLchar二級指針,可以理解為一個字符串數組(數組的每個元素都是一個字符串),組合成着色器源代碼。這里傳入source的地址&source,表示該數組(雖然只有一個元素)。
length:有些復雜,暫不解釋。這里直接傳入nullptr,表示每一個字符串(這里只有一個)都以空字符結尾。
glCompileShader()很簡單,有一個shader參數,它將編譯shader。注意,着色器的編譯和一般編程語言的編譯類似,但有不同。着色器在程序的運行時間(runtime)編譯。
if (!getCompileStatus(shader, false)) { printInfoLog(shader, type); glDeleteShader(type); return 0; }
着色器編譯不一定成功,因為着色器源代碼中可能有錯誤。因此就需要檢查是否編譯成功。getCompileStatus()的第一個參數是一個OpenGL對象(着色器或着色器程序),第二個參數表示該對象是否是着色器程序。這里shader是着色器而不是着色器程序,所以getCompileStatus()的第二個參數,我們傳入false。如果編譯成功,getCompileStatus()就會返回true,否則返回false。如果失敗,使用printInfoLog()函數打印着色器編譯日志,並使用glDeleteShader()刪除該shader,返回0。
delete [] source; return shader; }
加載成功后,delete掉source指向的內存,返回shader。loadShader()函數編寫完成。
char * loadShaderFromFile(const char * filename) {
loadShaderFromFile()用於讀取着色器文件的內容。
std::ifstream fin; int size; char * source;
fin是一個ifstream對象,在后面用於讀取文件內容。size用於記錄文件大小。source是着色器源代碼。
fin.open(filename); if (!fin.is_open()) { cout << "Cannot open shader file " << filename << " (maybe not exist)!\n"; return nullptr; }
用fin打開filename文件。而filename文件可能不存在,因此就要檢查文件是否是打開的。如果不是,說明文件不存在或者存在其它問題,並返回nullptr。
fin.seekg(0, std::ios_base::end); size = fin.tellg(); source = new char[size + 1]{'\0'};
獲得文件大小size(以字節為單位),分配一個有size+1個元素的char數組。之所以是size+1,是因為要為末尾的空字符流出空間。
還有一個值得注意的地方,第二行最后是{'\0'},表示將該數組的每個元素都設為空字符。因為在Windows上,換行符是\r\n兩個字符(size算入了這2個字符),而C/C++讀取時會將\r\n轉換成\n,因此讀取的字符數實際上小於size。如果不初始化為空字符,數組結尾的元素就是隨機的,這會導致glCompileShader()失敗。
fin.seekg(0, std::ios_base::beg); fin.read(source, size);
將文件指針重置到文件頭,然后讀取size個字節(即整個文件)。實際上,前面說過,C/C++讀取文件時,如果文件里有換行,實際讀取的字符數會小於size。但C++遇到EOF(文件尾)時就不會繼續讀取了,所以這樣是安全的。
fin.close(); return source; }
關閉文件,返回讀取到的文件內容。loadShaderFromFile()函數結束。
接下來是makeProgram()函數。
GLuint makeProgram(GLuint vShader, GLuint fShader)
{
makeProgram()接受兩個參數vShader和fShader(表示頂點着色器和片元着色器),鏈接這兩個着色器,創建並返回相應的着色器程序。
if (vShader == 0 || fShader == 0) return 0;
如果任意一個着色器編譯失敗(值為0),則返回0表示失敗。
GLuint program = glCreateProgram();
glAttachShader(program, vShader);
glAttachShader(program, fShader);
glLinkProgram(program);
這幾行代碼應該很直觀。
glCreateProgram()創建一個着色器程序(shader program)。這里使用program保存該ID。
創建完了着色器程序,還不行,因為着色器程序是空的。我們需要使相應的着色器對象與它關聯。glAttachShader(GLuint program, GLuint shader)將shader與program關聯。這里我們調用了兩次glAttachShader(),分別將頂點着色器(vShader)、片元着色器(fShader)和着色器對象關聯。
關聯完着色器后,需要使用glLinkProgram()鏈接着色器程序(這里是program)的着色器對象,這類似於編譯器的鏈接(linking)。編譯器的鏈接將源代碼文件、.lib文件鏈接成一個.exe,OpenGL將着色器鏈接成一個着色器程序。
if (!getCompileStatus(program, true)) { printInfoLog(program, PROGRAM); program = 0; }
注意到這里getCompileStatus()的第二個參數是true,表示program是着色器程序(而不是着色器)。如果鏈接失敗,getCompileStatus()將返回false,這時使用printInfoLog()打印錯誤信息,並將program設為0表示失敗。
這里用到了前面定義的常量PROGRAM。實際上PROGRAM的值只要不同於GL_VERTEX_SHADER和GL_FRAGMENT_SHADER就可以了,不一定要是0(定義為0可以說是習慣)。printInfoLog()的第二個參數傳入PROGRAM表示program是着色器程序,對於着色器程序,獲取日志的方式略有不同。
glDeleteShader(vShader); glDeleteShader(fShader); return program; }
鏈接完畢,兩個着色器就不需要了,因此應該將它們刪除。glDeleteShader()用於刪除着色器。最后返回着色器程序program(如果鏈接出錯,返回0)。
接下來是getCompileStatus()函數。
bool getCompileStatus(GLuint id, bool isProgram) { GLint status; if (isProgram) glGetProgramiv(id, GL_LINK_STATUS, &status); else glGetShaderiv(id, GL_COMPILE_STATUS, &status); return status == GL_TRUE; }
這個函數相對前面的簡單了許多。讓我們看看glGetShaderiv()和glGetProgramiv()的定義:
void glGetShaderiv(GLuint shader, GLenum pname, GLint *param); void glGetProgramiv(GLuint program, GLenum pname, GLint *param);
兩個函數分別用來獲取着色器和着色器程序的一些信息,並且該信息可以用一個整數表達(結尾的iv,i表示GLint,v表示指針)。第一個參數是相應的對象;第二個參數是要獲取的信息類型,對於glGetShaderiv(),GL_COMPILE_STATUS表示着色器編譯情況,對於glGetProgramiv(),GL_LINK_STATUS表示着色器程序鏈接情況。第三個參數是一個GLint指針,用於存儲相應的信息。
對於glGetShaderiv(),pname為GL_COMPILE_STATUS時,*param將為GL_TRUE或GL_FALSE表示編譯是否成功;對於glGetProgramiv(),pname為GL_LINK_STATUS時,*param也是GL_TRUE或GL_FALSE表示鏈接是否成功。因此status為GL_TRUE時,就說明成功。
接下來是倒數第二個函數printInfoLog()。
void printInfoLog(GLuint id, GLenum type) { char * infoLog; int len; if (type == PROGRAM) { glGetProgramiv(id, GL_INFO_LOG_LENGTH, &len); infoLog = new char[len + 1]; glGetProgramInfoLog(id, len + 1, nullptr, infoLog); cout << "Program linking failed, info log:\n" << infoLog << endl; } else { glGetShaderiv(id, GL_INFO_LOG_LENGTH, &len); infoLog = new char[len + 1]; glGetShaderInfoLog(id, len + 1, nullptr, infoLog); cout << "Shader compilation failed, type: " << getShaderName(type) << ", info log:\n" << infoLog << endl; } delete [] infoLog; }
又遇見了glGetShaderiv()和glGetProgramiv()兩個函數。如果第二個參數為GL_INFO_LOG_LENGTH,表示我們要獲取的是着色器程序或着色器的信息日志長度。獲取了這一長度len之后,申請一個長度為len+1的char數組,分別用glGetShaderInfoLog()和glGetProgramInfoLog()獲取相應的日志,並輸出。
void glGetShaderInfoLog(GLuint shader, GLint bufSize, GLsizei *length, GLchar *infoLog); void glGetProgramInfoLog(GLuint program, GLint bufSize, GLsizei *length, GLchar *infoLog);
兩個函數用於獲取信息日志。shader/program為着色器/着色器程序。bufSize為infoLog的長度。length暫不介紹,直接傳入nullptr。infoLog用來存儲信息日志。
最后,printInfoLog()通過判斷id是否等於PROGRAM來判斷id是否是着色器程序。
總算到最后一個函數getShaderName()了。用處就是獲得一種着色器類型的字符串表示,沒什么難的。
const char * getShaderName(GLenum type) { switch (type) { case GL_VERTEX_SHADER: return "vertex"; case GL_FRAGMENT_SHADER: return "fragment"; default: return "UNKNOWN"; } }
呼,shader.cpp總算完結了~
下面是完整的源代碼:
#include "shader.h" #include <iostream> #include <fstream> using std::cout; using std::endl; const int PROGRAM = 0; GLuint loadShader(const char * filename, GLenum type); char * loadShaderFromFile(const char * filename); GLuint makeProgram(GLuint vShader, GLuint fShader); bool getCompileStatus(GLuint id, bool isProgram); void printInfoLog(GLuint id, GLenum type); const char * getShaderName(GLenum type); GLuint loadProgram(const char * vFilename, const char * fFilename) { GLuint vShader = loadShader(vFilename, GL_VERTEX_SHADER); GLuint fShader = loadShader(fFilename, GL_FRAGMENT_SHADER); GLuint program = makeProgram(vShader, fShader); return program; } GLuint loadShader(const char * filename, GLenum type) { char * source; GLuint shader; source = loadShaderFromFile(filename); if (source == nullptr) return 0; shader = glCreateShader(type); glShaderSource(shader, 1, &source, nullptr); glCompileShader(shader); if (!getCompileStatus(shader, false)) { printInfoLog(shader, type); glDeleteShader(type); return 0; } delete [] source; return shader; } char * loadShaderFromFile(const char * filename) { std::ifstream fin; int size; char * source; fin.open(filename); if (!fin.is_open()) { cout << "Cannot open shader file " << filename << " (maybe not exist)!\n"; return nullptr; } fin.seekg(0, std::ios_base::end); size = fin.tellg(); source = new char[size + 1]{'\0'}; fin.seekg(0, std::ios_base::beg); fin.read(source, size); fin.close(); return source; } GLuint makeProgram(GLuint vShader, GLuint fShader) { if (vShader == 0 || fShader == 0) return 0; GLuint program = glCreateProgram(); glAttachShader(program, vShader); glAttachShader(program, fShader); glLinkProgram(program); if (!getCompileStatus(program, true)) { printInfoLog(program, PROGRAM); program = 0; } glDeleteShader(vShader); glDeleteShader(fShader); return program; } bool getCompileStatus(GLuint id, bool isProgram) { GLint status; if (isProgram) glGetProgramiv(id, GL_LINK_STATUS, &status); else glGetShaderiv(id, GL_COMPILE_STATUS, &status); return status == GL_TRUE; } void printInfoLog(GLuint id, GLenum type) { char * infoLog; int len; if (type == PROGRAM) { glGetProgramiv(id, GL_INFO_LOG_LENGTH, &len); infoLog = new char[len + 1]; glGetProgramInfoLog(id, len + 1, nullptr, infoLog); cout << "Program linking failed, info log:\n" << infoLog << endl; } else { glGetShaderiv(id, GL_INFO_LOG_LENGTH, &len); infoLog = new char[len + 1]; glGetShaderInfoLog(id, len + 1, nullptr, infoLog); cout << "Shader compilation failed, type: " << getShaderName(type) << ", info log:\n" << infoLog << endl; } delete [] infoLog; } const char * getShaderName(GLenum type) { switch (type) { case GL_VERTEX_SHADER: return "vertex"; case GL_FRAGMENT_SHADER: return "fragment"; default: return "UNKNOWN"; } }
總結:
創建着色器過程:
1*. 從文件里讀取其源代碼
2. 使用glCreateShader()創建一個着色器
3. 使用glShaderSource()給其提供源代碼
4. 使用glCompileShader()編譯着色器
5*. 檢查編譯是否成功
創建着色器程序過程:
1. 創建好所有的着色器(這里只有頂點着色器和片元着色器)
2. 使用glCreateProgram()創建一個着色器程序
3. 使用glAttachShader()將所有着色器與該着色器程序關聯
4. 使用glLinkProgram()鏈接着色器程序
5. 檢查是否鏈接成功
6. 使用glDeleteShader()刪除所有着色器
(注:有*的步驟表示,該步驟是可選的)
頂點數據
下面我們進入main.cpp。
前面說過,作為輸入,我們會給圖形管線傳入一組數據,叫做頂點數據。頂點數據描述了一組頂點的信息。頂點着色器接受一個頂點作為輸入,這個頂點就來自我們提供了頂點數據。
因為我們這一講要畫一個三角形,所以我們傳入的頂點數據包含了三角形的三個頂點的位置信息。前面說過,頂點着色器中如果聲明了in變量,該變量的數值將會來自頂點數據。這里,頂點着色器的position變量的數據就是來自下面的數組(頂點數據)。我們將其命名為vertexes(意思是頂點)。
const GLfloat vertexes[] = { -0.5f, -0.5f, 0.5f, -0.5f, 0.0f, 0.5f };
因為vertexes數組不需要被修改,因此將其聲明為const。vertexes數組的每一行分別表示三角形每個頂點的x、y坐標。需要注意的是,我們在頂點着色器中,直接把position(來自頂點數據)賦給gl_Position。而gl_Position是NDC坐標,因此position也需要是NDC坐標,進而頂點數據指定的頂點也需要是NDC坐標。在頂點數據中,我們指定了(-0.5, -0.5)、(0.5, -0.5)、(0.0, 0.5)這3個頂點。注意,我們沒有使用二維數組,而是簡單地定義了一個一維的float數組,將每個點的X、Y坐標一個接一個地寫在vertexes數組中。他們(NDC坐標)在屏幕上的位置如下(圖片來自LearnOpenGL):
還有一個要注意的地方,我們提供的頂點數據只包含了頂點的x、y坐標,但是着色器的position變量類型卻是vec4。當我們只提供x、y坐標時,position的z、w分量就會被設置為默認的0.0和1.0。
頂點緩存對象(VBO)和頂點數組對象(VAO)
接下來需要做的事就是將頂點數據傳給圖形管線的第一步——頂點着色器。
我們的頂點數據是這么存儲的:
也就是說:
1. 頂點位置的數據以32位浮點值(float類型)的形式存儲;
2. 每個頂點的數據都占有2個32位浮點值(float類型);
3. 每組(2個)數據表示的都是頂點坐標,它們之間沒有間隔;
4. 數據中的第一個值處於緩存(buffer)的開頭處。
(未完)