Esfog_UnityShader教程_遮擋描邊(實現篇)


   在上一篇中,我們基本上說明了遮擋描邊實現的一種基本原理。這一篇中我們將了解一下基於這種原理的具體實現代碼。本篇中的內容和前幾篇教程相比,相對比較難一些,建議先有一些基本的Unity的C#腳本編程經驗和基本的Unity Shader基礎(可參考前幾篇教程)。

  下面我們就開始講解具體的實現代碼(由於代碼較多,所以這里只對需要講解的地方進行講解):


 C#腳本部分


 

  與之前不同,這一次需要寫一個C#腳本來輔助我們實現這個功能。它所做的主要工作就是創建一個臨時攝像機用來獲取我們想進行描邊的物體的深度圖,以及通過OnRenderImage函數對已經渲染完畢的主攝像機圖像利用我們編寫的Shader進行特殊處理,將處理完的結果再渲染到屏幕上。

 1 /*************
 2 ** author:esfog
 3 /************/
 4 using UnityEngine;
 5 using System.Collections;
 6 
 7 public class PlayerOutLine : MonoBehaviour 
 8 {
 9     //遮擋描邊顏色
10     public Color OutLineColor = Color.green;
11 
12     private GameObject _cameraGameObject;
13     //攝像機(專門用來處理遮擋描邊的)
14     private Camera _camera;
15     private Camera _mainCamera;
16     //所需的RenderTexture
17     private RenderTexture _renderTextureDepth;
18     private RenderTexture _renderTextureOcclusion;
19     private RenderTexture _renderTextureStretch;
20 
21     //臨時材質
22     private Material _materialOcclusion;
23     private Material _materialStretch;
24     private Material _materialMix;
25     //用來處理玩家深度的Shader
26     private Shader _depthShader;
27 
28     // 相關初始化
29     void Start () 
30     {
31         _mainCamera = Camera.main;
32         _mainCamera.depthTextureMode = DepthTextureMode.Depth;
33         _depthShader = Shader.Find("Esfog/OutLine/Depth");
34 
35         _cameraGameObject = new GameObject ();
36         _cameraGameObject.transform.parent = _mainCamera.transform;
37         _cameraGameObject.transform.localPosition = Vector3.zero;
38         _cameraGameObject.transform.localScale = Vector3.one;
39         _cameraGameObject.transform.localRotation = Quaternion.identity;
40 
41         _camera = _cameraGameObject.AddComponent<Camera> ();
42         _camera.aspect = _mainCamera.aspect;
43         _camera.fieldOfView = _mainCamera.fieldOfView;
44         _camera.orthographic = false;
45         _camera.nearClipPlane = _mainCamera.nearClipPlane;
46         _camera.farClipPlane = _mainCamera.farClipPlane;
47         _camera.rect = _mainCamera.rect;
48         _camera.depthTextureMode = DepthTextureMode.None;
49         _camera.cullingMask = 1 << (int)LayerMask.NameToLayer ("Main Player");
50         _camera.enabled = false;
51         _materialOcclusion = new Material(Shader.Find("Esfog/OutLine/Occlusion"));
52         _materialStretch = new Material (Shader.Find ("Esfog/OutLine/Stretch"));
53         _materialMix = new Material (Shader.Find("Esfog/OutLine/Mix"));
54         Shader.SetGlobalColor ("_OutLineColor",OutLineColor);
55         if (!_depthShader.isSupported || !_materialMix.shader.isSupported || !_materialMix.shader.isSupported || !_materialOcclusion.shader.isSupported) 
56         {
57             return;
58         }
59     }
60 
61     void OnRenderImage(RenderTexture source,RenderTexture destination)
62     {
63         _renderTextureDepth = RenderTexture.GetTemporary(Screen.width,Screen.height,24,RenderTextureFormat.Depth);
64         _renderTextureOcclusion = RenderTexture.GetTemporary (Screen.width,Screen.height,0);
65         _renderTextureStretch = RenderTexture.GetTemporary (Screen.width,Screen.height,0);
66         _camera.targetTexture = _renderTextureDepth;
67 
68         _camera.fieldOfView = _mainCamera.fieldOfView;
69         _camera.aspect = _mainCamera.aspect;
70         _camera.RenderWithShader (_depthShader,string.Empty);
71 
72         //對比我們為角色生成的RenderTexture和主攝像機自身的深度緩沖區,計算出角色的哪些區域被擋住了
73         Graphics.Blit(_renderTextureDepth,_renderTextureOcclusion,_materialOcclusion);
74         var screenSize = new Vector4(1.0f/Screen.width,1.0f/Screen.height,0.0f,0.0f);
75 
76         _materialStretch.SetVector ("_ScreenSize",screenSize);
77         Graphics.Blit (_renderTextureOcclusion,_renderTextureStretch,_materialStretch,0);
78         Graphics.Blit (_renderTextureStretch,_renderTextureStretch,_materialStretch,1);
79 
80         _materialMix.SetTexture ("_OcclusionTex",_renderTextureOcclusion);
81         _materialMix.SetTexture ("_StretchTex",_renderTextureStretch);
82         Graphics.Blit (source,destination,_materialMix);
83 
84         RenderTexture.ReleaseTemporary (_renderTextureDepth);
85         RenderTexture.ReleaseTemporary (_renderTextureOcclusion);
86         RenderTexture.ReleaseTemporary (_renderTextureStretch);
87 
88     }
89 }

   10~26行,這部分主要是聲明一些我們在后面將要使用到的變量,包括我們指定的描邊顏色,對主攝像機的引用,以及對我們在后面要創建的臨時攝像機的引用,3個RenderTexture,1個Shader,以及三個Material.對RenderTexture做一下解釋:一般來說場景中的Camera渲染完畢后圖像是直接顯示在游戲屏幕上的,但是我們也可以創建一個RenderTexture,然后把相機的輸出目標制定為這個RenderTexture上面,那么我們將不會在屏幕上看到這個相機的任何渲染結果,因為結果已經被保存到我們指定的RenderTexture了。你也可以把屏幕窗口理解為一個默認的RenderTexture.后面具體應用還會說明。

  31~33行,首先獲得對場景主攝像機的引用,再通過_mainCamera.depthTextureMode = DepthTextureMode.Depth;將主攝像機的深度圖模式設置為Depth.這樣我們的主攝像機就會為我們提供深度圖了,Shader中我們就可以直接使用_CameraDepthTexture來使用這張深度圖了.然后我們通過Shader.Find()來初始化前面我們聲明的一個Shader變量,后面我們會用到。

  35~50行,我們開始創建我們的專門用來生成被描邊物體的深度圖的攝像機了.我們把生成的攝像機作為主攝像機的子物體,主要是為了比較容易調整位置,沒什么特殊意義.由於我們要保證這個臨時攝像機和主攝像機拍攝角度和范圍一模一樣,我們把子物體的位置等信息都置零,然后把臨時相機的參數全部設置成和主攝像機一樣,這里的臨時攝像機我們通過_camera.depthTextureMode = DepthTextureMode.None;讓它不生成深度圖,因為前面我們已經讓主攝像機的深度模式為Depth了,而Shader中的_CameraDepthTexture只能用於一個主攝像機的深度圖,所以所以這里面我們把臨時相機的DepthTextureMode設置為None.然后我們后面通過我們自己編寫的Shader來獲得深度信息。最后這句_camera.cullingMask = 1 << (int)LayerMask.NameToLayer ("Main Player");是為了讓我們的相機只渲染我們想要描邊的物體,cullingMask屬性是一個通過不同位上的01值來判斷是否渲染該層的物體,這里我們創建一共新層叫"Main Player",並在場景中把我們的要渲染的物體的Layer設置為Main Player.這樣就能通過這行代碼就實現了整個目的。

  51~58行,前三行,我們初始化了前面聲明的三個Material變量.可以看到Material的構造函數需要我們提供一個Shader,這里面我們通過Shader.Find()把我們編寫的3個Shader用到三個Material上,Shader內容和用途在后面說明.Shader.SetGlobalColor ("_OutLineColor",OutLineColor);這句話就是一種通過C#腳本來給Shader中變量進行賦值的一種方法,但是這種方法是全局設置的,所以可能所有Shader的同名變量都會受到印象,所以使用前要確保只初始化了想要初始化的Shader變量。這句話就是把我們Shader中的描邊顏色設置成我們OutLineColor表示的顏色.最后面的if語句,是用來判斷一下是否我們編寫的4個Shader是否能被支持.

  61行,OnRenderImage(RenderTexture source,RenderTexture destination),這個方法MonoBehaviour提供的,如果你在腳本中寫上這個函數的話,那么如果你把這個腳本掛在含有Camera組件的物體時候,當這個攝像機渲染完的時候會調用這個OnRenderImage,把當前渲染結果當做第一個參數傳入,也就是這里的source,然后通過我們對source的處理,最后把處理結果賦給destination,這個destination就是攝像機最終的渲染結果了。所以我們主要的處理步驟都是在OnRenderImage里面進行的.

  63~65行,我們前面聲明了三個RenderTexture,這里就是初始化他的地方,其中_renderTextureDepth用來保存臨時攝像機獲取到的深度圖信息,_renderTextureOcclusion用來保存我們進行Occlusion處理后的圖像信息,這個處理就是通過Shader來找出需要描邊物體的被遮擋部分._renderTextureStretch用來保存被遮擋區域拉伸后的信息,這幾個操作都是按次序來個,前一個的輸出作為后一個的輸入.RenderTexture.GetTemporary()函數是Unity提供給我們初始化RenderTexture使用的,前兩個參數是指定Texture的寬高,第三個參數是指定這個RenderTexture處理時候所涉及到的深度緩沖的具體精度(24是最大精度),第四個參數是指定圖像保存的格式.這里面我們只有_renderTextureDepth比較特殊,由於他在處理過程中需要涉及到深度信息,所以第三個參數設置為24,最后一個參數設置為RenderTextureFormat.Depth是為了於Unity默認處理深度圖的格式保持一致,以便我們后面處理。而其他兩張RenderTexture都只是對二維圖像進行處理,不需要深度信息,所以第三個參數可以設置為0.

  66~70行,_camera.targetTexture = _renderTextureDepth;=這一句把臨時攝像機的渲染目標指定為為我們剛剛初始化的RenderTexture,前面已經解釋過為什么這么做了.中間兩句讓臨時攝像機的FOV和Aspect和主攝像機保持一致,因為有時候游戲中是可以拉近或者拉遠視角的,而我們只有在Start方法里初始化了一次臨時攝像機的參數。所以如果不修改的話可能導致兩個攝像機參數不一致的問題。_camera.RenderWithShader這一句是為了讓臨時攝像機利用這個我們提供的Shader來進行渲染, 也就是把這個攝像機處理的所有頂點信息傳進這個Shader,然后把Shader的返回結果作為輸出保存到攝像機的targetTexture上.其中第一個參數是指定我們用到的Shader,第二個參數用不到,這里不解釋了.

  73行,Graphics.Blit()是后期處理中的常用方法,他把第一個參數作為輸入,利用第三個參數提供的材質所包含的Shader進行處理,處理后把結果輸出到第二個參數上去.所以這一行中我們把上一步中得到的臨時相機深度圖作為輸入,利用我們初始化的負責Occlusion處理的Material來獲取被遮擋區域,再把結果保存到_renderTextureOcclusion上.

  74~78行,由於我們下面要對被遮擋區域進行分別進行一次橫縱拉伸一像素的操作,而在Shader中我們只能對UV來處理,所以必須知道屏幕的一像素代表多少UV,第一行代碼就是為了進行這個計算.第76行把計算好的值傳到Shader中,這個方法和前面的全局給Shader賦值不同,這里只對相應材質對應的Shader進行了賦值.最后兩行通過Blit分別進行了橫縱的拉伸處理,最后面參數的0和1,是指調用Shader中的第幾個Pass,其中0代表第一個.后面看具體Shader代碼就明白了.

  80~82行,將前面兩步處理的RenderTexture當做參數傳遞給最后一個Shader,然后我們通過Blit把主攝像機的原始圖像當做輸入,利用包含這個Shader的Material來處理,處理完成后把結果保存到destination上顯示到游戲屏幕.

  84~86行,處理完畢要釋放掉我們前面想系統申請的RenderTexture.

 

  下面分別來講解我們用到的4個Shader:


