您當前的位置:首頁 > 攝影

UE4 Mobile IBL烘焙實現分析

作者:由 Jiff 發表于 攝影時間:2019-06-29

Split Sum

Split Sum是UE4 IBL的基礎,其思想是把環境反射由單個蒙特卡洛積分分成兩個獨立分部

:入射光貢獻部分和BRDF積分部分

。渲染方程到Split Sum的推導如下

UE4 Mobile IBL烘焙實現分析

渲染方程的蒙特卡羅積分

UE4 Mobile IBL烘焙實現分析

UE4 蒙特卡羅積分的Split Sum近似

理解Split Sum的關鍵在於如何理解分離後的Li對i的總和 。

在渲染方程中,L表示的是反射方向上的入射光線 ,但在UE4的IBL的單個畫素,表示的是Li在i上的和,是反射椎中所有入射光貢獻的總和

。反射錐的示意如下

UE4 Mobile IBL烘焙實現分析

由於UE4 PBR模型中的法線分佈項決定,這個錐體的分佈模型也同樣是GGX模型,椎體大小則由物體的Roughness及視線(V)和表面法向(N)共同決定。

UE4對此做了進一步簡化,假定V和N同向,從而使最終結果變為Roughness的單變數函式.

UE4 Mobile IBL烘焙實現分析

至此,

UE4再使紋理Mipmap層級和Roughness一一對應

,把確定的Roughness的入射光總和編碼到Cubemap對應的Mip層級中即可。

Split Sum的侷限性

除Karis自己提到的

Split Sum不能模擬拉伸的高光

(因為在儲存IBL的時候假定了 V = N)外,我的理解裡,還有其它的一些侷限,如果我理解有誤,歡迎指正:

Split Sum的假設是各向同性的,所以不能模擬各向異性

Split Sum近似在數學上並不正確,嚴格來說它是對原有積分的過高估計,所以它會顯得更亮,反射範圍也更大(最終結果會顯得反射高亮、擴散)

UE4 Mobile IBL烘焙實現分析

Split Sum IBL可編碼儲存的有效Roughness值域有限,線性插值多個Roughness出來的高光並不符合GGX分佈。要達到PBR材質中L8編碼的Roughness同等精度是不可能任務。

反射球Cubemap構建流程

UE4的反射球支援兩種光源來源:一種是直接以當前場景做為光源(CaptureScene),另一種是以HDR格式的經緯圖做為光源(SpecifiedCubemap)。其生成最終IBL Cubemap流程分為以下幾步

更新做為光源的Cubemap

更新光源Cubemap的實現部分在FScene::CaptureOrUploadReflectionCapture函式中。分別針對CaptureScene和SpecifiedCubemap進行處理。

if

CaptureComponent

->

ReflectionSourceType

==

EReflectionSourceType

::

CapturedScene

{

bool

const

bCaptureStaticSceneOnly

=

CVarReflectionCaptureStaticSceneOnly

GetValueOnGameThread

()

!=

0

CaptureSceneIntoScratchCubemap

this

CaptureComponent

->

GetComponentLocation

()

+

CaptureComponent

->

CaptureOffset

ReflectionCaptureSize

false

bCaptureStaticSceneOnly

0

false

false

FLinearColor

());

}

else

if

CaptureComponent

->

ReflectionSourceType

==

EReflectionSourceType

::

SpecifiedCubemap

{

UTextureCube

*

SourceTexture

=

CaptureComponent

->

Cubemap

float

SourceCubemapRotation

=

CaptureComponent

->

SourceCubemapAngle

*

PI

/

180。f

);

ERHIFeatureLevel

::

Type

InFeatureLevel

=

FeatureLevel

ENQUEUE_RENDER_COMMAND

CopyCubemapCommand

)(

SourceTexture

ReflectionCaptureSize

SourceCubemapRotation

InFeatureLevel

](

FRHICommandList

&

RHICmdList

{

CopyCubemapToScratchCubemap

RHICmdList

InFeatureLevel

SourceTexture

ReflectionCaptureSize

false

false

SourceCubemapRotation

FLinearColor

());

});

}

