Unity Shader入門精要學習筆記 - 第13章 使用深度和法線紋理

轉載自 馮樂樂的 《Unity Shader 入門精要》

獲取深度和法線紋理

雖然在Unity裏獲取深度和法線紋理的代碼非常簡單,但是我們有必要在這之前首先了解它們背後的實現原理。

深度紋理實際上就是一張渲染紋理,只不過它裏面存儲的像素值不是顏色值而是一個高精度的深度值。由於被存儲在一張紋理中,深度紋理裏的深度值範圍是[0,1],而且通常是非線性分佈的。那麼,這些深度值是從哪裏得到的呢?總體來說,這些深度值來自於頂點變換後得到的歸一化的設備座標(Normalized Device Coordinates, NDC)。一個模型要想最終被繪製在屏幕上,需要把它的頂點從模型空間變換到齊次裁剪座標系下,這是通過在頂點着色器中乘以MVP變換矩陣得到的。在變換的最後一步,我們需要使用一個投影矩陣來變換頂點,當我們使用的是透視投影類型的攝像機時,這個投影矩陣就是非線性的。

下圖顯示了之前給出的Unity中透視投影對頂點的變換過程,下圖最左側的圖顯示了投影變換前,即觀察空間下視錐體的結果以及相應的頂點位置,中間的圖顯示了應用透視裁剪矩陣後的變換結果,即頂點着色器階段輸出的頂點變換結果,最右側的圖則是底層硬件進行了透視除法後得到的歸一化的設備座標。需要注意的是,這裏的投影過程是建立在Unity對座標系的假定上的,也就是說,我們針對的是觀察空間爲右手座標系,使用列矩陣在矩陣右側進行相乘,且變換到NDC後z分量範圍將在[-1,1]之間的情況。而類似DirectX 這樣的圖形接口中,變換後z分量範圍將在[0,1]之間。


下圖顯示了再使用正交攝像機時投影變換的過程。同樣變換後會得到一個範圍爲[-1,1]的立方體。正交投影使用的變換矩陣是線性的。


在得到NDC後,深度紋理中的像素值就可以很方便地計算得到了,這些深度值就對應了NDC中頂點座標的z分量的值。由於NDC中z分量的範圍在[-1,1],爲了讓這些值能夠存儲在一張圖像中,我們需要使用下面的公式對其進行映射:


其中,d對應了深度紋理中的像素值,Z(ndc)對應了NDC座標中的z分量的值。

在Unity中,深度紋理可以直接來自於真正的深度緩存,也可以是由一個單獨的Pass渲染而得,這取決於使用的渲染路徑和硬件。通常來講,當使用延遲渲染路徑時,深度紋理理所當然可以訪問到,因爲延遲渲染會把這些信息渲染到G-buffer 中。而當無法直接獲取深度緩存時,深度和法線紋理是通過一個單獨的Pass渲染而得的。具體實現是,Unity會使用着色器替換技術選擇那些渲染類型爲Opaque的物體,判斷它們使用的渲染隊列是否小於2500,如果滿足條件,就把它渲染到深度和法線紋理中。因此,要想讓物體能夠出現在深度和法線紋理中,就必須在Shader中設置正確的RenderType 標籤。

在Unity中,我們可以選擇讓一個攝像機生成一張深度紋理或是一張深度+法線紋理。當渲染前者,即只需要一張單獨的深度紋理時,Unity會直接獲取深度緩存或是按之前講到的着色器替換技術,選取需要的不透明物體,並使用它投射陰影時使用的Pass(即LightMode被設置爲ShadowCaster的Pass)來得到深度紋理。如果Shader 中不包含這樣一個Pass,那麼這個物體就不會出現在深度紋理中(當然,它也不能向其他物體投射陰影)。深度紋理的精度通常是24位或16位,這取決於使用的深度緩存的精度。如果選擇生成一張深度+法線紋理,Unity會創建一張和屏幕分辨率相同、精度爲32位的(紋理),其中觀察空間下的法線信息會被編碼進紋理的R和G通道,而深度信息會被編碼進B和A通道。法線信息的獲取在延遲渲染中是可以非常容易就得到的,Unity只需要合併深度和法線緩存即可。而在前向渲染中,默認情況下是不會創建法線緩存的,因此Unity底層使用了一個單獨的Pass把整個場景再次渲染一遍來完成。這個Pass被包含在Unity內置的一個Unity Shader 中,我們可以在內置的builtin_shaders-xxx/DefaultResources/Camera-DepthNormalTexture.shader文件中找到這個用於渲染深度和法線信息的Pass。

在Unity中,獲取深度紋理是非常簡單的,我們只需要告訴Unity“把深度紋理給我!”然後再在Shader中直接訪問特定的紋理屬性即可。這個與Unity溝通的過程是通過在腳本中設置攝像機的depthTextureMode來完成的,例如我們可以通過下面的代碼來獲取深度紋理:

camera.depthTextureMode = DepthTextureMode.Depth;
一旦設置好了上面的攝像機模式後,我們就可以在Shader中通過聲明_CameraDepthTexture變量來訪問它。這個過程非常簡單,但我們需要知道兩行代碼的背後,Unity爲我們做了許多工作。

同理,如果想要獲取深度+法線紋理,我們只需要在代碼中這樣設置:

camera.depthTextureMode = DepthTextureMode.DepthNormals;
然後在Shader中通過聲明_CameraDepthNormalsTexture變量來訪問它。

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

camera.depthTextureMode |= DepthTextureMode.Depth;
camera.depthTextureMode |= DepthTextureMode.DepthNormals;
在Unity5中,我們還可以在攝像機的Camera組件上看到當前攝像機是否需要渲染深度或深度+法線紋理。當在Shader中訪問到深度紋理_CameraDepthTexture 後,我們就可以使用當前像素的紋理座標對它進行採樣。絕大多數情況下,我們直接使用tex2D函數採樣即可,但在某些平臺上,我們需要一些特殊處理。Unity爲我們提供了一個統一的宏SAMPLE_DEPTH_TEXTURE,用來處理這些由於平臺差異造成的問題。而我們只需要在Shader中使用SAMPLE_DEPTH_TEXTURE宏對深度紋理進行採樣,例如:

float d = SMAPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
其中,i.uv 是一個float2類型的變量,對應了當前像素的紋理座標。類似的宏還有SAMPLE_DEPTH_TEXTURE_PROJ 和 SAMPLE_DEPTH_TEXTURE_LOD。SAMPLE_DEPTH_TEXTURE_PROJ 宏同樣接受兩個參數——深度紋理和一個float3或float4類型的紋理座標,它的內部使用了tex2Dproj這樣的函數進行投影紋理採樣,紋理座標的前兩個分量首先會除以最後一個分量,再進行紋理採樣。如果提供了第四個分量,還會進行一次比較, 通常用於陰影的實現中。SAMPLE_DEPTH_TEXTURE_PROJ 的第二個參數通常是由頂點着色器輸出插值而得的屏幕座標,例如:
float d = SMAPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.srcPos));
其中,i.srcPos 是在頂點着色器中通過調用ComputeScreenPos(o.pos)得到的屏幕座標。上述這些宏,可以在Unity內置的HLSLSupport.cginc文件中找到。

當通過紋理採樣得到深度值後,這些深度值往往是非線性的,這種非線性來自於透視投影使用的裁剪矩陣。然而,在我們的計算過程中通常是需要線性的深度值,也就是說,我們需要把投影后的深度值變換到線性空間下,例如視角空間下的深度值。那麼,我們應該如何進行這個轉換呢?實際上,我們只需要倒推頂點變換的過程即可。下面我們以透視投影爲例,推導如何由深度紋理中的深度信息計算得到視角空間下的深度值。

我們之前已知,當我們使用透視投影的裁剪矩陣P(clip)對視角空間下的一個頂點進行變換後,裁剪空間下頂點的z和w分量爲:


其中,Far 和 Near 分別是遠近裁剪平面的距離。然後,我們通過齊次除法就可以得到NDC下的z分量:


之前我們知道,深度紋理中的深度值是 通過下面的公式由NDC計算而得的:


由上面的這些式子,我們可以推導出用d表示而得的Z(visw)的表達式:


由於在Unity使用的視角空間中,攝像機正向對應的z值均爲負值,因此爲了得到深度值的正數表示,我們需要對上面的結果取反,最後得到的結果如下:


它的取值範圍就是視錐體深度範圍,即[Near,Far]。如果我們想要得到範圍在[0, 1]之間的深度值,只需要把上面得到的結果除以Far即可。這樣,0就表示該點與攝像機位於同一位置,1表示該點位於視錐體的遠裁剪平面上。結果如下:


幸運的是,Unity提供了兩個輔助函數來爲我們進行上述的計算過程——LinearEyeDepth 和 Linear01Depth。LinearEyeDepth 負責把深度紋理的採樣結果轉換到視角空間下的深度值,也 就是我們上面得到的Z(visw)。而 Linear01Depth 則會返回一個範圍在[0, 1]的線性深度值,也就是我們上面得到的Z(01),這兩個函數內部使用了內置的_ZBufferParams變量來得到遠近裁剪平面的距離。

如果我們需要獲取深度+法線紋理,可以直接使用tex2D函數對_CameraDepthNormalsTexture 進行採樣,得到裏面存儲的深度和法線信息。Unity提供了輔助函數來爲我們隊這個採樣結果進行解碼,從而得到深度值和法線方向。這個函數是DecodeDepthNormal,它在UnityCG.cginc裏被定義:

inline void DecodeDepthNormal(float4 enc, out float depth,out float3 normal){
	depth = DecodeFloatRG(enc.zw);
	normal = DecodeViewNormalStereo(enc);
}
DecodeDepthNormal 的第一個參數是對深度+法線紋理的採樣結果,這個採樣結果是Unity對深度和法線信息編碼後的結果,它的xy分量存儲的是視角空間下的法線信息,而深度信息被編碼進了zw分量。通過調用DecodeDepthNormal 函數對採樣結果解碼後,我們就可以得到解碼後的深度值和法線。這個深度值是範圍在[0, 1]的線性深度值(這與單獨的深度紋理中存儲的深度值不同),而得到的法線則是視角空間下的法線方向。同樣,我們也可以通過調用DecodeFloatRG 和 DecodeViewNormalStereo來解碼深度+法線紋理中的深度和法線信息。

很多時候,我們希望可以查看生成的深度和法線紋理,以便對Shader進行調試。Unity 5 提供了一個方便的方法來查看攝像機生成的深度和法線紋理,這個方法就是利用幀調試器。下圖顯示了幀調試器查看到的深度紋理和深度+法線紋理。


使用幀調試器查看到的深度紋理是非線性空間的深度值,而深度+法線紋理都是由Unity編碼後的結果。有時,顯示出線性空間下的深度信息或解碼後的法線方向會更加有用。此時,我們可以自行在片元着色器中輸出轉換或解碼後的深度和法線值,如下圖所示。


輸出代碼非常簡單,我們可以使用類似下面的代碼來輸出線性深度值:

float depth = SMAPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv);
float linearDepth = Linear01Depth(depth);
return fixed4(linearDepth,linearDepth,linearDepth,1.0);
或是輸出法線方向:

fixed3 normal = DecodeViewNormalStereo(tex2D(_CameraDepthNormalsTexture, i.uv).xy);
return fixed4(normal * 0.5 + 0.5, 1.0);

在查看深度紋理時,我們得到的畫面可能幾乎是全黑或全白的。這時我們可以把攝像機的遠裁剪平面的距離(Unity默認爲1000)調小,使視錐體的範圍剛好覆蓋場景的所在區域。這是因爲,由於投影變換時需要覆蓋從近裁剪平面到遠裁剪平面的所有深度區域,當遠裁剪平面的距離過大時,會導致離攝像機較近的距離被映射到非常小的深度值,如果場景是一個封閉的區域,那麼這就會導致畫面看起來幾乎是全黑的。相反,如果場景是一個開放區域,且物體離攝像機的距離較遠,就會導致畫面幾乎是全白的。


再談運動模糊

在之前,我們學習瞭如何通過混合多張屏幕圖像來模擬運動模糊的效果。但是,另外一種應用更加廣泛的技術則是使用速度映射圖。速度映射圖中存儲了每個像素的速度,然後使用這個速度來決定模糊的方向和大小。速度緩衝的生成有多種方法,一種方法是把場景中所有物體的速度渲染到一張紋理中。但這個方法的缺點在於需要修改場景中所有物體的Shader代碼,使其添加計算速度的代碼並輸出到一個渲染紋理中。

《GPU Gems》在第27章中介紹了一種生成速度映射圖的方法。這種方法利用深度紋理在片元着色器中爲每個像素計算其在世界空間下的位置,這是通過使用當前的視角*投影矩陣的逆矩陣對NDC下的頂點座標進行變換得到的。當得到世界空間中的頂點座標後,我們計算前一幀和當前幀的位置差,生成該像素的速度。這種方法的有點是可以在一個屏幕後處理步驟中完成整個效果的模擬,但缺點是需要在片元着色器中進行兩次矩陣乘法的操作,對性能有所影響。

爲了使用深度紋理模擬運動模糊,我們進行如下準備工作:

1)新建場景,去掉天空盒子

2)搭建一個測試運動模糊的場景,構建了一個包含3面牆的方法,並放置了4個立方體。

3)在攝像機上新建一個腳本MotionBlurWithDepthTexture.cs

4)新建一個Shader Chapter13-MotionBlurWithDepthTexture

我們先編寫MotionBlurWithDepthTexture.cs 腳本

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 Camera camera {
		get {
			if (myCamera == null) {
				myCamera = GetComponent<Camera>();
			}
			return myCamera;
		}
	}
	//定義運動模糊時模糊圖像使用的大小
	[Range(0.0f, 1.0f)]
	public float blurSize = 0.5f;

	//保存上一幀攝像機的視角*投影矩陣
	private Matrix4x4 previousViewProjectionMatrix;
	
	void OnEnable() {
		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部分:

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 = mul(UNITY_MATRIX_MVP, v.vertex);
			
			o.uv = v.texcoord;
			o.uv_depth = v.texcoord;
			
			//進行平臺差異化處理
			#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 {
			// Get the depth buffer value at this pixel.
			float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
			// H is the viewport position at this pixel in the range -1 to 1.
			float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1);
			// Transform by the view-projection inverse.
			float4 D = mul(_CurrentViewProjectionInverseMatrix, H);
			// Divide by w to get the world position. 
			float4 worldPos = D / D.w;
			
			// Current viewport position 
			float4 currentPos = H;
			// Use the world position, and transform by the previous view-projection matrix.  
			float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos);
			// Convert to nonhomogeneous points [-1,1] by dividing by w.
			previousPos /= previousPos.w;
			
			// Use this frame's position and last frame's to compute the pixel velocity.
			float2 velocity = (currentPos.xy - previousPos.xy)/2.0f;
			
			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
}
我們在這邊實現的運動模糊適用於靜止場景、攝像機快速運動的情況,這是因爲我們在計算時只考慮了攝像機的運動。如果把這邊的代碼應用到一個物體快速運動而攝像機靜止的場景,會發現不會產生任何運動模糊效果。

全局霧效

霧效是遊戲裏經常使用的一種效果。Unity內置的霧效可以產生基於距離的線性或指數霧效。然而,要想在自己編寫的頂點/片元着色器中實現這些霧效,我們需要在Shader 中添加#pragma multi_compile_fog 指令,同時還需要使用相關的內置宏,例如UNITY_FOG_COORDS、UNITY_TRANSFER_FOG和UNITY_APPLY_FOG等。這種方法的缺點在於,我們不僅需要爲場景中所有物體添加相關的渲染呆嗎,而且能夠實現的效果也非常有限。當我們需要對霧效進行一些個性化操作時,例如使用基於高度的霧效等,僅僅使用Unity內置的霧效就變得不再可行。

我們使用一種基於屏幕後處理的全局霧效的實現。使用這種方法,我們不需要更改場景內渲染的物體所使用的Shader 代碼,而僅僅依靠一次屏幕後處理的步驟即可。這種方法的自由度高,我們可以方便地模擬各種霧效,例如均勻的霧效、基於距離的線性/指數霧效、基於高度的霧效等。我們可以得到類似下圖中的結果。

基於屏幕後處理的全局霧效的關鍵是,根據深度紋理來重建每個像素在世界空間下的位置。儘管之前我們在模擬運動模糊時已經實現了這個要求,即構建出當前像素的NDC座標,再通過當前攝像機的視角*投影矩陣的逆矩陣來得到世界空間下的像素座標,但是,這樣的實現需要在片元着色器中進行矩陣乘法的操作,而這通常會影響遊戲性能。我們學習一個快速從深度紋理中重建世界座標的方法。這種方法首先對圖像空間下的視錐體射線(從攝像機出發,指向圖像上的某點的射線)進行插值,這條射線存儲了該像素在世界空間下到攝像機的方向信息。然後,我們把該射線和線性化後的視角空間下的深度值相乘,再加上攝像機的世界位置,就可以得到該像素在世界空間下的位置。當我們得到世界座標後,就可以輕鬆地使用各個公式來模擬全局霧效了。

我們知道,座標系中的一個頂點座標可以通過它相對於另一個頂點座標的偏移量來求得。重建像素的世界座標也是基於這樣的思想。我們只需要知道攝像機在世界空間下的位置,以及世界空間下該像素相對於攝像機的偏移量,把它們相加就可以得到該像素的世界座標。整個過程可以使用下面的代碼來表示:

float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolateRay;

其中,_WorldSpaceCameraPos 是攝像機在世界空間下的位置,這可以由Unity 的內置變量直接訪問得到。而linearDepth * interpolatedRay 則可以計算得到該像素相對於攝像機的偏移量,linearDepth 是由深度紋理得到的線性深度值,interpolatedRay 是由頂點着色器輸出並插值後得到的射線,它不僅包含了該像素到攝像機的方向,也包含了距離信息。

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


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


其中,Near 是近裁剪平面的距離,FOV是豎直方向的視角範圍,camera.up、camera.right分別對應了攝像機的正上方和正右方。當得到這兩個輔助向量後,我們就可以計算4個角相對於攝像機的方向了。我們以左上角爲例,它的計算公式如下:


同理,其他3個角的計算也是類似的:


注意,上面求得的4個向量不僅包含了方向信息,它們的模對應了4個點到攝像機的空間距離。由於我們得到的線性深度值並非是點到攝像機的歐式距離,而是在z方向上的距離,因此,我們不能直接使用深度值和4個角的單位方向的乘積來計算它們到攝像機的偏移量,如下圖所示。


想要在深度值轉換成到攝像機的歐式距離也很簡單,我們以TL點爲例,根據相似三角形原理,TL所在的射線上,像素的深度值和它到攝像機的實際距離的比等於近裁剪平面的距離和TL向量的模的比,即


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


由於4個點相互對稱,因此其他3個向量的模和TL相等,即我們可以使用同一個因子和單位向量相乘,得到它們對應的向量值:


屏幕後處理的原理是使用特定的材質去渲染一個剛好填充整個屏幕的四邊形面片。這個四邊形面片的4個頂點就對應了近裁剪平面的4個角。由此,我們可以把上面的計算結果傳遞給頂點着色器,頂點着色器根據當前的位置選擇它所對應的向量,然後再將其輸出,經插值後傳遞給片元着色器得到interpolatedRay,我們就可以利用之前提到的公式重建該像素在世界空間下的位置了。

在簡單的霧效實現中,我們需要計算一個霧效係數f,作爲混合原始顏色和霧的顏色的混合係數:

float3 afterFog = f*fogColor + (1 - f) * origColor;
這個霧效係數f 有很多計算方法。在Unity 內置的霧效實現中,支持三種霧的計算方式——線性、指數以及指數的平方。當給定距離z後,f的計算公式分別如下:

Linear:


Exponential:


Exponential Squared:


我們使用類似線性霧的計算方式,計算基於高度的霧效。具體方法是,當給定一點在世界空間下的高度y後,f的計算公式爲:


爲了在Unity中實現基於屏幕後處理的霧效,我們需要進行如下準備工作。

1)新建一個場景,去掉天空盒子。

2)構建一個包含3面牆的房間,放置兩個立方體和兩個球體

3)在攝像機上新建一個腳本FogWithDepthTexture.cs。

4)新建一個Shader  Chapter13-FogWithDepthTexture。

首先,我們編寫FogWithDepthTexture.cs

public class FogWithDepthTexture : PostEffectsBase {

	public Shader fogShader;
	private Material fogMaterial = null;

	public Material material {  
		get {
			fogMaterial = CheckShaderAndCreateMaterial(fogShader, fogMaterial);
			return fogMaterial;
		}  
	}

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

	private Transform myCameraTransform;
	public Transform cameraTransform {
		get {
			if (myCameraTransform == null) {
				myCameraTransform = camera.transform;
			}

			return myCameraTransform;
		}
	}

	//控制霧的濃度
	[Range(0.0f, 3.0f)]
	public float fogDensity = 1.0f;

	//控制霧的顏色
	public Color fogColor = Color.white;
	//霧效的起始高度
	public float fogStart = 0.0f;
	//霧效的終止高度。
	public float fogEnd = 2.0f;

	void OnEnable() {
		camera.depthTextureMode |= DepthTextureMode.Depth;
	}
	
	void OnRenderImage (RenderTexture src, RenderTexture dest) {
		if (material != null) {
			Matrix4x4 frustumCorners = Matrix4x4.identity;
			//先計算近裁剪平面的四個角對應的向量
			float fov = camera.fieldOfView;
			float near = camera.nearClipPlane;
			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();
			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;
			//將4個向量存儲在矩陣類型的frustumCorners 中
			frustumCorners.SetRow(0, bottomLeft);
			frustumCorners.SetRow(1, bottomRight);
			frustumCorners.SetRow(2, topRight);
			frustumCorners.SetRow(3, topLeft);

			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部分:

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"
		
		float4x4 _FrustumCornersRay;
		
		sampler2D _MainTex;
		half4 _MainTex_TexelSize;
		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 = mul(UNITY_MATRIX_MVP, v.vertex);
			
			o.uv = v.texcoord;
			o.uv_depth = v.texcoord;
			
			#if UNITY_UV_STARTS_AT_TOP
			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;
			}

			#if UNITY_UV_STARTS_AT_TOP
			if (_MainTex_TexelSize.y < 0)
				index = 3 - index;
			#endif
			
			o.interpolatedRay = _FrustumCornersRay[index];
				 	 
			return o;
		}
		
		fixed4 frag(v2f i) : SV_Target {
			float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth));
			float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;
						
			float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart); 
			fogDensity = saturate(fogDensity * _FogDensity);
			
			fixed4 finalColor = tex2D(_MainTex, i.uv);
			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
}
需要注意的是,這裏的實現是基於攝像機的投影類型是透視投影的前提下的。如果需要在正交投影的情況下重建世界座標,需要使用不同的公式。

再談邊緣檢測

之前我們曾介紹如何使用Sobel算子對屏幕圖像進行邊緣檢測,實現描邊的效果。但是,這種直接利用顏色信息進行邊緣檢測的方法會產生很多我們不希望得到的邊緣線,如下圖所示。


可以看出,物體的紋理、陰影等位置也被描上黑邊,而這往往不是我們希望看到的。我們將學習如何在深度和法線紋理上進行邊緣檢測,這些圖像不會受紋理和光照的影響,而僅僅保存了單錢渲染物體的模型信息,通過這樣的方式檢測出來的邊緣更加可靠。我們可以得到類似下圖中的結果。


我們使用Robert算子來進行邊緣檢測。它使用的卷積核如下圖所示。


Roberts 算子的本質就是計算左上角和右下角的插值,乘上右上角和左下角的差值,作爲評估邊緣的依據。在下面的實現中,我們也會按這樣的方式,取對角方向的深度或法線值,比較它們之間的差值,如果超過某個閾值,就認爲它們之間存在一條邊。

我們進行如下準備工作。

1)新建一個場景,去掉天空盒子。

2)構建一個包含3面牆的房間,放置兩個立方體和兩個球體。

3)攝像機上添加EdgeDetectNormalsAndDepth.cs 腳本

4)新建一個Shader Chapter13-EdgeDetectNormalAndDepth。

我們先修改EdgeDetectNormalsAndDepth.cs 腳本

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;

	public float sampleDistance = 1.0f;

	public float sensitivityDepth = 1.0f;

	public float sensitivityNormals = 1.0f;
	
	void OnEnable() {
		GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
	}

	//[ImageEffectOpaque] 讓透明物體不被描邊
	[ImageEffectOpaque]
	void OnRenderImage (RenderTexture src, RenderTexture dest) {
		if (material != null) {
			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

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)
		_SampleDistance ("Sample Distance", Float) = 1.0
		_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;
			half2 uv[5]: TEXCOORD0;
		};
		  
		v2f vert(appdata_img v) {
			v2f o;
			o.pos = mul(UNITY_MATRIX_MVP, 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
			
			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;
			float centerDepth = DecodeFloatRG(center.zw);
			half2 sampleNormal = sample.xy;
			float sampleDepth = DecodeFloatRG(sample.zw);
			
			// difference in normals
			// do not bother decoding normals - there's no need here
			half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x;
			int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;
			// difference in depth
			float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
			// scale the required threshold by the distance
			int isSameDepth = diffDepth < 0.1 * centerDepth;
			
			// return:
			// 1 - if normals and depth are similar enough
			// 0 - otherwise
			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;
			
			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
}
我們實現的描邊效果是基於整個屏幕空間進行的,也就是說,場景內所有物體都會被添加描邊效果。但有時,我們希望只對特定的物體進行描邊,例如當玩家渲染場景中的某個物體後,我們想要在該物體周圍添加一層描邊效果。這時,我們需要使用Unity提供的Graphics.DrawMesh 或 Graphics.DrawMeshNow 函數把需要描邊的物體再次渲染一次(在所有不透明物體渲染完畢後),然後再使用本節提到的邊緣檢測算法計算深度或法線紋理中每個像素的梯度值,判斷它們是否小於某個閾值,如果是,就再Shader 中使用clip函數將該像素剔除掉,從而顯示原來的物體顏色。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章