1. 什么是着色器
简单来说,着色器就是运行在 GPU 上的一段程序代码在渲染管线流程中,运行着色器是非常重要的步骤,可以这么理解:CPU 将需要渲染的数据提交到 GPU,着色器将这些数据作为输出,执行着色器中的逻辑,最终输出每个像素的颜色。着色器是 GPU 程序,那么用来编写程序的语言是什么呢?最常见的着色器语言包括三种
- GLSL OpenGL Shading Language
OpenGL 提供的着色器语言,以C语言为基础 - HLSL High Level Shading Language
高级着色语言,由微软开发并提供给微软的 Direct3D 和 XNA 使用,HLSL 是 GLSL 的先辈,不能与 OpenGL 兼容 - CG C for Graphics
转为 GPU 编程设计的高级着色语言,由 Nvidia 公司开发
我们主要看OpenGL 中的 GLSL 着色语言
2. 着色器在什么时候执行
着色器执行是渲染管线中的可编程部分,顶点着色器和片段着色器是分开执行的,在绘制一个物体时,通常情况下GPU 会对物体的每一个顶点调用一次顶点着色器,得到 n 个顶点着色器的输出数据,然后对每一个被物体覆盖到的片段(像素)调用一次片段着色器,它的输入数据是这样得到的:
根据该像素中心点距离物体上覆盖到像素的三个顶点距离进行插值得到,最终片段着色器返回出一个该片段的颜色,进入到下一个渲染阶段
3. GLSL 结构分析
3.1 使用文件中的着色器代码
这篇文章我们主要针对着色器进行分析,而渲染的顶点数据是固定的,为了便于分析,将着色器代码解耦出来,我们添加一个头文件 shader_reader.h
,实现一个读取着色器代码的工具方法
#ifndef SHADER_READER_H
#define SHADER_READER_H
#include
#include
#include
#include
#include
#include
using namespace std;
const GLchar* read_shader(const GLchar* path)
{
stringstream sourceStream;
sourceStream << "../valor/shaders/" << path << ".shader";
ifstream reader;
stringstream shaderStream;
reader.exceptions(ifstream::badbit);
try {
reader.open(sourceStream.str().c_str());
shaderStream << reader.rdbuf();
cout << "read:" << shaderStream.rdbuf() << endl;
reader.close();
}
catch (ifstream::failure e)
{
cout << "exception:" << endl;
}
cout << shaderStream.str() << endl;
const char* result;
result = _strdup(shaderStream.str().c_str());
cout << result << endl;
return result;
}
#endif
新建 shaders 目录并添加最简单的顶点和片段着色器 simplest_vext.shader 和 simplest_frag.shader 文件
# version 330 core
layout (location = 0) in vec3 position;
void main()
{
gl_Position = vec4(position.xyz, 1.0);
}
#version 330 core
void main()
{
// 返回红色
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
使用 [OpenGL]绘制三角形中的三角形绘制代码,只不过将用于编译的着色器字符串由原本写死的改为从文件读取的,渲染部分的代码:
int draw()
{
// 初始化 OpenGL
init_opengl();
const GLchar* vertexShader = read_shader("simplest_vert");
const GLchar* fragmentShader = read_shader("simplest_frag");
// 编译和链接着色器
GLuint shaderProgram = compile_shader(vertexShader, fragmentShader);
// 顶点数据
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f, // Left
0.5f, -0.5f, 0.0f, // Right
0.0f, 0.5f, 0.0f // Top
};
// 申请缓冲区
GLuint VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// 绑定VAO,表示
glBindVertexArray(VAO);
// 提交数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 使用Buffer中的数据在 VAO 生成 0 号顶点属性的指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
// 启用 0 号顶点属性
glEnableVertexAttribArray(0);
// 解绑VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 解绑VAO
glBindVertexArray(0);
while (!glfwWindowShouldClose(window))
{
// 处理事件
glfwPollEvents();
// 清屏
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 指定着色器程序
glUseProgram(shaderProgram);
// 绑定VAO
glBindVertexArray(VAO);
// 绘制指令
glDrawArrays(GL_TRIANGLES, 0, 3);
// 解绑 VAO
glBindVertexArray(0);
// 双缓冲交换
glfwSwapBuffers(window);
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glfwTerminate();
return 0;
}
使用读取的着色器代码来进行绘制,得到了一个红色的三角形:
3.2 分析着色器结构
典型的着色器结构如下
#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;
}
-
version 声明着色器版本号
-
in
表示着色器的输入变量,顶点着色器输入的是顶点属性数据,通常还会通过制定layout(location=n)
的方式来指定该变量读取的是哪一个顶点属性,片段着色器的输入是顶点着色器的输出数据 -
out
表示着色器的输出,顶点着色器中输出的通常是顶点的裁剪坐标、顶点颜色、uv 等信息,片段着色器输出的是该片段的最终颜色值 -
uniform
是一种从 CPU 向GPU着色器发送数据的方式,uniform是全局的,所有的着色器程序对象共享的数据,可以被着色器程序的任意着色器在任意阶段访问,并且无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被 CPU 端重置或更新
3.3 在着色器中加入顶点颜色
其实在之前的文章 [OpenGL]VBO,VAO和EBO详解我们已经使用了顶点的颜色数据,这里我们修改一下着色器程序,使他支持顶点颜色数据:
# version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec4 color;
out vec4 vert_color;
void main()
{
gl_Position = vec4(position.xyz, 1.0);
vert_color = color;
}
顶点着色器所做的修改包括:
- 增加
in
输入变量 color,指定使用 1 号顶点属性 - 增加
out
输出变量,将 color 直接返回
#version 330 core
in vec4 vert_color;
void main()
{
gl_FragColor = vert_color;
}
片段着色器所做的修改包括:
- 增加一个输入变量,接收顶点着色器输出的值
- 将顶点颜色作为像素的颜色直接返回
三角形绘制代码,主要是增加了颜色缓冲区数据的处理,并为 VAO 提供颜色数据的解析
int draw()
{
// 初始化 OpenGL
init_opengl();
const GLchar* vertexShader = read_shader("vertColor_vert");
const GLchar* fragmentShader = read_shader("vertColor_frag");
// 编译和链接着色器
GLuint shaderProgram = compile_shader(vertexShader, fragmentShader);
// 三角形1顶点数据
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f, // Left
0.5f, -0.5f, 0.0f, // Right
0.0f, 0.5f, 0.0f, // Top
};
// 颜色数据
GLfloat colors[] = {
1.0f, 0.0f, 0.0f, 1.0f,
0.0f, 1.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f, 1.0f
};
// 申请缓冲区
GLuint VBO, ColorVBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &ColorVBO);
// 绑定VAO
glBindVertexArray(VAO);
// 提交数据和解析规则
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, ColorVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(1);
// 解绑VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 绑定VAO
glBindVertexArray(VAO);
// 解绑VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 解绑VAO
glBindVertexArray(0);
while (!glfwWindowShouldClose(window))
{
// 处理事件
glfwPollEvents();
// 清屏
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 指定着色器程序
glUseProgram(shaderProgram);
// 绑定VAO
glBindVertexArray(VAO);
// 绘制指令
glDrawArrays(GL_TRIANGLES, 0, 3);
// 解绑 VAO
glBindVertexArray(0);
// 双缓冲交换
glfwSwapBuffers(window);
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glfwTerminate();
return 0;
}
得到的渲染结果:
3.4 在着色器代码中加入 uniform 变量
保持顶点着色器不变,修改片段着色器代码,增加一个 float 类型的 uniform 变量,并将返回颜色的3个分量都设置为该变量的值:
#version 330 core
in vec4 vert_color;
uniform float r;
void main()
{
gl_FragColor = vec4(r, r, r, 1.0);
}
uniform 类型的变量需要我们从 CPU 端传递给 GPU,我们考虑这样的逻辑:将鼠标屏幕坐标的 x 分量映射到 [0, 1]之间,然后通过 uniform 变量传递给这段片段着色器代码,以返回不同的程度的灰色,在渲染循环中增加如下的逻辑来获取鼠标的坐标
// 获取光标坐标
GLdouble xpos, ypos;
glfwGetCursorPos(window, &xpos, &ypos);
那么该如何实时的将 uniform 变量传递到 GPU 端对应的着色器程序呢?使用 glGetUniformLocation
返回某个 uniform 变量的位置,需要提供着色器程序和变量名,拿到位置后,使用 glUniform
系列的方法将参数传递过去
// 指定着色器程序
glUseProgram(shaderProgram);
// 获取变量在着色器中的位置
GLint rColorLocation = glGetUniformLocation(shaderProgram, "r");
// 往对应位置传递参数
glUniform1f(rColorLocation, xpos / WIDTH);
渲染结果如下图所示,当在水平方向移动鼠标时,三角形的颜色将发生变化
3.5 将三角形上下倒置
只需要在顶点着色器中,将返回的顶点坐标 y 分量取反(归一化坐标 y 方向取值范围是 [-1, 1]):
# version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec4 color;
out vec4 vert_color;
void main()
{
gl_Position = vec4(position.x, 0 - position.y, position.z, 1.0);
vert_color = color;
}
倒置后的三角形