unity-shader-基于图像的光照IBL

IBL 全称是 Image Based Lighting,实际上就是做了环境反射,就跟原来反 Cubemap 做金属效果和水面是一样的,在IBL里稍微不一样的是,为了尽量真实的模拟粗糙表面的环境反射,则需要给 Cubemap 做一层 Blur,在引擎里面为了优化是采用了texCubelod,只需要把 Cubemap 的 lod 打开就行了,然后通过 Roughness 来决定所采 lod 的层级,这样就可以达到粗糙表面的模糊反射。


前篇


渲染方程

IBL 实际上就是做了环境反射, 反射部分 L0 = 漫反射-Diffuse + 高光-Specular
$$
L_{o}=\int_{\Omega}\left(f_{d}+f_{s}\right) L_{i}\left(p_{i}, w_{i}\right) n \cdot w_{i} d w_{i}
$$
可以把渲染方程拆成两个部分进行处理
$$
L_{o}=\int_{\Omega} f_{d} L_{i} n \cdot w_{i} d w_{i}+\int_{\Omega} f_{s} L_{i} n \cdot w_{i} d w_{i}
$$
对于这个方程,我们就可以将周围环境的所有光照信息保存在一张环境贴图中,而这个环境贴图就模拟了所有的Li。


环境贴图

制作环境贴图

参考总结: art-cubemap贴图制作.md

现在业界,对于IBL普遍使用的是Cube Map的形式。

环境光照贴图可以从 sIBL 中获取,这个网站里面有很多免费使用的HDR光照贴图


漫反射-Diffuse

公式

$$
L_{o}=\int_{\Omega} f_{d} L_{i} n \cdot w_{i} d w_{i}
$$

对于Diffuse部分的积分方程,我们知道如下的信息
$$
f_{d}=k D \frac{c}{\pi}
$$
也就是说,对于同一个点来说 fd 是一个常量,所以上述的方程可以简化为:
$$
L_{o}=f_{d} \int_{\Omega} L_{i} n \cdot w_{i} d w_{i}
$$

渲染方程球面坐标表示

也就是公里的这部分
$$
\int_{\Omega} L_{i} n \cdot w_{i} d w_{i}
$$
这部分就是个要将环境渲染到一个球面上, 也就是渲染到一个 cubemap 中, 下面用来采样, 作为反射值

此图也就是线面代码中的 glb_IrradianceMap 参数

光照计算

已经通过预计算保存在了 Cube Map 里面,所以我们只要根据法线 n 获取 Cube Map 里面对应的值,然后乘上剩下的 kD∗c 就可以了。以下是完整的代码:

1
2
3
4
5
6
7
8
9
10
11
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);

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

vec3 irradiance = filtering_cube_map(glb_IrradianceMap, n); // 采样 cubemap

return kD * albedo * irradiance;
}

需要额外说明的是,我们这里计算Fresnel系数的方式和前面直接光照系统的计算方式有所不同。这是因为对于IBL来说,我们没有办法得到一个单一的half向量,所以这里就直接使用了表面法线nn来代替。但是这样就丢失了表面粗糙度的影响,所以重新设计了新的Fresnel函数,并且将roughness属性考虑进去。以下是考虑了roughness属性的新的Fresnel系数计算函数:

1
2
3
4
vec3 calc_fresnel_roughness(vec3 n, vec3 v, vec3 F0, float roughness) {
float ndotv = max(dot(n, v), 0.0);
return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - ndotv, 5.0);
}

总结

PBR中的环境光的漫反射部分的计算.

  1. 采样 CubeMap !
  2. 通过球形坐标系来简化反射方程.
  3. 通过 黎曼和 来求解积分.
  4. 最后的结果也是存储在 CubeMap 中.

高光-Specular

公式

$$
L_{o}=\int_{\Omega} f s L_{i} n \cdot w_{i} d w_{i}
$$

在Diffuse部分的时候,我们只要使用Riemman Sum就能够计算渲染方程积分。但是对于Specular来说,它的效果更加精细,信号也是高频的,所以如果只是使用Riemman Sum来求解积分的话,误差较大,效果不够好,所以我们需要使用其他的积分求解方案。业界对于这个部分,常用的就是Monte Carlo积分,在此基础上使用Importance Sampling来进行加速,降低误差。

现在的光照流程变成了如下:
1.输入一张描述周围环境光照的CubeMap,然后预计算LD项,这个操作被称为Prefilter Environment Map。
2.预计算DFG项。
3.在实际渲染的时候,使用 LD * DFG 来得到Specular的IBL的结果。

