引子:从"简单"到"进阶",向前一大步
嗨,小朋友!
上次我们看了一个"简单的Shader"**——只会做两件事:
- 贴图
- 染色
你可能觉得:"这也太简单了吧!——游戏里那些炫酷的火焰、流动的水面、发光的宝剑——是怎么做出来的?"
答案是——它们的Shader比我们上次看的复杂得多**!
但别怕——“复杂"不代表"看不懂”:
- 它只是"用了更多小技巧"
- **每个小技巧——都能拆开慢慢看
- 拆开之后——你会发现"哦,原来是这样啊!"
今天——我们要看一份真正的进阶Shader——一个会"呼吸、发光、摆动"的"魔法水晶"Shader!
**看完这个——你就能理解"游戏里那些炫酷特效的原理"**了!
一、先看看今天的"主角"
今天的Shader要做出这样的效果:
- 🌊 会摆动——像水晶在轻轻晃动
- ✨ 会发光——边缘有一圈光晕
- 💗 会呼吸——亮度一亮一暗地变化
- 🌈 有花纹——表面有滚动的能量纹路
听起来很酷对不对?——这就是一个"魔法水晶"Shader!
完整代码在这里(先别怕,我们会一点点拆开看):
Shader "MyShader/MagicCrystal" { Properties { _MainTex ("主贴图", 2D) = "white" {} _NoiseTex ("能量纹路", 2D) = "white" {} _BaseColor ("基础颜色", Color) = (0.2, 0.5, 1.0, 1) _RimColor ("边缘光颜色", Color) = (0.5, 0.8, 1.0, 1) _RimPower ("边缘光强度", Range(0.1, 10)) = 3.0 _WaveSpeed ("摆动速度", Range(0, 5)) = 1.0 _WaveStrength ("摆动幅度", Range(0, 0.5)) = 0.1 _ScrollSpeed ("纹路滚动速度", Range(0, 5)) = 1.0 _PulseSpeed ("呼吸速度", Range(0, 5)) = 2.0 } SubShader { Tags { "RenderType" = "Transparent" "Queue" = "Transparent" } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off Cull Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; sampler2D _NoiseTex; float4 _BaseColor; float4 _RimColor; float _RimPower; float _WaveSpeed; float _WaveStrength; float _ScrollSpeed; float _PulseSpeed; struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 worldNormal : TEXCOORD1; float3 viewDir : TEXCOORD2; }; v2f vert(appdata v) { v2f o; // 顶点摆动 float wave = sin(_Time.y * _WaveSpeed + v.vertex.y * 3) * _WaveStrength; v.vertex.x += wave; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.uv; o.worldNormal = UnityObjectToWorldNormal(v.normal); float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.viewDir = normalize(_WorldSpaceCameraPos - worldPos); return o; } fixed4 frag(v2f i) : SV_Target { // 主贴图 fixed4 mainCol = tex2D(_MainTex, i.uv); // 滚动的能量纹路 float2 scrollUV = i.uv + float2(_Time.y * _ScrollSpeed, 0); fixed4 noiseCol = tex2D(_NoiseTex, scrollUV); // 边缘光 float rim = 1 - saturate(dot(i.worldNormal, i.viewDir)); rim = pow(rim, _RimPower); // 呼吸 float pulse = (sin(_Time.y * _PulseSpeed) + 1) * 0.5; pulse = lerp(0.5, 1.0, pulse); // 合成最终颜色 fixed3 finalColor = mainCol.rgb * _BaseColor.rgb; finalColor += noiseCol.rgb * 0.3; finalColor += _RimColor.rgb * rim; finalColor *= pulse; return fixed4(finalColor, mainCol.a * _BaseColor.a); } ENDCG } } FallBack "Diffuse" }**看到这一大堆——你可能又开始晕了——别慌!
深呼吸——我们一块一块拆开看——每一块都不难!
二、Properties:这次的"材料清单"更丰富了
Properties { _MainTex ("主贴图", 2D) = "white" {} _NoiseTex ("能量纹路", 2D) = "white" {} _BaseColor ("基础颜色", Color) = (0.2, 0.5, 1.0, 1) _RimColor ("边缘光颜色", Color) = (0.5, 0.8, 1.0, 1) _RimPower ("边缘光强度", Range(0.1, 10)) = 3.0 _WaveSpeed ("摆动速度", Range(0, 5)) = 1.0 _WaveStrength ("摆动幅度", Range(0, 0.5)) = 0.1 _ScrollSpeed ("纹路滚动速度", Range(0, 5)) = 1.0 _PulseSpeed ("呼吸速度", Range(0, 5)) = 2.0 }**上次只有2个材料——这次有9个!
打个比方:
- 上次做菜——只放盐、糖
- 这次做菜——放盐、糖、辣椒、酱油、醋、姜、蒜、料酒、花椒——做一道大餐**!
看看每个材料:
_MainTex——主贴图(水晶本身的样子)_NoiseTex——能量纹路(会滚动的花纹)_BaseColor——基础颜色(水晶是什么色的)_RimColor——边缘光颜色(边上光晕什么色)_RimPower——边缘光有多亮_WaveSpeed——摆得多快_WaveStrength——摆得多厉害_ScrollSpeed——纹路滚多快_PulseSpeed——呼吸多快
看到Range(0.1, 10)了吗?:
- 这个意思是"可以在0.1到10之间调"**
- 在Unity里——会显示成一个滑块——美术姐姐可以拖着调
这就是Shader的贴心之处——把所有"可以调"的东西都摆到Inspector里**——美术不用改代码就能调效果!
三、SubShader的"新东西":让物体半透明
SubShader { Tags { "RenderType" = "Transparent" "Queue" = "Transparent" } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off Cull Off // ... }**这里有几个新东西——都和"透明"有关:
新东西1:"RenderType" = "Transparent"
上次是Opaque(不透明)——这次是Transparent(透明)!
告诉Unity:**"我是透明物体——请按透明物体的方式对待我"。
就像贴标签:
- 上次的标签是"实心物体"**
- 这次的标签是"半透明水晶"**
新东西2:"Queue" = "Transparent"
这是"渲染队列"——决定"先画谁后画谁"!
打个比方:
- 画画的时候——要"先画背景,再画前面的东西"
- 不然前面的物体会被背景盖住
Queue——就是"画的顺序"
Transparent队列——排在后面——**保证先把不透明物体画完——再画透明物体。
为什么要这样?——因为透明物体要"混合"背景色——必须先有背景才行!
新东西3:Blend SrcAlpha OneMinusSrcAlpha
这就是"透明混合"的魔法!
**看起来很吓人——别慌——它的意思是:
“把我的颜色 和 后面的背景 混合起来”
具体怎么混合?:
SrcAlpha——我的透明度是多少(比如0.5,就是半透明)OneMinusSrcAlpha——1减去我的透明度(0.5,就是背景占一半)
混合公式:
最终颜色 = 我的颜色 × 0.5 + 背景颜色 × 0.5结果就是:我一半、背景一半——看起来半透明!
这就是玻璃、水、透明水晶的效果!
新东西4:ZWrite Off
“ZWrite"是"深度写入”——**关掉它——让别的物体能透过我被看见。
打个比方:
ZWrite On——“我很霸道,我挡住谁谁就看不见”ZWrite Off——“我很客气,你们随便被看见”
透明物体一般都要ZWrite Off——不然透明后面的物体会被"莫名挡住"。
新东西5:Cull Off
“Cull"是"剔除”——“要不要剔除背面”**。
普通物体——只画正面(Cull Back)——背面看不到反正。
透明水晶——**能看到里面——内外都要画(Cull Off)!
这三个设置Blend + ZWrite Off + Cull Off——就是"半透明物体的标配"**!
四、顶点魔法师升级:让物体摆动!
v2f vert(appdata v) { v2f o; // 顶点摆动 float wave = sin(_Time.y * _WaveSpeed + v.vertex.y * 3) * _WaveStrength; v.vertex.x += wave; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.uv; o.worldNormal = UnityObjectToWorldNormal(v.normal); float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.viewDir = normalize(_WorldSpaceCameraPos - worldPos); return o; }**这次的顶点魔法师——比上次多了几个新技能!
新技能1:摆动魔法
float wave = sin(_Time.y * _WaveSpeed + v.vertex.y * 3) * _WaveStrength; v.vertex.x += wave;**这两行——就让水晶摆动起来了!
让我们拆开看:
_Time.y是什么?——Unity送给你的"时间"变量:
- **游戏一直在跑——时间一直在增加
_Time.y就是"现在过了多少秒"**
sin(...)是什么?——数学里的"正弦函数":
- 它的值——在-1到1之间来回摇摆**
- 像一个"来回荡的秋千"**
- 时间不断变化——sin的值不断在-1到1之间跳
打个比方:
- 想象一个钟摆**——**左右左右不停摆
- **左边最远——值是-1
- **中间——值是0
- **右边最远——值是1
- **sin就是这个钟摆!
v.vertex.y * 3是什么?——“位置越高,摆得越不一样”:
- 让每个位置的顶点——摆动稍微不一样
- 不然整个物体就像"僵尸一样一起动"**——很假!
* _WaveStrength——“摆多厉害由你决定”:
- **
_WaveStrength = 0.1——摆一点点 - **
_WaveStrength = 0.5——摆很大
v.vertex.x += wave;——把这个"摆动值"加到x坐标上:
- 每一帧——顶点位置稍微不一样
- 看起来就像——水晶在左右摆动!
这就是"顶点动画"的魔法——通过修改顶点位置,让物体"动起来"!
很多游戏里的旗帜、草、水面、果冻**——都是用这个原理做的!
新技能2:算出"世界法线"
o.worldNormal = UnityObjectToWorldNormal(v.normal);"法线"是什么?——它是"每个顶点朝哪个方向"**:
打个比方:
- 想象你在墙上贴一张贴纸
- 贴纸的"正面"朝哪个方向?
- 朝"离开墙"的方向——这就是"法线"
球的每一个点——都有一个"法线"——朝"远离球心"的方向。
为什么要算这个?——用来做"边缘光"**(下面会用到)!
UnityObjectToWorldNormal(...)——**Unity帮我们写好的函数——把"模型自己坐标系的法线"变成"游戏世界里的法线"。
新技能3:算出"看的方向"
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.viewDir = normalize(_WorldSpaceCameraPos - worldPos);“viewDir"是"视线方向”——“摄像机在看这个点的方向”:
打个比方:
- **你的眼睛——**摄像机
- **一个物体上的点
- **从物体指向你眼睛——**就是"视线方向"
这两行的意思:
- **
worldPos——**这个顶点在游戏世界里的位置 - **
_WorldSpaceCameraPos——**摄像机的位置(Unity送的) - **两者相减——**就是"看向摄像机的方向"
为什么要算这个?——还是为了"边缘光"**!
五、片段魔法师升级:颜色的"大合成"!
fixed4 frag(v2f i) : SV_Target { // 主贴图 fixed4 mainCol = tex2D(_MainTex, i.uv); // 滚动的能量纹路 float2 scrollUV = i.uv + float2(_Time.y * _ScrollSpeed, 0); fixed4 noiseCol = tex2D(_NoiseTex, scrollUV); // 边缘光 float rim = 1 - saturate(dot(i.worldNormal, i.viewDir)); rim = pow(rim, _RimPower); // 呼吸 float pulse = (sin(_Time.y * _PulseSpeed) + 1) * 0.5; pulse = lerp(0.5, 1.0, pulse); // 合成最终颜色 fixed3 finalColor = mainCol.rgb * _BaseColor.rgb; finalColor += noiseCol.rgb * 0.3; finalColor += _RimColor.rgb * rim; finalColor *= pulse; return fixed4(finalColor, mainCol.a * _BaseColor.a); }这次片段魔法师——做了"四件事"——每一件都很酷!
事情1:取主贴图颜色(老朋友)
fixed4 mainCol = tex2D(_MainTex, i.uv);**这个我们上次学过——从贴图上取颜色。
事情2:滚动的能量纹路(新魔法!)
float2 scrollUV = i.uv + float2(_Time.y * _ScrollSpeed, 0); fixed4 noiseCol = tex2D(_NoiseTex, scrollUV);"让贴图动起来"的魔法!
核心:i.uv + float2(_Time.y * _ScrollSpeed, 0):
- 原来的uv——是固定的位置
- 加上"时间×速度"——位置一直在移动!
打个比方:
- 你在窗户外——**看外面的风景
- **窗户不动——**风景不动
- **窗户往右移——**风景就"往左滚过"你的视线!
Shader里:
- uv不变——贴图不动
- uv随时间加一个值——贴图滚动!
结果:能量纹路一直在物体表面"流动"——像水晶里能量在涌动!
这个技巧——在游戏里用得超级多:
- 传送带的效果
- 能量护盾的花纹
- 瀑布的水流
- 火焰的动态
事情3:边缘光(最酷的魔法!)
float rim = 1 - saturate(dot(i.worldNormal, i.viewDir)); rim = pow(rim, _RimPower);**这两行——做出了"边缘发光"的效果——就是那种"物体边上有一圈光晕"的效果!
让我们拆开看:
dot(i.worldNormal, i.viewDir)是什么?:
- **
dot——**数学里的"点积"(不用管公式,只要知道它的意义) - 它算的是"两个方向有多像"**
具体意义:
- 法线朝着摄像机——dot值≈1
- 法线垂直摄像机——dot值≈0(这就是"边缘")
- 法线背对摄像机——dot值≈-1
1 - dot——反过来:
- 中间的地方(法线朝摄像机)——rim值≈0(不发光)
- 边缘的地方(法线垂直摄像机)——rim值≈1(发光!)
这就是"边缘光"的原理!
打个比方:
- **想象一个球
- **正中间——**你看到的是"球的正面"——很清楚
- **球的边上——**几乎是"侧着的"——**你只能勉强看到
**“越侧的地方”——**就"越是边缘"——就"越亮"——这就是边缘光!
pow(rim, _RimPower)——让边缘光更集中:
- **
_RimPower = 1——**光很散 - **
_RimPower = 5——**光只在很窄的边缘 - **
_RimPower = 10——**光只在最边最边
**很多游戏里"发光的水晶、护盾、幽灵"——都是用这个原理!
事情4:呼吸效果(可爱的魔法!)
float pulse = (sin(_Time.y * _PulseSpeed) + 1) * 0.5; pulse = lerp(0.5, 1.0, pulse);又见到我们的老朋友sin了——这次让物体"一亮一暗"地呼吸**。
sin(_Time.y * _PulseSpeed):
- 值在-1到1之间跳
+1:变成0到2之间* 0.5:变成0到1之间
lerp(0.5, 1.0, pulse):
lerp是"在两个数之间平滑过渡"- 让pulse从"0到1"——变成"0.5到1"
- 保证物体永远不会完全变黑**——最暗也是"一半亮"
结果:亮度在"一半亮"到"完全亮"之间来回变化——像"呼吸"**!
大合成:把所有效果混在一起!
fixed3 finalColor = mainCol.rgb * _BaseColor.rgb; finalColor += noiseCol.rgb * 0.3; finalColor += _RimColor.rgb * rim; finalColor *= pulse;**四行代码——做四件事:
第一行:主贴图 × 基础颜色——“给贴图染色”
第二行:加上0.3的能量纹路——“加入流动的花纹”
第三行:加上边缘光——“边缘发光”
第四行:乘以呼吸值——“一亮一暗”
打个做菜的比方:
- 先炒主菜**(主贴图×基础颜色)
- 加点调料**(能量纹路)
- 撒点装饰**(边缘光)
- 最后大火收汁**(呼吸变亮暗)
四步下来——一份炫酷的魔法水晶就做好了**!
最后一行:输出!
return fixed4(finalColor, mainCol.a * _BaseColor.a);**fixed4(...)——把颜色打包成4个数:
- **前面
finalColor——**红绿蓝三个值 - **后面
mainCol.a * _BaseColor.a——**透明度
“透明度也要乘”——因为要让物体半透明**!
六、完整流程回顾:一个像素的"魔法之旅"
**让我们跟着一个"水晶边缘"的像素——看它是怎么被画出来的:
Step 1:顶点魔法师算它的位置
- **加上sin摆动——它稍微偏了一点
- 算出屏幕位置
- 算出法线、视线方向
Step 2:片段魔法师上场
- 从主贴图取颜色——得到淡蓝色
- 和
_BaseColor相乘——变成深蓝色
Step 3:取能量纹路
- **uv加上时间——采样滚动的位置
- 得到一个"能量花纹"的颜色**
- 加到基础色上——变成"有花纹的深蓝色"
Step 4:算边缘光
- **这是边缘像素——dot值≈0
- **1-dot ≈ 1——rim值很大
- 加上边缘光颜色——变成"发光的深蓝色"
Step 5:呼吸
- **假设当前sin=0.5——**pulse≈0.75
- **颜色乘以0.75——稍微暗一点
- **下一帧——**pulse可能=1——又亮一点
Step 6:输出
- 加上透明度
- 和背景混合
- 屏幕上出现——一个摆动、发光、呼吸的魔法水晶的边缘像素!
七、这个Shader的"精妙之处"
看完这个例子——能提炼几个Shader的核心技巧:
技巧1:用_Time让物体动起来
**只要有_Time.y——任何东西都能动:
- **顶点位置动——**摆动、飘扬
- **uv动——**滚动、流动
- **颜色动——**呼吸、闪烁
技巧2:用sin做周期性变化
sin是"来回摇摆"的魔法:
- 摆动
- 呼吸
- 闪烁
- 波浪
**几乎所有"周期性动画"——都能用sin做!
技巧3:用dot(法线, 视线)做边缘光
这是"边缘检测"的经典技巧——**几乎所有:
- 护盾
- 幽灵
- 能量物体
- 卡通描边
——都会用到这个原理!
技巧4:颜色层层叠加
复杂的效果——都是简单效果的叠加:
- 基础色 × 主贴图
- 加上滚动纹路
- 加上边缘光
- 乘以呼吸
**一层一层——堆出复杂效果!
结语:一份"魔法配方"的进阶之旅
从上次的"简单Shader"(只会贴图染色),
到这次的"魔法水晶"(会摆动、会呼吸、会发光、会流动)——
我们做了一次"从新手到进阶"的完整旅程:
- 多学了"透明混合"**——让物体半透明
- 多学了"顶点动画"**——让物体摆动
- 多学了"uv滚动"**——让贴图流动
- 多学了"边缘光"**——让物体发光
- 多学了"呼吸变化"**——让物体呼吸
- 多学了"多层合成"**——堆出复杂效果
它像是一份"进阶菜谱"**:
- 上次的菜谱——“白饭配盐”**
- 这次的菜谱——“海鲜大餐”**
- 原料多了几倍
- 步骤多了几倍
- 但每一步都能理解**!
它有Properties里丰富的"可调参数"——让美术能调出各种风格的水晶。
它有渲染状态的"透明配置"——让物体像水晶一样半透明。
它有顶点着色器的"摆动魔法"——让物体动起来。
它有片段着色器的"四种效果合成"——滚动+边缘光+呼吸+染色。
它有sin、dot、pow这些"数学工具"——让复杂效果变简单。
下次当你看到游戏里的"炫酷特效"——请记得**:
那看起来"神奇的画面"——背后是:
- 一堆Properties让美术能调
- 一些顶点动画让物体动
- 一些uv滚动让贴图流
- 一些sin/dot运算让效果酷
- 一些颜色叠加让画面丰富
每一个"炫酷效果"——归根到底就是我们今天讲的这些基础技巧的组合。
每一份"复杂Shader"——都可以拆成几十个"小技巧"慢慢理解。
每一位曾经"看代码就晕"的学习者——只要肯拆开慢慢看——都能像今天这样"看懂了"。
每一个_Time.y——都是"时间的魔法棒"——让物体动起来。
每一个sin(...)——都是"摇摆的秋千"——让效果周期变化。
每一个dot(法线, 视线)——都是"边缘的检测器"——让物体发光。
每一次+叠加——都在"堆积炫酷"——从简单到复杂。
这——就是Shader进阶的秘密:
- 不是"神秘的黑魔法"
- 而是"几个基础技巧的组合"**
- 不是"天才才能写"
- 而是"愿意慢慢学的人都能掌握"**
- 不是"看代码就头疼的东西"
- 而是"拆开就能理解的清晰配方"**
在这份"魔法配方"的背后:
- 是Unity为你准备的丰富工具箱**(
_Time、sin、pow、dot……) - 是数学与美学的巧妙结合**
- 是简单技巧堆积成复杂效果的智慧路径**
- 是每一位创作者都能实现的炫酷梦想**
这——就是Shader的进阶之旅——不是"越来越难"——而是"越来越好玩"——每多学一个技巧,就多一份创造的能力——是每一位怀着好奇心的学习者,都能一步步走完的"魔法之路"。
每一位读到这里的小朋友——都已经掌握了游戏Shader的很多核心秘密了!
每一次你多看一个Shader——都会发现"哦,这也是我学过的技巧!"**
每一位持续探索的学习者——总有一天能写出属于自己的炫酷Shader**!
这——就是Unity送给每一位小小学习者的、最鼓舞人心的"进阶礼物"。 🌟✨🧙♂️
加油——你已经在Shader的路上走得比想象中远了! 💪🎨