着色器
着色器程序看起来确实和C语言非常类似,它们从入口点main函数开始,并且使用同样的字符集和注释约定,以及很多相同的处理指令。着色器是运行在GPU上的小程序。这些小程序为图形渲染管线的一个特定部分而运行。从基本意义上来说,着色器不是别的,只是一种把输入转化为输出的程序。着色器也是一种相当独立的程序,它们不能相互通信;只能通过输入和输出的方式来进行沟通。
- 着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。
- 每个着色器的入口都是main函数,在这里我们处理所有输入变量,用输出变量输出结果。
一个典型的着色器有下面的结构:
#version version_number
in type in_variable_name;
in type in_variable_name;
out type out_variable_name;
uniform type uniform_name;
void main()
{
// 处理输入
...
// 输出
out_variable_name = weird_stuff_we_processed;
}
以《OpenGL编程指南(原书第八版)》第一张的顶点着色器:triangles.vert为例
#version 430 core
layout(location = 0) in vec4 vPosition;
void main()
{
gl_Position = vPosition;
}
第一行“#version 430 core”指定了我们所用的OpenGL着色语言的版本。这里的“430”表示我
准备使用OpenGL4.3对应的GLSL语言。这里的“core”表示我们将使用OpenGL核心模式(core profile)
第二行我们分配了一个着色器变量。着色器变量是着色器与外部世界的联系所在。换句话说
着色器并不知道自己的数据从哪里来,它只是在每次运行时直接获取数据对应的输入变量。
最后,在着色器的main()函数中实现它的柱体部分。对于这个着色器而言,它所实现的就是将输入的顶点位置复制到顶点着色器的指定输出位置gl_Position中。OpenGL所提供的一些着色器变量,它们全部都是以gl_作为前缀的。
上面的//是注释符号,它到当前行的末尾结束,这一点与C语言一致。此外着色器程序也支持C语言形式的多行注释符号(/和/)。但是,与ANSI C语言不同,这里的main()函数不需要返回一个整数值,它被声明为void。
更多的GLSL语法细节,我们在后面用到时再详述。
着色器的编译
OpenGL着色器程序的编写与C语言等基于编译器的语言非常类似。我们使用编译器来解析程序,检查是否存在错误,然后将它翻译为目标代码。然后,在链接过程中将一系列目标文件合并,并产生最终的可执行程序。在程序中使用GLSL着色器的过程与之类似,只不过编译器和链接器都是OpenLG API的一部分而已。下图给出了创建GLSL着色器对象并且通过链接来生成可执行着色器程序的过程。
一般的流程如下:
- 先处理每个着色器对象(创建一个着色器对象->将着色器源代码编译成对象->验证着色器的编译是否成功)
- 链接着色器对象为一个着色器程序(创建一个着色器程序->j将着色器对象关联到着色器程序->链接着色器程序->判断着色器的链接过程是否成功完成->使用着色器来处理顶点和片元)
示例演示
目前我们可以将GLSL编程流程总结为:
1. 向OpenGL传递数据(如上节的glBufferData等工作)
2. 着色器的编译(流程如上)
3. 指定着色器的变量和缓存对象中数据的关系(如glVertexAttribPointer等工作)
这里我们按照该流程,在上一节代码基础上渲染出一个三个点颜色不同的三角形。主要代码如下:
void MyQGLWidget::initShader()
{
glGenVertexArrays(NumVAOs, VAOs);
glBindVertexArray(VAOs[Triangles]);
GLfloat vertices[] = {
// 位置 // 颜色
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 // 顶部
};
glGenBuffers(NumBuffers, Buffers);
glBindBuffer(GL_ARRAY_BUFFER, Buffers[ArrayBuffer]);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(vPosition);
glVertexAttribPointer(vPosition, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(vColor);
glVertexAttribPointer(vColor, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3* sizeof(GLfloat)));
const char* vertexShaderCode =
"#version 430 \n"
""
"layout(location = 0) in vec3 vPosition;"
"layout(location = 1) in vec3 color;"
"out vec3 myColor;"
""
"void main()"
"{"
" gl_Position = vec4(vPosition, 1.0);"
" myColor = color;"
"}";
const char* fragmentShaderCode =
"#version 430 \r \n"
""
"in vec3 myColor;"
"out vec4 fColor;"
""
"void main()"
"{"
" fColor = vec4(myColor, 1.0f);"
"}";
GLuint vertexShaderID = glCreateShader(GL_VERTEX_SHADER);
GLuint fragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER);
const char* adapter[1];
adapter[0] = vertexShaderCode;
glShaderSource(vertexShaderID, 1, adapter, 0);
adapter[0] = fragmentShaderCode;
glShaderSource(fragmentShaderID, 1, adapter, 0);
glCompileShader(vertexShaderID);
glCompileShader(fragmentShaderID);
GLuint programID = glCreateProgram();
glAttachShader(programID, vertexShaderID);
glAttachShader(programID, fragmentShaderID);
glLinkProgram(programID);
glUseProgram(programID);
}
运行结果:
代码分析:
顶点着色器源代码中出现了如下代码:
layout(location = 0) in vec3 vPosition;
layout(location = 1) in vec3 color;
这里layout(location = 0) 或 layout(location = 1)叫做布局限定符,目的是为变量提供元数据(meta data)。我们可以使用布局限定符来设置很多不同的数学。0或1对应glEnableVertexAttribArray和
glVertexAttribPointer的第一参数。