刚好需要解决一下UI抗锯齿的问题,顺便记录一下。
最常见的锯齿就是圆形的锯齿。
我们先创建一个圆形,使用CanvasRender绘制:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CircleCanvasMesh : MonoBehaviour
{
public float radius = 10f;
public int segement = 10;
public Material mat;
private CanvasRenderer canvasRender;
private float lastRadius = 0f;
private int lastSegement = 0;
private Mesh mesh;
void Start()
{
canvasRender = GetComponent<CanvasRenderer>();
canvasRender.SetMaterial(mat, null);
}
void Update()
{
if (lastRadius != radius || lastSegement != segement)
{
if (mesh != null)
{
mesh.Clear();
mesh = null;
}
mesh = new Mesh();
List<Vector3> vertlist = new List<Vector3>();
List<Vector2> uvlist = new List<Vector2>();
List<int> trilist = new List<int>();
Vector3 cvert = Vector3.zero;
vertlist.Add(cvert);
Vector2 cuv = Vector2.zero;
uvlist.Add(cuv);
float segdeg = Mathf.PI * 2f / (float)segement;
for (int i = 0; i < segement; i++)
{
float deg = i * segdeg;
float cos = Mathf.Cos(deg);
float sin = Mathf.Sin(deg);
Vector3 segvert = cvert + new Vector3(cos * radius, sin * radius, 0);
vertlist.Add(segvert);
Vector2 seguv = cuv + new Vector2(cos, sin);
uvlist.Add(seguv);
trilist.AddRange(new int[]
{
0,i+1,(i+2)>segement?(i+2-segement):(i+2),
});
}
mesh.vertices = vertlist.ToArray();
mesh.uv = uvlist.ToArray();
mesh.triangles = trilist.ToArray();
canvasRender.SetMesh(mesh);
lastRadius = radius;
lastSegement = segement;
}
}
}
默认效果如下:
可以看得出来锯齿感很严重,所以需要抗锯齿技术来处理一下。
一般我们设置全局抗锯齿都是基于最终画面4x8x的超采样,所以开发UI功能的这种情况就不适用,就得使用类似FXAA(fast-approximate anti-alaising),也就是基于posteffect后期处理的边缘模糊,属于模糊锯齿。
FXAA wiki
那么我们怎么模糊这个圆形的锯齿呢?可以做基于uv距离的边缘alpha渐变(有个专业名词叫SDF,以后详细聊),如下:
Shader "SDFAntiAlias/ColorAntiAliasUnlitShader"
{
Properties
{
_MainTex("Texture", 2D) = "white" {}
_EdgeWidth("Edge Width",range(0,0.05)) = 0
[Toggle]_IsAA("Is AntiAlias",int) = 0
}
SubShader
{
Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }
LOD 100
Pass
{
Cull front
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _EdgeWidth;
int _IsAA;
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
if (_IsAA)
{
float uvlen = sqrt(i.uv.x * i.uv.x + i.uv.y * i.uv.y);
float inlen = 1 - _EdgeWidth;
//根据内边缘距离进行alpha插值
if (uvlen > inlen)
{
col.a = lerp(1, 0, (uvlen - inlen) / _EdgeWidth);
}
}
return col;
}
ENDCG
}
}
}
这里设定一个内边缘uv宽度,然后判断uv对应的pixel在这个宽度内,从内向外alpha插值(1->0),就成了这样:
可以看得出来边缘经过alpha插值模糊之后,就表现得平滑了。
不过这种方法也是局限性很大的,比如:
如果我们是不规则图形,那么无法使用uv_distance准确的进行“边缘”判断,上图明显看出10条边没有模糊,所以还得给每个pixel增加一个数值,标识pixel到中心的距离,那么我们还得额外增加一张贴图记录数据,太麻烦了。
那么有没有方法可以判断一个不规则图形的边界呢?可以使用顶点函数着色,如下:
Shader "SDFAntiAlias/VertexColorUnlitShader"
{
Properties
{
_CenterColor ("Center Color", Color) = (1,1,1,1)
_EdgeColor("Edge Color",Color) = (0,0,0,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
Cull front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 col : COLOR;
float4 vertex : SV_POSITION;
};
float4 _CenterColor;
float4 _EdgeColor;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//根据中心到边缘进行插值,白色插值到黑色
//为了效率使用RGB单个分量即可
float len = v.uv.x*v.uv.x+v.uv.y*v.uv.y;
o.col = lerp(_CenterColor,_EdgeColor,len);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = i.col;
return col;
}
ENDCG
}
}
}
效果如下:
利用顶点着色的颜色插值,我们可以得到中心白色边缘黑色的渐变色域,那么我们根据R分量[1->0],就可以得到边缘(黑色)了。
当然如果有很特殊的情况,我们也可以根据mesh.colors或vertexid来自行定义每个顶点的颜色。
Color ccol = Color.white;
collist.Add(ccol);
for (int i = 0; i < segement; i++)
{
collist.Add(Color.black);
}
mesh.colors = collist.ToArray();
或者
Shader "SDFAntiAlias/VertexIdColorUnlitShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
Cull front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 col : COLOR;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v,uint vid : SV_vertexID)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
if(vid == 0)
{
o.col = float4(1,1,1,1);
}
else
{
o.col = float4(0,0,0,0);
}
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = i.col;
return col;
}
ENDCG
}
}
}
都可以达到颜色插值的效果,接下来就是根据边缘黑色进行模糊,如下:
Shader "SDFAntiAlias/ColorAntiAlias2UnlitShader"
{
Properties
{
_MainTex("Texture", 2D) = "white" {}
_EdgeWidth("Edge Width",range(0,0.05)) = 0
[Toggle]_IsAA("Is AntiAlias",int) = 0
}
SubShader
{
Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }
LOD 100
Pass
{
Cull front
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 col : COLOR;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _EdgeWidth;
int _IsAA;
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
float len = v.uv.x*v.uv.x+v.uv.y*v.uv.y;
o.col = lerp(float4(1,1,1,1),float4(0,0,0,1),len);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
if (_IsAA)
{
float edge = i.col.r;
//根据边缘黑色作为阈值进行alpha插值即可
if(edge<_EdgeWidth)
{
col.a = lerp(0, 1, edge / _EdgeWidth);;
}
}
return col;
}
ENDCG
}
}
}
根据边缘黑色和阈值进行判断alpha插值就能完成抗锯齿,效果如下:
可以看的出来抗锯齿效果还行。
总结就是UI图形抗锯齿最重要的就是“边缘”的查找和模糊,主要使用基于UV或颜色值的距离判断。