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

基础渲染系列(十三)——延迟着色

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

本文重点:

探索延迟着色 填充几何缓冲区(G Buffers) 支持HDR和LDR 和延迟反射一起生效(Deferred Reflections)

这是关于渲染的系列教程的第13部分。上一部分涵盖了半透明阴影。现在我们来看一下延迟着色。

本教程是使用Unity 5.5.0f3制作的。

(几何结构)

1 另一条渲染路径

到目前为止,我们一直使用Unity的前向渲染路径。但这不是Unity支持的唯一渲染方法。还有延迟的路径。此外,还有遗留的顶点光照和遗留的延迟路径,但我们不介绍它们。

所以还有一个延迟渲染路径,但是为什么需要新加路径呢?毕竟,可以使用前向路径渲染所有想要的东西。为了回答这个问题,让我们研究它们之间的差异。

1.1 切换路径

使用哪个渲染路径由项目设置的图形设置定义的。你可以通过“Edit/ Project Settings/Graphics”到达那里。渲染路径和其他一些设置分为三个层级。这些层级对应于不同类别的GPU。GPU越好,Unity使用的层级就越高。你可以通过“Editor /Graphics Emulation”子菜单选择编辑器使用的层级。

(图形设置 逐层级)

要更改渲染路径,请禁用所需层级的“Use Defaults”,然后选择“Forward”或“Deferred”作为“Rendering Path”。

1.2 比较 Draw Calls

我们使用“渲染7,阴影”教程中的“Shadows Scene”来比较这两种方法。此场景的“环境强度”(Ambient Intensity)设置为零,以使阴影更明显。由于我们自己的着色器尚不支持延迟着色器,因此请更改使用的材质,使其依赖于标准着色器。

场景中有很多物体和两个定向光。让我们看一下灯光阴影同时不启用和同时启用的效果。

(没有VS有阴影)

在使用前向渲染路径时,请使用帧调试器检查场景的渲染方式。

场景中有66个几何对象,全部可见。如果可以进行动态批处理,则这些批处理可能少于66个批次。但是,这仅适用于单个定向光。由于存在其他光源,因此不能使用动态批处理。而且由于有两个定向光,所有几何图形都会绘制两次。因此,这是132个Draw Call,其中第133个是Skybox。

(前向渲染 没有阴影)

启用阴影后,我们需要更多的Draw Calls才能生成级联的阴影贴图。回想一下如何创建定向阴影贴图。首先,由于有一些动态批处理,深度缓冲区被填充仅需要48个Draw Calls。然后,创建级联阴影贴图。第一个灯光的阴影贴图最终需要111个Draw Calls,而第二个灯光阴影贴图则需要121个Draw Calls。这些阴影贴图被渲染到执行过滤的屏幕空间缓冲区。然后绘制几何图形,每个光绘制一次。完成所有这些操作需要418次Draw Calls。

(前向渲染 带有阴影)

现在,再次禁用阴影并切换到延迟渲染路径。除了已关闭MSAA之外,该场景看起来仍然相同。这次如何绘制的呢?

为什么MSAA无法在延迟模式下工作?

延迟着色依赖于每个片段存储的数据,这是通过纹理完成的。这与MSAA不兼容,因为该抗锯齿技术依赖于子像素数据。尽管三角形边缘仍然可以从MSAA中受益,但延迟的数据仍会锯齿。你必须依靠一个后处理过滤器来进行抗锯齿。

(延迟渲染,没有阴影)

显然,渲染GBuffer,这需要45个Draw Calls。每个对象一个,并带有一些动态批处理。然后复制深度纹理,然后进行Draw Calls以进行反射。之后,我们开始进行灯光计算,这需要两个Draw Calls,每个光一个。然后是最后一个pass和天空盒,总共进行了55次Draw Calls。

55比133小很多。看起来deferred总共只绘制每个对象一次,而不是每个灯光一次。除此以外,还有其他工作,每个光都有自己的Draw Call。那启用阴影呢?

(延迟渲染,有阴影)

我们看到两个阴影贴图都被渲染了,然后在绘制光线之前在屏幕空间中进行了过滤。就像在前向模式下一样,这增加了236个绘制调用,总计291个。由于延迟已经创建了深度纹理,我们免费获得了该纹理。同样,291比418小很多。

1.3 分解

