背景
不少同学在用shader实现某个特效后都会遇到这样一个问题,在测试的时候效果完全正确,一集成到项目中后纹理就发生错位。这种情况大多是由于shader使用的纹理参与了合图导致uv发生了变化。
简单的处理方法是去掉纹理的packable属性,使其不参与合图。但是这样会带来一个问题: 不合图意味着渲染时无法参与合批,如果有大量节点用到了这个shader,那么drawcall就会较高。
经过一段时间的论坛灌水,发现有不少关于 图片遮罩的话题,所以本文就以批量图片遮罩为例介绍合批处理方法。
先来看两张效果图,一张是用 shader画圆做遮罩,这里的遮罩效果可以任意替换为其他shader效果;另一张是 自定义纹理做遮罩,合批渲染后均只占一个draw call。
Demo快速传送门
本文的实现基于这篇分享所介绍的 自定义顶点格式,想要了解实现原理的同学可以去回顾一下。
纹理uv坐标
注:为了简单起见,本文不对图片的 透明裁剪 和合图时的 旋转 进行讨论。实际的代码里会做相应细节处理 所有描述都以图片不做透明裁剪以及不旋转合图讨论
ccc中纹理uv坐标 以左上角为原点。 cc.SpriteFrame
的uv属性中,uv坐标按 左下、右下、左上、右上顺序排列,其值的含义是这个 cc.SpriteFrame
的四个顶点在 cc.Texture
纹理空间中的坐标。
shader画圆遮罩
如果要用shader对图片做一个圆形遮罩,通常要计算像素距离图片中心的距离,这里需要获取图片中心的uv坐标。
但是实际上在合图之后,基于图片中心uv的计算很难保证画出一个圆,即使当前的 cc.SpriteFrame
长宽是相同的,在合图后的大纹理内 并不能保证相对的长宽比例相同 。
个人的推荐做法是将 cc.SpriteFrame
的 uv重新映射到[0,1]区间,方便shader处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 将[a, b]区间映射到[0, 1]区间
// t是[a, b]区间内的值
// 函数返回t被映射后的值
float Remap01(float a, float b, float t) {
return (t-a) / (b-a);
}
void main () {
vec2 uv = v_uv0.xy;
vec4 col = texture(texture, uv);
// v_xrange.xy分别表示子纹理的x轴左右边缘坐标
// v_yrange.xy分别表示子纹理的y轴上下边缘坐标
// uv的xy轴分别映射到[0, 1]区间,之后shader的写法即可按照未合图前的方式处理
uv.x = Remap01(v_xrange.x, v_xrange.y, uv.x);
uv.y = Remap01(v_yrange.x, v_yrange.y, uv.y);
// 画圆形遮罩,以(0.5, 0.5)为圆心
float d = distance(uv, vec2(0.5, 0.5));
float r = 0.5;
float mask = smoothstep(r + 0.01, r - 0.01, d);
col.a = mask;
gl_FragColor = col;
}
上面这段片元着色器中用到了 v_xrange
, v_yrange
两个变量,需要以参数形式输入。
为了 不打断合批,使用这篇分享所介绍的 自定义顶点格式方式传入。
这种映射不仅能处理画圆的场景,任何需要计算相对位置、距离的shader都可以用这种方式处理合图后的uv 实际的Demo代码中使用的是Remap01优化后的变种,但是原理是完全一致的
纹理遮罩
纹理遮罩合批需要保证底图和遮罩都参与合图。
可以选择 底图合一张大图,遮罩合另一张大图;也可以选择 底图、遮罩都合到一起。
遮罩的uv也是通过顶点属性传入shader。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
in vec2 v_uv0; // 底图uv
uniform sampler2D texture; // 底图纹理
in vec2 v_mask_uv; // 遮罩图uv,通过顶点属性传入
uniform sampler2D mask; // 遮罩图纹理,通过材质属性传入
uniform UARGS {
float enableMask; // 遮罩图开关控制,通过材质属性传入
};
void main () {
// 对底图采样
vec4 col = texture(texture, v_uv0);
// 对遮罩图采样
vec4 maskCol = texture(mask, v_mask_uv);
// 片元透明度使用遮罩图透明度
// enableMask控制是否让遮罩生效
col.a = mix(col.a, maskCol.a, enableMask);
gl_FragColor = col;
}
材质属性 or 顶点属性?
自定义顶点格式这么好用,是不是可以抛弃材质属性(uniform变量)了?
NO!
用顶点属性做传参是为了实现合批渲染的一种权宜的处理方式。
- 顶点属性会在每个顶点上 冗余一份数据,即使这些数据是一模一样的,会少量增加内存使用和顶点数据拷贝时间。所以用顶点属性传参适合顶点数量少的渲染组件。一次draw call中统一的数据务必使用材质属性。
- 如果某个特效只对少数几个渲染组件生效,甚至是否合批都无所谓,建议使用材质属性进行传参
两种传参方式可以结合,实际应用中根据项目需求灵活运用。
Demo地址