Unity Shader實現描邊效果


http://gad.qq.com/article/detail/28346

 

描邊效果是游戲里面非常常用的一種效果,一般是為了凸顯游戲中的某個對象,會給對象增加一個描邊效果。本篇文章和大家介紹下利用Shader實現描邊效果,一起來看看吧。

 

最近又跑回去玩了玩《劍靈》,雖然出了三年了,感覺在現在的網游里面畫面仍然算很好的了,劍靈里面走近或者選中NPC的一瞬間,NPC就會出現描邊效果,不過這個描邊效果是漸變的,會很快減弱最后消失(抓了好久才抓住一張圖....)


還有就是最常見的LOL中的塔,我們把鼠標移動到塔上,就會有很明顯的描邊效果:

 

簡單描邊效果的原理

 
描 邊效果有幾種實現方式。其實邊緣光效果與描邊效果有些類似,適當調整邊緣光效果,其實也可以達到凸顯要表達的對象的意思。邊緣光的實現最為簡單,只是在計 算的時候增加了一次計算法線方向與視線方向的夾角計算,用1減去結果作為系數乘以一個邊緣光顏色就達到了邊緣光的效果,是性能最好的一種方法,關於邊緣光 效果,可以參考一下之前的一篇文章:邊緣光效果。邊緣光的效果如下圖所示:
 
原始模型渲染:

使用了邊緣光的效果:

 
邊 緣光效果雖然簡單,但是有很大的局限性,邊緣光效果只是在當前模型本身的光照計算時調整了邊緣位置的顏色值,並沒有達到真正的“描邊”(當然,有時候我們 就是想要這種邊緣光的效果),而我們希望的描邊效果,一般都是在正常模型的渲染狀態下,在模型外面擴展出一個描邊的效果。既然要讓模型的形狀有所改變(向 外拓一點),那么肯定就和vertex shader有關系了。而我們的描邊效果,肯定就是要讓模型更“胖”一點,能夠把我們原來的大小包裹住;微觀一點來看,一個面,如果我們讓它向外拓展,而 我們指的外,也就是這個面的法線所指向的方向,那么就讓這個面朝着法線的方向平移一點;再微觀一點來看,對於頂點來說,也就是我們的vertex shader真正要寫的內容了,我們正常計算頂點的時候,傳入的vertex會經過MVP變換,最終傳遞給fragment shader,那么我們就可以在這一步讓頂點沿着法線的方向稍微平移一些。我們在描邊后,描邊這一次渲染的邊緣其實是沒有辦法和我們正常的模型進行區分 的,為了解決這個問題,就需要用兩個Pass來渲染,第一個Pass渲染描邊的效果,進行外拓,而第二個Pass進行原本效果的渲染,這樣,后面顯示的就 是稍微“胖”一點的模型,然后正常的模型貼在上面,把中間的部分擋住,邊緣擋不住就露出了描邊的部分了。
 

開啟深度寫入,剔除正面的描邊效果

 
知 道了原理,我們來考慮一下外拓的實現,我們可以在vertex階段獲得頂點的坐標,並且有法線的坐標,最直接的方式就是直接用頂點坐標+法線方向*描邊粗 細參數,然后用這個偏移的坐標值再進行MVP變換;但是這樣做有一個弊端,其實就是我們透視的近大遠小的問題,模型上離相機近的地方描邊效果較粗,而遠的 地方描邊效果較細。一種解決的方案是先進行MPV變換,變換完之后再去按照法線方向調整外拓。代碼如下:
[csharp] view plain copy
  1. //描邊Shader  
  2. //by:puppet_master  
  3. //2017.1.5  
  4.   
  5. Shader "ApcShader/Outline"  
  6. {  
  7.     //屬性  
  8.     Properties{  
  9.         _Diffuse("Diffuse", Color) = (1,1,1,1)  
  10.         _OutlineCol("OutlineCol", Color) = (1,0,0,1)  
  11.         _OutlineFactor("OutlineFactor", Range(0,1)) = 0.1  
  12.         _MainTex("Base 2D", 2D) = "white"{}  
  13.     }  
  14.   
  15.     //子着色器    
  16.     SubShader  
  17.     {  
  18.           
  19.         //描邊使用兩個Pass,第一個pass沿法線擠出一點,只輸出描邊的顏色  
  20.         Pass  
  21.         {  
  22.             //剔除正面,只渲染背面,對於大多數模型適用,不過如果需要背面的,就有問題了  
  23.             Cull Front  
  24.               
  25.             CGPROGRAM  
  26.             #include "UnityCG.cginc"  
  27.             fixed4 _OutlineCol;  
  28.             float _OutlineFactor;  
  29.               
  30.             struct v2f  
  31.             {  
  32.                 float4 pos : SV_POSITION;  
  33.             };  
  34.               
  35.             v2f vert(appdata_full v)  
  36.             {  
  37.                 v2f o;  
  38.                 //在vertex階段,每個頂點按照法線的方向偏移一部分,不過這種會造成近大遠小的透視問題  
  39.                 //v.vertex.xyz += v.normal * _OutlineFactor;  
  40.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  41.                 //將法線方向轉換到視空間  
  42.                 float3 vnormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);  
  43.                 //將視空間法線xy坐標轉化到投影空間,只有xy需要,z深度不需要了  
  44.                 float2 offset = TransformViewToProjection(vnormal.xy);  
  45.                 //在最終投影階段輸出進行偏移操作  
  46.                 o.pos.xy += offset * _OutlineFactor;  
  47.                 return o;  
  48.             }  
  49.               
  50.             fixed4 frag(v2f i) : SV_Target  
  51.             {  
  52.                 //這個Pass直接輸出描邊顏色  
  53.                 return _OutlineCol;  
  54.             }  
  55.               
  56.             //使用vert函數和frag函數  
  57.             #pragma vertex vert  
  58.             #pragma fragment frag  
  59.             ENDCG  
  60.         }  
  61.           
  62.         //正常着色的Pass  
  63.         Pass  
  64.         {  
  65.             CGPROGRAM     
  66.           
  67.             //引入頭文件  
  68.             #include "Lighting.cginc"  
  69.             //定義Properties中的變量  
  70.             fixed4 _Diffuse;  
  71.             sampler2D _MainTex;  
  72.             //使用了TRANSFROM_TEX宏就需要定義XXX_ST  
  73.             float4 _MainTex_ST;  
  74.   
  75.             //定義結構體:vertex shader階段輸出的內容  
  76.             struct v2f  
  77.             {  
  78.                 float4 pos : SV_POSITION;  
  79.                 float3 worldNormal : TEXCOORD0;  
  80.                 float2 uv : TEXCOORD1;  
  81.             };  
  82.   
  83.             //定義頂點shader,參數直接使用appdata_base(包含position, noramal, texcoord)  
  84.             v2f vert(appdata_base v)  
  85.             {  
  86.                 v2f o;  
  87.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  88.                 //通過TRANSFORM_TEX宏轉化紋理坐標,主要處理了Offset和Tiling的改變,默認時等同於o.uv = v.texcoord.xy;  
  89.                 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);  
  90.                 o.worldNormal = mul(v.normal, (float3x3)_World2Object);  
  91.                 return o;  
  92.             }  
  93.   
  94.             //定義片元shader  
  95.             fixed4 frag(v2f i) : SV_Target  
  96.             {  
  97.                 //unity自身的diffuse也是帶了環境光,這里我們也增加一下環境光  
  98.                 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;  
  99.                 //歸一化法線,即使在vert歸一化也不行,從vert到frag階段有差值處理,傳入的法線方向並不是vertex shader直接傳出的  
  100.                 fixed3 worldNormal = normalize(i.worldNormal);  
  101.                 //把光照方向歸一化  
  102.                 fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);  
  103.                 //根據半蘭伯特模型計算像素的光照信息  
  104.                 fixed3 lambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;  
  105.                 //最終輸出顏色為lambert光強*材質diffuse顏色*光顏色  
  106.                 fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;  
  107.                 //進行紋理采樣  
  108.                 fixed4 color = tex2D(_MainTex, i.uv);  
  109.                 color.rgb = color.rgb* diffuse;  
  110.                 return fixed4(color);  
  111.             }  
  112.   
  113.             //使用vert函數和frag函數  
  114.             #pragma vertex vert  
  115.             #pragma fragment frag     
  116.   
  117.             ENDCG  
  118.         }  
  119.     }  
  120.     //前面的Shader失效的話,使用默認的Diffuse  
  121.     FallBack "Diffuse"  
  122. }  
