动画效果
动画效果需要引入时间变量,Unity内置的时间变量:
这些时间变量可以用来实现纹理动画和顶点动画
纹理动画
- 序列帧动画
序列帧动画是通过依次播放一系列关键帧图像,当播放速度达到一定数值时,看起来就像是一个连续的动画。灵活性较强,通过一张包含关键帧的图像可以得到比较细腻的动画效果,缺点在于需要大量时间制作包含关键帧的图像。
关键帧实现的关键在于,每个时刻计算该时刻下应该播放的关键帧的位置
完整代码
Shader "Custom/Chapter11_ImageSequenceAnimation" {
Properties{
_Color("Main Clolr",Color)=(1,1,1,1)
_MainTex("MainTex",2D)="white"{}
_HorizantalAmount("HorizantalAmount",Float)=4
_VerticalAmount("VerticalAmount",Float)=4
_Speed("Speed",Range(1,100))=30
}
SubShader{
Tags{"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
//关键帧动画中使用的关键帧图像一般包含透明通道,因此当成半透明来处理
Pass{
Tags{"LightMode"="ForwardBase"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _HorizantalAmount;
float _VerticalAmount;
float _Speed;
struct a2v{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
};
struct v2f{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
return o;
}
fixed4 frag(v2f i):SV_Target{
float time=floor(_Time.y*_Speed); //内置时间变量 _Time(t/20,t,2t,3t)
float row=floor(time/_HorizantalAmount);
float column=time-row*_VerticalAmount;
//根据播放的速度计算对应的序列帧行和列
//纹理中包含许多关键帧图像,将采样坐标映射到每一个关键帧的坐标范围内
//纹理中的采样方向与关键帧的播放顺序在Y方向是反的,因此Y坐标做减法
half2 uv=float2(i.uv.x/_HorizantalAmount,i.uv.y/_VerticalAmount);
uv.x+=column/_HorizantalAmount;
uv.y-=row/_VerticalAmount;
fixed4 c=tex2D(_MainTex,uv);
c.rgb*=_Color;
return c;
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}
- 滚动背景
滚动背景一般使用多个层以不同的速度进行滚动,滚动的关键在于使滚动方向上的纹理坐标的增量与时间变量相乘,这样随着时间的变化,滚动方向上的采样坐标不断变换,画面也能连续变化。
完整代码:
Shader "Custom/Chapter11_ScrollingBackground" {
Properties{
_MainTex("Base Layer",2D)="white"{}
_DetialTex("Second Layer",2D)="white"{}
_ScrollX("Base Layer Scroll Speed",Float)=1.0
_Scroll2X("Second Layer Scroll Speed",Float)=1.0
_Multiplier("Layer Multiplier",Float)=1
}
SubShader{
Pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _DetialTex;
float4 _DetialTex_ST;
float _ScrollX;
float _Scroll2X;
float _Multiplier;
struct a2v{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
};
struct v2f {
float4 pos:SV_POSITION;
float4 uv:TEXCOORD0;
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv.xy=TRANSFORM_TEX(v.texcoord,_MainTex)+frac(float2(_ScrollX,0)*_Time.y);
o.uv.zw=TRANSFORM_TEX(v.texcoord,_DetialTex)+frac(float2(_Scroll2X,0)*_Time.y);
//frac取小数函数,使取样坐标在[0,1]范围内,背景连续重复滚动
return o;
}
fixed4 frag(v2f i):SV_Target{
fixed4 baseLayer=tex2D(_MainTex,i.uv.xy);
fixed4 secondLayer=tex2D(_DetialTex,i.uv.zw);
fixed4 c=lerp(baseLayer,secondLayer,secondLayer.a);
c.rgb*=_Multiplier;
//_Multiplier用来控制整体亮度
return c;
}
ENDCG
}
}
FallBack "VertexLit"
}
- 顶点动画
游戏中通常使用顶点动画模拟飘动旗帜、湍流小溪的效果,其主要方式是使模型顶点随着时间变化而变化。
完整代码:
Shader "Custom/Chapter11_Water" {
Properties{
_Color("Main Color",Color)=(1,1,1,1)
_MainTex("MainTex",2D)="white"{}
_Magnitude("Magnitude",Float)=1
_Frequency("Frequency",Float)=1
_InvWaveLength("InvWaveLength",Float)=10
_Speed("Speed",Float)=0.5
}
SubShader{
Tags{"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
//为透明效果设置对应标签,这里“DisableBatching”的标签是关闭批处理,
//包含模型空间顶点动画的Shader是需要特殊处理的Shader,
//而批处理会合并所有相关的模型,这些模型各自的模型空间会丢失
//而顶点动画需要在模型空间对顶点进行偏移
Pass{
Tags{"LightMode"="ForwardBase"}
ZWrite Off
Blend SrcAlpha OneMinusDstAlpha
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "UnityCG.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _Magnitude;
float _Frequency;
float _InvWaveLength;
float _Speed;
struct a2v{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
};
struct v2f{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
};
v2f vert(a2v v){
v2f o;
float4 offset;
offset.yzw=float3(0.0,0.0,0.0);
offset.x=sin(_Frequency*_Time.y+v.vertex.x*_InvWaveLength+v.vertex.y*_InvWaveLength+v.vertex.z*_InvWaveLength)*_Magnitude;
//在顶点进行空间变换前,对x分量进行正弦操作
o.pos=UnityObjectToClipPos(v.vertex+offset);
o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
o.uv+=float2(0.0,_Time.y*_Speed);
//进行纹理动画
return o;
}
fixed4 frag(v2f i):SV_Target{
fixed4 c=tex2D(_MainTex,i.uv);
c.rgb*=_Color.rgb;
return c;
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}
- 广告牌技术
广告牌技术是另一种常见的顶点动画。广告牌会根据视角方向来旋转一个被纹理着色的多边形,使多边形看起来始终朝着摄像机。
广告牌技术的本质是构建旋转矩阵。计算过程中先根据初始计算得到目标的表面法线(例如视角方向)和指向上的方向,这两者的方向往往不是垂直的,根据这两者的方向做叉乘得到垂直于两者的方向,再根据这个得到的方向,假定之前的两个方向某一个不变,计算另一个垂直的方向,这样得到三个正交的方向,计算过程类似于:
在广告牌技术中需要指定一个锚点,这个锚点在旋转过程中是固定不变的,以此来确定多边形在空间中的位置。
完整代码:
Shader "Custom/Chapter11_Billboarding" {
Properties{
_Color("Color",Color)=(1,1,1,1)
_MainTex("MainTex",2D)="white"{}
_VerticalBillboarding("VerticalBillboarding",Range(0,1))=1
}
SubShader{
Tags{"Queue"="Transparent" "IgnoreProjector"="True" "RendererType"="Transparent" "DisableBatching"="True"}
Pass{
Tags{"LightMode"="ForwardBase"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _VerticalBillboarding;
struct a2v{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
};
struct v2f{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
};
v2f vert(a2v v){
v2f o;
float3 center=float3(0,0,0);
//选择模型空间的原点作为变换锚点
float3 viewer=mul(unity_WorldToObject,float4(_WorldSpaceCameraPos,1));
//将模型空间的观察方向作为法线方向
float3 normalDir=viewer-center;
normalDir.y=normalDir.y*_VerticalBillboarding;
normalDir=normalize(normalDir);
//当_VerticalBillboarding的值为1时,法线方向固定为视角方向,
//当_VerticalBillboarding的值为0时,法线在Y方向上没有分量,那么向上的方向固定为(0,1,0),这样才能保证与法线方向垂直
float3 upDir=abs(normalDir.y)>0.999 ? float3(0,0,1) : float3(0,1,0);
//这里对向上方向是否与法向方向相平行,防止得到错误的叉乘结果
float3 rightDir=normalize(cross(normalDir,upDir));
upDir=normalize(cross(normalDir,rightDir));
float3 offset=v.vertex.xyz-center;
float3 localPos=center+rightDir*offset.x+upDir*offset.y+normalDir*offset.z;
o.pos=UnityObjectToClipPos(float4(localPos,1));
o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
return o;
}
fixed4 frag(v2f i):SV_Target{
fixed4 c=tex2D(_MainTex,i.uv);
c.rgb*=_Color.rgb;
return c;
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}
注意事项:
在顶点变换的Shader中需要做特殊处理,即关闭批处理,这样能防止Unity自动对相关模型做合并处理,从而丢失模型空间。
在对顶点进行变换后,如果想要得到正确的阴影,需要添加一个自定义的ShadowCaster Pass,否则得到的阴影会是变换前的阴影。自定义的Pass中,需要使用内置的宏。完整代码为:
Shader "Custom/Chapter 11_Vertex Animation With Shadow" { Properties { _MainTex ("Main Tex", 2D) = "white" {} _Color ("Color Tint", Color) = (1, 1, 1, 1) _Magnitude ("Distortion Magnitude", Float) = 1 _Frequency ("Distortion Frequency", Float) = 1 _InvWaveLength ("Distortion Inverse Wave Length", Float) = 10 _Speed ("Speed", Float) = 0.5 } SubShader { Tags {"DisableBatching"="True"} Pass { Tags { "LightMode"="ForwardBase" } Cull Off CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; float _Magnitude; float _Frequency; float _InvWaveLength; float _Speed; struct a2v { float4 vertex : POSITION; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert(a2v v) { v2f o; float4 offset; offset.yzw = float3(0.0, 0.0, 0.0); offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength + v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude; o.pos = mul(UNITY_MATRIX_MVP, v.vertex + offset); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); o.uv += float2(0.0, _Time.y * _Speed); return o; } fixed4 frag(v2f i) : SV_Target { fixed4 c = tex2D(_MainTex, i.uv); c.rgb *= _Color.rgb; return c; } ENDCG } Pass { Tags { "LightMode" = "ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_shadowcaster #include "UnityCG.cginc" float _Magnitude; float _Frequency; float _InvWaveLength; float _Speed; struct v2f { V2F_SHADOW_CASTER; }; v2f vert(appdata_base v) { v2f o; float4 offset; offset.yzw = float3(0.0, 0.0, 0.0); offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength + v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude; v.vertex = v.vertex + offset; TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) return o; } fixed4 frag(v2f i) : SV_Target { SHADOW_CASTER_FRAGMENT(i) } ENDCG } } FallBack "VertexLit" }