OpenGL 学习笔记(六)
一、OpenGL索引缓冲对象(EBO)
如果我们要用OpenGL绘制一个正方形,能想到的是用两个三角形来拼凑,这个时候就要用到 EBO 了。
1、VBO、VAO 与 EBO 之间的联系与区别
(1).顶点缓冲对象 VBO 是在显卡存储空间中开辟出的一块内存缓存区,用于存储顶点的各类属性信息,如顶点坐标、顶点法向量、顶点颜色数据等。在渲染时,可以直接从 VBO 中取出顶点的各类属性数据,由于 VBO 在显存而不是在内存中,不需要从CPU传输数据,所以处理效率更高。所以可以理解为 VBO 就是显存中的一个存储区域,可以保持大量的顶点属性信息。并且可以开辟很多个 VBO ,每个 VBO 在 OpenGL 中有它的唯一标识 ID ,这个 ID 对应着具体的 VBO 的显存地址,通过这个 ID 可以对特定的 VBO 内的数据进行存取操作。
(2).VAO 是一个保存了所有顶点数据属性的状态结合,它存储了顶点数据的格式以及顶点数据所需的 VBO 对象的引用。
因为 VBO 保存了一个模型的顶点属性信息,每次绘制模型之前需要绑定顶点的所有信息。当数据量很大时,重复这样的动作变得非常麻烦。VAO 可以把这些所有的配置都存储在一个对象中,每次绘制模型时,只需要绑定这个 VAO 对象就可以了。另外,VAO 本身并没有存储顶点的相关属性数据,这些信息是存储在 VBO 中的,VAO 相当于是对很多个 VBO 的引用,把一些 VBO 组合在一起作为一个对象统一管理。
(3).索引缓冲对象 EBO 相当于 OpenGL 中的顶点数组的概念,是为了解决同一个顶点多次重复调用的问题,可以减少内存空间浪费,提高执行效率。当需要使用重复的顶点时,通过顶点的位置索引来调用顶点,而不是对重复的顶点信息重复记录,重复调用。
EBO 中存储的内容就是顶点位置的索引 indices,EBO 跟 VBO 类似,也是在显存中的一块内存缓冲器,只不过 EBO 保存的是顶点的索引。
2、EBO
● 首先,我们现在要绘制正方形,则需要 4 个顶点。在主函数中我们将 4 个顶点的位置信息给出。并用顶点位置的索引 indices 数组将它们 “缝合” 起来。示意图如下:

代码如下:
/* 编写各顶点位置 */
GLfloat vertices_1[] =
{
//position
0.5f, 0.5f, 0.0f, // top right 0
0.5f, -0.5f, 0.0f, // bottom right 1
-0.5f, -0.5f, 0.0f, // bottom left 2
-0.5f, 0.5f, 0.0f, // top left 3
};
/* 四个顶点的连接信息给出来 */
GLuint indices_1[] =
{
0, 1, 3, //序号为 0、1、3 的顶点组合成一个三角形
1, 2, 3 //序号为 1、2、3 的顶点组合成一个三角形
};
然后,在创建完VAO和VBO后,再创建EBO并绑定,用 glBufferData(以GL_ELEMENT_ARRAY_BUFFER为参数)把索引存储到 EBO 中:
GLuint EBO;
glGenBuffers(1, &EBO); //绑定 EBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); //使用 glBindBuffer 函数把新创建的索引缓冲对象绑定到 GL_ELEMENT_ARRAY_BUFFER目标上
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices_1), indices_1, GL_STATIC_DRAW); // GL_STATIC_DRAW:静态的画图(因为要频繁地读)
当用 EBO 绑定顶点索引的方式绘制模型时,需要使用 glDrawElements 而不是 glDrawArrays :
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); //绑定 EBO
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); //画两个三角形 从第0个顶点开始 一共画 6 次(顺序为0,1,3,1,2,3)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); //解绑定 EBO
当用 EBO 绑定顶点索引的方式绘制模型时,需要使用 glDrawElements 而不是 glDrawArrays :
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); //绑定 EBO
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); //画两个三角形 从第0个顶点开始 一共画 6 次(顺序为0,1,3,1,2,3)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); //解绑定 EBO
glDrawElements() 函数说明:
(1).第一个参数:绘制的模式
(2).第二个参数:绘制的顶点个数
(3).第三个参数:索引的数据类型
(4).第四个参数:可选的 EBO 中偏移量设定
二、Uniform使用说明
若要画一个颜色跟着时间变化的正方形,着色器的传入就是随时间变化的。以往的知识满足不了这一点,这时就需要用到 Uniform。
Uniform 是一种从 CPU中的应用 向 GPU中的着色器 发送数据的方式,但 uniform 和顶点属性有些不同。首先,uniform 是全局的(Global)。全局意味着 uniform 变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。其次,无论你把 uniform 值设置成什么,uniform 会一直保存它们的数据,直到它们被重置或更新。
修改过后的顶点着色器:
#version 330 core //3.30版本
layout(location = 0) in vec3 position;//位置变量的属性位置值为0
void main()
{
gl_Position = vec4(position, 1.0f);//核心函数(位置信息赋值)
}
修改过后的片元着色器:
#version 330 core //3.30版本
out vec4 FragColor; //输出是四个浮点数构成的一个向量 RGB+aerfa
uniform vec4 time; //在OpenGL程序代码中设定这个变量(uniform:实时地变量表示)
void main()
{
FragColor = time; //颜色随时间变化
}
“ Shader.h ”头文件,不用修改(和彩色三角形的一样)。
这个 uniform 现在还是空的。我们还没有给它添加任何数据。首先需要用 glGetUniformLocation() 函数找到着色器中 uniform 属性的索引 (即位置值) 。当我们得到 uniform 的索引后,就可以用 glUniform…() 相关函数来更新它的值了。
float time = glfwGetTime(); //获取时间(运行的秒数)
float redValue = sin(time) / 2.0f + 0.5f; //红色数值计算,范围[0,1]
float greenValue = 1 - redValue; // 绿色数值计算,范围[0.1]。且满足 “redValue + greenValue = 1”
int vertexColorLocation = glGetUniformLocation(ourShader.Program, "time");//找到 “time” 的索引
glUniform4f(vertexColorLocation, redValue, greenValue, 0.0f, 1.0f );//更新颜色
说明:OpenGL在其核心是一个 C库,所以它不支持类型重载,在函数参数不同的时候就要为其定义新的函数。glUniform 函数是一个典型例子,它有一个特定的后缀。标识设定为 uniform 的类型时,可能的后缀有:

