作者: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的魅力。
优化方法
首先,我们给出我们需要计算的渲染方程:
在前面一篇文章里面,我们知道了,可以通过Importace Sampling,使用Monte Carlo积分来求解上面的积分方程,如下:
为了加速对该公式的计算,Unreal4将上述公式划分成了两个不同求和部分(为什么我想不出来这种近似的方法,唉!!!),这两个不同的求和部分能够分别通过预先计算来得到结果。
从上面的公式中可以看出,我们能够分别计算这两个求和部分。所以,我们通过预计算的形式,将两个不同的部分预先计算好结果,等到实际进行光照计算的时候,我们只要获取预计算的结果,然后直接进行计算即可。我们分别将上面两个求和部分命名为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项的公式如下所示:
为了获得 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的结果:
下面的代码展示了如何对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项的公式如下:
我们知道,要计算这样的公式,我们需要如下的信息:
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公式进行一点变化。
我们知道如下的关系:
所以:
通过上面的转换,我们可以看出,我们将F0参数提到了求和公式的外面,也就是说,实际上我们不用考虑F0,只需要考虑
这两个结果即可,然后在实际计算的时候,通过材质本身的albedo和metallic属性计算出F0,然后访问预计算的scale和bais,即:
通过上面的描述,我们总结如下:
1.需要两个输入:roughness和ndotv
2.结果需要保存为scale,bais两个值
很自然的,我们就可以想到,使用一张2D贴图来进行处理,其中u坐标表示ndotv,v坐标表示roughness。每一个像素的r表示scale,g表示bais,如下图所示:
下面给出计算该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项。通过预计算,这里的处理是不是变得十分的简单,只要获取结果,然后简单处理下就可以了。不使用材质贴图的情况,得到如下的结果:
走样
如果你和我一样用的OpenGL,那么你可能会遇到如下图所显示的走样问题:
这个问题主要是因为,当我们使用更高的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