本文章首发于我的个人博客,希望大家多多支持!


Hi! This is Showhoop Studio!

如果要从代码层面去理解渲染管线的工作,学习使用OpenGL编程可以说是一个不错的选择。这里我将记录下自己的一下学习笔记,以便日后复习和引用。对于刚刚开始学习或者准备入门学习OpenGL的人,推荐去看LearnOpenGL,除了理论知识之外,这个教程会同时手把手教你搭建自己的OpenGL程序!


什么是VAO,VBO和EBO

VAO,Vertex Array Object:顶点数组对象,
VBO,Vertex Buffer Object:顶点缓冲对象,
EBO(或IBO),Element Buffer Object(或Index Buffer Object):索引缓冲对象。

顶点缓冲对象VBO

我们先来介绍顶点缓冲对象VBO。简单来说,VBO是位于GPU内存(即显存)中的一块内存区域,存储了大量顶点数据。使用VBO的好处就是可以让我们一次性发送大量数据到GPU上,毕竟从CPU到GPU的数据传输在计算机层面来看是一个相对较慢的过程。

这个缓冲对象有一个独一无二的ID,所以我们可以使用glGenBuffers函数和一个缓冲ID来生成一个VBO对象:

unsigned int VBO;
glGenBuffers(1, &VBO);

在OpenGL中有很多种缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER上:

glBindBuffer(GL_ARRAY_BUFFER, VBO);

在这之后,我们使用任何在GL_ARRAY_BUFFER目标上的缓冲调用都会用来配置当前绑定的缓冲(VBO)。然后我们调用glBufferData函数可以把定义好的顶点数据复制到缓冲的内存中:

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

因为我们的重点是理解这些对象,而不是如何在OpenGL中使用,所以这里不具体介绍这个函数每个参数的意义,如果你想要了解可以参考这里。

那么,VBO到底是什么呢?我们可以在之前的简单说明下,具体的解释一番。之前说到,VBO中存储了大量顶点数据,而这些顶点数据是紧密排列的,即使是不同的数据(如法线和颜色)也可能彼此相连。我们假设目前只定义了顶点的位置数据(OpenGL中的顶点数据均为浮点值),那么它们在VBO中排列就会像下图所展示出来的那样:

我们知道,一个浮点数为4个字节,顶点位置一共由三个浮点数值来表示,分别是x、y和Z,因为这是我们定义的。然而在VBO中,并不会像图片上那样有明显的分割线,所以GPU不知道该如何处理这些数据。

顶点数组对象VAO

有了顶点数组对象VAO的帮助,上面的问题就可以得到解决了。VAO是由一系列 顶点属性指针(Vertex Attribute Pointer) 组成的,每一个指针都会指向VBO中的一块区域。那么,VAO是如何实现这一点的呢?

首先,和VBO对象一样,我们也要创建一个新的VAO对象并绑定:

unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);

在把顶点数组中的数据复制到缓冲中之后,开始配置顶点属性指针,告诉OpenGL如何解析VBO中的数据:

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
// 顶点属性默认是禁用的,启用顶点属性
glEnableVertexAttribArray(0);

通过上述的glVertexAttribPointer函数,我们便完成了顶点属性指针的配置:

  • 第一个参数指定我们要配置的顶点属性,这一点要结合顶点着色器来看。在顶点着色器中,其顶点数据的输入需要location来指定,下面就是一个例子。在这个示例中,我们给顶点着色器定义了一个输入,也就是顶点的位置,它被指定在输入布局的0的位置,所以第一个参数我们指定为0,告诉OpenGL,这个指针所指向的VBO中的数据要传入着色器中顶点的位置数据里。
