【Unity Shader】(十) —— UV动画原理及简易实现

笔者使用的是 Unity 2018.2.0f2 + VS2017,建议读者使用与 Unity 2018 相近的版本,避免一些因为版本不一致而出现的问题。

 

【Unity Shader】(三) —— 光照模型原理及漫反射和高光反射的实现
【Unity Shader】(四) —— 纹理之法线纹理、单张纹理及遮罩纹理的实现
【Unity Shader】(五) —— 透明效果之半透明效果的实现及原理
【Unity Shader】(六) —— 复杂的光照(上)
【Unity Shader】(七) —— 复杂的光照(下)
【Unity Shader】(八) —— 高级纹理之立方体纹理及光线反射、折射的实现
【Unity Shader】(九) —— 高级纹理之渲染纹理及镜子与玻璃效果的实现

前言

纯粹的静态美景宛如一张漂亮的贴图,而在游戏中,这种没有一点动画的情况往往是十分无趣且让人感到别扭的。所以本文会介绍一些简单的UV动画。

 

一. 时间变量

在我们写游戏逻辑时,涉及到随时间移动或旋转这种动作时,我们一般都会使用 Time.time 这个变量,同样,在 Unity Shader 中,我们需要实现一些动画时,也需要时间变量。下图是 Unity 内置的时间变量

名称 类别 作用
_Time  float4  t 是从场景加载开始时经历的时间,(t/20 , t , 2t , 3t)
_SinTime  float4  t 是时间的正弦值,(t/8 , t/4 , t/2 ,t)
_CosTime  float4  t 是时间的余弦值,(t/8 , t/4 , t/2 ,t)
unity_DeltaTime  float4  dt 是时间增量,(dt , 1/dt , smoothDt, 1/smoothDT)

 

比如我们使用 _Time.y 时,就相当于 _Time 的 t 变量,即会记录场景加载后经历的时间。下面我们使用它来实现一些效果

 

二. 序列帧动画

 

序列帧动画是一种十分常见的动画,它就像播放电影一样,把一连串的关键帧图像以一定的速度播放出来,看起来就是一段连续的动画。而它的优缺点也十分明显:

  • 灵活性强,不需要进行物理上的计算,比如光照,阴影等计算
  • 制作序列帧的美术工作量大

本文以制作一个火焰效果为例。我们需要用到一张序列帧图像,读者可以在本文末端下载,也可以使用自己的图像,先看一下我们要实现的效果

 

2.1 准备工作

(1)创建一个场景,这次为了效果明显,我们去掉天空盒子

(2)创建一个 Quad,一个 Material,一个 shader,命名为 SequenceAnimation

(3)准备一张序列帧图像,这里笔者使用的是一张包含了 4 x 4 张关键帧的图像

这 16 张关键帧图像的大小相同,我们要实现的是让它们从左到右,从上到下播放。所以我们要做的就很简单了,只需要在播放时记录下应该播放的关键帧的位置(UV坐标),然后进行采样就行了。

 

2.2 Shader 实现

序列帧图像往往被当成是一个半透明对象,所以我们以对待半透明对象的方法来对待它。如果对半透明原理及实现方法不熟悉的读者可以翻看这篇博文 【Unity Shader】(五) —— 透明效果之半透明效果的实现及原理

 

I.定义 Properties 块

1     Properties
2  { 3 _Color ("Color", Color) = (1,1,1,1) 4 _MainTex ("Sequence Image", 2D) = "white" {} 5 _Speed("Speed", Range(1,100)) = 50 6 _HorizontalAmount ("Horizontal Amount",float) = 4 7 _VerticalAmount ("Vertical Amount",float) = 4 8 }

 

MainTex 对应着我们准备的序列帧图像,Speed 代表播放速度,HorizontalAmount 和 VerticalAmount 代表着图像在水平方向和竖直方向包含的关键帧图像个数。

 

II.定义 Tags

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

序列帧图像一般都是透明纹理,所以这里我们设置为 Transparent

 

III. 定义相关属性与做出声明

 1             Tags{"LightMode" = "ForwardBase"}
 2  ZWrite Off 3  Blend SrcAlpha OneMinusSrcAlpha 4 5  CGPROGRAM 6 #pragma multi_compile_fwdbase 7 #include "UnityCG.cginc" 8 #pragma vertex vert 9 #pragma fragment frag 10 11  fixed4 _Color; 12  sampler2D _MainTex; 13  float4 _MainTex_ST; 14 float _Speed; 15 float _HorizontalAmount; 16 float _VerticalAmount;

由于是半透明物体,所以我们关闭深度写入并开启混合。定义与 Properties 块中想匹配的属性

 

