Unity-Shader读书笔记(10)

        这篇文章主要讲如何使用Unity Shader内置的时间变量来实现动画效果。而Unity Shader中内置的时间变量大家可以看官网的描述:内置着色器变量 - Unity 手册

纹理动画

序列帧动画

         序列帧动画是一种常见的动画形式之一,其原理是在“连续的关键帧”中分解动画动作,也就是在时间轴的每帧上逐帧绘制不同的内容,使其连续播放从而形成动画。git也是差不多的原理。

PS:序列帧动画是使用Assets.png图片。然后我们只需要一个Quad让其正对摄像机就可以了。

具体代码如下:

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
Shader "learn/SequenceAnimationShader"
{
Properties
{
_Color ("Color Tint", Color) = (1,1,1,1)
_MainTex ("Image Sequence", 2D) = "white" {}
_HorizontalAmount ("Horizontal Amount",Float) = 4
_VerticalAmount ("Vertical Amount",Float) = 4
_Speed ("Speed",Range(1,100)) = 30
}
SubShader
{
Tags {"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"}

Pass
{
Tags { "LightMode"="ForwardBase"}

ZWrite off
Blend SrcAlpha OneMinusSrcAlpha

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

#include "UnityCG.cginc"

fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _HorizontalAmount;
float _VerticalAmount;
float _Speed;

struct a2v
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
};

v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
float time = floor(_Time.y * _Speed);
float row = floor(time / _HorizontalAmount);
float col = time - row * _VerticalAmount;
half2 uv = i.uv + half2(row,-col);
uv.x /= _HorizontalAmount;
uv.y /= _VerticalAmount;
fixed4 finalColor = tex2D(_MainTex, uv);
finalColor *= _Color;
return finalColor;
}
ENDCG
}
}
}

        属性中_HorizontalAmount和_VerticalAmount分别表示序列帧纹理中图像在水平方向和竖直方向包含关键帧的个数。我们所使用的boom.png就是8*8个关键帧数,所以之后我们要在Inspector界面中将_HorizontalAmount和_VerticalAmount设定为8。而_Speed属性用于控制序列帧动画播放速度。

        因为序列帧图像通常是存在透明通道的,我们可以将其看作是一个半透明对象。所以我们使用透明度混合来进行操作。因此我们标签等设定也和透明度混合一样。因为我们只是渲染纹理中的信息,所以我们也不存在高光反射和漫反射,我们只需要把纹理显示出来。故顶点函数中就十分简单了,我们主要修改的是片元函数。

        片元函数中,我们需要计算出当前时间对应的帧数。由于序列帧纹理都是按行按列排的。因此这个位置可以认为是该关键帧所在的行列索引数。代码中将_Time.y * _Speed来得出时间,并且使用floor来保证是整数值。接下来我们通过floor(time / _HorizontalAmount)来得到现在的行数,那么现在所在的列就是time - row * _VerticalAmount。这里我们不需要考虑行数超出的情况,因为我们只要将其纹理wrap设定为repeat就好了。

        由于序列帧图像包含了许多关键帧的图像,那么我们计算后的uv应该要锁定在这些关键帧图像中。我们可以先让纹理采集得到的uv按照每个关键帧大小进行缩放。比如我们现在是8*8所以我们要将得到的 i.uv.x / 8,且 i.uv.y / 8。然后我们再使用当前的行列数进行偏移,得到当前子图像的纹理坐标。需要注意的是,对竖直方向的坐标偏移要使用减法,这是因为Unity中纹理坐标竖直方向的顺序(从下到上逐渐增大)和序列帧纹理中的顺序是相反的(播放顺序从上到下)。但是这个我觉得还是看你是怎么生成序列帧纹理的。那么具体的过程就是下发的代码:

1
2
3
4
5
6
// 将uv值设定在一个关键帧图像范围内
half2 uv = float2(i.uv.x / _HorizontalAmount,i.uv.y / _VerticalAmount);
// 对其做偏移移动到对应的关键帧范围内比如在第二帧的时候col = 1,那么这个帧范围应该就是在1/8到1/4间
uv.x += col / _HorizontalAmount;
// 同col一样
uv.y -= row / _VerticalAmount;

之后我们经过整理得到:

1
2
3
half2 uv = i.uv + half2(row,-col);
uv.x /= _HorizontalAmount;
uv.y /= _VerticalAmount;

滚动背景

        很多2D游戏都使用了不断滚动的背景来模拟游戏角色在场景中的穿梭(类似2D跑酷一类的游戏),这些背景往往包含了多层(layers)来模拟视差。现在我们也是使用两层来模拟无限滚动的背景。

