UBO 内存布局策略

Cocos Shader 规定,所有非 sampler 类型的 uniform 都应以 UBO(Uniform Buffer Object/Uniform Block)形式声明。

以内置着色器 builtin-standard.effect 为例,其 uniform block 声明如下:

  1. uniform Constants {
  2. vec4 tilingOffset;
  3. vec4 albedo;
  4. vec4 albedoScaleAndCutoff;
  5. vec4 pbrParams;
  6. vec4 miscParams;
  7. vec4 emissive;
  8. vec4 emissiveScaleParam;
  9. };

并且所有的 UBO 应当遵守以下规则:

  1. 不应出现 vec3 成员;
  2. 对数组类型成员,每个元素 size 不能小于 vec4;
  3. 不允许任何会引入 padding 的成员声明顺序。

Cocos Shader 在编译时会对上述规则进行检查,以便在导入错误(implicit padding 相关)时及时提醒修改。

这可能听起来有些过分严格,但背后有非常务实的考量:
首先,UBO 是渲染管线内要做到高效数据复用的唯一基本单位,离散声明已不是一个选项;
其次,WebGL2 的 UBO 只支持 std140 布局,它遵守一套比较原始的 padding 规则1

  • 所有 vec3 成员都会补齐至 vec4:

    1. uniform ControversialType {
    2. vec3 v3_1; // offset 0, length 16 [IMPLICIT PADDING!]
    3. }; // total of 16 bytes
  • 任意长度小于 vec4 类型的数组和结构体,都会将元素补齐至 vec4:

    1. uniform ProblematicArrays {
    2. float f4_1[4]; // offset 0, stride 16, length 64 [IMPLICIT PADDING!]
    3. }; // total of 64 bytes
  • 所有成员在 UBO 内的实际偏移都会按自身所占字节数对齐2

    1. uniform IncorrectUBOOrder {
    2. float f1_1; // offset 0, length 4 (aligned to 4 bytes)
    3. vec2 v2; // offset 8, length 8 (aligned to 8 bytes) [IMPLICIT PADDING!]
    4. float f1_2; // offset 16, length 4 (aligned to 4 bytes)
    5. }; // total of 32 bytes
    6. uniform CorrectUBOOrder {
    7. float f1_1; // offset 0, length 4 (aligned to 4 bytes)
    8. float f1_2; // offset 4, length 4 (aligned to 4 bytes)
    9. vec2 v2; // offset 8, length 8 (aligned to 8 bytes)
    10. }; // 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 Wikihttps://www.khronos.org/opengl/wiki/Interface_Block_(GLSL)#Memory_layout#Memory_layout)