神奇的深度圖:複雜的效果,不復雜的原理

0x00 前言

本文是《有趣的深度圖》的第二篇文章,上一篇文章《有趣的深度圖:可見性問題的解法》中已經和你們介紹了深度圖在解決可見性問題中的應用。其實,利用深度信息咱們能夠實現不少有趣而又顯得「高大上」的效果。
不過這些效果雖然看上去高大上,可是一旦瞭解了原理就會發現實現這種效果實際上是十分簡單的。
那麼本文會包括如下四個有趣的效果在Unity中的實現:html

  • 有點科幻的掃描網
  • 透過牆壁繪製背後的「人影」
  • 護盾/能量場效果
  • 邊緣檢測

0x01 獲取深度信息

爲了利用深度信息來實現若干效果,咱們首先須要獲取場景的深度信息。在移動遊戲開發中經常使用的前向渲染路徑(Forward Rendering)下,咱們須要手動設置相機,讓它提供場景的深度信息。git

camera.depthTextureMode = DepthTextureMode.Depth;

若是在延遲渲染路徑(Deferred Lighting)下,因爲延遲渲染須要場景的深度信息和法線信息來作光照計算,因此並不須要咱們手動設置相機。github

這樣咱們就能夠在shader中訪問_CameraDepthTexture來獲取保存的場景的深度信息,以後再利用UNITY_SAMPLE_DEPTH這個宏來處理_CameraDepthTexture的值,這樣咱們就獲取了某個像素的深度值。算法

float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, uv));

可是正如上一篇文章中所說,此時的深度值並不是是線性的,所以咱們經常須要利用另外一個內建的方法Linear01Depth將結果轉化爲線性的。這樣,咱們就能將場景的深度信息渲染爲一張灰度圖。編程

float linear01Depth = Linear01Depth(depth);

QQ截圖20170619232147.png

0x02 有點科幻的掃描網

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

nomansky.gif

咱們也能夠在Unity中實現相似的效果,關鍵就是利用場景的深度信息。spa

scaneffect.gif

所以若是項目使用了前向渲染路徑,咱們就必須在腳本中手動將相機的depthTextureMode 設置爲DepthTextureMode.Depth,若是是延遲渲染則不須要咱們手動設置。3d

camera.depthTextureMode = DepthTextureMode.Depth;

其次,這種全屏效果經常做爲屏幕特效(image effect)來實現,也就是說咱們須要攝像機先將場景渲染成一副圖片,以後對這張圖片的像素作處理。設想一下若是不這樣作的話,咱們不只要計算場景內全部被渲染對象和攝像機的距離,還須要至少兩個pass,其中一個返回被渲染物體的正常顏色,另外一個則來實現和掃描顏色的疊加。若是場景內被渲染的對象不少的話,這樣的操做效率就變得十分低下了。
因此,在cs腳本中咱們還會用到OnRenderImage這個回調以獲取攝像機渲染的場景圖像。code

void OnRenderImage(RenderTexture src, RenderTexture dst)
{
     //TODO
}

再次,隨着時間的流逝掃描網逐漸掃描整個場景顯然是一個動態的效果。所以咱們還須要把時間這個因子也引入,時間影響了掃描網和起點的距離。固然,咱們既能夠在shader文件中考慮時間的影響,也能在cs文件中考慮時間的影響。orm

若是咱們要直接在shader中獲取時間的信息的話,就須要用到unity的內置變量float4 _Time : Time (t/20, t, t*2, t*3) 了。它的4個份量分別表示了t/20、t、t*二、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
        }
    }

不過牆後的敵人若是隻是顯示一個紅色是否有點太單調了呢?還有不少遊戲,它的透視效果是下面這樣的:目標身上多了一些描邊。
殺出重圍3人類革命
這個效果的實現其實也很簡單。咱們能夠根據觀察方向和目標多邊形的法線方向的夾角來判斷目標的邊緣——畢竟目標面向咱們的面的法線和咱們觀察方向的夾角相對較小,而邊緣部分的面的法線和咱們的觀察方向的夾角顯然更大——這裏的邊緣判斷用到了觀察方向,下文咱們還會聊聊跟觀察方向無關的邊緣檢測。
QQ截圖20170625232518.png

因此,給牆後的目標描邊這件事就又變得十分簡單了。咱們只須要在處理被遮擋部分的那個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;

seethewall4.gif
完整的項目能夠到這裏到這裏下載:UnitySpecialEffectWithDepth

0x04 護盾/能量場效果

Winston_overwatch_gamemela.jpg

不少科幻遊戲也有這種能量場或者護盾的效果。例如暴雪的守望先鋒中的猩猩溫斯頓的屏障發射器、光環系列的聖堂防衛者的能量護盾甚至一些手遊中也有相似的效果,好比網易的光明大陸。
maxresdefault.jpg
這個效果的實現和原理其實也並不複雜。簡單的說能夠分爲如下這幾個部分:

  • 半透明效果
  • 相交高亮,主要指能量場和別的物體相交的地方是高亮顯示
  • 表面扭曲
  • 一個和觀察方向相關的描邊效果

首先咱們要開啓透明混合並指定渲染隊列爲透明。

SubShader
{
    ZWrite Off
    Cull Off
    Blend SrcAlpha OneMinusSrcAlpha

    Tags
    {
        "RenderType" = "Transparent"
        "Queue" = "Transparent"
    }

    ...
}

以後像上一個例子那樣,根據觀察方向繪製能量場的邊緣。

//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)));

這樣,咱們就獲得了一個半透且帶有描邊效果球體,能量場已經初具雛形了。

unitytip3.gif

接下來,咱們就要實現相交高亮的效果了。所謂的相交高亮指的是能量場和別的物體相交時,在相交處繪製出高亮效果。這時咱們就要用到深度信息了。當能量場和某個物體相交時,兩者的深度信息應該一致,基於這個對比深度信息,咱們能夠用來估計一個像素的「相交程度」。

須要注意的是,能量場的shader在執行時_CameraDepthTexture中只保存了場景中不透明物體的深度信息,所以這個時候沒法從CameraDepthTexture中獲取能量場的深度信息,因此要在vert中計算頂點的深度,這裏我利用了COMPUTE_EYEDEPTH這個內置的宏。在以後的frag內就能夠很方便的獲取場景和能量場當前片元的深度了。

//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就獲得了一個「相交程度」。

float diff = sceneZ - partZ;

float intersect = (1 - diff) * _IntersectPower;

unitytip4.gif

最後咱們還須要實現一個能量場的扭曲效果。扭曲效果是遊戲裏面常常有的一個效果,其實也很簡單,咱們只須要一張渲染能量場以前的場景的渲染圖,以後隨時間調整uv的偏移就能夠模擬扭曲的效果了。

GrabPass
{
    "_GrabTempTex"
}

...
  
//frag
float4 offset = tex2D(_NoiseTex, i.uv - _Time.xy) * _DistortTimeFactor;
i.grabPos.xy -= offset.xy * _DistortStrength;
fixed4 color = tex2Dproj(_GrabTempTex, i.grabPos);

...

unitytip901.gif
完整的項目能夠到這裏到這裏下載:UnitySpecialEffectWithDepth

0x05 邊緣檢測

邊緣檢測的目的是標識數字圖像中屬性顯著變化的點。圖像屬性中的顯著變化一般反映了屬性的重要變化。這些包括:

  • 深度上的不連續
  • 表面法線方向不連續
  • 顏色不連續
  • 亮度不連續

QQ截圖20170623191104.png

須要注意的是邊緣可能與觀察方向有關——也就是說邊緣可能隨着觀察方形的不一樣而變化,例如上文中的描邊實現;也可能與觀察方向無關——這一般反映被觀察物體的屬性如表面紋理和表面形狀。在這個部分,咱們的關注點主要是後者。

所以,根據不一樣的屬性變化也有不少種策略來處理邊緣檢測,例如利用深度、利用法線、利用深度+法線、利用顏色等等。邊緣是灰度值不連續的結果,這種不連續常可利用求導數方便地檢測到,通常經常使用一階和二階導數來檢測邊緣。其中一階導數的幅度值來檢測邊緣的存在,幅度峯值通常對應邊緣位置。
11.png
不過爲了簡化計算,在實際中經常使用小區域模板卷積來近似計算偏導數。對Gx和Gy各用1個模板,因此須要2個模板組合起來以構成1個梯度算子。最簡單的梯度算子是羅伯特交叉(Roberts cross)算子。

Roberts-Cross-convolution-filter.png

其實在unity的image effect中就包含了描邊這個效果,而其中又有5種不一樣的方式,其中的一種叫作RobertsCrossDepthNormals即是利用了羅伯特算子,各位若是有興趣的話能夠參考。

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】題圖來自《殺手5:赦免》

-華麗的分割線-
最後打個廣告,歡迎支持個人書《Unity 3D腳本編程》

歡迎你們關注個人公衆號慕容的遊戲編程:chenjd01

相關文章
相關標籤/搜索