咱们已经学会了建立窗口,这一讲,咱们将学习如何使用现代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)的开头处。
(未完)