第十章 让画面动起来(1)
1. UnityShader中的内置变量(时间篇)
动画效果往往都是把时间添加到一些变量的计算中,以便在时间变化时画面也可以随之变化。UnityShader提供了一系列关于时间的内置变量允许我们方便的在Shader中访问运行时间,实现各种动画效果。下表给出了这些内置的时间变量。
名称 | 类型 | 描述 |
_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) |
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)声明新的属性:
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"