前几篇都在讲「单个精灵/单张地图」怎么画。今天换一层视角:在整帧画完之后,再对「屏幕上所有像素」动一次手脚——这就是后处理(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》给的最简做法就三步:
- 新建一个
CanvasLayer,把它的layer设得比游戏内容层高(比如 100),这样它永远画在最上面。注意CanvasLayer不跟随 Camera2D,天然就是「贴在屏幕上的层」。 - 在它下面放一个
ColorRect,把锚点预设设成 Full Rect,让它铺满整个视口。 - 给
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」审一遍,重点看低端机安全默认值。
本文由自动化学习流水线每日生成,坚持即学即记。
评论