IV. 定义输入输出结构体

 1             struct a2v
 2  { 3  float4 vertex : POSITION; 4  float4 texcoord : TEXCOORD0; 5  }; 6 7 struct v2f 8  { 9  float4 pos : SV_POSITION; 10  float2 uv : TEXCOORD0; 11 };

 

这个 shader 中我们主要是计算关键帧的位置和纹理采样,所以输入输出结构体我们不需要太复杂

 

V. 定义顶点着色器

1             v2f vert(a2v v)
2  { 3  v2f o; 4 o.pos = UnityObjectToClipPos(v.vertex); 5 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); 6 return o; 7 }

 

 我们使用 TRANSFORM_TEX 来得到最终的纹理坐标。我们可以在 UnityCG.cginc 找到 TRANSFORM_TEX 的定义

1 // Transforms 2D UV by scale/bias property
2 #define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)

name##_ST.xy 代表缩放,name##_ST.zw 代表偏移,这里的 name##_ST 就是我们定义的 _MainTex_ST

 

VI. 定义片元着色器

 1             fixed4 frag(v2f i) : SV_Target
 2  { 3 float time = floor(_Time.y * _Speed); 4 float row = floor(time / _HorizontalAmount); 5 float colum = time - row * _HorizontalAmount; 6 7 half2 uv = i.uv + half2(colum, -row); 8 uv.x /= _HorizontalAmount; 9 uv.y /= _VerticalAmount; 10 11 fixed4 c = tex2D(_MainTex, uv); 12 c.rgb += _Color; 13 return c; 14 }

 

(1)定义时间变量,记录场景经历的时间,当然要记得乘上播放速度。其中 floor 函数是一个向下取整的函数,我们可以在MSDN上找到它的定义

(2)计算行列索引值。我们使用的序列帧图像是包含 n x n 张关键帧纹理的图像,所以可以把它当做 n x n 的数组。而行列索引值的计算也很好理解。

  • 时间 / 行个数 = 行索引
  • 时间 – 行个数 * 行索引  =  时间 % 行个数 ,即余数就是列索引

(3)利用索引值得到真正的采样坐标。

  • 在原先的 UV 加上一个由第2步求得的行列索引构建成的 half2 ,代表偏移。这个偏移值会随着时间而改变
  • 在采样之前要先进行等分,实际上相当于 UV原点 + 偏移量(行索引 / 行等分个数 , 列索引 / 列等分个数)

(4)最后进行采样并加上主颜色即可

 

 疑惑点:

  • 随着时间的增长,变量 time 不是会变得越来越大吗,同时 row 也会越来越大,当 row 很大的时候,采样不会出错吗?
  • 进行偏移时,为什么加的是 half2(colum,-row),而不是 half2(row,colum)?

 

解答点:

(1)

  • 随着时间增长,row 会越来越大,所以为了限制 UV 在可采样范围内,我们需要把序列帧图像 Wrap Mode 设置为 Repeat,如下图。
  • 在 Repeat 模式下,当 UV 值超过 1 时,会舍弃整数值,使用小数部分进行采样,这样就会形成纹理重复或者说循环的效果
  • 可能有的读者想到使用 % 求余操作,如果只是单纯的求余有可能会导致部分少数的关键帧没有被采集到,因为在 uv 坐标数值上映射不到一些关键帧的位置。当然读者可以自行实现一下。查看效果。

(2)

  • 进行偏移时使用的是 half2(colum,-row) 是因为:对 x 轴进行偏移时,我们使用列索引来进行操作,对 y 轴进行偏移时,我们使用行索引来进行操作,所以是 (colum,row)。
  • 之所以 row 取负,是因为在 Unity 中进行采样时,竖直方向即 y 轴的坐标顺序是(从下往上递增),而我们所期待的播放顺序是(从上往下递增),两者相反,所以这里的 row 取负

 

VII. 最后关闭 FallBack 或者 Fallback “Transparent/VertexLit” 均可

 

