UE5渲染管线--Lights通道与光照渲染

UE5渲染管线--Lights通道与光照渲染
https://zhuanlan.zhihu.com/p/620914861

26 人赞同了该文章

前言:本篇是继“UE5渲染前数据处理”专栏之后,渲染管线系列之Lights通道篇。

1、Lights是什么

Lights通道主要处理两个任务:利用ShadowPass与BasePass得到的GBuffer,进行阴影与光照的渲染合成。这两项任务都与光源密切相关,而本文也是围绕光源的实现进行展开,接下来简单总结一下:引擎在Editor或Runtime环境,如何处理光源的阴影与光照。

根据移动性分类:

光源类型

  1. Static Lights(静态光源)
  2. Stationary Lights(固定光源)
  3. Movable Lights(可移动光源)

1.1、Static Lights(静态光源)

静态光源不可移动,而且它的光源颜色、强度等属性都不可改变。

该光源阴影与光照都是通过烘焙实现的。所以修改迭代光源需要重新烘焙,但运行时消耗很低。

  1. 静态物体的直接光照与间接光照,以及阴影都是烘焙到LightMap,然后在运行时在BasePass通道采样LightMap,并将结果存入对应GBuffer通道,在后续LightsPass实现阴影与光照效果。
  2. 动态物体不产生阴影,而光照则是通过间接光照缓存(ILC)或者体积光照贴图Volume LightMap(VLM),烘焙时在周围放置采样点并计算其3阶球谐函数的系数,然后在运行时通过计算得到相关系数,从而实现光照效果。这些采样点可以通过编辑器的可视化选项进行查看。

1.2、Stationary Lights(固定光源)

固定光源介于静态光源与可移动光源之间,不可移动,大部分属性也不可改变,但是光源颜色与强度是可以改变的。

该光源的间接阴影与间接光照都烘焙到LightMap。而直接阴影则是通过前一章 UE5渲染管线--ShadowPass通道与VSM 分析的ShadowMap,在运行时直接采样。因为ShadowMap只有RGBA 4个通道,所以只能存储4个光源的阴影,如果超过4个,则其他光源的阴影会使用动态阴影渲染。而由于ShadowMap分辨率限制,如果项目开启了距离场(UE5渲染--距离场简析) ,那么远距离渲染阴影时会切换为距离场阴影,得到低消耗的阴影,但阴影会比较模糊。距离场也可以通过编辑器的可视化选项进行查看。

  1. 静态物体的间接阴影与间接光照都烘焙到LightMap,而直接阴影则通过预先生成的ShadowMap实现。直接光照则是像动态光源一样进行实时渲染。
  2. 动态物体不能烘焙,对于间接阴影与光照只能通过GI实现,如Lumen;而直接光照则是像动态光源一样进行实时渲染。

1.3、Movable Lights(可移动光源)

可移动光源的属性都可以在运行时修改,因此消耗最高灵活性也最好。因为属性都可改变,所以无法烘焙,一切都是运行时动态计算。

该光源的间接阴影与光照可通过GI(如Lumen)实现。阴影通过ShadowMap或者VSM进行动态投影来生成,参考上一章(UE5渲染管线--ShadowPass通道与VSM) 。而直接光照也是动态生成。

另外,当 点光源 或 聚光源 没有移动时,该光源对应的ShadowMap没有改动时,则可以缓存到下一帧使用。

有了以上了解,开始进入实现上的分析,包括阴影与光照。入口函数:

void Render(FRDGBuilder& GraphBuilder)
{
	void FDeferredShadingSceneRenderer::RenderLights(/**/)
	{
		// SortedLights光源列表按顺序存储:[SimpleLights,Clustered,UnbatchedLights,LumenLights]
		RDG_EVENT_SCOPE(GraphBuilder, "Lights");
		
		{
			// 处理可Batched光源
			RDG_EVENT_SCOPE(GraphBuilder, "BatchedLights");
			bool bRenderSimpleLightsStandardDeferred = SortedLightSet.SimpleLights.InstanceData.Num() > 0;

			// True if the clustered shading is enabled and the feature level is there, and that the light grid had lights injected.
			if (ShouldUseClusteredDeferredShading() && AreLightsInLightGrid())
			{
				// Tell the trad. deferred that the clustered deferred capable lights are taken care of.
				// This includes the simple lights
				StandardDeferredStart = SortedLightSet.ClusteredSupportedEnd;
				// Tell the trad. deferred that the simple lights are spoken for.
				// 简单光源也是走Cluster
				bRenderSimpleLightsStandardDeferred = false;
				// 渲染光源列表里可Cluster的光源
				AddClusteredDeferredShadingPass(GraphBuilder, SceneTextures, SortedLightSet, ShadowSceneRenderer->VirtualShadowMapMaskBits, ShadowSceneRenderer->VirtualShadowMapMaskBitsHairStrands);
			}

			if (bRenderSimpleLightsStandardDeferred)
			{
				// 渲染光源列表里的SimpleLights
				RenderSimpleLightsStandardDeferred(GraphBuilder, SceneTextures, SortedLightSet.SimpleLights);
			}
			// 渲染剩下可batched的光源,这步之后就只剩下UnbatchedLight
			for (int32 LightIndex = StandardDeferredStart; LightIndex < UnbatchedLightStart; LightIndex++)
			{
				// Render the light to the scene color buffer, using a 1x1 white texture as input
				const FLightSceneInfo* LightSceneInfo = SortedLights[LightIndex].LightSceneInfo;
				RenderLight(GraphBuilder, Scene, View, SceneTextures, LightSceneInfo, nullptr, LightingChannelsTexture, false /*bRenderOverlap*/, false /*bCloudShadow*/);
			}

		}
		
		{
			// 处理UnBatched光源
			RDG_EVENT_SCOPE(GraphBuilder, "UnbatchedLights");
			// 调试用的flag,可以禁用直接光照,只应用间接光照
			const bool bDirectLighting = ViewFamily.EngineShowFlags.DirectLighting;
			
			// Draw shadowed and light function lights
			// 遍历UnbatchedLights,进行阴影与光照渲染,而剩下的LumenLight有单独的逻辑,以后分析
			for (int32 LightIndex = UnbatchedLightStart; LightIndex < LumenLightStart; LightIndex++)
			{
				// 该光源是否渲染阴影
				const bool bDrawShadows = SortedLightInfo.SortKey.Fields.bShadowed;
				if (bDrawShadows) {...}
				
				// Render light function to the attenuation buffer.
				// 光照函数,注意这里的bDirectLighting是直接光照的意思,而非方向光含义
				if (bDirectLighting) {...}
				
				// Render the light to the scene color buffer, conditionally using the attenuation buffer or a 1x1 white texture as input 
				// 把关照结果混合到SceneColor
				if (bDirectLighting) {...}
			}
		}
	}
}

以上代码添加了关键注释,主要对光源列表SortedLights的光源进行处理,而这个列表按顺序[SimpleLights, Clustered, UnbatchedLights, LumenLights]存放了所有的光源。这个列表是在方法FSceneRenderer::GatherAndSortLights里收集排序的。

  1. SimpleLights:Particle与Niagara系统里,发光材质作为简单光源。简单光源被视为点光源类型处理;
  2. Clustered:非方向光,非Rect光,无Texture,无阴影或者开启ClusteredShadow,非Lumen等。其中SimpleLights是可以Clustered的;
  3. UnbatchedLights:不可Clustered的光源;
  4. LumenLights:Lumen相关的照明。

Clustered Lights的条件

分析了光源列表,现在总结一下RenderLights流程:

首先处理BatchedLights,也就是SimpleLights+Clustered,因为SimpleLights是可以Clustered的,所以如果有Clustered,那么SimpleLights就和Clustered一起合批Draw。否则就单独逐光源处理SimpleLights。

然后逐光源处理UnbatchedLights,进行阴影与光照的渲染。

接下来分别分析SimpleLights、Clustered、UnbatchedLights的渲染。

2、SimpleLights的实现

SimpleLights使用additive 混合模式进行ScreenColor(RT)输出,入口函数:FDeferredShadingSceneRenderer::RenderSimpleLightsStandardDeferred。

使用的Shader变体

IMPLEMENT_GLOBAL_SHADER(FDeferredLightVS, "/Engine/Private/DeferredLightVertexShaders.usf", "VertexMain", SF_Vertex);
IMPLEMENT_GLOBAL_SHADER(FDeferredLightPS, "/Engine/Private/DeferredLightPixelShaders.usf", "DeferredLightPixelMain", SF_Pixel);

VertexShader

实现了SpotLight(Cone) 和 PointLight(Sphere) 范围内顶点的Position转换,在PixelShader通过 该点所处光源的空间的位置 来计算光照的衰减。

PixelShader

  1. 通过UV计算场景Depth,从而可以得到该Pixel对应的WorldPosition;
  2. 计算LightData,该结构体记录了某个光源信息,包括类型、颜色、方向、强度因子等属性;
  3. 从LightAttenuationTexture采样光照衰减;
  4. 调用GetDynamicLighting,将以上信息混合GBuffer调用IntegrateBxDF方法进行光照计算输出Radiance;
  5. 最终会走到ShadingModels.ush里的IntegrateBxDF方法,根据ShadingModelID进行不同的光照。

ShadingModels.ush

3、Clustered的实现

入口函数:FDeferredShadingSceneRenderer::AddClusteredDeferredShadingPass

IMPLEMENT_GLOBAL_SHADER(FScreenPassVS, "/Engine/Private/ScreenPass.usf", "ScreenPassVS", SF_Vertex);
IMPLEMENT_GLOBAL_SHADER(FClusteredShadingVS, "/Engine/Private/ClusteredDeferredShadingVertexShader.usf", "ClusteredShadingVertexShader", SF_Vertex);
IMPLEMENT_GLOBAL_SHADER(FClusteredShadingPS, "/Engine/Private/ClusteredDeferredShadingPixelShader.usf", "ClusteredShadingPixelShader", SF_Pixel);

注意FClusteredShadingVS只用于hair strands lighting,而其他情况则使用FScreenPassVS:画一个全屏的Quad,进行简单的顶点转换,这里就不赘述了。

PixelShader入口:ClusteredShadingPixelShader:

void ClusteredShadingPixelShader(
	float2 ScreenUV : TEXCOORD0,
	float3 ScreenVector : TEXCOORD1,
	in float4 SvPosition : SV_Position,
	out float4 OutColor : SV_Target0
#if STRATA_OPAQUE_ROUGH_REFRACTION_ENABLED
	, out float3 OutOpaqueRoughRefractionSceneColor : SV_Target1
	, out float3 OutSubSurfaceSceneColor : SV_Target2
#endif
	)
{
	// 获取对应Grid数据
	const uint GridIndex = ComputeLightGridCellIndex(uint2(LocalPosition.x, LocalPosition.y), SceneDepth);
	const FCulledLightsGridData CulledLightGridData = GetCulledLightsGrid(GridIndex, EyeIndex);

	// 遍历该Grid所有的Simple lights,进行着色
	CompositedLighting += GetSimpleLightLighting(StrataAddressing, StrataPixelHeader, OpaqueRoughRefractionSceneColor, SubSurfaceSceneColor,
		CulledLightGridData, TranslatedWorldPosition, CameraVector, EyeIndex, FirstNonSimpleLightIndex);

	// 遍历该Grid所有的Regular lights,进行着色
	CompositedLighting += GetLightGridLocalLighting(StrataAddressing, StrataPixelHeader, OpaqueRoughRefractionSceneColor, SubSurfaceSceneColor,
		CulledLightGridData, TranslatedWorldPosition, CameraVector, ScreenUV, EyeIndex, Dither, FirstNonSimpleLightIndex);
	OutColor = CompositedLighting;
}

Clustered之后,如果还有光源不能注入到Grid,那么将逐光源进行 non-shadowed non-light function 渲染,直到UnbatchedLights部分,然后进入下一阶段渲染。

4、UnbatchedLights的实现

遍历所有UnbatchedLights,逐光源进行 shadow、attenuation buffer、screen color三步渲染。

将阴影部分展开:

首先对阴影进行合批batching,然后三个分支:

  1. RHI光线追踪,并且有预处理的Shadow,默认没启用;
  2. 阴影使用光线追踪进行探测是否遮挡,也就是使用RT进行ShadowMap的生成;
  3. 传统方式,使用ShadowDepth,VSM也属于该类型。

上一篇文章分析的ShadowPass生成的VSM,就是在这里的第三个分支处理,将阴影渲染到ScreenShadowMaskTexture。

然后,LightFunction是指有Material的光源,比如将某张贴图像一个光源一样投射出去。那么,如果是LightFunction,则处理attenuation buffer。

最后,调用RenderLight,将该光源的阴影与光照渲染到ScreenColor,即RT[0]。该阶段使用的Shader和SimpleLights一样,是FDeferredLightVS与FDeferredLightPS。Shader存在光源类型Radial还是Directional变体。

ScreenColor

总结:分析了引擎对三种类型的光源,在烘焙与实时渲染上,各自的实现方案。其中烘焙的数据有些存储在Lightmap有些存储在ShadowMap。对于Lightmap,在BasePass阶段,会将其信息输出到GBuffer的Color、ShadowFactor等属性上。对于ShadowMap可以在渲染时,直接采样,避免了每帧动态生成ShadowMap。然后,接着分析了引擎对于光源列表的管理策略,分析了SimpleLights、ClusteredLights、UnbatchedLights的渲染流程。经过Lights通道之后,屏幕已经可以输出原始的ScreenColor。但由于画面还没有经过任何后处理,所以看起来很粗糙。这也说明后处理很强大很重要,以后有时间分析一下。

下一篇打算分析一下全局光照Lumen。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值