前言
最近由于已上线的游戏根据数据反馈需要缩小包体,过了一篇游戏内缩减包体比较快和有效的办法就是去掉TMP文本控件,使用旧版的Text文本控件,这样就可以省下TMP需要的各种多语言字体空间,综合下来省下的量还是比较可观的。但TMP自带的描边和阴影等效果就不能使用了,旧版Text的outline效果简值就是应付式的交付,没啥作用。如果游戏里面需要用到描边和阴影,就只能自己想办法实现了。于是,从各种网友那儿过了一篇实现的方法,再经过自己的整理和修改,最终实现的效果还算可以,应该可以应用到游戏中了。在此,先上图:
下面我们一起来探讨一下TEXT的描边和阴影是如果实现的。
一、描边实现的思路
总体的思路大概是这样:
1、把Text 顶点外扩
2、重新映射外扩后正确的顶点UV
3、通过alpha值检测边缘并采样
4、通过采样后的alpha值透出描边颜色
5、对描边进行效果进行锐化,清晰描边
二、阴影实现的思路
总体的思路大概是这样:
1、复制多一份顶点数据并偏移至目标方向
2、对顶点数据进行区分描边和阴影分别处理
3、描边和阴影是可选的,也可同时存在
4、存在阴影时,阴影渲染过滤掉描边数据,恢复纯正的阴影
三、C#完整代码
首先我们来看C#的完整代码,如下:
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.UI;
using UnityEngine.UIElements;
namespace Gamelogic
{
/// <summary>
/// 描边阴影特效
/// </summary>
public class OutlineExt : BaseMeshEffect
{
public Color OutlineColor = Color.white;
public int OutlineWidth = 1;
/// <summary>
/// 描边锐化
/// </summary>
public bool OutlineSharpening = false;
public bool Shadow = false;
public Vector2 ShadowOffset;
public Color ShadowColor = Color.white;
public Shader ShaderExt;
protected override void Start()
{
base.Awake();
graphic.material = new Material(ShaderExt);
//开启多个uv通道,以将数据传入shader处理
var v1 = graphic.canvas.additionalShaderChannels;
var v2 = AdditionalCanvasShaderChannels.TexCoord1;
if ((v1 & v2) != v2)
graphic.canvas.additionalShaderChannels = v2;
v2 = AdditionalCanvasShaderChannels.TexCoord2;
if((v1 & v2) != v2)
graphic.canvas.additionalShaderChannels |= v2;
Refresh();
}
#if(UNITY_EDITOR)
protected override void OnValidate()
{
base.OnValidate();
if (graphic.material != null)
{
Refresh();
}
}
#endif
private void Refresh()
{
graphic.material.SetColor("_OutlineColor", OutlineColor);
graphic.material.SetInt("_OutlineWidth", OutlineWidth);
graphic.material.SetInt("_Sharpening", OutlineSharpening ? 1 : 0);
graphic.material.SetInt("_Shadow", Shadow ? 1 : 0);
graphic.material.SetVector("_ShadowOffset", ShadowOffset);
graphic.material.SetColor("_ShadowColor", ShadowColor);
graphic.SetVerticesDirty();
}
/// <summary>
/// 修改顶点
/// </summary>
/// <param name="vh"></param>
public override void ModifyMesh(VertexHelper vh)
{
List<UIVertex> allVertexs = null;
if (Shadow)
{//处理阴影
allVertexs = new List<UIVertex>();
var vertexs = ProcessShadowVertices(vh);
allVertexs.AddRange(vertexs);
}
if (OutlineWidth > 0)
{//处理描边
if(allVertexs == null)
allVertexs = new List<UIVertex>();
var vertexs = ProcessOutlineVertices(vh);
allVertexs.AddRange(vertexs);
}
if(allVertexs != null && allVertexs.Count > 0)
{
vh.Clear();
vh.AddUIVertexTriangleStream(allVertexs);
}
}
/// <summary>
/// 处理阴影顶点
/// </summary>
/// <param name="vh"></param>
/// <param name="offset"></param>
/// <returns></returns>
private List<UIVertex> ProcessShadowVertices(VertexHelper vh)
{
var allVertexs = new List<UIVertex>();
var vertexs = new List<UIVertex>();
vh.GetUIVertexStream(vertexs);
for (var i = 0; i < vertexs.Count; i++)
{//复制一份顶点,并将顶点进行偏移即可
var v = vertexs[i];
v.position += new Vector3(ShadowOffset.x, ShadowOffset.y, 0);
v.uv2.x = -1; //存储-1,标志此顶点是阴影顶点
vertexs[i] = v;
}
allVertexs.AddRange(vertexs);
if (OutlineWidth <= 0)
{//如果描边没有生成,需要保持原来的顶点。顶点是后加进列表后渲染。
vertexs.Clear();
vh.GetUIVertexStream(vertexs);
allVertexs.AddRange(vertexs);
}
return allVertexs;
}
/// <summary>
/// 处理描边顶点,对顶点进行外扩后得到描边
/// </summary>
/// <param name="vh"></param>
/// <returns></returns>
private List<UIVertex> ProcessOutlineVertices(VertexHelper vh)
{
var vtexs = new List<UIVertex>();
vh.GetUIVertexStream(vtexs);
var count = vtexs.Count - 3;
for(var i = 0; i <= count; i += 3)
{
//获取三个顶点,顶点顺充是依次排列的
var v1 = vtexs[i];
var v2 = vtexs[i + 1];
var v3 = vtexs[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 pCenter = 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;
}
var uvMin = Min(v1.uv0, v2.uv0, v3.uv0);
var uvMax = Max(v1.uv0, v2.uv0, v3.uv0);
v1 = GetNewPosAndUV(v1, pCenter, trix, triy, uvx, uvy, uvMin, uvMax);
v2 = GetNewPosAndUV(v2, pCenter, trix, triy, uvx, uvy, uvMin, uvMax);
v3 = GetNewPosAndUV(v3, pCenter, trix, triy, uvx, uvy, uvMin, uvMax);
vtexs[i] = v1;
vtexs[i + 1] = v2;
vtexs[i + 2] = v3;
}
return vtexs;
}
/// <summary>
/// 获取偏移后新的顶点和uv数据
/// </summary>
/// <param name="orgVertex"></param>
/// <param name="posCenter"></param>
/// <param name="trix"></param>
/// <param name="triy"></param>
/// <param name="uvx"></param>
/// <param name="uvy"></param>
/// <param name="uvMin"></param>
/// <param name="uvMax"></param>
/// <returns></returns>
private UIVertex GetNewPosAndUV(UIVertex orgVertex, Vector2 posCenter, Vector2 trix, Vector2 triy, Vector2 uvx, Vector2 uvy, Vector2 uvMin, Vector2 uvMax)
{
//偏移顶点
var pos = orgVertex.position;
var posOffsetx = pos.x > posCenter.x ? OutlineWidth : -OutlineWidth;
var posOffsety = pos.y > posCenter.y ? OutlineWidth : -OutlineWidth;
pos.x += posOffsetx;
pos.y += posOffsety;
orgVertex.position = pos;
//映射回正确的顶点UV
var uv = orgVertex.uv0;
var uvOffsetx = uvx / trix.magnitude * posOffsetx * (Vector2.Dot(trix, Vector2.right) > 0 ? 1 : -1);
var uvOffsety = uvy / triy.magnitude * posOffsety * (Vector2.Dot(triy, Vector2.up) > 0 ? 1 : -1);
uv.x += (uvOffsetx.x + uvOffsety.x);
uv.y += (uvOffsetx.y + uvOffsety.y);
orgVertex.uv0 = uv;
orgVertex.uv1 = new Vector4(uvMin.x, uvMin.y, uvMax.x, uvMax.y);
orgVertex.uv2.x = 0;//存储0,标志此顶点不是阴影顶点
return orgVertex;
}
private float Min(float pa, float pb, float pc)
{
return Mathf.Min(Mathf.Min(pa, pb), pc);
}
private float Max(float pa, float pb, float pc)
{
return Mathf.Max(Mathf.Max(pa, pb), pc);
}
private 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 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));
}
}
}
代码比较简短,关键的地方都有注释,比较难理解的是外扩UV映射那段。特别要注意的是,阴影与描边的顶点添加,需要按先渲染的先添加到列表内,否则有可能阴影会渲染到字面上。
四、Shader完整代码
我们再来看Shader的完整代码,如下:
Shader "UI/My/OutlineExt"
{
Properties
{
[PerRendererData]
_MainTex ("Main Texture", 2D) = "white" {}
_Color("Tint", COLOR) = (1, 1, 1, 1)
_OutlineColor("Outline Color", COLOR) = (1, 1, 1, 1)
_OutlineWidth("OutlineWidth", int) = 1
[Toggle]_Sharpening("Sharpening", int) = 0
[Toggle]_Shadow("Shadow", int) = 0
_ShadowOffset("ShadowOffset", Vector) = (1, 1, 0, 0)
_ShadowColor("ShadowColor", 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
}
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
#include "UnityCG.cginc"
sampler2D _MainTex;
fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _MainTex_TexelSize;
float4 _OutlineColor;
int _OutlineWidth;
int _Sharpening;
int _Shadow;
float2 _ShadowOffset;
float4 _ShadowColor;
struct appdata
{
float4 pos : POSITION;
float2 tex : TEXCOORD0; //原uv
fixed4 col : COLOR;
float4 uv1 : TEXCOORD1; //偏移顶点后映射后的uv
float4 uv2 : TEXCOORD2; //顶点是否是阴影的uv
};
struct v2f
{
float4 svPos : SV_POSITION;
float2 tex : TEXCOORD0;
fixed4 col : COLOR;
float4 uv1 : TEXCOORD1;
float4 uv2 : TEXCOORD2;
};
v2f vert (appdata ind)
{
v2f outd;
outd.svPos = UnityObjectToClipPos(ind.pos);
outd.tex = ind.tex;
outd.uv1 = ind.uv1;
outd.uv2 = ind.uv2;
outd.col = ind.col;
return outd;
}
//偏移后的顶点有可能进入其它贴图范围,所以裁剪属于自身范围内的
fixed IsInRect(float2 pos, float2 minXY, float2 maxXY)
{
pos = step(minXY, pos) * step(pos, maxXY);
return pos.x * pos.y;
}
//对alpha 取样,边缘一般都是有灰度的像素,有灰度就会有alpha值,所以会得到正确的描边
fixed SampleAlpha(int pIndex, v2f v)
{
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 = v.tex + _MainTex_TexelSize.xy * float2(cosArray[pIndex], sinArray[pIndex]) * _OutlineWidth;
return IsInRect(pos, v.uv1.xy, v.uv1.zw) * (tex2D(_MainTex, pos) + _TextureSampleAdd).a * _OutlineColor.a;
}
fixed4 frag (v2f v) : SV_Target
{
//原颜色采样,unity 字体必须加上 _TextureSampleAdd 采样,否则始终会返回黑色,其它的tex2d采样就不需要
fixed4 col = (tex2D(_MainTex, v.tex) + _TextureSampleAdd) * v.col;
if(_Shadow > 0 && v.uv2.x == -1)
{
col = _ShadowColor * col.a;//渲染阴影顶点
}
if(_OutlineWidth > 0 && v.uv2.x != -1 )
{
col.w *= IsInRect(v.tex, v.uv1.xy, v.uv1.zw);
half4 val = half4(_OutlineColor.x, _OutlineColor.y, _OutlineColor.z, 0);
//alpha 叠加值作为描边的透明度
val.w += SampleAlpha(0, v);
val.w += SampleAlpha(1, v);
val.w += SampleAlpha(2, v);
val.w += SampleAlpha(3, v);
val.w += SampleAlpha(4, v);
val.w += SampleAlpha(5, v);
val.w += SampleAlpha(6, v);
val.w += SampleAlpha(7, v);
val.w += SampleAlpha(8, v);
val.w += SampleAlpha(9, v);
val.w += SampleAlpha(10, v);
val.w += SampleAlpha(11, v);
val.w = clamp(val.w, 0, 1);
if(_Sharpening > 0)
{
val.w = step(0.5f, val.w); //描边锐化处理
}
col = (val * (1.0 - col.a)) + (col * col.a);//反向透明透出描边颜色,叠加上原来的预乘颜色值,预乘能减少模糊抗锯齿
col.a *= v.col.a;
}
return col;
}
ENDCG
}
}
}
Shader 代码也是比较简短,关键的地方也有注释。需要注意的时,因为描边与阴影可以同时存在,所以在frag阶段要分开着色,它们的区分点是UV2从C#传过来的-1值。描边锐化的操作,实际上是将alpha值去除头尾。
五、效果测试
阴影,如图:
描边,如图:
阴影和描边,如图:
属性的设置如图:
六、提供菜单方便使用
我们再来看看编辑器的拓展,主要方便使用,代码如下:
using Gamelogic;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
public class WinOutline : EditorWindow
{
[MenuItem("Tools/AddOutlineExt", false, 10)]
static void DoIt()
{
AddOuline();
}
//右键菜单
[MenuItem("GameObject/Effects/OutlineExt", false, 10)]
static void DoIt2()
{
AddOuline();
}
private static void AddOuline()
{
var selected = Selection.activeGameObject;
if (selected != null)
{
var mat = AssetDatabase.LoadAssetAtPath<Material>("Assets/Res/Shaders/OutlineExt/matOutlineExt.mat");
var oext = selected.GetComponent<OutlineExt>();
if(oext == null)
{
oext = selected.AddComponent<OutlineExt>();
}
oext.ShaderExt = mat.shader;
var txt = selected.GetComponent<Text>();
txt.material = mat;
}
}
}
感谢阅读!