1. 程式人生 > >Unity ShaderLab開發實戰(四)描邊

Unity ShaderLab開發實戰(四)描邊

      之前可能在面剔除中提到過,面剔除可以用來實現描邊效果。(以下效果圖來自Unity3D ShaderLab開發實戰詳解)

       原理:這是一個最簡單的描邊,使用面剔除:Cull指令,上圖中 ,最左邊的球使用的是Cull Front, 中間的使用Cull Back。最右邊的球第一個Pass使用了Cull Front並且將球體沿法線擠出一點點,第二個Pass使用Cull Back正常渲染,從而產生了描邊效果。下面開始講一下各種描邊。

1.最簡單的方式,一個pass講物體沿法線擠出,形成輪廓。

效果:

程式碼:

Shader "Tut/Shader/Toon/Outline_1" {
    Properties {
        _Outline("Outline",range(0,0.2))=0.02
    }
    SubShader {
        pass{
        Tags{"LightMode"="Always"}
        Cull Off
        ZWrite Off
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"
        float _Outline;
        struct v2f {
            float4 pos:SV_POSITION;
        };

        v2f vert (appdata_full v) {
            v2f o;
            v.vertex.xyz+=v.normal*_Outline;
            o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
            return o;
        }
        float4 frag(v2f i):COLOR
        {
            float4 c=0;
            return c;
        }
        ENDCG
        }//end of pass
        pass{
        Tags{"LightMode"="ForwardBase"}
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"

        float4 _LightColor0;
        struct v2f {
            float4 pos:SV_POSITION;
            float3 lightDir:TEXCOORD0;
            float3 viewDir:TEXCOORD1;
            float3 normal:TEXCOORD2;
        };

        v2f vert (appdata_full v) {
            v2f o;
            o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
            o.normal=v.normal;
            o.lightDir=ObjSpaceLightDir(v.vertex);
            o.viewDir=ObjSpaceViewDir(v.vertex);

            return o;
        }
        float4 frag(v2f i):COLOR
        {
            float4 c=1;
            float3 N=normalize(i.normal);
            float3 viewDir=normalize(i.viewDir);
            float diff=dot(N,i.lightDir);
            diff=(diff+1)/2;
            diff=smoothstep(diff/12,1,diff);
            c=_LightColor0*diff;
            return c;
        }
        ENDCG
        }
    } 
}

       這個方法有兩個問題:1.重疊物體區域沒有描邊,因為關閉了ZWrite,後面渲染的物體根據ZTest的結果將物體自己渲染輸出寫入,把輪廓擦掉了;2.輪廓的粗細和相機遠近有關,距離越遠,輪廓越細;3.有些地方輪廓是間斷的,比如上圖中cube,相鄰兩個面的法線方向是分離的,再沿法線擠出去後當然就被分開了。

2.先解決第一和第二個問題,開啟ZWrite,第一個問題就解決了,第二個問題我們希望最終的輸出是在螢幕上看到的那樣擠出來,而不是模型上,不希望考慮鏡頭的遠近,也就是在視空間下擠。

效果:

程式碼:

