UBO 内存布局策略
Cocos Shader 规定,所有非 sampler 类型的 uniform 都应以 UBO(Uniform Buffer Object/Uniform Block)形式声明。
以内置着色器 builtin-standard.effect
为例,其 uniform block 声明如下:
uniform Constants {
vec4 tilingOffset;
vec4 albedo;
vec4 albedoScaleAndCutoff;
vec4 pbrParams;
vec4 miscParams;
vec4 emissive;
vec4 emissiveScaleParam;
};
并且所有的 UBO 应当遵守以下规则:
- 不应出现 vec3 成员;
- 对数组类型成员,每个元素 size 不能小于 vec4;
- 不允许任何会引入 padding 的成员声明顺序。
Cocos Shader 在编译时会对上述规则进行检查,以便在导入错误(implicit padding 相关)时及时提醒修改。
这可能听起来有些过分严格,但背后有非常务实的考量:
首先,UBO 是渲染管线内要做到高效数据复用的唯一基本单位,离散声明已不是一个选项;
其次,WebGL2 的 UBO 只支持 std140 布局,它遵守一套比较原始的 padding 规则[^1]:
所有 vec3 成员都会补齐至 vec4:
glsluniform ControversialType { vec3 v3_1; // offset 0, length 16 [IMPLICIT PADDING!] }; // total of 16 bytes
任意长度小于 vec4 类型的数组和结构体,都会将元素补齐至 vec4:
glsluniform ProblematicArrays { float f4_1[4]; // offset 0, stride 16, length 64 [IMPLICIT PADDING!] }; // total of 64 bytes
所有成员在 UBO 内的实际偏移都会按自身所占字节数对齐[^2]:
glsluniform IncorrectUBOOrder { float f1_1; // offset 0, length 4 (aligned to 4 bytes) vec2 v2; // offset 8, length 8 (aligned to 8 bytes) [IMPLICIT PADDING!] float f1_2; // offset 16, length 4 (aligned to 4 bytes) }; // total of 32 bytes uniform CorrectUBOOrder { float f1_1; // offset 0, length 4 (aligned to 4 bytes) float f1_2; // offset 4, length 4 (aligned to 4 bytes) vec2 v2; // offset 8, length 8 (aligned to 8 bytes) }; // total of 16 bytes
这意味着大量的空间浪费,且某些设备的驱动实现也并不完全符合此标准[^3],因此目前 Cocos Shader 选择限制这部分功能的使用,以帮助排除一部分非常隐晦的运行时问题。
再次提醒:uniform 的类型与 inspector 的显示和运行时参数赋值时的程序接口可以不直接对应,通过 property target 机制,可以独立编辑任意 uniform 的具体分量。
[^1]: OpenGL 4.5, Section 7.6.2.2, page 137
[^2]: 注意在示例代码中,UBO IncorrectUBOOrder 的总长度为 32 字节,实际上这个数据到今天也依然是平台相关的,看起来是由于 GLSL 标准的疏忽,更多相关讨论可以参考 这里。
[^3]: Interface Block - OpenGL Wiki:https://www.khronos.org/opengl/wiki/Interface_Block_(GLSL)#Memory_layout