四、蒙特卡洛积分
蒙特卡洛积分在前篇计算漫反射项时就已经应用过了
对于定积分 ,如果无法简单求得原函数,一个尝试去求得其近似解的方法就是蒙特卡洛积分:
- 在积分区间
上进行均匀采样得到样本
,就可以求和得到
,随着样本数量
的增大,其结果也会逐渐收敛到积分的精确解
这个方法和黎曼和的求法非常类似: 就是图中矩形的宽,而
就是对应矩形的高
4.1 概率密度函数
不过上面的计算没有考虑一个东西,那就是对于样本 ,它不一定是均匀分布的。对于前面章节漫反射的预计算,我们可以认为任意方向的光源对出射向量
的贡献都是一致的,这就意味着以下两点
- 积分仅依赖于
,而不依赖于
- 生成的样本均匀分布,就意味着随着样本数量的不断增加,我们最终将收敛到积分的精确解(无偏估计)
![](https://i-blog.csdnimg.cn/blog_migrate/5df20ccb1723cc2ac6e24579425cabfd.png)
但是镜面反射就不一定了,任意方向的光源对出射向量 的贡献并不一致:极端情况对于绝对光滑的平面,有且只有一条光线对
有 1.0 的贡献,其它光线对其贡献都为 0.0。这时如果再对入射光方向
进行随机采样,必然会得出错误的结果,因为你无论怎么采样,都几乎不可能正好命中到那条有 100% 贡献的入射光,这时得出的辐射率结果就会为 0,毕竟这些向量都与积分无关。就算你运气好,其中一次采样刚好命中了那条有 100% 贡献的入射光,你会发现随着采样次数变多,得出来的结果也会越加趋近于 0,也就是收敛到一个错误值
怎么算都不对,这就是因为没有考虑概率密度函数,由于这个那个例子的有偏性,随机采样可能永远不会收敛到精确解,或者说是会收敛到一个错误解
那么这个问题怎么破呢?这就要引用概率密度函数:
众所周知:
- 假设某一连续型随机变量
的样本空间为
,其概率密度分布函数为
,数学期望就为
- 若另一连续随机变量
满足
,则
的数学期望为
这样对于上面绝对光滑平面镜面反射的例子,就可以认为对于那条有 100% 贡献的入射光 ,其概率密度函数
满足
,其它无贡献的入射光
。换成用概率密度函数的蒙特卡洛积分描述就是:
- 在积分区间
上进行采样得到样本
(这个采样要根据概率密度函数指定规则,并非均匀采样),最终积分结果就可近似为:
这里也可以发现前面推导的 其实就是上面的一种特殊情况:均匀分布,即
满足
4.2 重要性采样
还有一个问题需要解决,那就是对于非均匀分布的样本,如何对其采样才可得到准确的收敛结果呢?
可以再举一个例子:现在要统计全国大学生的平均身高,有以下2个方案:
- 直接挨家挨户调查全国所有大学生每一个人的身高,然后求其平均
- 随便进入一所大学,统计这所大学里面所有学生的身高,然后求其平均作为近似解
可见,选择方案②确实是一个很不错的选择,它就是随机采样的一个案例,但是现在有这样一个问题:这所大学是文科大学,也因此男女比例是 1:2,要知道女性的平均身高是要低于男性的,这样若只是单纯的随机抽样必然会得出错误的结果。对于这个问题,该如何设计采样方案,才能使得最终的结果尽可能的客观真实呢?
没错这就是一道初中题,但是他和我们现在要解决的问题却是一致的
我们已知 在概率分布
的期望为
,这样就是要找一个采样分布函数
满足
,也就是函数
在概率分布
的期望可以被看作函数
在概率分布
上的分布,并且这个
是支持均匀采样的,这样就可以得到:
- 在分布区间
上进行采样得到样本
,最终积分结果就可近似为:
,其中样本
为实际均匀样本
关于
的映射
,
就是重要性权重
回到上面那个统计身高的例子,假设男生 , 女生
,那么就可以得到对应的概率密度函数
,一个计算
的方向是,尽可能保证
和需要积分的函数
尽可能接近,此时
,完美,用简单的话来讲就是:每次抽样到男生的时候,都对他的身高加权,权值为女生的两倍。又或者说将男生分成一模一样的两份,和女生混合后作为样本从中抽取求平均
五、镜面IBL预滤波项
对于反射方程的镜面反射项,可以进行卷积近似:
其中 这一部分被称为预滤波项,和上一章的辐照度图一样,这里也可以用一样的方式预处理,不过这次在确定入射光线
时,反射光线
的分布并不均匀(反过来也一样),并且同时还受表面的粗糙度影响
就如上图,确定入射光线 时,所有的反射光线
构成的形状称为镜面波瓣,随着粗糙度的增加,镜面波瓣的大小增加;随着入射光方向不同,形状也会发生变化,因此对于影响光线分布的两个因素:粗糙度(Roughness) 和光方向,就需要做额外的考量
5.1 低差异序列(Low Discrepancy Sequence)
确定粗糙度时,由于不同方向上光线密度不同,需要引用上一节中的重要性采样来确保积分结果的正确收敛以及得到更快的收敛速度,当然还可以使用一个黑科技,那就是低差异序列:
低差异序列很简单,可以理解为完全随机采样和均分采样的一个折中,一个经典的低差异序列是 Hammersley 序列,关于低差异序列和随机,体量不小可以后面开个专篇讲,这里暂时了解下就好了
- 对于一个在
区域内的点集,任意选取一个空间中的区域
,此区域内点的数量
和点集个数的总量
的比值和此区域的体积
的差的绝对值的最大值,就是这个点集的差异(Discrepancy) ,即有
,分布越均匀的点集,任意区域内的点集数量占点总数量的比例也会越接近于这个区域的体积
float RadicalInverse_VdC(uint bits)
{
bits = (bits << 16u) | (bits >> 16u);
bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}
// ----------------------------------------------------------------------------
vec2 Hammersley(uint i, uint N)
{
return vec2(float(i)/float(N), RadicalInverse_VdC(i));
}
上面就是 Hammersley 序列的具体实现,除此之外在 openGL.cn 的网站上,也有 Hammersley 序列的非位运算版本
5.2 基于GGX的重要性采样
在这一章中,了解了几个 NDF,其中一个重要的 GGX - NDF 如下:
,使用倾斜角
和航向角
来表示就是
为了求得累积分布函数(Cumulative Distribution Function),也就是是概率密度函数的积分,需要先求得 和
的边缘概率密度函数:
然后就可以得到 CDF:
定义前面依据低差异序列生成的随机向量 对应航向角
和倾斜角
,那么就有
这基于逆变换算法:对于概率分布函数 ,以及它的累积分布函数
,根据性质可以知道它的值域是一定是分布在
区间内的,这样通过它的反函数就可以实现输入
之间的随机数,得到符合概率密度分布的采样点。这个反函数也正对应着前一节的
,openGL.cn 中的参考代码如下:
vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float a)
{
float phi = 2.0 * PI * Xi.x;
float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y));
float sinTheta = sqrt(1.0 - cosTheta*cosTheta);
// from spherical coordinates to cartesian coordinates
vec3 H;
H.x = cos(phi) * sinTheta;
H.y = sin(phi) * sinTheta;
H.z = cosTheta;
// from tangent-space vector to world-space sample vector
vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
vec3 tangent = normalize(cross(up, N));
vec3 bitangent = cross(N, tangent);
vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z;
return normalize(sampleVec);
}
5.3 粗糙度(Roughness) 和光方向考量
好了,接下来就是如何处理不同粗糙度和光方向
粗糙度:关于粗糙度的处理非常的暴力,那就是对于每个特定粗糙度级别都算一遍卷积的结果,并把它们存在预滤波环境贴图的 mipmap 中,对于中间的粗糙度可以对两张 mipmap 进行插值
![](https://i-blog.csdnimg.cn/blog_migrate/50bfec9f856a6cf5a232c2fd4eda55c2.png)
光方向:规定镜面反射方向 、以及视角方向
相等,此时也满足
=
,这样掠射角方向可能会得出不太正确的结果,但是也算是一种折中
由于低级的 mipmap 精度不够,在这个过程中需要注意的是:
- 在立方体贴图边缘插值时,需要考虑相邻的立方体面
- 在采样环境贴图的时,最好对同级的环境贴图 mipmap 进行采样,否则在采样精度不足的情况下可能会出现噪点
这里更多的是实践
六、预计算 BRDF 项
接下来就是计算 BRDF 项:
它受到3个参数影响:、粗糙度
以及菲涅尔系数
,但是一个好消息是,由于不需要考虑光照辐射度
(已经在前一节被抽离出来了),因此在这3个值确定时,得出来的结果也是一个定值
离线打表的手微微颤抖,这不就又可以打表嘛,直接开个3维常量数组,把结果算好打进去就可以了。但如果只需要存2维数据的话,是不是就可以将这些数据存到纹理中而不是作为常量呢?很显然是可以的!
6.1 BRDF项查找纹理
一张非常著名的 2D 查找纹理(Look Up Texture, LUT) 横空出世:其中
- 横向对应
,也就是
,范围刚好是 [0, 1],
- 纵向对应粗糙度常数
,范围也是 [0, 1]
- 纹理 R 通道为积分比例项,G 通道为积分偏差项(至于比例项和偏差项是什么可以看后面的推导)
那现在要解决的问题就是:对于3个输入参数:、粗糙度
以及菲涅尔系数
,需要先干掉一个参数
,步骤如下:
- 尝试把 NDF 中的菲涅尔项 F 移出去:
- 对于右边的
将其展开:
- 用
替换
,可以得到:
最后拆掉,并且将常量移出积分,就可以得到:
其中 为
的比例项,
为偏差项,搞定!
6.2 完成IBL反射
按理说到这里就要结尾了,不过还是有一个东西要提一下
在计算阴影遮蔽函数(几何函数G)时,对于 ,其一般在针对光照和针对 IBL 时其
项不同:这里应用第二个
- 针对光照时:
- 针对 IBL 的重映射(Remapping):
除此之外,生成的 LUT 应该是浮点数存储,一般都为16位精度浮点格式
……
OK,搞定所有的离线计算后,计算实时光照时就非常简单了:
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);
//因为NDF已经乘过了菲涅耳系数,所以这里不需要KS
vec3 ambient = (kD * diffuse + specular) * ao;
如果顺利的话,就可以得到一个非常不错的效果:
当然这些都是 learnopengl.cn 上的效果,后面会有一篇文章,尝试在 Unity 中实现 PBR(造轮子)
参考文章:
- https://zhuanlan.zhihu.com/p/337044893
- https://learnopengl-cn.github.io/07%20PBR/03%20IBL/02%20Specular%20IBL/
- https://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf