UE4 Lightmap格式解析

一、UE4光照图介绍

UE4的光照图分为低质量(LQ)和高质量(HQ)两种类型,引擎默认的,把HQ的光照图适用于PC/主机平台的静态光照,而LQ的光照图则用于移动平台。相比LQ形式,HQ光照图提供了更好的光照质量、更平滑的明暗交界过渡,但同时也会耗费更多的内存和Shader指令数。

UE4的高低两种质量的光照图均存储了以下三种信息:照明颜色、照明亮度、入射光线的簇的最大贡献方向,其在存储格式、编码方式上的不同决定了其质量上的差别。

1.低质量光照图(LQ)

. 使用24位的RGB8格式的文件存储光照信息,默认的,采用平台相关的压缩方式对光照图进行压缩(如ETC2,ASTC)以降低光照图的存储空间、内存占用和渲染时占用的GPU带宽。

. 光照信息分为上下两部分,上半部分存储的是线性空间的光照颜色和亮度的预乘信息,下半部分存储是入射光照的最大贡献方向

LQ光照图如下图所示,红框部分存储的就是入射光线的最大贡献方向

图1

2.高质量光照图(HQ)

. 使用32位的RGBA8格式的文件存储HQ质量的光照图,同样的,采用平台相关的压缩方式对光照图进行压缩(如ETC2,ASTC),但即使同样ETC2方式进行压缩,HQ的的光照图由于多一个Alpha通道信息,同样尺寸的光照图,其大小会是LQ贴图大小的2倍。

光照图的RGB通道上下两个分区包含的信息和低质量光照图类似,上半部分存储的是光照的颜色信息(和LQ不同,它不包含亮度信息且是在gamma空间),下半部分存储的是入射光线的最大贡献方向。

光照图的A通道存储的是光照图的亮度信息,同样它分为上下两个部分,上半部分表示的是光照亮度的整数部分,下半部分表示的是亮度信息的小数部分。

HQ光照图示例的RGB和Alpha部分如下图所示:

图2
图3

可以看到HQ和LQ的光照图中,RGB通道的下半部分存储的方向信息部分基本一致(实际上略有不同,下面会有具体的分析),但上半部分因为颜色空间的不同,同时LQ的颜色又预乘了亮度信息,从而明显的比HQ图要暗很多。


二.光照图的编码方式

由于光照亮度的生成时其亮度范围远大于1的浮点数区间,故其最好的存储方式应当是浮点纹理,比如通用的exr格式。exr的缺点是更大的存储、内存和渲染时的带宽占用,同时有多平台兼容性问题,故目前主流的商业引擎或各大厂的自研引擎,其在移动端的光照图也大都使用RGB8或RGBA8图进行存储。

把亮度数据从浮点数压缩到0~256的整数时需要进行编码进行存储(UE4中编码这一部分也叫做量化),通过此编码映射把无限范围的浮点数域映射到有限的[0,255]的整数域中,其压缩方式是有损的,在压缩过程中的信息损失会造成光照图的质量的下降和不可逆失真。

UE4中光照亮度编码映射没有采用传统的RGBM,RGBD,LOGLUV方式编码,而是使用自己开发的编码方式,分别对LQ和HQ光照的亮度信息进行压缩。

1.LQ编码

(1).LQ亮度压缩的量化函数(函数1):

图4

y表示编码后的亮度,值域为 y∈[0,1]

常数0.00390625为最小亮度

值域偏移0.5是为了把亮度x∈[0,1]之间的y的值域从[-0.5,0]平移到[0,0.5]之间,不丢失暗部信息

log结果除以16,有两个好处,一是可以把更多的原始亮度信息编码到值域中。原本按照 y∈[0,1]的约束,log(x+0.00390625)的中x取值范围约为x∈[0,1.41],而在除了16之后,x的取值范围大约在x∈[0,255]之间。二是因为需要预乘颜色和亮度值,较小的亮度值乘以颜色可以很大程度规避掉数值上的上溢出。

(2).函数图示,如下所示

图5

观察此函数曲线的形状可以得到如下结论

.x取值越接近于0,曲丝切线斜率越大,事实上从从0上升到1时,y的值域从0上升到了0.5,即占总定义域中1/255区间长度的x取值占了一半的y的值域。这说明做为取舍,UE4的亮度编码函数的设计,是把主要的精度留给了暗部而选择损失了部分亮部的精度。由于Lightmap烘焙的信息大都是间接光照明,可以解释其亮度细节留给暗部的合理性。

.x可以容纳的亮度宽度是0~255,但大部分的光照图的亮度变化范围不会有这么大,所以如果简单的使用此量化函数进行编码,其结果是浪费一部分或大部分的存储精度。故此函数不能直接应用于亮度编码,UE4的实作编码的过程中也按实际的y值区间对最终结果进行了归一化,使其值域落在[0,1]的区间内,从而最大化利用存储精度。

(3).LQ光照编码的代码实现

. 使用图4的函数1对单份lightmap进行求值,并同时求得光照色和亮度预乘的的最大值和最小值,实现代码如下

float L, U, V, W;
GetLUVW( SourceSample.Coefficients[2], L, U, V, W );

float LogL = FMath::Log2( L + SimpleLogBlackPoint ) / SimpleLogScale + 0.5f;

float LogR = LogL * U;
float LogG = LogL * V;
float LogB = LogL * W;

MinCoefficient[2][0] = FMath::Min( MinCoefficient[2][0], LogR );
MaxCoefficient[2][0] = FMath::Max( MaxCoefficient[2][0], LogR );

MinCoefficient[2][1] = FMath::Min( MinCoefficient[2][1], LogG );
MaxCoefficient[2][1] = FMath::Max( MaxCoefficient[2][1], LogG );
				
MinCoefficient[2][2] = FMath::Min( MinCoefficient[2][2], LogB );
MaxCoefficient[2][2] = FMath::Max( MaxCoefficient[2][2], LogB );

使用y的最大和最小值对归一化参数进行变换,其变换公式为

N_mul = 1 /(max-min)

N_add = -min/(max-min)

float CoefficientMultiply[LM_NUM_STORED_LIGHTMAP_COEF][4];
float CoefficientAdd[LM_NUM_STORED_LIGHTMAP_COEF][4];
for (int32 CoefficientIndex = 0; CoefficientIndex < LM_NUM_STORED_LIGHTMAP_COEF; CoefficientIndex++)
{
   for (int32 ColorIndex = 0; ColorIndex < 4; ColorIndex++)
   {
        CoefficientMultiply[CoefficientIndex][ColorIndex] = 
                  1.0f / FMath::Max<float>(MaxCoefficient[CoefficientIndex][ColorIndex] - MinCoefficient[CoefficientIndex][ColorIndex], DELTA);
        CoefficientAdd[CoefficientIndex][ColorIndex] = 
                  -MinCoefficient[CoefficientIndex][ColorIndex] / FMath::Max<float>(MaxCoefficient[CoefficientIndex][ColorIndex] - MinCoefficient[CoefficientIndex][ColorIndex], DELTA);
    }
}

. 计算归1化的亮度和颜色积,归一化之后的颜色采用4舍5入的方式量化到[0,255]颜色区间(RoundToInt/Clamp)

LQ_LightMap_Color = ( y * LinearColor - min) / (max - min)

= y * LinearColor * N_mul + N_add

float L, U, V, W;
GetLUVW( SourceSample.Coefficients[2], L, U, V, W );

// LogRGB encode color
float LogL = FMath::Log2( L + SimpleLogBlackPoint ) / SimpleLogScale + 0.5f;

float LogR = LogL * U * CoefficientMultiply[2][0] + CoefficientAdd[2][0];
float LogG = LogL * V * CoefficientMultiply[2][1] + CoefficientAdd[2][1];
float LogB = LogL * W * CoefficientMultiply[2][2] + CoefficientAdd[2][2];

// LogR, LogG, LogB
DestCoefficients.Coefficients[2][0] = (uint8)FMath::Clamp<int32>( FMath::RoundToInt( LogR * 255.0f ), 0, 255 );
DestCoefficients.Coefficients[2][1] = (uint8)FMath::Clamp<int32>( FMath::RoundToInt( LogG * 255.0f ), 0, 255 );
DestCoefficients.Coefficients[2][2] = (uint8)FMath::Clamp<int32>( FMath::RoundToInt( LogB * 255.0f ), 0, 255 );
DestCoefficients.Coefficients[2][3] = 255;

2.HQ编码

(1).HQ亮度压缩的量化函数(函数2)

图6

上式中y表示编码后的亮度取值域为 y∈[0,1]

常数0.01858136为最小亮度

(2).函数图示,如下所示

图7

观察此函数曲线的形状可以得到如下结论:

