天天看點

videowriter最小的編碼格式_UE4 Lightmap格式解析

一、UE4光照圖介紹

UE4的光照圖分為低品質(LQ)和高品質(HQ)兩種類型,引擎預設的,把HQ的光照圖适用于PC/主機平台的靜态光照,而LQ的光照圖則用于移動平台。相比LQ形式,HQ光照圖提供了更好的光照品質、更平滑的明暗交界過渡,但同時也會耗費更多的記憶體和Shader指令數。

UE4的高低兩種品質的光照圖均存儲了以下三種資訊:

照明顔色、照明亮度、入射光線的簇的最大貢獻方向

,其在存儲格式、編碼方式上的不同決定了其品質上的差别。

1.低品質光照圖(LQ) .

使用

24位的RGB8

格式的檔案存儲光照資訊,預設的,采用平台相關的壓縮方式對光照圖進行壓縮(如ETC2,ASTC)以降低光照圖的存儲空間、記憶體占用和渲染時占用的GPU帶寬。

.

光照資訊分為上下兩部分,上半部分存儲的是

線性空間的光照顔色和亮度的預乘資訊

,下半部分存儲是入射光照的最大貢獻方向

LQ光照圖如下圖所示,紅框部分存儲的就是入射光線的最大貢獻方向

videowriter最小的編碼格式_UE4 Lightmap格式解析

圖1

2.高品質光照圖(HQ) .

使用

32位的RGBA8

格式的檔案存儲HQ品質的光照圖,同樣的,采用平台相關的壓縮方式對光照圖進行壓縮(如ETC2,ASTC),但即使同樣ETC2方式進行壓縮,HQ的的光照圖由于多一個Alpha通道資訊,同樣尺寸的光照圖,其大小會是LQ貼圖大小的2倍。

光照圖的RGB通道上下兩個分區包含的資訊和低品質光照圖類似,上半部分存儲的是光照的顔色資訊(

和LQ不同,它不包含亮度資訊且是在gamma空間

),下半部分存儲的是入射光線的最大貢獻方向。

光照圖的A通道存儲的是光照圖的亮度資訊,同樣它分為上下兩個部分,上半部分表示的是光照亮度的整數部分,下半部分表示的是亮度資訊的小數部分。

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

videowriter最小的編碼格式_UE4 Lightmap格式解析

圖2

videowriter最小的編碼格式_UE4 Lightmap格式解析

圖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):
videowriter最小的編碼格式_UE4 Lightmap格式解析

圖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).函數圖示,如下所示
videowriter最小的編碼格式_UE4 Lightmap格式解析

圖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)
videowriter最小的編碼格式_UE4 Lightmap格式解析

圖6

上式中y表示編碼後的亮度

取值域為 y∈[0,1]

常數0.01858136為最小亮度

(2).函數圖示,如下所示
videowriter最小的編碼格式_UE4 Lightmap格式解析

圖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

中找到。