【Unity Shaders】使用纹理效果 – 让精灵片动起来

本篇主要讲如何使用sprite sheet制作2D动画。在Unity中,实际上已经有很多插件可以完成这个工作,例如2dTookit,新版的Unity中支持2D应该也会提供类似的功能了。虽然如此,我还是希望通过这篇文章可以更深入地理解2D动画的原理。说到动画的原理,先提到一个名词,sprite atlas,也可以称为图像序列,实际上就是一张大图中包含了很多张小图,如下:

当我们按照一定速率滚动这张图时,就会发现图片动起来了,这我们应该都有经验,以前看小人书快速翻动时就发现小人在动。这里实际上也是这个原理。

准备工作

  1. 准备一张精灵片,你可以自己画一张也可以从网上下载一张,或是直接使用本书资源。这张图不需要太复杂,只需要包含一组图片序列来供滚动浏览就行。是本书中的实例图片(5084_Code / Unity assets / 5084_02_UnityAssets / Textures / Chapter02_SpriteSheet005.png):
  2. 创建一个新的着色器和一个新的材质,名称分别为AnimateSprites;
  3. 新建一个场景,名为AnimateSprites_Scene,在场景中添加平行光,创建一个新的平面,并把上一步中的物料设为其材质,将第一步中的图片拖到材质的图片上。最后如下所示:

实现

  1. 打开着色器编辑器,向属性  区域添加三个新的属性:
    	属性{
    		_MainTex(“Base(RGB)”,2D)=“white”{}
    		
    		//创建下面的属性
    		_TexWidth(“Sheet Width”,float)= 0.0
    		_CellAmount(“Cell Amount”,float)= 0.0
    		_Speed(“速度”,范围(0.01,32))= 12
    	}
  2. 在SubShader区域为每个新增的属性增加对应的引用,以便在代码中使用它们的值:
    		CGPROGRAM
    		#pragma表面冲浪Lambert
    
    		sampler2D _MainTex;
    		
    		//创建与内部属性的连接 
    		// CG程序
    		float _TexWidth;
    		float _CellAmount;
    		float _Speed;

    下面更改冲浪函数。

  3. 将_MainTex的UV坐标先存储到单独的变量中:
    			//让我们的UVs存储在一个单独的变量中
    			float2 spriteUV = IN.uv_MainTex;

    之后,我们将使用该变量计算新的UV坐标。

  4. 下面,我们需要计算每个小图的宽度,最后计算得到每个小图宽度占整体的百分比。
    原书中代码如下:

    			//让我们计算单个单元格的宽度
    			// sprite sheet,并获得每个cel占用的uv百分比。
    			float cellPixelWidth = _TexWidth / _CellAmount;
    			float cellUVPercentage = cellPixelWidth / _TexWidth;

    但通过观察代码可以发现,缩短为一行即可:

    			//让我们计算单个单元格的宽度
    			// sprite sheet,并获得每个cel占用的uv百分比。
    			float cellUVPercentage = 1.0 / _CellAmount;

    对于示例图片,其宽度为512,包含了9张小图,检查器中配置如下:

  5. 下面,通过得到系统时间来计算需要在原图上的偏移量,来得到不同的小图:
    			//让我们得到一个阶梯值,所以我们可以增加
    			// uv offset
    			float timeVal = fmod(_Time.y * _Speed,_CellAmount);
    			timeVal = ceil(timeVal);
  6. 最后,计算在X方向上的最终偏移量。
    原书中代码如下:

    			//将uv的前进动作以宽度为中心 
    			//每个单元格
    			float xValue = spriteUV.x;
    			xValue + = cellUVPercentage * timeVal * _CellAmount;
    			xValue * = cellUVPercentage;

    同样,观察cellUVPercentage的计算式,可以简化上述代码如下:

    			//将uv的前进动作以宽度为中心 
    			//每个单元格
    			float xValue = spriteUV.x;
    			xValue + = timeVal;
    			xValue * = cellUVPercentage;
  7. 应用最终偏移量,显示到平面上:
    			spriteUV = float2(xValue,spriteUV.y);
    			
    			half4 c = tex2D(_MainTex,spriteUV);
    			o.Albedo = c.rgb;
    			o.Alpha = ca;
最终着色器代码如下:
着色器“自定义/ AnimateSprites”{
	属性{
		_MainTex(“Base(RGB)”,2D)=“white”{}
		
		//创建下面的属性
		_TexWidth(“Sheet Width”,float)= 0.0
		_CellAmount(“Cell Amount”,float)= 0.0
		_Speed(“速度”,范围(0.01,32))= 12
	}
	SubShader {
		标签{“RenderType”=“不透明”}
		LOD 200
		
		CGPROGRAM
		#pragma表面冲浪Lambert

		sampler2D _MainTex;
		
		//创建与内部属性的连接 
		// CG程序
		float _TexWidth;
		float _CellAmount;
		float _Speed;

		struct输入{
			float2 uv_MainTex;
		};

		void surf(Input IN,inout SurfaceOutput o){
			//让我们的UVs存储在一个单独的变量中
			float2 spriteUV = IN.uv_MainTex;
			
			//让我们计算单个单元格的宽度
			// sprite sheet,并获得每个cel占用的uv百分比。
			float cellUVPercentage = 1.0 / _CellAmount;
			
			//让我们得到一个阶梯值,所以我们可以增加
			// uv offset
			float timeVal = fmod(_Time.y * _Speed,_CellAmount);
			timeVal = ceil(timeVal);
			
			//将uv的前进动作以宽度为中心 
			//每个单元格
			float xValue = spriteUV.x;
			xValue + = timeVal;
			xValue * = cellUVPercentage;
			
			spriteUV = float2(xValue,spriteUV.y);
			
			half4 c = tex2D(_MainTex,spriteUV);
			o.Albedo = c.rgb;
			o.Alpha = ca;
		}
		ENDCG
	} 
	FallBack“Diffuse”
}
保存后,点击运行即可看到动画效果。

