第11章 让画面动起来

没有动画的画面往往让人觉得很无趣。在本章中,我们将会学习如何向Unity Shader 中引入时间变量,以实现各种动画效果。

在 11.1 节中,我们首先会介绍Unity Shader 内置的时间变量,在随后的章节中我们会使用这些时间变量来实现动画。

11.2 节会介绍两种常见的纹理动画,即序列帧动画和背景循环滚动动画。

在11.3 节,我们会学习使用顶点动画来实现流动的河流、广告牌等动画效果,并在最后给出一些在实现顶点动画时的注意事项。

11.1 Unity Shader 中的内置变量(时间篇)

动画效果往往都是把时间添加到一些变量的计算中,以便在时间变化时画面也可以随之变化。Unity Shader 提供了一系列关于时间的内置变量来允许我们方便地在Shader 中访问运行时间, 实现各种动画效果。表11.1 给出了这些内置的时间变量。

在后面的章节中,我们会使用上述时间变量来实现纹理动画和顶点动画。

11.2 纹理动画

纹理动画在游戏中的应用非常广泛。尤其在各种资源都比较局限的移动平台上,我们往往会使用纹理动画来代替复杂的粒子系统等模拟各种动画效果。

11.2.1 序列帧动画

最常见的纹理动画之一就是序列帧动画。序列帧动画的原理非常简单,它像放电影一样,依次播放一系列关键帧图像,当播放速度达到一定数值时,看起来就是一个连续的动画。它的优点在于灵活性很强,我们不需要进行任何物理计算就可以得到非常细腻的动画效果。而它的缺点也很明显,由于序列帧中每张关键帧图像都不一样,因此,要制作一张出色的序列帧纹理所需要的美术工程量也比较大。
要想实现序列帧动画,我们先要提供一张包含了关键帧图像的图像。在本书资源中,我们提供了这样一张图像
(Assets/Textures/Chapter11/Boom.png),如图11.1 所示。

上述图像包含了8x8 张关键帧图像,它们的大小相同,而且播放顺序为从左到右、从上到下。图11.2 给出了不同时刻播放的不同动画效果。

为了在Unity 中实现序列帧动画,我们需要做如下准备工作。
(1)新建一个场景。在本书资源中,该场景名为Scene_11_2_1 。在Unity 5.2 中,默认情况下场景将包含一个摄像机和一个平行光,并且使用了内置的天空盒子。在Window → Lighting → Skybox 中去掉场景中的天空盒子。
(2)新建一个材质。在本书资源中,该材质名为ImageSequenceAnimationMat 。
(3)新建一个Unity Shader。在本书资源中,该Shader 名为Chapter11-ImageSequenceAnimation。把新的Shader 赋给第2 步中创建的材质。
(4)在场景中创建一个四边形( Quad ),调整它的位置使其正面朝向摄像机,并把第2 步中的材质拖给曳它。
上述序列帧动画的精髓在于,我们需要在每个时刻计算该时刻下应该播放的关键帧的位置,并对该关键帧进行纹理采样。打开新建的Chapter11-ImageSequenceAnimation,删除原有的代码,并添加如下关键代码。
(1)我们首先声明了多个属性,以设置该序列帧动画的相关参数:
	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
	}
_Main_Tex 就是包含了所有关键帧图像的纹理。_HorizontalAmount 和 _VerticalAmount 分别代表了该图像在水平方向和坚直方向包含的关键帧图像的个数。而 _Speed 属性用于控制序列帧动画的播放速度。
(2)由于序列帧图像通常是透明纹理,我们需要设置Pass 的相关状态,以渲染透明效果:
	SubShader {
		Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
		
		Pass {
			Tags { "LightMode"="ForwardBase" }
			
			ZWrite Off
			Blend SrcAlpha OneMinusSrcAlpha
由于序列帧图像通常包含了透明通道,因此可以被当成是一个半透明对象。在这里我们使用半透明的“标配”来设置它的SubShader 标签,即把Queue 和RenderType 设置成Transparent,把IgnoreProjector 设置为True。在Pass 中,我们使用 _Blend 命令来开启并设置混合模式,同时关闭了深度写入。
(3)顶点着色器的代码非常简单,我们进行了基本的顶点变换, 并把顶点纹理坐标存储到了v2f 结构体里:
	v2f vert (a2v v) {  
		v2f o;  
		o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
		o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);  
		return o;
	}  
( 4 )片元着色器是我们的重头戏:
	fixed4 frag (v2f i) : SV_Target {
		float time = floor(_Time.y * _Speed);  
		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;
				
		fixed4 c = tex2D(_MainTex, uv);
		c.rgb *= _Color;
				
		return c;
	}
要播放帧动画,从本质来说,我们需要计算出每个时刻需要播放的关键帧在纹理中的位置。而由于序列帧纹理都是按行按列排列的,因此这个位置可以认为是该关键帧所在的行列索引数。因此, 在上面的代码的前3 行中我们计算了行列数,其中使用了Unity 的内置时间变量 _Time。由11.1 节可以知道,_Time.y 就是自该场景加载后所经过的时间。我们首先把 _Time.y 和速度属性 _Speed 相乘来得到模拟的时间, 并使用CG 的floor 函数对结果值取整来得到整数时间time 。然后, 我们使用time 除以 _HorizontalAmount 的结果值的商来作为当前对应的行索引,除法结果的余数则是列索引。接下来,我们需要使用行列索引值来构建真正的采样坐标。由于序列帧图像包含了许多关键帧图像, 这意味着采样坐标需要映射到每个关键帧图像的坐标范围内。我们可以首先把原纹理坐标i.uv 按行数和列数进行等分,得到每个子图像的纹理坐标范围。然后, 我们需要使用当前的行列数对上面的结果进行偏移, 得到当前子图像的纹理坐标。需要注意的是,对竖直方向的坐标偏移需要使用减法, 这是因为在Unity 中纹理坐标竖直方向的顺序(从下到上运渐增大)和序列帧纹理中的顺序(播放顺序是从上到下〉是相反的。这对应了上面代码中注释掉的代码部分。我们可以把上述过程中的除法整合到一起, 就得到了注释下方的代码。这样, 我们就得到了真正的纹理来样坐标。
( 5 )最后, 我们把Fallback 设置为内置的Transparent/VertexLit (也可以选择关闭Fallback ):
FallBack "Transparent/VertexLit"
保存后返回场景, 我们将Assetsrrextures/Chapter11 /Boom.png (注意, 由于是透明纹理,因此需要勾选该纹理的Alpha Is Transparency 属性〉赋给ImageSequenceAnimationMat 中的Image Sequence 属性,并将Horizontal Amount 和Vertical Amount 设置为8 (因为Boom.png 包含了8 行 8 列的关键帧图像),完成后单击播放, 并调整Speed 属性,就可以得到一段连续的爆炸动画。

11.2.2 滚动的背景

很多2D 游戏都使用了不断滚动的背景来模拟游戏角色在场景中的穿梭, 这些背景往往包含了多个层( layers )来模拟一种视差效果。而这些背景的实现往往就是利用了纹理动画。在本节中,我们将实现一个包含了两层的无限滚动的2D 游戏背景。本节使用的纹理资源均来自OpenGameArt ( http://opengameart.org )网站。在学习完本节后, 我们可以得到类似图11.3 中的效果。单击运行
后, 就可以得到一个无限滚动的背景效果。
为此,我们需要进行如下准备工作。
(1)新建一个场景, 在本书资源中, 该场景名为Scene_11_2_2。在Unity 5.2 中,默认情况下场景将包含一个摄像机和一个平行光, 并且使用了内置的天空盒子。在Window → Lighting → Skybox 中去掉场景中的天空盒子。由于本例模拟的是2D 游戏中的滚动背景, 因此我们需要把摄像机的投影模式设置为正交投影。
( 2)新建一个材质。在本书资源中, 该材质名为ScrollingBackgroundMat。
(3)新建一个Unity Shader。在本书资源中,该Shader 名为Chapter11-ScrollingBackground。把新的Shader 赋给第2 步中创建的材质。
( 4)在场景中创建一个四边形( Quad ) ,调整它的位置和大小,使它充满摄像机的视野范围,然后把第2 步中的材质拖曳给它。该四边形将用于显示游戏背景。
打开新建的Chapter11-ScrollingBackground , 删除原有的代码, 并添加如下关键代码。
( 1)我们首先声明了新的属性:

	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
	}
其中, _MainTex 和 _DetailTex 分别是第一层(较远〉和第二层(较近〉的背景纹理,而 _ScrollX 和 _Scroll2X 对应了各自的水平滚动速度。_Multiplier 参数则用于控制纹理的整体亮度。
( 2)我们的顶点着色器代码非常简单:
	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;
	}
我们首先进行了最基本的顶点变换,把顶点从模型空间变换到裁剪空间中。然后, 我们计算了两层背景纹理的纹理坐标。为此,我们首先利用TRANSFORM_TEX 来得到初始的纹理坐标。然后,我们利用内置的 _Time.y 变量在水平方向上对纹理坐标进行偏移,以此达到滚动的效果。我们把两张纹理的纹理坐标存储在同一个变量o.uv 中,以减少占用的插值寄存器空间。
(3)片元着色器的工作就相对比较简单:
	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;
	}
我们首先分别利用i.uv.xy 和i.uv.zw 对两张背景纹理进行采样。然后,使用第二层纹理的透明通道来混合两张纹理,这使用了CG 的lerp 函数。最后,我们使用 _Multiplier 参数和输出颜色进行相乘,以调整背景亮度。
(4)最后,我们把Fallback 设置为内置的VertexLit (也可以选择关闭Fallback) :
FallBack "VertexLit"
保存后返回场景,把本书资源中的Assets/Textures/Chapter11/Far_Background.png 和 Assets/Textures/Chapter11/Near_Background.png 分别赋给材质的Base Layer 和 2nd Layer 属性, 并调整它们的滚动速度(由于我们想要在视觉上模拟Base Layer 比 2nd Layer 更远的效果,因此Base Layer 的滚动速度要比2nd Layer 的速度慢一些)。单击运行后,就可以得到类似图11.3 中的效果。

11.3 顶点动画

如果一个游戏中所有的物体都是静止的,这样枯燥的世界恐怕很难引起玩家的兴趣。顶点动画可以让我们的场景变得更加生动有趣。在游戏中,我们常常使用顶点动画来模拟飘动的旗帜、涓流的小溪等效果。在本节中,我们将学习两种常见的顶点动画的应用一一流动的河流以及广告牌技术。在本节最后,我们还将给出一些顶点动画中的注意事项及解决方法。

11.3.1 流动的河流

河流的模拟是顶点动画最常见的应用之一。它的原理通常就是使用正弦函数等来模拟水流的波动效果。在本小节中,我们将学习如何模拟一个2D 的河流效果。在学习完本节后,我们可以得到类似图11.4 中的效果。当单击运行后,可以观察到河流不断流动的效果。

为此,我们需要进行如下准备工作。
( 1 )新建一个场景。在本书资源中,该场景名为Scene_11_3_1。在Unity 5.2 中,默认情况下场景将包含一个摄像机和一个平行光,并且使用了内置的天空盒子。在Window → Lighting → Skybox 中去掉场景中的天空盒子。由于本节模拟的是2D 效果,因此我们需要把摄像机的投影类型设置为正交投影。
(2)新建一个材质。在本书资源中,该材质名为WaterMat。由于本例需要模拟多层水流效果,我们还创建了WaterMat 和WaterMat2 材质。
(3)新建一个Unity Shader。在本书资源中,该Shader 名为Chapter11-Water。把新的Shader赋给第2 步中创建的材质。
(4)在场景中创建多个Water 模型,调整它们的位置、大小和方向,然后把第2 步中的材质拖曳给它们。
打开新建的Chapter11-Water,删除原来的代码,并添加如下关键代码。
(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。我们在3.3.3 节中介绍过该标签的含义:一些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。然后,我们利用 _Frequency 属性和内置的 _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;
	} 
( 6 )最后,我们把Fallback 设置为内置的Transparent/VertexLit (也可以选择关闭Fallback ):
FallBack "Transparent/VertexLit"
保存后返回场景,把Assets/Textures/Chapter11 /Water.psd 拖曳到材质的Main Tex 属性上, 并调整相关参数。为了让河流更加美观,我们可以复制多个材质并使用不同的参数,再赋给不同的Water 模型, 就可以得到类似图11.4 中的效果。

11.3.2 广告牌

另一种常见的顶点动画就是广告牌技术(Billboarding )。广告牌技术会根据视角方向来旋转一个被纹理着色的多边形(通常就是简单的四边形,这个多边形就是广告牌〉,使得多边形看起来好像总是面对着摄像机。广告牌技术被用于很多应用,比如渲染烟雾、云朵、闪光效果等。
广告牌技术的本质就是构建旋转矩阵,而我们知道一个变换矩阵需要3 个基向量。广告牌技术使用的基向量通常就是表面法线( normal )、指向上的方向( up )以及指向右的方向( right ) 。除此之外,我们还需要指定一个锚点(anchor location ), 这个锚点在旋转过程中是固定不变的,以此来确定多边形在空间中的位置。
广告牌技术的难点在于,如何根据需求来构建3 个相互正交的基向量。计算过程通常是,我们首先会通过初始计算得到目标的表面法线(例如就是视角方向〉和指向上的方向,而两者往往是不垂直的。但是,两者其中之一是固定的,例如当模拟草丛时,我们希望广告牌的指向上的方向永远是(0, 1,0),而法线方向应该随视角变化;而当模拟粒子放果时,我们希望广告牌的法线方向是固定
的,即总是指向视角方向,指向上的方向则可以发生变化。我们假设法线方向是固定的,首先,我们根据初始的表面法线和指向上的方向来计算出目标方向的指向右的方向(通过叉积操作):

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

下面,我们将在Unity 中实现上面提到的广告牌技术。在学习完本节后,我们可以得到类似图11.6 中的效果。

为此,我们需要进行如下准备工作。
( 1 )新建一个场景。在本书资源中,该场景名为Scene_11_3_2。在Unity 5 .2 中,默认情况下场景将包含一个摄像机和一个平行光,并且使用了内置的天空盒子。在Window → Lighting →Skybox 中去掉场景中的天空盒子。
(2)新建一个材质。在本书资源中,该材质名为BillboardMat 。
(3)新建一个Unity Shader . 在本书资源中,该Shader 名为Chapter11-Billboard 。把新的Shader赋给第2 步中创建的材质。
(4)在场景中创建多个四边形( Quad),调整它们的位置和大小,然后把第2 步中的材质拖曳给它们。这些四边形就是用于广告牌技术的广告牌。
打开新建的Chapter11-Billboard,删除原有的代码,添加如下关键代码。
( 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"}
在上面的设置中,我们除了为透明效果设置Queue 、IgnoreProjector 和RenderType 外,还设置了一个新的标签—— DisableBatcbing。我们在3.3.3 节中介绍过该标签的含义:一些SubShader在使用Unity 的批处理功能时会出现问题,这时可以通过该标签来直接指明是否对该SubShader使用批处理。而这些需要特殊处理的Shader 通常就是指包含了模型空间的顶点动画的Shader。这是因为,批处理会合并所有相关的模型,而这些模型各自的模型空间就会被丢失。而在广告牌技术中,我们需要使用物体的模型空间下的位置来作为锚点进行计算。因此。在这里需要取消对该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(_World2Object,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 = mul(UNITY_MATRIX_MVP, float4(localPos, 1));
( 5 )片元着色器的代码非常简单,我们只需要对纹理进行采样, 再与颜色值相乘即可:
	fixed4 frag (v2f i) : SV_Target {
		fixed4 c = tex2D (_MainTex, i.uv);
		c.rgb *= _Color.rgb;
				
		return c;
	}
( 6 )最后,我们把Fallback 设置为内置的Transparent/VertexLit (也可以选择关闭Fallback):
	FallBack "Transparent/VertexLit"

需要说明的是,在上面的例子中,我们使用的是Unity 自带的四边形( Quad ) 来作为广告牌,而不能使用自带的平面( Plane)。这是因为,我们的代码是建立在一个竖直摆放的多边形的基础上的,也就是说,这个多边形的顶点结构需要满足在模型空间下是竖直排列的。只有这样,我们才能使用 v.vertex 来计算得到正确的相对于中心的位置偏移量。
保存后返回场景,把本书资源中的Assets/Textures/Chapter11/star.png 拖曳到材质的 Main Tex中,即可得到类似图11.6 中的效果。

11.3.3 注意事项

顶点动画虽然非常灵活有效,但有一些注意事项需要在此提醒读者。
首先,如11.3.2 节看到的那样,如果我们在模型空间下进行了一些顶点动画,那么批处理往往就会破坏这种动画效果。这时,我们可以通过SubShader 的DisableBatching 标签来强制取消对该Unity Shader 的批处理。然而,取消批处理会带来一定的性能下降,增加了Draw Call,因此我们应该尽量避免使用模型空间下的一些绝对位置和方向来进行计算。在广告牌的例子中,为了避免显式使用模型空间的中心来作为锚点,我们可以利用顶点颜色来存储每个顶点到锚点的距离值,这种做法在商业游戏中很常见。
其次,如果我们想要对包含了顶点动画的物体添加阴影, 那么如果仍然像9.4 节中那样使用内置的Diffuse 等包含的阴影Pass 来渲染,就得不到正确的阴影效果(这里指的是无法向其他物体正确地投射阴影〉。这是因为,我们讲过Unity 的阴影绘制需要调用一个ShadowCaster Pass ,而如果直接使用这些内置的ShadowCaster Pass,这个Pass 中并没有进行相关的顶点动画, 因此Unity
会仍然按照原来的顶点位置来计算阴影,这并不是我们希望看到的。这时,我们就需要提供一个自定义的ShadowCaster Pass, 在这个Pass 中,我们将进行同样的顶点变换过程。需要注意的是,在前面的实现中,如果涉及半透明物体我们都把Fall back 设置成了Transparent/VertexLit,而Transparent/VertexLit 没有定义ShadowCaster Pass , 因此也就不会产生阴影(详见9.4.5 节〉。
在本书资源的Scene 11_3_3 场景中,我们给出了计算顶点动画的阴影的一个例子。在这个例子中,我们使用了11.3.1 节中的大部分代码,模拟一个波动的水流。同时,我们开启了场景中平行光的阴影效果,并添加了一个平面来接收来自“水流”的阴影。我们还把这个Unity Shader 的Fall back 设置为了内置的VertexLit ,这样Unity 将根据Fallback 最终找到VertexLit 中的ShadowCaster Pass 来渲染阴影。图11.7 给出了这样的结果。

可以看出, 此时虽然Water 模型发生了形变,但它的阴影并没有产生相应的动画效果。为了正确绘制变形对象的阴影, 我们就需要提供自定义的ShadowCaster Pass。读者可以在本书资源的Chapter11-VertexAnimation WithShadow 中找到对应的Unity Shader。使用该Shader得到的阴影效果如图11.8 所示。
在这个Shader 中,我们提供了一个ShadowCaster Pass , 相关代码如下:
	// 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_CAST_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即可。在15.1 节中,我们还会看到如何在阴影投射的Pass 中剔除片元,以实现自定义的透明度测试效果。
























  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值