Unity Shader-後處理:景深
阿新 • • 發佈:2018-12-22
一.簡介
景深一直是我最喜歡的效果之一,最早接觸CE3的時候,發現CE引擎預設就支援景深的效果,當時感覺這個效果特別酷炫,如今投身於Unity的懷抱中,準備用Unity實現以下傳說中的景深效果。
所謂景深,是攝影的一個專業術語:在聚焦完成後,在焦點前後的範圍內都能形成清晰的像,這一前一後的距離範圍,便叫做景深,也是被攝物體能清晰成像的空間深度。在景深範圍內景物影像的清晰度並不完全一致,其中焦點上的清晰度是最高的,其餘的影像清晰度隨著它與焦點的距離成正比例下降。先附上一張正常的照片和使用景深控制的照片:
通過左右兩張照片的對比,我們很容易發現,通過景深處理的照片,我們可以很容易地抓住照片的重點部分。這也就是景深最大的用處,能夠突出主題,並且可以使畫面更有層次感。
在攝影技術中的景深,是通過調整相機的焦距,光圈來控制景深的,這裡就不多說了。而我們的遊戲中要想出現這種效果,就需要下一番功夫了。首先拆分一下影象的效果,影象中主要分為兩部分,後面的模糊背景和前面清晰的“主題”部分。後面的背景模糊我們可以通過前面的兩篇文章Unity Shader-後處理:高斯模糊,Unity Shader後處理-均值模糊來實現,而前景部分就是一張清晰的場景圖,最後通過一定的權值將兩者混合,離攝像機(準確地說是焦距)越遠的部分,模糊圖片的權重越高,離攝像機越近的部分,清晰圖片的權重越高。那麼問題來了,我們怎麼知道哪個部分離攝像機更近呢?
二.Camera Depth Texture
- SubShader
- {
- Tags { "RenderType"="Opaque" }
- Pass
- {
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #include "UnityCG.cginc"
- struct v2f
- {
- float4 pos : POSITION;
- #ifdef UNITY_MIGHT_NOT_HAVE_DEPTH_TEXTURE
- float2 depth : TEXCOORD0;
- #endif
- };
- v2f vert( appdata_base v )
- {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- UNITY_TRANSFER_DEPTH(o.depth);
- return o;
- }
- fixed4 frag(v2f i) : COLOR {
- UNITY_OUTPUT_DEPTH(i.depth);
- }
- ENDCG
- }
- }
- clip( texcol.a*_Color.a - _Cutoff );
- // Depth render texture helpers
- #if defined(UNITY_MIGHT_NOT_HAVE_DEPTH_TEXTURE)
- #define UNITY_TRANSFER_DEPTH(oo) oo = o.pos.zw
- #define UNITY_OUTPUT_DEPTH(i) return i.x/i.y
- #else
- #define UNITY_TRANSFER_DEPTH(oo)
- #define UNITY_OUTPUT_DEPTH(i) return 0
- #endif
- 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.
關於深度紋理,深度法線紋理,運動方向紋理Unity官方文件有很好的介紹,我們就不多說了,下面我們看一下怎麼在Unity中開啟深度的渲染。通過Camera.DepthTextureMode這個變數我們就可以控制是否開啟深度的渲染,預設這個值是None,我們可以將其設為None,Depth,DepthNormals三種類型。只要開啟了Depth模式,我們就可以在shader中通過_CameraDepthTexture來獲得螢幕深度的紋理了。Unity官方文件中也有詳細介紹。下面我們通過一個後處理來實現一個最簡單的輸出螢幕深度的效果: C#指令碼
- using UnityEngine;
- using System.Collections;
- [ExecuteInEditMode]
- publicclass DepthTextureTest : PostEffectBase
- {
- void OnEnable()
- {
- GetComponent<Camera>().depthTextureMode |= DepthTextureMode.Depth;
- }
- void OnDisable()
- {
- GetComponent<Camera>().depthTextureMode &= ~DepthTextureMode.Depth;
- }
- void OnRenderImage(RenderTexture source, RenderTexture destination)
- {
- if (_Material)
- {
- Graphics.Blit(source, destination, _Material);
- }
- }
- }
- Shader "Custom/DepthTest" {
- CGINCLUDE
- #include "UnityCG.cginc"
- //仍然要宣告一下_CameraDepthTexture這個變數,雖然Unity這個變數是unity內部賦值
- sampler2D _CameraDepthTexture;
- sampler2D _MainTex;
- float4 _MainTex_TexelSize;
- struct v2f
- {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- };
- v2f vert(appdata_img v)
- {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- o.uv.xy = v.texcoord.xy;
- return o;
- }
- fixed4 frag(v2f i) : SV_Target
- {
- //直接根據UV座標取該點的深度值
- float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, 1 - i.uv);
- //將深度值變為線性01空間
- depth = Linear01Depth(depth);
- return float4(depth, depth, depth, 1);
- }
- ENDCG
- SubShader
- {
- Pass
- {
- ZTest Off
- Cull Off
- ZWrite Off
- Fog{ Mode Off }
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- ENDCG
- }
- }
- }
找一個場景,將該指令碼掛在攝像機並賦予材質。(注:PostEffectBase類為後處理基類,在之前的文章中有詳細實現,此處不予貼出),找到一個場景,我們測試一下: 原始場景效果:
開啟輸出深度後處理效果:
恩,場景圖變成了一張黑白影象,越遠的地方越亮,越近的地方越暗,也就是我們shader中所寫的,直接按照深度值來輸出了一幅圖片。不過注意,這張圖中,我把攝像機的遠裁剪面調整到了50這一比較小的距離,這樣,圖中的遠近資訊顯示得更加明顯,而如果攝像機的遠裁剪面距離很大,那麼這張圖的輸出就會整體偏黑,因為離我們較近的物體距離佔遠裁剪面的距離太小了,幾乎為0,所以就是黑的,如下圖所示,當遠裁剪面改為1000時深度圖,僅有窗戶的位置能看到白色:
關於CameraDepthTexture,在Unity4中CameraDepthTexture仍然是通過上面我們說的shader替換技術實現的,所以,一旦我們開啟深度渲染,會導致DrawCall翻倍!而在Unity5中,這個CameraDepthTexture與Shadow Caster使用的是一套DepthTexture,通過帶有Shadow Caster的物件才會被渲染到深度快取中。關於Unity5和Unity4中深度快取的渲染,這篇文章介紹得很詳細,可以進行參考。
三.景深效果實現
終於到了這篇文章的主題了,我們通過shader實現一個景深的效果。思路上面已經說過了,通過兩張圖片,一張清晰的,一張經過高斯模糊的,然後根據圖片中每個畫素的深度值在兩張圖片之間差值,就可以達到景深的效果了。下面附上景深效果程式碼: shader部分:- Shader "Custom/DepthOfField" {
- Properties{
- _MainTex("Base (RGB)", 2D) = "white" {}
- _BlurTex("Blur", 2D) = "white"{}
- }
- CGINCLUDE
- #include "UnityCG.cginc"
- struct v2f_blur
- {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- float4 uv01 : TEXCOORD1;
- float4 uv23 : TEXCOORD2;
- float4 uv45 : TEXCOORD3;
- };
- struct v2f_dof
- {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- float2 uv1 : TEXCOORD1;
- };
- sampler2D _MainTex;
- float4 _MainTex_TexelSize;
- sampler2D _BlurTex;
- sampler2D_float _CameraDepthTexture;
- float4 _offsets;
- float _focalDistance;
- float _nearBlurScale;
- float _farBlurScale;
- //高斯模糊 vert shader(上一篇文章有詳細註釋)
- v2f_blur vert_blur(appdata_img v)
- {
- v2f_blur o;
- _offsets *= _MainTex_TexelSize.xyxy;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- o.uv = v.texcoord.xy;
- o.uv01 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1);
- o.uv23 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 2.0;
- o.uv45 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 3.0;
- return o;
- }
- //高斯模糊 pixel shader(上一篇文章有詳細註釋)
- fixed4 frag_blur(v2f_blur i) : SV_Target
- {
- fixed4 color = fixed4(0,0,0,0);
- color += 0.40 * tex2D(_MainTex, i.uv);
- color += 0.15 * tex2D(_MainTex, i.uv01.xy);
- color += 0.15 * tex2D(_MainTex, i.uv01.zw);
- color += 0.10 * tex2D(_MainTex, i.uv23.xy);
- color += 0.10 * tex2D(_MainTex, i.uv23.zw);
- color += 0.05 * tex2D(_MainTex, i.uv45.xy);
- color += 0.05 * tex2D(_MainTex, i.uv45.zw);
- return color;
- }
- //景深效果 vertex shader
- v2f_dof vert_dof(appdata_img v)
- {
- v2f_dof o;
- //mvp矩陣變換
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- //uv座標傳遞
- o.uv.xy = v.texcoord.xy;
- o.uv1.xy = o.uv.xy;
- //dx中紋理從左上角為初始座標,需要反向
- #if UNITY_UV_STARTS_AT_TOP
- if (_MainTex_TexelSize.y < 0)
- o.uv.y = 1 - o.uv.y;
- #endif
- return o;
- }
- fixed4 frag_dof(v2f_dof i) : SV_Target
- {
- //取原始清晰圖片進行uv取樣
- fixed4 ori = tex2D(_MainTex, i.uv1);
- //取模糊普片進行uv取樣
- fixed4 blur = tex2D(_BlurTex, i.uv);
- //取當位置對應的深度值
- float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
- //將深度值轉化到01線性空間
- depth = Linear01Depth(depth);
- //如果depth小於焦點的物體,那麼使用原始清晰影象,否則使用模糊的影象與清晰影象的差值,通過差值避免模糊和清晰之間明顯的邊界,結果為遠景模糊效果
- fixed4 final = (depth <= _focalDistance) ? ori : lerp(ori, blur, clamp((depth - _focalDistance) * _farBlurScale, 0, 1));
- //上面的結果,再進行一次計算,如果depth大於焦點的物體,使用上面的結果和模糊影象差值,得到近景模糊效果
- final = (depth > _focalDistance) ? final : lerp(ori, blur, clamp((_focalDistance - depth) * _nearBlurScale, 0, 1));
- //焦點位置是清晰的影象,兩邊分別用當前畫素深度距離焦點的距離進行差值,這樣就達到原理焦點位置模糊的效果
- //上面的?在編譯時會被編譯成if語句,GPU並不擅長分支計算,而且如果有分支,兩個分支都要跑。這裡給了一個更優化一些的計算方式,不過語法比較晦澀
- //float focalTest = clamp(sign(depth - _focalDistance),0,1);
- //fixed4 final = (1 - focalTest) * ori + focalTest * lerp(ori, blur, clamp((depth - _focalDistance) * _farBlurScale, 0, 1));
- //final = (focalTest)* final + (1 - focalTest) * lerp(ori, blur, clamp((_focalDistance - depth) * _nearBlurScale, 0, 1));
- return final;
- }
- ENDCG
- SubShader
- {
- //pass 0: 高斯模糊
- Pass
- {
- ZTest Off
- Cull Off
- ZWrite Off
- Fog{ Mode Off }
- CGPROGRAM
- #pragma vertex vert_blur
- #pragma fragment frag_blur
- ENDCG
- }
- //pass 1: 景深效果
- Pass
- {
- ZTest Off
- Cull Off
- ZWrite Off
- Fog{ Mode Off }
- ColorMask RGBA
- CGPROGRAM
- #pragma vertex vert_dof
- #pragma fragment frag_dof
- ENDCG
- }
- }
- }
- using UnityEngine;
- using System.Collections;
- [ExecuteInEditMode]
- publicclass DepthOfFiled : PostEffectBase {
- [Range(0.0f, 100.0f)]
- publicfloat focalDistance = 10.0f;
- [Range(0.0f, 100.0f)]
- publicfloat nearBlurScale = 0.0f;
- [Range(0.0f, 1000.0f)]
- publicfloat farBlurScale = 50.0f;
- //解析度
- publicint downSample = 1;
- //取樣率
- publicint samplerScale = 1;
- private Camera _mainCam = null;
- public Camera MainCam
- {
- get
- {
- if (_mainCam == null)
- _mainCam = GetComponent<Camera>();
- return _mainCam;
- }
- }
- void OnEnable()
- {
- //maincam的depthTextureMode是通過位運算開啟與關閉的
- MainCam.depthTextureMode |= DepthTextureMode.Depth;
- }
- void OnDisable()
- {
- MainCam.depthTextureMode &= ~DepthTextureMode.Depth;
- }
- void OnRenderImage(RenderTexture source, RenderTexture destination)
- {
- if (_Material)
- {
- //首先將我們設定的焦點限制在遠近裁剪面之間
- Mathf.Clamp(focalDistance, MainCam.nearClipPlane, MainCam.farClipPlane);
- //申請兩塊RT,並且解析度按照downSameple降低
- RenderTexture temp1 = RenderTexture.GetTemporary(source.width >> downSample, source.height >> downSample, 0, source.format);
- RenderTexture temp2 = RenderTexture.GetTemporary(source.width >> downSample, source.height >> downSample, 0, source.format);
- //直接將場景圖拷貝到低解析度的RT上達到降解析度的效果
- Graphics.Blit(source, temp1);
- //高斯模糊,兩次模糊,橫向縱向,使用pass0進行高斯模糊
- _Material.SetVector("_offsets", new Vector4(0, samplerScale, 0, 0));
- Graphics.Blit(temp1, temp2, _Material, 0);
- _Material.SetVector("_offsets", new Vector4(samplerScale, 0, 0, 0));
- Graphics.Blit(temp2, temp1, _Material, 0);
- //景深操作,景深需要兩的模糊效果圖我們通過_BlurTex變數傳入shader
- _Material.SetTexture("_BlurTex", temp1);
- //設定shader的引數,主要是焦點和遠近模糊的權重,權重可以控制插值時使用模糊圖片的權重
- _Material.SetFloat("_focalDistance", FocalDistance01(focalDistance));
- _Material.SetFloat("_nearBlurScale", nearBlurScale);
- _Material.SetFloat("_farBlurScale", farBlurScale);
- //使用pass1進行景深效果計算,清晰場景圖直接從source輸入到shader的_MainTex中
- Graphics.Blit(source, destination, _Material, 1);
- //釋放申請的RT
- RenderTexture.ReleaseTemporary(temp1);
- RenderTexture.ReleaseTemporary(temp2);
- }
- }
- //計算設定的焦點被轉換到01空間中的距離,以便shader中通過這個01空間的焦點距離與depth比較
- privatefloat FocalDistance01(float distance)
- {
- return MainCam.WorldToViewportPoint((distance - MainCam.nearClipPlane) * MainCam.transform.forward + MainCam.transform.position).z / (MainCam.farClipPlane - MainCam.nearClipPlane);
- }
- }
四.效果展示
在MainCamera上掛上DepthOfField指令碼,將DepthOfFileld.shader賦給shader槽,即可看見景深的效果。 首先我們看一下清晰的場景圖:開啟遠景模糊的景深效果:
遠近同時模糊的效果,只有焦點距離的物件清晰:
原文地址:http://blog.csdn.net/puppet_master/article/details/52819874