解释

为了每刻只显示一张小图,我们需要将sprite atlas进行放缩,这就需要计算放缩比例cellUVPercentage。
float cellUVPercentage = 1.0 / _CellAmount;
事例中_CellAmount为9,则cellUVPercentage为0.11。
下面根据时间计算偏移量,这些偏移量随时间增大而且为整数,直到增大为小图的数目大小。
float timeVal = fmod(_Time.y * _Speed,_CellAmount);
timeVal = ceil(timeVal);
这需要使用CGFX的内置函数fmod。
如上所示,fmod函数将返回x除了y的余数。示例中,fmod函数将循环返回范围为0至9之间的小数。为了得到整数,再使用ceil函数向上取整。
下面这部分代码最难理解:
float xValue = spriteUV.x;
xValue + = timeVal;
xValue * = cellUVPercentage;

第一行首先声明一个新的变量x值,用于存储用于图片采样的X坐标。它首先被初始为冲浪函数的输入参数在的横坐标。类型为输入的输入参数在代表输入的纹理的UV坐标,范围为0到1.第二行向原值加上小图的整数偏移量,最后为了只显示一张小图,我们还需将x值乘以小图所占百分比cellUVPercentage。

为了理解,我们举例来说。假设此时 timeVal值为0,即显示第一张小图,则此时xValue的范围将会(0 + 0)* 0.11 = 0至(1 + 0)* 0.11 = 0.11 ,即第一张小图对应的贴图范围。时间推进,当timeVal值增大为1,即需要显示第二张小图时,xValue值范围将是 (0 + 1)* 0.11 = 0.11至(1 + 1)* 0.11 = 0.22,即第二张小图对应的贴图范围。这样就按顺序显示小图,从而让整个画面动起来了。

扩展

在上例中,我们只需要偏移X值即可但有时,X和ÿ方向都需要进行偏移来移动小图如下图。:
这时你只需要像移动X方向那样移动ý即可。虽然这样可以达到动画要求,但是这张在着色器端开发的方法会添加过多的着色器指令,从而造成程序性能下降。
为了改善这一情况,我们可以将帧偏移选择的代码转移到C#脚本中,从而让CPU来分担GPU的部分负担。这种同时使用CPU和GPU,让其配合相互工作的方法在优化性能方面非常有用。按照这种想法我们改写上述Shader代码,并创建一个新的C#代码,SpriteAnimator.cs,并将其拖拉到飞机上面。
更改后的着色器代码如下:
着色器“自定义/ AnimateSprites”{
	属性{
		_MainTex(“Base(RGB)”,2D)=“white”{}
		
		//创建下面的属性
		_CellAmount(“Cell Amount”,float)= 0.0
		_TimeValue(“Time Value”,float)= 0.0
	}
	SubShader {
		标签{“RenderType”=“不透明”}
		LOD 200
		
		CGPROGRAM
		#pragma表面冲浪Lambert

		sampler2D _MainTex;
		
		//创建与内部属性的连接 
		// CG程序
		float _CellAmount;
		float _TimeValue;

		struct输入{
			float2 uv_MainTex;
		};

		void surf(Input IN,inout SurfaceOutput o){
			//让我们的UVs存储在一个单独的变量中
			float2 spriteUV = IN.uv_MainTex;
			
			//让我们计算单个单元格的宽度
			// sprite sheet,并获得每个cel占用的uv百分比。
			float cellUVPercentage = 1.0 / _CellAmount;
			
			//将uv的前进动作以宽度为中心 
			//每个单元格
			float xValue = spriteUV.x;
			xValue + = _TimeValue;
			xValue * = cellUVPercentage;
			
			spriteUV = float2(xValue,spriteUV.y);
			
			half4 c = tex2D(_MainTex,spriteUV);
			o.Albedo = c.rgb;
			o.Alpha = ca;
		}
		ENDCG
	} 
	FallBack“Diffuse”
}

C#代码如下:

使用UnityEngine;
使用System.Collections;

公共类SpriteAnimator:MonoBehaviour 
{

	公众浮动速度= 5.0f;
	public int cellAmount = 0; 

	float timeValue = 0.0f;

	void Start()
	{
		transform.renderer.material.SetFloat(“_ CellAmount”,cellAmount);
	}

	//每帧调用一次更新
	void FixedUpdate() 
	{
		timeValue = Mathf.Ceil(Time.time * speed%9);
		transform.renderer.material.SetFloat(“_ TimeValue”,timeValue);
	}
}

在督察界面上,我们只需要调整C#代码的两个变量即可,而不需要更改材质的变量。这样,我们将计算整数偏移量的工作转移到了C#代码中。

当然,如果你不想自己实现一个功能完全的2D动画系统,你可以到Asset Store上下一些插件,这些插件可以帮你完成大部分工作。这些插件有:
如果你需要寻找一些应用来制作精灵,可以使用下列应用:
这刀刀都在肉上,当然我相信大家总会有办法的。