Qt-OpenGL VAO, VBO, IBO简析

本文最后更新于:2023年3月6日 晚上

前情提要

众所周知,初入OpenGL炼狱,便是一坨让人不明所以、一脸懵逼的名词

比如什么:VAOVBOIBO

啊,对,就你们仨,枪打出头鸟啊,就你们仨打头阵啊,还都以O结尾啊

那就干你们了

VBO(Vertex Buffer Object :顶点缓冲对象)

这一听就和顶点有关,实际上就是用来存储顶点相关信息的(坐标、颜色、贴图坐标、法向量等)

这个对象可以把顶点相关数据一次性发送到GPU,减少与CPU通信开销

毕竟三维世界也需要二维的线和一维的点进行构建嘛

所以顶点信息是最基本的

比如这样:

1
2
3
4
5
6
float pos[] = {
-150, -150, 0, 0,
150, -150, 1, 0,
150, 150, 1, 1,
-150, 150, 0, 1
};

每一行是一个顶点的信息,前两个数代表二维坐标,后两个数代表贴图坐标(贴图与顶点的对应关系)

但是你会发现一个奇怪的点,为什么我要用一维数组呢,酱紫不是没有任何结构(struct or class)吗?

一点也不面向对象啊,这是因为OpenGL提供的API就只支持一维数组,并且需要手动解释其中的数据(区分哪些是坐标、哪些是颜色、贴图之类的)

类似于:

1
2
3
4
5
6
7
8
9
10
11
12
GLuint buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(float)*8, pos, GL_STATIC_DRAW);
//↑绑定数据(上传到GPU)

//↓解释数据
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float)*4, 0);

glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(float)*4, (void*)(sizeof(float) * 2));

顶点数据是怎么解释的呢,就是通过glEnableVertexAttribArray这个函数

1
2
3
4
5
6
7
8
void glVertexAttribPointer(
GLuint index,
GLint size,
GLenum type,
GLboolean normalized,
GLsizei stride,
const GLvoid* pointer
);

比如这一句:

1
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float)*4, 0);

ta的意思就是:

对于0号属性(也就是坐标,数字不重要 不重复就行),由相邻的两个元素组成(二维坐标),类型是GL_FLOAT(浮点型),不开启归一化(GL_FALSE),两个数据(坐标)之间的步长是4个float字节的长度,并且距离数组起点的偏移量是0

对于贴图坐标,也可以酱紫解释(命名为1号属性),这里属性的顺序啥的都不重要,只要在shader(着色器)中能自己对应上就可以

Why

为什么要如此麻烦呢,为什么不给一些类和结构,而是从一维数组,原始数据开始解释呢

推测:OpenGL是底层图形库,酱紫可以提供最高的性能以及自由度,供程序员自由封装;同时也对GPU来讲更好读取

IBO (Index Buffer Object:索引数组对象) or EBO(Element Buffer Object)

其实有了VBO就可以开始绘制图形了,为啥还要IBO

IBO也叫EBO,两种叫法,但是第一种更见名知意,一看就知道是保存索引的

IBO的存在是为了优化内存

比如:我们要画一个正方形,众所周知,一个正方形可以由两个三角形构成(对角分)

两个三角形有6个顶点,但是正方形只需要4个顶点

正方形

如此一来,有两个顶点的数据就是重复的,在VBO里包含6个顶点的信息显然是十分滴stupid

那为何不直接用索引来代表顶点呢,比如给四个顶点编号(1,2,3,4)

然后告诉OpenGL,我要绘制两个三角形,一个是(1,2,4),一个是(2,3,4)

酱紫可不方便多了

保存索引的对象就叫IBO,索引缓冲对象

1
2
3
4
GLuint indices[] = {
0,1,2, //三角形1
2,3,0 //三角形2
};

VAO(Vertex Array Object:顶点数组对象)

诶诶,有数据有索引了,还优化内存了,你还想咋样a,aaaaaa

