天天看點

GraphicsLab Project之Physical based Shading-Image based Lighting(Specular篇)(二)引言優化方法LD項DFG項光照計算走樣貼圖總結參考文獻

作者:idovelemon

日期:2018-03-17

來源:CSDN

主題:Prefilter Environment Map

引言

前面的章節裡面,我們講述了如何通過brute force的方式去實作Specular的Image based Lighting。但是這種實作,在實際的遊戲運作過程中消耗太大,實用價值不高。是以,本篇文章将給出對于這中brute force方式的優化處理,以加快Specular Image based Lighting的計算。這個處理,就是Unreal4引擎的實作方式。同時,添加對Albedo Map,Roughness Map,Metallic Map和Normal Map的應用,徹底展現下IBL的魅力。

GraphicsLab Project之Physical based Shading-Image based Lighting(Specular篇)(二)引言優化方法LD項DFG項光照計算走樣貼圖總結參考文獻

優化方法

首先,我們給出我們需要計算的渲染方程:

Lo=∫ΩLi(l)f(l,v)cosθdwi L o = ∫ Ω L i ( l ) f ( l , v ) c o s θ d w i

在前面一篇文章裡面,我們知道了,可以通過Importace Sampling,使用Monte Carlo積分來求解上面的積分方程,如下:

L0≈1N∑k=1NLi(lk)f(lk,v)cosθkp(lk,v) L 0 ≈ 1 N ∑ k = 1 N L i ( l k ) f ( l k , v ) c o s θ k p ( l k , v )

為了加速對該公式的計算,Unreal4将上述公式劃分成了兩個不同求和部分(為什麼我想不出來這種近似的方法,唉!!!),這兩個不同的求和部分能夠分别通過預先計算來得到結果。

L0≈1N∑k=1NLi(lk)f(lk,v)cosθkp(lk,v)≈(1∑Nk=1cosθk∑k=1NLi(lk)cosθk)(1N∑k=1Nf(lk,v)cosθkp(lk,v)) L 0 ≈ 1 N ∑ k = 1 N L i ( l k ) f ( l k , v ) c o s θ k p ( l k , v ) ≈ ( 1 ∑ k = 1 N c o s θ k ∑ k = 1 N L i ( l k ) c o s θ k ) ( 1 N ∑ k = 1 N f ( l k , v ) c o s θ k p ( l k , v ) )

從上面的公式中可以看出,我們能夠分别計算這兩個求和部分。是以,我們通過預計算的形式,将兩個不同的部分預先計算好結果,等到實際進行光照計算的時候,我們隻要擷取預計算的結果,然後直接進行計算即可。我們分别将上面兩個求和部分命名為LD項和DFG項。其中,LD項是對入射光進行求和的部分,需要輸入描述周圍環境光照的環境貼圖(主要是CubeMap);DFG項和光照資訊無關,是以隻要預計算一次,就能夠重複利用了。

是以,現在的光照流程變成了如下:

1.輸入一張描述周圍環境光照的CubeMap,然後預計算LD項,這個操作被稱為Prefilter Environment Map。

2.預計算DFG項。

3.在實際渲染的時候,使用LD*DFG來得到Specular的IBL的結果。

上面的LD和DFG都是通過貼圖的形式被儲存下來。LD是對CubeMap進行預計算,它的結果也是一張CubeMap。DFG項是一張2D的貼圖。通過這樣的方法,我們最終在遊戲裡面的計算就變成了對這兩個貼圖的采樣了,大大的簡化了計算量。

下面分别講述,如何預計算LD和DFG項。

LD項

LD項的公式如下所示:

LD=1∑Nk=1cosθk∑k=1NLi(lk)cosθk L D = 1 ∑ k = 1 N c o s θ k ∑ k = 1 N L i ( l k ) c o s θ k

