OpenGL自制3D游戏引擎入门V
Shader I
一、概述
- 着色器(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 Shader 的时候,每个输入变量叫做顶点属性(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;
三、数据类型
基础数据类型:int,float,double,uint,bool 两种容器类型Vertex,Matrix
向量是一个可以包含多个分量的容器,有以下一些类型
- vecn 包含n个float分量的向量
- bvecn 包含n个bool分量的向量
- ivecn uvecn dvecn以此类推
向量的分量可以通过 vec.x 这种方式取得,$.x\ .y\ .z\ .w$分别取得其中第一、二、三、四个分量
(GLSL允许对颜色使用rgba,对纹理使用stpq来访问对应的分量)向量允许灵活的分量选择方式,这种方式叫做重组(Swizzling)(Tip:但不允许vec2.z这样的分量获取方式)
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 关键字设定输入输出,但在顶点和片段着色器中会有不同表现
顶点着色器
#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;
}
- 可以看到顶点着色器的输入比较特殊,它从顶点数据中直接接受输入,为了定义顶点数据该如何管理,我们使用 location 这一元数据指定输入变量,这样我们就可以在CPU上配置顶点属性,顶点着色器需要为它提供一个额外的 layout标识,这样我们才能把它链接到顶点数据
- 另外一个例子是片段着色器,它需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终的颜色,如果不在片段着色器中定义输出颜色,OpenGL会把物体渲染为黑色或白色
- 如果我们打算从一个着色器发送数据,我们必须在发送着色器中声明一个输出,在接收方着色器中声明一个类似的输入,当类型和名字都一样的时候,OpenGL会将两个变量链接到一起,它们之间就能发送数据(见上例 vertexColor 变量)
四、Uniform
除了VAOs能够从CPU向GPU输入信息,uniform也是一种从CPU中的应用向GPU中的着色器发送数据的方式
uniform和顶点属性有些不同,uniform是全局的,全局则意味着uniform变量在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问,同时uniform在被设置数值后会一直保存它们的数据直至被重置或更新
我们可以在一个着色器中添加uniform关键字至变量名前来声明一个GLSL的uniform
#version 330 core out vec4 FragColor; uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量 void main() { FragColor = ourColor; }
Tip:如果声明了一个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);
- 使用 glGetUniformLocation 函数查询 uniform ourColor 的位置,只要提供需要查询的着色器程序和uniform名字即可(返回值为-1时代表没有找到这个位置值)
- 使用 glUniform4f 函数设置 uniform 值(Tip:查询uniform地址不要求之前使用过着色器程序,但更新一个uniform前必须调用 glUseProgram 函数使用程序)
- OpenGL核心是一个C库,不支持重载,因此函数参数不同时需要为其定义新的函数,glUniform函数就是一个经典的例子,这个函数有特定的后缀:
- f 函数需要一个float作为它的值
- ui 函数需要一个unsigned int作为它的值
- 4f 函数需要四个float作为它的值
- fv 函数需要一个向量/数组作为它的值
- 以此类推
在循环中实现颜色改变
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 // 顶部 };
可以看到现在既要发送顶点位置,又要发送颜色信息,所以需要调整顶点着色器
#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设置为我们从顶点数据那里得到的输入颜色 }
由于不再使用uniform传递片段颜色,现在使用 ourColor 输出变量,所以需要调整片段着色器
#version 330 core out vec4 FragColor; in vec3 ourColor; void main() { FragColor = vec4(ourColor, 1.0); }
由于新增了颜色信息,使用更新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);