《shader入门精要》笔记-第11章-让画面动起来

Unity Shader中的内置变量(时间篇)

名称 类型 描述
_Time float4 t是自该场景加载开始所经过的时间,4个分量分别是(t/20, t, 2t, 3t)
_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)

纹理动画

纹理动画在游戏中的应用非常广泛.尤其在各种资源都比较局限的移动平台上,我们往往会使用纹理动画来代替复杂的粒子系统等模拟各种动画效果

序列帧动画

最常见的纹理动画之一就是序列帧动画.序列帧动画的原理非常简单,它像放电影一样,依次播放一系列关键帧图像.
它的优点在于灵活性极强,不需要任何物理计算就能得到非常细腻的动画效果.

想要实现序列帧动画,我们先要提供一张包含了关键帧图像的图像.

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 "Unlit/11.2"
{
Properties
{
_Color("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Image Sequence", 2D) = "white" {}

// 水平方向和竖直方向包含的关键帧的个数
_HorizontalAmount ("Horizontal Amount", Float) = 4.0
_VerticalAmount ("Vertical Amount",Float) = 4.0

_Speed("Speed",Range(1, 100)) = 30

}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" }

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

ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha

CGPROGRAM

#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

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

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

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

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

fixed4 frag(v2f i) : SV_TARGET {
float time = floor(_Time.y * _Speed);
float row = floor(time / _HorizontalAmount);
float column = time - row * _HorizontalAmount;
half2 uv = i.uv + half2(column, -row);
uv.x /= _HorizontalAmount;
uv.y /= _VerticalAmount;

fixed4 c = tex2D(_MainTex, uv);
c.rgb *= _Color;
return c;
}
ENDCG
}
}
}

滚动的背景

很多2D游戏都使用了不同的滚动背景来模拟游戏角色在场景中的穿梭.这些背景往往包含了多个层来模拟一种视差效果.而这些背景的实现往往就是利用了纹理动画.

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
Shader "Unity Shaders Book/Chapter 11/Water" {
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 {

// 一些SubShader在使用Unity批处理时会出现问题,这是可以通过DisableBatching标签指明是否对该SubShader使用批处理.
// 批处理会合并所有相关的模型,而这些模型各自的模型空间就会丢失.而顶点动画需要在物体的模型空间下进行偏移.
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}

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

ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
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
}
}
FallBack "Transparent/VertexLit"
}

广告牌

另一种常见的顶点动画就是广告牌技术(Billboarding).
广告牌技术会根据视角方向来旋转一个被纹理着色的多边形(通常就是简单的四边形,这个多边形就是广告牌),使得多边形看起来好像总面对着摄像机.

广告牌技术的本质就是构建旋转矩阵,而我们知道一个矩阵需要三个基向量.
广告牌技术使用的基向量通常就是表面法线(normal),指向上的方向以及指向右的方向.
除此之外,我们还需要一个锚点(anchor location),这个锚点在旋转的过程中是固定不变的,以此来确定多边形在空间中的位置.

广告牌技术的难点在于,如何根据需求来构建3个相互正交的基向量.
计算过程通常是,我们首先会通过初始计算得到目标的表面法线(例如视角方向)和指向上的方向,而两者往往是不垂直的.但是,两者其中之一是固定的,例如模拟草丛时,我们希望广告牌的指向上的方向永远是(0, 1, 0),而法线方向应该随视角变化;而当模拟粒子效果时,我们希望广告牌的法线方向是固定的,即总指向视角方向,指向上的方向则可以发生变化.

我们假设法线方向是固定的,首先,我们根据初始的表面法线和指向上的方向来计算目标方向和指向右的方向(通过叉积操作): right = up X normal
对其归一化后,再由法线方向和指向右的方向计算出正交的指向上的方向: up’ = normal X right
20190326142905.png
如此,我们就可以得到用于旋转的三个正交基了.上图给出了计算过程的演示.
如果指向上的方向是固定的,计算过程也是类似.

实践: 广告牌

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
Shader "Unity Shaders Book/Chapter 11/Billboard" {
Properties {
_MainTex ("Main Tex", 2D) = "white" {}
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_VerticalBillboarding ("Vertical Restraints", Range(0, 1)) = 1
}
SubShader {
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
Pass {
Tags { "LightMode"="ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
fixed _VerticalBillboarding;
struct a2v {
float4 vertex : POSITION;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert (a2v v) {
v2f o;
float3 center = float3(0, 0, 0);

// 获取摄像机位置
float3 viewer = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos, 1));

// 目标法线
float3 normalDir = viewer - center;

// _VerticalBillboarding 调整固定法线还是固定指向上
// 当该值为1时,固定法线,为1时,固定向上方向为(0, 1, 0)
normalDir.y =normalDir.y * _VerticalBillboarding;

normalDir = normalize(normalDir);

// 如果目标法线跟向上方向平行,两者的叉积则会出错.
// 这里对目标法线的分量y进行了判断,以得到合适的向上方向.
float3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);

// 求向上方向和目标法线的垂线,即向右向量,并归一化
float3 rightDir = normalize(cross(upDir, normalDir));

// 因为固定了法线,所以重新求得向上向量
upDir = normalize(cross(normalDir, rightDir));

float3 centerOffs = v.vertex.xyz - center;

// 这里卡了我好一会儿,以为是内积求投影来着,然而是分量(一个标量)和正交基相乘
float3 localPos = center + rightDir * centerOffs.x + upDir * centerOffs.y + normalDir * centerOffs.z;

o.pos = UnityObjectToClipPos(float4(localPos, 1));
o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
return o;
}

fixed4 frag (v2f i) : SV_Target {
fixed4 c = tex2D (_MainTex, i.uv);
c.rgb *= _Color.rgb;
return c;
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}

注意事项

批处理

之前说的顶点动画必须关闭批处理,然而关闭批处理就会增多Draw Call,降低性能.
因此我们应该尽量避免使用模型空间下一些绝对位置和方向进行计算.
在广告牌的例子中,为了避免显式使用模型空间的中心点作为锚点,可以利用顶点颜色来存储每个顶点到锚点的距离值(smg),这种做法在商业游戏里很常见.

阴影

到时候再看吧.
P239