Cocos Creator Shader 入门 ⒂ —— 自定义后处理管线

1,210 阅读8分钟

💡 本系列文章收录于个人专栏 ShaderMyHead

💡 本文案例可以在 Github 上进行演示

在某些场景可能需要创建多个摄像头 + RenderTexture 文件来叠加着色器效果,例如在《高斯模糊的实现》一文中,我们需要通过这种麻烦的操作来应用 gaussian-blur-vertical.mtl 材质,才能在原本应用了水平方位高斯模糊材质的基础上,进一步在垂直方位应用高斯模糊。

按照以往的方案,如果需要叠加 N 个着色器,我们就得创建 N 个摄像头、RenderTexture 文件、绘制前一个摄像头捕获画面的 SpriteFrame 节点,这会让操作变得麻烦,层级管理器上的节点也会变得杂乱和难以阅读,Draw Call 数量也会跟着变多。

Cocos Creator 提供了摄像机后处理(Post Process)的能力,通过该能力可以极大减少着色器应用的复杂度。

一、后处理功能的使用

假设场景上存在一个仅捕获 DEFAULT 层级的摄像机 CameraForDefault,和一张处于 DEFAULT 层级的绿叶背景图 BG 节点:

image.png

假设我们希望对绿叶背景图做 4 次高斯模糊(水平和垂直方向上各 2 次,且需要按顺序执行),并再染上 30% 透明度的绿色,这番操作会涉及三个着色器 —— 水平模糊着色器、垂直模糊着色器、染色着色器,它们的执行次序预期为:

水平模糊着色器 -> 垂直模糊着色器 -> 水平模糊着色器 -> 垂直模糊着色器 -> 染色着色器

按照以往的方案,我们需要额外创建 5 个摄像头用于生成 RenderTexture、5 个 SpriteFrame 节点用于缓存和绘制各步骤处理后的纹理,整个操作会很繁琐和低效。

启用摄像机的后处理功能,则仅需要新增一个节点用于绘制多个着色器处理后的画面。

1.1 启用后处理功能

后处理功能依赖于自定义渲染管线,在 Cocos Creator 3.8.6 中需要先在「项目设置 -> 图像设置 -> 新渲染管线」处确保勾选「后处理模块」,且必须填写一个自定义的管线名称(如下图所示,填写了一个叫 Customize 的名称):

image.png

接着我们勾选上摄像机属性检查器界面的 Use Post Process,表示对此摄像机启用自定义的后处理管线:

image.png

💡 Cocos Creator 官方目前暂停了对后处理模块的维护(因此标记为废弃),不排除未来会调整后处理功能的使用形式。

💡 Cocos Creator 官方也提供了自定义渲染管线新方案(实验性质)的 Demo,个人研究后不推荐使用(需要自行开发组件脚本,使用起来比较复杂,且极容易报错)。

目前我们已经为 CameraForDefault 摄像机启用了后处理管线能力,但还需要告诉游戏引擎「应该把该摄像机拍摄的画面交给谁来处理」。

1.2 新增后处理节点并绑定摄像机

我们再新增一个位于 DEFAULT 层级的空节点 PostProcessForDefault,它将用于绘制后处理管线处理后的最终画面。

该节点需要添加专门的组件,才能接收和处理来自指定摄像机捕获的离屏画面。

我们为其添加名为 BlitScreen 的组件(该组件依赖于 PostProcess 组件,因此会同步添加上 PostProcess 组件),在 BlitScreen 的组件的 Materials 处,可以自行新增多个材质插槽:

889.gif

💡 BlitScreen 主要负责维护一个有序的后处理材质列表,PostProcess 组件作为后处理管理器,为每个摄像机提供对应的后处理实例配置。

     在渲染流程中, PostProcess 会获取摄像头捕获的画面,将其交由 BlitScreen 维护的材质列表进行链式依序处理,每个材质的输出作为下一个材质的输入,最终形成完整的后处理效果链。

留意去掉 PostProcess 组件的 Global 选项,若勾选了表示它会作为全局默认的后处理设置,将应用于所有开启了后处理功能但未绑定 PostProcess 节点的摄像机。

最后我们将该节点拖入 CameraForDefault 摄像机的 Post Process 处进行绑定:

22.gif

这样 Cocos Creator 会把摄像机捕获的离屏画面交给 PostProcessForDefault 节点的后处理组件去处理。

1.3 后处理着色器

后处理管线使用的着色器语法与我们之前编写的基本一致,但有几个关键点需要留意:

  • CCEffect 的 passes 中必须声明 pass: post-process,表示该着色器通道(pass)的类型为后处理通道;
  • cc_timecc_spriteTexture 等全局 uniform 变量会失效;
  • 当前纹理变量(原本的 cc_spriteTexture)应更换为 inputTexture,且需要使用 #pragma rate inputTexture pass 预处理器指令将其指向 BlitScreen 在通道中传递的纹理名称
  • 鉴于获取到的是全屏画面的像素,故顶点着色器无需做坐标系转换等处理(顶点着色器代码变得很精简)。

依照上述几条规则,「给画面染上 30% 透明度的绿色」的着色器(green-screen.effect)代码为:

CCEffect %{
  techniques:
  - name: opaque
    passes:
    - vert: green-screen-vs
      frag: green-screen-fs:frag
      pass: post-process  # 新增声明
      blendState:
        targets:
          - blend: true
            blendSrc: src_alpha
            blendDst: one_minus_src_alpha
      depthStencilState:
        depthTest: false
        depthWrite: false
}%

