【Opengl概念】VBO和VAO到底是个啥关系?
目录
- 一、前言
- 二、VAO和VBO的关系
- 2.1 什么是VAO、VBO
- 2.2 如何将多个VBO投入到同一个VAO名下
- 三、使用VBO注意点
- 3.1 什么是VBO
- 3.2 如何创建VBO
- 3.3 VBO可能绑定的目标
- 3.4 给VBO灌入数据
- 3.5 给VBO数据定义指针
- 四、关于VAO下操作事项
一、前言
对于Opengl中的VBO和VAO相信很多人都熟悉这2个名字,但是有时候缺容易混淆2个概念或者说不理解这2个概念的作用是什么。本文对这2个概念做了对比和参照。
二、VAO和VBO的关系
2.1 什么是VAO、VBO
对于VBO我们首先应该立刻明白,它就是命令GPU生成一个Buffer,并且通过VBO这个标识符号访问操作。那么VAO呢?VAO是个虚的标识符号,在传唤VAO时候,VAO旗下的所有VBO都被执行。
这里打个浅显的比方:VAO表示一个文件夹,VBO是这个文件夹下的具体文件。当我们用:
COPY VAO sss;
将VAO和其中内容全部拷贝到sss下面,因此,VAO下面的VBO自然也不例外了。手册上说VAO是管理VBO的,这样说有点过头,其实VOA从来没有管理过VBO,只是简单标注一下而已。
2.2 如何将多个VBO投入到同一个VAO名下
这个问题很简单,我们将用一个实际小例子回答:
step1:创建一个VAO
unsigned int VAO;
glGenVertexArrays(1, &VAO);
此步骤告诉GPU。已经有一个VAO试图对应某些缓存。
step2:打开一个VAO
glBindVertexArray(VAO);
此处打开一个VAO,好比操作系统打开一个空文件夹。有待于建立文件。下面操作均在VOA名下完成。
step3:在一个VAO下挂载多个VBO
1)建立一个VBO1
unsigned int VBO1;
glGenBuffers(1, &VBO1);
glBindBuffer(GL_ARRAY_BUFFER, VBO1); 此处打开一个VBO1,后面操作均在VBO1之下
... some operation...
以上对VBO1进行操作,操作后关闭此VBO1:
glBindBuffer(GL_ARRAY_BUFFER, 0); 解绑上文的VBO1
2)再建立一个VBO2
unsigned int VBO2;
glGenBuffers(1, &VBO2);
glBindBuffer(GL_ARRAY_BUFFER, VBO2); 此处打开一个VBO2,后面操作均在VBO1之下
... some operation...
基本上按照上面操作,这里不再赘述;值得强调的是,这里统统在VAO打开的前提操作,因此自然全挂在VAO名分之下。
3)VAO解绑
glBindVertexArray(0);
此处关闭VAO状态,以后的VBO建立自然不在VAO名下。
step4:以后使用VAO
很简单,只要:
glBindVertexArray(VAO);
下面可以使用VBO1和VBO2了
三、使用VBO注意点
3.1 什么是VBO
VBO全名顶点缓冲对象(Vertex Buffer Object),他主要的作用就是可以一次性的发送一大批顶点数据到显卡上,而不是每个顶点发送一次。我们知道CPU传送数据给GPU其实是比较耗费时间的,所以尽可能的一次性把需要的顶点数据全部传给GPU,这样顶点着色器几乎能立即访问到顶点,有助于加快顶点着色器效率。
3.2 如何创建VBO
就目前所有游戏引擎来说VBO的机制已经是基础了,我们看下VBO在Opengl中是如何创建的。
首先,我们需要在Opengl生成一个缓冲类型的ID。
unsigned int VBO;
glGenBuffers(1, &VBO);
这里我们创建了一个缓冲的ID,当然了你也可以通过数组来批量创建一系列的VBO的ID,
unsigned int VBO[3];
glGenBuffers(3,VBO);
注意:这个时候GPU并不产生缓存。相当于定义了一个空指针。
接下来,我们要将这个ID绑定给指定类型来告诉Opengl这个缓冲是什么类型的,此操作相当于对buffer的数据属性进行指定;
glBindBuffer(GL_ARRAY_BUFFER, VBO);
这里我们将VBO赋予了GL_ARRAY_BUFFER的类型,告诉Opengl这个VBO变量是一个顶点缓冲对象。
注意1,这里是顶点类型的缓存,也可能是其它的缓存。
注意2,从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲操作都会用来配置当前绑定的缓冲(VBO)
至此对于VBO的声明就结束了,其实也是蛮简单的,我们回顾下:
1)生成一个缓冲类型的ID;
2)指定ID的缓冲类型为GL_ARRAY_BUFFER;
3.3 VBO可能绑定的目标
上文谈到 *glBindBuffer(GL_ARRAY_BUFFER, VBO);*绑定了VBO
到GL_ARRAY_BUFFER类型缓存。其实可以绑定的缓存有很多种,下表列出:
3.4 给VBO灌入数据
接下来我们看下,如何给VBO赋值,我们假定有如下三角形的顶点数据,此数据在CPU一侧,待灌入VOB:
float vertices[] = { // 位置 // 颜色0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部};
Opengl通过glBufferData接口来复制顶点数据到缓冲中供Opengl使用,对于上面的三角形顶点数据,代码就可能是这样的:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
第一个参数表示目标的缓冲类型,这里指当前绑定到GL_ARRAY_BUFFER上的顶点缓冲对象,第二个参数表示数据大小(字节为单位),第三个参数表示我们实际发出的数据。第四个参数GL_STATIC_DRAW表示Opengl如何处理上传的数据,Opengl中一共有三种类型:
GL_STATIC_DRAW :数据不会或几乎不会改变。
GL_DYNAMIC_DRAW:数据会被改变很多。
GL_STREAM_DRAW :数据每次绘制时都会改变。
到这一步,我们的数据就已经上传到GPU上去了。一个VBO过程也就结束了。
3.5 给VBO数据定义指针
那这里引申出一个问题,我们看到vertices中有注释,表明这个数组里面有两种数据,一种是顶点坐标,一种是顶点颜色,那都放在一个float的数组中,Opengl如何来区分他们呢?这里就要用到glVertexAttribPointer这个接口了,该接口就是帮助Opengl解释如何处理数据的。我们来看下对于vertices的数据处理的代码:
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
我们分析下这段代码,首先我们来解释下glVertexAttribPointer的各个参数的意义。
第一个参数表示我们希望数据中哪部分数据放在对应的Location位置上,这个可能主要体现在shader部分,我们看下顶点着色器的代码:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
我们根据变量名大致能猜出来location为0的是顶点坐标,location为1的是颜色值,这里的location就是对应了上面代码种glVertexAttribPointer第一个参数,location没有准确的位置定义,并没有说location为0的一定要是顶点坐标属性,也可以是颜色或者uv坐标属性,只是常规来说,我们习惯将坐标放在第一个位置。
glVertexAttribPointer第二个参数表示属性大小,坐标和颜色的大小都是3,所以这里填3;
第三个参数表示数据类型;
第四个参数表示我们是否希望数据标准化。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE;
第五个参数表示步长,数据之间的间隔,我们这里不管坐标还是颜色都是3个分量,所以坐标和坐标数据之间隔了6个float字节,颜色和颜色之间隔了6个float字节;
第六个参数表示偏移量,即在一段数据中,指定的数据偏移多少位置开始。在这里,坐标数据都是每段数据的起始位置,所以偏移量是0,而颜色数据在坐标数据之后,坐标数据有3个分量,所以每个颜色数据偏移三个float字节开始算;
每次设定好一个location的值之后,记得要开启对应的位置数据glEnableVertexAttribArray,因为Opengl默认是全关闭的。
经过glVertexAttribPointer执行之后,shader中对应layout的位置数据就有对应的值了。