三、绘制正方形
使用OpenGL绘制正方形代码如下:
/* 引入相应的库 */
#include <iostream>
using namespace std;
#define GLEW_STATIC
#include<glew.h>
#include<glfw3.h>
#include"Shader.h"
/* 编写各顶点位置 */
GLfloat vertices_1[] =
{
//position
0.5f, 0.5f, 0.0f, // top right 0
0.5f, -0.5f, 0.0f, // bottom right 1
-0.5f, -0.5f, 0.0f, // bottom left 2
-0.5f, 0.5f, 0.0f, // top left 3
};
/* 四个顶点的连接信息给出来 */
GLuint indices_1[] =
{
0, 1, 3, // 序号为 0、1、3 的顶点组合成一个三角形
1, 2, 3 // 序号为 1、2、3 的顶点组合成一个三角形
};
const GLint WIDTH = 600, HEIGHT = 600; // 正方形窗口
int main()
{
glfwInit();
GLFWwindow* window_1 = glfwCreateWindow(WIDTH, HEIGHT, "Learn OpenGL Triangle test", nullptr, nullptr);
int screenWidth_1, screenHeight_1;
glfwGetFramebufferSize(window_1, &screenWidth_1, &screenHeight_1);
glfwMakeContextCurrent(window_1);
glewInit();
/* 将我们自己设置的着色器文本传进来 */
Shader ourShader = Shader("shader_v.txt", "shader_f.txt"); // 相对路径
/* 设置顶点缓冲对象(VBO) + 设置顶点数组对象(VAO) */
GLuint VAO, VBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_1), vertices_1, GL_STATIC_DRAW);
/* 设置链接顶点属性 */
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0); // 通道 0 打开
/* 设置索引缓冲对象 */
GLuint EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices_1), indices_1, GL_STATIC_DRAW);
// draw loop 画图循环
while (!glfwWindowShouldClose(window_1))
{
// 视口 + 时间
glViewport(0, 0, screenWidth_1, screenHeight_1);
glfwPollEvents();
// 渲染 + 清除颜色缓冲
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
/* 绘制图形 */
ourShader.Use();
float time = glfwGetTime(); // 获取时间
float redValue = sin(time) / 2.0f + 0.5f; // 红色数值计算,范围[0,1]
float greenValue = 1 - redValue; // 绿色数值计算,范围[0.1]。且满足 “redValue + greenValue = 1”
int vertexColorLocation = glGetUniformLocation(ourShader.Program, "time");
glUniform4f(vertexColorLocation, redValue, greenValue, 0.0f, 1.0f );
glBindVertexArray(VAO); // 绑定 VAO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); // 绑定 EBO
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); // 画两个三角形 从第0个顶点开始 一共画6次
glBindVertexArray(0); // 解绑定 VAO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); // 解绑定 EBO
// 交换缓冲
glfwSwapBuffers(window_1);
}
glDeleteVertexArrays(1, &VAO); // 释放资源
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
glfwTerminate(); // 结束
return 0;
}
运行效果:
