程序丨Unity 渲染系列教程(十三):延迟渲染
译者:崔嘉艺(milan21)
审校:王磊(未来的未来)
· 对延迟渲染的探索。
· 填充几何缓冲区。
· 既支持高动态光照渲染也支持低动态光照渲染。
· 与延迟处理的反射相兼容。
这是关于渲染基础的系列教程的第十三部分。在前面的部分里我们讲解了半透明阴影的实现,现在我们来看看延迟渲染。
系列回顾:
这个教程是使用Unity 5.5.0f3开发的。
几何体的解刨。
另一个渲染路径
到目前为止,我们一直使用的是Unity的前向渲染路径。但这不是Unity支持的唯一渲染方法。还有延迟渲染路径。而且还有遗留的顶点光照和传统的延迟渲染路径,但是我们不会涉及这些遗留的路径。
所以除了前向渲染路径之外,还有一个延迟渲染路径,但是为什么我们要使用这个延迟渲染路径呢?毕竟,我们可以使用前向渲染路径来渲染我们想要的一切。为了回答这个问题,我们来看看这两种渲染方法之间的差异。
渲染路径之间 31 46941 31 14985 0 0 3940 0 0:00:11 0:00:03 0:00:08 3940切换
使用哪个渲染路径由项目的图形设置来定义。你可以通过编辑/项目设置/图形来找到这个设置。渲染路径和其他一些设置分三个层级进行配置。这些层级对应于不同类别的图形处理器。图形处理器越好,Unity使用的层级就越高。你可以通过编辑器/图形仿真子菜单来选择编辑器使用的层级。
每一层级的图形设置。
要更改渲染路径,请禁用所需层级的“使用默认值”,然后选择“前向渲染”或是“延迟渲染”作为渲染路径。
绘制调用的比较
我将使用《渲染7:阴影》教程中的阴影场景来比较两种渲染方法。这个场景的环境光抢断设置为零,以便使得阴影更加可见。因为我们自己的着色器不支持延迟渲染,所以更改所使用的材质,因此现在它依赖于标准着色器。
场景中有不少物体和两个方向光源。让我们来看看两个方向光源不启用阴影和启用了阴影之间的对比。
阴影场景,两个方向光源不启用阴影和启用了阴影之间的对比。
在使用前向渲染路径的同时,使用帧调试器来检查场景的渲染方式。
场景中有66个几何对象,全部可见。如果动态批处理是可用的,那么这些几何对象就可以少于66个批次绘制出来。然而,这只适用于单个定向方向光源。由于有额外的光源,不能使用动态批处理。并且因为有两个方向光源,所有的几何对象被绘制两次。所以一共需要132个绘制调用来绘制这些几何对象,加上天空盒的绘制一共133个绘制调用。
前向渲染,在没有阴影的情况下。
当启用阴影的时候,我们需要更多的绘制调用来生成级联阴影贴图。回想下如何创建方向光源的阴影贴图。首先,深度缓冲区被填充,由于一些动态批处理,这只需要48次绘制调用。然后,创建级联阴影贴图。第一个光源的阴影贴图最终需要111个绘制调用,而第二个光源的阴影贴图最终需要121个绘制调用。这些阴影贴图被渲染屏幕空间缓冲区,在那里会执行过滤。然后几何体被绘制,每次只针对亮一个光源。这样做需要418个绘制调用。
前向渲染,在启用阴影的情况下。
现在,再次禁用阴影并切换到延迟渲染路径。 除了多重采样抗锯齿已经关闭以外,场景看起来还是一样的。这次会如何绘制?
为什么多重采样抗锯齿不能再延迟渲染模式下工作?
延迟渲染依赖于每个片段存储的数据,这是通过纹理来完成的。这与多重采样抗锯齿不兼容,因为抗锯齿技术依赖于子像素数据。虽然三角形的边缘仍然可以从多重采样抗锯齿中受益,但延迟渲染的数据仍然是混叠的。你将不得不依靠一个后处理过滤器来进行抗锯齿。
延迟渲染,在没有阴影的情况下。
显然,一个GBuffer被渲染,这需要45个绘制调用。每个对象一个绘制调用,再加上一些动态批处理。然后深度纹理被复制,然后紧跟着三个绘制调用来处理反射。在那之后,我们处理光照,这需要两个绘制调用,每个光源一个。最后还有一个渲染通道来处理天空盒,总共55个绘制调用。
55比起133来说是少了不少。看起来延迟渲染只绘制每个对象一次,而不是每个光源一次。除此之外,还有一些其他工作,每个光源都有它自己的绘制调用。那么当启用阴影的时候该怎么办?
延迟渲染,在启用阴影的情况下。
我们看到两个阴影贴图都被渲染,然后在屏幕空间中进行过滤,就在光照绘制之前。就像在前向渲染模式一样,这增加了236个绘制调用,总共291个绘制调用。由于延迟渲染已经创建了深度纹理,所以我们可以免费得到这个深度纹理。291次绘制调用也比418次绘制调用少了不少。
拆分工作
与前向渲染相比,在渲染多个光源的时候,延迟渲染似乎更加有效。前向渲染对每个物体每个光源都需要一个额外的附加渲染通道,但延迟渲染不需要这个额外的附加渲染通道。当然,这两种渲染方法还是要渲染阴影贴图,但延迟渲染不需要为方向光源阴影所需的深度纹理支付额外的成本。延迟渲染路径如何做到这一点?
要渲染一些东西,着色器必须先获取网格数据,将网格数据转换为正确的空间、进行插值、检索和导出表面属性,并计算光照。前向渲染的着色器必须对照亮对象的每个像素光重复所有这些过程。由于深度缓冲器已经被预分配好,所以附加渲染通道比基础渲染通道成本更低,并且附加渲染通道不使用间接光照。但是,附加渲染通道还必须重复基础渲染通道已经完成的大部分工作。
重复的工作。
既然几何体的属性每次都相同,为什么我们不缓存它们? 让基础渲染通道把这些数据存储在缓冲区中。然后附加渲染通道可以重用这些数据,消除重复的工作。我们必须为每个片段存储这些数据,所以我们需要一个适合显示的缓冲区,就像深度缓冲区和帧缓冲区一样。
缓存表面的属性。
现在我们在缓冲区里面拥有我们计算光照需要的所有几何数据。唯一缺少的就是光源本身。但是这意味着我们不再需要渲染几何。我们可以渲染光照效果。此外,基础渲染通道只需要填充缓冲区。所有的直接光照计算都可以推迟到光源被单独渲染的时候。因此这种方法被称为延迟渲染。
延迟渲染。
许多个光源
如果你只使用一个光源,那么延迟这个光源的光照计算本身就不会提供任何好处。但是当使用很多光源的时候,延迟光源的光照计算就会起效果。每一个额外的光源只增加一点额外的工作,只要他们不投射阴影就没问题。
另外,当单独渲染几何体和灯光的时候时,有多少个光源可以影响对象是没有数量限制的。所有光源都是像素光,照亮其范围内的一切。像素光数量这个质量设置不适用。
十个聚光光源,延迟渲染这种方法成功的渲染出来了,而前向渲染这种方法失败了。
渲染灯光
那么灯光本身如何渲染呢? 由于方向光源影响到所有的内容,因此它们将使用覆盖整个视图的单个四边形进行渲染。
使用单个四边形的方向光源。
这个四边形使用Internal-DeferredShading着色器进行渲染。它的片段程序从缓冲区中获取几何数据,并依赖于UnityDeferredLibrary导入文件来配置光源。然后它计算光照,就像一个前向着色器那么处理。
聚光光源的工作方式相同,不同之处在于它们不必覆盖整个视图。相反,只有位于聚光光源照亮的锥形区域会被渲染。所以只有位于这个可见区域的物体会被渲染。如果这个可见最终完全隐藏在其他几何体后面,则不会对这个光源执行任何渲染操作。
使用一个锥形区域来表示的聚光光源。
如果这个锥形区域的片段被渲染,那么将对这个片段执行光照计算。但是这只有在几何体实际位于聚光光源的锥形区域中才是有意义的。锥形区域后面的几何体不需要渲染,因为光线到达不了那里。为了防止渲染这些不必要的片段,锥形区域首先使用Internal-StencilWrite着色器进行渲染。这个渲染通道会将结果写入模板缓冲区,可以用于后面对哪些片段需要渲染进行掩码处理。这种技术无法使用的唯一情况是聚光光源照亮的范围与相机的近平面相交的情况。
点光源使用相同的方法,除了它照亮的范围是一个球而不是锥形区域。
使用一个球形区域来表示的点光源。
光源的覆盖范围
如果你一直在使用帧调试器进行步进,你可能已经注意到,在延迟光照阶段,颜色看起来很奇怪。就好像它们是颠倒的,就像一张照片一样。最终的延迟渲染通道将此中间状态转换为最终正确的颜色。
倒置的颜色。
当场景以低动态光照渲染(LDR )进行渲染的时候,Unity执行这个操作,这是默认的行为。在这种情况下,颜色将写入ARGB32纹理。Unity对颜色进行对数编码以达到比通常情况更大的动态范围。 最终的延迟渲染通道将结果转换为正常的颜色。
当场景以高动态光照渲染(HDR)进行渲染的时候,Unity使用ARGBHalf格式。在这种情况下,不需要特殊的编码,并且没有最终的延迟渲染通道。是否启用高动态光照渲染是相机的属性。打开这个属性,那么当使用帧调试器的时候,我们就能看到正常的颜色。
启用了高动态光照渲染。
几何缓冲区
缓存数据的缺点是这些缓存的数据必须存储在某个地方。延迟渲染路径为此目的使用多个渲染纹理。这些纹理被称为几何缓冲区,或称为G缓冲区。
延迟渲染需要四个G缓冲区。它们组合以后的尺寸在低动态光照渲染的情况下是每个像素占160位,在高动态光照渲染的情况下是每个像素占192位。 这比单个32位帧缓冲区稍微大一点。现代台式机的图形处理器可以处理这一点,但移动平台甚至笔记本电脑的图形处理器可能在更高分辨率的情况下会遇到问题。
你可以通过场景窗口来检查G缓冲区中的一些数据。使用窗口左上角的按钮选择不同的显示模式。默认设置为“渲染”。使用延迟渲染路径的时候,你可以选择四种Deferred 选项之一。举个简单的例子来说,Normal选项会显示包含曲面法线的缓冲区的RGB通道。
标准球体及其在延迟渲染中的法线。
你还可以通过帧调试器来检查绘制调用的多个渲染目标。有一个下拉菜单可以选择窗口右侧菜单左上角的渲染目标。默认是第一个目标,即RT 0。
选择一个渲染目标。
渲染模式的混合
我们自己的着色器不支持延迟渲染路径。那么如果场景中的某些对象是使用我们自己的着色器,而另外一些对象是使用延迟渲染模式会发生什么情况呢?
混合后的球体及其在延迟渲染中的法线。
我们的对象似乎渲染良好。事实证明,延迟渲染首先进行,之后是额外的前向渲染阶段。在延迟渲染阶段,前向渲染的对象不存在。唯一的例外是当有方向光的阴影的时候。在这种情况下,前向渲染的对象需要一个深度渲染通道。这是在填充G缓冲区之后直接完成的。这样做的副作用是,前向渲染的对象在反射率缓冲区中最终表现为纯黑色。
既有延迟渲染又有前向渲染的结果。
对于透明对象也是如此。像往常一样,它们需要单独的前向渲染阶段。
对于不透明物体同时使用延迟渲染和前向渲染,再加上对不透明物体的渲染。
填充G缓冲区
现在我们了解了延迟渲染是如何工作的,我们将其添加到My First Lighting着色器中。这是通过将它的LightMode标签设置为Deferred来添加一个渲染通道完成的。渲染通道的顺序并不重要。我把这个渲染通道放在附加渲染通道和阴影渲染通道之间。
白色的法线。
Unity检测到我们的着色器具有一个延迟渲染通道,因此它会包括在延迟阶段使用我们的着色器的不透明和切割对象。当然,透明对象仍将是在透明阶段进行渲染的。
因为我们的渲染通道是空的,所有的东西都被渲染为纯白色。我们必须添加着色器的功能和代码。延迟渲染通道与基础渲染通道大致相同,因此复制基础渲染通道的内容,然后进行一些修改。首先,我们将定义DEFERRED_PASS而不是FORWARD_BASE_PASS。其次,延迟渲染通道不需要_RENDERING_FADE和_RENDERING_TRANSPARENT关键字的变体。第三,仅当图形处理器支持写入多个渲染目标的时候,延迟渲染才是可能的。因此,当不支持这些指令的时候,我们将添加一个指令来排除延迟渲染。我标出了这些差异。
渲染后的法线。
现在延迟渲染通道的功能大致就像基础渲染通道的功能一样。所以它最终将渲染的结果写入G缓冲区,而不是几何数据。这是不对的 我们必须输出几何数据,而不是计算直接光照。
四个输出
在My Lighting之中,我们必须支持MyFragmentProgram的两种输出。 在延迟渲染通道的情况下,我们一共需要填充四个缓冲区。我们通过输出到四个目标来做到这一点。在所有其他情况下,我们可以只输出到一个目标。我们定义一个输出结构,位于在MyFragmentProgram的正上方。
不应该是SV_TARGET吗?
你可以将目标语义的大小写混起来用,Unity能够明白。 这里我使用的是Unity大部分着色器使用的格式。
请注意,不是语义都是这样。举个简单的例子来说,顶点数据的语义必须全部为大写。
调整MyFragmentProgram,以返回我们刚才定义的结构。对于延迟渲染通道,我们必须为所有四个输出分配值,我们稍后会做这个事情。其他渲染通道只是简单的复制最终的渲染颜色。
缓冲区0
第一个G缓冲区用于存储漫反射率和表面遮挡。这是一个ARGB32纹理,就像一个常规的帧缓冲区。反射率存储在RGB通道中,遮挡存储在A通道中。 我们知道此时的反射率颜色,我们可以使用GetOcclusion来访问这些遮挡值。
反射率和表面遮挡。
你可以使用场景视图或是帧调试器来检查第一个G缓冲区的内容,以验证我们是否正确填充。这将显示你的RGB通道。但是,A通道未显示。要检查遮挡数据,你可以暂时将这个数据分配给RGB通道。
缓冲区1
第二个G缓冲器用于存储RGB通道中的镜面高光颜色,以及A通道中的平滑度值。它也是一个ARGB32纹理。 我们知道镜面高光色调是什么,并且可以使用GetSmoothness来检索平滑度值。
镜面高光和平滑度值。
场景视图允许我们直接看到平滑度值,所以我们不必使用一个技巧来验证它们。
缓冲区2
第三个G缓冲区包含的是世界空间中的法向量。它们存储在ARGB2101010纹理的RGB通道中。这意味着每个坐标都是使用十位进行存储,而不是通常的八位。这使得它们更加精确。A通道只有两位-所以总共再次是32位 - 但是它没有被使用,所以我们只需将它设置为1。法线的编码就像是一个普通的法线贴图。
法线。
缓冲区3
最后的G缓冲区用于累积场景的光照。它的格式取决于相机是否设置为高动态光照渲染或是低动态光照渲染。在低动态光照渲染的情况下,它是一个ARGB2101010纹理,就像正常的缓冲区一样。当启用高动态光照渲染的时候,格式为ARGBHalf,它存储每个通道的16位浮点值,总共64位。 所以高动态光照渲染版本他缓冲区的大小是其他缓冲区的两倍。仅仅使用RGB通道,因此A通道可以再次设置为1。
我们不能使用RGBHalf格式而不是ARGBHalf格式么?
如果我们不使用A通道,那么这意味着每像素16位将留着不使用。不是有个RGBHalf格式么?这将使得每像素只需要48位,而不是64位。
我们使用ARGBHalf格式的原因是大多数图形处理器都使用四个字节的块。大多数纹理是每个像素32位,对应一个块。64位需要两个块,这样也可以。但是48位对应于1.5块。这导致了没有对齐,这可以通过使用两个48位的块来防止。这导致每个像素有16位的填充,实际大小与ARGBHalf格式相同。
我们因为相同的原因使用ARGB2101010格式。两个未使用的位用于填充。而RGB24纹理通常作为ARGB32格式存储在图形处理器的内存之中。
添加到该缓冲区中的第一个光源是自发光。没有一个单独的光源来处理这个是事情,所以我们必须在这个渲染通道中对它进行处理。让我们从我们已经计算出来的颜色开始做这个事情。
要预览这个缓冲区,请使用帧调试器,或暂时将此颜色分配给第一个G缓冲区。
有自发光,但是效果是错的。
我们现在使用的颜色完全是按照在方向光源的情况下进行渲染的,这是不正确的。我们可以通过对延迟渲染通道使用黑色的虚拟光源来消除所有的直接光计算。
当我们调整CreateLight时,我们还可以摆脱light.ndotl计算。
Unity已经弃用了这个结构字段和DotClamped函数。
我们已经关闭了直接光照,但是我们仍然需要包含自发光源。目前,它仅用于前向基础渲染通道。但是也要确保它包含在延迟渲染通道之中。
自发光。
环境光照
结果看起来不错,但还不完整。 我们缺少环境光照。
没有环境光照的效果。
环境光照没有单独的渲染通道。像自发光一样,当G缓冲区被填充的时候,它必须被添加进去。所以让我们在延迟渲染通道也启用间接光照。
带有环境光照的效果。
高动态光照渲染和低动态光照渲染
我们的着色器现在在前向渲染模式和延迟渲染模式下都产生相同的结果。至少在使用高动态光照渲染的相机的时候是如此。在低动态光照渲染模式下看起来有不少的错误。
在低动态光照渲染模式下效果是不正确的。
这是因为Unity预期低动态光照渲染数据将用对数进行编码,如前所述。所以我们也必须使用这个方式编码自发光和环境光照的贡献。
首先,我们必须知道我们使用的颜色范围。这是根据UNITY_HDR_ON关键字添加一个multi-compile指令到我们的渲染通道中来实现的。
现在,当定义了这个关键字以后,我们可以转换我们的颜色数据。对数编码用公式2-C完成,其中C是原始颜色。 我们可以在这里使用exp2函数。
从0到1变化的x 和2-x 函数。
在低动态光照渲染模式下和高动态光照渲染模式下累加光照的效果。
延迟渲染中的反射
在《渲染8:反射》这篇教程里面介绍了Unity如何使用反射探测器向表面添加镜面高光反射。然而,那里面描述的方法适用于前向渲染路径。当使用延迟渲染路径的时候,默认使用一个不同的方法。我将使用“反射场景”来比较两种方法。这个场景也将环境光的强度设置为零。打开场景后,确保用于镜面发射球体和地板的材质的金属和光滑度设置为1。另外,它还必须使用我们的着色器。
场景和反射探测器。
场景有三个反射探测器。一个反射探测器覆盖了结构内的区域。另一个反射探测器覆盖了结构外的一个小区域。这些反射探测器不重叠。第三个反射探测器位于它们之间,并且部分重叠。它被放置在那里以在结构内部和外部之间创建更好的混合过渡。仔细看看这个区域,无论是在前向渲染模式还是延迟渲染模式都是如此。
前向渲染模式和延迟渲染模式中的反射。
看起来中间的反射探测器在延迟渲染模式下要强得多。它主导着过渡的中间地带。更糟糕的是,它也影响了楼层的反射,这看起来是错的。
每个像素上的反射探测器
延迟渲染模式的不同之处在于每个物体不会混合不同反射探测器的结果。相反,它们的结果在每个像素上混合。这是由Internal-DeferredReflections 着色器完成的。为了使这个事情显而易见,放大地板上的镜子,使它的延伸超出结构,并从远处看这个镜子。然后比较前向渲染模式和延迟渲染模式。
大的地板镜子,在前向渲染模式和延迟渲染模式下的结果。
在前向渲染模式下,地板被迫将结构内的探头用于整个表面。结果就是,盒体投影在外面变得无意义。你还可以看到它与其他探测器的结果混合了一点。
地板的网格渲染器,在前向渲染模式和延迟渲染模式下的结果。
在延迟渲染模式下,反射探测器本身被渲染。它们被投影到与其体积相交的几何体上。因此,结构体内的反射探测器本的反射不超出其界限。实际上,他们会延伸一点,因为有淡出。其他两个反射探测器本也一样。
绘制延迟渲染下的反射效果。
首先渲染天空盒,覆盖整个视图。然后每个反射探测器都被渲染,就像光源一样,除了它们是使用立方体以外。
每个反射探测器完全覆盖其体积内的表面。以前渲染的任何反射都将被覆盖。Unity决定了渲染探测器的顺序。事实证明,较大的体积会首先被绘制,稍后才会绘制较小的体积。这样,局部的小型反射探测器可以推翻较大的反射探测器的结果。你可以通过在检视器里面使用探测器的重要性值来调整这个顺序。
一些反射探测器的设置。
融合距离
在延迟渲染模式下,探测器的反射在其自己的范围内是百分百强度的。但它们也会超出自己的探测范围。它们会在超出范围的时候做淡出处理并与已经渲染的其他反射相融合。延伸出去的距离由探测器的淡出距离进行控制,默认设置为一个单位。仅当使用延迟渲染路径的时候才启用此设置。
融合距离变化带来的效果:https://gfycat.com/MealyIndelibleIridescentshark。
融合距离有效地增加了反射探测器的范围。用于计算盒体投影的边界同样扩展相同的大小。结果就是,在前向渲染模式下正确的盒体投影在延迟渲染模式下可能是错误的,反之亦然。 在我们的例子中,结构中的反射可以通过将探测器的混合距离减小到零来修正这个问题。
由于混合距离造成的范围增加也是中间探测器影响地板镜子的原因。探测器的扩展范围互相交叠。我们无法将这个探测器的混合距离设置为零,因为这将消除混合的效果。相反,我们必须减小探测器范围的垂直尺寸,因此探测器范围不再与地板互相交叠。
调整探测器。
延迟渲染通道中的反射
虽然延迟渲染中的反射是有效的,并且可以对每个物体混合超过两个探测器的结果,但还是存在缺点。比如说不可能使用“锚点覆盖”来强制对象使用特定的反射探测器。但有时这是确保对象收到正确反射的唯一方法。举个简单的例子来说,当在不是轴对齐矩形的结构内部和外部都有反射探测器的时候。
幸运的是,你可以通过图形设置来禁用延迟渲染中的反射效果。要实现这一点,将“延迟反射”选项从”内置着色器“切换到”不支持“。
禁用延迟渲染中的反射效果。
当延迟渲染中的反射效果被禁用的时候,延迟渲染通道必须在反射探测器之间进行混合,像是前向基础渲染通道所做的那样。得到的结果被添加到自发光颜色之中。当G缓冲区被填充的时候,可以通过帧调试器检查第四个缓冲区RT 3来看到这一点。
有反射和没有反射的自发光效果。
事实证明,我们的延迟渲染通道已经在需要的时候提供了对反射的渲染,否则的话就会留下一片黑色。事实上,我们一直在使用反射探测器。只是在不使用的时候这些反射探测器会被设置为黑色。
采样黑色的探测器是浪费时间。让我们确保我们的延迟渲染通道只有在需要的时候才这样做。我们可以使用UNITY_ENABLE_REFLECTION_BUFFERS来检查这一点。当延迟渲染的反射效果启用的时候,它被定义为1。
下一个教程将涵盖更多有关着色器特效的内容。
【版权声明】
原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权。
今日推荐
一键添加
加小编微信,享双重福利
1.加入GAD程序猿交流群,获取行业干货;
2.领取60G腾讯内部分享等独家程序资料。