一、Unity Shader中的内置变量(时间篇)
动画效果往往都是把时间添加到一些变量的计算中,以便在时间变化的同时也可以随之变化。Unity shader提供了一系列关于时间的内置变量来允许我们方便地在Shader中访问运行时间,实现各种动画效果
二、纹理动画
常用于资源局限的移动平台、用于代替复杂的粒子系统等模拟出各种动画效果
1.序列帧动画
原理:
像放电影一样,依次播放一系列关键帧图像,当播放速度达到一定数值时,看起来就是一个连续的动画。
优点:灵活性强,不需要进行任何物理计算就可以得到非常细腻的动画效果。
缺点:由于序列帧中每张关键帧图像都不一样,因此,要制作一张出色的序列帧纹理所需要的美术工程量也比较大
实现:
需要一张包含了关键帧图像的图像。
大小相同,播放顺序从左到右、从上到下
设置序列帧动画的相关参数
Properties
{
_Color ("Color Tint", Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}包含了所有关键帧图像的纹理。
_HorizontalAmount ("Horizontal Amount", Float) = 4//_HorizontalAmount代表图像水平方向包含的关键帧图像的个数
_VerticalAmount ("Vertical Amount", Float) = 4//_VerticalAmount代表图像竖直方向包含的关键帧图像个数
_Speed ("Speed", Range(1, 100)) = 30 //控制序列帧动画的播放速度
}
渲染图像通常是透明纹理,我们需要设置Pass的相关状态,以渲染透明效果
由于序列帧图像包含了透明通道,因此可以被当成是一个半透明对象。
SubShader
{
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
Pass
{
Tags { "LightMode"="ForwardBase" }
ZWrite off
Blend SrcAlpha OneMinusSrcAlpha
设置SubShader标签:
"Queue"="Transparent" "RenderType"="Transparent"
把Queue和RenderType设置为Transparent 即半透明标配
把IgnoreProjector设置为True
设置Pass:
用Blend命令来开启并设置混合模式
ZWrite off 关闭深度写入
要播放序列帧动画,需要计算出每个时刻需要的关键帧在纹理中的位置,而由于序列帧纹理都是按行按列排列的,因此这个位置可以认为是该关键帧所在的行列的行列索引数。
前三行计算行列数
float time = floor(_Time.y * _Speed);
float row = floor(time / _HorizontalAmount);
float column = time - row * _HorizontalAmount;
_Time.y是该场景加载后所经历的时间
float time = floor(_Time.y * _Speed);
把_Time.y和速度属性_Speed相乘来得到模拟的时间
并使用CG的float函数对结果取整来得到time
float row = floor(time / _HorizontalAmount);
使用time除以_HorizontalAmount的结果值的商来作为对于的行索引
float column = time - row * _HorizontalAmount;
除法结果的余数则是列索引
half2 uv = i.uv + half2(column, -row);
uv.x /= _HorizontalAmount;
uv.y /= _VerticalAmount;
使用行列索引值来构建真正的采样坐标
序列帧图像包含了许多关键帧图像,采样坐标需要映射到每个关键帧图像的坐标范围内
先把原纹理坐标i.uv按行数和列数进行等分,得到每个子图像的纹理坐标范围
注意:对竖直方向的纹理坐标需要使用减法,因为在Unity中纹理坐标数值方向的顺序(从上到下逐渐增大)和序列帧纹理中的顺序(播放顺序是从上到下)是相反的
Shader "Shader/ImageSequenceAnimation"
{
Properties
{
_Color ("Color Tint", Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}//包含了所有关键帧图像的纹理。
_HorizontalAmount ("Horizontal Amount", Float) = 4//_HorizontalAmount代表图像水平方向包含的关键帧图像的个数
_VerticalAmount ("Vertical Amount", Float) = 4//_VerticalAmount代表图像竖直方向包含的关键帧图像个数
_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 appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float time = floor(_Time.y * _Speed);
float row = floor(time / _HorizontalAmount);
float column = time - row * _HorizontalAmount;
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"
}
2.滚动的背景
2d游戏常用不断滚动的背景来模拟游戏角色在场景中的穿梭,这些背景包含了很多个层(Layers)来来模拟一种视差。这些背景就是利用了纹理动画
Shader "Shader/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 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 appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
};
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);//顶点变换,裁剪空间
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex) + frac(float2(_ScrollX, 0.0) * _Time.y);
o.uv.zw = TRANSFORM_TEX(v.uv, _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);//使用第二张纹理的透明通道来混合两张纹理,使用lerp函数
c.rgb *= _Multiplier;
return c;
}
ENDCG
}
}
FallBack "VertexLit"
}
三、顶点动画
顶点动画常用来模拟飘动的旗帜、湍急的小溪等效果
1.流动的河流
原理:使用正弦函数等来模拟水流的波动效果。
DisableBatching:一些SubShader在使用Unity的批处理功能时,会出现问题,这时可以通过该标签来直接指明是否对该SubShader使用批处理。而这些需要特殊处理的Shader通常就是指包含了模型空间的顶点动画的Shader。这是因为,批处理会合并所有相关的模型,而这些模型各自的模型空间就会丢失。
Shader "Shader/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 appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert (appdata v)
{
v2f o;
//计算顶点偏移量
float4 offset;
offset.yzw = float3(0.0, 0.0, 0.0);//对x坐标进行偏移,因此yzw坐标设置为0
//_Frequency和_Time.y控制正弦函数的频率 加上上下位置分量 乘以_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.uv, _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"
}
2.广告牌
广告牌技术:
广告牌技术会根据视角方向来旋转一个被纹理着色的多边形(通常就是简单的四边形,这个多边形就是广告牌),使得多边形看起来好像总是面对着摄像机。
广告牌技术被应用于如:渲染烟雾、云朵、闪光效果等
本质:
构建旋转矩阵,我们知道一个矩阵三个基向量
我们先计算基向量通常就是表面法线(normal)、指向上的方向(up)、以及指向右的方向(right)
还需要指定一个锚点,这个锚点在旋转过程中是固定不变的,以此来确定多边形在空间中的位置
难点:如何根据需求构建3个相互正交的基向量。
计算过程:
通过初始计算得到目标的表面法线(就如视角方向)和指向上的方向,因为两种往往是不垂直的。(但两者之一是固定的,例如当模拟草丛时我们希望广告牌的指向上的方向永远是(0,1,0),而法线方向应该随视角变化;当模拟粒子效果时,希望法线方向是固定的,即总是指向视角方向,指向上的方向则可以发生变化。)
假设法线方向是固定的,先根据初始的表面法线和指向上的方向来计算出目标方向的指向右的方向(通过叉积操作):
对其归一化后,再由法线方向和指向右的方向计算出正交的指向上的方向即可:
我们得到了用于旋转3个正交基
Shader "Shader/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"}
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;
fixed _VerticalBillboarding;
struct appdata
{
float4 vertex : POSITION;
float4 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert (appdata 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;//根据_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.uv, _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.注意事项
如果在模型空间进行了一些顶点动画,那么批处理往往会破坏掉这种动画效果。这时通过SubShader的DisableBatching标签来强制取消对该UnityShader的批处理。但取消批处理会带来一定的性能下降,增加了Draw Call,因此要尽量避免使用模型空间下的一些绝对位置和方向进行计算。
如果想要对包含了顶点动画的物体添加阴影,我们就要提供一个自定义的ShaderCaster Pass,这个Pass中,进行顶点变换,注意:在设计半透明物体时,要把Fallback设置成Transparent/VertexLit,而Transparent/VertexLit没有定义ShaderCaster Pass,因此也就不会产生阴影