Unity Shader-後處理:景深

一.簡介


景深一直是我最喜歡的效果之一,最早接觸CE3的時候,發現CE引擎默認就支持景深的效果,當時感覺這個效果特別酷炫,如今投身於Unity的懷抱中,準備用Unity實現以下傳說中的景深效果。


所謂景深,是攝影的一個專業術語:在聚焦完成後,在焦點前後的範圍內都能形成清晰的像,這一前一後的距離範圍,便叫做景深,也是被攝物體能清晰成像的空間深度。在景深範圍內景物影像的清晰度並不完全一致,其中焦點上的清晰度是最高的,其餘的影像清晰度隨着它與焦點的距離成正比例下降。先附上一張正常的照片和使用景深控制的照片:

通過左右兩張照片的對比,我們很容易發現,通過景深處理的照片,我們可以很容易地抓住照片的重點部分。這也就是景深最大的用處,能夠突出主題,並且可以使畫面更有層次感。

在攝影技術中的景深,是通過調整相機的焦距,光圈來控制景深的,這裏就不多說了。而我們的遊戲中要想出現這種效果,就需要下一番功夫了。首先拆分一下圖像的效果,圖像中主要分爲兩部分,後面的模糊背景和前面清晰的“主題”部分。後面的背景模糊我們可以通過前面的兩篇文章Unity Shader-後處理:高斯模糊Unity Shader後處理-均值模糊來實現,而前景部分就是一張清晰的場景圖,最後通過一定的權值將兩者混合,離攝像機(準確地說是焦距)越遠的部分,模糊圖片的權重越高,離攝像機越近的部分,清晰圖片的權重越高。那麼問題來了,我們怎麼知道哪個部分離攝像機更近呢?


二.Camera Depth Texture


上面說到,我們要怎麼得到攝像機渲染的這個場景的圖片中哪個部分離我們更遠,哪個部分離我們更近?答案就是Camera Depth Texture這個東東。從字面上理解這個就是相機深度紋理。在Unity中,相機可以產生深度,深度+法線或者運動方向紋理,這是一個簡化版本的G-Buffer紋理,我們可以用這個紋理進行一些後處理操作。這張紋理圖記錄了屏幕空間所有物體距離相機的距離。深度紋理中每個像素所記錄的深度值是從0 到1 非線性分佈的。精度通常是 24 位或16 位,這主要取決於所使用的深度緩衝區。當讀取深度紋理時,我們可以得到一個0-1範圍內的高精度值。如果你需要獲取到達相機的距離或者其他線性關係的值,那麼你需要手動計算它。

關於這張圖是怎麼樣產生的,在Unity5之前是通過一個叫做Shader ReplaceMent的操作完成的。這個操作簡單來說就是臨時把場景中所有的shader換成我們想要的shader,然後渲染到張圖上,我們通過Shader ReplaceMent操作,將場景中的對象shader換成按照深度寫入一張紋理圖。我們可以在5.X版本之前Unity自帶的Shader中找到這個生成深度圖的shader:Camera-DepthTexture.shader,這裏我摘出一小段Tags中RenderType爲Opaque的subshader:
[csharp] view plain copy
  1. SubShader  
  2. {  
  3.     Tags { "RenderType"="Opaque" }  
  4.     Pass   
  5.     {  
  6.         CGPROGRAM  
  7.         #pragma vertex vert  
  8.         #pragma fragment frag  
  9.         #include "UnityCG.cginc"  
  10.         struct v2f  
  11.         {  
  12.             float4 pos : POSITION;  
  13.             #ifdef UNITY_MIGHT_NOT_HAVE_DEPTH_TEXTURE  
  14.             float2 depth : TEXCOORD0;  
  15.             #endif  
  16.         };  
  17.   
  18.         v2f vert( appdata_base v )  
  19.         {  
  20.             v2f o;  
  21.             o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  22.             UNITY_TRANSFER_DEPTH(o.depth);  
  23.             return o;  
  24.         }  
  25.   
  26.         fixed4 frag(v2f i) : COLOR {  
  27.             UNITY_OUTPUT_DEPTH(i.depth);  
  28.         }  
  29.   
  30.         ENDCG  
  31.     }  
  32. }  