為了獲得 Li(lk) L i ( l k ) ,我們需要得到一個 Lk L k 向量,這樣我們才能夠使用它來通路環境貼圖CubeMap。而得到 Lk L k 向量的方法就是通過Importance Sampling來對GGX進行采樣。這個采樣的操作和前面一篇文章中的采樣方法一緻。但是,如果我們要對GGX進行Importance Sampling,就需要知道normal和view這兩個向量。是以對于這個處理,Unreal4直接假定normal = view = reflect向量。這個假設也是這個方法主要的瑕疵所在。同時,對GGX進行Importace Sampling還需要提供表面的roughness屬性。但是由于最終進行IBL渲染的表面roughness不是固定的一個值,是以業界常用的處理方式就是對不同的roughness分别進行預計算,然後将結果儲存在LD的CubeMap的不同mipmap level上面。roughness為0的LD項,儲存在LD CubeMap的mipmap level 0裡面;roughness為1的LD項,儲存在LD CubeMap的最後一個mipmap level上,其中部分的roughness值分别儲存在對應的mipmap level上面。如下圖就是對LD項進行計算之後,儲存在不同mipmap level的結果:

GraphicsLab Project之Physical based Shading-Image based Lighting(Specular篇)(二)引言優化方法LD項DFG項光照計算走樣貼圖總結參考文獻

下面的代碼展示了如何對LD項進行計算:

void DrawConvolutionCubeMapSpecularLD() {
    // Setup shader
    render::Device::SetShader(m_SpecularLDCubeMapProgram);
    render::Device::SetShaderLayout(m_SpecularLDCubeMapProgram->GetShaderLayout());

    // Setup texture
    render::Device::ClearTexture();
    render::Device::SetTexture(, m_CubeMap, );

    // Setup mesh
    render::Device::SetVertexLayout(m_ScreenMesh->GetVertexLayout());
    render::Device::SetVertexBuffer(m_ScreenMesh->GetVertexBuffer());

    // Setup render state
    render::Device::SetDepthTestEnable(true);
    render::Device::SetCullFaceEnable(true);
    render::Device::SetCullFaceMode(render::CULL_BACK);

    int32_t miplevels = log() / log() + ;
    float roughnessStep = f / miplevels;
    int32_t width = , height = ;
    for (int32_t j = ; j < miplevels; j++) {
        // Render Target
        render::Device::SetRenderTarget(m_SpecularLDCubeMapRT[j]);

        // View port
        render::Device::SetViewport(, , width, height);
        width /= ;
        height /= ;

        for (int32_t i = ; i < ; i++) {
            // Set Draw Color Buffer
            render::Device::SetDrawColorBuffer(static_cast<render::DrawColorBuffer>(render::COLORBUF_COLOR_ATTACHMENT0 + i));

            // Clear
            render::Device::SetClearColor(.0f, .0f, .0f);
            render::Device::SetClearDepth(f);
            render::Device::Clear(render::CLEAR_COLOR | render::CLEAR_DEPTH);

            // Setup uniform
            render::Device::SetUniformSamplerCube(m_SpecularLDProgram_CubeMapLoc, );
            render::Device::SetUniform1i(m_SpecularLDProgram_FaceIndexLoc, i);
            render::Device::SetUniform1f(m_SpecularLDProgram_RoughnessLoc, j * roughnessStep);

            // Draw
            render::Device::Draw(render::PT_TRIANGLES, , m_ScreenMesh->GetVertexNum());
        }
    }
}
           

下面的GLSL代碼展示了如何進行積分預算和Importance Sampling:

vec3 convolution_cube_map(samplerCube cube, int faceIndex, vec2 uv) {
    // Calculate tangent space base vector
    vec3 n = calc_normal(faceIndex, uv);
    n = normalize(n);
    vec3 v = n;
    vec3 r = n;

    // Convolution
    uint sampler = u;
    vec3 color = vec3(, , );
    float weight = ;
    for (uint i = u; i < sampler; i++) {
        vec2 xi = hammersley(i, sampler);
        vec3 h = importance_sampling_ggx(xi, glb_Roughness, n);
        vec3 l =  * dot(v, h) * h - v;

        float ndotl = max(, dot(n, l));
        if (ndotl > ) {
            color = color + filtering_cube_map(glb_CubeMap, l).xyz * ndotl;
            weight = weight + ndotl;
        }
    }

    color = color / weight;

    return color;
}
           

DFG項

DFG項的公式如下:

DFG=1N∑k=1Nf(lk,v)cosθkp(lk,v) D F G = 1 N ∑ k = 1 N f ( l k , v ) c o s θ k p ( l k , v )

我們知道,要計算這樣的公式,我們需要如下的資訊:

1.需要v,l,n

2.需要roughness

3.通過albedo和metallic來計算Fresnel系數中的F0項

在前面講述LD項的時候,我們知道了可以通過對GGX進行Importance Sampling來擷取l,是以我們需要一個roughnesss。

另外在這裡,我們不能夠假設n=v=r,這樣的假設會導緻DFG項出現重大的錯誤。是以,我們需要一個辦法來擷取n,v,r。由于這裡的計算是在切空間中進行的,是以n總是為(0.0, 0.0, 1.0),是以隻要我們給定一個 ndotv=dot(n,v) n d o t v = d o t ( n , v ) 的值,我們就能夠計算出v和r來,由此我們需要一個ndotv值。

至于計算Fresenl系數的F0項,因為需要使用材質本身的albedo和metallic資訊,這裡沒有辦法做任何的假設。是以,我們需要對上面的DFG公式進行一點變化。

我們知道如下的關系:

f(lk,v)=DFG4(n⋅l)(n⋅v) f ( l k , v ) = D F G 4 ( n ⋅ l ) ( n ⋅ v )

p(lk,v)=D(n⋅h)4(v⋅h) p ( l k , v ) = D ( n ⋅ h ) 4 ( v ⋅ h )

cosθk=n⋅l c o s θ k = n ⋅ l

是以:

DFG=1N∑k=1Nf(lk,v)cosθkp(lk,v)=1N∑k=1NDFG4(n⋅l)(n⋅v)⋅4(v⋅h)D(n⋅h)⋅(n⋅l)=1N∑k=1NFG(n⋅v)⋅(v⋅h)(n⋅h)=1N∑k=1N(F0+(1−F0)(1−v⋅h)5)G(n⋅v)⋅(v⋅h)(n⋅h)=1N∑k=1N(F0⋅[1−(1−v⋅h)5]+(1−v⋅h)5)G(n⋅v)⋅(v⋅h)(n⋅h)=F0⋅[1N∑k=1N([1−(1−v⋅h)5])G(n⋅v)⋅(v⋅h)(n⋅h)]+[1N∑k=1N(1−v⋅h)5G(n⋅v)⋅(v⋅h)(n⋅h)](1) (1) D F G = 1 N ∑ k = 1 N f ( l k , v ) c o s θ k p ( l k , v ) = 1 N ∑ k = 1 N D F G 4 ( n ⋅ l ) ( n ⋅ v ) ⋅ 4 ( v ⋅ h ) D ( n ⋅ h ) ⋅ ( n ⋅ l ) = 1 N ∑ k = 1 N F G ( n ⋅ v ) ⋅ ( v ⋅ h ) ( n ⋅ h ) = 1 N ∑ k = 1 N ( F 0 + ( 1 − F 0 ) ( 1 − v ⋅ h ) 5 ) G ( n ⋅ v ) ⋅ ( v ⋅ h ) ( n ⋅ h ) = 1 N ∑ k = 1 N ( F 0 ⋅ [ 1 − ( 1 − v ⋅ h ) 5 ] + ( 1 − v ⋅ h ) 5 ) G ( n ⋅ v ) ⋅ ( v ⋅ h ) ( n ⋅ h ) = F 0 ⋅ [ 1 N ∑ k = 1 N ( [ 1 − ( 1 − v ⋅ h ) 5 ] ) G ( n ⋅ v ) ⋅ ( v ⋅ h ) ( n ⋅ h ) ] + [ 1 N ∑ k = 1 N ( 1 − v ⋅ h ) 5 G ( n ⋅ v ) ⋅ ( v ⋅ h ) ( n ⋅ h ) ]

通過上面的轉換,我們可以看出,我們将F0參數提到了求和公式的外面,也就是說,實際上我們不用考慮F0,隻需要考慮

