基於Shader實現的UGUI描邊解決方案

基於Shader實現的UGUI描邊解決方案

前言

大紮好,我係狗猥。當你們都覺得我鴿了的時候,我又出現了,這也是一種鴿。創業兩年失敗後歸來,今天想給你們分享一個我最近研究出來的好康的,比遊戲還刺激,還能夠教你登dua郎喔(大誤html

此次給你們帶來的是基於Shader實現的UGUI描邊,也支持對Text組件使用。git

首先請你們看看最終效果(上面放了一個Image和一個Text):
github

(8102年了怎麼還在艦算法

接下來,我會向你們介紹思路和具體實現過程。若是你想直接代到項目裏使用,請自行跳轉到本文最後,那裏有完整的C#和Shader代碼。canvas

本方案在Unity 2017.3.1p1下測試經過。c#

本文參考了http://blog.sina.com.cn/s/blog_6ad33d350102xb7v.htmlapp

轉載請註明出處:http://www.javashuo.com/article/p-dupzxqqa-ge.html編輯器


爲何要這麼作

就我參加工做這些年接觸到的UI美術來看,他們都挺喜歡用描邊效果。誠然這個效果可讓文字更加突出,看着也挺不錯。對美術來講作描邊簡單的一比,PS里加個圖層樣式就搞定,可是對咱們程序來講就是一件很痛苦的事。ide

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, float4 pClipRect)
            {
                pPos = step(pClipRect.xy, pPos) * step(pPos, pClipRect.zw);
                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了,有對象的)的時候我真的受到了極大刺激。一個妹子都能鑽得這麼深,我應該更加努力啊。學習是從搖籃到墳墓的過程,但願你們無論學什麼都要堅持。

還有一點就是創業真的要謹慎。最近了解到國家出了條例要對國產遊戲限量發行,對各個遊戲公司想必都是一記悶錘。加之統一徵收社保,引發的連鎖反應必然會波及到遊戲行業。惟一欣慰的是咱們還能作遊戲,還能在這條路上繼續走。那麼就繼續走下去吧,不要停下來啊!(指加班)

很慚愧,就作了一點微小的工做,謝謝你們!

相關文章
相關標籤/搜索