UGUI的文本描边
如果当前渲染的像素Alpha>0,那么这个像素肯定是文字本身的像素。
如果当前渲染的像素Alpha<=0,那么这个像素肯定不是文字本身的像素。
当渲染像素 2 的时候,会采样到像素 1
//如果想要描边效果更佳平滑的话,升采样的像素点可以扩大到12或者更高,但是会带来更高的性能消耗
static const half2 UpSamplePixelCoord[8] =
{
half2 (-1, 1), half2 (0, 1), half2 (1, 1),
half2 (-1, 0), half2 (1, 0),
half2 (-1, -1), half2 (0, -1), half2 (1, -1)
};
//升采样,每个像素根据周边8个像素的透明度来确定是否显示描边颜色
fixed UpSamplePixel(int index, v2f IN)
{
half2 realOutlineWidth = _MainTex_TexelSize.xy * UpSamplePixelCoord[index] * _OutlineWidth;
half2 pixelUV = IN.texcoord + realOutlineWidth;
half4 pixelAlpha = (tex2D(_MainTex, pixelUV) + _TextureSampleAdd).w;
return pixelAlpha;
}
fixed4 frag(v2f IN) : SV_Target
{
//当前像素中心点的颜色
fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
half4 outlineColor = half4(_OutlineColor.xyz, 0);
int index = 0;
for (; index < 8; ++index)
{
outlineColor.w += UpSamplePixel(index, IN);
}
outlineColor.w = clamp(outlineColor.w, 0, 1);
//把文字本身的颜色和描边的颜色做一个过渡。
color = lerp(outlineColor, color, color.a);
return color;
}
可以看到描边的效果有了,但是在字体的边缘都被截掉了。这是因为文字的顶点组成的矩形区域是确定的(每个文字是由两个三角形组成的矩形,UGUI处理之后到着色器中是Mesh数据),我们增加的描边相当于加宽了文字,但是原本文字的区域我们没有加大,自然描边超出的部分就会被截掉。所以接下来我们要处理的就是把组成单个文字的两个三角形加宽,同时我们得让文字本身的大小保持不变,这样文字四周的宽度留出来给描边。类似于这样:
public override void ModifyMesh(VertexHelper vh)
{
//...省略很少一部分代码
List<UIVertex> vertexStream = ListPool<UIVertex>.Get();
vh.GetUIVertexStream(vertexStream);
float expandWidth = Mathf.Abs(effectDistance.x * 0.5f);
float expandHeight = Mathf.Abs(effectDistance.y * 0.5f);
// v1----v2
// | \ |
// | \ |
// | \|
// v4----v3
// 顺序按照vertex来确定的,一个文字由两个triangle组成,并且任意文字的vertex顺序都是相同的
// 但是不同文字的uv的顺序不一样
int length = vertexStream.Count;
for (int i = 0; i < length; i += 6)
{
Vector2 expandPositionSize = GetExpandPositionSize(expandWidth, expandHeight);
Vector2 shrinkUvSize = GetShrinkUvSize(vertexStream, expandWidth, expandHeight, i);
RePackTextVertex(vertexStream, i, expandPositionSize, shrinkUvSize);
}
vh.Clear();
vh.AddUIVertexTriangleStream(vertexStream);
ListPool<UIVertex>.Release(vertexStream);
//...省略很少一部分代码
}
代码中的 GetExpandPositionSize 比较好处理,因为文字的顶点顺序是确定的,按照固定的顺序去增加或者减去expand值,保证每个顶点的坐标向外扩即可。GetShrinkUvSize这个处理起来要稍微注意下,因为文字的UV的坐标不确定,我没有法线任何规律,也就是说两个文字的同一个顶点处,uv坐标顺序可能会不一样。这就需要自己根据每个uv的值来判断了,保证每个顶点的uv是向内缩即可,实现的原理如图(图中假设内缩大小为(0.2, 0.2)):
private Vector2 ShrinkUvSize(Vector2 uv, Vector2 leftVertexUv, Vector2 rightVertexUv, Vector2 shrinkSize)
{
float x = ShrinkValue(uv.x, leftVertexUv.x, rightVertexUv.x, shrinkSize.x);
float y = ShrinkValue(uv.y, leftVertexUv.y, rightVertexUv.y, shrinkSize.y);
return new Vector2(x, y);
}
/// <summary>
/// left 三个uv点关系。value:当前uv点,left:value的左边uv点,right:value的右边UV点
/// |
/// |
/// value --- right
/// </summary>
private float ShrinkValue(float value, float left, float right, float shrinkValue)
{
if (value < left)
{
value -= shrinkValue;
}
else if (value == left)
{
value = value < right ? value-shrinkValue : value +shrinkValue;
}
else
{
value += shrinkValue;
}
return value;
}
处理了文字描边被裁减的问题之后:
每个文字的边缘出现了不需要的颜色像素,因为我们修改了uv,把文字的顶点的uv值放大了,所以放大的那部分uv对应的像素原本是其他文字的像素现在被采样近来了。所以我们需要剔除掉这部分本不是当前文字的像素。这个可以直接在着色器中处理:
//判断像素是否在三角形中:像素点依次和三角形的两个顶点的叉积的方向同向。
fixed IsPixelInTriangle(float3 pixelPos, float3 a, float3 b, float3 c)
{
float z1 = cross(pixelPos - a, a - b).z;
float z2 = cross(pixelPos - b, b - c).z;
float z3 = cross(pixelPos - c, c - a).z;
return z1 * z2 > 0 && z2 * z3 > 0;
}
//判断像素是否在一个Rect中,由于Rect是由两个Triangle组成,所以只需要判断顶点是否在任意一个Triangle中即可。
fixed IsPixelInRect(float3 pixelPos, float3 a, float3 b, float3 c, float3 d)
{
return IsPixelInTriangle(pixelPos, a, b, c) || IsPixelInTriangle(pixelPos, c, d, a);
}