Cocos Creator Shader 入门 ⒀ —— UBO 内存布局策略

💡 本系列文章收录于个人专栏 ShaderMyHead

💡 本章是对《Cocos Creator 官方文档 ------ UBO 内存布局策略》的详细解释,也是对《着色器语法》一文的知识点扩展。

一、奇怪的报错

在之前的文章中我们了解到,Cocos Creator 着色器里,所有非 sampler 类型的 uniform(如 float, vec4)都必须放到一个 UBO(Uniform Block Object)块中声明,而不是一个个地零散声明。

示例:

js 复制代码
  uniform sampler2D renderTexture;  // 纹理变量无需走 UBO 形式定义

  uniform Constants {
    vec4 glowColor;     // 发光颜色
    vec2 textureAspect; // 纹理宽高比
    float glowSpread;   // 发光最大半径(基于 UV 坐标体系)
  };

上述的代码来自于《描边和发光效果的实现》案例,第 3 行代码开始就是一个标准的 UBO 定义(名为 Constants),它包含了三个不同类型的参数。

此时如果我们在 UBO 中把 vec2 textureAspect 放置到 vec4 glowColor 的声明上方:

js 复制代码
  uniform Constants {
    vec2 textureAspect;   // 将 textureAspect 的声明放到第一行
    vec4 glowColor;
    float glowSpread;
  };

Cocos Creator 编辑器会直接报错:

或者当我们新增了一个 vec3 类型的 uniform 变量:

js 复制代码
CCEffect %{
  techniques:
  - name: glow
    passes:
    - vert: vs:vert
      frag: fs:frag
      # 略...
      properties:
        colorRGB: { value: [1.0, 0.8, 0.3] }      # 新增 vec3 变量
        # 略...
}%


CCProgram fs %{
  // 略...
  
  uniform Constants {
    vec4 glowColor; 
    vec3 colorRGB;      // 新增 vec3 变量
    vec2 textureAspect;
    float glowSpread; 
  };
  
  // 略...
}%

也会出现类似的 implicit padding 报错信息:

这些都是为了兼容 OpenGL std140 规则导致的报错。

二、std140 规则

2.1 规则和示例

OpenGL 的 std140 是一种内存布局标准,用于定义 UBO 中各成员在内存中的排列方式。

它通过 对齐规则(alignment)填充策略(padding),实现显存访问的结构化和对齐,使 GPU 能更快地读取数据,避免跨字节读取,提升访问效率。

std140 的核心规则(对齐 + 填充)如下:

数据类型 对齐要求(字节) 占用大小(字节) 说明
float 4 4 可单独对齐
vec2 8 8 必须 8 字节对齐
vec3 / vec4 16 16 vec3 实际会补齐为 16 字节
mat4 16 64 被看作 4 个 vec4
数组元素 元素对齐 = 16 每个元素占 16 字节 不管你放 float/vec2,都会被扩充到 16 字节
结构体成员 每个成员必须按它自己的对齐来对齐;结构体总大小也必须是最大对齐值的倍数

💡「对齐」是指该类型的数据在显存中的地址偏移量(offset)必须是指定对齐要求的倍数,比如:

  • float 要求 4 字节对齐 → 地址必须是 4 的倍数(0x00、0x04、0x08...);
  • vec2 要求 8 字节对齐 → 地址必须是 8 的倍数(0x00、0x08、0x10...);
  • vec4 要求 16 字节对齐 → 地址必须是 16 的倍数(0x00、0x10、0x20...)。

示例一:vec3 会被补齐成 vec4(占 16 字节)

原本一个非 UBO 的 vec3 类型在显存中是占用 12 字节的,但在 UBO 声明时,会被填充到 16 字节(即跟 vec4 占用的长度一致):

js 复制代码
uniform Example {
    vec3 v3; // 实际占 16 字节(从 12 字节填充到 16 字节)
};

示例二:float 数组里的每个元素都对齐成 vec4

js 复制代码
uniform Example {
    float a[4]; // 每个 float 元素实际占 16 字节,数组总共占 16 x 4 = 64 字节!
};

示例三:错误的成员顺序将触发 padding

js 复制代码
uniform WrongOrder {
    float a;   // offset 0
    vec2 b;    // offset 8(vec2 要 8 字节对齐,因此在前面 padding 了 4 字节)
    float c;   // offset 16
};

