unity Shader——shader中的动画效果:下篇(顶点动画)

顶点动画可以让我们的场景变得更加生动有趣。在游戏中,我们常常使用顶点动画来模拟飘动的旗帜,河流的效果。下面将学习两种常见的顶点动画应用:流动的河流以及广告牌技术,最后还给出一些顶点动画中的注意事项以及解决方法。

一、流动的河流

河流的模拟是顶点动画最常见的应用之一。原理通常是使用正弦函数等来模拟水流的波动效果。本节会展示如何模拟一个2D的河流效果。

unity Shader——shader中的动画效果:下篇(顶点动画)

1. 声明属性:

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用于控制河流纹理的移动速度。

2. 为透明效果设置合适的Subshader标签:

SubShader {
		// Need to disable batching because of the vertex animation
		Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}

我们除了为透明效果设置Queue、IgnoreProjector和RenderType外,还设置了一个新的标签——DisableBatching。一些subshader在使用unity批处理功能时会出现问题,这时可以通过该标签来直接指明是否对该Subshader使用批处理。而这些需要特殊处理的shader通常就是指包含了模型空间的顶点动画的shader。这是因为批处理会合并所有相关的模型,而这些,模型各自的模型空间就会丢失,本例我们需要在模型空间下对顶点位置进行偏移,因此这里取消对该shader的批处理操作。

3. 接着设置了Pass的渲染状态:

Pass {
			Tags { "LightMode"="ForwardBase" }
			
			ZWrite Off
			Blend SrcAlpha OneMinusSrcAlpha
			Cull Off

这里关闭了深度写入,开启并设置了混合模型,并关闭了剔除功能,这是为了让水流的每个面都能显示。

4. 在顶点着色器进行了相关的顶点动画:

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;
			}

首先计算顶点位移量。我们只希望对顶点x方向进行位移,因此yzw的位移量被设置为0,然后利用_Frequecy属性和内置的_Time.y变量来控制正弦函数的频率。为了让不同位置具有不同的位移,我们对上述结果加上了模型空间下的位置分量,并乘以_InvWaveLength来控制波长。最后对结果值乘以_Magnitude属性来控制波动幅度,得到最终的位移。剩下的工作只需要把位移量添加到顶点位置上,在进行正常的顶点变换即可。

在上面的代码中,还进行了纹理动画,即使用_Time.y和_Speed来控制在水平方向上的纹理动画。

5. 片元着色器的代码非常简单,只需要对纹理采样再添加颜色控制即可:

fixed4 frag(v2f i) : SV_Target {
				fixed4 c = tex2D(_MainTex, i.uv);
				c.rgb *= _Color.rgb;
				
				return c;
			} 

二、广告牌

另一种常见的顶点动画就是广告牌技术。广告牌技术会根据视角方向来旋转一个被纹理着色的多边形(通常就是简单的四边形,这个多边形就是广告牌),使得多边形看起来好像总是面对着摄像机。广告牌技术被用于很多应该,比如渲染烟雾、云朵、闪光效果等。

广告牌技术的本质就是构建渲染矩阵,而我们知道一个变换矩阵需要3个基向量。广告牌技术使用的基向量通常就是表面法线、指向上的方向以及指向右的方向。除此之外,我们还需要指定一个锚点,这个锚点在旋转过程中是固定不变的,以此来确定多边形在空间中的位置。

广告牌技术的难点在于,如何根据需求来构建3个相互正交的基向量。计算过程通常是,我们首先会通过初始计算得到目标的表面法线(例如就算视角方向)和指向上的方向,而两者往往是不垂直的。但是两者其中之一是固定的,例如当模拟草丛时,我们希望广告牌的指向上的方向永远是(0,1,0),而法线方向应该随视角变化;而当模拟粒子效果时,我们希望广告牌的法线方向是固定的,即总是指向视角方向,指向上的方向则可以发生变化。我们假设法线方向是固定的,首先根据初始的表面法线和指向上的方向来计算出目标方向和指向右的方向(通过叉积操作):

right=upxnormal

对其归一化后,再由法线方向和指向右的方向计算出正交的指向上的方向即可:

up'=normalxright

至此可以得到用于选择的3个正交基了,下图给出了上述计算过程的图示,如果指向上的方向是固定的,计算过程也是类似的。

unity Shader——shader中的动画效果:下篇(顶点动画)

下面我们将在unity中实现上面提到的广告牌技术。效果图如下:

unity Shader——shader中的动画效果:下篇(顶点动画)

1. 声明属性:

	Properties {
		_MainTex ("Main Tex", 2D) = "white" {}
		_Color ("Color Tint", Color) = (1, 1, 1, 1)
		_VerticalBillboarding ("Vertical Restraints", Range(0, 1)) = 1 
	}

其中_MainTex是广告牌显示的透明纹理,_Color用于控制显示整体颜色,_VerticalBillboarding则是用于调整是固定法线还是固定指向上的方向,即约束垂直方向的程度。

2. 设置subshader标签:

SubShader {
		// Need to disable batching because of the vertex animation
		Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}

再广告牌技术中,我们需要使用物体的模型空间下的位置来作为锚点进行计算。因此,在这里需要取消对该shader的批处理操作。

3. 设置Pass渲染状态:

Pass { 
			Tags { "LightMode"="ForwardBase" }
			
			ZWrite Off
			Blend SrcAlpha OneMinusSrcAlpha
			Cull Off

这里关闭了深度写入,开启并设置了混合模式,并关闭了剔除功能,这是为了让广告牌的每个面都能显示。

4. 顶点着色器是核心,所有的计算都是在模型空间下进行的。

我们首先选择模型空间的原点作为广告牌的锚点,并利用内置变量获取模型空间下的视角位置:

// Suppose the center in object space is fixed
				float3 center = float3(0, 0, 0);
				float3 viewer = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos, 1));

