unity屏幕特效綜述5 全局霧效

霧的計算原理其實很簡單
float3 finalColor = f * fogColor + (1-f)originColor
也就是按照某種比例把霧的紋理和原始的紋理相融合,其中比較關鍵的點就是f的選取
f的選取有多種方式,比如說f=dmax-d/dmax-dmin 這種的就是線性的霧,直觀的感覺就是高度越接近dmin,霧的濃度越大,越接近dmax,霧的濃度就越小,此外,還有指數形式的計算方法,公式如下所示,實現的時候只需要把d作爲輸入參數,然後替換一下shader裏面的公式即可。
在這裏插入圖片描述
我們觀察線性霧的公式,其中d是我們不知道的,也就是世界座標的高度值,這就意味着我們想要實現全局霧效的效果就必須要獲得世界座標,獲得世界座標的辦法在運動模糊裏面通過世界投影矩陣矩陣已經實現過了,但是之前的那種辦法需要在shader裏面進行兩次矩陣的乘法運算,很影響算法的效率,在這裏,我們使用另外一種方法來獲取世界座標。
我們將攝像機與屏幕上某一點連線,獲得一個方向向量,然後把這個方向向量
深度值+攝像機的位置就是世界座標了,直觀上也很容易理解。
float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolatedRay;
上面公式中,只有interpolatedRay是未知的,這裏我們簡單的推導一下interpolatedRay是怎麼來的。
interpolatedRay來自於對近裁剪面四個頂點與攝像機形成的方向向量的差值。首先我們需要獲得攝像機和近裁剪面的四個頂點的連線向量。
在這裏插入圖片描述
halfHeight是近裁剪面高度的一半,toTop則是方向朝上,大小爲halfHeight的向量,toRight則是方向朝右,大小爲近裁剪面寬度一半的向量。
在這裏插入圖片描述
在這裏插入圖片描述
上面則是我們需要的四個方向向量的值,理解起來也很容易,就是單純的向量加減運算。
此外,還需要注意一點,深度紋理獲得的深度值是z的值,而不是沿着這四個方向的深度值,我們需要的深度值是下面?的那個值,那纔是真正的深度。
在這裏插入圖片描述由相似三角形也能推導出來
在這裏插入圖片描述
我們把
在這裏插入圖片描述
提取出來,做一個常量,乘以歸一化的上面的四個向量,然後就可以通過float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolatedRay;來求得最後的世界座標了。
首先我們來看一下腳本部分:
腳本中最重要的內容就是計算出來上面提到的四個向量,而計算過程也是中規中矩,按照我們的公式來的。如果上面的公式弄懂了的話,代碼也是水到渠成的。

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;
frustumCorners.SetRow(0, bottomLeft);
frustumCorners.SetRow(1, bottomRight);
frustumCorners.SetRow(2, topRight);
frustumCorners.SetRow(3, topLeft);
                     
material.SetMatrix("_FrustumCornersRay", frustumCorners);

後面就是shader部分了,基本上的思路還是很清晰的,頂點着色器的內容就是判斷出屏幕上的點離近裁剪面四個頂點最近的是哪個向量,然後返回那個向量。中間很長一段的代碼都是在做平臺差異化處理。
片元着色器是整個算法的核心,首先根據前面所討論出來的算法計算出來世界座標,雖然頂點着色器傳過去的只是一個給定的邊緣向量,但是在片元着色器中會進行差值,具體是怎麼差值的我們後面再討論。
然後就是採樣噪聲圖,並通過_Time.y來動態改變採樣的座標,實現動畫效果,再後面就是我們之前提到的線性的霧計算公式中的f的計算,計算出來的f還會根據採樣的噪聲圖進行偏移,得到更加真實的效果,最後再把霧和原圖進行混合即可。
shader源碼:

Shader "Unity Shaders Book/Chapter 15/Fog With Noise" {
	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
		_NoiseTex ("Noise Texture", 2D) = "white" {}
		_FogXSpeed ("Fog Horizontal Speed", Float) = 0.1
		_FogYSpeed ("Fog Vertical Speed", Float) = 0.1
		_NoiseAmount ("Noise Amount", Float) = 1
	}
	SubShader {
		CGINCLUDE
		
		#include "UnityCG.cginc"
		
		float4x4 _FrustumCornersRay;
		
		sampler2D _MainTex;
		half4 _MainTex_TexelSize;
		sampler2D _CameraDepthTexture;
		half _FogDensity;
		fixed4 _FogColor;
		float _FogStart;
		float _FogEnd;
		sampler2D _NoiseTex;
		half _FogXSpeed;
		half _FogYSpeed;
		half _NoiseAmount;
		
		struct v2f {
			float4 pos : SV_POSITION;
			float2 uv : TEXCOORD0;
			float2 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;
			
			#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;
			
			float2 speed = _Time.y * float2(_FogXSpeed, _FogYSpeed);
			float noise = (tex2D(_NoiseTex, i.uv + speed).r - 0.5) * _NoiseAmount;
					
			float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart); 
			fogDensity = saturate(fogDensity * _FogDensity * (1 + noise));
			
			fixed4 finalColor = tex2D(_MainTex, i.uv);
			finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);
			
			return finalColor;
		}
		
		ENDCG
		
		Pass {          	
			CGPROGRAM  
			
			#pragma vertex vert  
			#pragma fragment frag  
			  
			ENDCG
		}
	} 
	FallBack Off
}

對比圖如下:
在這裏插入圖片描述在這裏插入圖片描述 不過,其中我還是有一處疑問的,那就是傳遞到v2f的座標是如何差值到片元着色器的,爲此,我進行了一個小實驗,首先把四個向量的頂點座標映射到(0,1),這樣做是爲了方便的把其作爲顏色顯示出來。然後輸出這四個向量以及差值後的向量

//tst
bottomLeft.x = (bottomLeft.x + 1.1f) / 2.2f;
bottomLeft.y = (bottomLeft.y + 1) / 2;
bottomLeft.z = (bottomLeft.z - 0.6f) / 0.6f;
bottomRight.x = (bottomRight.x + 1.1f) / 2.2f;
bottomRight.y = (bottomRight.y + 1) / 2;
bottomRight.z = (bottomRight.z - 0.6f) / 0.6f;
topLeft.x = (topLeft.x + 1.1f) / 2.2f;
topLeft.y = (topLeft.y + 1) / 2;
topLeft.z = (topLeft.z - 0.6f) / 0.6f;
topRight.x = (topRight.x + 1.1f) / 2.2f;
topRight.y = (topRight.y + 1) / 2;
topRight.z = (topRight.z - 0.6f) / 0.6f;
//

然後我們改寫片元着色器,直接輸出四個向量值
在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述最後再輸出interpolatedRay的值,可以很清楚的看到,它確實是經過線性差值了,至於怎麼差值的,那是硬件做的事情了,不做過多的討論。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章