UE4 Lightmap的解码

前文《UE4 Lightmap格式解析》在述及HQ和LQ两种光照图存储空间时还留有一个小尾巴:UE4做为一个以渲染高质量图像著称的3D引擎,其线性空间的工作流是完全成熟和配套的,为什么反而会选择把HQ高质量的光照颜色存储在GammaSpace而LQ的颜色混亮度却相反的存储在LinearSpace?这个问题的答案留给读者自行思考了。

这篇文章要解决的问题是:当你拥有一张UE4中的Lightmap,在渲染时该如何进行解码。


LQ质量Lightmap解码流程

. 纹理采样

由于LQ Lightmap使用单24位的位图进行存储,且上半部分存储的是Lightmap的颜色和亮度,下半部分存储的是SH参数。故其在使用方向信息时,需要做采样一次颜色信息,再采样一次方向信息共计两次纹理采样,代码如下

half4 Lightmap0 = Texture2DSample( LightmapResourceCluster.LightMapTexture, LightmapResourceCluster.LightMapSampler, LightmapUV0 );	
half4 Lightmap1 = Texture2DSample( LightmapResourceCluster.LightMapTexture, LightmapResourceCluster.LightMapSampler, LightmapUV1 );

. 亮度和光照颜色还原

先把混合的颜色亮度值从0~1还原到编码时的亮度的[min,max]范围内

归一化亮度计算公式为: Lc = Lo * N_mul + N_add ,其中(N_mul = 1 /(max-min),N_add = -min/(max-min)) ,Lc为归一化后的亮度,Lo为归一化前的亮度

所以解码时计算公式为: Lo = (Lc - N_add) / N_mul = Lc * (1.0/N_mul ) + (-N_add/N_mul).

其中 1.0/N_mul -N_add/N_mul是每张光照图所共用的,作为优化,UE4会把这两个参数存储在lightmap的附加信息里,在解码时通过uniform buffer传给Shader而不是每次采样都重新计算,所以还原亮度到归一化之前的代码只有一行,实现如下

half3 LogRGB = Lightmap0.rgb * GetLightmapData(LightmapDataIndex).LightMapScale[0].xyz + GetLightmapData(LightmapDataIndex).LightMapAdd[0].xyz;	

还原编码前的亮度

使用编码函数的反函数进行还原 ,反函数为

代码实现如下

const half LogBlackPoint = 0.00390625;
half L = exp2( LogL * 16 - 8 ) - LogBlackPoint;

计算法线方向的光照贡献

在材质启用了方向贡献时,使用球谐参数计算法线方向上光照图的贡献,如果材质没有启用方向计算,则使用经验常数0.6对光照强度进行削弱。(注:实际测试中启用方向贡献之后,非直接光照到的暗部带有法线的模型在结构细节上表现会比无方向的lightmap要有更多细节)

代码实现如下

#if USE_LM_DIRECTIONALITY		
	float4 SH = Lightmap1 * GetLightmapData(LightmapDataIndex).LightMapScale[1] + GetLightmapData(LightmapDataIndex).LightMapAdd[1];
	half Directionality = max( 0.0, dot( SH, float4(WorldNormal.yzx, 1) ) );	// 1 dot, 1 smax
#else
	half Directionality = 0.6;
#endif

应用方向贡献计算最终的光照图颜色

half Luma = L * Directionality;
half3 Color = LogRGB * (Luma / LogL);

UE4关于LQ Lightmap的解码实现在LightmapCommon.ush的GetLightMapColorLQ函数中实现,官方的这个函数,截止到UE 4.22.2都有一个小的瑕疵,做了一个错误的示范:不管你的材质是否有勾选方向性贡献(Directionality),都会对Lightmap进行两次采样。但实际上未启用材质方向性的时候,仅需要一次光照图采样就够了(虽然这个瑕疵可能在编译时被Shader编译器优化掉)

其完整的实现代码如下

更好的实现是挪一下Lightmap1的采样,当且仅且方向性被启用时才进行Lightmap1的采样


HQ Lightmap解码流程

HQ Lightmap使用的是32位的RGBA8图来存储,解码流程和LQ的解码方式大体上一致,走的 纹理采样 --> 颜色还原到线性空间 -->还原亮度 --> 计算方向贡献 --> 应用到最终光照颜色这几步来完成。

