注:本系列为shader实战的精品案例,这些是必须要掌握的

Shader实战案例

一、UV扰动之水纹效果

思路:利用噪声图信息偏移主UV图坐标,形成扰动效果。利用_Time平移顶点实现动画效果_,使扭曲纹理看起来在 X 和 Y 方向上滚动。

  • 准备一张水面图,一张噪声图,噪声图可以看老唐shader,会讲如何制作。

image-20250519230353232

效果图:

1747666947745

下面将一些:前置的知识点。

1.什么是噪声

它的本质是一种由数学算法公式生成的有规则性或可控的随机数据。

  • 特性:
    • 随机性:噪声数据本质上是随机的,这意味着它具有不规则性,无法通过简单的模式预测
    • 平滑性:数据不会突然跳跃或变化,而是呈现出逐渐过渡的效果,适合模拟自然现象
    • 周期性:随着坐标的变化,噪声值会重复出现,形成一个封闭的循环。也可以通过调整噪声的范围或修 改算法来避免
    • 可调性:大多数噪声算法的输出是可以调整的,这种可调性允许你根据需求灵活地控制噪声的表现,适 应不同的视觉效果或逻辑处理需求 5. 多维性:能够处理多维数据,这意味着它们可以生成二维、三维甚至更高维度的噪声数据
  • 注:因为有周期性的存在,它更像是一种周期函数,使每个顶点偏离的值不同,从而达到扰动效果
  • 注:如果每个顶点偏移的都一样,就会出现整体移动的效果

2.主要实现

注:CG语言。该代码块内部不支持识别CG语言。用C#代的,凑合看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
Shader "Unlit/Water" {
Properties {
_MainTex ("Texture", 2D) = "white" { }
_DistortTex ("DistortTex", 2D) = "white" { }
_DistortAmount ("Distortion Amount", Range(0, 2)) = 0.5 // 扭曲强度
_DisTexXSpeed ("ScorllSpeedX", Range(-50, 50)) = 5 //扭曲纹理x轴滚动速度
_DisTexYSpeed ("ScorllSpeedY", Range(-50, 50)) = 5 //扭曲纹理y轴滚动速度
[HideInInspector] _RandomSeed ("_MaxYUV", Range(0, 10000)) = 0.0 // 隐藏在Inspector中,随机种子值

}
SubShader {
Tags { "Queue" = "Transparent" // 设置渲染队列为透明队列,表示该对象会在不透明对象之后渲染,数值越大越后渲染 "RenderType" = "Transparent" // 设置渲染类型为透明类型 }

}
Blend SrcAlpha OneMinusSrcAlpha // 设置混合模式,使用源和目标的Alpha值进行混合

LOD 100 // 设置Shader的细节层次,用于区分不同性能设备支持的渲染效果

Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog

#include "UnityCG.cginc"

struct appdata {
float4 vertex : POSITION;
float2 disuv : TEXCOORD1;
float2 uv : TEXCOORD0;
};

struct v2f {
float2 uv : TEXCOORD0;
float2 disuv : TEXCOORD1;
float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _DistortTex; // 扭曲纹理采样器
half4 _DistortTex_ST; // 扭曲纹理的缩放和偏移
half _DisTexXSpeed, _DisTexYSpeed, _DistortAmount; // 扭曲相关的参数
UNITY_DEFINE_INSTANCED_PROP(float, _RandomSeed) // 定义实例化属性,随机种子

v2f vert(appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.disuv = TRANSFORM_TEX(v.disuv, _DistortTex);
return o;
}

fixed4 frag(v2f i) : SV_Target {
half randomSeed = UNITY_ACCESS_INSTANCED_PROP(Props, _RandomSeed); // 获取随机种子值
//通过时间变量 _Time.x 和滚动速度(_DistortTexXSpeed 和 _DistortTexYSpeed)动态调整扭曲纹理的UV坐标
//这一步产生一个动画效果,使扭曲纹理看起来在 X 和 Y 方向上滚动扰动强度计算
i.disuv.x += (_Time.x + _RandomSeed) * _DisTexXSpeed % 1;
i.disuv.y += (_Time.x + _RandomSeed) * _DisTexYSpeed % 1;

//采样噪声图,取出里面的噪声信息用来计算扰动值
// half strong = (tex2D(_DistortTex, i.disuv).r - 0.5) * _DistortAmount;
half strong = tex2D(_DistortTex, i.disuv).r * _DistortAmount;
//将计算出的扰动量添加到主纹理的 UV 坐标:
i.uv.x += strong;
i.uv.y += strong;


// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);


return col;
}
ENDCG
}
}
}

