Unity Shader - 深度圖基礎及應用(轉)

Unity Shader - 深度圖基礎及應用

最近看了一些關于深度圖及應用的文章鱼冀,這篇是寫的比較完整的挡逼,另外在untiy3d中還提供了深度偏移的指令Offset

Pass
{
    Name "FORWARD" 
    Tags { "LightMode" = "ForwardBase" }
    ZWrite On
    Offset 3000,0
}

文章內容

  1. 深度圖基礎
  2. 訪問深度圖
  3. 利用深度圖重建世界坐標
  4. 深度圖應用
    • 渲染深度圖
    • 相交高亮
    • 能量場
    • 全局霧效
    • 掃描線
    • 水淹
    • 垂直霧效
    • 邊緣檢測
    • 運動模糊
    • 景深
  5. 參考資料

深度圖基礎

深度圖里存放了[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值和深度圖中的深度,我們可以重建出物體在世界空間中的坐標。
主要有以下兩種方法:

  1. 利用VP矩陣的逆矩陣對NDC坐標進行轉換香嗓。
  2. 找到從攝像機指向該點的方向向量(需要是單位向量)迅腔,將該方向向量乘上深度值就能得到攝像機指向該點的向量,將該向量加上攝像機位置就能得到該點的世界坐標靠娱。

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)詳解》

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末砌们,一起剝皮案震驚了整個濱河市杆麸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌浪感,老刑警劉巖昔头,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異影兽,居然都是意外死亡揭斧,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門峻堰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來讹开,“玉大人,你說我怎么就攤上這事茧妒。” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵除破,是天一觀的道長瑰枫。 經(jīng)常有香客問我光坝,道長甥材,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上辅鲸,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好雾叭,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布织狐。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪雨效。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天叮姑,我揣著相機與錄音极颓,去河邊找鬼托享。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的传惠。 我是一名探鬼主播尘吗,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼浇坐!你這毒婦竟也來了睬捶?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤近刘,失蹤者是張志新(化名)和其女友劉穎擒贸,沒想到半個月后臀晃,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡酗宋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年积仗,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜕猫。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡寂曹,死狀恐怖,靈堂內的尸體忽然破棺而出回右,到底是詐尸還是另有隱情隆圆,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布翔烁,位于F島的核電站渺氧,受9級特大地震影響,放射性物質發(fā)生泄漏蹬屹。R本人自食惡果不足惜侣背,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望慨默。 院中可真熱鬧贩耐,春花似錦、人聲如沸厦取。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽虾攻。三九已至铡买,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間霎箍,已是汗流浹背奇钞。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留漂坏,地道東北人蛇券。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像樊拓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子塘慕,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內容