第十章 让画面动起来(1)

1. UnityShader中的内置变量(时间篇)

动画效果往往都是把时间添加到一些变量的计算中,以便在时间变化时画面也可以随之变化。UnityShader提供了一系列关于时间的内置变量允许我们方便的在Shader中访问运行时间,实现各种动画效果。下表给出了这些内置的时间变量。

Unity内置的时间变量
名称 类型 描述
_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)
在后面的章节中,我们会使用上述变量来实现纹理动画和顶点动画。 ## 2. 纹理动画 纹理动画在游戏中的应用十分广泛。尤其在各种资源都比较局限的移动平台上,我们往往会使用纹理动画来代替复杂的粒子系统等模拟各种动画的效果。 ### 2.1 序列帧动画 最常见的纹理动画之一就是序列帧动画。序列帧动画的原理非常简单,它像放电影一样,依次播放一些列关键帧图像,当播放速度达到一定数值时,看起来就是一个连续的动画。它的优点在于灵活性很强,我们不需要任何物理计算就可以得到非常细腻的动画效果。而它的缺点也很明显,由于序列帧中每张关键帧图像都不一样,因此要制作一张出色的序列帧纹理所需要的美术工程量也比较大。 要想实现序列帧动画,我们首先要提供一张包含了关键帧图像的图像,如下图所示: ![在这里插入图片描述](https://img-blog.****img.cn/20190506141317580.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0NTYyMzU1,size_16,color_FFFFFF,t_70) 上述图像包含了8×8张关键帧图像,它们的大小相同,而且播放顺序为从左到右、从上到下。下图给出了不同时刻播放的不同效果: ![在这里插入图片描述](https://img-blog.****img.cn/20190506141505247.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0NTYyMzU1,size_16,color_FFFFFF,t_70) (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
}
}

由于序列帧图像通常包含了透明通道,因此可以被当成是一个半透明对象。在这里我们使用半透明的“标配”来设置它的SubShader标签,即把Queue和RenderType设置成Transparent,把IgnoreProjector设置为True。在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/_VertiaclAmount;
half2 uv=i.uv+half2(column,-row);
uv.x/=_HorizontalAmount;
uv.y/=_VerticalAmount;
fixed4 c =tex2D(_MainTex,uv);
c.rgb*=_Color;
return c;
}

要播放帧动画,从本质上说,我们需要计算出每个时刻需要播放的关键帧在纹理中的位置。而由于序列帧纹理都是按行按列排列的,因此这个位置可以认为是该关键帧所在的行列索引数。因此,在上面代码的前3行中我们计算了行列数,其中使用了Unity的内置时间变量_Time。_Time.y就是自该场景加载后所经过的时间。我们首先把_Time.y和速度属性_Speed相乘来得到模拟的时间,并使用CG的floor函数对结果值取整来得到整数时间time。然后我们使用time除以_HorizontalAmount的结果值的商来作为当前对应的行索引,除法结果的余数是列索引。接下来,我们需要使用行列索引值来构建真正的采样坐标。由于序列帧图像包含了许多关键帧图像,这意味着采样坐标需要映射到每个关键帧图像的坐标范围内。我们可以首先把原纹理坐标i.uv按行数和列数进行等分,得到每个子图像的纹理坐标范围。然后我们需要使用当前的行列数对上面的结果进行偏移,得到当前子图像的纹理坐标。需要注意的是,对竖直方向的坐标偏移需要使用减法,这是因为在Unity中纹理坐标竖直方向的顺序(从下到上逐渐增大)和序列帧纹理中的顺序(播放顺序是从上到下)是相反的。这对应了上面代码中注释掉的代码部分。我们可以把上述过程中的除法整合到一起,得到注释下方的代码。这样我们就得到真正的纹理采样坐标。
(5)最后我们把Fallback设置为内置的Transparent/VertexLit(也可以选择关闭Fallback):

Fallback"Transparent/VertexLit"

保存后返回场景,需要注意,由于是透明纹理,因此需要勾选该纹理的Alpha Is Transparency属性,赋给材质中的Image Sequence属性,并将Horizontal Amount和Vertical Amount设置为8(因为包含了8行8列的关键帧图像),完成后单击播放,并调整Speed属性,就可以得到一段连续的爆炸动画。

2.2 滚动背景

很多2D游戏都是用了不断滚动的背景来模拟游戏角色在场景中的穿梭,这些背景往往包含了多个层(layers)来模拟一种视差效果。而这些背景的实现往往就是利用了纹理动画。在本节中,我们将实现一个包含了两层的无限滚动的2D游戏背景。学习完本节后,我们可以得到类似下图的效果。单击运行后,就可以得到一个无限滚动的背景效果。
第十章 让画面动起来(1)
(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=mul(UNITY_MATRIX_MVP,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;
}

我们首先分别利用i.uv.xy和i.uv.zw对两张背景纹理进行采样。然后使用第二层纹理的透明通道来混合两张纹理,这使用了CG的lerp函数。最后我们使用_Multiplier参数和输出颜色进行相乘,以调整背景亮度。
(4)最后,我们把Fallback设置为内置的VertexLit(也可以选择关闭Fallback):

Fallback "VertexLit"