獲取深度信息的Shader


 

  第一個是用來獲取臨時相機深度圖的.

 1 Shader "Esfog/OutLine/Depth" 
 2 {
 3     SubShader 
 4     {
 5         Tags { "RenderType"="Opaque" }
 6         Pass 
 7         {
 8             CGPROGRAM
 9 
10             #pragma vertex vert
11             #pragma fragment frag
12             #include "UnityCG.cginc"
13 
14             struct v2f 
15             {
16                 float4 pos : SV_POSITION;
17                 float2 depth : TEXCOORD0;
18             };
19 
20             v2f vert (appdata_base v) 
21             {
22                 v2f o;
23                 o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
24                 o.depth = o.pos.zw;
25                 return o;
26             }
27 
28             half4 frag(v2f i) : COLOR 
29             {
30                 half x= i.depth.x/i.depth.y;
31                 return half4(x, x, x, x);
32             }
33             ENDCG
34         }
35     }
36 }

  就不逐行講解了,主要注意一下下面幾處:

   24行,這里我們用一個float2的depth變量來保存進行了MVP變換后的頂點的的z和w信息,這里要說明一下,透視投影要分為矩陣變換和,透視除法兩個部分.UnityShader在頂點着色器處理完后只完成了矩陣變換,還沒有進行透視除法,而o.pos.zw中利用z/w就能才能把頂點在投影空間的距離轉換到0~1的空間.而后面的第31行正是進行了這樣的處理。為什么返回half4(x,x,x,x).這可能是由於深度圖的格式要求吧,這段代碼在Unity官方文檔上有.另外可以用UNITY_TRANSFER_DEPTH(o.depth);替換24行.用UNITY_OUTPUT_DEPTH(i.depth);來替換30~31行.這些在UnityCG.cginc上有定義,有興趣可以看看.


