OpenGL之VAO,VBO和EBO

一、BO(Buffer Object,缓冲对象)

        缓冲对象是OpenGL管理的一段内存,为了与我们CPU的内存区分开,一般称OpenGL管理的内存为:显存。

        显存,也就是显卡里的内存。显卡访问显存比较快,而Buffer Object,就是由OpenGL维护的一块显存区域。比如说在一块显存为2G的显卡里,分配了128K大小的内存区域给OpenGL使用,这个128K大小的内存区域,就叫一个Buffer Object。

        由于显卡访问显存,比访问内存(CPU里的内存区域)要快很多。而且显卡做运算,一般都是访问显存的数据,然后运算得到结果,并把结果也都保存在显存中。所以一般,需要先把数据,从内存传输到显存中去。

        显卡里申请的这片显存区域,存放顶点数据,就叫VBO,存放图像数据,就叫PBO,根据它存放的数据的不同,有不同的叫法。

  • 顶点数组对象:Vertex Array Object,VAO
  • 顶点缓冲对象:Vertex Buffer Object,VBO
  • 元素缓冲对象:Element Buffer Object,EBO 或 索引缓冲对象 Index Buffer Object,IBO

二、VBO(作用:管理在GPU上创建的显存

开始绘制图形之前,我们需要先给OpenGL输入一些顶点数据。OpenGL是一个3D图形库,所以在OpenGL中我们指定的所有坐标都是3D坐标(x、y和z)。OpenGL不是简单地把所有的3D坐标变换为屏幕上的2D像素;OpenGL仅当3D坐标在3个轴(x、y和z)上-1.0到1.0的范围内时才处理它。所有在这个范围内的坐标叫做标准化设备坐标(Normalized Device Coordinates),此范围内的坐标最终显示在屏幕上(在这个范围以外的坐标则不会显示)。

由于我们希望渲染一个三角形,我们一共要指定三个顶点,每个顶点都有一个3D位置。我们会将它们以标准化设备坐标的形式(OpenGL的可见区域)定义为一个float数组。

/*
由于OpenGL是在3D空间中工作的,而我们渲染的是一个2D三角形,
我们将它顶点的z坐标设置为0.0。这样子的话三角形每一点的深度(Depth)都是一样的,
从而使它看上去像是2D的。
*/
float vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

定义这样的顶点数据以后,我们会把它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。顶点着色器会在GPU上创建显存用于储存这些顶点数据,同时我们还需要告诉OpenGL如何解释这些显存(比如告诉OpenGL,顶点数据前三个是物体的三维坐标,后三个是顶点法线,再后两个是纹理坐标)。

顶点缓冲对象(VBO)的作用就是管理这个在GPU上创建的显存。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器访问顶点是个非常快的过程的。

  • 顶点缓冲对象(VBO)就像OpenGL中的其它对象一样,这个缓冲有一个独一无二的ID,所以我们可以使用glGenBuffers函数和一个缓冲ID生成一个VBO对象:
unsigned int VBO;
glGenBuffers(1, &VBO);

void glGenBuffers(GLsizei n,GLuint * buffers);这个函数的解释如下:

将n个当前未使用的缓冲对象名称(也就是ID),保存到buffers所指的内存区域中。这n个缓冲对象ID不一定是连续的整型数据(比如可能是1,5,8,而不一定是1,2,3,它们之间没有连续关系)

当然也可以声明一个unsigned int 数组,那么创建的n个缓冲对象的ID会依次保存在数组里。

unsigned int VBO[3];
glGenBuffers(3,VBO);

 也就是说,这时候VBO内会是一个从未被使用过的缓冲对象的ID,类似于给缓冲区起名字,起了一个独一无二名字。

  • 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);

glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。它的第一个参数是目标缓冲的类型:当前绑定到GL_ARRAY_BUFFER目标上的顶点缓冲对象。第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof计算出顶点数据大小就行。第三个参数是我们希望发送的实际数据。

第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:

  • GL_STATIC_DRAW :数据不会或几乎不会改变。
  • GL_DYNAMIC_DRAW:数据会被改变很多。
  • GL_STREAM_DRAW :数据每次绘制时都会改变。

三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它的使用类型最好是GL_STATIC_DRAW。如果,比如说一个缓冲中的数据将频繁被改变,那么使用的类型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,这样就能确保显卡把数据放在能够高速写入的内存部分。

现在我们已经把顶点数据储存在显卡的内存中,用VBO这个顶点缓冲对象管理。下面我们会创建一个顶点着色器和片段着色器来真正处理这些数据。现在我们开始着手创建它们吧。

