神奇的深度圖:複雜的效果,不複雜的原理
0x00 前言
本文是《有趣的深度圖》的第二篇文章,上一篇文章《有趣的深度圖:可見性問題的解法》中已經和大家介紹了深度圖在解決可見性問題中的應用。其實,利用深度信息我們可以實現很多有趣而又顯得「高大上」的效果。
不過這些效果雖然看上去高大上,但是一旦了解了原理就會發現實現這種效果其實是十分簡單的。 那麼本文會包括以下四個有趣的效果在Unity中的實現:- 有點科幻的掃描網
- 透過牆壁繪製背後的「人影」
- 護盾/能量場效果
- 邊緣檢測
0x01 獲取深度信息
為了利用深度信息來實現若干效果,我們首先需要獲取場景的深度信息。在移動遊戲開發中常用的前向渲染路徑(Forward Rendering)下,我們需要手動設置相機,讓它提供場景的深度信息。
camera.depthTextureMode = DepthTextureMode.Depth;
如果在延遲渲染路徑(Deferred Lighting)下,由於延遲渲染需要場景的深度信息和法線信息來做光照計算,所以並不需要我們手動設置相機。
這樣我們就可以在shader中訪問_CameraDepthTexture來獲取保存的場景的深度信息,之後再利用UNITY_SAMPLE_DEPTH這個宏來處理_CameraDepthTexture的值,這樣我們就獲取了某個像素的深度值。
float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, uv));
但是正如上一篇文章中所說,此時的深度值並非是線性的,因此我們常常需要利用另一個內建的方法Linear01Depth將結果轉化為線性的。這樣,我們就能將場景的深度信息渲染為一張灰度圖。
float linear01Depth = Linear01Depth(depth);

0x02 有點科幻的掃描網
不知道有沒有小夥伴玩過《無人深空》這款遊戲,當初ps4版預售時我就用行動支持了這款看上去很有吸引力的沙盒遊戲,當然第二天掛閑魚就是後話了。雖然這款遊戲讓人感到有些失望,但是其中的一些畫面效果還是很有趣的,而且也和這篇文章的主題相關——利用場景的深度信息來實現一些科幻效果——比如說,在星球上用掃描儀進行掃描的效果。

我們也可以在Unity中實現類似的效果,關鍵就是利用場景的深度信息。

因此如果項目使用了前向渲染路徑,我們就必須在腳本中手動將相機的depthTextureMode 設置為DepthTextureMode.Depth,如果是延遲渲染則不需要我們手動設置。
camera.depthTextureMode = DepthTextureMode.Depth;
其次,這種全屏效果常常作為屏幕特效(image effect)來實現,也就是說我們需要攝像機先將場景渲染成一副圖片,之後對這張圖片的像素做處理。設想一下如果不這樣做的話,我們不僅要計算場景內所有被渲染對象和攝像機的距離,還需要至少兩個pass,其中一個返回被渲染物體的正常顏色,另一個則來實現和掃描顏色的疊加。如果場景內被渲染的對象很多的話,這樣的操作效率就變得十分低下了。
所以,在cs腳本中我們還會用到OnRenderImage這個回調以獲取攝像機渲染的場景圖像。
void OnRenderImage(RenderTexture src, RenderTexture dst){ //TODO}
再次,隨著時間的流逝掃描網逐漸掃描整個場景顯然是一個動態的效果。因此我們還需要把時間這個因子也引入,時間影響了掃描網和起點的距離。當然,我們既可以在shader文件中考慮時間的影響,也能在cs文件中考慮時間的影響。
如果我們要直接在shader中獲取時間的信息的話,就需要用到unity的內置變數float4 _Time : Time (t/20, t, t*2, t*3)了。它的4個分量分別表示了t/20、t、t*2、t*3。因此,在shader中我們使用_Time.y就可以獲取當前的時間,根據時間我們就能算出掃描網當前移動的大概距離了。
除此之外,我們當然也可以在cs文件中直接利用Time類和Update方法直接計算掃描網的移動距離,然後再將結果傳入shader。這樣,我們就完成了一個超級簡單的cs腳本:
/* * Created by Chenjd * http://www.cnblogs.com/murongxiaopifu/ */ using UnityEngine;using System.Collections;public class ScannerEffect : MonoBehaviour{ #region 欄位 public Material mat; public float velocity = 5; private bool isScanning; private float dis; #endregion #region unity 方法 void Start() { Camera.main.depthTextureMode = DepthTextureMode.Depth; } void Update() { if (this.isScanning) { this.dis += Time.deltaTime * this.velocity; } //無人深空中按c開啟掃描 if (Input.GetKeyDown(KeyCode.C)) { this.isScanning = true; this.dis = 0; } } void OnRenderImage(RenderTexture src, RenderTexture dst) { mat.SetFloat("_ScanDistance", dis); Graphics.Blit(src, dst, mat); } #endregion}
至於shader?那就更簡單了,現在我們獲取了相機渲染之後的場景圖,這樣圖上的每個像素只需要獲取自己的深度信息:
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth); float linear01Depth = Linear01Depth(depth);
然後再和掃描網現在的位置做個對比——當然我們還可以加入掃描網的寬度這個概念——符合條件的像素顏色和掃描網的顏色進行疊加就可以了。最後為了更完美一點,我們還需要判斷一下深度值是否比1小,因為深度值在[0,1]這個區間內,而1對應的是遠裁切面,因此如果不判斷1的話,整個遠方最後都會被掃描網的顏色進行疊加。
if (linear01Depth < _ScanDistance && linear01Depth > _ScanDistance - _ScanWidth && linear01Depth < 1){ float diff = 1 - (_ScanDistance - linear01Depth) / (_ScanWidth); _ScanColor *= diff; return col + _ScanColor;}
完整的項目可以到這裡到這裡下載:UnitySpecialEffectWithDepth
0x03 透過牆壁繪製背後的「人影」
透過障礙物看到障礙物後的高亮目標,國內外很多遊戲都會用到類似的效果。

這個看上去很有高大上的視覺效果,其實從創建一個unity的Unlit shader文件到最後完成這個效果只需要大概30s。
原理很簡單,即根據目標是否被遮擋返回不同的顏色即可。目標被障礙物遮住的部分其深度值必然要大於障礙物,因此我們可以用一個pass處理當深度值大於障礙物的時也就是目標被障礙物遮住的部分的顏色——例如我們返回紅色。
Pass { ZTest Greater ... fixed4 frag (v2f i) : SV_Target { fixed4 col = fixed4(1, 0, 0, 1); return col; } }
再用另一個pass處理目標未被遮擋住的部分,也就是深度值小於障礙物時返回目標的正常顏色。
Pass { ZTest Less ... fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); return col } }



所以,給牆後的目標描邊這件事就又變得十分簡單了。我們只需要在處理被遮擋部分的那個pass中返回的紅色變為與法線和觀察方向的夾角相關的一個值就好了。
為了實現這個目標,我們首先要獲取法線和觀察方向的信息。o.normal = UnityObjectToWorldNormal(v.normal);o.viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);
之後再計演算法線和觀察方向的夾角信息:
float NdotV = 1 - dot(i.normal, i.viewDir) ;
最後,只需要把這個值當作影響最後顏色輸出的因素就好了。
return _EdgeColor * NdotV;

0x04 護盾/能量場效果

很多科幻遊戲也有這種能量場或者護盾的效果。例如暴雪的守望先鋒中的猩猩溫斯頓的屏障發射器、光環系列的聖堂防衛者的能量護盾甚至一些手游中也有類似的效果,比如網易的光明大陸。

- 半透明效果
- 相交高亮,主要指能量場和別的物體相交的地方是高亮顯示
- 表面扭曲
- 一個和觀察方向相關的描邊效果
首先我們要開啟透明混合併指定渲染隊列為透明。
SubShader{ ZWrite Off Cull Off Blend SrcAlpha OneMinusSrcAlpha Tags { "RenderType" = "Transparent" "Queue" = "Transparent" } ...}
之後像上一個例子那樣,根據觀察方向繪製能量場的邊緣。
//verto.normal = UnityObjectToWorldNormal(v.normal);o.viewDir = normalize(UnityWorldSpaceViewDir(mul(unity_ObjectToWorld, v.vertex)));//fragfloat rim = 1 - abs(dot(i.normal, normalize(i.viewDir)));
這樣,我們就得到了一個半透且帶有描邊效果球體,能量場已經初具雛形了。