Shader "Tut/Shader/Toon/Outline_1x" {
    Properties {
        _Outline("Out line",range(0,0.1))=0.02
    }
    SubShader {
        pass{
        Tags{"LightMode"="Always"}
        Cull Front
        ZWrite On
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"
        float _Outline;
        struct v2f {
            float4 pos:SV_POSITION;
        };

        v2f vert (appdata_full v) {
            v2f o;
            o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
            float3 norm   = mul ((float3x3)UNITY_MATRIX_IT_MV, v.normal);
            float2 offset = TransformViewToProjection(norm.xy);
            //offset=normalize(offset);
            o.pos.xy += offset * o.pos.z *_Outline;
            return o;
        }
        float4 frag(v2f i):COLOR
        {
            return 0;
        }
        ENDCG
        }//end of pass
        pass{
        Tags{"LightMode"="ForwardBase"}
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"
        float4 _LightColor0;
        sampler2D _MainTex;

        struct v2f {
            float4 pos:SV_POSITION;
            float3 lightDir:TEXCOORD0;
            float3 viewDir:TEXCOORD1;
            float3 normal:TEXCOORD2;
        };

        v2f vert (appdata_full v) {
            v2f o;
            o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
            o.normal=v.normal;
            o.lightDir=ObjSpaceLightDir(v.vertex);
            o.viewDir=ObjSpaceViewDir(v.vertex);

            return o;
        }
        float4 frag(v2f i):COLOR
        {
            float4 c=1;
            float3 N=normalize(i.normal);
            float3 viewDir=normalize(i.viewDir);
            float diff=dot(N,i.lightDir);
            diff=(diff+1)/2;
            diff=smoothstep(diff/12,1,diff);
            c=_LightColor0*diff;
            return c;
        }
        ENDCG
        }
    } 
}
3.接下來解決間斷的問題:如果相鄰的面法線方向不太一致,擠出來後的方向就會產生斷裂。如果不把頂點資料當作位置,而當作是一個方向向量,則不管它被多少面共享,這些共享頂點的方向是唯一的。這樣交接處就不會因為往不同方向擠出而導致斷裂了。

效果:

程式碼:

Shader "Tut/Shader/Toon/Outline_2" {
    Properties {
        _Outline("Out line",range(0,0.1))=0.02
    }
    SubShader {
        pass{
        Tags{"LightMode"="Always"}
        Cull Front
        ZWrite On
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"
        float _Outline;
        struct v2f {
            float4 pos:SV_POSITION;
        };

        v2f vert (appdata_full v) {
            v2f o;
            o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
            float3 dir=normalize(v.vertex.xyz);
            dir   = mul ((float3x3)UNITY_MATRIX_IT_MV, dir);
            
            float2 offset = TransformViewToProjection(dir.xy);
            //offset=normalize(offset);
            o.pos.xy += offset * o.pos.z *_Outline;
            return o;
        }
        float4 frag(v2f i):COLOR
        {
            return 0;
        }
        ENDCG
        }//end of pass
        pass{
        Tags{"LightMode"="ForwardBase"}
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"
        float4 _LightColor0;
        sampler2D _MainTex;

        struct v2f {
            float4 pos:SV_POSITION;
            float3 lightDir:TEXCOORD0;
            float3 viewDir:TEXCOORD1;
            float3 normal:TEXCOORD2;
        };

        v2f vert (appdata_full v) {
            v2f o;
            o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
            o.normal=v.normal;
            o.lightDir=ObjSpaceLightDir(v.vertex);
            o.viewDir=ObjSpaceViewDir(v.vertex);

            return o;
        }
        float4 frag(v2f i):COLOR
        {
            float4 c=1;
            float3 N=normalize(i.normal);
            float3 viewDir=normalize(i.viewDir);
            float diff=dot(N,i.lightDir);
            diff=(diff+1)/2;
            diff=smoothstep(diff/12,1,diff);
            c=_LightColor0*diff;
            return c;
        }
        ENDCG
        }
    } 
}
4.上面對第三個問題的解決方法還有點小問題,當物體的模型空間的原點不在幾何中心時,就會出現某些地方沿頂點擠出的方向看不到,描邊會很不均勻(下圖),需要調和法線和頂點方向(其實就是頂點的方向和法線的方向相互拉一拉,靠攏一下)。

解決方法,簡單的,比如第一個Pass的頂點shader改為法線和頂點方向的調和:

v2f vert (appdata_full v) {
            v2f o;
            o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
            float3 dir=normalize(v.vertex.xyz);
            float3 dir2=v.normal;
            dir=lerp(dir,dir2,_Factor);
            dir= mul ((float3x3)UNITY_MATRIX_IT_MV, dir);
            float2 offset = TransformViewToProjection(dir.xy);
            offset=normalize(offset);
            o.pos.xy += offset * o.pos.z *_Outline;
            return o;
        }

也可以通用一些,試用於複雜圖形。通過頂點方向和法線的點積為正或負來判斷是指向幾何中心還是背離幾何中心,並且可以得到指向或背離的程度。

效果:

