着色器(shader)
是运行在GPU上的小程序,将输入转化为输出。着色器之间不能相互通信,唯一的沟通方式是输入和输出。
着色器使用GLSL
编写(类似C语言),GLSL专为图形计算设计,包含针对向量和矩阵操作的特性。
着色器的开头是声明版本
,其次是输入和输出变量、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;
int main()
{
// 处理输入并进行一些图形操作
...
// 输出处理过的结果到输出变量
out_variable_name = weird_stuff_we_processed;
}
其中顶点着色器的每个输入变量称之为顶点属性(Vertex Attribute)
。能声明的顶点属性是有上限
的(一般由硬件决定)。OpenGL确定至少有16个包含4分量的顶点属性可用,可用通过查询GL_MAX_VERTEX_ATTRIBS
来获取具体的上限:
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
GLSL包含C语言的大部分默认基础类型:int
、float
、double
、uint(unsigned int)
和bool
。GLSL有两种容器类型:向量(vertor)和矩阵(Matrix)
(之后再介绍)。
GLSL中的向量可以是包含2、3或者4分量的容器,分量的类型可以是默认基础类型的任意一个,可以是以下形式(n表示分量的数量):
类型 | 含义 |
---|---|
vecn | n个float分量 |
bvecn | n个bool分量 |
ivecn | n个int分量 |
uvecn | n个unsigned int 分量 |
dvecn | n个double分量 |
一个向量的分量可以通过.x,.y,.z,.w
来分别获取1、2、3、4个分量。GLSL也允许对颜色使用rgba,对纹理坐标使用stpq进行访问获取。
向量允许使用灵活的分量选择方式,称之为重组(swizzling)
。重组允许这样的语法:
vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
也可以把向量作为一个参数传给不同的向量构造函数:
vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);
着色器的输入和输出是数据交流和传递的途径。GLSL定义了in
和out
关键字,每个着色器使用这两个关键字设定输入和输出。
顶点着色器从顶点数据中直接接收输入。使用location
这一元数据指定输入变量,这样才能在CPU上配置顶点属性。顶点着色器需要为其输入设置layout
标识,这样才能链接到顶点数据。(也可以省略layout (location = 0)
,在OpenGL中使用glGetAttribLocation
查询属性位置值,但在着色器中设置更便于理解和减少工作量)。
片元着色器需要一个vec4
颜色输出变量,若没有定义输出,则OpenGL会把物体渲染成黑色(或白色)。
因此,当一个着色器向另一个着色器发送数据时,必须在发送方着色器中声明一个输出,并在接收方着色器声明一个输入,两者的类型和名称都完全一致时,OpenGL会把两个变量链接到一起,他们之间就能发送数据了。
示例如下,在顶点着色器中声明了一个vertexColor
变量作为vec4
输出,并在片元着色器中声明了一个类似的vertexColor
。两者的类型和名称完全一致,两者就会被链接
顶点着色器:
#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0
out vec4 vertexColor; // 为片段着色器指定一个颜色输出
void main()
{
gl_Position = vec4(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数
vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色
}
片元着色器:
#version 330 core
out vec4 FragColor;
in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)
void main()
{
FragColor = vertexColor;
}
Uniform
是一种从CPU应用程序向GPU中的着色器发送数据的方式,uniform与顶点属性不同,一是uniform变量是全局(global)
变量,即uniform变量在每个着色器程序对象中都是唯一的,且可以被任意着色器在任意阶段访问;二是uniform会一直保持数据,直到被重置或更新。
可以在着色器中使用uniform关键字在类型和变量名前声明一个GLSL的uniform变量。
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量
void main()
{
FragColor = ourColor;
}
在片元着色器中声明了一个uniform vec4
类型的ourColor
变量,并设置为片元着色器的输出。
若声明了一个uniform变量,但在GLSL代码中没有使用,编译器会移除这个变量,但可能会导致错误!
为了给uniform变量添加数据,首先需要找到着色器中uniform属性的索引/位置值,然后更新其值。
float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
上述代码首先通过glfwGetTime()
获取运行秒数,然后使用sin函数,让其在0.0-1.0之间改变,最后将结果存储到greenValue
中。接着使用glGetUniformLocation
查询uniform ourcolor
的位置值,若返回-1表示没有找到这个位置值,最后通过glUniform4f
函数设置uniform
的值,在更新uniform
变量之前,必须要激活着色器程序。
OpenGL核心是一个C语言库,不支持类型重载,因此在函数参数不同时,需要定义新的函数,以glUniform
为例,使用不同的后缀表示不同的uniform
类型:
后缀 | 含义 |
---|---|
f | float |
i | int |
ui | unsigned int |
3f | 3个float值 |
fv | float向量 |
当将颜色变化时,需要在每一帧中更新uniform
,如下:
while(!glfwWindowShouldClose(window))
{
// 输入
processInput(window);
// 渲染
// 清除颜色缓冲
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 记得激活着色器
glUseProgram(shaderProgram);
// 更新uniform颜色
float timeValue = glfwGetTime();
float greenValue = sin(timeValue) / 2.0f + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
// 绘制三角形
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
// 交换缓冲并查询IO事件
glfwSwapBuffers(window);
glfwPollEvents();
}
可以将顶点位置信息和颜色信息都存储进顶点数据中,如下:
float 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 // 顶部
};
现在将更多的数据发送到顶点着色器,需要调整定点着色器,使其能够接收颜色值作为一个顶点属性输入,使用layout
将aColor
属性的位置值设置为1:
#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0
layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1
out vec3 ourColor; // 向片元着色器输出一个颜色
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
}
在片元着色器中接收ourColor
颜色输入:
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
void main()
{
FragColor = vec4(ourColor, 1.0);
}
更新后的VBO
内存如下所示:
然后使用glVertexAttribPointer
函数更新顶点格式:
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));
glEnableVertexAttribArray(1);
由于包含了位置和颜色信息,因此这里的步长设置为6个float
,即3个是位置值,3个是颜色值。对于每个顶点来说,位置顶点属性在前,所以偏移量为0,颜色属性在后,偏移量为3*sizeof(float)
。
运行结果如上图所示,因为只提供了三个颜色,在片元着色器中会进行片元插值(Fragment Interpolation)
。当渲染三角形时,光栅化(Rasterization)
会产生比顶点更多的片元,光栅会根据每个片元在三角形形状上所处的相对位置决定这些片元的位置。基于这些位置,会插值所有片元着色器的输入变量,如一个线段上端为绿色,下端为蓝色,那么在距上端70%位置的颜色线性插值为70%蓝色+30%绿色。