但由于硬件性能的不同,UE4的HQ的Lightmap可能使用VirtualTexture来采样。实现代码如下所示

#if LIGHTMAP_VT_ENABLED
	LightmapUV0 *= float2(1, 2);
#endif

	half4 Lightmap0 = SampleLightmapVT( LightmapUV0 , LightmapResourceCluster.LightMapTexture, LightmapResourceCluster.LightMapSampler, SvPositionXY, VTRequest); 

	uint VTRequest_notused = 0;
	half4 Lightmap1 = SampleLightmapVT(
#if LIGHTMAP_VT_ENABLED
		LightmapUV0, LightmapResourceCluster.LightMapTexture_1, 
#else
		LightmapUV1, LightmapResourceCluster.LightMapTexture,
#endif
		LightmapResourceCluster.LightMapSampler, SvPositionXY, VTRequest_notused); 

还原颜色到编码前

由于颜色编码最后一步转到了gammaspace,在解码的时候同样要先从gammaspace转线性空间,再还原其到[min,max]值域.UE4这两步是在一行代码里完成的,代码如下

half3 UVW = Lightmap0.rgb * Lightmap0.rgb * GetLightmapData(LightmapDataIndex).LightMapScale[0].rgb + GetLightmapData(LightmapDataIndex).LightMapAdd[0].rgb;

回忆一下,HQ Lightmap的亮度部分存在Alpha通道中,其中上半图存的是编码后的整数部分,下半图存的是余数部分,所以它的亮度值还原,第一步是先把整数和余数部分拼起来;第二步和LQ 一样,使用一个mad指令完成[min,max]范围的还原,代码实现如下

half LogL = Lightmap0.w;	
LogL += Lightmap1.w * (1.0 / 255) - (0.5 / 255);

LogL = LogL * GetLightmapData(LightmapDataIndex).LightMapScale[0].w + GetLightmapData(LightmapDataIndex).LightMapAdd[0].w;

亮度还原的最后一步是使用编码函数的反函数对亮度进行解码,反函数为

代码实现如下

const half LogBlackPoint = 0.01858136;
half L = exp2( LogL ) - LogBlackPoint;

方向贡献的计算方式、最终光照图颜色的返回均和LQ的计算一模一样。所不同的是,它为双面植被做了特化,同时由于下半图的alpha通道存储了亮度的小数部分,所以它不管如何,都需要采样光照图两次。

HQ Lightmap解码的实现完整的,可以参见LightmapCommon.ush的GetLightMapColorHQ函数。


UE4 Lightmap特点的一些思考

好处

. 自适应的亮度区间,UE4的编码方式确定了它表可以表达的亮度区间是灵活多变的,相比之下,Unity3D在移动图Lightmap所使用的亮度信息则只有[0~2],网上也看到很多人抱怨或尝试解决过Unity3D Lightmap在移动端变暗的问题,究其本质,还是其所能适应的亮度区间是固定且过小

. 更细致的暗部,由UE4的Lightmap所使用的编码函数凹凸即决定给了暗部更多的细节表达 ,HQ Lightmap的颜色之所以编码在Gammaspace,同样也是基于给暗部更多的细节的考虑而使用,具体的从gamma变换函数的凹凸性可以得出.LinearSpace到GammaSpace的变换为:


不足

. 在不需要Directionality的材质中,浪费了一半的贴图大小用于存储SH参数,去年吃鸡在UOD上的分享里,就明确的说过他们通过修改Lightmap的编码,去掉了方向性这一半的空间浪费

. 由于使用了非线性函数(log 或者指数函数)进行编码,生成后的光照贴图不具备美术手工修图的可能性

. 由于使用了非线性函数进行编码,其性能消耗要远高于诸如Unity3D的Lightmap,也大于RGBM,RGBD等光照图

. 但由于UE4采用的是非线性函数的编码,故其采用双线性采样/三线性等纹理采样的线性过滤方式时,相对那些使用线性编码光照图的3D引擎来说(如UNITY3D),其失真速度更高。出于同样的理由,其在使用ETC/ASTC等硬件支持的纹理压缩模式也更易失真。在出现失真情况的时候,往往需要用更高的单位面积像素比来弥补线性采样时细节损失

发布于 2019-06-15 18:23