一、背景
渲染大量物体时,场景中可能存在不同类型的几何与材质的组合,比如使用Blinn-Phong光照材质的圆柱物体、使用Disney-PBR材质的圆球物体。绘制一帧时,将绘制对象按照Pass、渲染状态、材质排序,这样会一定程度上解决OpenGL状态切换的问题,也会减少Shader的切换带来的Uniform重复设置。但当场景的材质类型变多,以及和场景设置相关的公共Uniform变多时,我们仍然会感受到明明很多shader参数是一模一样的,切换shader时却不得不重新设置一遍Uniform。通过使用Uniform Buffer可以解决这个问题,比如创建相机视图矩阵和投影矩阵的Uniform Buffer,创建ClipPlane的Uniform Buffer,创建光照相关的Uniform Buffer,然后绑定这些Buffer到所有Shader上,Shader的GLSL代码格式固定,这时Uniform Buffer数据改变时,所有Shader都会自动同步。
二、OpenGL API
2.1 生成与删除 UBO
void glGenBuffers(GLsizei n, GLuint *buffers);
-
作用:生成一个或多个 buffer 对象 ID(包括 UBO)。
-
参数:
n:需要生成的 buffer 对象数量。buffers:输出数组,存储生成的 buffer ID。
-
注意:生成后 buffer 还未绑定或分配存储。
void glDeleteBuffers(GLsizei n, const GLuint *buffers);
- 作用:删除一个或多个 buffer 对象。
- 参数:
n:要删除的对象数量。buffers:需要删除的 buffer ID 数组。
- 注意:删除后,任何绑定该 buffer 的操作都会失效。
2.2 绑定 UBO
void glBindBuffer(GLenum target, GLuint buffer);
-
作用:绑定 buffer 对象到指定 target。
-
参数:
target:缓冲对象的类型,UBO 需要用GL_UNIFORM_BUFFER。buffer:要绑定的 buffer ID。
void glBindBufferBase(GLenum target, GLuint index, GLuint buffer);
-
作用:将 UBO 绑定到 指定的 uniform block binding point。
-
参数:
target:GL_UNIFORM_BUFFERindex:绑定点索引,对应 shader 中layout(binding = index)。buffer:UBO 的 ID。
void glBindBufferRange(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);
-
作用:绑定 buffer 的一部分到绑定点。
-
参数:
target:GL_UNIFORM_BUFFERindex:binding pointbuffer:UBO IDoffset:buffer 起始偏移size:绑定的长度
-
应用场景:同一个大 buffer 存储多个 uniform block,可以用不同 offset 分别绑定。
2.3 分配/更新 UBO 内存
void glBufferData(GLenum target, GLsizeiptr size, const void *data, GLenum usage);
-
作用:为 buffer 分配存储,并可初始化。
-
参数:
-
target:GL_UNIFORM_BUFFER -
size:buffer 字节大小 -
data:初始数据(可为 NULL) -
usage:使用模式-
GL_STATIC_DRAW:数据不常改变 -
GL_DYNAMIC_DRAW:数据会频繁更新 -
GL_STREAM_DRAW:数据每帧都会更新
-
-
void glBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const void *data);
-
作用:更新 buffer 的一部分数据。
-
参数:
target:GL_UNIFORM_BUFFERoffset:从 buffer 起始位置偏移size:更新数据大小data:数据源
void* glMapBuffer(GLenum target, GLenum access);
-
作用:映射整个 buffer 到 CPU 内存,允许直接写入。
-
参数:
target:GL_UNIFORM_BUFFERaccess:访问模式GL_READ_ONLYGL_WRITE_ONLYGL_READ_WRITE
-
返回值:指向 buffer 内存的指针
void* glMapBufferRange(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access);
-
作用:映射 buffer 的一部分,更灵活。
-
access flags:
GL_MAP_READ_BIT/GL_MAP_WRITE_BITGL_MAP_INVALIDATE_BUFFER_BIT(可提高性能)GL_MAP_UNSYNCHRONIZED_BIT等
GLboolean glUnmapBuffer(GLenum target);
-
作用:解除映射,使 GPU 可以使用更新后的数据。
-
返回值:
GL_TRUE成功,GL_FALSE数据被 GPU 丢弃。
如果要直接映射数据到缓冲,而不事先将其存储到临时内存中,glMapBuffer这个函数会很有用。比如说,你可以从文件中读取数据,并直接将它们复制到缓冲内存中。
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));
// 记得告诉OpenGL我们不再需要这个指针了
glUnmapBuffer(GL_ARRAY_BUFFER);
2.4 查询 UBO 信息
GLuint glGetUniformBlockIndex(GLuint program, const GLchar *uniformBlockName);
GLuint blockIndex = glGetUniformBlockIndex(program, "Matrices");
-
作用:获取 shader 中 uniform block 的索引。
-
参数:
program:着色器程序 IDuniformBlockName:uniform block 名字
void glUniformBlockBinding(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding);
-
作用:将 shader 中的 uniform block 索引绑定到 UBO 绑定点。
-
参数:
program:shader program IDuniformBlockIndex:block 索引uniformBlockBinding:binding point
三、基于Triple-buffer的Uniform Buffer类
3.1 整体概述
GLUniformbufferObject 是一个用于高效管理 OpenGL Uniform Buffer 的类,它的设计目标是 CPU 更新 Uniform 数据时不等待 GPU 完成读取(无 Stall)。在传统方式下,如果 CPU 写入的 UBO 正在被 GPU 读取,就可能阻塞 CPU,这时通常会插入 Fence(GPU 完成信号)来检测 GPU 是否完成,但 Fence 会增加开销,影响性能;为了避免这种等待,GLUniformbufferObject 使用 三帧循环(Triple-buffer):为每帧预先分配独立的连续内存区域,CPU 写入当前帧数据时不会覆盖上一帧 GPU 仍在使用的区域,从而实现 无 Fence、无 Stall。
在每帧内部,它还使用 环形分配器(RingBuffer) 来管理内存:从头到尾线性分配每个对象的 Uniform 数据,分配到尾部后回绕到开头继续使用,这样可以灵活处理 Camera、Object、Material 中不同大小和数量的数据,同时自动处理 256 字节对齐。常用 API 包括 Create/CreateWithData(创建 UBO)、BeginFrame(切换到下一帧循环区域)、Allocate(为当前帧分配一块可写内存,返回 CPU 指针和 GPU 偏移)、Commit(提交映射)、BindRange(绑定当前分配到 Binding Point)、Bind(绑定 shader 块)、Upload(一次性覆盖数据)、Destroy、IsValid。使用时,每帧先调用 BeginFrame 切换帧,再通过 Allocate 拿到内存写入数据,Commit 提交,最后用 BindRange 绑定到 shader,即使像 CameraData 这种每帧大小固定的数据也需要 Allocate 来保证写入安全,而 ObjectData/MaterialData 多实例数据则通过环形分配器动态管理,保证高效、连续、异步安全的 GPU 访问。

