Unity Shader - 描邊

我們都知道描邊效果在遊戲中很常見,比如選中某個角色時需要凸顯該模型,就會採用描邊效果,今天我們就來實現一下該效果。

描邊的效果實現方式有很多種,就以目前我知道的就有三種方式。

一:模型擴張

效果圖:
在這裏插入圖片描述

大致思路:需要兩個pass,一個pass渲染背面並且沿着法線方向擴張,用來作爲輪廓,一個pass渲染正面,正常渲染。

核心:主要在第一個pass的頂點着色器中對頂點的偏移,偏移方向爲法線方向。

話不多說直接上代碼,具體可看代碼,有詳細註釋。

//--------------------------- 【描邊】 - 法線擴張---------------------
//create by 長生但酒狂

Shader "lcl/shader3D/outLine3D_swell"
{
    //---------------------------【屬性】---------------------------
    Properties
    {   
        // 主紋理
        _MainTex ("Texture", 2D) = "white" {}
        // 主顏色
        _Color("Color",Color)=(1,1,1,1)
        // 描邊強度
        _power("power",Range(0,0.2)) = 0.05
        // 描邊顏色
        _lineColor("lineColor",Color)=(1,1,1,1)
    }
    // ------------------------【CG代碼】---------------------------
    CGINCLUDE
    #include "UnityCG.cginc"
    #include "Lighting.cginc"
    //頂點着色器輸入結構體
    struct appdata
    {
        float4 vertex : POSITION;//頂點座標
        float2 uv : TEXCOORD0;//紋理座標
        float3 normal:NORMAL;//法線
    };
    //頂點着色器輸出結構體
    struct v2f
    {
        float4 vertex : SV_POSITION;//像素座標
        float2 uv : TEXCOORD0;//紋理座標
        float3 worldNormalDir:COLOR0;//世界空間裏的法線方向
        float3 worldPos:COLOR1;//世界空間裏的座標

    };
    // ------------------------【變量聲明】---------------------------
    //紋理
    sampler2D _MainTex;
    //內置的變量,紋理中的單像素尺寸
    float4 _MainTex_TexelSize;
    //主顏色
    float4 _Color;
    //描邊強度
    float _power;
    //描邊顏色
    float4 _lineColor;

    // ------------------------【背面-頂點着色器】---------------------------
    v2f vert_back (appdata v)
    {
        v2f o;
        //法線方向
        v.normal = normalize(v.normal);
        //頂點沿着法線方向擴張
        v.vertex.xyz +=  v.normal * _power;
        //由模型空間座標系轉換到裁剪空間
        o.vertex = UnityObjectToClipPos(v.vertex);
        //輸出結果
        return o;
    }

    // ------------------------【背面-片元着色器】---------------------------
    fixed4 frag_back (v2f i) : SV_Target
    {
        //直接輸出顏色
        return _lineColor;
    }

    // ------------------------【正面-頂點着色器】---------------------------
    v2f vert_front (appdata v)
    {
        //正常渲染
        v2f o;
        o.uv = v.uv;
        //法線從模型空間座標系轉換到世界座標系
        o.worldNormalDir = mul(v.normal,(float3x3) unity_WorldToObject);
        //頂點從模型空間座標系轉換到世界座標系
        o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; 
        //歸一化
        v.normal = normalize(v.normal);
        //由模型空間座標系轉換到裁剪空間
        o.vertex = UnityObjectToClipPos(v.vertex);
        return o;
    }
    // ------------------------【正面-片元着色器】---------------------------
    fixed4 frag_front (v2f i) : SV_Target
    {
        //正常渲染
        //紋理顏色值
        fixed4 col = tex2D(_MainTex, i.uv);
        //環境光
        fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Color.xyz;
        //視角方向
        float3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
        //法線方向
        float3 normaleDir = normalize(i.worldNormalDir);
        //光照方向歸一化
        fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
        //半蘭伯特模型
        fixed3 lambert = 0.5 * dot(normaleDir, worldLightDir) + 0.5;
        //漫反射
        fixed3 diffuse = lambert * _Color.xyz * _LightColor0.xyz + ambient;
        //最終結果
        fixed3 result = diffuse * col.xyz;
        return float4(result,1);
    }
    ENDCG
    // ------------------------【子着色器】---------------------------
    SubShader
    {
        //透明度混合模式
        Blend SrcAlpha OneMinusSrcAlpha
        //渲染隊列
        Tags{ "Queue" = "Transparent"}
        
        // ------------------------【背面通道】---------------------------
        Pass
        {
            //剔除正面
            Cull Front
            //防止背面模型穿透正面模型
            //關閉深度寫入,爲了讓正面的pass完全覆蓋背面,同時要把渲染隊列改成Transparent,此時物體渲染順序是從後到前的
            ZWrite Off

            CGPROGRAM
            #pragma vertex vert_back
            #pragma fragment frag_back
            ENDCG
        }

        // ------------------------【正面通道】---------------------------
        Pass
        {
            //剔除背面
            Cull Back
            CGPROGRAM
            #pragma vertex vert_front
            #pragma fragment frag_front
            ENDCG
        }
    }
}