程式碼:

Shader "Tut/Shader/Toon/Outline_3x" {
    Properties {
        _Outline("Out line",range(0,0.1))=0.02
        _Factor("Factor",range(1,100))=1
    }
    SubShader {
        pass{
        Tags{"LightMode"="Always"}
        Cull Front
        ZWrite On
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"
        float _Outline;
        float _Factor;
        struct v2f {
            float4 pos:SV_POSITION;
        };
        v2f vert (appdata_full v) {
            v2f o;
            o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
            float3 dir=normalize(v.vertex.xyz);
            float3 dir2=v.normal;
            float D=dot(dir,dir2);
            D=(D/_Factor+1)/(1+1/_Factor);
            dir=lerp(dir2,dir,D);
            dir= mul ((float3x3)UNITY_MATRIX_IT_MV, dir);
            float2 offset = TransformViewToProjection(dir.xy);
            offset=normalize(offset);
            o.pos.xy += offset * o.pos.z *_Outline;
            return o;
        }
        float4 frag(v2f i):COLOR
        {
            return 0;
        }
        ENDCG
        }//end of pass
        pass{
        Tags{"LightMode"="ForwardBase"}
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"
        float4 _LightColor0;

        struct v2f {
            float4 pos:SV_POSITION;
            float3 lightDir:TEXCOORD0;
            float3 viewDir:TEXCOORD1;
            float3 normal:TEXCOORD2;
        };

        v2f vert (appdata_full v) {
            v2f o;
            o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
            o.normal=v.normal;
            o.lightDir=ObjSpaceLightDir(v.vertex);
            o.viewDir=ObjSpaceViewDir(v.vertex);
            return o;
        }
        float4 frag(v2f i):COLOR
        {
            float4 c=1;
            float3 N=normalize(i.normal);
            float3 viewDir=normalize(i.viewDir);
            float diff=max(0,dot(N,i.lightDir));
            diff=(diff+1)/2;
            diff=smoothstep(0,1,diff);
            c=_LightColor0*diff;
            return c;
        }
        ENDCG
        }
    } 
}
5.上面的方法導致出現一個新問題,cube中間的稜沒描出來,解決方法:可以使用兩個Pass中和,第一個Pass沿法線方向擠出,這個時候有裂痕了,第二個Pass在裂痕的基礎上更多的向頂點方向擠出,這樣就在裂痕處形成描邊。

SubShader {
        pass{
        Tags{"LightMode"="Always"}
        Cull Back
        ZWrite On
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"
        float _Outline;
        float _Factor;
        struct v2f {
            float4 pos:SV_POSITION;
        };

        v2f vert (appdata_full v) {
            v2f o;
            o.pos=mul(UNITY_MATRIX_MVP,v.vertex);

            float3 dir=normalize(v.vertex.xyz);
            float3 dir2=v.normal;
            dir=lerp(dir,dir2,_Factor);
            dir= mul ((float3x3)UNITY_MATRIX_IT_MV, dir);
            float2 offset = TransformViewToProjection(dir.xy);
            offset=normalize(offset);
            o.pos.xy += offset * o.pos.z *_Outline;

            return o;
        }
        float4 frag(v2f i):COLOR
        {
            return float4(1,1,1,1);
        }
        ENDCG
        }//end of pass .1
        pass{
        Tags{"LightMode"="Always"}
        Cull Front
        ZWrite Off
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"
        float _Outline2;
        float _Factor2;
        struct v2f {
            float4 pos:SV_POSITION;
        };

        v2f vert (appdata_full v) {//.2
            v2f o;
            o.pos=mul(UNITY_MATRIX_MVP,v.vertex);

            float3 dir=normalize(v.vertex.xyz);
            float3 dir2=v.normal;
            dir=lerp(dir,dir2,_Factor2);
            dir= mul ((float3x3)UNITY_MATRIX_IT_MV, dir);
            float2 offset = TransformViewToProjection(dir.xy);
            offset=normalize(offset);
            o.pos.xy += offset * o.pos.z *_Outline2;
            return o;
        }
        float4 frag(v2f i):COLOR
        {
            return float4(0,0,0,0);
        }
        ENDCG
        }//end of pass .2
    } 

    _Factor2在第一個係數_Factor的結果上來調整描邊。但是現在還缺少一個光照上色的Pass,所以使用混合,最終效果(自行調整引數):

