什么是 Uniform Buffer Object?

见WIKI: https://www.khronos.org/opengl/wiki/Uniform_Buffer_Object

及 learnopengl 中的示例:https://learnopengl.com/Advanced-OpenGL/Advanced-GLSL#:~:text=the%20geometry%20shader.-,Uniform%20buffer%20objects,-We%27ve%20been%20using

在项目中使用 UBO 时,遇到了一些坑,这里记录一下。

坑1:UBO 的对齐问题

在使用 UBO 时,需要注意 UBO 的对齐问题。为了代码的可移植性,一般会直接使用 std140 来定义 UBO 的内存布局,如:

1
2
3
4
5
6
7
layout (std140) uniform ExampleBlock
{
float value;
vec3 vector;
mat4 matrix;
bool boolean;
};

std140 的布局规则 理解了是一回事,但是在C++中写一个 UBO 对应的struct 的时候,还是会出现对齐问题。比如C++ 中用下面这个 struct 来对应上面的 ExampleBlock

1
2
3
4
5
6
struct ExampleBlock {
float value;
glm::vec3 vector;
glm::mat4 matrix;
bool boolean;
};

这个结构体在C++编译器的眼里是按C++ 自己的内存布局规则来的,这时候我们需要手动按std140对齐:

1
2
3
4
5
6
struct alignas(16) ExampleBlock {
float value;
glm::vec4 vector;
glm::mat4 matrix;
alignas(4) bool boolean;
};

注意上面例子中的 struct 中的 bool 类型,需要对齐到 4字节。

但是,如果你这么写了,仍然可能会遇到一个问题,C++ 代码中明明设置的是 false ,但是程序执行后,在 Shader Program 中读取到的却是 true。(゚Д゚≡゚д゚)!?

1
2
3
4
5
6
ExampleBlock block;
block.boolean = false;

// 传递给 GLSL
glBindBuffer(GL_UNIFORM_BUFFER, ubo);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(ExampleBlock), &block);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#version 410 core

out vec4 FragColor;

layout (std140) uniform ExampleBlock
{
float value;
vec3 vector;
mat4 matrix;
bool boolean;
};
void main () {
if (boolean) {
FragColor = vec4(1.0f, 0.0f, 0.0f, 1.0f);
} else {
FragColor = vec4(0.0f, 1.0f, 0.0f, 1.0f);
}
}

C++ 中boolean的值为 false,但显示的是红色!

这是因为 bool 类型在 C++ 中是 1 字节,而在 GLSL 中是 4 字节,故而在 C++ 中,为 bool 增加了 3 个字节的 padding, 而 padding 的值是不确定的(通常取决于编译器行为)。因为 padding 的存在,导致C++中明明看起来是false,在 Shader 中按 bool 类型 读到的却是 true等于00.0 的是 false,其他任何值都是 true)。

如下所示,bool boolean 字段的内存中 padding 位被填充了非0的数据

1
2
3
00000000 00000001 00100000 00100000 00000000
^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ padding
false in C++

解决办法,在C++中 用 int32_t 类型代替 bool 类型,在 GLSL 中还是用 bool 类型来读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// bool in std140 layout
struct bool_std140 {
int32_t v;
constexpr bool_std140() : v(0) {}
constexpr bool_std140(bool b) : v(b ? ~0 : 0) {}
// In GLSL, 0 is false, anything else is true
constexpr bool_std140(int32_t i) : v(i != 0 ? ~0 : 0) {}
inline bool_std140 &operator=(bool b) {
v = b ? ~0 : 0;
return *this;
}
inline bool_std140 &operator=(int32_t i) {
v = i != 0 ? ~0 : 0;
return *this;
}
inline constexpr operator bool() const noexcept { return v != 0; }
};


struct alignas(16) ExampleBlock {
float value;
glm::vec4 vector;
glm::mat4 matrix;
alignas(4) bool_std140 boolean;
};
1
2
ExampleBlock block;
block.boolean = false;

同样的问题,还会体现在 mat3 类型上,mat3 在 GLSL 中是 3 * vec3 (mat3x3),在 C++ 中需要手动对齐为 3 * vec4 (column major, mat4x3)。

1
2
3
4
5
6
7
8
9
10
11
12
13
// vec3 in std140 layout
struct alignas(16) vec3_std140 : public std::array<float, 3> { };
// mat3 in std140 layout
struct mat33_std140 : public std::array<vec3_std140, 3> {
mat33_std140& operator=(glm::mat3 const& rhs) noexcept {
for (int i = 0; i < 3; i++) {
(*this)[i][0] = rhs[i][0];
(*this)[i][1] = rhs[i][1];
(*this)[i][2] = rhs[i][2];
}
return *this;
}
};

坑2:同名 UBO

如果在不同 Shader 中定义了两个同名的 UBO,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1.frag
layout (std140) uniform ExampleBlock
{
float value;
vec3 vector;
};

// 2.frag
layout (std140) uniform ExampleBlock
{
mat4 matrix;
bool boolean;
};

假如这两个Shader 同时被use,在 C++ 中传递 UBO 数据的时候,只有最后一个 UBO 会被传递到 GPU,而前面的会被忽略(甚至有可能导致内存问题)。

这是因为 UBO 在 OpenGL 中是全局的,如果定义了两个同名的 UBO,OpenGL 会认为这是同一个 UBO,只有最后一个 UBO 的数据会被传递到 GPU。

解决办法是,给不同结构的 UBO 用不同的名字(就是这么朴素而有效 <(▰˘◡˘▰)> )

Refs