UE4.26 ILC加载与渲染

建议放大120%观看。这篇博文主要是UE4 ILC的工程逻辑分析,主要帮助想要改ILC(TOD)的人快速了解整个流程,不太涉及到数学相关知识。 该篇博文包括:

  1. Level加载ILC到渲染线程的逻辑分析。
  2. 渲染线程更新ILC数据的分析。
  3. GPU、CPU的SH数据通信分析。

ILC是UE4动态物体预烘焙全局光照的一种解决办法,原理简单来说就是在烘焙场景光照时,按照特定规则在场景中摆放一些静态Box,然后光子评估时记录每个Box的受光信息,这些受光信息通过低阶球谐函数编码在MapBuildData中,渲染时,在CPU端根据世界空间坐标,在一个Bounds范围内逐Box插值,并将插值后的值传入GPU中,在Shader中逐像素解码球谐光并添加到间接光照中去。

Image

总结的数据与逻辑关系:

Image

UE4 灯光数据发生变化时(烘焙Lightmap、更改LightScenario等),将会触发UWorld::PropagateLightingScenarioChange()。

Image

在该函数中,将会调用ULevel::InitializeRenderingResources(),重新初始化当前UWorld中每个已加载的子关卡(存放在UWorld::Levels数组中)的烘焙光照数据。

Image

每个ULevel都持有一份FPrecomputedLightVolume数据,里面存储了ILC数据。

红框部分调用FPrecomputedLightVolume::AddToScene函数将PrecomputedLightVolume数据压入渲染线程:

Image

该函数通过GetLevelPrecomputedLightVolumeBuildData(LevelBuildDataId)获取对应Level的ILC数据,接着通过宏ENQUEUE_RENDER_COMMAND做好数据的Binding,最后调用FScene::AddPrecomputedLightVolume注册到渲染场景中。

Image

FScene中使用PrecomputedLightVolumes一维指针数组简单存储每个Level中的ILC数据(它将会在后面的采样插值时用到),并且使用类FIndirectLightingCache来处理ILC的更新、插值等计算。

在函数FIndirectLightingCache::SetLightingCacheDirty中,遍历判断当前FScene中存储的每个几何体是否和压入的Level Precomputed LightVolume(ILC Bounds)相交,然后分别做好脏标记。

Image

此时,从游戏线程压入ILC数据的逻辑处理已完成,接下来是渲染线程的更新。

UE4为多个平台都分别写了一个渲染器,移动端渲染器入口函数为FMobileSceneRenderer::Render,每帧调用。

Image

渲染函数一开始,将会进入InitViews函数,在该函数中处理几何体可见性剔除和ubo更新等。

Image

InitViews函数在可见性剔除计算完后立即进入PostVisibilityFrameSetup函数中:

Image

在PostVisibilityFrameSetup中将会启用FIndirectLightingCache的StartUpdateCachePrimitivesTask填充任务:

Image

FUpdateCachePrimitivesTask为FIndirectLightingCache的TaskGraph代理类,在其DoTask函数中将调用对应的Update函数,更新所有受ILC影响的Primitive:

Image

Image

函数UpdateCachePrimitivesInternal有一些简单的逻辑判断,但最终还是会进入FIndirectLightingCache::UpdateCachePrimitive,根据先前已经设置的ILC脏标记bIsDirty调用FIndirectLightingCache::UpdateCacheAllocation,来更新FScene中每个脏掉的Primitive的ILC。

Image

UpdateCacheAllocation主要用于更新VolumeBlocks中的数据,它的主要逻辑如下:

Image

ILC默认为SH点采样,首先通过FindBlock查找当前Primitive对应的ILC Block:

Image

先介绍VolumeBlocks是如何填充的:

为游戏场景添加几何体时,将会触发FPrimitiveSceneInfo::AddToScene函数。

如果是首次添加,还会触发FIndirectLightingCache::AllocatePrimitive函数:

Image

AllocatePrimitive函数将会调用CreateAllocation初始化ILC Block:

Image

FIndirectLightingCacheBlock为简单的数据存储类。

Image

TexelSize记录了采样盒体的大小:

Image

接下来利用CalculateBlockPositionAndSize函数计算ILC计算所使用的BoundBox大小。

Image

然后稍微缩放一下Bounds:

Image

注意这里会根据原有的Primitive Bounds做一些阶梯函数式的缩放,确保物体的包围盒改变不大时保持ILC结果相对稳定,减少高频移动物体的ILC闪烁。

Image

此时,VolumeBlocks填充完毕。

回到UpdateCacheAllocation,找到当前PrimitiveAllocation对应的 ILC Block后,同样先更新一下ILC Block Bounds,然后填入BlocksToUpdate和TransitionsOverTimeToUpdate中。

Image

Image

FBlockUpdateInfo就一个暂存数据的结构体,里面的Allocation指针指向每个PrimitiveAllocation的FIndirectLightingCacheAllocation。

BlocksToUpdate和TransitionsOverTimeToUpdate将通过引用填入到InitViews函数的ILCTaskData中:

Image

在InitViews一大堆RenderPass Setup设置计算完成后,再用ILCTaskData来一次FinalizeCacheUpdates保证所有ILC计算完毕,同时尽可能多利用多线程的计算优势。

Image

在该函数中Flush ILC的计算任务如下,可以看到先前填充的ILCTaskData将会在这里用于SH的插值计算:

Image

Image

计算主要逻辑位于UpdateBlocks中:

Image

Image

VLM和ILC的插值都混杂在这里,重点观察InterpolatePoint函数:

Image

先前在GameThread填入的PrecomputedLightVolumes在这里派上了用场。

Image

在FPrecomputedLightVolume::InterpolateIncidentRadiancePoint函数中,利用已建好的渲染场景八叉树结构加速空间相交查询,然后根据两点之间的间距插值。

408到410行:ILC采样点在生成时,都会有一个采样半径(权重),该采样半径(权重)将会在这里参与插值计算。

414行:应该是4.26.2的新优化,为ILC采样加入了SkyBentNormal的影响,这样理论上可以减少漏光,但我测试好像没啥卵用。

球谐光插值需要注意的细节之一就是减低振铃现象。

这里有个很不错的相关论文(其实是):

Deringing Spherical Harmonics - Peter-Pike Sloan

UE4的做法很独特:

Image

Image

注意第959行的注释。

至此,CPU端的ILC插值计算完毕。

GPU SH参数的GPU更新部分:

在MobileRenderer正式渲染开始前,先调用UpdatePrimitiveIndirectLightingCacheBuffers函数更新ILC数据:

Image

在该函数中,逐物体调用UpdateIndirectLightingCacheBuffer函数:

Image

最终会调用到GetIndirectLightingCacheParameters函数:

这里的LightingAllocation指针和上文提到的填充到FBlockUpdateInfo中的Allocation指针的地址相同,它的数据已经在对应的Task中插值更新完了。

Image

如果开启了ILC就填入对应的数据。

接下来调用RHI函数将数据更新到GPU:

Image

Image

FIndirectLightingCacheUniformParameters结构和GPU保持一致的声明:

Image

Shader部分,直接使用解码SH即可:

Image

最后。

友情Tip

如果你正在为UE4开发两套ILC Lerp融合的Time Of Day方案,同情……技术上确实可行,但是根本没有落实到项目中的可能性,首先看看上方的流程就知道ILC的更新逻辑有多复杂,并且UE4根本没打算支持两套的插值,所以整套逻辑都是线性的,不好扩展,当你千辛万苦的加入另一套PreComputeLightingVolume的插值,你会发现,Cpu的运算压力和机器内存压力都会急剧上升,移动端性能本来就吃紧,这做法完全没有必要,项目评估时就会毙掉,然后你的杰作就放在那里吃尘了。

© - 2024 · 月光下的旅行。 禁止转载