一.簡介
景深一直是我最喜歡的效果之一,最早接觸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:
-
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
-
}
-
}
我們看到,當物體的渲染Tag爲Opaque也就是不透明的時候,會寫入深度紋理。而這個文件中其他的幾個subshader也分別對針對不同類型的type,比如RenderType爲TransparentCutout的subshader,就增加了一句下面的判斷,去掉了所有應該透明的地方:
-
clip( texcol.a*_Color.a - _Cutoff );
而且這個shader中沒有出現RnderType爲Transparent類型的,因爲透明的物體不會寫入我們的深度貼圖,也就是說我們開啓了alpha blend類型的對象是不會寫入深度的。
上面的代碼中有幾個宏定義,我們可以從UnityCG.cginc文件中找到這幾個宏定義的實現:
-
-
#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
結合上面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#腳本
-
using UnityEngine;
-
using System.Collections;
-
-
[ExecuteInEditMode]
-
public class 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部分:
-
Shader "Custom/DepthTest" {
-
-
CGINCLUDE
-
#include "UnityCG.cginc"
-
-
-
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
-
{
-
-
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, 1 - i.uv);
-
-
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;
-
-
-
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;
-
}
-
-
-
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;
-
}
-
-
-
v2f_dof vert_dof(appdata_img v)
-
{
-
v2f_dof o;
-
-
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
-
-
o.uv.xy = v.texcoord.xy;
-
o.uv1.xy = o.uv.xy;
-
-
#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
-
{
-
-
fixed4 ori = tex2D(_MainTex, i.uv1);
-
-
fixed4 blur = tex2D(_BlurTex, i.uv);
-
-
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
-
-
depth = Linear01Depth(depth);
-
-
-
fixed4 final = (depth <= _focalDistance) ? ori : lerp(ori, blur, clamp((depth - _focalDistance) * _farBlurScale, 0, 1));
-
-
final = (depth > _focalDistance) ? final : lerp(ori, blur, clamp((_focalDistance - depth) * _nearBlurScale, 0, 1));
-
-
-
-
-
-
-
return final;
-
}
-
-
ENDCG
-
-
SubShader
-
{
-
-
Pass
-
{
-
ZTest Off
-
Cull Off
-
ZWrite Off
-
Fog{ Mode Off }
-
-
CGPROGRAM
-
#pragma vertex vert_blur
-
#pragma fragment frag_blur
-
ENDCG
-
}
-
-
-
Pass
-
{
-
-
ZTest Off
-
Cull Off
-
ZWrite Off
-
Fog{ Mode Off }
-
ColorMask RGBA
-
-
CGPROGRAM
-
#pragma vertex vert_dof
-
#pragma fragment frag_dof
-
ENDCG
-
}
-
-
}
-
}
C#部分:
-
using UnityEngine;
-
using System.Collections;
-
-
[ExecuteInEditMode]
-
public class DepthOfFiled : PostEffectBase {
-
-
[Range(0.0f, 100.0f)]
-
public float focalDistance = 10.0f;
-
[Range(0.0f, 100.0f)]
-
public float nearBlurScale = 0.0f;
-
[Range(0.0f, 1000.0f)]
-
public float farBlurScale = 50.0f;
-
-
public int downSample = 1;
-
-
public int samplerScale = 1;
-
-
private Camera _mainCam = null;
-
public Camera MainCam
-
{
-
get
-
{
-
if (_mainCam == null)
-
_mainCam = GetComponent<Camera>();
-
return _mainCam;
-
}
-
}
-
-
void OnEnable()
-
{
-
-
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);
-
-
-
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);
-
-
-
Graphics.Blit(source, temp1);
-
-
-
_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);
-
-
-
_Material.SetTexture("_BlurTex", temp1);
-
-
_Material.SetFloat("_focalDistance", FocalDistance01(focalDistance));
-
_Material.SetFloat("_nearBlurScale", nearBlurScale);
-
_Material.SetFloat("_farBlurScale", farBlurScale);
-
-
-
Graphics.Blit(source, destination, _Material, 1);
-
-
-
RenderTexture.ReleaseTemporary(temp1);
-
RenderTexture.ReleaseTemporary(temp2);
-
}
-
}
-
-
-
private float FocalDistance01(float distance)
-
{
-
return MainCam.WorldToViewportPoint((distance - MainCam.nearClipPlane) * MainCam.transform.forward + MainCam.transform.position).z / (MainCam.farClipPlane - MainCam.nearClipPlane);
-
}
-
-
-
}
由於上面的原理&代碼的註釋已經比較清楚,這裏不多介紹。景深效果是一個複合效果,其中的模糊效果前面的文章也有介紹,這篇文章的重點也就是通過DepthTexture來混合清晰和模糊的圖像,來達到我們想要的“重點”清晰,“陪襯”模糊的效果。
大部分的景深效果是前景清晰,遠景模糊,這也是景深的標準用法,不過有時候也有需要近景模糊,遠景清晰的效果,或者前後都模糊,中間焦點位置清晰,在實現上我們通過像素點深度到達焦點的距離作爲參數,在清晰和模糊圖像之間插值,先計算遠景的,結果與模糊圖片再進行插值,得到最終的效果。
四.效果展示
在MainCamera上掛上DepthOfField腳本,將DepthOfFileld.shader賦給shader槽,即可看見景深的效果。
首先我們看一下清晰的場景圖:
開啓遠景模糊的景深效果:
遠近同時模糊的效果,只有焦點距離的對象清晰:
景深效果雖好,還是需要慎用,畢竟高斯模糊和深度圖這兩個東東都是耗費性能的大戶。最近突然有了個腦洞,正好研究Command Buffer,實現了一版假的景深效果,其實叫背景虛化更加貼切一些,只是將需要突出的物體通過Command Buffer設置在Image Effects之後渲染(或者可以另外創建一個新的攝像機渲染需要突出的物體),其他物體通過主相機渲染,在主相機上直接增加一個高斯模糊的後處理。具體實現可以參照
Command
Buffer的使用這篇文章。