入门图形学:UI抗锯齿

      刚好需要解决一下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或颜色值的距离判断。

猜你喜欢

转载自blog.csdn.net/yinhun2012/article/details/121480391