【OpenGL】着色器(二)

GLSL 是一门类 C 语言,为图形计算量身定制,包含了很多针对向量和矩阵运行的特性。GLSL 的变量分为输入变量、输出变量和 uniform 变量,除了基础的数据类型外,还定义了向量、矩阵等常用数据类型。

前言

上一节我们已经简单地介绍了着色器的用法,这一节以更广泛的方法详细讲解着色器。shader 是运行在 GPU 上的小程序,为图形渲染管线的某个特定部分而运行。shader 只是一种把输入转为输出的程序。每个 shader 都是独立的小程序,它们之间不能无师自通通信,只能通过输入和输出进行沟通,即渲染管线的上一阶段把输出作为下一阶段的输入。

GLSL

OpenGL 着色器是使用一种叫 GLSL 的类 C 语言编写的,GLSL 是为图形计算量身定制的,它包含了很多针对向量和矩阵操作的特性。

GLSL 总是要先声明版本,接着是定义输入和输出变量,然后定义 uniform 变量,最后在 main 函数里把输入变量转换成输出变量,一个典型的着色器有下面的结构。

1
2
3
4
5
6
7
8
9
10
11
12
#version version_number core

in type in_var
out type out_var

uniform type u_var

int main()
{
...
out_var = ...
}

顶点着色器的输入变量叫顶点属性,我们能声明的顶点属性个数是有限的,一般由硬件决定;OpenGL 保证了至少有 16 个 4 分量的顶点属性可用,硬件支持的个数可能不只 16 个,可以在程序中查询。

1
2
GLint max_vertex_attrib;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &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
2
3
4
5
6
vec2 v1 = vec2(0.5f, 0.7f);
vec4 v2 = v1.xyyx;
vec3 v3 = v2.ywx;
vec4 v4 = v1.xxxx + v3.xyzw;
vec4 v5 = vec4(v1, 0.0f, 0.0f);
vec4 v6 = vec4(1.0f, v3);

输入变量与输出变量

每个着色器都可以使用 in 关键字来声明输入变量,使用 out 关键字来声明输出变量。着色器会从渲染管线的上阶段接收数据作为输入变量的值,并把输出变量的值给下一阶段使用。如果要使上阶段着色器的输出变量能给下阶段的着色器使用,则上阶段的输出变量和下阶段的输入变量在类型和名称上必须一样。

我们修改之前的着色器,在顶点着色器中声明一个输出变量颜色;然后在片段着色器中声明一个输入变量颜色,接收从顶点着色器传过来的颜色值。

vertex.vs

1
2
3
4
5
6
7
8
9
10
#version 440 core

layout (location = 0) in vec3 position;
out vec4 vertexColor;

void main()
{
gl_Position = vec4(position.x, position.y, position.z, 1.0);
vertexColor = vec4(1.0f, 0.2f, 0.3f, 1.0f);
}

fragment.fs

1
2
3
4
5
6
7
8
#version 440 core
in vec4 vertexColor;
out vec4 color;

void main()
{
color = vertexColor;
}

uniform

uniform 是 CPU 中的应用程序向 GPU 中的着色器发送数据的一种方式,即在着色器中使用 uniform 声明一个变量,然后在应用程序中赋值。uniform 是全局的,这意味着每个着色器都能使用,所以每个着色器中的 uniform 变量必须是独一无二的。另外,uniform 变量一旦赋值就一直保持着这个值,直到被重置或更新。

接下来修改片段着色器,使用 uniform 变量来声明颜色。

1
2
3
4
5
6
7
8
9
#version 440 core
out vec4 color;

uniform vec4 ourColor;

void main()
{
color = ourColor;
}

然后在 C++ 代码中给这个变量赋值。

1
2
3
4
GLint uniformLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(uniformLocation, 0.0f, 1.0f, 0.0f, 1.0f);
glDrawArrays(GL_TRIANGLES, 0, 3);

使用 glGetUniformLocation 来获取 uniform 变量的位置,这条语句可以在使用着色器程序之前;然后使用 glUniform 来设置 uniform 变量的值,这条语句必须在使用着色器程序之后。

glUniform 有多个形式的函数,这里设置颜色需要 4 个 float 分量,所以使用 glUniform4f,也可以使用向量的形式,即 glUniform4fv,另外还有 glUniform1f, glUniform2d, glUniform2uiv 等等。上面的例子也可以这样写。

1
2
GLfloat color[4] = { 0.0f,1.0f,0.0f,1.0f };
glUniform4fv(uniformLocation, 1, color);

我们可以让 uniform 变量的值随时间不断不变,从而达到三角形颜色不断变化的效果。

1
2
GLint second = clock();
glUniform4f(uniformLocation, cos(second) / 2 + 0.5, sin(second) / 2 + 0.5, 0.0f, 1.0f);