我們看到,當物體的渲染Tag爲Opaque也就是不透明的時候,會寫入深度紋理。而這個文件中其他的幾個subshader也分別對針對不同類型的type,比如RenderType爲TransparentCutout的subshader,就增加了一句下面的判斷,去掉了所有應該透明的地方:
[csharp] view plain copy
  1. clip( texcol.a*_Color.a - _Cutoff );  
而且這個shader中沒有出現RnderType爲Transparent類型的,因爲透明的物體不會寫入我們的深度貼圖,也就是說我們開啓了alpha blend類型的對象是不會寫入深度的。

上面的代碼中有幾個宏定義,我們可以從UnityCG.cginc文件中找到這幾個宏定義的實現:
[csharp] view plain copy
  1. // Depth render texture helpers  
  2. #if defined(UNITY_MIGHT_NOT_HAVE_DEPTH_TEXTURE)  
  3.     #define UNITY_TRANSFER_DEPTH(oo) oo = o.pos.zw  
  4.     #define UNITY_OUTPUT_DEPTH(i) return i.x/i.y  
  5. #else  
  6.     #define UNITY_TRANSFER_DEPTH(oo)   
  7.     #define UNITY_OUTPUT_DEPTH(i) return 0  
  8. #endif  
結合上面shader的使用,我們看出:UNITY_TRANSFER_DEPTH宏將傳入vertex shader中的position的最後兩位返回,也就是z座標和w座標,在unity裏面也就是從屏幕向裏看的那個方向就是z軸的方向。而UNITY_OUTPUT_DEPTH通過將z/w將真正的深度返回。UNITY_MIGHT_NOT_HAVE_DEPTH_TEXTURE是如果沒有深度圖的意思,也就是說,僅當沒有獲得深度圖的時候,纔會通過這個計算來計算深度,否則就無操作或者返回0。那麼,這種情況下,深度信息從哪裏來呢?我們看一下Unity的文檔就知道了:

  • UNITY_TRANSFER_DEPTH(o): computes eye space depth of the vertex and outputs it in o (which must be a float2). Use it in a vertex program when rendering into a depth texture. On platforms with native depth textures this macro does nothing at all, because Z buffer value is rendered implicitly.
  • UNITY_OUTPUT_DEPTH(i): returns eye space depth from i (which must be a float2). Use it in a fragment program when rendering into a depth texture. On platforms with native depth textures this macro always returns zero, because Z buffer value is rendered implicitly.
也就是說,如果硬件支持硬件深度的話,也就是直接從z buffer取深度,那麼這個宏就沒有必要了,因爲這樣的話,z buffer的深度是隱式渲染的。
關於深度紋理,深度法線紋理,運動方向紋理Unity官方文檔有很好的介紹,我們就不多說了,下面我們看一下怎麼在Unity中開啓深度的渲染。通過Camera.DepthTextureMode這個變量我們就可以控制是否開啓深度的渲染,默認這個值是None,我們可以將其設爲None,Depth,DepthNormals三種類型。只要開啓了Depth模式,我們就可以在shader中通過_CameraDepthTexture來獲得屏幕深度的紋理了。Unity官方文檔中也有詳細介紹。下面我們通過一個後處理來實現一個最簡單的輸出屏幕深度的效果:

C#腳本
[csharp] view plain copy
  1. using UnityEngine;  
  2. using System.Collections;  
  3.   
  4. [ExecuteInEditMode]  
  5. public class DepthTextureTest : PostEffectBase  
  6. {  
  7.     void OnEnable()  
  8.     {  
  9.         GetComponent<Camera>().depthTextureMode |= DepthTextureMode.Depth;  
  10.     }  
  11.   
  12.     void OnDisable()  
  13.     {  
  14.         GetComponent<Camera>().depthTextureMode &= ~DepthTextureMode.Depth;  
  15.     }  
  16.   
  17.     void OnRenderImage(RenderTexture source, RenderTexture destination)  
  18.     {  
  19.         if (_Material)  
  20.         {  
  21.             Graphics.Blit(source, destination, _Material);  
  22.         }  
  23.     }  
  24. }  