檢測遮擋區域的Shader


 

  第二個Shader是用來利用前面得到的深度圖並結合出攝像機深度圖來找出被遮擋區域的.

 1 Shader "Esfog/OutLine/Occlusion" 
 2 {
 3     Properties 
 4     {
 5         _MainTex ("Base (RGB)", 2D) = "white" {}
 6     }
 7     SubShader 
 8     {
 9         Tags { "RenderType"="Opaque" }
10         
11         Pass
12         {
13             CGPROGRAM
14             #pragma vertex vert_img
15             #pragma fragment frag
16             #include "UnityCG.cginc"
17 
18             uniform sampler2D _MainTex;
19             uniform sampler2D _CameraDepthTexture;
20             uniform float4 _OutLineColor;
21             
22             float4 frag(v2f_img i):COLOR
23             {
24                 float playerDepth = tex2D(_MainTex,i.uv);
25                 float bufferDepth = tex2D(_CameraDepthTexture,i.uv);
26                 float4 resColor = float4(0,0,0,0);
27                 if((playerDepth < 1.0) && (playerDepth- bufferDepth)>0.0002)
28                 {
29                     resColor = float4(_OutLineColor.rgb,1);
30                 }
31                 return resColor;
32             }
33             ENDCG
34         }
35     } 
36     FallBack "Diffuse"
37 }