VIII. 完整代码

 1 Shader "Unity/01-SequenceAnimation" 
 2 {
 3  Properties 4  { 5 _Color ("Color", Color) = (1,1,1,1) 6 _MainTex ("Sequence Image", 2D) = "white" {} 7 _Speed("Speed", Range(1,100)) = 50 8 _HorizontalAmount ("Horizontal Amount",float) = 4 9 _VerticalAmount ("Vertical Amount",float) = 4 10  } 11  SubShader 12  { 13 Tags{"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"} 14 15  Pass 16  { 17 Tags{"LightMode" = "ForwardBase"} 18  ZWrite Off 19  Blend SrcAlpha OneMinusSrcAlpha 20 21  CGPROGRAM 22 #pragma multi_compile_fwdbase 23 #include "UnityCG.cginc" 24 #pragma vertex vert 25 #pragma fragment frag 26 27  fixed4 _Color; 28  sampler2D _MainTex; 29  float4 _MainTex_ST; 30 float _Speed; 31 float _HorizontalAmount; 32 float _VerticalAmount; 33 34 struct a2v 35  { 36  float4 vertex : POSITION; 37  float4 texcoord : TEXCOORD0; 38  }; 39 40 struct v2f 41  { 42  float4 pos : SV_POSITION; 43  float2 uv : TEXCOORD0; 44  }; 45 46  v2f vert(a2v v) 47  { 48  v2f o; 49 o.pos = UnityObjectToClipPos(v.vertex); 50 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); 51 return o; 52  } 53 54  fixed4 frag(v2f i) : SV_Target 55  { 56 float time = floor(_Time.y * _Speed); 57 float row = floor(time _HorizontalAmount); 58 59 float colum = time - row * _HorizontalAmount; 60 61 half2 uv = i.uv + half2(colum,-row); 62 uv.x /= _HorizontalAmount; 63 uv.y /= _VerticalAmount; 64 65 fixed4 c = tex2D(_MainTex, uv); 66 c.rgb += _Color; 67 return c; 68  } 69 70 71  ENDCG 72 73  } 74 75  } 76 Fallback "Transparent/VertexLit" 77 }

 

IX. 保存,回到 Unity,把准备好的序列帧图像赋予 MainTex 查看效果

 

 

2.3 总结

序列帧动画是一种很常见的应用,读者也许使用 UI 制作过序列帧动画,而本文则是侧重于 shader 的实现。原理也是十分的简单,只是对正确的 UV 坐标做纹理采样。不过需要注意一些细节之处,比如行列索引的相关计算,只要明白这一点,相信读者能十分轻松地理解本例。

 

 

三. 背景滚动

在笔者的童年时,曾玩过红白机,里面的游戏许多都是一些横版过关的游戏。在这种 2D 型游戏中,我们可以发现有许多场景中背景一直在滚动,营造了一种主角在移动的感觉。而在现今的 2D 游戏中,这种滚动的背景依旧是我们常用的,所以此处我们来介绍这种效果的 shader 实现。

先看一下我们要实现的效果:

 

实现这个效果我们使用了两张图像,读者可以在本文末端下载,也可以使用自行准备的图像

 

3.1 准备工作

(1)创建一个场景,去掉天空盒子

(2)创建一个 Quad,一个 Material,一个 shader,命名为 ScrollingBackground,Quad 最好调整为充满屏幕

(3)准备两张图像,一张 “远景”(Far),一张 “近景”(Near)

 

3.2 shader 实现

 

I. 定义 Properties 块

1     Properties {
2         _Color ("Color", Color) = (1,1,1,1) 3 _MainTex ("FarLayer ", 2D) = "white" {} 4 _DetailTex("NearLayer ", 2D) = "white" {} 5 _ScrollX ("Far layer scroll Speed",Float) = 1.0 6 _Scroll2X("Near layer scroll Speed",Float) = 1.0 7 _Multiplier ("Layer Multiplier",Float) = 1.0 8 }

_MainTex 代表远景图,这里我使用的是一张纯背景色的图像;_DetailTex 代表近景图,这里我使用的是一张有楼宇的图像;两个 _Scroll 代表了两张图像的滚动速度。_Multiplier 代表了纹理整体亮度,这个如果觉得没必要可以不写。

 

II. 定义相关属性和做出声明

 1 Tags{"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"}
 2  Pass 3  { 4 Tags{"LightMode" = "ForwardBase"} 5  ZWrite Off 6  Blend SrcAlpha OneMinusSrcAlpha 7 8  CGPROGRAM 9 #include "UnityCG.cginc" 10 #pragma multi_compile_fwdbase 11 #pragma vertex vert 12 #pragma fragment frag 13 14  fixed4 _Color; 15  sampler2D _MainTex; 16  float4 _MainTex_ST; 17  sampler2D _DetailTex; 18  float4 _DetailTex_ST; 19 float _ScrollX; 20 float _Scroll2X; 21 float _Multiplier;

 我们同样把它当做透明物体看待,关闭深度写入和开启混合,再定义相匹配的变量

 

III. 定义输入输出结构体

 1             struct a2v
 2  { 3  float4 vertex : POSITION; 4  float4 texcoord : TEXCOORD0; 5  }; 6 7 struct v2f 8  { 9  float4 pos : SV_POSITION; 10  float4 uv : TEXCOORD0; 11 };

这里只是简单的处理图片采样,所以输入输出结构体比较简单

 

IV. 定义顶点着色器

1     v2f vert(a2v v)
2  { 3  v2f o; 4 o.pos = UnityObjectToClipPos(v.vertex); 5 o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex) + frac(float2(_ScrollX, 0.0) * _Time.y); 6 o.uv.zw = TRANSFORM_TEX(v.texcoord, _DetailTex) + frac(float2(_Scroll2X, 0.0) * _Time.y); 7 return o; 8 }

 

我们使用一个插值寄存器存储两张纹理的坐标,两张纹理都进行了同样的操作:先回复到正确的纹理坐标,再在水平方向上进行偏移 。我们使用了 frac 函数进行偏移,有关 frac 函数的定义,我们可以在 MSDN 上找到

 

 

 

这个函数会返回参数 x 的小数部分,相当于在 0 ~ 1 之间循环,纹理会在水平方向上循环偏移

 

V. 定义片元着色器

1     fixed4 frag(v2f i) : SV_Target
2  { 3 fixed4 firstLayer = tex2D(_MainTex,i.uv.xy); 4 fixed4 secondLayer = tex2D(_DetailTex, i.uv.zw); 5 fixed4 c = lerp(firstLayer, secondLayer, secondLayer.a); 6 c.rgb *= _Multiplier; 7 c.rgb *= _Color.rgb; 8 return c; 9 }

片元着色器比较简单,主要是对两张纹理采样,然后进行混合

 

VI. 最后关闭 FallBack 或者 Fallback “VertexLit” 均可

 

VII. 完整代码

 1 Shader "Unity/02-ScrollingBackground" {
 2  Properties { 3 _Color ("Color", Color) = (1,1,1,1) 4 _MainTex ("FarLayer ", 2D) = "white" {} 5 _DetailTex("NearLayer ", 2D) = "white" {} 6 _ScrollX ("Far layer scroll Speed",Float) = 1.0 7 _Scroll2X("Near layer scroll Speed",Float) = 1.0 8 _Multiplier ("Layer Multiplier",Float) = 1.0 9  } 10  SubShader 11  { 12 Tags{"Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"} 13  Pass 14  { 15 Tags{"LightMode" = "ForwardBase"} 16  ZWrite Off 17  Blend SrcAlpha OneMinusSrcAlpha 18 19  CGPROGRAM 20 #include "UnityCG.cginc" 21 #pragma multi_compile_fwdbase 22 #pragma vertex vert 23 #pragma fragment frag 24 25  fixed4 _Color; 26  sampler2D _MainTex; 27  float4 _MainTex_ST; 28  sampler2D _DetailTex; 29  float4 _DetailTex_ST; 30 float _ScrollX; 31 float _Scroll2X; 32 float _Multiplier; 33 34 35 struct a2v 36  { 37  float4 vertex : POSITION; 38  float4 texcoord : TEXCOORD0; 39  }; 40 41 struct v2f 42  { 43  float4 pos : SV_POSITION; 44  float4 uv : TEXCOORD0; 45  }; 46 47  v2f vert(a2v v) 48  { 49  v2f o; 50 o.pos = UnityObjectToClipPos(v.vertex); 51 o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex) + frac(float2(_ScrollX, 0.0) * _Time.y); 52 o.uv.zw = TRANSFORM_TEX(v.texcoord, _DetailTex) + frac(float2(_Scroll2X, 0.0) * _Time.y); 53 return o; 54  } 55 56  fixed4 frag(v2f i) : SV_Target 57  { 58 fixed4 firstLayer = tex2D(_MainTex,i.uv.xy); 59 fixed4 secondLayer = tex2D(_DetailTex, i.uv.zw); 60 fixed4 c = lerp(firstLayer, secondLayer, secondLayer.a); 61 c.rgb *= _Multiplier; 62 c.rgb *= _Color.rgb; 63 return c; 64  } 65 66  ENDCG 67  } 68  } 69 FallBack "VertexLit" 70 }

 

 VIII. 回到 Unity ,把准备好的图像赋予 shader ,查看效果

 

 

3.3 总结

背景滚动是十分常用的技术,实现起来也是比较简单,只是对纹理坐标进行水平上的循环偏移,然后进行采样即可,关于视觉效果,读者则可以按照自己喜欢进行调参。 

 

四. 总结

本文介绍了两种纹理动画,在实现上思路相似,都是对 UV 值进行偏移修改,然后对纹理进行采样。纹理动画实现起来是比较简单的,与之相关的另外一种动画,称为顶点动画,我们将在下一篇博文中介绍这种动画效果并列出值得注意的事项。

虽然纹理动画并不复杂,但其仍然是我们常用的技术实现。本文篇幅不多,希望能对读者学 UV 动画这一知识点有所帮助。