Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

深度纹理实现全局雾效

一、为什么要实现深度纹理实现全局雾效

主要是因为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.关键思路

  • 顶点着色器中:
    1. 屏幕后处理中处理的内容是一张抓取的屏幕图像。相当于是一个面片,它具有四个顶点
    2. 通过c#代码计算四个顶点在世界坐标系下的射线方向后传递给顶点着色器
  • 片元着色器中: 当数据传送到片元着色器要处理的每个像素时,像素对应的射线方向是基于4个顶点的射线插值计算而来
    1. 利用(像素的世界坐标 = 摄像机位置+ 观察空间线性深度值* 摄像机指向像素世界坐标的方向向量),得到对应像素在世界空间下的位置
    2. 利用得到的世界空间下位置,利用雾的公式计算出对应雾效的颜色

注意点:

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.)关键思路

  1. 屏幕后处理中处理的内容是一张抓取屏幕图像,相当于一个面片,它具有四个顶点
  2. 通过C#代码计算四个顶点在世界坐标下的射线方向后传递给顶点着色器
    当数据传递到片元着色器要处理每一个像素时,像素对应的射线方向时基于4个顶点的射线插值计算而来
  3. 利用 像素世界坐标 = 摄像机位置 + 深度值*世界空间下射线方向得到对应像素在世界空间下的位置
  4. 利用得到的世界空间下位置,通过利用雾的三个公式计算出对应的雾效颜色

2.)实现步骤

  1. 继承PostEffectBase,重写OnRenderImage
  2. 在Start中开启深度纹理
  3. 声明雾相关属性
    颜色、浓度、开始距离、最浓距离
  4. 计算四个顶点的四个射线向量
  5. 通过4x4的矩阵装载向量,传递给材质
    注意:为了之后方便考虑uv翻转问题,我们按左下,右下,右上,左上的逆时针顺序存储
  6. 将定义好的属性变量传递给材质球

3.)实现示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class Fog : PostEffectBase
{
//雾的颜色
public Color fogColor = Color.gray;
//雾的浓度
[Range(0f, 3f)]
public float fogDensity = 1.2f;
//雾开始的距离
public float fogStart = 0f;
//雾最浓的距离
public float fogEnd = 5f;

//4x4的矩阵 用于传递 4个向量参数
private Matrix4x4 rayMatrix;

private void Start()
{
Camera.main.depthTextureMode = DepthTextureMode.Depth;
}
protected override void UpadateProerty()
{
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);
}
}
}

2.Shader部分

1.)实现步骤

  1. 声明属性,映射属性
  2. 屏幕后处理标配
    • ZTest Always
    • Cull Off
    • ZWrite Off
  3. v2f结构体
    • 考虑翻转深度纹理half2 uv_depth:TEXCOORD1
    • 射线向量 float4 ray:TEXCOORD2
  4. 顶点着色器 图片有四个顶点,会进四次,我们需要判断每一个顶点使用哪一个射线向量 坐标转换、uv赋值(注意考虑深度纹理翻转) 根据uv纹理坐标判断顶点位置,决定赋值哪一个向量(注意考虑深度纹理翻转)
  5. 顶点着色器 深度纹理采样,转换到观察空间下摄像机的实际距离 利用 摄像机的位置+深度值*射线向量 得到世界空间坐标 通过雾的公式 计算混合因子(这里我们不使用传统的公式) 我们在这里实现一种特殊的基于高度的线性雾效,并且把浓度也用上 进行颜色混合
  6. 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
}

评论