上方代码段中,假设 float a 在显存中的地址偏移量为 0,则紧接着的 vec2 b 在显存中的地址偏移数将是 4 字节,这是不符合 vec2 类型的对齐要求的(UBO 中 vec2 类型需要对齐 8 个字节),因此会触发 padding,在 float a 的后面填充 4 个字节:

即这段代码会让显存浪费掉 4 字节的空间。但如果我们修改下声明顺序,把 float c 放到 vec2 b 上方,则不会触发 padding:

js 复制代码
uniform WrongOrder {
    float a;   // offset 0
    float c;   // offset 4
    vec2 b;    // offset 8
};

因为此时 float cvec2 b 的地址偏移字节数,都刚好满足它们的对齐要求:

2.2 为什么 std140 会更高效?

  • 首先内存对齐使 GPU 更易于读取:

    GPU 的内存控制器通常以 16 字节(128 位)为单位读取数据,如果你的数据跨越了这个边界:

    • 会导致需要两次读取 + 拼接,增加负担;
    • 无法使用 SIMD(单指令多数据)或向量化加速。

    std140 强制所有复杂类型都对齐到 16 字节,保证了每次读取都在对齐边界,GPU 可以快速、整块地读取。

  • 其次数据结构统一,可以避免平台差异

    不同厂商/设备/驱动实现可能有不同的默认布局,但 std140 是强制标准:

    • 着色器编译器可以预测每个变量偏移位置;
    • 应用层(CPU 端)可以一次性上传 UBO,不用担心对齐出错;
    • GPU 不需要解析复杂的偏移关系,直接按固定偏移读取。

std140 确实牺牲了一些 GPU 内存空间,也导致我们在 Cocos Creator 着色器的开发中容易出错,但这些代价是值得的,尤其在实时渲染中,带宽和读取性能更关键

三、Cocos Creator 中的限制

为了应对 std140 的统一规范,Cocos Creator 着色器中有如下三条规则:

  • 规则一、不应出现 vec3 成员

    std140 规则下,vec3 类型在 UBO 中实际占用空间会被自动填充至与 vec4 相同的 16 字节,会导致显存空间的浪费,因此 Cocos Creator 干脆直接禁止你在 UBO 中使用 vec3

    这也是文章开头声明 vec3 类型会报错的原因。

  • 规则二、对数组类型成员,每个元素的 size 不能小于 vec4

    这是为了避免数组中的每个元素被强行补齐(且你却不知情):

    js 复制代码
      uniform BadArray {
        float values[4]; // 每个 float 元素实际会被填充为 vec4 大小,即每项占 16 字节!
      };

    这段声明看似只用了 4 × 4 = 16 字节,实际却用了 4 × 16 = 64 字节,Cocos Creator 会要求你必须在明确了解这块浪费的前提下来声明数组。

  • 规则三、不允许任何会触发 padding 的成员声明顺序

    在前文我们了解到,std140 规定错误的成员顺序将触发 padding,会造成 GPU 内存浪费。Cocos Creator 干脆在编译阶段帮你兜底检查,发现错误的声明顺序直接报错。

    这也是文章开头修改 UBO 成员声明顺序后会报错的原因。

相关推荐
成长ing121389 天前
cocos creator 3.x shader 流光
前端·cocos creator
VaJoy10 天前
Cocos Creator Shader 入门 ⑾ —— 光照跟随
cocos creator
成长ing1213811 天前
闪白效果
前端·cocos creator
冷水金枪鱼12 天前
Light2D光照系统(基于CocosCreater引擎3.x/2.x)
cocos creator
VaJoy14 天前
Cocos Creator Shader 入门 ⑽ —— 拖尾效果的实现
cocos creator
VaJoy24 天前
Cocos Creator Shader 入门 ⑼ —— 溶解动画
cocos creator
VaJoy2 个月前
Cocos Creator Shader 入门 ⑺ —— 图层混合样式的实现与 Render Texture
cocos creator
VaJoy2 个月前
Cocos Creator Shader 入门 ⑹ —— 灰阶、反色等滤镜的实现
cocos creator
VaJoy2 个月前
Cocos Creator Shader 入门 ⑸ —— 代码复用与绿幕抠图技术
cocos creator