Unity 渲染教程(十四):雾

发表于2017-07-14
评论0 7.6k浏览

译者: 崔嘉艺(milan21   审校:王磊(未来的未来)

·         给物体应用雾的效果。

·         基于距离或深度的雾。

·         创建一个基于图像的效果。

·         让雾也支持延迟渲染。

这是关于渲染基础的系列教程的第十四部分。在前面的部分里我们介绍了延迟渲染。这一次我们将在场景中加上雾。

这个教程是使用Unity 5.5.0f3开发的。

 

物体往往随着距离的增长而发生褪色。

前向渲染中的雾

到目前为止,我们一直把光线看作是在真空进行传输的。当你的场景是设置在太空里的时候,这可能是准确的,但是在其他情况下,光线必须穿过大气层或是液体。在这种情况下,光线可以被吸收、散射和反射到空间的任何地方,而不仅仅是撞击固体表面。

准确地渲染大气干扰需要昂贵的体积方法,这是我们通常负担不起的。取而代之的是,我们将满足一个近似方法,它只依赖于一些恒定的雾参数。它被称为雾,因为这种效果通常被用于雾状大气。由清晰的大气层造成的视觉扭曲通常是如此的微妙,以至于在较短的距离内都可以忽略它们。

标准雾

Unity的光源窗口包含了一段场景中雾的设置。在默认情况下它是禁用的。当被激活的时候,你会得到默认的灰色雾。但是,这只适用于使用前向渲染路径渲染的对象。当延迟模式处于激活状态的时候,在雾的设置区域就会提到这一点。

默认的雾处于激活状态。

稍后我们将讨论延迟渲染模式中的雾。现在,让我们把注意力集中在前向渲染的雾上。为此,我们需要使用前向渲染模式。你可以更改全局渲染模式,或者强制主摄像机使用所需的渲染模式。因此,设置摄像机的渲染路径为前向渲染模式。现在让我们禁用高动态光照渲染渲染。

使用前向渲染模式的摄像机。

创建一个小的测试场景,就像在平面或立方体上的几个球体一样。使用Unity的默认白色材质。

不明显的雾。

环境光照设置为默认强度1,你会得到一些非常明亮的物体,而且没有明显的雾。

线性雾

为了让雾更加的明显,把它的颜色设置成黑色。这代表了一种吸收光线而不会散射的大气,就像浓黑的烟雾那样。

将雾的模式设置为线性。这和现实的情况不相符,但是很容易配置。你可以设定雾开始有影响的距离,以及它进行完全遮挡的距离。在中间雾的强度线性增加。这是用视图中的距离来测量的。在雾开始影响的距离之前,能见度是正常的。过了这段距离,雾将逐渐模糊物体。最后,除了雾的颜色外,什么也看不见。

 

线性雾。

线性雾的因子是通过函数来计算的,其中c是雾的坐标, SE是雾开始和结束的位置。然后将这个因子限制在0-1的范围内,并用于在雾和物体的渲染颜色之间进行插值。

为什么雾不会影响到天空盒?

雾的效果调整了前向渲染物体的碎片颜色。因此,它只影响那些物体,而不会影响天空盒。

指数雾

Unity支持的第二种雾模式是指数雾,这是一种更接近于雾的近似。它使用函数 ,其中e是欧拉数,d是雾的密度因子。这个方程永远不会得到零,不像线性版本。将密度增加到0.1,使雾看起来更靠近镜头。

 

指数雾。

指数平方雾

最后一种模式是指数平方雾。这就像指数雾,但是使用了函数 ,这样就能在近距离产生较少的雾,但是它强度增长的速度会更快。

 

指数平方雾。

添加雾

现在我们知道了雾是什么样的,让我们把对它的支持添加到我们自己的前向渲染过程中。为了便于比较,将一半的物体设置为使用我们自己的材质,而剩下一半的物体则使用默认的材质。

我们的材质在左边,右边是标准材质。

雾模式由着色器关键字控制,因此我们必须添加一个多编译指令来支持它们。 有一个预定义的multi_compile_fog指令,我们可以用于此目的。 它为FOG_LINEARFOG_EXPFOG_EXP2关键字带来了额外的着色器变体。 将这个指令添加到两个前向渲染通道里面。

1
#pragma multi_compile_fog

接下来,让我们在“MyLighting”中添加一个函数,以将雾应用到我们的片段颜色。 它将当前颜色和内插器作为参数,并应返回应用了雾的最后的颜色。

1
2
3
float4 ApplyFog (float4 color, Interpolators i) {
    return color;
}

雾的效果是基于视距,等于相机位置和片段的世界位置之间的矢量的长度。 我们可以访问这两个位置,所以我们可以计算这个距离。

1
2
3
4
float4 ApplyFog (float4 color, Interpolators i) {
    float viewDistance = length(_WorldSpaceCameraPos - i.worldPos);
    return color;
}

然后我们使用这个信息作为雾密度函数的雾坐标,它由UNITY_CALC_FOG_FACTOR_RAW宏计算。 这个宏创建了unityFogFactor变量,我们可以使用这个变量在雾和片段颜色之间进行插值。雾的颜色存储在unity_FogColor中,它在ShaderVariables中定义。

1
2
3
4
5
float4 ApplyFog (float4 color, Interpolators i) {
    float viewDistance = length(_WorldSpaceCameraPos - i.worldPos);
    UNITY_CALC_FOG_FACTOR_RAW(viewDistance);
    return lerp(unity_FogColor, color, unityFogFactor);
}

 

 

UNITY_CALC_FOG_FACTOR_RAW是如何工作的?

宏是在UnityCG中定义。雾的哪个关键字被定义决定了那些会被计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#if defined(FOG_LINEAR)
    // factor = (end-z)/(end-start) = z * (-1/(end-start)) (end/(end-start))
    #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = \
        (coord) * unity_FogParams.z unity_FogParams.w
#elif defined(FOG_EXP)
    // factor = exp(-density*z)
    #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = \
        unity_FogParams.y * (coord); \
        unityFogFactor = exp2(-unityFogFactor)
#elif defined(FOG_EXP2)
    // factor = exp(-(density*z)^2)
    #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = \
        unity_FogParams.x * (coord); \
        unityFogFactor = exp2(-unityFogFactor*unityFogFactor)
#else
    #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = 0.0
#endif

还有一个UNITY_CALC_FOG_FACTOR宏,它会使用UNITY_CALC_FOG_FACTOR_RAW宏。它嘉定雾坐标是需要转换的特定类型,这就是为什么我们直接使用原始版本。

unity_FogParams变量在UnityShaderVariables中定义,并包含一些有用的预计算的值。

1
2
3
4
5
// x = density / sqrt(ln(2)), useful for Exp2 mode
// y = density / ln(2), useful for Exp mode
// z = -1/(end-start), useful for Linear mode
// w = end/(end-start), useful for Linear mode
float4 unity_FogParams;

由于雾的参数可能会超出0-1范围,所以在插值之前必须进行大小的限制。

1
return lerp(unity_FogColor, color, saturate(unityFogFactor));

此外,由于雾不会影响透明度分量,我们可以将其从插值中排除。

1
2
color.rgb = lerp(unity_FogColor.rgb, color.rgb, saturate(unityFogFactor));
return color;

现在我们可以将雾应用到MyFragmentProgram中的最终前向渲染通道得到的颜色。

1
2
3
4
5
6
7
8
9
10
11
12
13
#if defined(DEFERRED_PASS)
    #if !defined(UNITY_HDR_ON)
        color.rgb = exp2(-color.rgb);
    #endif
    output.gBuffer0.rgb = albedo;
    output.gBuffer0.a = GetOcclusion(i);
    output.gBuffer1.rgb = specularTint;
    output.gBuffer1.a = GetSmoothness(i);
    output.gBuffer2 = float4(i.normal * 0.5 0.5, 1);
    output.gBuffer3 = color;
#else
    output.color = ApplyFog(color, i);
#endif

都是线性雾,但是效果不同。

我们自己的着色器现在也包括雾的效果。但是,它与由标准着色器计算出的雾不完全匹配。为了使差异更加清楚,请使用具有起始和终止位置相同或几乎相同值的线性雾。这会导致从没有雾到很大雾的突然过渡。

弯曲与直接雾过渡。

基于深度的雾

我们的着色器和标准着色器之间的差异是由于我们计算雾坐标的方式造成的。使用世界空间的视图距离是有意义的,标准着色器使用的是不同的度量。具体来说,它使用的是裁剪空间的深度值。因此,视角不影响雾坐标。此外,在某些情况下,距离会受到相机的近裁剪平面距离的影响,从而将雾气推开一点。

平坦深度与距离的效果对比。

使用深度而不是距离的优点是你不必计算平方根,因此更快。 此外,尽管不太现实,但在某些情况下,以深度为基础的雾可能是可取的,比如横向卷轴游戏。不利之处在于,由于观看角度被忽略,摄像机方向会影响雾。随着它的旋转,雾的密度会变化,而这在逻辑上是不应该的。

旋转会影响深度。

让我们为我们的着色器添加基于深度的雾的支持,以配合Unity的方法。 这需要对我们的代码进行一些修改。 我们现在必须将片段空间的深度值传递给片段程序。 因此,当其中一个雾模式处于激活状态的时候,请定义FOG_DEPTH关键字。

1
2
3
4
5
6
#include "UnityPBSLighting.cginc"
#include "AutoLight.cginc"
 
#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
    #define FOG_DEPTH 1
#endif

我们必须为深度值包含一个插值器。但并不是给它一个独立的内插器,我们可以把它搭载在世界坐标上,作为它的第四个组成部分。

1
2
3
4
5
6
7
8
9
10
11
struct Interpolators {
    
     
    #if FOG_DEPTH
        float4 worldPos : TEXCOORD4;
    #else
        float3 worldPos : TEXCOORD4;
    #endif
     
    
}

为了确保我们的代码保持正确,请使用i.worldPos.xyz替换所有用到i.worldPos的地方。在这之后,在需要的时候,将片段程序中的裁剪空间深度值分配给i.worldPos.w。它只是齐次裁剪空间位置的Z坐标,因此要在将其转换为0-1范围内的值之前使用它。

1
2
3
4
5
6
7
8
9
10
11
Interpolators MyVertexProgram (VertexData v) {
    Interpolators i;
    i.pos = UnityObjectToClipPos(v.vertex);
    i.worldPos.xyz = mul(unity_ObjectToWorld, v.vertex);
    #if FOG_DEPTH
        i.worldPos.w = i.pos.z;
    #endif
    i.normal = UnityObjectToWorldNormal(v.normal);
 
    
}

ApplyFog中,使用内插后的深度值覆盖计算出的视图距离。保留旧的计算出来的值,因为我们稍后会使用这个值。

1
2
3
4
5
6
7
8
float4 ApplyFog (float4 color, Interpolators i) {
    float viewDistance = length(_WorldSpaceCameraPos - i.worldPos.xyz);
    #if FOG_DEPTH
        viewDistance = i.worldPos.w;
    #endif
    UNITY_CALC_FOG_FACTOR_RAW(viewDistance);
    return lerp(unity_FogColor, color, saturate(unityFogFactor));
}

基于裁剪空间深度值的雾。

现在你很可能得到的是与标准着色器相同的结果。但是,在某些情况下,裁剪空间配置不同,会产生不正确的雾。要补偿这一点的话,请使用UNITY_Z_0_FAR_FROM_CLIPSPACE宏来转换深度值。

1
viewDistance = UNITY_Z_0_FAR_FROM_CLIPSPACE(i.worldPos.w); 

UNITY_Z_0_FAR_FROM_CLIPSPACE宏做了什么?

最重要的是,它补偿了可能发生反转的裁剪空间的Z维度值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#if defined(UNITY_REVERSED_Z)
    //D3d with reversed Z =>
    //z clip range is [near, 0] -> remapping to [0, far]
    //max is required to protect ourselves from near plane not being
    //correct/meaningfull in case of oblique matrices.
    #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) \
        max(((1.0-(coord)/_ProjectionParams.y)*_ProjectionParams.z),0)
#elif UNITY_UV_STARTS_AT_TOP
    //D3d without reversed z => z clip range is [0, far] -> nothing to do
    #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) (coord)
#else
    //Opengl => z clip range is [-near, far] -> should remap in theory
    //but dont do it in practice to save some perf (range is close enought)
    #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) (coord)
#endif

请注意,宏代码里面提到OpenGL还需要进行转换,但认为这么做不值得。

UNITY_CALC_FOG_FACTOR宏简单地将上述内容提供给它的原始等式。

1
2
#define UNITY_CALC_FOG_FACTOR(coord) \
    UNITY_CALC_FOG_FACTOR_RAW(UNITY_Z_0_FAR_FROM_CLIPSPACE(coord)) 

深度还是距离

那么,我们应该使用哪个指标来控制我们的雾? 裁剪空间的深度值或是世界空间中的距离?让我们同时支持这两者! 但是它不值得让它成为一个着色器功能。我们将使其成为着色器的配置选项,如BINORMAL_PER_FRAGMENT。假设基于深度的雾是默认的,你可以通过定义FOG_DISTANCE来切换到基于距离的雾,这个定义位于我们着色器顶部附近的CGINCLUDE部分。

1
2
3
4
5
6
CGINCLUDE
 
#define BINORMAL_PER_FRAGMENT
#define FOG_DISTANCE
 
ENDCG

我们在“My Lighting ”中必须做的是切换到基于距离的雾,如果已经定义了FOG_DISTANCE,那么要去掉FOG_DEPTH定义。

1
2
3
4
5
#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
    #if !defined(FOG_DISTANCE)
        #define FOG_DEPTH 1
    #endif
#endif

禁用雾

当然,我们并不总是想用雾。 所以只有在实际开启的时候才会包含雾的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
    #if !defined(FOG_DISTANCE)
        #define FOG_DEPTH 1
    #endif
    #define FOG_ON 1
#endif
 
 
float4 ApplyFog (float4 color, Interpolators i) {
    #if FOG_ON
        float viewDistance = length(_WorldSpaceCameraPos - i.worldPos.xyz);
        #if FOG_DEPTH
            viewDistance = UNITY_Z_0_FAR_FROM_CLIPSPACE(i.worldPos.w);
        #endif
        UNITY_CALC_FOG_FACTOR_RAW(viewDistance);
        color.rgb = lerp(unity_FogColor.rgb, color.rgb, saturate(unityFogFactor));
    #endif
    return color;
}

多个光源

我们的雾在单一光源下正常工作,但是当场景中有多个光源的时候,它的表现如何? 当我们使用黑色的雾的时候,看起来很好,但是让我们尝试使用另一种颜色。

在一个方向光源和两个方向光源下灰色雾的效果。

得到的结果太亮了。这是因为我们为每个光源添加一次雾的颜色。当雾色为黑色时,这不是问题。所以解决方案是在附加渲染通道中总是使用黑色。那样的话,雾会淡出额外的光源,而不会使雾本身变亮。

1
2
3
4
5
float3 fogColor = 0;
#if defined(FORWARD_BASE_PASS)
    fogColor = unity_FogColor.rgb;
#endif
color.rgb = lerp(fogColor, color.rgb, saturate(unityFogFactor));

两个光源下正确的雾的效果。

项目文件下载地址:unitypackage

延迟渲染下的雾

现在我们已经在前向渲染路径下能够让雾正常工作了,所以让我们切换到延迟渲染路径。复制前向渲染路径下的摄像机。将复制的摄像机更改为延迟渲染路径下的摄像机,然后禁用前向渲染路径下的摄像机。这样,你可以通过更改启用哪个相机来快速切换渲染模式。

你会注意到,使用延迟渲染路径的时候根本没有雾。这是因为雾必须应用在计算所有光照之后。所以我们不能在我们的着色器的延迟渲染通道中添加雾。

要在同一图像中比较延迟渲染路径和前向渲染路径,你可以强制某些对象以前向渲染模式进行。举个简单的例子来说,通过使用透明材质同时保持其完全不透明。

不透明和透明的材质。

果然,使用透明材质的物体受到雾的影响。

为什么会有两个球体不见了?

右侧的物体使用透明材质,即使它们完全不透明。因此,在渲染它们的时候,Unity会按照从后到前的顺序对它们进行渲染。最远的两个球体最终被渲染在他们下方的立方体之前。由于透明物体不写入深度缓冲区,所以立方体被绘制在这些球体之上。

应用效果到渲染的图像

为了给延迟渲染添加雾的效果,我们必须等待所有的光源都被渲染,然后再做一次渲染,以决定雾的效果。由于雾适用于整个场景,它的渲染就像渲染方向光源一样。

添加这样一个渲染通道的简单方法是通过向摄像机添加自定义组件。 所以创建一个DeferredFogEffect类,而不是扩展MonoBehaviour。因为在编辑模式下能够看到雾是有用的,所以给它ExecuteInEditMode属性。 将此组件添加到我们的延迟渲染模式下的摄像机。这最终会使游戏视图中出现雾。

1
2
3
4
5
using UnityEngine;
 
[ExecuteInEditMode]
public class DeferredFogEffect : MonoBehaviour {
}

带有雾效果的延迟渲染模式下的摄像机

要向渲染过程添加额外的全屏渲染通道,请给我们的组件一个OnRenderImage方法。 Unity将检查相机是否具有此方法的组件,并在渲染场景后调用它们。这允许你更改或应用效果到渲染图像。如果有多个这样的组件,它们将按照它们添加到相机的顺序被调用。

OnRenderImage方法有两个RenderTexture参数。第一个参数是源纹理,其中包含场景的最终颜色,到目前为止的最终颜色。第二个参数是我们必须渲染的目标纹理。它可能为空,这意味着它直接进入帧缓冲区。

1
2
void OnRenderImage (RenderTexture source, RenderTexture destination) {
}

一旦我们添加了这个方法,游戏视图将无法渲染。我们必须确保我们绘制了某些东西。为此,请调用具有两个纹理作为参数的Graphics.Blit方法。该方法将使用着色器绘制一个全屏四面体,只需读取源纹理并输出采样的颜色,即未做修改。

1
2
3
void OnRenderImage (RenderTexture source, RenderTexture destination) {
    Graphics.Blit(source, destination);
}

场景再次像往常一样渲染。但是,如果你检查帧调试器的话,你会看到已经为图像效果添加了一个渲染通道。

绘制一个渲染图像。

雾的着色器

简单地复制图像数据是无用的。我们必须创建一个新的自定义着色器,以将雾的效果应用于图像。我们会从一个极简单极基础的着色器开始。因为我们只是绘制一个全屏四面体,应该涵盖整个场景,我们应该忽略裁剪和深度缓冲。我们也不应该写入深度缓冲区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Shader "Custom/Deferred Fog" {
     
    Properties {
        _MainTex ("Source", 2D) = "white" {}
    }
 
    SubShader {
        Cull Off
        ZTest Always
        ZWrite Off
 
        Pass {
        }
    }
}

我们的特效组件需要这个着色器,所以添加一个公共字段,然后把我们新的着色器分配给这个字段。

1
public Shader deferredFog;

使用一个雾效果的着色器。

我们还需要一个材质来使用我们的着色器进行渲染。我们只在激活的时才需要它,因此不需要任何资源。使用非序列化字段来保存对它的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;
using System;
 
[ExecuteInEditMode]
public class DeferredFogEffect : MonoBehaviour {
 
    public Shader deferredFog;
 
    [NonSerialized]
    Material fogMaterial;
     
    
}

OnRenderImage方法中,我们现在开始检查是否有一个材质实例。如果没有的话,创建一个使用雾着色器的新的材质实例。 然后用这种材质调用Graphics.Blit方法。

1
2
3
4
5
6
void OnRenderImage (RenderTexture source, RenderTexture destination) {
    if (fogMaterial == null) {
        fogMaterial = new Material(deferredFog);
    }
    Graphics.Blit(source, destination, fogMaterial);
}

这将产生一个白色图像。我们必须创建我们自己的着色器渲染通道来渲染一些有用的东西。让我们从简单的顶点和片段程序开始,从源纹理中复制RGB颜色,使用全屏四边形的顶点位置和UV数据。另外,让我们为雾的模式包含多个模式的多编译指令。

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
Pass {
    CGPROGRAM
 
    #pragma vertex VertexProgram
    #pragma fragment FragmentProgram
 
    #pragma multi_compile_fog
 
    #include "UnityCG.cginc"
 
    sampler2D _MainTex;
 
    struct VertexData {
        float4 vertex : POSITION;
        float2 uv : TEXCOORD0;
    };
 
    struct Interpolators {
        float4 pos : SV_POSITION;
        float2 uv : TEXCOORD0;
    };
 
    Interpolators VertexProgram (VertexData v) {
        Interpolators i;
        i.pos = UnityObjectToClipPos(v.vertex);
        i.uv = v.uv;
        return i;
    }
 
    float4 FragmentProgram (Interpolators i) : SV_Target {
        float3 sourceColor = tex2D(_MainTex, i.uv).rgb;
        return float4(sourceColor, 1);
    }
 
    ENDCG
}


基于深度的雾

因为我们使用的是延迟渲染,我们知道有一个深度缓冲区可用。毕竟,光照渲染通道需要深度缓冲区才能正常工作。所以我们也可以读取深度缓冲区,这意味着我们可以用深度缓冲区来计算基于深度的雾的效果。

Unity通过_CameraDepthTexture变量使深度缓冲区可用,因此将其添加到我们的着色器。

1
sampler2D _MainTex, _CameraDepthTexture;

我们可以对这个纹理进行采样,尽管确切的语法取决于目标平台。我们可以使用在HLSLSupport中定义的SAMPLE_DEPTH_TEXTURE宏。

1
2
3
4
5
6
float4 FragmentProgram (Interpolators i) : SV_Target {
    float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
 
    float3 sourceColor = tex2D(_MainTex, i.uv).rgb;
    return float4(sourceColor, 1);
}

这给了我们来自深度缓冲区的原始数据,所以在从齐次坐标转换到0-1范围内的裁剪空间值之后。 我们必须转换这个值,使其成为世界空间中的线性深度值。 首先,我们可以使用UnityCG中定义的Linear01Depth函数将其转换到线性范围。

1
2
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
depth = Linear01Depth(depth);

 

Linear01Depth函数是什么样子的?

它使用两个方便的预定义值执行简单的转换。

1
2
3
4
5
// Z buffer to linear 0..1 depth
inline float Linear01Depth( float z )
{
    return 1.0 / (_ZBufferParams.x * z _ZBufferParams.y);
}

缓冲区参数在UnityShaderVariables中定义。

1
2
3
4
5
6
7
// Values used to linearize the Z buffer
// x = 1-far/near
// y = far/near
// z = x/far
// w = y/far
float4 _ZBufferParams;

接下来,我们必须通过到远裁剪平面的距离来缩放这个值,以获得实际的基于深度的视图距离。通过在UnityShaderVariables中定义的float4_ProjectionParams变量可以使裁剪空间的设置可用。它的Z分量包含到远平面的距离。

1
2
3
depth = Linear01Depth(depth);
 
float viewDistance = depth * _ProjectionParams.z;

一旦我们有了距离,我们可以计算雾的因子并进行插值。

1
2
3
4
5
6
7
8
9
float viewDistance = depth * _ProjectionParams.z;
 
UNITY_CALC_FOG_FACTOR_RAW(viewDistance);
unityFogFactor = saturate(unityFogFactor);
         
float3 sourceColor = tex2D(_MainTex, i.uv).rgb;
float3 foggedColor =
    lerp(unity_FogColor.rgb, sourceColor, unityFogFactor);
return float4(foggedColor, 1);

不正确的雾的效果。

修正雾的效果

不幸的是,我们的雾的效果还不是很正确。最明显的错误是我们是在透明的几何图形之上绘制的雾。为了防止这种情况发生,我们必须在绘制透明对象之前应用雾的效果。我们可以将ImageEffectOpaque属性附加到我们的方法来指示Unity这样做。

1
2
3
4
[ImageEffectOpaque]
void OnRenderImage (RenderTexture source, RenderTexture destination) {
    Graphics.Blit(source, destination, fogMaterial);
}

添加雾要在绘制不透明物体之后,但要在绘制透明物体之前。

另一个问题是雾的颜色显然是错误的。当不使用高动态光照渲染相机时会发生这种情况,高动态光照渲染相机会弄歪颜色。所以我们只是简单的在延迟渲染模式的相机上启用高动态光照渲染。

 相机带有高动态光照渲染的效果。

最后,我们再次可以得到深度的差异,因为我们没有考虑到近平面。

不同的深度。

我们可以通过从视距中减去近平面的距离来稍微进行补偿。它存储在_ProjectionParamsY组件中。 不幸的是,它不会产生完全的匹配,因为我们必须转换深度值的顺序。 Unity中雾的效果也可以用来它来对雾进行调整,所以让我们来做这个事情。

1
2
float viewDistance =
    depth * _ProjectionParams.z - _ProjectionParams.y;

部分补偿的深度。

基于距离的雾

延迟光源的着色器从深度缓冲区中重建世界空间的位置,来计算光照。 我们也可以做到这一点。

透视摄像机的裁剪空间定义了空间的一个梯形区域。如果我们忽略近平面的话,那么我们在摄像机的世界位置的顶端会得到一个金字塔结构。它的高度等于摄像机的远平面距离。其尖端的线性化深度为0,其底部的线性化深度为1

金字塔的侧视图。

对于我们图像的每个像素,我们可以从金字塔的顶部发射一个到金字塔底部的射线。如果没有触碰到任何物体,那么射线会到达底部,这就是远平面。否则,它会触碰到任何需要渲染的物体。

每个像素一个射线。

如果某些东西被击中,那么相应的像素的深度值小于1。举个简单的例子来说,如果它在一半的地方击中了一点,则深度值将为1/2。这意味着射线的Z坐标是这个射线如果一直没有被阻挡能穿越的长度的一半。由于光线的方向仍然相同,这意味着XY坐标也要减半。一般来说,我们可以发射一个光线然后一直射到远平面,然后将其按照深度值缩放来找到实际的光线。

对光线进行放缩。

一旦我们有了这个光线,我们可以将它添加到摄像机的位置,以找到渲染曲面的世界空间位置。但是,由于我们只对距离感兴趣,所有我们真正需要的是这条射线的长度。

为了让这个方法有用,我们必须知道那些从每个像素由摄像机发出到达远平面的光线。 其实我们只需要四个光线就够了,每个光线对应金字塔的一个角。插值可以算出我们所有像素的对应光线。

计算对应的光线

我们可以根据摄像机的远平面及其视场角来构造射线。摄像机的方向和位置对于距离来说无关紧要,所以我们可以忽略它的转换。 Camera.CalculateFrustumCorners方法可以为我们做到这一点。它有四个参数。第一个参数是要使用的矩形区域,在我们的例子中是整个图像。第二个参数是投射光线有多远,这必须与远平面相匹配。第三个参数涉及立体渲染。 我们只是使用目前活跃的视角。 最后,该方法需要一个3D矢量的数组来存储光线。所以我们必须缓存对摄像机和矢量数组的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[NonSerialized]
Camera deferredCamera;
 
[NonSerialized]
Vector3[] frustumCorners;
 
[ImageEffectOpaque]
void OnRenderImage (RenderTexture source, RenderTexture destination) {
    if (fogMaterial == null) {
        deferredCamera = GetComponent<camera>();
        frustumCorners = new Vector3[4];
        fogMaterial = new Material(deferredFog);
    }
    deferredCamera.CalculateFrustumCorners(
        new Rect(0f, 0f, 1f, 1f),
        deferredCamera.farClipPlane,
        deferredCamera.stereoActiveEye,
        frustumCorners
    );
 
    Graphics.Blit(source, destination, fogMaterial);
}

接下来,我们必须将这些数据传递给着色器。 我们可以用矢量数组来做这个事情。但是,我们不能直接使用frustumCorners。第一个原因是我们只能传递四维矢量到着色器。 所以让我们包括一个Vector4 []字段,并将它作为_FrustumCorners传递给着色器。

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
[NonSerialized]
Vector4[] vectorArray;
 
[ImageEffectOpaque]
void OnRenderImage (RenderTexture source, RenderTexture destination) {
    if (fogMaterial == null) {
        deferredCamera = GetComponent<camera>();
        frustumCorners = new Vector3[4];
        vectorArray = new Vector4[4];
        fogMaterial = new Material(deferredFog);
    }
    deferredCamera.CalculateFrustumCorners(
        new Rect(0f, 0f, 1f, 1f),
        deferredCamera.farClipPlane,
        deferredCamera.stereoActiveEye,
        frustumCorners
    );
 
    vectorArray[0] = frustumCorners[0];
    vectorArray[1] = frustumCorners[1];
    vectorArray[2] = frustumCorners[2];
    vectorArray[3] = frustumCorners[3];
    fogMaterial.SetVectorArray("_FrustumCorners", vectorArray);
 
    Graphics.Blit(source, destination, fogMaterial);
}

第二个问题是必须改变角的顺序。 CalculateFrustumCorners对它们按照左下角、左上角、右上角、右下角的顺序排序。但是,用于渲染图像效果的四边形的顶点的顺序是左下角、右下角、左上角、右上角。所以让我们重新排序它们以匹配四边形的顶点。

1
2
3
4
vectorArray[0] = frustumCorners[0];
vectorArray[1] = frustumCorners[3];
vectorArray[2] = frustumCorners[1];
vectorArray[3] = frustumCorners[2];


推断距离

要访问着色器中的光线,请添加一个类型为浮点数组的变量。实际上我们不必再添加一个属性,因为我们不会手动编辑它们。虽然我们只能将四维向量传递给着色器,但在内部我们只需要前三个分量。所以float3类型就足够了。

1
2
3
sampler2D _MainTex, _CameraDepthTexture;
 
float3 _FrustumCorners[4];

接下来,定义FOG_SHADER来表示我们要将雾放置的实际的距离,就像我们的其他着色器所做的一样。

1
2
3
#pragma multi_compile_fog
 
#define FOG_DISTANCE

当我们需要距离的时候,我们必须插入光线并将它们发送到片段程序。

1
2
3
4
5
6
7
8
struct Interpolators {
    float4 pos : SV_POSITION;
    float2 uv : TEXCOORD0;
     
    #if defined(FOG_DISTANCE)
        float3 ray : TEXCOORD1;
    #endif
};

在顶点程序中,我们可以简单地使用UV坐标来访问角的数组。坐标为(0,0),(10),(0,1)和(1,1)。 所以索引是u 2v

1
2
3
4
5
6
7
8
9
Interpolators VertexProgram (VertexData v) {
    Interpolators i;
    i.pos = UnityObjectToClipPos(v.vertex);
    i.uv = v.uv;
    #if defined(FOG_DISTANCE)
        i.ray = _FrustumCorners[v.uv.x 2 * v.uv.y];
    #endif
    return i;
}

最后,我们可以用片段程序中的实际距离替换基于深度的距离。

1
2
3
4
5
float viewDistance =
    depth * _ProjectionParams.z - _ProjectionParams.y;
#if defined(FOG_DISTANCE)
    viewDistance = length(i.ray * depth);
#endif

基于距离雾的效果。

除了深度缓冲区的精度限制之外,前向渲染方向和延迟渲染方法都会产生效果相同的基于距离的雾。

受雾影响的天空盒

实际上,在前向渲染路径的雾和延迟渲染路径的雾之间仍然存在显着差异。 你可能已经注意到延迟渲染路径下的雾也影响了天空盒。它的作用就像远处的平面是一个坚实的障碍,会受到雾的影响。

受雾影响的天空盒。

我们知道,当深度值接近1的时候,我们已经到达了远平面。如果我们不想让天空盒受到雾的影响,我们可以通过将雾的因数设置为1来防止这种情况。

1
2
3
4
5
UNITY_CALC_FOG_FACTOR_RAW(viewDistance);
unityFogFactor = saturate(unityFogFactor);
if (depth > 0.9999) {
    unityFogFactor = 1;
}

天空盒不受雾影响的效果。

如果你想要对整个图像都应用雾的效果,可以通过宏定义进行控制。当定义FOG_SKYBOX的时候,会将雾应用于天空盒,否则不会。

1
2
3
4
5
6
7
8
9
10
11
12
            #define FOG_DISTANCE
//          #define FOG_SKYBOX
 
            
 
                UNITY_CALC_FOG_FACTOR_RAW(viewDistance);
                unityFogFactor = saturate(unityFogFactor);
                #if !defined(FOG_SKYBOX)
                    if (depth > 0.9999) {
                        unityFogFactor = 1;
                    }
                #endif

没有雾的效果

最后,我们必须考虑雾的效果已被停用的情况。

没有雾的效果,不正确。

这也可以通过强制雾的因之为1,并且没有定义任何雾的关键字的情况下来做到。这将使我们的着色器变成一个单纯的纹理复制操作,所以如果不需要的话,它可以更好地停用或去除雾的组件。

1
2
3
4
5
6
7
8
#if !defined(FOG_SKYBOX)
    if (depth > 0.9999) {
        unityFogFactor = 1;
    }
#endif
#if !defined(FOG_LINEAR) && !defined(FOG_EXP) && !defined(FOG_EXP2)
    unityFogFactor = 1;
#endif

这个系列的下一篇教程是:延迟光源

工程文件下载地址:unitypackage

PDF下载地址:PDF


【版权声明】

原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

标签: