在OpenGL Shader中,如果逻辑比较复杂,使用的uniform变量较多。通常多个着色器使用同一个uniform变量。由于uniform变量的位置是着色器链接时候产生的,因此它在应用程序中获得的索引会有变化。Uniform Buffer Object(UBO)是一种优化uniform变量访问,不同着色器直接共享unfiorm数据的方法。
在Overload引擎中,很多Shader包含如下片段,这里就是定义了一个UBO变量。 它将MVP矩阵一起放入到UBO变量中。
layout (std140) uniform EngineUBO
{
mat4 ubo_Model;
mat4 ubo_View;
mat4 ubo_Projection;
vec3 ubo_ViewPos;
float ubo_Time;
};
std140是内存布局限定符,除此之外还有std430、binding、packed等限定符。
Overload引擎中对UBO的封装在UniformBuffer.h、UniformBuffer.inl、UniformBuffer.cpp文件中,将其操作包装成了一个类UniformBuffer。使用的时候先调用Bind,结束后UnBind,设置值使用SetSubData。
namespace OvRendering::Buffers
{
/**
* OpenGL UBO的封装
*/
class UniformBuffer
{
public:
/**
* Create a UniformBuffer
* @param p_size (Specify the size in bytes of the UBO data)
* @param p_bindingPoint (Specify the binding point on which the uniform buffer should be binded)
* @parma p_offset (The offset of the UBO, sizeof previouses UBO if the binding point is != 0)
* @param p_accessSpecifier
*/
UniformBuffer(size_t p_size, uint32_t p_bindingPoint = 0, uint32_t p_offset = 0, EAccessSpecifier p_accessSpecifier = EAccessSpecifier::DYNAMIC_DRAW);
/**
* Destructor of the UniformBuffer
*/
~UniformBuffer();
/**
* Bind the UBO
*/
void Bind();
/**
* Unbind the UBO
*/
void Unbind();
/**
* Set the data in the UBO located at p_offset to p_data
* @param p_data
* @param p_offset
*/
template
void SetSubData(const T& p_data, size_t p_offset);
/**
* Set the data in the UBO located at p_offset to p_data
* @param p_data
* @param p_offsetInOut (Will keep track of the current stride of the data layout)
*/
template
void SetSubData(const T& p_data, std::reference_wrapper p_offsetInOut);
/**
* Return the ID of the UBO
*/
uint32_t GetID() const;
/**
* Bind a block identified by the given ID to given shader
* @param p_shader
* @param p_uniformBlockLocation
* @param p_bindingPoint
*/
static void BindBlockToShader(OvRendering::Resources::Shader& p_shader, uint32_t p_uniformBlockLocation, uint32_t p_bindingPoint = 0);
/**
* Bind a block identified by the given name to the given shader
* @param p_shader
* @param p_name
* @param p_bindingPoint
*/
static void BindBlockToShader(OvRendering::Resources::Shader& p_shader, const std::string& p_name, uint32_t p_bindingPoint = 0);
/**
* Return the location of the block (ID)
* @param p_shader
* @param p_name
*/
static uint32_t GetBlockLocation(OvRendering::Resources::Shader& p_shader, const std::string& p_name);
private:
uint32_t m_bufferID;
};
}
#include "OvRendering/Buffers/UniformBuffer.inl"
其具体实现在UniformBuffer.cpp中。我们先看看构造函数代码:
OvRendering::Buffers::UniformBuffer::UniformBuffer(size_t p_size, uint32_t p_bindingPoint, uint32_t p_offset, EAccessSpecifier p_accessSpecifier)
{
// 生成buffer
glGenBuffers(1, &m_bufferID);
// 绑定UBO
glBindBuffer(GL_UNIFORM_BUFFER, m_bufferID);
// 分配内存
glBufferData(GL_UNIFORM_BUFFER, p_size, NULL, static_cast(p_accessSpecifier));
glBindBuffer(GL_UNIFORM_BUFFER, 0);
// 将缓存对象m_bufferID绑定到索引为p_bindingPoint的UBO上
glBindBufferRange(GL_UNIFORM_BUFFER, p_bindingPoint, m_bufferID, p_offset, p_size);
}
在构造函数中直接创建了UBO的buffer,并绑定到索引是p_bindingPoint的UBO上。这里用到了OpenGL函数glBindBufferRange,如无需指定偏移量与size值可使用glBindBufferBase函数。
UniformBuffer.cpp中Bind()、UnBind()过于简单不再分析。往下接着看有个static函数BindBlockToShader。这个函数主要是显式绑定一个uniform块到p_bindingPoint索引,这样可以绑定同一个缓存。这里使用到了glUniformBlockBinding函数,这个函数主要是显示指定BUO的索引,可以保证多个不同的Shader程序之间UBO的索引是一样的,但需要在调用glLinkProgram之前调用。
void OvRendering::Buffers::UniformBuffer::BindBlockToShader(OvRendering::Resources::Shader& p_shader, uint32_t p_uniformBlockLocation, uint32_t p_bindingPoint)
{
glUniformBlockBinding(p_shader.id, p_uniformBlockLocation, p_bindingPoint);
}
void OvRendering::Buffers::UniformBuffer::BindBlockToShader(OvRendering::Resources::Shader& p_shader, const std::string& p_name, uint32_t p_bindingPoint)
{
glUniformBlockBinding(p_shader.id, GetBlockLocation(p_shader, p_name), p_bindingPoint);
}
// 获取UBO的索引位置
uint32_t OvRendering::Buffers::UniformBuffer::GetBlockLocation(OvRendering::Resources::Shader& p_shader, const std::string& p_name)
{
return glGetUniformBlockIndex(p_shader.id, p_name.c_str());
}
但在Overload引擎中,调用这个方法是在调用glProgram之后调用的,而且索引值使用的是GetBlockLocation获取的,这也是UBO在Shader的默认索引值,所以这个方法应该是可以删除的。我注释这个方法使用上没有发现什么问题。
最后看一下如何给UBO设置值,其实现是在UniformBuffer.inl文件中,主要使用glBufferSubData函数,指定其偏移值与数据大小即可。
template
inline void UniformBuffer::SetSubData(const T& p_data, size_t p_offsetInOut)
{
Bind();
glBufferSubData(GL_UNIFORM_BUFFER, p_offsetInOut, sizeof(T), std::addressof(p_data));
Unbind();
}
template
inline void UniformBuffer::SetSubData(const T& p_data, std::reference_wrapper p_offsetInOut)
{
Bind();
size_t dataSize = sizeof(T);
glBufferSubData(GL_UNIFORM_BUFFER, p_offsetInOut.get(), dataSize, std::addressof(p_data));
p_offsetInOut.get() += dataSize;
Unbind();
}
Shader Storage Buffer Object(SSBO),着色器存储缓存对象,其行为类似于UBO,但其功能上更为强大。首先,着色器可以写入buffer块,修改其内容并呈现给其他Shader或应用程序本身。其次,可以在渲染之前再觉得其大小,而不是编译与链接时。在Overload中,灯光信息是用SSBO存储的,看以下Shader片段:
layout(std430, binding = 0) buffer LightSSBO
{
mat4 ssbo_Lights[];
};
在着色器中可以使用length()获取ssbo_Lights的长度。
设置SSBO的方式与设置UBO类似,不过glBindBuffer()、glBindBufferRange()、glBindBufferBase()需要使用GL_SHADER_STORAGE_BUFFER作为目标参数。
Overload是将SSBO的操作封装到类ShaderStorageBuffer中,具体代码就不分析了,与UBO大同小异。