1. 顶点动画的原理
顶点动画的原理是,在顶点着色器中按照一定的规则或函数计算得到一段偏移量对顶点进行移动,最后将改变位置后的顶点变换到裁剪空间进行后续的渲染工作。
可见,与纹理动画只是改变从纹理中哪一部分开始显示图案不同,顶点动画的原理是通过移动顶点改变物体的形状。
当我们在模型空间中操作顶点实现顶点动画时,如果使用顶点的绝对位置和方向,就需要将SubShader的 “DisableBatching” 标签设置为“True”,否可会因为合批丢失模型空间信息。更好的做法是避免使用绝对位置和方向,通过颜色通道存储顶点到模型原点的距离,这样就可以不必打断合批。
另外,由于顶点动画移动了顶点位置,因此在用到 ShadowCaster 相关的效果时(比如生成阴影),都会出现错误。如果由这种需求,就需要在 Shader 中手动实现对应的 ShadowCaster 的Pass。
2. 流动的河流
下面的例子通过顶点动画实现简单的河流的效果。原理为利用正弦函数的图形变换:f(X)= Asin(ωX+φ),于每一时刻计算正弦函数的值作为当前顶点的偏移量。
例子中使用了书里类似的模型网格,如下图所示:
代码中有两处不容易理解的地方,其实都跟使用的网格有关:
- 为什么偏移量是加到顶点的x分量上而不是y分量上?
因为代码中移动顶点的操作是在模型空间里进行的,因此需要按照模型空间的坐标轴决定移动哪一个分量,由上图可以看到,例子中使用的网格,控制上下起伏的轴是模型空间的x轴而不是y轴。 - 为什么代码中将顶点的xyz分量都乘以了 _InvWaveLen 后加到一起来进行平移效果?
流动效果需要在表示河流方向的轴上做正弦函数的平移,书中三个分量都处理是为了使代码在任意方向上流动都生效,其实在这个例子中没必要,观察上图可以看到,河流网格是在Z轴方向上延申的(依然要看模型空间的轴),因此实际只需要将 Z 分量乘以 _InvWaveLen 作为函数平移即可。
测试Shader如下:
Shader "MyShader/Chapter_11/Chapter_11_Flow_Shader"
{
Properties
{
_MainTex("MainTex", 2D) = "white"{}
_Color("Color", Color) = (1, 1, 1, 1)
_Magnitude("Magnituede", Float) = 1
_Frequency("Frequency", Float) = 1
_InvWaveLen("InvWaveLen", Float) = 1
_UVSpeed("UVSpeed", Float) = 1
}
SubShader
{
Tags{"Queue" = "Transparent" "RenderType" = "Transparent" "IgnoreProjector" = "true" "DisableBatching" = "True"}
Pass
{
Tags{"LightMode" = "ForwardBase"}
Cull Off
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
struct a2v
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
half _Magnitude;
half _Frequency;
half _InvWaveLen;
half _UVSpeed;
v2f vert(a2v v)
{
v2f o;
float4 _offset = float4(0, 0, 0, 0);
//_offset.x = _Magnitude * sin(_Frequency * _Time.y + _InvWaveLen * v.vertex.x + _InvWaveLen * v.vertex.y + _InvWaveLen * v.vertex.z);
_offset.x = _Magnitude * sin(_Frequency * _Time.y + _InvWaveLen * v.vertex.z);
o.pos = UnityObjectToClipPos(v.vertex + _offset);
o.uv = TRANSFORM_TEX(v.uv, _MainTex) + float2(0, frac(_UVSpeed * _Time.y));
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed4 _samplerColor = tex2D(_MainTex, i.uv);
return fixed4(_samplerColor.rgb * _Color.rgb, _samplerColor.a);
}
ENDCG
}
}
}
效果如下:
3. 广告牌技术
广告牌(Billboarding)是在实际项目中经常被使用到的一种技术,原理是通过摄像机与当前物体的连线方向,实时修改顶点位置,使物体整体发生旋转,从而做到让物体的某一面始终朝向摄像机。
在实际游戏中,Billboarding 通常分为两种效果:
- 一种是Y轴固定,即物体的Y轴不发生倾斜永远指向上方,只是在XZ平面跟随摄像机旋转,比如地上的草。
- 另一种是朝向固定,即物体的Z轴永远指向摄像机,使物体永远都保持正面正对摄像机,此时Y轴不再固定指向上方,有可能发生倾斜,比如天上的云。
要计算顶点旋转后的位置,我们只需要得到旋转后的模型空间,然后按照新模型空间在XYZ分量上分别做平移再加上新模型空间的原点偏移即可,因此最重要的便是构建旋转后的模型空间。
根据上面提到的两种效果,构建模型空间也有两种情况:
-
Y轴固定
此时 Y 轴固定为(0,1, 0),我们可以用摄像机与物体的连线方向作为 Z’,通过 cross(Y, Z’) 得到 X 轴方向,然后用 cross(X, Y) 得到真正的 Z 轴方向。 -
朝向固定
此时 摄像机与物体的连线方向即为 Z 轴,我们可以取(0, 1, 0)也就是图中的 Y‘ 作为参考轴,通过 cross(Y’, Z) 得到 X 轴方向,然后用 cross(Z, X) 得到真正的 Y 轴方向。
但其中会有一种特殊的情况,即 Z’ 于 Y 轴重合时,如下图:
这时候在计算时, Y(或Y‘)轴就不能直接用(0, 1, 0)而要改成用(0,0,1):
另外需要注意的一点是,在计算Z轴的时候,要以模型空间的原点与摄像机的连线作为方向,而不能直接取ObjSpaceViewDir,否则就变成每个顶点单独算出一个Z轴,导致各个顶点最后不在同一个坐标空间下,显示就会出错。
测试Shader如下:
Shader "MyShader/Chapter_11/Chapter_11_Billboarding_Shader"
{
Properties
{
_MainTex("MainTex", 2D) = "white"{}
_BillboardingFixZ("_BillboardingFixZ", Range(0, 1)) = 1
}
SubShader
{
Tags{"Queue" = "Transparent" "RenderType" = "Transparent" "IgnoreProjector" = "true" "DisableBatching" = "True"}
Pass
{
Tags{"LightMode" = "ForwardBase"}
Cull Off
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
struct a2v
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
//为 0 时相当于把Z轴放平了,因此后续不需要改变叉乘的顺序,依然保持Y轴向上,从而实现Y轴固定的效果
//为 1 时就是正常的固定朝向
fixed _BillboardingFixZ;
v2f vert(a2v v)
{
v2f o;
float3 _camDir = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1)).xyz;
_camDir.y *= _BillboardingFixZ;
float3 _z = normalize(_camDir);
float3 _y = abs(_z.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);
float3 _x = normalize(cross(_y, _z));
_y = normalize(cross(_z, _x));
float3 _pos = v.vertex.x * _x + v.vertex.y * _y + v.vertex.z * _z;
o.pos = UnityObjectToClipPos(float4(_pos, 1));
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
return tex2D(_MainTex, i.uv);
}
ENDCG
}
}
}
效果如下: