【Unity3D】流动雾效

1 前言

屏幕深度和法线纹理简介中对深度和法线纹理的来源、使用及推导过程进行了讲解,激光雷达特效中讲述了一种重构屏幕像素点世界坐标的方法,本文将介绍使用深度纹理重构屏幕像素点在相机坐标系下的坐标计算方法,并使用重构后的坐标模拟雾效,再结合噪声纹理实现流动的雾效。

​ 雾效即离观察者越远的点越趋近于雾的颜色,并且雾的浓度越大。本文将使用屏幕后处理技术,计算每个顶点与相机的距离,并根据距离计算雾的浓度,依据浓度给该像素点混合原始颜色与雾效颜色。

​ 本文完整资源见→Unity3D流动雾效

2 由深度纹理重构相机坐标系下坐标

1)重构像素点在相机坐标系下坐标

​ 对于屏幕上的任意一点,它对应的相机坐标系中的点记为 P,对应的近裁剪平面上的点记为 Q,相机位置记为 O(坐标为 (0, 0, 0)),假设 P 点的深度为 depth(由 LinearEyeDepth 函数获取),相机到近平面的距离为 near,如下图所示。

img

​ 根据上图,可以列出以下方程组关系。其中,公式 2 由三角形相似原理得到,公式 3 由 O、P、Q 三点共线得到。

img

​ 化简得:

img

​ Q 点在近平面上,可以通过近裁剪平面的四个角插值得到,O 和 near 为定值,因此 (OQ / near) 也可以通过插值得到。假设近裁剪平面的四个角分别为 A、B、C、D,我们将 (OA / near)、(OB / near)、(OC / near)、(OD / near) 输入顶点着色器中,光珊化会自动为我们计算插值后的 (OQ / near)。

​ 如下,我们可以在插值寄存器中定义变量 interpolatedRay,用于存储向量 (OQ / near)。

struct v2f {
    float4 pos : SV_POSITION; // 裁剪空间顶点坐标
    half2 uv : TEXCOORD0; // 纹理uv坐标, 
    float4 interpolatedRay : TEXCOORD1; // 插值射线向量(由相机指向近平面上点的向量除以near后的坐标)
};

2)近裁剪平面四角射线向量计算

​ 记近裁剪平面上左下角、右下角、右上角、左上角、中心、右中心、上中心顶点分别为 A、B、C、D、Q、E、F,相机位置为 O 点,如下:

img

​ 根据几何关系,可以计算向量 OA、OB、OC、OD 如下:

​ 假设像机竖直方向的视野角度为 fov(通过 camera.fieldOfView 获取),屏幕宽高比为 aspect(通过 camera.aspect 获取),相机距离近裁剪平面的距离为 near(通过 camera.nearClipPlane 获取),相机向右、向上、向前方向的单位方向向量分别为 right(坐标为 (1, 0, 0))、up(坐标为 (0, 1, 0)、forward(坐标为 (0, 0, 1),则向量 OQ、QE、QF 的计算如下:

img

​ 假设摄像机竖直方向的视野角度为 fov(通过 camera.fieldOfView 获取),屏幕宽高比为 aspect(通过 camera.aspect 获取),相机距离近裁剪平面的距离为 near(通过 camera.nearClipPlane 获取),相机向右、向上、向前方向的单位方向向量分别为 right、up、forward(通过 camera.transform 组件获取),则向量 OQ、QE、QF 的计算如下:

img

2 雾效因子计算

​ 雾效因子的计算一般采样线性衰减、反比衰减、指数衰减等方法。假设顶点与相机间的距离为 dist,雾效因子为 factor,则 factor 的计算如下。

1)线性衰减雾效因子

img

​ 其中,dist_min 和 dist_max 分别为受雾效影响的最小距离和最大距离。

2)反比衰减雾效因子

img

​ 其中,r 为衰减速率参数。

3)指数衰减雾效因子

img

​ 其中,r 为衰减速率参数。

3 雾效实现

​ FogEffect.cs

using UnityEngine;

[RequireComponent(typeof(Camera))] // 需要相机组件
public class FogEffect : MonoBehaviour {
    public Material material = null; // 材质
    private Camera cam; // 相机

    private void Awake() {
        cam = GetComponent<Camera>();
        material.hideFlags = HideFlags.DontSave;
    }

    private void OnEnable() {
        cam.depthTextureMode |= DepthTextureMode.Depth;
    }

    private void Update() {
        float scroll = Input.GetAxis("Mouse ScrollWheel");
        if (Mathf.Abs(scroll) > 0)
        { // 缩放场景
            cam.transform.position += cam.transform.forward * scroll * 15;
        }
    }

    private void OnRenderImage(RenderTexture src, RenderTexture dest) {
        if (material != null) {
            Matrix4x4 frustumCorners = GetFrustumCornersRay();
            material.SetMatrix("_FrustumCornersRay", frustumCorners);
            Graphics.Blit(src, dest, material);
        } else {
            Graphics.Blit(src, dest);
        }
    }

    private Matrix4x4 GetFrustumCornersRay() { // 获取插值射线向量(由相机指向近平面上四个角点的向量除以near后的坐标)
        Matrix4x4 frustumCorners = Matrix4x4.identity;
        float fov = cam.fieldOfView;
        float near = cam.nearClipPlane;
        float aspect = cam.aspect;
        float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
        Vector3 toRight = Vector3.right * halfHeight * aspect; // 指向右方的向量
        Vector3 toTop = Vector3.up * halfHeight; // 指向上方的向量
        Vector3 toForward = Vector3.forward * near; // 指向前方的向量
        Vector3 bottomLeft = (toForward - toTop - toRight) / near; // 指向左下角的射线
        Vector3 bottomRight = (toForward + toRight - toTop) / near; // 指向右下角的射线
        Vector3 topRight = (toForward + toRight + toTop) / near; // 指向右上角的射线
        Vector3 topLeft = (toForward + toTop - toRight) / near; // 指向左上角的射线
        frustumCorners.SetRow(0, bottomLeft);
        frustumCorners.SetRow(1, bottomRight);
        frustumCorners.SetRow(2, topRight);
        frustumCorners.SetRow(3, topLeft);
        return frustumCorners;
    }
}

​ 说明:FogEffect 脚本组件挂在相机上。

​ FogEffect.shader

Shader "MyShader/FogEffect" { // 雷达波特效
    Properties{
        _MainTex("Base (RGB)", 2D) = "white" {} // 主纹理
        _FogColor("Fog Color", Color) = (1, 0, 0, 1) // 雾的颜色
        _MinDist("Min Dist", Range(0, 20)) = 1 // 雾的最近距离(线性衰减函数才生效)
        _MaxDist("Max Dist", Range(30, 1000)) = 1000 // 雾的最远距离(线性衰减函数才生效)
        _R("Density", Range(0, 1)) = 0.01 // 衰减速率参数(反比衰减和指数衰减函数才生效)
    }

    SubShader{
        Pass {
            // 深度测试始终通过, 关闭深度写入
            ZTest Always ZWrite Off

            CGPROGRAM

            #include "UnityCG.cginc"

            #pragma vertex vert
            #pragma fragment frag

            sampler2D _MainTex; // 主纹理
            sampler2D _CameraDepthTexture; // 深度纹理
            float4x4 _FrustumCornersRay; // 视锥体四角射线向量(由相机指向近平面上四个角点的向量除以near后的坐标)
            fixed4 _FogColor; // 雾的颜色
            float _MinDist; // 雾的最近距离(线性衰减函数才生效)
            float _MaxDist; // 雾的最远距离(线性衰减函数才生效)
            float _R; // 衰减速率参数(反比衰减和指数衰减函数才生效)

            struct v2f {
                float4 pos : SV_POSITION; // 裁剪空间顶点坐标
                half2 uv : TEXCOORD0; // 纹理uv坐标
                float4 interpolatedRay : TEXCOORD1; // 插值射线向量(由相机指向近平面上点的向量除以near后的坐标)
            };

            float4 getInterpolatedRay(half2 uv) { // 获取插值射线向量(由相机指向近平面上四个角点的向量除以near后的坐标)
                int index = 0;
                if (uv.x < 0.5 && uv.y < 0.5) {
                    index = 0;
                } else if (uv.x > 0.5 && uv.y < 0.5) {
                    index = 1;
                } else if (uv.x > 0.5 && uv.y > 0.5) {
                    index = 2;
                } else {
                    index = 3;
                }
                return _FrustumCornersRay[index];
            }

            float getFactory(float len) { // 获取雾效因子
                float factor = saturate((_MaxDist - len) / (_MaxDist - _MinDist)); // 线性雾效衰减因子
                //float factor = 1 / (_R * len + 1); // 反比雾效衰减因子
                //float factor = exp(-_R * len); // 指数雾效衰减因子
                return factor;
            }

            v2f vert(appdata_img v) { // 顶点着色器中只处理4个顶点, 每个顶点对应了1条射线(O点指向近平面的四角)
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex); // 计算裁剪坐标系中顶点坐标, 等价于: mul(unity_MatrixMVP, v.vertex)
                o.uv = v.texcoord;
                o.interpolatedRay = getInterpolatedRay(v.texcoord); // 获取插值射线向量(由相机指向近平面上四个角点的向量除以near后的坐标)
                return o;
            }

            fixed4 frag(v2f i) : SV_Target{ // v2f_img为内置结构体, 里面只包含pos和uv
                float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); // 非线性的深度, tex2D(_CameraDepthTexture, i.uv).r
                float linearDepth = LinearEyeDepth(depth); // 线性的深度
                //if (linearDepth > _ProjectionParams.z - 2) {
                //  return tex2D(_MainTex, i.uv); // 天空不参与雾效
                //}
                float3 viewPos = linearDepth * i.interpolatedRay.xyz; // 顶点在相机坐标系下的坐标(不是观察坐标系, 观察坐标系与相机坐标系z轴方向相反)
                float len = length(viewPos);
                float factor = getFactory(len); // 获取雾效因子
                fixed4 tex = tex2D(_MainTex, i.uv);
                fixed4 color = lerp(_FogColor, tex, factor);
                return color;
            }

            ENDCG
        }
    }

    FallBack off
}

​ 说明:在 Assets 目录下面新建 Resources 目录,接着在 Resources 目录下面创建材质,重命名为 FogMat,将 FogEffect.shader 与 FogMat 材质绑定,再将 FogMat 拖拽到相机对象的 FogEffect 脚本组件里。

​ 运行效果:

img

4 流动雾效实现

​ 流动雾效原理:在第 3 节的基础上,对雾效因子 factor 进行周期性偏移(偏移量记为 offset),从而实现雾的流动效果,为使雾呈现团状,偏移量可以从团状的噪声纹理中采样得到。如下是一些常用的噪声纹理。

img

​ 这些噪声纹理都有以下共同点:

  • 在较小的邻域范围内,灰度是渐变的,使得模拟的雾效效果更加和谐;
  • 将它们向四周平铺展开,边界处刚好衔接,使得模拟的雾效效果更加自然;

​ DynamicFogEffect.shader

Shader "MyShader/DynamicFogEffect" { // 雷达波特效
    Properties{
        _MainTex("Base (RGB)", 2D) = "white" {} // 主纹理
        _NoiseTex("Noise Tex", 2D) = "white" {} // 噪声纹理
        _FogColor("Fog Color", Color) = (1, 0, 0, 1) // 雾的颜色
        _MinDist("Min Dist", Range(0, 20)) = 1 // 雾的最近距离(线性衰减函数才生效)
        _MaxDist("Max Dist", Range(30, 1000)) = 1000 // 雾的最远距离(线性衰减函数才生效)
        _R("Density", Range(0, 1)) = 0.01 // 衰减速率参数(反比衰减和指数衰减函数才生效)
        _SpeedX("Speed X", Range(0, 1)) = 0.45 // 雾气在水平方向飘动的速度
        _SpeedY("Speed Y", Range(0, 1)) = 0.3 // 雾气在竖直方向飘动的速度
        _NoiseScale("Noise Scale", Range(0, 1)) = 0.3 // 噪声放大系数
    }

    SubShader{
        Pass {
            // 深度测试始终通过, 关闭深度写入
            ZTest Always ZWrite Off

            CGPROGRAM

            #include "UnityCG.cginc"

            #pragma vertex vert
            #pragma fragment frag

            sampler2D _MainTex; // 主纹理
            sampler2D _NoiseTex; // 噪声纹理
            sampler2D _CameraDepthTexture; // 深度纹理
            float4x4 _FrustumCornersRay; // 视锥体四角射线向量(由相机指向近平面上四个角点的向量除以near后的坐标)
            fixed4 _FogColor; // 雾的颜色
            float _MinDist; // 雾的最近距离(线性衰减函数才生效)
            float _MaxDist; // 雾的最远距离(线性衰减函数才生效)
            float _R; // 衰减速率参数(反比衰减和指数衰减函数才生效)
            float _SpeedX; // 雾气在水平方向飘动的速度
            float _SpeedY; // 雾气在竖直方向飘动的速度
            float _NoiseScale; // 噪声放大系数

            struct v2f {
                float4 pos : SV_POSITION; // 裁剪空间顶点坐标
                half2 uv : TEXCOORD0; // 纹理uv坐标
                float4 interpolatedRay : TEXCOORD1; // 插值射线向量(由相机指向近平面上点的向量除以near后的坐标)
            };

            float4 getInterpolatedRay(half2 uv) { // 获取插值射线向量(由相机指向近平面上四个角点的向量除以near后的坐标)
                int index = 0;
                if (uv.x < 0.5 && uv.y < 0.5) {
                    index = 0;
                } else if (uv.x > 0.5 && uv.y < 0.5) {
                    index = 1;
                } else if (uv.x > 0.5 && uv.y > 0.5) {
                    index = 2;
                } else {
                    index = 3;
                }
                return _FrustumCornersRay[index];
            }

            float getFactory(float len) { // 获取雾效因子
                float factor = saturate((_MaxDist - len) / (_MaxDist - _MinDist)); // 线性雾效衰减因子
                //float factor = 1 / (_R * len + 1); // 反比雾效衰减因子
                //float factor = exp(-_R * len); // 指数雾效衰减因子
                return factor;
            }

            v2f vert(appdata_img v) { // 顶点着色器中只处理4个顶点, 每个顶点对应了1条射线(O点指向近平面的四角)
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex); // 计算裁剪坐标系中顶点坐标, 等价于: mul(unity_MatrixMVP, v.vertex)
                o.uv = v.texcoord;
                o.interpolatedRay = getInterpolatedRay(v.texcoord); // 获取插值射线向量(由相机指向近平面上四个角点的向量除以near后的坐标)
                return o;
            }

            fixed4 frag(v2f i) : SV_Target{ // v2f_img为内置结构体, 里面只包含pos和uv
                float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); // 非线性的深度, tex2D(_CameraDepthTexture, i.uv).r
                float linearDepth = LinearEyeDepth(depth); // 线性的深度
                //if (linearDepth > _ProjectionParams.z - 2) {
                //  return tex2D(_MainTex, i.uv); // 天空不参与雾效
                //}
                float3 viewPos = linearDepth * i.interpolatedRay.xyz; // 顶点在相机坐标系下的坐标(不是观察坐标系, 观察坐标系与相机坐标系z轴方向相反)
                float len = length(viewPos);
                float factor = getFactory(len); // 获取雾效因子
                float2 dist = float2(_SpeedX, _SpeedY) * _Time.y; // 雾气移动的距离
                float offset = (tex2D(_NoiseTex, i.uv + dist).r - 0.5) * _NoiseScale; // 雾效因子偏移量
                factor = saturate(factor * (1 + offset));
                fixed4 tex = tex2D(_MainTex, i.uv);
                fixed4 color = lerp(_FogColor, tex, factor);
                return color;
            }

            ENDCG
        }
    }

    FallBack off
}

​ 说明:在 Assets 目录下面新建 Resources 目录,接着在 Resources 目录下面创建材质,重命名为 DynamicFogMat,将 DynamicFogEffect.shader 与 DynamicFogMat 材质绑定,并将噪声纹理拖拽到 DynamicFogMat 的 Noise Tex 中,最后将 DynamicFogMat 拖拽到相机对象的 FogEffect 脚本组件里。

​ 运行效果:

img

​ 声明:本文转自【Unity3D】流动雾效

posted @ 2023-08-12 10:32  little_fat_sheep  阅读(86)  评论(0编辑  收藏  举报