最近看了一些關于深度圖及應用的文章鱼冀,這篇是寫的比較完整的挡逼,另外在untiy3d中還提供了深度偏移的指令Offset
Pass
{
Name "FORWARD"
Tags { "LightMode" = "ForwardBase" }
ZWrite On
Offset 3000,0
}
文章內容
- 深度圖基礎
- 訪問深度圖
- 利用深度圖重建世界坐標
- 深度圖應用
- 渲染深度圖
- 相交高亮
- 能量場
- 全局霧效
- 掃描線
- 水淹
- 垂直霧效
- 邊緣檢測
- 運動模糊
- 景深
- 參考資料
深度圖基礎
深度圖里存放了[0,1]范圍的非線性分布的深度值部念,這些深度值來自NDC坐標穴翩。
在延遲渲染中,深度值默認已經(jīng)渲染到G-buffer荸哟;而在前向渲染中假哎,你需要去申請,以便Unity在背后利用Shader Replacement將RenderType為Opaque鞍历、渲染隊列小于等于2500并且有ShadowCaster Pass的物體的深度值渲染到深度圖中舵抹。
訪問深度圖
第一步:在C#中設置Camera.main.depthTextureMode = DepthTextureMode.Depth;
可以在主攝像機的Camera組件下看見提示:
這表明了主攝像機渲染了深度圖
第二步:在Shader中聲明_CameraDepthTexture
sampler2D _CameraDepthTexture;
第三步:訪問深度圖
//1.如果是后處理,可以直接用uv訪問
//vertex
//當有多個RenderTarget時劣砍,需要自己處理UV翻轉問題
#if UNITY_UV_STARTS_AT_TOP //DirectX之類的
if(_MainTex_TexelSize.y < 0) //開啟了抗鋸齒
o.uv.y = 1 - o.uv.y; //滿足上面兩個條件時uv會翻轉惧蛹,因此需要轉回來
#endif
//fragment
float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv));
//2.其他:利用投影紋理采樣
//vertex
o.screenPos = ComputeScreenPos(o.vertex);
//fragment
float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos));
float linear01Depth = Linear01Depth(depth); //轉換成[0,1]內的線性變化深度值
float linearEyeDepth = LinearEyeDepth(depth); //轉換到攝像機空間
重建世界坐標
利用覆蓋屏幕的uv值和深度圖中的深度,我們可以重建出物體在世界空間中的坐標。
主要有以下兩種方法:
- 利用VP矩陣的逆矩陣對NDC坐標進行轉換香嗓。
- 找到從攝像機指向該點的方向向量(需要是單位向量)迅腔,將該方向向量乘上深度值就能得到攝像機指向該點的向量,將該向量加上攝像機位置就能得到該點的世界坐標靠娱。
1. 利用VP矩陣重建
首先要在C#腳本中傳遞當前的VP逆矩陣:
Matrix4x4 currentVP = VPMatrix;
Matrix4x4 currentInverseVP = VPMatrix.inverse;
mat.SetMatrix("_CurrentInverseVP", currentInverseVP);
Graphics.Blit(source, destination, mat);
然后在Shader中首先制造NDC坐標:
float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv.zw));
float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, depth * 2 - 1, 1); //NDC坐標
利用當前的VP逆矩陣將NDC坐標轉換到世界空間:
float4 D = mul(_CurrentInverseVP, H);
float4 W = D / D.w; //將齊次坐標w分量變1得到世界坐標
具體用法可以到下面的MotionBlur例子中查看沧烈。
2. 利用方向向量重建
首先需要知道,Post Process實際上是渲染一個覆蓋屏幕的Quad像云,因此屏幕四個角對應攝像機的視椎體四個角锌雀。
首先是算出攝像機到四個角的向量:
float halfHeight = near * tan(fov/2);
float halfWidth = halfHeight * aspect;
Vector3 toTop = up * halfHeight;
Vector3 toRight = right * halfRight;
Vector3 toTopLeft = forward + toTop - toRight;
Vector3 toBottomLeft = forward - toTop - toRight;
Vector3 toTopRight = forward + toTop + toRight;
Vector3 toBottomRight = forward - toTop + toRight;
假設有個綠點在toTopLeft所在線上,利用相似三角形迅诬,可以得到:
toGreen / depth = toTopLeft / near
而depth是能夠在Shader中獲得的腋逆,因此我們只需要傳遞toTopLeft / near到Shader中就能計算出toGreen:
toTopLeft /= cam.nearClipPlane;
toBottomLeft /= cam.nearClipPlane;
toTopRight /= cam.nearClipPlane;
toBottomRight /= cam.nearClipPlane;
Matrix4x4 frustumDir = Matrix4x4.identity;
frustumDir.SetRow(0, toBottomLeft);
frustumDir.SetRow(1, toBottomRight);
frustumDir.SetRow(2, toTopLeft);
frustumDir.SetRow(3, toTopRight);
mat.SetMatrix("_FrustumDir", frustumDir);
在Vertex中判斷出對應頂點所在的向量:
可以看到uv值和對應的索引值正好是二進制的關系,所以可以如下求出:
int ix = (int)o.uv.z;
int iy = (int)o.uv.w;
o.frustumDir = _FrustumDir[ix + 2 * iy];
你可能奇怪這樣只能求到4個角線上的點侈贷,但vertex到fragment的過程中是有個東西叫插值的惩歉,這個插值正好能把每個像素所在的向量求出。
然后我們就能在fragment中求出世界坐標了:
float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv.zw));
float linearEyeDepth = LinearEyeDepth(depth);
float3 worldPos = _WorldSpaceCameraPos + linearEyeDepth * i.frustumDir.xyz;
具體的用法可以到下面的垂直霧效例子中找到铐维。
渲染深度圖
輸出[0,1]范圍的深度值即可柬泽,如下:
fixed4 frag (v2f i) : SV_Target
{
float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv));
float linear01Depth = Linear01Depth(depth);
return linear01Depth;
}
PS: 如果代碼沒錯,而看到的是全黑的嫁蛇,那么應該就是攝像機的Far Clip Plane設得太大。
相交高亮
思路是判斷當前物體的深度值與深度圖中對應的深度值是否在一定范圍內露该,如果是則判定為相交睬棚。
首先訪問當前物體的深度值:
//vertex
COMPUTE_EYEDEPTH(o.eyeZ);
然后訪問深度圖。由于此時不是Post Process解幼,因此需要利用投影紋理采樣來訪問深度圖:
//vertex
o.screenPos = ComputeScreenPos(o.vertex);
//fragment
float screenZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)));
最后就是進行相交判斷:
float halfWidth = _IntersectionWidth / 2;
float diff = saturate(abs(i.eyeZ - screenZ) / halfWidth); //除以halfWidth來控制相交寬度為_IntersectionWidth
fixed4 finalColor = lerp(_IntersectionColor, col, diff);
return finalColor;
能量場
在相交高亮效果的基礎上抑党,加上半透明和邊緣高亮,就能制造出一個簡單的能量場效果:
float3 worldNormal = normalize(i.worldNormal);
float3 worldViewDir = normalize(i.worldViewDir);
float rim = 1 - saturate(dot(worldNormal, worldViewDir)) * _RimPower;
float screenZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)));
float intersect = (1 - (screenZ - i.eyeZ)) * _IntersectionPower;
float v = max (rim, intersect);
return _MainColor * v;
全局霧效
思路是讓霧的濃度隨著深度值的增大而增大撵摆,然后進行的原圖顏色和霧顏色的插值:
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv.xy);
float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv.zw));
float linearDepth = Linear01Depth(depth);
float fogDensity = saturate(linearDepth * _FogDensity);
fixed4 finalColor = lerp(col, _FogColor, fogDensity);
return finalColor;
}
掃描線
思路與相交高亮效果類似底靠,只是這里是Post Process。自定義一個[0,1]變化的值_CurValue特铝,根據(jù)_CurValue與深度值的差進行顏色的插值:
fixed4 frag (v2f i) : SV_Target
{
fixed4 originColor = tex2D(_MainTex, i.uv.xy);
float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv.zw));
float linear01Depth = Linear01Depth(depth);
float halfWidth = _LineWidth / 2;
float v = saturate(abs(_CurValue - linear01Depth) / halfWidth); //線內返回(0, 1)暑中,線外返回1
return lerp(_LineColor, originColor, v);
}
水淹
利用上面提到的第二種重建世界空間坐標的方法得到世界空間坐標,判斷該坐標的Y值是否在給定閾值下鲫剿,如果是則混合原圖顏色和水的顏色:
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv.xy);
float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv.zw));
float linearEyeDepth = LinearEyeDepth(depth);
float3 worldPos = _WorldSpaceCameraPos.xyz + i.frustumDir * linearEyeDepth;
if(worldPos.y < _WaterHeight)
return lerp(col, _WaterColor, _WaterColor.a); //半透明
return col;
}
垂直霧效
利用上面提到的第二種重建世界空間坐標的方法得到世界空間坐標鳄逾,讓霧的濃度隨著Y值變化:
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv.xy);
float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv.zw));
float linearEyeDepth = LinearEyeDepth(depth);
float3 worldPos = _WorldSpaceCameraPos + linearEyeDepth * i.frustumDir.xyz;
float fogDensity = (worldPos.y - _StartY) / (_EndY - _StartY);
fogDensity = saturate(fogDensity * _FogDensity);
fixed3 finalColor = lerp(_FogColor, col, fogDensity).xyz;
return fixed4(finalColor, 1.0);
}
邊緣檢測
思路是取當前像素的附近4個角,分別計算出兩個對角的深度值差異灵莲,將這兩個差異值相乘就得到我們判斷邊緣的值雕凹。
首先是得到4個角:
//vertex
//Robers算子
o.uv[1] = uv + _MainTex_TexelSize.xy * float2(-1, -1);
o.uv[2] = uv + _MainTex_TexelSize.xy * float2(-1, 1);
o.uv[3] = uv + _MainTex_TexelSize.xy * float2(1, -1);
o.uv[4] = uv + _MainTex_TexelSize.xy * float2(1, 1);
然后是得到這4個角的深度值:
float sample1 = Linear01Depth(UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv[1])));
float sample2 = Linear01Depth(UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv[2])));
float sample3 = Linear01Depth(UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv[3])));
float sample4 = Linear01Depth(UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv[4])));
最后就是根據(jù)對角差異來得到判斷邊緣的值:
float edge = 1.0;
//對角線的差異相乘
edge *= abs(sample1 - sample4) < _EdgeThreshold ? 1.0 : 0.0;
edge *= abs(sample2 - sample3) < _EdgeThreshold ? 1.0 : 0.0;
return edge;
// return lerp(0, col, edge); //描邊
PS:上面這種只用深度值來檢測邊緣的效果并不太好,最好結合法線圖來判斷,原理都是一樣的枚抵。
運動模糊 (Motion Blur)
運動模糊主要用在競速類游戲中用來體現(xiàn)出速度感线欲。這里介紹的運動模糊只能用于周圍物體不動,攝像機動的情景汽摹。
思路是利用上面提到的重建世界坐標方法得到世界坐標李丰,由于該世界坐標在攝像機運動過程中都是不動的,因此可以將該世界空間坐標分別轉到攝像機運動前和運動后的坐標系中竖慧,從而得到兩個NDC坐標嫌套,利用這兩個NDC坐標就能得到該像素運動的軌跡,在該軌跡上多次取樣進行模糊即可圾旨。
首先是得到世界坐標(這里使用提到的第一種重建方法):
float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv.zw));
float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, depth * 2 - 1, 1); //NDC坐標
float4 D = mul(_CurrentInverseVP, H);
float4 W = D / D.w; //將齊次坐標w分量變1得到世界坐標
然后是計算出運算前后的NDC坐標:
float4 currentPos = H;
float4 lastPos = mul(_LastVP, W);
lastPos /= lastPos.w;
最后就是在軌跡上多次取樣進行模糊:
//采樣兩點所在直線上的點踱讨,進行模糊
fixed4 col = tex2D(_MainTex, i.uv.xy);
float2 velocity = (currentPos - lastPos) / 2.0;
float2 uv = i.uv;
uv += velocity;
int numSamples = 3;
for(int index = 1; index < numSamples; index++, uv += velocity)
{
col += tex2D(_MainTex, uv);
}
col /= numSamples;
景深 (Depth Of Field)
景深是一種聚焦處清晰,其他地方模糊的效果砍的,在攝影中很常見痹筛。
思路是首先渲染一張模糊的圖,然后在深度圖中找到聚焦點對應的深度廓鞠,該深度附近用原圖帚稠,其他地方漸變至模糊圖。
第一步是使用SimpleBlur Shader渲染模糊的圖床佳,這里我只是簡單地采樣當前像素附近的9個點然后平均滋早,你可以選擇更好的模糊方式:
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv[0] = v.uv + _MainTex_TexelSize.xy * float2(-1, -1) * _BlurLevel;
o.uv[1] = v.uv + _MainTex_TexelSize.xy * float2(-1, 0) * _BlurLevel;
o.uv[2] = v.uv + _MainTex_TexelSize.xy * float2(-1, 1) * _BlurLevel;
o.uv[3] = v.uv + _MainTex_TexelSize.xy * float2(0, -1) * _BlurLevel;
o.uv[4] = v.uv + _MainTex_TexelSize.xy * float2(0, 0) * _BlurLevel;
o.uv[5] = v.uv + _MainTex_TexelSize.xy * float2(0, 1) * _BlurLevel;
o.uv[6] = v.uv + _MainTex_TexelSize.xy * float2(1, -1) * _BlurLevel;
o.uv[7] = v.uv + _MainTex_TexelSize.xy * float2(1, 0) * _BlurLevel;
o.uv[8] = v.uv + _MainTex_TexelSize.xy * float2(1, 1) * _BlurLevel;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv[0]);
col += tex2D(_MainTex, i.uv[1]);
col += tex2D(_MainTex, i.uv[2]);
col += tex2D(_MainTex, i.uv[3]);
col += tex2D(_MainTex, i.uv[4]);
col += tex2D(_MainTex, i.uv[5]);
col += tex2D(_MainTex, i.uv[6]);
col += tex2D(_MainTex, i.uv[7]);
col += tex2D(_MainTex, i.uv[8]);
col /= 9;
return col;
}
第二步就是傳遞該模糊的圖給DepthOfField Shader:
RenderTexture blurTex = RenderTexture.GetTemporary(source.width, source.height, 16);
Graphics.Blit(source, blurTex, blurMat);
dofMat.SetTexture("_BlurTex", blurTex);
Graphics.Blit(source, destination, dofMat);
第三步就是在DepthOfField Shader中根據(jù)焦點來混合原圖顏色和模糊圖顏色:
fixed4 col = tex2D(_MainTex, i.uv.xy);
fixed4 blurCol = tex2D(_BlurTex, i.uv.zw);
float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, i.uv.zw));
float linearDepth = Linear01Depth(depth);
float v = saturate(abs(linearDepth - _FocusDistance) * _FocusLevel);
return lerp(col, blurCol, v);
完整項目地址
https://github.com/KaimaChen/Unity-Shader-Demo/tree/master/UnityShaderProject
參考
Unity Docs - Camera’s Depth Texture
Unity Docs - Platform-specific rendering differences
神奇的深度圖:復雜的效果,不復雜的原理
SPECIAL EFFECTS WITH DEPTH
GPU Gems - Chapter 27. Motion Blur as a Post-Processing Effect
《Unity Shader 入門精要》
《Unity 3D ShaderLab 開發(fā)實戰(zhàn)詳解》