注意以下幾處:

 14行,大家可能會奇怪為什么沒有定義vertex函數,我們通過#pragma vertex vert_img使用了Unity為我們定義好的一些vertex函數,避免了自己手動編寫,可以去UnityCG.cginc.看一下很簡單。

 18~20行,注意這里的_MainTex,因為我們在外面是通過Graphics.Blit函數來使用這個Shader的,所以Blit函數的第一個RenderTexture參數會自動被賦給_MainTex,后面的Shader也都是如此.第二行的_CameraDepthTexture在前面我們已經說過了,是Unity為什么定義好的變量,這里面代表主攝像機的深度圖.第三行的表面顏色,前面也已經在C#腳本里面賦值過了.

 24~30行,主攝像機的深度圖和臨時攝像機的深度圖都是相對我們整個游戲窗口的兩個二維圖片,而游戲屏幕的當成一個大的模型,這個模型相當於一個大的面片只有4個頂點構成,uv就是0~1范圍就對應於整個屏幕的寬高范圍。所以前兩行我們把兩個深度圖上相應uv上的像素顏色值取到,另外由於深度圖的最終深度值都是存儲在R通道上的,所以這里我們只用了兩個float變量來接收tex2D的返回值.由於深度圖中深度的信息范圍是(0~1)其中0代表近平面,1代表遠平面,值越大表示位置越靠后.if判斷中playerDepth < 1.0是因為默認攝像機填充深度圖的值就是1.0表示沒有拍攝到物體,而palyerDepth<1.0就表示這個像素是在我們要被描邊物體的身上的.而(playerDepth- bufferDepth)>0.0002是說明該像素所代表的位置在主攝像機上的深度比臨時攝像機的深度要小,也就是這個像素實際上是被擋住的,這里的0.0002是因為避免相機深度的精度誤差所引入的,不是必須的.如果if判斷為true說明這個像素被遮擋,可以把顏色設置為描邊顏色,否則使用本來的渲染顏色.

 


 拉伸遮擋區域的Shader


 

