注:本系列为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(所有方向都检测到透明像素)

2.主要实现

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
}
}
}


序列帧动画

  • 效果:

    20250801-1508-12.9241620

1. 分析利用纹理坐标制作序列帧动画的原理

  • 关键点:

    1. UV坐标范围0~1,原点为图片左下角
    2. 图集序列帧动画播放顺序为从左到右,从上到下

    注:Unity默认uv采样方向是从下到上从左到右

  • 分析问题:
    1. 如何得到当前应该播放哪一帧动画?
    2. 如何将采样规则从0~1修改为在指定范围内采样?
  • 问题解决思路

    1. 用内置时间参数 _Time.y 参与计算得到具体哪一帧时间是不停增长的数值

    用它对总帧数取余,便可以循环获取到当前帧数

    1. 利用时间得到当前应该绘制哪一帧后

    2. 我们只需要确认从当前小图片中,采样开始位置,采样范围即可

      采样开始位置,可以利用当前帧和行列一起计算

      采样范围可以将0~1范围 缩放转换到 小图范围内

  • 具体解决分析:

    image-20250801211032621
    • 首先图集是行纵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
    #pragma vertex vert
    #pragma fragment frag


    #include "UnityCG.cginc"

    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. 代码实现

  1. 新建shader,删除无用代码
  2. 声明属性,属性映射
    主纹理、u轴速度、v轴速度(两个速度的原因是因为图片可能竖直或水平滚动)
  3. 透明shader
    往往这种滚动背景图片都会有透明区域
    渲染标签修改、关闭深度写入、进行透明混合
  4. 结构体
    顶点和纹理坐标
  5. 顶点着色器
    顶点坐标转换,纹理坐标直接赋值
  6. 片元着色器
    利用时间和速度对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
    #pragma vertex vert
    #pragma fragment frag


    #include "UnityCG.cginc"

    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来实现动画,防止出现动画刷新到开始

  • 效果

    20250801-1642-21.8724039

  • 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
    #pragma vertex vert
    #pragma fragment frag


    #include "UnityCG.cginc"

    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));
    }
    }