<span = style = "color:red">PS:本节是用Assets_background.png图片和Assets_background.png图片。然后我们只需要一个Quad并让摄像机设置为正交且正对Quad。

具体的代码实现如下:

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
Shader "Learn/ScrollShader"
{
Properties
{
_NearTex ("Near Texture", 2D) = "white" {}
_FarTex ("Far Texture", 2D) = "white" {}
_NearScrollX ("Near Scroll Speed", Float) = 1
_FarScrollX ("Far Scroll Speed", Float) = 1
_Multiplier ("Layer Multiplier", Float) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct a2v
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float4 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};

sampler2D _NearTex;
float4 _NearTex_ST;
sampler2D _FarTex;
float4 _FarTex_ST;
float _NearScrollX;
float _FarScrollX;
float _Multiplier;

v2f vert (a2v v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.uv, _NearTex) + frac(float2(_NearScrollX,0) * _Time.y);
o.uv.zw = TRANSFORM_TEX(v.uv, _FarTex) + frac(float2(_FarScrollX,0) * _Time.y);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
fixed4 nearColor = tex2D(_NearTex, i.uv.xy);
fixed4 farColor = tex2D(_FarTex, i.uv.zw);
fixed4 col = lerp(farColor,nearColor,nearColor.a);
col.rgb *= _Multiplier;
return col;
}
ENDCG
}
}
}

        我们在属性中设定两个纹理一个是_NearTex表示近平面的纹理,_FarTex表示远平面的纹理。_NearScrollX和_FarScrollX分别用来控制近平面和远平面的滚动速度。_Multiplier用于控制亮度。

        顶点函数进行坐标变换和纹理坐标转换。而纹理坐标我们使用了当前的时间乘以速度进行偏移,这样就实现了水平滚动。在片元着色器中,我们分别对近平面图像和远平面图像进行采集。然后近平面中空白的部分由远平面进行填充,因此我们使用了lerp来进行这个操作。最后用_Multiplier来进行整体的亮度提升。

顶点动画

        我们常使用顶点动画来模拟飘动的旗帜、小溪等。这里我不在提供具体的项目设置,因为这个确实比较难说明。

流动的河流

        书本中使用正弦函数来模拟水流的波动效果(难道只要是周期性函数就可以用来模拟波动?)。

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
Shader "Learn/WaterFlowShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color Tint",Color) = (1,1,1,1)
// 控制水流波动幅度
_Magnitude ("Distortion Magnitude",Float) = 1
// 控制水流波动频率
_Frequency ("Distortion Frequency",Float) = 1
// 控制水流波长的倒数
_InvWaveLength ("Distortion Inverse Wave Length",Float) = 1
// 控制河流移动的速度
_Speed ("Speed", Float) = 0.5
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" "DisableBatching"="True"}
LOD 100

Pass
{
Tags {"LightMode"="ForwardBase"}

ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Cull Off

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 _Magnitude;
float _Frequency;
float _InvWaveLength;
float _Speed;
float4 _Color;

v2f vert (appdata v)
{
float4 offset;
offset.yzw = float3(0,0,0);
offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength + v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude;
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex + offset);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.uv += float2(0,_Time.y * _Speed);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
col.rgb *= _Color.rgb;
return col;
}
ENDCG
}
}
}

        这段Shader使用了透明度混合。其实是因为纹理中有部分存在a通道为0的纹素,所以才使用了透明度混合。如果你不用也可以,只是哪些a通道为0的地方就变成黑色显示了。

        属性中_Magnitude控制波动幅度,_Frequency控制波动频率、_InvWaveLength控制波长的倒数、_Speed控制移动的速度。而在标签的设置中,我们除了使用了透明度混合的设置还加入了DisableBatching的设置。这个是为了防止Unity对其使用批处理。这是因为批处理会会合并所有的相关模型,那么这些模型各自的模型空间就消失了。而我们需要物体在模型空间下进行偏移。因此我们取消批处理。接下来我们除了按照透明度混合一样设置混合和关闭深度写入外,我们还关闭了剔除。作者说这是为了将所有面都显示出来的而进行的效果,这是因为作者给的模型像Quad一样有些面不渲染,所以这种就要看你实际情况如何了。

        这段shader最主要的代码就是计算偏移值offset。我们使用_Frequency * _Time.y来控制sin函数的频率,但是作者并没有说明为什么为了让不同的位置不同的位移所以我们要写这样的计算v.vertex.x * _InvWaveLength + v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength。至于为什么只变换x轴就可以产生波形的效果,这是因为作者给的模型是平面而它只有改变x轴才有效果,所以这个还是要和你实际项目关联才行。

