基于Shader实现的UGUI描边解决方案

前言

大扎好,我系狗猥。当大家都以为我鸽了的时候,我又出现了,这也是一种鸽。创业两年失败后归来,今天想给大家分享一个我最近研究出来的好康的,比游戏还刺激,还可以教你登dua郎喔(大误

这次给大家带来的是基于Shader实现的UGUI描边,也支持对Text组件使用。

首先请大家看看最终效果(上面放了一个Image和一个Text):

(8102年了怎么还在舰

接下来,我会向大家介绍思路和具体实现过程。如果你想直接代到项目里使用,请自行跳转到本文最后,那里有完整的C#和Shader代码。

本方案在Unity 2018.4.0f1下测试通过。

本文参考了http://blog.sina.com.cn/s/blog_6ad33d350102xb7v.html

转载请注明出处:https://www.cnblogs.com/GuyaWeiren/p/9665106.html


为什么要这么做

就我参加工作这些年接触到的UI美术来看,他们都挺喜欢用描边效果。诚然这个效果可以让文字更加突出,看着也挺不错。对美术来说做描边简单的一比,PS里加个图层样式就搞定,但是对我们程序来说就是一件很痛苦的事。

UGUI自带的Outline组件用过的同学都知道,本质上是把元素复制四份,然后做一些偏移绘制出来。但是把偏移量放大,瞬间就穿帮了。如果美术要求做一个稍微宽一点的描边,这个组件是无法实现的。

然后有先辈提出按照Outline实现方式,增加复制份数的方法。请参考https://github.com/n-yoda/unity-vertex-effects。确实非常漂亮。但是这个做法有一个非常严重的问题:数量如此大的顶点数,对性能会有影响。我们知道每个字符是由两个三角形构成,总共6个顶点。如果文字数量大,再加上一个复制N份的脚本,顶点数会分分钟炸掉。

以复制8次为例,一段200字的文本在进行处理后会生成200 * 6 * (8+1) = 10800 个顶点,多么可怕。并且,Unity5.2以前的版本要求,每一个Canvas下至多只能有65535个顶点,超过就会报错。

TextMeshPro能做很多漂亮的效果。但是它的做法类似于图字,要提供所有会出现的字符。对于字符很少的英语环境,这没有问题,但对于中文环境,把所有字符弄进去是不现实的。还有最关键的是,它是作用于TextMesh组件,而不是UGUI的Text

于是乎,使用Shader变成了最优解。

概括讲,这个实现就是在C#代码中对UI顶点根据描边宽度进行外扩,然后在Shader的像素着色器中对像素的一周以描边宽度为半径采N个样,最后将颜色叠加起来。通常需要描边的元素尺寸都不大,故多重采样带来的性能影响几乎是可以忽略的。


在Shader中实现描边

创建一个OutlineEx.shader。对于描边,我们需要两个参数:描边的颜色和描边的宽度。所以首先将这两个参数添加到Shader的属性中:

_OutlineColor("Outline Color", Color) = (1, 1, 1, 1)
_OutlineWidth("Outline Width", Int) = 1

采样坐标用圆的参数方程计算。在Shader中进行三角函数运算比较吃性能,并且这里采样的角度是固定的,所以我们可以把坐标直接写死。在Shader中添加采样的函数。因为最终进行颜色混合的时候只需要用到alpha值,所以函数不返回rgb:

fixed SampleAlpha(int pIndex, v2f IN)
{
const fixed sinArray[12] = { 0, 0.5, 0.866, 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5 };
const fixed cosArray[12] = { 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5, 0, 0.5, 0.866 };
float2 pos = IN.texcoord + _MainTex_TexelSize.xy * float2(cosArray[pIndex], sinArray[pIndex]) * _OutlineWidth;
return (tex2D(_MainTex, pos) + _TextureSampleAdd).w * _OutlineColor.w;
}

然后在像素着色器中增加对方法的调用。

fixed4 frag(v2f IN) : SV_Target
{
fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color; half4 val = half4(_OutlineColor.x, _OutlineColor.y, _OutlineColor.z, 0);
// 注意:这里为了简化代码用了循环
// 尽量不要在Shader中使用循环,多复制几次代码都行
for (int i = 0; i < 12; i++)
{
val.w += SampleAlpha(i, IN);
}
color = (val * (1.0 - color.a)) + (color * color.a); return color;
}

接下来,在Unity中新建一个材质球,把Shader赋上去,挂在一个UGUI组件上,然后调整描边颜色和宽度,可以看到效果:

可以看到描边已经出现了,但是超出图片范围的部分被裁减掉了。所以接下来,我们需要对图片的区域进行调整,保证描边的部分也被包含在区域内。


在C#层进行区域扩展

要扩展区域,就得修改顶点。Unity提供了BaseMeshEffect类供开发者对UI组件的顶点进行修改。

创建一个OutlineEx类,继承于BaseMeshEffect类,实现其中的ModifyMesh(VertexHelper)方法。参数VertexHelper类提供了GetUIVertexStream(List<UIVertex>)AddUIVertexTriangleStream(List<UIVertex>)方法用于获取和设置UI物件的顶点。

这里我们可以把参数需要的List提出来做成静态变量,这样能够避免每次ModifyMesh调用时创建List对象。

public class OutlineEx : BaseMeshEffect
{
public Color OutlineColor = Color.white;
[Range(0, 6)]
public int OutlineWidth = 0; private static List<UIVertex> m_VetexList = new List<UIVertex>(); protected override void Awake()
{
base.Awake(); var shader = Shader.Find("TSF Shaders/UI/OutlineEx");
base.graphic.material = new Material(shader); var v1 = base.graphic.canvas.additionalShaderChannels;
var v2 = AdditionalCanvasShaderChannels.Tangent;
if ((v1 & v2) != v2)
{
base.graphic.canvas.additionalShaderChannels |= v2;
}
this._Refresh();
} #if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate(); if (base.graphic.material != null)
{
this._Refresh();
}
}
#endif private void _Refresh()
{
base.graphic.material.SetColor("_OutlineColor", this.OutlineColor);
base.graphic.material.SetInt("_OutlineWidth", this.OutlineWidth);
base.graphic.SetVerticesDirty();
} public override void ModifyMesh(VertexHelper vh)
{
vh.GetUIVertexStream(m_VetexList); this._ProcessVertices(); vh.Clear();
vh.AddUIVertexTriangleStream(m_VetexList);
} private void _ProcessVertices()
{
// TODO: 处理顶点
}
}

现在已经可以获取到所有的顶点信息了。接下来我们对它进行外扩。

我们知道每三个顶点构成一个三角形,所以需要对构成三角形的三个顶点进行处理,并且要将它的UV坐标(决定图片在图集中的范围)也做对应的外扩,否则从视觉上看起来就只是图片被放大了一点点。

于是完成_ProcessVertices方法:

private void _ProcessVertices()
{
for (int i = 0, count = m_VetexList.Count - 3; i <= count; i += 3)
{
var v1 = m_VetexList[i];
var v2 = m_VetexList[i + 1];
var v3 = m_VetexList[i + 2];
// 计算原顶点坐标中心点
//
var minX = _Min(v1.position.x, v2.position.x, v3.position.x);
var minY = _Min(v1.position.y, v2.position.y, v3.position.y);
var maxX = _Max(v1.position.x, v2.position.x, v3.position.x);
var maxY = _Max(v1.position.y, v2.position.y, v3.position.y);
var posCenter = new Vector2(minX + maxX, minY + maxY) * 0.5f;
// 计算原始顶点坐标和UV的方向
//
Vector2 triX, triY, uvX, uvY;
Vector2 pos1 = v1.position;
Vector2 pos2 = v2.position;
Vector2 pos3 = v3.position;
if (Mathf.Abs(Vector2.Dot((pos2 - pos1).normalized, Vector2.right))
> Mathf.Abs(Vector2.Dot((pos3 - pos2).normalized, Vector2.right)))
{
triX = pos2 - pos1;
triY = pos3 - pos2;
uvX = v2.uv0 - v1.uv0;
uvY = v3.uv0 - v2.uv0;
}
else
{
triX = pos3 - pos2;
triY = pos2 - pos1;
uvX = v3.uv0 - v2.uv0;
uvY = v2.uv0 - v1.uv0;
}
// 为每个顶点设置新的Position和UV
//
v1 = _SetNewPosAndUV(v1, this.OutlineWidth, posCenter, triX, triY, uvX, uvY);
v2 = _SetNewPosAndUV(v2, this.OutlineWidth, posCenter, triX, triY, uvX, uvY);
v3 = _SetNewPosAndUV(v3, this.OutlineWidth, posCenter, triX, triY, uvX, uvY);
// 应用设置后的UIVertex
//
m_VetexList[i] = v1;
m_VetexList[i + 1] = v2;
m_VetexList[i + 2] = v3;
}
} private static UIVertex _SetNewPosAndUV(UIVertex pVertex, int pOutLineWidth,
Vector2 pPosCenter,
Vector2 pTriangleX, Vector2 pTriangleY,
Vector2 pUVX, Vector2 pUVY)
{
// Position
var pos = pVertex.position;
var posXOffset = pos.x > pPosCenter.x ? pOutLineWidth : -pOutLineWidth;
var posYOffset = pos.y > pPosCenter.y ? pOutLineWidth : -pOutLineWidth;
pos.x += posXOffset;
pos.y += posYOffset;
pVertex.position = pos;
// UV
var uv = pVertex.uv0;
uv += pUVX / pTriangleX.magnitude * posXOffset * (Vector2.Dot(pTriangleX, Vector2.right) > 0 ? 1 : -1);
uv += pUVY / pTriangleY.magnitude * posYOffset * (Vector2.Dot(pTriangleY, Vector2.up) > 0 ? 1 : -1);
pVertex.uv0 = uv; return pVertex;
} private static float _Min(float pA, float pB, float pC)
{
return Mathf.Min(Mathf.Min(pA, pB), pC);
} private static float _Max(float pA, float pB, float pC)
{
return Mathf.Max(Mathf.Max(pA, pB), pC);
}

然后可以在编辑器中调整描边颜色和宽度,可以看到效果:

OJ8K,现在范围已经被扩大,可以看到上下左右四个边的描边宽度没有被裁掉了。


UV裁剪,排除不需要的像素

在上一步的效果图中,我们可以注意到图片的边界出现了被拉伸的部分。如果使用了图集或字体,在UV扩大后图片附近的像素也会被包含进来。为什么会变成这样呢?(先打死)

因为前面说过,UV裁剪框就相当于图集中每个小图的范围。直接扩大必然会包含到小图邻接的图的像素。所以这一步我们需要对最终绘制出的图进行裁剪,保证这些不要的像素不被画出来。

裁剪的逻辑也很简单。如果该像素处于被扩大前的UV范围外,则设置它的alpha为0。这一步需要放在像素着色器中完成。如何将原始UV区域传进Shader是一个问题。对于Text组件,所有字符的顶点都会进入Shader处理,所以在Shader中添加属性是不现实的。

好在Unity为我们提供了门路,可以看UIVertex结构体的成员:

public struct UIVertex
{
public static UIVertex simpleVert;
public Vector3 position;
public Vector3 normal;
public Color32 color;
public Vector2 uv0;
public Vector2 uv1;
public Vector2 uv2;
public Vector2 uv3;
public Vector4 tangent;
}

而Unity默认只会使用到positionnormaluv0color,其他成员是不会使用的。所以我们可以考虑将原始UV框的数据(最小x,最小y,最大x,最大y)赋值给tangent成员,因为它刚好是一个Vector4类型。

当然,你想把数据分别放在uv1uv2中也是可以的。

这里感谢真木网友的指正,UI在缩放时,tangent的值会被影响,导致描边显示不全甚至完全消失,所以应该赋值给uv1uv2。经测试,Unity 5.6自身有bug,uv2uv3无论怎么设置都不会被传入shader,但在2017.3.1p1和2018上测试通过。如果必须要使用低版本Unity,可以考虑使用uv1tangent.zw存储原始UV框的四个值,但要求UI的Z轴不能缩放,且Canvas和摄像机必须正交。

需要注意的是,在Unity5.4(大概是这个版本吧,记不清了)之后,UIVertex的非必须成员的数据默认不会被传递进Shader。所以我们需要修改UI组件的CanvasadditionalShaderChannels属性,让uv1uv2成员也传入Shader。

var v1 = base.graphic.canvas.additionalShaderChannels;
var v2 = AdditionalCanvasShaderChannels.TexCoord1;
if ((v1 & v2) != v2)
{
base.graphic.canvas.additionalShaderChannels |= v2;
}
v2 = AdditionalCanvasShaderChannels.TexCoord2;
if ((v1 & v2) != v2)
{
base.graphic.canvas.additionalShaderChannels |= v2;
}

将原始UV框赋值给uv1uv2成员

var uvMin = _Min(v1.uv0, v2.uv0, v3.uv0);
var uvMax = _Max(v1.uv0, v2.uv0, v3.uv0);
vertex.uv1 = new Vector2(pUVOrigin.x, pUVOrigin.y);
vertex.uv2 = new Vector2(pUVOrigin.z, pUVOrigin.w); private static Vector2 _Min(Vector2 pA, Vector2 pB, Vector2 pC)
{
return new Vector2(_Min(pA.x, pB.x, pC.x), _Min(pA.y, pB.y, pC.y));
} private static Vector2 _Max(Vector2 pA, Vector2 pB, Vector2 pC)
{
return new Vector2(_Max(pA.x, pB.x, pC.x), _Max(pA.y, pB.y, pC.y));
}

然后在Shader的顶点着色器中获取它:

struct appdata
{
// 省略
float2 texcoord1 : TEXCOORD1;
float2 texcoord2 : TEXCOORD2;
}; struct v2f
{
// 省略
float2 uvOriginXY : TEXCOORD1;
float2 uvOriginZW : TEXCOORD2;
}; v2f vert(appdata IN)
{
// 省略
o.uvOriginXY = IN.texcoord1;
o.uvOriginZW = IN.texcoord2;
// 省略
}

判定一个点是否在给定矩形框内,可以用到内置的step函数。它常用于作比较,替代if/else语句提高效率。它的逻辑是:顺序给定两个参数a和b,如果 a > b 返回0,否则返回1。

添加判定函数:

fixed IsInRect(float2 pPos, float2 pClipRectXY, float2 pClipRectZW)
{
pPos = step(pClipRectXY, pPos) * step(pPos, pClipRectZW);
return pPos.x * pPos.y;
}

然后在采样和像素着色器中添加对它的调用:

fixed SampleAlpha(int pIndex, v2f IN)
{
// 省略
return IsInRect(pos, IN.uvOriginXY, IN.uvOriginZW) * (tex2D(_MainTex, pos) + _TextureSampleAdd).w * _OutlineColor.w;
} fixed4 frag(v2f IN) : SV_Target
{
// 省略
if (_OutlineWidth > 0)
{
color.w *= IsInRect(IN.texcoord, IN.uvOriginXY, IN.uvOriginZW);
// 省略
}
}

最终代码

那么现在就可以得到最终效果了。在我的代码中,对每个像素做了12次采样。如果美术要求对大图片进行比较粗的描边,需要增加采样次数。当然,如果字本身小,也可以降低次数。

由于这个Shader是给UI用的,所以需要将UI-Default.shader中的一些属性和设置复制到我们的Shader中。

//————————————————————————————————————————————
// OutlineEx.cs
//
// Created by Chiyu Ren on 2018/9/12 23:03:51
//————————————————————————————————————————————
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic; namespace TooSimpleFramework.UI
{
/// <summary>
/// UGUI描边
/// </summary>
public class OutlineEx : BaseMeshEffect
{
public Color OutlineColor = Color.white;
[Range(0, 6)]
public int OutlineWidth = 0; private static List<UIVertex> m_VetexList = new List<UIVertex>(); protected override void Start()
{
base.Start(); var shader = Shader.Find("TSF Shaders/UI/OutlineEx");
base.graphic.material = new Material(shader); var v1 = base.graphic.canvas.additionalShaderChannels;
var v2 = AdditionalCanvasShaderChannels.TexCoord1;
if ((v1 & v2) != v2)
{
base.graphic.canvas.additionalShaderChannels |= v2;
}
v2 = AdditionalCanvasShaderChannels.TexCoord2;
if ((v1 & v2) != v2)
{
base.graphic.canvas.additionalShaderChannels |= v2;
} this._Refresh();
} #if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate(); if (base.graphic.material != null)
{
this._Refresh();
}
}
#endif private void _Refresh()
{
base.graphic.material.SetColor("_OutlineColor", this.OutlineColor);
base.graphic.material.SetInt("_OutlineWidth", this.OutlineWidth);
base.graphic.SetVerticesDirty();
} public override void ModifyMesh(VertexHelper vh)
{
vh.GetUIVertexStream(m_VetexList); this._ProcessVertices(); vh.Clear();
vh.AddUIVertexTriangleStream(m_VetexList);
} private void _ProcessVertices()
{
for (int i = 0, count = m_VetexList.Count - 3; i <= count; i += 3)
{
var v1 = m_VetexList[i];
var v2 = m_VetexList[i + 1];
var v3 = m_VetexList[i + 2];
// 计算原顶点坐标中心点
//
var minX = _Min(v1.position.x, v2.position.x, v3.position.x);
var minY = _Min(v1.position.y, v2.position.y, v3.position.y);
var maxX = _Max(v1.position.x, v2.position.x, v3.position.x);
var maxY = _Max(v1.position.y, v2.position.y, v3.position.y);
var posCenter = new Vector2(minX + maxX, minY + maxY) * 0.5f;
// 计算原始顶点坐标和UV的方向
//
Vector2 triX, triY, uvX, uvY;
Vector2 pos1 = v1.position;
Vector2 pos2 = v2.position;
Vector2 pos3 = v3.position;
if (Mathf.Abs(Vector2.Dot((pos2 - pos1).normalized, Vector2.right))
> Mathf.Abs(Vector2.Dot((pos3 - pos2).normalized, Vector2.right)))
{
triX = pos2 - pos1;
triY = pos3 - pos2;
uvX = v2.uv0 - v1.uv0;
uvY = v3.uv0 - v2.uv0;
}
else
{
triX = pos3 - pos2;
triY = pos2 - pos1;
uvX = v3.uv0 - v2.uv0;
uvY = v2.uv0 - v1.uv0;
}
// 计算原始UV框
//
var uvMin = _Min(v1.uv0, v2.uv0, v3.uv0);
var uvMax = _Max(v1.uv0, v2.uv0, v3.uv0);
var uvOrigin = new Vector4(uvMin.x, uvMin.y, uvMax.x, uvMax.y);
// 为每个顶点设置新的Position和UV,并传入原始UV框
//
v1 = _SetNewPosAndUV(v1, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvOrigin);
v2 = _SetNewPosAndUV(v2, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvOrigin);
v3 = _SetNewPosAndUV(v3, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvOrigin);
// 应用设置后的UIVertex
//
m_VetexList[i] = v1;
m_VetexList[i + 1] = v2;
m_VetexList[i + 2] = v3;
}
} private static UIVertex _SetNewPosAndUV(UIVertex pVertex, int pOutLineWidth,
Vector2 pPosCenter,
Vector2 pTriangleX, Vector2 pTriangleY,
Vector2 pUVX, Vector2 pUVY,
Vector4 pUVOrigin)
{
// Position
var pos = pVertex.position;
var posXOffset = pos.x > pPosCenter.x ? pOutLineWidth : -pOutLineWidth;
var posYOffset = pos.y > pPosCenter.y ? pOutLineWidth : -pOutLineWidth;
pos.x += posXOffset;
pos.y += posYOffset;
pVertex.position = pos;
// UV
var uv = pVertex.uv0;
uv += pUVX / pTriangleX.magnitude * posXOffset * (Vector2.Dot(pTriangleX, Vector2.right) > 0 ? 1 : -1);
uv += pUVY / pTriangleY.magnitude * posYOffset * (Vector2.Dot(pTriangleY, Vector2.up) > 0 ? 1 : -1);
pVertex.uv0 = uv;
// 原始UV框
pVertex.uv1 = new Vector2(pUVOrigin.x, pUVOrigin.y);
pVertex.uv2 = new Vector2(pUVOrigin.z, pUVOrigin.w); return pVertex;
} private static float _Min(float pA, float pB, float pC)
{
return Mathf.Min(Mathf.Min(pA, pB), pC);
} private static float _Max(float pA, float pB, float pC)
{
return Mathf.Max(Mathf.Max(pA, pB), pC);
} private static Vector2 _Min(Vector2 pA, Vector2 pB, Vector2 pC)
{
return new Vector2(_Min(pA.x, pB.x, pC.x), _Min(pA.y, pB.y, pC.y));
} private static Vector2 _Max(Vector2 pA, Vector2 pB, Vector2 pC)
{
return new Vector2(_Max(pA.x, pB.x, pC.x), _Max(pA.y, pB.y, pC.y));
}
}
}