二:基於法線與視角方向的夾角

大致思路:計算法線與視角方向的夾角,夾角越大,越接近邊緣。

效果圖如下:
在這裏插入圖片描述

shader代碼:

//--------------------------- 【描邊】 - 基於法線與視角夾角---------------------
//create by 長生但酒狂
Shader "lcl/shader3D/outline3D"
{
    //---------------------------【屬性】---------------------------
    Properties
    {
        // 主紋理
        _MainTex ("Texture", 2D) = "white" {}
        // 主顏色
        _Color("Color",Color)=(1,1,1,1)
        // 描邊強度
        _power("lineWidth",Range(0,10)) = 1
        // 描邊顏色
        _lineColor("lineColor",Color)=(1,1,1,1)
    }
    // ------------------------【子着色器】---------------------------
    SubShader
    {
        //渲染隊列
        Tags{
            "Queue" = "Transparent"
        }
        Blend SrcAlpha OneMinusSrcAlpha
        // 通道
        Pass
        {
            // ------------------------【CG代碼】---------------------------
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            //頂點着色器輸入結構體
            struct appdata
            {
                float4 vertex : POSITION;//頂點座標
                float2 uv : TEXCOORD0;//紋理座標
                float3 normal:NORMAL;//法線
            };
            //頂點着色器輸出結構體
            struct v2f
            {
                float4 vertex : SV_POSITION;//像素座標
                float2 uv : TEXCOORD0;//紋理座標
                float3 worldNormalDir:COLOR0;//世界空間裏的法線方向
                float3 worldPos:COLOR1;//世界空間裏的座標
            };

            // ------------------------【頂點着色器】---------------------------
            v2f vert (appdata v)
            {
                v2f o;
                o.uv = v.uv;
                o.worldNormalDir = mul(v.normal,(float3x3) unity_WorldToObject);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;  
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }
            // ------------------------【變量聲明】---------------------------
            sampler2D _MainTex;
            float4 _MainTex_TexelSize;
            float4 _Color;
            float _power;
            float4 _lineColor;
            // ------------------------【片元着色器】---------------------------
            fixed4 frag (v2f i) : SV_Target
            {
                //紋理顏色
                fixed4 col = tex2D(_MainTex, i.uv);
                //環境光
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Color.xyz;
                //視角方向
                float3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                //法線方向
                float3 normaleDir = normalize(i.worldNormalDir);
                //光照方向歸一化
                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                //半蘭伯特模型
                fixed3 lambert = 0.5 * dot(normaleDir, worldLightDir) + 0.5;
                //漫反射
                fixed3 diffuse = lambert * _Color.xyz * _LightColor0.xyz + ambient;
                fixed3 result = diffuse * col.xyz;
                //計算視角方向與法線的夾角(夾角越大,value值越小,越接近邊緣)
                float value = dot(viewDir,normaleDir);
                //
                value = 1 - saturate(value);
                //通過_power調節描邊強度
                value = pow(value,_power);
                //源顏色值和描邊顏色做插值
                result =lerp(result,_lineColor,value) ;

                return float4(result,1);
            }
            ENDCG
        }
    }
}

還有第三種方法是基於圖像實現的,比上面的兩種方式稍微複雜一點,這裏暫時就不詳說了,後面再單獨出一篇文章來詳解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章