你可能看不到变化的效果,那是因为 glut 主回调函数并不是每帧调用一次的,而是当窗口有变化需要重新绘制时才调用,比如窗口大小改变时。可以使用 glutPostRedisplay 函数来立即刷新,在主回调函数绘制完成后调用该函数,就可以实现每一帧重绘。

1
2
3
4
glClear(GL_COLOR_BUFFER_BIT);
// ...
glutSwapBuffers();
glutPostRedisplay();

使用更多属性

到目前为止,我们定义的顶点数组就只有位置一个属性,接下来我们扩展数据,在顶点缓冲区中保存顶点位置和颜色两个属性。

首先,定义顶点数据。

1
2
3
4
5
6
7
8
9
10
11
GLuint VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);

GLfloat vertices[][6] =
{
{ -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f },
{ 0.5f, -0.5f, 0.0f , 0.0f, 1.0f, 0.0f },
{ 0.0f, 0.5f, 0.0f , 0.0f, 0.0f, 1.0f }
};
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

现在顶点颜色作为顶点属性保存在顶点缓冲区中,所以我们要修改我们的着色器,顶点颜色从顶点属性传给顶点着色器,再由顶点着色器传给片段着色器。

顶点着色器如下。

1
2
3
4
5
6
7
8
9
10
11
12
#version 440 core

layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;

out vec4 vertexColor;

void main()
{
gl_Position = vec4(position.x, position.y, position.z, 1.0);
vertexColor = vec4(color, 1.0f);
}

片段着色器如下。

1
2
3
4
5
6
7
8
9
#version 440 core

in vec4 vertexColor;
out vec4 fragmentColor;

void main()
{
fragmentColor = vertexColor;
}

接下来就是链接顶点属性指针了,现在有位置和颜色两个属性,先看一下顶点数据在缓冲区中的存储方式。

顶点数据

现在两个属性,六个分量,所以每个属性值的步长都是 6 * sizeof(GL_FLOAT),位置属性在属性组的起始处,所以偏移值为 0,颜色属性在位置属性之后,偏移值为 3 * sizeof(GL_FLOAT)

1
2
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GL_FLOAT), (GLvoid*)0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GL_FLOAT), (GLvoid*)(3 * sizeof(GL_FLOAT)));

注意最后一个参数的类型是 void,所以要进行类型转换;最后不要忘记开启第二个属性,否则会看不到颜色效果,完整的绘制部分代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void render()
{
glClear(GL_COLOR_BUFFER_BIT);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GL_FLOAT), (GLvoid*)0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GL_FLOAT), (GLvoid*)(3 * sizeof(GL_FLOAT)));

glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glUseProgram(shaderProgram);
glDrawArrays(GL_TRIANGLES, 0, 3);
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);

glutSwapBuffers();
}

运行效果如下,之所以会有渐变效果,是因为片段着色器会进行片段插值。

shader

封装自己的着色器类

创建着色器、编译着色器,链接着色器是一个复杂又单一的过程,每个程序都要重复这个过程是很痛苦的,我们可以封装一个 Shader 类,专门用于管理着色器。

Shader.h

1
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
#ifndef __SHADER_H__
#define __SHADER_H__

#include <GL\glew.h>
#include <GL\glut.h>
#include <math3d.h>
#include <fstream>
#include <iostream>
#include <sstream>

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;
};

#endif // !__SHADER_H__

Shader.cpp

1
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
#include "Shader.h"

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
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
#include "Shader.h"

Shader shader;
GLfloat vertices[][6] =
{
{ -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f },
{ 0.5f, -0.5f, 0.0f , 0.0f, 1.0f, 0.0f },
{ 0.0f, 0.5f, 0.0f , 0.0f, 0.0f, 1.0f }
};

bool init();
void render();

int main(int argc, char** argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);

glutInitWindowPosition(400, 400);
glutInitWindowSize(400, 400);
glutCreateWindow(argv[0]);

if (GLEW_OK != glewInit())
{
std::cout << "glew init failded!" << std::endl;
return 1;
}

if (!init())
{
std::cout << "init shader failed!" << std::endl;
return 1;
}

glClearColor(0.2f, 0.3f, 0.5f, 1.0f);
glutDisplayFunc(render);
glutMainLoop();

return 0;
}

bool init()
{
GLuint VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

if (shader.init("vertex.vs", "fragment.fs") != Shader::SUCCESS)
{
return false;
}

return true;
}

void render()
{
glClear(GL_COLOR_BUFFER_BIT);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GL_FLOAT), (GLvoid*)0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GL_FLOAT), (GLvoid*)(3 * sizeof(GL_FLOAT)));

glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
shader.use();
glDrawArrays(GL_TRIANGLES, 0, 3);
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);

glutSwapBuffers();
glutPostRedisplay();
}