1. OpenGL图形管道简介
-
OpenGL图形管道(graphics pipeline)可以简单分成两大部分:
-
- 将3D坐标(coordinates)转换为2D坐标。
-
- 将2D坐标转换为实际的颜色像素。
-
OpenGL渲染管道每个步骤在GPU上运行的小程序我们成为着色器(shader)。着色器是使用OpenGL着色语言(OpenGL shading Language, GLSL)编写的。
-
OpenGL的图形管道:下图中管道的蓝色区域是可以插入我们自己的着色器的地方。(图片截取自书中)
- 输入:一个顶点数据集合。
-
- 顶点着色器(vertex shader):转换3D坐标,允许我们对顶点属性做基本处理。
-
- 基元组装(primitive assembly):组装顶点形成基元图形(primitive shape)(图中为一个三角形)。
-
- 几何着色器(geometry shader):可通过生成新的顶点来形成新的基元图形。
-
- 光栅化阶段(rasterization stage):将基元映射为最终屏幕的像素点,产生片元(fragment)。在进入片元着色器前可能执行裁剪操作,丢弃视口外的片元。
-
- 片元着色器(fragment shader):计算像素点最终颜色值,这里一般是OpenGL进行高级处理的地方。
-
-
alpha测试和混合阶段:检查片元的深度,确定对象的前后位置或是否需要丢弃,检查alpha值进行合适的混合操作。
-
顶点(vertex) 就是3D坐标数据的集合,顶点数据由顶点属性(vertex atrributes) 表示。最简单的顶点可以看作是一个3D位置和一些颜色值。
片元(fragment):在OpenGL中片元是渲染一个像素点需要的全部数据。
现代OpenGL中,我至少需要自己定义一个顶点和片元着色器,因为GPU中没有默认的顶点/片元着色器。
2. OpenGL图形管道编程
2.1 顶点输入
- OpenGL只处理范围在-1.0到1.0之间的3D坐标——称为标准化设备坐标(normalized device coordinates),只有在该范围内的坐标才会显示在屏幕上。
// 一个三角形的顶点数据,因为是一个2D三角形,所以z坐标都为0
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
- 顶点缓冲区对象(vertex buffer objects, VBO):在GPU显存中管理和存储顶点数据。顶点缓冲区对象在OpenGL中是
GL_ARRAY_BUFFER
缓冲区类型。
// 创建顶点缓冲区对象
unsigned int VBO;
glGenBuffers(1, &VBO);
// 绑定顶点缓冲区对象到GL_ARRAY_BUFFER类型的缓冲区
glBindBuffer(GL_ARRAY_BUFFER, VBO);
- 当我们将顶点缓冲区绑定到OpenGL
GL_ARRAY_BUFFER
类型的缓冲区上,后续该类型目标缓冲区的调用都将作用于我们创建的顶点缓冲区——VBO。下面是一个拷贝顶点数据到缓冲区的操作:
// vertices是我们前面创建的顶点数据
// glBufferData: 拷贝用户定义的数据到当前绑定的缓冲区
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
- 图形卡管理数据的3种方式:
-
-
GL_STREAM_DRAW
:数据只设置一次,GPU最多使用几次。
-
-
-
GL_STATIC_DRAW
:数据只设置一次,使用多次。
-
-
-
GL_DYNAMIC_DARW
:数据经常变动且会使用多次
-
-
2.2 顶点着色器(vertex shader)
- 一个简单的顶点着色器(只包含位置数据)。
// 版本:OpenGL 3.3, 核心模式
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
1. 每个着色器都从版本声明开始
2. in关键字:声明输入顶点属性。(示例只有位置信息)
3. 顶点着色器通过将位置数据设置给预定义变量`gl_Position`来设置着色器的输出。
- 创建和编译着色器步骤:
-
- 将着色器源码存放到字符串变量中(一般存放在独立文件读取,这里为了简单采用这种方法)
const char* vertexShaderSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" "}\0";
-
- 创建着色器对象
unsigned int vertexShader; vertexShader = glCreateShader(GL_VERTEX_SHADER);
-
- 将源码附加到着色器对象并编译
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader);
-
- 错误检查
int success; char infoLog[512]; glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; }
-
2.3 片元着色器(fragment shader)
- 计算机图形学中颜色一般由4个值的数组代表:红,绿,蓝和alpha(透明度)四个组成部分,通常缩写为RGBA。在OpenGL中,我将四个组成部分设置为0.0到1.0之间的值。
- 片元着色器只需要一个定义最终颜色的输出变量,该变量是一个维度为4的矢量,需要我们自行计算。一个简单的片元着色器程序:
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
1. 使用`out`关键字定义输出变量。
- 片元着色器的创建和编译过程与顶点着色器类似:
const char* fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\0";
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}
- 一个着色器程序对象(shader program object)就是多个着色器结合链接到一起的最终版本。激活的着色器程序对象将在调用渲染操作时使用。创建着色器程序对象和连接着色器如下代码所示:
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// 着色器程序对象错误检查
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINK_FAILED\n" << infoLog << std::endl;
}
// 链接完着色器程序我们可以直接删除着色器对象
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
- 使用着色器程序对象(一般在渲染循环中绘制前调用):
glUseProgram(shaderProgram);
2.4 链接顶点属性
- 顶点着色器允许我们以顶点属性的形式指定输入数据,这意味着需要我们在顶点着色器中手动指定输入数据与定点属性之间的对应关系,相当于告诉OpenGL应该如何解释(interpret)我们的输入数据。这通过使用
glVertexAttribPointer
函数来实现。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
1. 第一个参数指定我们要配置的顶点属性,对应我们在顶点着色器中指定的`location`。
2. 第二个参数指定顶点属性的大小。
3. 第三个参数指定数据的类型。
4. 第四个参数指定是否标准化(normalized)数据。
5. 第五个参数为步长(stride),指定连续顶点属性之间的间距。
6. 最后一个参数是缓冲区中数据的偏移量(offset)。
每个顶点属性(vertex attribute)都从VBO管理的内存中获取数据,具体从哪一个VBO中获取数据,取决于调用
glVertexAttribPointer
方法时当前绑定的VBO。顶点数组对象(vertex array object, VAO) 可以像顶点缓冲区对象一样进行绑定,当我们绑定了顶点数组对象后,后续所有顶点属性相关的调用都会存储在VAO中。这可以让我们只进行一次顶点属性配置,然后在绘制对象时绑定我们配置好的VAO即可。
OpenGL核心渲染模式要求我们至少使用一个顶点数组对象(VAO)。
顶点数组对象的创建与VBO类似:
unsigned int VAO;
glGenVertexArrays(1, &VAO);
- 最后图形(三角形)的绘制(在渲染循环中):
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
-
最终界面显示效果:
2.5 元素缓冲区对象
- 元素缓冲区对象(element buffer object, EBO):是一个类似顶点缓冲区对象的缓冲区,存储OpenGL用于决定绘制那个顶点的索引。这也叫做索引绘制(indexed drawing)。
- 元素缓冲区对象的创建和绑定
// 顶点数据
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f,
-0.5f, 0.5f, 0.0f
};
// 索引数据
unsigned int indices[] = {
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
// 1. 创建元素缓冲区对象
unsigned int EBO;
glGenBuffers(1, &EBO);
// 2. 绑定元素缓冲区对象
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
// 3. 拷贝索引数组到元素还钱给OpenGL使用
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
- 使用元素缓冲区对象绘制图形(在渲染循环中):
// 设置线框模式(wireframe mode)
//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
// 绘制元素
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
-
运行界面显示效果
3. 个人小结
-
OpenGL绘图的基本流程
-
OpenGL对象
-
OpenGL对象中VBO、VAO和EBO之间的关系(图片取自书中)
- 顶点缓冲区对象(VBO):管理GPU上的一块用于存放顶点数据的显存。用于存放进入OpenGL渲染管道的顶点数据。
- 顶点数组对象(VAO):存放顶点属性信息,需与对应的VBO关联。用于告诉OpenGL,VBO上的顶点数据应该如何解释(如每个顶点数据的长度,间距等)。内部同时保存一个EBO对象,因此绑定VAO会自动绑定对应的EBO。
- 元素缓冲区对象(EBO):类似于VBO,但是存放的是索引数据。