🎨 Shader 核心数学模式与实战指南 (The Math of Shaders)
Shader 编程的本质不是写代码,而是数学建模。 我们要做的就是把光照、纹理、时间等输入,通过数学公式,映射为屏幕上的每一个像素颜色。 本文档总结了游戏开发(特别是 Roguelike/塔防类)中最常用的 Shader 效果及其背后的理论公式。1. 基础工具箱:核心函数 (The Toolkit)
在 Shader Graph 或 HLSL 中,以下 5 个函数构成了 90% 的特效基础。1.1 saturate(x) —— 安全钳制
- 公式:
- 作用: 确保数值永远在 0 到 1 之间。
- 为什么重要: 颜色、透明度、UV 坐标通常都不应超过 1 或低于 0。任何光照计算后都建议加一个 saturate。
1.2 lerp(a, b, t) —— 线性插值
- 公式:
- 作用: 在 a 和 b 之间混合。
- 几何直觉: 当 t=0 是 a,t=1 是 b,t=0.5 是中间。
- 应用: 颜色混合、纹理混合、受击闪白 (
lerp(BaseColor, White, FlashStrength)).
1.3 step(edge, x) & smoothstep(min, max, x) —— 边缘与过渡
- step: 硬切。如果 返回 0,否则返回 1。
- 应用: 溶解效果的硬边缘、卡通渲染的色阶。
- smoothstep: 平滑过渡。在 min 和 max 之间生成一条 S 型曲线 (Hermite Interpolation)。
- 应用: 软边缘、抗锯齿、能量护盾的边缘衰减。
1.4 frac(x) —— 周期循环
- 公式: (取小数部分)
- 图像: 锯齿波 (0 -> 1, 0 -> 1…)
- 应用: 制作条纹 (Stripes)、时间循环 (
frac(_Time.y * speed)).
1.5 pow(x, p) —— 强度控制 (Gamma 曲线)
- 作用:
- : 曲线下凹,暗部更暗,高光更集中(锐化)。
- : 曲线上凸,整体变亮(柔化)。
- 应用: 菲涅尔边缘光的宽度控制、高光范围控制。
2. 经典效果详解 (The Cookbook)
2.1 菲涅尔效应 (Fresnel / Rim Light)
视觉: 物体边缘发光(幽灵、能量盾、选中高亮)。理论: 当视线方向 () 与表面法线 () 垂直时,反射最强。 核心公式:
- 参数:
- : World Normal (世界法线)
- : View Direction (视线方向,Camera 位置 - 像素位置)
- : 越接近 1 代表正对着相机(中心),越接近 0 代表垂直相机(边缘)。
- : Power (指数),控制边缘光的宽窄。P 越大,光越细。
2.2 溶解/燃烧 (Dissolve / Burn)
视觉: 物体像纸烧焦一样消失,边缘有亮光。理论: 使用一张噪点图 (Noise) 作为高度图,切掉 (Clip) 低于阈值的像素。
-
核心逻辑:
- 采样:
- 裁剪:
- 边缘光: 处于裁剪边缘的像素 () 上色。
- 这表示:比阈值高一点点的那部分区域,返回 1 (发光),其余为 0。
2.3 UV 流动与扭曲 (UV Scrolling & Distortion)
视觉: 滚动的岩浆、流动的水面、被热浪扭曲的背景。理论: 在采样纹理之前,修改 UV 坐标。 UV 滚动 (Scrolling): UV 扭曲 (Distortion): 使用另一张噪点图来干扰当前的 UV。
- 解释: 就像透过凹凸不平的玻璃看东西,看到的像素位置被偏移了。
2.4 受击闪白 (Hit Flash)
视觉: 怪物受击时瞬间变全白。理论: 简单的颜色插值,不涉及光照。 公式:
- 注意:
FlashStrength通常由 C# 脚本控制协程或 AnimationCurve 来驱动 (0 -> 1 -> 0)。
2.5 UI 去色 (Grayscale)
视觉: 技能冷却时图标变黑白。理论: 人眼对绿色的敏感度最高,对蓝色最低。不能简单平均 RGB。 亮度公式 (Luma): 可控饱和度:
3. 顶点动画理论 (Vertex Displacement)
除了改变颜色(像素着色器),我们还可以改变形状(顶点着色器)。 性能优势: 计算量取决于顶点数,远少于像素数。适合大面积草地、水面波浪。3.1 简单的旗帜飘动 (Sine Wave)
理论: 将顶点的 Y 轴高度作为输入,用正弦波偏移 X 轴或 Z 轴。 公式:Vertex.y作为相位偏移,确保旗帜不同高度的摆动不同步。
3.2 呼吸效果 (膨胀)
理论: 沿法线方向移动顶点。 公式:- 应用: 史莱姆怪物的呼吸、选中目标时的脉冲框。
4. 常见渲染模式 (Rendering Modes)
理解这些决定了你的 Shader 能做什么,不能做什么。| 模式 | 描述 | 深度写入 (ZWrite) | 渲染顺序 | 适用场景 |
|---|---|---|---|---|
| Opaque (不透明) | 最快。从前向后渲染 (利用 Early-Z 剔除)。 | On | 2000 | 角色、墙壁、地面 |
| Cutout (镂空) | 要么全透要么不透。硬边缘。 | On | 2450 | 草丛、铁丝网、溶解效果 |
| Transparent (半透明) | 最慢。从后向前渲染 (画家算法)。不能写深度。 | Off | 3000 | 玻璃、特效粒子、UI |
⚠️ 透明度排序问题: 半透明物体如果不写入深度,经常会出现“在这个角度看是对的,转个角度就穿帮了”的问题。这是计算机图形学的经典难题。解决方法通常是:少用半透明,或者接受瑕疵。
5. 性能优化 (Optimization)
- 避免
if-else吗?- 现代 GPU 对分支预测已经做得很好。但如果是复杂的逻辑分支,且不同像素走向不同分支(Divergency),依然会降速。
- 技巧: 尽量用
step或lerp代替if。- Bad:
if (x > 0.5) col = white; else col = black; - Good:
col = lerp(black, white, step(0.5, x));
- Bad:
- 纹理采样 (Texture Fetch) 是昂贵的:
- 尽量利用纹理的 RGBA 四个通道。比如 R 放噪点,G 放遮罩,B 放高光强度。不要为了一个遮罩单独读一张图。
- 浮点精度:
- 在移动端 (Mobile),
float(32bit) 比half(16bit) 慢且费电。 - 位置、UV 用
float。 - 颜色、法线、方向通常用
half足够。
- 在移动端 (Mobile),
6. 2D Sprite 专用特效 (Sprite Magic)
在 2D 或 2.5D 游戏中,Sprite 的处理逻辑与 3D 物体不同。我们通常处理的是MainTex (Sprite 图集) 和 Color (顶点色)。
6.1 2D 描边 (Outline)
视觉: 角色边缘有一圈亮色轮廓(选中效果)。理论: 检测当前像素周围是否是透明像素。如果我是透明的,但我旁边有不透明的像素,那我就是“外轮廓”。 采样公式 (十字采样法): 取上下左右 4 个点的 Alpha 值累加。
- 优化: 仅仅采样 4 次可能不够平滑,高质量描边通常采样 8 次(米字型)。
6.2 2D 投影/斜切 (2D Planar Shadow)
视觉: 2D 角色脚下有一个倾斜的黑色影子。理论: 利用顶点着色器,将顶点的 Y 轴映射到 X 轴偏移上。 顶点变换:
- 注意: 需要两个 Pass。第一个 Pass 渲染影子(纯黑、半透、无 ZWrite),第二个 Pass 渲染角色本身。
7. 屏幕后处理数学 (Post-Processing Math)
后处理是在渲染完所有物体后,对整个屏幕图像 (_MainTex) 进行二次处理。
7.1 暗角 (Vignette)
视觉: 屏幕四角变暗,模拟相机镜头或压抑氛围(低血量)。理论: 计算 UV 坐标距离中心 (0.5, 0.5) 的距离。 公式:
7.2 马赛克 (Pixelation)
视觉: 画面变模糊成大方块(眩晕、复古滤镜)。理论: 降低 UV 的精度。将连续的 UV 坐标“量化”为台阶状。 公式:
- 例子: 如果
Resolution是 100,那么 0.015 会变成 。0.010 到 0.019 之间的所有 UV 都会变成同一个值,采到同一个颜色。
7.3 色差/故障风 (Chromatic Aberration / Glitch)
视觉: RGB 三色分离,像旧电视或赛博朋克干扰。理论: 采样三次纹理,但每次给 R、G、B 通道不同的 UV 偏移。 公式:
- Glitch 进阶:
Offset可以是一个随时间快速变化的随机数 (frac(sin(time)*large_number)).
8. 数学速查表 (Math Cheat Sheet)
| 想要做… | 用这个函数… |
|---|---|
| 循环/条纹 | frac(x * scale) |
| 硬边缘 | step(edge, x) |
| 软边缘 | smoothstep(min, max, x) |
| 闪烁/脉冲 | sin(_Time.y * speed) |
| 混合/过渡 | lerp(a, b, t) |
| 变亮/变暗 (非线性) | pow(x, power) |
| 距离/圆 | distance(uv, center) 或 length(vec) |
| 旋转 UV | 乘旋转矩阵 [cos -sin; sin cos] |
9. 程序化噪声与随机性 (Procedural Noise)
有时候我们不想用额外的纹理贴图(为了省内存),而是想直接在代码里生成“随机感”。9.1 伪随机函数 (Pseudo-Random)
Shader 里没有Random.Range。我们利用高频正弦波的 frac 部分来模拟随机。
经典单次哈希 (One-line Hash):
- 原理: 将 UV 坐标投影到一个大数上,取正弦波极其细碎的部分,看起来就像电视雪花。
- 应用: 故障风特效的随机跳变、星星闪烁。
9.2 简单值噪声 (Value Noise)
如果把随机点平滑连接起来,就得到了“云”一样的效果。- 应用: 动态水面、火焰扰动、不需要贴图的溶解遮罩。
10. 进阶 UI/道具特效 (Loot & Card Effects)
在 Loot 类游戏中,如何表现“传说装备”或“稀有卡牌”?全靠 Shader。10.1 扫光/流光 (Sheen / Shiny Effect)
视觉: 一道亮光快速划过卡牌表面。理论: 在 UV 空间定义一条倾斜的线,计算当前像素距离这条线的距离。
-
数学推导:
- 定义光带位置: (45 度斜线)。
- 让光带移动: 。
-
限制光带宽度: 使用
smoothstep或pow提取中间亮的两边暗的区域。
10.2 圆形进度/冷却 (Radial Fill)
视觉: 技能 CD 转圈,或者圆环血条。理论: 笛卡尔坐标 (x, y) 转 极坐标 (Angle, Distance)。
- 核心函数:
atan2(y, x)- 返回值为 (即 -3.14 到 3.14)。 归一化角度:
- 结果为 0 到 1 的线性增长值。
- 如果当前角度小于填充量,显示颜色,否则透明。
10.3 全息投影/扫描网格 (Hologram / Scanline)
视觉: 科幻风格的 UI,有水平扫描线上下移动。理论: 利用 WorldPos 或 ScreenPos 的 Y 轴分量,结合正弦波。 公式:
- 只取波峰最顶端的 5% 作为亮线。
11. HLSL 基础语法速查 (Syntax Cheat Sheet)
手写代码时的快速参考。11.1 向量与矩阵 (Vectors & Matrices)
- 声明:
float(32 位),half(16 位),fixed(11 位, 仅旧硬件).float4 v = float4(1, 0, 0, 1);float2 uv = v.xy;(Swizzling: 随意组合分量)float3 color = v.rgb;
- 构造:
float3(uv, 1.0) - 矩阵乘法:
mul(MATRIX, vector)(注意顺序!)mul(unity_ObjectToWorld, v.vertex): 模型转世界
11.2 纹理采样 (Texture Sampling)
- 2D 纹理:
- 声明:
sampler2D _MainTex; - 采样:
fixed4 col = tex2D(_MainTex, i.uv); - 纹理大小:
_MainTex_TexelSize(x=1/w, y=1/h, z=w, w=h)
- 声明:
11.3 常用 Unity 宏 (Common Macros)
UnityObjectToClipPos(v.vertex): 顶点着色器必须调用的,将模型点转为裁剪空间坐标。TRANSFORM_TEX(v.uv, _MainTex): 应用材质球上的 Tiling & Offset 设置。
11.4 常用数学函数
abs(x): 绝对值ceil(x)/floor(x): 向上/向下取整round(x): 四舍五入min(a, b)/max(a, b): 最小值/最大值clamp(x, min, max): 钳制范围length(v): 向量长度distance(p1, p2): 两点距离normalize(v): 归一化 (变为长度为 1 的单位向量)dot(a, b): 点积cross(a, b): 叉积reflect(i, n): 计算反射向量