scale=1N∑k=1N([1−(1−v⋅h)5])G(n⋅v)⋅(v⋅h)(n⋅h) s c a l e = 1 N ∑ k = 1 N ( [ 1 − ( 1 − v ⋅ h ) 5 ] ) G ( n ⋅ v ) ⋅ ( v ⋅ h ) ( n ⋅ h )

bais=1N∑k=1N(1−v⋅h)5G(n⋅v)⋅(v⋅h)(n⋅h) b a i s = 1 N ∑ k = 1 N ( 1 − v ⋅ h ) 5 G ( n ⋅ v ) ⋅ ( v ⋅ h ) ( n ⋅ h )

這兩個結果即可,然後在實際計算的時候,通過材質本身的albedo和metallic屬性計算出F0,然後通路預計算的scale和bais,即:

DFG=F0∗scale+bias D F G = F 0 ∗ s c a l e + b i a s

通過上面的描述,我們總結如下:

1.需要兩個輸入:roughness和ndotv

2.結果需要儲存為scale,bais兩個值

很自然的,我們就可以想到,使用一張2D貼圖來進行處理,其中u坐标表示ndotv,v坐标表示roughness。每一個像素的r表示scale,g表示bais,如下圖所示:

GraphicsLab Project之Physical based Shading-Image based Lighting(Specular篇)(二)引言優化方法LD項DFG項光照計算走樣貼圖總結參考文獻

下面給出計算該DFG項的代碼:

void DrawConvolutionCubeMapSpecularDFG() {
    // Setup shader
    render::Device::SetShader(m_SpecularDFGCubeMapProgram);
    render::Device::SetShaderLayout(m_SpecularDFGCubeMapProgram->GetShaderLayout());

    // Setup texture
    render::Device::ClearTexture();

    // Setup mesh
    render::Device::SetVertexLayout(m_ScreenMesh->GetVertexLayout());
    render::Device::SetVertexBuffer(m_ScreenMesh->GetVertexBuffer());

    // Setup render state
    render::Device::SetDepthTestEnable(true);
    render::Device::SetCullFaceEnable(true);
    render::Device::SetCullFaceMode(render::CULL_BACK);

    // Render Target
    render::Device::SetRenderTarget(m_SpecularDFGCubeMapRT);

    // View port
    render::Device::SetViewport(, , , );

    // Set Draw Color Buffer
    render::Device::SetDrawColorBuffer(static_cast<render::DrawColorBuffer>(render::COLORBUF_COLOR_ATTACHMENT0));

    // Clear
    render::Device::SetClearColor(f, f, f);
    render::Device::SetClearDepth(f);
    render::Device::Clear(render::CLEAR_COLOR | render::CLEAR_DEPTH);

    // Draw
    render::Device::Draw(render::PT_TRIANGLES, , m_ScreenMesh->GetVertexNum());
}
           

GLSL代碼:

vec3 convolution_cube_map(vec2 uv) {
    vec3 n = vec3(, , );
    float roughness = uv.y;
    float ndotv = uv.x;

    vec3 v = vec3(, , );
    v.x = sqrt( - ndotv * ndotv);
    v.z = ndotv;

    float scalar = ;
    float bias = ;

    // Convolution
    uint sampler = u;
    for (uint i = u; i < sampler; i++) {
        vec2 xi = hammersley(i, sampler);
        vec3 h = importance_sampling_ggx(xi, roughness, n);
        vec3 l =  * dot(v, h) * h - v;

        float ndotl = max(, l.z);
        float ndoth = max(, h.z);
        float vdoth = max(, dot(v, h));

        if (ndotl > ) {
            float G = calc_Geometry_Smith_IBL(n, v, l, roughness);

            float G_vis = G * vdoth / (ndotv * ndoth);
            float Fc = pow( - vdoth, );

            scalar = scalar + G_vis * ( - Fc);
            bias = bias + G_vis * Fc;
        }
    }


    vec3 color = vec3(scalar, bias, );
    color = color / sampler;

    return color;
}
           

光照計算

