前几篇都在讲「单个精灵/单张地图」怎么画。今天换一层视角:在整帧画完之后,再对「屏幕上所有像素」动一次手脚——这就是后处理(Post-Processing)。暗角、色差、CRT 扫描线、受击红屏,都属于它。它是独立游戏拉「完成度」最便宜的一招:代码量不大,观感却是质变。

今日速览

  • 后处理的本质:Godot 先把这一帧(游戏内容 + UI)渲染完,再让一个铺满全屏的 ColorRect 把结果「读回来」重新着色输出。所以它不改变游戏逻辑,只改「观众看到的画面」。
  • 三层结构:CanvasLayer(给一个够高的 layer,压在最上面)→ ColorRect(锚点设 Full Rect)+ ShaderMaterial(挂 shader_type canvas_item 的 Shader)。
  • 采样靠两个内置量:uniform sampler2D screen_texture : hint_screen_texture 拿「已渲染的画面」,SCREEN_UV 拿「当前像素在屏幕上的坐标」。SCREEN_PIXEL_SIZE 则告诉你「一个屏幕像素占地多少 UV」,是像素级效果的关键。
  • 一个必踩的坑:2D 里 hint_screen_texture 只在第一次被某节点用到时做一次全屏拷贝;如果多个用到它的 shader 重叠,第二个读到的不是第一个的结果。要串联就用 BackBufferCopy 节点。
  • 性能红线:全屏 shader 是「每个像素、每帧」运行——它很爽,但别在里面写昂贵循环。低端机/Steam Deck 上尤其要克制。

核心学习内容

1. 为什么是「CanvasLayer + ColorRect + Shader」三层

Godot 官方《Custom post-processing》给的最简做法就三步:

  1. 新建一个 CanvasLayer,把它的 layer 设得比游戏内容层高(比如 100),这样它永远画在最上面。注意 CanvasLayer 不跟随 Camera2D,天然就是「贴在屏幕上的层」。
  2. 在它下面放一个 ColorRect,把锚点预设设成 Full Rect,让它铺满整个视口。
  3. 给 ColorRect 新建 ShaderMaterial → 新建 Shader(shader_type canvas_item),在 fragment() 里读 screen_texture、算完再写回 COLOR。

关键理解:这块 ColorRect 本来什么颜色都不该有,它只是一个「全屏的着色器载体」。你在 fragment 里把采样到的画面处理一遍再输出,就等于给整帧加了一层滤镜。

最小可用骨架(能跑,但画面等于原样):

shader_type canvas_item;

uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest;

void fragment() {
    COLOR = textureLod(screen_texture, SCREEN_UV, 0.0);
}

2. 读懂 screen_texture / SCREEN_UV / SCREEN_PIXEL_SIZE

  • SCREEN_UV 是屏幕空间的 UV(0~1)。你「输出像素的位置」其实就是 SCREEN_UV,用它可以反推「该去原画面的哪里取色」。做暗角、色差全靠它。
  • SCREEN_PIXEL_SIZE = vec2(1.0/屏幕宽, 1.0/屏幕高),即「一个像素有多宽」。做扫描线、像素化时用它把 UV 换算成像素单位。
  • textureLod(tex, uv, lod) 的第三个参数是 mipmap 级别:0.0 是原图;把 hint 改成带 mipmap 的过滤(如 filter_linear_mipmap),再用 lod=2.0 就能白嫖一张模糊图——柔光、Bloom 感、暗角软化都靠它,比手写高斯模糊便宜得多。

BackBufferCopy 的坑:文档明确说,2D 中当第一个用到 hint_screen_texture 的节点被绘制时,Godot 才拷贝一次全屏;后续节点不再复制。所以当你有两层后处理叠加(比如「全局暗角」+「受伤红屏」),第二个会读到没经过第一个处理的画面,出现「效果互相看不见」。解法:在两者之间插一个 BackBufferCopy 节点,强制再拷贝一次。

3. 四个能直接抄的全屏效果

(a) 暗角 Vignette——根据像素到屏幕中心的距离压暗:

shader_type canvas_item;
uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest;
uniform float vignette_strength : hint_range(0.0, 2.0) = 0.6;
uniform float vignette_softness : hint_range(0.1, 1.5) = 0.6;

