OpenGL入门——使用EBO绘制三角形
上一节OpenGL入门——第一个三角形(1) - 一只小瓶子 - 博客园 (cnblogs.com)介绍了opengl怎么使用VAO和VBO绘制一个三角形
这一节介绍一下使用EBO绘制
元素缓冲对象(Element Buffer Object,EBO),也叫索引缓冲对象(Index Buffer Object,IBO)。
为什么会需要用到元素缓冲对象呢?因为上一节我们提到了绘制三角形就是输入3个顶点进行绘制,但是当我们在构建一个平面需要用到很多个三角形的时候,这里面就会有一些三角形的顶点是是重叠的。最简单的例子就是一个矩形,可以看出是两个三角形,但是这两个三角形有两个顶点重叠了
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 // 左上角 };
如上所示,右下角和左上角出现了两次,这样就会产生额外的开销,矩形其实使用4个顶点就够了。同样的,如果一个模型由成千上万个三角形组成的,就会产生一大堆浪费。
对于这个物体,OpenGL提供了EBO缓冲对象,存储绘制的顶点索引(称为索引绘制)。这样我们就不需要重复定义顶点了,如下所示
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 // 第二个三角形 };
使用EBO对象和VAO、VBO类似,先创建一个EBO对象,把函数调用放在绑定和解绑函数调用之间
//生成EBO对象,缓冲ID为EBO unsigned int EBO; glGenBuffers(1, &EBO); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);//缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);//数据传入缓冲内存中 ... ... while (!glfwWindowShouldClose(window)) { ... ... ///绘制物体 glUseProgram(shaderProgram);//激活程序对象 glBindVertexArray(VAO); //使用EBO绘制, 缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);//缓冲目标GL_ELEMENT_ARRAY_BUFFER glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);//绘制图元为三角形,绘制顶点数量6,索引类型,偏移量0 ... ... }
这里看一下VAO、VBO、EBO三者的关系:
如上图所示,在绑定VAO时,绑定的最后一个EBO对象存储为VAO的元素缓冲区对象,所以在绘制的时候绑定到VAO也会自动绑定该EBO。
注意事项:在VAO未解绑前,不可以解绑EBO,否则会没有这个EBO配置了。
完整代码
//GLAD的头文件包含了正确的OpenGL头文件(例如GL/gl.h),所以需要在其它依赖于OpenGL的头文件之前包含GLAD #include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> #include "shader.h" #include "stb_image.h" const char *vertexShaderSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;// 位置变量的属性位置值为 0 \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" "}\0"; //改变窗口大小 void framebuffer_size_callback(GLFWwindow* window, int width, int height) { glViewport(0, 0, width, height); } //输入 void processInput(GLFWwindow *window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//点击ESC键退出绘制 glfwSetWindowShouldClose(window, true); } GLFWwindow* init_window() { ///窗口初始化 glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本号,当API以不兼容的方式更改时,该值会增加。 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本号,当特性被添加到API中时,它会增加,但是它保持向后兼容。 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//使用核心模式,不兼容已废弃函数 //创建glfw窗口 GLFWwindow* window = glfwCreateWindow(800, 600, "ping-window", NULL, NULL); if (window == NULL) { std::cout << "failed to create GLFW window" << std::endl; glfwTerminate();//释放/删除之前的分配的所有资源 return nullptr; } glfwMakeContextCurrent(window);//将窗口的上下文设置为当前线程的主上下文 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);//注册为调整窗口回调函数 //GLAD是用来管理OpenGL的函数指针的,在调用任何OpenGL的函数之前初始化GLAD if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))//给GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数 { std::cout << "failed to intialize GLAD" << std::endl; return nullptr; } glViewport(0, 0, 800, 600);//处理过的OpenGL坐标范围只为-1到1,因此我们事实上将(-1到1)范围内的坐标映射到(0, 800)和(0, 600) return window; } int triangle_by_EBO() { GLFWwindow* window = init_window(); ///定义着色器 //创建一个顶点着色器对象,注意还是用ID来引用的 unsigned int vertexShader; vertexShader = glCreateShader(GL_VERTEX_SHADER); //着色器源码附加到着色器对象上 glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);//要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量,这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL glCompileShader(vertexShader);//编译源码 int success; char infoLog[512]; glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);//用glGetShaderiv检查是否编译成功 if (!success) { glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; } //创建一个片段着色器对象,注意还是用ID来引用的 unsigned int fragmentShader; fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader);//编译源码 glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);//用glGetShaderiv检查是否编译成功 if (!success) { glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl; } //创建一个着色器对程序 unsigned int shaderProgram; shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader);//把之前编译的着色器附加到程序对象上 glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram);//glLinkProgram链接它们 glGetProgramiv(shaderProgram, GL_COMPILE_STATUS, &success);//用glGetProgramiv检查是否编译成功 if (!success) { glGetShaderInfoLog(shaderProgram, 512, NULL, infoLog); std::cout << "ERROR::SHADER::PROGRAM::LINK_FAILED\n" << infoLog << std::endl; } //链接后即可删除 glDeleteShader(vertexShader); glDeleteShader(fragmentShader);//*/ CShader shader("hello_triangle.vs", "hello_triangle.fs"); //定义顶点对象 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 // 第二个三角形 }; //生成VAO对象,缓冲ID为VAO unsigned int VAO; glGenVertexArrays(1, &VAO); glBindVertexArray(VAO);//绑定VAO,从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO,供之后使用 //生成VBO对象,缓冲ID为VBO unsigned int VBO; glGenBuffers(1, &VBO);//第一个参数GLsizei是要生成的缓冲对象的数量,第二个GLuint是要输入用来存储缓冲对象名称的数组 //生成EBO对象,缓冲ID为EBO unsigned int EBO; glGenBuffers(1, &EBO); //绑定到目标对象,VBO变成了一个顶点缓冲类型 glBindBuffer(GL_ARRAY_BUFFER, VBO);//第一个就是缓冲对象的类型,第二个参数就是要绑定的缓冲对象的名称 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);//数据传入缓冲内存中,GL_STATIC_DRAW:数据不会或几乎不会改变; GL_DYNAMIC_DRAW:数据会被改变很多; GL_DYNAMIC_DRAW:数据会被改变很多 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); //glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);//不要在VAO处于活动状态时取消绑定EBO,因为绑定的元素缓冲区对象存储在VAO中;保持EBO绑定。 //设置顶点属性指针,如何解析顶点数据 /* 第一个参数指定我们要配置的顶点属性,顶点着色器中使用layout(location = 0)定义 第二个参数指定顶点属性的大小 第三个参数指定数据的类型 第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间 第五个参数步长(Stride),它告诉我们在连续的顶点属性组之间的间隔 最后一个参数的类型是void*,数据在缓冲中起始位置的偏移量(Offset) */ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0);//启用顶点属性layout(location = 0),顶点属性默认是禁用的 glBindBuffer(GL_ARRAY_BUFFER, 0);//设置完属性,解绑VBO glBindVertexArray(0);//配置完VBO及其属性,解绑VAO //绘制模式为线条GL_LINE,填充面GL_FILL glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);//正反面 while (!glfwWindowShouldClose(window)) { processInput(window); //清空屏幕 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); ///绘制物体 glUseProgram(shaderProgram);//激活程序对象 glBindVertexArray(VAO); //glDrawArrays(GL_TRIANGLES, 0, 3);//使用VAO绘制:绘制图元为三角形,起始索引0,绘制顶点数量3 glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);//使用EBO绘制:绘制图元为三角形,绘制顶点数量6,索引类型,偏移量0 //glBindVertexArray(0);//只绘制一个物体,不需要重复绑定和解绑VAO //glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);//不要在VAO处于活动状态时取消绑定EBO,因为绑定的元素缓冲区对象存储在VAO中;保持EBO绑定。 glfwSwapBuffers(window);//交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲) glfwPollEvents();//检查有没有触发什么事件 } //释放对象 glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &EBO);//VAO解绑后,可以解绑EBO。 glDeleteProgram(shaderProgram); std::cout << "finish!" << std::endl; glfwTerminate();//释放/删除之前的分配的所有资源 return 0; } int main() { triangle_by_EBO(); return 0; }
运行结果
PS.为了能看出是绘制的两个三角形,这里使用的是绘制线框模式
//绘制模式为线条GL_LINE,填充面GL_FILL glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);//正反面