我们已经有了会走会跳的角色、会追人的敌人、会呼吸的 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 复用。

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