void fragment() {
    vec3 c = textureLod(screen_texture, SCREEN_UV, 0.0).rgb;
    float d = distance(SCREEN_UV, vec2(0.5)) / vignette_softness;
    float v = smoothstep(0.4, 1.0, d) * vignette_strength;
    COLOR.rgb = mix(c, c * 0.25, clamp(v, 0.0, 1.0));
}

(b) 色差 Chromatic Aberration——R/G/B 用稍微不同的偏移采样,做出「镜头/故障感」:

uniform float ca_amount : hint_range(0.0, 0.02) = 0.003;
void fragment() {
    vec2 dir = SCREEN_UV - vec2(0.5);
    float r = textureLod(screen_texture, SCREEN_UV + dir * ca_amount, 0.0).r;
    float g = textureLod(screen_texture, SCREEN_UV, 0.0).g;
    float b = textureLod(screen_texture, SCREEN_UV - dir * ca_amount, 0.0).b;
    COLOR = vec4(r, g, b, 1.0);
}

(c) CRT 扫描线 Scanlines——用 SCREEN_PIXEL_SIZE.y 把 UV 换成像素行:

uniform float scanline_density = 260.0; // 屏幕共多少条线
uniform float scanline_strength : hint_range(0.0, 1.0) = 0.18;
void fragment() {
    vec3 c = textureLod(screen_texture, SCREEN_UV, 0.0).rgb;
    float line = sin(SCREEN_UV.y * scanline_density * 3.14159) * 0.5 + 0.5;
    c *= 1.0 - line * scanline_strength;
    COLOR.rgb = c;
}

(d) 受击红屏 Damage Vignette——用一个 uniform 控制红色暗角强度,从 GDScript 侧驱动:

uniform float damage : hint_range(0.0, 1.0) = 0.0;
void fragment() {
    vec3 c = textureLod(screen_texture, SCREEN_UV, 0.0).rgb;
    float d = distance(SCREEN_UV, vec2(0.5));
    float edge = smoothstep(0.25, 0.75, d);
    c = mix(c, vec3(0.6, 0.0, 0.0), edge * damage);
    COLOR.rgb = c;
}

在玩家受伤时,用 Tween 把 damage 从 1.0 迅速拉回 0.0,就是一次天然的红屏反馈:

func flash_damage() -> void:
    var mat := $CanvasLayer/ColorRect.material as ShaderMaterial
    mat.set_shader_parameter("damage", 1.0)
    create_tween().tween_method(
        func(v): mat.set_shader_parameter("damage", v), 1.0, 0.0, 0.35)

4. 性能与军规

  • 每像素成本是真金白银:全屏后处理在 1080p 下每帧要跑 200 万次 fragment。能用 textureLod 的 mipmap 模糊就别手写多重采样;能一个 pass 解决就别拆三个(除非像高斯模糊这种「多 pass 反而更快」的情况)。
  • 层次要想清楚:CanvasLayer 的 layer 决定它压在谁上面。如果你把后处理层放到 UI 之上,UI 也会被一起滤镜(有时是想要的「整体电影感」,有时会让文字发虚)——建议后处理层与 UI 层分开排布。
  • 拉伸模式有影响:项目用 canvas_items 拉伸时,SCREEN_UV 是屏幕空间,效果不受内部分辨率影响;用 viewport 拉伸则要注意像素化和缩放叠加。做像素游戏时和「像素完美渲染」一起考虑。
  • 给参数加 hint_range:像上面那样用 uniform float x : hint_range(0.0, 1.0) = 0.5,编辑器里就出现滑条,美术/策划可以自己调,不用改代码。

🎯 DeepSeek 提示词

提示词 1:让 DeepSeek 一次性写出可调参的全屏后处理

适用场景:想给游戏加一层「电影感/复古感」滤镜,但不想自己从 shader 语法抠起。

你是 Godot 4 图形程序员,精通 canvas_item 着色器与全屏后处理。

【任务】写一个 Godot 4 全屏后处理方案,包含:
1. 场景结构说明:CanvasLayer(layer=100)+ 铺满的 ColorRect(Full Rect 锚点)+ ShaderMaterial 该如何搭
2. 一个 shader_type canvas_item 的 fragment 着色器,同时实现三种效果并各自可调:
   - 暗角(vignette):参数 strength、softness
   - 色差(chromatic aberration):参数 amount
   - 扫描线(scanlines):参数 density、strength
3. 所有参数用 `uniform float 名 : hint_range(最小值, 最大值) = 默认值` 声明,方便编辑器调参

