1、为什么要实现屏幕后处理效果的全局雾效
既然Unity中已经提供了全局雾效,那为什么还要自己来实现呢?主要是因为Unity自带的全局雾效有以下几个缺点:
- 需要为每个自定义Shader按规则书写雾效处理代码
- 自带的全局雾效无法实现一些自定义效果,比如:
- 基于高度的雾效 - 可以用来做出悬浮的水雾效果
- 不规则的雾效(结合噪声图实现) - 可以为雾增加随机性和不规则形
- 动态变化的雾、基于纹理的雾 等等
总体而言,就是Unity自带的全局雾效只能满足最基础的效果,较为局限。
因此通过结合深度纹理来制作屏幕后处理的全局雾效,来感受同一种效果的不同实现思路。
而基于深度纹理的全局雾效,它相对于Unity自带的全局雾效的好处是:
- 一次屏幕后处理便可以得到雾的效果,不用为每个自定义Shader添加雾效代码
- 可以基于该全局雾效拓展出多种雾效,可以方便的模拟出线性、指数、指数平方雾效,甚至实现一些基于高度的雾效、使用噪声图的雾效、动态变化的雾效等等
2、基本原理
想要实现基于深度纹理的屏幕后处理的全局雾效的关键点就是如何利用深度纹理来获得每个像素在世界空间下的位置,这样才能计算出离摄像机的距离,才能利用雾的计算公式计算混合因子来实现雾效!
在【基于深度纹理实现的运动模糊】相中,也利用了深度纹理获取了像素点在世界空间下的位置
但是这种做法有一个很大的缺点,那就是性能消耗较高,原因主要有以下两点:
- 在片元着色器函数中进行计算,计算次数较大
- 每次都进行了矩阵变换计算,计算量较大
因此在实现全局雾效时将不会使用这种方式,而是使用一种性能更好的计算方式!
这种性能更好的新方法的主要思路还是利用深度纹理来获得每个像素在世界空间下的位置
除此以外我们还需要获得摄像机指向像素对应世界坐标的方向向量
并利用坐标偏移的方式得到像素的世界坐标!
像素的世界坐标 = 摄像机位置 + 观察空间线性深度值 * 摄像机指向像素世界坐标的方向向量
而其中摄像机位置已知,观察空间线性深度值已知(从深度纹理中采样后计算)
那么关键点就是讲解如何计算出 摄像机指向像素世界坐标的方向向量
关键思路:
- 顶点着色器中
- 屏幕后处理中处理的内容是一张抓取的屏幕图像,相当于是一个面片,它具有4个顶点(四个角),这四个顶点即顶点着色器中处理的所有顶点
- 我们其实可以认为屏幕后处理中处理的屏幕图像的四个顶点,就是摄像机视锥体中近裁剪面的四个角,因为在裁剪空间变换中,我们是将观察空间变换到了裁剪空间再到NDC空间中,最终又变换到了屏幕空间中,可以理解相当于把近裁剪面变换到了屏幕空间中。因此近裁剪面的四个角相当于是屏幕图像四个顶点在世界空间下的位置、
- 通过C#代码计算四个顶点在世界坐标系下的射线方向后传递给顶点着色器
- 这一步我们可以在C#中计算好,然后将结果作为参数传递到Shader的变量中,在顶点着色器中使用即可:
- halfH = Near * tan(FOV/2) halfW = halfH * aspect
- toTop = Camera.up * halfH
- toTop = Camera.up * halfH
- TL = Camera.forward * Near + toTop – toRight
- TR = Camera.forward * Near + toTop + toRight
- BL = Camera.forward * Near - toTop – toRight
- BR = Camera.forward * Near - toTop + toRight
- 屏幕后处理中处理的内容是一张抓取的屏幕图像,相当于是一个面片,它具有4个顶点(四个角),这四个顶点即顶点着色器中处理的所有顶点
推导出来了四个顶点的方向向量,我们是不是就可以利用它们得到四个顶点的世界空间下坐标了呢?比如得到左上角的方向向量的单位向量,然后乘以左上角顶点对应像素点的深度值:
左上角像素点对应世界坐标 = 摄像机位置 + TL.Normalized * Depth ;
注意,如果这样去计算,那么得到的结果是错误的!!!!
因为深度值Depth即使我们将其转换为观察空间下的线性值,它表示的也是离摄像机在Z轴方向的距离,并不是两点之间的距离(欧式距离),因此我们还需要对该向量进行处理!
我们可以利用相似三角形的原理,推导出深度值和两点之间距离(欧式距离)的关系。
而 左上角像素点对应世界坐标 = 摄像机位置 + TL.Normalized * Depth 就变为了:
- 左上角像素点对应世界坐标 = 摄像机位置 + TL.Normalized * |TL|/Near * Depth
那也就意味着,真正最终和深度一起计算的确定世界坐标位置的方向向量其实就是::TL.Normalized * |TL|/Near
由于近裁剪面4个点是对称的, |TL|/Near 可以通用 ,只需要变换前面的单位向量即可。
通过推导,我们已经得到了四个顶点对应的方向向量信息了
- Scale = |TL|/Near
- RayTL = TL.Normalized * Scale
- RayTR = TR.Normalized * Scale
- RayBL = BL.Normalized * Scale
- RayBR = BR.Normalized * Scale
我们只需要在顶点着色器中根据顶点的位置设置对应的向量即可!
- 片元着色器中:当数据传递到片元着色器要处理每个像素时,像素对应的射线方向是基于4个顶点的射线插值计算而来(无需我们自己计算)
- 利用 像素世界坐标 = 摄像机位置 + 深度值 * 世界空间下射线方向 得到对应像素在世界空间下位置
- 已经有了对应的射线,直接从深度纹理中采样获取深度值,并利用 LinearEyeDepth 内置函数得到像素到摄像机的实际距离 便可以利用上面的公式进行计算得到每个像素在世界空间下的位置了
- 利用得到的世界空间下位置利用雾的公式计算出对应雾效颜色
- 有了世界空间下的位置,我们就可以利用雾的计算公式进行雾效的混合因子计算
利用算出的雾效混合因子参与雾颜色和像素颜色的混合运算即可
- 有了世界空间下的位置,我们就可以利用雾的计算公式进行雾效的混合因子计算
- 利用 像素世界坐标 = 摄像机位置 + 深度值 * 世界空间下射线方向 得到对应像素在世界空间下位置
3、实现
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FogWithDepthTexture : PostEffectBase
{
// 雾的颜色
public Color fogColor = Color.gray;
// 雾的浓度
[Range(0, 3)]
public float fogDensity = 1f;
public float fogStart = 0f;
public float fogEnd = 5f;
// 4x4 的矩阵用于传递4个角的向量参数
private Matrix4x4 rayMatrix;
// Start is called before the first frame update
void Start()
{
Camera.main.depthTextureMode = DepthTextureMode.Depth;
}
protected override void UpdateProperty() {
if (material != null) {
float fov = Camera.main.fieldOfView / 2f;
float near = Camera.main.nearClipPlane;
float aspect = Camera.main.aspect;
float halfH = near * Mathf.Tan(fov * Mathf.Deg2Rad);
float halfW = halfH * aspect;
// 计算竖直向上的和水平向右的偏移向量
Vector3 toTop = Camera.main.transform.up * halfH;
Vector3 toRight = Camera.main.transform.right * halfW;
// 算出指向四个顶点的向量
Vector3 TL = Camera.main.transform.forward * near + toTop - toRight;
Vector3 TR = Camera.main.transform.forward * near + toTop + toRight;
Vector3 BL = Camera.main.transform.forward * near - toTop - toRight;
Vector3 BR = Camera.main.transform.forward * near - toTop + toRight;
//为了让深度值 计算出来是两点间距离 所以需要乘以一个缩放值
float scale = TL.magnitude / near;
//真正的最终想要的四条射线向量
TL = TL.normalized * scale;
TR = TR.normalized * scale;
BL = BL.normalized * scale;
BR = BR.normalized * scale;
rayMatrix.SetRow(0, BL);
rayMatrix.SetRow(1, BR);
rayMatrix.SetRow(2, TR);
rayMatrix.SetRow(3, TL);
//设置材质球相关属性(Shader属性)
material.SetColor("_FogColor", fogColor);
material.SetFloat("_FogDensity", fogDensity);
material.SetFloat("_FogStart", fogStart);
material.SetFloat("_FogEnd", fogEnd);
material.SetMatrix("_RayMatrix", rayMatrix);
}
}
}
Shader "ShaderProj/13/fogWithDepthTexture"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_FogColor ("FogColor", Color) = (1,1,1,1)
_FogDensity ("FogDensity", Float) = 1
_FogStart ("FogStart", Float) = 0
_FogEnd ("FogEnd", Float) = 10
}
SubShader
{
ZTest Always
Cull Off
ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float2 uv : TEXCOORD0;
float2 uv_depth : TEXCOORD1;
//顶点射线 指向四个角的方向向量 (传递到片元时 会自动进行插值 运算)
float4 ray : TEXCOORD2;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
half4 _MainTex_TexelSize;
sampler2D _CameraDepthTexture;
fixed4 _FogColor;
fixed _FogDensity;
float _FogStart;
float _FogEnd;
//矩阵相关 里面存储了 4条射线向量
//0-左下 1-右下 2-右上 3-左上
float4x4 _RayMatrix;
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
o.uv_depth = v.texcoord;
//顶点着色器函数 每一个顶点都会执行一次
//对于屏幕后处理来说 就会执行4次 因为有4个顶点 (4个角)
//通过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;
//判断 是否需要进行纹理翻转 如果翻转了 深度的uv和对应顶点需要变化
#if UNITY_UV_STARTS_AT_TOP
if(_MainTex_TexelSize.y < 0)
{
o.uv_depth.y = 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(_CameraDepthTexture, 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
}