目录
一、缓冲区对象概念
缓冲区对象(Buffer Object)是OpenGL中用于在GPU内存中存储数据的一种机制,它们使得数据传输和渲染更加高效。
二、分类
常用的缓冲区分类如下:
VAO(Vertex Array Object): 顶点数组对象,用于存储顶点数据
VBO(Vertex Buffer Object): 顶点缓冲对象,用于存储顶点数据
EBO(Element Buffer Object): 元素缓冲对象,用于存储顶点的索引数据
IBO(Index Buffer Object): 索引缓冲对象,用于存储索引数据
FBO(Index Buffer Object):帧缓冲对象,用于渲染到纹理或离屏渲染。
PBO(Pixel Buffer Object):像素缓冲区对象
GL_ARRAY_BUFFER
标志指定的数组缓冲区对象用于创建保存顶点数据的缓冲区对象。GL_ELEMENT_ARRAY_BUFFER
标志指定的元素数组缓冲区对象用于创建保存图元索引的缓冲区对象。
应用程序对顶点属性数据和元素索引使用顶点缓冲区对象。
三、顶点缓冲区对象VBO
1、概念
VBO(Vertex Buffer Object)顶点缓冲对象,是在显卡存储空间(即,GPU内存)中开辟的一块区域,在显卡存储空间中开辟一块区域,用于存放顶点的各类属性信息。如顶点坐标、纹理坐标、顶点颜色等数据。
在渲染时直接从显VBO去取数据而不必与CPU进行数据交换。
2、为什么使用VBO
什么是顶点缓冲区对象,为什么要使用它们
解释1:
将对象数据存储在客户端内存中,只有在渲染时将其传输到GPU内存(即,显存)中。没有大量数据传输时,这很好,但随着我们的场景越来越复杂,会有更多的物体和三角形,这会给CPU和内存增加额外的成本。
我们能做些什么呢?我们可以使用顶点缓冲对象,而不是每帧从客户端内存传输顶点信息,信息将被传输一次,然后渲染器将从该图形存储器缓存中得到数据。
解释2:
直接只用顶点数组来绘图,在客户端制定顶点数据,每次绘制时从先从内存中加载这些数据,这样会带来绘制延时,因此我们想着在显存中开辟一块区域,在每次绘制间隔就将顶点数据加载过来,这样绘制时直接读取显存中的数据就可以了,明显提高渲染速度。于是,我们需要引入顶点缓冲区。
一般在两种不同处理速度的物理组件之间进行数据传输时都要用到缓冲区,OpenGL由于需要处理内存与GPU的数据传输,也要用到一系列缓冲区对象,顶点缓冲区只是其中之一。在顶点数组的基础上使用定点缓冲区可以提高渲染速率。
解释3:
VBO允许开发者将大量顶点数据从CPU传输到GPU,减少每帧的CPU-GPU通信量,提高渲染效率。缓冲区对象通过在 GPU 上存储数据,提高了数据传输和渲染的效率。
使用顶点数组指定的顶点数据保存在客户内存中。在进行 glDrawArrays或者
glLDrawElements 等绘图调用时,这些数据必须从客户内存复制到图形内存。但是,如果我们没有必要在每次绘图调用时都复制顶点数据,而是在图形内存中缓存这些数据,那就好得多了。这种方法可以显著地改进渲染性能,也会降低内存带宽和电力消耗需求,对于手持设备相当重要。这是顶点缓冲区对象发挥作用的地方。
顶点组冲区对象使应用程序可以在高性能的图形内存中分配和缓存顶点数据,并从这个内存进行泻染,从而避免在每次绘制图元的时候重新发送数据。不仅是项点数据,描述图元顶点索引、作为 glDrawElements 参数传递的元素索引也可以缓存。
解释4:
通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。
从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。
解释5:
为什么需要使用VBO
将顶点数据保存在内存中,在调用glDrawArrays或者glDrawElements等绘制方法前需要调用相应的方法将数据送入显存,I/O开销大,性能不够好。
若采用顶点缓冲区对象存放顶点数据,则不需要在每次绘制前都将顶点数据复制进显存,而是在初始化顶点缓冲区对象时一次性将顶点数据送入显存,
每次绘制时直接使用显存中的数据,可以大大提高渲染性能。
3、如何使用VBO
顶点缓冲区的使用需要经历一下步骤:
生成缓冲区对象
glGenBuffers(GLsizei n, GLuint* buffers);
分配n个缓冲区对象标识符,返回的标识符存储在buffers数组中。每一个标识符表示一个已经开始使用的缓冲区对象。具体如何分配有OpenGL内部决定,与调用者无关。此外,该方法调用之后在buffers中存储的标识符并不一定是连续的数字,而且0作为一个被保留的缓冲区对象名称,该方法从来不会返回0标识符。
绑定缓冲区对象
glBindBuffer(GLenum target, GLuint buffer);
绑定缓冲区对象。OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。buffer是glGenBuffers()返回的标识符数组的一个值,target表示改缓冲区应该绑定为什么类型的缓冲区对象,有GL_ARRAY_BUFFER(顶点数组缓冲区)、GL_ELEMENT_ARRAY_BUFFER(索引数组缓冲区)等。需要注意的是(以GL_ARRAY_BUFFER为例):
- 如果buffer为0,则停用顶点数组缓冲区;
- 如果buffer为非零整数,且buffer代表的缓冲区之前未绑定过,则创建一个新的缓冲区对象和buffer相对应;
- 如果buffer之前绑定过,则激活buffer对应的缓冲区。
OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上。
输入缓冲区数据
glBufferData(GLenum target, GLsizeiptr size, const void* data, GLenum usage);
初始化缓冲区对象,并指定缓冲区数据。target和glBindBuffer()中的target一样,data表示要写入到缓冲区的数据数组头指针,size表示data数组的字节数,usage表示数据在分配到缓冲区之后如何进行读取和写入,主要用来提供性能,指定了我们希望显卡如何管理给定的数据。其值为“GL_频率_操作”格式,有
GL_STREAM_DRAW、
GL_STREAM_READ、
GL_STREAM_COPY、
GL_STATIC_*、
GL_DYNAMIC_*。
其中,“频率”指缓冲区数据的读取或者渲染速率,有流模式、静态模式,动态模式:
- STREAM:流模式,当缓冲区中的数据更新频率高,但使用频率低时使用;
- STATIC:静态模式,当缓冲区中的数据只需制定一次,但使用频率比较高时使用;
- DYNAMIC:动态模式,当缓冲区中的数据更新频率高,并且使用频率高时使用。
“操作”有绘制 、读取和拷贝:
- DRAW:绘制,缓冲区中的数据直接用于渲染,也就是内存中的数据直接作为渲染数据;
- READ:读取,缓冲区中的数据主要用于应用程序的计算,而不是直接作用于渲染;
- COPY:拷贝,缓冲区中的数据对渲染来说是只读数据,需要拷贝后将拷贝数据作为渲染数据。
例如,如下三种形式的含义:
- GL_STATIC_DRAW 数据不会或几乎不会改变。仅在显存上进行读取,不进行写入,适用于静态数据。
- GL_DYNAMIC_DRAW数据会被改变很多。仅在显存上进行读取,并允许写入,数据经常改变。
- GL_STREAM_DRAW 数据每次绘制时都会改变。仅在显存上进行读取,不进行写入,数据每次绘制都改变。
一般情况下,位置数据不会改变,每次渲染调用时都保持原样,所以它的使用类型最好是GL_STATIC_DRAW。如果,比如说一个缓冲中的数据将频繁被改变,那么使用的类型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,这样就能确保显卡把数据放在能够高速写入的内存部分。现在我们已经把顶点数据储存在显卡的内存中,用VBO这个顶点缓冲对象管理。
更新缓冲区中的数据
glBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const void* data);
更新缓冲区中的数据。target和之前两个方法的参数一样,data指写入缓冲区的数据所在数组头指针,size指data数组中要写入缓冲区的数据的字节数,offset指要写入缓冲区的第一个数据在data数组的位置。也即将data数组中从offset(字节为单位)开始的size个字节写入缓冲区。
删除缓冲区
glDeleteBuffers(GLsizei n, const GLuint* buffers);
删除buffers中前n个缓冲区对象,他们的名称就是buffers数组中的元素。
4、VBO应用
创建VBO四步:
val buffers = IntArray(1)
//1.生成一个VBO,并ID存储在VBO中,此时VBO拥有了其唯一的id
GLES20.glGenBuffers(buffers.size, buffers, 0)
if (buffers[0] == 0) {
Log.d(TAG, "can not create a new vertex buffer object.")
}
bufferId = buffers[0]
//2.绑定缓冲区
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffers[0])
val vertexArray = ByteBuffer
.allocateDirect(indexData.size * BYTES_PER_SHORT)
.order(ByteOrder.nativeOrder())
.asShortBuffer()
.put(indexData)
vertexArray.position(0)
//3.将顶点数据复制到缓冲中,并指定数据用以静态访问,
将顶点数据上传到GPU内存中,以便后续的渲染操作。
GLES20.glBufferData(
GLES20.GL_ARRAY_BUFFER,
vertexArray.capacity() * BYTES_PER_SHORT,
vertexArray,
GLES20.GL_STATIC_DRAW
)
//4.将buffer id设置为0,解绑缓冲区
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0)
四、顶点数组对象VAO
1、概念
VAO(vertex Array Object):顶点数组对象。
注意: VAO是OpenGL ES 3.0之后才推出的新特性, 所以在使用VAO前需要确定OpenGL ES的版本是否是3.0之后的版本。
顶点数组对象可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。
VAO用于保存顶点属性数组的状态,包括与VBO的绑定以及顶点属性的配置。
2、为什么使用VAO
解释1:
顶点属性存储有2种方式:客户端顶点数组和顶点缓冲区对象。VAO使得客户端顶点数组的数据传入VBO的速度加快。
通过VAO,开发者可以一次性配置好顶点数组的所有状态,在后续的绘制中快速恢复这些状态,减少OpenGL函数调用次数,提升效率。
解释2:
在上面VBO的介绍中我们知道每次在绘制的时候都需要频繁地绑定与解绑VBO,每次绘制还需要取出VBO中的数据进行赋值之后才能进行绘制渲染。当数据量大的时候,重复这些操作就会变得很繁琐。通过VAO就可以简化这个过程,因此VAO可以简单理解成VBO的管理者,避免在帧绘制时再去手动操纵VBO,VAO不能单独使用,
需要搭配VBO使用。
对于GPU来说VBO就是一堆数据,但是这堆数据怎么解析使用,需要glEnableVertexAttribArray
等相关函数在每次绘制的时候告诉GPU,那么VAO的作用就是简化这个过程的,只需要在初始化的时候将这些解析逻辑与VAO绑定一次即可,
然后每次在绘制的时候只需绑定对应的VAO,而不必每次再绑定VBO,然后去告诉GPU如何解析相关数据了,可以说是一个性能的优化了。
解释3:
当我们再绘制不同的顶点数据或者是其他数据时,我们需要重新进行一遍绑定设置等等操作来完成绘制不同,这时候就引出顶点数组对象。
在OpenGL中,顶点数组对象(Vertex Array Object,VAO)是一种OpenGL对象,用于存储顶点数据的状态信息,包括顶点坐标、法线、纹理坐标等。VAO可以看作是一种包含了多个顶点属性配置的容器,使得你可以在绘制时轻松地切换和使用不同的顶点数据。
顶点数组对象的使用可以帮助你更有效地组织和管理顶点数据,特别是在绘制多个物体或者在渲染循环中多次切换不同的顶点数据时。
其实可能还不太明白,其实使用的时候确实都是要走绑定顶点缓冲区,然后设置顶点的布局等等,但是我们现在把上面一套流程放进了一个叫VAO的对象中,也就是生成绑定它就相当于走完了内部的一套流程,在换顶点数据或者顶点布局的时候,就不需要再写一遍这么麻烦了,相当于是封装了一层,我们后续可以看看其实也是真的要封装一层的,我们可以通过在换顶点布局或者顶点数据的时候体现流程:
老一套的流程是:
1.解绑并重新绑定新的着色器程序 | glUseProgram(0); glUseProgram(shaderID); |
2.解绑和重新绑定顶点缓冲区 | glBindBuffer(GL_ARRAY_BUFFER, 0); glBindBuffer(GL_ARRAY_BUFFER, buffer)); |
3.重新设置顶点的布局 | glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0); |
4.绑定我们的索引缓冲区 | glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo)); |
5.绘制调用 | glDrawElements |
引入VAO之后,我们要更改数据就变成了这样:
1.解绑并重新绑定新的着色器程序 | glUseProgram(0); glUseProgram(shaderID); |
2.绑定顶点数组(包含了绑定顶点缓冲区,设置顶点的布局,方便直接绑定别的,可以切换) | glBindVertexArray(vao); |
3.绑定索引缓冲 | glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo); |
4.绘制调用 | glDrawElements |
另外这个顶点数组对象对于OpenGL是强制性的,因为即使我们没有创建它,还是走的老一套流程,其实状态还是由顶点数组对象来维护的。
3、如何使用VAO
生成VAO
unsigned int VAO;
glGenVertexArrays(1, &VAO);
绑定VAO
glBindVertexArray(VAO)
解绑VAO
glBindVertexArray(0)
删除VAO
glDeleteVertexArrays
4、VAO应用
// 生成VAO和VBO
GLuint vao;
glGenVertexArrays(1, &vao);
GLuint vbo;
glGenBuffers(1, &vbo);
// 绑定VAO
glBindVertexArray(vao);
// 绑定VBO并设置顶点数据
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 配置顶点属性,用于将当前的顶点属性与顶点缓冲对象(VBO)关联起来
//配置顶点属性指针: 使用 glVertexAttribPointer 函数配置顶点属性指针,告诉OpenGL如何解释顶点数据。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 解绑VAO和VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
五、索引缓冲区对象EBO/IBO
1、概念
IBO/EBO, Element Buffer Objec,索引缓冲区对象。EBO用于存储顶点的索引数据,允许通过索引引用顶点,避免重复存储相同的顶点数据。
索引缓冲区和顶点缓冲区类似