在 UI 开发里,常见的背景通常是美术切好的 PNG/JPG。
但对于一些简单的规则纹理(比如条纹、栅格、渐变),完全可以用 Shader 程序化生成,这样:
- 不占贴图内存;
- 可以在 Inspector 上实时调参数;
- 同一套 Shader 可以复用在很多地方(不同颜色、角度、密度)。
这篇文章是一个 Cocos Creator 自定义 Effect 的实战:
用一个简单的片元着色器生成"双色条纹背景",支持:
- 双色配置;
- 条纹数量(密度);
- 整体旋转角度;
- 边缘平滑度(硬切换 / 柔和过渡);
- 透明 / 不透明两种渲染模式。
完整文件:BackgroundCreate.effect。
一、Effect 结构概览
Effect 文件由三部分组成:
CCEffect:定义 techniques / passes / properties;CCProgram background-vs:顶点着色器;CCProgram unlit-fs:片元着色器(真正生成条纹的地方)。
1. Techniques 与 Passes
yaml
CCEffect %{
techniques:
- name: opaque
passes:
- vert: background-vs:vert
frag: unlit-fs:frag
properties: &props
colorA: { value: [1, 1, 1, 1], editor: { type: color } }
colorB: { value: [0.85, 0.85, 0.85, 1], editor: { type: color } }
stripeCount: { value: 10 }
rotateDegrees: { value: 0 }
smoothness: { value: 0.0 }
- name: transparent
passes:
- vert: background-vs:vert
frag: unlit-fs:frag
depthStencilState:
depthTest: false
depthWrite: false
blendState:
targets:
- blend: true
blendSrc: src_alpha
blendDst: one_minus_src_alpha
blendSrcAlpha: src_alpha
blendDstAlpha: one_minus_src_alpha
properties: *props
}%
这里定义了两套 technique:
opaque:不透明背景;transparent:透明混合背景(启用了blend: true和 alpha blend)。
二者共用同一套 props:
colorA:条纹颜色 A;colorB:条纹颜色 B;stripeCount:条纹数量(密度);rotateDegrees:整体旋转角度(度);smoothness:条纹边缘柔和度。
这些属性会直接在 Creator 编辑器里暴露出来,可调。
二、顶点着色器:传入 UV 和本地坐标
glsl
CCProgram background-vs %{
precision highp float;
#include <builtin/uniforms/cc-global>
#if USE_LOCAL
#include <builtin/uniforms/cc-local>
#endif
in vec3 a_position;
in vec2 a_texCoord;
out vec2 v_uv;
out vec3 v_position;
out vec2 v_local;
vec4 vert () {
vec4 pos = vec4(a_position, 1.0);
#if USE_LOCAL
pos = cc_matWorld * pos;
#endif
// MVP 变换
pos = cc_matViewProj * pos;
// 传递 UV 给片元着色器
v_uv = a_texCoord;
// 传递本地坐标(如果需要按模型空间做归一化,可以用 v_local)
v_local = a_position.xy;
return pos;
}
}%
要点:
- 输入:
a_position,a_texCoord; - 输出给片元着色器:
v_uv:标准 UV(0~1),用于条纹计算;v_local:模型本地坐标,如果想按几何尺寸做更复杂的效果可以用,这里暂时没用到;
- 使用
cc_matWorld和cc_matViewProj做常规的世界变换和投影。
对当前这个效果来说,顶点着色器的责任非常简单:
把顶点送到屏幕上,并把 UV 传到片元阶段即可。
三、片元着色器:用正弦波生成条纹
真正有意思的部分在 unlit-fs:
glsl
CCProgram unlit-fs %{
precision highp float;
#include <builtin/uniforms/cc-global>
#include <legacy/output>
#include <legacy/fog-fs>
in vec2 v_uv;
in vec3 v_position;
in vec2 v_local;
uniform Constant {
vec4 colorA;
vec4 colorB;
float stripeCount;
float rotateDegrees;
float smoothness;
};
vec4 frag () {
vec2 p = v_uv;
// 旋转
float rad = radians(rotateDegrees);
float c = cos(rad);
float s = sin(rad);
vec2 rp = vec2(c * p.x - s * p.y, s * p.x + c * p.y);
// 使用正弦波的符号切换颜色(每跨越 π 即切换一次)
float wave = sin(3.14159265359 * stripeCount * rp.x);
// 边缘处理:smoothness==0 则硬切换,否则平滑过渡
float t = smoothness > 0.0 ? smoothstep(-smoothness, smoothness, wave) : step(0.0, wave);
vec4 col = mix(colorA, colorB, t);
return CCFragOutput(col);
}
}%
逐行拆一下核心逻辑。
1. 旋转 UV 坐标
glsl
vec2 p = v_uv;
float rad = radians(rotateDegrees);
float c = cos(rad);
float s = sin(rad);
vec2 rp = vec2(c * p.x - s * p.y, s * p.x + c * p.y);
v_uv是原始 UV(0~1 范围);- 把角度
rotateDegrees转成弧度,用标准 2D 旋转矩阵对 UV 坐标进行旋转:- 这样条纹原本沿 x 方向分布(垂直条纹),旋转后可以得到任意角度的条纹;
rp是旋转后的坐标,用于后续条纹计算。
2. 用正弦波生成条纹模式
glsl
float wave = sin(3.14159265359 * stripeCount * rp.x);
stripeCount:条纹数量;- 沿着
rp.x方向取正弦波:sin(π * stripeCount * x); - 每跨越 π 就完成一个"高→低→高"的周期;
- 之后只关心
wave的正负(符号),用来决定当前像素用colorA还是colorB。
3. 条纹边缘硬切换 / 平滑过渡
glsl
float t = smoothness > 0.0 ? smoothstep(-smoothness, smoothness, wave) : step(0.0, wave);
vec4 col = mix(colorA, colorB, t);
- 当
smoothness == 0.0:- 使用
step(0.0, wave); wave >= 0→ t = 1;wave < 0→ t = 0;- 条纹边缘是硬切换(类似硬边界)。
- 使用
- 当
smoothness > 0.0:- 使用
smoothstep(-smoothness, smoothness, wave); - 在
wave从负到正的过渡区域里平滑插值; - 条纹边缘会带一点模糊渐变,更柔和。
- 使用
最后用 mix(colorA, colorB, t) 在两种颜色之间插值:
t = 0→colorA;t = 1→colorB;- 中间值 → 两者混合。
四、Inspector 上可调的几个参数
在 Effect 里定义的 properties 决定了编辑器里能看到哪些可调项:
yaml
properties: &props
colorA: { value: [1, 1, 1, 1], editor: { type: color } }
colorB: { value: [0.85, 0.85, 0.85, 1], editor: { type: color } }
stripeCount: { value: 10 }
rotateDegrees: { value: 0 }
smoothness: { value: 0.0 }
在 Creator 中挂上这个材质后,可以直接调:
colorA/colorB:两种条纹颜色;stripeCount:- 小值 → 条纹宽、数量少;
- 大值 → 条纹窄、数量多;
rotateDegrees:- 0 度:竖直条纹;
- 90 度:水平条纹;
- 任意角度:斜向条纹;
smoothness:- 0:极硬边界,像 UI 分割纹理;
- 稍大一些:边缘渐变,类似光滑过渡。
五、透明 / 不透明两种模式
opaque 和 transparent 两个 technique 的区别在于:
-
opaque:默认深度写入、不混合,适合做普通背景; -
transparent:yamldepthStencilState: depthTest: false depthWrite: false blendState: targets: - blend: true blendSrc: src_alpha blendDst: one_minus_src_alpha blendSrcAlpha: src_alpha blendDstAlpha: one_minus_src_alpha- 关闭深度测试和写入;
- 启用标准透明混合,使条纹背景可以叠加在其他 UI 之上。
你可以为这个 Effect 做两个不同材质:
- 一个用
opaque,作为底层背景; - 一个用
transparent,叠在 UI 上当装饰纹理。
六、可以继续扩展的方向
在这个基础上,你还可以考虑:
- 替换
sin为abs(sin),做对称的条纹; - 在 y 方向叠加另一组条纹,形成网格背景;
- 使用噪声函数(简单 Perlin/Value Noise)混合颜色,做"云雾背景";
- 根据时间(
cc_time)偏移 UV,做动态流动条纹背景。
思路都是类似的:
顶点阶段负责把 UV 坐标送进来,片元阶段用数学函数把 UV 映射成颜色。
总结
这个 BackgroundCreate.effect 做了一件很实用又"性价比极高"的事:
- 用极少的代码实现了程序化双色条纹背景;
- 保留了 Rich UI 所需的几乎所有参数:
- 颜色、密度、旋转、边缘柔和度;
- 透明 / 不透明模式;
- 完全摆脱了纹理资源,不再需要为简单背景切图。
对于日常 UI 开发,这类小型 Shader 工具非常值得积累起来:
一方面减轻美术资源压力,另一方面也让你对 Cocos Creator 的 Effect / Shader 流程有更深的掌握。