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

我们已经学会了创建窗口,这一讲,我们将学习如何使用现代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)!
";
    return nullptr;
}


用fin打开filename文件。而filename文件可能不存在,因此就要检查文件是否是打开的。如果不是,说明文件不存在或者存在其它问题,并返回nullptr。

fin.seekg(0, std::ios_base::end);
size = fin.tellg();
source = new char[size + 1]{''};


获得文件大小size(以字节为单位),分配一个有size+1个元素的char数组。之所以是size+1,是因为要为末尾的空字符流出空间。


还有一个值得注意的地方,第二行最后是{''},表示将该数组的每个元素都设为空字符。因为在Windows上,换行符是 两个字符(size算入了这2个字符),而C/C++读取时会将 转换成 ,因此读取的字符数实际上小于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:
" << 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:
" << 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)!
";
        return nullptr;
    }
    fin.seekg(0, std::ios_base::end);
    size = fin.tellg();
    source = new char[size + 1]{''};
    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:
" << 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:
" << 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)的开头处。

(未完)

原文地址:https://www.cnblogs.com/collectionne/p/6618448.html