開啟了描邊效果:

原始模型渲染采用了半蘭伯特Diffuse進 行渲染,主要是前面多了一個描邊的Pass。這個Pass里,我們沒有關閉深度寫入,主要是開啟了模型的正面剔除,這樣,在這個Pass渲染的時候,就只 會渲染模型的背面,讓背面向外拓展一下,既不會影響什么,並且背面一般都在正面的后面,一般情況下不會遮擋住正面,正好符合我們后面的部分外拓的需求。這 個的主要優點是沒有關閉深度寫入,因為關閉深度寫入,引入的其他問題實在是太多了。
附上一張進行了Cull Front操作的效果,只渲染了我們正常看不到的面,效果比較驚悚:
然 后再來看看轉換的部分,我們通過UNITY_MATRIX_IT_MV矩陣將法線轉換到視空間,這里可能會比較好奇,為什么不用正常的頂點轉化矩陣來轉化 法線,其實主要原因是如果按照頂點的轉換方式,對於非均勻縮放(scalex, scaley,scalez不一致)時,會導致變換的法線歸一化后與面不垂直。如下圖所示,左邊是變化前的,而中間是沿x軸縮放了0.5倍的情況,顯然變 化后就不滿足法線的性質了,而最右邊的才是我們希望的結果。造成這一現象的主要原因是法線只能保證方向的一致性,而不能保證位置的一致性;頂點可以經過坐 標變換變換到正確的位置,但是法線是一個向量,我們不能直接使用頂點的變換矩陣進行變換。
我們可以推導一個法線的變換矩陣,就能夠保證轉化后的法線與面垂直,法線的變換矩陣為模型變換矩陣的逆轉置矩陣。具體推導過程可以參考這篇文章
在 把法線變換到了視空間后,就可以取出其中只與xy面有關的部分,視空間的z軸近似於深度,我們只需要法線在x,y軸的方向,再通過 TransformViewToProjection方法,將這個方向轉化到投影空間,最后用這個方向加上經過MVP變換的坐標,實現輕微外拓的效果。 (從網上和書上看到了不少在這一步計算的時候,又乘上了pos.z的操作,個人感覺沒有太大的用處,而且會導致描邊效果越遠,線條越粗的情況,離遠了就會 出現一團黑的問題,所以把這個去掉了)
 
上 面說過,一般情況下背面是在我們看到的后面的部分,但是理想很美好,現實很殘酷,具體情況千差萬別,比如我之前常用的一個模型,模型的袖子里面,其實用的 就是背面,如果想要渲染,就需要關閉背面剔除(Cull Off),這種情況下,使用Cull Front只渲染背面,就有可能和第二次正常渲染的時候的背面穿插,造成效果不對的情況,比如:

不過,解決問題的方法肯定要比問題多,我們可以用深度操作神器Offset指令,控制深度測試,比如我們可以讓渲染描邊的Pass深度遠離相機一點,這樣就不會與正常的Pass穿插了,修改一下描邊的Pass,其實只多了一句話Offset 1,1:
[csharp] view plain copy
  1. //描邊使用兩個Pass,第一個pass沿法線擠出一點,只輸出描邊的顏色  
  2.         Pass  
  3.         {  
  4.             //剔除正面,只渲染背面,對於大多數模型適用,不過如果需要背面的,就有問題了  
  5.             Cull Front  
  6.             //控制深度偏移,描邊pass遠離相機一些,防止與正常pass穿插  
  7.             Offset 1,1  
  8.             CGPROGRAM  
  9.             #include "UnityCG.cginc"  
  10.             fixed4 _OutlineCol;  
  11.             float _OutlineFactor;  
  12.               
  13.             struct v2f  
  14.             {  
  15.                 float4 pos : SV_POSITION;  
  16.             };  
  17.               
  18.             v2f vert(appdata_full v)  
  19.             {  
  20.                 v2f o;  
  21.                 //在vertex階段,每個頂點按照法線的方向偏移一部分,不過這種會造成近大遠小的透視問題  
  22.                 //v.vertex.xyz += v.normal * _OutlineFactor;  
  23.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  24.                 //將法線方向轉換到視空間  
  25.                 float3 vnormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);  
  26.                 //將視空間法線xy坐標轉化到投影空間  
  27.                 float2 offset = TransformViewToProjection(vnormal.xy);  
  28.                 //在最終投影階段輸出進行偏移操作  
  29.                 o.pos.xy += offset * _OutlineFactor;  
  30.                 return o;  
  31.             }  
  32.               
  33.             fixed4 frag(v2f i) : SV_Target  
  34.             {  
  35.                 //這個Pass直接輸出描邊顏色  
  36.                 return _OutlineCol;  
  37.             }  
  38.               
  39.             //使用vert函數和frag函數  
  40.             #pragma vertex vert  
  41.             #pragma fragment frag  
  42.             ENDCG  
  43.         }  
這樣,我們的描邊效果也可以支持不能背面剔除的模型了:
 

Offset指令

 
寫 到這里強行插一波基礎知識。上面的描邊效果,我們用了一個Offset指令,很好地解決了穿插的問題。其實Offset就是解決Stiching和Z- Fighting的最佳途徑之一。當然,也可以用模板測試,但是Offset操作更快一點。關於Stiching和Z-Fighting,引用一下這篇文章
 
在 OpenGL中,如果想繪制一個多邊形同時繪制其邊界,可是先使用多邊形模式GL_FILL繪制物體,然后使用多邊形模式GL_LINE和不同的顏色再次 繪制這個多邊形。但是由於直線和多邊形的光柵化方式不同,導致位於同一位置的多邊形和直線的深度值並不相同,進而導致直線有時在多邊形的里面,有時在多邊 形的外面,這種現象就是"Stiching"。而Z-fighting主要是指當兩個面共面時,二者的深度值一樣,深度緩沖就不能清楚的將它們兩者分離開 來,位於后面的圖元上的一些像素就會被渲染到前面的圖元上,最終導致圖象在幀與幀之間產生微弱的閃光。

比 如我們要繪制兩個面完全共面時,兩者深度值完全相同,那么我們在進行深度測試的時候,就不能分辨到底哪個在前,哪個在后了。類似我們上面的例子,當我們需 要渲染背面時,通過背面進行外拓的Pass渲染的結果就和正常的Pass有穿插了。那么,要解決這個問題,很明顯,我們就可以強行設置某個pass的深度 偏移,推測這個offset的偏移值是針對ZTest階段,在進行深度測試的時候,將當前pass的深度用offset進行調整再與深度緩沖區中的值進行 比對。附上一張官方文檔中關於Offset的部分:
Offset 指令有兩個參數,一個是Factor,主要影響我們繪制多邊形的深度斜率slope的最大值;另一個是Units,主要影響的是能產生在窗口坐標系的深度 值中可變分辨率差異的最小值r,這個r一般是OpenGL平台給定的常量。最終的Offset = slope * Factor + r * Units。Units我們一般在有使用Offset指令的地方給一個統一的值就可以了,主要起作用的就是Factor。Offset操作的層面是像素級 別的,多邊形光柵化之后對應的每個Fragment都有一個偏移值,我們調整Factor,其實相當於沿着當前多邊形的斜率深度前進或者后退了一段距離, 默認的深度方向是向Z正方向,如果我們給一個大於0的Factor,那么偏移值就會指向Z正方向,深度測試的時候相當於更遠了一點;而如果給了個小於0的 Factor,相當於原理Z正方向,深度測試時就更近了一點。
總結一句話就是:Offset大於0,Pass對應的模型離攝像機更遠;Offset小於0,Pass對應的模型離攝像機更近。
 
