💡 本系列文章收录于个人专栏 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 c
和 vec2 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
这是为了避免数组中的每个元素被强行补齐(且你却不知情):
jsuniform BadArray { float values[4]; // 每个 float 元素实际会被填充为 vec4 大小,即每项占 16 字节! };
这段声明看似只用了 4 × 4 = 16 字节,实际却用了 4 × 16 = 64 字节,Cocos Creator 会要求你必须在明确了解这块浪费的前提下来声明数组。
-
规则三、不允许任何会触发 padding 的成员声明顺序
在前文我们了解到,
std140
规定错误的成员顺序将触发 padding,会造成 GPU 内存浪费。Cocos Creator 干脆在编译阶段帮你兜底检查,发现错误的声明顺序直接报错。这也是文章开头修改 UBO 成员声明顺序后会报错的原因。