Skip to content

UBO 内存布局策略

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

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

glsl
uniform Constants {
    vec4 tilingOffset;
    vec4 albedo;
    vec4 albedoScaleAndCutoff;
    vec4 pbrParams;
    vec4 miscParams;
    vec4 emissive;
    vec4 emissiveScaleParam;
  };

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

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

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

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

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

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

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

    glsl
    uniform 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 Wikihttps://www.khronos.org/opengl/wiki/Interface_Block_(GLSL)#Memory_layout