CCProgram green-screen-vs %{
  precision highp float;
  in vec3 a_position;
  in vec2 a_texCoord;
  out vec2 uv;

  void main () {
    vec4 pos = vec4(a_position, 1.0);
    gl_Position = pos;
    uv = a_texCoord;
  }
}%

CCProgram green-screen-fs %{
  precision highp float;
  
  in vec2 uv;

  #pragma rate inputTexture pass
  uniform sampler2D inputTexture;   // 留意当前纹理不再是 cc_spriteTexture

  vec4 frag () {
    vec4 color = texture(inputTexture, uv);

    vec4 green = vec4(0.0, 1.0, 0.0, 1.0);
    return mix(color, green, 0.3);
  }
}%

按规则修改上篇文章的高斯模糊着色器代码后,创建 hor-blur.mtlver-blur.mtlgreen-screen.mtl 三个对应的材质,并将它们拖入 BlitScreen 组件的材质插槽:

image.png

在 Cocos Creator 编辑器中执行项目,通过控制各材质下的 Enable 选项,可以决定是否启用该材质:

55.gif

可以看到后处理管线会将 BlitScreen 组件中的着色器材质依序逐个应用,最终得到了我们想要的画面处理效果:

水平模糊 -> 垂直模糊 -> 水平模糊 -> 垂直模糊 -> 染上30%绿色

二、多摄像机后处理

若场景存在多个摄像机,每个摄像机可以走同样的方式启用互相独立的后处理管线。

2.1 启用后处理功能

我们新增一个 CameraForUI2D 摄像机用于拍摄 UI_2D 层级的内容,以及一个 PostProcessForUI2D 节点。

摄像机勾选 Use Post Process 后,将 PostProcessForUI2D 节点绑定到摄像机的 Post Process 处:

image.png

同时新增一个层级位于 UI_2D 的 Label(只会被 CameraForUI2D 摄像机捕获):

image.png

我们新建一个着色器和对应的材质,来给这个 Label 染上 30% 透明度的红色,其中着色器文件(red-screen.effect)与前文的 green-screen.effect 内容基本一致,只是将片元着色器中的混合颜色改为红色:

  vec4 frag () {
    vec4 color = texture(inputTexture, v_uv);
    
    // 混合的颜色为红色
    vec4 red = vec4(1.0, 0.0, 0.0, 1.0);
    return mix(color, red, 0.3);
  }

最后一步是为 PostProcessForUI2D 节点添加 BlitScreen 组件并绑定对应的材质:

image.png

此时新增的摄像机也拥有了自己独立的后处理管线。

2.2 启用 Blending

在上述步骤完成后执行项目,会发现 Label 确实染上了 30% 的红色,但下层 PostProcessForDefault 的画面被黑色完全覆盖了:

099.jpg

这是由于启用了后处理的摄像机最终会走到一个名为 post-final 的后处理 Pass,该 Pass 对应的着色器文件内部没有开启 Blending 混合模式,导致在与背景色混合时不会开启 Alpha 通道。

我们可以在资源管理器里搜索 post-final.effect 找到该内置的着色器文件:

12.png

打开后加入 blendState 配置,启用透明度混合功能:

13.png

保存修改后再执行,CameraForDefault 摄像机所捕获并后处理过的画面(即 PostProcessForDefault 节点),就能被作为背景色混合并展示出来:

14.png

💡 post-final.effect 仅会在摄像机开启 usePostProcess 时才被使用到,故其修改对整体影响不大(值得修改)。

2.3 适配窗口变化

由于新增的摄像机不被 Canvas 绑定,其离屏幕渲染的内容无法套用画布的宽高适配策略,具体表现为该摄像机所拍摄的画面不会跟随窗口的缩放而缩放:

41.gif

这需要我们监听窗口变化事件,在窗口尺寸变化时同步调整该摄像机的 orthoHeight 值:

import { _decorator, Component, Camera, screen, view } from 'cc';
const { ccclass, property } = _decorator;

enum FitPolicy {
    FitWidth = 1,  // 适配宽度
    FitHeight = 2, // 适配高度
    FitBoth = 3,   // 适配宽度和高度
}

@ccclass('EntryComp')
export class EntryComp extends Component {
    @property({ type: Camera })
    cameraB: Camera | null = null;

     @property
    fitPolicy: FitPolicy = FitPolicy.FitBoth;

    // 设计分辨率
    designWidth = 1280;
    designHeight = 720;

    onEnable() {
        // 监听窗口变化事件
        view.on('canvas-resize', this.updateOrthoHeight, this);
        this.updateOrthoHeight();
    }

    onDisable() {
        view.off('canvas-resize', this.updateOrthoHeight, this);
    }

    updateOrthoHeight() {
        if (!this.cameraB?.orthoHeight) return;

        const dw = this.designWidth;
        const dh = this.designHeight;

        const frameSize = screen.windowSize;
        const ww = frameSize.width;
        const wh = frameSize.height;

        let orthoH = this.cameraB.orthoHeight;

        switch (this.fitPolicy) {
            case FitPolicy.FitWidth:
                // 修正:随窗口变小而变小(与 Canvas FitWidth 同向)
                orthoH = (wh / 2) * (dw / ww);
                break;
            case FitPolicy.FitHeight:
                orthoH = dh / 2;
                break;
            case FitPolicy.FitBoth: {
                const scale = Math.min(ww / dw, wh / dh);
                orthoH = (wh / 2) / scale;
                break;
            }
        }

        this.cameraB.orthoHeight = orthoH;
    }
}

此时再执行项目,所有内容均可自动适配窗口的变化:

42.gif