💡 本系列文章收录于个人专栏 ShaderMyHead。
💡 本文案例可以在 Github 上进行演示。
在某些场景可能需要创建多个摄像头 + RenderTexture 文件来叠加着色器效果,例如在《高斯模糊的实现》一文中,我们需要通过这种麻烦的操作来应用 gaussian-blur-vertical.mtl 材质,才能在原本应用了水平方位高斯模糊材质的基础上,进一步在垂直方位应用高斯模糊。
按照以往的方案,如果需要叠加 N 个着色器,我们就得创建 N 个摄像头、RenderTexture 文件、绘制前一个摄像头捕获画面的 SpriteFrame 节点,这会让操作变得麻烦,层级管理器上的节点也会变得杂乱和难以阅读,Draw Call 数量也会跟着变多。
Cocos Creator 提供了摄像机后处理(Post Process)的能力,通过该能力可以极大减少着色器应用的复杂度。
一、后处理功能的使用
假设场景上存在一个仅捕获 DEFAULT 层级的摄像机 CameraForDefault,和一张处于 DEFAULT 层级的绿叶背景图 BG 节点:
假设我们希望对绿叶背景图做 4 次高斯模糊(水平和垂直方向上各 2 次,且需要按顺序执行),并再染上 30% 透明度的绿色,这番操作会涉及三个着色器 —— 水平模糊着色器、垂直模糊着色器、染色着色器,它们的执行次序预期为:
水平模糊着色器 -> 垂直模糊着色器 -> 水平模糊着色器 -> 垂直模糊着色器 -> 染色着色器
按照以往的方案,我们需要额外创建 5 个摄像头用于生成 RenderTexture、5 个 SpriteFrame 节点用于缓存和绘制各步骤处理后的纹理,整个操作会很繁琐和低效。
启用摄像机的后处理功能,则仅需要新增一个节点用于绘制多个着色器处理后的画面。
1.1 启用后处理功能
后处理功能依赖于自定义渲染管线,在 Cocos Creator 3.8.6 中需要先在「项目设置 -> 图像设置 -> 新渲染管线」处确保勾选「后处理模块」,且必须填写一个自定义的管线名称(如下图所示,填写了一个叫 Customize 的名称):
接着我们勾选上摄像机属性检查器界面的 Use Post Process,表示对此摄像机启用自定义的后处理管线:
💡 Cocos Creator 官方目前暂停了对后处理模块的维护(因此标记为废弃),不排除未来会调整后处理功能的使用形式。
💡 Cocos Creator 官方也提供了自定义渲染管线新方案(实验性质)的 Demo,个人研究后不推荐使用(需要自行开发组件脚本,使用起来比较复杂,且极容易报错)。
目前我们已经为 CameraForDefault 摄像机启用了后处理管线能力,但还需要告诉游戏引擎「应该把该摄像机拍摄的画面交给谁来处理」。
1.2 新增后处理节点并绑定摄像机
我们再新增一个位于 DEFAULT 层级的空节点 PostProcessForDefault,它将用于绘制后处理管线处理后的最终画面。
该节点需要添加专门的组件,才能接收和处理来自指定摄像机捕获的离屏画面。
我们为其添加名为 BlitScreen 的组件(该组件依赖于 PostProcess 组件,因此会同步添加上 PostProcess 组件),在 BlitScreen 的组件的 Materials 处,可以自行新增多个材质插槽:
💡
BlitScreen主要负责维护一个有序的后处理材质列表,PostProcess组件作为后处理管理器,为每个摄像机提供对应的后处理实例配置。在渲染流程中,
PostProcess会获取摄像头捕获的画面,将其交由BlitScreen维护的材质列表进行链式依序处理,每个材质的输出作为下一个材质的输入,最终形成完整的后处理效果链。
留意去掉 PostProcess 组件的 Global 选项,若勾选了表示它会作为全局默认的后处理设置,将应用于所有开启了后处理功能但未绑定 PostProcess 节点的摄像机。
最后我们将该节点拖入 CameraForDefault 摄像机的 Post Process 处进行绑定:
这样 Cocos Creator 会把摄像机捕获的离屏画面交给 PostProcessForDefault 节点的后处理组件去处理。
1.3 后处理着色器
后处理管线使用的着色器语法与我们之前编写的基本一致,但有几个关键点需要留意:
- CCEffect 的
passes中必须声明pass: post-process,表示该着色器通道(pass)的类型为后处理通道; cc_time、cc_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.mtl、ver-blur.mtl、green-screen.mtl 三个对应的材质,并将它们拖入 BlitScreen 组件的材质插槽:
在 Cocos Creator 编辑器中执行项目,通过控制各材质下的 Enable 选项,可以决定是否启用该材质:
可以看到后处理管线会将 BlitScreen 组件中的着色器材质依序逐个应用,最终得到了我们想要的画面处理效果:
水平模糊 -> 垂直模糊 -> 水平模糊 -> 垂直模糊 -> 染上30%绿色
二、多摄像机后处理
若场景存在多个摄像机,每个摄像机可以走同样的方式启用互相独立的后处理管线。
2.1 启用后处理功能
我们新增一个 CameraForUI2D 摄像机用于拍摄 UI_2D 层级的内容,以及一个 PostProcessForUI2D 节点。
摄像机勾选 Use Post Process 后,将 PostProcessForUI2D 节点绑定到摄像机的 Post Process 处:
同时新增一个层级位于 UI_2D 的 Label(只会被 CameraForUI2D 摄像机捕获):
我们新建一个着色器和对应的材质,来给这个 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 组件并绑定对应的材质:
此时新增的摄像机也拥有了自己独立的后处理管线。
2.2 启用 Blending
在上述步骤完成后执行项目,会发现 Label 确实染上了 30% 的红色,但下层 PostProcessForDefault 的画面被黑色完全覆盖了:
这是由于启用了后处理的摄像机最终会走到一个名为 post-final 的后处理 Pass,该 Pass 对应的着色器文件内部没有开启 Blending 混合模式,导致在与背景色混合时不会开启 Alpha 通道。
我们可以在资源管理器里搜索 post-final.effect 找到该内置的着色器文件:
打开后加入 blendState 配置,启用透明度混合功能:
保存修改后再执行,CameraForDefault 摄像机所捕获并后处理过的画面(即 PostProcessForDefault 节点),就能被作为背景色混合并展示出来:
💡
post-final.effect仅会在摄像机开启usePostProcess时才被使用到,故其修改对整体影响不大(值得修改)。
2.3 适配窗口变化
由于新增的摄像机不被 Canvas 绑定,其离屏幕渲染的内容无法套用画布的宽高适配策略,具体表现为该摄像机所拍摄的画面不会跟随窗口的缩放而缩放:
这需要我们监听窗口变化事件,在窗口尺寸变化时同步调整该摄像机的 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;
}
}
此时再执行项目,所有内容均可自动适配窗口的变化: