深度纹理实现全局雾效
一、为什么要实现深度纹理实现全局雾效
主要是因为Unity自带的全局雾效有一下几个缺点 1. 需要为每一个自定义Shader按照书写规则书写雾处理的代码 2. 自带的全局雾效无法实现一些自定义效果 比如: - 基于高度的雾效——可以用来做出悬浮的水雾效果 - 不规则的雾效(结合噪声图实现)——可以为雾增加随机性 - 动态变化的雾、基于纹理的雾等等
二、基本原理
1.基本问题分析
- Linear : f = (end - |d|) / (end – start)
- Exponential : f = 1 – pow(e,−density∗|d|)
- Exponenttial Squared : f = 1 – pow(e,−(density−|d|)²)
通过三个公式可以发现想要计算全局雾效 关键点是得到摄像机的距离 也就是通过深度纹理来获得每一个像素在世界空间下的位置 像素的世界坐标 = 摄像机位置+ 观察空间线性深度值* 摄像机指向像素世界坐标的方向向量 而其中摄像机位置已知,观察空间线性深度值已知(从深度纹理中采样后计算)
那么问题就变成了如何获得摄像机指向像素对应世界坐标的方向向量 并利用坐标偏移的方式得到像素的世界坐标 那么关键点就是讲解如何计算出摄像机指向像素世界坐标的方向向量
2.关键思路
- 顶点着色器中:
- 屏幕后处理中处理的内容是一张抓取的屏幕图像。相当于是一个面片,它具有四个顶点
- 通过c#代码计算四个顶点在世界坐标系下的射线方向后传递给顶点着色器
- 片元着色器中: 当数据传送到片元着色器要处理的每个像素时,像素对应的射线方向是基于4个顶点的射线插值计算而来
- 利用(像素的世界坐标 = 摄像机位置+ 观察空间线性深度值* 摄像机指向像素世界坐标的方向向量),得到对应像素在世界空间下的位置
- 利用得到的世界空间下位置,利用雾的公式计算出对应雾效的颜色
注意点:
1.)
推导出来了四个顶点的方向向量,我们是不是就可以利用它们得到四个顶点的世界空间下坐标了呢? 比如得到左上角的方向向量的单位向量,然后乘以左上角顶点对应像素点的深度值 左上角像素点对应世界坐标= 摄像机位置+ TL.Normalized * Depth ; 注意,如果这样去计算,那么得到的结果是错误的!!!! 因为深度值 Depth即使我们将其转换为观察空间下的线性值,它表示的也是离摄像机在Z轴方向的距离, 并不是两点之间的距离(欧式距离),因此我们还需要对该向量进行处理!
正确处理方法: 
我们可以利用相似三角形的原理,推导出深度值和两点之间距离(欧式距离)的关系。 (Depth / Near) = (dis / |TL|) ==> dis = (|TL| / Near) * Depth
而左上角像素点对应世界坐标 = 摄像机位置 + TL.Normalized * Depth就变为了 左上角像素点对应世界坐标= 摄像机位置 + TL.Normalized * |TL|/Near * Depth 那也就意味着,真正最终和深度一起计算的确定世界坐标位置的方向向量其实就是TL.Normalized * |TL|/Near 由于近裁剪面4个点是对称的,|TL|/Near 可以通用 ,只需要变换前面的单位向量即可
三、实现示例
1.C#部分
1.)关键思路
- 屏幕后处理中处理的内容是一张抓取屏幕图像,相当于一个面片,它具有四个顶点
- 通过C#代码计算四个顶点在世界坐标下的射线方向后传递给顶点着色器 当数据传递到片元着色器要处理每一个像素时,像素对应的射线方向时基于4个顶点的射线插值计算而来
- 利用 像素世界坐标 = 摄像机位置 + 深度值*世界空间下射线方向得到对应像素在世界空间下的位置
- 利用得到的世界空间下位置,通过利用雾的三个公式计算出对应的雾效颜色
2.)实现步骤
- 继承PostEffectBase,重写OnRenderImage
- 在Start中开启深度纹理
- 声明雾相关属性 颜色、浓度、开始距离、最浓距离
- 计算四个顶点的四个射线向量
- 通过4x4的矩阵装载向量,传递给材质 注意:为了之后方便考虑uv翻转问题,我们按左下,右下,右上,左上的逆时针顺序存储
- 将定义好的属性变量传递给材质球
3.)实现示例
1 | public class Fog : PostEffectBase |
2.Shader部分
1.)实现步骤
- 声明属性,映射属性
- 屏幕后处理标配
- ZTest Always
- Cull Off
- ZWrite Off
- v2f结构体
- 考虑翻转深度纹理half2 uv_depth:TEXCOORD1
- 射线向量 float4 ray:TEXCOORD2
- 顶点着色器 图片有四个顶点,会进四次,我们需要判断每一个顶点使用哪一个射线向量 坐标转换、uv赋值(注意考虑深度纹理翻转) 根据uv纹理坐标判断顶点位置,决定赋值哪一个向量(注意考虑深度纹理翻转)
- 顶点着色器 深度纹理采样,转换到观察空间下摄像机的实际距离 利用 摄像机的位置+深度值*射线向量 得到世界空间坐标 通过雾的公式 计算混合因子(这里我们不使用传统的公式) 我们在这里实现一种特殊的基于高度的线性雾效,并且把浓度也用上 进行颜色混合
- FallBack Off
2.实现示例
Shader "Unlit/fog"
{
Properties
{
_MainTex("MainTex",2D) = "white" {}
_FogColor ("FogColor", Color) = (1,1,1,1)
_FogDensity ("FogDensity", Float) = 1.2
_FogStart ("FogStart", Float) = 0
_FogEnd ("FogEnd", Float) = 5
}
SubShader
{
ZTest Always
Cull Off
ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
//纹素 用来判断翻转
half4 _MainTex_TexelSize;
//深度纹理
sampler2D _CamerDepthTexture;
//雾相关的属性
fixed4 _FogColor;
fixed _FogDensity;
float _FogStart;
float _FogEnd;
//矩阵相关 里面存储了 4条射线向量
//0-左下 1-右下
//2-右上 3- 左上
float4x4 _RayMatrix;
struct v2f
{
float2 uv : TEXCOORD0;
//深度纹理uv
float2 uv_depth : TEXCOORD1;
//顶点射线 指向四个角的方法向量(传递到片元时 会自动插值计算)
float4 ray:TEXCOORD2;
float4 vertex : SV_POSITION;
};
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
o.uv_depth = v.texcoord;
//顶点着色器函数 每一个顶点都会执行一次
//对于屏幕后处理来说 就执行四次 因为有四个顶点
//通过uv坐标 判断当前的顶点位置
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)
{
o.uv_depth = 1 - o.uv_depth.y;
index = 3 - index;
}
#endif
//根据顶点的位置 决定使用哪一个射线向量
o.ray = _RayMatrix[index];
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//观察空间下 离摄像机的实际距离(z分量)
float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CamerDepthTexture,i.uv_depth));
//计算世界空间下 像素的坐标
float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.ray;
//雾相关的计算
//计算混合因子
float f = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);
//取零到一之间 超过会去极值
f = saturate(f * _FogDensity);
//利用插值 在两个颜色中进行融合
fixed3 color = lerp(tex2D(_MainTex,i.uv).rgb,_FogColor.rgb,f);
return fixed4(color.rgb,1);
}
ENDCG
}
}
Fallback Off
}