(1)基础纹理的UV坐标

  • 每个像素都有一个对应的UV坐标,用于映射到主纹理(_MainTex)上。
  • UV坐标的默认范围是 [0, 1],它决定了如何从纹理中采样颜色。

(2)引入扭曲纹理

扭曲纹理(_DistortTex)是一个灰度图,用于存储扰动信息。每个像素的灰度值表示该点的扰动强度。

(3)灰度值范围:

  • 值为 0.5 表示无扰动。
  • 值小于 0.5 表示负扰动(向反方向偏移)。
  • 值大于 0.5 表示正扰动(向正方向偏移)。

(4)滚动扭曲纹理

通过时间变量 _Time.x 和滚动速度(_DistortTexXSpeed_DistortTexYSpeed)动态调整扭曲纹理的UV坐标:

1
2
i.uvDistTex.x += ((_Time.x + randomSeed) * _DistortTexXSpeed) % 1;
i.uvDistTex.y += ((_Time.x + randomSeed) * _DistortTexYSpeed) % 1;

这一步产生一个动画效果,使扭曲纹理看起来在 X 和 Y 方向上滚动。

(5) 扰动强度计算

1
half distortAmnt = (tex2D(_DistortTex, i.uvDistTex).r - 0.5) * 0.2 * _DistortAmount;

