《Cocos 游戏开发高级编程》3.3 后处理与全屏特效

后处理是把整帧渲染结果当输入纹理再算一遍。本节讲清 Cocos 3.8 的 PostProcess 与 BlitScreen 框架、如何写一个全屏灰度 Effect、如何在编辑器里搭起后处理链,并给出可编译的装载代码;同时算清全屏带宽、shadingScale 与多 pass 泛光的性能代价。

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(tag 3.8.8)完全一致。
  • strength 属性:0 是原图、1 是全灰,用 mix 插值。灰度权重 0.2126 / 0.7152 / 0.0722 是 Rec.709 亮度系数,与内置 IS_GRAY 用的是同一组。

3.3.4 在编辑器里搭一条后处理链

有了 Effect,把它挂到相机上。这一步需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现:

  1. 在场景里选中主相机节点;
  2. 添加组件 PostProcess,勾选 global(对整个相机生效);
  3. 再添加组件 BlitScreen——由于 @requireComponent(PostProcess),它不会重复添加 PostProcess;
  4. 新建一个材质 assets/materials/gray.mtl,Effect 选刚才的 builtin-gray-post;
  5. 把该材质拖进 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,tag 3.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 工程决策

后处理该不该上,判断链如下:

  1. **这个效果能不能在几何着色器里做?**能就做在那里——比如精灵染色,用 sprite.color 就够,不必上全屏后处理。
  2. **它需要访问「整幅画面」吗?**只有需要全局信息(全屏泛光、径向模糊、色调映射)时才值得付出全屏采样的代价。
  3. **它出现在什么设备上?**低端机把后处理按品质档关掉,或把 shadingScale 调低。沿用卷二 13.3 的帧预算(60 FPS → 16.6 ms/帧),一个全屏 pass 在低端机上可能就吃掉 2–3 ms,必须真机复测,编辑器里的数字不可信。
  4. 它会一直开着吗?后处理适合「过场、受击、低速状态」这类短时场景。常驻全屏滤镜要格外谨慎。

不该用的场合:给一个本就吃紧的项目再加「全屏泛光 + 全屏色调映射 + 全屏颗粒」三件套,往往是把帧预算直接打穿。后处理是「锦上添花」的一层,不是优化的手段——它只会让渲染更贵,不会更便宜。

第 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 光照与法线 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「cocos」更多文章

  1. 《Cocos 游戏联机实战》9.3 延迟补偿与体验权衡
  2. 《Cocos 游戏联机实战》9.2 网络状态 UI 与提示
  3. 《Cocos 游戏联机实战》9.1 同步表现与插值动画