本篇文章将会向unity shader中引入时间变量,以实现各种动画效果。
一、unity shader中的内置变量(时间)
动画效果往往都是把时间添加到一些变量的计算中,以便在时间变化时画面也可以随之变化。unity shader提供了一系列关于时间的内置变量来允许我们方便地在shader中访问运行时间,实现各种动画效果。
后面会使用上述时间变量实现纹理动画和顶点动画。
二、纹理动画
纹理动画在游戏应用非常广泛,尤其在各种资源都比较局限的移动平台上,我们往往会使用纹理动画来代理复杂的粒子系统等模拟各种动画效果。
2.1序列帧动画
这是最常见的纹理动画,序列帧动画原理很简单,像放电影一样,依次播放一系列关键帧图像,当播放速度达到一定数值时,看起来就是一个连续的动画它的优点在于灵活性很强,我们不需要进行任何物体计算就可以得到非常细腻的动画效果。而它的缺点也很明显,由于序列帧中每帧关键帧图像都不一样,因此制作一张出色的序列帧纹理所需要的美术工程量也比较大。
要想实现序列帧动画,需要先提供一张包含了关键帧图像的图像。如下图:
上述图像包含了8X8张关键帧图像,它们的大小相同,而且播放顺序为从左到右、从上到下。
实现:
1. 声明多个属性,以设置该序列帧动画的相关参数:
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 }
_MainTex就是包含了所有关键帧图像的纹理。_HorizontalAmount和_VerticalAmount分别代替了该图像在水平方向和竖直方向包含的关键帧图像的个数。而_Speed属性用于控制序列帧动画的播放速度。
2. 由于序列帧图像通常是透明纹理,我们需要设置Pass的相关状态,以渲染透明效果:
SubShader { Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"} Pass { Tags { "LightMode"="ForwardBase" } ZWrite Off Blend SrcAlpha OneMinusSrcAlpha
由于序列帧图像包含了透明通道,因此可以被当成是一个半透明对象。在这里我们使用半透明的渲染类型,pass中使用Blend命令来开启并设置混合模式,同时关闭了深度写入。
3. 顶点着色器进行了基本的顶点变换,并把顶点纹理坐标存储到了v2f结构体里:
v2f vert (a2v v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); return o; }
4. 片元着色器:
fixed4 frag (v2f i) : SV_Target { float time = floor(_Time.y * _Speed); float row = floor(time / _HorizontalAmount); float column = time - row * _HorizontalAmount; // half2 uv = float2(i.uv.x /_HorizontalAmount, i.uv.y / _VerticalAmount); // uv.x += column / _HorizontalAmount; // uv.y -= row / _VerticalAmount; half2 uv = i.uv + half2(column, -row); uv.x /= _HorizontalAmount; uv.y /= _VerticalAmount; fixed4 c = tex2D(_MainTex, uv); c.rgb *= _Color; return c; }
要播放帧动画,我们需要计算出每个时刻需要播放的关键帧在纹理中的位置。而由于序列帧纹理都是按列排列的,因此这个位置可以认为是关键帧所在的行列索引数。因此代码的前三行计算了行列数,其中使用了Unity的内置时间变量_Time。_Time.y就是自该场景加载后所经过的时间。首先把_Time.y和速度属性_Speed相乘来得到模拟的时间,并使用CG的floor函数对结果值取整来得到整数时间time。然后使用time除以_HorizontalAmount的结果值的商来作为当前对应的行索引,除法结果的余数则是列索引。接下来我们需要使用行列索引值来构建真正的采样坐标。由于序列帧图像包含了许多关键帧图像,这意味着采样坐标需要映射到每个关键帧图像的坐标范围内。我们可以首先把原纹理坐标i.uv按行数和列数进行等分,得到每个子图像的纹理坐标范围。然后我们需要使用当前的行列数对上面的结果进行偏移,得到当前子图像的纹理坐标。需要注意的是,对竖直方向的坐标偏移要使用减法,因为在unity中纹理坐标竖直方向的顺序(从下到上逐渐增大)和序列帧纹理中的顺序(播放顺序是从上到下)是相反的。这对应了上面代码中注释的部分,我们把上述过程中的除法整合到一起,得到了注释下方的代码,这样就得到了真正的纹理采样坐标。
运行就能看到一连串的爆炸动画。
2.2 滚动的背景
很多2D游戏都使用了不断滚动的背景来模拟游戏校色在场景中的穿梭,这些背景往往包含了多个层(layers)来模拟一种视差效果。而这些背景的实现往往是利用了纹理动画。我们将制作一个包含了2层的无限滚动的2D游戏背景。
1. 声明新的属性:
Properties { _MainTex ("Base Layer (RGB)", 2D) = "white" {} _DetailTex ("2nd Layer (RGB)", 2D) = "white" {} _ScrollX ("Base layer Scroll Speed", Float) = 1.0 _Scroll2X ("2nd layer Scroll Speed", Float) = 1.0 _Multiplier ("Layer Multiplier", Float) = 1 }
其中,_MainTex和_DetailTex分别是第一层(较远)和第二层(较近)的背景纹理,而_ScrollX和_Scroll2X对应了各自的水平滚动速度。_Multiplier参数则用于控制纹理的整体亮度。
2. 顶点着色器:
v2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex) + frac(float2(_ScrollX, 0.0) * _Time.y); o.uv.zw = TRANSFORM_TEX(v.texcoord, _DetailTex) + frac(float2(_Scroll2X, 0.0) * _Time.y); return o; }
首先进行基本的顶点变换,然后计算了两层背景纹理的纹理坐标。为此首先利用TRANSFORM_TEX来得到初始的纹理坐标,然后利用内置的_Time.y变量在水平方向上对纹理坐标进行偏移,以此达到滚动的效果,我们把两张纹理的纹理坐标存储在同一个变量o.uv中,以减少占用的插值寄存器空间。
3. 片元着色器:
fixed4 frag (v2f i) : SV_Target { fixed4 firstLayer = tex2D(_MainTex, i.uv.xy); fixed4 secondLayer = tex2D(_DetailTex, i.uv.zw); fixed4 c = lerp(firstLayer, secondLayer, secondLayer.a); c.rgb *= _Multiplier; return c; }
首先对两张背景纹理采样,然后使用第二层纹理的透明通道来混合两张纹理,这使用了CG的lerp函数。最后使用_Multiplier参数和输出颜色进行相乘,以调整背景亮度。
版权声明:本文为CSDN博主「小橙子0」的原创文章,
遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cgy56191948/article/details/101217945