LD项

$$
L D=\frac{1}{\sum_{k=1}^{N} \cos \theta_{k}} \sum_{k=1}^{N} L_{i}\left(l_{k}\right) \cos \theta_{k}
$$

此图也就是线面代码中的 glb_PerfilterEnvMap 参数

DFG项

$$
D F G=\frac{1}{N} \sum_{k=1}^{N} \frac{f\left(l_{k}, v\right) \cos \theta_{k}}{p\left(l_{k}, v\right)}
$$

我们知道,要计算这样的公式,我们需要如下的信息:

1.需要v,l,n
2.需要roughness
3.通过albedo和metallic来计算Fresnel系数中的F0项

毕竟这个图其实是通用的啊,复制粘贴走就可以,连代码都不需要抄,

此图也就是线面代码中的 glb_IntegrateBRDFMap 参数


总的 IBL 光照计算

  • 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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); // 采样 cubemap
    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); // 根据 粗糙度 采样有 mipmap 的 cubemap
    vec2 dfg = textureLod(glb_IntegrateBRDFMap, vec2(ndotv, roughness), 0.0).xy;
    vec3 specular = ld * (F0 * dfg.x + dfg.y);

    return diffuse + specular;
    }

球谐光照-Spherical Harmonics Lighting

简单的理解为一个 cubemap, 意思就是通过 球谐函数 预计算当前环境的所有光源信息 及 场景信息 到一个 环境贴图 cubemap 中, 实时渲染中去采样 cubemap 作为反射.


Unity PBS 中的 IBL 代码剖析

在材质上反应出周围的环境也是PBS的重要组成部分。在光照模型中一般把周围的环境当作一个大的光源来对待,不过环境光不同于实时光,而是作为间接光(indirect light)通过IBL( Image Based Lighting)来实现。间接光计算也包含 漫反射 部分和 镜面反射 部分。

实现代码主要在 UnityGlobalIllumination.cginc 文件中

Unity 的 BRDF 实现按平台分为3个档次,这里讨论的是针对 Console/PC 平台,光照模型更加精确的第1档实现 BRDF1_Unity_PBS。

Unity 内置了unity_Lightmap、unity_SHAr 等全局变量,来从预先烘焙好的 Lightmap 贴图或 light probe 中读取颜色,其中 UNITY_SHOULD_SAMPLE_SH 代码段处理的是从 light probe 中读取颜色值。一般渲染时静态物体读取lightmap,非静态物体读取light probe。

  • UnityGI_Base 函数返回的颜色值为 间接光的漫反射 部分。

  • UnityGI_IndirectSpecular 函数返回的颜色值为 间接光的镜面反射 部分。

    1
    2
    3
    4
    5
    6
    inline UnityGI UnityGlobalIllumination (UnityGIInput data, half occlusion, half3 normalWorld, Unity_GlossyEnvironmentData glossIn)
    {
    UnityGI o_gi = UnityGI_Base(data, occlusion, normalWorld);
    o_gi.indirect.specular = UnityGI_IndirectSpecular(data, occlusion, glossIn);
    return o_gi;
    }

    采样的 cubemap 是使用 Reflection Probe 生成的, 参考总结: unity-反射探针ReflectionProbe.md

另外,“粗糙的表面反射的光线分散且暗,光滑的表面反射集中且亮” 能量守恒在这里同样被遵守,函数输入参数包含粗糙度信息,用于环境光贴图的LOD取值:

1
2
half mip = roughness * UNITY_SPECCUBE_LOD_STEPS;
half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(tex, glossIn.reflUVW, mip);

表面越粗糙,用于采样 mipmap 贴图的 LOD 值越高,UNITY_SAMPLE_TEXCUBE_LOD 采样的结果 越模糊,反之亦然。

正如光照计算公式中多个光源的强度是叠加关系,PBS模型光照计算的结果是实时光BRDF与间接光IBL之和。BRDF1_Unity_PBS函数最后的颜色返回值代码:

1
2
3
4
half grazingTerm = saturate(oneMinusRoughness + (1-oneMinusReflectivity));
half3 color = diffColor * (gi.diffuse + light.color * diffuseTerm)
+ specularTerm * light.color * FresnelTerm (specColor, lh)
+ surfaceReduction * gi.specular * FresnelLerp (specColor, grazingTerm, nv);
  • gi.diffusegi.specular 分别是间接光的 漫反射 部分和 镜面反射 部分. ( 从形参获取 half4 BRDF1_Unity_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness, float3 normal, float3 viewDir, UnityLight light, UnityIndirect gi) )