索引缓冲对象
看下面的数据:
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, // 右下 -0.5f, -0.5f, 0.0f,// 左下 -0.5f, 0.5f, 0.0f // 左上 };
如果我们要画2个三角形,使用上面的数据,最终这2个三角形会构成一个长方形(之所以是长方形不是正方形和像素有关系,后面会了解),使用下面代码绘制:
glDrawArrays(GL_TRIANGLES, 0, 6);
第三个参数之所以是6,是因为这里要绘制出6个顶点元素,结果:
可以看到,有几个顶点叠加了。我们指定了右下角
和左上角
两次!一个矩形只有4个而不是6个顶点,这样就产生50%的额外开销。当我们有包括上千个三角形的模型之后这个问题会更糟糕,这会产生一大堆浪费。更好的解决方案是只储存不同的顶点,并设定绘制这些顶点的顺序。这样子我们只要储存4个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。
我们可以指定用线框模式来绘制,这样我们就能看的更加清晰直接了:
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
这个代码执行一次就可以,没必要放在循环里,
为了不造成浪费,我们需要使用索引(元素)缓冲对象,首先更改一个顶点数组,将重复的去掉:
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开始! // 此例的索引(0,1,2,3)就是顶点数组vertices的下标, // 这样可以由下标代表顶点组合成矩形 0, 1, 3, // 第一个三角形 1, 2, 3 // 第二个三角形 };
我们也可以不用元素缓冲对象,直接根据上面的这2个定义来处理,替换掉glDrawArray,但是不建议,最好还是用EBO:
// glDrawArrays(GL_TRIANGLES, 0,6); // 还是绘制6个顶点, glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, indices);
下面我们介绍EBO的使用流程,首先创建一个EBO对象:
unsigned int EBO; glGenBuffers(1, &EBO);
与VBO类似,我们先绑定EBO然后用glBufferData把索引复制到缓冲里。同样,和VBO类似,我们会把这些函数调用放在绑定和解绑函数调用之间,只不过这次我们把缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER。
注意:我们传递了GL_ELEMENT_ARRAY_BUFFER当作缓冲目标。最后一件要做的事是用glDrawElements来替换glDrawArrays函数,表示我们要从索引缓冲区渲染三角形。使用glDrawElements时,我们会使用当前绑定的索引缓冲对象中的索引进行绘制:
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取其索引。这意味着我们每次想要使用索引渲染对象时都必须绑定相应的EBO,这又有点麻烦。碰巧顶点数组对象也跟踪元素缓冲区对象绑定。 在绑定VAO时,绑定的最后一个元素缓冲区对象存储为VAO的元素缓冲区对象。然后,绑定到VAO也会自动绑定该EBO,比如下面代码:
// GLAD的include文件包含所需的OpenGL头文件(如GL/GL.h),因此确保在其他需要OpenGL的头文件(如GLFW)之前包含GLAD。就是#include <glad/glad.h> 放在最前面 #include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> 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 // 第二个三角形 }; 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"; const char* fragmentShaderSource = "#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"; void framebuffer_size_callback(GLFWwindow* window, int width, int height); void processInput(GLFWwindow* window); int main() { // 初始化GLFW,只有初始化完成之后才能够使用GLFW的函数 glfwInit(); // GLFW配置设置 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 如果是苹果系统的话使用下面代码 #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); #endif // 创建窗口 大小和名称 GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window" << std::endl; // 此函数销毁所有剩余的窗口和光标 glfwTerminate(); return -1; } //GLFW将窗口的上下文设置为当前线程的上下文 glfwMakeContextCurrent(window); // 告诉GLFW我们希望每当窗口调整大小的时候调用这个函数 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); //GLAD // glad: 加载所有OpenGL函数指针 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } #pragma region 着色器 // 创建和编译着色器程序 //顶点着色器 unsigned int 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; } // 片段着色器 unsigned int 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; } //着色器程序 unsigned int 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::LINKING_FAILED\n" << infoLog << std::endl; } glDeleteShader(vertexShader); glDeleteShader(fragmentShader); #pragma endregion //创建VBO和VAO对象,并赋予ID unsigned int VBO, VAO, EBO; //创建1个VAO对象 glGenVertexArrays(1, &VAO); //创建1个VBO对象 glGenBuffers(1, &VBO); //绑定VBO和VAO对象 glBindVertexArray(VAO); //缓冲对象如果绑定的是顶点属性则用:GL_ARRAY_BUFFER glBindBuffer(GL_ARRAY_BUFFER, VBO); //为当前绑定到target的缓冲区对象创建一个新的数据存储。 //如果data不是NULL,则使用来自此指针的数据初始化数据存储 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //上面我们将数据存到了缓冲区对象中,下面就需要 //告知Shader(着色器)如何解析缓冲里的属性值 //第一个属性是从哪个位置开始解析 //第二个属性是每个顶点属性由几个组合而成 //第三个属性是数组中每个元素的类型 //第四个表示是否标准化,这里暂时不需要,需要注意只有整型值才会有效,如果浮点型的数据不会起作用 //第五个是步长,因为我们这个是数组中每3个元素组成一个顶点属性,而且是float类型 //第六个是偏移量,我们这里为0,就是从0开始读取数组中的数据的 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); //开启VAO管理的第一个属性值 glEnableVertexAttribArray(0); //作为习惯,用完之后将VBO,VAO解绑,需要的时候可以重新绑定 glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); // EBO对象创建并绑定
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices),indices, GL_STATIC_DRAW); // 解绑EBO glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // 渲染循环 一个循环就是一帧 只要是窗体不关闭,就会一直循环 while (!glfwWindowShouldClose(window)) { processInput(window); // 在这里,我们将屏幕设置为了类似黑板的深蓝绿色 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //状态设置 // 调用glClear函数,清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。 glClear(GL_COLOR_BUFFER_BIT); //状态使用 #pragma region 绘制三角形 // 首先,要选择使用哪个着色器 每个着色器调用和渲染调用都会使用这个程序对象 glUseProgram(shaderProgram); // 需要注意的是前面我们已经解绑了VAO,所以现在是无法解析数据的,所以我们需要重新绑定, // 至于数据我们已经存到缓冲区了 glBindVertexArray(VAO);// 从数据数组中indexwei0处开始读取,每三个做一个三角形的顶点(这是在VAO定义的),。 // glDrawArrays(GL_TRIANGLES, 0,6); // 还是绘制6个顶点, glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); #pragma endregion // glfw: 交换缓冲区 该函数在指定窗口的前后缓冲区交换 // 前缓冲区:屏幕上显示的图像 // 后缓冲区:正在渲染的图像 // glfwSwapBuffers函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲), // 它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上 glfwSwapBuffers(window); // 轮询IO事件(按键按下 / 释放、鼠标移动等)通过下面方法就可以是得窗体对鼠标做出的动作做出反应,比如关闭,移动窗体等 glfwPollEvents(); } // glfw: 回收前面分配的GLFW先关资源. 一定要注意,只有关闭窗体之后才会跳出while循环走到这一步!!! glfwTerminate(); // 在while循环退出后释放内存: glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteProgram(shaderProgram); return 0; } // glfwGetKey函数:需要一个窗口以及一个按键作为输入;函数将会返回这个按键是否正在被按下 void processInput(GLFWwindow* window) { // 如果按下了ESC键,设置窗体的关闭标志为true,代表窗体可以退出 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); } // 当改变窗口的大小的时候,视口也应该被调整。我们可以对窗口注册一个回调函数(Callback Function),它会在每次窗口大小被调整的时候被调用 void framebuffer_size_callback(GLFWwindow* window, int width, int height) { // 设置窗口维度 // glViewport(前两参数为窗口左下角位置,3.宽度,4.高度) glViewport(0, 0, width, height); }
结果还是会成功绘制,虽然已经解绑了EBO,因为在绑定VAO的时候自动绑定了该EBO,,所以我们在解绑EBO之后才能解绑VAO,顺序不能乱。然后在循环里使用的时候需要重新绑定EBO:
// GLAD的include文件包含所需的OpenGL头文件(如GL/GL.h),因此确保在其他需要OpenGL的头文件(如GLFW)之前包含GLAD。就是#include <glad/glad.h> 放在最前面 #include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> 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 // 第二个三角形 }; 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"; const char* fragmentShaderSource = "#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"; void framebuffer_size_callback(GLFWwindow* window, int width, int height); void processInput(GLFWwindow* window); int main() { // 初始化GLFW,只有初始化完成之后才能够使用GLFW的函数 glfwInit(); // GLFW配置设置 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 如果是苹果系统的话使用下面代码 #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); #endif // 创建窗口 大小和名称 GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window" << std::endl; // 此函数销毁所有剩余的窗口和光标 glfwTerminate(); return -1; } //GLFW将窗口的上下文设置为当前线程的上下文 glfwMakeContextCurrent(window); // 告诉GLFW我们希望每当窗口调整大小的时候调用这个函数 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); //GLAD // glad: 加载所有OpenGL函数指针 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } #pragma region 着色器 // 创建和编译着色器程序 //顶点着色器 unsigned int 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; } // 片段着色器 unsigned int 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; } //着色器程序 unsigned int 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::LINKING_FAILED\n" << infoLog << std::endl; } glDeleteShader(vertexShader); glDeleteShader(fragmentShader); #pragma endregion //创建VBO和VAO对象,并赋予ID unsigned int VBO, VAO, EBO; //创建1个VAO对象 glGenVertexArrays(1, &VAO); //创建1个VBO对象 glGenBuffers(1, &VBO); //绑定VBO和VAO对象 glBindVertexArray(VAO); //缓冲对象如果绑定的是顶点属性则用:GL_ARRAY_BUFFER glBindBuffer(GL_ARRAY_BUFFER, VBO); //为当前绑定到target的缓冲区对象创建一个新的数据存储。 //如果data不是NULL,则使用来自此指针的数据初始化数据存储 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //上面我们将数据存到了缓冲区对象中,下面就需要 //告知Shader(着色器)如何解析缓冲里的属性值 //第一个属性是从哪个位置开始解析 //第二个属性是每个顶点属性由几个组合而成 //第三个属性是数组中每个元素的类型 //第四个表示是否标准化,这里暂时不需要,需要注意只有整型值才会有效,如果浮点型的数据不会起作用 //第五个是步长,因为我们这个是数组中每3个元素组成一个顶点属性,而且是float类型 //第六个是偏移量,我们这里为0,就是从0开始读取数组中的数据的 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); //开启VAO管理的第一个属性值 glEnableVertexAttribArray(0); //作为习惯,用完之后将VBO,VAO解绑,需要的时候可以重新绑定 glBindBuffer(GL_ARRAY_BUFFER, 0); // EBO对象创建并绑定 glGenBuffers(1, &EBO); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices),indices, GL_STATIC_DRAW); // 解绑EBO glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); // 解绑VAO 在解绑EBO后面 glBindVertexArray(0); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // 渲染循环 一个循环就是一帧 只要是窗体不关闭,就会一直循环 while (!glfwWindowShouldClose(window)) { processInput(window); // 在这里,我们将屏幕设置为了类似黑板的深蓝绿色 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //状态设置 // 调用glClear函数,清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。 glClear(GL_COLOR_BUFFER_BIT); //状态使用 #pragma region 绘制三角形 // 首先,要选择使用哪个着色器 每个着色器调用和渲染调用都会使用这个程序对象 glUseProgram(shaderProgram); // 需要注意的是前面我们已经解绑了VAO,所以现在是无法解析数据的,所以我们需要重新绑定, // 至于数据我们已经存到缓冲区了 glBindVertexArray(VAO); // 一定是要在VAO在的时候进行EBO的绑定 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); // 从数据数组中indexwei0处开始读取,每三个做一个三角形的顶点(这是在VAO定义的),。 // glDrawArrays(GL_TRIANGLES, 0,6); // 还是绘制6个顶点, glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); #pragma endregion // glfw: 交换缓冲区 该函数在指定窗口的前后缓冲区交换 // 前缓冲区:屏幕上显示的图像 // 后缓冲区:正在渲染的图像 // glfwSwapBuffers函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲), // 它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上 glfwSwapBuffers(window); // 轮询IO事件(按键按下 / 释放、鼠标移动等)通过下面方法就可以是得窗体对鼠标做出的动作做出反应,比如关闭,移动窗体等 glfwPollEvents(); } // glfw: 回收前面分配的GLFW先关资源. 一定要注意,只有关闭窗体之后才会跳出while循环走到这一步!!! glfwTerminate(); // 在while循环退出后释放内存: glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteProgram(shaderProgram); return 0; } // glfwGetKey函数:需要一个窗口以及一个按键作为输入;函数将会返回这个按键是否正在被按下 void processInput(GLFWwindow* window) { // 如果按下了ESC键,设置窗体的关闭标志为true,代表窗体可以退出 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); } // 当改变窗口的大小的时候,视口也应该被调整。我们可以对窗口注册一个回调函数(Callback Function),它会在每次窗口大小被调整的时候被调用 void framebuffer_size_callback(GLFWwindow* window, int width, int height) { // 设置窗口维度 // glViewport(前两参数为窗口左下角位置,3.宽度,4.高度) glViewport(0, 0, width, height); }
参考:https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/#_10