UE4.26 Lightmap从烘焙到渲染

这篇博文主要是UE4 Lightmap的工程逻辑分析,主要帮助想要改Lightmap(TOD)的人快速了解整个流程,不太涉及到数学相关知识。

UE4.26.2移动端使用LightMass烘焙的光照信息。

关于LightMass,知乎上的Jiff大佬的解析非常的详细。

我们重点放在Lightmass烘焙完光照信息后,传回UE4时发生的事情。

首先是量化(Quantize)光照信息,也就是把一个float类型的光照信息,转成uint8(byte)大小的数据,便于之后存放在Lighmap(R8G8B8)中。

这部分代码函数位于项目 UnrealLightmass/Private/Lighting/LightmapData.cpp中:

Image

为了减少量化过程的光照信息损失,UE4采用的特殊的量化函数,其中,移动端的量化函数如下:

Image

Jiff大佬把函数公式和图像都给出来了:

Image

Image

这样,光照图编码时暗部的保留细节更多。

接下来是LightMap的Encode && Saved部分,此时Lighmass光照数据回流到引擎中,进入Engine/Private/Lightmap.cpp:

主要逻辑记录在FLightMap2D::EncodeTextures中:

Image

UE4增加了一个FLightMapPendingTexture结构体来处理这些光照数据,在里面通过StartEncoding来处理Lightmap的编码及压缩:

Image

进入该StartEncoding函数,它包含了所有类型的Lightmap编码函数,比如ShadowMap的编码、SkylightOcclusion的编码、Coefficient的编码。

Image

这下面几个函数非常重要,基本上如果想改UE4的Lightmap编码的话,基本都在这里面改:

Image

以EncodeCoefficientTexture为例走一遍这个流程:

首先在StartEncoding内设置对应的Texture格式:

Image

进入EncodeCoefficientTexture函数,首先注意我下面红框处的代码:

Image

在这里设置了LQ质量的Lightmap使用R8G8B8格式,节省了Alpha通道,相应的,LQ Lightmap因此可以使用DXT1压缩格式,大小比HQ小了一半:

Image

Image

UE4的LightMap上半部分存的是光照颜色,下半部分存的是最大贡献的光照方向,这样,在渲染时,可以用像素法线点积最大贡献的光照方向,近似模拟一个像素级的SH漫反射。

Image

这个USE_LM_DIRECTIONALITY宏在材质球里可以直接通过去勾的方式取消开启,这样UE就用默认的0.6作为SH经验值了。

Image

另外,存数据的时候,UE4用的最原始的操纵内存的方式。233。

首先申请一块足够大小的MipCoverageData0, 然后计算好上半部分和下半部分的内存偏移。

Image

接下来就是分别为RGBA填入数据了:

Image

如果要自定义Lightmap格式,比如把下半部分的最大贡献光照方向去掉,或者在LQ Lightmap的A通道存放AO信息等,基本都在类似的地方修改。

剩下的Lightmap2D::PostEncode部分基本都是些UE相关的处理,基本没有改动的必要。

接下来是Lightmap的指定部分。

使用到Lightmap的只有静态物体:StaticMeshComponent和LandscapeComponent。

首先来康康StaticMeshComponent是怎么指定Lightmap的:

要想缕清这部分的代码逻辑,必须清楚UE4是一个多线程引擎架构,GameThread和RenderThread是分离开的,而为了两者之间的通讯,一般GameThread中的对象,会备份一个Proxy,当GameThread中该对象发生更改时,必须手动通知Renderer更新数据。

以StaticMesh为例,UStaticMeshComponent类为游戏线程中的StaticMeshComponent,相应的,UE4增加了一个FStaticMeshSceneProxy类:

Image

Lightmap指定发生在FStaticMeshSceneProxy的构造函数中:

Image

注意红框标出部分,我们进入该LOD构造函数:

Image

逻辑很清晰,GameThread中的UStaticMeshComponent作为InComponent,传入对应的FMeshMapBuildData。

溯源到UStaticMeshComponent::GetMeshMapBuildData。

Image

其实这里已经可以看到LightScenario的相关逻辑处理了233。

很简单,获取Component的Level,然后获取Level中对应的MapBuildData并返回,当然这里额外处理了LightScenario的逻辑。

此时,返回之前的LOD构造函数,我们看看那几个Set函数,指定了FLightCacheInterface中的LightMap等信息。

Image

注意一点喔,FLODInfo是继承自FLightCacheInterface的喔。(奇怪的继承)

Image

除开StaticMesh会用到Lightmap,Landscape也会用到Lightmap。

Landscape也是同样的套路,使用FLandscapeLCI代理存储在FLandscapeComponentSceneProxy中,注意它同样继承自FLightCacheInterface:

Image

由于LandscapeComponent没有LOD,因此直接设置LigmapData就可以了。

Shader的Binding部分

