UnityShader實現物體被遮擋描邊


  之前在網上看到物體遮擋描邊的功能,自己也拿來實現了一番。算作第一篇博客的開篇。

  先貼出幾張效果圖,也是個人思路和方案的改進路線吧。

 //////////////////////////////////////////////////////////////////方案實現////////////////////////////////////////////////////////////////////////////////////////

  看到描邊的功能,最先想到的就是用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 }
View Code

 

  主要功能在第三個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 }
View Code

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

  這段代碼沒什么好說的了,就是輸出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 }
View Code

  輸出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 }
View Code

  這段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 }
View Code

  最終混合的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 }
View Code

  17行:記錄所有待描邊的物體

  44、45行:定義了兩張臨時的RenderTexture,其實主要目的就是為了將上一次的圖像輸出用作下一次的圖像輸入。當然,實現這種功能第一想法就是Graphics.Blit(renderTexture,renderTexture,material),但實際Blit函數並不允許這樣的參數操作,各位兄弟可以自己實際測試下。

  53、54、77行:將描邊的物體單獨設置為描邊渲染的層。

  68行:shader改了名字而已,和上面貼出的代碼一致。

 

  功能寫到這就結束了,當然根據各種不同的需求可以對功能進行修改。這是自己第一篇博文,不足之處請大家指出,也歡迎大家一起評論交流。

  

  


免責聲明!

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



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