接下來,我們就要實現相交高亮的效果了。所謂的相交高亮指的是能量場和別的物體相交時,在相交處繪製出高亮效果。這時我們就要用到深度信息了。當能量場和某個物體相交時,二者的深度信息應該一致,基於這個對比深度信息,我們可以用來估計一個像素的「相交程度」。
需要注意的是,能量場的shader在執行時_CameraDepthTexture中只保存了場景中不透明物體的深度信息,因此這個時候無法從CameraDepthTexture中獲取能量場的深度信息,所以要在vert中計算頂點的深度,這裡我利用了COMPUTE_EYEDEPTH這個內置的宏。在之後的frag內就可以很方便的獲取場景和能量場當前片元的深度了。
//verto.screenPos = ComputeScreenPos(o.vertex);COMPUTE_EYEDEPTH(o.screenPos.z);//fragfloat sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)));float partZ = i.screenPos.z;
兩者相減就是深度的差異diff,再用1 - diff就得到了一個「相交程度」。
float diff = sceneZ - partZ;float intersect = (1 - diff) * _IntersectPower;

最後我們還需要實現一個能量場的扭曲效果。扭曲效果是遊戲裡面經常有的一個效果,其實也很簡單,我們只需要一張渲染能量場之前的場景的渲染圖,之後隨時間調整uv的偏移就可以模擬扭曲的效果了。
GrabPass{ "_GrabTempTex"}... //fragfloat4 offset = tex2D(_NoiseTex, i.uv - _Time.xy) * _DistortTimeFactor;i.grabPos.xy -= offset.xy * _DistortStrength;fixed4 color = tex2Dproj(_GrabTempTex, i.grabPos);...

0x05 邊緣檢測
邊緣檢測的目的是標識數字圖像中屬性顯著變化的點。圖像屬性中的顯著變化通常反映了屬性的重要變化。這些包括:
- 深度上的不連續
- 表面法線方向不連續
- 顏色不連續
- 亮度不連續

需要注意的是邊緣可能與觀察方向有關——也就是說邊緣可能隨著觀察方形的不同而變化,例如上文中的描邊實現;也可能與觀察方向無關——這通常反映被觀察物體的屬性如表面紋理和表面形狀。在這個部分,我們的關注點主要是後者。
因此,根據不同的屬性變化也有很多種策略來處理邊緣檢測,例如利用深度、利用法線、利用深度+法線、利用顏色等等。邊緣是灰度值不連續的結果,這種不連續常可利用求導數方便地檢測到,一般常用一階和二階導數來檢測邊緣。其中一階導數的幅度值來檢測邊緣的存在,幅度峰值一般對應邊緣位置。


0x06 小結
以上便是常見的幾種利用深度信息來實現的視覺效果。
完整的項目可以到這裡到這裡下載:UnitySpecialEffectWithDepth 各位如果覺得有趣的話,歡迎點個贊。ref:
【1】Siggraph2011_SpecialEffectsWithDepth_WithNotes。「Special Effects with Depth」 talk at SIGGRAPH – Unity Blog
【2】Unity Shaders - Depth and Normal Textures (Part 2)
【3】Using Depth Textures Unity手冊
【4】Unity - Get Unity - Download Archive Unity內置shader
【5】題圖來自《殺手5:赦免》
-華麗的分割線-
最後打個廣告,歡迎支持我的書《Unity 3D腳本編程》

推薦閱讀:
※基於物理的渲染—基於球面調和基的實時全局光照明
※電影工業中的流體模擬(四)- 納維斯托克斯方程(下)
※利用Stencil來優化局部後處理特效
※在中國科幻電影人才缺乏時,是否能通過設立電影物理學專業,實現彎道接近國外頂尖水準?
※遊戲後期特效第四發 -- 屏幕空間環境光遮蔽(SSAO)

