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
        }
    }
}

还有第三种方法是基于图像实现的,比上面的两种方式稍微复杂一点,这里暂时就不详说了,后面再单独出一篇文章来详解。

发布了50 篇原创文章 · 获赞 864 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_28299311/article/details/103788028