UE4 ILC加载与渲染
Posted qiutang
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UE4 ILC加载与渲染相关的知识,希望对你有一定的参考价值。
建议放大120%观看。该篇博文包括:
- Level加载ILC到渲染线程的逻辑分析。
- 渲染线程更新ILC数据的分析。
- GPU、CPU的SH数据通信分析。
ILC是UE4动态物体预烘焙全局光照的一种解决办法,原理简单来说就是在烘焙场景光照时,按照特定规则在场景中摆放一些静态Box,然后光子评估时记录每个Box的受光信息,这些受光信息通过低阶球谐函数编码在MapBuildData中,渲染时,在CPU端根据世界空间坐标,在一个Bounds范围内逐Box插值,并将插值后的值传入GPU中,在Shader中逐像素解码球谐光并添加到间接光照中去。
总结的数据与逻辑关系:
UE4 灯光数据发生变化时(烘焙Lightmap、更改LightScenario等),将会触发UWorld::PropagateLightingScenarioChange()。
在该函数中,将会调用ULevel::InitializeRenderingResources(),重新初始化当前UWorld中每个已加载的子关卡(存放在UWorld::Levels数组中)的烘焙光照数据。
每个ULevel都持有一份FPrecomputedLightVolume数据,里面存储了ILC数据。
红框部分调用FPrecomputedLightVolume::AddToScene函数将PrecomputedLightVolume数据压入渲染线程:
该函数通过GetLevelPrecomputedLightVolumeBuildData(LevelBuildDataId)获取对应Level的ILC数据,接着通过宏ENQUEUE_RENDER_COMMAND做好数据的Binding,最后调用FScene::AddPrecomputedLightVolume注册到渲染场景中。
FScene中使用PrecomputedLightVolumes一维指针数组简单存储每个Level中的ILC数据(它将会在后面的采样插值时用到),并且使用类FIndirectLightingCache来处理ILC的更新、插值等计算。
在函数FIndirectLightingCache::SetLightingCacheDirty中,遍历判断当前FScene中存储的每个几何体是否和压入的Level Precomputed LightVolume(ILC Bounds)相交,然后分别做好脏标记。
此时,从游戏线程压入ILC数据的逻辑处理已完成,接下来是渲染线程的更新。
UE4为多个平台都分别写了一个渲染器,移动端渲染器入口函数为FMobileSceneRenderer::Render,每帧调用。
渲染函数一开始,将会进入InitViews函数,在该函数中处理几何体可见性剔除和ubo更新等。
InitViews函数在可见性剔除计算完后立即进入PostVisibilityFrameSetup函数中:
在PostVisibilityFrameSetup中将会启用FIndirectLightingCache的StartUpdateCachePrimitivesTask填充任务:
FUpdateCachePrimitivesTask为FIndirectLightingCache的TaskGraph代理类,在其DoTask函数中将调用对应的Update函数,更新所有受ILC影响的Primitive:
函数UpdateCachePrimitivesInternal有一些简单的逻辑判断,但最终还是会进入FIndirectLightingCache::UpdateCachePrimitive,根据先前已经设置的ILC脏标记bIsDirty调用FIndirectLightingCache::UpdateCacheAllocation,来更新FScene中每个脏掉的Primitive的ILC。
UpdateCacheAllocation主要用于更新VolumeBlocks中的数据,它的主要逻辑如下:
ILC默认为SH点采样,首先通过FindBlock查找当前Primitive对应的ILC Block:
先介绍VolumeBlocks是如何填充的:
为游戏场景添加几何体时,将会触发FPrimitiveSceneInfo::AddToScene函数。
如果是首次添加,还会触发FIndirectLightingCache::AllocatePrimitive函数:
AllocatePrimitive函数将会调用CreateAllocation初始化ILC Block:
FIndirectLightingCacheBlock为简单的数据存储类。
TexelSize记录了采样盒体的大小:
接下来利用CalculateBlockPositionAndSize函数计算ILC计算所使用的BoundBox大小。
然后稍微缩放一下Bounds:
注意这里会根据原有的Primitive Bounds做一些阶梯函数式的缩放,确保物体的包围盒改变不大时保持ILC结果相对稳定,减少高频移动物体的ILC闪烁。
此时,VolumeBlocks填充完毕。
回到UpdateCacheAllocation,找到当前PrimitiveAllocation对应的 ILC Block后,同样先更新一下ILC Block Bounds,然后填入BlocksToUpdate和TransitionsOverTimeToUpdate中。
FBlockUpdateInfo就一个暂存数据的结构体,里面的Allocation指针指向每个PrimitiveAllocation的FIndirectLightingCacheAllocation。
BlocksToUpdate和TransitionsOverTimeToUpdate将通过引用填入到InitViews函数的ILCTaskData中:
在InitViews一大堆RenderPass Setup设置计算完成后,再用ILCTaskData来一次FinalizeCacheUpdates保证所有ILC计算完毕,同时尽可能多利用多线程的计算优势。
在该函数中Flush ILC的计算任务如下,可以看到先前填充的ILCTaskData将会在这里用于SH的插值计算:
计算主要逻辑位于UpdateBlocks中:
VLM和ILC的插值都混杂在这里,重点观察InterpolatePoint函数:
先前在GameThread填入的PrecomputedLightVolumes在这里派上了用场。
在FPrecomputedLightVolume::InterpolateIncidentRadiancePoint函数中,利用已建好的渲染场景八叉树结构加速空间相交查询,然后根据两点之间的间距插值。
408到410行的注释阐述了UE4的一点优化,ILC采样点在生成时,都会有一个采样半径(权重),该采样半径(权重)将会在这里参与插值计算。
414行应该是4.26.2的新优化,为ILC采样加入了SkyBentNormal的影响,这样理论上可以减少漏光,但我测试好像没啥卵用。
球谐光插值需要注意的细节之一就是减低振铃现象。
这里有个很不错的相关论文:
Deringing Spherical Harmonics - Peter-Pike Sloan
UE4的做法很独特:
注意第959行的注释,他们加了个0.05%的最低亮度,应该是积累的经验数字。
至此,CPU端的ILC插值计算完毕。
GPU SH参数的GPU更新部分:
在MobileRenderer正式渲染开始前,先调用UpdatePrimitiveIndirectLightingCacheBuffers函数更新ILC数据:
在该函数中,逐物体调用UpdateIndirectLightingCacheBuffer函数:
最终会调用到GetIndirectLightingCacheParameters函数:
这里的LightingAllocation指针和上文提到的填充到FBlockUpdateInfo中的Allocation指针的地址相同,它的数据已经在对应的Task中插值更新完了。
如果开启了ILC就填入对应的数据。
接下来调用RHI函数将数据更新到GPU:
FIndirectLightingCacheUniformParameters结构和GPU保持一致的声明:
Shader部分,直接使用解码SH即可:
最后。
以上是关于UE4 ILC加载与渲染的主要内容,如果未能解决你的问题,请参考以下文章
UE4是基于物理的渲染PBR,结合自己理解说一说ue4中啥是材质,啥材质实例?