GraphicsLab Project之Physical based Shading-Image based Lighting(Specular篇)(二)

作者: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篇)(二)_第1张图片


优化方法


首先,我们给出我们需要计算的渲染方程:

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积分来求解上面的积分方程,如下:
L01Nk=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将上述公式划分成了两个不同求和部分(为什么我想不出来这种近似的方法,唉!!!),这两个不同的求和部分能够分别通过预先计算来得到结果。
L01Nk=1NLi(lk)f(lk,v)cosθkp(lk,v)(1Nk=1cosθkk=1NLi(lk)cosθk)(1Nk=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=1Nk=1cosθkk=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篇)(二)_第2张图片

下面的代码展示了如何对LD项进行计算:

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

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

    // 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(256) / log(2) + 1;
    float roughnessStep = 1.0f / miplevels;
    int32_t width = 256, height = 256;
    for (int32_t j = 0; j < miplevels; j++) {
        // Render Target
        render::Device::SetRenderTarget(m_SpecularLDCubeMapRT[j]);

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

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

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

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

            // Draw
            render::Device::Draw(render::PT_TRIANGLES, 0, 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 = 1024u;
    vec3 color = vec3(0.0, 0.0, 0.0);
    float weight = 0.0;
    for (uint i = 0u; i < sampler; i++) {
        vec2 xi = hammersley(i, sampler);
        vec3 h = importance_sampling_ggx(xi, glb_Roughness, n);
        vec3 l = 2.0 * dot(v, h) * h - v;

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

    color = color / weight;

    return color;
}


DFG项


DFG项的公式如下:

DFG=1Nk=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(nl)(nv) f ( l k , v ) = D F G 4 ( n ⋅ l ) ( n ⋅ v )

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

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

所以:
DFG=1Nk=1Nf(lk,v)cosθkp(lk,v)=1Nk=1NDFG4(nl)(nv)4(vh)D(nh)(nl)=1Nk=1NFG(nv)(vh)(nh)=1Nk=1N(F0+(1F0)(1vh)5)G(nv)(vh)(nh)=1Nk=1N(F0[1(1vh)5]+(1vh)5)G(nv)(vh)(nh)=F0[1Nk=1N([1(1vh)5])G(nv)(vh)(nh)]+[1Nk=1N(1vh)5G(nv)(vh)(nh)](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=1Nk=1N([1(1vh)5])G(nv)(vh)(nh) s c a l e = 1 N ∑ k = 1 N ( [ 1 − ( 1 − v ⋅ h ) 5 ] ) G ( n ⋅ v ) ⋅ ( v ⋅ h ) ( n ⋅ h )

bais=1Nk=1N(1vh)5G(nv)(vh)(nh) 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=F0scale+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篇)(二)_第3张图片


下面给出计算该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(0, 0, 128, 128);

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

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

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

GLSL代码:

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

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

    float scalar = 0.0;
    float bias = 0.0;

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

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

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

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

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


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

    return color;
}


光照计算


当我们成功的实现了如上两个步骤之后,我们就可以在实际渲染的时候计算IBL了,如下GLSL代码完成光照计算:

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

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

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

    // Specular part
    float ndotv = max(0.0, dot(n, v));
    vec3 r = 2.0 * ndotv * n - v;
    vec3 ld = filtering_cube_map_lod(glb_PerfilterEnvMap, r, roughness * 9.0);
    vec2 dfg = textureLod(glb_IntegrateBRDFMap, vec2(ndotv, roughness), 0.0).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篇)(二)_第4张图片


走样


如果你和我一样用的OpenGL,那么你可能会遇到如下图所显示的走样问题:


GraphicsLab Project之Physical based Shading-Image based Lighting(Specular篇)(二)_第5张图片


这个问题主要是因为,当我们使用更高的roughness的去采样LD贴图的时候,会采样到更高的mipmap level。而更高的mipmap level意味着图像的像素更少。这时候,采样CubeMap不考虑周边面的问题就变得十分突出。默认情况下,OpenGL对CubeMap的采样,只会在你给定的面里面进行采样。但是为了获得无缝(seamless)的结果,你需要对周围的面也进行采样。

很幸运,在OpenGL中,提供了一个名为GL_TEXTURE_CUBEMAP_SEAMLESS的选项,它能够开启硬件对周围CubeMap面进行采样的功能。如下代码:

glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);  // For cubemap seamless filtering

如果你的库里面,不存在这个选项,那么你要更新你的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

你可能感兴趣的:(3D引擎,OpenGL,GraphicsLab,Project,图形分析,GPU,游戏开发,算法设计,Shader,图形试验室)