unity-shader-基于图像的光照IBL
IBL 全称是 Image Based Lighting,实际上就是做了环境反射,就跟原来反 Cubemap 做金属效果和水面是一样的,在IBL里稍微不一样的是,为了尽量真实的模拟粗糙表面的环境反射,则需要给 Cubemap 做一层 Blur,在引擎里面为了优化是采用了texCubelod,只需要把 Cubemap 的 lod 打开就行了,然后通过 Roughness 来决定所采 lod 的层级,这样就可以达到粗糙表面的模糊反射。
前篇
IBL 插件 - Skyshop的介绍 - https://blog.csdn.net/ys5773477/article/details/53502786
Unity的PBR扩展(二)——PBS代码剖析 ( 里面分析到了 IBL ) - https://zhuanlan.zhihu.com/p/49736244
猴子都能看懂的PBR(才怪) - https://zhuanlan.zhihu.com/p/33464301
渲染方程
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
- learnopengl - Diffuse irradiance - https://learnopengl.com/PBR/IBL/Diffuse-irradiance
- 基于图像的光照(Image Based Lighting)(Diffuse篇) (下面的计算来源于这篇文章) - https://blog.csdn.net/i_dovelemon/article/details/79091105
- 基于图像的光照IBL(Diffuse篇) - https://blog.csdn.net/qjh5606/article/details/89948573
公式
$$
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 | vec3 calc_ibl(vec3 n, vec3 v, vec3 albedo, float roughness, float metalic) { |
需要额外说明的是,我们这里计算Fresnel系数的方式和前面直接光照系统的计算方式有所不同。这是因为对于IBL来说,我们没有办法得到一个单一的half向量,所以这里就直接使用了表面法线nn来代替。但是这样就丢失了表面粗糙度的影响,所以重新设计了新的Fresnel函数,并且将roughness属性考虑进去。以下是考虑了roughness属性的新的Fresnel系数计算函数:
1 | vec3 calc_fresnel_roughness(vec3 n, vec3 v, vec3 F0, float roughness) { |
总结
PBR中的环境光的漫反射部分的计算.
- 采样
CubeMap
! - 通过球形坐标系来简化反射方程.
- 通过
黎曼和
来求解积分. - 最后的结果也是存储在
CubeMap
中.
高光-Specular
- learnopengl - Specular IBL - https://learnopengl.com/PBR/IBL/Specular-IBL
- 基于图像的光照(Image Based Lighting)(Specular篇)(一) - https://blog.csdn.net/i_dovelemon/article/details/79251920
- 基于图像的光照(Image Based Lighting)(Specular篇)(二) - https://blog.csdn.net/i_dovelemon/article/details/79598921
公式
$$
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
20vec3 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
- 球谐光照(Spherical Harmonics Lighting) - https://www.bbsmax.com/A/A7zgOmkz4n/
- 球谐光照(Spherical Harmonics Lighting)及其应用-实验篇 - https://www.bbsmax.com/A/A7zgOmkz4n/
简单的理解为一个 cubemap, 意思就是通过 球谐函数 预计算当前环境的所有光源信息 及 场景信息 到一个 环境贴图 cubemap 中, 实时渲染中去采样 cubemap 作为反射.
Unity PBS 中的 IBL 代码剖析
- Unity的PBR扩展(二)——PBS代码剖析 ( 里面分析到了 IBL ) - https://zhuanlan.zhihu.com/p/49736244
在材质上反应出周围的环境也是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
6inline 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 | half mip = roughness * UNITY_SPECCUBE_LOD_STEPS; |
表面越粗糙,用于采样 mipmap 贴图的 LOD 值越高,UNITY_SAMPLE_TEXCUBE_LOD 采样的结果 越模糊,反之亦然。
正如光照计算公式中多个光源的强度是叠加关系,PBS模型光照计算的结果是实时光BRDF与间接光IBL之和。BRDF1_Unity_PBS函数最后的颜色返回值代码:
1 | half grazingTerm = saturate(oneMinusRoughness + (1-oneMinusReflectivity)); |
- gi.diffuse 和 gi.specular 分别是间接光的 漫反射 部分和 镜面反射 部分. ( 从形参获取
half4 BRDF1_Unity_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness, float3 normal, float3 viewDir, UnityLight light, UnityIndirect gi)
)