理论支线:PBR - 基于图像的照明( image based lighting-IBL)

介绍

这通常通过作立方体贴图环境贴图(取自现实世界或从三维场景生成)实现,使我们能直接将其用于光照方程:将每个立方体贴图像素视为光发射体。这样我们就能有效捕捉环境的全局光照和整体感觉,让物体在环境中有更好的归属感。

IBL渲染方程方程

\[L_o(p,\omega_o) = \int\limits_{\Omega}(k_d\frac{c}{\pi} + k_s\frac{DFG}{4(\omega_o \cdot n)(\omega_i \cdot n)}) L_i(p,\omega_i) n \cdot \omega_i d\omega_i\]

这里可以拆分成两个部分

\[L_o(p,\omega_o) = \int\limits_{\Omega} (k_d\frac{c}{\pi}) L_i(p,\omega_i) n \cdot \omega_i d\omega_i + \int\limits_{\Omega} (k_s\frac{DFG}{4(\omega_o \cdot n)(\omega_i \cdot n)}) L_i(p,\omega_i) n \cdot \omega_i d\omega_i\]

这里其实看公式可以知道这就是漫反射 + 镜面反射,这个内容和直接光是类似的 那么环境光漫反射其实就是SH(球谐光照),镜面反射则是源自于环境反射的HDR图,这里主要是讲镜面反射

采样

通常采用是texCUBElod,因为需要将粗糙度和mipLevel相关联。

1
2
3
float mipLevel = roughness* 7.0;
float4 envSpecularRaw = texCUBElod(_CubeMap, float4(reflviewDir, mipLevel));
half3 envSpecular = DecodeHDR(envSpecularRaw, _CubeMap_HDR); 

与GGX的关系和使用方法

直接光照的镜面反射(GGX)

\[f_{DirectSpecular}^{GGX}(l, v) = \frac{D(h) \cdot F(v, h) \cdot G(l, v, h)}{4 \cdot (n \cdot l) \cdot (n \cdot v)} \cdot (n \cdot l) \cdot L_{light}\]

其中:

\[F(v, h) = F_0 + (1 - F_0)(1 - v \cdot h)^5\]

Environment BRDF

对于直接光,是只需要处理单一光源,所以F和G项都是实时计算的,但是对于环境光镜面反射来说,光线是四面八方传来,这意味着实时计算需要计算无数光线方向,这样肯定是不行的,所以环境光需要一个更好的处理办法,那就是Environment BRDF

它是一张2D查找表(LUT),描述镜面反射能量随观察角度的衰减曲线,其实就是控制衰减形状和距离程度 想象一张图片:

  • 横轴(X): NoV (法线和视角夹角余弦值,范围0→1)
  • 纵轴(Y): roughness (粗糙度,范围0→1)
  • 像素值: RG两通道 存储 (scale, bias) 两个数值

环境光照的镜面反射(IBL)

简化只处理菲涅尔项

能用,几乎没有衰减也就是比较明亮,卡通渲染应该比较合适

\[f_{IBLSpecular} = \underbrace{L_{prefiltered}(r, \text{roughness})}_{\text{D和G已经在这里了!}} \cdot F(n,v)\]

其中: F项发生了变化,因为环境光是四周辐射的而非根据视线方向,所以它的因子变成了法线

\[F(n, v) = F_0 + (1 - F_0)(1 - n \cdot v)^5\]

使用近似拟合方法

1
2
3
4
5
6
7
8
9
10
float2 EnvBRDFApprox(float roughness, float NoV)
{
    // Epic Games通过拟合大量数据得出的魔法系数
    const float4 c0 = float4(-1.0, -0.0275, -0.572, 0.022);
    const float4 c1 = float4(1.0, 0.0425, 1.04, -0.04);
    float4 r = roughness * c0 + c1;
    float a004 = min(r.x * r.x, exp2(-9.28 * NoV)) * r.x + r.y;
    float2 AB = float2(-1.04, 1.04) * a004 + r.zw;
    return AB;  // AB.x = scale, AB.y = bias
}

使用近似后和仅处理理菲涅尔项的对比

使用Unity默认LUT

IBL贴图过滤采样中处理D和G项,但G项并不完整或者没有。所以更好的方案是在额外计算F和G积分项,也就是使用LUT图 Epic Games的聪明解决方案:拆成两部分

\[\int L_i \cdot BRDF \approx \underbrace{\int L_i \cdot D}_{\text{第1部分:预过滤环境贴图}} \times \underbrace{\int \frac{FG}{4(n \cdot v)} }_{\text{第2部分:Environment BRDF}}\]

关键思想:

  • 第1部分只和环境光、粗糙度有关 → 提前烘焙成Cubemap的不同mip层
  • 第2部分只和材质参数(roughness, NoV)有关 → 提前计算成一张2D表

采样Unity默认LUT

1
2
 float2 brdfLUT = tex2D(unity_NHxRoughness, float2(roughness, nov)).rg;
 envSpecular =  envSpecular * (f0 * brdfLUT.x + brdfLUT.y);

使用自定义LUT

图片:https://learnopengl-cn.github.io/07%20PBR/03%20IBL/02%20Specular%20IBL/

这里使用了一个_UseCustomLUT作为开关

1
2
3
4
5
6
7
8
9
10
11
12
13
[Header(EnvBRDFLUT)]
[Toggle]_UseCustomLUT("Custom BRDF LUT", float) = 1.0
_CustomBRDFLUT("Custom BRDF LUT", 2D) = "white" {}
----------------------------------------
//采样BRDFLUT
float2 brdfLUT = tex2D(unity_NHxRoughness, float2(roughness, nov)).rg;
float2 CustombrdfLUT = tex2D(_CustomBRDFLUT, float2(roughness, nov)).rg;
                
if(_UseCustomLUT > 0.5)
{
    brdfLUT = CustombrdfLUT;
}
envSpecular =  envSpecular * (f0 * brdfLUT.x + brdfLUT.y);

对比

项目 直接光(GGX) 环境光(IBL)
完整公式 $\frac{DFG}{4(n \cdot l)(n \cdot v)} \cdot (n \cdot l)$ $L_{prefiltered}(r) \cdot F$
D项 D(h) 实时计算 预计算到贴图Mip里
F项 $F_0 + (1-F_0)(1-v \cdot h)^5$ $F_0 + (1-F_0)(1-n \cdot v)^5$
G项 G(l,v,h)实时计算 预计算到贴图里
     
项目 直接光照(GGX) 环境光照(IBL)
F公式 $F_0 + (1-F_0)(1-v \cdot h)^5$ $F_0 + (1-F_0)(1-n \cdot v)^5$
输入角度 VoH NoV
物理含义 光线在微表面上的菲涅尔 宏观表面的菲涅尔
计算时机 每个光源都要算 算一次就够
  • 环境光来自各个方向,没有单一的L
  • 近似认为反射方向R周围的锥形区域贡献最大
  • 这个锥形区域的中心轴 ≈ 法线N
  • 所以用NoV(法线到视角)

代码参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//环境光漫反射
float3 f_indirect = f0 + (1-f0) * pow(1 - max(0,(dot(viewDir, pixelNormal))), 5);
float3 kD_indirect  = (1.0 - f_indirect) * (1.0 - metallic);
float3 indirectDiffuse  = kD_indirect * envColorSH;
            
//环境光镜面漫反射
float mipLevel = roughness* 7.0;
float4 envSpecularRaw = texCUBElod(_CubeMap, float4(reflviewDir, mipLevel));
half3 envSpecular = DecodeHDR(envSpecularRaw, _CubeMap_HDR); 

//1.拟合BRDF(未使用)
float2 envBRDF = EnvBRDFApprox(roughness, nov);
float3 envSpecularEasy = envSpecular * f_indirect;
//2.采样BRDFLUT
float2 brdfLUT = tex2D(unity_NHxRoughness, float2(roughness, nov)).rg;
float2 CustombrdfLUT = tex2D(_CustomBRDFLUT, float2(roughness, nov)).rg;
            
if(_UseCustomLUT > 0.5)
{
    brdfLUT = CustombrdfLUT;
}
envSpecular =  envSpecular * (f0 * brdfLUT.x + brdfLUT.y);
            
//AO
float AO = tex2D(_AOTex, texcoord).r;

最终环境光的混合需要和AO相乘但和直接光的阴影不相干,有些教程里面将AO直接和baseColor相乘这其实是错误的。 混合输出方式参考

1
2
//output
float3 finalColor = (pbrDiffuseColor + pbrSpecularColor) * shadow + (indirectDiffuse + envSpecular )* AO ;



    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • URP - RendererFeature :ScreenSpaceOutline
  • 平滑法线处理 - 八面体映射
  • Lv.3 Unity主线:一个简单的PBRShader
  • 理论支线:直接光漫反射与GGX高光的混合问题
  • Lv.3 Unity主线:视差(Parallax Mapping)

  • # #