描邊效果是游戲里面非常常用的一種效果,一般是為了凸顯游戲中的某個對象,會給對象增加一個描邊效果。本篇文章和大家介紹下利用Shader實現描邊效果,一起來看看吧。
最近又跑回去玩了玩《劍靈》,雖然出了三年了,感覺在現在的網游里面畫面仍然算很好的了,劍靈里面走近或者選中NPC的一瞬間,NPC就會出現描邊效果,不過這個描邊效果是漸變的,會很快減弱最后消失(抓了好久才抓住一張圖....)
還有就是最常見的LOL中的塔,我們把鼠標移動到塔上,就會有很明顯的描邊效果:
簡單描邊效果的原理
描 邊效果有幾種實現方式。其實邊緣光效果與描邊效果有些類似,適當調整邊緣光效果,其實也可以達到凸顯要表達的對象的意思。邊緣光的實現最為簡單,只是在計 算的時候增加了一次計算法線方向與視線方向的夾角計算,用1減去結果作為系數乘以一個邊緣光顏色就達到了邊緣光的效果,是性能最好的一種方法,關於邊緣光 效果,可以參考一下之前的一篇文章:邊緣光效果。邊緣光的效果如下圖所示:
原始模型渲染:
使用了邊緣光的效果:
邊 緣光效果雖然簡單,但是有很大的局限性,邊緣光效果只是在當前模型本身的光照計算時調整了邊緣位置的顏色值,並沒有達到真正的“描邊”(當然,有時候我們 就是想要這種邊緣光的效果),而我們希望的描邊效果,一般都是在正常模型的渲染狀態下,在模型外面擴展出一個描邊的效果。既然要讓模型的形狀有所改變(向 外拓一點),那么肯定就和vertex shader有關系了。而我們的描邊效果,肯定就是要讓模型更“胖”一點,能夠把我們原來的大小包裹住;微觀一點來看,一個面,如果我們讓它向外拓展,而 我們指的外,也就是這個面的法線所指向的方向,那么就讓這個面朝着法線的方向平移一點;再微觀一點來看,對於頂點來說,也就是我們的vertex shader真正要寫的內容了,我們正常計算頂點的時候,傳入的vertex會經過MVP變換,最終傳遞給fragment shader,那么我們就可以在這一步讓頂點沿着法線的方向稍微平移一些。我們在描邊后,描邊這一次渲染的邊緣其實是沒有辦法和我們正常的模型進行區分 的,為了解決這個問題,就需要用兩個Pass來渲染,第一個Pass渲染描邊的效果,進行外拓,而第二個Pass進行原本效果的渲染,這樣,后面顯示的就 是稍微“胖”一點的模型,然后正常的模型貼在上面,把中間的部分擋住,邊緣擋不住就露出了描邊的部分了。
開啟深度寫入,剔除正面的描邊效果
知 道了原理,我們來考慮一下外拓的實現,我們可以在vertex階段獲得頂點的坐標,並且有法線的坐標,最直接的方式就是直接用頂點坐標+法線方向*描邊粗 細參數,然后用這個偏移的坐標值再進行MVP變換;但是這樣做有一個弊端,其實就是我們透視的近大遠小的問題,模型上離相機近的地方描邊效果較粗,而遠的 地方描邊效果較細。一種解決的方案是先進行MPV變換,變換完之后再去按照法線方向調整外拓。代碼如下:
-
- Shader "ApcShader/Outline"
- {
-
- Properties{
- _Diffuse("Diffuse", Color) = (1,1,1,1)
- _OutlineCol("OutlineCol", Color) = (1,0,0,1)
- _OutlineFactor("OutlineFactor", Range(0,1)) = 0.1
- _MainTex("Base 2D", 2D) = "white"{}
- }
-
-
- SubShader
- {
-
-
- Pass
- {
-
- Cull Front
-
- CGPROGRAM
- #include "UnityCG.cginc"
- fixed4 _OutlineCol;
- float _OutlineFactor;
-
- struct v2f
- {
- float4 pos : SV_POSITION;
- };
-
- v2f vert(appdata_full v)
- {
- v2f o;
-
-
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
-
- float3 vnormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
-
- float2 offset = TransformViewToProjection(vnormal.xy);
-
- o.pos.xy += offset * _OutlineFactor;
- return o;
- }
-
- fixed4 frag(v2f i) : SV_Target
- {
-
- return _OutlineCol;
- }
-
-
- #pragma vertex vert
- #pragma fragment frag
- ENDCG
- }
-
-
- Pass
- {
- CGPROGRAM
-
-
- #include "Lighting.cginc"
-
- fixed4 _Diffuse;
- sampler2D _MainTex;
-
- float4 _MainTex_ST;
-
-
- struct v2f
- {
- float4 pos : SV_POSITION;
- float3 worldNormal : TEXCOORD0;
- float2 uv : TEXCOORD1;
- };
-
-
- v2f vert(appdata_base v)
- {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
-
- o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
- o.worldNormal = mul(v.normal, (float3x3)_World2Object);
- return o;
- }
-
-
- fixed4 frag(v2f i) : SV_Target
- {
-
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;
-
- fixed3 worldNormal = normalize(i.worldNormal);
-
- fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
-
- fixed3 lambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
-
- fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;
-
- fixed4 color = tex2D(_MainTex, i.uv);
- color.rgb = color.rgb* diffuse;
- return fixed4(color);
- }
-
-
- #pragma vertex vert
- #pragma fragment frag
-
- ENDCG
- }
- }
-
- FallBack "Diffuse"
- }
開啟了描邊效果:
原始模型渲染采用了半蘭伯特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:
- Pass
- {
-
- Cull Front
-
- Offset 1,1
- CGPROGRAM
- #include "UnityCG.cginc"
- fixed4 _OutlineCol;
- float _OutlineFactor;
-
- struct v2f
- {
- float4 pos : SV_POSITION;
- };
-
- v2f vert(appdata_full v)
- {
- v2f o;
-
-
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
-
- float3 vnormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
-
- float2 offset = TransformViewToProjection(vnormal.xy);
-
- o.pos.xy += offset * _OutlineFactor;
- return o;
- }
-
- fixed4 frag(v2f i) : SV_Target
- {
-
- return _OutlineCol;
- }
-
-
- #pragma vertex vert
- #pragma fragment frag
- ENDCG
- }
這樣,我們的描邊效果也可以支持不能背面剔除的模型了:
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達到描邊的目的,不過效果很差,這里簡單實驗了一版:
-
- Shader "ApcShader/OutlineZOffset"
- {
-
- Properties{
- _Diffuse("Diffuse", Color) = (1,1,1,1)
- _OutlineCol("OutlineCol", Color) = (1,0,0,1)
- _MainTex("Base 2D", 2D) = "white"{}
- }
-
-
- SubShader
- {
-
- Pass
- {
-
- Cull Front
-
- Offset -1,-1
-
- CGPROGRAM
- #include "UnityCG.cginc"
- fixed4 _OutlineCol;
- float _OutlineFactor;
-
- struct v2f
- {
- float4 pos : SV_POSITION;
- };
-
- v2f vert(appdata_full v)
- {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- return o;
- }
-
- fixed4 frag(v2f i) : SV_Target
- {
-
- return _OutlineCol;
- }
-
-
- #pragma vertex vert
- #pragma fragment frag
- ENDCG
- }
-
-
- Pass
- {
-
- Offset 3,-1
- CGPROGRAM
-
-
- #include "Lighting.cginc"
-
- fixed4 _Diffuse;
- sampler2D _MainTex;
-
- float4 _MainTex_ST;
-
-
- struct v2f
- {
- float4 pos : SV_POSITION;
- float3 worldNormal : TEXCOORD0;
- float2 uv : TEXCOORD1;
- };
-
-
- v2f vert(appdata_base v)
- {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
-
- o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
- o.worldNormal = mul(v.normal, (float3x3)_World2Object);
- return o;
- }
-
-
- fixed4 frag(v2f i) : SV_Target
- {
-
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;
-
- fixed3 worldNormal = normalize(i.worldNormal);
-
- fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
-
- fixed3 lambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
-
- fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;
-
- fixed4 color = tex2D(_MainTex, i.uv);
- color.rgb = color.rgb* diffuse;
- return fixed4(color);
- }
-
-
- #pragma vertex vert
- #pragma fragment frag
-
- ENDCG
- }
-
-
- }
-
- FallBack "Diffuse"
- }
效果如下:
效 果確實不怎么樣,圓球的描邊很明顯會看出Z-Fighting的痕跡,而人物的渲染,帽子直接就不對了。不過這種實現的描邊效果計算最為簡單,而且不存在 邊緣不連續時會出現描邊的斷裂的問題。這種方式,主要是通過把后面的描邊Pass向前提前,由於描邊Pass只渲染了背面,正常情況下是不可見的,而正常 的Pass又向后推了一點,導致重合的部分發生了Z-Fighting。
關閉深度寫入的描邊效果實現
個 人不是很喜歡這種方式,關了深度寫入麻煩事太多。還是硬着頭皮練習一下吧。上面的描邊shader,如果注意觀察的話,其實並不僅僅是描物體的外輪廓邊, 在模型內部(模型面前,不是邊緣的部分)也被描上了邊,不過並不影響表現。而我們通過關閉深度寫入實現的描邊效果,則僅僅會描模型的外輪廓。代碼如下:
-
- Shader "ApcShader/OutlineZWriteOff"
- {
-
- Properties{
- _Diffuse("Diffuse", Color) = (1,1,1,1)
- _OutlineCol("OutlineCol", Color) = (1,0,0,1)
- _OutlineFactor("OutlineFactor", Range(0,1)) = 0.1
- _MainTex("Base 2D", 2D) = "white"{}
- }
-
-
- SubShader
- {
-
-
- Pass
- {
-
- Cull Front
-
- ZWrite Off
-
- CGPROGRAM
- #include "UnityCG.cginc"
- fixed4 _OutlineCol;
- float _OutlineFactor;
-
- struct v2f
- {
- float4 pos : SV_POSITION;
- };
-
- v2f vert(appdata_full v)
- {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
-
- float3 vnormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
-
- float2 offset = TransformViewToProjection(vnormal.xy);
-
- o.pos.xy += offset * _OutlineFactor;
- return o;
- }
-
- fixed4 frag(v2f i) : SV_Target
- {
-
- return _OutlineCol;
- }
-
-
- #pragma vertex vert
- #pragma fragment frag
- ENDCG
- }
-
-
- Pass
- {
- CGPROGRAM
-
-
- #include "Lighting.cginc"
-
- fixed4 _Diffuse;
- sampler2D _MainTex;
-
- float4 _MainTex_ST;
-
-
- struct v2f
- {
- float4 pos : SV_POSITION;
- float3 worldNormal : TEXCOORD0;
- float2 uv : TEXCOORD1;
- };
-
-
- v2f vert(appdata_base v)
- {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
-
- o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
- o.worldNormal = mul(v.normal, (float3x3)_World2Object);
- return o;
- }
-
-
- fixed4 frag(v2f i) : SV_Target
- {
-
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;
-
- fixed3 worldNormal = normalize(i.worldNormal);
-
- fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
-
- fixed3 lambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
-
- fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;
-
- fixed4 color = tex2D(_MainTex, i.uv);
- color.rgb = color.rgb* diffuse;
- return fixed4(color);
- }
-
-
- #pragma vertex vert
- #pragma fragment frag
-
- ENDCG
- }
- }
-
- FallBack "Diffuse"
- }
結果如下:
看着效果不錯,而且只有最外邊有黑色輪廓。然而事情沒有這么簡單....比如我們加一個天空盒,描邊效果就不見鳥!
萬 惡的ZWrite Off,一定要慎用啊!其實這個問題在上一篇文章中遇到過,簡單解釋一下,默認的渲染隊列是Geometry,而天空盒渲染在Geometry之后,描邊 部分沒有寫深度,那么當渲染天空盒的時候,深度小於無窮,深度測試通過,就會把描邊的部分覆蓋了。如下圖,在畫完模型本身時描邊還是可見的,再畫天空盒就 覆蓋了描邊。
通過上一篇文章我們可以知道,調整渲染隊列就可以解決這個問題。但是對於同一個渲染隊列,又會有別的問題,我們復制一個一樣的對象,有一部分重合,重合的部分描邊效果又不見鳥!!!
出現這個情況的原因也是沒寫深度造成描邊被覆蓋了:對於不透明類型的物體,unity的渲染順序是從前到后。前面的描邊渲染之后,渲染后面的模型,后面的模型在描邊部分深度測試仍然通過,就覆蓋了。
怎 么解決這個問題呢?首先我們需要找到一個靠后渲染的渲染隊列,保證我們的描邊效果不被其他geomerty類型的對象遮擋;而對於同一渲染隊列,我們也希 望最前面的物體描邊效果不被遮擋,也就是說渲染順序最好是從后向前。那么,答案已經有了,把渲染隊列改成Transparent,unity對於透明類型 的物體渲染順序是從后到前,這就符合我們的需求了。修改后的shader如下,只加了一句話,把隊列改成Transparent。
-
- Shader "ApcShader/OutlineZWriteOff"
- {
-
- Properties{
- _Diffuse("Diffuse", Color) = (1,1,1,1)
- _OutlineCol("OutlineCol", Color) = (1,0,0,1)
- _OutlineFactor("OutlineFactor", Range(0,1)) = 0.1
- _MainTex("Base 2D", 2D) = "white"{}
- }
-
-
- SubShader
- {
-
- Tags{"Queue" = "Transparent"}
-
- Pass
- {
-
- Cull Front
-
- ZWrite Off
-
- CGPROGRAM
- #include "UnityCG.cginc"
- fixed4 _OutlineCol;
- float _OutlineFactor;
-
- struct v2f
- {
- float4 pos : SV_POSITION;
- };
-
- v2f vert(appdata_full v)
- {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
-
- float3 vnormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
-
- float2 offset = TransformViewToProjection(vnormal.xy);
-
- o.pos.xy += offset * _OutlineFactor;
- return o;
- }
-
- fixed4 frag(v2f i) : SV_Target
- {
-
- return _OutlineCol;
- }
-
-
- #pragma vertex vert
- #pragma fragment frag
- ENDCG
- }
-
-
- Pass
- {
- CGPROGRAM
-
-
- #include "Lighting.cginc"
-
- fixed4 _Diffuse;
- sampler2D _MainTex;
-
- float4 _MainTex_ST;
-
-
- struct v2f
- {
- float4 pos : SV_POSITION;
- float3 worldNormal : TEXCOORD0;
- float2 uv : TEXCOORD1;
- };
-
-
- v2f vert(appdata_base v)
- {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
-
- o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
- o.worldNormal = mul(v.normal, (float3x3)_World2Object);
- return o;
- }
-
-
- fixed4 frag(v2f i) : SV_Target
- {
-
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;
-
- fixed3 worldNormal = normalize(i.worldNormal);
-
- fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
-
- fixed3 lambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
-
- fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;
-
- fixed4 color = tex2D(_MainTex, i.uv);
- color.rgb = color.rgb* diffuse;
- return fixed4(color);
- }
-
-
- #pragma vertex vert
- #pragma fragment frag
-
- ENDCG
- }
- }
-
- FallBack "Diffuse"
- }
這樣,我們的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為后處理基類,見簡單屏幕較色):
- using UnityEngine;
- using System.Collections;
-
- public class OutlinePostEffect : PostEffectBase
- {
-
- private Camera mainCam = null;
- private Camera additionalCam = null;
- private RenderTexture renderTexture = null;
-
- public Shader outlineShader = null;
-
- public float samplerScale = 1;
- public int downSample = 1;
- public int iteration = 2;
-
- void Awake()
- {
-
- InitAdditionalCam();
-
- }
-
- private void InitAdditionalCam()
- {
- mainCam = GetComponent();
- if (mainCam == null)
- return;
-
- Transform addCamTransform = transform.FindChild("additionalCam");
- if (addCamTransform != null)
- DestroyImmediate(addCamTransform.gameObject);
-
- GameObject additionalCamObj = new GameObject("additionalCam");
- additionalCam = additionalCamObj.AddComponent();
-
- SetAdditionalCam();
- }
-
- private void SetAdditionalCam()
- {
- if (additionalCam)
- {
- additionalCam.transform.parent = mainCam.transform;
- additionalCam.transform.localPosition = Vector3.zero;
- additionalCam.transform.localRotation = Quaternion.identity;
- additionalCam.transform.localScale = Vector3.one;
- additionalCam.farClipPlane = mainCam.farClipPlane;
- additionalCam.nearClipPlane = mainCam.nearClipPlane;
- additionalCam.fieldOfView = mainCam.fieldOfView;
- additionalCam.backgroundColor = Color.clear;
- additionalCam.clearFlags = CameraClearFlags.Color;
- additionalCam.cullingMask = 1 << LayerMask.NameToLayer("Additional");
- additionalCam.depth = -999;
- if (renderTexture == null)
- renderTexture = RenderTexture.GetTemporary(additionalCam.pixelWidth >> downSample, additionalCam.pixelHeight >> downSample, 0);
- }
- }
-
- void OnEnable()
- {
- SetAdditionalCam();
- additionalCam.enabled = true;
- }
-
- void OnDisable()
- {
- additionalCam.enabled = false;
- }
-
- void OnDestroy()
- {
- if (renderTexture)
- {
- RenderTexture.ReleaseTemporary(renderTexture);
- }
- DestroyImmediate(additionalCam.gameObject);
- }
-
-
- void OnPreRender()
- {
-
- if(additionalCam.enabled)
- {
-
-
- if (renderTexture != null && (renderTexture.width != Screen.width >> downSample || renderTexture.height != Screen.height >> downSample))
- {
- RenderTexture.ReleaseTemporary(renderTexture);
- renderTexture = RenderTexture.GetTemporary(Screen.width >> downSample, Screen.height >> downSample, 0);
- }
- additionalCam.targetTexture = renderTexture;
- additionalCam.RenderWithShader(outlineShader, "");
- }
- }
-
- void OnRenderImage(RenderTexture source, RenderTexture destination)
- {
- if (_Material && renderTexture)
- {
-
-
- RenderTexture temp1 = RenderTexture.GetTemporary(source.width >> downSample, source.height >> downSample, 0);
- RenderTexture temp2 = RenderTexture.GetTemporary(source.width >> downSample, source.height >> downSample, 0);
-
-
- _Material.SetVector("_offsets", new Vector4(0, samplerScale, 0, 0));
- Graphics.Blit(renderTexture, temp1, _Material, 0);
- _Material.SetVector("_offsets", new Vector4(samplerScale, 0, 0, 0));
- Graphics.Blit(temp1, temp2, _Material, 0);
-
-
- for(int i = 0; i < iteration; i++)
- {
- _Material.SetVector("_offsets", new Vector4(0, samplerScale, 0, 0));
- Graphics.Blit(temp2, temp1, _Material, 0);
- _Material.SetVector("_offsets", new Vector4(samplerScale, 0, 0, 0));
- Graphics.Blit(temp1, temp2, _Material, 0);
- }
-
-
- _Material.SetTexture("_BlurTex", temp2);
- Graphics.Blit(renderTexture, temp1, _Material, 1);
-
-
- _Material.SetTexture("_BlurTex", temp1);
- Graphics.Blit(source, destination, _Material, 2);
-
- RenderTexture.ReleaseTemporary(temp1);
- RenderTexture.ReleaseTemporary(temp2);
- }
- else
- {
- Graphics.Blit(source, destination);
- }
- }
-
-
- }
Prepass Shader(用於把模型渲染到RT的shader):
-
- Shader "ApcShader/OutlinePrePass"
- {
-
- SubShader
- {
-
- Pass
- {
- CGPROGRAM
- #include "UnityCG.cginc"
- fixed4 _OutlineCol;
-
- struct v2f
- {
- float4 pos : SV_POSITION;
- };
-
- v2f vert(appdata_full v)
- {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- return o;
- }
-
- fixed4 frag(v2f i) : SV_Target
- {
-
- return fixed4(1,0,0,1);
- }
-
-
- #pragma vertex vert
- #pragma fragment frag
- ENDCG
- }
- }
- }
后處理shader(三個Pass,模糊處理,摳出輪廓,最終混合):
-
- Shader "Custom/OutLinePostEffect" {
-
- Properties{
- _MainTex("Base (RGB)", 2D) = "white" {}
- _BlurTex("Blur", 2D) = "white"{}
- }
-
- CGINCLUDE
- #include "UnityCG.cginc"
-
-
- struct v2f_cull
- {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- };
-
-
- struct v2f_blur
- {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- float4 uv01 : TEXCOORD1;
- float4 uv23 : TEXCOORD2;
- float4 uv45 : TEXCOORD3;
- };
-
-
- struct v2f_add
- {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- float2 uv1 : TEXCOORD1;
- };
-
- sampler2D _MainTex;
- float4 _MainTex_TexelSize;
- sampler2D _BlurTex;
- float4 _BlurTex_TexelSize;
- float4 _offsets;
-
-
- v2f_cull vert_cull(appdata_img v)
- {
- v2f_cull o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- o.uv = v.texcoord.xy;
-
- #if UNITY_UV_STARTS_AT_TOP
- if (_MainTex_TexelSize.y < 0)
- o.uv.y = 1 - o.uv.y;
- #endif
- return o;
- }
-
- fixed4 frag_cull(v2f_cull i) : SV_Target
- {
- fixed4 colorMain = tex2D(_MainTex, i.uv);
- fixed4 colorBlur = tex2D(_BlurTex, i.uv);
-
-
- return colorBlur - colorMain;
- }
-
-
- v2f_blur vert_blur(appdata_img v)
- {
- v2f_blur o;
- _offsets *= _MainTex_TexelSize.xyxy;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- o.uv = v.texcoord.xy;
-
- o.uv01 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1);
- o.uv23 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 2.0;
- o.uv45 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 3.0;
-
- return o;
- }
-
-
- fixed4 frag_blur(v2f_blur i) : SV_Target
- {
- fixed4 color = fixed4(0,0,0,0);
- color += 0.40 * tex2D(_MainTex, i.uv);
- color += 0.15 * tex2D(_MainTex, i.uv01.xy);
- color += 0.15 * tex2D(_MainTex, i.uv01.zw);
- color += 0.10 * tex2D(_MainTex, i.uv23.xy);
- color += 0.10 * tex2D(_MainTex, i.uv23.zw);
- color += 0.05 * tex2D(_MainTex, i.uv45.xy);
- color += 0.05 * tex2D(_MainTex, i.uv45.zw);
- return color;
- }
-
-
- v2f_add vert_add(appdata_img v)
- {
- v2f_add o;
-
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
-
- o.uv.xy = v.texcoord.xy;
- o.uv1.xy = o.uv.xy;
- #if UNITY_UV_STARTS_AT_TOP
- if (_MainTex_TexelSize.y < 0)
- o.uv.y = 1 - o.uv.y;
- #endif
- return o;
- }
-
- fixed4 frag_add(v2f_add i) : SV_Target
- {
-
- fixed4 ori = tex2D(_MainTex, i.uv1);
-
- fixed4 blur = tex2D(_BlurTex, i.uv);
-
- fixed4 final = ori + blur;
- return final;
- }
-
- ENDCG
-
- SubShader
- {
-
- Pass
- {
- ZTest Off
- Cull Off
- ZWrite Off
- Fog{ Mode Off }
-
- CGPROGRAM
- #pragma vertex vert_blur
- #pragma fragment frag_blur
- ENDCG
- }
-
-
- Pass
- {
- ZTest Off
- Cull Off
- ZWrite Off
- Fog{ Mode Off }
-
- CGPROGRAM
- #pragma vertex vert_cull
- #pragma fragment frag_cull
- ENDCG
- }
-
-
-
- Pass
- {
-
- ZTest Off
- Cull Off
- ZWrite Off
- Fog{ Mode Off }
-
- CGPROGRAM
- #pragma vertex vert_add
- #pragma fragment frag_add
- ENDCG
- }
-
- }
- }
描邊結果(把要描邊的對象放到Additional層中):