layout (location = 0) in vec3 position;
  • 第二个参数指定顶点属性的大小。顶点的位置属性是一个vec3,由3个值组成,所以大小是3。
  • 第三个参数指定数据类型,GL_FLOAT即浮点值。
  • 第四个参数定义我们是否希望数据被标准化,一般置为GL_FALSE。
  • 第五个参数叫做步长(Stride),这个非常重要,它告诉我们连续的顶点数据之间的间隔。一个位置数据由三个float组成,下一个位置数据的开始必定在三个float之后,所以我们指定步长为3 * sizeof(float),这样我们就将每个顶点的位置数据分隔开了。
  • 最后一个参数是void*,所以我们需要进行奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量。由于数据在数组的开头,所以这里是0(如果你没有理解也没关系,之后会具体介绍这一个参数)。

使用VAO的好处显而易见:

  1. 首先,它告诉了OpenGL如何解析VBO中的数据,
  2. 其次,在有很多种不同物体存在的情况下,我们可以将它们的属性链接到不同的VAO上,在绘制的时候,绑定不同的VAO就可以了。

想要把不同物体的顶点数据链接到不同的VAO上,需要给这些物体各指定一个不同的VBO。而顶点属性具体从哪个VBO中获取数据则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。所以我们在用顶点属性获取不同VBO的数据之前要更换VAO和VBO的绑定。不妨看一段示例代码来帮助理解。需要注意的一点是,我们在配置顶点属性指针之前要先绑定VAO,在启用之后会自动解绑,也就是说,你在绘制之前要再一次绑定VAO。在你已经配置了多个VAO的情况下,你想绘制哪个图形,就绑定哪个VAO。

// 绑定第一个物体的VAO
glBindVertexArray(VAO1);
// 绑定第一个物体的VBO
glBindBuffer(GL_ARRAY_BUFFER, VBO1);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices1), vertices1, GL_STATIC_DRAW);
// 设置顶点指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);// 绑定第二个物体的VAO
glBindVertexArray(VAO2);
// 绑定第二个物体的VBO
glBindBuffer(GL_ARRAY_BUFFER, VBO2);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices2), vertices2, GL_STATIC_DRAW);
// 设置顶点指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttributePointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);// 在绘制之前,如果你想绘制第一个图形就绑定VAO1
glBindVertexArray(VAO1);

对应上述代码的图片描述如下:

通过图片,我们可以更清楚的看到VAO和VBO之间的关系:

  1. 每一种相同的物体对应一个VAO和一个VBO,
  2. VAO中的一个指针对应了VBO中的一种顶点属性,有几种顶点属性,就需要几个指针。换句话说,调用一次glVertexAttribPointer就是设置一个指针。

现在,我们来吧重点放到第二个物体的配置上,因为它有两种顶点属性,一种是顶点位置,另一种是纹理坐标。还记得我们之前所讲的glVertexAttribPointer中的最后一个参数吗?我们将通过这里来进行具体的介绍。我们在配置第二个物体的顶点属性时,调用了两次函数,显然每一次对应一种属性。第一次调用是在配置位置属性:

  1. 第一个参数我们指定为0,这是由顶点着色器中的定义layout (location = 0) in vec3 position决定的,
  2. 第二个参数指定为3,因为顶点位置属性由3个浮点值组成,
  3. 第五个参数需要注意,与之前不同的是,这里我们需要指定步长为5 * sizeof(float),也就是对应一个顶点的所有的属性的大小,位置属性占3个浮点值,纹理坐标占2个浮点值。
  4. 最后的参数指定为(void*)0,因为位置属性是从一个顶点属性的起始开始算起的(也是就是0),再结合第二个参数,属性指针就知道获取位置属性要从一个顶点的所有属性内,从0开始读取3个浮点值。

对于第二次调用,我们的目的是获取纹理坐标:

  1. 第一个参数我们指定为1,这是由顶点着色器中的定义layout (location = 1) in vec2 texcoord决定的(当然你也可以指定其他数字,只要不重复即可),
  2. 第二个参数我们指定为2,因为纹理坐标属性由2个浮点值组成,
  3. 第五个参数与之前无异,
  4. 最后一个参数指定为(void*)3 * sizeof(float),因为纹理坐标属性紧密排列在位置属性之后,位置属性从0开始占用了3 * sizeof(float)的大小,那么纹理坐标属性必然是从3 * sizeof(float)开始读取2个浮点值,一共是5个浮点值,也就是一个顶点所有属性的大小。

