UE5渲染管线--Lights通道与光照渲染
https://zhuanlan.zhihu.com/p/620914861
26 人赞同了该文章
前言:本篇是继“UE5渲染前数据处理”专栏之后,渲染管线系列之Lights通道篇。
1、Lights是什么
Lights通道主要处理两个任务:利用ShadowPass与BasePass得到的GBuffer,进行阴影与光照的渲染合成。这两项任务都与光源密切相关,而本文也是围绕光源的实现进行展开,接下来简单总结一下:引擎在Editor或Runtime环境,如何处理光源的阴影与光照。
根据移动性分类:

光源类型
- Static Lights(静态光源)
- Stationary Lights(固定光源)
- Movable Lights(可移动光源)
1.1、Static Lights(静态光源)
静态光源不可移动,而且它的光源颜色、强度等属性都不可改变。
该光源阴影与光照都是通过烘焙实现的。所以修改迭代光源需要重新烘焙,但运行时消耗很低。
- 静态物体的直接光照与间接光照,以及阴影都是烘焙到LightMap,然后在运行时在BasePass通道采样LightMap,并将结果存入对应GBuffer通道,在后续LightsPass实现阴影与光照效果。
- 动态物体不产生阴影,而光照则是通过间接光照缓存(ILC)或者体积光照贴图Volume LightMap(VLM),烘焙时在周围放置采样点并计算其3阶球谐函数的系数,然后在运行时通过计算得到相关系数,从而实现光照效果。这些采样点可以通过编辑器的可视化选项进行查看。
1.2、Stationary Lights(固定光源)
固定光源介于静态光源与可移动光源之间,不可移动,大部分属性也不可改变,但是光源颜色与强度是可以改变的。
该光源的间接阴影与间接光照都烘焙到LightMap。而直接阴影则是通过前一章 UE5渲染管线--ShadowPass通道与VSM 分析的ShadowMap,在运行时直接采样。因为ShadowMap只有RGBA 4个通道,所以只能存储4个光源的阴影,如果超过4个,则其他光源的阴影会使用动态阴影渲染。而由于ShadowMap分辨率限制,如果项目开启了距离场(UE5渲染--距离场简析) ,那么远距离渲染阴影时会切换为距离场阴影,得到低消耗的阴影,但阴影会比较模糊。距离场也可以通过编辑器的可视化选项进行查看。
- 静态物体的间接阴影与间接光照都烘焙到LightMap,而直接阴影则通过预先生成的ShadowMap实现。直接光照则是像动态光源一样进行实时渲染。
- 动态物体不能烘焙,对于间接阴影与光照只能通过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里收集排序的。
- SimpleLights:Particle与Niagara系统里,发光材质作为简单光源。简单光源被视为点光源类型处理;
- Clustered:非方向光,非Rect光,无Texture,无阴影或者开启ClusteredShadow,非Lumen等。其中SimpleLights是可以Clustered的;
- UnbatchedLights:不可Clustered的光源;
- 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

- 通过UV计算场景Depth,从而可以得到该Pixel对应的WorldPosition;
- 计算LightData,该结构体记录了某个光源信息,包括类型、颜色、方向、强度因子等属性;
- 从LightAttenuationTexture采样光照衰减;
- 调用GetDynamicLighting,将以上信息混合GBuffer调用IntegrateBxDF方法进行光照计算输出Radiance;
- 最终会走到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,然后三个分支:
- RHI光线追踪,并且有预处理的Shadow,默认没启用;
- 阴影使用光线追踪进行探测是否遮挡,也就是使用RT进行ShadowMap的生成;
- 传统方式,使用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。



996

被折叠的 条评论
为什么被折叠?



