前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基础渲染系列(十四)——雾

基础渲染系列(十四)——雾

作者头像
放牛的星星
发布2020-07-10 17:27:17
2.8K0
发布2020-07-10 17:27:17
举报
文章被收录于专栏:壹种念头

本文重点:

给物体应用雾 基于距离和深度的基础雾 创建图像效果(Image Effect) 支持延迟雾

这是渲染教程系列的第14篇文章。上一章我们介绍了延迟着色,这次我们把雾效果添加到场景中。

本教程 使用Unity5.5.0f3。

(随着距离增加,物体逐渐消退)

1 前向雾

到目前为止,我们一直将光线视为通过真空传播。当场景设置在宇宙中时,这可能是准确的,否则,光就必须穿过大气层或液体。这时,光线不仅会撞击固体表面,而且会在空间中的任何地方被吸收,散射和反射。

准确绘制大气干扰需要昂贵的体积算法,而这通常是我们无法承受的。取而代之的是,用几个恒定的雾参数来进行近似。之所以称为雾,是因为该效果通常用于有雾的气氛。清晰的气氛所引起的视觉失真通常非常小,以至于在较短距离内可以忽略不计。

1.1 标准雾

Unity的“Lighting”窗口包含具有场景雾设置的部分。默认情况下是禁用的。激活后,你将获得默认的灰色雾。但是,这仅适用于使用正向渲染路径渲染的对象。当延迟模式处于活动状态时,雾的状态在下面的白字部分有说明。

(开启默认雾)

稍后我们将处理延迟模式。现在,我们先集中讨论前向雾。为此,我们需要使用前向渲染模式。你可以更改全局渲染模式,或强制主相机使用所需的渲染模式。将相机的Rendering Path 设置为“Forward”。现在先暂时禁用HDR渲染。

(前向摄像机)

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

(不明显的雾)

将环境照明设置为默认强度1时,你会看到一些非常明亮的对象,并且根本没有很明显的雾。

1.2 线性雾

为了使雾更加明显,请将其颜色设置为纯黑色。它将代表一种吸收光而没有太多散射的气氛,例如浓浓的黑烟。

将Fog Mode设置为Linear。这样的效果并不真实,但易于配置。你可以设置雾影响开始的距离和完全变为雾的距离。他们之间会线性增加。这是以视距进行测量的。在雾开始之前,能见度是正常的。超过该距离,雾将逐渐遮挡物体。最终,除了雾的颜色,什么都看不到。

(线性雾)

线性雾化因子通过函数

进行计算,其中c 是雾化坐标,S和E 分别是起点和终点。然后将此因子钳制在0–1范围内,并用于在雾和对象的阴影颜色之间进行插值。

为什么雾不影响天空盒?

雾效果可调整正向渲染对象的片段颜色。因此,它仅影响这些对象,而不影响天空盒。

1.3 指数雾

Unity支持的第二种雾模式是指数模式,这是雾的更逼真的近似。它使用函数

,其中d是雾的密度因子。与线性版本不同,该方程永远不会达到零。将强度提高到0.1,使雾气看起来更靠近相机。

(指数雾)

1.4 指数平方雾

最后一种模式是指数平方雾。它就像指数雾一样工作,但是使用函数

,在近距离范围内雾量较小,但增加较快。

(指数平方雾)

1.5 增加雾

现在我们知道了雾的表现了,那我们将对它的支持添加到自己的正向着色器中。为了让效果更容易比对,将一半的对象设置为使用我们的材质,而其余的则继续使用默认材质。

(左边是我们的材质,右边是标准材质)

雾模式由着色器关键字控制,因此我们必须添加多编译指令以支持它们。可以为此使用一个预定义的multi_compile_fog指令。这将为FOG_LINEAR,FOG_EXP和FOG_EXP2关键字带来额外的着色器变体。仅将此指令添加到两个前向pass中。

接下来,向“My Lighting”添加一个函数以将雾应用于片段颜色。它以当前颜色和插值器为参数,并应在应用雾的情况下返回最终颜色。

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

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

UNITY_CALC_FOG_FACTOR_RAW如何工作?

宏在UnityCG中定义。定义哪个fog关键字确定要计算的内容。

还有一个UNITY_CALC_FOG_FACTOR宏,它使用此宏。它假定雾坐标是需要转换的特定类型,因此我们直接使用原始版本。

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

由于雾度因子最终可能超出0–1范围,因此我们必须在插值之前对其进行钳位。

另外,由于雾不影响alpha分量,因此我们可以将其排除在插值之外。

现在,我们可以将雾应用于MyFragmentProgram中的最终的forward-pass颜色。

(线性雾 但是有区别)

我们自己的着色器现在包含雾了。但是,它与标准着色器计算的雾度不完全匹配。为了使差异更加清楚,请使用具有相同或几乎相同值的起点和终点的线性雾。它会导致突然从无雾过渡到全雾。

(曲线与直线过渡)

1.6 基于深度的雾

我们和标准着色器之间的差异是由于我们计算雾化坐标的方式所致。尽管使用世界空间视距是有意义的,但标准着色器使用了不同的度量标准。具体来说,它使用剪辑空间深度值。结果,视角不会影响雾坐标。同样,在某些情况下,距离会受到相机的接近剪辑平面距离的影响,这会将雾稍微推开。

(平面深度与距离)

使用深度而不是距离的优点是你不必计算平方根,因此速度更快。同样,虽然不太现实,但在某些情况下(例如,横向滚动游戏)可能需要基于深度的雾。不利之处在于,由于忽略了视角,因此相机的方向会影响雾。随着旋转,雾密度发生变化,而从逻辑上讲它不应发生改变。

(旋转会改变深度)

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

我们必须包括一个用于深度值的插值器。但是,除了为其提供单独的插值器外,我们还可以将其作为第四部分搭载在世界坐标上。

为确保我们的代码正确无误,请将i.worldPos的所有用法替换为i.worldPos.xyz。之后,在需要时将片段空间深度值分配给片段程序中的i.worldPos.w。它只是同质剪辑空间位置的Z坐标,因此在将其转换为0–1范围内的值之前。

在ApplyFog中,使用插值深度值覆盖计算的视图距离。保留旧的计算,因为稍后我们将继续使用它。

(基于剪辑空间深度的雾)

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

UNITY_Z_0_FAR_FROM_CLIPSPACE是做什么的?

最重要的是,它补偿了可能反转的剪辑空间Z尺寸。

请注意,宏代码提到OpenGL也需要转换,但我觉得这样做不值得。

UNITY_CALC_FOG_FACTOR宏仅将上述内容提供给其原始等效内容。

1.7 深度还是距离

那么,我们应该对雾使用哪个度量呢?剪辑空间深度还是世界空间距离?那就都支持吧!但是,让它成为着色器功能并不划算。我们将使其成为着色器配置选项,例如BINORMAL_PER_FRAGMENT。假设基于深度的雾是默认设置,你可以通过在着色器顶部附近的CGINCLUDE部分中定义FOG_DISTANCE切换到基于距离的雾。

如果已经定义了FOG_DISTANCE,那么在My Lighting中要切换到基于距离的雾,要做的就是摆脱FOG_DEPTH定义。

1.8 禁止雾

当然,我们并不总是要使用雾。因此,仅在雾代码真正打开时才包括它。

1.9 多灯光

我们的雾在单个灯光下可以正常工作,但是当场景中有多个灯光时,它的表现如何?当我们使用黑雾时,它看起来不错,但也可以尝试使用其他颜色。

(灰色雾 在1个和2个方向光下的表现)

结果太亮了。发生这种情况是因为我们为每个灯光都添加了一次雾色。当雾色为黑色时,这不是问题。因此解决方案是在附加通道中始终使用黑色。这样,雾就使附加光的作用减弱,而又不会使雾本身变亮。

(两个灯光下正确的灰色雾)

2 延迟雾

现在,我们在正向渲染路径上使用了雾,让我们切换到延迟路径。复制前向模式相机。将重复副本更改为延迟相机,然后禁用前向相机。这样,你可以通过更改启用的相机来快速在渲染模式之间切换。

你会注意到,使用延迟渲染路径时根本没有雾。这是因为在计算完所有光照之后必须应用雾。因此,我们无法在着色器的deferred pass中添加雾。

要比较同一图像中的延迟渲染和正向渲染,可以强制某些对象以正向模式渲染。例如,通过使用透明材质,同时使其完全不透明。

(不透明和透明材质)

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

为什么少了两个球?

右侧的对象使用透明的材质,即使它们是完全不透明的。结果,Unity在渲染它们时从后到前排序。最远的两个球体最终在它们下面的立方体之前渲染。由于透明对象不写入深度缓冲区,因此在这些球体前面绘制了立方体。

2.1 图像效果(影像效果)

要将雾添加到延迟渲染中,我们必须等到所有灯光都渲染完毕后,再进行一次pass以将雾因素叠加。由于雾应用于整个场景,所以,可以像渲染定向光一样。

添加此类pass的一种简单方法是将自定义组件添加到相机。因此,创建一个DeferredFogEffect类从MonoBehaviour继承。因为在编辑模式下能够看到雾非常有用,所以请为其指定ExecuteInEditMode属性。将此组件添加到我们的延迟相机中。最终会让雾效果出现在游戏视图中。

(使用雾效果的延迟摄像机)

要向渲染过程添加其他full-screen pass,请为我们的组件提供一个OnRenderImage方法。Unity将检查相机是否具有使用此方法的组件,并在渲染场景后调用它们。这让你可以更改效果或将效果应用于渲染的图像。如果有多个这样的组件,则会按照它们连接到相机的顺序来调用它们。

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

添加此方法后,游戏视图将无法渲染。我们必须确保要绘制一些东西。为此,请使用两个纹理作为参数调用Graphics.Blit方法。该方法将绘制一个带有着色器的全屏四边形,该着色器仅读取源纹理并输出未经修改的采样颜色。

场景再次像往常一样被渲染。但是,如果你检查帧调试器,则会看到为我们的图像效果添加了一个pass。

(绘制 image effect)

2.2 雾着色器

简单地复制图像数据是没有用的。我们必须创建一个新的自定义着色器,以将雾化效果应用于图像。从一个简单的着色器开始。因为我们只绘制一个应该覆盖所有内容的全屏四边形,所以应该忽略剔除和深度缓冲区,也不应该写入深度缓冲区。

我们的效果组件需要此着色器,因此为其添加一个公共字段,然后为其分配新的着色器。

(使用雾着色器)

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

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

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

2.3 基于深度的雾

因为我们使用的是延迟渲染,所以我们知道有可用的深度缓冲区。毕竟,light pass需要它来工作。我们可以从中读取信息,这意味着我们可以使用它来计算基于深度的雾。

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

尽管确切的语法取决于目标平台,但我们可以对此纹理进行采样。HLSLSupport中定义的SAMPLE_DEPTH_TEXTURE宏为我们解决了这一问题。

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

Linear01Depth是什么样的?

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

缓冲区参数在UnityShaderVariables中定义。

接下来,我们必须按远裁剪平面的距离缩放此值,以获得实际的基于深度的视图距离。剪辑空间设置可通过float4 _ProjectionParams变量获得,该变量在UnityShaderVariables中定义。它的Z分量包含远端平面的距离。

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

(错误的雾)

2.4 修复雾

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

(吴在不透明之后,透明之前)

另一个问题是雾色显然是错误的。当不使用HDR相机时,会发生这种情况,因为相机会弄乱颜色。这很简单,可以在我们的延迟摄像机上启用HDR。

(使用HDR相机)

最后,由于我们没有考虑近平面,因此可能再次在深度上有所不同。

(不同深度)

可以通过从视图距离中减去近平面距离来对此进行稍微补偿。它存储在_ProjectionParams的Y组件中。不幸的是,由于我们转换深度值的顺序,它不会完全匹配。但Unity的雾效果也会使用它来调整雾,所以我们也这样做。

(部分补偿深度)

2.5 基于距离的雾

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

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

(金字塔的侧视角)

对于图像的每个像素,我们可以从顶部到金字塔底部的某个点发出光线。如果没有任何障碍物,则光线到达底部,即远平面。否则,它将击中渲染的任何对象。

(每个像素一条射线)

如果碰到某物,则相应像素的深度值小于1。例如,如果碰到一半,则该深度值将为1/2。这意味着,射线的Z坐标是未被遮挡时的尺寸的一半。由于射线的方向仍然相同,这意味着X和Y坐标也减半。通常,我们可以从一直延伸到远平面的光线开始,然后按深度值进行缩放来找到实际光线。

(射线缩放)

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

为了使它有效,必须知道每个像素从相机到平面的光线。实际上,我们只需要四束光线,金字塔的每个角一个。插值为我们提供介于两者之间所有像素的光线。

2.6 计算光线

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

接下来,必须将此数据传递给着色器。我们可以使用向量数组来实现。但是,不能直接使用frustumCorners。第一个原因是我们只能将4D向量传递给着色器。因此,还包括一个Vector4 []字段,并将其作为_FrustumCorners传递给着色器。

第二个问题是必须更改拐角的顺序。CalculateFrustumCorners将它们排序为左下,左上,右上,右下。但是,用于渲染图像效果的四边形的角顶点按左下,右下,左上,右上的顺序排列。因此,我们对它们进行重新排序以匹配四边形的顶点。

2.7 得出距离

要访问着色器中的光线,请添加一个float数组变量。实际上,我们不需要为此添加属性,因为无论如何我们都不会手动对其进行编辑。尽管我们只能将4D向量传递给着色器,但在内部,我们仅需要前三个分量。所以float3类型就足够了。

接下来,定义FOG_DISTANCE,以表明我们希望将雾化基于实际距离,就像在其他着色器中一样。

当需要距离时,我们必须对光线进行插值并将其发送到片段程序。

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

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

(基于距离的雾)

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

2.8 雾化天空盒

实际上,前向雾和延迟雾之间仍然存在显着差异。你可能已经注意到,延迟的雾也会影响天空盒。它的作用就像是一个远方平面是一个固体屏障,受雾影响。

(雾化天空盒)

我们知道,当深度值接近1时,我们已经到达了远平面。如果不想对天空盒进行雾化,可以通过将雾化因子设置为1来防止这种情况。

(天空盒没有雾化)

如果确实要对整个图像应用雾化效果,则可以通过宏定义对其进行控制。定义FOG_SKYBOX后,请向天空盒添加雾,否则不要添加雾。

2.9 没有雾

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

(没有雾,但不正确)

当未定义任何雾气关键字时,可以通过将雾系数强制为1来完成此操作。这使我们的着色器只是进行纹理复制操作,而实际上,如果不需要它,最好停用或删除雾化组件。

下一章,介绍延迟光照。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-05-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 壹种念头 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 前向雾
    • 1.1 标准雾
      • 1.2 线性雾
        • 1.3 指数雾
          • 1.4 指数平方雾
            • 1.5 增加雾
              • 1.6 基于深度的雾
                • 1.7 深度还是距离
                  • 1.8 禁止雾
                    • 1.9 多灯光
                    • 2 延迟雾
                      • 2.1 图像效果(影像效果)
                        • 2.2 雾着色器
                          • 2.3 基于深度的雾
                            • 2.4 修复雾
                              • 2.5 基于距离的雾
                                • 2.6 计算光线
                                  • 2.7 得出距离
                                    • 2.8 雾化天空盒
                                      • 2.9 没有雾
                                      领券
                                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档