概念理解
顶点缓冲对象VBO(Vertex Buffer Objects)
顶点缓冲对象VBO是在显卡存储空间中开辟出的一块内存缓存区,用于存储顶点的各类属性信息,如顶点坐标,顶点法向量,顶点颜色数据等。在渲染时,可以直接从VBO中取出顶点的各类属性数据,由于VBO在显存而不是在内存中,不需要从CPU传输数据,处理效率更高。
所以可以理解为VBO就是显存中的一个存储区域,可以保持大量的顶点属性信息。并且可以开辟很多个VBO,每个VBO在OpenGL中有它的唯一标识ID,这个ID对应着具体的VBO的显存地址,通过这个ID可以对特定的VBO内的数据进行存取操作
顶点数组对象(Vertex Arrary Object,VAO)
VBO保存了一个模型的顶点属性信息,每次绘制模型之前需要绑定顶点的所有信息,当数据量很大时,重复这样的动作变得非常麻烦。VAO可以把这些所有的配置都存储在一个对象中,每次绘制模型时,只需要绑定这个VAO对象就可以了。
VAO是一个保存了所有顶点数据属性的状态结合,它存储了顶点数据的格式以及顶点数据所需的VBO对象的引用。VAO本身并没有存储顶点的相关属性数据,这些信息是存储在VBO中的,VAO相当于是对很多个VBO的引用,把一些VBO组合在一起作为一个对象统一管理。
- 基础的绘制过程:OpenGL初始化,顶点输入,数据处理,着色器计算以及渲染
- 用VAO/VBO的方式绘制三角形
OpenGL初始化:包括初始化GLFW,创建窗口,初始化GLAD,创建视口四个部分。
// 初始化 GLFW glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); glfwWindowHint(GLFW_RESIZABLE, FALSE); // 创建窗口(宽、高、窗口名称) auto window = glfwCreateWindow(screen_width, screen_height, "Triangle", nullptr, nullptr); if (window == nullptr) { std::cout << "Failed to Create OpenGL Context" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); // 初始化 GLAD,加载 OpenGL 函数指针地址的函数 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } // 指定当前视口尺寸(前两个参数为左下角位置,后两个参数是渲染窗口宽、高) glViewport(0, 0, screen_width, screen_height);
顶点输入:给出三角形的顶点数据,并对数据做出一些处理(包括生成绑定VAO,VBO和属性设置),最后将其解绑
// 三角形的顶点数据 const float triangle[] = { // ---- 位置 ---- -0.5f, -0.5f, 0.0f, // 左下 0.5f, -0.5f, 0.0f, // 右下 0.0f, 0.5f, 0.0f // 正上 };
数据处理:VAO VBO
创建的VBO可用来保存不同类型的顶点数据,创建之后需要通过分配的ID绑定(bind)一下制定的VBO,对于同一类型的顶点数据一次只能绑定一个VBO。绑定操作通过glBindBuffer来实现,第一个参数指定绑定的数据类型,可以是GL_ARRAY_BUFFER, GL_ELEMENT_ARRAY_BUFFER, GL_PIXEL_PACK_BUFFER或者GL_PIXEL_UNPACK_BUFFER中的一个。
接下来调用glBufferData把用户定义的数据传输到当前绑定的显存缓冲区中。
// 生成并绑定 VBO GLuint vertex_buffer_object; glGenBuffers(1, &vertex_buffer_object); glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object); // 将顶点数据绑定至当前默认的缓冲中 glBufferData(GL_ARRAY_BUFFER, sizeof(triangle), triangle, GL_STATIC_DRAW);
// 生成并绑定 VAO
//执行VAO绑定之后其后的所有VBO配置都是这个VAO对象的一部分,可以说VBO是对顶点属性信息的绑定,VAO是对很多个VBO的绑定。
GLuint vertex_array_object; glGenVertexArrays(1, &vertex_array_object); glBindVertexArray(vertex_array_object);
数据处理:顶点数据解释
发送到 GPU 之后我们还需要告诉 OpenGL 我们如何解释这些顶点数据。因 此我们用 glVertexAttribPointer 这个函数告诉 OpenGL 我们如何解释这些顶点数 据。
// 设置顶点属性指针 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0)
这个函数第一个参数是我们后面会用到的顶点着色器的位置值,3 表示的 是顶点属性是一个三分量的向量,第三个参数表示的是我们顶点的类型,第四 个是我们是否希望数据被标准化,就是映射到 0-1 之间,第五个参数叫做步长, 它表示连续顶点属性之间的间隔,因为我们这里只有顶点的位置,所以我们将 步长设置为这个,表示下组数据在 3 个 float 之后。最后一个是数据的偏移量, 这里我们的位置属性是在数组的开头,因此这里是 0,并且由于参数类型的限 制,我们需要将其进行强制类型转换。
而下面 Enable 的函数则是表明我们开启了 0 的这个通道,默认状态下是关 闭的,因此我们在这里要开启一下。
// 解绑VAO和VBO glBindVertexArray(0); glBindBuffer(GL_ARRAY_BUFFER, 0);
等到设置属性指针完成之后,我们这里需要解绑 VAO 和 VBO。那么,我 们可以思考一下,为什么我们在这里要解绑 VAO 和 VBO 呢? 一个原因是因为在防止之后再继续绑定 VAO 的时候会影响当前的 VAO, 另一个原因是为了使代码更加灵活规范,在渲染需要的时候我们会再绑定 VAO。 我们已经通过 VAO、VBO 将顶点数据储存在显卡的 GPU 上了,接下来我 们会创建顶点和片段着色器真正处理这些数据。这里我们会给出着色器的源码, 然后生成并编译着色器,最后将顶点和片段着色器链接到一个着色器程序,在 之后的渲染流程中我们会使用这个着色器程序,最后将之前的着色器删除。
顶点着色器和片段着色器
这时GLSL语言编写的着色器源码。
这里我们给出的这两段分别是顶点着色器的源码和片段着色器的源码,这 个是用 GLSL 语言来编写的,而且这个 GLSL 语言看起来与 C 语言的风格类似, 很容易懂。我们先看顶点着色器,第一行表示我们使用的是 OpenGL3.3 的核心 模式,第二行就是我们之前说到的位置值。Main 函数中的部分就是将我们之前 的顶点数据直接输出到 GLSL 已经定义好的一个内建变量 gl_Position 中,这个 就是我们顶点着色器的输出,也就是说我们在顶点着色器这里什么都没做,就 只是将顶点位置作为顶点着色器的输出。
// 顶点着色器源码 const char *vertex_shader_source = "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos, 1.0);\n" "}\n\0";
接下来是片段着色器,前面两行类似,这里的 out 表示输出变量,就像之 前的 in 表示输入变量。然后我们这里的四分量向量就是我们之前看到的三角形 是红色的来源,是一个四分量的 RGBA,那么我们也可以将其更改一下,我们 输出的三角形颜色就会发生变化。
// 片段着色器源码 const char *fragment_shader_source = "#version 330 core\n" "out vec4 FragColor;\n" "void main()\n" "{\n" " FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n" "}\n\0";
有了顶点和片段着色器的源码,我们还需要生成和编译着色器,那么这里 我们先生成了顶点着色器,并且附加上了之前的源码,将其进行编译,然后我 们这里对其进行检测,是否成功编译,如果编译不成功就打印错误信息。 同样的片段着色器也是一样的。 6 最后我们将顶点和片段着色器链接到一个着色器程序中,这样我们在渲染 时只需要调用一个着色器程序就可以了,同样这里我们检测了一下链接是否成 功。 最后删除掉顶点和片段着色器。因为在后面渲染的时候我们只需要用那个 我们之前链接好的着色器程序就可以了,不需要再使用顶点和片段着色器了。
// 生成并编译着色器 // 顶点着色器 int vertex_shader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertex_shader, 1, &vertex_shader_source, NULL); glCompileShader(vertex_shader); int success; char info_log[512]; // 检查着色器是否成功编译,如果编译失败,打印错误信息 glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(vertex_shader, 512, NULL, info_log); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << info_log << std::endl; }
// 生成并编译着色器 // 片段着色器 int fragment_shader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragment_shader, 1, &fragment_shader_source, NULL); glCompileShader(fragment_shader); // 检查着色器是否成功编译,如果编译失败,打印错误信息 glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(fragment_shader, 512, NULL, info_log); std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << info_log << std::endl; }
// 链接顶点和片段着色器至一个着色器程序 int shader_program = glCreateProgram(); glAttachShader(shader_program, vertex_shader); glAttachShader(shader_program, fragment_shader); glLinkProgram(shader_program); // 检查着色器是否成功链接,如果链接失败,打印错误信息 glGetProgramiv(shader_program, GL_LINK_STATUS, &success); if (!success) { glGetProgramInfoLog(shader_program, 512, NULL, info_log); std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << info_log << std::endl; }
// 删除着色器 glDeleteShader(vertex_shader); glDeleteShader(fragment_shader);
渲染
接下来我们进入我们的渲染阶段,当窗口没有关闭的时候我们就一直进行 渲染。首先我们先清空颜色缓冲,我们这里用的是黑色的背景色来清空屏幕颜 色缓冲,当然这里我们可以更换颜色。接下来我们使用我们之前已经链接好的 着色器程序,和 VAO,来绘制三角形,绘制三角形其实只要一句话,就是这个 glDrawArrays。这里的第一个参数表示我们是要绘制三角形,第二个参数表示 我们顶点数组的起始索引值,第三个参数表示我们要绘制的顶点数量,这里绘 制三角形我们要绘制三个顶点。绘制结束后解除绑定。最后我们会交换一下缓 冲,这里我们使用的是一个双缓冲的做法,前缓冲保存着输出的图像,而渲染 指令都在后缓冲中进行,当指令执行完毕后我们交换前后缓冲,最后我们还会 检测是否有触发一些回调函数。
while (!glfwWindowShouldClose(window)) { // 清空颜色缓冲 glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // 使用着色器程序 glUseProgram(shader_program); // 绘制三角形 glBindVertexArray(vertex_array_object); // 绑定VAO glDrawArrays(GL_TRIANGLES, 0, 3); // 绘制三角形 glBindVertexArray(0); // 解除绑定 // 交换缓冲并且检查是否有触发事件(比如键盘输入、鼠标移动等) glfwSwapBuffers(window); glfwPollEvents(); }
当我们的窗口关闭之后,我们还会进行一些善后工作,这里包括删除我们 之前所创建的 VAO、VBO,以及调用 GLFW 的函数来清理所有的资源并退出 程序。 整个绘制三角形的程序就到此为止。
实验结果