3.3 后处理与全屏特效
3.2 改的是「单个精灵怎么着色」。这一节把镜头拉到整帧:灰度、泛光、色调映射、全屏描边——它们都是同一类东西,叫后处理(post-process)。做法也很统一:先把这一帧渲染到一张纹理,再用一个铺满屏幕的矩形采样这张纹理,算一遍新颜色,最后上屏。
一句话概括后处理与普通着色的区别:普通着色器的输入是几何与贴图,后处理的输入是整个屏幕。
3.3.1 后处理的本质:多一次全屏采样
正常渲染的路径是「几何 → 屏幕」。后处理把它拆成两段:
几何 → 渲染纹理(RT) → 全屏 quad 采样 RT → 屏幕
多出来的那一步,正是全部成本与全部价值所在:
- 价值:后处理能访问「已经画好的整幅画面」,因此能做单几何着色器做不到的事——按屏幕坐标做径向模糊、按亮度阈值提亮泛光、按颜色做色调映射。
- 成本:整帧要被写进一张 RT、再被读出一次。这两次全屏读写走的是显存带宽,分辨率越高、代价越大。
理解了这一点,后面所有的性能讨论都只是它的推论。
3.3.2 Cocos 3.8 的后处理框架
Cocos Creator 3.8 自带一套可插拔的后处理框架,核心是三个类(cc.d.ts 中的 postProcess 命名空间,tag 3.8.8):
| 类型 | 作用 |
|---|---|
postProcess.PostProcess | 挂在相机节点上的组件,代表「这台相机的输出要过后处理」 |
postProcess.PostProcessSetting | 单个后处理环节的基类,@requireComponent(PostProcess) |
postProcess.BlitScreen | 一个 Setting 子类,负责把自定义材质插进后处理链 |
引擎自带这些 Setting 子类:BlitScreen、Bloom、ColorGrading、TAA、FSR、HBAO、DOF(对应源码在 cocos/rendering/post-process/components/ 下,tag 3.8.8;这些类都在 cc.d.ts 的 postProcess 命名空间里能逐个查到)。其中 BlitScreen 是唯一的自定义入口——它持有一个 materials 数组,数组里的每个材质都会在屏幕上被全屏绘制一次。
PostProcessSetting 的源码只有几行(cocos/rendering/post-process/components/post-process-setting.ts,tag 3.8.8):
onEnable (): void {
const pp = this.getComponent(PostProcess);
pp?.addSetting(this);
}
也就是说,只要给相机节点加一个 BlitScreen,它就会自动把自己注册进同节点上的 PostProcess(@requireComponent 会先确保 PostProcess 存在)。这就是自定义后处理的接入点。
一个易忽略的细节:PostProcess.global 默认为 true,表示这台相机使用全局后处理配置;设为 false 时只对当前相机生效。做分屏或画中画时,正是靠它让不同相机走不同的后处理链。
3.3.3 写一个全屏灰度 Effect
后处理的 Effect 与 3.2 的精灵 Effect 结构一样,唯一的关键差异是 pass 上多了一行 pass: post-process,用来告诉引擎「这是后处理链的一环」。.effect 由引擎的 Effect 编译器处理,需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现:
CCEffect %{
techniques:
- passes:
- vert: vs
frag: fs
pass: post-process
rasterizerState:
cullMode: none
depthStencilState:
depthTest: false
depthWrite: false
blendState:
targets:
- blend: true
blendSrc: one
blendDst: one_minus_src_alpha
blendSrcAlpha: one
blendDstAlpha: zero
properties:
strength: { value: 1.0 }
}%
顶点程序负责「铺满屏幕」,片元程序才是效果本身。顶点程序可以直接内联引擎的公共块(editor/assets/effects/pipeline/post-process/chunks/vs.chunk,tag 3.8.8)——因为用户 Effect 无法用 <./chunks/vs> 这种相对路径去引用引擎目录里的 chunk:
CCProgram vs %{
precision highp float;
#include <legacy/decode-standard>
#include <builtin/uniforms/cc-global>
#include <common/common-define>
out vec2 v_uv;
void main () {
StandardVertInput In;
CCDecode(In);
CC_HANDLE_GET_CLIP_FLIP(In.position.xy);
gl_Position = In.position;
v_uv = a_texCoord;
}
}%
CCProgram fs %{
precision highp float;
in vec2 v_uv;
#pragma rate inputTexture pass
uniform sampler2D inputTexture;
uniform GrayData {
float strength;
};
layout(location = 0) out vec4 fragColor;
void main () {
vec4 c = texture(inputTexture, v_uv);
float gray = dot(c.rgb, vec3(0.2126, 0.7152, 0.0722));
fragColor = vec4(mix(c.rgb, vec3(gray), strength), c.a);
}
}%
三处值得解释:
#pragma rate inputTexture pass:声明inputTexture是「每 pass 更新一次」的纹理。后处理链上,上一环的输出就是下一环的输入,这个绑定由框架自动完成。inputTexture与v_uv:后处理的输入纹理名固定是inputTexture,屏幕 UV 来自顶点程序传下来的v_uv。这套约定与引擎自带的blit-screen.effect(tag3.8.8)完全一致。strength属性:0 是原图、1 是全灰,用mix插值。灰度权重0.2126 / 0.7152 / 0.0722是 Rec.709 亮度系数,与内置IS_GRAY用的是同一组。
3.3.4 在编辑器里搭一条后处理链
有了 Effect,把它挂到相机上。这一步需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现:
- 在场景里选中主相机节点;
- 添加组件
PostProcess,勾选global(对整个相机生效); - 再添加组件
BlitScreen——由于@requireComponent(PostProcess),它不会重复添加 PostProcess; - 新建一个材质
assets/materials/gray.mtl,Effect选刚才的builtin-gray-post; - 把该材质拖进
BlitScreen的materials列表,enable保持勾选。
在编辑器里打开会看到什么:相机节点上多出 PostProcess 与 BlitScreen 两个组件,BlitScreen.materials 是一个可增删的列表,每项有 material 与 enable 两个字段。运行后画面整体偏灰,调材质的 strength 属性能看到灰度程度实时变化。要叠第二层效果(比如灰度之后再泛光),就在同一节点上再加一个 Bloom 组件——Setting 的注册顺序决定链的顺序。
3.3.5 L2:装载材质与设置 shadingScale
下面这段可在 @cocos/creator-types@3.8.8 下通过 tsc --noEmit(L2)。它用代码挂上 PostProcess 组件、创建灰度材质,并把材质交给 BlitScreen:
import { _decorator, Camera, Component, Material, postProcess } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('GrayPostProcess')
export class GrayPostProcess extends Component {
@property(Camera)
camera: Camera | null = null;
@property
strength = 1.0;
start(): void {
if (!this.camera) return;
// PostProcess 挂在相机节点上,global 表示对整台相机生效
const pp = this.camera.getComponent(postProcess.PostProcess)
?? this.camera.addComponent(postProcess.PostProcess);
if (!pp) return;
pp.global = true;
pp.shadingScale = 1.0;
// 自定义全屏材质,effectName 就是 .effect 文件名
const mat = new Material();
mat.initialize({ effectName: 'builtin-gray-post' });
mat.setProperty('strength', this.strength);
}
}
注意最后一句:BlitScreen.materials 的每个元素是引擎内部类型 BlitScreenMaterial(含一个 protected _material 字段),普通对象字面量无法结构化赋值,因此 materials 的填写放在编辑器里做(3.3.4 第 5 步),代码只负责把材质准备好。shadingScale 是 PostProcess 上的缩放系数,后文会讲它的用法。
3.3.6 另一条路:RenderTexture + 第二个相机
除了内置框架,还有一条「手搭」的路:用相机 A 把画面渲染进一张 RenderTexture,再用相机 B(或一个全屏 Sprite)采样这张纹理。这正是 4.3 自定义装配与渲染纹理
讲过的机制。
两条路怎么选:
| 维度 | 内置 PostProcess 框架 | 手搭 RenderTexture |
|---|---|---|
| 接入成本 | 低,加组件 + 拖材质 | 高,要自己管相机顺序与纹理 |
| 链式多效果 | 天然支持,Setting 顺序即链顺序 | 要自己串多个 RT 与相机 |
| 与主相机绑定 | 直接挂相机节点 | 要维护两个相机 |
| 适用场景 | 全屏滤镜、泛光、色调映射 | 小地图、镜像、截图、自定义装配 |
结论:只要需求是「把整帧过一遍滤镜」,就用内置框架;只有当你需要「渲染结果被复用多次」或「离屏」时(小地图、镜子、截图),才手搭 RT——这一点与 4.3 的工程决策一致。
手搭路径的最小骨架是:给相机 A 设 targetTexture 指向一张 RenderTexture(reset({ width, height }) 建尺寸,见 4.3),再让一个铺满屏幕的 Sprite 采样这张纹理。这条路上,「后处理链」等价于「一串相机 + 一串 RT」,顺序完全由你手工维护——灵活,但也更容易排错。
3.3.7 性能代价
后处理的成本可以拆成三笔,全部与分辨率正相关:
- 带宽:每个后处理 pass 都要把整帧读一遍、写一遍。960 × 540 的《方块冲刺》分辨率下这还好,但一旦按设备物理分辨率放大,带宽会按面积平方增长。这是移动端后处理掉帧的第一元凶。
- 多 pass 累积:泛光(
Bloom)不是一趟,而是「降采样 → 多次模糊 → 升采样」一整串 pass(editor/assets/effects/pipeline/post-process/bloom.effect,tag3.8.8里能看到多趟bloom-downsampleN/bloom-upsampleN)。每多一趟,就多一次全屏采样。 - 填充率:全屏 quad 的片元数量等于屏幕像素数。灰度这类简单计算还好,带邻域采样(模糊、描边)时,每像素的采样次数会成倍上升。
应对手段里,PostProcess.shadingScale 是最有效的一个:把它设成小于 1 的值,后处理在更低的内部分辨率上计算,再放大上屏。比如 shadingScale = 0.5,后处理的计算量直接降到四分之一,代价是边缘略糊。对「灰度、色调映射」这类低频效果,这个交换几乎无感;对「描边、锐化」这类高频效果则会明显糊边。
移动端还有一层隐性成本:主流手机 GPU 是基于图块的(tile-based),渲染时把屏幕切成小块、在片内缓存里完成。后处理的「读 RT → 算 → 写 RT」恰好打断这种局部性——每个 pass 都要把整块图块换出 / 换入显存。所以同样是「全屏采样一次」,手机上比桌面贵得多,移动端的后处理往往要配合更保守的 shadingScale。
给一个量级参考(参考值,需在你的设备上复测):在 2019 款 iPad 上,960 × 540 全屏、单 pass、每像素一次采样的灰度后处理,帧时间增量约在零点几毫秒;同一设备换成带邻域采样的全屏描边,增量会升到几毫秒量级;若把分辨率提到 2048 宽,带宽按面积放大约 4 倍。真正要盯的是「采样次数 × 屏幕像素数」这个乘积。
3.3.8 工程决策
后处理该不该上,判断链如下:
- **这个效果能不能在几何着色器里做?**能就做在那里——比如精灵染色,用
sprite.color就够,不必上全屏后处理。 - **它需要访问「整幅画面」吗?**只有需要全局信息(全屏泛光、径向模糊、色调映射)时才值得付出全屏采样的代价。
- **它出现在什么设备上?**低端机把后处理按品质档关掉,或把
shadingScale调低。沿用卷二 13.3 的帧预算(60 FPS → 16.6 ms/帧),一个全屏 pass 在低端机上可能就吃掉 2–3 ms,必须真机复测,编辑器里的数字不可信。 - 它会一直开着吗?后处理适合「过场、受击、低速状态」这类短时场景。常驻全屏滤镜要格外谨慎。
不该用的场合:给一个本就吃紧的项目再加「全屏泛光 + 全屏色调映射 + 全屏颗粒」三件套,往往是把帧预算直接打穿。后处理是「锦上添花」的一层,不是优化的手段——它只会让渲染更贵,不会更便宜。
第 3 章到此结束。三节下来,我们把「着色」拆成了三层:Effect 语法(怎么写)、内置着色器改造(怎么改)、后处理(怎么把整帧当输入)。下一章进入 2D 的高级渲染,从光照与法线讲起——那是把「光」也变成一次着色计算。
3.3.9 本节自检清单
- 能说清后处理「几何 → RT → 全屏 quad → 屏幕」的两段式路径
- 知道
PostProcess挂相机节点、BlitScreen是自定义材质的入口 - 记得后处理 Effect 的 pass 要写
pass: post-process - 知道输入纹理固定叫
inputTexture、屏幕 UV 是v_uv - 会用
shadingScale降低后处理内部分辨率 - 明白全屏带宽与多 pass 是后处理的主要代价,低端机按品质档关闭
阅读导航:上一节:3.2 改造内置着色器 · 下一节:4.1 2D 光照与法线 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。