UE4有一套独特的MeshDrawProcessor处理流程,根据RenderPass需求的资源格式(比如ShadowDepth Pass仅需顶点坐标和Mask数据、BasePass需要大部分数据、PrePass需要顶点和Mask数据等),于是在FMeshPassProcessor中组织好这些Primitive资源,渲染时调用对应Primitives的Draw:

下面以移动端的Forward渲染管线为例:

首先是UE的Forward渲染函数FMobileSceneRenderer::RenderForward:

在渲染完PrePass后进入MobileBasePass:

Image

Image

在RenderMobileBasePass函数中,先更新完场景相机相关的View UBO后就会调用视角中ParallelMeshDrawCommandPasses的DispatchDraw函数。

而View中的ParallelMeshDrawCommandPasses则是在之前FMobileSceneRenderer::SetupMobileBasePassAfterShadowInit函数中Setup填充的。

Image

UE4专门设计了一个全局函数类,利用它的初始化函数往一个全局表FPassProcessorManager::JumpTable里面填每个Pass对应的创建函数:

Image

注册每个MeshPass时,则会经由该构造函数填入,比如MobileBasePass的注册如下:

Image

其中MobileBasePass的注册函数如下:

Image

FMeshPassProcessorRenderState 这个参数非常重要,标定了该Pass需要的一些渲染资源。

其构造函数分别传入一个FSceneView和一个FRHIUniformBuffer,前者用于填充该视口下公用的ViewUniformBuffer,后者则是该Pass独有的UniformBuffer:

对于MobileBasePass,其需求的列表如下:

Image

这里的参数基本可以每帧修改,比如UE4.26.2移动端Forward管线的GTAO,MobileBasePass用了上一帧的AO Mask,该Mask就在这里指定。

这里仅处理了MobileBasePass网格公用的UniformBuffer,但还没有处理每个网格独有的UniformBuffer。

好比说,MobileBasePass里的所有物体渲染时都需要AmbientOcclusionTexture,而静态的StaticMesh渲染时需要传入Lightmap数据,动态的SkeletonMesh不需要Lightmap数据但是需要SH(ILC)数据。

因此,UE用FMeshPassProcessor::Process函数来单独为每一个网格处理这种Shader变体情况。

还是以移动端的MobileBasePass为例:

Image

Image

以Lightmap的指定为例,Process函数参数需要传入ELightMapPolicyType,然后调用GetShaders找到对应的变体:

Image

UE为了节省移动端的性能,在计算移动端点光源的动态光照时,使用的宏分支来控制计算而不是动态分支,因此还需要在CPU侧处理好点光源的数目:

Image

在GetMobileBasePassShaders函数这里才是Lightmap的变体抉择策略:

Image

在GetUniformMobileBasePassShaders则转入了根据材质选择具体的的Shader时候:

Image

注意下方的模板继承关系:

Image

Image

TUniformLightMapPolicy的Shader变体编译函数根据不同的Lightmap策略选择对应的变体编译函数。

我们移动端就会跳到下面这个:

Image

TUniformLightMapPolicy继承自FUniformLightMapPolicy,在类FUniformLightMapPolicy中:

Image

Image

这里的LightmapResourceCluster就是指定的Lightmap数据了。

Image

Image

Image

走到这里,UE已经把RenderPass对应的UBO Parameter和Shader变体都找到了。

最后剩下的就是Lightmap UniformBuffer的Update了。

先前的MoblieBasePass的Shader变体选择完后会调用GetShaders函数,最终会调用到FUniformLightMapPolicy::GetPixelShaderBindings里,在该GetPixelShaderBindings函数中将会调用SetupLCIUniformBuffers获取FLightCacheInterface*对应的FLightmapResourceCluster。

很前面的时候提到FLodInfo和FLandscapeLCI都继承自FLightCacheInterface。经过MeshPassProcessor的逐物体处理时,该接口的FLightmapResourceCluster便可以直接获取调用了。

而FLightCacheInterface里的FLightmapResourceCluster数据,在StaticMesh在渲染线程时就已经指定了,也就是我们一开始分析的那里,调用的SetResourceCluster初始化。

到目前为止,所有的数据都已经设置完毕,渲染正常进行。

Lightmap的解码

咋编码的就咋解码,非常简单:

Image

友情Tip

如果你正在为UE4开发两套Lightmap Lerp融合的Time Of Day方案,却发现但BlendWeight为1时完全还原不了第二套Lightmap的颜色信息,请检查你是否把第二套Lightmap的LightMapScale和LightMapAdd数据都传进来并参与了插值(否则它永远将为第一套的数据)。

Image

另外,如果你是基于LightingScenario开发的,请立刻放弃换别的方法,因为LightScenario没办法做到Streaming加载,并且!!!要非常小心的处理昼夜过渡时的资源加载卸载,非常操蛋。

如果想为UE4移动端添加昼夜变换效果,用一套Lightmap就足够了,透过对比度和亮度调节,简单方便,包体大小友好。

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