GLSL 是一门类 C 语言,为图形计算量身定制,包含了很多针对向量和矩阵运行的特性。GLSL 的变量分为输入变量、输出变量和 uniform 变量,除了基础的数据类型外,还定义了向量、矩阵等常用数据类型。
前言
上一节我们已经简单地介绍了着色器的用法,这一节以更广泛的方法详细讲解着色器。shader 是运行在 GPU 上的小程序,为图形渲染管线的某个特定部分而运行。shader 只是一种把输入转为输出的程序。每个 shader 都是独立的小程序,它们之间不能无师自通通信,只能通过输入和输出进行沟通,即渲染管线的上一阶段把输出作为下一阶段的输入。
GLSL
OpenGL 着色器是使用一种叫 GLSL 的类 C 语言编写的,GLSL 是为图形计算量身定制的,它包含了很多针对向量和矩阵操作的特性。
GLSL 总是要先声明版本,接着是定义输入和输出变量,然后定义 uniform 变量,最后在 main 函数里把输入变量转换成输出变量,一个典型的着色器有下面的结构。
1 |
|
顶点着色器的输入变量叫顶点属性,我们能声明的顶点属性个数是有限的,一般由硬件决定;OpenGL 保证了至少有 16 个 4 分量的顶点属性可用,硬件支持的个数可能不只 16 个,可以在程序中查询。
1 | GLint max_vertex_attrib; |
数据类型
GLSL 的数据类型除了基本数据类型 int, uint, bool, float, double 这五种之外,还有向量和矩阵,矩阵后面再讲,先说一下向量。向量有一维、二维、三维和四维,其分量类型可以是前面五种基本数据类型之一,总结起来就是:
类型 | 含义 |
---|---|
vecn | n 维 float 型向量 |
dvecn | n 维 double 型向量 |
ivecn | n 维 int 型向量 |
uvecn | n 维 unsigned int 型向量 |
bvecn | n 维 bool 型向量 |
我们可以使用 vec.x vec.y vec.z vec.w
来获取向量的分量,如果是颜色的话也可以使用 rgba
来获取分量,贴图使用 stpq
来获取分量。
向量可以进行很灵活的重组,即根据一个向量生成另一个向量。
1 | vec2 v1 = vec2(0.5f, 0.7f); |
输入变量与输出变量
每个着色器都可以使用 in 关键字来声明输入变量,使用 out 关键字来声明输出变量。着色器会从渲染管线的上阶段接收数据作为输入变量的值,并把输出变量的值给下一阶段使用。如果要使上阶段着色器的输出变量能给下阶段的着色器使用,则上阶段的输出变量和下阶段的输入变量在类型和名称上必须一样。
我们修改之前的着色器,在顶点着色器中声明一个输出变量颜色;然后在片段着色器中声明一个输入变量颜色,接收从顶点着色器传过来的颜色值。
vertex.vs
1 |
|
fragment.fs
1 |
|
uniform
uniform 是 CPU 中的应用程序向 GPU 中的着色器发送数据的一种方式,即在着色器中使用 uniform 声明一个变量,然后在应用程序中赋值。uniform 是全局的,这意味着每个着色器都能使用,所以每个着色器中的 uniform 变量必须是独一无二的。另外,uniform 变量一旦赋值就一直保持着这个值,直到被重置或更新。
接下来修改片段着色器,使用 uniform 变量来声明颜色。
1 |
|
然后在 C++ 代码中给这个变量赋值。
1 | GLint uniformLocation = glGetUniformLocation(shaderProgram, "ourColor"); |
使用 glGetUniformLocation
来获取 uniform 变量的位置,这条语句可以在使用着色器程序之前;然后使用 glUniform 来设置 uniform 变量的值,这条语句必须在使用着色器程序之后。
glUniform
有多个形式的函数,这里设置颜色需要 4 个 float 分量,所以使用 glUniform4f
,也可以使用向量的形式,即 glUniform4fv
,另外还有 glUniform1f, glUniform2d, glUniform2uiv
等等。上面的例子也可以这样写。
1 | GLfloat color[4] = { 0.0f,1.0f,0.0f,1.0f }; |
我们可以让 uniform 变量的值随时间不断不变,从而达到三角形颜色不断变化的效果。
1 | GLint second = clock(); |
你可能看不到变化的效果,那是因为 glut 主回调函数并不是每帧调用一次的,而是当窗口有变化需要重新绘制时才调用,比如窗口大小改变时。可以使用 glutPostRedisplay
函数来立即刷新,在主回调函数绘制完成后调用该函数,就可以实现每一帧重绘。
1 | glClear(GL_COLOR_BUFFER_BIT); |
使用更多属性
到目前为止,我们定义的顶点数组就只有位置一个属性,接下来我们扩展数据,在顶点缓冲区中保存顶点位置和颜色两个属性。
首先,定义顶点数据。
1 | GLuint VBO; |
现在顶点颜色作为顶点属性保存在顶点缓冲区中,所以我们要修改我们的着色器,顶点颜色从顶点属性传给顶点着色器,再由顶点着色器传给片段着色器。
顶点着色器如下。
1 |
|
片段着色器如下。
1 |
|
接下来就是链接顶点属性指针了,现在有位置和颜色两个属性,先看一下顶点数据在缓冲区中的存储方式。
现在两个属性,六个分量,所以每个属性值的步长都是 6 * sizeof(GL_FLOAT)
,位置属性在属性组的起始处,所以偏移值为 0,颜色属性在位置属性之后,偏移值为 3 * sizeof(GL_FLOAT)
。
1 | glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GL_FLOAT), (GLvoid*)0); |
注意最后一个参数的类型是 void,所以要进行类型转换;最后不要忘记开启第二个属性,否则会看不到颜色效果,完整的绘制部分代码。
1 | void render() |
运行效果如下,之所以会有渐变效果,是因为片段着色器会进行片段插值。
封装自己的着色器类
创建着色器、编译着色器,链接着色器是一个复杂又单一的过程,每个程序都要重复这个过程是很痛苦的,我们可以封装一个 Shader 类,专门用于管理着色器。
Shader.h1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Shader
{
public:
Shader() {}
~Shader() {}
int init(GLchar* vertexPath, GLchar* fragmentPath);
void use();
private:
GLuint glProgram;
public:
static const int SUCCESS = 1;
static const int COMPILE_ERROR = 2;
static const int LINK_ERROR = 3;
};
Shader.cpp1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
using namespace std;
int Shader::init(GLchar * vertexPath, GLchar * fragmentPath)
{
//创建着色器
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
//读取着色器文件
ifstream vertexFile, fragmentFile;
stringstream vertexBuffer, fragmentBuffer;
string vertexString, fragmentString;
const GLchar* vertexSource;
const GLchar* fragmentSource;
vertexFile.open(vertexPath, ios::in);
vertexBuffer << vertexFile.rdbuf();
vertexString = vertexBuffer.str();
vertexSource = vertexString.c_str();
fragmentFile.open(fragmentPath, ios::in);
fragmentBuffer << fragmentFile.rdbuf();
fragmentString = fragmentBuffer.str();
fragmentSource = fragmentString.c_str();
//编译着色器
GLint status;
GLchar errInfo[512];
glShaderSource(vertexShader, 1, &vertexSource, NULL);
glCompileShader(vertexShader);
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &status);
if (!status)
{
glGetShaderInfoLog(vertexShader, 512, NULL, errInfo);
cout << errInfo << endl;
return Shader::COMPILE_ERROR;
}
glShaderSource(fragmentShader, 1, &fragmentSource, NULL);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &status);
if (!status)
{
glGetShaderInfoLog(vertexShader, 512, NULL, errInfo);
cout << errInfo << endl;
return Shader::COMPILE_ERROR;
}
//创建着色器程序
this->glProgram = glCreateProgram();
//链接着色器
glAttachShader(glProgram, vertexShader);
glAttachShader(glProgram, fragmentShader);
glLinkProgram(glProgram);
glGetProgramiv(glProgram, GL_LINK_STATUS, &status);
if (!status)
{
glGetProgramInfoLog(glProgram, 512, NULL, errInfo);
cout << errInfo << endl;
return Shader::LINK_ERROR;
}
//删除着色器
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return Shader::SUCCESS;
}
void Shader::use()
{
glUseProgram(glProgram);
}
使用的时候调用 init 函数,把顶点着色器文件路径和片段着色器文件路径传进去,会生成一个着色器程序对象。然后渲染的时候调用 use 函数即可。
1 |
|