有一種描邊效果的實現,其實是利用Offset強行導致Z-Fighting達到描邊的目的,不過效果很差,這里簡單實驗了一版:
[csharp] view plain copy
  1. //描邊Shader  
  2. //by:puppet_master  
  3. //2017.1.10  
  4.   
  5. Shader "ApcShader/OutlineZOffset"  
  6. {  
  7.     //屬性  
  8.     Properties{  
  9.         _Diffuse("Diffuse", Color) = (1,1,1,1)  
  10.         _OutlineCol("OutlineCol", Color) = (1,0,0,1)  
  11.         _MainTex("Base 2D", 2D) = "white"{}  
  12.     }  
  13.   
  14.     //子着色器    
  15.     SubShader  
  16.     {  
  17.         //描邊使用兩個Pass,第一個Pass渲染背面,但是拉近一點  
  18.         Pass  
  19.         {  
  20.             //剔除正面,只渲染背面  
  21.             Cull Front  
  22.             //拉近一點,為了與后面的Pass重疊  
  23.             Offset -1,-1  
  24.               
  25.             CGPROGRAM  
  26.             #include "UnityCG.cginc"  
  27.             fixed4 _OutlineCol;  
  28.             float _OutlineFactor;  
  29.               
  30.             struct v2f  
  31.             {  
  32.                 float4 pos : SV_POSITION;  
  33.             };  
  34.               
  35.             v2f vert(appdata_full v)  
  36.             {  
  37.                 v2f o;  
  38.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  39.                 return o;  
  40.             }  
  41.               
  42.             fixed4 frag(v2f i) : SV_Target  
  43.             {  
  44.                 //這個Pass直接輸出描邊顏色  
  45.                 return _OutlineCol;  
  46.             }  
  47.               
  48.             //使用vert函數和frag函數  
  49.             #pragma vertex vert  
  50.             #pragma fragment frag  
  51.             ENDCG  
  52.         }  
  53.           
  54.         //正常着色的Pass,拉遠一點  
  55.         Pass  
  56.         {  
  57.             //拉遠一點,強行導致上一個Pass渲染的背面與此處發生Z-Fighting  
  58.             Offset 3,-1  
  59.             CGPROGRAM     
  60.               
  61.             //引入頭文件  
  62.             #include "Lighting.cginc"  
  63.             //定義Properties中的變量  
  64.             fixed4 _Diffuse;  
  65.             sampler2D _MainTex;  
  66.             //使用了TRANSFROM_TEX宏就需要定義XXX_ST  
  67.             float4 _MainTex_ST;  
  68.   
  69.             //定義結構體:vertex shader階段輸出的內容  
  70.             struct v2f  
  71.             {  
  72.                 float4 pos : SV_POSITION;  
  73.                 float3 worldNormal : TEXCOORD0;  
  74.                 float2 uv : TEXCOORD1;  
  75.             };  
  76.   
  77.             //定義頂點shader,參數直接使用appdata_base(包含position, noramal, texcoord)  
  78.             v2f vert(appdata_base v)  
  79.             {  
  80.                 v2f o;  
  81.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  82.                 //通過TRANSFORM_TEX宏轉化紋理坐標,主要處理了Offset和Tiling的改變,默認時等同於o.uv = v.texcoord.xy;  
  83.                 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);  
  84.                 o.worldNormal = mul(v.normal, (float3x3)_World2Object);  
  85.                 return o;  
  86.             }  
  87.   
  88.             //定義片元shader  
  89.             fixed4 frag(v2f i) : SV_Target  
  90.             {  
  91.                 //unity自身的diffuse也是帶了環境光,這里我們也增加一下環境光  
  92.                 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;  
  93.                 //歸一化法線,即使在vert歸一化也不行,從vert到frag階段有差值處理,傳入的法線方向並不是vertex shader直接傳出的  
  94.                 fixed3 worldNormal = normalize(i.worldNormal);  
  95.                 //把光照方向歸一化  
  96.                 fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);  
  97.                 //根據半蘭伯特模型計算像素的光照信息  
  98.                 fixed3 lambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;  
  99.                 //最終輸出顏色為lambert光強*材質diffuse顏色*光顏色  
  100.                 fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;  
  101.                 //進行紋理采樣  
  102.                 fixed4 color = tex2D(_MainTex, i.uv);  
  103.                 color.rgb = color.rgb* diffuse;  
  104.                 return fixed4(color);  
  105.             }  
  106.   
  107.             //使用vert函數和frag函數  
  108.             #pragma vertex vert  
  109.             #pragma fragment frag     
  110.   
  111.             ENDCG  
  112.         }  
  113.           
  114.           
  115.     }  
  116.     //前面的Shader失效的話,使用默認的Diffuse  
  117.     FallBack "Diffuse"  
  118. }  
效果如下:
效 果確實不怎么樣,圓球的描邊很明顯會看出Z-Fighting的痕跡,而人物的渲染,帽子直接就不對了。不過這種實現的描邊效果計算最為簡單,而且不存在 邊緣不連續時會出現描邊的斷裂的問題。這種方式,主要是通過把后面的描邊Pass向前提前,由於描邊Pass只渲染了背面,正常情況下是不可見的,而正常 的Pass又向后推了一點,導致重合的部分發生了Z-Fighting。
 
關於Offset指令,再附上一篇參考文章
 

關閉深度寫入的描邊效果實現

 
個 人不是很喜歡這種方式,關了深度寫入麻煩事太多。還是硬着頭皮練習一下吧。上面的描邊shader,如果注意觀察的話,其實並不僅僅是描物體的外輪廓邊, 在模型內部(模型面前,不是邊緣的部分)也被描上了邊,不過並不影響表現。而我們通過關閉深度寫入實現的描邊效果,則僅僅會描模型的外輪廓。代碼如下:
[csharp] view plain copy
  1. //描邊Shader  
  2. //by:puppet_master  
  3. //2017.1.9  
  4.   
  5. Shader "ApcShader/OutlineZWriteOff"  
  6. {  
  7.     //屬性  
  8.     Properties{  
  9.         _Diffuse("Diffuse", Color) = (1,1,1,1)  
  10.         _OutlineCol("OutlineCol", Color) = (1,0,0,1)  
  11.         _OutlineFactor("OutlineFactor", Range(0,1)) = 0.1  
  12.         _MainTex("Base 2D", 2D) = "white"{}  
  13.     }  
  14.   
  15.     //子着色器    
  16.     SubShader  
  17.     {  
  18.           
  19.         //描邊使用兩個Pass,第一個pass沿法線擠出一點,只輸出描邊的顏色  
  20.         Pass  
  21.         {  
  22.             //剔除正面,只渲染背面  
  23.             Cull Front  
  24.             //關閉深度寫入  
  25.             ZWrite Off  
  26.               
  27.             CGPROGRAM  
  28.             #include "UnityCG.cginc"  
  29.             fixed4 _OutlineCol;  
  30.             float _OutlineFactor;  
  31.               
  32.             struct v2f  
  33.             {  
  34.                 float4 pos : SV_POSITION;  
  35.             };  
  36.               
  37.             v2f vert(appdata_full v)  
  38.             {  
  39.                 v2f o;  
  40.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  41.                 //將法線方向轉換到視空間  
  42.                 float3 vnormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);  
  43.                 //將視空間法線xy坐標轉化到投影空間  
  44.                 float2 offset = TransformViewToProjection(vnormal.xy);  
  45.                 //在最終投影階段輸出進行偏移操作  
  46.                 o.pos.xy += offset * _OutlineFactor;  
  47.                 return o;  
  48.             }  
  49.               
  50.             fixed4 frag(v2f i) : SV_Target  
  51.             {  
  52.                 //這個Pass直接輸出描邊顏色  
  53.                 return _OutlineCol;  
  54.             }  
  55.               
  56.             //使用vert函數和frag函數  
  57.             #pragma vertex vert  
  58.             #pragma fragment frag  
  59.             ENDCG  
  60.         }  
  61.           
  62.         //正常着色的Pass  
  63.         Pass  
  64.         {  
  65.             CGPROGRAM     
  66.               
  67.             //引入頭文件  
  68.             #include "Lighting.cginc"  
  69.             //定義Properties中的變量  
  70.             fixed4 _Diffuse;  
  71.             sampler2D _MainTex;  
  72.             //使用了TRANSFROM_TEX宏就需要定義XXX_ST  
  73.             float4 _MainTex_ST;  
  74.   
  75.             //定義結構體:vertex shader階段輸出的內容  
  76.             struct v2f  
  77.             {  
  78.                 float4 pos : SV_POSITION;  
  79.                 float3 worldNormal : TEXCOORD0;  
  80.                 float2 uv : TEXCOORD1;  
  81.             };  
  82.   
  83.             //定義頂點shader,參數直接使用appdata_base(包含position, noramal, texcoord)  
  84.             v2f vert(appdata_base v)  
  85.             {  
  86.                 v2f o;  
  87.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  88.                 //通過TRANSFORM_TEX宏轉化紋理坐標,主要處理了Offset和Tiling的改變,默認時等同於o.uv = v.texcoord.xy;  
  89.                 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);  
  90.                 o.worldNormal = mul(v.normal, (float3x3)_World2Object);  
  91.                 return o;  
  92.             }  
  93.   
  94.             //定義片元shader  
  95.             fixed4 frag(v2f i) : SV_Target  
  96.             {  
  97.                 //unity自身的diffuse也是帶了環境光,這里我們也增加一下環境光  
  98.                 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;  
  99.                 //歸一化法線,即使在vert歸一化也不行,從vert到frag階段有差值處理,傳入的法線方向並不是vertex shader直接傳出的  
  100.                 fixed3 worldNormal = normalize(i.worldNormal);  
  101.                 //把光照方向歸一化  
  102.                 fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);  
  103.                 //根據半蘭伯特模型計算像素的光照信息  
  104.                 fixed3 lambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;  
  105.                 //最終輸出顏色為lambert光強*材質diffuse顏色*光顏色  
  106.                 fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;  
  107.                 //進行紋理采樣  
  108.                 fixed4 color = tex2D(_MainTex, i.uv);  
  109.                 color.rgb = color.rgb* diffuse;  
  110.                 return fixed4(color);  
  111.             }  
  112.   
  113.             //使用vert函數和frag函數  
  114.             #pragma vertex vert  
  115.             #pragma fragment frag     
  116.   
  117.             ENDCG  
  118.         }  
  119.     }  
  120.     //前面的Shader失效的話,使用默認的Diffuse  
  121.     FallBack "Diffuse"  
  122. }  
