预览图,使用了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);
最后进行混合得到 最终效果。