PBR.IBL
概述:
IBL:image based lighting,一种间接光照(indirect lighting)技术,将周围的环境存在一张环境贴图(基于现实世界或3D场景生成)里面,然后将环境贴图上的每一个像素都当做一个光源发射器。这样我们能有效地捕捉环境的全局光照环境和大体感觉,让渲染物体有一种沉浸在环境中的视觉效果。在PBR pipeline中引入IBL环境光照可以让渲染结果更加的物理可信。
那么如何在PBR中实现IBL光照计算了,我们先来回忆一下PBR的反射率方程:
IBL和直接光照不一样(光照来源可枚举),每个材质表面上的点都需要计算半球领域上的所有入射光线。
那么两点:
(1)根据某个方向得到该方向上光照对应的辐射率?方法:贴图采样
vec3 radiance = texture(_cubemapEnvironment, w_i).rgb;
(2)需要解决实时积分运算的效率问题?方法:预处理卷积
首先将反射率方程的反射和折射部分分开计算:
那么我们就可以分开讨论两部分的优化实现方法。
一 折射部分,漫反射辐照度 Diffuse irradiance:
我们将不依赖积分因子的常量移出积分部分:
这样积分就只和wi入射光方向有关系了(在IBL计算中我们假设p处于环境的中心位置)。然后我们通过预计算的方式,生成一个新的cubemap(irradiance cubemap, convoluted cubemap),通过卷积计算将每个采样方向漫反射积分结果(diffuse integral's result)存储在cubemap对应的像素上。
卷积是将一些计算应用于数据集中的每一个条目,同时考虑到数据集中的所有其他条目。也就是说,新生成的cubemap每一个方向的采样结果,都已经直接考虑了半球领域里其他所有方向的采样值(最后取平均值),这样采样一次,就可以得到diffuse irradiance,效率问题解决。
左边是环境贴图cubemap,右边是预计算生成的irradiance map(辐照图贴图):
从任何方向采样这张辐照度贴图,都能得到该方向受到的场景辐照度(irradiance)。
vec3 irradiance = texture(irradianceMap, N);
PBR和HDR:
PBR和HDR紧密相联。irrandiance map使用每个像素存储indirect light intensity(间接光照强度)。物理上的环境光照范围很广,灯泡和太阳的光照强度差异非常之大,所以environment map光照强度的取值范围很广,也就是说这张环境贴图必须是HDR的。
普通的cubemap是LDR的(每个面各存储了一张普通LDR贴图),在使用时直接从某个面上的贴图采样颜色(颜色范围从0.0-1.0),这个小范围的值用来做颜色输出是没任何问题的,但是,当用来作PBR的物理输入参数时,0.0-1.0的取值范围就明显不够用了。
HDR辐射率格式文件(The radiance HDR file):
格式:***.hdr
存储:不使用每通道32位存储,而是使用每通道8位存储,然后使用alpha通道作为指数。不过这样确实会损失一些精度。
使用:需要手动做一次转换,将采样到的颜色值转换为对应的浮点值。
equirectangular map:从一个球体投射到平面上所得到的一张单一的图。多数情况,都采用水平视角来进行投影,不过也有从底部或顶部视角来投影的。
基本所有的HDR贴图都是默认处在线性空间。
diffuse lighting计算过程(重要):
因为IBL计算的是周围环境的光照影响,没有任何的直接光照,所以IBL计算出来的diffuse成分和specular成分都被当做ambient lighting(环境光)的组成部分。
首先引入预计算的irradiance map:
uniform samplerCube irradianceMap;
按照直接光照PBR的计算过程:
vec3 kS = fresnelSchlick(max(dot(N, V), 0.0), F0); vec3 kD = 1.0 - kS; vec3 irradiance = texture(irradianceMap, N).rgb; vec3 diffuse = irradiance * albedo; vec3 ambient = (kD * diffuse) * ao;
(1)因为光照来自于半球领域的所有方向,所有就没有直接光照的半角向量的概念,于是为了模拟Fresnel,我们使用法线和观察向量的夹角来计算Fresnel系数;
(2)因为没有考虑到roughness,所以反射率会偏高,按照直接光照的经验,我们希望粗糙的表面反射会弱一些,所以我们在计算菲涅尔系数时直接引入粗糙度,https://seblagarde.wordpress.com/2011/08/17/hello-world/:
vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) { return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0); }
引入粗糙度以后的计算过程:
vec3 kS = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); vec3 kD = 1.0 - kS; vec3 irradiance = texture(irradianceMap, N).rgb; vec3 diffuse = irradiance * albedo; vec3 ambient = (kD * diffuse) * ao;
从Equiretangular到Cubemap的转换(了解即可):
直接使用equirectangular map对比使用cubemap要耗费一些,因为需要额外的转换过程。
cubemap卷积计算思路(了解即可):
在半球领域按照立体角dw来计算有点困难,可将半球领域按照经纬度划分成小格子,然后根据经纬度计算积分:
反射率方程变为:
精度的取值范围是0-2π,纬度是0-1/2π,经纬度分别按照n1和n2进行刻度划分:
vec3 irradiance = vec3(0.0); vec3 up = vec3(0.0, 1.0, 0.0); vec3 right = cross(up, normal); up = cross(normal, right); float sampleDelta = 0.025; float nrSamples = 0.0; for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta) { for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta) { // spherical to cartesian (in tangent space) vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta)); // tangent space to world vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta); nrSamples++; } } irradiance = PI * irradiance * (1.0 / float(nrSamples));
二 反射部分,高光IBL Specular IBL:
这一部分我们将看到PBR真正的魅力。
specular积分项不是常量,而会依赖入射光方向、视角方向。这样积分运算非常之复杂,怎么办?
Epic Games提出了一个分段聚合近似(split sum approximation)方法,预计算specular积分项。
方法:将specular计算分为两部分,两部分可以单独卷积求解,然后乘在一起。
我们先来看一下反射率方程(只考虑specular部分):
按照Epic Games的方法分成两个积分项:
第一部分使用一张预生成的卷积计算的预过滤环境贴图(pre-filtered environment map),并考虑roughtness的影响。并且将每逐渐增加的roughness的卷积结果图以此存在pre-filtered map的mipmap levels里面。也就是使用cubemap的mipmap来实现对粗糙度的支持。
存储了5个不同roughness level的预过滤环境贴图(pre-filtered environment map):
Epic Games进一步通过以下假设来近似:
vec3 N = normalize(w_o); vec3 R = N; vec3 V = R;
这样卷积过程不需要考虑视线方向。那么从某些掠角观察的高光发射效果就不是很好,但总的来说这个方案是个不错的折衷。
第二部分,如果我们假设任何方向的入射光辐射率的值都是1,那么我们在给定roughness和n·wi的情况下可以算出BRDF的值。Epic Games将这些预计算的BRDF值存储在一张2D的LUT查询图(BRDF 2D LUT)里面,这张图叫做BRDF积分图(BRDF integration map),红色表示对菲涅尔系数的缩放,绿色表示对菲涅尔系数的偏移:
BRDF积分图是一张用于查询的LUT图,使用方法:
采样UV:(NdotV, roughness),第一个参数其实是n·wi,但同时采用V = R = N
采样值: x用于缩放菲涅尔系数F,y用于偏移F
float lod = getMipLevelFromRoughness(roughness); vec3 prefilteredColor = textureCubeLod(PrefilteredEnvMap, refVec, lod); vec2 envBRDF = texture2D(BRDFIntegrationMap, vec2(NdotV, roughness)).xy; vec3 indirectSpecular = prefilteredColor * (F * envBRDF.x + envBRDF.y)
Specular IBL计算过程(重要):
首先引入两张贴图:
uniform samplerCube prefilterMap;
uniform sampler2D brdfLUT;
使用反射向量(reflection vector)采样预过滤环境贴图(pre-filtered environment map)。这里我们需要依据粗糙度来采样cubemap对应的mipmap等级,越粗糙的表面其高光反射越模糊:
void main() { [...] vec3 R = reflect(-V, N); const float MAX_REFLECTION_LOD = 4.0; vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * MAX_REFLECTION_LOD).rgb; [...] }
然后依据材质的粗糙度和法线/视线方向的夹角查询BRDF 2D LUT贴图,
vec3 F = FresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); vec2 envBRDF = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg; vec3 specular = prefilteredColor * (F * envBRDF.x + envBRDF.y);=
现在结合IBL.Diffuse和IBL.Specular得到IBL的完整的PBR计算过程:
vec3 F = FresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); vec3 kS = F; vec3 kD = 1.0 - kS; kD *= 1.0 - metallic; vec3 irradiance = texture(irradianceMap, N).rgb; vec3 diffuse = irradiance * albedo; const float MAX_REFLECTION_LOD = 4.0; vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * MAX_REFLECTION_LOD).rgb; vec2 envBRDF = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg; vec3 specular = prefilteredColor * (F * envBRDF.x + envBRDF.y); vec3 ambient = (kD * diffuse + specular) * ao;
注意,specular部分没有乘以Ks,因为我们已经乘了菲涅尔系数。
说明:
上面提到的预处理的IBL贴图,都可以使用工具生成,比如cmftStudio或IBLBaker。
dds支持存储mip levels。