让画面动起来
1 Unity 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) |
2 纹理动画
2.1 序列帧动画
依次播放一系列关键帧,形成一段连续的动画
- 优点:灵活性强,不需要进行任何物理计算就可以得到非常细腻的效果
- 缺点:序列帧中每张关键图片都不一样,制作一张出色的序列帧纹理所需要的美术工程量也非常大
Shader "MyShader/Animation/ImageSequenceAnimation" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Texture", 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"
}
ZWrite off
Blend SrcAlpha OneMinusSrcAlpha
Pass {
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 {
// 计算行列数
float time = floor(_Time.y * _Speed); // floor函数对结果取整
float row = floor(time / _HorizontalAmount); // 根据时间获得行索引
float column = time - row * _VerticalAmount; // 根据时间获得列索引
half2 uv = i.uv + half2(column, -row); // 使用当前的行数对纹理采样的uv结果进行偏移
// 把原纹理坐标按行数和列数进行等分
uv.x /= _HorizontalAmount;
uv.y /= _VerticalAmount;
fixed4 c = tex2D(_MainTex, uv); // 进行纹理采样
c.rgb *= _Color;
return c;
}
ENDCG
}
}
Fallback "Transparent/VertexLit"
}
2.2 滚动的动画
Shader "MyShader/Animation/ScrollingBackground" {
Properties {
_MainTex ("Base Layer (RGB)", 2D) = "white" {} // 第一层背景纹理
_DetailTex ("2nd Layer (RGB)", 2D) = "white" {} // 第二层背景纹理
_ScrollX ("Base layer Scroll Speed", Float) = 1.0 // 第一层的滚动速度
_Scroll2X ("2nd layer Scrool Spedd", 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;
float4 _MainTex_ST;
sampler2D _DetailTex;
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);
// 计算纹理坐标,并利用 _Time.y 变量在水平方向上对纹理坐标进行偏移,达到滚动的效果
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"
}
3 顶点动画
顶点动画可以让场景变得更加生动有趣,我们常常使用顶点动画来模拟飘动的旗帜、湍流的小溪等
3.1 流动的河流
原理:使用正弦函数来模拟水流的波动效果
Shader "MyShader/Animation/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 {
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;
float2 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方向进行位移,将yzw设置为0
offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength +
v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude;
// 利用_Frequency属性和内置的_Time.y变量来控制正弦函数的频率
// 初相通过模型空间下的位置位置分量 * _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"
}
3.2 广告牌
另一种常见的顶点动画就是广告牌技术(Billboarding),广告牌技术会根据视角方向来旋转一个被纹理着色的多边形,使得多边形看起来好像总是面对着摄像机。广告牌技术被用于很多应用,比如渲染烟雾、云朵、闪光效果等
广告牌技术的本质就是构建一个旋转矩阵,我们知道一个变换矩阵需要 3 个基向量,广告牌技术使用的基向量通常就是表面法线(normal)、上方向(up)和右方向(right),这个锚点在旋转过程中是不变的,以此来确定多边形在空间中的位置
通过初始计算得到目标的表面法线和指向上的方向,两者往往不垂直,但其中之一是固定的
假设法线方向是固定的,根据初始的表面法线和指向上的方向来计算出目标方向的指向右的方向:
r
i
g
h
t
=
u
p
×
n
o
r
m
a
l
right = up \times normal
right=up×normal
对其归一化后,再由法线方向和指向右的方向计算出正交的指向上的方向即可:
u
p
′
=
n
o
r
m
a
l
×
r
i
g
h
t
up' = normal \times right
up′=normal×right
至此,我们就可以得到用于旋转的正交基了
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
Shader "MyShader/Animation/Billboard" {
Properties {
_MainTex ("Main Tex", 2D) = "white" {}
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_VerticalBillboarding ("Vertical Restraints", Range(0, 1)) = 1
}
SubShader {
Tags {
"Queue"="Transparent"
"IgnoreProjector" = "True"
"RenderType" = "Transparent"
"DisableBatching" = "True" // 表示不对该Shader使用批处理
}
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 _VerticalBillboarding;
struct a2v {
float4 vertex : POSITION;
float2 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时,意味着向上方向固定为(0, 1, 0)
float3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0); // 对y分量进行判断,得到合适的向上方向,防止法线方向和向上方向平行
float3 rightDir = normalize(cross(upDir, normalDir));
upDir = normalize(cross(normalDir, rightDir));
// 根据原始位置的位置相对于锚点的偏移量以及3个正交基向量,得到新的顶点位置
float3 centerOffset = v.vertex.xyz - center;
float3 localPos = center + rightDir * centerOffset.x + upDir * centerOffset.y + normalDir.z * centerOffset.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"
}
3.3 注意事项
- 如果在模型空间下使用顶点动画,就要通过把 DisableBatching 设置为 True 取消批处理
- 如果想要对包含了顶点动画的物体添加阴影,如果用内置的 Diffuse 等包含了阴影的 Pass 来渲染,就得不到正确的结果,需要自定义个 ShadowCasterPass,在该 Pass 中需要进行同样的顶点变换
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
}