原博主博客地址:http://blog.csdn.net/qq21497936
本文章博客地址:http://blog.csdn.net/qq21497936/article/details/78942027
《OpenGL学习笔记》系列博客目录地址:http://blog.csdn.net/qq21497936/article/category/7315532
OpenGL学习笔记(九):索引缓冲器(EBO /IBE)的理解与使用,引入线框/填充模式
前话
前面一章节介绍了VAO、VBO和SHADER的基本使用,本章节将继续引入索引缓冲器,使用索引缓冲器建立一个六边形,并使六边形两两相邻的三个顶点构成一个三角形,共六个角形(每个顶点被使用3次),并引入绘画填充模式。
Demo效果图
demo3下载地址: http://download.csdn.net/download/qq21497936/10182276
专业名词
EBO(Element Buffer Object)、EBO(Index Buffer Object):索引缓冲对象。
索引缓冲对象
索引缓冲对象专门储存索引,OpenGL调用这些顶点的索引来决定该绘制哪个顶点,使用索引可重复使用同一个顶点。
不使用索引缓冲器绘制
首先我们先看六边形的六个顶点坐标:
在不使用索引的情况下,我们的顶点数据如下:
// 7.顶点数据 float vertices[] = { -0.866, -0.5, 0.0, -0.866, 0.5, 0.0, 0.0 , 1.0, 0.0, // 三角形1 -0.866, 0.5, 0.0, 0.0 , 1.0, 0.0, 0.866, 0.5, 0.0, // 三角形2 0.0 , 1.0, 0.0, 0.866, 0.5, 0.0, 0.866, -0.5, 0.0, // 三角形3 0.866, 0.5, 0.0, 0.866, -0.5, 0.0, 0.0 , -1.0, 0.0, // 三角形4 0.866, -0.5, 0.0, 0.0 , -1.0, 0.0, -0.866, -0.5, 0.0, // 三角形5 0.0 , -1.0, 0.0, -0.866, -0.5, 0.0, -0.866, 0.5, 0.0, // 三角形6 };
绘制的顶点数量,是18个,所以修改程序的绘制顶点数量:
while (!glfwWindowShouldClose(window)) { …… // 绘制三角形:三角形 数组起始索引 绘制多少个顶点 // glDrawArrays(GL_TRIANGLES, 0, 3); glDrawArrays(GL_TRIANGLES, 0, 18); …… }
使用索引缓冲器绘制
使用索引缓冲器,其实我们所有的三角形只使用到了六个顶点(需要重复使用的通过索引可重复使用),VAO中就保存该6个顶点可以了:
// 7.0索引缓冲期 增加的索引缓冲器顶点索引 unsigned int indices[] = { // 注意索引从0开始! 0, 1, 2, // 第一个三角形 1, 2, 3, // 第二个三角形 2, 3, 4, // 第三个三角形 3, 4, 5, // 第四个三角形 4, 5, 0, // 第五个三角形 5, 0, 1, // 第六个三角形 };
使用EBO和VBO一样,首先是使用过程是一样的,其次是使用的注意点也是一样的,就是绑定EBO/VBO到解绑EBO/VBO(需要生效的)的过程一定要在绑定顶点数据和解绑顶点数据之间,否该绘制该顶点,缓存未被生效。
以下是在原来的VAO,VBO代码部分新增了EBO的代码:
// 8.使用VAO和VBO unsigned int VBO, VAO; // glGenVertexArrays() 创建一个顶点数组对象 // 第一个参数:需要创建的缓存数量 // 第二个参数:存储单一ID或多个ID的GLuint变量或数组的地址。 glGenVertexArrays(1, &VAO); // glGenBuffers() 创建一个缓存对象并且返回缓存对象的标示符。 glGenBuffers(1, &VBO); // 顶点对象创建之后,在使用缓存对象之前,需要将缓存对象连接到相应的缓存上。 // glBindBuffer()有1个参数:buffer。(绑定和解绑的顺序很重要,勿更改) glBindVertexArray(VAO); // 缓存对象创建之后,在使用缓存对象之前,需要将缓存对象连接到相应的缓存上。 // glBindBuffer()有2个参数:target与buffer。(绑定和解绑的顺序很重要,勿更改) glBindBuffer(GL_ARRAY_BUFFER, VBO); // 当缓存初始化之后,使用glBufferData()将顶点数据拷贝到缓存对象 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); #if 1 // 索引缓冲器-添加代码 // 8.0 与VBO类似,先创建EBO对象,再先绑定EBO // 然后用glBufferData把索引复制到缓冲里 // 同样,和VBO类似,把这些函数调用放在绑定和解绑函数调用之间 // 区别在于,只不过这次我们把缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER。 unsigned int EBO; glGenBuffers(1, &EBO); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); #endif // 设置顶点属性指针,glVertexAttribPointer()函数告诉OpenGL该如何解析顶点数据 // 顶点属性位置 顶点属性大小 数据的类型 是否被标准化 步长 偏移 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void *)0); // glEnableVertexAttribArray()以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的 glEnableVertexAttribArray(0); // 解绑缓存着色器(绑定和解绑的顺序很重要,勿更改) glBindBuffer(GL_ARRAY_BUFFER, 0); // 解绑顶点着色器(绑定和解绑的顺序很重要,勿更改) glBindVertexArray(0);
修改后的绘制循环代码:
while (!glfwWindowShouldClose(window)) { // 捕捉输入 processInput(window); // 清空颜色缓冲 // 添加渲染指令,背景RGBA的A不生效, 更改为白色 glClearColor(1.0f, 1.0f, 1.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); /* 新增代码 第二段/共三段----------------------------------START */ // 当我们渲染一个物体时要使用着色器程序 glUseProgram(shaderProgram); // 激活VAO表示的顶点缓存 glBindVertexArray(VAO); // 每次重新更新视口,如果不这样,拉升窗口后,会变形。 // 你把你要观察的视景体映射到你的视口就没有问题,但是如果你不设置视景体, // opengl默认会以屏幕坐标,就是屏幕中心为原点,到四周的距离为1的这个范围 // 作为视景体的观察部分映射到你设置的视口上。 // 可以尝试,屏蔽此句,然后运行程序拉升窗口试试效果,更好理解 // glViewport(0, 0, 600, 600); // 绘制三角形:三角形 数组起始索引 绘制多少个顶点 // 不使用索引缓冲器绘制顶点 #if 1 // 绘制六边的两两连接的三角形 // glDrawArrays(GL_TRIANGLES, 0, 18); #endif #if 1 // 索引缓冲器-添加代码 18个索引点(三角形,3个所以组成一个三角形) // 这里18代表是18个索引,顶点数坐标也是18,只是巧合罢了 glDrawElements(GL_TRIANGLES, 18, GL_UNSIGNED_INT, 0); #endif // 绘制三角形:线条 数组起始索引 绘制多少个顶点 // glDrawArrays(GL_LINES, 0 , 2); /* 新增代码 第二段/共三段----------------------------------END */ // 检查并调用事件,交换缓冲(执行交换缓存才会更新新数据到界面) glfwSwapBuffers(window); glfwPollEvents(); }
运行结果
源代码
#include <iostream> #include <process.h> #include <GLFW/glfw3.h> #include <glad/glad.h> void framebuffer_size_callback(GLFWwindow* window, int width, int height) { glViewport(0, 0, width, height); } // 在GLFW中实现一些输入控制,使用GLFW的glfwGetKey函数 void processInput(GLFWwindow *window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); } int main() { glfwInit(); /* 请确认您的系统支持OpenGL3.3或更高版本,否则此应用有可能会 崩溃或者出现不可预知的错误。如果想要查看OpenGL版本的话, 在Linux上运行glxinfo,或者在Windows上使用其它的工具(例如 OpenGL Extension Viewer)。如果你的OpenGL版本低于3.3,检 查一下显卡是否支持OpenGL 3.3+(不支持的话你的显卡真的太老 了),并更新你的驱动程序,有必要的话请更新显卡。 */ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 如果使用的是Mac OS X系统,你还需要加下面这行代码到你的 // 初始化代码中这些配置才能起作用 //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // 创建一个窗口对象,这个窗口对象存放了所有和窗口相关的数据, // 而且会被GLFW的其他函数频繁地用到 // glfwCreateWindow函数需要窗口的宽和高作为它的前两个参数。 // 第三个参数表示这个窗口的名称(标题),最后两个参数暂时忽略。 // 函数会返回一个GLFWwindow对象,我们会在其它的GLFW操作中使用到。 GLFWwindow* window = glfwCreateWindow(800, 600, "QQ:21497936", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); // GLAD是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数 // 之前我们需要初始化GLAD。 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } // 还需要注册这个函数,告诉GLFW我们希望每当窗口调整大小的时候调用这个函数。 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); // 我们必须告诉OpenGL渲染窗口的尺寸大小,即视口(Viewport), // 这样OpenGL才只能知道怎样根据窗口大小显示数据和坐标。 // 可以通过调用glViewport函数来设置窗口的维度(Dimension)。 // OpenGL幕后使用glViewport中定义的位置和宽高进行2D坐标的转 // 换,将OpenGL中的位置坐标转换为你的屏幕坐标。例如,OpenGL // 中的坐标(-0.5, 0.5)有可能(最终)被映射为屏幕中的坐标(200,450)。 // 注意,处理过的OpenGL坐标范围只为-1到1,因此我们事实上 // 将(-1到1)范围内的坐标映射到(0, 800)和(0, 600)。 glViewport(0, 0, 600, 600); // 在我们主动关闭它之前不断绘制图像并能够接受用户输入。因此, // 需要在程序中添加一个while循环,我们可以把它称之为渲染循环(Render Loop), // 它能在我们让GLFW退出前一直保持运行。 /* 新增代码 第一段/共三段----------------------------------START */ // 1.顶点着色器 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" "}"; // 2.片段着色器 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" "}"; // 3.创建顶点着色器并编译 int success; char infoLog[512]; int vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader); glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if(!success) { glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; } // 4.创建片段着色器并编译 int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader); glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if(!success) { glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl; } // 5.连接着色器 int shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success); if(!success) { glGetShaderInfoLog(shaderProgram, 512, NULL, infoLog); std::cout << "ERROR::SHADER::PROGRAM::COMPILATION_FAILED\n" << infoLog << std::endl; } // 6.删除已经使用的着色器(因为后面不再需要绑定了) glDeleteShader(vertexShader); glDeleteShader(fragmentShader); #if 0 // 7.顶点数据 float vertices[] = { -0.866, -0.5, 0.0, -0.866, 0.5, 0.0, 0.0 , 1.0, 0.0, // 三角形1 -0.866, 0.5, 0.0, 0.0 , 1.0, 0.0, 0.866, 0.5, 0.0, // 三角形2 0.0 , 1.0, 0.0, 0.866, 0.5, 0.0, 0.866, -0.5, 0.0, // 三角形3 0.866, 0.5, 0.0, 0.866, -0.5, 0.0, 0.0 , -1.0, 0.0, // 三角形4 0.866, -0.5, 0.0, 0.0 , -1.0, 0.0, -0.866, -0.5, 0.0, // 三角形5 0.0 , -1.0, 0.0, -0.866, -0.5, 0.0, -0.866, 0.5, 0.0, // 三角形6 }; #endif #if 1 // 索引缓冲器-添加代码 // 7.顶点数据 float vertices[] = { -0.866, -0.5, 0.0, // 1 三角形5 -0.866, 0.5, 0.0, // 1 2 三角形6 0.0 , 1.0, 0.0, // 三角形1 2 3 0.866, 0.5, 0.0, // 三角形2 3 4 0.866, -0.5, 0.0, // 三角形3 4 5 0.0 , -1.0, 0.0, // 三角形4 5 6 }; // 7.0索引缓冲期 增加的索引缓冲器顶点索引 unsigned int indices[] = { // 注意索引从0开始! 0, 1, 2, // 第一个三角形 1, 2, 3, // 第二个三角形 2, 3, 4, // 第三个三角形 3, 4, 5, // 第四个三角形 4, 5, 0, // 第五个三角形 5, 0, 1 // 第六个三角形 }; #endif // 8.使用VAO和VBO unsigned int VBO, VAO; // glGenVertexArrays() 创建一个顶点数组对象 // 第一个参数:需要创建的缓存数量 // 第二个参数:存储单一ID或多个ID的GLuint变量或数组的地址。 glGenVertexArrays(1, &VAO); // glGenBuffers() 创建一个缓存对象并且返回缓存对象的标示符。 glGenBuffers(1, &VBO); // 顶点对象创建之后,在使用缓存对象之前,需要将缓存对象连接到相应的缓存上。 // glBindBuffer()有1个参数:buffer。(绑定和解绑的顺序很重要,勿更改) glBindVertexArray(VAO); // 缓存对象创建之后,在使用缓存对象之前,需要将缓存对象连接到相应的缓存上。 // glBindBuffer()有2个参数:target与buffer。(绑定和解绑的顺序很重要,勿更改) glBindBuffer(GL_ARRAY_BUFFER, VBO); // 当缓存初始化之后,使用glBufferData()将顶点数据拷贝到缓存对象 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); #if 1 // 索引缓冲器-添加代码 // 8.0 与VBO类似,先创建EBO对象,再先绑定EBO // 然后用glBufferData把索引复制到缓冲里 // 同样,和VBO类似,把这些函数调用放在绑定和解绑函数调用之间 // 区别在于,只不过这次我们把缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER。 unsigned int EBO; glGenBuffers(1, &EBO); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); #endif // 设置顶点属性指针,glVertexAttribPointer()函数告诉OpenGL该如何解析顶点数据 // 顶点属性位置 顶点属性大小 数据的类型 是否被标准化 步长 偏移 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void *)0); // glEnableVertexAttribArray()以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的 glEnableVertexAttribArray(0); // 解绑缓存着色器(绑定和解绑的顺序很重要,勿更改) glBindBuffer(GL_ARRAY_BUFFER, 0); // 解绑顶点着色器(绑定和解绑的顺序很重要,勿更改) glBindVertexArray(0); /* 新增代码 第一段/共三段----------------------------------END */ while (!glfwWindowShouldClose(window)) { // 捕捉输入 processInput(window); // 清空颜色缓冲 // 添加渲染指令,背景RGBA的A不生效, 更改为白色 glClearColor(1.0f, 1.0f, 1.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); /* 新增代码 第二段/共三段----------------------------------START */ // 当我们渲染一个物体时要使用着色器程序 glUseProgram(shaderProgram); // 激活VAO表示的顶点缓存 glBindVertexArray(VAO); // 每次重新更新视口,如果不这样,拉升窗口后,会变形。 // 你把你要观察的视景体映射到你的视口就没有问题,但是如果你不设置视景体, // opengl默认会以屏幕坐标,就是屏幕中心为原点,到四周的距离为1的这个范围 // 作为视景体的观察部分映射到你设置的视口上。 // 可以尝试,屏蔽此句,然后运行程序拉升窗口试试效果,更好理解 // glViewport(0, 0, 600, 600); // 绘制三角形:三角形 数组起始索引 绘制多少个顶点 // 不使用索引缓冲器绘制顶点 #if 1 // 绘制六边的两两连接的三角形 // glDrawArrays(GL_TRIANGLES, 0, 18); #endif #if 1 // 索引缓冲器-添加代码 18个索引点(三角形,3个所以组成一个三角形) // 这里18代表是18个索引,顶点数坐标也是18,只是巧合罢了 glDrawElements(GL_TRIANGLES, 18, GL_UNSIGNED_INT, 0); #endif // 绘制三角形:线条 数组起始索引 绘制多少个顶点 // glDrawArrays(GL_LINES, 0 , 2); /* 新增代码 第二段/共三段----------------------------------END */ // 检查并调用事件,交换缓冲(执行交换缓存才会更新新数据到界面) glfwSwapBuffers(window); glfwPollEvents(); } /* 新增代码 第三段/共三段----------------------------------END */ glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); /* 新增代码 第三段/共三段----------------------------------END */ // 当渲染循环结束后我们需要正确释放/删除之前的分配的所有资源。 glfwTerminate(); return 0; }
拓展
只画边框(不填充,类似于画图的画刷),加上如下代码:
#if 1 // 使用xiankuan线框模式 随便放哪里,只要再glad之后(因为glad之后才能准确找到gl函数地址) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); #endi
线框模式(Wireframe Mode)
想用线框模式绘制图形,可知填充模式,通过glPolygonMode(GL_FRONT_AND_BACK,GL_LINE)函数配置OpenGL如何绘制图元。第一个参数表示我们打算将其应用到所有的三角形的正面和背面,第二个参数告诉用线来绘制。之后的绘制调用会一直以线框模式绘制三角形,直到我们用glPolygonMode(GL_FRONT_AND_BACK,GL_FILL)将其设置回默认模式。
拓展效果
原博主博客地址:http://blog.csdn.net/qq21497936
本文章博客地址:http://blog.csdn.net/qq21497936/article/details/78942027