結果如下:
 
看着效果不錯,而且只有最外邊有黑色輪廓。然而事情沒有這么簡單....比如我們加一個天空盒,描邊效果就不見鳥!
萬 惡的ZWrite Off,一定要慎用啊!其實這個問題在上一篇文章中遇到過,簡單解釋一下,默認的渲染隊列是Geometry,而天空盒渲染在Geometry之后,描邊 部分沒有寫深度,那么當渲染天空盒的時候,深度小於無窮,深度測試通過,就會把描邊的部分覆蓋了。如下圖,在畫完模型本身時描邊還是可見的,再畫天空盒就 覆蓋了描邊。
通過上一篇文章我們可以知道,調整渲染隊列就可以解決這個問題。但是對於同一個渲染隊列,又會有別的問題,我們復制一個一樣的對象,有一部分重合,重合的部分描邊效果又不見鳥!!!
出現這個情況的原因也是沒寫深度造成描邊被覆蓋了:對於不透明類型的物體,unity的渲染順序是從前到后。前面的描邊渲染之后,渲染后面的模型,后面的模型在描邊部分深度測試仍然通過,就覆蓋了。
 
怎 么解決這個問題呢?首先我們需要找到一個靠后渲染的渲染隊列,保證我們的描邊效果不被其他geomerty類型的對象遮擋;而對於同一渲染隊列,我們也希 望最前面的物體描邊效果不被遮擋,也就是說渲染順序最好是從后向前。那么,答案已經有了,把渲染隊列改成Transparent,unity對於透明類型 的物體渲染順序是從后到前,這就符合我們的需求了。修改后的shader如下,只加了一句話,把隊列改成Transparent。
[csharp] view plain copy
  1. //描邊Shader  
  2. //by:puppet_master  
  3. //2017.1.9  
  4.   
  5. Shader "ApcShader/OutlineZWriteOff"  
  6. {  
  7.     //屬性  
  8.     Properties{  
  9.         _Diffuse("Diffuse", Color) = (1,1,1,1)  
  10.         _OutlineCol("OutlineCol", Color) = (1,0,0,1)  
  11.         _OutlineFactor("OutlineFactor", Range(0,1)) = 0.1  
  12.         _MainTex("Base 2D", 2D) = "white"{}  
  13.     }  
  14.   
  15.     //子着色器    
  16.     SubShader  
  17.     {  
  18.         //讓渲染隊列靠后,並且渲染順序為從后向前,保證描邊效果不被其他對象遮擋。  
  19.         Tags{"Queue" = "Transparent"}  
  20.         //描邊使用兩個Pass,第一個pass沿法線擠出一點,只輸出描邊的顏色  
  21.         Pass  
  22.         {  
  23.             //剔除正面,只渲染背面  
  24.             Cull Front  
  25.             //關閉深度寫入  
  26.             ZWrite Off  
  27.               
  28.             CGPROGRAM  
  29.             #include "UnityCG.cginc"  
  30.             fixed4 _OutlineCol;  
  31.             float _OutlineFactor;  
  32.               
  33.             struct v2f  
  34.             {  
  35.                 float4 pos : SV_POSITION;  
  36.             };  
  37.               
  38.             v2f vert(appdata_full v)  
  39.             {  
  40.                 v2f o;  
  41.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  42.                 //將法線方向轉換到視空間  
  43.                 float3 vnormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);  
  44.                 //將視空間法線xy坐標轉化到投影空間  
  45.                 float2 offset = TransformViewToProjection(vnormal.xy);  
  46.                 //在最終投影階段輸出進行偏移操作  
  47.                 o.pos.xy += offset * _OutlineFactor;  
  48.                 return o;  
  49.             }  
  50.               
  51.             fixed4 frag(v2f i) : SV_Target  
  52.             {  
  53.                 //這個Pass直接輸出描邊顏色  
  54.                 return _OutlineCol;  
  55.             }  
  56.               
  57.             //使用vert函數和frag函數  
  58.             #pragma vertex vert  
  59.             #pragma fragment frag  
  60.             ENDCG  
  61.         }  
  62.           
  63.         //正常着色的Pass  
  64.         Pass  
  65.         {  
  66.             CGPROGRAM     
  67.               
  68.             //引入頭文件  
  69.             #include "Lighting.cginc"  
  70.             //定義Properties中的變量  
  71.             fixed4 _Diffuse;  
  72.             sampler2D _MainTex;  
  73.             //使用了TRANSFROM_TEX宏就需要定義XXX_ST  
  74.             float4 _MainTex_ST;  
  75.   
  76.             //定義結構體:vertex shader階段輸出的內容  
  77.             struct v2f  
  78.             {  
  79.                 float4 pos : SV_POSITION;  
  80.                 float3 worldNormal : TEXCOORD0;  
  81.                 float2 uv : TEXCOORD1;  
  82.             };  
  83.   
  84.             //定義頂點shader,參數直接使用appdata_base(包含position, noramal, texcoord)  
  85.             v2f vert(appdata_base v)  
  86.             {  
  87.                 v2f o;  
  88.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  89.                 //通過TRANSFORM_TEX宏轉化紋理坐標,主要處理了Offset和Tiling的改變,默認時等同於o.uv = v.texcoord.xy;  
  90.                 o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);  
  91.                 o.worldNormal = mul(v.normal, (float3x3)_World2Object);  
  92.                 return o;  
  93.             }  
  94.   
  95.             //定義片元shader  
  96.             fixed4 frag(v2f i) : SV_Target  
  97.             {  
  98.                 //unity自身的diffuse也是帶了環境光,這里我們也增加一下環境光  
  99.                 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;  
  100.                 //歸一化法線,即使在vert歸一化也不行,從vert到frag階段有差值處理,傳入的法線方向並不是vertex shader直接傳出的  
  101.                 fixed3 worldNormal = normalize(i.worldNormal);  
  102.                 //把光照方向歸一化  
  103.                 fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);  
  104.                 //根據半蘭伯特模型計算像素的光照信息  
  105.                 fixed3 lambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;  
  106.                 //最終輸出顏色為lambert光強*材質diffuse顏色*光顏色  
  107.                 fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;  
  108.                 //進行紋理采樣  
  109.                 fixed4 color = tex2D(_MainTex, i.uv);  
  110.                 color.rgb = color.rgb* diffuse;  
  111.                 return fixed4(color);  
  112.             }  
  113.   
  114.             //使用vert函數和frag函數  
  115.             #pragma vertex vert  
  116.             #pragma fragment frag     
  117.   
  118.             ENDCG  
  119.         }  
  120.     }  
  121.     //前面的Shader失效的話,使用默認的Diffuse  
  122.     FallBack "Diffuse"  
  123. }  
