球面高斯函数05——用球面高斯函数近似辐射率和辐射度

这是本系列最后一篇笔记。前两篇讨论了如何在使用球面高斯光源时近似出漫反射和镜面反射。但依然没有说明为什么要使用球面高斯光源照亮场景。回顾本系列第一篇的内容,讨论的是如何将预计算的辐射率和辐射度存储在光照贴图或探针网格中。这也是最后一篇要讨论的主题。

本系列的其他文章可以点击下方链接跳转。

寻找最佳匹配度

数学(尤其是统计学)中的一个常见过程就是获取一组数据点,尝试找出某种可以表示数据的分析曲线,这个过程被称为曲线拟合。也可以认为是一种有损压缩,因为几百个数据点可能需要上千字节的数据,但如果用曲线去近似可能只需要存储几个字节。下图是维基百科的一个示例,图像中有一堆需要去拟合的点,这些数据点来自于正弦波,但实际上可能是各种各样的形式。下方的公式分别对应红线,绿线,橙线和蓝线。

从上图可以看出更复杂的公式可以形成更复杂的曲线,但需要更多的系数,需要存储更多的数据,并意味着拟合过程更加困难或开销越大。用于拟合的常见技术之一是最小二乘法,将拟合曲线和原始数据差异的总和进行最小化。

另外可以发现最终的拟合曲线其实是多个基函数的线性组合,如$x,x^2,x^3$等。其实可以使用其他的基函数来代替,比如高斯函数!同多项式一样,高斯的总和可以用少数几个系数表示更复杂的函数。例如,根据一些点使用最小二乘法拟合不同数量的高斯函数,下图左显示了一个高斯拟合,图中为两个高斯拟合,图右是三个高斯拟合。

不难发现,添加更多高斯函数时,拟合的结果更接近原始数据。

球面拟合

这种拟合数据的方式不仅可以作用于一维的数据集,也可以是多维度的。例如在一个由2维球面坐标系定义的球面上有一些随机方向的样本。假设这些样板代表在该方向上无限狭窄的入射光。如果对这些数据进行最小二乘法,则得到N个球面高斯,它们相加之和可以作为球面上任意方向上辐射率的近似值。只需要拟合算法可以得出每个球面高斯的轴向,振幅和锐度,或者提前固定一些球面高斯的参数值,然后仅拟合其余参数。与庞大的辐射率样本相比,一组球面高斯系数占用的存储空间是非常少的。但如果仅使用几个球面高斯函数,得到的近似值相比原始数据会丢失很多细节。但这与球谐函数等其他存储辐射率或辐射度近似值的方式没有什么区别。下图使用HDR环境贴图作为输入数据,分别用12个和24个球面高斯做最小二乘法拟合之后的结果。

要注意的是,该拟合值作用于球面高斯的振幅上,轴向和锐度是基于球面高斯的数量提前确定好的。求解单个参数允许使用线性最小二乘法,同时拟合所有参数需要使用复杂和开销大的非线性最小二乘法。求解较少的参数降低了存储成本,因为每个探针只需要存储振幅,轴向和锐度可以用全局的常量控制。当然,增加波瓣的数量会增加复杂性和清晰度,会保留原始图像更多的高频细节。

负数

球谐函数多项式中使用了负系数所以会有“Ringing”效果,球面高斯也有类似问题。上图中的球面右下角有过暗的区域。最小二乘法为了减少拟合的误差,某些波瓣返回了负系数。假如是一维的情况,有一个连续性很差的点集。用最小二乘法使两个高斯波瓣去拟合,情况如下。

优化过程中,第一个波瓣的振幅为正,第二个波瓣的振幅为负。这就导致了最终会有过暗的区域。第一篇笔记中有提到过球谐函数的类似情况以及解决办法。

那么我们需要给最小二乘法加上约束保证返回正系数。好在已经有了名为非负最小二乘法(non-negative least squares, NNLS)。如果使用该方法用球面高斯拟合原始的辐射率,那么结果会自然和正确很多。下图是HDR环境贴图、12个和24个球面高斯使用NNLS拟合之后的结果。

虽然右下角的明暗趋于正常了,但代价是相比原来的方式,近似的结果更加的模糊。

辐射度比较

下面使用不同方式近似辐射率,使用辐射率直接计算出兰伯特漫反射光照的辐射度并比较它们的结果。

除了Ambient Cubemap,其他的近似值都很好。但该HDR环境贴图的照明比较柔和,不容易看出问题,下面使用明暗变化比较极端的HDR环境贴图进行比较。

这次的差异就比较明显了。有大面积的明亮光源的情况,球面高斯的效果更胜一筹,尤其是NNLS版本的12个球面高斯。

因为波瓣方向和锐度都是固定值,那么只需要储存振幅的数据即可。即12个球面高斯波瓣的存储成本等于12组RGB系数(总共36个浮点数)。L2的球谐函数需要9组RGB系数(27个浮点数)。环境立方体贴图仅需要6组RGB系数,仅为球面高斯的一半,所以使用球面高斯需要更多存储空间。然而换个角度,这也是球面高斯的优点。立方体贴图只能使用6个波瓣,职业才可以保持正交性;而球面高斯可以根据质量、性能和存储成本使用任意数量的波瓣。

半球拟合

因为实际烘焙光照时,样本点存在于模型表面,所以有一半的球体是位于模型背面的,计算这部分是毫无意义的。我们只需要对模型表面的半球部分做光照的预计算然后存储起来即可。而对于球面高斯来说是很好修改的,只需要求解位于围绕面法线的上半球内的波瓣即可。

采样整个场景为光照贴图每个纹素生成辐射率的近似值,最好的方式是使用光线追踪器(ray tracer),使用诸如路径跟踪(path tracing)之类的算法来计算特定光线的入射辐射率。下图展示了一个场景的光照贴图的可视化效果,为光照贴图每个纹素生成半球辐射率探针,每个探针使用9个围绕面法线朝向的球面高斯。

镜面反射

上篇文章介绍了如何从球面高斯光源计算出镜面反射项。那么在近似入射辐射率方面使用该方式。可以计算出整个场景镜面反射的近似效果。数量较少的波瓣只能应对粗糙度较高的材质,因为球面高斯对入射辐射率的近似不能捕获环境的高频细节。下图是粗糙度为0.25的GGX分布项的测试场景。

与真实的效果相比,球面高斯在某些地方的近似不错,在某些地方的近似差一些。但总体来说比L2的球谐函数结果好不少,而且存储成本相同(27个浮点数)。球谐函数使用3D查找纹理存储预计算的球谐系数,如果仔细观察垂直相机的墙面就可以看到插值导致的瑕疵。

《教团:1886》中的实现

对于场景中的静态网格体,在2D光照贴图中储存5-12个轴向和锐度值固定的球面高斯波瓣。对于动态的网格体,会提前烘焙包含球形探针的3D Grid到3D纹理中,每个探针有9个球面高斯波瓣。3D Grid是由灯光艺术家在场景中手动摆放的。然后在计算每个像素的光照前,利用硬件的纹理过滤在相邻的探针间做插值。

早期的测试是用CPU烘焙的,但最终使用的是CUDA烘焙工具做GPU烘焙,将计算结果投影到球面高斯波瓣上。下图比较了计算球面高斯波瓣计算的辐射率近似值。

为了性能和存储考虑,没有使用太多的球面高斯波瓣,这样在粗糙度较低的材质表面就会丢失很多细节。所以最终的方案是基于模型表面材质粗糙度在环境贴图和光照贴图两者中做选择,并在很小的区域内将两者的镜面反射进行混合。

总要恰饭的嘛