V7投稿 | 2.x 同时渲染多材质

前因

本文基于 CocosCreator2.4.x,抛砖引玉。

在项目中常有自定义的多种材质,如:引擎内置的变灰(builtin-2d-gray-sprite)、描边、颜色渐变等。

[例子]描边

代码见:https://forum.cocos.org/t/topic/157396
效果:

image

[例子] 颜色渐变 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;
  }
}%

效果:

image

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

image

多材质渲染方案

针对多材质一起渲染,网上已经有多种方案。

这里使用几种 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;
  }
}%

输出效果:

image

从上面的代码中,可以看到,我们将两个效果代码组合在一起,成功生成最后想要的效果。

从这个例子中我们可以总结出以下优点:

  1. 手搓效果完整;手动将多个效果组合起来,仔细调试过后是能符合我们的预期需求的。
  2. 运行效率高;只有 1 个 DrawCall

当然也有下面的缺点:

  1. 重复编写;代码无法复用,需要针对每个组合效果都单独成一个新的材质;
  2. 代码量大:这里还只是两个效果组合,如果是多个效果一起,再将其 两两组合,想想就头皮发麻;
  3. 效果组合复杂:将多种效果组合起来对 shader 代码要求一定熟练度。
  4. 组合顺序固定:组合顺序根据代码写死,无法动态调整;

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 不支持

运行起来后,可以确认效果无误。

现在可以针对此种方式作出总结,有优点:

  1. fragment shader 复用:可以直接将之前写好的代码复制过来直接使用,不必再考虑多效果之前的组合代码。

它也有缺点:

  1. 低效:每个 pass 会占用一个 DrawCall,这个例子会占用2个 DrawCall
  2. 不灵活:针对其中某个效果无法动态开关,而手搓中可以通过 宏定义 来动态决定是否启用某个效果;
  3. 组合顺序固定:组合顺序由 passes 数组决定,无法动态调整;
  4. 代码量大:需要将每个效果 shader 都放到一个材质中,当然这也是不灵活的表现。

3. 多材质依次渲染

前面两种方式都是将已有的多个 shader 放入一个材质中来实现,复杂且不灵活。

有没有方法可以做到,利用 已有的材质 自由组合、依次渲染呢?可惜引擎本身并未提供。

还好引擎够强大,自由度够高。来分析一下渲染流程:

搜索一下 gl.drawElementsgl.drawArrays,我们发现其在 device.js 中,打个断点分析:

image

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

image

看看_checkBatch源码:

image

发现这里永远只渲染第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 中就好了。

总结这种方式有优点:

  1. 重复利用:能有效利用已写好的材质,不必为组合新增材质;
  2. 灵活:材质可以通过代码自由组合、开关;

当然也有缺点:

  1. DrawCall: 一个材质会占用一个DrawCall;
  2. 可能需要定制引擎:上面给的代码 BatcherMaterialsRender 中已经注释,这里必没有考虑 _postRender,要解决需要定制一下引擎。

本文讨论了在 CC2.x 中多材质渲染的几种方式,仅供参考。

彩蛋:

本文一直使用 描边 --> 渐变 的组合方式,你可以试着调换一下顺序,看看会发生什么,为什么?怎么解决?

提示:WebGLFrameBuffer

3赞