与前向阴影相比,在渲染多个光源时,延迟阴影似乎更有效。前向渲染需要每个物体每个灯光额外增加一次pass,但延迟渲染不需要这样做。当然,两者仍然都必须渲染阴影贴图,但是延迟不必为定向阴影所需的深度纹理支付额外的费用。延迟渲染路径是如何解决它的呢?

要渲染物体,着色器必须获取网格数据,将其转换为正确的空间,对其进行插值,检索和导出表面属性,并计算照明度。前向着色器必须对受光对象的每个像素光重复所有这些操作。附加的通道比基本通道便宜一些,因为深度缓冲区已经准备好了,它们不会被间接光打扰。但是他们仍然必须重复基本通道已经完成的大部分工作。

(重复工作)

由于几何的属性每次都是相同的,为什么不缓存它们呢?让基本通道将它们存储在缓冲区中。然后,附加通道可以重复使用该数据,从而消除了重复工作。我们必须按片段存储此数据,因此我们需要一个适合显示的缓冲区,就像深度缓冲区和帧缓冲区一样。

(缓存表面属性)

现在,缓冲区中提供了照明所需的所有几何数据。唯一缺少的是灯光本身。但这意味着我们不再需要渲染几何体。可以只渲染光就足够了。此外,基本通道只需要填充缓冲区。可以推迟所有直接照明计算,直到分别渲染它们为止。因此叫延迟着色。

(延迟着色)

1.4 更多的灯光

如果只使用一个光源,那么单个延迟将不会带来任何好处。但是当使用非常多灯光时,它就派上大用场了。只要不投射阴影,每增加一个灯光就只会增加一点点额外的工作。

同样,当分别渲染几何图形和灯光时,可以影响对象的灯光数量没有限制。所有的灯都是像素灯,并照亮其范围内的所有物体。质量设置里“Pixel Light Count ”不再适用。

(10个聚光灯,延迟渲染成功 前向渲染失败)

1.5 渲染灯光

那么灯光本身如何渲染?由于定向光源会影响所有事物,因此将使用覆盖整个视图的单个四边形对其进行渲染。

(方向光使用一个4边形)

该四边形使用Internal-DeferredShading着色器渲染。它的片段程序从缓冲区获取几何数据,并依赖UnityDeferredLibrary包含文件来配置灯光。然后,它像前向着色器一样计算照明。

聚光灯的工作方式相同,只是它们不必覆盖整个视图。取而代之的是绘制一个金字塔,以适应聚光灯照亮的体积。因此,只会渲染此体积的可见部分。如果最终完全隐藏在其他几何图形的后面,则不会对此光源执行任何着色处理。

(聚光灯使用金字塔)

如果金字塔的一个片段被渲染,它将执行照明计算。但这仅在灯光体积内确实存在几何形状时才有意义。不需要渲染体积后面的几何形状,因为光线不会到达那里。为避免渲染这些不必要的片段,首先使用Internal-StencilWrite着色器渲染金字塔。此过程将写入模板缓冲区,该缓冲区可用于掩盖稍后渲染的片段。不能使用此技术的唯一情况是光量与相机的近平面相交。

点光源使用相同的方法,除了使用icosphere而不是金字塔。

(点光源使用icosphere(近似球体))

1.6 灯光半径

如果你一直在调试帧调试器,则可能已经注意到在延迟照明阶段颜色看起来很怪异。好像它们是倒置的,像照片反色一样。最终的延迟通道将中间状态转换为最终的正确颜色。

(反色)

当场景以低动态范围-LDR-颜色(默认设置)渲染时,Unity会执行此操作。在这种情况下,颜色将写入ARGB32纹理。Unity对数编码颜色,以实现比此格式通常更大的动态范围。最终的延迟通过将转换为正常颜色。

在高动态范围内渲染场景– HDR – Unity使用ARGBHalf格式。在这种情况下,不需要特殊的编码,也没有最终的延迟通道。是否启用HDR是摄像机的属性。将其打开,以便在使用帧调试器时看到正常的颜色。

(启用HDR)

1.7 几何缓冲区(GBuffers)

缓存数据的缺点是必须将其存储在某个位置。为此,延迟的渲染路径使用了多个渲染纹理。这些纹理称为几何缓冲区,简称G缓冲区。

