Ⅷ使用深度和法線紋理

使用深度和法線紋理

獲取深度和法線紋理

背后原理

深度紋理實際就是一張渲染紋理巧婶,只不過它里面存儲的像素值不是顏色值而是一個高精度的深度值。由于被存儲在一張紋理中壕翩,深度紋理的深度值范圍是[0,1]蛉迹,而且通常是非線性分布的。

這些深度值是從哪里得到的呢放妈?

總體來說北救,這些深度值來自于頂點變換后得到的歸一化的設(shè)備坐標(Normalized Device Coordinates,NDC)芜抒≌洳撸回顧一下,一個模型要想最終被繪制在屏幕上宅倒,需要把它的頂點從模型空間變換到變換到齊次裁剪坐標系下攘宙,這是通過在頂點著色器中乘以MVP變換矩陣得到的。在變換的最后一步拐迁,我們需要使用一個投影矩陣來變換頂點,當我們使用的是透視投影類型的攝像機時铺韧,這個投影矩陣就是非線性的缓淹。

下圖顯示了Unity中透視投影對頂點變換的過程:

圖中顯示了投影變換前,即觀察空間下視椎體的結(jié)構(gòu)及相應(yīng)的頂點位置料仗,中間的圖顯示了應(yīng)用透視裁剪矩陣后的變換結(jié)果,即頂點著色器階段輸出的頂點變換結(jié)果鹏溯,最右側(cè)的圖則是底層硬件進行了透視除法后得到的歸一化的設(shè)備坐標丙挽。

需要注意的是,這里的投影過程是建立在Unity對坐標系的假定上的平窘,也就是說瑰艘,我們針對的是觀察空間為右手坐標系紫新,使用列矩陣在矩陣右側(cè)進行相乘芒率,且變換到NDC后z分量范圍在[-1,1]之間的情況偶芍。而且類似DirectX這樣的圖形接口中匪蟀,變換后z分量范圍將在[0,1]之間。如果需要在其它圖形接口下實現(xiàn)本章類似的效果观挎,需要對一些計算參數(shù)作出相應(yīng)的變化键兜。

上圖顯示了在使用正交攝像機時投影變換的過程。同樣谜疤,變換后會得到一個范圍為[-1,1]的立方體佃延。正交投影使用的變換矩陣是線性的履肃。

在得到NDC后,深度紋理中的像素值就可以很方便的計算得到了膘螟,這些深度值就對應(yīng)了NDC中頂點坐標的z分量的值荆残。由于NDC中z分量的范圍在[-1,1],為了讓這些值能夠存儲在一張圖像中内斯,我們需要使用下面的公式對其進行映射:

d = 0.5 · znad + 0.5

其中俘闯,d對應(yīng)了深度紋理中的像素值真朗,Zndc對應(yīng)了NDC坐標中的z分量的值蜜猾。

那么Unity是怎么得到這樣一張深度紋理的呢衍菱?

在Unity中脊串,深度紋理可以直接來自于真正的深度緩存琼锋,也可以是由一個單獨的Pass渲染而得缕坎,這取決于使用的渲染路徑和硬件谜叹。

  • 通常來講荷腊,當使用延遲渲染路徑(包括遺留的延遲渲染路徑時),深度紋理理所當然可以訪問到疾忍,因為延遲渲染會把這些信息渲染到G-buffer中锭碳。
  • 當無法直接獲取深度緩存時擒抛,深度和法線紋理是通過一個單獨的Pass渲染而得的歧沪。具體實現(xiàn)是暖夭,Unity會使用著色器替換(Shader Replacement)技術(shù)選擇那些渲染類型(即SubShader的RenderType標簽)為Opaque的物體迈着,判斷它們使用的渲染隊列是否小于等于2500(內(nèi)置的Background裕菠、Geometry和AlphaTest渲染隊列均在此范圍內(nèi))奴潘,如果滿足條件,就把它渲染到深度和法線紋理中奈虾。因此愚墓,要想讓物體能夠出現(xiàn)在深度和法線紋理中,就必須在Shader中設(shè)置正確的RenderType標簽岗照。

在Unity中,我們可以選擇讓一個攝像機生成一張深度紋理或是一張深度紋理+法線紋理迫吐。

  • 當選擇前者志膀,即只需要一張單獨的深度紋理時溉浙,Unity會直接獲取深度緩存或是按之前講到的著色器替換技術(shù)戳稽,選取需要的不透明物體互躬,并使用它投射陰影時使用的Pass(即LightMode被設(shè)置為ShadowCaster的Pass)來得到深度紋理吨铸。

    如果Shader中不包含這樣一個Pass诞吱,那么這個物體就不會出現(xiàn)在深度紋理中(當然,它也不能向其它物體投射陰影)咙俩。深度紋理的精度通常是24位或16位阿趁,這取決于深度緩存的精度脖阵。

  • 如果選擇生成一張深度+法線紋理,Unity會創(chuàng)建一張和屏幕分辨率相同悍募,精度為32位(每個通道為8位)的紋理坠宴,其中觀察空間下的法線信息會被編碼進紋理的R和G通道喜鼓,而深度信息會被編碼進B和A通道址晕。

    • 法線信息的獲取在延遲渲染中是可以非常容易得到的谨垃,Unity只需要合并深度和法線緩存即可刘陶。
    • 而在前向渲染中,默認情況下是不會創(chuàng)建法線緩存的纷责,因此Unity底層使用了一個單獨的Pass把整個場景再次渲染一遍來完成再膳。

如何獲取

獲取紋理

我們可以通過下面的代碼來獲取深度紋理:

camera.depthTextureMode=DepthTextureMode.Depth;

一旦設(shè)置好了上面的攝像機模式后,我們就可以在Shader中通過聲明_CameraDepthTexture變量來訪問它灾杰。這個過程非常簡單,但我們需要知道這兩行代碼的背后讲竿,Unity為我們做了很多工作。(上一節(jié)背后的原理)

同理膀捷,如果想要獲取深度+法線紋理秀仲,我們只需要在代碼中這樣設(shè)置:

camera.depthTextureMode=DepthTextureMode.DepthNormals;

我們還可以組合這些模式神僵,讓一個攝像機同時產(chǎn)生一張深度和深度+法線紋理:

camera.depthTextureMode|=DepthTextureMode.Depth;
camera.depthTextureMode|=DepthTextureMode.DepthNormals;

采樣

