1. 着色器的含义
着色器是GPU上的小程序,为图形渲染管线的某个特定部分运行。换句话说,着色器就是将输入转化为输出的程序。
着色器是由一种叫做GLSL的类C语言编写成的。
2. GLSL
着色器的开头要声明版本、输入输出、uniform和main函数。比如顶点着色器,它的输入变量叫做顶点属性,我们能声明的顶点属性是有上限的,一般由硬件决定。OpenGL至少确保有16个包含4分量的顶点属性可用。
一个简单着色器如下:
#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;
}
2.1 数据类型
- 默认数据类型 :int float double unit bool
- 容器类型:向量(Vector)和矩阵(Matrix)
2.2 输入输出
每个着色器使用关键字in和out决定输入输出,只要输出变量与下一个着色器的输入相匹配,它就会不断传递下去。顶点着色器与片段着色器会有点不一样。
- 顶点着色器:我们必须使用location这个一元数据去指定输入变量,这样我们才可以在GPU上设置顶点属性。顶点着色器需要为它的输入变量提供额外的layout标识,这样我们才能将它链接到顶点数据。
- 片段着色器:它需要一个vec4颜色输出变量,因为片段着色器最终会生成颜色,如果你没有设置它的输出变量,那么OpenGL就会自动将你的物体渲染为黑色或白色。
所以,如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。
- 顶点着色器
#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;
}
-
最终效果
2.3 Uniform
Uniform是一种从CPU应用向GPU着色器发送数据的一种方式。Uniform是全局的,在某一着色器里声明了它,其他着色器就可以使用它。
比如下面我们将在片段着色器里声明一个Uniform
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量
void main()
{
FragColor = ourColor;
}
下面我们来看一个特殊的函数glUniform4f,4f代表什么?4f其实就是函数需要4个float作为它的值。
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
如下是我们函数名可能有的后缀
下面我们打算当每一次渲染迭代都更新Uniform
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();
}
2.4 从文件流中读取着色器内容
Shader(const char* vertexPath, const char* fragmentPath)
{
// 1. 从文件路径中获取顶点/片段着色器
std::string vertexCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;
// 保证ifstream对象可以抛出异常:
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();
// 转换数据流到string
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();
[...]
3. 顶点缓冲对象(VBO)
3.1 定义
顶点缓冲对象是在显卡里开辟一块内存,用于存储顶点的各类属性信息。在渲染时,可以直接从VBO中取出顶点数据进行渲染,由于顶点数据存储在显卡中而不是内存中,不需要从CPU传输数据,渲染效率更高。
每个VBO在OpenGL里都有对应的ID,这个ID对应VBO的显存地址,通过这个ID可以对VBO的数据进行存取
3.2 VBO的创建和配置
- 开辟显存空间并分配VBO的ID
//创建vertex buffer object对象
GLuint vboId;//vertex buffer object句柄
glGenBuffers(1, &vboId);
- 创建后需要通过分配的ID来绑定VBO,对于同一种类型的顶点数据一次只能绑定一个VBO。
第一个参数指的是绑定的数据类型:GL_ARRAY_BUFFER(顶点数组传值), GL_ELEMENT_ARRAY_BUFFER(索引数组传值), GL_PIXEL_PACK_BUFFER,GL_PIXEL_UNPACK_BUFFER
glBindBuffer(GL_ARRAY_BUFFER, vboId);
- 将用户数据传输到当前绑定的显存缓冲区中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
- 顶点数据传入GPU后,还需要告诉OpenGL如何解析这些顶点数据
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);
第一个参数指定顶点属性位置(与顶点着色器的layout(location=0)有关)
第二个参数指定顶点数据的大小
第三个参数指定顶点数据类型
第四个参数指定顶点数据是否被标准化
第五个参数是步长,指定顶点之间的间隔
第六个参数是位置数据在缓冲区起始的偏移量
顶点属性glVertexAttribPointer默认是关闭的,使用时要以顶点属性位置值为参数调用glEnableVertexAttribArray开启。如glEnableVertexAttribArray(0);
总的来讲,VBO的配置如下:
- 开辟显存空间并分配ID
- 通过分配的ID来绑定VBO
- 传入用户数据到绑定的显存缓冲区BO
- 设置OpenGL如何解析顶点数据
4. 顶点数组对象(VAO)
4.1 优点
当我们使用VBO时,每次绘制模型的时候都需要绑定顶点的所有信息,数据量大的时候就显得十分麻烦。VAO可以将所有的配置信息都保存在对象中,下一次绘制模型的时候直接绑定VAO对象就可以了。
4.2 含义
VAO保存了所有顶点属性的状态集合,它存储了顶点数据的格式以及顶点数据所需的VBO对象的引用.它不能单独使用,都是结合VBO来一起使用的
4.3 VAO的配置
- 生成一个VAO对象并绑定VAOID,之后我们所有关于顶点数据的设置都会被存储在VAO中,在设置完成之后一般会解绑VAO,然后在需要绘制的时候启用相应的VAO对象。
//创建VAO
GLuint VAO;
glGenVertexArrays(1, &VAO);
//设置当前VAO,之后所有操作(注意:这些操作必须是上文VAO中包含的内容所注明的调用,其他非VAO中存储的内容即使调用了也不会影响VAO)存储在该VAO中
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO); //设置了VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);//设置VBO中的数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0); //设置顶点属性(索引为0的属性,与shader中的内容有交互)
glEnableVertexAttribArray(0); //设置开启顶点属性(索引为0的属性,与shader中的内容有交互)
glBindVertexArray(0); //解绑VAO(解绑主要是为了不影响后续VAO的设置,有点类似于C++中指针delete后置空,是个好习惯)
当我们需要绘制的时候
glUseProgram(shaderProgram);
glBindVertexArray(VAO); //绑定我们需要的VAO,会导致上面所有VAO保存的设置自动设置完成
someOpenGLFunctionThatDrawsOurTriangle();
glBindVertexArray(0); //解绑VAO
VAO对象中主要包含以下几个信息:
- VAO开启或关闭的状态(glEnableVertexAttribArray和glDisableVertexAttribArray)
- 使用glVertexAttribPointer设置顶点属性信息
- 存储顶点数据的VBO对象
4. 索引缓冲对象(EBO)
4.1 含义
索引缓冲对象是为了解决同一顶点重复调用的问题,可以减少内存浪费提高执行效率。当需要使用重复顶点的时候,可以通过顶点索引来调用顶点,而不是重复记录。
是不是看起来有点糊里糊涂,下面我们用一个小例子来说明索引缓存对象的用处。
比如我们在绘制一个正方体的时候,它的结构如下:
正方体的顶点只有八个,但是我们在构造它的数组的时候可以发现有许多重复的值, 最好的方式应该是每个顶点只需要存储一次,当我们需要这些顶点时,只需要调用顶点的索引来引用的需要的顶点数据
GLfloat vertices[] = {
//前
-1,-1,1, //v4
1, -1, 1 //v5
1, 1, 1 //v6
-1, 1, 1 //v7
//左
-1,1,-1 //v0
-1,-1,1 //v4
-1,1,1 //v7
-1,1,-1 //v3
......
};
4.2 索引的使用
- 创建EBO(与VBO类似)
GLuint eboID;
glGenBuffers(1, &eboID);
- 绑定EBO,传入索引数据到EBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); //绑定EBO
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//传入索引数据到EBO中
- 绘制几何体(glDrawElements,如果不使用EBO,就是使用glDrawArrays进行绘制)
void glDrawElements(
GLenum mode, //绘制模式,可以是GL_TRIANGLES、GL_POINTS等
GLsizei count, //绘制顶点的次数
GLenum type, //索引数据的类型
const GLvoid * indices //EBO中的偏移量(如果不使用EBO,那么indices指向的是索引数组的指针)
);
当我们使用了EBO后,VAO中也会存储EBO的信息
引用:
OpenGL缓冲区对象之VAO
OpenGL缓冲区对象之EBO
OpenGL图形渲染管线、VBO、VAO、EBO概念及用例
官方文档-着色器