三、链接顶点属性(设置顶点属性指针

即:将顶点属性链接到顶点数据(VBO),也可以说是为顶点数据设置顶点属性指针

1、glVertexAttribPointer(作用:配置顶点属性或者设置顶点属性指针):

我们已经把顶点数据发送给了GPU,但OpenGL还不知道它该如何解释内存中的顶点数据。GPU内的这块显存区域里是紧密连续的一块数据,我们需要告诉OpenGL,从哪里到哪里是一个顶点的数据,从哪里到哪里是这个顶点的RGB值等。

同时,我们也该告诉OpenGL,该如何将顶点数据链接到顶点着色器的属性上。

由于每个顶点都有一个3D坐标,我们就创建一个vec3输入变量aPos。我们同样也通过layout (location = 0)设定了输入变量的位置值(Location)你后面会看到为什么我们会需要这个位置值。

#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

在上文中,我们开辟了一片GPU显存空间,并调用glBufferData把顶点数据拷贝了进去,在那片显存空间中的数据应该是如下的形式:

我们想要OpenGL这样解析这段显存空间:

  • 位置数据被储存为32位(4字节)浮点值。
  • 每个位置包含3个这样的值。
  • 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。
  • 数据中第一个值在缓冲开始的位置。

我们可以使用glVertexAttribPointer函数,告诉OpenGL按照上面四条信息解析

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

glVertexAttribPointer函数的参数介绍:

  • 第一个参数指定我们要配置的顶点属性顶点属性的位置是定义在顶点着色器中的,就是上面我们在顶点着色器代码中使用layout(location = 0)定义了position顶点属性的位置值(Location),传0的意思是我们要把顶点数据链接到顶点着色器位置为0顶点属性上。
  • 第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
  • 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
  • 下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE
  • 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float之后,我们把步长设置为3 * sizeof(float)。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。
  • 最后一个参数的类型是void*,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。

每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取,则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。由于在调用glVertexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性0现在会链接到它的顶点数据。 

示例代码:(顶点着色器如下,顶点着色器指明了一个顶点有三个属性的输入

#version 330 core
layout (location = 0) in vec3 Attrib1;
layout (location = 1) in vec4 Attrib2;
layout (location = 2) in vec2 Attrib3;

......

void main()
{
    ......
}

在调用glBufferData将顶点数据输入到GPU缓冲内存中后,我们调用glVertexAttribPointer来设置顶点属性指针,告诉GPU如何解析顶点数据

//-- 将顶点属性1链接到当前VBO,即:为当前VBO设置顶点属性指针:0
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
//-- 为顶点属性2链接到当前VBO,即:为当前VBO设置顶点属性指针:1
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(3*sizeof(float)));
glEnableVertexAttribArray(1);
//--为顶点属性3链接到当前VBO,即:为当前VBO设置顶点属性指针:2
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(7*sizeof(float)));
glEnableVertexAttribArray(2);

2、glEnableVertexAttribArray:

glEnableVertexAttribArray(0)启用了顶点着色器的(location = 0)的属性变量

默认情况下,出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的,意味着数据在着色器端是不可见的,哪怕数据已经上传到GPU。只有由glEnableVertexAttribArray启用指定属性,才可在顶点着色器中访问逐顶点的属性数据。着色器能否读取到数据,由是否启用了对应的属性决定,这就是glEnableVertexAttribArray的功能,允许顶点着色器读取GPU显存内的数据。

至此,我们已经完成了:
1、将数据从CPU内存传输进GPU显存中
2、告诉OpenGL该如何解释显存内的数据
3、赋予了顶点着色器读取显存内数据的权限

四、VAO(Vertex Array Object)

顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中(下方蓝色字)。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中

OpenGL的核心模式要求我们使用VAO,所以它知道该如何处理我们的顶点输入。如果我们绑定VAO失败,OpenGL会拒绝绘制任何东西。

一个顶点数组对象会储存以下这些内容

  • glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
  • 通过glVertexAttribPointer设置的顶点属性配置。
  • 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。

创建一个VAO和创建一个VBO很类似:

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

要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。这段代码应该看起来像这样:

// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

[...]

// ..:: 绘制代码(渲染循环中) :: ..
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();

就这么多了!前面做的一切都是等待这一刻,一个储存了我们顶点属性配置的VAO和这个VAO所使用的VBO。一般当你打算绘制多个物体时,你首先要生成/配置所有的VAO和必须的VBO及属性指针,然后储存它们供后面使用。当我们打算绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。

五、EBO

自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,建立了一个顶点和一个片段着色器,并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。在OpenGL中绘制一个物体,代码会像是这样:

// 0. 复制顶点数组到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. 为当前VBO设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2. 当我们渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);
// 3. 绘制物体
someOpenGLFunctionThatDrawsOurTriangle();

每当我们绘制一个物体的时候都必须重复这一过程。这看起来可能不多,但是如果有超过5个顶点属性,上百个不同物体呢(这其实并不罕见)。绑定正确的缓冲对象,为每个物体配置所有顶点属性很快就变成一件麻烦事。有没有一些方法可以使我们把所有这些状态配置储存在一个对象中,并且可以通过绑定这个对象来恢复状态呢?

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>