這樣,我們的ZWrite Off版本的描邊效果也OK了。效果如下:
仔細觀察一下,雖然腿的部分描邊效果正常了,但是手的部分,由於穿插過於密集,還是有一些穿幫的地方。總之,沒事不要關閉深度寫入...
 

基於后處理的描邊效果

 
除了Cull Front+法線外拓+Offset實現的一版描邊效果還不錯,其他的描邊效果弊端都比較明顯,而法線外拓實現的描邊都存在一個問題,如果相鄰面的法線方向彼此分離,比如一個正方體,相鄰面的法線方向互相垂直,就會造成輪廓間斷,如下圖所示:

有一種解決方案,就是使用模型空間的頂點方向和法線方向插值得到的值進行外拓,並且需要判斷頂點的指向(可以參考《Shader Lab開發實戰詳解》)。不過個人感覺一般描邊效果用於的模型面數較高,法線方向過渡較為平緩,也就不會出現這種斷裂的情況。
 
要 放大招啦,當普通shader搞不定的時候,那就用后處理吧!用后處理進行描邊的原理,就是把物體的輪廓渲染到一張RenderTexture上,然后把 RT按照某種方式再貼回原始場景圖。那么,我們要怎么樣得到物體的輪廓呢?首先,我們可以渲染一個物體到RT上,可以通過RenderCommond進行 處理,不過RenderCommond是Unity5才提供的特性,加上這篇文章實在拖太久了,決定還是先用比較土的辦法實現吧。
 
用 一個額外的攝像機:通過增加一個和Main Camera一樣的攝像機,通過設置攝像機的LayerMask,將要渲染的對象設置到這個層中,然后將攝像機的Render Target設置為我們設定好的一張Render Texture上,就實現了渲染到RT上的部分,而這張RT由於我們需要在后處理的時候使用,所以我們在之前獲得這張RT,Unity為我們提供了一 個 OnPreRender函數,這個函數是在渲染之前的回調,我們就可以在這個地方完成RT的渲染。但是還有一個問題,就是我們默認的shader是模 型自身設置的shader,而不是純色的shader,我們要怎么臨時換成一個純色的shader呢?其實Unity也為我們准備好了一個函 數:Camera.RenderWithShader,可以讓攝像機的本次渲染采用我們設置的shader,這個函數接受兩個參數,第一個是需要用的 shader,第二個是一個字符串,還記得shader里面經常寫的RenderType嗎,其實主要就是為了RenderWithShader服務的, 如果我們沒給RenderType,那么攝像機需要渲染的所有物體都會被替換shader渲染,如果我們給了RenderType,Unity就會去對比 目前使用的shader中的RenderType,有的話才去渲染,不匹配的不會被替換shader渲染(關於RenderWithShader,可以參 考這篇文章)。到了這里,我們就能夠得到渲染到RT上的純色的渲染RT了,如下圖:

下 一步,為了讓輪廓出現,我們需要考慮的是怎么讓這個輪廓圖“胖一點”,回想一下之前的幾篇文章,通過模糊效果,就可以讓輪廓圖胖一些,所謂模糊,就是讓當 前像素的顏色值從當前像素以及像素周圍的幾個采樣點按照加權平均重新計算,很明顯,上面的這張圖進行計算時,人邊緣部分的顏色肯定會和周圍的黑色平均,導 致顏色溢出,進而達到發胖的效果。關於模糊,可以參考之前的兩篇文章:簡單均值模糊高斯模糊,這里就不多做解釋了,經過模糊后的結果如下:

然后呢,我們就可以讓兩者相減一下,讓胖的扣去瘦的部分,就留下了輪廓部分:

最后,再把這張RT和我們正常渲染的場景圖進行結合,就可以得到基於后處理的描邊效果了。最后的結合方式有很多種,最簡單的方式是直接疊加,附上后處理的C#及Shader代碼,為了更清晰,此處把每個步驟拆成單獨的Pass實現了。
C#腳本部分(PostEffect為后處理基類,見簡單屏幕較色):
[csharp] view plain copy
  1. /******************************************************************** 
  2.  FileName: OutlinePostEffect.cs 
  3.  Description: 后處理描邊效果 
  4.  Created: 2017/01/12 
  5.  history: 12:1:2017 0:42 by puppet_master 
  6. *********************************************************************/  
  7. using UnityEngine;  
  8. using System.Collections;  
  9.   
  10. public class OutlinePostEffect : PostEffectBase  
  11. {  
  12.   
  13.     private Camera mainCam = null;  
  14.     private Camera additionalCam = null;  
  15.     private RenderTexture renderTexture = null;  
  16.   
  17.     public Shader outlineShader = null;  
  18.     //采樣率  
  19.     public float samplerScale = 1;  
  20.     public int downSample = 1;  
  21.     public int iteration = 2;  
  22.   
  23.     void Awake()  
  24.     {  
  25.         //創建一個和當前相機一致的相機  
  26.         InitAdditionalCam();  
  27.   
  28.     }  
  29.   
  30.     private void InitAdditionalCam()  
  31.     {  
  32.         mainCam = GetComponent();  
  33.         if (mainCam == null)  
  34.             return;  
  35.   
  36.         Transform addCamTransform = transform.FindChild("additionalCam");  
  37.         if (addCamTransform != null)  
  38.             DestroyImmediate(addCamTransform.gameObject);  
  39.   
  40.         GameObject additionalCamObj = new GameObject("additionalCam");  
  41.         additionalCam = additionalCamObj.AddComponent();  
  42.   
  43.         SetAdditionalCam();  
  44.     }  
  45.   
  46.     private void SetAdditionalCam()  
  47.     {  
  48.         if (additionalCam)  
  49.         {  
  50.             additionalCam.transform.parent = mainCam.transform;  
  51.             additionalCam.transform.localPosition = Vector3.zero;  
  52.             additionalCam.transform.localRotation = Quaternion.identity;  
  53.             additionalCam.transform.localScale = Vector3.one;  
  54.             additionalCam.farClipPlane = mainCam.farClipPlane;  
  55.             additionalCam.nearClipPlane = mainCam.nearClipPlane;  
  56.             additionalCam.fieldOfView = mainCam.fieldOfView;  
  57.             additionalCam.backgroundColor = Color.clear;  
  58.             additionalCam.clearFlags = CameraClearFlags.Color;  
  59.             additionalCam.cullingMask = 1 << LayerMask.NameToLayer("Additional");  
  60.             additionalCam.depth = -999;   
  61.             if (renderTexture == null)  
  62.                 renderTexture = RenderTexture.GetTemporary(additionalCam.pixelWidth >> downSample, additionalCam.pixelHeight >> downSample, 0);  
  63.         }  
  64.     }  
  65.   
  66.     void OnEnable()  
  67.     {  
  68.         SetAdditionalCam();  
  69.         additionalCam.enabled = true;  
  70.     }  
  71.   
  72.     void OnDisable()  
  73.     {  
  74.         additionalCam.enabled = false;  
  75.     }  
  76.   
  77.     void OnDestroy()  
  78.     {  
  79.         if (renderTexture)  
  80.         {  
  81.             RenderTexture.ReleaseTemporary(renderTexture);  
  82.         }  
  83.         DestroyImmediate(additionalCam.gameObject);  
  84.     }  
  85.   
  86.     //unity提供的在渲染之前的接口,在這一步渲染描邊到RT  
  87.     void OnPreRender()  
  88.     {  
  89.         //使用OutlinePrepass進行渲染,得到RT  
  90.         if(additionalCam.enabled)  
  91.         {  
  92.             //渲染到RT上  
  93.             //首先檢查是否需要重設RT,比如屏幕分辨率變化了  
  94.             if (renderTexture != null && (renderTexture.width != Screen.width >> downSample || renderTexture.height != Screen.height >> downSample))  
  95.             {  
  96.                 RenderTexture.ReleaseTemporary(renderTexture);  
  97.                 renderTexture = RenderTexture.GetTemporary(Screen.width >> downSample, Screen.height >> downSample, 0);  
  98.             }  
  99.             additionalCam.targetTexture = renderTexture;  
  100.             additionalCam.RenderWithShader(outlineShader, "");  
  101.         }  
  102.     }  
  103.   
  104.     void OnRenderImage(RenderTexture source, RenderTexture destination)  
  105.     {  
  106.         if (_Material && renderTexture)  
  107.         {  
  108.             //renderTexture.width = 111;  
  109.             //對RT進行Blur處理  
  110.             RenderTexture temp1 = RenderTexture.GetTemporary(source.width >> downSample, source.height >> downSample, 0);  
  111.             RenderTexture temp2 = RenderTexture.GetTemporary(source.width >> downSample, source.height >> downSample, 0);  
  112.   
  113.             //高斯模糊,兩次模糊,橫向縱向,使用pass0進行高斯模糊  
  114.             _Material.SetVector("_offsets"new Vector4(0, samplerScale, 0, 0));  
  115.             Graphics.Blit(renderTexture, temp1, _Material, 0);  
  116.             _Material.SetVector("_offsets"new Vector4(samplerScale, 0, 0, 0));  
  117.             Graphics.Blit(temp1, temp2, _Material, 0);  
  118.   
  119.             //如果有疊加再進行迭代模糊處理  
  120.             for(int i = 0; i < iteration; i++)  
  121.             {  
  122.                 _Material.SetVector("_offsets"new Vector4(0, samplerScale, 0, 0));  
  123.                 Graphics.Blit(temp2, temp1, _Material, 0);  
  124.                 _Material.SetVector("_offsets"new Vector4(samplerScale, 0, 0, 0));  
  125.                 Graphics.Blit(temp1, temp2, _Material, 0);  
  126.             }  
  127.   
  128.             //用模糊圖和原始圖計算出輪廓圖  
  129.             _Material.SetTexture("_BlurTex", temp2);  
  130.             Graphics.Blit(renderTexture, temp1, _Material, 1);  
  131.   
  132.             //輪廓圖和場景圖疊加  
  133.             _Material.SetTexture("_BlurTex", temp1);  
  134.             Graphics.Blit(source, destination, _Material, 2);  
  135.   
  136.             RenderTexture.ReleaseTemporary(temp1);  
  137.             RenderTexture.ReleaseTemporary(temp2);  
  138.         }  
  139.         else  
  140.         {  
  141.             Graphics.Blit(source, destination);  
  142.         }  
  143.     }  
  144.   
  145.   
  146. }  
