解决UGUI的图集导致Shader采样时UV错误的问题

大家好,我是阿赵。
在我们用UGUI的时候,很多时候需要通过在UI上面挂材质球,写Shader,来实现一些特殊的效果。
这里句一个很简单的例子,只为说明问题。

一、简单例子说明

这个例子是这样的,我想在某个Image上面加一个渐变遮罩,只显示角色的头像。
这里我准备了一张角色贴图,然后根据角色头像的位置画了个遮罩。
解决UGUI的图集导致Shader采样时UV错误的问题_第1张图片
解决UGUI的图集导致Shader采样时UV错误的问题_第2张图片

接下来的实现很简单,通过图片的UV采样遮罩贴图,然后和原来的图片叠加透明度,之后就得到了这样的效果:
解决UGUI的图集导致Shader采样时UV错误的问题_第3张图片

这个例子的shader是这样的:

Shader "azhao/UIAlphaMask"
{
	Properties
	{
		[PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
		_Color("Tint", Color) = (1,1,1,1)

		_StencilComp("Stencil Comparison", Float) = 8
		_Stencil("Stencil ID", Float) = 0
		_StencilOp("Stencil Operation", Float) = 0
		_StencilWriteMask("Stencil Write Mask", Float) = 255
		_StencilReadMask("Stencil Read Mask", Float) = 255

		_ColorMask("Color Mask", Float) = 15
		[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip("Use Alpha Clip", Float) = 0
		_MaskMap("MaskMap",2D) = "white"{}
	}

		SubShader
	{
		Tags
		{
			"Queue" = "Transparent"
			"IgnoreProjector" = "True"
			"RenderType" = "Transparent"
			"PreviewType" = "Plane"
			"CanUseSpriteAtlas" = "True"
		}

		Stencil
		{
			Ref[_Stencil]
			Comp[_StencilComp]
			Pass[_StencilOp]
			ReadMask[_StencilReadMask]
			WriteMask[_StencilWriteMask]
		}

		Cull Off
		Lighting Off
		ZWrite Off
		ZTest[unity_GUIZTestMode]
		Blend SrcAlpha OneMinusSrcAlpha
		ColorMask[_ColorMask]
		Pass
		{
			Name "Default"
		CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma target 2.0

			#include "UnityCG.cginc"
			#include "UnityUI.cginc"

			#pragma multi_compile __ UNITY_UI_ALPHACLIP

			struct appdata_t
			{
				float4 vertex   : POSITION;
				float4 color    : COLOR;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float4 vertex   : SV_POSITION;
				fixed4 color : COLOR;
				half2 uv  : TEXCOORD0;
				float4 worldPosition : TEXCOORD1;
			};

			fixed4 _Color;
			fixed4 _TextureSampleAdd;
			float4 _ClipRect;
			sampler2D _MaskMap;

			v2f vert(appdata_t i)
			{
				v2f o;
				o.worldPosition = i.vertex;
				o.vertex = UnityObjectToClipPos(o.worldPosition);

				o.uv = i.uv;

				#ifdef UNITY_HALF_TEXEL_OFFSET
				o.vertex.xy += (_ScreenParams.zw - 1.0) * float2(-1,1) * o.vertex.w;
				#endif

				o.color = i.color * _Color;
				return o;

			}

			sampler2D _MainTex;

			half4 frag(v2f i) : SV_Target
			{
				half4 color = (tex2D(_MainTex, i.uv) + _TextureSampleAdd) * i.color;

				half4 maskCol = tex2D(_MaskMap, i.uv);
				color.a = maskCol.r*color.a;
				return color;
			}
		ENDCG
		}
	}
}

二、打图集之后,遇到的问题。

由于这个图片是用在UI的image上的,所以它的类型是Sprite。然后一般来说,使用UGUI的Sprite都会打成图集来使用。
于是我对这张图片设置一下PackingTag,让它成为一张图集的一部分。
解决UGUI的图集导致Shader采样时UV错误的问题_第4张图片

然后把图片打包成AssetBundle,再加载使用,这个时候,发现刚才显示很正常的例子,变得不正常了。
解决UGUI的图集导致Shader采样时UV错误的问题_第5张图片

三、分析问题

我修改一下shader,单独把这种图片的颜色显示出来。

half4 frag(v2f i) : SV_Target
{
	half4 color = (tex2D(_MainTex, i.uv) + _TextureSampleAdd) * i.color;

	half4 maskCol = tex2D(_MaskMap, i.uv);
	color.a = maskCol.r*color.a;
	return maskCol;
}

如果没有打包AssetBundle加载的时候,我们的遮罩图的位置应该是这样的。
解决UGUI的图集导致Shader采样时UV错误的问题_第6张图片

但如果通过AssetBundle加载之后,遮罩的位置会变成这样:
解决UGUI的图集导致Shader采样时UV错误的问题_第7张图片

比较容易就能看出来,出现问题的原因是,采样遮罩图的UV似乎变得不正确了。
由于刚才用于例子的两张图都太居中,不利于分析问题,于是我把角色图和遮罩图都修改了一下位置,变成这样:
解决UGUI的图集导致Shader采样时UV错误的问题_第8张图片
解决UGUI的图集导致Shader采样时UV错误的问题_第9张图片

如果正常显示,现在的效果应该是这样的:
解决UGUI的图集导致Shader采样时UV错误的问题_第10张图片

由于Unity的图集为了能适配硬件的图片压缩,会自动变成2的次幂,所以会对原图做一定的修改,先去掉空白的地方,然后再补到最接近的2的次幂的大小。

用工具打开AssetBundle,可以看到,这张图片去除了空白之后,实际sprite的像素是256*577
解决UGUI的图集导致Shader采样时UV错误的问题_第11张图片

而由于577已经超过了512,所以下一级是1024,于是这个贴图的完整尺寸是256*1024
解决UGUI的图集导致Shader采样时UV错误的问题_第12张图片

所以,实际上这张图片在AssetBundle里面是长这样的:
解决UGUI的图集导致Shader采样时UV错误的问题_第13张图片

这张图在Unity里面打开SpritePacker可以看到
解决UGUI的图集导致Shader采样时UV错误的问题_第14张图片

然后,把场景渲染模式改成渲染+线框
解决UGUI的图集导致Shader采样时UV错误的问题_第15张图片

可以看到,虽然原始的Image的范围是在红框这么大,但实际上,生成的网格只有绿框的范围。
解决UGUI的图集导致Shader采样时UV错误的问题_第16张图片

网上很多文章在介绍到这一步的时候,为了说明原理,就开始看UGUI的源码了,不过我认为,源码估计很多人都不想去看,或者说看不太懂。所以我我直接说结论。
一般图集的优化,包括我以前自己写的引擎,对于图集的实现方式,都是这样,在父级设置一个和原图大小一样的范围,然后在里面,只在有效像素范围内生成网格模型,这个网格模型的UV就不能是0-1,而是有效像素实际占有原图的比例。
这个UV比例,Unity是提供了方法给我们获取的

Vector4 outerUV = UnityEngine.Sprites.DataUtility.GetOuterUV(sprite);

这个例子,把outerUV打印出来,发现值是:
outerUV:(0,0,1,0.56)
这是怎么理解呢?
解决UGUI的图集导致Shader采样时UV错误的问题_第17张图片

由于图片本身是256宽,而有效像素也是256,所以uv的x坐标是完全使用了这张贴图的整个宽度,所以取值范围是0-1
由于图片高度是1024,而有效的像素范围只到577,所以只用到了图片的0.56。
于是,生成的小网格的UV坐标,实际上是这样的一个取值,宽度0-1,高度只取到0-0.56;

那是不是我们只需要重新计算一下UV坐标,就能解决问题呢?
其实并不是的,由于实际生成的网格只占原图的一部分,那么网格的四条边,离整个Image的四条边分别是多远呢?Unity同样提供了方法给我们查询

Vector4 padding = UnityEngine.Sprites.DataUtility.GetPadding(sp);

把padding打印出来:
padding:(388,9,54,28)
这里padding的含义是如下图所示的:
解决UGUI的图集导致Shader采样时UV错误的问题_第18张图片

通过打印sprite.rect,可以得到这个sprite在没有去掉空白之前的像素是698*698
所以通过padding,我们就可以算出这个小网格的4条边离Image的四条边的距离在UV上的比例

Vector4 customUV = new Vector4();
customUV.x = padding.x/ sp.rect.width;
customUV.y = padding.y / sp.rect.height;
customUV.z = padding.z / sp.rect.width;
customUV.w = padding.w / sp.rect.height;

由于上面的例子只有一张图片,不够复杂,所以下面把图集再扩充一下,打多几张图进去:
解决UGUI的图集导致Shader采样时UV错误的问题_第19张图片

我们挑了其中一个图作为渲染,打印出的outerUV是:
outerUV:(0.378,0.282,0.5,0.56)
实际上,在整张图集里面,它的UV范围会是这样的:
解决UGUI的图集导致Shader采样时UV错误的问题_第20张图片

虽然打多了很多张图进去,但padding是不会变的,sp.rect也是不会变的。

四、解决问题

通过上面的一堆分析,我们知道了这几个事情:
1.打成图集之后,生成的网格是只有在有效像素范围的,所以UV并不是0-1,而是截取一小部分
2.我们要算出离边缘的UV比例,然后把采样的范围从网格内部延伸到整个原始图片的大小。

1、重新映射UV范围:

实际上我们要做的事情,是从原来的0-1的uv范围,截取出一个当前小网格对应的部分
解决UGUI的图集导致Shader采样时UV错误的问题_第21张图片
解决UGUI的图集导致Shader采样时UV错误的问题_第22张图片
解决UGUI的图集导致Shader采样时UV错误的问题_第23张图片
解决UGUI的图集导致Shader采样时UV错误的问题_第24张图片

比如这个例子,原始的UV是0-1,但小网格里面的UV范围
x轴是0.378-0.5
y轴是0.282-0.56
为了能让这个小范围映射回0-1,在shader里面可以这样处理
先写一个Remap方法

float Remap(float val,float minOld,float maxOld,float minNew,float maxNew)
{
	return (minNew + (val - minOld) * (maxNew - minNew) / (maxOld - minOld));
}

然后

float minNew = 0;
float maxNew = 1;
float minOld = _OuterUV.x;
float maxOld = _OuterUV.z;
float tx = Remap(i.uv.x, minOld, maxOld, minNew, maxNew);
float _minOld = _OuterUV.x;
float _maxOld = _OuterUV.z;
float ty = Remap(i.uv.y, minOld, maxOld, minNew, maxNew);

这样,UV就从小网格映射回大Image范围了。

2、计算Padding

要计算边缘的范围,实际上就是把刚才重新映射的UV,再添加一个Padding的开始和结束位置的偏移,具体是这样算的:

tx = _Padding.x + (tx * (1 - _Padding.z - _Padding.x));
ty = _Padding.y + (ty * (1 - _Padding.w - _Padding.y));

通过这两步计算之后,UV坐标的映射和偏移都做完了,为了让大家看得更清楚,我准备了一张数字网格图(本来是打算用来做另外一个例子的)。计算后,通过对同一张图进行采样,背后比较暗的是原图,前面比较亮的是通过小网格反过来推算UV的结果。可以看出来,两张图的接缝处是完全重叠的,没有一点点偏差。
解决UGUI的图集导致Shader采样时UV错误的问题_第25张图片

到了这一步,其实问题已经解决完成了。把计算出来的新UV采样遮罩图,然后叠加颜色,就能得出正确的结果。下面是裁剪后的图和原遮罩图的对比,可以看出,裁剪的范围刚刚好是在原图的白色区域。
解决UGUI的图集导致Shader采样时UV错误的问题_第26张图片

五、源码

1、C#端获取outerUV和padding的代码

Sprite sp = imgAB.LoadAsset("longzhu");
image.sprite = sp;
Vector4 outerUV = UnityEngine.Sprites.DataUtility.GetOuterUV(sp);
Vector4 padding = UnityEngine.Sprites.DataUtility.GetPadding(sp);
Vector4 customUV = new Vector4();

customUV.x = padding.x/ sp.rect.width;
customUV.y = padding.y / sp.rect.height;
customUV.z = padding.z / sp.rect.width;
customUV.w = padding.w / sp.rect.height;

image.material.SetVector("_Padding", customUV);
image.material.SetVector("_OuterUV", outerUV);

2、完整Shader

Shader "azhao/UIAlphaMask"
{
	Properties
	{
		[PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
		_Color("Tint", Color) = (1,1,1,1)

		_StencilComp("Stencil Comparison", Float) = 8
		_Stencil("Stencil ID", Float) = 0
		_StencilOp("Stencil Operation", Float) = 0
		_StencilWriteMask("Stencil Write Mask", Float) = 255
		_StencilReadMask("Stencil Read Mask", Float) = 255

		_ColorMask("Color Mask", Float) = 15
		_Padding("Padding",Vector) = (0,0,0,0)
		_OuterUV("OuterUV",Vector) = (0,0,1,1)
		[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip("Use Alpha Clip", Float) = 0
		_MaskMap("MaskMap",2D) = "white"{}
	}

		SubShader
	{
		Tags
		{
			"Queue" = "Transparent"
			"IgnoreProjector" = "True"
			"RenderType" = "Transparent"
			"PreviewType" = "Plane"
			"CanUseSpriteAtlas" = "True"
		}

		Stencil
		{
			Ref[_Stencil]
			Comp[_StencilComp]
			Pass[_StencilOp]
			ReadMask[_StencilReadMask]
			WriteMask[_StencilWriteMask]
		}

		Cull Off
		Lighting Off
		ZWrite Off
		ZTest[unity_GUIZTestMode]
		Blend SrcAlpha OneMinusSrcAlpha
		ColorMask[_ColorMask]
		Pass
		{
			Name "Default"
		CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma target 2.0

			#include "UnityCG.cginc"
			#include "UnityUI.cginc"

			#pragma multi_compile __ UNITY_UI_ALPHACLIP

			struct appdata_t
			{
				float4 vertex   : POSITION;
				float4 color    : COLOR;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float4 vertex   : SV_POSITION;
				fixed4 color : COLOR;
				half2 uv  : TEXCOORD0;
				float4 worldPosition : TEXCOORD1;
			};

			fixed4 _Color;
			fixed4 _TextureSampleAdd;
			float4 _ClipRect;
			sampler2D _MaskMap;
			float4 _Padding;
			float4 _OuterUV;

			v2f vert(appdata_t i)
			{
				v2f o;
				o.worldPosition = i.vertex;
				o.vertex = UnityObjectToClipPos(o.worldPosition);

				o.uv = i.uv;

				#ifdef UNITY_HALF_TEXEL_OFFSET
				o.vertex.xy += (_ScreenParams.zw - 1.0) * float2(-1,1) * o.vertex.w;
				#endif

				o.color = i.color * _Color;
				return o;

			}

			sampler2D _MainTex;

			float Remap(float val,float minOld,float maxOld,float minNew,float maxNew)
			{
				return (minNew + (val - minOld) * (maxNew - minNew) / (maxOld - minOld));
			}

			half4 frag(v2f i) : SV_Target
			{
				half4 color = (tex2D(_MainTex, i.uv) + _TextureSampleAdd) * i.color;
				float minNew = 0;
				float maxNew = 1;
				float minOld = _OuterUV.x;
				float maxOld = _OuterUV.z;
				float tx = Remap(i.uv.x, minOld, maxOld, minNew, maxNew);
				tx = _Padding.x + (tx * (1 - _Padding.z - _Padding.x));
				float _minOld = _OuterUV.x;
				float _maxOld = _OuterUV.z;
				minOld = _OuterUV.y;
				maxOld = _OuterUV.w;
				float ty = Remap(i.uv.y, minOld, maxOld, minNew, maxNew);
				ty = _Padding.y + (ty * (1 - _Padding.w - _Padding.y));
				float2 maskUV = float2(tx, ty);
				half4 maskCol = tex2D(_MaskMap, maskUV);
				color.a = maskCol.r*color.a;
				return color;
			}
		ENDCG
		}
	}
}

你可能感兴趣的:(Unity功能与问题解决,uv,贴图,unity,sprite)