实践项目:https://github.com/JackieLong/OpenGL/tree/main/project_VBO_EBO_VAO
缓冲对象(Buffer Object)在OpenGL中大量地应用,它是一个管理一块内存(显存)的对象,把它绑定到一个缓冲目标上(Buffer Target)才赋予其意义,比如绑定到GL_ARRAY_BUFFER,它就是一个顶点缓冲对象,绑定到GL_ELEMENT_ARRAY_BUFFER时,它就是一个索引缓冲对象,OpenGL会根据目标的不同,以不同的方式处理缓冲。
使用缓冲对象的好处是,可以一次发送大量的(顶点)数据到显卡中并常驻显存如果内存足够,而不是一次发送一个(顶点)数据,显卡从显存中访问数据的速度非常快(instant access),而GPU和CPU之间的总线传输相对来说就是龟速。
一、VBO
Vertex Buffer Object,顶点缓冲对象。管理的内存中存储大量的顶点数据,作为顶点着色器的输入数据。
GLuint VBO; // VBO对象的句柄
glGenBuffers(1, &VBO); // 创建一个VBO对象
// glGenBuffers(10, &VBO); // 创建10个VBO对象,此时VBO必须是一个数组
glBindBuffer(GL_ARRAY_BUFFER, VBO); // VBO将绑定为顶点缓冲对象,OpenGL规定每个类型的缓冲对象只能绑定一个
glBindBuffer(GL_ARRAY_BUFFER, VBO1); // 将覆盖上次的绑定,VBO1将作为顶点缓冲对象
float vertices[] = {
// 顶点坐标
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
// 1、给顶点缓冲对象分配数据存储区(data storage),在显存中。
// 2、将CPU内存中的数据批量拷贝至存储区。
glBufferData(GL_ARRAY_BUFFER, // 拷贝到哪个缓冲上
sizeof(vertices), // 数据字节数
vertices, // 数据指针
GL_STATIC_DRAW); // 希望显卡如何管理这些数据:
// GL_STATIC_DRAW :数据不会或几乎不改变。
// GL_DYNAMIC_DRAW:数据会被改变很多。
// GL_STREAM_DRAW :数据每次绘制时都会改变。
// 根据提供的参数,显卡自然知道如何管理这些数据更好。
glGenBuffers
void glGenBuffers(GLsizei n, GLuint *buffers);
// 返回n个当前未使用的缓存对象名称,并保存到buffers数组中。
// 1、返回的并不一定是连续的整形数据
// 2、返回的名称只用于分配其他缓存对象,它们在绑定之后只会记录一个可用的状态。
// 3、0 是一个保留的缓存对象名称,glGenBuffers() 永远都不会返回这个值的缓存对象。
glBindBuffer
void glBindBuffer(GLenum target,
GLuint buffer);
// 绑定buffer到指定目标对象类型target上。
// target: 绑定到哪个目标上,可能的值如下:
// GL_ARRAY_BUFFER 顶点缓冲对象
// GL_ELEMENT_ARRAY_BUFFER 索引缓冲对象
// GL_PIXEL_PACK_BUFFER
// GL_PIXEL_UNPACK_BUFFER
// GL_COPY_READ_BUFFER
// GL_COPY_WRITE_BUFFER
// GL_TRANSFORM_FEEDBACK_BUFFER
// GL_UNIFORM_BUFFER
// buffer:要绑定的缓存对象name
// 1、如果第一次绑定buffer并且buffer!=0,将创建一个与该名称相对应的新缓存对象。
// 2、如果绑定到已经创建的缓存对象,它将成为当前被激活的缓存对象,之前的绑定将被覆盖
// 3、如果buffer=0,OpenGL将不再对当前target应用任何缓存对象。
如果buffer不存在,则会创建一个name为buffer的缓冲对象。
先前的调用的glBindBuffer将被覆盖(失效)。
buffer=0值是保留值,但并不是所有的target都有默认Buffer Object,但是调用glBindBuffer(…, 0)可以用有效unbind先前的绑定。
buffer在第一次glBindBuffer之后的状态立即变成一个未映射的0大小内存的Buffer Object,访问权限GL_READ_ACCESS和GL_STATIC_DRAW的usage。
glBindBuffer binds a buffer object to the specified buffer binding point. Calling glBindBuffer with target set to one of the accepted symbolic constants and buffer set to the name of a buffer object binds that buffer object name to the target. If no buffer object with name buffer exists, one is created with that name. When a buffer object is bound to a target, the previous binding for that target is automatically broken.
Buffer object names are unsigned integers. The value zero is reserved, but there is no default buffer object for each buffer object target. Instead, buffer set to zero effectively unbinds any buffer object previously bound, and restores client memory usage for that buffer object target (if supported for that target). Buffer object names and the corresponding buffer object contents are local to the shared object space of the current GL rendering context; two rendering contexts share buffer object names only if they explicitly enable sharing between contexts through the appropriate GL windows interfaces functions.
glGenBuffers must be used to generate a set of unused buffer object names.
The state of a buffer object immediately after it is first bound is an unmapped zero-sized memory buffer with GL_READ_WRITE access and GL_STATIC_DRAW usage.
While a non-zero buffer object name is bound, GL operations on the target to which it is bound affect the bound buffer object, and queries of the target to which it is bound return state from the bound buffer object. While buffer object name zero is bound, as in the initial state, attempts to modify or query state on the target to which it is bound generates an GL_INVALID_OPERATION error.
When a non-zero buffer object is bound to the GL_ARRAY_BUFFER target, the vertex array pointer parameter is interpreted as an offset within the buffer object measured in basic machine units.
While a non-zero buffer object is bound to the GL_ELEMENT_ARRAY_BUFFER target, the indices parameter of
- glDrawElements,
- glDrawElementsInstanced,
- glDrawElementsBaseVertex,
- glDrawRangeElements,
- glDrawRangeElementsBaseVertex,
- glMultiDrawElements
- glMultiDrawElementsBaseVertex
is interpreted as an offset within the buffer object measured in basic machine units.
While a non-zero buffer object is bound to the GL_PIXEL_PACK_BUFFER target, the following commands are affected:
- glGetCompressedTexImage
- glGetTexImage
- glReadPixels.
The pointer parameter is interpreted as an offset within the buffer object measured in basic machine units.
While a non-zero buffer object is bound to the GL_PIXEL_UNPACK_BUFFER target, the following commands are affected:
- glCompressedTexImage1D,
- glCompressedTexImage2D,
- glCompressedTexImage3D,
- glCompressedTexSubImage1D,
- glCompressedTexSubImage2D,
- glCompressedTexSubImage3D,
- glTexImage1D
- glTexImage2D
- glTexImage3D
- glTexSubImage1D
- glTexSubImage2D
- glTexSubImage3D.
The pointer parameter is interpreted as an offset within the buffer object measured in basic machine units.
The buffer targets GL_COPY_READ_BUFFER and GL_COPY_WRITE_BUFFER are provided to allow glCopyBufferSubData to be used without disturbing the state of other bindings. However, glCopyBufferSubData may be used with any pair of buffer binding points.
The GL_TRANSFORM_FEEDBACK_BUFFER buffer binding point may be passed to glBindBuffer, but will not directly affect transform feedback state. Instead, the indexed GL_TRANSFORM_FEEDBACK_BUFFER bindings must be used through a call to glBindBufferBase or glBindBufferRange. This will affect the generic GL_TRANSFORM_FEEDBACK_BUFFER binding.
Likewise, the GL_UNIFORM_BUFFER buffer binding point may be used, but does not directly affect uniform buffer state. glBindBufferBase or glBindBufferRange must be used to bind a buffer to an indexed uniform buffer binding point.
A buffer object binding created with glBindBuffer remains active until a different buffer object name is bound to the same target, or until the bound buffer object is deleted with glDeleteBuffers.
Once created, a named buffer object may be re-bound to any target as often as needed. However, the GL implementation may make choices about how to optimize the storage of a buffer object based on its initial binding target.
glBufferData
void glBufferData(GLenum target,
GLsizeiptr size,
const GLvoid *data,
GLenum usage);
// 创建并用data初始化绑定到target的Buffer Object的内存。
// target: 绑定的目标缓冲对象,
// GL_ARRAY_BUFFER 对于顶点属性数据。
// GL_ELEMENT_ARRAY_BUFFER 索引数据
// GL_PIXEL_UNPACK_BUFFER OpenGL 的像素数据
// GL_PIXEL_PACK_BUFFER 对于从OpenGL中获取的像素数据
// GL_COPY_READ_BUFFER 对于缓存之间的复制数据
// GL_COPY_WRITE_BUFFER 对于缓存之间的复制数据
// GL_TEXTURE_BUFFER 对于纹理缓存中存储的纹理数据
// GL_TRANSFORM_FEEDBACK_BUFFER 对于通过transform feedback着色器获得的结果
// GL_UNIFORM_BUFFER 一致变量
// size: 开辟的内存字节数。
// data: 用于初始化的数据,如果是NULL,则表示只创建没有初始化数据。
// usage: 期望这块内存的usage。可能值:
// GL_STREAM_DRAW
// GL_STREAM_READ
// GL_STREAM_COPY
// GL_STATIC_DRAW
// GL_STATIC_READ
// GL_STATIC_COPY
// GL_DYNAMIC_DRAW
// GL_DYNAMIC_READ
// GL_DYNAMIC_COPY
// 这个参数的意义在于告诉OpenGL这块dataStore将被如何使用,这样OpenGL可以针对性做性能优化。
// 被如何使用主要体现在两个方面:
// 1、访问频率:
// STREAM: modified once and used at most a few times.
// STATIC: modified once and used many times.
// DYNAMIC: modified repeatedly and used many times.
// 2、访问性质:
// DRAW: modified by the application, and used as the source for GL drawing and image specification commands.
// READ: modified by reading data from the GL, and used to return that data when queried by the application.
// COPY: modified by reading data from the GL, and used as the source for GL drawing and image specification commands.
必须做好内存对齐。
Clients must align data elements consistent with the requirements of the client platform, with an additional base-level requirement that an offset within a buffer to a datum comprising N bytes be a multiple of N.
GL_INVALID_ENUM is generated if target is not one of the accepted buffer targets.
GL_INVALID_ENUM is generated if usage is not 上面指定的合法值。
GL_INVALID_VALUE is generated if size is negative.
GL_INVALID_OPERATION is generated if the reserved buffer object name 0 is bound to target.
GL_OUT_OF_MEMORY is generated if the GL is unable to create a data store with the specified size.
glDeleteBuffers
void glDeleteBuffers(GLsizei n, const GLuint *buffers);
// 删除n个保存在buffers数组中的缓存对象。
// 1、被释放的缓存对象可以重用(通过glGenBuffers)。
// 2、如果删除的缓存对象已经被绑定,那么该对象的所有绑定将会重置为默认的缓存对象(相当于glBindBuffer(0))
// 3、缓存对象不存在,或者缓存对象=0,将忽略该操作(不会产生错误)。
glIsBuffer
GLboolean glIsBuffer(GLuint buffer);
// 返回GL_TRUE:如果buffer是一个已经分配并且没有释放的缓存对象的名称,则返回GL_TRUE。
// 返回GL_FALSE:如果buffer 为0 或者不是缓存对象的名称,
定义顶点属性
顶点着色器支持输入任意顶点数据(自定义顶点属性vertex attribute),这样顶点着色器就可以干很多事情了,既然是自定义的数据,那就需要告知OpenGL如何去“解释”VBO中的数据,使VBO中的每个数据对应到顶点着色器中的输入的顶点属性。
我们的顶点数据是这个样子:
float vertices[] = {
-0.5f, -0.5f, 0.0f, // 第一个顶点
0.5f, -0.5f, 0.0f, // 第二个顶点
0.0f, 0.5f, 0.0f, // 第三个顶点
};
对应的数据结构就是这个样子:
我们通过glVertexAttribPointer函数来完成:
// 向OpenGL“解释”,当前绑定的VBO中的数据结构。
glVertexAttribPointer(0, // 对应顶点着色器中的哪个顶点属性,这里对应的是layout(location = 0)属性。
3, // 属性有多少个分量,比如一个c struct有多少个成员变量。
GL_FLOAT, // 分量的数据类型
GL_FALSE, // 是否希望数据被标准化(Normalize)。
// GL_TRUE: 有符号数据,映射到[-1, 1]
// 无符号数据,映射到[0, 1]
// GL_FALSE: 啥都不干
3 * sizeof(GLfloat), // 步长Stride,两个属性之间的间隔字节数
// 若是紧闭排列(tightly packed,属性间没有空隙),设为0,OpenGL会自动计算好。
(GLvoid*)0); // 第一个属性第一个分量的偏移量。
// 开启顶点属性。
glEnableVertexAttribArray(0); // 0,对应上面的第一个参数,即着色器中顶点属性位置值
glVertexAttribPointer
void glVertexAttribPointer(GLuint index,
GLint size,
GLenum type,
GLboolean normalized,
GLsizei stride,
const GLvoid * pointer);
void glVertexAttribIPointer(GLuint index,
GLint size,
GLenum type,
GLsizei stride,
const GLvoid * pointer);
void glVertexAttribLPointer(GLuint index,
GLint size,
GLenum type,
GLsizei stride,
const GLvoid * pointer);
// 定义一个通用顶点属性数组。
// index: 属性索引值,对应顶点着色器代码中的:
// layout (location = 0) in vec2 pos;中的location的值
// size: 每个顶点属性的分量数目,可以是1、2、3、4、GL_BGRA。默认4
// type: 分量的数据类型,默认GL_FLOAT,每个版本函数的type取值有不同:
//
// glVertexAttribPointer版本;
// GL_BYTE
// GL_UNSIGNED_BYTE
// GL_SHORT
// GL_UNSIGNED_SHORT
// GL_INT
// GL_UNSIGNED_INT
// GL_HALF_FLOAT,
// GL_FLOAT,
// GL_DOUBLE,
// GL_FIXED,
// GL_INT_2_10_10_10_REV,
// GL_UNSIGNED_INT_2_10_10_10_REV
// GL_UNSIGNED_INT_10F_11F_11F_REV
//
// glVertexAttribIPointer版本;
// GL_BYTE
// GL_UNSIGNED_BYTE
// GL_SHORT
// GL_UNSIGNED_SHORT
// GL_INT
// GL_UNSIGNED_INT
//
// glVertexAttribLPointer版本:这是双精度版本
// GL_DOUBLE
// normalized: 设置顶点数据在存储前是否需要进行归一化(或者使用glVertexAttribFourN*() 函数)。
// GL_TRUE: signed integer将被映射到到[-1.0,1.0]之间的浮点型。
// unsigned integer将被映射到[0.0, 1.0]之间的浮点型。
// GL_FALSE: 直接转换成分浮点型。
// stride: 数组中每两个元素之间的大小偏移值(byte)。如果stride为0,则数据应该紧密地封装在一起。
// 0:数据是“紧密封装”的,每组数据值在内存中是相邻的。
// 初始值为0。
// pointer: 表示当前绑定VBO的data store的字节偏移值,初始值为0。
// BUFFER_OFFSET(0):数据是从缓存对象的第一个字节开始的。
// BUFFER_OFFSET是宏,#define BUFFER_OFFSET(offset) ((void *)(offset))
// 常见代码((GLvoid*)(2 * sizeof(GLfloat)))
这个函数只是定义通用顶点属性数组,默认是没有启用的,必须调用glEnableVertexAttribArray(index)启用。
调用glEnableVertexAttribArray将启用顶点属性数组,当调用下面绘制指令时,顶点属性数组将被使用:
- glDrawArrays
- glMultiDrawArrays
- glDrawElements,
- glMultiDrawElements
- glDrawRangeElements
GL_INVALID_VALUE is generated if index is greater than or equal to GL_MAX_VERTEX_ATTRIBS.
GL_INVALID_VALUE is generated if size is not 1, 2, 3, 4 or (for glVertexAttribPointer), GL_BGRA.
GL_INVALID_ENUM is generated if type is not an accepted value.
GL_INVALID_VALUE is generated if stride is negative.
GL_INVALID_OPERATION is generated if size is GL_BGRA and type is not GL_UNSIGNED_BYTE, GL_INT_2_10_10_10_REV or GL_UNSIGNED_INT_2_10_10_10_REV.
GL_INVALID_OPERATION is generated if type is GL_INT_2_10_10_10_REV or GL_UNSIGNED_INT_2_10_10_10_REV and size is not 4 or GL_BGRA.
GL_INVALID_OPERATION is generated if type is GL_UNSIGNED_INT_10F_11F_11F_REV and size is not 3.
GL_INVALID_OPERATION is generated by glVertexAttribPointer if size is GL_BGRA and noramlized is GL_FALSE.
GL_INVALID_OPERATION is generated if zero is bound to the GL_ARRAY_BUFFER buffer object binding point and the pointer argument is not NULL.
glEnableVertexAttribArray
glDisableVertexAttribArray
void glEnableVertexAttribArray(GLuint index);
void glDisableVertexAttribArray(GLuint index);
// enable/disable顶点通用属性数组。默认都是disable。
// index: 顶点通用属性的索引值,对应顶点着色器代码中的:
// layout (location = 0) in vec2 pos;中的location的值
// 取值范围:[0, GL_MAX_VERTEX_ATTRIBS-1]
设置是否启用与index索引相关联的顶点数组。index必须是一个介于0到GL_MAX_VERTEX_ATTRIBS-1之间的值。
GL_INVALID_VALUE is generated if index is greater than or equal to GL_MAX_VERTEX_ATTRIBS.
二、VAO
Vertex Array Object,顶点数组对象。
绘制一个物体的最简单过程如下:
unsigned int VBO;
glGenBuffers(1, &VBO); // 创建VBO对象
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定到当前VBO对象
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), // 复制数据到当前VBO
vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, // OpenGL如何解析当前VBO数据
GL_FALSE, 3 * sizeof(float),
(void*)0);
glEnableVertexAttribArray(0); // 开启位置=0的节点属性配置,默认是关闭的,
//glEnableVertexAttribArray(1); // 如果顶点着色器有多个节点属性,切记每个都要开启。
glUseProgram(shaderProgram); // 启动一个shader program
// do some rendering
glDrawArrays( GL_TRIANGLES, // 绘制的图元类型
0, // 顶点数据其实索引
3 ); // 打算绘制多少个顶点
如果还有配置很多不同的顶点属性,然后又再这些属性间不断切换状态,将会变得极其麻烦,OpenGL提供了一个Object对象,可以保存这些设置,只要切换这单个对象就能切换到对应的状态。这个对象就是VAO,一个VAO对象可以存储如下设置状态:
- glEnableVertexAttribArray的调用
- glDisableVertexAttribArray的调用
- 通过glVertexAttribPointer设置的顶点属性配置。
- 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。
- 当目标是GL_ELEMENT_ARRAY_BUFFER的时候,会储存glBindBuffer的函数调用,下面讲。
VAO用法如下:
// 1. 绑定VAO
unsigned int VAO;
glGenVertexArrays(1, &VAO);
// 2. 绑定到当前VAO,接下来的设置都将保存到当前这个VAO中。
glBindVertexArray(VAO);
// 3. 把顶点数组复制到缓冲中供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. 等等还要一些其他状态设置
//......
// 5. 绘制物体
while(...){ // 渲染循环
glUseProgram(shaderProgram);
glBindVertexArray(VAO); // 恢复到这个VAO对应的状态
doSomeDraw(); // 在这里可以开始画三角形了。
// 比如:glDrawArrays( GL_TRIANGLES, 0, 3 );
glBindVertexArray(0); // 回复到默认状态
}
glGenVertexArrays
void glGenVertexArrays(GLsizei n, GLuint *arrays);
// 返回n个未使用的对象名到数组arrays中,用作顶点数组对象。注意不能保证是连续的数值。
// 返回的名字可以用来分配更多的缓存对象,并且它们已经使用未初始化的顶点数组集合的默认状态进行了数值的初始化。
GL_INVALID_VALUE is generated if n is negative.
glBindVertexArray
void glBindVertexArray(GLuint array);
// 1、array != 0
// 将创建一个新的顶点数组对象并且与其名称关联起来。
// 如果绑定到已创建的顶点数组对象,将激活这个顶点数组对象并直接影响对象中所保存的顶点数组状态。
// 2、array = 0,
// OpenGl将不再使用程序所分配的任何顶点数组对象,并且将渲染状态重设为顶点数组的默认状态。
GL_INVALID_OPERATION is generated if array is not zero or the name of a vertex array object previously returned from a call to glGenVertexArrays.
glDeleteVertexArrays
void glDeleteVertexArrays(GLsizei n, GLuint *arrays);
// 删除n个在arrays中定义的顶点数组对象,这样所有的名称可以再次用作顶点数组。
// 如果绑定的顶点数组已经被删除,那么当前绑定的顶点数组对象被重设为0(类似glBindBuffer(0) ),
// 而默认的顶点数组会变成当前对象。在arrays当中未使用的名称都会被释放,但是当前顶点数组的状态不会发生任何变化。
GL_INVALID_VALUE is generated if n is negative.
glIsVertexArray
GLboolean glIsVertexArray(GLuint array);
// 判断顶点数组对象名称array是不是有效的。
// 1、返回GL_TRUE:如果array是valid的:即是glGenVertexArrays创建的且没有被删除。
// 2、返回GL_FALSE:array=0或者array不是任何顶点数组对象的名称。
OpenGL的核心模式要求我们使用VAO,所以它知道该如何处理我们的顶点输入。如果我们绑定VAO失败,OpenGL会拒绝绘制任何东西。
三、EBO
Element Buffer Object,索引缓冲对象,也叫IBO,Index Buffer Object。
它的作用,请看这样一个场景,假如我们要用4个顶点绘制两个三角形来组成矩形,顶点数据如下:
float vertices[] = {
// 第一个三角形
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, 0.5f, 0.0f, // 左上角
// 第二个三角形
0.5f, -0.5f, 0.0f, // 右下角,重复了一次
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角,重复了一次
};
重复了两个顶点数据,数据规模一大,这会产生极大浪费,EBO的作用就是不直接操作顶点数据,而是操作顶点数据的索引,这样就不必担心重复顶点数据的额外开销了,如下:
float vertices[] = {
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, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
unsigned int EBO;
glGenBuffers(1, &EBO); // 创建一个Buffer,本质和VBO一样,只不过存储的是索引
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); // 绑定到当前EBO
// 写入顶点数据的索引。
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 注意,不是用glDrawArrays了。
// glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取索引。
glDrawElements(GL_TRIANGLES, // 绘制模式
6, // 打算绘制顶点数量
GL_UNSIGNED_INT, // 索引类型
0); // EBO中的偏移量
四、顶点属性布局
通过使用glVertexAttribPointer,我们能够指定顶点数组缓冲内容的属性布局。我们可以采用不同的顶点属性布局来应对不同的情况:
交错布局:123123
顶点属性数据是交错布局,顶点的不同属性数据(位置、纹理坐标、法向量)放置在一起,数据顶点数组属性数据的组织形式如下:
const GLfloat cubeVertices[] = // 箱子顶点数据
{
// 顶点坐标 // 纹理坐标 // 法向量
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 0.0f, -1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 1.0f, 1.0f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f, -1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 1.0f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, -1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f
};
我们可以编写如下方法函数来创建顶点数组缓冲:
void createVertexBuffer( const GLfloat *vertices, // 数据来源,交错布局方式,这里限制了都是float类型数组
const int &len, // 数据字节数
const std::string &components, // 数据属性的分量组成,如“332”表示有三个顶点属性,第一、二个属性有3个分量、第三个有2个分量。
GLuint *VAO, // 保存创建的VAO
GLuint *VBO, // 保存创建的VBO
const GLuint *indices /*= nullptr*/, // 不为空则表示要创建EBO
const int &lenIndices /*= 0*/, // EBO的字节数
GLuint *EBO /*= nullptr */ ) // 创建的EBO的保存地址
{
glGenVertexArrays( 1, VAO ); // 创建VAO
glBindVertexArray( *VAO );
glGenBuffers( 1, VBO ); // 创建VBO
glBindBuffer( GL_ARRAY_BUFFER, *VBO );
glBufferData( GL_ARRAY_BUFFER, len, vertices, GL_STATIC_DRAW ); // 填充数据
if( EBO )
{
glGenBuffers( 1, EBO ); // 创建EBO
glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, *EBO );
glBufferData( GL_ELEMENT_ARRAY_BUFFER, lenIndices, indices, GL_STATIC_DRAW ); // 填充数据
}
GLint componentNum; // 当前顶点属性的分量数量
GLsizei stride = 0; // 步长,一组属性的字节数
GLint tmpOffset = 0; // 数据在缓冲中起始位置的偏移量
for( size_t i = 0; i < components.size(); i++ )
{
stride += ( components.at( i ) - '0' ) * sizeof( GLfloat );
}
for( size_t i = 0; i < components.size(); i++ )
{
componentNum = components.at( i ) - '0';
glEnableVertexAttribArray( i );
glVertexAttribPointer( i, // vertex attribute的位置值,这个值对应在顶点着色器中的location=index的属性
componentNum, // 顶点属性的分量数量
GL_FLOAT, // 分量数据类型
GL_FALSE, // 数据是否需要被标准化
stride, // 步长Stride,属性间隔,注意是两个属性值相同位置的间隔,不是首尾间隔。
( void * )( tmpOffset ) ); // 数据在缓冲中起始位置的偏移量
tmpOffset += componentNum * sizeof( GLfloat );
}
glBindVertexArray( 0 );
}
使用方式如下:
GLuint VAO_cube;
GLuint VBO_cube;
createVertexBuffer( cubeVertices, sizeof( cubeVertices ), "323", &VAO_cube, &VBO_cube );
分类布局:112233
数据组织方式如下,顶点的坐标、纹理坐标、法向量分别集中保存:
const float posVertices[] = // 箱子顶点坐标
{
// positions
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, -0.5f,
};
const float texCoordsVertices[] = // 箱子顶点对应纹理坐标
{
0.0f, 0.0f,
1.0f, 0.0f,
1.0f, 1.0f,
1.0f, 1.0f,
0.0f, 1.0f,
0.0f, 0.0f,
0.0f, 0.0f,
1.0f, 0.0f,
1.0f, 1.0f,
1.0f, 1.0f,
0.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f,
1.0f, 1.0f,
0.0f, 1.0f,
0.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f,
1.0f, 0.0f,
1.0f, 1.0f,
0.0f, 1.0f,
0.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f,
0.0f, 1.0f,
1.0f, 1.0f,
1.0f, 0.0f,
1.0f, 0.0f,
0.0f, 0.0f,
0.0f, 1.0f,
0.0f, 1.0f,
1.0f, 1.0f,
1.0f, 0.0f,
1.0f, 0.0f,
0.0f, 0.0f,
0.0f, 1.0f,
};
const float normalVertices[] = // 箱子顶点所在的法向量
{
0.0f, 0.0f, -1.0f,
0.0f, 0.0f, -1.0f,
0.0f, 0.0f, -1.0f,
0.0f, 0.0f, -1.0f,
0.0f, 0.0f, -1.0f,
0.0f, 0.0f, -1.0f,
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 1.0f,
-1.0f, 0.0f, 0.0f,
-1.0f, 0.0f, 0.0f,
-1.0f, 0.0f, 0.0f,
-1.0f, 0.0f, 0.0f,
-1.0f, 0.0f, 0.0f,
-1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f,
0.0f, -1.0f, 0.0f,
0.0f, -1.0f, 0.0f,
0.0f, -1.0f, 0.0f,
0.0f, -1.0f, 0.0f,
0.0f, -1.0f, 0.0f,
0.0f, -1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 1.0f, 0.0f
};
我们可以按如下方法创建顶点数组缓冲:
const GLfloat pos[] = { ... };
const GLfloat normal[] = { ... };
const GLfloat tex[] = { ... };
GLuint VBO;
glGenBuffers( 1, &VBO );
glBindBuffer( GL_ARRAY_BUFFER, VBO );
glBufferData( GL_ARRAY_BUFFER, len, vertices, GL_STATIC_DRAW );
// 填充缓冲
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(positions), &positions);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions), sizeof(normals), &normals);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions) + sizeof(normals), sizeof(tex), &tex);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), 0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)(sizeof(positions)));
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)(sizeof(positions) + sizeof(normals)));
glEnableVertexAttribArray( 0 );
glEnableVertexAttribArray( 1 );
glEnableVertexAttribArray( 2 );
数据导入缓冲方法1:glBufferSubData
glBufferData函数用来创建并(可能)初始化缓冲对象所管理的内存。如果我们将它的data参数设置为NULL,那么这个函数将只会分配内存,但不进行填充。这在我们需要预留(Reserve)特定大小的内存,之后回到这个缓冲一点一点填充的时候会很有用。
除了使用一次函数调用填充整个缓冲之外,我们也可以使用glBufferSubData,填充缓冲的特定区域。
glBufferSubData
void glBufferSubData( GLenum target,
GLintptr offset,
GLsizeiptr size,
const GLvoid *data );
// target: 目标缓冲对象,只能是以下值之一
// GL_ARRAY_BUFFER
// GL_COPY_READ_BUFFER
// GL_COPY_WRITE_BUFFER
// GL_ELEMENT_ARRAY_BUFFER
// GL_PIXEL_PACK_BUFFER
// GL_PIXEL_UNPACK_BUFFER
// GL_TEXTURE_BUFFER
// GL_TRANSFORM_FEEDBACK_BUFFER
// GL_UNIFORM_BUFFER
// offset : 在缓冲对象内存中的偏移字节数,从这里开始填充数据
// size : 需要填充的字节数
// data : 数据指针
填充数据到目标缓冲的指定区域[offset, offset + size],数据来源是data。如果指定区域超出了目标缓冲对象内存区域将报错。
在替换整个数据存储时,请考虑使用glBufferSubData,而不是使用glBufferData完全重新创建数据存储。这避免了重新分配数据存储的成本。
考虑使用多个缓冲区对象,以避免在数据存储更新期间暂停渲染管道。如果管道中的任何渲染引用了由glBufferSubData更新的缓冲区对象中的数据,特别是来自要更新的特定区域的数据,则在更新数据存储之前,则必须先走完该渲染管道。
客户端必须将数据元素与客户端平台的要求保持一致,并附加一个基本级别的要求,即缓冲区内到包含N个字节的数据的偏移量是N的倍数。
GL_INVALID_ENUM is generated if target is not one of the accepted buffer targets.
GL_INVALID_VALUE is generated if offset or size is negative, or if together they define a region of memory that extends beyond the buffer object’s allocated data store.
GL_INVALID_OPERATION is generated if the reserved buffer object name 0 is bound to target.
GL_INVALID_OPERATION is generated if the buffer object being updated is mapped.
数据导入缓冲方法2:glMapBuffer
将数据导入缓冲的另外一种方法是,请求缓冲内存的指针,直接将数据复制到缓冲当中。通过调用glMapBuffer函数,OpenGL会返回当前绑定缓冲的内存指针,供我们操作:
...... // 按前面的方法创建了缓冲buffer
float data[] = {
0.5f, 1.0f, -0.35f
...
};
glBindBuffer(GL_ARRAY_BUFFER, buffer);
void *ptr = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY); // 获取指针
memcpy(ptr, data, sizeof(data)); // 复制数据到内存
if(glUnmapBuffer(GL_ARRAY_BUFFER) == GL_TRUE) // 记得告诉OpenGL我们不再需要这个指针了
{ // // ptr将不再可用。
std::cout << "unmapping sucessfully." << endl;
}
glMapBuffer
void * glMapBuffer(GLenum target,
GLenum access);
// target: 需要被映射(map)的目标缓冲对象,可能值如下:
// GL_ARRAY_BUFFER
// GL_COPY_READ_BUFFER
// GL_COPY_WRITE_BUFFER
// GL_ELEMENT_ARRAY_BUFFER
// GL_PIXEL_PACK_BUFFER
// GL_PIXEL_UNPACK_BUFFER
// GL_TEXTURE_BUFFER
// GL_TRANSFORM_FEEDBACK_BUFFER
// GL_UNIFORM_BUFFER
// access: 返回的指针的数据访问权限,可能值如下:
// GL_READ_ONLY
// GL_WRITE_ONLY
// GL_READ_WRITE
// 返回目标缓冲内存区的访问指针,如果发生错误,则返回NULL,glUnmapBuffer返回GL_FALSE
glMapBuffer将当前绑定到目标的buffer对象的整个数据存储映射到客户机的地址空间。然后,根据指定的访问策略,可以相对于返回的指针直接读取和/或写入数据。如果GL无法映射缓冲区对象的数据存储,则glMapBuffer将生成错误并返回NULL。这可能是由于系统特定的原因,例如虚拟内存不足。
如果以与access不一致的方式访问映射的数据存储,不会发生错误,但性能可能受影响,并可能导致系统错误,包括程序终止。与glBufferData的usage参数不同,access不是一个hint,事实上它限制了一些GL实现中的映射数据存储的使用。为了获得最高性能,缓冲区对象的数据存储应该以与其指定的用法和访问参数一致的方式使用。
映射的数据存储必须先用glUnmapBuffer解除映射,然后才能使用其缓冲区对象。否则,任何试图取消对缓冲区对象数据存储的引用的GL命令都将生成错误。当数据存储解除映射时,指向其数据存储的指针将变为无效。glUnmapBuffer返回GL_TRUE,r若在映射数据存储期间数据存储内容已损坏,这可能是由于影响图形内存可用性的系统特定原因造成的,例如屏幕模式更改。在这种情况下,返回GL_FALSE,并且数据存储内容未定义。应用程序必须检测到这种罕见的情况并重新初始化数据存储。
当删除缓冲区对象或使用glBufferData重新创建其数据存储时,缓冲区对象的映射数据存储将自动取消映射。
GL_INVALID_ENUM is generated if target 值不合法。
GL_INVALID_ENUM is generated if access is not GL_READ_ONLY, GL_WRITE_ONLY, or GL_READ_WRITE.
GL_OUT_OF_MEMORY is generated when glMapBuffer is executed if the GL is unable to map the buffer object’s data store. This may occur for a variety of system-specific reasons, such as the absence of sufficient remaining virtual memory.
GL_INVALID_OPERATION is generated if the reserved buffer object name 0 is bound to target.
GL_INVALID_OPERATION is generated if glMapBuffer is executed for a buffer object whose data store is already mapped.
GL_INVALID_OPERATION is generated if glUnmapBuffer is executed for a buffer object whose data store is not currently mapped.
glUnmapBuffer
GLboolean glUnmapBuffer(GLenum target);
// target: 需要被解除映射(unmap)的目标缓冲对象,可能值如下:
// GL_ARRAY_BUFFER
// GL_COPY_READ_BUFFER
// GL_COPY_WRITE_BUFFER
// GL_ELEMENT_ARRAY_BUFFER
// GL_PIXEL_PACK_BUFFER
// GL_PIXEL_UNPACK_BUFFER
// GL_TEXTURE_BUFFER
// GL_TRANSFORM_FEEDBACK_BUFFER
// GL_UNIFORM_BUFFER
// 如果数据成功映射到缓冲区,则此调用返回GL_TRUE
五、复制缓冲
可能会想与其它的缓冲共享其中的数据,或者想要将缓冲的内容复制到另一个缓冲当中。glCopyBufferSubData能够让我们相对容易地从一个缓冲中复制数据到另一个缓冲中。
glCopyBufferSubData
void glCopyBufferSubData(GLenum readtarget,
GLenum writetarget,
GLintptr readoffset,
GLintptr writeoffset,
GLsizeiptr size);
// 从readtarget缓冲的readoffset起始偏移位置拷贝size个字节数据到writetarget缓冲的writeoffset起始偏移位置。
// readtarget: 从哪里拷贝
// writetarget: 拷贝到哪里,这两者的可能值如下:
// GL_ARRAY_BUFFER
// GL_COPY_READ_BUFFER
// GL_COPY_WRITE_BUFFER
// GL_ELEMENT_ARRAY_BUFFER
// GL_PIXEL_PACK_BUFFER
// GL_PIXEL_UNPACK_BUFFER
// GL_TEXTURE_BUFFER
// GL_TRANSFORM_FEEDBACK_BUFFER
// GL_UNIFORM_BUFFER
// readoffset: 从readtarget的哪个位置开始读取,in basic machine units
// writeoffset: 从writetarget的哪个位置开始写入,in basic machine units
// size: 拷贝多少数据,in basic machine units
readtarget和writetarget可以是同一个缓冲对象,这时,readoffset+size和writeoffset+size区域不能重叠。
如果想从一个顶点数组缓冲拷贝到另一个顶点数组缓冲,OpenGL规定无法同时绑定到同一个缓冲目标上,这时候可以使用GL_COPY_READ_BUFFER和GL_COPY_WRITE_BUFFER。
具体做法如下:
GLuint vbo1;
GLuint vbo2;
...... // 创建VBO
GLfloat vertexData[] = { ... };
glBindBuffer(GL_COPY_READ_BUFFER, vbo1);
glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2);
// 从一个VBO拷贝到另一个VBO
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData));
GL_INVALID_VALUE is generated if any of readoffset, writeoffset or size is negative, if readoffset + size exceeds the size of the buffer object bound to readtarget or if writeoffset + size exceeds the size of the buffer object bound to writetarget.
GL_INVALID_VALUE is generated if the same buffer object is bound to both readtarget and writetarget and the ranges [readoffset, readoffset + size) and [writeoffset, writeoffset + size) overlap.
GL_INVALID_OPERATION is generated if zero is bound to readtarget or writetarget.
GL_INVALID_OPERATION is generated if the buffer object bound to either readtarget or writetarget is mapped.