Shader

Shader "TSF Shaders/UI/OutlineEx"
{
Properties
{
_MainTex ("Main Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1, 1, 1, 1)
_OutlineColor ("Outline Color", Color) = (1, 1, 1, 1)
_OutlineWidth ("Outline Width", Int) = 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
} 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 "OUTLINE" CGPROGRAM
#pragma vertex vert
#pragma fragment frag sampler2D _MainTex;
fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _MainTex_TexelSize; float4 _OutlineColor;
int _OutlineWidth; struct appdata
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float2 texcoord1 : TEXCOORD1;
float2 texcoord2 : TEXCOORD2;
fixed4 color : COLOR;
}; struct v2f
{
float4 vertex : SV_POSITION;
float2 texcoord : TEXCOORD0;
float2 uvOriginXY : TEXCOORD1;
float2 uvOriginZW : TEXCOORD2;
fixed4 color : COLOR;
}; v2f vert(appdata IN)
{
v2f o; o.vertex = UnityObjectToClipPos(IN.vertex);
o.texcoord = IN.texcoord;
o.uvOriginXY = IN.texcoord1;
o.uvOriginZW = IN.texcoord2;
o.color = IN.color * _Color; return o;
} fixed IsInRect(float2 pPos, float2 pClipRectXY, float2 pClipRectZW)
{
pPos = step(pClipRectXY, pPos) * step(pPos, pClipRectZW);
return pPos.x * pPos.y;
} fixed SampleAlpha(int pIndex, v2f IN)
{
const fixed sinArray[12] = { 0, 0.5, 0.866, 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5 };
const fixed cosArray[12] = { 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5, 0, 0.5, 0.866 };
float2 pos = IN.texcoord + _MainTex_TexelSize.xy * float2(cosArray[pIndex], sinArray[pIndex]) * _OutlineWidth;
return IsInRect(pos, IN.uvOriginXY, IN.uvOriginZW) * (tex2D(_MainTex, pos) + _TextureSampleAdd).w * _OutlineColor.w;
} fixed4 frag(v2f IN) : SV_Target
{
fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
if (_OutlineWidth > 0)
{
color.w *= IsInRect(IN.texcoord, IN.uvOriginXY, IN.uvOriginZW);
half4 val = half4(_OutlineColor.x, _OutlineColor.y, _OutlineColor.z, 0); val.w += SampleAlpha(0, IN);
val.w += SampleAlpha(1, IN);
val.w += SampleAlpha(2, IN);
val.w += SampleAlpha(3, IN);
val.w += SampleAlpha(4, IN);
val.w += SampleAlpha(5, IN);
val.w += SampleAlpha(6, IN);
val.w += SampleAlpha(7, IN);
val.w += SampleAlpha(8, IN);
val.w += SampleAlpha(9, IN);
val.w += SampleAlpha(10, IN);
val.w += SampleAlpha(11, IN); val.w = clamp(val.w, 0, 1);
color = (val * (1.0 - color.a)) + (color * color.a);
}
return color;
}
ENDCG
}
}
}

最终效果:


优化点

可以看到在最后的像素着色器中使用了if语句。因为我比较菜,写出来的颜色混合算法在描边宽度为0的时候看起来效果很不好。

如果有大神能提供一个更优的算法,欢迎在评论中把我批判一番。把if语句去掉,可以提升一定的性能。

还有一点是,如果将图片或文字本身的透明度设为0,并不能得到镂空的效果。如果美术提出要这个效果,请毫不犹豫打死(误

最后一点,仔细观察上面最终效果的Ass,可以发现它们的字符本身被后一个字符的描边覆盖了一部分。使用两个Pass可以解决,一个只绘制描边,另一个只绘制本身。

Pass1

fixed4 frag(v2f IN) : SV_Target
{
// 省略
val.w = clamp(val.w, 0, 1);
return val;
}

Pass2

fixed4 frag(v2f IN) : SV_Target
{
fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
color.w *= IsInRect(IN.texcoord, IN.uvOriginXY, IN.uvOriginZW);
return color;
}

改动很简单,具体实现就留给读者了。


后记

首先要感谢提供这个思路的原作者。不然我还真想不出可以这么做。看来我毕竟还是图样。

希望这篇博文能帮到需要的朋友,因为网上几乎没有这个的教程。之前在别人的博客看到一句话:人生就是水桶,前三十年大家给你灌水,后三十年你给大家灌水。感觉挺有意思。今后会继续分享一些自己搞出的、网上少有的东西(虽然我还没到30)。

最近倒是没有特别在做什么,不过有在学习Shader,进入了未知♂领域。买了一些书,想给大家推荐冯乐乐的《Unity Shader入门精要》(博客https://blog.csdn.net/candycat1992/),对入门挺有帮助。知道该书作者是比我小一岁但是比我牛逼太多的美女程序媛(不要YY了,有对象的)的时候我真的受到了极大刺激。一个妹子都能钻得这么深,我应该更加努力啊。学习是从摇篮到坟墓的过程,希望大家不管学什么都要坚持。

还有一点就是创业真的要谨慎。最近了解到国家出了条例要对国产游戏限量发行,对各个游戏公司想必都是一记闷锤。加之统一征收社保,引起的连锁反应必然会波及到游戏行业。唯一欣慰的是我们还能做游戏,还能在这条路上继续走。那么就继续走下去吧,不要停下来啊!(指加班)

很惭愧,就做了一点微小的工作,谢谢大家!

最新文章

  1. CSS3选择器——基本选择器
  2. jQuery原型方法each使用和源码分析
  3. 使用Reveal
  4. Excel 函数
  5. QQ登入(5)获取空间相册,新建相册,上传图片到空间相册
  6. NodeJS模块
  7. ssh 内在溢出
  8. hdoj 1226 超级password 【隐图BFS】
  9. ftpclient 550 permission denied
  10. 从基本理解到深入探究 Linux kernel 通知链(notifier chain)【转】
  11. mysql锁2
  12. Flask请求流程超清大图
  13. 二、网络编程-socket之TCP协议开发客户端和服务端通信
  14. SQL server类型转换
  15. [LeetCode&amp;Python] Problem 237. Delete Node in a Linked List
  16. 9.Libraries and visibility 库和可见性
  17. 转:Citrix虚拟化--转自CSDN
  18. 不要问我有多懒,写个脚本跑django
  19. SQLServer转MYSQL的方法(连数据)[传]
  20. 【剑指Offer】俯视50题之1-10题

热门文章

  1. NGUI Clip Animation (UI动画)
  2. 【转】Java学习---算法那些事
  3. Yii2 使用 RESTful 写API接口 实例
  4. You are not late! You are not early!
  5. vue-devtoools 调试工具安装
  6. CGJ02、BD09、西安80、北京54、CGCS2000常用坐标系详解
  7. 友盟推送SDK集成测试、常见问题以及注意事项总结
  8. Codeforces Round #553 (Div. 2)B. Dima and a Bad XOR 思维构造+异或警告
  9. C/C++常用库及工具
  10. 20145236《网络对抗》Exp1 逆向及Bof基础