對光源Cubemap進行Prefilter生成IBL用的Cubmap

Prefilter Cubemap的入口同樣在FScene::CaptureOrUploadReflectionCapture中,但其核心實現則在ComputeAverageBrightness和FilterReflectionEnvironment兩個函數里。UE4生成IrrandianceMap和Prefilter Specular Cubemap的實現都是走的Render Pipeline。

用於計算IrrandianceMap的ComputeDiffuseIrradiance也是在FilterReflectionEnvironment中呼叫。雖然最終IrrandianceMap的輸出是一組球諧引數,但其渲染的Pass用的比生成Cubemap的Pass多得多。

Prefilter Cubemap使用了Mip層數 * 6 個Pass,逐mip逐cube face進行濾波。濾波的具體實現不在C++程式碼中,而是在ReflectionEnvironmentShaders。usf的FilterPS實現。下面對FilterPS進行簡要分析。

一開始是計算Cubemap的座標和TBN

float2

ScaledUVs

=

Input

UV

*

2

-

1

float3

CubeCoordinates

=

GetCubemapVector

ScaledUVs

);

float3

N

=

normalize

CubeCoordinates

);

float3x3

TangentToWorld

=

GetTangentBasis

N

);

接著是計算當前Mipmap對應的Roughness

float

Roughness

=

ComputeReflectionCaptureRoughnessFromMip

MipIndex

NumMips

-

1

);

計算Mip對應Roughness並不是線性對映,而是指數函式,UE4的實現實現如下

#define REFLECTION_CAPTURE_ROUGHEST_MIP 1

#define REFLECTION_CAPTURE_ROUGHNESS_MIP_SCALE 1。2

float

ComputeReflectionCaptureRoughnessFromMip

float

Mip

half

CubemapMaxMip

{

float

LevelFrom1x1

=

CubemapMaxMip

-

1

-

Mip

return

exp2

REFLECTION_CAPTURE_ROUGHEST_MIP

-

LevelFrom1x1

/

REFLECTION_CAPTURE_ROUGHNESS_MIP_SCALE

);

}

把1和1。2代入到上式中,x軸為mip層級,y軸為對應的roughness,這個函式的影象是這樣的

UE4 Mobile IBL烘焙實現分析

這個影象有兩重意思:一是mip層級小的部分給了roughness小的部分,即越光滑的材質,它對應的cube size會越大;二是mip越接近0,它對應的roughness增長越慢,圖上可以看到0~3共計4級mip層級,對應的不過是[0,0。2)之間的roughness,而4,5,6共3級mip對應了[0。2,1]之間的roughness。應用到IBL上,就是越光滑的材質會有越清晰的反射或者說編碼的精度傾向於給更光滑的表面。

再接下來,FilterPS開始針對Roughness實現不同的 ImportanceSample策略

對於幾乎完全鏡面的mip只採樣一次

if

Roughness

<

0。01

{

OutColor

=

SourceTexture

SampleLevel

SourceTextureSampler

CubeCoordinates

0

);

return

}

對於幾乎完全粗糙的mip直接使用Cosine分佈在上半球進行取樣求解

if

Roughness

>

0。99

{

// Roughness=1, GGX is constant。 Use cosine distribution instead

LOOP

for

uint

i

=

0

i

<

NumSamples

i

++

{

float2

E

=

Hammersley

i

NumSamples

0

);

float3

L

=

CosineSampleHemisphere

E

)。

xyz

float

NoL

=

L

z

float

PDF

=

NoL

/

PI

float

SolidAngleSample

=

1。0

/

NumSamples

*

PDF

);

float

Mip

=

0。5

*

log2

SolidAngleSample

/

SolidAngleTexel

);

L

=

mul

L

TangentToWorld

);

FilteredColor

+=

SourceTexture

SampleLevel

SourceTextureSampler

L

Mip

);

}

OutColor

=

FilteredColor

/

NumSamples

}

對於roughness在(0。01,0。99)之間的mip執行ggx prefilter

float

Weight

=

0

LOOP

