平时在工作中少不了遇到光照贴图相关的表现问题,为了做到知其然知其所以然,就兴起了探索UE光照贴图编码方案的想法。
UE的Lightmap编码实现细节,Jiff的知乎文章UE4 Lightmap格式解析与UE4 Lightmap的解码有比较详细的介绍,总的来说,UE基于适配的需要,分别实现了两套Lightmap方案(分别用LQ-低质量与HQ-高质量表示)。
LQ的Lightmap格式为24位的RGB8,HQ的Lightmap格式为32位的RGBA8。无论是LQ还是HQ,每套Lightmap数据都可以分成两个部分:存储于Lightmap上半部分的Color&Lum数据以及存储于Lightmap下半部分的LightDirection信息。且为了提升显示精度,还会通过一套归一化算法对Lightmap数据进行偏移与缩放。
总的来说,Jiff的文章已经将UE的Lightmap方案介绍的比较清楚了,不过这里还有一些细节与问题在其中并没有给出对应的解释,下面我们尝试对这些问题来做一些解答,其中部分内容为基于部分事实给出的推测,如存在有误之处烦请不吝指出,万分感激。
对了,整个编码流程在LightmapData.cpp文件的QuantizeLightSamples接口中都可以找到。
关于LQ
首先看下LQ下的亮度编码方式:
其中为L原始亮度,而LogL则是压缩后的亮度值,整个Lightmap编码过程可以分成如下几个步骤,我们来对这几个步骤进行拆解分析:
-
首先在烘焙的时候根据当前像素的Color计算出对应的亮度。
- 这一步没有什么可说的,按照公式直接计算即可。得到亮度之后接着会将颜色按照亮度进行缩放,这里为了避免亮度为0产生的奇异点,为先判定L的大小再决定后续处理
-
根据上面的亮度编码方式,将L转换为LogL
-
这个公式是如何推导出来的?可以做如下推测:
-
首先需要一个Log函数对亮度进行压缩
-
其次,为了避免Log函数在自变量为0处的undefined behavior,就需要添加一个常量c
-
再者,需要确保输入亮度为0时,编码后的亮度也为0(为什么有这种需求?推测大概是因为Lightmap中大多是低亮度的数值,即暗部信息比较丰富,而0点附近的浮点数精度是最高的,因此为了尽可能的提高精度,最好做这样的一重偏移映射,出于同样的考虑,常量c不能过大,否则会对导致暗部数据精度的丢失)
-
最后,大部分场景的亮度范围不超过256(经验数值,怎么得来的?实际上,间接光亮度应该在500左右,这里取256,是因为想更多的兼顾暗部信息?),这里希望将这个数值正好编码成1.0(因为贴图采样结果基本上都会塞入到[0, 1]范围,如果此处编码数值过大,后面颜色数据就需要做进一步压缩,精度反而有损,参考)
-
从上面两个结论可以看到,我们将L从[0, 256]压缩到了[0, 1],那么其中的精度应该为1/256,这个数值应该与c相匹配,否则也会有精度的损失。
-
总结起来,编码公式应该具有如下形式:,将上述的几个取值代入可以得到:
-
-
-
同步对Color进行相应的编码,即除以L乘上LogL
- 亮度从颜色中得到,亮度压缩,颜色数值也需要做同步压缩
-
统计编码后的Color的Min/Max信息,出于精度的考虑,对其进行Offset&Scale处理
- 虽然有了压缩,依然无法保证最终的颜色结果落在[0, 1]范围之内,为了避免硬件裁切,通过软件对其进行缩放将之塞入[0, 1]范围。
关于HQ
这里的编码过程跟LQ基本一致,除了以下几点存在区别,下面逐一进行说明:
-
编码方程的不同
-
HQ编码方程中的常量c跟LQ方程中的c不同,且没有前面的系数与偏移,那么为什么会有这样的区别呢?为什么不再需要将LogL映射到[0, 1]范围了呢?这是因为在HQ模式下,Color数据不再需要乘以LogL进行归一化了,且LogL后面也会根据局部数据Min/Max进行Offset&Scale,因此此处的归一化就完全不必要了(顺带提一句,为了进一步提升亮度表示的精度,还会将归一化后的亮度乘上255(这里叙述做了简化,实际过程比这个稍微复杂一点,会对小数做一个锯齿状映射(0.0, 0.5) -> (0.5, 1.0) ->(0.5, 0.0) -> (1.0, 0.5)),并将整数跟小数分别存储到两个channel中)。
-
另外,从代码中我们看到常量,这个11.5代表了什么呢?这个暂时没有什么头绪,如果有人知道这个数值怎么来的,麻烦留言告知一下我,感激不尽。
-
-
LQ编码过程中第三步中Color数据不必要乘以LogL
- 这是因为,HQ模式下会直接将LogL数据写入lightmap的alpha通道,因此无需再通过Color获取到LogL数据了。
-
Color在Lightmap中存储使用的是sRGB格式(LQ中的Color使用的是Linear格式)
-
我们知道,sRGB格式有助于存储更多的暗部信息,但是其代价却是需要增加编码与解码的消耗,而我们注意到,编码的时候(C++侧)使用的是,而在解码(shader侧)的时候则是直接取平方,这其实也是出于性能消耗的考虑
-
为什么LQ直接使用Linear而不使用sRGB呢,自然也是拿精度换速度了。
-
关于Directionality
为什么要使用Directional信息,而不直接将Light与Surface作用后的结果存储在Lightmap中?这是因为出于对性能的考虑,Lightmap通常使用的分辨率不会特别高,至少不会高到屏幕中surface上的每个像素都能瓜分lightmap中的一个texel,那么就会出现屏幕中的多个像素对应于同一个texel的情况,如果直接存储light与surface作用的结果,那么这些像素将共享同一个结果,如果这些像素处于同一平面上,这个效果也没什么不对,但是如果这些像素对应于不同的world normal,那么效果看起来就不是那么真实了。通过引入Direction信息,可以现场根据WorldNormal调整Light与Surface的作用效果,无疑可以得到更为优越的效果(比如通过dot(WorldNormal, LightmapDir)得到有效光照的比例,再将之乘到Lightmap Color上就能得到不同的光照效果)。
网友评论