Unity Shader深度相關知識總結(jié)與效果實現(xiàn)
鳴謝:puppet_master (VIA CSDN)貢獻此文
前言
前言廢話依舊比較多,感覺我是個寫游戲體驗評測的,233。最近想起了《惡靈附身》這款游戲的幾個效果:
《惡靈附身》整款游戲都是在一個“瘋子”擼總的腦洞世界里面,游戲內(nèi)容相當恐怖(嚇得我當年一邊尖叫一邊玩,不光把我嚇夠嗆,把我室友也嚇壞了),有“貞子”,“保險箱怪”等等至今讓我久久不能忘懷的Boss,不過整個游戲既有恐怖的地方,又有刺激的戰(zhàn)斗,非常符合三上真司一貫的作風(我是三上生化危機系列的鐵粉,哇咔咔),可惜《惡靈附身2》玩法有點轉(zhuǎn)型,類似《喪尸圍城》了,前半段很好,后半段不知是否是經(jīng)費不足,感覺整體不如前半段好。不過還是很期待續(xù)作的。
既然整款游戲都是在腦洞世界里面,所以整個游戲的過程完全不按照常理出牌,可能前一秒還在平靜的醫(yī)院走廊,下一秒直接就直接切換到滿是怪物的場景。整個游戲里面大量運用了各種好玩的效果,上面的屏幕扭曲,屏幕掃描波,高度霧效(個人感覺《惡靈附身》里面的應該是特效做的,不過本文的實現(xiàn)方式不太一樣罷了)就都其中之一,今天主要是來整理一波深度圖的各種知識點,然后做幾個好玩的效果。
簡介
深度是個好東西哇,很多效果都需要深度,比如景深,屏幕空間掃描效果,軟粒子,陰影,SSAO,近似次表面散射(更確切的說是透射),對于延遲渲染來說,還可以用深度反推世界空間位置降低帶寬消耗,還可以用深度做運動模糊,屏幕空間高度霧,距離霧,部分Ray-Marching效果也都需要深度,可以說,深度是一些渲染高級效果必要的條件。另一方面,光柵化渲染本身可以得到正確的效果,就與深度(Z Buffer)有著密不可分的關系。
深度對于實時渲染的意義十分重大,OpenGL,DX,Unity為我們封裝好了很多深度相關的內(nèi)容,如ZTest,ZWrite,CameraDepthMode,Linear01Depth等等。今天我來整理一下與學習過程中遇到的深度相關的一些問題,主要是渲染中深度的一些問題以及Unity中深度圖生成,深度圖的使用,深度的精度,Reverse-Z等等問題,然后再用深度圖,實現(xiàn)一些好玩的效果。本人才疏學淺,如果有不正確的地方,還望各位高手批評指正。
畫家算法&ZBuffer算法
在渲染中為了保證渲染的正確,其實主要得益于兩個最常用的算法,第一個是畫家算法。所謂畫家算法,就是按照畫畫的順序,先畫遠處的內(nèi)容,再畫近處的內(nèi)容疊加上去,近處的會覆蓋掉遠處的內(nèi)容。即,在繪制之前,需要先按照遠近排序。但是畫家算法有一個很嚴重的問題,對于自身遮擋關系比較復雜的對象,沒有辦法保證繪制的正確;無法進行檢測,overdraw比較嚴重;再者對對象排序的操作,不適合硬件實現(xiàn)。
而另一種保證深度正確的算法就是ZBuffer算法,申請一塊和FrameBuffer大小一樣的緩沖區(qū),如果開啟了深度測試,那么在最終寫入FrameBuffer之前(Early-Z實現(xiàn)類似,只是時機效果不同),就需要進行測試,比如ZTest LEqual的話,如果深度小于該值,那么通過深度測試,如果開啟了深度寫入,還需要順便更新一下當前點的深度值,如果不通過,就不會寫入FrameBuffer。ZBuffer保證像素級別的深度正確,并且實現(xiàn)簡單,比起靠三角形排序這種不確定性的功能更加容易硬件化,所以目前的光柵化渲染中大部分都使用的是ZBuffer算法。ZBuffer算法也有壞處,第一就是需要一塊和顏色緩沖區(qū)一樣大小的Buffer,精度還要比較高,所以比較費內(nèi)存,再者需要逐像素計算Z值,但是為了渲染的正確,也就是透視校正紋理映射,Z值的計算是不可避免的,所以總體來看,ZBuffer的優(yōu)勢還是比較明顯的。關于ZBuffer的實現(xiàn)以及透視投影紋理映射,可以參照軟渲染實現(xiàn)。
對于不透明物體來說,ZBuffer算法是非常好的,可以保證遮擋關系沒有問題。但是透明物體的渲染,由于一般是不寫深度的,所以經(jīng)常會出現(xiàn)問題,對于透明物體,一般還是采用畫家算法,即由遠及近進行排序渲染。還有一種方案是關閉顏色寫入,先渲染一遍Z深度,然后再渲染半透,就可以避免半透明對象內(nèi)部也被顯示出來的問題,可以參考之前的遮擋處理這篇文章中遮擋半透的做法。
透視投影與光柵化過程
透視投影的主要知識點在于三角形相似以及小孔呈像,透視投影實現(xiàn)的就是一種“近大遠小”的效果,其實投影后的大小(x,y坐標)也剛好就和1/Z呈線性關系。看下面一張圖:
上圖是一個視錐體的截面圖(只看x,z方向),P為空間中一點(x,y,z),那么它在近裁剪面處的投影坐標假設為P’(x',y',z’),理論上來說,呈像的面應該在眼睛后方才更符合真正的小孔呈像原理,但是那樣會增加復雜度,沒必要額外引入一個負號(此處有一個裁剪的注意要點,下文再說),只考慮三角形相似即可。即三角形EAP’相似于三角形EGP,我們可以得到兩個等式:
x’/ x = z’/ z => x’= xz’/ z
y’/ y = z’/ z => y’= yz’/ z
由于投影面就是近裁剪面,那么近裁剪面是我們可以定義的,我們設其為N,遠裁剪面為F,那么實際上最終的投影坐標就是:
(Nx/z,Ny/z,N)。
投影后的Z坐標,實際上已經(jīng)失去作用了,只用N表示就可以了,但是這個每個頂點都一樣,每個頂點帶一個的話簡直是暴殄天物,浪費了一個珍貴的維度,所以這個Z會被存儲一個用于后續(xù)深度測試,透視校正紋理映射的變換后的Z值。
但是還有一個問題,這里我們得到是只是頂點的Z值,也就是我們在vertex shader中計算的結(jié)果,只有頂點,但是實際上,我們在屏幕上會看到無數(shù)的像素,換句話說,這些頂點的信息都是離散的,但是最終顯示在屏幕上的模型卻是連續(xù)的,這個那么每個像素點的值是怎么得到的呢?其實就是插值。一個三角形光柵化到屏幕空間上時,我們僅有的就是在三角形三個頂點所包含的各種數(shù)據(jù),其中頂點已經(jīng)是被變換過的了(Unity中常用的MVP變換),在繪制三角形的過程中,根據(jù)屏幕空間位置對上述數(shù)據(jù)進行插值計算,來獲得頂點之間對應屏幕上像素點上的顏色或其他數(shù)據(jù)信息。
這個Z值,還是比較有說道的。在透視投影變換之前,我們的Z實際上是相機空間的Z值,直接把這個Z存下來也無可厚非,但是后續(xù)計算會比較麻煩,畢竟沒有一個統(tǒng)一的標準。既然我們有了遠近裁剪面,有了Z值的上下限,我們就可以把這個Z值映射到[0,1]區(qū)間,即當在近裁剪面時,Z值為0,遠裁剪面時,Z值為1(暫時不考慮reverse-z的情況)。
首先,能想到的最簡單的映射方法就是depth = (Z(eye) - N)/ F - N。直接線性映射到(0,1)區(qū)間,但是這種方案是不正確的,看下面一張圖:
右側(cè)的三角形,在AB近裁剪面投影的大小一致,而實際上C1F1和F1E1相差的距離甚遠,換句話說,經(jīng)過投影變換的透視除法后,我們在屏幕空間插值的數(shù)據(jù)(根據(jù)屏幕空間距離插值),并不能保證其對應點在投影前的空間是線性變換的。關于透視投影和光柵化,可以參照上一篇文章中軟渲染透視投影和光柵化的內(nèi)容。
透視投影變換之后,在屏幕空間進行插值的數(shù)據(jù),與Z值不成正比,而是與1/Z成正比。所以,我們需要一個表達式,可以使Z = N時,depth = 0,Z = F時,depth = 1,并且需要有一個z作為分母,可以寫成(az + b)/z,帶入上述兩個條件:
(N * a + b) / N = 0 => b = -an
(F * a + b) / F = 0 => aF + b = F => aF - aN = F
進而得到:
a = F / (F - N)
b = NF / (N - F)
最終depth(屏幕空間) = (aZ + b)/ Z (Z為視空間深度)。
通過透視投影,在屏幕空間X,Y值都除以了Z(視空間深度),當一個值的Z趨近于無窮遠時,那么X,Y值就趨近于0了,也就是類似近大遠小的效果了。而對于深度值的映射,從上面看也是除以了Z的,這個現(xiàn)象其實也比較好理解,比如一個人在離相機200米的地方前進了1米,我們基本看不出來距離的變化,但是如果在相機面前2米處前進了1米,那么這個距離變化是非常明顯的,這也是近大遠小的一種體現(xiàn)。
Unity中生成深度圖
先來考古一下,我找到了一個上古時代的Unity版本,4.3,在4.X的時代,Unity生成深度圖使用的還是Hidden/Camera-DepthTexture這個函數(shù),機制就是使用Replacement Shader,在渲染時將shader統(tǒng)一換成Hidden/Camera-DepthTexture,不同類型的RenderType對應不同的SubShader,比如帶有Alpha Test就可以在fragment階段discard掉不需要的部分,防止在深度圖中有不該出現(xiàn)的內(nèi)容。那時候,也許還有些設備不支持原生的DepthTexture RT格式(SM2.0以上,DepthTexture支持)還有一個UNITY_MIGHT_NOT_HAVE_DEPTH_TEXTURE的宏,針對某些不支持深度格式的RT,使用普通的RGBA格式編碼深度圖進行輸出,采樣時再將RGBA解碼變回深度信息,使用編碼的好處主要在于可以充分利用顏色的四個通道(32位)獲得更高的精度,否則就只有一個通道(8位),編碼和解碼的函數(shù)如下:
- // Encoding/decoding [0..1) floats into 8 bit/channel RGBA. Note that 1.0 will not be encoded properly.
- inline float4 EncodeFloatRGBA( float v )
- {
- float4 kEncodeMul = float4(1.0, 255.0, 65025.0, 160581375.0);
- float kEncodeBit = 1.0/255.0;
- float4 enc = kEncodeMul * v;
- enc = frac (enc);
- enc -= enc.yzww * kEncodeBit;
- return enc;
- }
- inline float DecodeFloatRGBA( float4 enc )
- {
- float4 kDecodeDot = float4(1.0, 1/255.0, 1/65025.0, 1/160581375.0);
- return dot( enc, kDecodeDot );
- }
深度圖生成的函數(shù)如下,其實那時絕大多數(shù)情況都已經(jīng)支持DepthFormat格式了,所以直接使用了空實現(xiàn),顏色返回為0:
- #if defined(UNITY_MIGHT_NOT_HAVE_DEPTH_TEXTURE)
- #define UNITY_TRANSFER_DEPTH(oo) oo = o.pos.zw
- #if SHADER_API_FLASH
- #define UNITY_OUTPUT_DEPTH(i) return EncodeFloatRGBA(i.x/i.y)
- #else
- #define UNITY_OUTPUT_DEPTH(i) return i.x/i.y
- #endif
- #else
- #define UNITY_TRANSFER_DEPTH(oo)
- #define UNITY_OUTPUT_DEPTH(i) return 0
- #endif
在Unity5.X版本后,實際上深度的pass就變?yōu)榱薙hadowCaster這個pass,而不需要再進行Shader Raplacement的操作了(但是DepthNormalMap仍然需要),所謂ShadowCaster這個pass,其實就是用于投影的Pass,Unity的所有自帶shader都帶這個pass,而且只要我們fallback了Unity內(nèi)置的shader,也會增加ShadowCaster這個pass。我們應該也可以自己定義ShadowCaster這個pass,防止類似AlphaTest等造成深度圖中內(nèi)容與實際渲染內(nèi)容不符的情況。
ShadowCaster這個pass實際上是有兩個用處,第一個是屏幕空間的深度使用該pass進行渲染,另一方面就是ShadowMap中光方向的深度也是使用該pass進行渲染的,區(qū)別主要在與VP矩陣的不同,陰影的pass是相對于光空間的深度,而屏幕空間深度是相對于攝像機的。新版的Unity使用了ScreenSpaceShadowMap,屏幕空間的深度也是必要的(先生成DpehtTexture,再生成ShadowMap,然后生成ScreenSpaceShadowMap,再正常渲染物體采樣ScreenSpaceShadowMap)。所以,如果我們開了屏幕空間陰影,再使用DepthTexture,就相當于免費贈送,不用白不用嘍。
新版本的Unity,本人目前使用的是Unity2017.3版本,VS和PS階段的宏直接全部改為了空實現(xiàn):
- // Legacy; used to do something on platforms that had to emulate depth textures manually. Now all platforms have native depth textures.
- #define UNITY_TRANSFER_DEPTH(oo)
- // Legacy; used to do something on platforms that had to emulate depth textures manually. Now all platforms have native depth textures.
- #define UNITY_OUTPUT_DEPTH(i) return 0
而ShadowCaster的實現(xiàn)也是頗為簡單,VS階段不考慮ShadowBias的情況下其實就是MVP變換,而PS也直接是空實現(xiàn):
- #define SHADOW_CASTER_FRAGMENT(i) return 0;
為何Unity會如此肆無忌憚,直接空實現(xiàn)我們就可以得到一張深度圖呢?我們可以用framedebugger看到我們使用的深度圖的格式實際上是DepthFormat:
正如上文中Untiy新版本shader注釋中所說的,“現(xiàn)在所有平臺都支持原生的深度圖了",所以也就沒有必要再RGBA格式編碼深度然后在進行解碼這種費勁的方法,直接申請DepthFormat格式的RT即可,也就是在采樣時,只將DepthAttachment的內(nèi)容作為BindTexture的id。ColorBuffer輸出的顏色是什么都無所謂了,我們要的只是DepthBuffer的輸出,而這個輸出的結(jié)果就是正常我們渲染時的深度,也就是ZBuffer中的深度值,只是這個值系統(tǒng)自動幫我們處理了,類似固定管線Native方式。
至于Unity為何不直接用FrameBuffer中的Z,而是使用全場景渲染一遍的方式,個人猜測是為了更好的兼容性吧(如果有大佬知道,還望不吝賜教),再者本身一張RT同時讀寫在某些平臺就是未定義的操作,可能出現(xiàn)問題(本人之前測試是移動平臺上大部分都掛了,這也是為什么很多后處理,比如高斯模糊等在申請RT的時候要申請兩塊,在兩塊RT之間互相Blit的原因)。倒是之前了解過一個黑科技,直接bind一張RT的DepthAttachment到depth上,然后讀這張RT就是深度了,然而沒有大面積真機測試過,真是不太敢用 。
深度圖的使用
大概了解了一下Unity中深度圖的由來,下面準備使用深度圖啦。雖然前面說了這么多,然而實際上在Unity中使用深度圖,卻是一個簡單到不能再簡單的操作了,通過Camera的depthTextureMode即可設置DepthTexture。我們來用一個后處理效果把當前的深度圖繪制到屏幕上:
- /********************************************************************
- FileName: DepthTextureTest.cs
- Description:顯示深度貼圖
- Created: 2018/05/27
- history: 27:5:2018 1:25 by puppet_master
- *********************************************************************/
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- [ExecuteInEditMode]
- public class DepthTextureTest : MonoBehaviour
- {
- private Material postEffectMat = null;
- private Camera currentCamera = null;
- void Awake()
- {
- currentCamera = GetComponent<Camera>();
- }
- void OnEnable()
- {
- if (postEffectMat == null)
- postEffectMat = new Material(Shader.Find("DepthTexture/DepthTextureTest"));
- currentCamera.depthTextureMode |= DepthTextureMode.Depth;
- }
- void OnDisable()
- {
- currentCamera.depthTextureMode &= ~DepthTextureMode.Depth;
- }
- void OnRenderImage(RenderTexture source, RenderTexture destination)
- {
- if (postEffectMat == null)
- {
- Graphics.Blit(source, destination);
- }
- else
- {
- Graphics.Blit(source, destination, postEffectMat);
- }
- }
- }
Shader部分:
- //puppet_master
- //2018.5.27
- //顯示深度貼圖
- Shader "DepthTexture/DepthTextureTest"
- {
- CGINCLUDE
- #include "UnityCG.cginc"
- sampler2D _CameraDepthTexture;
- fixed4 frag_depth(v2f_img i) : SV_Target
- {
- float depthTextureValue = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
- //float linear01EyeDepth = LinearEyeDepth(depthTextureValue) * _ProjectionParams.w;
- float linear01EyeDepth = Linear01Depth(depthTextureValue);
- return fixed4(linear01EyeDepth, linear01EyeDepth, linear01EyeDepth, 1.0);
- }
- ENDCG
- SubShader
- {
- Pass
- {
- ZTest Off
- Cull Off
- ZWrite Off
- Fog{ Mode Off }
- CGPROGRAM
- #pragma vertex vert_img
- #pragma fragment frag_depth
- ENDCG
- }
- }
- }
依然是我最常用的測試場景,哇咔咔,場景原始效果如下:
顯示深度效果如下:
上面的Shader中我們使用了SAMPLE_DEPTH_TEXTURE這個宏進行了深度圖的采樣,其實這個宏就是采樣了DepthTexuter的r通道作為深度(除在PSP2平臺不一樣),其余平臺的定義都是下面的:
- define SAMPLE_DEPTH_TEXTURE(sampler, uv) (tex2D(sampler, uv).r)
LinearEyeDepth&Linear01Depth
在上面的Shader中,我們使用了LinearEyeDepth和LinearDepth對深度進行了一個變換之后才輸出到屏幕,那么實際上的Z值應該是啥樣的呢,我放置了四個距離相等的模型,來看一下常規(guī)的Z值直接輸出的情況(由于目前開啟了Reverse-Z,所以用1-z作為輸出),即:
- float depthTextureValue = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
- return 1 - depthTextureValue;
效果:
經(jīng)過Linear01Depth變換后的效果:
對比兩張圖我們應該也就比較清楚效果了,沒有經(jīng)過處理的深度,在視空間上不是線性變化的,近處深度變化較明顯,而遠處幾乎全白了,而經(jīng)過處理的深度,在視空間是線性變化的。
為什么會這樣呢,還是得從透視投影和光柵化說起,在視空間,每個頂點的原始的Z值是視空間的深度,但是經(jīng)過透視投影變換以及透視投影,轉(zhuǎn)化到屏幕空間后,需要保證在屏幕空間的深度與1/z成正比才可以在屏幕空間逐像素地進行插值進而獲得屏幕上任意一點像素的屏幕空間深度值,簡單來說,這個轉(zhuǎn)化的過程主要是為了從頂點數(shù)據(jù)獲得屏幕空間任意一點的逐像素數(shù)據(jù)。而得到屏幕空間深度之后,我們要使用時,經(jīng)過變換的這個屏幕空間的東西,又不是很直觀,最直觀的還是視空間的深度,所以我們要進行一步變換,把屏幕空間的深度再轉(zhuǎn)換回原始的視空間深度。
上文中,我們推導過從視空間深度轉(zhuǎn)化到屏幕空間深度的公式如下:
a = F / (F - N)
b = NF / (N - F)
depth(屏幕空間) = (aZ + b)/ Z (Z為視空間深度)。
那么,反推回Z(視空間) = b /(depth - a),進一步地,Z(視空間) = 1 / (depth / b - a / b),然后將上述a和b的值代入:
Z(視空間) = 1 / ((depth / (NF / (N - F)) - (F /(F - N)) / (NF / (N - F)))
化簡: Z(視空間) = 1 / (((N - F)/ NF) * depth + 1 / N)
Z(視空間) = 1 / (param1 * depth + param2),param1 = (N - F)/ NF,param2 = 1 / N。
下面讓我們來看看Unity自帶Shader中關于深度值LinearEyeDepth的處理:
- // Z buffer to linear depth
- inline float LinearEyeDepth( float z )
- {
- return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
- }
- // Values used to linearize the Z buffer (http://www.humus.name/temp/Linearize%20depth.txt)
- // x = 1-far/near
- // y = far/near
- // z = x/far
- // w = y/far
- float4 _ZBufferParams;
_ZBufferParams.z = _ZBufferParams.x / far = (1 - far / near)/ far = (near - far) / near * far
_ZBufferParams.w = _ZBufferParams.y / far = (far / near) / far = 1 / near
我們推導的param1 = _ZBufferParams.z,param2 = _ZBufferParams.w,實際上Unity中LinearEyeDepth就是將透視投影變換的公式反過來,用zbuffer圖中的屏幕空間depth反推回當前像素點的相機空間深度值。
下面再來看一下Linear01Depth函數(shù),所謂01,其實也比較好理解,我們上面得到的深度值實際上是真正的視空間Z值,但是這個值沒有一個統(tǒng)一的比較標準,所以這個時候依然秉承著映射大法好的理念,把這個值轉(zhuǎn)化到01區(qū)間即可。由于相機實際上可以看到的最遠區(qū)間就是F(遠裁剪面),所以這個Z值直接除以F即可得到映射到(0,1)區(qū)間的Z值了:
Z(視空間01) = Z(視空間) / F = 1 / (((N - F)/ N) * depth + F / N)
Z(視空間01) = 1 / (param1 * depth + param2),param1 = (N - F)/ N = 1 - F/N,param2 = F / N。
再來看一下Unity中關于Linear01Depth的處理:
- // Z buffer to linear 0..1 depth
- inline float Linear01Depth( float z )
- {
- return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
- }
- // Values used to linearize the Z buffer (http://www.humus.name/temp/Linearize%20depth.txt)
- // x = 1-far/near
- // y = far/near
- // z = x/far
- // w = y/far
- float4 _ZBufferParams;
可以看出我們推導的param1 = _ZBufferParams.x,param2 = _ZBufferParams.y。也就是說,Unity中Linear01Depth的操作值將屏幕空間的深度值還原為視空間的深度值后再除以遠裁剪面的大小,將視空間深度映射到(0,1)區(qū)間。
Unity應該是OpenGL風格(矩陣,NDC等),上面的推導上是基于DX風格的DNC進行的,不過,如果是深度圖的話,不管怎么樣都會映射到(0,1)區(qū)間的,相當于OpenGL風格的深度再進行一步映射,就與DX風格的一致了。個人感覺OpenGL風格的NDC在某些情況下并不是很方便(見下文Reverse-Z相關內(nèi)容)。
了解了這兩個Unity為我們提供的API具體是干什么的了之后,我們就可以放心大膽的使用了,因為實際上絕大多數(shù)情況下,我們都是需要相機空間的深度值或者映射到01區(qū)間的相機空間深度值。
Z&1/Z
通過上面的深度圖具體的使用,我們發(fā)現(xiàn),實際上真正使用的深度,是從頂點的視空間Z,經(jīng)過投影變成一個1/Z成正比的值(屏幕空間Depth),然后在使用時,再通過投影變換時的計算公式反推回對應視空間像素位置的Z??梢?,這個操作還是非常折騰的。那為何要如此費勁地進行上面的操作,而不是直接存一個視空間的值作為真正的深度呢?
其實前輩們也想過這個問題,原來的顯卡,甚至是不用我們當今的Z Buffer(存儲的是屏幕空間的Depth,也就是與1/Zview成正比的一個值)的,而是用了一個所謂的W Buffer(存儲的是視空間的Z)。W Buffer的計算表面上看起來應該是很簡單的,即在頂點計算時,直接將當前頂點的z值進行進行01映射,類似W = Zview / Far,就可以了把視空間的值映射到一個(0,1)區(qū)間的深度值。然后我們在Pixel階段要使用的時候,就需要通過光柵化階段頂點數(shù)據(jù)插值得到當前屏幕空間這一點的Z值,但是這又回到了一個問題,Z值是視空間的,經(jīng)過了透視投影變換之后變成了屏幕空間,我們插值的系數(shù)是屏幕空間位置,這個位置是與1/Z成正比的,換句話說,在屏幕空間插值時,必須要進行透視投影校正,類似透視投影校正紋理采樣,針對的是uv坐標進行了插值,大致思路是計算時對頂點數(shù)據(jù)先除以Z,然后屏幕空間逐像素插值,之后再乘回該像素真正的Z值??梢?,如果要使用這樣的W Buffer,雖然我們使用起來簡單了,但是硬件實現(xiàn)上,還是比較麻煩的,畢竟需要多做一次乘除映射。
所以,實際上,現(xiàn)在的Z Buffer使用的仍然是屏幕空間的Depth,也就是在透視投影變換時,使用透視投影矩陣直接相乘把頂點坐標xyz變換到齊次裁剪空間,然后統(tǒng)一透視除法除以w,就得到了一個在屏幕空間是線性的Depth值。這個值可以在屏幕上直接根據(jù)像素位置進行簡單線性插值,無需再進行透視校正,這樣的話,對于硬件實現(xiàn)上來說是最容易的。
其實在Unity中也是分為兩種DepthTexture的,一種是DepthTexture,存儲的是屏幕空間線性深度,也是最常見的深度的格式,上面已經(jīng)推導過了。而另一種是DepthNormalTexture(不僅僅是它除了Depth還包含Normal),存的就是相機空間的深度值,這個就是最基本的線性映射,把這個值作為頂點數(shù)據(jù)走透視投影校正后傳遞給Fragment階段,那么這個值其實直接就是在視空間是線性變換的了,不需要再進行類似普通DepthTexture的Linear操作。
DepthNormalTexture的生成用到的相關內(nèi)容(只看Depth部分):
- v2f vert( appdata_base v )
- {
- v2f o;
- o.pos = UnityObjectToClipPos(v.vertex);
- o.nz.xyz = COMPUTE_VIEW_NORMAL;
- o.nz.w = COMPUTE_DEPTH_01;
- return o;
- }
- fixed4 frag(v2f i) : SV_Target
- {
- return EncodeDepthNormal (i.nz.w, i.nz.xyz);
- }
- #define COMPUTE_DEPTH_01 -(UnityObjectToViewPos( v.vertex ).z * _ProjectionParams.w)
- // x = 1 or -1 (-1 if projection is flipped)
- // y = near plane
- // z = far plane
- // w = 1/far plane
- uniform vec4 _ProjectionParams;
新版本后處理包中對于深度的采樣大概是這個樣子的:
- // Depth/normal sampling functions
- float SampleDepth(float2 uv)
- {
- #if defined(SOURCE_GBUFFER) || defined(SOURCE_DEPTH)
- float d = LinearizeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv));
- #else
- float4 cdn = tex2D(_CameraDepthNormalsTexture, uv);
- float d = DecodeFloatRG(cdn.zw);
- #endif
- return d * _ProjectionParams.z + CheckBounds(uv, d);
- }
可見,對于兩種方式的深度,進行生成和采樣的方式是不同的,DepthNormal類型的深度直接就可以乘以遠裁剪面還原到視空間深度,而深度圖的需要進行Linearize變換??梢愿鶕?jù)需要定制自己的DepthTexture。主流一些的方式還是原生的DepthTextue方式,但是這種方式也有很一個很嚴重的問題,就是精度問題。
ZBuffer的精度問題
既然說了1/Z的好處,那再來看看1/Z的壞處。ZBuffer的這種設計可能會導致遠處深度精度不夠,進而會出現(xiàn)ZFighting的現(xiàn)象,前輩們一直在用各種方式來與ZBuffer的精度做著斗爭,我們也來看看這個問題。由于使用1/Z作為深度,深度的分布是不均勻的,以一個4Bit的深度緩存來看的話,Z值和Depth的關系如下圖:
4Bit的深度的精度可以表示2^4也就是1/16的精度,但是由上圖可以看出,在Z(相機空間)從near到far變化時,在near處精度很密集,而在Z超過1/2(far - near)這一段,幾乎只有幾個格子來表示這一段的精度了,也就是說,即使兩個對象在遠處離得很遠,可能在深度Buffer里面二者也是歸為深度相同的,那么在進行深度測試時,兩個物體深度相同,兩者的像素就都可能出現(xiàn)在前面,概率性地遮擋和不遮擋,就形成了ZFighting的現(xiàn)象。
要想緩解ZFighting,首先看一下深度表示的公式,以n位精度的深度來說,每一位的精度表示如下:
D(perBit) = (1<<n) * (aZ + b ) / Z = (1 <<n) * (a + b / z)
讓D(preBit)更小就是精度更高, 如果是線性深度,理論上我們的depth應該是 d = (Z - N)/ (F - N)插值,由于一般而言F遠遠大于N,所以實際上影響因子主要在于F。但是與1/Z作為插值的話,大概可以這樣理解d = (1/Z - 1/N)/ (1/F - 1/N),在這種情況下,實際上就是倒數(shù)影響了,那主要影響的因子實際上是N。
我們看一下近裁剪面對ZFighting的影響,看來Unity為了緩解ZFighting,近裁剪面最近只能設置為0.01(本人Unity版本2017.3),沒辦法看,所以這里用了之前的軟渲染做測試了。
正常渲染的情況下(近裁剪面0.1):
出現(xiàn)ZFighting的情況(近裁剪面0.00001f),在立方體的棱角位置出現(xiàn)了ZFighting:
一個有效緩解ZFighting的方案就是盡可能遠地放置近裁剪面(保證面前內(nèi)容表現(xiàn)效果的情況下,太遠會裁掉面前的東西)。將近裁剪面推遠后的深度分布曲線如下, 可見,深度的分布曲線在遠處被“提起來”,也就是遠處獲得了更大一些的精度分布:
目前正常ZBuffer的方式,簡單來說就是近處對象的深度精度極高,遠處對象的深度精度極地,差了N個數(shù)量級。其實正常來說,這樣的深度分布也是有好處的,因為我們在近處的精度高一些,遠處精度低點,感覺也比較符合正常思維。如果只是為了保證近處渲染的效果,那么直接用正常的ZBuffer就是最好的選擇了。但是,主要就在于超大視距,類似超大地圖這種,既需要保證遠處的精度,又希望保證近處的精度,遠處精度衰減太厲害,所以ZFighting現(xiàn)象就出現(xiàn)了。
本人所了解過的緩解ZFighting的方案主要是下面幾種(如果還有好玩的方案還望不吝賜教):
1)提高深度Buffer的精度,精度高了,自然表現(xiàn)效果就好了。在渲染到RT上時,經(jīng)常出現(xiàn)ZFighting的現(xiàn)象,16Bit滿足不了效果的情況下選擇24Bit深度。
2)盡可能遠地放置相機的近裁剪面。
3)對于特別近的兩個對象,適當考慮把二者之間的距離拉開一點,比如地面上的貼片,適當抬起來一點點(很無腦,但是最有效)。
4)對于實在有問題的情況,可以考慮Offset操作。本人曾經(jīng)遇到在魅族MX4機型上渲染半透Prepass之后,半透的Pass和Prepass深度沖突,后來無奈給半透的Pass增加了一個Offset解決問題(比較特殊情況,只有這個機型很多效果都不對,簡直給我?guī)砹藷o盡的煩惱)。
5)動態(tài)切換遠近裁剪面,即先設置很遠的近裁剪面和遠裁剪面,渲染遠景物體,然后ClearDepthBuffer保留ColorBuffer,修改近裁剪面到很低的值,遠裁剪面到剛才設置的近裁剪面值,再渲染近處物體。這個方案在分界處交叉的物體可能有問題,不過這個問題影響不大,主要是這樣會導致Early-Z失效,先渲后面的再渲染前面的,成了畫家算法,至少一遍overdarw。一般來說對于不透明物體的渲染順序應該還是先渲染近處的,再渲染遠處的物體(比如Unity)<個人感覺不太實用,目前Unity內(nèi)不太好實現(xiàn),這招是Ogre里面一個哥們分享的,但是方案比較好玩,可能是本渣渣沒做過什么大世界,沒有什么超大視距和近處細節(jié)同時兼顧的需求,所以沒有被精度逼到這種程度吧,萬一逼急了,誰管他overdraw呢?>。
6)不寫硬件深度,直接寫視空間深度,換句話說就是正常的線性深度,類似生成DepthNormalMap的方式。
7)Logarithmic Depth Buffer,對數(shù)深度。與上一條類似,都是自己生成深度圖。貌似GTA5中延遲渲染中生成深度的流程,就是自己算了一個對數(shù)深度,極大地提高了深度的精度,真是一切為了精度啊。比較復雜的玩法,沒有玩過,Unity目前應該也不支持,需要pixel shader里面對深度進行校正再寫回,應該也會導致Early-Z失效,只能使用最終的ZCheck,不過延遲渲染會好很多。
8)最后還有一種能夠有效緩解ZFighting的方法,就是Reverse-Z,這個Unity目前在一部分平臺<OpenGL ES木有,已哭瞎>已經(jīng)自帶了。
Reverse-Z
這一部分先貼出一篇Nvidia的關于Reverse-Z的文章(本文中深度精度分布圖來自該文章),里面講的很詳細。
所謂Reverse-Z,直接翻譯過來的意思是反轉(zhuǎn)Z。顧名思義的話,ZBuffer(深度圖)中存儲的值是反過來的,也就是近裁剪面的深度值實際上是1,而遠裁剪面的深度值是0。那么,我們的ZTest LEqual就得當做ZTest GEqual來處理,采樣的深度值都需要用1 - depth嘍,為何會做出如此反人道的設計呢?還是得從ZBuffer本身的存儲1/Z的設定以及浮點數(shù)精度的問題著手。
上面我們看到了用定點精度表示深度的分布曲線,那么,如果改為浮點數(shù),理論上浮點數(shù)可以表示的范圍要遠大于定點數(shù),是否會對其有所緩解呢?額,關于具體原因,還是看這位大佬分析的浮點數(shù)精度相關的blog吧(當年上計算機組成原理的時候,貌似我還在沉迷COC,不過好在沒掛科,233)。簡單總結(jié)一下就是:浮點雖然表示的范圍廣,但是有精度損失,一個浮點數(shù)表示的其實是其周圍的一個有理數(shù)區(qū)間,這個區(qū)間在0點處精度很高,而當浮點數(shù)本身很大時,根據(jù)科學計數(shù)法,小數(shù)部分乘以階碼表示最終的這個值,階碼越大,最終結(jié)果里面可以表示的真正小數(shù)的位數(shù)就變少,甚至沒有了,所以浮點數(shù)的精度分布大概是醬紫的:
如果我們用浮點數(shù)表示精度的話,精度的曲線如下:
尷尬,浮點精度雖然高了,但是還是都集中在了近裁剪面,本身這個地方精度已經(jīng)夠高了,再高的話就是浪費了。于是乎前輩們就想到了一個非常巧妙的方法,既然浮點精度和ZBuffer精度都是在近裁剪面精度高,浮點精度我們沒辦法控制(IEEE標準就這樣的),那就只能在ZBuffer的生成上做文章了。固定流水線的話,不好控制,但是目前基本都是可編程流水線,矩陣是自己傳給shader的,那么只要把上面的投影矩陣改一下,讓近裁剪面的深度置為1,遠裁剪面的深度置為0,這樣這個d = a + b/z的變換執(zhí)行了一個相當于反向映射的操作,也就成了所謂的Reverse-Z。通過這個操作,把浮點數(shù)在0附近精度高抵消了深度遠裁剪面精度低問題,使整體的深度Buffer精度有了較大的提高,使用了Reverse-Z的深度分布如下:
圖:
Reverse-Z的好處是提升了深度精度,壞處的話。。個人感覺應該就是不太好理解咯。主要的操作在于替換投影矩陣,深度映射提取時需要反向,ZTest全部反過來看,DepthClear需要修改。D3D的話,NDC是的Z是在01區(qū)間比較好實現(xiàn),但是OpenGL的話,NDC的Z是在-1,1區(qū)間,這個值需要映射到01區(qū)間,需要有glClipControl強行設置遠近裁剪面倒置。相當于多折騰一步映射,這個設定是在需要用Reverse-Z的情況還有寫入深度圖的時候都需要進行01映射,貌似在網(wǎng)上也看到不少人吐槽OpenGL強迫癥地設計一個正方體的NDC。
Unity在5.5之后的版本里面,開始使用了Reverse-Z。不過,Unity封裝得比較好,以至于一般情況下我們是不會發(fā)現(xiàn)問題的,Unity大法好啊。在上面推導了LinearEyeDepth和Linear01Depth兩個函數(shù)的實現(xiàn),但是還是建議使用Unity的API來進行這個變換,因為Unity不僅為我們封裝了上面的變換,可以很方便地使用,還有一個更重要的問題,就是Unity幫我們處理了Reverse-Z的情況,我們自己如果不處理的話,得到的深度實際上是反向的,因為DepthBufferParam這個值在是否開啟Reverse-Z的情況下,從引擎?zhèn)鬟^來的值是不一樣的,完整版本的如下:
- // Values used to linearize the Z buffer (http://www.humus.name/temp/Linearize%20depth.txt)
- // x = 1-far/near
- // y = far/near
- // z = x/far
- // w = y/far
- // or in case of a reversed depth buffer (UNITY_REVERSED_Z is 1)
- // x = -1+far/near
- // y = 1
- // z = x/far
- // w = 1/far
- float4 _ZBufferParams;
通過修改x,y的值,LinearEyeDepth和Linear01Depth最終對于是否Reverse-Z都能得到正確的深度結(jié)果。
Unity關于Reverse-Z的其他部分主要在于MotionVector的生成,陰影的計算等地方有區(qū)別,如果自己用深度計算的時候,可能也需要考慮一下這個問題。最后再來看看哪些平臺開了這個宏:
- #if defined(SHADER_API_D3D11) || defined(SHADER_API_PSSL) || defined(SHADER_API_XBOXONE) || defined(SHADER_API_METAL) || defined(SHADER_API_VULKAN) || defined(SHADER_API_SWITCH)
- // D3D style platforms where clip space z is [0, 1].
- #define UNITY_REVERSED_Z 1
- #endi
很遺憾,GLES沒開(GL Core4.5原生支持,老版本GL的需要用擴展,ES壓根沒提),不過Mental和DX11開了。所以設備上出現(xiàn)某些深度表現(xiàn)和DX11表現(xiàn)不同相,可以往這個方向考慮一下(老版本插件升級到5.5之后有可能會出現(xiàn)這個問題)。
所以這個問題告訴我們一個道理:能用官方API,就用官方API,即使知道API的實現(xiàn),也盡量別自己造輪子,造輪子神馬的是學習的時候用的,除非沒官方輪子,工程里如果不用官方API,純屬為后續(xù)升版本挖坑,逆著Unity干,一般沒啥好結(jié)果(恩,說的就是我這個渣渣,之前沒少干這種壞事,皮了一下很開心,升級時候改到死。不過,如果您是圖形學或者Unity大佬的話,那還是想怎么玩就怎么玩)。
基于Reverse-Z,后續(xù)又有人發(fā)現(xiàn)了一些減少深度計算誤差的方法,比如用無窮遠的遠裁剪面以及把投影矩陣單獨拆開來與頂點相乘(個人感覺會略微損失一點性能吧),可以參考這篇論文。
軟粒子效果
上面看過了深度相關的基本知識,下面就到了基本的效果實踐了。第一個,也是一個比較常見的深度的應用就是軟粒子效果。何謂軟,何謂硬,看一下下面的一張截圖:
左側(cè)為普通的粒子效果,而右側(cè)為開啟了軟粒子的粒子效果。普通的粒子效果,和非透明的地面穿插時,是直接硬插進地面了,而右側(cè)的軟粒子效果,越靠近地面,粒子的alpha權重越低,到地面的時候就透明了,可見,軟粒子相比于普通粒子能夠更好地做到和非半透對象平滑過渡,不至于有明顯的穿插。
下面看一下軟粒子的實現(xiàn),由于這個Unity是內(nèi)置了這個效果,所以我就直接找到軟粒子的shader源碼添加點注釋嘍:
- // Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)
- Shader "Particles/Additive (Soft)" {
- Properties {
- _MainTex ("Particle Texture", 2D) = "white" {}
- _InvFade ("Soft Particles Factor", Range(0.01,3.0)) = 1.0
- }
- Category {
- Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" }
- Blend One OneMinusSrcColor
- ColorMask RGB
- Cull Off Lighting Off ZWrite Off
- SubShader {
- Pass {
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #pragma target 2.0
- #pragma multi_compile_particles
- #pragma multi_compile_fog
- #include "UnityCG.cginc"
- sampler2D _MainTex;
- fixed4 _TintColor;
- struct appdata_t {
- float4 vertex : POSITION;
- fixed4 color : COLOR;
- float2 texcoord : TEXCOORD0;
- UNITY_VERTEX_INPUT_INSTANCE_ID
- };
- struct v2f {
- float4 vertex : SV_POSITION;
- fixed4 color : COLOR;
- float2 texcoord : TEXCOORD0;
- UNITY_FOG_COORDS(1)
- #ifdef SOFTPARTICLES_ON
- float4 projPos : TEXCOORD2;
- #endif
- UNITY_VERTEX_OUTPUT_STEREO
- };
- float4 _MainTex_ST;
- v2f vert (appdata_t v)
- {
- v2f o;
- UNITY_SETUP_INSTANCE_ID(v);
- UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
- o.vertex = UnityObjectToClipPos(v.vertex);
- #ifdef SOFTPARTICLES_ON
- //計算頂點在屏幕空間的位置(沒有進行透視除法)
- o.projPos = ComputeScreenPos (o.vertex);
- //計算頂點距離相機的距離
- COMPUTE_EYEDEPTH(o.projPos.z);
- #endif
- o.color = v.color;
- o.texcoord = TRANSFORM_TEX(v.texcoord,_MainTex);
- UNITY_TRANSFER_FOG(o,o.vertex);
- return o;
- }
- UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
- float _InvFade;
- fixed4 frag (v2f i) : SV_Target
- {
- #ifdef SOFTPARTICLES_ON
- //根據(jù)上面的屏幕空間位置,進行透視采樣深度圖(tex2dproj,即帶有透視除法的采樣,相當于tex2d(xy/w)),
- //得到當前像素對應在屏幕深度圖的深度,并轉(zhuǎn)化到視空間,線性化(深度圖中已有的不透明對象的深度)
- float sceneZ = LinearEyeDepth (SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos)));
- //本像素點在視空間真正的距離(粒子本身的深度)
- float partZ = i.projPos.z;
- //計算二者的深度差,該值越小,說明越近穿插
- float fade = saturate (_InvFade * (sceneZ-partZ));
- //上面的深度差調(diào)整粒子的alpha值
- i.color.a *= fade;
- #endif
- half4 col = i.color * tex2D(_MainTex, i.texcoord);
- col.rgb *= col.a;
- UNITY_APPLY_FOG_COLOR(i.fogCoord, col, fixed4(0,0,0,0)); // fog towards black due to our blend mode
- return col;
- }
- ENDCG
- }
- }
- }
- }
基本思想就是,在渲染粒子效果時,先取當前屏幕空間深度圖對應該像素點的深度值,然后計算該粒子對應該像素點位置的深度值(二者都轉(zhuǎn)化到了視空間),然后用兩個深度差作為一個系數(shù)調(diào)制粒子的alpha值,最終達到讓粒子接近不透明物體的部分漸變淡出的效果。
上面的函數(shù)中有使用了一個這樣的宏,通過該宏直接把頂點轉(zhuǎn)化到視空間,取z值的負數(shù)就是真正的視空間距離了。
- #define COMPUTE_EYEDEPTH(o) o = -UnityObjectToViewPos( v.vertex ).z
軟粒子效果,雖然使用了深度圖,但是比較麻煩,需要各種坐標轉(zhuǎn)換,因為要在對象空間使用屏幕空間的深度,所以不得不ComputeScreenPos,并tex2Dproj,非常折騰,這也足以見得軟粒子有多費,不僅僅在于渲染深度圖本身的消耗,自身計算也是非常費的,再加上粒子一般都是半透,不寫深度,沒辦法在粒子之間通過early-z優(yōu)化,導致overdraw非常高,逐像素計算爆炸??粗鵀榱诉@個漸變導致的這個計算量,我感覺移動上,粒子穿插還是忍了吧,萬一有美術同學問我,我就假裝不知道-_-。
其實Unity的軟粒子這套寫法,可以用在不少其他效果中,比如水面,海邊等根據(jù)深度漸變的效果(刷頂點色或許更省一些),我不只在一個水插件中看到上面的這套寫法了,變量名都一樣,今天才算是找到“始作俑者”,哈哈哈(額,這好像是個貶義詞,我特意百度了一下,實在沒找到啥別的詞兒,我沒有貶義的意思哈。。??磥砦业恼Z文是百度老師教的)。
基于深度的掃描波效果
下面來搞個很簡單,但是很好玩的效果,這個效果沒有亂七八糟的ComputeScreenPos之類的,直接就是在屏幕空間進行的,恩,也就是我最愛的后處理啦,開心!
這個效果即blog開頭《惡靈附身》截圖的第一張圖的類似效果。先觀察一下,基本效果就是一個高亮的區(qū)域,按照深度由遠及近地運動,直到略過攝像機。恩,我直接簡單粗暴地在shader里判斷了一下:
- //puppet_master
- //https://blog.csdn.net/puppet_master
- //2018.5.27
- //基于深度的掃描效果
- Shader "DepthTexture/ScreenDepthScan"
- {
- Properties
- {
- _MainTex("Base (RGB)", 2D) = "white" {}
- }
- CGINCLUDE
- #include "UnityCG.cginc"
- sampler2D _CameraDepthTexture;
- sampler2D _MainTex;
- fixed4 _ScanLineColor;
- float _ScanValue;
- float _ScanLineWidth;
- float _ScanLightStrength;
- float4 frag_depth(v2f_img i) : SV_Target
- {
- float depthTextureValue = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
- float linear01EyeDepth = Linear01Depth(depthTextureValue);
- fixed4 screenTexture = tex2D(_MainTex, i.uv);
- if (linear01EyeDepth > _ScanValue && linear01EyeDepth < _ScanValue + _ScanLineWidth)
- {
- return screenTexture * _ScanLightStrength * _ScanLineColor;
- }
- return screenTexture;
- }
- ENDCG
- SubShader
- {
- Pass
- {
- ZTest Off
- Cull Off
- ZWrite Off
- Fog{ Mode Off }
- CGPROGRAM
- #pragma vertex vert_img
- #pragma fragment frag_depth
- ENDCG
- }
- }
- }
C#代碼如下,一些邊界條件的判斷放在c#里面,要比shader全屏計算效率好得多:
- /********************************************************************
- FileName: ScreenDepthScan.cs
- Description:深度掃描線效果
- Created: 2018/05/27
- history: 27:5:2018 1:25 by puppet_master
- https://blog.csdn.net/puppet_master
- *********************************************************************/
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- [ExecuteInEditMode]
- public class ScreenDepthScan : MonoBehaviour
- {
- private Material postEffectMat = null;
- private Camera currentCamera = null;
- [Range(0.0f, 1.0f)]
- public float scanValue = 0.05f;
- [Range(0.0f, 0.5f)]
- public float scanLineWidth = 0.02f;
- [Range(0.0f, 10.0f)]
- public float scanLightStrength = 10.0f;
- public Color scanLineColor = Color.white;
- void Awake()
- {
- currentCamera = GetComponent<Camera>();
- }
- void OnEnable()
- {
- if (postEffectMat == null)
- postEffectMat = new Material(Shader.Find("DepthTexture/ScreenDepthScan"));
- currentCamera.depthTextureMode |= DepthTextureMode.Depth;
- }
- void OnDisable()
- {
- currentCamera.depthTextureMode &= ~DepthTextureMode.Depth;
- }
- void OnRenderImage(RenderTexture source, RenderTexture destination)
- {
- if (postEffectMat == null)
- {
- Graphics.Blit(source, destination);
- }
- else
- {
- //限制一下最大值,最小值
- float lerpValue = Mathf.Min(0.95f, 1 - scanValue);
- if (lerpValue < 0.0005f)
- lerpValue = 1;
- //此處可以一個vec4傳進去優(yōu)化
- postEffectMat.SetFloat("_ScanValue", lerpValue);
- postEffectMat.SetFloat("_ScanLineWidth", scanLineWidth);
- postEffectMat.SetFloat("_ScanLightStrength", scanLightStrength);
- postEffectMat.SetColor("_ScanLineColor", scanLineColor);
- Graphics.Blit(source, destination, postEffectMat);
- }
- }
- }
效果如下:
當然,如果為了好玩的話,可以再加點別的效果烘托一下分為,比如結(jié)合一下時空扭曲效果:
shader代碼:
- //puppet_master
- //https://blog.csdn.net/puppet_master
- //2018.6.10
- //基于深度的掃描效果,附帶扭曲
- Shader "DepthTexture/ScreenDepthScanWithDistort"
- {
- Properties
- {
- _MainTex("Base (RGB)", 2D) = "white" {}
- }
- CGINCLUDE
- #include "UnityCG.cginc"
- sampler2D _CameraDepthTexture;
- sampler2D _MainTex;
- fixed4 _ScanLineColor;
- float _ScanValue;
- float _ScanLineWidth;
- float _ScanLightStrength;
- float _DistortFactor;
- float _DistortValue;
- float4 frag_depth(v2f_img i) : SV_Target
- {
- float depthTextureValue = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
- float linear01EyeDepth = Linear01Depth(depthTextureValue);
- float2 dir = i.uv - float2(0.5, 0.5);
- float2 offset = _DistortFactor * normalize(dir) * (1 - length(dir));
- float2 uv = i.uv - offset * _DistortValue * linear01EyeDepth;
- fixed4 screenTexture = tex2D(_MainTex, uv);
- if (linear01EyeDepth > _ScanValue && linear01EyeDepth < _ScanValue + _ScanLineWidth)
- {
- return screenTexture * _ScanLightStrength * _ScanLineColor;
- }
- return screenTexture;
- }
- ENDCG
- SubShader
- {
- Pass
- {
- ZTest Off
- Cull Off
- ZWrite Off
- Fog{ Mode Off }
- CGPROGRAM
- #pragma vertex vert_img
- #pragma fragment frag_depth
- ENDCG
- }
- }
- }
C#代碼:
- /********************************************************************
- FileName: ScreenDepthScan.cs
- Description:深度掃描線效果,附帶扭曲
- Created: 2018/06/10
- history: 10:6:2018 10:25 by puppet_master
- https://blog.csdn.net/puppet_master
- *********************************************************************/
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- [ExecuteInEditMode]
- public class ScreenDepthScanWithDistort : MonoBehaviour
- {
- private Material postEffectMat = null;
- private Camera currentCamera = null;
- [Range(0.0f, 1.0f)]
- public float scanValue = 0.05f;
- [Range(0.0f, 0.5f)]
- public float scanLineWidth = 0.02f;
- [Range(0.0f, 10.0f)]
- public float scanLightStrength = 10.0f;
- [Range(0.0f, 0.04f)]
- public float distortFactor = 0.02f;
- public Color scanLineColor = Color.white;
- void Awake()
- {
- currentCamera = GetComponent<Camera>();
- }
- void OnEnable()
- {
- if (postEffectMat == null)
- postEffectMat = new Material(Shader.Find("DepthTexture/ScreenDepthScanWithDistort"));
- currentCamera.depthTextureMode |= DepthTextureMode.Depth;
- }
- void OnDisable()
- {
- currentCamera.depthTextureMode &= ~DepthTextureMode.Depth;
- }
- void OnRenderImage(RenderTexture source, RenderTexture destination)
- {
- if (postEffectMat == null)
- {
- Graphics.Blit(source, destination);
- }
- else
- {
- //限制一下最大值,最小值
- float lerpValue = Mathf.Min(0.95f, 1 - scanValue);
- if (lerpValue < 0.0005f)
- lerpValue = 1;
- //此處可以一個vec4傳進去優(yōu)化
- postEffectMat.SetFloat("_ScanValue", lerpValue);
- postEffectMat.SetFloat("_ScanLineWidth", scanLineWidth);
- postEffectMat.SetFloat("_ScanLightStrength", scanLightStrength);
- postEffectMat.SetFloat("_DistortFactor", distortFactor);
- postEffectMat.SetFloat("_DistortValue", 1 - scanValue);
- postEffectMat.SetColor("_ScanLineColor", scanLineColor);
- Graphics.Blit(source, destination, postEffectMat);
- }
- }
- }
效果:
個人感覺扭曲和深度重建某些情況下是沖突的,如果仔細觀察其實可能會發(fā)現(xiàn)圖片有重影,但是,鑒于掃描速度很快,這點穿幫其實應該還是可以接受的。這不由得讓我想起了做劇情的時候,給劇情做了不少效果,但是策劃妹紙?zhí)貏e愛用震屏,景深,徑向模糊這幾個效果,我十分不解,后來我才知道其中緣由:“我們有好多地方都有穿幫,震一下,或者模糊一下,玩家就不注意了”,正所謂天下武功唯快不破,哇咔咔。
根據(jù)深度重建世界坐標
下面打算再用深度做幾個更好玩的效果。但是這幾個效果略微有些復雜,主要就在于不僅僅需要的是深度信息,還需要得到世界坐標的信息,也就是說我需要根據(jù)深度圖反推當前世界坐標位置。
證明世界坐標重建正確的方法
首先,得先找到一種證明反推回世界空間位置正確的方法。這里,我在相機前擺放幾個物體,盡量使之在世界坐標下的位置小于1,方便判定顏色如下圖:
然后將幾個物體的shader換成如下的一個打印世界空間位置的shader:
- //puppet_master
- //https://blog.csdn.net/puppet_master
- //2018.6.10
- //打印對象在世界空間位置
- Shader "DepthTexture/WorldPosPrint"
- {
- SubShader
- {
- Tags { "RenderType"="Opaque" }
- LOD 100
- Pass
- {
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #include "UnityCG.cginc"
- struct appdata
- {
- float4 vertex : POSITION;
- float2 uv : TEXCOORD0;
- };
- struct v2f
- {
- float3 worldPos : TEXCOORD0;
- float4 vertex : SV_POSITION;
- };
- v2f vert (appdata v)
- {
- v2f o;
- o.vertex = UnityObjectToClipPos(v.vertex);
- o.worldPos = mul(unity_ObjectToWorld, v.vertex);
- return o;
- }
- fixed4 frag (v2f i) : SV_Target
- {
- return fixed4(i.worldPos, 1.0);
- }
- ENDCG
- }
- }
- //fallback使之有shadow caster的pass
- FallBack "Legacy Shaders/Diffuse"
- }
然后掛上上面的重建世界坐標位置的腳本,在開啟和關閉腳本前后,屏幕輸出完全無變化,說明通過后處理重建世界坐標位置與直接用shader輸出世界坐標位置效果一致:
逆矩陣方式重建
深度重建有幾種方式,先來看一個最簡單粗暴,但是看起來最容易理解的方法:
我們得到的屏幕空間深度圖的坐標,xyz都是在(0,1)區(qū)間的,需要經(jīng)過一步變換,變換到NDC空間,OpenGL風格的話就都是(-1,1)區(qū)間,所以需要首先對xy以及xy對應的深度z進行*2 - 1映射。然后再將結(jié)果進行VP的逆變換,就得到了世界坐標。
shader代碼如下:
- //puppet_master
- //https://blog.csdn.net/puppet_master
- //2018.6.10
- //通過逆矩陣的方式從深度圖構建世界坐標
- Shader "DepthTexture/ReconstructPositionInvMatrix"
- {
- CGINCLUDE
- #include "UnityCG.cginc"
- sampler2D _CameraDepthTexture;
- float4x4 _InverseVPMatrix;
- fixed4 frag_depth(v2f_img i) : SV_Target
- {
- float depthTextureValue = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
- //自己操作深度的時候,需要注意Reverse_Z的情況
- #if defined(UNITY_REVERSED_Z)
- depthTextureValue = 1 - depthTextureValue;
- #endif
- float4 ndc = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, depthTextureValue * 2 - 1, 1);
- float4 worldPos = mul(_InverseVPMatrix, ndc);
- worldPos /= worldPos.w;
- return worldPos;
- }
- ENDCG
- SubShader
- {
- Pass
- {
- ZTest Off
- Cull Off
- ZWrite Off
- Fog{ Mode Off }
- CGPROGRAM
- #pragma vertex vert_img
- #pragma fragment frag_depth
- ENDCG
- }
- }
- }
C#部分:
- /********************************************************************
- FileName: ReconstructPositionInvMatrix.cs
- Description:從深度圖構建世界坐標,逆矩陣方式
- Created: 2018/06/10
- history: 10:6:2018 13:09 by puppet_master
- https://blog.csdn.net/puppet_master
- *********************************************************************/
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- [ExecuteInEditMode]
- public class ReconstructPositionInvMatrix : MonoBehaviour {
- private Material postEffectMat = null;
- private Camera currentCamera = null;
- void Awake()
- {
- currentCamera = GetComponent<Camera>();
- }
- void OnEnable()
- {
- if (postEffectMat == null)
- postEffectMat = new Material(Shader.Find("DepthTexture/ReconstructPositionInvMatrix"));
- currentCamera.depthTextureMode |= DepthTextureMode.Depth;
- }
- void OnDisable()
- {
- currentCamera.depthTextureMode &= ~DepthTextureMode.Depth;
- }
- void OnRenderImage(RenderTexture source, RenderTexture destination)
- {
- if (postEffectMat == null)
- {
- Graphics.Blit(source, destination);
- }
- else
- {
- var vpMatrix = currentCamera.projectionMatrix * currentCamera.worldToCameraMatrix;
- postEffectMat.SetMatrix("_InverseVPMatrix", vpMatrix.inverse);
- Graphics.Blit(source, destination, postEffectMat);
- }
- }
- }
效果如下,重建ok:
看起來比較簡單,但是其中有一個/w的操作,如果按照正常思維來算,應該是先乘以w,然后進行逆變換,最后再把world中的w拋棄,即是最終的世界坐標,不過實際上投影變換是一個損失維度的變換,我們并不知道應該乘以哪個w,所以實際上上面的計算,并非按照理想的情況進行的計算,而是根據(jù)計算推導而來(更加詳細推導請參考這篇文章,不過我感覺這個推導有點繞)。
已知條件(M為VP矩陣,M^-1即為其逆矩陣,Clip為裁剪空間,ndc為標準設備空間,world為世界空間):
ndc = Clip.xyzw / Clip.w = Clip / Clip.w
world = M^-1 * Clip
二者結(jié)合得:
world = M ^-1 * ndc * Clip.w
我們已知M和ndc,然而還是不知道Clip.w,但是有一個特殊情況,是world的w坐標,經(jīng)過變換后應該是1,即
1 = world.w = (M^-1 * ndc).w * Clip.w
進而得到Clip.w = 1 / (M^ -1 * ndc).w
帶入上面等式得到:
world = (M ^ -1 * ndc) / (M ^ -1 * ndc).w
所以,世界坐標就等于ndc進行VP逆變換之后再除以自身的w。
不過這種方式重建世界坐標,性能比較差,一般來說,我們都是逐頂點地進行矩陣運算,畢竟定點數(shù)一般還是比較少的,但是全屏幕逐像素進行矩陣運算,這個計算量就不是一般的大了,性能肯定是吃不消的。
屏幕射線插值方式重建
這種方式的重建,可以參考Secrets of CryENGINE 3 Graphics Technology這個CryTech 2011年的PPT。借用一張圖:
然后偶再畫個平面的圖:
上圖中,A為相機位置,G為空間中我們要重建的一點,那么該點的世界坐標為A(worldPos) + 向量AG,我們要做的就是求得向量AG即可。根據(jù)三角形相似的原理,三角形AGH相似于三角形AFC,則得到AH / AC = AG / AF。由于三角形相似就是比例關系,所以我們可以把AH / AC看做01區(qū)間的比值,那么AC就相當于遠裁剪面距離,即為1,AH就是我們深度圖采樣后變換到01區(qū)間的深度值,即Linear01Depth的結(jié)果d。那么,AG = AF * d。所以下一步就是求AF,即求出相機到屏幕空間每個像素點對應的射線方向??吹缴厦娴牧Ⅲw圖,其實我們可以根據(jù)相機的各種參數(shù),求得視錐體對應四個邊界射線的值,這個操作在vertex階段進行,由于我們的后處理實際上就是渲染了一個Quad,上下左右四個頂點,把這個射線傳遞給pixel階段時,就會自動進行插值計算,也就是說在頂點階段的方向值到pixel階段就變成了逐像素的射線方向。
那么我們要求的其實就相當于AB這條向量的值,以上下平面為例,三維向量只比二維多一個維度,我們已知遠裁剪面距離F,相機的三個方向(相機transform.forward,.right,.up),AB = AC + CB,|BC| = tan(0.5fov) * |AC|,|AC| = Far,AC = transorm.forward * Far,CB = transform.up * tan(0.5fov) * Far。
我直接使用了遠裁剪面對應的位置計算了三個方向向量,進而組合得到最終四個角的向量。用遠裁剪面的計算代碼比較簡單(恩,我懶),不過《ShaderLab入門精要》中使用的是近裁剪面+比例計算,不確定是否有什么考慮(比如精度,沒有測出來,如果有大佬知道,還望不吝賜教)。
shader代碼如下:
- //puppet_master
- //https://blog.csdn.net/puppet_master
- //2018.6.16
- //通過深度圖重建世界坐標,視口射線插值方式
- Shader "DepthTexture/ReconstructPositionViewPortRay"
- {
- CGINCLUDE
- #include "UnityCG.cginc"
- sampler2D _CameraDepthTexture;
- float4x4 _ViewPortRay;
- struct v2f
- {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- float4 rayDir : TEXCOORD1;
- };
- v2f vertex_depth(appdata_base v)
- {
- v2f o;
- o.pos = UnityObjectToClipPos(v.vertex);
- o.uv = v.texcoord.xy;
- //用texcoord區(qū)分四個角,就四個點,if無所謂吧
- 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;
- o.rayDir = _ViewPortRay[index];
- return o;
- }
- fixed4 frag_depth(v2f i) : SV_Target
- {
- float depthTextureValue = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
- float linear01Depth = Linear01Depth(depthTextureValue);
- //worldpos = campos + 射線方向 * depth
- float3 worldPos = _WorldSpaceCameraPos + linear01Depth * i.rayDir.xyz;
- return fixed4(worldPos, 1.0);
- }
- ENDCG
- SubShader
- {
- Pass
- {
- ZTest Off
- Cull Off
- ZWrite Off
- Fog{ Mode Off }
- CGPROGRAM
- #pragma vertex vertex_depth
- #pragma fragment frag_depth
- ENDCG
- }
- }
- }
C#代碼如下:
- /********************************************************************
- FileName: ReconstructPositionViewPortRay.cs
- Description:通過深度圖重建世界坐標,視口射線插值方式
- Created: 2018/06/16
- history: 16:6:2018 16:17 by puppet_master
- https://blog.csdn.net/puppet_master
- *********************************************************************/
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- [ExecuteInEditMode]
- public class ReconstructPositionViewPortRay : MonoBehaviour {
- private Material postEffectMat = null;
- private Camera currentCamera = null;
- void Awake()
- {
- currentCamera = GetComponent<Camera>();
- }
- void OnEnable()
- {
- if (postEffectMat == null)
- postEffectMat = new Material(Shader.Find("DepthTexture/ReconstructPositionViewPortRay"));
- currentCamera.depthTextureMode |= DepthTextureMode.Depth;
- }
- void OnDisable()
- {
- currentCamera.depthTextureMode &= ~DepthTextureMode.Depth;
- }
- void OnRenderImage(RenderTexture source, RenderTexture destination)
- {
- if (postEffectMat == null)
- {
- Graphics.Blit(source, destination);
- }
- else
- {
- var aspect = currentCamera.aspect;
- var far = currentCamera.farClipPlane;
- var right = transform.right;
- var up = transform.up;
- var forward = transform.forward;
- var halfFovTan = Mathf.Tan(currentCamera.fieldOfView * 0.5f * Mathf.Deg2Rad);
- //計算相機在遠裁剪面處的xyz三方向向量
- var rightVec = right * far * halfFovTan * aspect;
- var upVec = up * far * halfFovTan;
- var forwardVec = forward * far;
- //構建四個角的方向向量
- var topLeft = (forwardVec - rightVec + upVec);
- var topRight = (forwardVec + rightVec + upVec);
- var bottomLeft = (forwardVec - rightVec - upVec);
- var bottomRight = (forwardVec + rightVec - upVec);
- var viewPortRay = Matrix4x4.identity;
- viewPortRay.SetRow(0, topLeft);
- viewPortRay.SetRow(1, topRight);
- viewPortRay.SetRow(2, bottomLeft);
- viewPortRay.SetRow(3, bottomRight);
- postEffectMat.SetMatrix("_ViewPortRay", viewPortRay);
- Graphics.Blit(source, destination, postEffectMat);
- }
- }
- }
開關后處理前后效果仍然不變:
這里我用了默認非線性的深度圖進行的深度計算,需要先進行Linear01Depth計算,如果用了線性深度,比如DepthNormalTexture,那么就進行一步簡單的線性映射即可。整體的射線計算,我用了Linear01Depth * 外圍計算好的距離。也可以用LinearEyeDepth * 外圍計算好的方向??傊?,方案還是蠻多的,變種也很多,還有自己重寫Graphic.Blit自己設置Quad的值把index設置在頂點的z值中。
屏幕空間高度或距離霧效果
在后處理階段拿到世界空間位置,我們就可以做一些更加好玩的效果啦。屏幕空間高度或者距離霧就是其中之一。正常Unity中的霧效,實際上是在shader計算結(jié)束之后和霧效顏色根據(jù)世界空間距離計算的指數(shù)或者線性霧,對于一般的表現(xiàn)已經(jīng)很好啦。而這個效果主要是可以模擬一些“體積霧”的感覺,讓霧效更加明顯,變成一個可以看得到的霧效,而不是僅僅附著在物體表面。
上面我們重建世界坐標后,我們就使用世界空間的高度作為霧效強度的判斷條件,在最終計算顏色時,根據(jù)霧效高度差將屏幕原始顏色與霧效進行插值計算,即可得到屏幕空間高度霧效的效果。我直接使用了線性插值,也可以使用exp之類的。
shader代碼如下:
- //puppet_master
- //https://blog.csdn.net/puppet_master
- //2018.6.16
- //屏幕空間高度霧效
- Shader "DepthTexture/ScreenSpaceHeightFog"
- {
- Properties
- {
- _MainTex("Base (RGB)", 2D) = "white" {}
- }
- CGINCLUDE
- #include "UnityCG.cginc"
- sampler2D _MainTex;
- sampler2D _CameraDepthTexture;
- float4x4 _ViewPortRay;
- float _FogHeight;
- float _WorldFogHeight;
- fixed4 _FogColor;
- struct v2f
- {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- float4 rayDir : TEXCOORD1;
- };
- v2f vertex_depth(appdata_base v)
- {
- v2f o;
- o.pos = UnityObjectToClipPos(v.vertex);
- o.uv = v.texcoord.xy;
- //用texcoord區(qū)分四個角
- 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;
- o.rayDir = _ViewPortRay[index];
- return o;
- }
- fixed4 frag_depth(v2f i) : SV_Target
- {
- fixed4 screenTex = tex2D(_MainTex, i.uv);
- float depthTextureValue = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
- float linear01Depth = Linear01Depth(depthTextureValue);
- float3 worldPos = _WorldSpaceCameraPos + linear01Depth * i.rayDir.xyz;
- float fogInensity = saturate((_WorldFogHeight - worldPos.y) / _FogHeight);
- return lerp(screenTex, _FogColor, fogInensity);
- }
- ENDCG
- SubShader
- {
- Pass
- {
- ZTest Off
- Cull Off
- ZWrite Off
- Fog{ Mode Off }
- CGPROGRAM
- #pragma vertex vertex_depth
- #pragma fragment frag_depth
- ENDCG
- }
- }
- }
C#代碼如下:
- /********************************************************************
- FileName: ScreenSpaceHeightFog.cs
- Description:屏幕空間高度霧效
- Created: 2018/06/16
- history: 16:6:2018 21:23 by puppet_master
- https://blog.csdn.net/puppet_master
- *********************************************************************/
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- [ExecuteInEditMode]
- public class ScreenSpaceHeightFog : MonoBehaviour {
- [Range(0.0f, 10.0f)]
- public float fogHeight = 0.1f;
- public Color fogColor = Color.white;
- public float horizontalPlane = 0.0f;
- private Material postEffectMat = null;
- private Camera currentCamera = null;
- void Awake()
- {
- currentCamera = GetComponent<Camera>();
- }
- void OnEnable()
- {
- if (postEffectMat == null)
- postEffectMat = new Material(Shader.Find("DepthTexture/ScreenSpaceHeightFog"));
- currentCamera.depthTextureMode |= DepthTextureMode.Depth;
- }
- void OnDisable()
- {
- currentCamera.depthTextureMode &= ~DepthTextureMode.Depth;
- }
- void OnRenderImage(RenderTexture source, RenderTexture destination)
- {
- if (postEffectMat == null)
- {
- Graphics.Blit(source, destination);
- }
- else
- {
- var aspect = currentCamera.aspect;
- var far = currentCamera.farClipPlane;
- var right = transform.right;
- var up = transform.up;
- var forward = transform.forward;
- var halfFovTan = Mathf.Tan(currentCamera.fieldOfView * 0.5f * Mathf.Deg2Rad);
- //計算相機在遠裁剪面處的xyz三方向向量
- var rightVec = right * far * halfFovTan * aspect;
- var upVec = up * far * halfFovTan;
- var forwardVec = forward * far;
- //構建四個角的方向向量
- var topLeft = (forwardVec - rightVec + upVec);
- var topRight = (forwardVec + rightVec + upVec);
- var bottomLeft = (forwardVec - rightVec - upVec);
- var bottomRight = (forwardVec + rightVec - upVec);
- var viewPortRay = Matrix4x4.identity;
- viewPortRay.SetRow(0, topLeft);
- viewPortRay.SetRow(1, topRight);
- viewPortRay.SetRow(2, bottomLeft);
- viewPortRay.SetRow(3, bottomRight);
- postEffectMat.SetMatrix("_ViewPortRay", viewPortRay);
- postEffectMat.SetFloat("_WorldFogHeight", horizontalPlane + fogHeight);
- postEffectMat.SetFloat("_FogHeight", fogHeight);
- postEffectMat.SetColor("_FogColor", fogColor);
- Graphics.Blit(source, destination, postEffectMat);
- }
- }
- }
效果如下:
霧效的判斷條件也可以修改為高度+距離:
- float fogInensity = (_WorldFogHeight - worldPos.y) / _FogHeight;
- fogInensity = max(linear01Depth * _FogHeight, fogInensity);
- return lerp(screenTex, _FogColor, saturate(fogInensity));
效果如下:
運動模糊效果
運動模糊效果還是有很多種其他的方式去做的,比如渲染速度圖,不過本篇只考慮了深度重建世界空間位置的做法進行模糊處理。參考《GPU Gems 3-Motion Blur as a Post-Processing Effect》這篇文章。渲染速度圖的方式,需要額外的渲染批次,沒有MRT的話批次比較難搞。用深度圖進行運動模糊的話,無需額外的批次(不考慮深度本身的批次,畢竟開了深度是一個好多效果都可以用,性價比更高),不過也有一個問題,就是這種方式的運動模糊只能模糊相機本身的運動。
上面我們已經(jīng)過通過逆矩陣進行重建世界空間位置,那么視線運動模糊就好實現(xiàn)啦。相比于存儲一張上一幀的貼圖,這里我們直接存儲一下上一陣的(相機*投影矩陣)的逆矩陣,然后重建世界坐標,用其中差作為uv采樣的偏移值進行采樣,然后按權重進行模糊計算,模擬一個運動物體拖尾的效果。
shader代碼如下:
- //puppet_master
- //https://blog.csdn.net/puppet_master
- //2018.6.17
- //通過深度圖重建世界坐標方式運動模糊效果
- Shader "DepthTexture/MotionBlurByDepth"
- {
- Properties
- {
- _MainTex("Base (RGB)", 2D) = "white" {}
- }
- CGINCLUDE
- #include "UnityCG.cginc"
- sampler2D _MainTex;
- sampler2D _CameraDepthTexture;
- float4x4 _CurrentInverseVPMatrix;
- float4x4 _PreviousInverseVPMatrix;
- float4 _BlurWeight;
- float _BlurStrength;
- fixed4 frag_depth(v2f_img i) : SV_Target
- {
- float depthTextureValue = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
- //自己操作深度的時候,需要注意Reverse_Z的情況
- #if defined(UNITY_REVERSED_Z)
- depthTextureValue = 1 - depthTextureValue;
- #endif
- float4 ndc = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, depthTextureValue * 2 - 1, 1);
- float4 currentWorldPos = mul(_CurrentInverseVPMatrix, ndc);
- currentWorldPos /= currentWorldPos.w;
- float4 previousWorldPos = mul(_PreviousInverseVPMatrix, ndc);
- previousWorldPos /= previousWorldPos.w;
- float2 velocity = (currentWorldPos - previousWorldPos).xy * _BlurStrength;
- fixed4 screenTex = tex2D(_MainTex, i.uv);
- screenTex += tex2D(_MainTex, i.uv + velocity * 1.0) * _BlurWeight.x;
- screenTex += tex2D(_MainTex, i.uv + velocity * 2.0) * _BlurWeight.y;
- screenTex += tex2D(_MainTex, i.uv + velocity * 3.0) * _BlurWeight.z;
- screenTex /= (1.0 + _BlurWeight.x + _BlurWeight.y + _BlurWeight.z);
- return screenTex;
- }
- ENDCG
- SubShader
- {
- Pass
- {
- ZTest Off
- Cull Off
- ZWrite Off
- Fog{ Mode Off }
- CGPROGRAM
- #pragma vertex vert_img
- #pragma fragment frag_depth
- ENDCG
- }
- }
- }
C#代碼如下:
- /********************************************************************
- FileName: MotionBlurByDepth.cs
- Description:通過深度圖重建世界坐標方式運動模糊效果
- Created: 2018/06/17
- history: 17:6:2018 1:47 by puppet_master
- https://blog.csdn.net/puppet_master
- *********************************************************************/
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- [ExecuteInEditMode]
- public class MotionBlurByDepth : MonoBehaviour {
- private Material postEffectMat = null;
- private Camera currentCamera = null;
- private Matrix4x4 previouscurrentVPMatrix;
- [Range(0.0f, 0.02f)]
- public float blurStrength = 0.5f;
- public Vector3 blurWeight = new Vector3(0.6f, 0.3f, 0.1f);
- void Awake()
- {
- currentCamera = GetComponent<Camera>();
- }
- void OnEnable()
- {
- if (postEffectMat == null)
- postEffectMat = new Material(Shader.Find("DepthTexture/MotionBlurByDepth"));
- currentCamera.depthTextureMode |= DepthTextureMode.Depth;
- }
- void OnDisable()
- {
- currentCamera.depthTextureMode &= ~DepthTextureMode.Depth;
- }
- void OnRenderImage(RenderTexture source, RenderTexture destination)
- {
- if (postEffectMat == null)
- {
- Graphics.Blit(source, destination);
- }
- else
- {
- postEffectMat.SetMatrix("_PreviousInverseVPMatrix", previouscurrentVPMatrix);
- var currentVPMatrix = (currentCamera.projectionMatrix * currentCamera.worldToCameraMatrix).inverse;
- postEffectMat.SetMatrix("_CurrentInverseVPMatrix", currentVPMatrix);
- postEffectMat.SetFloat("_BlurStrength", blurStrength);
- postEffectMat.SetVector("_BlurWeight", blurWeight);
- Graphics.Blit(source, destination, postEffectMat);
- }
- }
- }
效果如下:
據(jù)我觀察,實際上有些游戲里面的運動模糊并不是全屏幕的,而是只模糊邊緣,相機中間不模糊,可以加個根據(jù)距離屏幕中心距離計算權重與原圖lerp的操作,不過如果不是很糾結(jié)效果的話,用徑向模糊來代替運動模糊也是個不錯的選擇。
真正的對高速運動物體進行運動模糊,肯定要比這種Trick的方式要復雜一點,也更真實一些,不過這個不在本文討論范圍了。
擴散掃描效果
接下來來實現(xiàn)一個更好玩一點的效果。類似開頭《惡靈附身》截圖中第二個的效果,在空間一點實現(xiàn)擴散掃描的效果,其實與屏幕空間掃描線的效果實現(xiàn)思路是一樣的,只不過實現(xiàn)了世界空間重建后,我們的判斷條件就可以更加復雜,用世界空間位置進行計算可以實現(xiàn)一些復雜一些的形狀的掃描線。比如判斷與一點的距離實現(xiàn)的環(huán)形擴散掃描:
Shader代碼如下:
- //puppet_master
- //https://blog.csdn.net/puppet_master
- //2018.6.18
- //擴散波動效果
- Shader "DepthTexture/SpreadWaveByDepth"
- {
- Properties
- {
- _MainTex("Base (RGB)", 2D) = "white" {}
- }
- CGINCLUDE
- #include "UnityCG.cginc"
- sampler2D _MainTex;
- sampler2D _CameraDepthTexture;
- float4x4 _ViewPortRay;
- fixed4 _ScanColor;
- float _ScanValue;
- float4 _ScanCenterPos;
- float _ScanCircleWidth;
- struct v2f
- {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- float4 rayDir : TEXCOORD1;
- };
- v2f vertex_depth(appdata_base v)
- {
- v2f o;
- o.pos = UnityObjectToClipPos(v.vertex);
- o.uv = v.texcoord.xy;
- //用texcoord區(qū)分四個角
- 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;
- o.rayDir = _ViewPortRay[index];
- return o;
- }
- fixed4 frag_depth(v2f i) : SV_Target
- {
- fixed4 screenTex = tex2D(_MainTex, i.uv);
- float depthTextureValue = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
- float linear01Depth = Linear01Depth(depthTextureValue);
- float3 worldPos = _WorldSpaceCameraPos + linear01Depth * i.rayDir.xyz;
- float dist = distance(worldPos, _ScanCenterPos.xyz);
- if (dist > _ScanValue && dist < _ScanCircleWidth + _ScanValue)
- return screenTex * _ScanColor;
- return screenTex;
- }
- ENDCG
- SubShader
- {
- Pass
- {
- ZTest Off
- Cull Off
- ZWrite Off
- Fog{ Mode Off }
- CGPROGRAM
- #pragma vertex vertex_depth
- #pragma fragment frag_depth
- ENDCG
- }
- }
- }
C#代碼如下:
- /********************************************************************
- FileName: SpreadWaveByDepth.cs
- Description:擴散波動效果
- Created: 2018/06/18
- history: 18:6:2018 15:56 by puppet_master
- https://blog.csdn.net/puppet_master
- *********************************************************************/
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- [ExecuteInEditMode]
- public class SpreadWaveByDepth : MonoBehaviour
- {
- public Color scanColor = Color.white;
- public float scanValue = 0.0f;
- public float scanCircleWidth = 1.0f;
- public Vector3 scanCenterPos = Vector3.zero;
- private Material postEffectMat = null;
- private Camera currentCamera = null;
- void Awake()
- {
- currentCamera = GetComponent<Camera>();
- }
- void OnEnable()
- {
- if (postEffectMat == null)
- postEffectMat = new Material(Shader.Find("DepthTexture/SpreadWaveByDepth"));
- currentCamera.depthTextureMode |= DepthTextureMode.Depth;
- }
- void OnDisable()
- {
- currentCamera.depthTextureMode &= ~DepthTextureMode.Depth;
- }
- void OnRenderImage(RenderTexture source, RenderTexture destination)
- {
- if (postEffectMat == null)
- {
- Graphics.Blit(source, destination);
- }
- else
- {
- var aspect = currentCamera.aspect;
- var far = currentCamera.farClipPlane;
- var right = transform.right;
- var up = transform.up;
- var forward = transform.forward;
- var halfFovTan = Mathf.Tan(currentCamera.fieldOfView * 0.5f * Mathf.Deg2Rad);
- //計算相機在遠裁剪面處的xyz三方向向量
- var rightVec = right * far * halfFovTan * aspect;
- var upVec = up * far * halfFovTan;
- var forwardVec = forward * far;
- //構建四個角的方向向量
- var topLeft = (forwardVec - rightVec + upVec);
- var topRight = (forwardVec + rightVec + upVec);
- var bottomLeft = (forwardVec - rightVec - upVec);
- var bottomRight = (forwardVec + rightVec - upVec);
- var viewPortRay = Matrix4x4.identity;
- viewPortRay.SetRow(0, topLeft);
- viewPortRay.SetRow(1, topRight);
- viewPortRay.SetRow(2, bottomLeft);
- viewPortRay.SetRow(3, bottomRight);
- postEffectMat.SetMatrix("_ViewPortRay", viewPortRay);
- postEffectMat.SetColor("_ScanColor", scanColor);
- postEffectMat.SetVector("_ScanCenterPos", scanCenterPos);
- postEffectMat.SetFloat("_ScanValue", scanValue);
- postEffectMat.SetFloat("_ScanCircleWidth", scanCircleWidth);
- Graphics.Blit(source, destination, postEffectMat);
- }
- }
- }
效果(開了Bloom后處理,有了Bloom,再挫的畫面也能加不少分,哈哈哈):
總結(jié)
本篇主要是總結(jié)了一下實時渲染當中關于深度(圖)相關的一些內(nèi)容,主要是透視投影,ZBuffer算法,1/Z問題,深度圖的基本使用,Linear01Depth,LinearEyeDepth,ZBuffer精度,Reverse-Z,根據(jù)深度重建世界坐標等內(nèi)容。另外,實現(xiàn)了軟粒子,屏幕空間掃描波,擴散波,屏幕空間高度霧,運動模糊等常見的使用深度圖的效果。效果的實現(xiàn)其實都比較基本,但是擴展性比較強,用深度可以做很多很多好玩的東西。文中的一些特殊效果實現(xiàn)已經(jīng)給出了參考鏈接,另外還參考了樂樂大佬的《shader lab入門精要》關于運動模糊以及高度霧效的部分。不過我還是低估了深度的內(nèi)容,精簡過后還是這么多,可見,深度對于渲染的重要性。更加復雜的渲染效果,如SSAO,陰影,基于深度和法線的描邊效果之類的,還是等之后在寫啦。
注:文中的shader目前都沒有考慮#if UNITY_UV_STARTS_AT_TOP的情況,在5.5版本以前(我只有4.3,5.3,5.5,2017,2018這幾個版本,具體哪個版本開始不需要考慮這個問題,不太確定),PC平臺,開啟AA的情況下會出現(xiàn)RT采樣翻轉(zhuǎn)的情況。以前的一些blog是都加了這個宏判斷的,不過目前測試的新版本2017.3貌似木有了這個問題(也可能觸發(fā)條件變了),所以我就愉快地偷了個懶嘍。
-
分享到:
全部評論:0條