第三個Shader是用來把上一步中得到的遮擋部分進行拉伸的.

 1 Shader "Esfog/OutLine/Stretch" 
 2 {
 3     Properties
 4     {
 5         _MainTex ("Base (RGB)", 2D) = "white" {}
 6     }
 7     SubShader 
 8     {
 9         Tags { "RenderType"="Opaque" }
10         Pass
11         {
12             CGPROGRAM
13             #pragma vertex vert_img
14             #pragma fragment frag
15             #include "UnityCG.cginc"
16             uniform sampler2D _MainTex;
17             uniform float4 _OutLineColor;
18             uniform float4 _ScreenSize;
19             
20             float4 frag(v2f_img i):COLOR
21             {
22                 float4 c = tex2D(_MainTex,i.uv);
23                 float4 c1 = tex2D(_MainTex,float2(i.uv.x-_ScreenSize.x,i.uv.y)); //左邊一個像素
24                 float4 c2 = tex2D(_MainTex,float2(i.uv.x+_ScreenSize.x,i.uv.y)); //右邊一個像素
25                 float3 totalCol = c.rgb + c1.rgb + c2.rgb;
26                 float avg = totalCol.r + totalCol.g + totalCol.b;
27                 if(avg > 0.01)
28                 {
29                     return _OutLineColor;
30                 }
31                 else
32                 {
33                     return float4(0,0,0,0);
34                 }
35             }
36             
37             ENDCG
38         }
39         
40         Pass
41         {
42             Blend One One
43             CGPROGRAM
44             #pragma vertex vert_img
45             #pragma fragment frag
46             #include "UnityCG.cginc"
47             uniform sampler2D _MainTex;
48             uniform float4 _OutLineColor;
49             uniform float4 _ScreenSize;
50             
51             float4 frag(v2f_img i):COLOR
52             {
53                 float4 c = tex2D(_MainTex,i.uv);
54                 float4 c1 = tex2D(_MainTex,float2(i.uv.x,i.uv.y-_ScreenSize.y)); //下邊一個像素
55                 float4 c2 = tex2D(_MainTex,float2(i.uv.x,i.uv.y+_ScreenSize.y)); //上邊一個像素
56                 float3 totalCol = c.rgb + c1.rgb + c2.rgb;
57                 float avg = totalCol.r + totalCol.g + totalCol.b;
58                 if(avg > 0.01)
59                 {
60                     return _OutLineColor;
61                 }
62                 else
63                 {
64                     return float4(0,0,0,0);
65                 }
66             }
67             
68             ENDCG
69         }
70     } 
71     FallBack "Diffuse"
72 }

  這個Shader比較長是因為寫了兩個Pass,這兩個Pass的內容很相近,第一個Pass用來橫向拉伸一個像素,第二個Pass進行縱向拉伸一個像素.前面說到我們通過Blit函數的第四個參數來決定使用哪個Pass.注意以下幾行:

  22~34行,在C#腳本中我們計算了游戲窗口的一個像素所代表的uv長度,所以我們這里以第一個Pass為例分別像當前像素的左邊一個像素和右邊一個像素取顏色,然后把他們的RGB值加在一起,如果大於0.01也就說明當前像素的左右區域是有顏色的,也就是屬於描邊區域,那么該像素就需要被設置為描邊區域,設置成表面顏色,否則設置為黑.縱向拉伸也是同樣的道理.


