大家好,我是阿赵。
在我们用UGUI的时候,很多时候需要通过在UI上面挂材质球,写Shader,来实现一些特殊的效果。
这里句一个很简单的例子,只为说明问题。
一、简单例子说明
这个例子是这样的,我想在某个Image上面加一个渐变遮罩,只显示角色的头像。
这里我准备了一张角色贴图,然后根据角色头像的位置画了个遮罩。
接下来的实现很简单,通过图片的UV采样遮罩贴图,然后和原来的图片叠加透明度,之后就得到了这样的效果:
这个例子的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,让它成为一张图集的一部分。
然后把图片打包成AssetBundle,再加载使用,这个时候,发现刚才显示很正常的例子,变得不正常了。
三、分析问题
我修改一下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加载的时候,我们的遮罩图的位置应该是这样的。
但如果通过AssetBundle加载之后,遮罩的位置会变成这样:
比较容易就能看出来,出现问题的原因是,采样遮罩图的UV似乎变得不正确了。
由于刚才用于例子的两张图都太居中,不利于分析问题,于是我把角色图和遮罩图都修改了一下位置,变成这样:
如果正常显示,现在的效果应该是这样的:
由于Unity的图集为了能适配硬件的图片压缩,会自动变成2的次幂,所以会对原图做一定的修改,先去掉空白的地方,然后再补到最接近的2的次幂的大小。
用工具打开AssetBundle,可以看到,这张图片去除了空白之后,实际sprite的像素是256*577
而由于577已经超过了512,所以下一级是1024,于是这个贴图的完整尺寸是256*1024
所以,实际上这张图片在AssetBundle里面是长这样的:
这张图在Unity里面打开SpritePacker可以看到
然后,把场景渲染模式改成渲染+线框
可以看到,虽然原始的Image的范围是在红框这么大,但实际上,生成的网格只有绿框的范围。
网上很多文章在介绍到这一步的时候,为了说明原理,就开始看UGUI的源码了,不过我认为,源码估计很多人都不想去看,或者说看不太懂。所以我我直接说结论。
一般图集的优化,包括我以前自己写的引擎,对于图集的实现方式,都是这样,在父级设置一个和原图大小一样的范围,然后在里面,只在有效像素范围内生成网格模型,这个网格模型的UV就不能是0-1,而是有效像素实际占有原图的比例。
这个UV比例,Unity是提供了方法给我们获取的
Vector4 outerUV = UnityEngine.Sprites.DataUtility.GetOuterUV(sprite);
这个例子,把outerUV打印出来,发现值是:
outerUV:(0,0,1,0.56)
这是怎么理解呢?
由于图片本身是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的含义是如下图所示的:
通过打印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;
由于上面的例子只有一张图片,不够复杂,所以下面把图集再扩充一下,打多几张图进去:
我们挑了其中一个图作为渲染,打印出的outerUV是:
outerUV:(0.378,0.282,0.5,0.56)
实际上,在整张图集里面,它的UV范围会是这样的:
虽然打多了很多张图进去,但padding是不会变的,sp.rect也是不会变的。
四、解决问题
通过上面的一堆分析,我们知道了这几个事情:
1.打成图集之后,生成的网格是只有在有效像素范围的,所以UV并不是0-1,而是截取一小部分
2.我们要算出离边缘的UV比例,然后把采样的范围从网格内部延伸到整个原始图片的大小。
1、重新映射UV范围:
实际上我们要做的事情,是从原来的0-1的uv范围,截取出一个当前小网格对应的部分
比如这个例子,原始的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的结果。可以看出来,两张图的接缝处是完全重叠的,没有一点点偏差。
到了这一步,其实问题已经解决完成了。把计算出来的新UV采样遮罩图,然后叠加颜色,就能得出正确的结果。下面是裁剪后的图和原遮罩图的对比,可以看出,裁剪的范围刚刚好是在原图的白色区域。
五、源码
1、C#端获取outerUV和padding的代码
Sprite sp = imgAB.LoadAsset<Sprite>("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
}
}
}