unityShader之颜色渐变与播放序列帧实现

需求

为了节省性能替代particle system,用了一堆trick实现了 从OnEnable开始,delay一段时间,从头开始序列帧播放和颜色渐变。
写这篇博文,是为了……虽然效果拼拼凑凑出来了,但是知其然还得知其所以然,好好记录一下~好,小学生作文时间开始。

参考

《Unity Shader 入门精要》源码
https://github.com/candycat1992/Unity_Shaders_Book/blob/master/Assets/Shaders/Chapter11/Chapter11-ImageSequenceAnimation.shader

shader中主要运算部分

uv运算所需参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Properties {
// 贴图
_MainTex ("Image Sequence", 2D) = "white" {}
// 纵横数量
_HorizontalAmount ("Horizontal Amount", Int) = 4
_VerticalAmount ("Vertical Amount", Int) = 4
// 移动速度
_Speed ("Speed", Range(1, 100)) = 30
// 延迟出现
_Delay("Delay", Float) = 1

//……颜色参数见下

//被OnEnable时间 由c#脚本传入
[HideInInspector]_TimeOnEnable("TimeOnEnable", Float) = 0
}

frag函数 uv运算部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//运行时间
float _time = _Time.y - _TimeOnEnable - _Delay;
//加速
float time = floor(_time * _Speed);

//为uv做准备 计算当前时间对应的行数与列数 当time为0时,应对应第一行第一列
//使用 time / _HorizontalAmount 的商作为行索引,余数作为列索引
//当time = 0, 得 row = 1, column = -4 理解为第一格的左下角
//当time = 1,得 row = 1, column = -3
float row = floor(time / _HorizontalAmount) + 1; // 商
float column = time - row * _HorizontalAmount; // 余数

//注意unity中纹理坐标竖直方向顺序与序列帧纹理顺序相反
half2 uv = i.uv + half2(column, -row);
uv.x /= _HorizontalAmount;
uv.y /= _VerticalAmount;
//当time = 0, i.uv=(0,0)(原左下角) uv = (-4 / 4, -1 / 4) = (-1, -0.25) 即对应序列帧第一格的左下角
//当time = 0. i.uv=(1,1)(原右上角) uv = (-3/4, 0/4) = (-0.75, 0) 即对应第一格的右上角
//当time = 1, i.uv=(0,0)(原左下角) uv = (-3/4, -1/3) = (-0.75, -0.25) 即对应序列帧第二格的左下角
//……

fixed4 c = tex2D(_MainTex, uv);
//好 至此我们已获得了当前像素点的颜色值~

颜色运算增加参数

1
2
3
4
5
6
//颜色变化
_ChangeDuring1("ChangeDuring1", Range(0,1000)) = 1
_ChangeDuring2("ChangeDuring2", Range(0,1000)) = 2
_Color1("Color1", Color) = (1,0.4995675,0.4995675,0)
_Color2("Color2", Color) = (0.2118362,0.1712803,0.7058823,0)
_Color3("Color3", Color) = (0.2118362,0.1712803,0.7058823,0)

frag函数 颜色渐变

需求结果为Color1花费ChangeDuring1时间,变为Color2,之后再花ChangeDuring2时间,从Color2变到Color3

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
//接上方frag 我们已知道当前时间_time 和 当前像素点的颜色c
//颜色值lerp公式 _Color1 * (1.0 - lerp) + _Color2 * lerp 期望lerp从0随时间变为1,那么color会从_Color1变为_Color2
//关键是lerp怎么算出,而且需要始终在[0,1]区间并且
// time<=0的时候 lerp = 0
// time∈[0,ChangeDuring1]时 lerp = _time/ChangeDuring1
// time>ChangeDuring1时 lerp = 1
// 总结一下就是 time: <0, [0,1], >1 => lerp: 0, [0,1], 1
float ori_lerp = _time / _ChangeDuring1;
// ori_lerp 值范围有三种形态, <0, [0,1], >1
float temp = 1.0 - ori_lerp;
// 对应ori_lerp三种形态 temp的值为 >1, [1,0], <0
// 要想办法把这三段都压到[0,1],总得先砍一半,再想办法砍一半。用的是max和saturate
// temp2: max(temp, 0) 对应范围为 >1, [1,0], 0
// temp3: saturate(temp2) 对应范围为 1, [1,0], 0
// 再用1-temp3一下 0, [0,1], 1
// 这样就满足之前的lerp需求了对不对~ 所以下面就是最终的lerp公式.
float lerp = (1.0 - saturate( max(temp, 0)));
// 验算一下~
// 当ori_lerp = -1, temp = 2, lerp = 0
// 当ori_lerp = 0.3, temp = 0.7, lerp = 0.3
// 当ori_lerp = 2, temp = -1, lerp = 1

fixed4 color1 = _Color1 * (1.0 - lerp) + _Color2 * lerp;
//好 所以到这里,前半段颜色就算完了。


float time2 = _time - _ChangeDuring1;
float ori_lerp2 = time2 / _ChangeDuring2;
float temp2 = 1.0 - ori_lerp2;
float lerp2 = 1.0 - saturate(max(temp2, 0));
//后半段颜色差不多也是这个公式
//比较trick的是 1.0-lerp2 在时间没到time2<0的时候,都=1,所以在ChangeDuring1时间内,都是显示的color1
//而在ChangeDuring2时间内,color1 = _Color2, 所以不用特地加判断再写成 _Color2 * (1.0-lerp2) + _Color3 * lerp2;

fixed4 color2 = color1 * (1.0-lerp2) + _Color3 * lerp2;
c.rgb *= color2.rgb;

// saturate(ceil(max(0.0,_time)))非0即1 用于delay的时候把a设为0,以隐藏面片
c.a *= saturate(ceil(max(0.0,_time))) * color2.a;

//终于算完了
return c;

多一嘴关于_TimeOnEnable

//怎么让_Time.y和unity时间对应

1
mat.SetFloat("_TimeOnEnable", Time.timeSinceLevelLoad);