獲取最終描邊輪廓Shader


 

  最后一個Shader是利用我們上面處理取得的RenderTexture對主攝像機渲染的原始圖像進行最終處理.

 1 Shader "Esfog/OutLine/Mix" 
 2 {
 3     Properties 
 4     {
 5         _MainTex ("Base (RGB)", 2D) = "white" {}
 6     }
 7     SubShader 
 8     {
 9         Tags { "RenderType"="Opaque" }
10         Pass
11         {
12             CGPROGRAM
13             #pragma vertex vert_img
14             #pragma fragment frag
15             #include "UnityCG.cginc"
16             
17             uniform sampler2D _MainTex;
18             uniform sampler2D _OcclusionTex;
19             uniform sampler2D _StretchTex;
20             
21             float4 frag(v2f_img i):COLOR
22             {
23                 float4 srcCol = tex2D(_MainTex,i.uv);
24                 float4 occlusionCol = tex2D(_OcclusionTex,i.uv);
25                 float4 stretchCol = tex2D(_StretchTex,i.uv);
26                 float occlusionTotal = occlusionCol.r + occlusionCol.g + occlusionCol.b;
27                 float stretchTotal = stretchCol.r + stretchCol.g + stretchCol.b;
28                 
29                 if(occlusionTotal <0.01f&&stretchTotal>0.01f)
30                 {
31                     return float4(stretchCol.rgb,srcCol.a);
32                 }
33                 else
34                 {
35                     return srcCol;
36                 }
37             }
38             
39             ENDCG
40         }
41     } 
42     FallBack "Diffuse"
43 }

  注意一下幾行:

  17~19行,這三個變量均有C#腳本中進行了賦值,其中_MainTex代表主攝像機原始渲染結果,_OcclusionTex是表示包含了被遮擋部分信息的RenterTexture,_StretchTex表示被遮擋區域橫縱拉伸一像素后的圖像信息.

  23~27行,和上一個Shader類似,取出這三個Texture在當前uv所對應的顏色信息.並將后兩者的rgb值各自進行累加.

  29~36行,在if判斷中,如果當前像素的uv值在被遮擋區域_OcclusionTex中沒有顏色(即occlusionTotal <0.01f),而在拉伸區域_StretchTex中有顏色值(即stretchTotal>0.01f)其實也就說明這個像素是在拉伸操作中被額外拉伸出來的那一像素.那么就把這個像素顏色設置為描邊顏色,否則設置為圖像正常渲染顏色。 

 

  總算是寫完了足足4個多小時,這篇下來確實比較長而且並沒有逐行的解釋說明,一方面我認為大家現在的水平已經不需要逐行解釋了,而來也是覺得沒必要事無巨細面面俱到,這樣子反而寫的很羅嗦,雖然現在這樣子也挺啰嗦的.如果大家一次沒有看明白,希望多看幾遍,主要是理解C#腳本和Shader如何協作,以及多Shader的應用和后期處理的基本流程.這篇教程里面有幾處地方我本人也並不是特別的清楚,所以只能是按照我自己的理解來進行了說明,希望大家在看的時候能夠自己認真思考思考,我說的並不一定對,如果你發現了筆者哪里有錯誤,請在留言中指出。如果你希望通過下篇教程講解哪方面的知識,也可以留言告訴我。希望大家有所收獲。就寫到這吧,我要下樓去轉轉了,坐的太久了.

 

  尊重他人智慧成果,歡迎轉載,請注明作者esfog,原文地址http://www.cnblogs.com/Esfog/p/CoverOutline_Shader_Code.html 

 

 


免責聲明!

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



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