Prepass Shader(用於把模型渲染到RT的shader):
[csharp] view plain copy
  1. //描邊Shader(輸出純色)  
  2. //by:puppet_master  
  3. //2017.1.12  
  4.   
  5. Shader "ApcShader/OutlinePrePass"  
  6. {  
  7.     //子着色器    
  8.     SubShader  
  9.     {  
  10.         //描邊使用兩個Pass,第一個pass沿法線擠出一點,只輸出描邊的顏色  
  11.         Pass  
  12.         {     
  13.             CGPROGRAM  
  14.             #include "UnityCG.cginc"  
  15.             fixed4 _OutlineCol;  
  16.               
  17.             struct v2f  
  18.             {  
  19.                 float4 pos : SV_POSITION;  
  20.             };  
  21.               
  22.             v2f vert(appdata_full v)  
  23.             {  
  24.                 v2f o;  
  25.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  26.                 return o;  
  27.             }  
  28.               
  29.             fixed4 frag(v2f i) : SV_Target  
  30.             {  
  31.                 //這個Pass直接輸出描邊顏色  
  32.                 return fixed4(1,0,0,1);  
  33.             }  
  34.               
  35.             //使用vert函數和frag函數  
  36.             #pragma vertex vert  
  37.             #pragma fragment frag  
  38.             ENDCG  
  39.         }  
  40.     }  
  41. }  
后處理shader(三個Pass,模糊處理,摳出輪廓,最終混合):
[csharp] view plain copy
  1. //后處理描邊Shader  
  2. //by:puppet_master  
  3. //2017.1.12  
  4.   
  5. Shader "Custom/OutLinePostEffect" {  
  6.   
  7.     Properties{  
  8.         _MainTex("Base (RGB)", 2D) = "white" {}  
  9.         _BlurTex("Blur", 2D) = "white"{}  
  10.     }  
  11.   
  12.     CGINCLUDE  
  13.     #include "UnityCG.cginc"  
  14.       
  15.     //用於剔除中心留下輪廓  
  16.     struct v2f_cull  
  17.     {  
  18.         float4 pos : SV_POSITION;  
  19.         float2 uv : TEXCOORD0;  
  20.     };  
  21.   
  22.     //用於模糊  
  23.     struct v2f_blur  
  24.     {  
  25.         float4 pos : SV_POSITION;  
  26.         float2 uv  : TEXCOORD0;  
  27.         float4 uv01 : TEXCOORD1;  
  28.         float4 uv23 : TEXCOORD2;  
  29.         float4 uv45 : TEXCOORD3;  
  30.     };  
  31.   
  32.     //用於最后疊加  
  33.     struct v2f_add  
  34.     {  
  35.         float4 pos : SV_POSITION;  
  36.         float2 uv  : TEXCOORD0;  
  37.         float2 uv1 : TEXCOORD1;  
  38.     };  
  39.   
  40.     sampler2D _MainTex;  
  41.     float4 _MainTex_TexelSize;  
  42.     sampler2D _BlurTex;  
  43.     float4 _BlurTex_TexelSize;  
  44.     float4 _offsets;  
  45.   
  46.     //Blur圖和原圖進行相減獲得輪廓  
  47.     v2f_cull vert_cull(appdata_img v)  
  48.     {  
  49.         v2f_cull o;  
  50.         o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  51.         o.uv = v.texcoord.xy;  
  52.         //dx中紋理從左上角為初始坐標,需要反向  
  53. #if UNITY_UV_STARTS_AT_TOP  
  54.         if (_MainTex_TexelSize.y < 0)  
  55.             o.uv.y = 1 - o.uv.y;  
  56. #endif    
  57.         return o;  
  58.     }  
  59.   
  60.     fixed4 frag_cull(v2f_cull i) : SV_Target  
  61.     {  
  62.         fixed4 colorMain = tex2D(_MainTex, i.uv);  
  63.         fixed4 colorBlur = tex2D(_BlurTex, i.uv);  
  64.         //最后的顏色是_BlurTex - _MainTex,周圍0-0=0,黑色;邊框部分為描邊顏色-0=描邊顏色;中間部分為描邊顏色-描邊顏色=0。最終輸出只有邊框  
  65.         //return fixed4((colorBlur - colorMain).rgb, 1);  
  66.         return colorBlur - colorMain;  
  67.     }  
  68.   
  69.     //高斯模糊 vert shader(之前的文章有詳細注釋,此處也可以用BoxBlur,更省一點)  
  70.     v2f_blur vert_blur(appdata_img v)  
  71.     {  
  72.         v2f_blur o;  
  73.         _offsets *= _MainTex_TexelSize.xyxy;  
  74.         o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  75.         o.uv = v.texcoord.xy;  
  76.   
  77.         o.uv01 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1);  
  78.         o.uv23 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 2.0;  
  79.         o.uv45 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 3.0;  
  80.   
  81.         return o;  
  82.     }  
  83.   
  84.     //高斯模糊 pixel shader  
  85.     fixed4 frag_blur(v2f_blur i) : SV_Target  
  86.     {  
  87.         fixed4 color = fixed4(0,0,0,0);  
  88.         color += 0.40 * tex2D(_MainTex, i.uv);  
  89.         color += 0.15 * tex2D(_MainTex, i.uv01.xy);  
  90.         color += 0.15 * tex2D(_MainTex, i.uv01.zw);  
  91.         color += 0.10 * tex2D(_MainTex, i.uv23.xy);  
  92.         color += 0.10 * tex2D(_MainTex, i.uv23.zw);  
  93.         color += 0.05 * tex2D(_MainTex, i.uv45.xy);  
  94.         color += 0.05 * tex2D(_MainTex, i.uv45.zw);  
  95.         return color;  
  96.     }  
  97.   
  98.     //最終疊加 vertex shader  
  99.     v2f_add vert_add(appdata_img v)  
  100.     {  
  101.         v2f_add o;  
  102.         //mvp矩陣變換  
  103.         o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  104.         //uv坐標傳遞  
  105.         o.uv.xy = v.texcoord.xy;  
  106.         o.uv1.xy = o.uv.xy;  
  107. #if UNITY_UV_STARTS_AT_TOP  
  108.         if (_MainTex_TexelSize.y < 0)  
  109.             o.uv.y = 1 - o.uv.y;  
  110. #endif    
  111.         return o;  
  112.     }  
  113.   
  114.     fixed4 frag_add(v2f_add i) : SV_Target  
  115.     {  
  116.         //取原始場景圖片進行采樣  
  117.         fixed4 ori = tex2D(_MainTex, i.uv1);  
  118.         //取得到的輪廓圖片進行采樣  
  119.         fixed4 blur = tex2D(_BlurTex, i.uv);  
  120.         //輸出:直接疊加  
  121.         fixed4 final = ori + blur;  
  122.         return final;  
  123.     }  
  124.   
  125.         ENDCG  
  126.   
  127.     SubShader  
  128.     {  
  129.         //pass 0: 高斯模糊  
  130.         Pass  
  131.         {  
  132.             ZTest Off  
  133.             Cull Off  
  134.             ZWrite Off  
  135.             Fog{ Mode Off }  
  136.   
  137.             CGPROGRAM  
  138.             #pragma vertex vert_blur  
  139.             #pragma fragment frag_blur  
  140.             ENDCG  
  141.         }  
  142.           
  143.         //pass 1: 剔除中心部分   
  144.         Pass  
  145.         {  
  146.             ZTest Off  
  147.             Cull Off  
  148.             ZWrite Off  
  149.             Fog{ Mode Off }  
  150.   
  151.             CGPROGRAM  
  152.             #pragma vertex vert_cull  
  153.             #pragma fragment frag_cull  
  154.             ENDCG  
  155.         }  
  156.   
  157.   
  158.         //pass 2: 最終疊加  
  159.         Pass  
  160.         {  
  161.   
  162.             ZTest Off  
  163.             Cull Off  
  164.             ZWrite Off  
  165.             Fog{ Mode Off }  
  166.   
  167.             CGPROGRAM  
  168.             #pragma vertex vert_add  
  169.             #pragma fragment frag_add  
  170.             ENDCG  
  171.         }  
  172.   
  173.     }  
  174. }  
