专栏首页壹种念头基础渲染系列(十四)——雾

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

本文重点:

给物体应用雾 基于距离和深度的基础雾 创建图像效果(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来完成此操作。这使我们的着色器只是进行纹理复制操作,而实际上,如果不需要它,最好停用或删除雾化组件。

下一章,介绍延迟光照。

本文分享自微信公众号 - 壹种念头(OneDay1Idea),作者:壹种念头

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-05-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Unity可编程渲染管线系列(三)光照(单通道 正向渲染)

    这是涵盖Unity可编写脚本的渲染管线的教程系列的第三部分。这次,我们将通过一个Drawcall为每个对象最多着色8个灯光来增加对漫反射光照的支持。

    放牛的星星
  • Unity基础教程系列(七)——可配置形状(Variety of Randomness)

    这是有关 对象管理 的系列教程中的第七篇。它为形状增加了一些行为,并可以针对每个生成区域配置它们。

    放牛的星星
  • 基础渲染系列(二十)——视差(基础篇完结)

    这是有关渲染的系列教程的第20部分。上一部分介绍了GPU实例化。在这一部分中,我们将添加到目前为止尚不支持的标准着色器的最后一部分,即视差贴图。

    放牛的星星
  • Django—跨域请求(jsonp)

    启动浏览器,访问http://127.0.0.1:8001/demo2,点击按钮,然后控制台报错

    py3study
  • python语句-for

    py3study
  • 为什么中位数(大多数时候)比平均值好

    开始我的数据分析冒险之旅,我发现了解数据描述的主要统计方法是非常必要的。当我深入研究时,我意识到我很难理解为给定的数据选择哪个集中趋势指标有三种:平均值,中位数...

    deephub
  • 实现用于意图识别的文本分类神经网络

    在这个教程中,我们将使用2层神经元(1个隐层)和词袋(bag of words)方法来组织我们的训练数据。 文本分类的方法有三种 : 模式匹配 , 传统算法和神...

    机器学习AI算法工程
  • 【javascript】谈谈HTML5—Web Worker+canvas+indexedDB+拖拽事件

    前言:作为一名Web开发者,可能你并没有对这个“H5”这个字眼投入太多的关注,但实际上它早已不知不觉进入到你的开发中,并且总有一天会让你不得不正视它,了解它并运...

    外婆的彭湖湾
  • 【javascript】谈谈HTML5: Web-Worker、canvas、indexedDB、拖拽事件

    前言:作为一名Web开发者,可能你并没有对这个“H5”这个字眼投入太多的关注,但实际上它早已不知不觉进入到你的开发中,并且总有一天会让你不得不正视它,了解它并运...

    外婆的彭湖湾
  • 郁金香商业辅助教程 2016 笔记 6~10

    我们希望把 DLL 和这个程序放到一起,那么 DLL 路径就是程序所在路径加上 DLL 的名称。

    ApacheCN_飞龙

扫码关注云+社区

领取腾讯云代金券