咱們已經學會了建立窗口,這一講,咱們將學習如何使用現代OpenGL畫一個三角形。在開始寫代碼以前,咱們須要先了解一些OpenGL概念。本文會很長,請你們作好心理準備~ios
注:如下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.五、0.五、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)的開頭處。
(未完)