描邊結果(把要描邊的對象放到Additional層中):

換個顏色,加大一下模糊程度:

這 種類型的shader其實跟最上面的劍靈中的描邊效果很像,尤其是第一張圖,描邊並不是一個硬邊,而是一個柔和的,漸變的邊緣效果,在最靠近模型的部分顏 色最強,越向外,描邊效果逐漸減弱。個人最喜歡這個描邊效果,不過這個后處理是真的費啊,強烈不推薦移動上使用,一般帶模糊的效果,都要慎用,超級費(然 而本人超級喜歡的效果基本都是需要模糊來實現的,比如景深,Bloom,毛玻璃等等,效果永遠是跟性能成反比的)。這個后處理還有一個問題,就是不能遮 擋,因為渲染到RT之后,再通過模糊減去原圖,只會留下整體的邊界,而不會把中間重疊的部分留下。暫時沒想到什么好辦法,如果哪位熱心人有好點子,還望不 吝賜教。
 
下 面再調整一下這個shader,首先,我們把這個描邊效果換成一個硬邊,跟我們最早通過增加個外拓Pass達到一樣的效果;然后就是讓我們輸出的顏色是我 們自己想要的顏色,因為上面的實現實際上是一種疊加,並不是我們原始的寫在Prepass那個shader里面輸出的顏色,而且那個是寫死在shader 里的,不能調整。我們希望給一個可調整的參數;最后,由於上面shader中最后的兩個Pass其實是可以合並成一個Pass來實現的,通過增加一個貼圖 槽,這樣就可以省下一次全屏Pass。
C#部分:
[csharp] view plain copy
  1. /******************************************************************** 
  2.  FileName: OutlinePostEffect.cs 
  3.  Description: 后處理描邊效果 
  4.  Created: 2017/01/12 
  5.  history: 12:1:2017 0:42 by puppet_master 
  6. *********************************************************************/  
  7. using UnityEngine;  
  8. using System.Collections;  
  9.   
  10. public class OutlinePostEffectX : PostEffectBase  
  11. {  
  12.   
  13.     private Camera mainCam = null;  
  14.     private Camera additionalCam = null;  
  15.     private RenderTexture renderTexture = null;  
  16.   
  17.     public Shader outlineShader = null;  
  18.     //采樣率  
  19.     public float samplerScale = 0.01f;  
  20.     public int downSample = 0;  
  21.     public int iteration = 0;  
  22.     public Color outlineColor = Color.green;  
  23.   
  24.     void Awake()  
  25.     {  
  26.         //創建一個和當前相機一致的相機  
  27.         InitAdditionalCam();  
  28.   
  29.     }  
  30.   
  31.     private void InitAdditionalCam()  
  32.     {  
  33.         mainCam = GetComponent();  
  34.         if (mainCam == null)  
  35.             return;  
  36.   
  37.         Transform addCamTransform = transform.FindChild("additionalCam");  
  38.         if (addCamTransform != null)  
  39.             DestroyImmediate(addCamTransform.gameObject);  
  40.   
  41.         GameObject additionalCamObj = new GameObject("additionalCam");  
  42.         additionalCam = additionalCamObj.AddComponent();  
  43.   
  44.         SetAdditionalCam();  
  45.     }  
  46.   
  47.     private void SetAdditionalCam()  
  48.     {  
  49.         if (additionalCam)  
  50.         {  
  51.             additionalCam.transform.parent = mainCam.transform;  
  52.             additionalCam.transform.localPosition = Vector3.zero;  
  53.             additionalCam.transform.localRotation = Quaternion.identity;  
  54.             additionalCam.transform.localScale = Vector3.one;  
  55.             additionalCam.farClipPlane = mainCam.farClipPlane;  
  56.             additionalCam.nearClipPlane = mainCam.nearClipPlane;  
  57.             additionalCam.fieldOfView = mainCam.fieldOfView;  
  58.             additionalCam.backgroundColor = Color.clear;  
  59.             additionalCam.clearFlags = CameraClearFlags.Color;  
  60.             additionalCam.cullingMask = 1 << LayerMask.NameToLayer("Additional");  
  61.             additionalCam.depth = -999;  
  62.             if (renderTexture == null)  
  63.                 renderTexture = RenderTexture.GetTemporary(additionalCam.pixelWidth >> downSample, additionalCam.pixelHeight >> downSample, 0);  
  64.         }  
  65.     }  
  66.   
  67.     void OnEnable()  
  68.     {  
  69.         SetAdditionalCam();  
  70.         additionalCam.enabled = true;  
  71.     }  
  72.   
  73.     void OnDisable()  
  74.     {  
  75.         additionalCam.enabled = false;  
  76.     }  
  77.   
  78.     void OnDestroy()  
  79.     {  
  80.         if (renderTexture)  
  81.         {  
  82.             RenderTexture.ReleaseTemporary(renderTexture);  
  83.         }  
  84.         DestroyImmediate(additionalCam.gameObject);  
  85.     }  
  86.   
  87.     //unity提供的在渲染之前的接口,在這一步渲染描邊到RT  
  88.     void OnPreRender()  
  89.     {  
  90.         //使用OutlinePrepass進行渲染,得到RT  
  91.         if (additionalCam.enabled)  
  92.         {  
  93.             //渲染到RT上  
  94.             //首先檢查是否需要重設RT,比如屏幕分辨率變化了  
  95.             if (renderTexture != null && (renderTexture.width != Screen.width >> downSample || renderTexture.height != Screen.height >> downSample))  
  96.             {  
  97.                 RenderTexture.ReleaseTemporary(renderTexture);  
  98.                 renderTexture = RenderTexture.GetTemporary(Screen.width >> downSample, Screen.height >> downSample, 0);  
  99.             }  
  100.             additionalCam.targetTexture = renderTexture;  
  101.             additionalCam.RenderWithShader(outlineShader, "");  
  102.         }  
  103.     }  
  104.   
  105.     void OnRenderImage(RenderTexture source, RenderTexture destination)  
  106.     {  
  107.         if (_Material && renderTexture)  
  108.         {  
  109.             //renderTexture.width = 111;  
  110.             //對RT進行Blur處理  
  111.             RenderTexture temp1 = RenderTexture.GetTemporary(source.width >> downSample, source.height >> downSample, 0);  
  112.             RenderTexture temp2 = RenderTexture.GetTemporary(source.width >> downSample, source.height >> downSample, 0);  
  113.   
  114.             //高斯模糊,兩次模糊,橫向縱向,使用pass0進行高斯模糊  
  115.             _Material.SetVector("_offsets"new Vector4(0, samplerScale, 0, 0));  
  116.             Graphics.Blit(renderTexture, temp1, _Material, 0);  
  117.             _Material.SetVector("_offsets"new Vector4(samplerScale, 0, 0, 0));  
  118.             Graphics.Blit(temp1, temp2, _Material, 0);  
  119.   
  120.             //如果有疊加再進行迭代模糊處理  
  121.             for (int i = 0; i < iteration; i++)  
  122.             {  
  123.                 _Material.SetVector("_offsets"new Vector4(0, samplerScale, 0, 0));  
  124.                 Graphics.Blit(temp2, temp1, _Material, 0);  
  125.                 _Material.SetVector("_offsets"new Vector4(samplerScale, 0, 0, 0));  
  126.                 Graphics.Blit(temp1, temp2, _Material, 0);  
  127.             }  
  128.   
  129.             //用模糊圖和原始圖計算出輪廓圖,並和場景圖疊加,節省一個Pass  
  130.             _Material.SetTexture("_OriTex", renderTexture);  
  131.             _Material.SetTexture("_BlurTex", temp2);  
  132.             _Material.SetColor("_OutlineColor", outlineColor);  
  133.             Graphics.Blit(source, destination, _Material, 1);  
  134.   
  135.             RenderTexture.ReleaseTemporary(temp1);  
  136.             RenderTexture.ReleaseTemporary(temp2);  
  137.         }  
  138.         else  
  139.         {  
  140.             Graphics.Blit(source, destination);  
  141.         }  
  142.     }  
  143. }  