shader部分:
[csharp] view plain copy
  1. Shader "Custom/DepthTest" {  
  2.   
  3.     CGINCLUDE  
  4.     #include "UnityCG.cginc"  
  5.   
  6.     //仍然要聲明一下_CameraDepthTexture這個變量,雖然Unity這個變量是unity內部賦值  
  7.     sampler2D _CameraDepthTexture;  
  8.     sampler2D _MainTex;  
  9.     float4    _MainTex_TexelSize;  
  10.   
  11.     struct v2f  
  12.     {  
  13.         float4 pos : SV_POSITION;  
  14.         float2 uv  : TEXCOORD0;  
  15.     };  
  16.   
  17.     v2f vert(appdata_img v)  
  18.     {  
  19.         v2f o;  
  20.         o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  21.         o.uv.xy = v.texcoord.xy;  
  22.   
  23.         return o;  
  24.     }  
  25.   
  26.     fixed4 frag(v2f i) : SV_Target  
  27.     {  
  28.         //直接根據UV座標取該點的深度值  
  29.         float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, 1 - i.uv);  
  30.         //將深度值變爲線性01空間  
  31.         depth = Linear01Depth(depth);  
  32.         return float4(depth, depth, depth, 1);  
  33.     }  
  34.   
  35.     ENDCG  
  36.   
  37.     SubShader  
  38.     {  
  39.         Pass  
  40.         {  
  41.   
  42.             ZTest Off  
  43.             Cull Off  
  44.             ZWrite Off  
  45.             Fog{ Mode Off }  
  46.   
  47.             CGPROGRAM  
  48.             #pragma vertex vert  
  49.             #pragma fragment frag  
  50.             ENDCG  
  51.         }  
  52.   
  53.     }  
  54. }  

 找一個場景,將該腳本掛在攝像機並賦予材質。(注:PostEffectBase類爲後處理基類,在之前的文章中有詳細實現,此處不予貼出),找到一個場景,我們測試一下:

原始場景效果:

開啓輸出深度後處理效果:

恩,場景圖變成了一張黑白圖像,越遠的地方越亮,越近的地方越暗,也就是我們shader中所寫的,直接按照深度值來輸出了一幅圖片。不過注意,這張圖中,我把攝像機的遠裁剪面調整到了50這一比較小的距離,這樣,圖中的遠近信息顯示得更加明顯,而如果攝像機的遠裁剪面距離很大,那麼這張圖的輸出就會整體偏黑,因爲離我們較近的物體距離佔遠裁剪面的距離太小了,幾乎爲0,所以就是黑的,如下圖所示,當遠裁剪面改爲1000時深度圖,僅有窗戶的位置能看到白色:



關於CameraDepthTexture,在Unity4中CameraDepthTexture仍然是通過上面我們說的shader替換技術實現的,所以,一旦我們開啓深度渲染,會導致DrawCall翻倍!而在Unity5中,這個CameraDepthTexture與Shadow Caster使用的是一套DepthTexture,通過帶有Shadow Caster的對象纔會被渲染到深度緩存中。關於Unity5和Unity4中深度緩存的渲染,這篇文章介紹得很詳細,可以進行參考。

三.景深效果實現


終於到了這篇文章的主題了,我們通過shader實現一個景深的效果。思路上面已經說過了,通過兩張圖片,一張清晰的,一張經過高斯模糊的,然後根據圖片中每個像素的深度值在兩張圖片之間差值,就可以達到景深的效果了。下面附上景深效果代碼:

