前因
本文基于 CocosCreator2.4.x,抛砖引玉。
在项目中常有自定义的多种材质,如:引擎内置的变灰(builtin-2d-gray-sprite)、描边、颜色渐变等。
[例子]描边
代码见:https://forum.cocos.org/t/topic/157396
效果:

[例子] 颜色渐变 shader
下面是一个简易渐变 shader 例子:
CCProgram fs %{
// xxx
uniform GRADIENT {
vec4 u_startColor; // 渐变起始色
vec4 u_endColor; // 渐变结束色
float u_dir; // 渐变方向:0:水平;1:垂直;
};
void main () {
vec4 o = vec4(1, 1, 1, 1);
float rate = mix(v_uv0.x, v_uv0.y, u_dir); // 即:texCoord.x * (1.0 - dir) + texCoord.y * dir;
o = mix(u_startColor, u_endColor, vec4(rate));
#if USE_TEXTURE
vec4 tex = vec4(1, 1, 1, 1);
CCTexture(texture, v_uv0, tex);
o.a = tex.a;
#endif
o *= v_color;
gl_FragColor = o;
}
}%
效果:

那么如何针对一张纹理同时使用 描边、渐变 呢?

多材质渲染方案
针对多材质一起渲染,网上已经有多种方案。
这里使用几种 CocosCreator 中适用的。
1、手搓
手搓,也就是将所有效果放到一个 Shader 中实现。下面是描边、渐变效果代码:
CCProgram fs %{
// xxxx 其他代码
void main () {
vec4 o = vec4(1, 1, 1, 1);
// 渐变
#if USE_GRADIENT
float rate = mix(v_uv0.x, v_uv0.y, u_dir); // 即:texCoord.x * (1.0 - dir) + texCoord.y * dir;
o = mix(u_startColor, u_endColor, vec4(rate));
#endif
// 描边
#if USE_TEXTURE
CCTexture(texture, v_uv0, o);
o = mix(outlineColor, o, o.a);
o.a = getOutlineAlpha(v_uv0, outlineSize);
#endif
o *= v_color;
ALPHA_TEST(o);
gl_FragColor = o;
}
}%
输出效果:

从上面的代码中,可以看到,我们将两个效果代码组合在一起,成功生成最后想要的效果。
从这个例子中我们可以总结出以下优点:
- 手搓效果完整;手动将多个效果组合起来,仔细调试过后是能符合我们的预期需求的。
- 运行效率高;只有 1 个
DrawCall。
当然也有下面的缺点:
- 重复编写;代码无法复用,需要针对每个组合效果都单独成一个新的材质;
- 代码量大:这里还只是两个效果组合,如果是多个效果一起,再将其 两两组合,想想就头皮发麻;
- 效果组合复杂:将多种效果组合起来对
shader代码要求一定熟练度。 - 组合顺序固定:组合顺序根据代码写死,无法动态调整;
2. passes
利用引擎为我们提供的 passes ,也可以达到目的:
CCEffect %{
techniques:
- passes:
- vert: vs
frag: outline-fs # 指向描边 fragment shader
xxxx
- vert: vs
frag: gradient-fs # 指向渐变 fragment shader
xxxx
}%
// 描边 fragment shader
CCProgram outline-fs %{
xxxx
}
// 渐变 fragment shader
CCProgram gradient-fs %{
xxxx
}
有点可惜的是在 CC2.x 中,还没有支持 include 自定义的shader代码机制:
#include "../headers/my-shading-algorithm.chunk" // 2.x 不支持
运行起来后,可以确认效果无误。
现在可以针对此种方式作出总结,有优点:
- fragment shader 复用:可以直接将之前写好的代码复制过来直接使用,不必再考虑多效果之前的组合代码。
它也有缺点:
- 低效:每个
pass会占用一个DrawCall,这个例子会占用2个DrawCall; - 不灵活:针对其中某个效果无法动态开关,而手搓中可以通过 宏定义 来动态决定是否启用某个效果;
- 组合顺序固定:组合顺序由
passes数组决定,无法动态调整; - 代码量大:需要将每个效果
shader都放到一个材质中,当然这也是不灵活的表现。
3. 多材质依次渲染
前面两种方式都是将已有的多个 shader 放入一个材质中来实现,复杂且不灵活。
有没有方法可以做到,利用 已有的材质 自由组合、依次渲染呢?可惜引擎本身并未提供。
还好引擎够强大,自由度够高。来分析一下渲染流程:
搜索一下 gl.drawElements 或 gl.drawArrays,我们发现其在 device.js 中,打个断点分析:

强大的你很快就发现了 渲染流程 RenderFlow,又顺藤摸瓜找到了 model-batcher._flush,最后找到了调用方:

看看_checkBatch源码:

发现这里永远只渲染第0个材质。
那接下来怎么办呢?还好 js 是门动态语言,我们可以修改 _checkBacth 方法的实现,让它强制将整个 _materials 数组都加进去即可。
写一个组件来实现:
const {ccclass, property, executionOrder, executeInEditMode} = cc._decorator;
@ccclass
@executeInEditMode()
@executionOrder(-1)
export default class BatcherMaterialsRender extends cc.Component {
protected onLoad(): void {
const renderComp = this.getComponent(cc.RenderComponent);
renderComp['_checkBacth'] = _checkBacth.bind(renderComp);
}
}
function _checkBacth (renderer, cullingMask) {
const count = this._materials.length;
for (let i = 0; i < count; i++) {
_doCheckBacth.bind(this)(renderer, cullingMask, this._materials[i], i === count - 1);
}
}
function _doCheckBacth(renderer, cullingMask, material, isLast: boolean) {
if ((material && material.getHash() !== renderer.material.getHash()) ||
renderer.cullingMask !== cullingMask) {
renderer._flush();
renderer.node = material.getDefine('CC_USE_MODEL') ? this.node : renderer._dummyNode;
renderer.material = material;
renderer.cullingMask = cullingMask;
if (!isLast) {
// NOTE: 这里并未考虑 `_postRender 中的 postFillBuffers`,所以是存在问题的
this._assembler.fillBuffers(this, renderer);
renderer._flush();
}
}
}
好了,现在我们只需要将想要的效果材质都加到 Materials 中就好了。
总结这种方式有优点:
- 重复利用:能有效利用已写好的材质,不必为组合新增材质;
- 灵活:材质可以通过代码自由组合、开关;
当然也有缺点:
- DrawCall: 一个材质会占用一个DrawCall;
- 可能需要定制引擎:上面给的代码
BatcherMaterialsRender中已经注释,这里必没有考虑_postRender,要解决需要定制一下引擎。
本文讨论了在 CC2.x 中多材质渲染的几种方式,仅供参考。
彩蛋:
本文一直使用 描边 --> 渐变 的组合方式,你可以试着调换一下顺序,看看会发生什么,为什么?怎么解决?
提示:WebGLFrameBuffer