Shader实战案例
注:
本系列为shader实战的精品案例,这些是必须要掌握的
UV扰动之水纹效果
思路:利用噪声图信息偏移主UV图坐标,形成扰动效果。利用_Time平移顶点实现动画效果_,使扭曲纹理看起来在 X 和 Y 方向上滚动。
- 准备一张水面图,一张噪声图,噪声图可以看老唐shader,会讲如何制作。
效果图:

下面将一些:前置的知识点。
1.什么是噪声
它的本质是一种由数学算法公式生成的有规则性或可控的随机数据。
- 特性:
- 随机性:噪声数据本质上是随机的,这意味着它具有不规则性,无法通过简单的模式预测
- 平滑性:数据不会突然跳跃或变化,而是呈现出逐渐过渡的效果,适合模拟自然现象
周期性
:随着坐标的变化,噪声值会重复出现,形成一个封闭的循环。也可以通过调整噪声的范围或修 改算法来避免- 可调性:大多数噪声算法的输出是可以调整的,这种可调性允许你根据需求灵活地控制噪声的表现,适 应不同的视觉效果或逻辑处理需求 5. 多维性:能够处理多维数据,这意味着它们可以生成二维、三维甚至更高维度的噪声数据
- 注:因为有周期性的存在,它更像是一种周期函数
,使每个顶点偏离的值不同,从而达到扰动效果
。- 注:
如果每个顶点偏移的都一样,就会出现整体移动的效果
2.主要实现
注:CG语言。该代码块内部不支持识别CG语言。只能标识C#,凑合看
1 | Shader "Unlit/Water" { |
(1)基础纹理的UV坐标
- 每个像素都有一个对应的UV坐标,用于映射到主纹理(
_MainTex
)上。- UV坐标的默认范围是 [0, 1],它决定了如何从纹理中采样颜色。
(2)引入扭曲纹理
扭曲纹理(
_DistortTex
)是一个灰度图,用于存储扰动信息。每个像素的灰度值表示该点的扰动强度。
(3)灰度值范围:
- 值为
0.5
表示无扰动。- 值小于
0.5
表示负扰动(向反方向偏移)。- 值大于
0.5
表示正扰动(向正方向偏移)。
(4)滚动扭曲纹理
通过时间变量
_Time.x
和滚动速度(_DistortTexXSpeed
和_DistortTexYSpeed
)动态调整扭曲纹理的UV坐标:
1 | i.uvDistTex.x += ((_Time.x + randomSeed) * _DistortTexXSpeed) % 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 | i.uv.x += 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函数实现主纹理和轮廓的叠加混合。
效果图:

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(所有方向都检测到透明像素)
2.主要实现
1 | Shader "Chapter1/chapter1_4" // 定义着色器名称为“Chapter1/chapter1_4” |
序列帧动画
效果:
1. 分析利用纹理坐标制作序列帧动画的原理
关键点:
- UV坐标范围0~1,原点为图片左下角
- 图集序列帧动画播放顺序为从左到右,从上到下
注:Unity默认uv采样方向是从下到上从左到右
- 分析问题:
- 如何得到当前应该播放哪一帧动画?
- 如何将采样规则从0~1修改为在指定范围内采样?
问题解决
思路
:
- 用内置时间参数
_Time.y
参与计算得到具体哪一帧时间是不停增长的数值
用它对总帧数取余
,便可以循环获取到当前帧数
利用时间得到当前应该绘制哪一帧后
我们只需要确认从当前小图片中,采样开始位置,采样范围即可
采样开始位置,可以利用当前帧和行列一起计算
采样范围可以将0~1范围 缩放转换到 小图范围内
具体解决分析:
![]()
- 首先图集是行纵8*8 ,64帧的图集,我们以左下角为起始坐标,可以分成64个小图片
- 我们只需要做到将每个图片的起始坐标计算出,去偏移主uv即可
2. 代码实现
重要公式说明
代码 解释 具体分析 frameIndex =floor(_Time.y * _Speed) %( _Rows * _Columns) 计算当前为第几帧 _ Time.y是_游戏从加载开始经历的时间。我们把时间对总帧取余,就可以得到当前是哪一帧,并且一直循环 _Time.y * _Speed 帧间隔 帧间隔就是:1/_ SPeed,当 _Speed = 64
且总帧数N = 64
时,1 秒内会播放完所有帧并循环。frameUV =float2( frameIndex % _Columns /_Columns , 1-(floor( frameIndex / _Columns) +1 )/_Rows); 当前序列帧图片原点在主UV的位置 frameIndex % _Columns 得到当前帧图片在图集的第几列 因为列数总是0-Columns循环,所以用列数取余 frameIndex / _Columns 得到当前帧图片在图集的第行列 因为一个Columns为一行,所以/Columns frameIndex % Columns / Columns 将列转换为小图原地x坐标 就是一共有8列,为了归一化/的8 1-(floor( frameIndex / _Columns) +1 )/_Rows 将行转换为小图原点y坐标 因为我们要从上到下播放,所以需要用1-我们实际计算的数,在归一化时,如果不+1则原点是左上角,而并被左下角。实际上就是往下串一行。比如第1帧时index 为0,不+1的话为1。而1就是左上角顶点。
代码;
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 Shader "Unlit/FrameAnimation"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Rows("Rows",int) = 8
_Columns("Columns",int) =8
_Speed("Speed",float)=1
}
SubShader
{
Tags { "RenderType"="Transparent" "IgnoreProjector"="True" "Queue"="Transparent" }
LOD 100
Pass
{
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _Rows;
float _Columns;
float _Speed;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//计算当前是第几帧
float frameIndex =floor(_Time.y * _Speed) %( _Rows * _Columns);
//计算小格子起始位置
// frameIndex % _Columns 第几列
//frameIndex / _Columns第几行
//1- 从顶开始 不 加1的话原点是顶格的(左上角,但是其实要的是左下角),所以要加一行
float2 frameUV =float2( frameIndex % _Columns /_Columns , 1-(floor( frameIndex / _Columns) +1 )/_Rows); //当前序列帧图片原点在主UV的位置
//计算缩放比例
float2 size =float2(1/_Columns,1/ _Rows);
//最终采样UV
// *size 相当于把0~1范围映射到 了0- 1/8范围,大图映射小图
// +frameUV 相当于把起始采样位置 移动到了对应帧小格子
float2 uv = i.uv * size +frameUV;
fixed4 col = tex2D(_MainTex,uv);
return col;
}
ENDCG
}
}
}
背景滚动动画
1. 函数介绍
内置函数 frac(参数)
该函数的内部计算规则为:frac(x) = x - floor(x)
一般用于保留数值的小数部分,但是负数时要注意
比如:
frac(2.5) = 2.5 - 2 = 0.5
它的主要作用:是可以帮助我们保证 uv坐标 范围在0~1之间
大于1的uv值重新从0开始向1方向取值
小于0的uv值重新从1开始向0方向取值
2. 分析利用纹理坐标制作滚动的背景的原理
注意点:
滚动的背景使用的美术资源图片,往往是首尾循环相连的
基本原理:
不停地利用时间变量对uv坐标进行偏移运算
超过1的部分从0开始采样
小于0的部分从1开始采样
3. 代码实现
- 新建shader,删除无用代码
- 声明属性,属性映射
主纹理、u轴速度、v轴速度(两个速度的原因是因为图片可能竖直或水平滚动)- 透明shader
往往这种滚动背景图片都会有透明区域
渲染标签修改、关闭深度写入、进行透明混合- 结构体
顶点和纹理坐标- 顶点着色器
顶点坐标转换,纹理坐标直接赋值- 片元着色器
利用时间和速度对uv坐标进行偏移计算
利用偏移后的uv坐标进行采样
代码:
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 Shader "Unlit/RollBackground" {
Properties {
_MainTex ("Texture", 2D) = "white" { }
_RollSpeedU ("RollSpeedU", float) = 1
_RollSpeedV ("RollSpeedV", float) = 1
}
SubShader {
Tags { "RenderType" = "Transparent" "IgnoreProjector" = "True" "Queue" = "Transparent" }
LOD 100
Pass {
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float _RollSpeedU;
float _RollSpeedV;
v2f vert(appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag(v2f i) : SV_Target {
float2 uv = i.uv + frac(float2(_RollSpeedU * _Time.y, _RollSpeedV * _Time.y));
fixed4 col = tex2D(_MainTex, uv);
return col;
}
ENDCG
}
}
}
4 . 可控制的背景滚动
从C#传递利用Time.deltaTime计算好的偏移值来偏移uv
在shader中不用_ Time来实现动画,防止出现动画刷新到开始
效果
shader代码
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 Shader "Unlit/RollBackground" {
Properties {
_MainTex ("Texture", 2D) = "white" { }
//_RollSpeedU ("RollSpeedU", float) = 1
//_RollSpeedV ("RollSpeedV", float) = 1
_Offset ("_Offset", Vector) = (0, 0, 0, 0)
}
SubShader {
Tags { "RenderType" = "Transparent" "IgnoreProjector" = "True" "Queue" = "Transparent" }
LOD 100
Pass {
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float _RollSpeedU;
float _RollSpeedV;
float2 _Offset;
v2f vert(appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag(v2f i) : SV_Target {
float2 uv = i.uv + frac(_Offset);
fixed4 col = tex2D(_MainTex, uv);
return col;
}
ENDCG
}
}
}
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 using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestShader : MonoBehaviour
{
public Material material;
void Start()
{
}
private Vector2 _offset = new Vector2(0, 0);
private Vector2 _currentSpeed;
float inputU;
float inputV;
public float Speed = 100;
// Update is called once per frame
void Update()
{
// 计算速度变化(例如通过输入控制)
// 示例:按A/D键控制水平速度,W/S键控制垂直速度
inputU = Input.GetAxis("Horizontal");
inputV = Input.GetAxis("Vertical");
_currentSpeed = new Vector2(inputU, inputV) * 0.5f; // 调整系数控制灵敏度
print(_currentSpeed.x);
// 累积偏移量
_offset.x += _currentSpeed.x * Time.deltaTime * Speed;
_offset.y += _currentSpeed.y * Time.deltaTime * Speed;
// 传递偏移量给Shader(取小数部分避免大数值问题)
material.SetVector("_Offset", new Vector4(_offset.x, _offset.y, 0, 0));
}
}