http://www.manew.com/th
神奇的深度圖:複雜的效果,不復雜的原理
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這個回調以獲取攝像機渲染的場景圖像。
1
2
3
|
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腳本:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
/* *
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?那就更簡單了,現在我們獲取了相機渲染之後的場景圖,這樣圖上的每個像素只需要獲取自己的深度信息:
1
|
float
depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth); float
linear01Depth = Linear01Depth(depth); |
然後再和掃描網現在的位置做個對比——當然我們還可以加入掃描網的寬度這個概念——符合條件的像素顏色和掃描網的顏色進行疊加就可以了。最後爲了更完美一點,我們還需要判斷一下深度值是否比1小,因爲深度值在[0,1]這個區間內,而1對應的是遠裁切面,因此如果不判斷1的話,整個遠方最後都會被掃描網的顏色進行疊加。
1
2
3
4
5
|
if
(linear01Depth < _ScanDistance && linear01Depth > _ScanDistance - _ScanWidth && linear01Depth < 1) { float
diff = 1 - (_ScanDistance - linear01Depth) / (_ScanWidth); _ScanColor
*= diff; return
col + _ScanColor; } |
完整的項目可以到這裏到這裏下載:UnitySpecialEffectWithDepth
https://github.com/chenjd/UnitySpecialEffectWithDepth
0x03 透過牆壁繪製背後的“人影”
透過障礙物看到障礙物後的高亮目標,國內外很多遊戲都會用到類似的效果。
這個看上去很有高大上的視覺效果,其實從創建一個unity的Unlit shader文件到最後完成這個效果只需要大概30s。
原理很簡單,即根據目標是否被遮擋返回不同的顏色即可。目標被障礙物遮住的部分其深度值必然要大於障礙物,因此我們可以用一個pass處理當深度值大於障礙物的時也就是目標被障礙物遮住的部分的顏色——例如我們返回紅色。
01
02
03
04
05
06
07
08
09
10
11
|
Pass { ZTest
Greater ... fixed4
frag (v2f i) : SV_Target { fixed4
col = fixed4(1, 0, 0, 1); return
col; } } |
再用另一個pass處理目標未被遮擋住的部分,也就是深度值小於障礙物時返回目標的正常顏色。
01
02
03
04
05
06
07
08
09
10
11
|
Pass { ZTest
Less ... fixed4
frag (v2f i) : SV_Target { fixed4
col = tex2D(_MainTex, i.uv); return
col } } |
不過牆後的敵人如果只是顯示一個紅色是否有點太單調了呢?還有很多遊戲,它的透視效果是下面這樣的:目標身上多了一些描邊。
這個效果的實現其實也很簡單。我們可以根據觀察方向和目標多邊形的法線方向的夾角來判斷目標的邊緣——畢竟目標面向我們的面的法線和我們觀察方向的夾角相對較小,而邊緣部分的面的法線和我們的觀察方向的夾角顯然更大——這裏的邊緣判斷用到了觀察方向,下文我們還會聊聊跟觀察方向無關的邊緣檢測。
所以,給牆後的目標描邊這件事就又變得十分簡單了。我們只需要在處理被遮擋部分的那個pass中返回的紅色變爲與法線和觀察方向的夾角相關的一個值就好了。
爲了實現這個目標,我們首先要獲取法線和觀察方向的信息。
1
|
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; |
完整的項目可以到這裏到這裏下載:UnitySpecialEffectWithDepth
https://github.com/chenjd/UnitySpecialEffectWithDepth
0x04 護盾/能量場效果
很多科幻遊戲也有這種能量場或者護盾的效果。例如暴雪的守望先鋒中的猩猩溫斯頓的屏障發射器、光環系列的聖堂防衛者的能量護盾甚至一些手遊中也有類似的效果,比如網易的光明大陸。
這個效果的實現和原理其實也並不複雜。簡單的說可以分爲以下這幾個部分:
半透明效果
相交高亮,主要指能量場和別的物體相交的地方是高亮顯示
表面扭曲
一個和觀察方向相關的描邊效果
首先我們要開啓透明混合並指定渲染隊列爲透明。
01
02
03
04
05
06
07
08
09
10
11
12
13
|
SubShader { ZWrite
Off Cull
Off Blend
SrcAlpha OneMinusSrcAlpha Tags { "RenderType"
= "Transparent" "Queue"
= "Transparent" } ... } |
之後像上一個例子那樣,根據觀察方向繪製能量場的邊緣。
1
2
3
4
5
6
7
|
//vert o.normal
= UnityObjectToWorldNormal(v.normal); o.viewDir
= normalize(UnityWorldSpaceViewDir(mul(unity_ObjectToWorld, v.vertex))); //frag float
rim = 1 - abs(dot(i.normal, normalize(i.viewDir))); |
這樣,我們就得到了一個半透且帶有描邊效果球體,能量場已經初具雛形了。
接下來,我們就要實現相交高亮的效果了。所謂的相交高亮指的是能量場和別的物體相交時,在相交處繪製出高亮效果。這時我們就要用到深度信息了。當能量場和某個物體相交時,二者的深度信息應該一致,基於這個對比深度信息,我們可以用來估計一個像素的“相交程度”。
需要注意的是,能量場的shader在執行時_CameraDepthTexture中只保存了場景中不透明物體的深度信息,因此這個時候無法從CameraDepthTexture中獲取能量場的深度信息,所以要在vert中計算頂點的深度,這裏我利用了COMPUTE_EYEDEPTH這個內置的宏。在之後的frag內就可以很方便的獲取場景和能量場當前片元的深度了。
1
2
3
4
5
6
7
|
//vert o.screenPos
= ComputeScreenPos(o.vertex); COMPUTE_EYEDEPTH(o.screenPos.z); //frag float
sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos))); float
partZ = i.screenPos.z; |
兩者相減就是深度的差異diff,再用1 - diff就得到了一個“相交程度”。
1
2
|
float
diff = sceneZ - partZ; float
intersect = (1 - diff) * _IntersectPower; |
最後我們還需要實現一個能量場的扭曲效果。扭曲效果是遊戲裏面經常有的一個效果,其實也很簡單,我們只需要一張渲染能量場之前的場景的渲染圖,之後隨時間調整uv的偏移就可以模擬扭曲的效果了。
01
02
03
04
05
06
07
08
09
10
11
12
|
GrabPass { "_GrabTempTex" } ... //frag float4
offset = tex2D(_NoiseTex, i.uv - _Time.xy) * _DistortTimeFactor; i.grabPos.xy
-= offset.xy * _DistortStrength; fixed4
color = tex2Dproj(_GrabTempTex, i.grabPos); ... |
完整的項目可以到這裏到這裏下載:UnitySpecialEffectWithDepth
https://github.com/chenjd/UnitySpecialEffectWithDepth
0x05 邊緣檢測
邊緣檢測的目的是標識數字圖像中屬性顯著變化的點。圖像屬性中的顯著變化通常反映了屬性的重要變化。這些包括:
深度上的不連續
表面法線方向不連續
顏色不連續
亮度不連續
需要注意的是邊緣可能與觀察方向有關——也就是說邊緣可能隨着觀察方形的不同而變化,例如上文中的描邊實現;也可能與觀察方向無關——這通常反映被觀察物體的屬性如表面紋理和表面形狀。在這個部分,我們的關注點主要是後者。
因此,根據不同的屬性變化也有很多種策略來處理邊緣檢測,例如利用深度、利用法線、利用深度+法線、利用顏色等等。邊緣是灰度值不連續的結果,這種不連續常可利用求導數方便地檢測到,一般常用一階和二階導數來檢測邊緣。其中一階導數的幅度值來檢測邊緣的存在,幅度峯值一般對應邊緣位置。
不過爲了簡化計算,在實際中常用小區域模板卷積來近似計算偏導數。對Gx和Gy各用1個模板,所以需要2個模板組合起來以構成1個梯度算子。最簡單的梯度算子是羅伯特交叉(Roberts cross)算子。
其實在unity的image effect中就包含了描邊這個效果,而其中又有5種不同的方式,其中的一種叫做RobertsCrossDepthNormals便是利用了羅伯特算子,各位如果有興趣的話可以參考。
0x06 小結
以上便是常見的幾種利用深度信息來實現的視覺效果。
完整的項目可以到這裏到這裏下載:UnitySpecialEffectWithDepth
https://github.com/chenjd/UnitySpecialEffectWithDepth
各位如果覺得有趣的話,歡迎點個贊。
ref:
【1】Siggraph2011_SpecialEffectsWithDepth_WithNotes。“Special Effects with Depth” talk at SIGGRAPH – Unity Blog
https://blogs.unity3d.com/cn/2011/09/08/special-effects-with-depth-talk-at-siggraph/
【2】Unity Shaders - Depth and Normal Textures (Part 2)
http://williamchyr.com/2013/11/unity-shaders-depth-and-normal-textures-part-2/
【3】題圖來自《殺手5:赦免》
-華麗的分割線-
最後打個廣告,歡迎支持我的書《Unity 3D腳本編程》
https://item.jd.com/12035114.html
read-105501-1-1.html