3.2 功能设计
3.2.1 Create函数
Create 函数的作用是初始化一个 三帧循环的 Uniform Buffer 对象(UBO),它首先会销毁已有的 buffer(保证不会泄漏),然后查询 OpenGL 支持的 最小 UBO 对齐值(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT),把每帧的大小 frameSize 按这个对齐值对齐,计算整个 buffer 的总大小(m_iFrameSize * 3),保存绑定点和使用方式。接着调用 glGenBuffers 生成 OpenGL buffer ID,再用 glBufferData 分配 GPU 内存(不初始化内容),然后解绑 buffer。最后把三帧循环的索引和当前分配头置零,并标记没有活跃映射。整个过程保证 每帧都有一个独立的对齐区域,支持多帧动态更新而不阻塞 GPU,同时为后续的 Allocate/Commit 做好准备。
3.2.2 Alloacte函数
当前帧的环形分配区域里分配一块连续内存,供 CPU 写入 UBO 数据:它首先检查 buffer 是否有效和上一次映射是否已提交(防止覆盖未提交数据),然后把请求的 size 按 UBO 对齐规则(通常 256 字节)对齐;如果当前帧剩余空间不足,它会回绕到帧起始位置(环形分配器);接着计算在整个三帧循环 buffer 中的 全局偏移,调用 glMapBufferRange 映射这一段 GPU 内存给 CPU,返回一个 Allocation 结构包含 CPU 指针、偏移和大小,同时标记映射处于活跃状态,等待 Commit 提交。这样每帧可以多次 Allocate,不会阻塞 GPU,也不会覆盖其他帧的数据。
3.2.3 Commit函数
完成对上一次 Allocate 映射内存的写入并更新环形分配器状态:当 CPU 写入完映射的 UBO 区域后,调用 Commit 会用 glUnmapBuffer 解除映射(把数据提交到 GPU),并检查是否成功;然后把 m_bMapActive 标记为 false,表示可以进行下一次 Allocate;最后把当前帧的分配头 m_iFrameHead 增加已提交的大小,保证环形分配器在当前帧不会重复分配同一块内存。这一步是 环形分配器管理和三帧循环安全的关键,确保每帧的数据不会覆盖尚未使用的区域,同时 GPU 能够安全读取上一帧的数据。
3.2.4 BindRange函数
BindRange 函数的作用是把 Allocate/Commit 后的那一块 UBO 内存绑定到指定的 binding 点,让 Shader 可以访问这一段数据;它通过 glBindBufferRange 指定 buffer ID、偏移和大小,只暴露当前分配的区域给 GPU,而不是整个 buffer,这样即使一个大 buffer 管理多帧循环或多个对象的数据,Shader 也只会读取当前帧或当前对象的数据,保证了环形分配器的安全和灵活性。
3.2.5 BindProgram函数
函数的作用是把 UBO 的绑定点和 Shader 中对应的 uniform block 关联起来,它通过 glGetUniformBlockIndex 获取 Shader 中 block 的索引,然后用 glUniformBlockBinding 把这个索引绑定到 UBO 的 binding 点,这样 Shader 在执行时就会知道从哪个绑定点读取对应的数据;换句话说,BindProgram 是 把 GPU buffer 和 Shader uniform block 对应起来的桥梁,只需在初始化或 Shader 切换时调用一次即可,不需要每帧重复绑定。
3.2.5 Destroy函数
Destroy 函数的作用是 安全释放 UBO 占用的 GPU 资源:如果当前有活跃映射,它会先调用 glUnmapBuffer 解除映射,确保数据提交;然后调用 glDeleteBuffers 删除 GPU buffer,并把对象 ID、大小和映射状态重置,保证这个 GLUniformbufferObject 不再使用旧资源,同时避免内存泄漏或悬空指针。
