Unity Shader入门精要 第十一章——纹理动画、顶点动画

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

        动画效果往往都是把时间添加到一些变量的计算中,随着时间变换画面也可以随之变换。

11.2 纹理动画

11.2.1 序列帧动画

        序列帧动画也叫逐帧动画,是一种常见的动画形式,其原理是在“连续的 关键帧 ”中分解动画动作,也就是在时间轴的每帧上逐帧绘制不同的内容,使其连续播放而成动画。因此要制作一张出色的序列帧纹理需要的美术工程量也比较大。

        下面我们使用下面这张序列帧纹理来制作一个序列帧动画。

         实现的Shader如下:

Shader "MyShader/Chapter 11/Image Sequence Animation" {
	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 {
		//序列帧图像通常是透明纹理,所以我们要设置Pass的相关状态,渲染透明效果
		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 a2v {  
			    float4 vertex : POSITION; 
			    float2 texcoord : TEXCOORD0;
			};  
			
			struct v2f {  
			    float4 pos : SV_POSITION;
			    float2 uv : TEXCOORD0;
			};  
			
			v2f vert (a2v v) {  
				v2f o;  
				o.pos = UnityObjectToClipPos(v.vertex); //顶点变换 
				//采样
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);  //顶点纹理坐标存储到v2f结构体
				return o;
			}  
			
			fixed4 frag (v2f i) : SV_Target {
				//floor函数:向下取整
				float time = floor(_Time.y * _Speed);  //_Time.y是场景加载后所经过的时间,乘以速度得到模拟的时间
				//根据时间获取当前序列的行、列索引
				float row = floor(time / _HorizontalAmount); //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);//因为Unity纹理坐标是从下到上增大的,而纹理的顺序是上到下,所以y的地方要加负号
				uv.x /=  _HorizontalAmount;
				uv.y /= _VerticalAmount;

				tex2D是用来在一张贴图中对一个点进行采样的方法,返回一个float4,通过一个二维uv坐标在纹理上,获取该处值​​​​​​​。
				fixed4 c = tex2D(_MainTex, uv);
				c.rgb *= _Color;
				
				return c;
			}
			
			ENDCG
		}  
	}
	FallBack "Transparent/VertexLit"
}

        需要注意的是我们要把纹理图的Wrap Mode设置为Repeat(值超过1的坐标会舍弃整数部分,取小数进行计算),这样才可以正常播放。

11.2.2 滚动的背景

        使用不断滚动的背景来模拟游戏角色在场景中的穿梭,这些背景往往包括多个层来模拟一种视差效果。这些背景就可以利用纹理动画来实现。这节我们要实现类似下面的效果,有两层背景,可以设置每层背景的移动速度。

         shader代码如下:

Shader "MyShader/Chapter 11/Scrolling Background" {
	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 a2v {
				float4 vertex : POSITION;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				float4 uv : TEXCOORD0;
			};
			
			v2f vert (a2v v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);//顶点变换,模型空间->裁剪空间
				
				//TRANSFORM_TEX得到初始纹理坐标。利用_Time.y变量在水平方向上对纹理坐标进行偏移。,达到滚动的效果
				//frac函数返回标量或每个矢量中各分量的小数部分。记得这里的纹理也要设置为Repeat
				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);
				//lerp为插值函数,第一个参数表示起始值,第二个参数表示结束值。第三个参数表示插值的权重
				//如果第三个参数为1,就是返回secondLayer,0就是返回firstLayer,0.5就是返回前两者的平均值
				fixed4 c = lerp(firstLayer, secondLayer, secondLayer.a);
				c.rgb *= _Multiplier;//_Multiplier与输出颜色相乘,调整背景亮度
				
				return c;
			}
			
			ENDCG
		}
	}
	FallBack "VertexLit"
}

        对应纹理图片和参数设置如下:

        

11.3 顶点动画