现在,你应该对最后一个参数有了更加深刻的理解。

索引缓冲对象EBO

要解释索引缓冲对象,我们先来假设一个情景:我们要绘制两个三角形来组成一个矩形(OpenGL主要处理三角形),我们要定义如下顶点集合:

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   // 左上角
}

显而易见,一个矩形只有四个顶点,而我们定义了六个顶点,因为我们指定了 右下角左上角 两次!这样就产生了额外开销,而当我们有包括上千个三角形的模型之后问题会更糟糕。更好的解决方案是,只储存不同的顶点,并设定绘制这些顶点的顺序,而EBO恰好提供了这样的功能。

我们首先要定义不重复的顶点和每个顶点的索引(顶点索引从0开始):

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  // 第二个三角形
}

与VBO类似,下一步我们要创建EBO对象并绑定:

unsigned int EBO;
glGenBuffer(1, &EBO);
// 还记得吗?我们之前提到过,OpenGL允许同时绑定多个Buffer,
// 但不能是同一类型,这里我们指定为GL_ELEMENT_ARRAY_BUFFER
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

要注意的是,我们传递了GL_ELEMENT_ARRAY_BUFFER当作缓冲目标。最后一件要做的事是用glDrawElements来替换glDrawArrays函数,来指明我们从索引缓冲渲染。使用glDrawElements时,我们会使用当前绑定的索引缓冲对象中的索引进行绘制:

// 跟VAO类似,在绘制之前需要再绑定一次,但这不是必须的(之后有解释)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

第一个参数指定了我们绘制的模式,这个和glDrawArrays的一样。第二个参数是我们打算绘制顶点的个数,这里填6,也就是说我们一共需要绘制6个顶点。第三个参数是索引的类型,这里是GL_UNSIGNED_INT。最后一个参数里我们可以指定EBO中的偏移量(或者传递一个索引数组,但是这是当你不在使用索引缓冲对象的时候),但是我们会在这里填写0。

glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取索引。这意味着我们必须在每次要用索引渲染一个物体时绑定相应的EBO,这还是有点麻烦。不过顶点数组对象同样可以保存索引缓冲对象的绑定状态。VAO绑定时正在绑定的索引缓冲对象会被保存为VAO的元素缓冲对象。绑定VAO的同时也会自动绑定EBO。


引用和参考

[1] LearnOpenGL link


欢迎在评论区留下自己的问题,我会尽快给出回复。你也可以指出文章的纰漏,或是给出你对文章的其他看法。你也可以在文章页面右侧的在线聊天室与我取得联系。如果你喜欢这篇文章,可以点击文末或者页面左侧的分享按钮分享给其他人,非常感谢!

