第十一章 让画面动起来
这个系列其实是 复述一次Unity入门精要这本书
把每一章关于个人的理解加进来,加深记忆。
动画效果往往需要一些时间变量的存在, UnityShader 内置了一些变量使用,实现各种效果。各种变量都是float4,每个分量不同含义,shader常用技巧,不同分量存储值提高性能。
image
纹理动画
序列帧
原理就不进行介绍 。直接代码
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
}
//采样的序列帧图像都是透明 需要Pass设置通道
SubShader{
//在这里我们使用半透明的“标配”来设置它的SubShader 标签,即把Queue 和RenderType 设置成Transparent,把IgnoreProjector 设置为True。
Tags{ " Queue" = " Transparent" " IgnoreProjector" ="True" "RenderType" ="Transparent"}
Pass{
Tags{ "LightMode" = "ForwardBase"} //前向渲染
ZWrite Off //透明效果 需要关闭 深度写入
Blend SrcAlpha OneMinusSrcAlpha
}
}
顶点着色器的代码,基本的顶点转换 换到投影空间
v2f vert(a2v v){
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); //顶点纹理坐标存储到v2f 结构体 里面的实现值得看一下
return o;
}
片元着色器代码
其中使用了Unity 的内置时间变量 _Time。由11.1 节可以知道,_Time.y 就是自该场景加载后所经过的时间。我们首先把 _Time.y 和速度属性 _Speed 相乘来得到模拟的时间,
采样坐标需要映射到每个关键帧图像的坐标范围内。我们可以首先把原纹理坐标i.uv 按行数和列数进行等分,得到每个子图像的纹理坐标范围。
我们需要使用当前的行列数对上面的结果进行偏移, 得到当前子图像的纹理坐标。需要注意的是,对竖直方向的坐标偏移需要使用减法, 这是因为在Unity 中纹理坐标竖直方向的顺序(从下到上运渐增大)和序列帧纹理中的顺序(播放顺序是从上到下〉是相反的。这对应了上面代码中注释掉的代码部分。我们可以把上述过程中的除法整合到一起, 就得到了注释下方的代码。这样, 我们就得到了真正的纹理来样坐标。
fixed4 frag(v2f i): SV_Target{
float time =floor(_Time.y * _Speed); //Unity 的内置时间变量
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;
half2 uv =i.uv + half2(column, -row);//下面代码是上面代码公式的推导
uv.x /= _HorizontalAmount;
uv.y /= _VerticalAmount;
fixed3 c =tex2D(_MainTex, uv);
c.rgb *=_Color;
return c;
}
滚动的背景
用两个层(layer)的图片穿梭来模拟场景的变化。
Properties{
_MainTex("Base Layer(RGB)", 2D) = "white" {}
_DetailTex ("2nd Layer (RGB)", 2D) ="white"{}
_ScrollX ("Base layer Scroll Speed", Float) = 1.0
_Scroll2X ("2nd layerScroll Speed", Float) =1.0
_Multiplier (" Layer Multiplier", Float) =1 //用于控制纹理的整体亮度
}
顶点着色器代码
我们计算了两层背景纹理的纹理坐标。为此,我们首先利用TRANSFORM_TEX 来得到初始的纹理坐标。然后,我们利用内置的 _Time.y 变量在水平方向上对纹理坐标进行偏移,以此达到滚动的效果。我们把两张纹理的纹理坐标存储在同一个变量o.uv 中,以减少占用的插值寄存器空间。
v2f vert (a2v v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
//使用一个缓存器 分别存储 纹理 坐标
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;
}
片元着色器
两张纹理采样
使用第二层纹理的透明通道来混合两张纹理,这使用了CG 的lerp 函数。最后,我们使用 _Multiplier 参数和输出颜色进行相乘,以调整背景亮度
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;
}
FallBack "VertexLit"
顶点动画
这个厉害了,我们常常使用顶点动画来模拟飘动的旗帜、涓流的小溪等效果。在本节中,我们将学习两种常见的顶点动画的应用一一流动的河流以及广告牌技术。
模拟水面的代码
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
}
其中,_MainTex 是河流纹理,_Color 用于控制整体颜色,_Magnitude 用于控制水流波动的幅度,_Frequency 用于控制波动频率,_InvWaveLength 用于控制波长的倒数(_InvWaveLength 越大,波长越小),_Speed 用于控制河流纹理的移动速度。
和前面的序列帧一样,也需要透明效果,设置基础属性
SubShader {
// Need to disable batching because of the vertex animation
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
Pass {
Tags { "LightMode"="ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Cull Off //关闭剔除功能 ,让背后的面也可以显示
还设置了一个新的标签——DisableBatching。我们在3.3.3 节中介绍过该标签的含义:一些SubShader 在使用Unity 的批处理功能时会出现问题,这时可以通过该标签来直接指明是否对该SubShader 使用批处理。而这些需要特殊处理的Shader 通常就是指包含了模型空间的顶点动画的Shader。这是因为,批处理会合并所有相关的模型,而这些模型各自的模型空间就会丢失。而在本例中,我们需要在物体的模型空间下对顶点位置进行偏移。因此,在这里需要取消对该Shader 的批处理操作。
这里关闭了剔除功能。这是为了让水流的每个面都能显示
顶点着色器
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 = mul(UNITY_MATRIX_MVP, v.vertex + offset);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv += float2(0.0, _Time.y * _Speed);
return o;
}
1.因为要上下移动,就先计算了顶点位移量,只有x变量 其他yzw都是0,
2.我们利用 _Frequency 属性和内置的 _Time.y 变量来控制正弦函数的频率。
3.为了让不同位置具有不同的位移,我们对上述结果加上了模型空间下的位置分量,
4.并乘以 _InvWaveLength 来控制波长。
5.我们对结果值乘以 _Magnitude 属性来控制波动幅度,得到最终的位移。
6.我们只需要把位移量添加到顶点位置上, 再进行正常的顶点变换即可。
7.我们还进行了纹理动画,即使用 _Time.y 和 _Speed 来控制在水平方向上的纹理动画。
片元着色器代码
只是对纹理采样再添加颜色控制即可
fixed4 frag(v2f i) : SV_Target {
fixed4 c = tex2D(_MainTex, i.uv);
c.rgb *= _Color.rgb;
return c;
}
广告牌
广告牌技术(Billboarding ), 使一个被纹理着色的多边形,通常是4边行,让他看起来总是对着摄像机,比如 渲染烟雾,云朵,闪光效果等。
这个仔细想就是要实现矩阵的旋转,这需要3个基向量来构建一个旋转矩阵。通常使用的向量就是 表面法线(normal), 指向上的方向(UP)和指向右的方向(right)。
在这些向量计算过程中 因为总是有一个方向是不变的。一个有规律的变化。例如,两者其中之一是固定的,例如当模拟草丛时,我们希望广告牌的指向上的方向永远是(0, 1,0),而法线方向应该随视角变化;而当模拟粒子效果时,我们希望广告牌的法线方向是固定
的,即总是指向视角方向,指向上的方向则可以发生变化。
利用这两个有关联的向量求出一个平面 ,求出向右向量,归一后再求出了另外一个变化的确切方向。
图11.5 给出了上述计算过程的图示。如果指向上的方向是固定的,计算过程也是类似的。
两个有关联的向量 求面,再求垂直向量,在求准确的变化的量(因为变化的向量可以假设方向) Unity实现的效果
Properties{
_MainTex("Main Tex", 2D) ="white"{}
_Color ("Color Tint", Color) =(1,1,1,1)
_VerticalBillBoarding("Vertical Restraints", Range(0,1)) =1 //控制指向摄像机还是摄像机指向它
}
_VerticalBillboarding 则用于调整是固定法线坯是固定指向上的方向,即约束垂直方向的程度。
纹理有透明度 设置SubShader的状态
没有啥 特别的 就是一个关闭批处理,这个会合并模型,我还需要各自的模型空间下的位置来作为锚点进行计算。批处理会舍弃模型的模型空间的信息
SubShader {
// Need to disable batching because of the vertex animation
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
Pass {
Tags { "LightMode"="ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Cull Off
关闭剔除 ,因为我们会旋转等等 这样让每个面都会显示
顶点着色器
所有的计算都在模型空间下,选择模型空间的原点作为广告牌的锚点。内置变量获取模型空间下的视角位置
计算3个正交矢量
根据观察位置和锚点来计算目标的法线方向。根据 _VerticalBillboarding 属性来控制垂直方向上约束读(就是到底是法线动还是Up方向动)当 _VerticalBillboarding 为1 时, 意味着法线方向固定为视角方向;当 _VerticalBillboarding 为0 时,意味着向上方向固定为(0,1,0)。最后,我们需要对计算得到的法线方向进行归一化操作来得到单位矢量
float3 center = float3(0,0,0);
float3 viewer = mul(_World2Object,float4(_WorldSpaceCamerPos,1));
float3 normalDir = viewer - center ; //视点减物体中心 就是目标的法线方向
normalDir.y = normalDir.y * _VerticalBillBoarding;
normalDir = normalize(normalDir);//得到单位矢量
//防止法线方向和向上方向平行,当y=1时 就是向上的方向了所以Up就需要换一个方向。且只有法线固定才有这种可能
float3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);
//依次求出 向右,通过向右再准确的求出 向上 这里有个数学技巧,上面一旦向上固定是0时,就都固定了其实。下面都不用求了。
float3 rightDir = normalize(cross(upDir, normalDir));
upDir = normalize(cross(normalDir, rightDir));
// Use the three vectors to rotate the quad
//我们得到了所需的3 个正交基矢量。我们根据原始的位置相对于锚点的偏移量以及3个正交基矢量,以计算得到新的顶点位置:
float3 centerOffs = v.vertex.xyz - center;
float3 localPos = center + rightDir * centerOffs.x + upDir * centerOffs.y + normalDir * centerOffs.z;
o.pos = mul(UNITY_MATRIX_MVP, float4(localPos, 1));
片元着色器 就是上色 采样纹理结果 然后颜色融合
fixed4 frag (v2f i) : SV_Target {
fixed4 c= tex2D (_MainTex, i.uv);
c.rgb *= _Color.rgb;
return c;
}
这里有个坑 只能用Quad 作为广告牌 不能用平面(Plane)。这是因为上面计算顶点偏移量 时使用 的V.vertex 只有在满足模型空间是竖直排列时才可以。
性能点
我们可以通过SubShader 的DisableBatching 标签来强制取消对该Unity Shader 的批处理。然而,取消批处理会带来一定的性能下降,增加了Draw Call,因此我们应该尽量避免使用模型空间下的一些绝对位置和方向来进行计算。在广告牌的例子中,为了避免显式使用模型空间的中心来作为锚点,我们可以利用顶点颜色来存储每个顶点到锚点的距离值,这种做法在商业游戏中很常见。
上面的方法 对阴影效果是没有的。Unity 绘制阴影需要一个ShadowCaster Pass。直接使用内置的方法,不会有刚刚加的顶点动画。只会是一个四边形。需要重新自己写ShadowCaster Pass 内置的VertexLit ShadowCaster Pass下面是一个可以正常显示阴影的shader 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
}
阴影投射的重点在于我们需要按正常Pass 的处理来剔除片元或进行顶点动画,以便阴影可以和物体正常渲染的结果相匹配。在自定义的阴影投射的Pass 中,我们通常会使用Unity 提供的**内置宏V2F_SHADOW_CASTER 、
TRANSFER_SHADOW_CASTER_NORMALOFFSET ( 旧版本中会使用TRANSFER_SHADOW_CASTER )和SHADOW_CAST_FRAGMENT **来计算阴影投射时需要的各种变量,而我们可以只关注自定义计算的部分。