赞
踩
https://zhuanlan.zhihu.com/p/49436452
https://lianera.github.io/post/2016/sh-lighting-exp/
https://lianera.github.io/post/2017/sh-lighting-apply/ 源码
球谐光照(spherical harmonics lighting)就是基于球面调和(SH,Spherical Harmonics)这个数学工具的一种着色算法。
一般来说,球谐光照可以用有限带宽的Spherical Harmonics来模拟低频的环境光照明,反射光和高光是高频率,用低阶球谐函数来编码精度不够。unity里面的light probe使用的是3阶SH来捕捉光照,所以我觉得light probe就有点像是使用spherical harmonics来编码的光场(light field)捕捉器。
渲染方程
最简单的光照模型之一就是漫反射模型(diffuse surface reflection model)了,它也被称作是“点积光照”(dot product lighting),因为光照的强度是要乘个系数的,这系数就是表面法线和
入射光向量的点积,也就是:
这是渲染方程(The Rendering Equation)的简化,而渲染方程是基于物理建模推算出来的。考虑到一个点,由各个方向的入射光照亮,所以对上半球进行积分:
spherical harmonics 的harmonics一般译作是调和,所以SH就叫球谐了。而调和函数(harmonic function)就是[3]拉普拉斯方程(Laplace’s Equation)
的解,解一般都含有sin和cos项,所以满足拉普拉斯方程的函数就是harmonics的。在傅立叶分析里面,我们有时会把单位圆上的周期函数(periodic function)用调和函数进行展开。把这操作推广到n维球面,我们就会得到球面调和(Spherical Harmonics)。
很多论文都会直接把球谐函数的多项式扔给你,没有可视化,非常抽象。上面的图就给出了前5个band的球谐函数的可视化结果。绿色是正值,红色是负值,离中心越远的地方绝对值就越大。这些基底是对称的,这个对称主要因为球谐波函数的对称性(和)。
也就是要得到频域上的系数Clm,就要通过求原函数f与Ylm的乘机上的球面上的积分。一般我们把这个求系数的过程叫做投影(projection)。在实际操作中,我们写程序不可能会对无穷级数进行储存和卷积的操作,一般展开项只能是有限项,也就是:
其中n是球谐基band的数量,显然n个band的球谐基数量是n平方个。这个过程是一个带限的近似。大于一定阈值的高频信号就被去掉了。我们只能用n平方个预计算的球谐系数(SH Coefficient)和球谐函数本身近似地重建出原函数:
从图中可以看出,球谐展开阶数越高,能重构出来的信号就越精确。
在实际操作中,球面积分一般也不太能直接求出解析解,需要使用monte-carlo积分近似求解积分。
蒙特卡洛积分法:
其中权重
那么对于均匀的球面上采样来说,权重就是:
所以球谐系数 (SH Coefficient)的数值估计表达式是:
在写代码实现的时候,用蒙特卡洛积分求系数的过程大概就是一系列的相乘与求和,伪代码:
void SH_Coefficients() { double weight =4.0 * PI; //生成n条光线进行采样 for(int i=0; i<n_samples; ++i) { //生成带抖动的无偏采样方向(θ,ϕ) for(int n=0; n<n_coeff; ++n) { //对于某一个light probe,它的每个球谐展开系数c_i就要累加起所有的【某方向上的irradiance * 这个方向上SH函数值】 result[n] += light(θ,ϕ)* samples[i].SH_basis_coeff[n]; } } // 把蒙特卡洛积分的常数项乘上去(恒定的采样权重,总采样数) double factor = weight / n_samples; for(i=0; i<n_coeff; ++i) { result[i] = result[i] * factor; } }
上面的result[i]就是某个点上的球谐系数。我们可以用离线预计算的系数,在运行时快速近似重构原来的光照。
一般我们可以用SH coefficients来编码低频的环境光。
球谐函数的性质
标准正交性
球谐函数Ylm不只是正交的,它还是标准正交的(orthonormal)的,作为一族基函数来说,它是优秀的。
旋转不变性:
其次,球谐函数还是旋转不变的(rotationally invariant)的。这个意思是,如果我们有旋转操作R,那么如果我们有一个定义在单位球面上的原函数f(s),设旋转过后的函数是g(s),也就是:
那么我们会有:
函数乘积的积分等于其球谐系数向量的点积
SH函数的这个特性是杀手锏级别的牛逼。在我们做光照的时候,通常情况下我们需要某种形式的入射光描述以及某种形式描述的表面反射(我们叫这个描述表面反射的函数叫传输函数(transfer function),可以是BRDF之类的reflection descriptor function)。把它们相乘,来得到结果的反射光。但是我们还是需要对整个半球面的入射光与传输函数的乘积进行积分。也就是我们要计算:
其中 [公式] 是入射光, [公式] 是传输函数。
球谐函数的旋转(SH Rotation)
《Spherical Harmonics Lighting:the gritty details》原文其实并没有给出很细致的球谐旋转相关的东西,就给你留下前3阶的一些hardcoded过的SH旋转什么的。
旋转一个球谐函数的动机:用预处烘焙看的球谐做ambient occlusion是可以显著提高光照的效果的。到那时不少物体是需要动起来的(比如旋转),每个顶点的SH都要跟着旋转。那么问题来了,我们不可能说物体旋转了一下,我们得重新做SH Projection来得到旋转过的函数SH。 所以需要一种可以直接操作SH space的系数的快速旋转的方法。
更精确地描述,球谐函数旋转的问题:给定一个球谐系数向量,它表示的球面函数。现在要找出另外一个球谐系数向量
来表示旋转后的球面函数,其中R是旋转操作。
我们可以用要给n平方xn平方的旋转矩阵来完成这一个操作。从SH函数的正交性出发,我们可以推出SH旋转过程是一个线性操作,理论上旋转后的函数对应的每个SH coefficient,都是可以表示为同一band里面的SH coefficient的线性组合(所以才能用矩阵搞定),而且不同band之间的系数是不会交互的。而且这个矩阵理论上是稀疏的。
球谐光照实际上是一种对光照的简化,对于空间上的一点,受到的光照在各个方向上是不同的,也即是各向异性,所以空间上一点如果要完全还原光照情况,那就需要记录周围球面上所有方向的光照。注意这里考虑的是周围环境往往是复杂的情况,而不是几个简单的光源,如果是那样的话,直接用光源的光照模型求和就可以了。
如果环境光照可以用简单函数表示,那自然直接求点周围球面上的积分就可以了。但是通常光照不会那么简单,并且用函数表示光照不方便,所以经常用的方法是使用环境贴图,比如像这样的:
上面的图是立方体展开得到的,这种贴图也就是cubemap,需要注意的是一般的cubemap是从里往外看的。
考虑一个简单场景中的一个点,它周围的各个方向上的环境光照就是上面的cubemap呈现的,假如我想知道整个点各个方向的光照情况,那么就必须在cubemap对应的各个方向进行采样。对于一个大的场景来说,每个位置点的环境光都有可能不同,如果把每个点的环境光贴图储存起来,并且每次获取光照都从相应的贴图里面采样,可想而知这样的方法是非常昂贵的。
利用球谐函数就可以很好的解决这个问题,球谐函数的主要作用就是用简单的系数表示复杂的球面函数。关于球谐函数的理论推导与解释可以参考wiki。如果只是要应用和实现球谐光照,不会涉及到推导过程,不过球谐基函数却是关键的内容,球谐基函数已经有人在wiki上列好了表格,参考https://en.wikipedia.org/wiki/Table_of_spherical_harmonics
,前三阶的球谐基函数如下:
这里值得注意的是很多资料用这张图来描述球谐基函数:
我刚开始看到这张图的时候简直觉得莫名其妙,实际上这里面每个曲面都是用球坐标系表示的,球谐基都是定义在球坐标系上的函数,r(也就是离中心的距离)表示的就是这个球谐基在这个方向分量的重要程度。我是用类比傅里叶变换的方法来理解的,其实球谐函数本身就是拉普拉斯变换在球坐标系下的表示,这里的每个球谐基可以类比成傅里叶变换中频域的各个离散的频率,各个球谐基乘以对应的系数就可以还原出原来的球面函数。一个复杂的波形可以用简单的谐波和相应系数表示,同样的,一个复杂的球面上的函数也可以用简单的球谐基和相应的系数表示。
由于球谐基函数阶数是无限的,所以只能取前面几组基来近似,一般在光照中大都取3阶,也即9个球谐系数。
实验:
我们先考虑简单的情况,比如说定义一个光照函数:
在球坐标系下,将该函数的值当做光照强度值,可以画出光照在球面上的分布情况:
不过由于这种方式可视化方式对于亮度变换不是很敏感,所以我们把强度当成球坐标系的r,画出来是这个样子:
现在要将这个函数转换成球谐系数表示,首先要做的就是对其进行采样,采样的目标是确定在某个球谐基方向上强度的大小,也即求得每个球谐基Yi对应的系数Ci,具体的采样方法如下:
其中N为采样次数。也就是说在计算某个球谐系数Ci的时候,首先在球面上采许多点,然后把这些点的光照强度和球谐基相乘(在那个方向上,球谐基函数的分量或者说重要程度就是Yi(xi)),通过这些采样点,从而得到了在每个球谐基函数上光照的分布情况。由于某个球谐基只能大致代表它那个方向上的光照强度,所以需要组很多个球谐基函数才能近似还原出原光照。
需要注意的是:采样时必须要在球面上均匀采样,如果在cubemap的每个图像上逐像素采样,将会导致每个面边角亮度提高,中心亮度降低。关于如何在球面上均匀采样方法有很多,比如用正态分布随机生成x,y,z,然后归一化成单位向量。
还原的过程比较简单,通过球谐基与对应的系数相乘得到:
这里L’是还原后的光照,s是球面上的一点(也可以看成某个方向),n是球谐函数的阶数,n平方也即球谐系数的个数。
值得注意的是,采样和计算Ci是预先进行的,比如说复杂场景中,某个位置预先用光线跟踪方法计算环境光,从而采样出ci,这样这个位置的光照信息就压缩成了几个Ci表示了。但是重建光照的过程是在运行时实时进行的。从重建光照的过程中可以看出该式子非常简单,其中Yi的计算从球谐基函数的表中就可以看出只涉及到简单的乘法和加法,完全可以在shader中实现(球谐基函数中的r一般默认都设置成1)。所以,如果给我们一个点的球谐系数,利用上面的公式马上就得到每个方向上的光照强度。
对于上面的那个光照函数来说,首先对原函数进行采样,采样1000个点并计算出前6阶36个球谐系数,计算出的球谐系数(部分)如下:
计算好了球谐系数之后,我们就可以利用这些系数来还原原光照了,利用第二个公式还原之后的效果如下:
从左至右分别是原光照、02阶球谐光照、05阶球谐光照,从中可以看出到第5阶球谐光照与原光照已经很接近了,只是有小部分的高频信息不同。说明球谐系数越多,还原的效果越好,同时还原光照时能够较好地保留低频部分,而高频信息则丢失得比较多。不过对于光照来说,一般都是比较低频的信息,所以3阶,也就是到l=2时就已经足够了。
如果用CubeMap的方式来可视化就是这个样子:
左图为原环境光的CubeMap,右图为0~5阶球谐系数还原之后的光照,可以看出已经还原得很好了。
抛开简单的函数,如果是复杂的环境光贴图,过程也是一样的,比如对于一个这样的环境光:
对它进行采样并还原之后,得到了这样的结果:
效果还不错,只是高频丢失了很多。不过这是对光照的还原,因此丢失了高频信息关系也不大。
如果把这两个光照投射到球面上进行可视化,就是这个样子:
球谐光照的应用:
我们知道,球谐光照实际上就是将周围的环境光采样成几个系数,然后渲染的时候用这几个次数来对光照进行还原,这种过程可以看做是对周围环境光的简化 ,从而简化计算过程。因为如果按照采样的方法进行渲染,每次渲染的时候都得对周围环境采样,从而都会耗费大量的计算时间。所以球谐光照的实现可以分成两个部分,一是环境光贴图的采样和积分运算,生成球谐参数,二是利用球谐参数对模型进行渲染。
采样器
采样是从环境光上面采样,而环境光我们可以用环境贴图表示。环境光贴图则可以采用cubemap的形式,也就是上面的十字状的贴图,不过这里为了方便,把cubemap分成6个面,分布表示一个立方体的正x、负x、正y、负y、正z、负z。有了这六个贴图,通过一种映射关系,我们就能知道空间中的一点周围各个方向的光照值。具体的映射方法可以参考cubemap的wiki。
知道了每一个方向的光照值,要进行采样,还需要计算出球谐基。球谐基实际上相当于某个方向上分量的多少,多个球谐基在不同的方向上分量不同,所以才能够利用球谐基和球谐参数进行光照的还原。球谐基的计算方法,上一篇已经给出,例如,前四个分量的球谐基实际计算过程如下:
basis[0] = 1.f / 2.f * sqrt(1.f / PI);
basis[1] = sqrt(3.f / (4.f*PI))*y / r;
basis[2] = sqrt(3.f / (4.f*PI))*z / r;
basis[3] = sqrt(3.f / (4.f*PI))*x / r;
这样,每采样到一个像素,就计算相应的球谐基,并且对像素与对应的球谐基相乘后再求和,这样就相当于每个球谐基在所有像素上的积分。不过,为了得到球谐基上的平均光照强度,还需要将积分得到的数值乘以立体角并且除以总像素。简单说来就是运用这个公式求得球谐系数:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。