Unity 渲染教程13延迟渲染.docx
《Unity 渲染教程13延迟渲染.docx》由会员分享,可在线阅读,更多相关《Unity 渲染教程13延迟渲染.docx(33页珍藏版)》请在冰点文库上搜索。
Unity渲染教程13延迟渲染
Unity渲染教程(13):
延迟渲染
译者:
崔嘉艺(milan21) 审校:
王磊(未来的未来)
∙ 对延迟渲染的探索。
∙ 填充几何缓冲区。
∙ 既支持高动态光照渲染也支持低动态光照渲染。
∙ 与延迟处理的反射相兼容。
这是关于渲染基础的系列教程的第十三部分。
在前面的部分里我们讲解了半透明阴影的实现,现在我们来看看延迟渲染。
这个教程是使用Unity5.5.0f3开发的。
几何体的解刨。
另一个渲染路径
到目前为止,我们一直使用的是Unity的前向渲染路径。
但这不是Unity支持的唯一渲染方法。
还有延迟渲染路径。
而且还有遗留的顶点光照和传统的延迟渲染路径,但是我们不会涉及这些遗留的路径。
所以除了前向渲染路径之外,还有一个延迟渲染路径,但是为什么我们要使用这个延迟渲染路径呢?
毕竟,我们可以使用前向渲染路径来渲染我们想要的一切。
为了回答这个问题,我们来看看这两种渲染方法之间的差异。
渲染路径之间的切换
使用哪个渲染路径由项目的图形设置来定义。
你可以通过编辑/项目设置/图形来找到这个设置。
渲染路径和其他一些设置分三个层级进行配置。
这些层级对应于不同类别的图形处理器。
图形处理器越好,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通道。
标准球体及其在延迟渲染中的法线。
你还可以通过帧调试器来检查绘制调用的多个渲染目标。
有一个下拉菜单可以选择窗口右侧菜单左上角的渲染目标。
默认是第一个目标,即RT0。
选择一个渲染目标。
渲染模式的混合
我们自己的着色器不支持延迟渲染路径。
那么如果场景中的某些对象是使用我们自己的着色器,而另外一些对象是使用延迟渲染模式会发生什么情况呢?
混合后的球体及其在延迟渲染中的法线。
我们的对象似乎渲染良好。
事实证明,延迟渲染首先进行,之后是额外的前向渲染阶段。
在延迟渲染阶段,前向渲染的对象不存在。
唯一的例外是当有方向光的阴影的时候。
在这种情况下,前向渲染的对象需要一个深度渲染通道。
这是在填充G缓冲区之后直接完成的。
这样做的副作用是,前向渲染的对象在反射率缓冲区中最终表现为纯黑色。
既有延迟渲染又有前向渲染的结果。
对于透明对象也是如此。
像往常一样,它们需要单独的前向渲染阶段。
对于不透明物体同时使用延迟渲染和前向渲染,再加上对不透明物体的渲染。
工程文件下载地址:
unitypackage。
填充G缓冲区
现在我们了解了延迟渲染是如何工作的,我们将其添加到MyFirstLighting着色器中。
这是通过将它的LightMode标签设置为Deferred来添加一个渲染通道完成的。
渲染通道的顺序并不重要。
我把这个渲染通道放在附加渲染通道和阴影渲染通道之间。
1
2
3
4
5
Pass{
Tags{
"LightMode"="Deferred"
}
}
白色的法线。
Unity检测到我们的着色器具有一个延迟渲染通道,因此它会包括在延迟阶段使用我们的着色器的不透明和切割对象。
当然,透明对象仍将是在透明阶段进行渲染的。
因为我们的渲染通道是空的,所有的东西都被渲染为纯白色。
我们必须添加着色器的功能和代码。
延迟渲染通道与基础渲染通道大致相同,因此复制基础渲染通道的内容,然后进行一些修改。
首先,我们将定义DEFERRED_PASS而不是FORWARD_BASE_PASS。
其次,延迟渲染通道不需要_RENDERING_FADE和_RENDERING_TRANSPARENT关键字的变体。
第三,仅当图形处理器支持写入多个渲染目标的时候,延迟渲染才是可能的。
因此,当不支持这些指令的时候,我们将添加一个指令来排除延迟渲染。
我标出了这些差异。
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
Pass{
Tags{
"LightMode"="Deferred"
}
CGPROGRAM
#pragmatarget3.0
#pragmaexclude_renderersnomrt
#pragmashader_feature__RENDERING_CUTOUT
#pragmashader_feature_METALLIC_MAP
#pragmashader_feature__SMOOTHNESS_ALBEDO_SMOOTHNESS_METALLIC
#pragmashader_feature_NORMAL_MAP
#pragmashader_feature_OCCLUSION_MAP
#pragmashader_feature_EMISSION_MAP
#pragmashader_feature_DETAIL_MASK
#pragmashader_feature_DETAIL_ALBEDO_MAP
#pragmashader_feature_DETAIL_NORMAL_MAP
#pragmavertexMyVertexProgram
#pragmafragmentMyFragmentProgram
#defineDEFERRED_PASS
#include"MyLighting.cginc"
ENDCG
}
渲染后的法线。
现在延迟渲染通道的功能大致就像基础渲染通道的功能一样。
所以它最终将渲染的结果写入G缓冲区,而不是几何数据。
这是不对的我们必须输出几何数据,而不是计算直接光照。
四个输出
在MyLighting之中,我们必须支持MyFragmentProgram的两种输出。
在延迟渲染通道的情况下,我们一共需要填充四个缓冲区。
我们通过输出到四个目标来做到这一点。
在所有其他情况下,我们可以只输出到一个目标。
我们定义一个输出结构,位于在MyFragmentProgram的正上方。
1
2
3
4
5
6
7
8
9
10
structFragmentOutput{
#ifdefined(DEFERRED_PASS)
float4gBuffer0:
SV_Target0;
float4gBuffer1:
SV_Target1;
float4gBuffer2:
SV_Target2;
float4gBuffer3:
SV_Target3;
#else
float4color:
SV_Target;
#endif
};
不应该是SV_TARGET吗?
你可以将目标语义的大小写混起来用,Unity能够明白。
这里我使用的是Unity大部分着色器使用的格式。
请注意,不是语义都是这样。
举个简单的例子来说,顶点数据的语义必须全部为大写。
调整MyFragmentProgram,以返回我们刚才定义的结构。
对于延迟渲染通道,我们必须为所有四个输出分配值,我们稍后会做这个事情。
其他渲染通道只是简单的复制最终的渲染颜色。
1
2
3
4
5
6
7
8
9
10
FragmentOutputMyFragmentProgram(Interpolatorsi){
…
FragmentOutputoutput;
#ifdefined(DEFERRED_PASS)
#else
output.color=color;
#endif
returnoutput;
}
缓冲区0
第一个G缓冲区用于存储漫反射率和表面遮挡。
这是一个ARGB32纹理,就像一个常规的帧缓冲区。
反射率存储在RGB通道中,遮挡存储在A通道中。
我们知道此时的反射率颜色,我们可以使用GetOcclusion来访问这些遮挡值。
1
2
3
4
#ifdefined(DEFERRED_PASS)
output.gBuffer0.rgb=albedo;
output.gBuffer0.a=GetOcclusion(i);
#else
反射率和表面遮挡。
你可以使用场景视图或是帧调试器来检查第一个G缓冲区的内容,以验证我们是否正确填充。
这将显示你的RGB通道。
但是,A通道未显示。
要检查遮挡数据,你可以暂时将这个数据分配给RGB通道。
缓冲区1
第二个G缓冲器用于存储RGB通道中的镜面高光颜色,以及A通道中的平滑度值。
它也是一个ARGB32纹理。
我们知道镜面高光色调是什么,并且可以使用GetSmoothness来检索平滑度值。
1
2
3
4
output.gBuffer0.rgb=albedo;
output.gBuffer0.a=GetOcclusion(i);
output.gBuffer1.rgb=specularTint;
output.gBuffer1.a=GetSmoothness(i);
镜面高光和平滑度值。
场景视图允许我们直接看到平滑度值,所以我们不必使用一个技巧来验证它们。
缓冲区2
第三个G缓冲区包含的是世界空间中的法向量。
它们存储在ARGB2101010纹理的RGB通道中。
这意味着每个坐标都是使用十位进行存储,而不是通常的八位。
这使得它们更加精确。
A通道只有两位-所以总共再次是32位 - 但是它没有被使用,所以我们只需将它设置为1。
法线的编码就像是一个普通的法线贴图。
1
2
3
output.gBuffer1.rgb=specularTint;
output.gBuffer1.a=GetSmoothness(i);
output.gBuffer2=float4(i.normal*0.5+0.5,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格式存储在图形处理器的内存之中。
添加到该缓冲区中的第一个光源是自发光。
没有一个单独的光源来处理这个是事情,所以我们必须在这个渲染通道中对它进行处理。
让我们从我们已经计算出来的颜色开始做这个事情。
1
2
output.gBuffer2=float4(i.normal*0.5+0.5,1);
output.gBuffer3=color;
要预览这个缓冲区,请使用帧调试器,或暂时将此颜色分配给第一个G缓冲区。
有自发光,但是效果是错的。
我们现在使用的颜色完全是按照在方向光源的情况下进行渲染的,这是不正确的。
我们可以通过对延迟渲染通道使用黑色的虚拟光源来消除所有的直接光计算。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
UnityLightCreateLight(Interpolatorsi){
UnityLightlight;
#ifdefined(DEFERRED_PASS)
light.dir=float3(0,1,0);
light.color=0;
#else
#ifdefined(POINT)||defined(POINT_COOKIE)||defined(SPOT)
light.dir=normalize(_WorldSpaceLightPos0.xyz-i.worldPos);
#else
light.dir=_WorldSpaceLightPos0.xyz;
#endif
UNITY_LIGHT_ATTENUATION(attenuation