关于VAO,VBO和EBO的理解-OpenGL学习笔记相关推荐

  1. OPenGL 学习笔记之 VBO VAO EBO 概念和使用方法总结

    目录 一.  基本概念: 二. 理解缓冲对象 glVertex 函数 顶点数组(Vertex Array) 三.  VBO(Vertex Buffer Object)顶点缓冲区对象 大体流程理解: Q ...

  2. OpenGL学习笔记(八):进一步理解VAO、VBO和SHADER,并使用VAO、VBO和SHADER绘制一个三角形

    原博主博客地址:http://blog.csdn.net/qq21497936 本文章博客地址:http://blog.csdn.net/qq21497936/article/details/7888 ...

  3. OpenGL学习笔记(十三):将纹理贴图应用到四边形上,对VAO/VBO/EBO/纹理/着色器的使用方式进行总结

    原博主博客地址:http://blog.csdn.net/qq21497936 本文章博客地址:http://blog.csdn.net/qq21497936/article/details/7919 ...

  4. OpenGL学习笔记(一):环境搭建、三维空间坐标系理解以及OpenGL的基本使用

    原博主博客地址:http://blog.csdn.net/qq21497936 本文章博客地址:http://blog.csdn.net/qq21497936/article/details/7866 ...

  5. 【OpenGL学习笔记⑧】——键盘控制正方体+光源【冯氏光照模型 光照原理 环境光照+漫反射光照+镜面光照】

    ✅ 重点参考了 LearnOpenGL CN 的内容,但大部分知识内容,小编已作改写,以方便读者理解. 文章目录 零. 成果预览图 一. 光照原理与投光物的配置 1.1 光照原理 1.2 投光物 二. ...

  6. 【OpenGL学习笔记】地月系

    OpenGL学习笔记2-地月系 文章目录 OpenGL学习笔记2-地月系 前言 运行结果 纹理图片 一.TexturePool 1.**TexturePool.h** 2.**TexturePool. ...

  7. 【OpenGL学习笔记⑥】——3D变换【旋转的正方体 实现地月系统 旋转+平移+缩放】

    ✈️ 文章目录 零. 成果预览图 一.3D立方体的顶点数组 二.纹理旋转 三.纹理缩放 四.画n个3D图形 五.轨道的数学公式 六.深度缓冲(Z 缓冲) 七.完整代码 八.参考附录: 神器的正方体 ☁ ...

  8. OpenGL学习笔记:矩阵变换

    文章目录 缩放 glm矩阵表示 glm缩放矩阵实现 位移 齐次坐标 glm位移矩阵实现 旋转 沿x轴旋转 沿y轴旋转 沿z轴旋转 沿任意轴旋转 glm旋转矩阵实现 矩阵的组合 glm矩阵组合使用 接上 ...

  9. OpenGL 学习笔记 II:初始化 API,第一个黑窗,游戏循环和帧率,OpenGL 默认垂直同步,glfw 帧率

    前情提要: 上一篇: OpenGL 学习笔记 I:OpenGL glew glad glfw glut 的关系,OpenGL 状态机,现代操作系统的窗口管理器,OpenGL 窗口和上下文 OpenGL ...

最新文章

  1. 下拉菜单被挡住了,DIV置于最底层的方法
  2. SpringBoot与Shiro整合-概述
  3. 关于Ubuntu16.04下安装VMwareTools失败,未发现软件包open-vm-dkms,open-vm-tools问题解决
  4. wireshark 十六进制过滤_Wireshark过滤表达式大全
  5. 有监督学习 —— KNN算法
  6. vCenter如何逃离Windows的坑(转)
  7. java 代码整洁快捷方式_代码整洁之道:你的代码是否足够优雅、整洁、易懂?...
  8. 免费的中文OCR软件
  9. caffe for Windows下的编译错误
  10. mysql的db.opt文件_MySQL数据库的db.opt文件
  11. LVS(三)LVS集群NAT模式
  12. java爱心效果代码来了
  13. protues仿真控制舵机
  14. 利用python将Mooc缓存转换为视频文件
  15. vm安装win7系统
  16. css+div透明参数设置
  17. JNI基础:JNI数据类型和类型描述符
  18. 视频教程-2019年人工智能热门案例精讲之歌词生成器-机器学习
  19. python圆形代码_python圆形函数
  20. Spark SQL_JZZ158_MBY

热门文章

  1. 项目奖金一般是多少_放下DOTA2和LOL不谈,奖金池20万美元的VR电竞是什么等级
  2. c语言中十进制转化二进制八进制十六进制,进制转换:二进制、八进制、十六进制、十进制之间的转换...
  3. 微信小程序与H5标签、样式对比
  4. AutoJs学习-实现王者自动截图
  5. Windows权限维持--创建隐藏账户(影子账户)
  6. File/String类常用方法
  7. ffmpeg命令把Hevc 10bit转成8bit,不需要SEI
  8. 扫盲:-zilla的来源
  9. 查看SQL数据库中是否含有全角字符
  10. PNG图片素材转化矢量化的图形,应用到ppt一招就够