笔者使用的是 Unity 2018.2.0f2 + VS2017,建议读者使用与 Unity 2018 相近的版本,避免一些因为版本不一致而出现的问题。
前言
纯粹的静态美景宛如一张漂亮的贴图,而在游戏中,这种没有一点动画的情况往往是十分无趣且让人感到别扭的。所以本文会介绍一些简单的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 动画这一知识点有所帮助。
【Unity Shader】(十) ------ UV动画原理及简易实现的更多相关文章
-
Unity Shader 之 uv动画
Unity 动画 Unity Shader 内置时间变量 引入时间变量 名称 类型 描述 _Time float4 t是自该场景加载开始所经过的时间,4个分量分别是(t/20, t, 2t, 3t) ...
-
Unity Shader播放序列帧动画
Shader "LordShader/AnimateSprite" { Properties { _MainTint (,,,) //颜色属性,可以在u3d inspector面板 ...
-
Unity Shader学习笔记 - 用UV动画实现沙滩上的泡沫
这个泡沫效果来自远古时代的Unity官方海岛Demo, 原效果直接复制3个材质球在js脚本中做UV动画偏移,这里尝试在shader中做动画并且一个pass中完成: // Upgrade NOTE: r ...
-
unity shader序列帧动画代码,顺便吐槽一下unity shader系统
一.看到UNITY论坛里有些人求unity shader序列帧动画,写shader我擅长啊,就顺势写了个CG的shader.代码很简单,就是变换UV采样序列帧贴图,美术配置行数列数以及变换速度. Sh ...
-
Unity Shader序列帧动画学习笔记
Unity Shader序列帧动画学习笔记 关于无限播放序列帧动画的一点问题 在学shader的序列帧动画时,书上写了这样一段代码: fixed4 frag(v2f i){ // 获得整数时间 flo ...
-
【Unity Shader】(五) ------ 透明效果之半透明效果的实现及原理
笔者使用的是 Unity 2018.2.0f2 + VS2017,建议读者使用与 Unity 2018 相近的版本,避免一些因为版本不一致而出现的问题 [Unity Shader学习笔记](三) -- ...
-
Unity Shader 序列帧动画
shader中的序列帧动画属于纹理动画中的一种,主要原理是将给定的纹理进行等分,再根据时间的变化循环播放等分中的一部分. Unity Shader 内置时间变量 名称 类型 描述 _Time floa ...
-
Unity Shader - 消融效果原理与变体
基本原理与实现 主要使用噪声和透明度测试,从噪声图中读取某个通道的值,然后使用该值进行透明度测试. 主要代码如下: fixed cutout = tex2D(_NoiseTex, i.uvNoiseT ...
-
【Unity Shader】从NDC(归一化的设备坐标)坐标转换到世界坐标的数学原理
从NDC(归一化的设备坐标)坐标转换到世界坐标要点 参考资料 How to go from device coordinates back to worldspace http://feepingcr ...
随机推荐
-
async/await Task Timeout
async/await Task Timeout 在日常的电脑使用过程中,估计最难以忍受的就是软件界面"卡住""无响应",在我有限的开发生涯中一直都是在挑战 它 ...
-
PIC32MZ tutorial -- Watchdog Timer
Watchdog is a very necessary module for embedded system. Someone said that embedded system operates ...
-
升级到WP8必需知道的13个特性
http://www.cnblogs.com/sonic1abc/archive/2012/11/28/2792467.html Windows phone 8 SDK 已经发布一段时间了, 已经 ...
-
NoSQL与关系型数据库比较
虽然09年出现了比较激进的文章<关系数据库已死>,但是我们心里都清楚,关系数据库其实还活得好好的,你还不能不用关系数据库.但是也说明了一个事实,关系数据库在处理WEB2.0数据的时候,的确 ...
-
sass教程
sass教程 1. 使用变量; sass让人们受益的一个重要特性就是它为css引入了变量.你可以把反复使用的css属性值 定义成变量,然后通过变量名来引用它们,而无需重复书写这一属性值.或者,对于仅使 ...
-
CSS预处理器——Sass、LESS和Stylus实践
CSS(Cascading Style Sheet)被译为级联样式表,做为一名前端从业人员来说,这个专业名词并不陌生,在行业中通常称之为“风格样式表(Style Sheet)”,它主要是用来进行网页风 ...
-
phonegap的照相机API
1. Camera Api简单介绍 2. 拍照 3. 预览照片 一. Camera Api简单介绍 Camera选择使用摄像头拍照,或从设备相册中获取一张照片.图片以base64编码的 字符串或图片U ...
-
Android高斯模糊技术,实现毛玻璃效果(转)
本博客转自郭霖公众号:http://mp.weixin.qq.com/s?__biz=MzA5MzI3NjE2MA==&mid=2650235930&idx=1&sn=e328 ...
-
bootstrap模态对话框(最简单)
根据公司的需求,需要一个对话框来返回给客户的失败原因,刚刚开在百度上搜了老半天,嫩是没有搜索一个自己想要的,后来发送私信给一个博友,经过他哪里找到了自己想要的答案,废话不多说直接看源码: <!D ...
-
剑指Offer_编程题_20
题目描述 从上往下打印出二叉树的每个节点,同层节点从左至右打印. /* struct TreeNode { int val; struct TreeNode *left; struct TreeN ...