shader部分:
[csharp] view plain copy
  1. Shader "Custom/DepthOfField" {  
  2.   
  3.     Properties{  
  4.         _MainTex("Base (RGB)", 2D) = "white" {}  
  5.         _BlurTex("Blur", 2D) = "white"{}  
  6.     }  
  7.   
  8.     CGINCLUDE  
  9.     #include "UnityCG.cginc"  
  10.   
  11.     struct v2f_blur  
  12.     {  
  13.         float4 pos : SV_POSITION;  
  14.         float2 uv  : TEXCOORD0;  
  15.         float4 uv01 : TEXCOORD1;  
  16.         float4 uv23 : TEXCOORD2;  
  17.         float4 uv45 : TEXCOORD3;  
  18.     };  
  19.   
  20.     struct v2f_dof  
  21.     {  
  22.         float4 pos : SV_POSITION;  
  23.         float2 uv  : TEXCOORD0;  
  24.         float2 uv1 : TEXCOORD1;  
  25.     };  
  26.   
  27.     sampler2D _MainTex;  
  28.     float4 _MainTex_TexelSize;  
  29.     sampler2D _BlurTex;  
  30.     sampler2D_float _CameraDepthTexture;  
  31.     float4 _offsets;  
  32.     float _focalDistance;  
  33.     float _nearBlurScale;  
  34.     float _farBlurScale;  
  35.   
  36.     //高斯模糊 vert shader(上一篇文章有詳細註釋)  
  37.     v2f_blur vert_blur(appdata_img v)  
  38.     {  
  39.         v2f_blur o;  
  40.         _offsets *= _MainTex_TexelSize.xyxy;  
  41.         o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  42.         o.uv = v.texcoord.xy;  
  43.   
  44.         o.uv01 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1);  
  45.         o.uv23 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 2.0;  
  46.         o.uv45 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 3.0;  
  47.   
  48.         return o;  
  49.     }  
  50.   
  51.     //高斯模糊 pixel shader(上一篇文章有詳細註釋)  
  52.     fixed4 frag_blur(v2f_blur i) : SV_Target  
  53.     {  
  54.         fixed4 color = fixed4(0,0,0,0);  
  55.         color += 0.40 * tex2D(_MainTex, i.uv);  
  56.         color += 0.15 * tex2D(_MainTex, i.uv01.xy);  
  57.         color += 0.15 * tex2D(_MainTex, i.uv01.zw);  
  58.         color += 0.10 * tex2D(_MainTex, i.uv23.xy);  
  59.         color += 0.10 * tex2D(_MainTex, i.uv23.zw);  
  60.         color += 0.05 * tex2D(_MainTex, i.uv45.xy);  
  61.         color += 0.05 * tex2D(_MainTex, i.uv45.zw);  
  62.         return color;  
  63.     }  
  64.   
  65.     //景深效果 vertex shader  
  66.     v2f_dof vert_dof(appdata_img v)  
  67.     {  
  68.         v2f_dof o;  
  69.         //mvp矩陣變換  
  70.         o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  71.         //uv座標傳遞  
  72.         o.uv.xy = v.texcoord.xy;  
  73.         o.uv1.xy = o.uv.xy;  
  74.         //dx中紋理從左上角爲初始座標,需要反向  
  75.         #if UNITY_UV_STARTS_AT_TOP  
  76.         if (_MainTex_TexelSize.y < 0)  
  77.             o.uv.y = 1 - o.uv.y;  
  78.         #endif    
  79.         return o;  
  80.     }  
  81.   
  82.     fixed4 frag_dof(v2f_dof i) : SV_Target  
  83.     {  
  84.         //取原始清晰圖片進行uv採樣  
  85.         fixed4 ori = tex2D(_MainTex, i.uv1);  
  86.         //取模糊普片進行uv採樣  
  87.         fixed4 blur = tex2D(_BlurTex, i.uv);  
  88.         //取當位置對應的深度值  
  89.         float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);  
  90.         //將深度值轉化到01線性空間  
  91.         depth = Linear01Depth(depth);  
  92.           
  93.         //如果depth小於焦點的物體,那麼使用原始清晰圖像,否則使用模糊的圖像與清晰圖像的差值,通過差值避免模糊和清晰之間明顯的邊界,結果爲遠景模糊效果  
  94.         fixed4 final = (depth <= _focalDistance) ? ori : lerp(ori, blur, clamp((depth - _focalDistance) * _farBlurScale, 0, 1));  
  95.         //上面的結果,再進行一次計算,如果depth大於焦點的物體,使用上面的結果和模糊圖像差值,得到近景模糊效果  
  96.         final = (depth > _focalDistance) ? final : lerp(ori, blur, clamp((_focalDistance - depth) * _nearBlurScale, 0, 1));  
  97.         //焦點位置是清晰的圖像,兩邊分別用當前像素深度距離焦點的距離進行差值,這樣就達到原理焦點位置模糊的效果  
  98.   
  99.         //上面的?在編譯時會被編譯成if語句,GPU並不擅長分支計算,而且如果有分支,兩個分支都要跑。這裏給了一個更優化一些的計算方式,不過語法比較晦澀  
  100.         //float focalTest = clamp(sign(depth - _focalDistance),0,1);  
  101.         //fixed4 final = (1 - focalTest) * ori + focalTest * lerp(ori, blur, clamp((depth - _focalDistance) * _farBlurScale, 0, 1));  
  102.         //final = (focalTest)* final + (1 - focalTest) * lerp(ori, blur, clamp((_focalDistance - depth) * _nearBlurScale, 0, 1));  
  103.         return final;  
  104.     }  
  105.   
  106.     ENDCG  
  107.   
  108.     SubShader  
  109.     {  
  110.         //pass 0: 高斯模糊  
  111.         Pass  
  112.         {  
  113.             ZTest Off  
  114.             Cull Off  
  115.             ZWrite Off  
  116.             Fog{ Mode Off }  
  117.   
  118.             CGPROGRAM  
  119.             #pragma vertex vert_blur  
  120.             #pragma fragment frag_blur  
  121.             ENDCG  
  122.         }  
  123.   
  124.         //pass 1: 景深效果  
  125.         Pass  
  126.         {  
  127.   
  128.             ZTest Off  
  129.             Cull Off  
  130.             ZWrite Off  
  131.             Fog{ Mode Off }  
  132.             ColorMask RGBA  
  133.   
  134.             CGPROGRAM  
  135.             #pragma vertex vert_dof  
  136.             #pragma fragment frag_dof  
  137.             ENDCG  
  138.         }  
  139.   
  140.     }  
  141. }  