【约束】
- 用 hint_screen_texture + SCREEN_UV + textureLod 读取屏幕,不要用已废弃的 SCREEN_TEXTURE
- 每个 uniform 后写一行中文注释说明「调大/调小会发生什么」
- 不要解释原理,直接给可复制粘贴的代码块

提示词 2:让 DeepSeek 把后处理接到游戏事件上

适用场景:已经有后处理 shader,想让「受击/低血量/死亡」时画面有反馈。

你是 Godot 4 游戏程序员。我有一块挂了 ShaderMaterial 的全屏 ColorRect(后处理层),
shader 里有一个 uniform:`uniform float damage : hint_range(0.0, 1.0) = 0.0;`

【任务】写一段 GDScript(Godot 4),实现以下「屏幕反馈」逻辑:
1. 玩家受伤时:damage 瞬间拉到 1.0,0.35 秒内平滑回落到「当前血量越低、残留值越高」的基线
   (血量 <30% 时基线约 0.25,否则为 0)
2. 玩家死亡时:damage 保持在 0.8,并叠加一次 0.6 秒的慢速色差脉冲
3. 提供 damage 的基线随血量变化的映射函数(输入血量百分比 0~1,输出 0~0.3)

【约束】
- 用 Tween(create_tween)做插值,不要用 _process 里手写计时器
- 假设节点路径为 $CanvasLayer/ColorRect,material 是 ShaderMaterial
- 注意重复触发时先 kill 上一条 Tween,避免叠加
- 给出「如何用信号连接到玩家受伤」的一行示例

提示词 3:让 DeepSeek 审查一个后处理 shader 的性能与正确性

适用场景:效果做出来了但掉帧,或画面在某些情况下不对,需要一双「审稿的眼睛」。

你是 Godot 4 渲染性能专家。下面是我的全屏后处理 shader:

<在此粘贴你的 shader 完整代码>

【任务】给出一份审查报告:
1. 逐行指出「每片段成本高」的地方(多余的 texture 采样、可省略的运算、可合并的 pass),
   并给出优化后的代码
2. 检查 screen_texture / SCREEN_UV / SCREEN_PIXEL_SIZE 的用法是否正确,
   有没有踩「2D 里 screen texture 只拷贝一次、需要 BackBufferCopy」的坑
3. 判断是否值得拆成多 pass(例如模糊类效果),若值得,给出 pass 拆分方案
4. 列出 3 个「在 Steam Deck 等低端机上最容易翻车」的参数,并建议安全默认值

【约束】
- 只针对 Godot 4(不要给 Godot 3 的 SCREEN_TEXTURE 写法)
- 优化建议要给出可直接替换的代码,而不是空泛描述
- 输出用「问题 → 影响 → 修法」的表格

学习心得

后处理这件事,最能体现「用 AI 做游戏」的正确姿势:它属于「逻辑简单、观感收益巨大、但语法门槛劝退」的典型任务。一个独立开发者手写 CRT 扫描线、色差、暗角的 shader,可能要查一下午 GLSL;但在 DeepSeek 面前,你只要把「想要什么效果 + 什么参数可调 + 用什么 API」讲清楚,代码基本一次就对。

关键是把需求拆成 AI 能接住的结构:说清「哪个节点挂什么、采样用什么内置量、参数留哪几个旋钮、约束是什么(Godot 4、不要废弃 API、用 Tween)」。这恰好也是这个系列一直在练的能力——不是让 AI 替你想创意,而是让 AI 替你把已经想清楚的东西翻译成引擎能跑的代码。

对「做一款能上架 Steam 的游戏」来说,后处理是性价比最高的打磨层:它不增加玩法,却能立刻让截图、预告片、商店页看着「像个成品」。今天这套暗角 + 色差 + 红屏,几乎可以原样塞进任何 2D 项目里。

明日关注(Checklist)

  • 在自己项目里搭一个 CanvasLayer(layer=100) + ColorRect(Full Rect) + ShaderMaterial,先跑通「原样输出」这个最小骨架,确认 UI 与控制都能正常工作。
  • 用「提示词 1」生成一版三合一 shader,把 vignette_strength / ca_amount / scanline_strength 三个旋钮分别从 0 调到最大,肉眼记住各自的手感。
  • 用「提示词 2」把受击红屏接进战斗,注意先 kill 旧 Tween;再用「提示词 3」审一遍,重点看低端机安全默认值。

本文由自动化学习流水线每日生成,坚持即学即记。