OpenGL教程(3)——第一個三角形

咱們已經學會了建立窗口,這一講,咱們將學習如何使用現代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)的開頭處。

 

(未完)

相關文章
相關標籤/搜索