我们已经有了会走会跳的角色、会追人的敌人、会呼吸的 2D 光照。但真上手做,你会发现一个尴尬:AI 生成的素材是「静止的平面图」。怪被打中不闪、道具吃完直接消失、可交互物品和背景糊在一起看不出区别。靠逐帧画受击图(AI 也画不出一致的)成本高到劝退。而 shader 能在不改一张图的前提下,让同一张素材变出十种状态——这是独立开发者最划算的特效杠杆。
今日速览
- Shader 是「改像素的公式」,不是新素材。同一张 PNG,配不同 shader 就是不同表现,美术量直接省掉一半。
- 2D 的一切都走
canvas_item:Sprite2D、AnimatedSprite2D、TileMapLayer、Control 全部适用。 - 主战场是
fragment():读TEXTURE+UV,算完写进COLOR,就这一件事。 uniform是给策划/自己的旋钮:写uniform float flash : hint_range(0.0, 1.0) = 0.0;,Inspector 里就出现滑条,代码里set_shader_parameter()可实时改。- 两个坑先记住:
TIME受Engine.time_scale影响但不受暂停影响;shader 里局部变量不会自动初始化为 0。
核心学习内容
一、一个 .gdshader 文件长什么样
Godot 4 的 shader 是独立资源文件(.gdshader),核心就三段:
shader_type canvas_item; // 2D 固定用这个
uniform float flash : hint_range(0.0, 1.0) = 0.0;
void fragment() {
vec4 tex = texture(TEXTURE, UV); // 取这一像素的原图颜色
COLOR = tex; // 写回输出
}
fragment() 是逐像素执行的函数。你只干一件事:算出这个像素最终该是什么颜色,赋给 COLOR。默认什么都不写,COLOR 就等于原图,所以「空 shader」是安全的。
用法:在 Sprite2D 的 Material 属性里新建 ShaderMaterial → Shader 指向这个文件。之后节点上的 modulate 依然生效(引擎会把它乘进 COLOR),这点别搞混。
二、受伤闪白:三行代码,零美术成本
最经典也最实用的一个。原理是「按比例把 RGB 往目标色推」,但必须保留 alpha,否则整个精灵变实心方块:
shader_type canvas_item;
uniform vec4 flash_color : source_color = vec4(1.0);
uniform float flash_amount : hint_range(0.0, 1.0) = 0.0;
void fragment() {
vec4 tex = texture(TEXTURE, UV);
tex.rgb = mix(tex.rgb, flash_color.rgb, flash_amount);
COLOR = tex; // 注意:只改 rgb,alpha 保持原样
}
GDScript 侧配合一个 Tween 就能做出「闪一下再退回去」:
func hit_flash() -> void:
var mat := material as ShaderMaterial
mat.set_shader_parameter("flash_amount", 1.0)
create_tween().tween_method(
func(v: float): mat.set_shader_parameter("flash_amount", v),
1.0, 0.0, 0.12)
source_color 这个 hint 很关键,它让 Inspector 用取色器显示而不是四个数字。同一份 shader 换个 flash_color 就是中毒(绿)、冰冻(蓝)、暴击(黄)——一套逻辑通吃。
三、噪声溶解:让「消失」有过程
道具被吃掉、敌人死亡,直接 queue_free() 太生硬。溶解的原理是:拿一张噪声图采样,噪声值低于阈值就丢弃该像素,阈值从 0 涨到 1,物体就一点点化掉。
shader_type canvas_item;
uniform sampler2D noise_tex : repeat_enable;
uniform float dissolve : hint_range(0.0, 1.0) = 0.0;
uniform vec4 edge_color : source_color = vec4(1.0, 0.5, 0.1, 1.0);
uniform float edge_width : hint_range(0.0, 0.2) = 0.05;
void fragment() {
vec4 tex = texture(TEXTURE, UV);
float n = texture(noise_tex, UV).r;
if (n < dissolve) {
discard; // 这块像素不画
}
// 阈值附近画一圈亮边,制造「烧灼」感
float edge = smoothstep(dissolve, dissolve + edge_width, n);
tex.rgb = mix(edge_color.rgb, tex.rgb, edge);
COLOR = tex;
}
三个易踩的点:噪声图要设 repeat_enable,否则 UV 边缘会断裂;discard 会丢掉 alpha 混合,所以溶解中的物体不要叠大量半透明;smoothstep 的边界比 step 好看一个档次,别省。
四、像素描边:让可交互物「自己跳出来」
可拾取物最难的问题是「玩家看不见」。描边不用美术画,采样自身四周的 alpha 就行:
shader_type canvas_item;
uniform vec4 outline_color : source_color = vec4(1.0, 0.9, 0.3, 1.0);
uniform float width : hint_range(1.0, 8.0) = 2.0;
void fragment() {
vec4 tex = texture(TEXTURE, UV);
vec2 px = TEXTURE_PIXEL_SIZE * width;
float a = 0.0;
a = max(a, texture(TEXTURE, UV + vec2( px.x, 0.0)).a);
a = max(a, texture(TEXTURE, UV + vec2(-px.x, 0.0)).a);
a = max(a, texture(TEXTURE, UV + vec2(0.0, px.y)).a);
a = max(a, texture(TEXTURE, UV + vec2(0.0, -px.y)).a);
// 自己透明、邻居不透明 → 这里是轮廓
if (tex.a < 0.1 && a > 0.5) {
COLOR = outline_color;
} else {
COLOR = tex;
}
}
TEXTURE_PIXEL_SIZE 是「1 / 贴图尺寸」,所以 width 的单位是像素而不是 UV,改起来直观。想更细腻就把四方向扩成八方向,代价是多 4 次采样——对 2D 完全无所谓。
性能上提醒一句:ShaderMaterial 是按材质合批的。给 30 个敌人各建一个 ShaderMaterial,就是 30 个 draw call;共用同一个材质、把差异放进 modulate 或顶点色,才是正确做法。
🎯 DeepSeek 提示词
提示词 1 · 从「我想要的效果」生成可运行 shader
你是 Godot 4 的 2D 着色器专家,熟悉 canvas_item shader 的内置变量
(TEXTURE / UV / COLOR / TEXTURE_PIXEL_SIZE / TIME / SCREEN_UV)。
任务:为 Godot 4.x 写一个 canvas_item 着色器,实现【效果描述,例如:
角色受击时整体闪白并轻微向外发光】。
约束:
1. 只输出一个完整的 .gdshader 文件内容,shader_type 必须是 canvas_item。
2. 所有可调参数用 uniform 暴露,带类型 hint:
颜色用 source_color,0~1 的用 hint_range(0.0,1.0)。
3. 必须保留原图 alpha,不得让透明区域变成实心。
4. 关键行加中文注释,说明为什么这么写。
5. 最后附 5 行 GDScript,演示如何用 set_shader_parameter() 在运行时驱动它。
输出格式:先代码块,再一段 ≤100 字的「接入步骤」(挂在哪个节点、Material 怎么建)。
提示词 2 · 让 DeepSeek 审你的 shader(找错专用)
你是 Godot 4 着色器代码审查员。下面是我的 shader 和它表现出的错误现象。
【shader 代码】<粘贴>
【现象】<例如:整个精灵变成纯白方块 / 边缘有黑边 / 溶解时闪烁>
【节点类型】Sprite2D / AnimatedSprite2D / TileMapLayer(选一个)
请按顺序检查并给出结论:
1. 是否误改了 alpha 通道,导致透明区被填实?
2. 采样是否越界?UV 偏移是否缺少 repeat_enable 或 clamp 设置?
3. 是否有未初始化的局部变量(Godot shader 不会自动置 0)?
4. 是否把应该用 mix()/smoothstep() 的地方写成了硬 if 判断,造成锯齿或突变?
5. 是否使用了当前渲染后端不支持的写法?
输出格式:表格列出「问题 / 原因 / 修改后的代码行」,最后给一份完整修正版 shader。
不要重写我没问的部分。
提示词 3 · 一次生成「一套状态特效」而不是一个
你是独立游戏的 2D 特效技术美术。我要给游戏里的角色做一个统一的受击/状态
特效系统,用同一份 canvas_item 着色器驱动,避免为每个状态单独做素材。
输入:角色是 128x128 的像素风 Sprite2D,已有 AnimatedSprite2D 动画。
需要支持的状态:普通、受击闪白、中毒(绿色脉动)、冰冻(蓝色 + 轻微错位)、
死亡(噪声溶解)。
要求:
1. 设计一份 shader,用 uniform 暴露:flash_color、flash_amount、tint_color、
dissolve_amount、noise_tex、wobble_strength。
2. 说明为什么把多个效果合到一个 shader 而不是拆成四个(合批 / draw call 角度)。
3. 给出 GDScript 的封装:一个 apply_state(state: String) 方法,用 match 切换参数,
并用 Tween 做 0.1~0.3 秒的过渡,不要瞬切。
4. 指出这套方案在「多个敌人同屏」时的性能注意点。
输出:shader 全文 + GDScript 全文 + 不超过 5 条的注意事项。
学习心得
今天最大的认知转变是:shader 不是「高手才碰的东西」,它是「美术不够时的替代方案」。我们这条路线的核心矛盾是——一个人 + AI 出图,做不出一致性强的多状态素材。AI 能给你一张漂亮的待机图,但你让它画「同一角色被击中瞬间」,它给你的往往是另一个角色。
而 shader 把这个矛盾绕开了:素材只需要一张,状态由公式生成。闪白、溶解、描边、色调、扭曲,全都是对同一张图做数学变换,天然风格统一。这对「用 DeepSeek 做一款 Steam 游戏」意义很直接——DeepSeek 写 shader 的准确率远高于写复杂游戏逻辑(语法小、可验证、报错明确),你完全可以让它生成、你只负责粘进引擎看效果。这也是我建议的下一阶段主力工作方式:逻辑靠拆解,表现靠 shader。
明日关注
- 把「受伤闪白」接到昨天的输入/战斗流程里,验证 Tween 与
set_shader_parameter的时序是否冲突。 - 试
hint_screen_texture+SCREEN_UV,做一个全屏后处理(低血量红边 / 命中时轻微色差)。 - 查 Godot 官方《Shaders style guide》,把今天三个 shader 的命名与注释规范统一,方便后续让 DeepSeek 复用。
本文由自动化学习流水线每日生成,坚持即学即记。
评论