換個顏色,加大一下模糊程度:
這 種類型的shader其實跟最上面的劍靈中的描邊效果很像,尤其是第一張圖,描邊並不是一個硬邊,而是一個柔和的,漸變的邊緣效果,在最靠近模型的部分顏 色最強,越向外,描邊效果逐漸減弱。個人最喜歡這個描邊效果,不過這個后處理是真的費啊,強烈不推薦移動上使用,一般帶模糊的效果,都要慎用,超級費(然 而本人超級喜歡的效果基本都是需要模糊來實現的,比如景深,Bloom,毛玻璃等等,效果永遠是跟性能成反比的)。這個后處理還有一個問題,就是不能遮 擋,因為渲染到RT之后,再通過模糊減去原圖,只會留下整體的邊界,而不會把中間重疊的部分留下。暫時沒想到什么好辦法,如果哪位熱心人有好點子,還望不 吝賜教。
下 面再調整一下這個shader,首先,我們把這個描邊效果換成一個硬邊,跟我們最早通過增加個外拓Pass達到一樣的效果;然后就是讓我們輸出的顏色是我 們自己想要的顏色,因為上面的實現實際上是一種疊加,並不是我們原始的寫在Prepass那個shader里面輸出的顏色,而且那個是寫死在shader 里的,不能調整。我們希望給一個可調整的參數;最后,由於上面shader中最后的兩個Pass其實是可以合並成一個Pass來實現的,通過增加一個貼圖 槽,這樣就可以省下一次全屏Pass。
C#部分:
- using UnityEngine;
- using System.Collections;
-
- public class OutlinePostEffectX : PostEffectBase
- {
-
- private Camera mainCam = null;
- private Camera additionalCam = null;
- private RenderTexture renderTexture = null;
-
- public Shader outlineShader = null;
-
- public float samplerScale = 0.01f;
- public int downSample = 0;
- public int iteration = 0;
- public Color outlineColor = Color.green;
-
- void Awake()
- {
-
- InitAdditionalCam();
-
- }
-
- private void InitAdditionalCam()
- {
- mainCam = GetComponent();
- if (mainCam == null)
- return;
-
- Transform addCamTransform = transform.FindChild("additionalCam");
- if (addCamTransform != null)
- DestroyImmediate(addCamTransform.gameObject);
-
- GameObject additionalCamObj = new GameObject("additionalCam");
- additionalCam = additionalCamObj.AddComponent();
-
- SetAdditionalCam();
- }
-
- private void SetAdditionalCam()
- {
- if (additionalCam)
- {
- additionalCam.transform.parent = mainCam.transform;
- additionalCam.transform.localPosition = Vector3.zero;
- additionalCam.transform.localRotation = Quaternion.identity;
- additionalCam.transform.localScale = Vector3.one;
- additionalCam.farClipPlane = mainCam.farClipPlane;
- additionalCam.nearClipPlane = mainCam.nearClipPlane;
- additionalCam.fieldOfView = mainCam.fieldOfView;
- additionalCam.backgroundColor = Color.clear;
- additionalCam.clearFlags = CameraClearFlags.Color;
- additionalCam.cullingMask = 1 << LayerMask.NameToLayer("Additional");
- additionalCam.depth = -999;
- if (renderTexture == null)
- renderTexture = RenderTexture.GetTemporary(additionalCam.pixelWidth >> downSample, additionalCam.pixelHeight >> downSample, 0);
- }
- }
-
- void OnEnable()
- {
- SetAdditionalCam();
- additionalCam.enabled = true;
- }
-
- void OnDisable()
- {
- additionalCam.enabled = false;
- }
-
- void OnDestroy()
- {
- if (renderTexture)
- {
- RenderTexture.ReleaseTemporary(renderTexture);
- }
- DestroyImmediate(additionalCam.gameObject);
- }
-
-
- void OnPreRender()
- {
-
- if (additionalCam.enabled)
- {
-
-
- if (renderTexture != null && (renderTexture.width != Screen.width >> downSample || renderTexture.height != Screen.height >> downSample))
- {
- RenderTexture.ReleaseTemporary(renderTexture);
- renderTexture = RenderTexture.GetTemporary(Screen.width >> downSample, Screen.height >> downSample, 0);
- }
- additionalCam.targetTexture = renderTexture;
- additionalCam.RenderWithShader(outlineShader, "");
- }
- }
-
- void OnRenderImage(RenderTexture source, RenderTexture destination)
- {
- if (_Material && renderTexture)
- {
-
-
- RenderTexture temp1 = RenderTexture.GetTemporary(source.width >> downSample, source.height >> downSample, 0);
- RenderTexture temp2 = RenderTexture.GetTemporary(source.width >> downSample, source.height >> downSample, 0);
-
-
- _Material.SetVector("_offsets", new Vector4(0, samplerScale, 0, 0));
- Graphics.Blit(renderTexture, temp1, _Material, 0);
- _Material.SetVector("_offsets", new Vector4(samplerScale, 0, 0, 0));
- Graphics.Blit(temp1, temp2, _Material, 0);
-
-
- for (int i = 0; i < iteration; i++)
- {
- _Material.SetVector("_offsets", new Vector4(0, samplerScale, 0, 0));
- Graphics.Blit(temp2, temp1, _Material, 0);
- _Material.SetVector("_offsets", new Vector4(samplerScale, 0, 0, 0));
- Graphics.Blit(temp1, temp2, _Material, 0);
- }
-
-
- _Material.SetTexture("_OriTex", renderTexture);
- _Material.SetTexture("_BlurTex", temp2);
- _Material.SetColor("_OutlineColor", outlineColor);
- Graphics.Blit(source, destination, _Material, 1);
-
- RenderTexture.ReleaseTemporary(temp1);
- RenderTexture.ReleaseTemporary(temp2);
- }
- else
- {
- Graphics.Blit(source, destination);
- }
- }
- }
描邊Shader部分:
-
- Shader "Custom/OutLinePostEffectX" {
-
- Properties{
- _MainTex("Base (RGB)", 2D) = "white" {}
- _BlurTex("Blur", 2D) = "white"{}
- _OriTex("Ori", 2D) = "white"{}
- }
- CGINCLUDE
- #include "UnityCG.cginc"
-
-
-
- struct v2f_blur
- {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- float4 uv01 : TEXCOORD1;
- float4 uv23 : TEXCOORD2;
- float4 uv45 : TEXCOORD3;
- };
-
-
- struct v2f_add
- {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- float2 uv1 : TEXCOORD1;
- float2 uv2 : TEXCOORD2;
- };
-
- sampler2D _MainTex;
- float4 _MainTex_TexelSize;
- sampler2D _BlurTex;
- float4 _BlurTex_TexelSize;
- sampler2D _OriTex;
- float4 _OriTex_TexelSize;
- float4 _offsets;
- fixed4 _OutlineColor;
-
-
- v2f_blur vert_blur(appdata_img v)
- {
- v2f_blur o;
- _offsets *= _MainTex_TexelSize.xyxy;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- o.uv = v.texcoord.xy;
-
- o.uv01 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1);
- o.uv23 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 2.0;
- o.uv45 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 3.0;
-
- return o;
- }
-
-
- fixed4 frag_blur(v2f_blur i) : SV_Target
- {
- fixed4 color = fixed4(0,0,0,0);
- color += 0.40 * tex2D(_MainTex, i.uv);
- color += 0.15 * tex2D(_MainTex, i.uv01.xy);
- color += 0.15 * tex2D(_MainTex, i.uv01.zw);
- color += 0.10 * tex2D(_MainTex, i.uv23.xy);
- color += 0.10 * tex2D(_MainTex, i.uv23.zw);
- color += 0.05 * tex2D(_MainTex, i.uv45.xy);
- color += 0.05 * tex2D(_MainTex, i.uv45.zw);
- return color;
- }
-
-
- v2f_add vert_add(appdata_img v)
- {
- v2f_add o;
-
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
-
- o.uv.xy = v.texcoord.xy;
- o.uv1.xy = o.uv.xy;
- o.uv2.xy = o.uv.xy;
- #if UNITY_UV_STARTS_AT_TOP
-
- o.uv.y = 1 - o.uv.y;
- o.uv2.y = 1 - o.uv2.y;
- #endif
- return o;
- }
-
- fixed4 frag_add(v2f_add i) : SV_Target
- {
-
- fixed4 scene = tex2D(_MainTex, i.uv1);
-
-
- fixed4 blur = tex2D(_BlurTex, i.uv);
-
- fixed4 ori = tex2D(_OriTex, i.uv);
-
- fixed4 outline = blur - ori;
-
-
- fixed4 final = scene * (1 - all(outline.rgb)) + _OutlineColor * any(outline.rgb);
- return final;
- }
-
- ENDCG
-
- SubShader
- {
-
- Pass
- {
- ZTest Off
- Cull Off
- ZWrite Off
- Fog{ Mode Off }
-
- CGPROGRAM
- #pragma vertex vert_blur
- #pragma fragment frag_blur
- ENDCG
- }
-
-
- Pass
- {
- ZTest Off
- Cull Off
- ZWrite Off
- Fog{ Mode Off }
-
- CGPROGRAM
- #pragma vertex vert_add
- #pragma fragment frag_add
- ENDCG
- }
-
- }
- }
結果如下:
總結
本 篇文章主要研究了一下描邊效果的幾種類型(邊緣發光型,硬描邊,柔和邊緣的描邊)以及實現方式(邊緣光,深度偏移,法線外拓,后處理):幾種描邊效果各有 各的優點和缺點,最省的是邊緣光效果,深度偏移+法線外拓的方式基本可以滿足真正的描邊需求,而后處理的效果比較好,但是如果能只增加一個pass就能得 到的效果,就沒有必要用后處理了,尤其是移動平台上。最后推薦一個后處理的插件:Highting System,里面有各種類型的描邊效果,不過這個插件也是通過后處理來實現的(使用了RenderCommand+后處理),也是比較費。插件中模糊的 描邊效果:
硬邊的描邊效果: