渲染13——延迟渲染

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wodownload2/article/details/82744489

研究延迟渲染
填充Gbuffer
支持HDR和LDR
延迟反射
本节是渲染的第13节内容,前一小节介绍的是半透明阴影,本节将学习延迟渲染。
本节使用unity版本为:5.5.0f3
这里写图片描述

1 另外一个渲染路径
到目前为止我们使用都是unity得向前渲染路径。但是这并不是unity唯一支持的渲染方式。unity还支持延迟渲染路径。还有遗留的顶点光照以及遗留的延迟渲染方式。我们并不打算介绍这个。

我们有了延迟渲染路径,但是我们为什么要因为它而烦恼呢?毕竟我们可以使用forward 的渲染方式渲染所有的东西。为了回答这个问题,我们必须知道两者的渲染方式的不同点在哪里。

1.1 切换渲染路径
到底使用的是哪个渲染路径是通过project的graphic设置的,如下所示:
这里写图片描述

这里有三个等级:Low、Medium、High,这是是GPU的质量由低到高的设置。
为了改变渲染路径必须把Use Defaults改为不勾选,然后选择Rendering Path。
这里写图片描述

1.2 比较draw call
我们使用第7节的是阴影场景,来比较两个渲染路径下的draw call。这个场景的ambient intensity 设置为0。因为我们的shader还不支持延迟渲染,改变所有的材质球使用默认的标准shader。

这个场景包含了一些物体以及两个平行光,我们把阴影关掉和不关掉来看看效果:

这里写图片描述

这里写图片描述

当使用向前渲染的时候,查看frame debug如何渲染场景的。

场景里包含了66个物体,所有的都可见。如果动态批处理开启的话,将会少于66个批处理。但是,这个只针对于一个平行光。因为有另外一个平行光,所以,动态批处理不能使用。因为所有的物体都会被画两次,一共有132个批处理,包括天空盒共计133个。
这里写图片描述

当阴影被开启的时候,将会产生更多的批处理,为了产生级联阴影。回忆下平行光阴影产生的方式。第一个,要有深度缓冲,只需要有48个批处理,这个因为有动态批合并。接着,级联阴影的创建。第一盏灯需要111个批处理,第二个灯需要121个批处理。阴影映射贴图被渲染到屏幕坐标缓冲。然后就是物体的绘制,攻击418个批处理。

然后把阴影关闭,切换到延迟渲染。场景看起来一样,处理的MSAA关闭了。这次是怎么绘制的呢?

为什么MSAA不能工作在延迟渲染模式下?
延迟渲染依赖于每个像素的储存的数据。这个是MSAA不兼容。由于MSAA是依赖于多个像素数据。

很明显,GBuffer渲染需要45个批处理。然后深度数据被拷贝,共计55个批处理。

55 比133 少多了。 看起来延迟对所有物体只绘制一次,而不是每个灯一次。而当阴影开启额时候,情况又是怎样的呢?

1.3 分解工作
延迟渲染对于多盏灯的情况下,相较于向前渲染效率要高一些。向前渲染需要额外的通道给每个物体每个灯,延迟渲染不需要。两者都需要渲染阴影映射,但是延迟渲染不需要额外的深度缓冲(为平行光阴影所需要)。延迟渲染是如何做到这个的呢?

为了渲染物体,shader必须抓住网格数据,把它转换到正确的空间,插值计算,检索并验算表面属性,然后计算灯光。向前渲必须对每个物体的每盏灯都重要重复做这样的事情。附加通道比基础通道要耗时小,因为深度缓冲已经被计算,而且不受间接光的影响,但是附加通道依然要做大量的和基础通道相同的工作。

下图示两种通道要干的事情:
这里写图片描述

由于几何信息是相同,所以为什么不缓存它呢?把基础通道的数据缓冲起来,然后附加通道直接使用,避免了重复的工作。我们必须保存每个像素的数据,所以我们需要一个缓冲,它和深度以及帧缓存一样。

这里写图片描述

现在,所有的集合数据都被缓存起来了。只有一个数据被遗漏了,那就是灯光数据。但是我们不用渲染所有的几何体了,我们可以只渲染灯光。除此之外,基础数据只为了缓存数据。所有的平行光都可以延迟到需要时候渲染,因此出来了一个延迟shader。
这里写图片描述

1.4 更多的灯
如果你只有一盏灯,那么延迟渲染不能体现出很好的效率优势。但是如果有多盏灯,那么这个效率提升是很明显的。每一个额外的灯,都只需要很少的额外的工作,只要它没有释放阴影。

同样的,几何体和灯光是独立渲染的,一个物体受多少盏灯影响是没有限制的。所有的灯都是像素灯,影响它周围的物体,在Pixel Light Count设置的数字也不用关心了。

1.5 渲染灯
灯光他们是怎么渲染的呢?因为平行光影响所有的物体,他们被渲染到一个覆盖全屏的方块中。

这个方块使用的是Internal-DeferredShading。它的像素着色器获取所有的几何体数据。然后计算灯光信息。
聚光灯也类似,但是它不是覆盖全屏的。它渲染处理的是一个金字塔的椎体形状。所以只有可见的区域才会被渲染,如果是不可见的,那么没有shader会被渲染。
如果一个像素在金字塔中,那么将执行光照计算。在椎体的后面的物体是不需要渲染的,所以为了不渲染这些不需要的像素,金字塔首先使用Internal-StencilWrite shader来渲染。这个通道写入到模板缓冲,然后作为一个遮罩供后面的渲染使用。

点光源使用类似的方法,唯一不同的是,它的形状不是金字塔而是都变形球体。

1.6 灯光范围
如果你打开frame debugger,你会注意到延迟渲染的灯光阶段的颜色看起来很诡异。看起来像是反转过来的,在最后的阶段颜色才是正确的。

unity默认是使用LDR模式渲染颜色的。这种方式,颜色是存储在ARGB32贴图中。 unity把颜色按照指数形式编码,为了得到一个更广的颜色区间,然后再延迟阶段转换为正常的颜色。

如果是使用HDR模式,那么unity会把颜色以ARGBHalf格式存储。这种情况,特殊的编码将不再需要,也没有最后渲染阶段处理。HDR是否使用,可以在摄像机参数中配置。

1.7 几何缓冲
缓冲数据的弊端是要在某个地方进行存储。延迟渲染路径使用多个贴图存储数据。这写贴图就是通常提到的几何缓冲,也叫g-buffer。

延迟渲染需要4个g-buffer。他们的大小对于LDR是160位,而对于HDR是192位。这个比单个32位的帧缓存要大的多。当前台式GPU可以支持到。而手机、平板的GPU在高分辨率下可能会遇到些问题。

你可以从Scene视口中查看些g-buff的信息。
这里写图片描述

你还可以查看frame debugger中的渲染阶段对应的贴图:
这里写图片描述

1.8 混合渲染模式
我们的shader目前并不支持延迟渲染路径。那么如果场景中的有些物体使用我们的shader,而且是延迟渲染模式下会如何呢?
这里写图片描述
这里写图片描述

这里写图片描述

从渲染的结果来,延迟先做,然后是向前渲染阶段。在延迟阶段,向前物体不会被渲染。唯一例外的是平行光阴影阶段,在那个阶段,向前物体需要深度通道。这个直接在g-buffer之后被填充。不好的地方是,向前物体,在环境缓冲中的颜色为黑色。
这个对透明物体也是这样的,他们需要一个独立的向前渲染阶段。

2 填充g-buffer
现在我们知道延迟渲染的原理了,所以修改shader,总加要给通道,其渲染模式为deffered。 其他通道不变,把此通道放在附加通道和阴影通道之间。


Pass {
			Tags {
				"LightMode" = "Deferred"
			}
		}

当unity侦测打我们的shader中有延迟通道,它会把不透明物体、剔除物体都包含在延迟阶段。透明物体还是在透明阶段。

由于目前这个通道还是空的,所有的物体都会被渲染为白色。我们要添加shader属性以及程序。延迟通道的代码和base 通道的代码类似,所以拷贝过来即可,然后做一点改动:
首先是关键字改变:FORWARD_BASE_PASS 替换为 DEFERRED_PASS
然后是延迟通道不需要:_RENDERING_FADE 和 _RENDERING_TRANSPARENT
最后,延迟阶段只有当gpu支持多写入目标情况才能使用,所以我们要如多重编译排除不支持的情况。

Pass {
			Tags {
				"LightMode" = "Deferred"
			}

			CGPROGRAM

			#pragma target 3.0
			#pragma exclude_renderers nomrt

			#pragma shader_feature _ _RENDERING_CUTOUT
			#pragma shader_feature _METALLIC_MAP
			#pragma shader_feature _ _SMOOTHNESS_ALBEDO _SMOOTHNESS_METALLIC
			#pragma shader_feature _NORMAL_MAP
			#pragma shader_feature _OCCLUSION_MAP
			#pragma shader_feature _EMISSION_MAP
			#pragma shader_feature _DETAIL_MASK
			#pragma shader_feature _DETAIL_ALBEDO_MAP
			#pragma shader_feature _DETAIL_NORMAL_MAP

			#pragma vertex MyVertexProgram
			#pragma fragment MyFragmentProgram

			#define DEFERRED_PASS

			#include "My Lighting.cginc"

			ENDCG
		}

现在延迟阶段大体上像基础通道,但是它把最终的渲染结果写入到了g-buffer,这是不正确的,我们需要输出几何数据而不包含灯光数据。

2.1 四个输出
在灯光shader中,要支持两种像素着色器的计算。延迟通道需要四个输出。其他模式下需要一个输出。所以我们定义一个输出结构:

struct FragmentOutput {
	#if defined(DEFERRED_PASS)
		float4 gBuffer0 : SV_Target0;
		float4 gBuffer1 : SV_Target1;
		float4 gBuffer2 : SV_Target2;
		float4 gBuffer3 : SV_Target3;
	#else
		float4 color : SV_Target;
	#endif
};

修改像素着色器的代码:

FragmentOutput MyFragmentProgram (Interpolators i) {
	…

	FragmentOutput output;
	#if defined(DEFERRED_PASS)
	#else
		output.color = color;
	#endif
	return output;
}

2.2 缓冲0
第一个g-buffer存储的是漫反射以及表面透明度。它是ARGB32格式的贴图,漫反射存储在RGB通道,排除画在A通道。可以单独提取一个函数:

	#if defined(DEFERRED_PASS)
		output.gBuffer0.rgb = albedo;
		output.gBuffer0.a = GetOcclusion(i);
	#else

你可使用frame debugger查看g-buffer的填充效果。

2.3 缓冲1
第二个g-buffer存储的是镜面反射的RGB通道,光滑度存储在A通道。它同样也是ARGB32格式贴图。我们单独提出计算镜面方法:

output.gBuffer0.rgb = albedo;
output.gBuffer0.a = GetOcclusion(i);
output.gBuffer1.rgb = specularTint;
output.gBuffer1.a = GetSmoothness(i);

2.4 缓冲2
第三个缓冲存储的是世界法线向量,它存储在RGB通道,考虑精度依然采用32位存储。

output.gBuffer1.rgb = specularTint;
output.gBuffer1.a = GetSmoothness(i);
output.gBuffer2 = float4(i.normal * 0.5 + 0.5, 1);

2.5 缓冲3
第四个缓冲是用来计算场景中灯光。它的格式依赖于摄像机使用的是LDR还是HDR,如果使用的是LDR,它的就以ARGB2101010 格式存储。如果是HDR,则使用的是ARGBHalf格式存储,每个通道占16位,共计64位。所以使用HDR的是其缓冲是其他缓冲的两倍。只有RGB被使用到,A通道没有使用全部置为1。

第一个灯光被加到缓存的是自发光。这里没有一个单独的自发光灯,所以我们自己加入通道,使用的数据是已经计算出来的颜色值。

output.gBuffer2 = float4(i.normal * 0.5 + 0.5, 1);
output.gBuffer3 = color; //直接赋上一个颜色。

我们要把平行光光照效果剔除掉,直接用以一个黑色代替。

UnityLight CreateLight (Interpolators i) {
	UnityLight light;

	#if defined(DEFERRED_PASS)
		light.dir = float3(0, 1, 0);
		light.color = 0;
	#else
		#if defined(POINT) || defined(POINT_COOKIE) || defined(SPOT)
			light.dir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos);
		#else
			light.dir = _WorldSpaceLightPos0.xyz;
		#endif

		UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
		
		light.color = _LightColor0.rgb * attenuation;
	#endif
	//light.ndotl = DotClamped(i.normal, light.dir);
	return light;
}

我们还要去掉DotClamped去掉,因为这个函数unity已经认为是过时的了。
我们去掉了平行光的效果,但是要包括自发光。所以要在自发光计算函数中加入宏:

float3 GetEmission (Interpolators i) {
	#if defined(FORWARD_BASE_PASS) || defined(DEFERRED_PASS)
		…
	#else
		return 0;
	#endif
}

2.6 环境光
效果看起来不错,但是还不够完整,我们丢掉了环境光。
环境光和自发光一样,没有单独的通道。所以也必须自己手动的叠加上去,我们在计算间接光的时候加上去。

UnityIndirect CreateIndirectLight (Interpolators i, float3 viewDir) {
	…

	#if defined(FORWARD_BASE_PASS) || defined(DEFERRED_PASS)
		…
	#endif

	return indirectLight;
}

猜你喜欢

转载自blog.csdn.net/wodownload2/article/details/82744489
今日推荐