纹理动画
纹理动画在游戏中的应用非常广泛。尤其在各种资源都比较局限的移动平台上,我们往往会使用纹理动画来代替复杂的粒子系统等模拟各种动画效果。
11.2.1序列帧动画
_Time是float4类型,
_Time.x表示当前时间 / 20,
_Time.y表示当前时间,
_Time.z表示当前时间 * 2,
_Time.w表示当前时间 * 3;
8*8的帧数量,设置播放速度30/s,需要2.6s播放完
Shader "MyShader/11-ImageSequenceAnimation"
{
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
}
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 "UnityCG.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _HorizontalAmount;
float _VerticalAmount;
float _Speed;
struct a2v {
float4 vertex : POSITION;
float2 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 {
//_Time场景加载经过时间 计算当前模拟的时间 即帧
float time = floor(_Time.y * _Speed);
//整除数量的到行列数, 得到对应模拟采样到小块纹理行列的时间 floor()向下取整
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;
// Uity 中纹理坐标竖直方向的顺序(从下到上逐渐增大)和序列帧纹理中的顺序(播放顺序是从上到下)是相反的。这
// i.uv是纹理的采样坐标,
half2 uv = i.uv + half2(column, -row);
//除以行列得到对应小纹理的坐标
uv.x /= _HorizontalAmount;
uv.y /= _VerticalAmount;
fixed4 c = tex2D(_MainTex, uv);
c.rgb *= _Color;
return c;
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}
我们需要使用行列索引值来构建真正的采样坐标。由于序列顿图像包含了许多关键帧图像,这意味着采样坐标需要映射到每个关键帧图像的坐标范围内。我们可以首先把原纹理坐标i.uv 按行数和列数进行等分,得到每个子图像的纹理坐标范围。然后,我们需要使用当前的行列数对上面的结果进行偏移,得到当前子图像的纹理坐标。需要注意的是,对坚直方向的坐标偏移需要使用减法,这是因为在 Unity 中纹理坐标直方向的顺序(从下到上逐渐增大)和序列帧纹理中的顺序(播放顺序是从上到下)是相反的。这对应了上面代码中注释掉的代码部分。我们可以把上述过程中的除法整合到一起,就得到了注释下方的代码。这样,我们就得到了真正的纹理采样坐标。
注意纹理坐标最大点是(1,1),这就是问什么要按照行列数缩放采样的原因
滚动背景
很多2D游戏都使用了不断滚动的背景来模拟游戏角色在场景中的穿梭,这些背景往往包含了多个层(layers)来模拟一种视差效果。而这些背景的实现往往就是利用了纹理动画。
我们首先分别利用iuvxy 和 uvzw 对两张背景纹理进行采样。然后,使用第二层纹理的明通道来混合两张纹理,这使用了CG的lerp 函数。
Shader "MyShader/Scrolling Background" {
Properties {
_MainTex ("Base Layer (RGB)", 2D) = "white" {} //纹理1
_DetailTex ("2nd Layer (RGB)", 2D) = "white" {} //纹理2
_ScrollX ("Base layer Scroll Speed", Float) = 1.0
_Scroll2X ("2nd layer Scroll Speed", Float) = 1.0
_Multiplier ("Layer Multiplier", Float) = 1 //控制纹理亮度
}
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
sampler2D _DetailTex;
float4 _MainTex_ST;
float4 _DetailTex_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.0) * _Time.y); //我们利用内置的Time.y变量在水平方向上对纹理坐标进行偏移
o.uv.zw = TRANSFORM_TEX(v.texcoord, _DetailTex) + frac(float2(_Scroll2X, 0.0) * _Time.y); //第二张纹理偏移
return o;
}
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;
}
ENDCG
}
}
FallBack "VertexLit"
}
顶点动画
如果一个游戏中所有的物体都是静止的,这样枯燥的世界恐怕很难引起玩家的兴趣。顶点动画可以让我们的场景变得更加生动有趣。在游戏中,我们常常使用顶点动画来模拟飘动的旗帜、湍流的小溪等效果。
河流的模拟是顶点动画最常见的应用之一。它的原理通常就是使用正弦函数等来模拟水流的波动效果。
(这里使用材质和网格渲染器生成模型,材质使用shader和纹理采样生成)
Shader "MyShader/11-Water"
{
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 {
// 设置合适和标签 DisableBatching关闭批处理
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
//
Pass {
Tags { "LightMode"="ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
//关闭剔除
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); //只移动x分量
//对于顶点来说只在一个方向移动
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"
}
实现这个效果,一方面要在顶点着色器对模型进行顶点变形,另一方面根据时间调整纹理采样速度。
广告牌
广告牌技术会根据视角方向来旋转一个被纹理着色的多边形(通常就是简单的四边形,这个多边形就是广告牌),使得多边形看起来好像总是面对着摄像机。广告牌技术被用于很多应用,比如渲染烟雾、云朵、闪光效果等。
广告牌技术的本质就是构建旋转矩阵,而我们知道一个变换矩阵需要3个基向量。广告牌技术使用的基向量通常就是表面法线(normal)、指向上的方向(up)以及指向右的方向(right)除此之外,我们还需要指定一个锚点 (anchorlocation),这个锚点在旋转过程中是固定不变的,以此来确定多边形在空间中的位置。
我们假设法线方向是固定的,首先,我们根据初始的表面法线和指向上的方向来计算出目标方向的指向右的方向(通过叉积操作):right-upxnormal
广告牌应用1:比如原神中火焰效果需要固定向上的轴向,法线不根据视角而变化(不随俯仰角度变化)。
广告牌应用2:而有的粒子效果,我们需要法线方向固定为视角方向(整个片面永远面向摄像机)。
Shader "MyShader/11-BillboardShader"
{
Properties {
_MainTex ("Main Tex", 2D) = "white" {}
_Color ("Color Tint", Color) = (1, 1, 1, 1) //控制整体显示颜色
_VerticalBillboarding ("Vertical Restraints", Range(0, 1)) = 1 //调整固定法线/固定向上
}
SubShader {
// 取消对所有使用了该shader的模型的批量处理
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
Pass {
Tags { "LightMode"="ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
fixed _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;
// 当VerticalBillboarding为1时,意味着法线方向固定为视角方向
// 当VerticalBillboarding为0时,意味着向上方向固定为(0,1,0)
normalDir.y =normalDir.y * _VerticalBillboarding;
normalDir = normalize(normalDir);
// 得到粗略的向上方方向
// 为了防止法线方向和向上方向平行(如果平行,那么叉积得到的结果将是错误的)
float3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);
// 我们对法线方向的,分量进行判断,以得到合适的向上方向
float3 rightDir = normalize(cross(upDir, normalDir));
upDir = normalize(cross(normalDir, rightDir));
// 使用这三个分量旋转得到新的顶点位置
float3 centerOffs = v.vertex.xyz - center;
float3 localPos = center + rightDir * centerOffs.x + upDir * centerOffs.y + normalDir * centerOffs.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"
}
法线方向固定广告牌
向上方向固定广告牌
动画注意事项
1 取消批处理会带来一定的性能下降
增加了draw call,应该尽量避免使用模型空间下的一些绝对位置和方向来进行计算。在广告牌的例子中,为了避免显式使用模型空间的中心来作为锚点,我们可以利用顶点颜色来存储每个顶点到锚点的距离值,这种做法在商业游戏中很常见
2 添加阴影的问题
如果我们想要对包含了顶点动画的物体添加阴影,那么如果仍然像 9.4 节中那样使用内置的 Difuse 等包含的阴影 Pass 来染,就得不到正确的影效果(这里指的是无法向其他物体正确地投射阴影。这是因为,我们讲过 Unity 的阴影绘制需要调用一个ShadowCaster Pass,而如果直接使用这些内置的ShadowCaster Pass,这个Pass中并没有进行相关的顶点动画,因此Unity会仍然按照原来的顶点位置来计算阴影,这并不是我们希望看到的。
自定义Shadow Pass:
Shader "MyShader/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 {
// Need to disable batching because of the vertex animation
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 = 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
}
// Pass to render object as a shadow caster
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 {
//V2FSHADOWCASTER来定义阴影投射需要定义的变量
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;
// unity计算阴影位置
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
return o;
}
fixed4 frag(v2f i) : SV_Target {
// Unity完成阴影投射
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
}
FallBack "VertexLit"
}
水流需要设置为两面才能够显示出投影。