預覽圖,使用了Noise+屏幕後處理霧
Unity中也有內置 的霧,在Lighting-setting下,有個fog選項。
但是這種霧是非動效的,
大概就是這樣的。
參數信息: 中文文檔。
https://connect.unity.com/doc/Manual/GlobalIllumination
官方也是推薦使用 後期渲染來實現霧效;
屏幕後處理:
主要是利用 相機渲染時,對原渲染圖進行處理,
以得到一些效果。
在U3D 中實現、
可以通過Monobehaviour 自帶的OnRenderImage(RenderTexture src, RenderTexture dest)
函數,第一個參數是原圖像,dest是 目標。 即輸出結果。
可以使用Graphics.Blit(src,dest,material);
使用改材質球裏的shader 對src進行處理,然後存儲在dest中;
全局霧效的實現關鍵
根據深度紋理來重建每個像素在世界空間下的位置。
如何得到世界座標
座標系中的一個頂點座標可以通過它相對於另一個頂點座標的偏移量來對得;
在頂點作色器下,對圖像空間下的視錐體射線(從攝像機出發,指向圖像上的某點的射線)進行插值,這條射線存儲了該像素在空間下到攝像機的方向信息。然後把該射線和線性化的視角空間下的深度值相乘,再加上相機的位置。就得到了該像素在世界空間下位置。
另一種計算
-
是在片元函數裏,利用相機視角*投影矩陣的逆矩陣來得到世界空間下的像素座標,比較消耗性能,在運動模糊的章節裏也使用了這個方法,計算前兩幀下的像素的座標,然後做差得到速度,然後根據速度來進行UV計算,以此達到模糊的目的;
-
比較方便和暴力的做法是使用一個通道來存儲worldPos,
o.worldPos=mul(unity_ObjectToWorld,v.vertex);
使用矩陣進行轉換,這樣的化,其實屏幕每個頂點都進行了計算,性能也會比較消耗;
OnRenderImage
在OnRenderImage方法裏,利用
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material==null)
{
material=CheckShaderAndCreateMaterial(fogSharder,material);
Graphics.Blit(src,dest);
}else
{
Matrix4x4 frustumCorners= Matrix4x4.identity;
float fov=Camera.fieldOfView;
float near=Camera.nearClipPlane;
float far=Camera.farClipPlane;
float aspect=Camera.aspect;
//halfHeight= Near* tan(FOV/2)
float halfHeight=near* Mathf.Tan(fov*0.5f*Mathf.Deg2Rad);
Vector3 toRight=MyCameraTransForm.right*halfHeight*aspect;
Vector3 toTop=MyCameraTransForm.up*halfHeight;
Vector3 TL=MyCameraTransForm.forward*near+toTop-toRight;
float scale=TL.magnitude/near;
TL.Normalize();
TL*=scale;
Vector3 TR=MyCameraTransForm.forward*near+toTop+toRight;
TR.Normalize();
TR*=scale;
Vector3 BL=MyCameraTransForm.forward*near-toTop-toRight;
BL.Normalize();
BL*=scale;
Vector3 BR=MyCameraTransForm.forward*near-toTop+toRight;
BR.Normalize();
BR*=scale;
frustumCorners.SetRow(0,BL);
frustumCorners.SetRow(1,BR);
frustumCorners.SetRow(2,TR);
frustumCorners.SetRow(3,TL);
material.SetMatrix("_FrustumConrners",frustumCorners);
material.SetMatrix("_ViewProjectionInverseMatrix",(Camera.cameraToWorldMatrix*Camera.projectionMatrix).inverse);
material.SetFloat("_density",density);
material.SetFloat("_fogEnd",fogEnd);
material.SetFloat("_fogStart",fogStart);
material.SetColor("_fogColor",fogcorlor);
material.SetFloat("_fogSpeedX",FogSpeedX);
material.SetFloat("_fogSpeedY",FogSpeedY);
material.SetFloat("_NoiseAmount",NoiseAmount);
material.SetTexture("_NoiseTex",NoiseTexture);
Graphics.Blit(src,dest,material);
}
CheckShaderAndCreateMaterial 函數會檢查該材質是否擁有該shader,如果不是就使用該shader創建一個材質球。
解析
halfHeight的計算 ,tan(FOV/2)*near ,也就是toTop的絕對值。
tan公式是 對邊/領邊,fov 也就是視野最下面的三角形的角度,
FOV
什麼是FOV
near
near是
相對的Far就是 到遠裁剪面的距離;
參考基礎知識 第四章p78
根據 tan的 計算, 對邊就是 halfheight, tan(fov/2)實際上就是 halfheight/near,爲了得到 halfheight,只需要*上 near 即可;
toRight的計算可以 根據 相機的寬高比得到。其餘的 剩下 就比較好求了。
近似原理
但是得到的並不是點到攝像機的歐式距離,而是在z方向上的距離。但是可以根據三角形的相似原理得到。 TL,TR,BL,BR,四個點是互相對稱的,也就是說它們的長度是相等的。
這裏就得到了 scale =|TL| / Near
使用Normalize得到單位向量;
因此在上面的代碼中求 scale 以TL的長度/near得到 scale。
最後求得這個四個角對應的變量並把它們存儲在一個矩陣類型的變量中;
然後傳遞所需要的參數;
Shader "Custom/FogWithDepthTexture"
{
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_density ("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
_fogSpeedX("_fogSpeedX",Float)=1
_fogSpeedY("_fogSpeedY",Float)=1
_NoiseAmount("NoiseAmount",float)=1
_NoiseTex("NoiseTex",2D)=""{}
}
SubShader {
CGINCLUDE
#include "UnityCG.cginc"
float4x4 _FrustumConrners;
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _CameraDepthTexture;
sampler2D _NoiseTex;
float4 _NoiseTex_ST;
half _density;
fixed4 _fogColor;
float _fogStart;
float _fogSpeedX;
float _fogSpeedY;
float _NoiseAmount;
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;
#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 = _FrustumConrners[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(_fogSpeedX,_fogSpeedY);
float noise=(tex2D(_NoiseTex,i.uv+speed).r-0.5)*_NoiseAmount;
float fogDensity = (_fogEnd - worldPos.y) / (_fogEnd - _fogStart);
fogDensity = saturate(fogDensity * _density*(1+noise));
fixed4 finalColor = tex2D(_MainTex, i.uv);
finalColor.rgba = lerp(finalColor.rgba, _fogColor.rgba, fogDensity);
return finalColor;
}
ENDCG
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
FallBack Off
}
在vert 中通過 0.5 (原點)來判斷它們的方位,分別對應在CS腳本中構建矩陣的點。分別是左下,右下,右上,左上;
UNITY_UV_STARTS_AT_TOP 這個是Unity的宏定義,判斷平臺是 UV不不是 從上開始的渲染的,這個主要是因爲平臺差異性,Dx是從上往下,而OpenGL是從下往上;因此需要水平翻轉下。
在片元着色器中
SAMPLE_DEPTH_TEXTURE()從相機深度紋理中進行採樣得到 深度值,但得到是非線性的,因此
使用LinearEyeDepth 得到線性深度值;
接着 就可以根據那個原理計算 世界座標了;
霧的計算公式
基於高度的霧的計算公式
float2 speed=_Time.yfloat2(_fogSpeedX,_fogSpeedY)
使用 _Time 內置時間 變量和 自帶的參數 來控制速度
float noise=(tex2D(_NoiseTex,i.uv+speed).r-0.5)_NoiseAmount;
從噪音貼圖上進行 採樣,使用R通道來進行 計算;noise係數;
fogDensity = saturate(fogDensity * _density*(1+noise)); 計算濃度
作爲 混合 係數;
fixed4 finalColor = tex2D(_MainTex, i.uv);
finalColor.rgba = lerp(finalColor.rgba, _fogColor.rgba, fogDensity);
最後進行混合得到 最終效果。