然后开始计算3个正交矢量。首先我们根据观察位置和锚点计算目标法线方向,并根据_VerticalBillboarding属性来控制垂直方向上的约束度。

float3 normalDir = viewer - center;
				// If _VerticalBillboarding equals 1, we use the desired view dir as the normal dir
				// Which means the normal dir is fixed
				// Or if _VerticalBillboarding equals 0, the y of normal is 0
				// Which means the up dir is fixed
				normalDir.y =normalDir.y * _VerticalBillboarding;
				normalDir = normalize(normalDir);

当_VerticalBillboarding为1时,意味着法线方向固定为视角方向;当_VerticalBillboarding为0时,意味着向上固定为(0,1,0)。最后我们需要对计算得到的法线方向进行归一化操作来得到单位矢量。

接着我们得到了粗略的向上方向,为了防止法线方向和向上方向平行(如果平行,那么叉积得到的结果将是错误的),我们对法线方向的y分量进行判断,以得到合适的向上方向。然后根据法线方向和粗略的向上方向得到向右方向,并对结果进行归一化。但由于此时向上的方向还是不准确的,我们又根据准确的法线方向和向右方向得到最后的向上方向:

// Get the approximate up dir
				// If normal dir is already towards up, then the up dir is towards front
				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个正交基矢量,以计算得到新的顶点位置:

// Use the three vectors to rotate the quad
				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);

5. 片元着色器只需要对纹理机芯采样,再与颜色值相乘即可:

fixed4 frag (v2f i) : SV_Target {
				fixed4 c = tex2D (_MainTex, i.uv);
				c.rgb *= _Color.rgb;
				
				return c;
			}

6. Fallback:

FallBack "Transparent/VertexLit"

三、注意事项

顶点动画虽然非常灵活有效,但有一些注意事项需要知道。

如果我们在模型空间下进行了一些顶点动画,那么批处理往往就会破坏这种动画效果,这时我们通过subshader的DisableBatching标签来强制取消对该unity shader的批处理。然而取消批处理会带来一定的性能下降,增加了Draw Call。因此我们应该尽量避免使用模型空间下的一些绝对位置和方向来进行计算。在广告牌例子中,为了避免显式使用模型空间的中心来作为锚点,我们可以利用顶点颜色来存储每个顶点到锚点的距离值,这种做法在商业游戏中很常见。

其次,如果我们想要对包含了顶点动画的物体添加阴影,如果还使用内置的Diffuse等包含的阴影,就得不到正确的阴影效果(这里指的是无法向其他物体正确地投射阴影)。这时因为unity的阴影绘制需要调用一个ShadowCasterPass,而如果直接使用这些内置的ShadowCasterPass,这个Pass中并没有进行相关的顶点动画,因此unity会仍然按照原来的顶点位置来计算阴影,这并不是我们希望看到的。这时我们需要提供一个自定义的ShadowCasterPass,在这个Pass中,我们将进行同样的顶点变换过程。需要注意的是,在前面的实现中,如果涉及半透明物体我们都把Fallback设置成了Transparent/VertexLit,而这里面没有定义ShadowCasterPass,因此也就不会产生阴影。

下面给出一个计算顶点动画的阴影的一个例子。我们使用第一节流动的河流的大部分代码,并开启unity的阴影效果,如果使用内置的VertexLit作为Fallback,这样unity会根据Fallback找到里面的ShadowCasterPass来渲染阴影,效果如下:

unity Shader——shader中的动画效果:下篇(顶点动画)

可以看出虽然Water模型发生了形变,但它的阴影并没有产生相应的动画效果,为了正确绘制变形对象的阴影,我们就需要提供自定义的ShadowCasterPass。如下:

// 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
		}

阴影投射的重点在于我们需要按正常Pass的处理来剔除片元或进行顶点动画,以便阴影可以和物体正常的渲染的结果相匹配。在自定义的阴影投射的Pass中,我们通常会使用unity提供的内置宏V2F_SHADOW_CASTER、TRANSFER_SHADOW_CASTER_NORMALOFFSET(旧版本中会使用TRANSFER_SHADOW_CASTER)和SHADOW_CASTER_FRAGMENT来计算阴影投射时需要的各种变量,而我们可以只关注自定义计算的部分。

上面代码中,我们首先在v2f结构体中利用V2F_SHADOW_CASTER来定义阴影投射需要定义的变量。随后在顶点着色器中,我们首先按之前对顶点的处理方法计算顶点的偏移量,不同的是,我们自己把偏移值加到顶点位置中变量中,在使用TRANSFER_SHADOW_CASTER_NORMALOFFSET来让unity完成剩下的事情,在片元着色器中,我们直接使用SHADOW_CASTER_FRAGMENT来让unity自动完成阴影投射的部分,把结果输出到深度图和阴影映射纹理中。

通过unity提供的这3个内置宏(unityCG.cginc文件中被定义),我们可以方便地自定义需要的投影映射的Pass,但由于这些宏利需要使用一些特定的输入变量,因此我们需要保证为他们提供了这些变量。例如TRANSFER_SHADOW_CASTER_NORMALOFFSET需要使用名称v作为输入结构体,v中需要包含顶点位置v.vertex和顶点法线v.normal的信息,我们可以直接使用内置的appdata_base结构体,它包含了这些必须的顶点变量。如果我们需要进行顶点动画,可以在顶点着色器中直接修改v.vertex,再传递给TRANSFER_SHADOW_CASTER_NORMALOFFSET即可。

版权声明:本文为CSDN博主「小橙子0」的原创文章,
遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cgy56191948/article/details/102674621

最新文章