C#部分:
[csharp] view plain copy
  1. using UnityEngine;  
  2. using System.Collections;  
  3.   
  4. [ExecuteInEditMode]  
  5. public class DepthOfFiled : PostEffectBase {  
  6.   
  7.     [Range(0.0f, 100.0f)]  
  8.     public float focalDistance = 10.0f;  
  9.     [Range(0.0f, 100.0f)]  
  10.     public float nearBlurScale = 0.0f;  
  11.     [Range(0.0f, 1000.0f)]  
  12.     public float farBlurScale = 50.0f;  
  13.     //分辨率  
  14.     public int downSample = 1;  
  15.     //採樣率  
  16.     public int samplerScale = 1;  
  17.   
  18.     private Camera _mainCam = null;  
  19.     public Camera MainCam  
  20.     {  
  21.         get  
  22.         {  
  23.             if (_mainCam == null)  
  24.                 _mainCam = GetComponent<Camera>();  
  25.             return _mainCam;  
  26.         }  
  27.     }  
  28.   
  29.     void OnEnable()  
  30.     {  
  31.         //maincam的depthTextureMode是通過位運算開啓與關閉的  
  32.         MainCam.depthTextureMode |= DepthTextureMode.Depth;  
  33.     }  
  34.   
  35.     void OnDisable()  
  36.     {  
  37.         MainCam.depthTextureMode &= ~DepthTextureMode.Depth;  
  38.     }  
  39.   
  40.     void OnRenderImage(RenderTexture source, RenderTexture destination)  
  41.     {  
  42.         if (_Material)  
  43.         {  
  44.             //首先將我們設置的焦點限制在遠近裁剪面之間  
  45.             Mathf.Clamp(focalDistance, MainCam.nearClipPlane, MainCam.farClipPlane);  
  46.   
  47.             //申請兩塊RT,並且分辨率按照downSameple降低  
  48.             RenderTexture temp1 = RenderTexture.GetTemporary(source.width >> downSample, source.height >> downSample, 0, source.format);  
  49.             RenderTexture temp2 = RenderTexture.GetTemporary(source.width >> downSample, source.height >> downSample, 0, source.format);  
  50.   
  51.             //直接將場景圖拷貝到低分辨率的RT上達到降分辨率的效果  
  52.             Graphics.Blit(source, temp1);  
  53.   
  54.             //高斯模糊,兩次模糊,橫向縱向,使用pass0進行高斯模糊  
  55.             _Material.SetVector("_offsets"new Vector4(0, samplerScale, 0, 0));  
  56.             Graphics.Blit(temp1, temp2, _Material, 0);  
  57.             _Material.SetVector("_offsets"new Vector4(samplerScale, 0, 0, 0));  
  58.             Graphics.Blit(temp2, temp1, _Material, 0);  
  59.   
  60.             //景深操作,景深需要兩的模糊效果圖我們通過_BlurTex變量傳入shader  
  61.             _Material.SetTexture("_BlurTex", temp1);  
  62.             //設置shader的參數,主要是焦點和遠近模糊的權重,權重可以控制插值時使用模糊圖片的權重  
  63.             _Material.SetFloat("_focalDistance", FocalDistance01(focalDistance));  
  64.             _Material.SetFloat("_nearBlurScale", nearBlurScale);  
  65.             _Material.SetFloat("_farBlurScale", farBlurScale);  
  66.   
  67.             //使用pass1進行景深效果計算,清晰場景圖直接從source輸入到shader的_MainTex中  
  68.             Graphics.Blit(source, destination, _Material, 1);  
  69.   
  70.             //釋放申請的RT  
  71.             RenderTexture.ReleaseTemporary(temp1);  
  72.             RenderTexture.ReleaseTemporary(temp2);  
  73.         }  
  74.     }  
  75.   
  76.     //計算設置的焦點被轉換到01空間中的距離,以便shader中通過這個01空間的焦點距離與depth比較  
  77.     private float FocalDistance01(float distance)  
  78.     {  
  79.         return MainCam.WorldToViewportPoint((distance - MainCam.nearClipPlane) * MainCam.transform.forward + MainCam.transform.position).z / (MainCam.farClipPlane - MainCam.nearClipPlane);  
  80.     }  
  81.   
  82.   
  83. }  