这里的计算逻辑:

  • tex2D(_DistortTex, i.uvDistTex).r:采样扭曲纹理的红色通道值。
  • 减去 0.5:将灰度值映射到 [-0.5, 0.5] 的范围。
  • 乘以 0.2_DistortAmount:控制扰动强度。
  • 这里面其实不映射也可以,(tex2D(_DistortTex, i.uvDistTex).r* _DistortAmount;这么写效果也能出来,只是上面写的要规范一些,实际测试容易点。其实就是把灰度图里面的灰度信息取出来然后偏移uv坐标就行了,主要就是计算每个顶点不同的偏移值然后叠加实现扰动效果。

注:每个rgba通道都是独立的一张灰度图,然后里面灰度值都相同,只是扰动的轴不一样(比如r是扰动y轴),所以用哪个通道都无所谓。然后其实还可以,在别的通道上再去叠加另一组噪声,实现更复杂一点的效果。

(6)扰动应用到主纹理UV

1
2
i.uv.x += distortAmnt;
i.uv.y += distortAmnt;
  • 结果是主纹理的采样点发生了偏移,产生扭曲效果。

(7)为什么可以复用主UV坐标来生成扭曲纹理的UV坐标?

在Shader中,可以使用TRANSFORM_TEX宏来对UV坐标进行变换,包括缩放(Tiling)和偏移(Offset)。这个宏会根据纹理的_ST属性(缩放和偏移)来调整UV坐标。因此,即使两个纹理的原始UV坐标不同,通过适当的变换,也可以使它们对齐或产生特定的视觉效果

这种写法常常可以少去定义顶点着色器中appdata结构体中的变量,进而减少内存的开销。

之后的例子也会这么写。

名称 类型 说明
_Time float4 t 是自该场景加载开始所经过的时间,4个分量分别是 (t/20, t, t2, t3)
_SinTime float4 t 是时间的正弦值,4个分量分别是 (t/8, t/4, t/2, t)
_CosTime float4 t 是时间的余弦值,4个分量分别是 (t/8, t/4, t/2, t)
unity_DeltaTime float4 dt 是时间增量,4个分量的值分别是(dt, 1/dt, smoothDt, 1/smoothDt)

二、UV扰动之描边燃烧

思路:通过采样判断像素四周Alpha值进行描边检测,利用原始透明度和轮廓透明度计算出外轮廓透明度值,之后将轮廓失真纹理进行时间滚动处理和扰动处理,最后利用lerp函数实现主纹理和轮廓的叠加混合。

效果图:

dd017633-8a6f-4442-8637-4902f9649286

1.边缘检测算法

思想:通过采样当前像素四周的Alpha值,当相邻像素透明而当前像素不透明时,判定为轮廓边缘

采样方向 坐标偏移 检测目标
左方 i.uv - half2(destUv.x, 0) 检测右侧相邻像素是否透明
右方 i.uv + half2(destUv.x, 0) 检测左侧相邻像素是否透明
上方 i.uv + half2(0, destUv.y) 检测下方相邻像素是否透明
下方 i.uv - half2(0, destUv.y) 检测上方相邻像素是否透明
  • 当前像素不透明(Alpha≈1)而相邻像素透明(Alpha≈0)时,对应方向的采样值≈0
  • 四个方向的采样值相加后,有效轮廓区域的总值≈1(仅一个方向检测到边缘)
  • 非轮廓区域的总值≈4(所有方向都检测到不透明像素)或≈0(所有方向都检测到透明像素)

二、主要实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
Shader "Chapter1/chapter1_4" // 定义着色器名称为“Chapter1/chapter1_4”
{
Properties
{
_MainTex ("Texture", 2D) = "white" {} // 主纹理,默认为白色纹理
_Color("Main Color", Color) = (1,1,1,1) // 主颜色,默认为白色

_OutlineTex("Outline Texture", 2D) = "white" {} // 轮廓纹理,默认为白色纹理
_OutlineTexXSpeed("Texture scroll speed X", Range(-50,50)) = 10 // 轮廓纹理在X轴上的滚动速度
_OutlineTexYSpeed("Texture scroll speed Y", Range(-50,50)) = 0 // 轮廓纹理在Y轴上的滚动速度
[HideInInspector] _RandomSeed("_MaxYUV", Range(0, 10000)) = 0.0 // 随机种子,影响轮廓随机效果

[Space] // 空格,用于分隔属性
_OutlineDistortTex("Outline Distortion Texture", 2D) = "white" {} // 轮廓失真纹理,默认为白色纹理
_OutlineDistortAmount("Outline Distortion Amount", Range(0,2)) = 0.5 // 轮廓失真强度
_OutlineDistortTexXSpeed("Distortion scroll speed X", Range(-50,50)) = 5 // 失真纹理在X轴的滚动速度
_OutlineDistortTexYSpeed("Distortion scroll speed Y", Range(-50,50)) = 5 // 失真纹理在Y轴的滚动速度

[Space] // 空格,用于分隔属性
_OutlineColor("Outline Base Color", Color) = (1,1,1,1) // 轮廓的基本颜色,默认为白色
_OutlineAlpha("Outline Base Alpha", Range(0,1)) = 1 // 轮廓透明度,默认为1(完全不透明)
_OutlineGlow("Outline Base Glow", Range(1,100)) = 1.5 // 轮廓发光强度
_OutlineWidth("Outline Base Width", Range(0,0.2)) = 0.004 // 轮廓宽度
}
SubShader
{
Tags {
"Queue"="Transparent" // 设置渲染队列为透明队列,表示该对象会在不透明对象后渲染
"RenderType"="Transparent" // 设置渲染类型为透明类型
}

Blend SrcAlpha OneMinusSrcAlpha // 设置混合模式,基于源Alpha进行混合
LOD 100 // 设定Shader的细节层次为100

Pass
{
CGPROGRAM
#pragma vertex vert // 使用顶点着色器
#pragma fragment frag // 使用片段着色器
// 使雾效工作
#pragma multi_compile_fog // 启用多种雾效编译选项

#include "UnityCG.cginc" // 引入Unity的通用着色器代码库

// 定义输入结构体,用于接收顶点数据
struct appdata
{
float4 vertex : POSITION; // 顶点位置
float2 uv : TEXCOORD0; // 顶点纹理坐标
half4 color : COLOR; // 顶点颜色
};

// 定义输出结构体,传递数据到片段着色器
struct v2f
{
float2 uv : TEXCOORD0; // 顶点的纹理坐标
half4 color : COLOR; // 顶点颜色
half2 uvOutTex : TEXCOORD1; // 用于轮廓纹理的纹理坐标
half2 uvOutDistTex : TEXCOORD2; // 用于轮廓失真纹理的纹理坐标
float4 vertex : SV_POSITION; // 顶点位置(裁剪空间)
};

// 声明所有纹理采样器和参数
sampler2D _MainTex; // 主纹理
float4 _MainTex_ST, _MainTex_TexelSize, _Color; // 主纹理的变换和颜色值

sampler2D _OutlineTex; // 轮廓纹理
half4 _OutlineTex_ST; // 轮廓纹理的变换
half _OutlineTexXSpeed, _OutlineTexYSpeed; // 轮廓纹理滚动速度

half4 _OutlineColor; // 轮廓的基本颜色
half _OutlineAlpha, _OutlineGlow, _OutlineWidth; // 轮廓透明度、发光强度、宽度

sampler2D _OutlineDistortTex; // 轮廓失真纹理
half4 _OutlineDistortTex_ST; // 轮廓失真纹理的变换
half _OutlineDistortTexXSpeed, _OutlineDistortTexYSpeed, _OutlineDistortAmount; // 失真纹理的滚动速度和强度

UNITY_DEFINE_INSTANCED_PROP(float, _RandomSeed) // 随机种子,用于每个物体实例化

// 顶点着色器
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex); // 将顶点坐标转换到裁剪空间
o.uv = TRANSFORM_TEX(v.uv, _MainTex); // 将纹理坐标从模型空间转换到UV空间
o.color = v.color; // 将顶点颜色传递给片段着色器
o.uvOutTex = TRANSFORM_TEX(v.uv, _OutlineTex); // 轮廓纹理的坐标变换
o.uvOutDistTex = TRANSFORM_TEX(v.uv, _OutlineDistortTex); // 轮廓失真纹理的坐标变换
return o;
}

// 片段着色器
fixed4 frag (v2f i) : SV_Target
{
half4 col = tex2D(_MainTex, i.uv); // 获取主纹理的颜色
half originalAlpha = col.a; // 记录主纹理的Alpha值

half randomSeed = UNITY_ACCESS_INSTANCED_PROP(Props, _RandomSeed); // 获取随机种子值

// 处理轮廓失真
i.uvOutDistTex.x += ((_Time.x + randomSeed) * _OutlineDistortTexXSpeed) % 1; // 轮廓失真纹理X轴滚动
i.uvOutDistTex.y += ((_Time.x + randomSeed) * _OutlineDistortTexYSpeed) % 1; // 轮廓失真纹理Y轴滚动
half outDistortAmnt = (tex2D(_OutlineDistortTex, i.uvOutDistTex).r - 0.5) * 0.2 * _OutlineDistortAmount; // 获取失真强度
//_MainTex_TexelSize是贴图 _MainTex 的像素尺寸大小,值: Vector4(1 / width, 1 / height, width, height)
half2 destUv = half2(_OutlineWidth * _MainTex_TexelSize.x * 200, _OutlineWidth * _MainTex_TexelSize.y * 200); // 计算失真偏移
destUv.x += outDistortAmnt; // 应用X轴失真
destUv.y += outDistortAmnt; // 应用Y轴失真

// 采样主纹理的四个方向的透明度值,来检测轮廓区域
half spriteLeft = tex2D(_MainTex, i.uv + half2(destUv.x, 0)).a;
half spriteRight = tex2D(_MainTex, i.uv - half2(destUv.x, 0)).a;
half spriteBottom = tex2D(_MainTex, i.uv + half2(0, destUv.y)).a;
half spriteTop = tex2D(_MainTex, i.uv - half2(0, destUv.y)).a;
half result = spriteLeft + spriteRight + spriteBottom + spriteTop; // 计算轮廓效果的强度
//saturate(x)的作用是如果x取值小于0,则返回值为0。如果x取值大于1,则返回值为1。若x在0到1之间,则直接返回x的值
//step(edge,x):当x>edge时返回1,否则返回0
result = step(0.05, saturate(result)); // 通过saturate和step函数生成轮廓区域

// 轮廓纹理滚动
i.uvOutTex.x += ((_Time.x + randomSeed) * _OutlineTexXSpeed) % 1; // 轮廓纹理X轴滚动
i.uvOutTex.y += ((_Time.x + randomSeed) * _OutlineTexYSpeed) % 1; // 轮廓纹理Y轴滚动
half4 tempOutColor = tex2D(_OutlineTex, i.uvOutTex); // 获取轮廓纹理的颜色
tempOutColor *= _OutlineColor; // 应用轮廓颜色
_OutlineColor = tempOutColor;
half4 outline = _OutlineColor * i.color.a; // 轮廓颜色,按顶点颜色加权
outline.rgb *= _OutlineGlow; // 应用轮廓发光效果
result *= (1 - originalAlpha) * _OutlineAlpha; // 根据主纹理透明度调整轮廓透明度
outline.a = result; // 轮廓的透明度为轮廓强度

//Lerp用于得出两个状态(两个值, 两个点, 两种颜色, 两个角度等)的中间状态, 插值一般取0~1,
//靠近0时接近第一个状态, 靠近1时接近第二个状态
col = lerp(col, outline, result); // 将主纹理与轮廓纹理混合
col *= _Color; // 应用主颜色

return col; // 返回最终的颜色结果
}
ENDCG
}
}
}