广告牌

        广告牌技术就是根据视角方向来旋转一个被纹理着色的多边形(通常就是简单的四边形,这个多边形就是广告牌),使得多边形看起来总是面对摄像机。这里大家可能会想到Unity脚本中用lookat正对摄像机的操作。但是我们使用脚本的话是连模型在世界空间下的旋转信息一起改变的,这里并没有改变这个信息。这项技术应用于渲染烟雾、云朵和闪光特效等。

        广告牌技术的本质就是构建旋转矩阵,这就要我们指定3个基向量(广告牌的3个基向量,书中是指模型空间下的表示)。广告牌技术使用的基向量通常是表面法线、指向上的方向以及指向右的方向。除此之外,我们还需要一个锚点,这个锚点在旋转的过程中是不变的,以此来确定多边形在空间中的位置。

        广告牌的难点就在于我们如何根据不同的要求得到3个基向量。z方向比较容易确定,因为我们想让广告牌正对摄像机,所以z方向必然是视角方向。接下来我们只需要找到x和y方向就好了。首先我们会根据要求找到不变的方向(书上说是指向上的方向,但是我觉得这个会和3个基向量中向上方向混起来)。而我们可以知道视角方向,根据这个不变方向和视角方向我们可以使用叉乘得到垂直于这两个方向所构成平面的方向。这个得到的方向再使用这个方向和视角方向进行叉积,这样我们就得到3个基向量。至于你怎么分配x和y,看你项目需求。这里需要强调的是,我们所说的视角方向不是观察方向。视角方向是摄像机位置 - 锚点位置。这样我们才能保证锚点在旋转的过程中是不变的。

        具体的应用代码,我就不再说明了。主要我真的不是很理解他们的处理方式。

注意事项

        顶点动画大都是在模型空间下修改顶点的坐标位置来实现的。这里就有会产生一些问题。第一点:在模型空间下的修改,那么我们必须要关闭Unity的批处理,即我们要将DisableBatching标签设置为false。这样会产生额外的Draw Call,从而影响游戏的性能。第二点,我们只是在模型空间下修改顶点,而用来计算影子的顶点是不会跟着一起改变。这就需要我们重新写一个ShadowCaster。书中有给出实现这个ShadowCaster的代码,但是这个只作用于河流那个顶点动画。

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
Shader "Unity Shaders Book/Chapter 11/Vertex Animation With Shadow" {
Properties {
_MainTex ("Main Tex", 2D) = "white" {}
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_Magnitude ("Distortion Magnitude", Float) = 1
_Frequency ("Distortion Frequency", Float) = 1
_InvWaveLength ("Distortion Inverse Wave Length", Float) = 10
_Speed ("Speed", Float) = 0.5
}
SubShader {
// Need to disable batching because of the vertex animation
Tags {"DisableBatching"="True"}

Pass {
Tags { "LightMode"="ForwardBase" }

Cull Off

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
float _Magnitude;
float _Frequency;
float _InvWaveLength;
float _Speed;

struct a2v {
float4 vertex : POSITION;
float4 texcoord : TEXCOORD0;
};

struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};

v2f vert(a2v v) {
v2f o;

float4 offset;
offset.yzw = float3(0.0, 0.0, 0.0);
offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength + v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude;
o.pos = UnityObjectToClipPos(v.vertex + offset);

o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv += float2(0.0, _Time.y * _Speed);

return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed4 c = tex2D(_MainTex, i.uv);
c.rgb *= _Color.rgb;

return c;
}

ENDCG
}

// Pass to render object as a shadow caster
Pass {
Tags { "LightMode" = "ShadowCaster" }

CGPROGRAM

#pragma vertex vert
#pragma fragment frag

#pragma multi_compile_shadowcaster

#include "UnityCG.cginc"

float _Magnitude;
float _Frequency;
float _InvWaveLength;
float _Speed;

struct v2f {
V2F_SHADOW_CASTER;
};

v2f vert(appdata_base v) {
v2f o;

float4 offset;
offset.yzw = float3(0.0, 0.0, 0.0);
offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength + v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude;
v.vertex = v.vertex + offset;

TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)

return o;
}

fixed4 frag(v2f i) : SV_Target {
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
FallBack "VertexLit"
}

        这里我们新使用了几个Unity Shader的内置宏。V2F_SHADOW_CASTER设定了阴影所需的变量,TRANSFER_SHADOW_CASTER_NORMALOFFSET用来计算这些变量的值。但是我们要先将v.vertex进行变换,这样得到的效果才是正确的。最后我们在SHADOW_CASTER_FRAGMENT让Unity自动完成阴影投射的部分。