延迟着色需要四个G缓冲区。对于LDR,它们的组合大小为每像素160位,对于HDR,它们的组合大小为每像素192位。这比单个32位帧缓冲区要多得多。现代的台式机GPU可以解决这个问题,但是移动甚至笔记本电脑的GPU在分辨率更高时都会遇到麻烦。

你可以通过场景窗口检查G缓冲区中的某些数据。使用窗口左上方的按钮选择其他显示模式。默认情况下设置为“Shaded”。使用延迟渲染路径时,可以选择四个选项之一的“Deferred”。例如,“Normal”显示包含表面法线的缓冲区的RGB通道。

(标准球和它们的延迟法线)

你还可以通过帧调试器检查绘制调用的多个渲染目标。在窗口右侧的菜单左上方,有一个下拉菜单可以选择渲染目标。默认值为第一个目标,即RT 0。

(选择渲染目标)

1.8 混合渲染模式

我们自己的着色器尚不支持延迟的渲染路径。那么,如果在延迟模式下使用我们的着色器渲染场景中的某些对象会发生什么?

(混合球,带有延迟法线)

我们的对象看起来很好。事实证明,延迟渲染是首先完成的,然后是附加的正向渲染阶段。在延迟渲染阶段,前向对象不存在。唯一的例外是存在定向阴影时。在这种情况下,前向物体需要进行深度通道。G缓冲区填满后直接执行此操作。副作用是,前向对象在反照率缓冲区中最终变为纯黑色。

(延迟和前向一起)

透明对象也是如此。与往常一样,它们需要一个单独的前向渲染阶段。

(延迟一个前向的不透明物体,叠加透明物体)

2 填充G-Buffers

现在我们已经了解了延迟着色的工作原理,让我们为“My First Lighting Shader”添加对它的支持。这可以通过添加其LightMode标签设置为Deferred的通道来完成。通道的顺序无关紧要。我把它放在附加和阴影通道之间。

(白色的法线)

Unity检测到我们的着色器具有延迟的pass,因此它包含在延迟阶段使用我们的着色器的不透明对象和剪切对象。当然,透明对象仍将在透明阶段渲染。

因为我们的pass是空的,所以所有内容都会呈现为纯白色。必须添加着色器功能和程序。延迟的pass与基本pass基本相同,因此请复制该pass的内容,然后进行一些更改。首先,我们将定义DEFERRED_PASS而不是FORWARD_BASE_PASS。其次,延迟的pass不需要_RENDERING_FADE和_RENDERING_TRANSPARENT关键字的变体。第三,仅当GPU支持写入多个渲染目标时才可以使用延迟着色。因此,当不支持这些指令时,我们将添加一个指令以将其排除。我标记了这些差异。

(着色法线)

现在,deferred pass 的功能大致类似于base pass。因此,最终会直接着色结果而不是将几何数据写入G缓冲区。这个流程是不正确的。我们必须输出几何数据,而不要直接计算照明。

2.1 4个输出

在My Lighting中,在使用deferred pass的情况下,我们必须为MyFragmentProgram支持两种输出,还需要填充四个缓冲区。通过输出到四个target来实现。而在其他情况下,只需一个即可。让我们在MyFragmentProgram上方直接为此定义一个输出结构。

不应该是SV_TARGET吗?

可以混合使用大写字母和小写字母作为目标语义,Unity可以全部理解。在这里,我使用的是Unity最新着色器的相同格式。

请注意,并非所有语义都是大小写混写正确的。例如,顶点数据语义必须全部为大写。

调整MyFragmentProgram,使其返回此结构。对于deferred pass,需要为所有四个输出分配值,我们将在稍后进行操作。其他pass只复制最终的阴影颜色。

2.2 Buffer 0

第一个G缓冲区用于存储漫反射反照率和表面遮挡。它是ARGB32纹理,就像常规的帧缓冲区一样。反照率存储在RGB通道中,遮挡存储在A通道中。我们知道此时的反照率颜色,并且可以使用GetOcclusion访问遮挡值。

(反照率和遮挡关系)

你可以使用场景视图或帧调试器检查第一个G缓冲区的内容,以验证我们是否正确填充了它。这会向你显示其RGB通道。但是,不会显示A通道。要检查遮挡数据,可以将其临时分配给RGB通道。

2.3 Buffer 1

第二个G缓冲区用于在RGB通道中存储镜面颜色,在A通道中存储平滑度值。它也是ARGB32纹理。我们知道镜面反射的色调是什么,并且可以使用GetSmoothness检索平滑度值。

(镜面颜色和平滑度)

场景视图使我们可以直接看到平滑度值,因此我们无需使用技巧即可对其进行验证。

2.4 Buffer 2

第三个G缓冲区包含世界空间法线向量。它们存储在ARGB2101010纹理的RGB通道中。这意味着每个坐标使用10位存储,而不是通常的8位,这使它们更加精确。A通道只有2位-因此总数又是32位,但它未使用,因此我们将其设置为1。法线的编码方式类似于常规法线贴图。

(法线)

2.5 Buffer 3

最终的G缓冲区用于累积场景的光照。其格式取决于相机是设置为LDR还是HDR。就LDR而言,它是ARGB2101010纹理,就像法线的缓冲区一样。启用HDR时,格式为ARGBHalf,每个通道存储一个16位浮点值,总共64位。因此,HDR版本是其他缓冲区的两倍。仅使用RGB通道,因此可以将A通道再次设置为1。

能使用RGBHalf代替ARGBHalf吗?

如果我们不使用A通道,则意味着每个像素16位未使用。

没有RGBHalf格式吗?

那将只需要每个像素48位,而不是64位。

我们使用ARGBHalf的原因是大多数GPU都使用四个字节的块。大多数纹理是每个像素32位,相当于一个块。64位需要两个块,因此也可以使用。但是48位对应于1.5个块。这会导致未对齐,可以通过将两个块用于48位来避免。这导致每个像素填充16位,又与ARGBHalf相同了。

出于相同的原因,使用了ARGB2101010。两个未使用的位为填充。RGB24纹理通常作为ARGB32存储在GPU内存中。

添加到此缓冲区的第一个光是自发光。没有单独的自发光通道,因此我们必须在此步骤中进行。让我们开始使用我们已经计算出的颜色。

要预览此缓冲区,请使用帧调试器,或将此颜色临时分配给第一个G缓冲区。

(自发光,但是是错的)

我们现在使用的颜色已完全阴影化,好像有定向光一样,这是不正确的。可以通过将延迟设置为黑色的虚拟光消除所有的直接光计算。

在调整CreateLight的同时,我们也要摆脱light.ndotl的计算。Unity不赞成使用该结构字段和DotClamped函数。

我们已经关闭了方向光,但是我们仍然需要包含自发光。目前,仅针对forward base pass 进行检索。当然deferred pass也要包含以一下。

(自发光)

2.6 环境光

结果看起来不错,但尚未完成。因为我们缺少环境光。

(没有环境光)

没有单独的环境光通道。像自发光一样,必须在填充G缓冲区时添加它。因此,让我们也为deferred pass启用间接光。

(带有环境光)

2.7 HDR和LDR

现在,我们的着色器在正向和延迟模式下都产生相同的结果,至少在使用HDR摄像机时是这样。在LDR模式下看起来也很不对劲。

(LDR模式下不正确)

发生这种情况是因为Unity期望对LDR数据进行对数编码,如前所述。因此,对于自发光和环境影响,我们也必须使用这种编码。

首先,我们必须知道我们使用的颜色范围。这是通过在关键字中添加一个基于UNITY_HDR_ON的多编译指令来完成的。

现在,我们可以在定义了此关键字后转换颜色数据。对数编码是使用公式2-C完成的,其中C是原始颜色。我们可以为此使用exp2函数。

(在LDR和HRD模式下的光叠加)

3 延迟反射

Rendering 8,Reflections教程介绍了Unity如何使用反射探针将镜面反射添加到表面。但是,此处描述的方法适用于正向渲染路径。使用延迟路径时,默认情况下使用其他方法。我将使用反射场景来比较这两种方法。该场景的环境强度(Ambient Intensity)也设置为零。打开场景后,请确保用于镜像球体和地板的材质的“Metallic ”和“Smoothness”设置为1。此外,还必须使用我们的着色器。

(场景和反射探针)

该场景具有三个反射探测器。一个覆盖结构内部的区域。另一个覆盖结构外部的一个小区域。这些探针不重叠。第三个探针位于它们之间,并且部分重叠。将其放置在此处可在结构内部和外部之间创建更好的混合过渡。在前进和后退模式下,请仔细查看该区域。

(前向和延迟的反射)

似乎中间探针在延迟模式下要强得多。它主导了过渡的中间区域。更糟糕的是,它还会影响地板的反射,这看起来是非常错误的。

3.1 逐像素探针

延迟模式的不同之处在于,不会针对每个对象混合探针。相反,它们是按像素混合的。这是由Internal-DeferredReflections着色器完成的。为了使这一点更加明显,请放大地板镜,使其延伸到结构之外,并从远处看。然后比较前向和延迟。

(巨大的地面镜子 前向和延迟)

在前向模式下,地板被迫在结构的整个表面上使用探针。结果,盒子的投影在外面变得毫无意义。你还可以看到它与其他探针之一融合在一起。

(地表的Mesh renderer 前向和延迟)

在延迟模式下,反射探针本身将被渲染。它们被投影到与它们的体积相交的几何体上。因此,结构内部探针的反射不会超出其范围。实际上,它们逐渐淡出时会延伸一点。其他两个探针也是如此。

(绘制延迟反射)

首先渲染天空盒,覆盖整个视图。然后,每个探针都被渲染,就像灯光一样,只是它们使用立方体。

每个探针最终完全覆盖其体积内的表面。先前渲染的所有反射都将被覆盖。Unity决定探针的渲染顺序。事实证明,首先绘制较大的体积,然后绘制较小的体积。这样,局部小探针可以覆盖大面积探针。你可以通过探针的检查器使用探针的“Importance”值来调整此顺序。

(一些反射探针设置)

3.2 融合距离

在延迟模式下,探针的反射在其体积内处于最大强度。但是它们也超出了阙值。它们淡出并与已经渲染的其他反射混合。延伸距离受探针的“Fade Distance”控制,默认情况下将其设置为一个单位。仅在使用延迟渲染路径时启用此设置。

(变化的Blend Distance)

混合距离有效地增加了反射探针的体积。用于计算盒投影的边界将扩大相同的数量。结果,在正向模式下正确的盒投影在延迟模式下可能会出错,反之亦然。现在,可以通过将探头的“Blend Distance”减小为零来固定结构内部的反射。

由于混合距离而导致的体积增加也是中间探针影响地板反射镜的原因。探针的扩展体积与其相交。我们不能将此探针的混合距离设置为零,因为这样可能会消除混合。取而代之的是,我们必须减小探针体积的垂直大小,因此它不再与地板相交。

(调整探针)

3.3 在Deferred Pass中反射

尽管延迟反射很有效,并且每个对象可以混合两个以上的探针,但存在不利之处。无法使用“Anchor Override”来强制对象使用特定的反射探针。但这有时是确保对象接收正确反射的唯一方法。例如,当在不是轴对齐矩形的结构的内部和外部都有反射探针时。

幸运的是,可以通过图形设置禁用延迟反射。为此,请将Deferred Reflections图形设置从“Built-in Shader”切换为“No Support”。

(禁用延迟反射)

禁用延迟反射时,deferred pass必须像常规 前向 pass一样在反射探针之间混合,并结果添加到自发光颜色。当G缓冲区已满时,你可以通过帧调试器检查第四个缓冲区RT 3来看到这一点。

(有反射和无反射的自发光)

事实证明,当需要时,我们的 deferred pass 已经渲染了反射,不然的话会将其保持黑色。实际上,我们一直都在使用反射探针。只是它们在不使用时设置为黑色。

对黑色探头进行采样是浪费时间。所以我们确保仅在需要时才执行deferred pass。我们可以使用UNITY_ENABLE_REFLECTION_BUFFERS进行检查。启用延迟反射时,将其定义为1。

下一章,介绍雾。

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 另一条渲染路径
    • 1.1 切换路径
      • 1.2 比较 Draw Calls
        • 1.3 分解
          • 1.4 更多的灯光
            • 1.5 渲染灯光
              • 1.6 灯光半径
                • 1.7 几何缓冲区(GBuffers)
                  • 1.8 混合渲染模式
                    • 2 填充G-Buffers
                      • 2.1 4个输出
                        • 2.2 Buffer 0
                          • 2.3 Buffer 1
                            • 2.4 Buffer 2
                              • 2.5 Buffer 3
                                • 2.6 环境光
                                  • 2.7 HDR和LDR
                                  • 3 延迟反射
                                    • 3.1 逐像素探针
                                      • 3.2 融合距离
                                        • 3.3 在Deferred Pass中反射
                                        领券
                                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档