. 和LQ一样,同为log函数,故同样在数值分布上倾向于给暗部保留更多的细节

. 可以容纳的原始亮度范围有限,x取值范围非常小,只有原始亮度在x∈[1,2-0.01858136]之间时,编码后的亮度才落在y∈[0,1]之间在做此映射后,x的取值范围比y的值域还小,同时丢失掉了原始亮度中的x∈[0,1)的暗部。所以UE4并不是原始的使用此函数对光照图进行压缩,而是利用和LQ光照图同样的方式对函数的结果进行了归一化,从而同时保证了暗部的完整性,亮度范围可变性和精度的充分利用。

(3).HQ亮度压缩的实现步骤

. 使用函数2对单份lightmap进行求值,求得y的最大值和最小值,实现代码如下

// Complex
float L, U, V, W;
GetLUVW( SourceSample.Coefficients[0], L, U, V, W );

float LogL = FMath::Log2( L + LogBlackPoint );

MinCoefficient[0][0] = FMath::Min( MinCoefficient[0][0], U );
MaxCoefficient[0][0] = FMath::Max( MaxCoefficient[0][0], U );

MinCoefficient[0][1] = FMath::Min( MinCoefficient[0][1], V );
MaxCoefficient[0][1] = FMath::Max( MaxCoefficient[0][1], V );

MinCoefficient[0][2] = FMath::Min( MinCoefficient[0][2], W );
MaxCoefficient[0][2] = FMath::Max( MaxCoefficient[0][2], W );

MinCoefficient[0][3] = FMath::Min( MinCoefficient[0][3], LogL );
MaxCoefficient[0][3] = FMath::Max( MaxCoefficient[0][3], LogL );

. 使用y的最大和最小值对入射光线的最大贡献方向进行变换,实现代码和LQ在一个地方

. 对光照颜色进行归一化变换编码(gamma空间)

float L, U, V, W;
GetLUVW( SourceSample.Coefficients[0], L, U, V, W );

U = U * CoefficientMultiply[0][0] + CoefficientAdd[0][0];
V = V * CoefficientMultiply[0][1] + CoefficientAdd[0][1];
W = W * CoefficientMultiply[0][2] + CoefficientAdd[0][2];

DestCoefficients.Coefficients[0][0] = (uint8)FMath::Clamp<int32>( FMath::RoundToInt( FMath::Pow( U, 1.0f / 2.2f ) * 255.0f ), 0, 255 );
DestCoefficients.Coefficients[0][1] = (uint8)FMath::Clamp<int32>( FMath::RoundToInt( FMath::Pow( V, 1.0f / 2.2f ) * 255.0f ), 0, 255 );
DestCoefficients.Coefficients[0][2] = (uint8)FMath::Clamp<int32>( FMath::RoundToInt( FMath::Pow( W, 1.0f / 2.2f ) * 255.0f ), 0, 255 );
DestCoefficients.Coefficients[0][3] = (uint8)FMath::Clamp<int32>( FMath::RoundToInt( LogL * 255.0f ), 0, 255 );

. 归一化亮度编码,其归一化之后的亮度的量化方式和LQ的方式不一样----它会量化出两个值,用类似定点数的方式进行存储,一个是整数部分,一个是小数部分,公式如下

整数部分 zi = floor(y * 255)

余数部分 zf = (y * 255 - ceil(y * 255) + 0.5) * 255

这两个值都存在光照图的Alpha通道中,整数部分存储在上半图,余数部分在下半图。UE4的这种编码方式可以很容易看出:在单光照图亮度分布范围低于255个强度差时,其还原精度接近到小数点后3位。具体的代码实现如下

float L, U, V, W;
GetLUVW( SourceSample.Coefficients[0], L, U, V, W );

// LogLUVW encode color
float LogL = FMath::Log2( L + LogBlackPoint );
float Residual = LogL * 255.0f - FMath::RoundToFloat( LogL * 255.0f ) + 0.5f;

DestCoefficients.Coefficients[0][3] = (uint8)FMath::Clamp<int32>( FMath::RoundToInt( LogL * 255.0f ), 0, 255 );
DestCoefficients.Coefficients[1][3] = (uint8)FMath::Clamp<int32>( FMath::RoundToInt( Residual * 255.0f ), 0, 255 );

以上所有关于UE4光照图编码部分的具体代码,均可在LightmapData.cpp中的QuantizeLightSamples中找到。

编辑于 2019-11-12 06:25