1. 程式人生 > >Unity Shader-後處理:景深

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:
  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.         v2f vert( appdata_base v )  
  18.         {  
  19.             v2f o;  
  20.             o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  21.             UNITY_TRANSFER_DEPTH(o.depth);  
  22.             return o;  
  23.         }  
  24.         fixed4 frag(v2f i) : COLOR {  
  25.             UNITY_OUTPUT_DEPTH(i.depth);  
  26.         }  
  27.         ENDCG  
  28.     }  
  29. }  
我們看到,當物體的渲染Tag為Opaque也就是不透明的時候,會寫入深度紋理。而這個檔案中其他的幾個subshader也分別對針對不同型別的type,比如RenderType為TransparentCutout的subshader,就增加了一句下面的判斷,去掉了所有應該透明的地方:
  1. clip( texcol.a*_Color.a - _Cutoff );  
而且這個shader中沒有出現RnderType為Transparent型別的,因為透明的物體不會寫入我們的深度貼圖,也就是說我們開啟了alpha blend型別的物件是不會寫入深度的。 上面的程式碼中有幾個巨集定義,我們可以從UnityCG.cginc檔案中找到這幾個巨集定義的實現:
  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#指令碼
  1. using UnityEngine;  
  2. using System.Collections;  
  3. [ExecuteInEditMode]  
  4. publicclass DepthTextureTest : PostEffectBase  
  5. {  
  6.     void OnEnable()  
  7.     {  
  8.         GetComponent<Camera>().depthTextureMode |= DepthTextureMode.Depth;  
  9.     }  
  10.     void OnDisable()  
  11.     {  
  12.         GetComponent<Camera>().depthTextureMode &= ~DepthTextureMode.Depth;  
  13.     }  
  14.     void OnRenderImage(RenderTexture source, RenderTexture destination)  
  15.     {  
  16.         if (_Material)  
  17.         {  
  18.             Graphics.Blit(source, destination, _Material);  
  19.         }  
  20.     }  
  21. }  
shader部分:
  1. Shader "Custom/DepthTest" {  
  2.     CGINCLUDE  
  3.     #include "UnityCG.cginc"
  4.     //仍然要宣告一下_CameraDepthTexture這個變數,雖然Unity這個變數是unity內部賦值
  5.     sampler2D _CameraDepthTexture;  
  6.     sampler2D _MainTex;  
  7.     float4    _MainTex_TexelSize;  
  8.     struct v2f  
  9.     {  
  10.         float4 pos : SV_POSITION;  
  11.         float2 uv  : TEXCOORD0;  
  12.     };  
  13.     v2f vert(appdata_img v)  
  14.     {  
  15.         v2f o;  
  16.         o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  17.         o.uv.xy = v.texcoord.xy;  
  18.         return o;  
  19.     }  
  20.     fixed4 frag(v2f i) : SV_Target  
  21.     {  
  22.         //直接根據UV座標取該點的深度值
  23.         float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, 1 - i.uv);  
  24.         //將深度值變為線性01空間
  25.         depth = Linear01Depth(depth);  
  26.         return float4(depth, depth, depth, 1);  
  27.     }  
  28.     ENDCG  
  29.     SubShader  
  30.     {  
  31.         Pass  
  32.         {  
  33.             ZTest Off  
  34.             Cull Off  
  35.             ZWrite Off  
  36.             Fog{ Mode Off }  
  37.             CGPROGRAM  
  38.             #pragma vertex vert
  39.             #pragma fragment frag
  40.             ENDCG  
  41.         }  
  42.     }  
  43. }  

 找一個場景,將該指令碼掛在攝像機並賦予材質。(注:PostEffectBase類為後處理基類,在之前的文章中有詳細實現,此處不予貼出),找到一個場景,我們測試一下: 原始場景效果:
開啟輸出深度後處理效果:
恩,場景圖變成了一張黑白影象,越遠的地方越亮,越近的地方越暗,也就是我們shader中所寫的,直接按照深度值來輸出了一幅圖片。不過注意,這張圖中,我把攝像機的遠裁剪面調整到了50這一比較小的距離,這樣,圖中的遠近資訊顯示得更加明顯,而如果攝像機的遠裁剪面距離很大,那麼這張圖的輸出就會整體偏黑,因為離我們較近的物體距離佔遠裁剪面的距離太小了,幾乎為0,所以就是黑的,如下圖所示,當遠裁剪面改為1000時深度圖,僅有窗戶的位置能看到白色:
關於CameraDepthTexture,在Unity4中CameraDepthTexture仍然是通過上面我們說的shader替換技術實現的,所以,一旦我們開啟深度渲染,會導致DrawCall翻倍!而在Unity5中,這個CameraDepthTexture與Shadow Caster使用的是一套DepthTexture,通過帶有Shadow Caster的物件才會被渲染到深度快取中。關於Unity5和Unity4中深度快取的渲染,這篇文章介紹得很詳細,可以進行參考。

三.景深效果實現

終於到了這篇文章的主題了,我們通過shader實現一個景深的效果。思路上面已經說過了,通過兩張圖片,一張清晰的,一張經過高斯模糊的,然後根據圖片中每個畫素的深度值在兩張圖片之間差值,就可以達到景深的效果了。下面附上景深效果程式碼: shader部分:
  1. Shader "Custom/DepthOfField" {  
  2.     Properties{  
  3.         _MainTex("Base (RGB)", 2D) = "white" {}  
  4.         _BlurTex("Blur", 2D) = "white"{}  
  5.     }  
  6.     CGINCLUDE  
  7.     #include "UnityCG.cginc"
  8.     struct v2f_blur  
  9.     {  
  10.         float4 pos : SV_POSITION;  
  11.         float2 uv  : TEXCOORD0;  
  12.         float4 uv01 : TEXCOORD1;  
  13.         float4 uv23 : TEXCOORD2;  
  14.         float4 uv45 : TEXCOORD3;  
  15.     };  
  16.     struct v2f_dof  
  17.     {  
  18.         float4 pos : SV_POSITION;  
  19.         float2 uv  : TEXCOORD0;  
  20.         float2 uv1 : TEXCOORD1;  
  21.     };  
  22.     sampler2D _MainTex;  
  23.     float4 _MainTex_TexelSize;  
  24.     sampler2D _BlurTex;  
  25.     sampler2D_float _CameraDepthTexture;  
  26.     float4 _offsets;  
  27.     float _focalDistance;  
  28.     float _nearBlurScale;  
  29.     float _farBlurScale;  
  30.     //高斯模糊 vert shader(上一篇文章有詳細註釋)
  31.     v2f_blur vert_blur(appdata_img v)  
  32.     {  
  33.         v2f_blur o;  
  34.         _offsets *= _MainTex_TexelSize.xyxy;  
  35.         o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  36.         o.uv = v.texcoord.xy;  
  37.         o.uv01 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1);  
  38.         o.uv23 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 2.0;  
  39.         o.uv45 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 3.0;  
  40.         return o;  
  41.     }  
  42.     //高斯模糊 pixel shader(上一篇文章有詳細註釋)
  43.     fixed4 frag_blur(v2f_blur i) : SV_Target  
  44.     {  
  45.         fixed4 color = fixed4(0,0,0,0);  
  46.         color += 0.40 * tex2D(_MainTex, i.uv);  
  47.         color += 0.15 * tex2D(_MainTex, i.uv01.xy);  
  48.         color += 0.15 * tex2D(_MainTex, i.uv01.zw);  
  49.         color += 0.10 * tex2D(_MainTex, i.uv23.xy);  
  50.         color += 0.10 * tex2D(_MainTex, i.uv23.zw);  
  51.         color += 0.05 * tex2D(_MainTex, i.uv45.xy);  
  52.         color += 0.05 * tex2D(_MainTex, i.uv45.zw);  
  53.         return color;  
  54.     }  
  55.     //景深效果 vertex shader
  56.     v2f_dof vert_dof(appdata_img v)  
  57.     {  
  58.         v2f_dof o;  
  59.         //mvp矩陣變換
  60.         o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  
  61.         //uv座標傳遞
  62.         o.uv.xy = v.texcoord.xy;  
  63.         o.uv1.xy = o.uv.xy;  
  64.         //dx中紋理從左上角為初始座標,需要反向
  65.         #if UNITY_UV_STARTS_AT_TOP
  66.         if (_MainTex_TexelSize.y < 0)  
  67.             o.uv.y = 1 - o.uv.y;  
  68.         #endif  
  69.         return o;  
  70.     }  
  71.     fixed4 frag_dof(v2f_dof i) : SV_Target  
  72.     {  
  73.         //取原始清晰圖片進行uv取樣
  74.         fixed4 ori = tex2D(_MainTex, i.uv1);  
  75.         //取模糊普片進行uv取樣
  76.         fixed4 blur = tex2D(_BlurTex, i.uv);  
  77.         //取當位置對應的深度值
  78.         float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);  
  79.         //將深度值轉化到01線性空間
  80.         depth = Linear01Depth(depth);  
  81.         //如果depth小於焦點的物體,那麼使用原始清晰影象,否則使用模糊的影象與清晰影象的差值,通過差值避免模糊和清晰之間明顯的邊界,結果為遠景模糊效果
  82.         fixed4 final = (depth <= _focalDistance) ? ori : lerp(ori, blur, clamp((depth - _focalDistance) * _farBlurScale, 0, 1));  
  83.         //上面的結果,再進行一次計算,如果depth大於焦點的物體,使用上面的結果和模糊影象差值,得到近景模糊效果
  84.         final = (depth > _focalDistance) ? final : lerp(ori, blur, clamp((_focalDistance - depth) * _nearBlurScale, 0, 1));  
  85.         //焦點位置是清晰的影象,兩邊分別用當前畫素深度距離焦點的距離進行差值,這樣就達到原理焦點位置模糊的效果
  86.         //上面的?在編譯時會被編譯成if語句,GPU並不擅長分支計算,而且如果有分支,兩個分支都要跑。這裡給了一個更優化一些的計算方式,不過語法比較晦澀
  87.         //float focalTest = clamp(sign(depth - _focalDistance),0,1);
  88.         //fixed4 final = (1 - focalTest) * ori + focalTest * lerp(ori, blur, clamp((depth - _focalDistance) * _farBlurScale, 0, 1));
  89.         //final = (focalTest)* final + (1 - focalTest) * lerp(ori, blur, clamp((_focalDistance - depth) * _nearBlurScale, 0, 1));
  90.         return final;  
  91.     }  
  92.     ENDCG  
  93.     SubShader  
  94.     {  
  95.         //pass 0: 高斯模糊
  96.         Pass  
  97.         {  
  98.             ZTest Off  
  99.             Cull Off  
  100.             ZWrite Off  
  101.             Fog{ Mode Off }  
  102.             CGPROGRAM  
  103.             #pragma vertex vert_blur
  104.             #pragma fragment frag_blur
  105.             ENDCG  
  106.         }  
  107.         //pass 1: 景深效果
  108.         Pass  
  109.         {  
  110.             ZTest Off  
  111.             Cull Off  
  112.             ZWrite Off  
  113.             Fog{ Mode Off }  
  114.             ColorMask RGBA  
  115.             CGPROGRAM  
  116.             #pragma vertex vert_dof
  117.             #pragma fragment frag_dof
  118.             ENDCG  
  119.         }  
  120.     }  
  121. }  
