【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 动画这一知识点有所帮助。

 

转载于:https://www.cnblogs.com/BFXYMY/p/10022826.html

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Unity Shader是一种用于在Unity引擎创建和控制图形渲染效果的编程语言。通过使用Unity Shader,开发人员可以自定义游戏各种物体的外观和行为,从而实现更加逼真和出色的视觉效果。 而热图(Heatmap)是一种用于显示某个区域内物体热度分布的视觉化工具。在游戏开发,热图通常用于统计和分析玩家在游戏的行为和偏好,以便开发人员可以根据这些数据进行游戏优化和改进。 为了创建一个热图效果,我们可以使用Unity Shader实现。首先,我们需要将游戏各个物体按照玩家与其的互动情况和频率进行区分,不同的行为和频率可以对应不同的颜色或者纹理。接着,我们可以在Shader根据这些信息来着色和渲染物体,以展示物体的热度分布。 在Shader,我们可以通过为物体添加一张热图纹理,并使用该纹理来表示物体的热度值。热图纹理可以是一张灰度图,不同的灰度值对应不同的热度。然后,我们可以使用纹理坐标和采样操作来获取每个像素对应的热度值,并根据这些值来着色和渲染物体。 除了使用纹理来表示热度分布,我们还可以使用其他的技术和效果来增强热图的可视化效果。例如,我们可以使用颜色渐变和透明度来形成平滑的过渡效果,以更好地显示物体的热度变化。我们还可以添加动画效果,使热图效果更加生动和有趣。 总结而言,Unity Shader可以用于创建热图效果,通过着色和渲染来展示物体的热度分布。这样的热图可以帮助开发人员分析游戏玩家的行为和偏好,从而优化和改进游戏的设计和内容。这些热图效果能够增强游戏的可视化效果,并提供有价值的数据供开发人员参考。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值