描邊Shader部分:
[csharp] view plain copy
  1. //后處理描邊Shader  
  2. //by:puppet_master  
  3. //2017.1.12  
  4.   
  5. Shader "Custom/OutLinePostEffectX" {  
  6.   
  7.     Properties{  
  8.         _MainTex("Base (RGB)", 2D) = "white" {}  
  9.         _BlurTex("Blur", 2D) = "white"{}  
  10.         _OriTex("Ori", 2D) = "white"{}  
  11.     }  
  12.     CGINCLUDE  
  13.     #include "UnityCG.cginc"  
  14.       
  15.   
  16.     //用於模糊  
  17.     struct v2f_blur  
  18.     {  
  19.         float4 pos : SV_POSITION;  
  20.         float2 uv  : TEXCOORD0;  
  21.         float4 uv01 : TEXCOORD1;  
  22.         float4 uv23 : TEXCOORD2;  
  23.         float4 uv45 : TEXCOORD3;  
  24.     };  
  25.   
  26.     //用於最后疊加  
  27.     struct v2f_add  
  28.     {  
  29.         float4 pos : SV_POSITION;  
  30.         float2 uv  : TEXCOORD0;  
  31.         float2 uv1 : TEXCOORD1;  
  32.         float2 uv2 : TEXCOORD2;  
  33.     };  
  34.   
  35.     sampler2D _MainTex;  
  36.     float4 _MainTex_TexelSize;  
  37.     sampler2D _BlurTex;  
  38.     float4 _BlurTex_TexelSize;  
  39.     sampler2D _OriTex;  
  40.     float4 _OriTex_TexelSize;  
  41.     float4 _offsets;  
  42.     fixed4 _OutlineColor;  
  43.   
  44.     //高斯模糊 vert shader(之前的文章有詳細注釋,此處也可以用BoxBlur,更省一點)  
  45.     v2f_blur vert_blur(appdata_img v)  
  46.     {  
  47.         v2f_blur o;  
  48.         _offsets *= _MainTex_TexelSize.xyxy;  
  49.         o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  50.         o.uv = v.texcoord.xy;  
  51.   
  52.         o.uv01 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1);  
  53.         o.uv23 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 2.0;  
  54.         o.uv45 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 3.0;  
  55.   
  56.         return o;  
  57.     }  
  58.   
  59.     //高斯模糊 pixel shader  
  60.     fixed4 frag_blur(v2f_blur i) : SV_Target  
  61.     {  
  62.         fixed4 color = fixed4(0,0,0,0);  
  63.         color += 0.40 * tex2D(_MainTex, i.uv);  
  64.         color += 0.15 * tex2D(_MainTex, i.uv01.xy);  
  65.         color += 0.15 * tex2D(_MainTex, i.uv01.zw);  
  66.         color += 0.10 * tex2D(_MainTex, i.uv23.xy);  
  67.         color += 0.10 * tex2D(_MainTex, i.uv23.zw);  
  68.         color += 0.05 * tex2D(_MainTex, i.uv45.xy);  
  69.         color += 0.05 * tex2D(_MainTex, i.uv45.zw);  
  70.         return color;  
  71.     }  
  72.   
  73.     //最終疊加 vertex shader  
  74.     v2f_add vert_add(appdata_img v)  
  75.     {  
  76.         v2f_add o;  
  77.         //mvp矩陣變換  
  78.         o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  79.         //uv坐標傳遞  
  80.         o.uv.xy = v.texcoord.xy;  
  81.         o.uv1.xy = o.uv.xy;  
  82.         o.uv2.xy = o.uv.xy;  
  83. #if UNITY_UV_STARTS_AT_TOP  
  84.         //if  (_OriTex_TexelSize.y < 0)  
  85.             o.uv.y = 1 - o.uv.y;  
  86.             o.uv2.y = 1 - o.uv2.y;  
  87. #endif    
  88.         return o;  
  89.     }  
  90.   
  91.     fixed4 frag_add(v2f_add i) : SV_Target  
  92.     {  
  93.         //取原始場景紋理進行采樣  
  94.         fixed4 scene = tex2D(_MainTex, i.uv1);  
  95.         //return scene;  
  96.         //對blur后的紋理進行采樣  
  97.         fixed4 blur = tex2D(_BlurTex, i.uv);  
  98.         //對blur之前的rt進行采樣  
  99.         fixed4 ori = tex2D(_OriTex, i.uv);  
  100.         //輪廓是_BlurTex - _OriTex,周圍0-0=0,黑色;邊框部分為描邊顏色-0=描邊顏色;中間部分為描邊顏色-描邊顏色=0。最終輸出只有邊框  
  101.         fixed4 outline = blur - ori;  
  102.           
  103.         //輸出:blur部分為0的地方返回原始圖像,否則為0,然后疊加描邊  
  104.         fixed4 final = scene * (1 - all(outline.rgb)) + _OutlineColor * any(outline.rgb);//0.01,1,1  
  105.         return final;  
  106.     }  
  107.   
  108.         ENDCG  
  109.   
  110.     SubShader  
  111.     {  
  112.         //pass 0: 高斯模糊  
  113.         Pass  
  114.         {  
  115.             ZTest Off  
  116.             Cull Off  
  117.             ZWrite Off  
  118.             Fog{ Mode Off }  
  119.   
  120.             CGPROGRAM  
  121.             #pragma vertex vert_blur  
  122.             #pragma fragment frag_blur  
  123.             ENDCG  
  124.         }  
  125.   
  126.         //pass 1: 剔除中心部分以及最后和場景圖疊加  
  127.         Pass  
  128.         {  
  129.             ZTest Off  
  130.             Cull Off  
  131.             ZWrite Off  
  132.             Fog{ Mode Off }  
  133.   
  134.             CGPROGRAM  
  135.             #pragma vertex vert_add  
  136.             #pragma fragment frag_add  
  137.             ENDCG  
  138.         }  
  139.   
  140.     }  
  141. }  
結果如下:

總結

 
本 篇文章主要研究了一下描邊效果的幾種類型(邊緣發光型,硬描邊,柔和邊緣的描邊)以及實現方式(邊緣光,深度偏移,法線外拓,后處理):幾種描邊效果各有 各的優點和缺點,最省的是邊緣光效果,深度偏移+法線外拓的方式基本可以滿足真正的描邊需求,而后處理的效果比較好,但是如果能只增加一個pass就能得 到的效果,就沒有必要用后處理了,尤其是移動平台上。最后推薦一個后處理的插件:Highting System,里面有各種類型的描邊效果,不過這個插件也是通過后處理來實現的(使用了RenderCommand+后處理),也是比較費。插件中模糊的 描邊效果:

硬邊的描邊效果:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM