UnityShader27:屏幕雾效

 

前置:UnityShader26:运动模糊

一、Linear01Depth & LinearEyeDepth

在前一章中,提到过这两个方法:

  • LinearEyeDepth(d):将获得视角空间下的线性深度值,范围为[Near, Far]
  • Linear01Depth(d):由 LinearEyeDepth(d) / Far 得到,范围为[Near/Far, 1]

如果尝试用公式递推的话:

根据《OpenGL基础29:深度测试》的内容可以得到:z_{ndc}=\frac{-\frac{f+n}{f-n} z_{view}-\frac{2 f n}{f-n}}{-z_{view}},其中 f 为远平面,n 为近平面

因此可以反推出 z_{view}=\frac{1}{\frac{f-n}{n \cdot f}d - \frac{1}{n}},其中 d = \frac{1}{2}z_{ndc}+\frac{1}{2},但考虑到 Unity 使用的视角空间中,摄像机正向对应的 z 值均为负值,因此前者还要将 z_{view} 取反,得到 z_{view}=\frac{1}{\frac{n-f}{n \cdot f}d + \frac{1}{n}},这个公式正是 LinearEyeDepth(d)!

做个简单除法就可得出 z_{01}=\frac{1}{\frac{n-f}{n}d + \frac{f}{n}},这个公式是 Linear01Depth(d)

但是,这些东西都很简单,在《UnityShader入门精要》这本书中也有一样的步骤,而现在已经是 2021 年了,随着平台的发展,不少的地方已经采用了深度反转(Reversed direction)的计策(这个在上一章里也提到过),NDC 空间中的深度值 z 范围不再是 [-1, 1] 而是 [1, 0],这主要取决于 P 矩阵的特殊处理,因此上面的公式结果在这些平台下当然就不再正确

在 UntiyShader 中,可以使用 UNITY_REVERSED_Z 宏来判断是否有进行深度反转

尽管如此,计算方法还是如出一辙,如果进行了深度反转,类比推理得到的公式为

z_{01}=\frac{1}{\frac{f-n}{n}d +1} 以及 z_{view}=\frac{1}{\frac{f-n}{n \cdot f}d + \frac{1}{f}}

这些都可以通过 Unity 内置变量以及 UnityCG.cginc 内函数实现确认:

// Values used to linearize the Z buffer (http://www.humus.name/temp/Linearize%20depth.txt)
// x = 1-far/near
// y = far/near
// z = x/far
// w = y/far
// or in case of a reversed depth buffer (UNITY_REVERSED_Z is 1)
// x = -1+far/near
// y = 1
// z = x/far
// w = 1/far
float4 _ZBufferParams;
// Z buffer to linear 0..1 depth
inline float Linear01Depth( float z )
{
    return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
}
// Z buffer to linear depth
inline float LinearEyeDepth( float z )
{
    return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
}

 

二、根据深度缓冲重建世界空间坐标

这个在前一章中也实现过了,方法是传入摄像机的逆 VP 矩阵到着色器,然后在片段着色器中进行空间变换

这样的方法没有问题,只不过在片段着色器中计算矩阵乘法可能有点吃性能,现在提供另一个重建世界空间坐标的方法

我们能够很轻松的拿到摄像机的世界空间坐标 Camera_{w},以及通过 LinearEyeDepth() 方法得到的线性深度值 depth,这个 depth 为当前片段所在世界坐标与摄像机 z 方向上的距离

将问题简单化,先假设这个片段所对应世界坐标上的点正好落入图中 TL 这个向量所在直线上,那么如何得到摄像机到这个点的向量 Dist 呢?如果我们能拿到这个向量,那么当然就可以拿到这个点的坐标(废话)

定义 Near 为摄像机到近裁平面的向量,TL 为摄像机到近裁平面左上角顶点的向量,那么根据相似三角形可以得到:

\frac{TL}{|Near|} = \frac{Dist}{depth},根据对称性,另外3个顶点的公式与此完全一致

完美,考虑到屏幕后处理本身就是渲染到一个刚好填充整个屏幕的四边形面片,而这个四边形面片正是图片中的 TLTRBL 和 BR,因此,我们可以直接将计算的结果 \frac{TL}{|Near|}\frac{TR}{|Near|}\frac{BL}{|Near|} 及 \frac{BR}{|Near|} 传入顶点着色器,这样到片段着色器后,就可以直接通过 Pos_{w} = Camera_{w} + Ray \cdot depth 计算得到片段所对应世界坐标了,其中 Ray 就是前面4个顶点向量插值后得到的

