本文同时发布在我的个人博客上:https://dragon_boy.gitee.io
请多多参考原文:https://learnopengl.com/Getting-started/Shaders
着色器
就像上篇文章所说,着色器是存储在GPU上的小程序,这些程序会在渲染管线的特定阶段使用。同时着色器是相互独立的,只能通过输入和输出来进行交流。着色器由GLSL编写,下面来详细说明一下。
GLSL
着色器由类似于C的GLSL语言编写,一种专门用来处理图形的语言,包含许多有用的对于向量和矩阵的操作。
一个典型的着色器通常包含版本声明,输入和输出变量声明,在主函数中处理输入和输出:
#version version_number
in type in_variable_name;
in type in_variable_name;
out type out_varialble_name;
uniform type uniform_name;
void main()
{
// 处理输入和一些图形处理
...
// 处理输出
out_variable_name = what_we_processed;
}
在顶点着色器中,每个输入的变量被称为顶点属性。由于硬件的限制,我们可以声明的顶点属性数量是有限制的。OpenGL保证至少有16个4元属性可以使用,当然,我们可以通过以下代码获取最大顶点属性数量:
int nrAttributes;
glGenIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported:" << nrAttributes << std::endl;
数据结构
GLSL拥有许多类C的数据类型:int, float, double, uint, bool,当然,还有两个很重要的数据类型:vector和matrix,向量和矩阵,这里先介绍向量。
向量
在GLSL中,向量有这么几种类型:vecn,bvecn,ivecn,uvecn,dvecn。从缩写就可以知道类型,这里不赘述。大多数时候只需要使用vecn即可。
针对vecn,我们可以通过.x,.y,.z和.w来访问向量的属性,除此之外,对于颜色和贴图信息,可以分别使用rgba和stpq来访问对应的属性。其次,向量类型允许我们混用,大致如下:
vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 another Vec = differentVec.zyw;
vec4 otherVec = someVec.xxx + another.yxzy;
当然,是不允许赋予某一个向量不存在的属性的,例如二维向量不接受.z属性。同时,向量也可以作为属性来组装为新的向量:
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关键字来定义输入和输出。顶点着色器必须要有输入,同时需要layout (location = x)来定义输入数据的位置;片元着色器需要有一个类型为vec4的颜色输出变量。
一个典型的顶点着色器如下:
#version 330 core
layout (location = 0) in vec3 aPos;
out vec4 vertexColor;
void main()
{
gl_position = vec4(aPos, 1.0);
vertexColor = vec4(0.5, 0.0, 0.0, 1.0);
}
一个典型的片元着色器如下:
#version 330 core
out vec4 FragColor;
in vec4 vertexColor;
void main()
{
FragColor = vertexColor;
}
如果我们将上述的着色器代码替换到上篇文章中,结果应该是这样:
Uniform
uniform是另一种不同于in与out的关键字。uniform定义的是全局变量,例如在片元着色器中,我们这样使用uniform:
# version 330 core
out vec4 FragColor;
uniform vec4 ourColor;
void main()
{
FragColor = ourColor;
}
定义之后,这个uniform变量还是空的,为了赋值,我们需要先获取这个变量的位置。下面我们为ourColor赋值,让它随时间变化:
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将值转换为-1到1的范围,接着/2.0f+0.5f将值变换到0到1(颜色的范围);接着通过glGetUniformLocation获取着色器程序中的uniform变量名的位置;在激活着色器程序后就可以通过glUniform4f设置该uniform变量的值了,我们将greenValue赋给G通道。(注意,glUniform后缀4f代表变量的类型,有4个参数,每个都是float类型,其它的类型依此类推)
我们需要将上述代码放到渲染循环中,渲染循环如下:
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);
// 交换缓冲及事件响应
glfwSwapBuffers(window);
glfwPollEvents();
}
运行上述代码会得到类似这样的结果(请自行查看视频)
由上可见,uniform非常适合用来设置每帧都会进行变化的变量。
为顶点添加颜色属性
我们针对上篇文章的代码,为每个顶点添加一个RGB颜色属性:
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;
layout (location = 1) in vec3 aColor;
out vec3 ourColor;
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
}
同位置属性一样,我们为颜色属性定义一个位置,同时将颜色赋值给ourColor输出。
由于增加了颜色属性,VBO中的数据得到了更新,我们需要重新配置一下顶点属性指针。
如上图,我们可以得到颜色,属性的位置,大小,步长,偏移等属性,配置如下:
// 位置属性
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);
运行修改后的代码,可以得到如下结果:
我们明明只设置了三个顶点的颜色,为什么会得到上面的结果呢?就像之前提到过的,在片元着色器之前会有一个光栅化的阶段,之后三角形将有许多许多的像素点组成,每个像素点会根据原来在三角形的位置插值赋予一个颜色,,所以会造成这种混合的效果。
编写Shader类
为了方便接下来的学习,我们将部分复用代码整合起来编写一个Shader类,以方便之后的程序的编写。这里使用C++的一些自带库进行编写。
和大多数头文件的编写一样,我们创建Shader.h文件,并加入以下代码:
#ifndef SHADER_H
#define SHADER_H
#include
#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
实现构造方法:
Shader(const char* vertexPath, const char* fragmentPath)
{
// 1、着色器代码路径获取代码
std::string vertexCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;
// 确保抛出错误
vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
try
{
// 打开文件
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;
// 读取文件缓冲至文件流
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
// 关闭文件
vShaderFile.close();
fShaderFile.close();
// 保存文件流中的代码
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
catch(std::ifstream::failure e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
}
const char* vShaderCode = vertexCode.c_str();
const char* fShaderCode = fragmentCode.c_str();
// 2.编译着色器
unsigned int vertex, fragment;
int success;
char infoLog[512];
// 顶点着色器
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
// 打印错误信息
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if(!success)
{
glGetShaderInfoLog(vertex, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
};
// 片元着色器(同顶点着色器)
[...]
// 着色器程序
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
// 打印错误信息
glGetProgramiv(ID, GL_LINK_STATUS, &success);
if(!success)
{
glGetProgramInfoLog(ID, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
// 删除着色器
glDeleteShader(vertex);
glDeleteShader(fragment);
实现use方法:
void use()
{
glUseProgram(ID);
}
实现设置uniform变量方法:
void setBool(const std::string &name, bool value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}
void setInt(const std::string &name, int value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}
void setFloat(const std::string &name, float value) const
{
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}
在编写完后,在主cpp文件中,我们便可以这样使用Shader类:
Shader ourShader("shader.vs", "shader.fs"); // 注意,文件的后缀名不需要一致,可以随意自定义
... //一些设置后
while(...)
{
... //一些设置后
ourShader.use();
ourShader.setFloat("someUniform", 1.0f);
DrawStuff();
}
这里给出原文的Shader类的代码:Shader.h,以及使用了Shader.h的一个例子的代码:Code。
最后,请多多关注原文内容:https://learnopengl.com/Getting-started/Shaders