OpenGL日常-三角形(下)——VBO/VAO/EBO

文章目录

    • 写在前面
    • 本文重点
      • glBegin/glEnd
      • DL
      • VA
      • VBO
      • VAO
      • EBO
    • 总结

大家好,欢迎来到听风的OpenGL日常。

写在前面

本文代码

上回说到,预期的三角形并没有,并没有,并没有渲染出来,今天我们来补充下,看看问题出在哪里。

本文重点

我们先来搞清楚VAO,VBO缓存到底做的是什么工作?

首先是VBO(vertex buffer object),为什么我们要用VBO?

不使用VBO时,我们每次绘制(glDrawArrays)图形时都是从本地内存处获取顶点数据然后传输给OpenGL来绘制,这样就会频繁的操作CPU->GPU增大开销,从而降低效率。
使用VBO,我们就能把顶点数据缓存到GPU开辟的一段内存中,然后使用时不必再从本地获取,而是直接从显存中获取,这样就能提升绘制的效率。

在讲清楚这个概念之前,我们还要补充一些概念;

glBegin/glEnd

以此文的例子解释,我们在传递顶点位置数据的时候,在OpenGL旧版本里,是通过glVertex逐个从CPU传递到GPU的,代码示例如下:

glBegin(GL_TRIANGLES);
    glVertex(0.0f, 0.0f);
    glVertex(1.0f, 0.0f);
    glVertex(0.0f, 1.0f);
glEnd();

这样每进行一次glVertex调用就会向GPU传递一次,由于传输是同步的,所以效率很低;

OpenGL日常-三角形(下)——VBO/VAO/EBO_第1张图片

于是:

DL

Display List(显示列表)的出现可以使CPU在传输的过程中等待数据打包完成,待其结束一次性发送到GPU,代码如:

GLuint listName = glGenLists (1);
glNewList (listName, GL_COMPILE);
    glBegin (GL_TRIANGLES);
        glVertex2f (0.0, 0.0);
        glVertex2f (1.0, 0.0);
        glVertex2f (0.0, 1.0);
    glEnd ();
glEndList ();
...
// 绘制(不传输数据)
glCallList(listName);

OpenGL日常-三角形(下)——VBO/VAO/EBO_第2张图片

显示列表加快了传输效率,但是绘制时是一次性的,那么如果列表中的某单个顶点发生变化时,那么就需要CPU重新生成新的顶点再发送到GPU,GPU收集完成后完成绘制,这样做是极其浪费资源的,当每一帧都有变化时,它就退化成了单个顶点传输的方式。

VA

Vertex Array,顶点数据要区别于我们开头提到的VAO,它跟缓存是没有关系的,它也是一种传输方案。VA也是通过收集顶点的方式来减少传输次数,但与显示列表不同的是,CPU端将会负责收集所有顶点,收集完成后一次性传输到GPU再进行绘制。

OpenGL日常-三角形(下)——VBO/VAO/EBO_第3张图片

这样做导致的结果是,每次进行绘制时,都会进行一次传输,所以绘制速度会低于显示列表。

// 每次绘制都将 vertices 传输一次
GLfloat vertices[] = {
    0.0f, 0.0f,
    1.0f, 0.0f, 
    0.0f, 1.0f
}
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(2,GL_FLOAT,0,vertices);
glDrawArray(GL_TRIANGLES, 0, 3); 

VBO

VBO是结合DL和VA的特点,既方便传输,又要兼顾修改。

由于VA在CPU收集的顶点是一个整体,所以在GPU向渲染流水线提交数据是由一个整体提交的,无法在渲染时做修改;而DL虽然可以单个修改,但是渲染时却需要等待CPU端修改完成等GPU端收集完成再进行。

为了既提高传输效率,又可以使渲染时数据在GPU端也可以修改,VBO应运而生。

OpenGL日常-三角形(下)——VBO/VAO/EBO_第4张图片

这样一来VBO保存了一份顶点数据,修改操作可以直接在GPU上进行,修改完成直接绘制。

所以按照这样理解,它的传输与修改是分开的,体现在代码上:

//生成VBO,并传输保存到GPU上
GLuint vbo;
glGenBuffer(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STREAM_DRAW);
...

// 绘制时直接从VBO中取得顶点数据
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2, (void*)0);
glDrawArray(GL_TRIANGLES, 0, 3);

...

但是这里只用VBO进行渲染没有可以参考的代码,本人也是进行了实验但是最终没有画出来,希望有厉害的小伙伴可以一起来交流一下这个问题,这里就留作一个探索。