现在问题就变成:如何求出向量 TL

这个更简单,根据图中可得:TL = Camera_{w}.forward \cdot |Near| + toTop - toRight

然后对于 toTop 和 toRight 又有

  • toTop = Camera_{w}.up * (|Near| * \tan(\frac{FOV}{2}) ),其中 FOV 为摄像机竖直方向的视角范围
  • toRight = toTop\cdot aspect,其中 aspect 为摄像机平面横纵比

对应的代码如下:

using UnityEngine;
using System.Collections;

public class Fog: PostEffectsBase
{
	public Shader shader;
	private Material _material;
	public Material material
	{
		get
		{
			_material = CheckShaderAndCreateMaterial(shader, _material);
			return _material;
		}
	}

    private Camera _myCamera;
    public Camera myCamera
    {
        get
        {
            if (_myCamera == null)
                _myCamera = GetComponent<Camera>();
            return _myCamera;
        }
    }

    [Range(0.0f, 3.0f)]
    //雾的浓度
    public float fogDensity = 1.2f;
    //雾的颜色
    public Color fogColor = Color.white;
    //世界坐标y轴大于fogEnd的部分不会有雾,小于fogStart的部分雾的浓度最高,在[fogStart, fogEnd]范围内雾的浓度和高度呈反比
    public float fogStart = -1.5f;
    public float fogEnd = 3.0f;

    void OnEnable()
    {
        //通知摄像机需要深度纹理,此后可以在着色器中使用
        //https://docs.unity3d.com/2021.1/Documentation/ScriptReference/DepthTextureMode.html
        myCamera.depthTextureMode |= DepthTextureMode.Depth;
    }

    void OnRenderImage(RenderTexture src, RenderTexture dest)
	{
		if (material != null)
		{
            Matrix4x4 frustumCorners = Matrix4x4.identity;

            //获得摄像机的竖直方向视角范围,单位为角度
            float fov = myCamera.fieldOfView;
            //获得摄像机与近裁平面的距离
            float near = myCamera.nearClipPlane;
            //获得摄像机平面纵横比
            float aspect = myCamera.aspect;

            //Mathf.Deg2Rad:同等于2PI/360,可以将角度转换为弧度,Mathf.Deg2Rad相反
            float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
            Vector3 toRight = myCamera.transform.right * halfHeight * aspect;
            Vector3 toTop = myCamera.transform.up * halfHeight;

            Vector3 topLeft = myCamera.transform.forward * near + toTop - toRight;
            //VectorX.magnitude:获得向量长度
            float scale = topLeft.magnitude / near;

            //向量归一化
            topLeft.Normalize();
            topLeft *= scale;

            Vector3 topRight = myCamera.transform.forward * near + toRight + toTop;
            topRight.Normalize();
            topRight *= scale;

            Vector3 bottomLeft = myCamera.transform.forward * near - toTop - toRight;
            bottomLeft.Normalize();
            bottomLeft *= scale;

            Vector3 bottomRight = myCamera.transform.forward * near + toRight - toTop;
            bottomRight.Normalize();
            bottomRight *= scale;

            frustumCorners.SetRow(0, bottomLeft);
            frustumCorners.SetRow(1, bottomRight);
            frustumCorners.SetRow(2, topRight);
            frustumCorners.SetRow(3, topLeft);

            material.SetMatrix("_FrustumCornersRay", frustumCorners);
            material.SetFloat("_FogDensity", fogDensity);
            material.SetColor("_FogColor", fogColor);
            material.SetFloat("_FogStart", fogStart);
            material.SetFloat("_FogEnd", fogEnd);

            Graphics.Blit(src, dest, material);
        }
		else
		{
			Graphics.Blit(src, dest);
		}
	}
}

需要注意的是:这些都是在透视投影下的计算,如果摄像机为正交投影,就需要用到不同的公式。当然篇幅有限,这里就不详细给出步骤了

 

三、屏幕雾效

下面计算雾效的方法非常简单,步骤如下:

  1. 对于当前片段,按照上一节的方法得到世界空间坐标
  2. 以世界空间坐标的 y 轴为参数,根据类插值公式得出当前点的雾效系数 F
  3. 根据雾效系数 F 来混和片段颜色和雾颜色

关键在于雾效系数 F 的计算,一般有三种方法(其中 z 为距离参数):

  • 线性(Linear):f=\frac{d_{\max }-|z|}{d_{\max }-d_{\min }},其中 d_{max} 和 d_{min} 分别为上界和下界
  • 指数(Exponential):f=e^{-d \cdot|z|},其中 d 为浓度参数
  • 指数平方(Exponential Squared):f=e^{-(d-|z|)^{2}},其中 d 为浓度参数

这里使用第一种:

Shader "Jaihk662/Fog"
{
    Properties
    {
        _MainTex("Base (RGB)", 2D) = "white" {}
		_FogDensity("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
    }

    CGINCLUDE
    #include "UnityCG.cginc"

    sampler2D _MainTex;
    half _FogDensity;
    fixed4 _FogColor;
    float _FogStart;
    float _FogEnd;
    //如果设置了 myCamera.depthTextureMode |= DepthTextureMode.Depth,那么这里就可以通过 _CameraDepthTexture 拿到深度图
    sampler2D _CameraDepthTexture;

    half4 _MainTex_TexelSize;
    float4x4 _FrustumCornersRay;
    struct vert2frag
    {
        float4 pos: SV_POSITION;
		half4 uv: TEXCOORD0;
        float4 ray : TEXCOORD2;
    };

    vert2frag vert(appdata_img v)
    {
		vert2frag o;
        o.pos = UnityObjectToClipPos(v.vertex);
        o.uv.xy = v.texcoord;		
        o.uv.zw = v.texcoord;
        
        //对深度纹理进行平台差异化处理
        #if UNITY_UV_STARTS_AT_TOP			
        if (_MainTex_TexelSize.y < 0.0)
            o.uv.w = 1.0 - o.uv.w;
        #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;

        //当在 DirectX 平台上使用渲染到纹理技术时,Unity 会自动为我们翻转屏幕图像纹理,所以大部分情况下我们都不需要关系在意纹理翻转问题,但是有特殊情况:一个例子就是开启抗锯齿后再对得到的渲染纹理进行后处理时,这些图像在竖直方向的朝向就可能是不同的
        //在这些情况下,_MainTex_TexelSize.y 就会为负值,我们需要对索引也进行翻转
        #if UNITY_UV_STARTS_AT_TOP
        if (_MainTex_TexelSize.y < 0)
            index = 3 - index;
        #endif
        o.ray = _FrustumCornersRay[index];

        return o;
	}

    fixed4 frag(vert2frag i): SV_Target
    {
        float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv.zw));
		float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.ray.xyz;
						
		float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart); 
		fogDensity = saturate(fogDensity * _FogDensity);
			
		fixed4 finalColor = tex2D(_MainTex, i.uv);
		finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);
		return finalColor;
    }
    ENDCG

    Subshader
    {
        ZTest Always Cull Off ZWrite Off
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
			#pragma fragment frag
            ENDCG
        }
    }
    FallBack Off
}

不过,这样实现的雾效有三个缺陷:

  1. 如果你的某个视角方向上没有任何物体(看到了背景的天空盒),这时对应片段所在世界空间坐标将会是摄像机的远裁平面,可想而知这会对本案例中雾的计算带来错误的插值结果,因此可以考虑设置背景颜色为雾的颜色,又或者调小摄像机的远裁平面
  2. 雾的浓度太过“均匀”了,它在某些时候应该是看起来飘渺的
  3. 真正的雾效应为场景雾效,因此想要表现出现实世界的雾的效果,就不能用简单的后处理了(可以想象从天上往下看一个被雾笼罩城市的感觉)

这些问题可以留到后面再解决吧

 

四、非均匀雾效

对于上面缺陷②的优化,只需要加一个噪声纹理就可以了:

着色器部分修改如下:

fixed4 frag(vert2frag i): SV_Target
{
    float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv.zw));
    float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.ray.xyz;
    
    float2 speed = _Time.y * float2(_FogXSpeed, _FogYSpeed);
    float noise = (tex2D(_NoiseTex, i.uv + speed).r - 0.5) * _NoiseAmount;

    float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart); 
    fogDensity = saturate(fogDensity * _FogDensity * (1 + noise));
        
    fixed4 finalColor = tex2D(_MainTex, i.uv);
    finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);
    return finalColor;
}

 

参考资料:

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值