由於上面的原理&代碼的註釋已經比較清楚,這裏不多介紹。景深效果是一個複合效果,其中的模糊效果前面的文章也有介紹,這篇文章的重點也就是通過DepthTexture來混合清晰和模糊的圖像,來達到我們想要的“重點”清晰,“陪襯”模糊的效果。

大部分的景深效果是前景清晰,遠景模糊,這也是景深的標準用法,不過有時候也有需要近景模糊,遠景清晰的效果,或者前後都模糊,中間焦點位置清晰,在實現上我們通過像素點深度到達焦點的距離作爲參數,在清晰和模糊圖像之間插值,先計算遠景的,結果與模糊圖片再進行插值,得到最終的效果。

四.效果展示


在MainCamera上掛上DepthOfField腳本,將DepthOfFileld.shader賦給shader槽,即可看見景深的效果。

首先我們看一下清晰的場景圖:

開啓遠景模糊的景深效果:

遠近同時模糊的效果,只有焦點距離的對象清晰:


景深效果雖好,還是需要慎用,畢竟高斯模糊和深度圖這兩個東東都是耗費性能的大戶。最近突然有了個腦洞,正好研究Command Buffer,實現了一版假的景深效果,其實叫背景虛化更加貼切一些,只是將需要突出的物體通過Command Buffer設置在Image Effects之後渲染(或者可以另外創建一個新的攝像機渲染需要突出的物體),其他物體通過主相機渲染,在主相機上直接增加一個高斯模糊的後處理。具體實現可以參照Command Buffer的使用這篇文章。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章