程式碼:

Shader "Tut/Shader/Toon/Outline_4.1" {
    Properties {
        _Outline("Out line",range(0,0.1))=0.02
        _Outline2("Out line2",range(0,0.1))=0.02
        _Factor("Factor",range(0,1))=0.5
        _Factor2("Factor",range(0,1))=0.5
    }
    SubShader {
        pass{
        Tags{"LightMode"="Always"}
        Cull Back
        ZWrite On
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"
        float _Outline;
        float _Factor;
        struct v2f {
            float4 pos:SV_POSITION;
        };

        v2f vert (appdata_full v) {
            v2f o;
            o.pos=mul(UNITY_MATRIX_MVP,v.vertex);

            float3 dir=normalize(v.vertex.xyz);
            float3 dir2=v.normal;
            dir=lerp(dir,dir2,_Factor);
            dir= mul ((float3x3)UNITY_MATRIX_IT_MV, dir);
            float2 offset = TransformViewToProjection(dir.xy);
            offset=normalize(offset);
            o.pos.xy += offset * o.pos.z *_Outline;

            return o;
        }
        float4 frag(v2f i):COLOR
        {
            return float4(1,1,1,1);
        }
        ENDCG
        }//end of pass .1
        pass{
        Tags{"LightMode"="Always"}
        Cull Front
        ZWrite Off
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"
        float _Outline2;
        float _Factor2;
        struct v2f {
            float4 pos:SV_POSITION;
        };

        v2f vert (appdata_full v) {//.2
            v2f o;
            o.pos=mul(UNITY_MATRIX_MVP,v.vertex);

            float3 dir=normalize(v.vertex.xyz);
            float3 dir2=v.normal;
            dir=lerp(dir,dir2,_Factor2);
            dir= mul ((float3x3)UNITY_MATRIX_IT_MV, dir);
            float2 offset = TransformViewToProjection(dir.xy);
            offset=normalize(offset);
            o.pos.xy += offset * o.pos.z *_Outline2;
            return o;
        }
        float4 frag(v2f i):COLOR
        {
            return float4(0,0,0,0);
        }
        ENDCG
        }//end of pass .2
        pass{
        Tags{"LightMode"="ForwardBase"}
        Blend DstColor Zero
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"

        float4 _LightColor0;
        sampler2D _MainTex;
        float _Factor;
        float _Outline;

        struct v2f {
            float4 pos:SV_POSITION;
            float3 lightDir:TEXCOORD0;
            float3 viewDir:TEXCOORD1;
            float3 normal:TEXCOORD2;
        };

        v2f vert (appdata_full v) {
            v2f o;
            o.pos=mul(UNITY_MATRIX_MVP,v.vertex);

            float3 dir=normalize(v.vertex.xyz);
            float3 dir2=v.normal;
            dir=lerp(dir,dir2,_Factor);
            dir= mul ((float3x3)UNITY_MATRIX_IT_MV, dir);
            float2 offset = TransformViewToProjection(dir.xy);
            offset=normalize(offset);
            o.pos.xy += offset * o.pos.z *_Outline;

            o.normal=v.normal;
            o.lightDir=ObjSpaceLightDir(v.vertex);
            o.viewDir=ObjSpaceViewDir(v.vertex);

            return o;
        }
        float4 frag(v2f i):COLOR
        {
            float4 c=1;
            float3 N=normalize(i.normal);
            float3 viewDir=normalize(i.viewDir);
            float diff=max(0,dot(N,i.lightDir));
            diff=(diff+1)/2;
            diff=smoothstep(0,1,diff);
            c=_LightColor0*diff;
            return c;
        }
        ENDCG
        }
    } 
}

補充:通過視線和法線的夾角來判定是否是物體邊緣進行描邊不穩定,這個方法常用於Rim效果,下節講Rim。