當我們成功的實作了如上兩個步驟之後,我們就可以在實際渲染的時候計算IBL了,如下GLSL代碼完成光照計算:

vec3 calc_ibl(vec3 n, vec3 v, vec3 albedo, float roughness, float metalic) {
    vec3 F0 = mix(vec3(, , ), albedo, metalic);
    vec3 F = calc_fresnel_roughness(n, v, F0, roughness);

    // Diffuse part
    vec3 T = vec3(, , ) - F;
    vec3 kD = T * ( - metalic);

    vec3 irradiance = filtering_cube_map(glb_IrradianceMap, n);
    vec3 diffuse = kD * albedo * irradiance;

    // Specular part
    float ndotv = max(, dot(n, v));
    vec3 r =  * ndotv * n - v;
    vec3 ld = filtering_cube_map_lod(glb_PerfilterEnvMap, r, roughness * );
    vec2 dfg = textureLod(glb_IntegrateBRDFMap, vec2(ndotv, roughness), ).xy;
    vec3 specular = ld * (F0 * dfg.x + dfg.y);

    return diffuse + specular;
}
           

其中glb_PrefilterEnvMap表示的就是LD項,而glb_IntegrateBRDFMap表示的就是DFG項。通過預計算,這裡的處理是不是變得十分的簡單,隻要擷取結果,然後簡單處理下就可以了。不使用材質貼圖的情況,得到如下的結果:

GraphicsLab Project之Physical based Shading-Image based Lighting(Specular篇)(二)引言優化方法LD項DFG項光照計算走樣貼圖總結參考文獻

走樣

如果你和我一樣用的OpenGL,那麼你可能會遇到如下圖所顯示的走樣問題:

GraphicsLab Project之Physical based Shading-Image based Lighting(Specular篇)(二)引言優化方法LD項DFG項光照計算走樣貼圖總結參考文獻

這個問題主要是因為,當我們使用更高的roughness的去采樣LD貼圖的時候,會采樣到更高的mipmap level。而更高的mipmap level意味着圖像的像素更少。這時候,采樣CubeMap不考慮周邊面的問題就變得十分突出。預設情況下,OpenGL對CubeMap的采樣,隻會在你給定的面裡面進行采樣。但是為了獲得無縫(seamless)的結果,你需要對周圍的面也進行采樣。

很幸運,在OpenGL中,提供了一個名為GL_TEXTURE_CUBEMAP_SEAMLESS的選項,它能夠開啟硬體對周圍CubeMap面進行采樣的功能。如下代碼:

如果你的庫裡面,不存在這個選項,那麼你要更新你的OpenGL。我這裡使用的是OpenGL4.5和glew2.0.0版本。

貼圖

前面的Demo,我都是使用參數來控制物體的材質,為了提供更加真實的效果,需要使用材質貼圖。是以我額外的寫了一個demo,來展示不同材質在PBS下的效果,這些效果可以在Github項目的首頁中看到,也可以在CSDN GraphicsLab學習項目的首頁看到。所有的材質貼圖,都是從這裡下載下傳。

總結

到了這裡,我們的PBS基礎功能已經實作完畢。之後可能會把這些demo特性內建到渲染器裡面去。同時在了解了這個基礎之後,就需要開始實作Light Probe以此來高效的在場景中使用IBL的功能,将又會有一大波功能和知識需要掌握,期待吧!

所有關于PBS的代碼,都已經上傳到了Github,這裡簡要的說明下各個項目的作用:

glb_pbs:用來展示PBS直接光照效果的demo,無貼圖材質

glb_ibl_diffuse:用來展示IBL中diffuse部分的demo,無貼圖材質

glb_ibl_specular_bruteforce:使用bruteforce的方式實作IBL中的specular部分,無貼圖材質

glb_ibl_specular_epic:使用epic的split approximation方式實作IBL中的specular部分,無貼圖

glb_pbs_texture:完整的PBS實作,具有直接光照,IBL光照,材質貼圖

參考文獻

[1]s2013_epic_pbs_notes_v2

[2]learnopengl.com

[3]moving frostbite to pbr

繼續閱讀