四、OpenGL 简单开始 --- 顶点着色器(shader)
上一篇:三、OpenGL 简单开始 --- 简单三角形绘制(vertex buffer & shader pre)
1. 什么是着色器?
着色器(Shader)是用来实现 图像渲染 的,用来替代固定渲染管线的可编辑程序。 其中如 Vertex Shader(顶点着色器)主要负责顶点的几何关系等的运算,Pixel Shader(像素着色器)主要负责片元颜色等的计算。简单讲就是:一个程序,负责告诉 GPU 我的这些数据怎么用,如如何进行计算位置、光照、阴影等渲染工作。
2. 两个着色器的工作
两个渲染管线中的基本着色器(还有一些扩展的,如曲面细分着色器、几何着色器):
- Vertex Shader(顶点着色器)主要负责顶点的几何关系等的运算。
- Pixel Shader(像素着色器)主要负责片元颜色等的计算。
2.1 两者关系
顶点着色器 处理后的数据为 顶点数据(带有其多个属性:位置、法线、颜色......),交给 光栅化 找出顶点围成三角形内部的 像素(或者说 片元),然后把这些像素信息交给 像素着色器,其负责给这些像素进行着色计算。
2.2 两者区别
- 顶点着色器 为每个顶点进行计算,如我们提供了三个点,则调用三次来计算顶点的属性;
- 像素着色器 为每个像素(片元) 计算,三个点围成的三角形内可能有很多个像素(成百上千),每个都要调用,这里也可以看出花销很大。
3. 代码参考
GL 的 API 参考地址。
主要学习:
- 与着色器绑定的程序对象
glCreateProgram、glAttachShader、glLinkProgram、glValidateProgram、glDeleteShader、glUseProgram、glDeleteProgram。
- 着色器相关
glCreateShader、glShaderSource、glCompileShader。
- 着色器错误处理
glGetShaderiv、glGetShaderInfoLog。
- 画出三角形
glDrawArrays。
3.1 生成着色器函数部分
/* ------------------------- 着色器 ------------------------- (static 限定在文件)*/
/* ~~~~~~ 编译着色器 ~~~~~~ */
// 返回值:返回一个编号值用于绑定
// 参数值:type 是着色器类型,string 接一下源代码
static unsigned int CompileShader(unsigned int type, const string& source)
{
// 1. Creates a shader object (指定着色器类型:vertex shader)
unsigned int id = glCreateShader(type);
// 不可变的字符数组指针
const char* src = source.c_str();
// 2. 着色器源代码替换
glShaderSource(
id, // 指定着色器
1, // 指定字符串数组和长度数组中元素的个数。
&src, // 指定指向字符串的指针数组,字符串中包含要加载到着色器中的源代码。
// 指定字符串长度的数组
// 如果 length 为 NULL,则每个字符串都被假定为空值结束。
// 如果 length 值不是 NULL,它将指向一个数组,其中包含字符串中每个相应元素的字符串长度。
nullptr
);
// 3. 编译 正式生成 新 Shader
glCompileShader(id);
// 4. 错误处理
int result;
glGetShaderiv(id, GL_COMPILE_STATUS, &result); // 获取错误状态
if (result == GL_FALSE)
{
int length;
glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length); // 获取错误信息长度
char* message = (char*)malloc(length * sizeof(char)); // 申请同大小堆空间,存放错误信息
glGetShaderInfoLog(id, length, &length, message); // 根据长度,空间,程序提取出 错误信息
// 输出
cout << "编译"<<
(type == GL_VERTEX_SHADER ? "顶点" : "片元")
<< "着色器失败!" << endl;
cout << message << endl;
return 0;
}
// 返回着色器编号
return id;
}
/* ~~~~~~ 编译着色器 ~~~~~~ */
/* ~~~~~~ 创建着色器 ~~~~~~ */
// 返回值:返回一个编号值用于绑定
// 参数值:这里用 string 接一下源代码,也可用 二进制文件
static int CreateShader(const string& vertextShader, const string& fragmentShader)
{
// 1. Creates a program object (程序对象是一个可以附加着色器对象的对象)
unsigned int program = glCreateProgram();
// 2. 编译着色器 (指定着色器类型,源代码)
unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertextShader);
unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);
// 3. 附加着色器对象
glAttachShader(program, vs);
glAttachShader(program, fs);
// 4. 链接程序对象(被用于创建在之前附加的着色器上运行的可执行文件)
glLinkProgram(program);
// 5. 检查程序中包含的可执行文件是否能在当前 OpenGL 状态下执行。
glValidateProgram(program);
// 6. 绑定生成后,删除之前临时的文件 (不用 detach 原因:留下的源代码可执行文件占用空间少,而且经常用)
glDeleteShader(vs);
glDeleteShader(fs);
// 返回程序对象
return program;
}
/* ~~~~~~ 创建着色器 ~~~~~~ */
/* ------------------------- 着色器 ------------------------- */
3.2 着色器使用部分
int main(void)
{
..................
..................
/* ------------------------- Draw A Triangle ------------------------- */
// !!!!!!!!!!! 数据 !!!!!!!!!!!
float posintions[6] = {
-0.5f, -0.5f,
0.0f, 0.5f,
0.5f, -0.5f
};
// !!!!!!!!!!!数据 !!!!!!!!!!!
// !!!!!!!!!!! 缓冲区 !!!!!!!!!!!
..................
// !!!!!!!!!!! 缓冲区 !!!!!!!!!!!
// !!!!!!!!!!! 顶点属性布局 !!!!!!!!!!!
..................
// !!!!!!!!!!! 顶点属性布局 !!!!!!!!!!!
// !!!!!!!!!!! 着色器 !!!!!!!!!!!
// 源代码
string vertexShader =
"#version 330 core\n"
"\n"
"layout(location = 0) in vec4 position;\n" // 顶点属性索引 0, 存放在四维数组位置处(跟齐次坐标有关)
"\n"
"void main()\n"
"{\n"
" gl_Position = position;\n"
"}\n";
string fragmentShader =
"#version 330 core\n"
"\n"
"layout(location = 0) out vec4 color;\n"
"\n"
"void main()\n"
"{\n"
" color = vec4(1.0, 0.0, 0.0, 1.0);\n"
"}\n";
// 创建
unsigned int shader = CreateShader(vertexShader, fragmentShader);
// 使用
glUseProgram(shader);
// !!!!!!!!!!! 着色器 !!!!!!!!!!!
/* ------------------------- Draw A Triangle ------------------------- */
/* Loop until the user closes the window */
while (!glfwWindowShouldClose(window))
{
/* Render here */
glClear(GL_COLOR_BUFFER_BIT);
// 画一下
glDrawArrays(GL_TRIANGLES, 0, 3);
..................
}
// 清除着色器程序对象
glDeleteProgram(shader);
glfwTerminate();
return 0;
}
结果如下:

片元着色器的源代码内故意忘写“;”, 测试错误处理结果:

4. 包装(文件形式读取源代码)
4.1 创建新文件夹与新文件。

4.2 文件内书写之前的着色器源代码 (修改了一下颜色:蓝色);用 #shader vertex 标识具体属于哪个着色器(顶点 or 像素)。
#shader vertex
#version 330 core
layout(location = 0) in vec4 position;
void main()
{
gl_Position = position;
};
#shader fragment
#version 330 core
layout(location = 0) out vec4 color;
void main()
{
color = vec4(0.2, 0.3, 0.8, 1);
};
4.3 代码修改
4.3.1 新建存放两个着色器字符串源码的结构体
// 程序源码(同时返回多个参数的载体)
struct ShaderProgramSource
{
string VertexSource;
string FragmentSource;
};
4.3.2 新增函数文件内提取源码
记得包含头文件 #include <fstream>、#include <string>、#include <sstream>。
// 提取源码(多个字符串返回)
static ShaderProgramSource ParseShader(const string& filePath)
{
// 着色器枚举类
enum class ShaderType
{
NONE = -1, VERTEX = 0, FRAGMENT = 1
};
// 默认类型定义
ShaderType type = ShaderType::NONE;
// 文件流输入流
ifstream stream(filePath);
// 字符串输入输出流 (字符串数组 大小为 2)
stringstream ss[2];
// 按行获取文件内容
string line;
while (getline(stream, line))
{
// 获取着色器类型
if (line.find("#shader") != string::npos)
{
if (line.find("vertex") != string::npos)
type = ShaderType::VERTEX;
else if (line.find("fragment") != string::npos)
type = ShaderType::FRAGMENT;
}
else
{
// 写入字符串数组中
ss[int(type)] << line << '\n';
}
}
return { ss[0].str(), ss[1].str() };
}
/* ~~~~~~ 提取着色器源代码 ~~~~~~ */
4.3.3 主函数体修改获取源代码方式
......
// !!!!!!!!!!! 着色器 !!!!!!!!!!!
// 源代码
ShaderProgramSource source = ParseShader("res/shaders/Basic.shader");
// 创建
unsigned int shader = CreateShader(source.VertexSource, source.FragmentSource);
// 使用
glUseProgram(shader);
// !!!!!!!!!!! 着色器 !!!!!!!!!!!
......
4.4 结果图


浙公网安备 33010602011771号