是酱紫,兄弟,我知道你很急,但是你先别急

你看a,一组数据和一组索引可以绘制一个图形,但是假如有很多图形,每次切换着绘制,绘制前都要绑定这绑定那的有点麻烦

不如来一个东东来记录一下我们的操作(比如对顶点信息的解释,比如顶点数据)

这个东东就是VAO,可以保存我们的操作,酱紫就可以只绑定VAO实现绘制(通过VAO访问各种数据)

具体保存了啥呢?

  • vertex attribute 对应的 VBO 的id,glBindBuffer 设置。

  • vertex attribute 的格式,由 glVertexAttribPointer 设置

  • vertex attribute 的开启或关闭,glEnableVertexAttribArrayglDisableVertexAttribArray

  • **#当前#**绑定的 GL_ELEMENT_ARRAY_BUFFER(索引缓冲数组,IBO) 的名字,由 glBindBuffer 设置

也就是保存了VBOIBO,但是他们之间有一个很大的区别

  • VBO是通过glVertexAttribPointer这个函数进行绑定的
  • 而IBO是通过glBindBuffer函数进行绑定的

这是因为一个VAO可以绑定多个VBO(可以用不同VBO保存不同数据(坐标、颜色可以分开))

但是只能绑定一个IBO(索引)

// 绑定0号缓冲区就是解绑:VBO: glBindBuffer(GL_ARRAY_BUFFER, 0) or IBO: glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)

所以VBO的解绑并不会影响VAO对其数据的访问(已经通过glVertexAttribPointer保存了指针),而IBO的解绑就会导致VAO无法访问索引(变成0号IBO了)

深入VAO

实际上,在现代OpenGL中,在VAO出现后(v3.0),必须使用VAO才能绘制

如果你不显示地定义一个VAO,那么默认(兼容模式)会提供的一个默认VAO以供绑定;//核心模式不提供

所以,我们可以简单地这么想:

OpenGLdraw绘制函数就是从当前的VAO中读取VBO和IBO信息,并进行绘制的

那么,只要在绘制前,VAO中绑定了IBO,就可以正常运行

而在定义IBO时,并不需要绑定VAO

所以我们可以酱紫:

初始化部分:伪代码

1
2
3
4
5
6
7
8
9
10
VAO.bind();
VBO.bind();
VBO.setData();
//解释VBO数据
VBO.unbind();
VAO.unbind();

IBO.bind();
IBO.setData();
IBO.unbind();

绘制部分:伪代码

1
2
3
4
5
6
7
8
VAO.bind();
IBO.bind(); //绘制前再绑定到VAO
//draw()绘制
IBO.unbind();

IBO2.bind(); //切换IBO
//draw()绘制
IBO2.unbind();

哦对,还想起个问题,为啥VAO这名嫩奇怪啊,顶点数组对象?

其实是酱紫,我们可以吧ta看成一个数组,每一个元素都是一组顶点数据,通过glVertexAttribPointer设置

比如VAO[0]是坐标,VAO[1]是颜色,VAO[2]是法向量酱紫(当然通过[]访问是为了好理解)

Peace

Ref

OpenGL理解VBO,IBO,VAO_vbo和ibo_Mhypnos的博客-CSDN博客

[Modern OpenGL]谈谈VAO、VBO、IBO_SixDayCoder的博客-CSDN博客

VAO和EBO解绑定的坑? - 知乎

OpenGL VAO VBO EBO(IBO)的绑定、解绑问题_解绑vbo,veo_csu_xiji的博客-CSDN博客

Opengl的VAO个人理解 - 知乎

特别鸣谢New Bing & ChatGPT


Qt-OpenGL VAO, VBO, IBO简析
https://mrbeancpp.github.io/2023/03/06/Qt-OpenGL-VAO与IBO简析/
作者
MrBeanC
发布于
2023年3月6日
许可协议