正如之前章节所提到的,着色器就是运行在GPU上的小程序,简单来说,着色器就是仅仅是一个将输入数据经过一定转换然后输出的过程,着色器之间是非常独立的,彼此之间除了输入输出之外没有其他交流,这篇文章将会详细介绍着色器以及编写着色器的语言GLSL。
着色器是使用一种类似C语言的语言GLSL编写的,GLSL是为显卡使用而设计的,包含了很多针对向量和矩阵操作的特性。着色器一般以版本声明为开头,紧接着就是一些输入和输出的变量,uniform以及main函数,每一个着色器的入口都是main函数,在main函数中处理输入变量然后将处理的结果输出到输出变量,至于uinform,之后我们会详细介绍,一个典型的着色器的结构如下
#version version_number
in type in_variable_name;
in type in_variable_name;
out type out_variable_name;
uniform type uniform_name;
void main()
{
// process input(s) and do some weird graphics stuff
...
// output processed stuff to output variable
out_variable_name = weird_stuff_we_processed;
}
当提及顶点着色器时我们都知道它的输入变量是顶点属性,硬件支持我们声明的顶点属性的数量有一个最大值,OpenGL规定至少有16个4分量的顶点属性可用,一些硬件可能会提供更多,这个值可以通过GL_MAX_VERTEX_ATTRIBS查询:
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout<< "Maxium nr of vertex attributes supported: " <
与其他编程语言相同,GLSL也拥有数据类型去指定我们使用的数据的类型,GLSL提供了C语言提供的默认基本类型如int, float, double, uint以及bool。GLSL同样提供了两种容器类型vectors和matrices,matrices之后我们再讨论。
GLSL的vector拥有1,2,3或4个之前提到的基本类型的分量,它们的形式有以下几种:
vector分量的获取有很多方式,也非常的灵活,例如vec.x就可以获取vector的第一个分量,使用.x, .y, .z 和.w可以分别获取vector的第一、第二、第三和第四个分量,同样的GLSL也允许通过rgba去获取颜色分量,通过stpq获取纹理的坐标,vector的赋值与计算也非常的灵活,就像这样:
vec2 someVec;
vec4 differentVec;
vec3 anotherVec = differentVec.zyw;
vec4 othorVec = someVec.xxxx + anotherVec.yxzy;
着色器本身是非常小的程序,但是是整个图形管线的一部分,所以每一个着色器都必须有输入和输出,GLSL提供了in和out关键字来指定输入和输出,一个着色器的输出将会是另一个着色器的输入。
顶点着色器需要接受特定形式的输入否则效率会特别低下,而且顶点着色器是直接从顶点数据获取输入,为了解决这个问题,可以通过指定变量的位置就像layout(location = 0)来说明顶点数据是如何组织的。
其实也可以通过glGetAttribLocation省略指定布局(layout(location=0)),但是还是建议写在顶点着色器中,这样易于理解,也可以减少我们和OpenGL的工作。
除了指定输入有时候我们还需要指定输出,例如片段着色器需要输出每一个像素的颜色,如果片段着色器没有输出颜色,OpenGL将会渲染成白色或黑色。所以如果想要从一个着色器向另一个着色器传递数据,我们需要在一个着色器声明输出,在一个着色器声明输入,当变量的类型和名字都相同时,OpenGL将会链接这些数据从而实现数据的传递(这个过程一般是在链接项目对象时实现的),为了说明这个过程,我们通过一下例子说明,在顶点着色器就定义颜色:
顶点着色器
#version 330 core
layout(location = 0) in vec3 aPos;
out vec4 vertexColor;
void main()
{
gl_Postion = vec4(aPos, 1.0);
vertexColor = vec4(0.5, 0.0, 0.0, 1.0);
}
片段着色器
out vec4 FragColor;
in vec4 vertexColor;
void main() {
FragColor = vertexColor;
}
uniform是另一种方式将CPU上的数据传递到GPU的着色器上,uniform与顶点属性差异很大,首先,uniform是全局的。全局则意味着在每一个着色器程序(shader program)中都是唯一的,其次无论给uniform设置了什么值,uniform都会保持这个值除非重置或更新这个值。
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor;
void main() {
FragColor = ourColor;
}
我们需要使用uniform关键字去声明,从上面代码可以看出我们使用在着色器中使用uniform,并且使用uniform定义三角形的颜色,由于uniform是一个全局变量,我们可以在任意的着色器中去定义。
需要注意的是如果声明了一个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函数将greenValue的值设置在0.0到1.0之间,然后通过glGetUniformLocation函数查询uniform变量的位置,其第一个参数是着色器程序,第二个参数是uinform变量的名称,如如果其返回值是-1则说明没有找到位置。然后通过glUniform4f函数来设置uniform变量的值。需要注意的是在查询uniform变量的位置时不需要使用该着色器程序,但是在更新uniform变量的之前需要使用着色器程序(通过调用glUseProgram),这是因为设置uniform变量是在当前活跃的着色器程序。
因为OpenGL是一个C库,所以不支持函数的重载,所以一个函数支持不同的数据类型,就需要为每一个数据类型定义一个新的函数,glUniform函数就是一个很好的例子,这个函数通过不同的后缀来区分不同类型的参数。
- f:入参是一个float
- i:入参是一个int
- ui:入参是一个unsigned int
- 3分:入参是3个float
- fv:入参是一个float类型的vector或array
我们使用的glUniform4f就是需要四个float类型入参。
现在我们已经知道了如何设置uniform变量的值,我们可以使用它们去渲染,如果我们想改变颜色,还需要在每一帧渲染的时候更新uniform,所以我们需要在渲染循环中计算和跟新greenValue。
while(!glfwWindowShouldClose(window)) {
processInput(window);
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
i nt vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);
glfwPollEvents();
}
以上可以看到uinform是为每一帧动态设置属性很好的方法,但是如果我们想要为每一个顶点设置颜色呢,通过以上的方法可以需要为每一个顶点声明一个uniform变量,显然这种方法比较繁琐,更好的方式是在顶点属性中定义更多的数据。
之前的章节我们介绍了如何填充VBO、配置顶点属性指针以及将它们全部存储到VAO中,这一次我们也可以将颜色数据添加到顶点数据中,所以我们需要将3个浮点数颜色数据添加到顶点数组中。
float vertices[] = {
// positions // colors
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // top
};
因为我们需要传递更多的数据给顶点着色器,所以我们需要调整顶点着色器去接受颜色值作为顶点属性的输入,以下代码则是修改后的顶点着色器,可以注意到,我们将颜色属性的layout设置为1。
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
out vec3 ourColor;
void main() {
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
}
由于我们不在片段着色器中使用uniform而是使用ourColor变量设置颜色,所以我们也需要修改片段着色器的内容:
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
void main() {
FragColor = vec4(ourColor, 1.0);
}
因为我们新增了其他的顶点属性,我们需要配置顶点属性指针,在VBO中更新后的数据的组织情况如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tq9nMPUP-1668824944502)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/54a8eb096643428585a03c683f11387b~tplv-k3u1fbpfcp-watermark.image?)]
已经知道了现在数据的布局,我们可以通过glVertexAttribPointer来格式化。
glVertexAttribPointer(0, 3, GL_FLOAT, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_POINTER, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
需要注意的是我们将位置属性的layout设置为0,颜色属性的layout设置为1,颜色属性在位置属性的厚片,位置属性是三个float,所以颜色属性的偏移量就是3 * sizeof(float)。
最后的结果如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6vwocAEY-1668824944502)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5a6b7470da8c4e4ead8cb2f6749522a2~tplv-k3u1fbpfcp-watermark.image?)]
代码如下
# include
# include
# include
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
const unsigned int WIDTH = 800;
const unsigned int HEIGHT = 600;
const char *vertexShaderSource ="#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"layout (location = 1) in vec3 aColor;\n"
"out vec3 ourColor;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos, 1.0);\n"
" ourColor = aColor;\n"
"}\0";
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"in vec3 ourColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(ourColor, 1.0f);\n"
"}\n\0";
int main() {
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
GLFWwindow *window = glfwCreateWindow(WIDTH, HEIGHT, "QStackOpenGL", NULL, NULL);
if (window == NULL) {
std::cout<<"creat window failed"<
编写、编译和管理着色器十分的繁琐,为了方便管理我们通过建立一个着色器的类从硬盘读取着色器,然后编译并且链接它们,检查是否有错误,十分方便使用。
出于学习的目的,我们将着色器的类定义在一个头文件内,其结构如下
#ifndef SHADER_H
#define SHADER_H
#include // include glad to get the required OpenGL headers
#include
#include
#include
#include
class Shader
{
public:
// 项目ID
unsigned int ID;
// 构造函数,读取并且构建着色器
Shader(const char* vertexPath, const char* fragmentPath);
// 使用着色器
void use();
// 使用uniform函数
void setBool(const std::string &name, bool value) const;
void setInt(const std::string &name, int value) const;
void setFloat(const std::string &name, float value) const;
};
#endif
着色器的类需要获取着色器项目的ID,它的构造函数需要顶点着色器和片段着色器源码文件的地址。
这篇文章主要介绍了编写着色器的语言GLSL,更多内容可以关注公众号QStack。