VAO

本文最终采用的是VAO结合VBO画出的三角形,上篇我们讨论过了,结果没有出来,今天我们把坑填上。

Vertex Array Object,顶点数据对象是为了简化VBO的流程,当所要传输的VBO有很多的时候,我们需要管理多个VBO,这样对每个VBO都进行记录会比较乱,我们首先想到的管理方式就是用一个数组将它们保存起来,没错,这就是VAO,很直观。

OpenGL日常-三角形(下)——VBO/VAO/EBO_第5张图片

如图所示,VAO保存了不同VBO的指针,用户可以通过这些指针来对数据进行操作。

本文中,我们先分别生成VAO和VBO,将VBO绑定到VAO中,将VAO绑定到缓存里

//VAO
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);

//VBO
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//设置对缓冲区访问的步长为3以及相位为0,告诉着色器,这个数据输入到着色器的第一个(索引为0)输入变量,数据的长度是3个float
GLuint uPos = glGetAttribLocation( shaderProgram, "aPos" );
glVertexAttribPointer(uPos, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(uPos);

//delete buffer and array
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);

这里,glGetAttribLocation( shaderProgram, "aPos" );一句中,shaderProgram就是我们上节中编译好的OpenGL程序,"aPos"是顶点着色器代码中的顶点,还记得吗?

OpenGL日常-三角形(下)——VBO/VAO/EBO_第6张图片

这里的location你可以修改下它的值,看看结果uPos的值,会有助于理解它的意义,当然着色器关键字这里先不进行讨论。

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

重点讨论一下这个函数:

第一个参数:对应着色器代码中的location;
第二个参数:顶点属性的大小,在这里是vec3,所以是3;
第三个参数:数据类型,没什么好说的;
第四个参数:定义是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间;
第五个参数:第五个参数叫做步长(Stride),还是用图来解释一下吧,比较直观;
第六个参数:表示位置数据在缓冲中起始位置的偏移量(Offset),当有多个VBO里,可以通过偏移量进行位置锁定;

上面的第五个参数中的步长为3,即在VBO里每一个顶点占12个位置,每个位置所占字节由其保存数据类型决定。

OpenGL日常-三角形(下)——VBO/VAO/EBO_第7张图片

函数glVertexAttribPointer给出了如何从VBO中取得顶点数据的方式,所谓的OpenGL位置学(我又开始胡说八道了)。

接下来,快结束战斗了,画三角形吧。

glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays( GL_TRIANGLES, 0, 3 );

那么我们就愉快的结束了。等下!!!

OpenGL日常-三角形(下)——VBO/VAO/EBO_第8张图片

EBO

对于VAO的这种方式,我们有点小想法,现有一个问题,如果我们要画两个三角形,你觉得最少可以用几个顶点呢?答案肯定是4个。但是如果用前面的方法,恐怕我们需要至少6个来完成,那么EBO,索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO)的出现就是为了重复利用顶点。

个人觉得索引这个概念特别好,将顶点用索引作标记,当使用顶点时用索引间接访问,如图:

OpenGL日常-三角形(下)——VBO/VAO/EBO_第9张图片

我们来定义一下数据和索引;

float vertices2[] = {
    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开始! 
    0, 1, 3, // 第一个三角形
    1, 2, 3  // 第二个三角形
};

建立索引缓冲对象:

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

接下来同VBO类似,将索引复制到缓冲里,

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

最终进行绘制:

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

数据调用的过程是,绘制时先从EBO从找到索引,再通过VAO中找到对应的VBO中的顶点,glDrawElements的参数:

第一个:与glDrawArrays一样,设置显示模式;
第二个:总共要绘制的顶点的个数;
第三个:索引的类型;
第四个:指定EBO中的偏移量,类似于VBO中的偏移量;

好了,就到这吧,EBO的部分不多做解释了;

OpenGL日常-三角形(下)——VBO/VAO/EBO_第10张图片

总结

BE -> DL -> VA -> VBO -> VAO -> EBO;

如果你最终懂得了这个链条的来源,那么恭喜你已经理解了。

本文参考:

  • OpenGL Vertex Buffer Object (VBO)
  • VAO 与 VBO 的前世今生
  • 你好,三角形
  • OpenGL简介-基于VBO绘制三角形

最后这篇复现没有成功,希望有复现的朋友可以给出点提示。

你可能感兴趣的:(OpenGL)