在Unity5中,我們還可以在攝像機的Camera組件上看到當前攝像機是否需要渲染深度或深度+法線紋理炮障。當在Shader中訪問到深度紋理_CameraDepthTexture后,我們就可以使用當前像素的紋理坐標對它進行采樣智末。絕大多數(shù)情況下系馆,我們使用tex2D函數(shù)采樣即可,但在某些平臺(例如PS3和PS2)上纵穿,我們需要一些特殊處理谓媒。Unity為我們提供了一個統(tǒng)一的宏SAMPLE_DEPTH_TEXTURE,用來處理這些由于平臺差異造成的問題抢野。而我們只需要在Shader中使用SAMPLE_DEPTH_TEXTURE宏對深度紋理進行采樣,例如:

float d = SAMPLE_DEPTH_TEXTURE ( _CameraDepthTexture,i.uv );

其中恃轩,i.uv是一個float2類型的變量松忍,對應(yīng)了當前像素的紋理坐標鸣峭。

類似的宏還有SAMPLE_DEPTH_TEXTURE_PROJ和SAMPLE_DEPTH_TEXTURE_LOD。

SAMPLE_DEPTH_TEXTURE_PROJ宏同樣接受兩個參數(shù)——深度紋理和一個float3或float4類型的紋理坐標更扁,它的內(nèi)部使用了tex2Dproj這樣的函數(shù)進行投影紋理采樣,紋理坐標的前兩個分量首先會除以最后一個分量膛薛,再進行紋理采樣哄啄。如果提供了第四個分量,還會進行一次比較锌半,通常用于陰影的實現(xiàn)中。SAMPLE_DEPTH_TEXTURE_PROJ的第二個參數(shù)通常是由頂點著色器輸出插值而得的屏幕坐標记焊,例如:

float d = SAMPLE_DEPTH_TEXTURE_PROJ ( _CameraDepthTexture , UNITY_PROJ_COORD ( i.srcPos ));

其中,i.srcPos是在頂點著色器中通過調(diào)用ComputeScreenPos(o.pos)得到的屏幕坐標捌归。

上述這些宏的定義,讀者可以在Unity內(nèi)置的HLSLSupport.cginc文件中找到巾兆。

線性轉(zhuǎn)換

當通過紋理采樣得到深度值后,這些深度值往往是非線性的圃伶,這種非線性來自于投射投影使用的裁剪矩陣。然而侥猩,我們在計算過程中通常是需要線性的深度值欺劳,也就是說,我們需要把投影后的深度值變換到線性空間下腔剂,例如視角空間下的深度值。那么湾碎,我們應(yīng)該如何進行這個轉(zhuǎn)換呢?

幸運的是溢陪,Unity提供了兩個輔助函數(shù)來為我們進行上述的計算過程——LinearEyeDepthLinear01Depth形真。LinerEyeDepth負責把深度紋理的采樣結(jié)果轉(zhuǎn)換到視角空間下的深度值。而Linear01Depth則會返回一個范圍在[0,1]的線性深度值蛾坯。這兩個函數(shù)內(nèi)部使用了內(nèi)置的_ZBufferParams變量來得到遠近裁剪平面的距離。

如果我們需要獲取深度+法線紋理,則可以直接使用tex2D函數(shù)對CameraDepthNormalsTexture進行采樣视事,得到里面存儲的深度和法線信息。Unity提供了輔助函數(shù)來為我們對這個采樣結(jié)果進行解碼虏辫,從而得到深度值和法線方向。這個函數(shù)是DecodeDepthNormal娄昆,它在UnityCG.cginc里被定義:

inline void DecodeDepthNormal(float4 enc,out float depth扒俯,out float3 normal)
{
    depth=DecodeFloatRG(enc.zw);
    normal=DecodeViewNormalStereo(enc);
}

DecodeDepthNormal的第一個參數(shù)是對深度+法線紋理的采樣結(jié)果夺姑,這個采樣結(jié)果是Unity對深度和法線信息編碼后的結(jié)果,它的xy分量存儲的是視角空間下的法線信息互纯,而深度信息被編碼進了zw分量瑟幕。通過調(diào)用DecodeDepthNormal函數(shù)對采樣結(jié)果解碼后,我們就可以得到解碼后的深度值和法線留潦。這個深度值是范圍在[0,1]的線性深度值(這與單獨的深度紋理中存儲的深度值不同)辣往,而得到的法線則是視角空間下的法線方向兔院。同樣,我們也可以通過調(diào)用DecodeFloatRGDecodeViewNormalStereo來解碼深度+法線紋理中的深度和法線信息站削。

再談運動模糊

上一章節(jié)坊萝,我們學(xué)習(xí)了如何通過混合多張屏幕圖像來模擬運動模糊的效果。但是许起,另一種應(yīng)用更加廣泛的技術(shù)則是使用速度映射圖十偶。速度映射圖中存儲了每個像素的速度,然后使用這個速度來決定模糊的方向和大小园细。速度緩沖的生成有多種方法:

  • 一種方法是把場景中所有物體的速度渲染到一張紋理中惦积。但這種方法的缺點在于需要修改場景中所有物體的Shader 代碼,使其添加計算速度的代碼并輸出到一個渲染紋理中猛频。
  • 另一種方法利用深度紋理在片元著色器中為每個像素計算其在世界空間下的位置狮崩,這是通過使用當前的視角*投影矩陣的逆矩陣對NDC下的頂點坐標進行變換得到的。當?shù)玫绞澜缈臻g中的頂點坐標后鹿寻,我們使用前一幀的視角*投影矩陣對其進行變換睦柴,得到該位置在前一幀中的NDC坐標。然后毡熏,我們計算前一幀和當前幀的位置差坦敌,生成該像素的速度。這種方法的優(yōu)點是可以在一個屏幕后處理步驟中完成整個效果的模擬痢法,但缺點是需要在片元著色器中進行兩次矩陣乘法的操作狱窘,對性能有所影響。

實現(xiàn)

  1. 新建一個腳本疯暑,名為MotionBlurWithDepthTexture.cs添加到攝像機上训柴。
  2. 在腳本中做以下幾點修改:
    • 定義一個變量來保存上一幀攝像機的視角*投影矩陣:private Matrix4x4 previousViewProjectionMatrix;
    • 在OnRenderImage函數(shù)中先需要計算和傳遞運動模糊中使用的各個屬性。本例需要使用兩個變換矩陣——前一幀的視角投影矩陣以及當前幀的視角投影矩陣的逆矩陣妇拯。因此幻馁,我們通過調(diào)用 camera.worldToCameraMatrixcamera.projectionMatrix來分別得到當前攝像機的視角矩陣和投影矩陣洗鸵。對它們相乘后取逆,得到當前幀的視角投影矩陣的逆矩陣*仗嗦,并傳遞給材質(zhì)膘滨。然后,我們把取逆前的結(jié)果存儲在previousViewProjectionMatrix變量中稀拐,以便在下一幀時傳遞給材質(zhì)的_PreviousViewProjectionMatrix屬性火邓。
  3. 新建UnityShader。Shader的重點在片元著色器中德撬。
    • 首先我們通過SAMPLE_DEPTH_TEXTURE宏對深度紋理進行采樣铲咨,獲得該像素的得到了深度值d
    • 由前面可知d是由NDC下的坐標映射而來的。我們想要構(gòu)建像素的NDC坐標H蜓洪,就需要把這個深度值重新映射回NDC纤勒。這個映射很簡單,只需要使用原映射的反函數(shù)即可隆檀,即d2-1摇天。同樣NDC的xy分量可以由像素的紋理坐標映射而來(NDC下的xyz分量范圍均為[-1,1])。
    • 當?shù)玫絅DC下的坐標H后恐仑,我們就可以使用當前幀的視角投影矩陣的逆矩陣對其進行變換泉坐,并把結(jié)果值除以它的w分量來得到世界空間下的坐標worldPos。
    • 一旦得到世界空間下的坐標裳仆,我們就可以使用前一幀的視角投影矩陣對它進行變換腕让,得到前一幀在NDC下的坐標previousPos。
    • 然后鉴逞,我們計算前一幀和當前幀在屏幕空間下的位置差记某,得到該像素的速度velocity。
    • 當?shù)玫皆撓袼氐乃俣群蠊辜瘢覀兙涂梢允褂迷撍俣戎祵λ泥徲蛳袼剡M行采樣液南,相加后取平均值得到一個模糊的效果。采樣時我們還是用了_BlurSize來控制采樣距離勾徽。

具體代碼實現(xiàn)見下文

全局霧效

霧效(Fog)是游戲里常見的一種效果滑凉。Unity內(nèi)置的霧效可以產(chǎn)生基于距離的線性或指數(shù)霧效。

Unity內(nèi)置霧效

想要在自己編寫的頂點/片元著色器中實現(xiàn)這些霧效喘帚,我們需要在Shader中添加#pragma multi_compile_fog指令畅姊,同時還要使用相關(guān)內(nèi)置宏,例如UNITY_FOG_COORDS吹由、UNITY_TRANSFER_FOG和UNITY_APPLY_FOG等若未。

這種方法的缺點在于,我們不僅需要為場景中的所有物體添加相關(guān)的渲染代碼倾鲫,而且能夠?qū)崿F(xiàn)的效果也非常有限粗合。當我們需要對霧效進行一些個性化操作時萍嬉,例如基于高度的霧效等,僅僅使用Unity內(nèi)置的霧效就變得不再可行隙疚。

屏幕后處理全局霧效

在本節(jié)中壤追,我們將會學(xué)習(xí)一種基于屏幕后處理的全局霧效的實現(xiàn)。使用這種方法供屉,我們不需要更改場景內(nèi)渲染物體所使用的Shader代碼行冰,而僅僅依靠一次屏幕后處理的步驟即可。這種方法的自由行很高伶丐,我們可以方便的模擬各種霧效悼做。例如均勻的霧效、基于距離的線性/指數(shù)霧效撵割、基于高度的霧效等贿堰。在學(xué)習(xí)完本節(jié)后,我們可以得到類似下圖的效果啡彬。

基于屏幕后處理的全局霧效的關(guān)鍵是,根據(jù)深度紋理來重建每個像素在世界空間下的位置故硅。

盡管在前面庶灿,我們在模擬運動模糊時已經(jīng)實現(xiàn)了這個要求,即構(gòu)建出當前像素的NDC坐標吃衅,再通過當前攝像機的視角投影矩陣的逆矩陣來得到世界空間下的像素坐標往踢,但是這樣的實現(xiàn)需要在片元著色器中進行矩陣乘法的操作,而這通常會影響游戲性能徘层。

在本節(jié)中峻呕,我們將會學(xué)習(xí)一個快速從深度紋理中重建世界坐標的方法。這種方法首先對圖像空間下的視椎體射線(從攝像機出發(fā)趣效,指向圖像上某點的射線)進行插值瘦癌,這條射線存儲了該像素在世界空間下到攝像機的方向信息。然后跷敬,我們把該射線和線性化后的視角空間下的深度值相乘讯私,再加上攝像機的世界位置,就可以得到該像素在世界空間下的位置西傀。當我們得到世界坐標后斤寇,就可以輕松地使用各個公式來模擬全局霧效了。

重建世界坐標

在開始動手寫代碼之前拥褂,我們首先來了解如何從深度紋理中重建世界坐標娘锁。我們知道,坐標系中的一個頂點坐標可以通過它相對于另一個頂點坐標的偏移量來求得饺鹃。重建像素的世界坐標也是基于這樣的思想莫秆,我們只需要知道攝像機在世界空間下的位置间雀,以及世界空間下該像素相對于攝像機的偏移量,把它們相加就可以得到該像素的世界坐標馏锡。整個過程可以使用下面的代碼來表示:

float4 worldPos=_WorldSpaceCameraPos+linearDepth*interpolatedRay;
  • _WorldSpaceCameraPos是攝像機在世界空間下的位置雷蹂,這可以由Unity的內(nèi)置變量直接訪問得到。

  • linearDepth*interpolatedRay則可以計算得到該像素相對于攝像機的偏移量

    • linearDepth是由深度紋理得到的線性深度值杯道,
    • interpolatedRay是由頂點著色器輸出并插值后得到的射線匪煌,它不僅包含了該像素到攝像機的方向,也包含了距離信息党巾。

    linearDepth的獲取我們已經(jīng)在前面詳細解釋過了萎庭,本節(jié)著重解釋interpolatedRay的求法。

interpolatedRay來源于對近裁剪平面的4個角的某個特定向量的插值齿拂,這4個向量包含了它們到攝像機的方向和距離信息驳规,我們可以利用攝像機的近裁剪平面距離、FOV署海、橫縱比計算而得吗购。下圖顯示了計算時使用的一些輔助向量。

為了方便計算砸狞,我們可以先計算兩個向量——toTop和toRight捻勉,它們是起點位于近裁剪面中心、分別指向攝像機正上方和正右方的向量刀森。它們的計算公式如下:

其中踱启,Near是近裁剪平面的距離,F(xiàn)OV是豎直方向的視角范圍研底,camera.up埠偿、camera.right分別對應(yīng)了攝像機的正上方和正右方。

當?shù)玫竭@兩個輔助向量后榜晦,我們就可以計算四個角相對于攝像機的方向了冠蒋。我們以左上角為例(見上圖中的TL點),它的計算公式如下:

讀者可以依靠基本的矢量運算驗證上面的結(jié)果芽隆。同理浊服,其它3個角的計算也是類似的:

注意,上面求得的4個向量不僅包含了方向信息胚吁,它們的模對應(yīng)了4個點到攝像機的空間距離牙躺。由于我們得到的線性深度值并非是點到攝像機的歐氏距離,而是在z方向的距離腕扶。因此我們不能直接使用深度值和4個角的單位方向的乘積來計算它們到攝像機的偏移量孽拷,如下圖所示:

想要把深度值轉(zhuǎn)換成到攝像機的歐氏距離也很簡單,我們以TL點為例半抱,根據(jù)相似三角形原理脓恕,TL所在的射線上膜宋,像素的深度值和它到攝像機的實際距離的比等于近裁剪平面的距離和TL向量的模的比,即:

由此可得炼幔,我們需要的TL距離攝像機的歐式距離dist:

由于4個點相互對稱秋茫,因此其他3個向量的模和TL相等,即我們可以使用同一個因子和單位向量相乘乃秀,得到它們對應(yīng)的向量值:

屏幕后處理的原理是使用特定的材質(zhì)去渲染一個剛好填充整個屏幕的四邊形面片肛著。這個四邊形面片的4個頂點就對應(yīng)了近裁剪平面的4個角。因此跺讯,我們可以把上面的計算結(jié)果傳遞給頂點著色器枢贿,頂點著色器根據(jù)當前的位置選擇它所對應(yīng)的的向量,然后再將其輸出刀脏,經(jīng)插值后傳遞給片元著色器得到interpolatedRay局荚,我們就可以直接利用本節(jié)一開始提到的公式重建該像素在世界空間下的位置了。

霧的計算

在簡單的霧效實現(xiàn)中愈污,我們需要計算一個霧效系數(shù)f耀态,作為混合原始顏色和霧的顏色的混合系數(shù):

float3 afterFog = f * fogColor + ( 1-f ) * origColor;

這個霧效系數(shù)f有很多計算方法暂雹。在Unity內(nèi)置的霧效實現(xiàn)中茫陆,支持3種霧的計算方式——線性(Linear)、指數(shù)(Exponential)以及指數(shù)的平方(Exponential Square的)擎析。當給定距離z后,f的計算公式分別如下:

Linear:

dmin和dmax分別表示受霧影響的最小距離和最大距離挥下。

Exponential:

d是控制霧的濃度的參數(shù)揍魂。

Exponential Squared:

d是控制霧的濃度的參數(shù)。

在本節(jié)中棚瘟,我們將使用類似線性霧的計算方式现斋,計算基于高度的霧效。具體方法是偎蘸,當給定一點在世界空間下的高度y后庄蹋,f的計算公式為:

Hstart和Hend分別表示受霧影響的起始高度和終止高度。

實現(xiàn)

  1. 新建一個腳本迷雪,名為FogWithDepthTexture.cs添加到攝像機上限书。

  2. 在腳本中做以下幾點修改:

    • 在本節(jié)中,我們需要獲取攝像機的相關(guān)參數(shù)章咧,例如近裁剪平面的距離倦西、FOV等,同時還需要獲取攝像機在世界空間下的前方赁严、上方和右方等方向扰柠,因此我們用兩個變量存儲攝像機的Camera和Transform組件
    • 定義模擬霧效時使用的各個參數(shù):ogDensity用于控制霧的濃度粉铐,fogColor用于控制霧的顏色。我們使用的霧效模擬函數(shù)是基于高度的卤档,因此fogStart用于控制霧效的起始高度蝙泼,fogEnd用于控制霧效的終止高度。
    • 最后劝枣,我們實現(xiàn)了OnRenderImage函數(shù):OnRenderImage首先計算了近裁剪平面的4個角對應(yīng)的向量汤踏,并把它們存儲在一個矩陣類型的變量(frustumCorners)中。計算過程我們在前面已經(jīng)解釋過了哨免,代碼只是套用之前講過的公式而已茎活。我們按一定順序把這4個方向存儲到了frustumCorners不同的行中,這個順序是非常重要的琢唾,因為這決定了我們在頂點著色器中使用哪一行作為該點的待插值向量载荔。隨后我們把結(jié)果和其它參數(shù)傳遞給材質(zhì),并調(diào)用Graphics.Blit (src, dest, material)把渲染結(jié)果顯示到屏幕上采桃。
  3. 新建UnityShader懒熙。Shader的重點在片元著色器中。

    • 聲明代碼中需要使用的各個變量:_FrustumCornersRay雖然沒有在Properties中聲明普办,但仍可以由腳本傳遞給Shader工扎。除了Properties中聲明的各個屬性,我們還聲明了_CameraDepthTexture衔蹲,Unity會在背后把得到的深度紋理傳遞給該值肢娘。

    • 在v2f結(jié)構(gòu)體中,我們除了定義頂點位置舆驶、屏幕圖像和深度紋理的紋理坐標外橱健,還定義了interpolatedRay變量存儲插值后的像素向量。

    • 在頂點著色器中沙廉,我們對深度紋理的采樣坐標進行了平臺差異化處理拘荡。更重要的是,我們要決定該點對應(yīng)了4個角中的那個角撬陵。我們采用的方法是判斷它的紋理坐標珊皿。

      不同平臺的紋理坐標不一定是滿足上述條件的,例如DirectX和Metal這樣的平臺巨税,左上角對應(yīng)了(0蟋定,0)點,但大多數(shù)情況下垢夹,Unity會把這些平臺下的屏幕圖像進行翻轉(zhuǎn)溢吻,因此,我們?nèi)匀豢梢岳眠@個條件。但如果在類似DirectX的平臺上開啟了抗鋸齒促王,Unity就不會進行這個翻轉(zhuǎn)犀盟。

      為了此時仍然可以得到相應(yīng)頂點位置的索引值,我們對索引值也進行了平臺差異化處理蝇狼,以便在必要時也對索引值進行翻轉(zhuǎn)阅畴。最后我們使用索引值來獲取_FrustumCornersRay中對應(yīng)的行作為該頂點的interpolatedRay值。

    • 在片元著色器中

      • 首先迅耘,我們需要重建該像素在世界空間中的位置贱枣。為此我們首先使用SAMPLE_DEPTH_TEXTURE對深度紋理進行采樣,再使用LinearEyeDepth得到視角空間下的線性深度值颤专。之后與interpolatedRay相乘后再和世界空間下的攝像機位置相加纽哥,即可得到世界空間下的位置。
      • 得到世界坐標后栖秕,模擬霧效就變得非常容易春塌。在本例中满粗,我們選擇實現(xiàn)基于高度的霧效模擬驻啤。我們根據(jù)材質(zhì)屬性_FogEnd和_FogStart計算當前的像素高度worldPo.y對應(yīng)的霧效系數(shù)fogDensity憨愉,再和參數(shù)_FogDensity相乘后悯姊,利用saturate函數(shù)截取到[0,1]范圍內(nèi),作為最后的霧效系數(shù)腹尖。
      • 最后根资,我們使用該系數(shù)將霧的顏色和原始顏色進行混合后返回盼樟。

具體代碼實現(xiàn)見下文事格。

本節(jié)介紹的使用深度紋理重建像素的世界坐標的方法是非常有用的惕艳。但需要注意的是,這里的實現(xiàn)是基于攝像機的投影類型是透視投影的前提下驹愚。如果需要在正交投影的情況下重建世界坐標需要使用不同的公式尔艇。

再談邊緣檢測

在前面,我們曾介紹如何使用Sobel算子對屏幕進行邊緣檢測么鹤,實現(xiàn)描邊的效果。但是味廊,這種直接利用顏色信息進行邊緣檢測的方法會產(chǎn)生很多我們不希望得到的邊緣線蒸甜,如下圖所示:

在本節(jié)中,我們將學(xué)習(xí)如何在深度和法線紋理上進行邊緣檢測余佛,這些圖像不會受紋理和光照的影響柠新,而僅僅保存了當前渲染物體的模型信息,通過這種方式檢測出來的邊緣更加可靠辉巡。學(xué)習(xí)完本節(jié)后恨憎,我們可以得到類似下圖的效果:

與前面使用的Sobel算子不同,本節(jié)將使用Roberts算子來進行邊緣檢測。它使用的卷積核如下圖所示:

Roberts算子的本質(zhì)就是計算左上角和右下角的差值憔恳,右上角和左下角的差值瓤荔,作為評估邊緣的依據(jù)。在下面的實現(xiàn)中钥组,我們也會按這樣的方式输硝,取對角方向的深度或法線值,比較它們之間的差值程梦,如果超過某個閾值(可由參數(shù)控制)点把,就認為他們之間存在一條邊。

實現(xiàn)

  1. 新建一個腳本屿附,名為EdgeDetectNormalsAndDepth.cs添加到攝像機上郎逃。

  2. 在腳本中提供了調(diào)整邊緣線強度描邊顏色以及背景顏色的參數(shù)。同時添加了控制采樣距離以及對深度和法線進行邊緣檢測時的靈敏度參數(shù):

    • sampleDistance用于控制對深度+法線紋理采樣時挺份,使用的采樣距離褒翰。從視覺上來看,sampleDistance值越大压恒,描邊越寬影暴。
    • sensitivityDepth 和sensitivityNormals將會影響當鄰域的深度值或法線值相差多少時,會被認為存在一條邊界探赫。如果把靈敏度跳得很大型宙,那么可能即使是深度或法線上很小的變化也會形成一條邊。
  3. 這里我們?yōu)?strong>OnRenderImage函數(shù)添加了[ImageEffectOpaque]屬性伦吠。我們希望在不透明的Pass(即渲染隊列小于等于2500的Pass妆兑,內(nèi)置的Background、Geometry和AlphaTest渲染隊列均在此范圍內(nèi))執(zhí)行完畢后立即調(diào)用該函數(shù)毛仪,而不對透明物體(渲染隊列為Transparent的Pass)產(chǎn)生影響搁嗓,該屬性幫助我們實現(xiàn)了這樣目的

  4. 新建UnityShader。在Shader我們做了以下的主要修改箱靴。

    • 我們在v2f結(jié)構(gòu)體中定義一個維數(shù)為5的紋理坐標數(shù)組腺逛。這個數(shù)組的第一個坐標存儲了屏幕顏色圖像的采樣紋理。數(shù)組中剩余的4個坐標則存儲了使用Roberts算子時需要采樣的紋理坐標衡怀,我們還使用了_SampleDistance來控制采樣距離棍矛。

      通過計算采樣紋理坐標的代碼從片元著色器中轉(zhuǎn)移到頂點著色器中,可以減少運算抛杨,提高性能够委。由于從頂點著色器到片元著色器的插值是線性的,因此這樣的轉(zhuǎn)移并不會影響紋理坐標的計算結(jié)果怖现。

    • 在片元著色器中茁帽,我們首先使用4個紋理坐標對深度+法線紋理進行采樣,再調(diào)用CheckSame函數(shù)來分別計算對角線上兩個紋理值的差值。當通過CheckSame函數(shù)得到邊緣信息后潘拨,片元著色器就會利用該值進行顏色混合吊输。

    • CheckSame函數(shù)中,首先對輸入?yún)?shù)進行處理战秋,得到兩個采樣點的法線和深度值璧亚。值得注意的是脂信,這里我們并沒有解碼得到真正的法線值癣蟋,而是直接使用了xy分量。這是因為我們只需要比較兩個采樣值之間的差異度狰闪,而不需要知道他們真正的法線值疯搅。

      然后,我們把兩個采樣點的對應(yīng)值相減并取絕對值埋泵,再乘以靈敏度參數(shù)幔欧,把差異值的每個分量相加再和一個閾值比較,如果他們的和小于閾值丽声,則返回1礁蔗,說明差異不明顯,不存在一條邊界雁社;否則浴井,返回0。最后我們把法線和深度的檢查結(jié)果相乘霉撵,作為組合后的返回值磺浙。

具體代碼實現(xiàn)見下文。

特定物體描邊

本節(jié)實現(xiàn)的描邊效果是基于整個屏幕空間進行的徒坡,也就是說撕氧,場景內(nèi)的所有物體都會被添加描邊效果。但有時喇完,我們希望只對特定的物體進行描邊伦泥,例如當玩家選中場景中的某個物體后,我們想要在該物體周圍添加一層描邊效果锦溪。這時我們可以使用Unity提供的Graphics.DrawMeshGraphics.DrawMeshNow函數(shù)把需要描邊的物體再渲染一遍(在所有不透明物體渲染完畢后)奄喂,然后再使用本節(jié)提到的邊緣檢測算法計算深度或法線紋理中每個像素的梯度值,判斷它們是否小于某個閾值海洼,如果是,就在Shader中使用clip()函數(shù)將該像素剔除掉富腊,從而顯示出原來物體的顏色坏逢。

代碼實現(xiàn)

運動模糊

using UnityEngine;
using System.Collections;

public class MotionBlurWithDepthTexture : PostEffectsBase
{
    public Shader motionBlurShader;
    private Material motionBlurMaterial = null;

    public Material material
    {
        get
        {
            motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
            return motionBlurMaterial;
        }
    }

    private Camera myCamera;
    public new Camera camera
    {
        get
        {
            if (myCamera == null)
            {
                myCamera = GetComponent<Camera>();
            }
            return myCamera;
        }
    }

    // 模糊大小
    [Range(0.0f, 1.0f)]
    public float blurSize = 0.5f;

    /// <summary>前一幀攝像機視角*投影矩陣</summary>
    private Matrix4x4 previousViewProjectionMatrix;

    void OnEnable()
    {
        // 設(shè)置攝像機的狀態(tài) = 產(chǎn)生深度紋理
        camera.depthTextureMode |= DepthTextureMode.Depth;
        // 當前幀的視角*投影矩陣
        previousViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
    }

    void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (material != null)
        {
            material.SetFloat("_BlurSize", blurSize);
            // 傳遞前一幀的視角*投影矩陣
            material.SetMatrix("_PreviousViewProjectionMatrix", previousViewProjectionMatrix);
            // 當前幀的視角*投影矩陣、其逆矩陣
            Matrix4x4 currentViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
            Matrix4x4 currentViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse;
            // 傳遞當前幀的視角*投影矩陣的逆矩陣
            material.SetMatrix("_CurrentViewProjectionInverseMatrix", currentViewProjectionInverseMatrix);
            previousViewProjectionMatrix = currentViewProjectionMatrix;

            Graphics.Blit(src, dest, material);
        }
        else
        {
            Graphics.Blit(src, dest);
        }
    }
}

Shader "Unity Shaders Book/Chapter 13/Motion Blur With Depth Texture"
{
    Properties
    {
        _MainTex ("Base (RGB)", 2D) = "white" { }
        // 模糊大小
        _BlurSize ("Blur Size", Float) = 1.0
    }
    SubShader
    {
        CGINCLUDE
        
        #include "UnityCG.cginc"
        
        sampler2D _MainTex;
        half4 _MainTex_TexelSize;
        // 深度紋理
        sampler2D _CameraDepthTexture;
        // 當前幀的視角*投影矩陣的逆矩陣
        float4x4 _CurrentViewProjectionInverseMatrix;
        // 前一幀的視角*投影矩陣
        float4x4 _PreviousViewProjectionMatrix;
        half _BlurSize;
        
        struct v2f
        {
            float4 pos: SV_POSITION;
            half2 uv: TEXCOORD0;
            half2 uv_depth: TEXCOORD1;
        };
        
        v2f vert(appdata_img v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            
            o.uv = v.texcoord;
            o.uv_depth = v.texcoord;
            
            // 檢查DirectX平臺
            #if UNITY_UV_STARTS_AT_TOP
                if (_MainTex_TexelSize.y < 0)
                    o.uv_depth.y = 1 - o.uv_depth.y;
            #endif
            
            return o;
        }
        
        fixed4 frag(v2f i): SV_Target
        {
            // 獲取該像素處的深度緩沖值。使用SAMPLE_DEPTH_TEXTURE宏對深度紋理進行采樣
            float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
            #if defined(UNITY_REVERSED_Z)
                d = 1.0 - d;
            #endif
            // 計算得到NDC下的坐標H是整,通過原映射的反函數(shù)得到NDC肖揣,即d2-1。NDC的xy分量可以由像素的紋理坐標映射而來浮入。
            float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1);
            // 使用當前幀的視角投影矩陣的逆矩陣對其進行變換
            float4 D = mul(_CurrentViewProjectionInverseMatrix, H);
            // 除以分量w得到世界空間下的位置
            float4 worldPos = D / D.w;
            
            // 當前幀在屏幕空間下的位置
            float4 currentPos = H;

            // 使用世界空間下坐標龙优,并通過上一幀的視角*投影矩陣進行轉(zhuǎn)換。
            float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos);
            // 前一幀在屏幕空間下的坐標事秀。
            previousPos /= previousPos.w;
            
            // 計算速度 使用該幀的位置和最后一幀的位置來計算像素速度彤断。
            float2 velocity = (currentPos.xy - previousPos.xy) / 2.0;
            
            float2 uv = i.uv;
            float4 c = tex2D(_MainTex, uv);
            
            uv += velocity * _BlurSize;
            // 使用速度值,對該像素鄰域像素進行采樣疊加易迹,最后取平均值
            for (int it = 1; it < 3; it ++, uv += velocity * _BlurSize)
            {
                float4 currentColor = tex2D(_MainTex, uv);
                c += currentColor;
            }
            c /= 3;
            
            return fixed4(c.rgb, 1.0);
        }
        
        ENDCG
        
        Pass
        {
            ZTest Always Cull Off ZWrite Off
            
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            
            ENDCG
            
        }
    }
    FallBack Off
}

全局霧效

using UnityEngine;
using System.Collections;

public class FogWithDepthTexture : PostEffectsBase
{
    public Shader fogShader;
    private Material fogMaterial = null;

    public Material material
    {
        get
        {
            fogMaterial = CheckShaderAndCreateMaterial(fogShader, fogMaterial);
            return fogMaterial;
        }
    }
    // 用于存儲相機Camera組件
    private Camera myCamera;
    public new Camera camera
    {
        get
        {
            if (myCamera == null)
            {
                myCamera = GetComponent<Camera>();
            }
            return myCamera;
        }
    }

    // 用于存儲相機Transform組件
    private Transform myCameraTransform;
    public Transform cameraTransform
    {
        get
        {
            if (myCameraTransform == null)
            {
                myCameraTransform = camera.transform;
            }

            return myCameraTransform;
        }
    }

    /// <summary>霧效濃度</summary>
    [Range(0.0f, 3.0f)]
    public float fogDensity = 1.0f;
    /// <summary>霧效顏色</summary>
    public Color fogColor = Color.white;
    /// <summary>霧效起始高度</summary>
    public float fogStart = 0.0f;
    /// <summary>霧效終止高度</summary>
    public float fogEnd = 2.0f;

    void OnEnable()
    {
        // 設(shè)置攝像機的狀態(tài) = 產(chǎn)生深度紋理
        camera.depthTextureMode |= DepthTextureMode.Depth;
    }

    void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (material != null)
        {
            Matrix4x4 frustumCorners = Matrix4x4.identity;

            // FOV是相機的豎直方向的視角范圍
            float fov = camera.fieldOfView;
            // Near是相機的近裁剪平面的距離
            float near = camera.nearClipPlane;
            // Aspect是相機的寬高比(寬度除以高度)
            float aspect = camera.aspect;

            // 近裁剪面中心
            float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
            // 相機正右方
            Vector3 toRight = cameraTransform.right * halfHeight * aspect;
            // 相機正上方
            Vector3 toTop = cameraTransform.up * halfHeight;

            // 左上角相對于相機的方向
            Vector3 topLeft = cameraTransform.forward * near + toTop - toRight;
            
            float scale = topLeft.magnitude / near;

            topLeft.Normalize();
            // 左上角距離攝像機的歐式距離dist
            topLeft *= scale;

            Vector3 topRight = cameraTransform.forward * near + toRight + toTop;
            topRight.Normalize();
            topRight *= scale;

            Vector3 bottomLeft = cameraTransform.forward * near - toTop - toRight;
            bottomLeft.Normalize();
            bottomLeft *= scale;

            Vector3 bottomRight = cameraTransform.forward * near + toRight - toTop;
            bottomRight.Normalize();
            bottomRight *= scale;

            // 將近裁剪平面的四個角對應(yīng)的向量存儲到矩陣類型的變量中(存入順序是非常重要的)
            frustumCorners.SetRow(0, bottomLeft);
            frustumCorners.SetRow(1, bottomRight);
            frustumCorners.SetRow(2, topRight);
            frustumCorners.SetRow(3, topLeft);

            // 將參數(shù)寫入材質(zhì)
            material.SetMatrix("_FrustumCornersRay", frustumCorners);

            material.SetFloat("_FogDensity", fogDensity);
            material.SetColor("_FogColor", fogColor);
            material.SetFloat("_FogStart", fogStart);
            material.SetFloat("_FogEnd", fogEnd);

            Graphics.Blit(src, dest, material);
        }
        else
        {
            Graphics.Blit(src, dest);
        }
    }
}

Shader "Unity Shaders Book/Chapter 13/Fog With Depth Texture"
{
    Properties
    {
        _MainTex ("Base (RGB)", 2D) = "white" { }
        // 霧效濃度
        _FogDensity ("Fog Density", Float) = 1.0
        // 霧效顏色
        _FogColor ("Fog Color", Color) = (1, 1, 1, 1)
        // 霧效起始高度
        _FogStart ("Fog Start", Float) = 0.0
        // 霧效終止高度
        _FogEnd ("Fog End", Float) = 1.0
    }
    SubShader
    {
        CGINCLUDE
        
        #include "UnityCG.cginc"
        
        // 近裁剪平面的四個角對應(yīng)的向量
        float4x4 _FrustumCornersRay;
        
        sampler2D _MainTex;
        half4 _MainTex_TexelSize;
        // 深度紋理宰衙,用于Unity背后傳遞該值
        sampler2D _CameraDepthTexture;
        half _FogDensity;
        fixed4 _FogColor;
        float _FogStart;
        float _FogEnd;
        
        struct v2f
        {
            float4 pos: SV_POSITION;
            half2 uv: TEXCOORD0;
            half2 uv_depth: TEXCOORD1;
            // 用于存儲插值后的像素向量
            float4 interpolatedRay: TEXCOORD2;
        };
        
        v2f vert(appdata_img v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            
            o.uv = v.texcoord;
            o.uv_depth = v.texcoord;
            
            // 檢測DirectX平臺
            #if UNITY_UV_STARTS_AT_TOP
                // 檢測Unity是否已自動翻轉(zhuǎn)了坐標
                if (_MainTex_TexelSize.y < 0)
                    o.uv_depth.y = 1 - o.uv_depth.y;
            #endif
            
            int index = 0;
            if(v.texcoord.x < 0.5 && v.texcoord.y < 0.5)
            {
                index = 0;
            }
            else if(v.texcoord.x > 0.5 && v.texcoord.y < 0.5)
            {
                index = 1;
            }
            else if(v.texcoord.x > 0.5 && v.texcoord.y > 0.5)
            {
                index = 2;
            }
            else
            {
                index = 3;
            }
            
            // 檢測DirectX平臺
            #if UNITY_UV_STARTS_AT_TOP
                // 檢測Unity是否已自動翻轉(zhuǎn)了坐標
                if (_MainTex_TexelSize.y < 0)
                    index = 3 - index;
            #endif
            
            // 使用索引值獲取四個頂點變量中對應(yīng)頂點作為插值后的像素向量
            o.interpolatedRay = _FrustumCornersRay[index];
            
            return o;
        }
        
        fixed4 frag(v2f i): SV_Target
        {
            // 視角空間下的線性深度值  LinearEyeDepth:線性轉(zhuǎn)換 SAMPLE_DEPTH_TEXTURE:深度紋理采樣
            float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth));
            // 世界空間下的深度值    _WorldSpaceCameraPos:世界空間下相機位置
            float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;
            
            // 高度霧效系數(shù) = (終止高度 - 當前像素高度)/(終止高度 - 起始高度)
            float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);
            // 計算濃度     saturate:截取到0-1
            fogDensity = saturate(fogDensity * _FogDensity);
            
            // 原始顏色
            fixed4 finalColor = tex2D(_MainTex, i.uv);
            // 插值原始顏色,與霧效顏色睹欲,使用霧效系數(shù)作為參數(shù)
            finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);
            
            return finalColor;
        }
        
        ENDCG
        
        Pass
        {
            ZTest Always Cull Off ZWrite Off
            
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            
            ENDCG
            
        }
    }
    FallBack Off
}

邊緣檢測

using UnityEngine;
using System.Collections;

public class EdgeDetectNormalsAndDepth : PostEffectsBase
{
    public Shader edgeDetectShader;
    private Material edgeDetectMaterial = null;
    public Material material
    {
        get
        {
            edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
            return edgeDetectMaterial;
        }
    }
    // 描邊強度
    [Range(0.0f, 1.0f)]
    public float edgesOnly = 0.0f;
    // 描邊顏色
    public Color edgeColor = Color.black;
    // 背景色
    public Color backgroundColor = Color.white;
    // 用于控制深度+發(fā)現(xiàn)紋理采樣時供炼,使用的采樣距離。視覺上值越大窘疮,描邊越寬
    public float sampleDistance = 1.0f;
    // 用于控制判定描邊的深度差袋哼,值越大,越容易出現(xiàn)描邊
    public float sensitivityDepth = 1.0f;
    // 用于控制判斷描邊的法線差闸衫,值越大涛贯,越容易出現(xiàn)描邊
    public float sensitivityNormals = 1.0f;

    void OnEnable()
    {
        // 設(shè)置相機狀態(tài) = 獲取深度和發(fā)現(xiàn)
        GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
    }

    [ImageEffectOpaque]// 聲明函數(shù)會在所有的不透明的Pass執(zhí)行完畢后立即調(diào)用(避免透明物體的影響)
    void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (material != null)
        {
            // 寫入?yún)?shù)
            material.SetFloat("_EdgeOnly", edgesOnly);
            material.SetColor("_EdgeColor", edgeColor);
            material.SetColor("_BackgroundColor", backgroundColor);
            material.SetFloat("_SampleDistance", sampleDistance);
            material.SetVector("_Sensitivity", new Vector4(sensitivityNormals, sensitivityDepth, 0.0f, 0.0f));

            Graphics.Blit(src, dest, material);
        }
        else
        {
            Graphics.Blit(src, dest);
        }
    }
}
Shader "Unity Shaders Book/Chapter 13/Edge Detection Normals And Depth"
{
    Properties
    {
        _MainTex ("Base (RGB)", 2D) = "white" { }
        // 描邊強度
        _EdgeOnly ("Edge Only", Float) = 1.0
        // 描邊顏色
        _EdgeColor ("Edge Color", Color) = (0, 0, 0, 1)
        // 背景色
        _BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)
        // 用于控制深度+發(fā)現(xiàn)紋理采樣時,使用的采樣距離楚堤。
        _SampleDistance ("Sample Distance", Float) = 1.0
        // xy對應(yīng)了法線和深度檢測的靈敏度
        _Sensitivity ("Sensitivity", Vector) = (1, 1, 1, 1)
    }
    SubShader
    {
        CGINCLUDE
        
        #include "UnityCG.cginc"
        
        sampler2D _MainTex;
        half4 _MainTex_TexelSize;
        fixed _EdgeOnly;
        fixed4 _EdgeColor;
        fixed4 _BackgroundColor;
        float _SampleDistance;
        half4 _Sensitivity;
        
        // 用于獲取深度和法線的紋理
        sampler2D _CameraDepthNormalsTexture;
        
        struct v2f
        {
            float4 pos: SV_POSITION;

            // 第一個坐標存儲了屏幕顏色圖像的采樣紋理
            // 后面四個坐標存儲了使用Roberts算子時需要采樣的紋理坐標疫蔓。
            half2 uv[5]: TEXCOORD0;
        };
        
        v2f vert(appdata_img v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            
            // 對紋理進行采樣,并放入第一個坐標中
            half2 uv = v.texcoord;
            o.uv[0] = uv;
            
            #if UNITY_UV_STARTS_AT_TOP
                if (_MainTex_TexelSize.y < 0)
                    uv.y = 1 - uv.y;
            #endif
            
            // 對紋理領(lǐng)域進行采樣身冬,并用_SampleDistance控制采樣距離
            o.uv[1] = uv + _MainTex_TexelSize.xy * half2(1, 1) * _SampleDistance;
            o.uv[2] = uv + _MainTex_TexelSize.xy * half2(-1, -1) * _SampleDistance;
            o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 1) * _SampleDistance;
            o.uv[4] = uv + _MainTex_TexelSize.xy * half2(1, -1) * _SampleDistance;
            
            return o;
        }
        
        half CheckSame(half4 center, half4 sample)
        {
            // 獲取法線近似值
            half2 centerNormal = center.xy;
            // DecodeFloatRG:解碼RG顏色到float
            float centerDepth = DecodeFloatRG(center.zw);
            half2 sampleNormal = sample.xy;
            float sampleDepth = DecodeFloatRG(sample.zw);
            
            // 法線差異 = 兩個采樣點對應(yīng)值相減后乘以靈敏度
            half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x;
            // 將分量相加與閾值比較來判斷是否存在邊界
            int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;
           
           // 深度差異 = 兩個采樣點對應(yīng)值相減后乘以靈敏度
            float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
            // 將深度差異與閾值比較得到是否存在邊界結(jié)果
            int isSameDepth = diffDepth < 0.1 * centerDepth;
            
            // 將深度與法線結(jié)果相乘得到最終結(jié)果
            // return:
            // 1 - 如果法線和深度足夠相似衅胀,即不存在邊界
            // 0 - 否則
            return isSameNormal * isSameDepth ? 1.0: 0.0;
        }
        
        fixed4 fragRobertsCrossDepthAndNormal(v2f i): SV_Target
        {
            // 使用紋理坐標,對深度+法線紋理進行采樣
            half4 sample1 = tex2D(_CameraDepthNormalsTexture, i.uv[1]);
            half4 sample2 = tex2D(_CameraDepthNormalsTexture, i.uv[2]);
            half4 sample3 = tex2D(_CameraDepthNormalsTexture, i.uv[3]);
            half4 sample4 = tex2D(_CameraDepthNormalsTexture, i.uv[4]);
            
            half edge = 1.0;
            
            // 計算對角線兩個紋理的插值     CheckSame:返回0或1
            edge *= CheckSame(sample1, sample2);
            edge *= CheckSame(sample3, sample4);
            
            // 插值描邊顏色
            fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[0]), edge);
            // 插值背景顏色
            fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
            
            // 插值描邊與背景顏色酥筝,由描邊強度控制
            return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
        }
        
        ENDCG
        
        Pass
        {
            ZTest Always Cull Off ZWrite Off
            
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment fragRobertsCrossDepthAndNormal
            
            ENDCG
            
        }
    }
    FallBack Off
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末滚躯,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子嘿歌,更是在濱河造成了極大的恐慌掸掏,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宙帝,死亡現(xiàn)場離奇詭異丧凤,居然都是意外死亡,警方通過查閱死者的電腦和手機步脓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門愿待,熙熙樓的掌柜王于貴愁眉苦臉地迎上來浩螺,“玉大人,你說我怎么就攤上這事仍侥∫觯” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵农渊,是天一觀的道長患蹂。 經(jīng)常有香客問我,道長砸紊,這世上最難降的妖魔是什么传于? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮批糟,結(jié)果婚禮上格了,老公的妹妹穿的比我還像新娘。我一直安慰自己徽鼎,他們只是感情好盛末,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著否淤,像睡著了一般悄但。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上石抡,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天檐嚣,我揣著相機與錄音,去河邊找鬼啰扛。 笑死嚎京,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的隐解。 我是一名探鬼主播鞍帝,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼煞茫!你這毒婦竟也來了帕涌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤续徽,失蹤者是張志新(化名)和其女友劉穎蚓曼,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钦扭,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡纫版,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了客情。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片其弊。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡会涎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瑞凑,到底是詐尸還是另有隱情,我是刑警寧澤概页,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布籽御,位于F島的核電站,受9級特大地震影響惰匙,放射性物質(zhì)發(fā)生泄漏技掏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一项鬼、第九天 我趴在偏房一處隱蔽的房頂上張望哑梳。 院中可真熱鬧,春花似錦绘盟、人聲如沸鸠真。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吠卷。三九已至,卻和暖如春沦零,著一層夾襖步出監(jiān)牢的瞬間祭隔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工路操, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留疾渴,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓屯仗,卻偏偏與公主長得像搞坝,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子祭钉,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容