C#部分:
  1. using UnityEngine;  
  2. using System.Collections;  
  3. [ExecuteInEditMode]  
  4. publicclass DepthOfFiled : PostEffectBase {  
  5.     [Range(0.0f, 100.0f)]  
  6.     publicfloat focalDistance = 10.0f;  
  7.     [Range(0.0f, 100.0f)]  
  8.     publicfloat nearBlurScale = 0.0f;  
  9.     [Range(0.0f, 1000.0f)]  
  10.     publicfloat farBlurScale = 50.0f;  
  11.     //解析度
  12.     publicint downSample = 1;  
  13.     //取樣率
  14.     publicint samplerScale = 1;  
  15.     private Camera _mainCam = null;  
  16.     public Camera MainCam  
  17.     {  
  18.         get
  19.         {  
  20.             if (_mainCam == null)  
  21.                 _mainCam = GetComponent<Camera>();  
  22.             return _mainCam;  
  23.         }  
  24.     }  
  25.     void OnEnable()  
  26.     {  
  27.         //maincam的depthTextureMode是通過位運算開啟與關閉的
  28.         MainCam.depthTextureMode |= DepthTextureMode.Depth;  
  29.     }  
  30.     void OnDisable()  
  31.     {  
  32.         MainCam.depthTextureMode &= ~DepthTextureMode.Depth;  
  33.     }  
  34.     void OnRenderImage(RenderTexture source, RenderTexture destination)  
  35.     {  
  36.         if (_Material)  
  37.         {  
  38.             //首先將我們設定的焦點限制在遠近裁剪面之間
  39.             Mathf.Clamp(focalDistance, MainCam.nearClipPlane, MainCam.farClipPlane);  
  40.             //申請兩塊RT,並且解析度按照downSameple降低
  41.             RenderTexture temp1 = RenderTexture.GetTemporary(source.width >> downSample, source.height >> downSample, 0, source.format);  
  42.             RenderTexture temp2 = RenderTexture.GetTemporary(source.width >> downSample, source.height >> downSample, 0, source.format);  
  43.             //直接將場景圖拷貝到低解析度的RT上達到降解析度的效果
  44.             Graphics.Blit(source, temp1);  
  45.             //高斯模糊,兩次模糊,橫向縱向,使用pass0進行高斯模糊
  46.             _Material.SetVector("_offsets"new Vector4(0, samplerScale, 0, 0));  
  47.             Graphics.Blit(temp1, temp2, _Material, 0);  
  48.             _Material.SetVector("_offsets"new Vector4(samplerScale, 0, 0, 0));  
  49.             Graphics.Blit(temp2, temp1, _Material, 0);  
  50.             //景深操作,景深需要兩的模糊效果圖我們通過_BlurTex變數傳入shader
  51.             _Material.SetTexture("_BlurTex", temp1);  
  52.             //設定shader的引數,主要是焦點和遠近模糊的權重,權重可以控制插值時使用模糊圖片的權重
  53.             _Material.SetFloat("_focalDistance", FocalDistance01(focalDistance));  
  54.             _Material.SetFloat("_nearBlurScale", nearBlurScale);  
  55.             _Material.SetFloat("_farBlurScale", farBlurScale);  
  56.             //使用pass1進行景深效果計算,清晰場景圖直接從source輸入到shader的_MainTex中
  57.             Graphics.Blit(source, destination, _Material, 1);  
  58.             //釋放申請的RT
  59.             RenderTexture.ReleaseTemporary(temp1);  
  60.             RenderTexture.ReleaseTemporary(temp2);  
  61.         }  
  62.     }  
  63.     //計算設定的焦點被轉換到01空間中的距離,以便shader中通過這個01空間的焦點距離與depth比較
  64.     privatefloat FocalDistance01(float distance)  
  65.     {  
  66.         return MainCam.WorldToViewportPoint((distance - MainCam.nearClipPlane) * MainCam.transform.forward + MainCam.transform.position).z / (MainCam.farClipPlane - MainCam.nearClipPlane);  
  67.     }  
  68. }  
由於上面的原理&程式碼的註釋已經比較清楚,這裡不多介紹。景深效果是一個複合效果,其中的模糊效果前面的文章也有介紹,這篇文章的重點也就是通過DepthTexture來混合清晰和模糊的影象,來達到我們想要的“重點”清晰,“陪襯”模糊的效果。 大部分的景深效果是前景清晰,遠景模糊,這也是景深的標準用法,不過有時候也有需要近景模糊,遠景清晰的效果,或者前後都模糊,中間焦點位置清晰,在實現上我們通過畫素點深度到達焦點的距離作為引數,在清晰和模糊影象之間插值,先計算遠景的,結果與模糊圖片再進行插值,得到最終的效果。

四.效果展示

在MainCamera上掛上DepthOfField指令碼,將DepthOfFileld.shader賦給shader槽,即可看見景深的效果。 首先我們看一下清晰的場景圖:
開啟遠景模糊的景深效果:
遠近同時模糊的效果,只有焦點距離的物件清晰:

原文地址:http://blog.csdn.net/puppet_master/article/details/52819874