之前在网上看到物体遮挡描边的功能,自己也拿来实现了一番。算作第一篇博客的开篇。

  先贴出几张效果图,也是个人思路和方案的改进路线吧。

unity 多个镂空的遮罩 unity 遮挡轮廓shader_#include

unity 多个镂空的遮罩 unity 遮挡轮廓shader_#include_02

unity 多个镂空的遮罩 unity 遮挡轮廓shader_#include_03

 //////////////////////////////////////////////////////////////////方案实现////////////////////////////////////////////////////////////////////////////////////////

  看到描边的功能,最先想到的就是用stencil的方法实现。功能最主要的部分就是如何判断边界像素,之后在FragmentShader中将该像素颜色设置成需要描边的颜色。

方案一:

  一个简单的VF Shader:

unity 多个镂空的遮罩 unity 遮挡轮廓shader_#include_04

unity 多个镂空的遮罩 unity 遮挡轮廓shader_unity 多个镂空的遮罩_05

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.将放大区域剔除遮挡区域就是描边的像素区域了,将该像素区域叠加到原图像中。

  工程代码如下:

unity 多个镂空的遮罩 unity 遮挡轮廓shader_#include_04

unity 多个镂空的遮罩 unity 遮挡轮廓shader_unity 多个镂空的遮罩_05

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的源码  

unity 多个镂空的遮罩 unity 遮挡轮廓shader_#include_04

unity 多个镂空的遮罩 unity 遮挡轮廓shader_unity 多个镂空的遮罩_05

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的要求?这里也做个标记)

unity 多个镂空的遮罩 unity 遮挡轮廓shader_#include_04

unity 多个镂空的遮罩 unity 遮挡轮廓shader_unity 多个镂空的遮罩_05

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方便理解

unity 多个镂空的遮罩 unity 遮挡轮廓shader_Code_12

unity 多个镂空的遮罩 unity 遮挡轮廓shader_#include_04

unity 多个镂空的遮罩 unity 遮挡轮廓shader_unity 多个镂空的遮罩_05

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功能是对原来遮挡输出的图像进行上下左右放大一个像素,之后将这张图的图像剔除遮挡部分就是描边的线条了。

unity 多个镂空的遮罩 unity 遮挡轮廓shader_#include_04

unity 多个镂空的遮罩 unity 遮挡轮廓shader_unity 多个镂空的遮罩_05

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中如果蓝方块在红方块后面,则无法描绘出边框,如果全部位于红色方块后,则描边的效果就消失了。


方案三:

  基于方案二的优化,在渲染depthmap时仅剔除自身。

  改进的思路:记录所有待描边的物体,描绘当前物体时仅将当前物体设为“Outline”层,讲所有描边的物体绘制出边框轮廓后再与原相机渲染的图像叠加。

  工程代码如下

unity 多个镂空的遮罩 unity 遮挡轮廓shader_#include_04

unity 多个镂空的遮罩 unity 遮挡轮廓shader_unity 多个镂空的遮罩_05

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改了名字而已,和上面贴出的代码一致。

 

  功能写到这就结束了,当然根据各种不同的需求可以对功能进行修改。这是自己第一篇博文,不足之处请大家指出,也欢迎大家一起评论交流。