for

uint

i

=

0

i

<

NumSamples

i

++

{

float2

E

=

Hammersley

i

NumSamples

0

);

// 6x6 Offset rows。 Forms uniform star pattern

//uint2 Index = uint2( i % 6, i / 6 );

//float2 E = ( Index + 0。5 ) / 5。8;

//E。x = frac( E。x + (Index。y & 1) * (0。5 / 6。0) );

E

y

*=

0。995

float3

H

=

ImportanceSampleGGX

E

Pow4

Roughness

)。

xyz

float3

L

=

2

*

H

z

*

H

-

float3

0

0

1

);

float

NoL

=

L

z

float

NoH

=

H

z

if

NoL

>

0

{

//float TexelWeight = CubeTexelWeight( L );

//float SolidAngleTexel = SolidAngleAvgTexel * TexelWeight;

//float PDF = D_GGX( Pow4(Roughness), NoH ) * NoH / (4 * VoH);

float

PDF

=

D_GGX

Pow4

Roughness

),

NoH

*

0。25

float

SolidAngleSample

=

1。0

/

NumSamples

*

PDF

);

float

Mip

=

0。5

*

log2

SolidAngleSample

/

SolidAngleTexel

);

float

ConeAngle

=

acos

1

-

SolidAngleSample

/

2

*

PI

);

L

=

mul

L

TangentToWorld

);

FilteredColor

+=

SourceTexture

SampleLevel

SourceTextureSampler

L

Mip

*

NoL

Weight

+=

NoL

}

}

OutColor

=

FilteredColor

/

Weight

Prefilered Cubemap針對移動端使用RGBM編碼到RGBA8紋理

RGBA8紋理的編碼是隻發生在移動端的,其具體實現在GenerateEncodedHDRData函數里。

這一步是完整的執行在CPU上而不使用Render Pipeline的。GenerateEncodedHDRData函式使用大量篇幅處理跨邊閃爍的問題(簡單粗暴使用均值解決)了,對於非邊界部分的編碼則透過呼叫RGBMEncode實現。

RGBMEncode實現步驟有

先開方為敬,目的和光照圖一樣,給暗部更多精度

Color

R

=

FMath

::

Sqrt

Color

R

);

Color

G

=

FMath

::

Sqrt

Color

G

);

Color

B

=

FMath

::

Sqrt

Color

B

);

接下來,按RGBM編碼的標準做法,是除max range(上面開過方,這兒能容納的亮度範圍實際上是256)

Color

/=

16。0f

float

MaxValue

=

FMath

::

Max

FMath

::

Max

Color

R

Color

G

),

FMath

::

Max

Color

B

DELTA

));

再接下來,正常來說,RGBM後續的編碼直接使用Color。A = MaxValue ,Color。RGB /= Color。A 就結束了。但UE4的實現並不是這樣,UE的工程師們還處理了實際亮度範圍超過256的情況。他們在MaxValue大於0。75的時候,使用了一個簡單的反函式做了一次ToneMapping。完整的實現程式碼是這樣的

if

MaxValue

>

0。75f

{

// Fit to valid range by leveling off intensity

float

Tonemapped

=

MaxValue

-

0。75

*

0。75

/

MaxValue

-

0。5

);

Color

*=

Tonemapped

/

MaxValue

MaxValue

=

Tonemapped

}

Encoded

A

=

FMath

::

Min

FMath

::

CeilToInt

MaxValue

*

255。0f

),

255

);

Encoded

R

=

FMath

::

RoundToInt

Color

R

*

255。0f

/

Encoded

A

*

255。0f

);

Encoded

G

=

FMath

::

RoundToInt

Color

G

*

255。0f

/

Encoded

A

*

255。0f

);

Encoded

B

=

FMath

::

RoundToInt

Color

B

*

255。0f

/

Encoded

A

*

255。0f

);

return

Encoded

這個ToneMapping函式的公式和影象如下

UE4 Mobile IBL烘焙實現分析

UE4 Mobile IBL烘焙實現分析

標簽: color  roughness  cubemap  FMath  UE4