赞
踩
Unity Shader中内置变量(时间)
动画效果往往都是把时间添加到一些变量的计算中,以便时间变化画面跟着变化。UnityShader提供了一系列关于时间的内置变量来允许我们方便的在Shader中进行访问运行时间,实现动画效果。
纹理动画
序列帧动画
依次播放一系列关键帧图像,播放速度达到一定数值,看起来是一个连续动画,优点在于灵活性强,不需要进行任何物理计算就可以得到细腻的动画效果,缺点明显,序列帧需要美术工程量较大。
Shader "Unity Shaders Book/SequenceAnimation" { 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" "RenderType"="Opaque" "IgnoreProjector"="True" } Pass { Tags {"LightMode"="ForwardBase"} ZWrite Off Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" float4 _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: TEXCOORD1; }; 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); float row = floor(time/_HorizontalAmount); float column = time - row*_VerticalAmount; // 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; fixed4 c = tex2D(_MainTex,uv); c.rgb*=_Color; return c; } ENDCG } } FallBack Off }
需要在每个时刻计算当前时刻应该播放的关键帧的位置,并对关键帧进行纹理采样
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
}
_MainTex 包含关键帧图像纹理
_HorizontalAmount图像在水平方向包含关键帧图像个数
_VerticalAmount图像在竖直方向包含关键帧图像个数
_Speed控制序列帧播放动画速度
Tags { "Queue"="Transparent" "RenderType"="Opaque" "IgnoreProjector"="True" }
Pass {
Tags {"LightMode"="ForwardBase"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
序列帧包含透明通道,当做半透明对象,使用半透明对应的标签,在Pass中使用Blend命令来开启并设置混合模式,关闭深度写入。
fixed4 frag(v2f i) : SV_Target { float time = floor(_Time.y*_Speed); float row = floor(time/_HorizontalAmount); float column = time - row*_VerticalAmount; // 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; fixed4 c = tex2D(_MainTex,uv); c.rgb*=_Color; return c; }
需要计算每个时刻需要播放关键帧在纹理中的位置,由于序列帧纹理按行列排列的,因此这个位置可以认为是该关键帧所在的行列索引数,上面三行计算了行列数,其中使用了Unity内置的时间变量_Time,乘以_Speed模拟时间,使用floor函数对结果取整。然后用time除以/_HorizontalAmount,的结果值的商作为当前对应的索引,结果的余数是列索引。接下来,需要使用行列索引来构建真正的采样坐标。由于序列帧图像包含了许多关键帧图像,意味着采样坐标需要映射到每个关键帧图像的坐标范围内。首先把原纹理坐标uv按行列进行等分,得到每个子图像的纹理坐标范围。然后,需要使用当前的行列数对上面的结果进行偏移,得到子图像的纹理坐标。需要注意的是,对竖直方向的坐标偏移需要使用减法,因为Unity纹理坐标竖直方向的顺序从下到上逐渐增加。对应了上面代码注释掉的代码部分,可以把上述过程中的除法整合在一起,得到下面的代码。
滚动背景
Shader "Unity Shaders Book/ScrollingBackground" { Properties { _MainTex ("Base Layer",2D)="white"{} _DetailTex("2nd Layer",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 { "Queue"="Transparent" "RenderType"="Opaque"} Pass { Tags {"LightMode"="ForwardBase"} CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _DetailTex; float4 _DetailTex_ST; sampler2D _MainTex; float4 _MainTex_ST; float _ScrollX; float _Scroll2X; float _Multiplier; struct a2v { float4 vertex : POSITION; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float4 uv: TEXCOORD1; }; 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); 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 Off }
首先进行了最基本的顶点变换,把顶点从模型空间变换到裁剪空间。然后,计算了两层背景纹理的纹理坐标。为此首先的到初始纹理坐标,然后用内置的_Time在水平方向上对纹理坐标进行偏移,依次达到滚动的效果。最后对两张背景图进行采样,然后使用第二层纹理的透明通道来混合两张纹理,使用了lerp函数,最后使用_Multiplier参数跟输出颜色相乘,调整背景亮度。
顶点动画
顶点动画常用来模拟飘动的旗帜,湍流的小溪等。
流动的河流
使用正弦函数来模拟水流波动
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' Shader "Unity Shaders Book/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 指明是否对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; 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 } } FallBack "Transparent/VertexLit" }
希望对顶点的x方向进行偏移,因此yzw为0,然后利用_Frequency属性和内置的_Time变量控制正弦函数频率。为了让不同位置具有不同的位移,对上述结果加上了模型空间下的位置分量,并乘以_InvWaveLength来控制波长,最后乘以_Magnitude属性来控制波动幅度,得到最终位移,后面只需要把位移量添加到顶点位置上,再进行正常的顶点变换。
广告牌
根据视角方向来旋转一个被纹理着色的多边形,使得多边形看起来像总是面对着摄像机。常见渲染烟雾,云朵,闪光效果等
本质是构建旋转矩阵,一个变换矩阵需要3个基向量。广告牌使用的基向量通常是表面法线,指向上的方向,以及右的方向。除此之外还需要一个锚点,在旋转过程固定不变,以此确定多边形在空间中的位置。
难点在于,如果根据需求来构建3个相互正交的基向量。计算过程通常是,首先会通过初始计算得到目标的表面法线(视角方向)和指向上的方向,两者往往不是垂直的。但是,两者其中之一是固定的,例如模拟草丛时,希望广告牌的指向上的方向永远是(0,1,0)法线方向应该随视角变换:当模拟粒子效果时,希望广告牌的法线方向是固定的,总是指向视角方向,指向上的方向则可以变化。假设法线方向固定,首先根据初始的表面法线和指向上的方向来计算出目标方向的指向右的方向: right = up x normal。对其归一化后,再由法线方向和指向右的方向计算出正交的指向上的方向:up = normal x right。至此,可以得到用于旋转的3个正交基了:
Shader "Unity Shaders Book/BillBoard" { Properties { _MainTex ("Main Tex", 2D) = "white" {} //广告牌显示的透明纹理 _Color ("Color Tint", Color) = (1, 1, 1, 1) //整体颜色 _VerticalBillboarding("Vertical Restraints",Range(0,1))=1 //用于调整是固定法线还是固定指向上的方向,约束垂直方向的程度 } SubShader { // DisableBatching 指明是否对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 _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)); //开始计算3个正交矢量 根据观察位置和锚点计算目标法线方向 float3 normalDir = viewer-center; //当_VerticalBillboarding=1时 法线方向固定位视角方向 //当_VerticalBillboarding=0时 意味着向上方向固定(0,1,0) //最后对计算得到的法线方向进行归一化操作得到单位矢量 normalDir.y = normalDir.y+_VerticalBillboarding; normalDir = normalize(normalDir); //接着得到粗略了向上方向,为了防止法线方向和向上方向平行(如果平行,叉积得到的结果将是错误的) //对发现方向的y分量进行判断,以得到合适的向上方向。然后,根据法线方向和粗略的向上方向得到向右方向 //对结果进行归一化。由于此时向上的方向还是不准确的,根据准确的法线方向和向右方向得到最后的向上方向: 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)); //得到了所需的3个正交基矢量,根据原始的位置相对于锚点的偏移量以及3个正交基矢量,计算新的顶点位置 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.批处理会破坏顶点动画,取消批处理会带来一定性能的下降,增加DC,因此尽量避免使用模型空间下的一些绝对位置和方向来进行计算。
2.对包含了顶点动画的物体添加阴影,需要提供一个自定义的ShadowCaster Pass,在这个Pass将进行同样的顶点变换过程,fallback也要修改
Shader "Unity Shaders Book/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 { 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" }
阴影投射的重点在于需要按正常Pass的处理来剔除片元或进行顶点动画,以便阴影可以和物体正常渲染的结果匹配。在自定义的阴影投射Pass中,会使用Unity内置宏V2F_SHADOW_CASTER TRANSFER_SHADOW_CASTER_NORMALOFFSET 和SHADOW_CASTER_FRAGMENT 来计算阴影投射时需要的各种变量。首先在v2f结构体利用V2F_SHADOW_CASTER来定义阴影投射需要定义的变量。随后,在顶点着色器中,首先按之前对顶点的处理方法计算顶点的偏移量,不同的是,直接把偏移值加到顶点位置变量,再使用TRANSFER_SHADOW_CASTER_NORMALOFFSET 让Unity为我们完成剩下的事情。在片元着色器中,直接使用SHADOW_CASTER_FRAGMENT 自动完成阴影投射的部分,结果输出到深度图和阴影映射纹理中。
上面的宏需要使用一些特定的输入变量,所以输入结构体要用v。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。