让画面动起来
UnityShader中的内置变量
Unity Shader 提供了一系列关于时间的内置变量来允许我们方便地在Shader中访问允许时间,实现各种动画效果。下表给出了这些内置的时间变量。
纹理动画
纹理动画在游戏中的应用非常广泛。尤其在各种资源都比较局限的移动平台上,我们往往会使用纹理动画来代替复杂的例子系统等模拟各种动画效果。
序列帧动画
想要实现序列帧动画,我们先要提供一张包含了关键帧图像的图像。如下图所示。
上图包含了8×8张关键帧图像,它们的大小相同,而且播放顺序为从左到右、从上到下、下图给出了不同时刻播放的不同动画效果。
在该着色器的实现上我们需要注意以下几个步骤:
- 声明四个属性,_MainTex是包含所有关键帧的纹理,_HorizontalAmount和_VerticalAmount分别代表了该图像在水平方向和垂直方向上包含的关键帧图像个数,_Speed用于控制序列帧动画的播放速度。
- 在顶点着色器中进行基本的顶点变换,并把顶点纹理坐标存储到结构体中。我们通过TRANSFORM_TEX函数来获取初始纹理坐标。
- 在片元着色器中,我们使用_Time.y函数和速度属性_Speed进行相乘来模拟时间,然后使用floor函数对结果取整来得到整数时间。
- 使用时间除以_HorizontalAmount来得到当前时间下对应的行索引,而列索引则为余数。
- 后面使用行列索引构建采样坐标。在计算竖直的偏移值时需要注意:由于Unity中纹理坐标竖直方向的顺序(从下到上逐渐增大)和序列帧纹理中的顺序(播放顺序从上到下)时相反的,所以计算竖直方向偏移量时要使用减法。
滚动背景
很多2D游戏都使用了不断滚动的背景来模拟游戏角色在场景中的穿梭,这些背景往往包含了多个层来模拟一种视觉效果。而这些背景的实现往往就是利用了纹理动画。我们将实现一个包含了两层的无限滚动的2D游戏背景。我们可以得到类似下图的效果。单击允许后,我们就可以得到一个无限滚动的背景效果。
实现上面效果,需要准备两张纹理,一张用于前景,一张用于后景;然后在顶点着色器中获取纹理顶点坐标后进行坐标偏移,最后我们在片元着色器中通过将前景透明度作为插值参考值的方式混合两张纹理得到最终的纹理效果。具体代码实现见下文。
顶点动画
流动的河流
该效果的原理是使用正弦函数来模拟水流的波动效果,在顶点着色器中使用正弦值对顶点坐标进行偏移得到对纹理扭曲效果。
需要特别注意的是:由于Unity的批处理会合并所有相关的模型,而这些模型各自的模型空间就会丢失,但我们使用的顶点偏移需要在物体的模型空间下进行偏移。所以我们需要声明一个标签——DisableBatching,告诉Unity需要取消对该Shader的批处理操作。
实现代码见下文。
广告牌技术
另一种常见的顶点动画就是广告牌技术。广告牌技术会根据视角方向来旋转一个被纹理着色的多边形(通常就是简单的四边形,这个多边形就是广告牌),是的多边形看起来好像总是面对着摄像机。广告牌技术被用于很多应用,比如渲染烟雾、运毒、闪光效果等。
思路
广告牌技术的本质就是构建旋转矩阵,而我们知道一个变换矩阵需要3个基向量。除此之外,我们还需要指定一个锚点。这个锚点在旋转的过程中是固定不变的,以此来确定多边形在空间中的位置。广告牌技术使用的基向量通常就是:
- 表面法线(normal)
- 指向上的方向(up)
- 指向右的方向(right)
广告牌技术的难点在于,如何根据需要来构建3个相互正交的基向量。计算过程通常是,我们首先会通过初始计算得到目标的表面法线(例如就是视角方向)和指向上的方向,而两者往往是不垂直的。但是,两者其中之一是固定的。例如:
- 当模拟草丛时,我们希望广告牌的指向上的方向永远是(0,1,0),而法线方向应该随视角变化。
- 当模拟粒子效果时,我们希望广告牌的法线方向是固定的,即总是指向视角方向,指向上的方向则可以发生变化。
计算方法
我们假设法线方向是固定的,首先,我们根据初始的表面法线和指向上的方向来计算出目标方向的指向右的方向(通过叉积操作):
right = up × normal
对其归一化后,再由法线方向和指向右的方向计算出正交的指向上的方向即可:
up' = normal × right
至此,我们就可以得到用于旋转的3个正交基了。下图给出了上述计算过程的图示。如果指向上的方向是固定的,计算过程也是类似的。
具体实现
- 首先我们在着色器中定义一个_VerticalBillboarding标签用于调整是固定法线还是固定指向上的方向
- 法线方向:将相机坐标转换到模型空间下后减去物体中心
- 向右方向:通过法线方向和指向上的方向(0,1,0)叉乘得到
- 向上方向:通过叉乘法线和向右方向后归一化得到
- 通过上面三个方向旋转正方形
效果
广告牌效果。左图显示了摄像机和5个广告牌之间的位置关系,摄像机是从斜上方向下 观察它们的。中间的图显示了当Vertical Restraints属性为1,即固定法线方向为观察视角时所 得到的效果,可以看出,所有的广告牌都完全面朝摄像机。右图显示了当Vertical Restraints 属性为0,即固定指向上的方向为(0, 1, 0)时所得到的效果,可以看出,广告牌虽然最大限度地面朝摄像机,但其指向上的方向并未发生改变
需要说明的是,在上面的例子中,我们使用的是Unity自带的Quad来作为广告牌,而不能使用自带的Plane。
这是因为,我们的代码是建立在一个竖直摆放的多边形的基础上的,也就是说,这个多边形的顶点结构需要满足在模型空间下是竖直排列的。只有这样,我们才能使用v.vertex来计算到正确的相对于中心的位置偏移量。
具体实现代码见下文。
顶点动画的阴影
问题:
如果我们想要对包含了顶点动画的物体添加阴影,那么如果像之前那样使用内置的Diffuse等包含的阴影Pass来渲染,就得不到正确的阴影效果(这里指的是无法向其他物体正确地投射阴影)。
原因:
这是因为,我们讲过Unity 的阴影绘制需要调用一个ShadowCaster Pass,而如果直接使用这些内置的ShadowCasterPass,这个Pass中并没有进行相关的顶点动画。
解决办法:
因此Unity 自定义的ShadowCaster Pass,而这个Pass中,我们将进行统一的顶点变换过程。需要注意的是,在前面的实现中,如果涉及半透明物体我们都把Fallback设置成了Transparent/VertexLit ,而Transparent/VertexLit没有定义ShadowCaster Pass,因此也就不会产生阴影。
需要注意的是,我们在ShadowCaster Pass采用的渲染通道与编译指令都与之前标准光照着色器中采用的有所不同,所以对应的阴影内置宏也不一样
实现代码见下文。
补充事项
- 在之前看到的那样,如果我们在模型空间下进行了一些顶点动画,那么批处理往往就会破坏这种动画效果。这时,我们可以通过SubShader的DisableBatching标签来强制取消对该Unity Shader的批处理。然而,取消批处理会带来一定的性能下降,增加了Draw Call,因此我们应该尽量避免使用模型空间下的一些绝对位置和方向来进行计算。
- 在广告牌的例子中,为了避免显示使用模型空间的中心来作为锚点,我们可以利用顶点颜色来存储每个顶点到锚点的距离值,这种做法在商业游戏中很常见。
实现代码
序列帧动画
Shader "Unity Shaders Book/Chapter 11/Image Sequence Animation"
{
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
{
// 渲染队列 = 透明对象 忽略投影 = True 渲染类型 = 透明对象
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);
// 对纹理坐标进行计算 TRANSFORM_TEX用于获取初始纹理坐标
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i): SV_Target
{
// 计算时间 floor:取整
float time = floor(_Time.y * _Speed);
// 计算行的索引
float row = floor(time / _HorizontalAmount);
// 计算垂直索引
float column = time - row * _HorizontalAmount;
// 使用行列索引值构建UV坐标,拆解后为下方注释部分代码
// 由于坐标竖直方向顺序和播放顺序相反,所以计算数值方向的坐标偏移需要取反
half2 uv = i.uv + half2(column, -row);
// half2 uv = float2(i.uv.x /_HorizontalAmount, i.uv.y / _VerticalAmount);
// uv.x += column / _HorizontalAmount;
// uv.y -= row / _VerticalAmount;
uv.x /= _HorizontalAmount;
uv.y /= _VerticalAmount;
// 对纹理进行采样
fixed4 c = tex2D(_MainTex, uv);
c.rgb *= _Color;
return c;
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}
滚动的背景
Shader "Unity Shaders Book/Chapter 11/Scrolling Background"
{
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
}
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);
// 获取初始纹理坐标后进行坐标偏移 TRANSFORM_TEX用于获取初始纹理坐标
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;
}
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 "Unity Shaders Book/Chapter 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
{
// 渲染队列 = 透明物体 忽略阴影 = True 渲染类型 = 透明物体 禁用批处理 = True
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;
// 除X轴外,其他轴不需要动画,所以置为0
offset.yzw = float3(0.0, 0.0, 0.0);
// 使用sin函数计算波动
// _Frequency控制波动频率,_InvWaveLength控制波长大小,_Magnitude控制波动大小
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
}
}
FallBack "Transparent/VertexLit"
}
广告牌技术
Shader "Unity Shaders Book/Chapter 11/Billboard"
{
Properties
{
// 用于显示的透明纹理
_MainTex ("Main Tex", 2D) = "white" { }
// 用于控制显示颜色
_Color ("Color Tint", Color) = (1, 1, 1, 1)
// 用于调整是固定法线还是固定指向上的方向
_VerticalBillboarding ("Vertical Restraints", Range(0, 1)) = 1
}
SubShader
{
// 渲染队列 = 透明物体 忽略阴影 = True 渲染类型 = 透明物体 禁用批处理 = True
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,则法线的y为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"
}
顶点动画的阴影
Shader "Unity Shaders Book/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
{
// 禁用批处理 = True
Tags { "DisableBatching" = "True" }
// 该Pass同流动效果Shader中的Pass
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
Pass
{
// 渲染通道 = 阴影投射
Tags { "LightMode" = "ShadowCaster" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 编译指令会保证Unity可以为相应类型的光照Pass生成所需的Shader变种
// 注意该指令与之前光照渲染中Pass中的"multi_compile_fwdbase"有所不同
#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"
}