11.3.1 流动的合理河流

        河流的模拟是顶点动画最常见的应用之一。原理就是使用正弦函数等来模拟水流的波动效果。本节我们会实现类似下面的效果,三层水流在上下波动,纹理也跟河水一样在流动

2d河流模拟
Shader "Unity Shaders Book/Chapter 11/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 {
		//“IgnoreProjector”=“True”告诉Unity3D,我们不希望任何投影类型材质或者贴图,影响我们的物体或者着色器。这个特性往往那个用在GUI上。程序默认“IgnoreProjector”=“Flase”。

		//透明混合是为了河流更加真实,同时加了个DisableBatching为True,
		//是关闭了批处理,因为开启批处理会导致模型空间的顶点不可操作,实际上是因为模型空间会消失,
		//因为批处理会合并所有的相关模型,而这些模型各自的模型空间就会丢失。顶点动画需要在物体的模型空间下对顶点位置进行偏移
		//因此我们需要关闭批处理,实际上所有顶点动画我们只要有使用到模型空间下的数据进行运算的都需要如此处理
		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);//我们希望只对顶点的x方向进行位移,所以yzw的位移量被设置为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"
}

11.3.2 广告牌

        广告牌技术(Billboarding)会根据视角方向来选择一个被纹理着色的多边形(通常就是简单的四边形),使多边形看起来总是面对着摄像机。经常运用于渲染烟雾、云朵、闪光效果等。

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

        如图,normal是根据我们的视角方向来决定的;然后up向量先根据noraml的y分量来取值,如果一般up就先默认取值(0,1,0)向上,如果normal的方向就非常接近甚至等于(0,1,0)的话,就把up设置为(0,0,1),这是为了避免up和normal平行。right向量就是通过normal和up叉乘的来的,如果前面两个向量平行的话,就没办法得到正确的right。最后因为up不一定和normal垂直,所以我们根据normal和right重新计算up,这样得到的三个基矢量才会互相垂直。

Shader "MyShader/Chapter 11/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 "Lighting.cginc"
			
			sampler2D _MainTex;
			float4 _MainTex_ST;
			fixed4 _Color;
			fixed _VerticalBillboarding;
			
			struct a2v {
				float4 vertex : POSITION;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
			};
			
			v2f vert (a2v v) {
				v2f o;
				
				// Suppose the center in object space is fixed
				float3 center = float3(0, 0, 0);//首先选择模型空间的原点作为广告牌的锚点。
				float3 viewer = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos, 1));//利用内置变量获取模型空间下的视角位置
				
				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

				//使用变量_VerticalBillboarding,控制垂直方向上的约束度
				//这个变量值为1的时候,意味着法线方向为固定视角方向,也就是怎么看,这个面都会垂直你的视角,这个物体都会是面对你的感觉,可以根据视角进行容易的旋转
				//为0时,意味着向上方向固定为(0,1,0),也就是,不管视角怎样变化,向上的方向是不会变的,也就是只会绕着模型空间的y轴旋转了
				normalDir.y =normalDir.y * _VerticalBillboarding; 
				normalDir = normalize(normalDir);//计算单位矢量
				// Get the approximate up dir
				// If normal dir is already towards up, then the up dir is towards front
				// 为了防止法线方向和向上方向平行(如果平行,那么后面计算的叉积的结果会是错误的),我们对y分量进行判断,以得到合适的向上向量
				float3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);//这里计算的up是不一定和normal垂直的,所以后面要重新计算一次
				float3 rightDir = normalize(cross(upDir, normalDir));//叉积计算right
				upDir = normalize(cross(normalDir, rightDir));//因为up不一定和normal垂直,所以通过right和normal叉乘再次计算up
				
				// 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);

				return o;
			}
		
_VerticalBillboarding值为1,法线固定
_VerticalBillboarding值为0,向上方向固定

猜你喜欢

转载自blog.csdn.net/buzhengli/article/details/132226053