之前在網上看到物體遮擋描邊的功能,自己也拿來實現了一番。算作第一篇博客的開篇。
先貼出幾張效果圖,也是個人思路和方案的改進路線吧。
//////////////////////////////////////////////////////////////////方案實現////////////////////////////////////////////////////////////////////////////////////////
看到描邊的功能,最先想到的就是用stencil的方法實現。功能最主要的部分就是如何判斷邊界像素,之后在FragmentShader中將該像素顏色設置成需要描邊的顏色。
方案一:
一個簡單的VF Shader:

1 Shader "Unlit/Shape" 2 { 3 Properties 4 { 5 _MainTex ("Texture", 2D) = "white" {} 6 _ShapeLineWidth("ShapeWidth",float) = 0.1 7 _ShapeColor("ShapeColor",COLOR) = (1,1,1,1) 8 } 9 SubShader 10 { 11 Tags { "Queue"="Geometry" } 12 LOD 100 13 //output origin color 14 Pass 15 { 16 17 CGPROGRAM 18 #pragma vertex vert 19 #pragma fragment frag 20 21 22 #include "UnityCG.cginc" 23 24 struct appdata 25 { 26 float4 vertex : POSITION; 27 float2 uv : TEXCOORD0; 28 }; 29 30 struct v2f 31 { 32 float2 uv : TEXCOORD0; 33 float4 vertex : SV_POSITION; 34 }; 35 36 sampler2D _MainTex; 37 float4 _MainTex_ST; 38 39 v2f vert (appdata v) 40 { 41 v2f o; 42 o.vertex = UnityObjectToClipPos(v.vertex); 43 o.uv = TRANSFORM_TEX(v.uv, _MainTex); 44 return o; 45 } 46 47 fixed4 frag (v2f i) : SV_Target 48 { 49 fixed4 col = tex2D(_MainTex, i.uv); 50 return col; 51 } 52 ENDCG 53 } 54 55 //output stencil to define occlued area 56 Pass 57 { 58 ColorMask 0 59 ZTest Off 60 Stencil 61 { 62 Ref 1 63 Comp Always 64 Pass Replace 65 } 66 CGPROGRAM 67 #pragma vertex vert 68 #pragma fragment frag 69 70 71 #include "UnityCG.cginc" 72 73 struct appdata 74 { 75 float4 vertex : POSITION; 76 }; 77 78 struct v2f 79 { 80 float4 vertex : SV_POSITION; 81 }; 82 83 84 v2f vert (appdata v) 85 { 86 v2f o; 87 o.vertex = UnityObjectToClipPos(v.vertex); 88 return o; 89 } 90 91 fixed4 frag (v2f i) : SV_Target 92 { 93 return fixed4(1,1,1,1); 94 } 95 ENDCG 96 } 97 98 //output outlinecolor 99 Pass 100 { 101 Stencil 102 { 103 Ref 0 104 Comp Equal 105 Pass Keep 106 } 107 ZWrite Off 108 ZTest Off 109 CGPROGRAM 110 #pragma vertex vert 111 #pragma fragment frag 112 113 114 #include "UnityCG.cginc" 115 116 struct v2f 117 { 118 float2 uv : TEXCOORD0; 119 float4 vertex : SV_POSITION; 120 }; 121 122 123 float _ShapeLineWidth; 124 fixed4 _ShapeColor; 125 126 v2f vert (appdata_base v) 127 { 128 v2f o; 129 v.vertex.xyz += v.normal * _ShapeLineWidth; 130 o.vertex = UnityObjectToClipPos(v.vertex); 131 return o; 132 } 133 134 [earlyDepthStencil] 135 fixed4 frag (v2f i) : SV_Target 136 { 137 fixed4 col = _ShapeColor; 138 return col; 139 } 140 ENDCG 141 } 142 } 143 }
主要功能在第三個pass中:將所有像素沿法線方向延伸(相當於將物體略微放大一些),再通過第二個pass寫入的stencil剔除中間區域,剩下就是邊緣的像素了。雖然成功實現了描邊的功能,但是未遮擋部位也被描邊了。
方案二:
利用后期渲染的技術,將所有描邊的物體輪廓先輸出到一張圖上,在最后與原圖疊加在一起。這種方案的優點更加靈活了。在Unity中,我們可以定義不同的相機來渲染出各種想要的圖像包含着豐富的信息。例如本例中在后期渲染中先后得到shadowmap,顏色緩存等圖像信息。
由於該方案比較復雜,先貼出思路:
1.獲得場景除去要描邊物體的depthmap
2.通過比較depthmap判定被遮擋區域,並將該區域放大
3.將放大區域剔除遮擋區域就是描邊的像素區域了,將該像素區域疊加到原圖像中。
工程代碼如下:

1 using UnityEngine; 2 using System.Collections; 3 4 public class ShapeOutline : MonoBehaviour { 5 6 public Camera objectCamera = null; 7 public Color outlineColor = Color.green; 8 Camera mainCamera; 9 RenderTexture depthTexture; 10 RenderTexture occlusionTexture; 11 RenderTexture strechTexture; 12 13 // Use this for initialization 14 void Start() 15 { 16 mainCamera = Camera.main; 17 mainCamera.depthTextureMode = DepthTextureMode.Depth; 18 objectCamera.depthTextureMode = DepthTextureMode.None; 19 objectCamera.cullingMask = 1 << LayerMask.NameToLayer("Outline"); 20 objectCamera.fieldOfView = mainCamera.fieldOfView; 21 objectCamera.clearFlags = CameraClearFlags.Color; 22 objectCamera.projectionMatrix = mainCamera.projectionMatrix; 23 objectCamera.nearClipPlane = mainCamera.nearClipPlane; 24 objectCamera.farClipPlane = mainCamera.farClipPlane; 25 objectCamera.aspect = mainCamera.aspect; 26 objectCamera.orthographic = false; 27 objectCamera.enabled = false; 28 } 29 30 void OnRenderImage(RenderTexture srcTex, RenderTexture dstTex) 31 { 32 depthTexture = RenderTexture.GetTemporary(Screen.width, Screen.height, 24, RenderTextureFormat.Depth); 33 occlusionTexture = RenderTexture.GetTemporary(Screen.width, Screen.height, 0); 34 strechTexture = RenderTexture.GetTemporary(Screen.width, Screen.height, 0); 35 36 objectCamera.targetTexture = depthTexture; 37 objectCamera.RenderWithShader(Shader.Find("ShapeOutline/Depth"), string.Empty); 38 39 Material mat = new Material(Shader.Find("ShapeOutline/Occlusion")); 40 mat.SetColor("_OutlineColor", outlineColor); 41 Graphics.Blit(depthTexture, occlusionTexture, mat); 42 43 mat = new Material(Shader.Find("ShapeOutline/StrechOcclusion")); 44 mat.SetColor("_OutlineColor", outlineColor); 45 Graphics.Blit(occlusionTexture, strechTexture, mat); 46 47 mat = new Material(Shader.Find("ShapeOutline/Mix")); 48 mat.SetColor("_OutlineColor", outlineColor); 49 mat.SetTexture("_occlusionTex", occlusionTexture); 50 mat.SetTexture("_strechTex", strechTexture); 51 Graphics.Blit(srcTex, dstTex, mat); 52 53 RenderTexture.ReleaseTemporary(depthTexture); 54 RenderTexture.ReleaseTemporary(occlusionTexture); 55 RenderTexture.ReleaseTemporary(strechTexture); 56 57 } 58 }
16-27行:創建一個專門用來渲染描邊的相機。該相機渲染出一個剔除了待描邊物體的depthmap用於判斷遮擋的區域。17將相機渲染模式設置為depth,這樣在之后的shader中就可以調用Unity的內置變量_CameraDepthTexture來獲取深度圖了。
30行:OnRenderImage()是Unity引擎內置的函數,在相機最終輸出圖像時會調用該函數,很多后期渲染處理都放在該函數中。具體的可以搜一下“Unity流程圖”,直觀的了解在一幀中Unity是如何調用各種內置的函數的。注意將該.cs腳本掛在對應相機對象上才啟用。
32-34行:調用RenderTexture.GetTemporary()來分配一個texture內存。為什么不用New呢?Unity的官方文檔解釋說這個比new快很多,也確實是。學習了。但使用后記得馬上ReleaseTemporary。另外特別注意的是在創建一個RenderTexture(不論是用new還是gettemporary)時對depthBuffer的設置,如果將33,34行的depthBuffer設置為16/24/32,Blit輸出的圖像始終就都是相機渲染的圖像?關於RenderTexture中depthBuffer這塊還不是很理解,之后還需要查下資料,這里暫標記下。有兄弟了解的可以在評論區交流。
接下來是幾個shader的源碼

1 Shader "ShapeOutline/Depth" 2 { 3 Properties 4 { 5 } 6 SubShader 7 { 8 Tags { "RenderType"="Opaque" } 9 LOD 100 10 Pass 11 { 12 CGPROGRAM 13 #pragma vertex vert 14 #pragma fragment frag 15 #include "UnityCG.cginc" 16 17 struct appdata 18 { 19 float4 vertex : POSITION; 20 float2 uv : TEXCOORD0; 21 }; 22 struct v2f 23 { 24 float2 depth : TEXCOORD0; 25 float4 vertex : SV_POSITION; 26 }; 27 v2f vert (appdata v) 28 { 29 v2f o; 30 o.vertex = UnityObjectToClipPos(v.vertex); 31 o.depth = o.vertex.zw; 32 return o; 33 } 34 fixed4 frag (v2f i) : SV_Target 35 { 36 float depth = i.vertex.z/i.vertex.w; 37 38 return fixed4(depth,depth,depth,0); 39 } 40 ENDCG 41 } 42 } 43 }
這段代碼沒什么好說的了,就是輸出outline層物體的depthmap,注意下輸出的格式。(DepthMap的要求?這里也做個標記)

1 Shader "ShapeOutline/Occlusion" 2 { 3 Properties 4 { 5 _MainTex ("Texture", 2D) = "white" {} 6 } 7 SubShader 8 { 9 Tags { "RenderType"="Opaque" } 10 LOD 100 11 Pass 12 { 13 CGPROGRAM 14 #pragma vertex vert 15 #pragma fragment frag 16 #include "UnityCG.cginc" 17 18 struct appdata 19 { 20 float4 vertex : POSITION; 21 float2 uv : TEXCOORD0; 22 23 }; 24 struct v2f 25 { 26 float4 ScreenPos : TEXCOORD0; 27 float4 vertex : SV_POSITION; 28 }; 29 sampler2D _MainTex; 30 float4 _MainTex_ST; 31 uniform sampler2D _CameraDepthTexture; 32 fixed4 _OutlineColor; 33 34 v2f vert (appdata v) 35 { 36 v2f o; 37 o.vertex = UnityObjectToClipPos(v.vertex); 38 o.ScreenPos = ComputeScreenPos(o.vertex); 39 return o; 40 } 41 fixed4 frag (v2f i) : SV_Target 42 { 43 i.ScreenPos.xy = i.ScreenPos.xy/i.ScreenPos.w; 44 float2 uv = float2(i.ScreenPos.x,i.ScreenPos.y); 45 float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, uv)); 46 float depthTex = tex2D(_MainTex,i.ScreenPos.xy); 47 if((depthTex > depth) && depthTex!= 1) 48 return fixed4(_OutlineColor.rgb,i.vertex.z); 49 else 50 return fixed4(0,0,0,1); 51 } 52 ENDCG 53 } 54 } 55 }
輸出Outline層物體被其他物體遮擋的部分。注意_CameraTexture變量的來源,上文已經提到了。另外就是在對輸入的Texture進行采樣時,不再是直接根據模型UV坐標來采樣了,而是應該用屏幕坐標來采樣。模型頂點坐標如何轉換為屏幕坐標請看37,38,43。這里貼一張OcclusionTexture方便理解

1 Shader "ShapeOutline/StrechOcclusion" 2 { 3 Properties 4 { 5 _MainTex ("Texture", 2D) = "white" {} 6 } 7 SubShader 8 { 9 Tags { "RenderType"="Opaque" } 10 LOD 100 11 Pass 12 { 13 CGPROGRAM 14 #pragma vertex vert 15 #pragma fragment frag 16 #include "UnityCG.cginc" 17 18 struct appdata 19 { 20 float4 vertex : POSITION; 21 float2 uv : TEXCOORD0; 22 }; 23 24 struct v2f 25 { 26 float4 screenPos : TEXCOORD0; 27 float4 vertex : SV_POSITION; 28 }; 29 30 sampler2D _MainTex; 31 float4 _MainTex_ST; 32 uniform fixed4 _OutlineColor; 33 34 v2f vert (appdata v) 35 { 36 v2f o; 37 o.vertex = UnityObjectToClipPos(v.vertex); 38 o.screenPos = ComputeScreenPos(o.vertex); 39 return o; 40 } 41 fixed4 frag (v2f i) : SV_Target 42 { 43 i.screenPos.xy = i.screenPos.xy/i.screenPos.w; 44 fixed4 col1 = tex2D(_MainTex,i.screenPos.xy); 45 fixed4 col2 = tex2D(_MainTex,float2(i.screenPos.x + 1/_ScreenParams.x,i.screenPos.y)); 46 fixed4 col3 = tex2D(_MainTex,float2(i.screenPos.x - 1/_ScreenParams.x,i.screenPos.y)); 47 fixed4 col4 = tex2D(_MainTex,i.screenPos.xy); 48 fixed4 col5 = tex2D(_MainTex,float2(i.screenPos.x ,i.screenPos.y+ 1/_ScreenParams.y)); 49 fixed4 col6 = tex2D(_MainTex,float2(i.screenPos.x ,i.screenPos.y- 1/_ScreenParams.y)); 50 if((col1.x + col1.y + col1.z 51 + col2.x + col2.y + col2.z 52 + col3.x + col3.y + col3.z 53 + col4.x + col4.y + col4.z 54 + col5.x + col5.y + col5.z 55 + col6.x + col6.y + col6.z 56 )>0.01) 57 return fixed4(_OutlineColor.rgb,i.vertex.z); 58 else 59 return fixed4(0,0,0,1); 60 } 61 ENDCG 62 } 63 } 64 }
這段Shader功能是對原來遮擋輸出的圖像進行上下左右放大一個像素,之后將這張圖的圖像剔除遮擋部分就是描邊的線條了。

1 Shader "ShapeOutline/Mix" 2 { 3 Properties 4 { 5 _MainTex ("Texture", 2D) = "white" {} 6 } 7 SubShader 8 { 9 Tags { "RenderType"="Opaque" } 10 LOD 100 11 Pass 12 { 13 CGPROGRAM 14 #pragma vertex vert 15 #pragma fragment frag 16 #include "UnityCG.cginc" 17 18 struct appdata 19 { 20 float4 vertex : POSITION; 21 float2 uv : TEXCOORD0; 22 }; 23 24 struct v2f 25 { 26 float4 screenPos : TEXCOORD0; 27 float4 vertex : SV_POSITION; 28 }; 29 30 sampler2D _MainTex; 31 float4 _MainTex_ST; 32 uniform sampler2D _occlusionTex; 33 uniform sampler2D _strechTex; 34 uniform fixed4 _OutlineColor; 35 36 v2f vert (appdata v) 37 { 38 v2f o; 39 o.vertex = UnityObjectToClipPos(v.vertex); 40 o.screenPos = ComputeScreenPos(o.vertex); 41 return o; 42 } 43 44 fixed4 frag (v2f i) : SV_Target 45 { 46 i.screenPos.xy /= i.screenPos.w; 47 fixed4 srcCol = tex2D(_MainTex,float2(i.screenPos.x,1-i.screenPos.y)); 48 fixed4 occlusionCol = tex2D(_occlusionTex,fixed2(i.screenPos.x,i.screenPos.y)); 49 fixed4 strechCol = tex2D(_strechTex,fixed2(i.screenPos.x,i.screenPos.y)); 50 float isOcclusion = occlusionCol.x + occlusionCol.y + occlusionCol.z; 51 float isStrech = strechCol.x + strechCol.y + strechCol.z; 52 if(isStrech > 0.5 && isOcclusion<0.1) 53 return _OutlineColor; 54 else 55 return srcCol; 56 } 57 ENDCG 58 } 59 } 60 }
最終混合的Shader,即將拉伸的圖像剔除遮擋部分,並與原相機的圖像進行疊加。
該方案實現了遮擋描邊的效果,但是問題又來了。圖2中如果藍方塊在紅方塊后面,則無法描繪出邊框,如果全部位於紅色方塊后,則描邊的效果就消失了。
這部分代碼參考了EsFog前輩的博客,原文地址:http://www.cnblogs.com/Esfog/p/CoverOutline_Shader.html
方案三:
基於方案二的優化,在渲染depthmap時僅剔除自身。
改進的思路:記錄所有待描邊的物體,描繪當前物體時僅將當前物體設為“Outline”層,講所有描邊的物體繪制出邊框輪廓后再與原相機渲染的圖像疊加。
工程代碼如下

1 using UnityEngine; 2 using System.Collections; 3 using System.Collections.Generic; 4 5 public class MultiShapeOutline : MonoBehaviour { 6 7 public Camera objectCamera = null; 8 public Color outlineColor = Color.green; 9 Camera mainCamera; 10 RenderTexture depthTexture; 11 RenderTexture occlusionTexture; 12 RenderTexture strechTexture; 13 RenderTexture outputTexture; 14 RenderTexture inputTexture; 15 Material m; 16 [SerializeField] 17 List<GameObject> renderObjects = new List<GameObject>(); 18 // Use this for initialization 19 void Start () { 20 mainCamera = Camera.main; 21 mainCamera.depthTextureMode = DepthTextureMode.Depth; 22 objectCamera.depthTextureMode = DepthTextureMode.None; 23 objectCamera.cullingMask = 1 << LayerMask.NameToLayer("Outline"); 24 objectCamera.fieldOfView = mainCamera.fieldOfView; 25 objectCamera.clearFlags = CameraClearFlags.Color; 26 objectCamera.projectionMatrix = mainCamera.projectionMatrix; 27 objectCamera.nearClipPlane = mainCamera.nearClipPlane; 28 objectCamera.farClipPlane = mainCamera.farClipPlane; 29 objectCamera.targetTexture = depthTexture; 30 objectCamera.aspect = mainCamera.aspect; 31 objectCamera.orthographic = false; 32 objectCamera.enabled = false; 33 34 m = new Material(Shader.Find("ShapeOutline/DoNothing")); 35 } 36 37 // Update is called once per frame 38 void Update () { 39 40 } 41 42 void OnRenderImage(RenderTexture srcTex, RenderTexture dstTex) 43 { 44 outputTexture = RenderTexture.GetTemporary(Screen.width, Screen.height, 0); 45 inputTexture = RenderTexture.GetTemporary(Screen.width, Screen.height, 0); 46 Graphics.Blit(srcTex, inputTexture, m); 47 for (int i = 0; i < renderObjects.Count; i++) 48 { 49 depthTexture = RenderTexture.GetTemporary(Screen.width, Screen.height, 24, RenderTextureFormat.Depth); 50 occlusionTexture = RenderTexture.GetTemporary(Screen.width, Screen.height, 0); 51 strechTexture = RenderTexture.GetTemporary(Screen.width, Screen.height, 0); 52 53 int orgLayer = renderObjects[i].layer; 54 renderObjects[i].layer = LayerMask.NameToLayer("Outline"); 55 56 objectCamera.targetTexture = depthTexture; 57 objectCamera.RenderWithShader(Shader.Find("ShapeOutline/Depth"), string.Empty); 58 59 Material mat = new Material(Shader.Find("ShapeOutline/Occlusion")); 60 mat.SetColor("_OutlineColor", outlineColor); 61 Graphics.Blit(depthTexture, occlusionTexture, mat); 62 63 mat = new Material(Shader.Find("ShapeOutline/StrechOcclusion")); 64 mat.SetColor("_OutlineColor", outlineColor); 65 Graphics.Blit(occlusionTexture, strechTexture, mat); 66 67 68 mat = new Material(Shader.Find("ShapeOutline/MultiMix")); 69 mat.SetColor("_OutlineColor", outlineColor); 70 mat.SetTexture("_occlusionTex", occlusionTexture); 71 mat.SetTexture("_strechTex", strechTexture); 72 Graphics.Blit(inputTexture, outputTexture, mat); 73 74 RenderTexture.ReleaseTemporary(depthTexture); 75 RenderTexture.ReleaseTemporary(occlusionTexture); 76 RenderTexture.ReleaseTemporary(strechTexture); 77 renderObjects[i].layer = orgLayer; 78 79 Graphics.Blit(outputTexture, inputTexture, m); 80 } 81 Graphics.Blit(outputTexture, dstTex, m); 82 83 RenderTexture.ReleaseTemporary(inputTexture); 84 RenderTexture.ReleaseTemporary(outputTexture); 85 } 86 }
17行:記錄所有待描邊的物體
44、45行:定義了兩張臨時的RenderTexture,其實主要目的就是為了將上一次的圖像輸出用作下一次的圖像輸入。當然,實現這種功能第一想法就是Graphics.Blit(renderTexture,renderTexture,material),但實際Blit函數並不允許這樣的參數操作,各位兄弟可以自己實際測試下。
53、54、77行:將描邊的物體單獨設置為描邊渲染的層。
68行:shader改了名字而已,和上面貼出的代碼一致。
功能寫到這就結束了,當然根據各種不同的需求可以對功能進行修改。這是自己第一篇博文,不足之處請大家指出,也歡迎大家一起評論交流。