第17章光传输II:体渲染SurfaceIntegrator是场景几何、材质、光源、于光传输方程相关的复杂算法、对场景中辐射亮度的分布的确定等等的交会点,同样地,VolumeIntegrator负责将参与介质的效果加入到这个进程之中,并确定它对辐射亮度的分布的影响。这一章主要介绍描述参与介质沿光线方向改变辐射亮度的传输方程,并描述VolumeIntegrator的接口极其一些简单的实现。前一章中关于表面上的光传输算法的许多方法可以被扩展开来,对参与介质做相应的处理。17.1传输方程传输方程是控制介质对光进行吸收、放射、散射等行为的基本方程。它解释了在第12章中介绍的所有的体散射过程--吸收、放射、内散射、外散射,并给出了描述辐射亮度在环境中分布的方程。光传输方程实际上是传输方程的一个特殊形式,只是缺少了参与介质,并只处理表面散射的情况。传输方程的最基本的形式是一个积分-微分方程,它描述了沿一条光束上的辐射亮度在空间一个点上的变化情况。它可以变换成一个描述源自光线上无限个点的参与介质效果的纯积分方程。我们可以用很直接的方式得到该方程,即从沿一个光束增加能量的过程(放射,内散射)的效应减去同一个光束上减少能量的过程(吸收,外散射)的效应即可。回忆一下第12.1.4节的源项,它给出了给定点p在特定方向ω上由放射和内散射引起的辐射亮度变化:源项负责说明所有使光线辐射亮度增加的过程。衰减系数σt(p,ω)负责说明在某点上所有使光线辐射亮度减少的过程,包括吸收和外散射。描述其效应的微分方程是:我们将这两种效应加在一起,就得到沿光线上点p'的总体上的辐射亮度微分变化,从而得到传输方程的积分-微分方程:在适当的边界条件下,我们可以将这个方程转换为一个纯积分方程。例如,如果我们假定场景中没有表面,那么光线就没有阻挡,故有无限的长度,传输积分方程就是:其中p'=p+tω。这个方程的意义还是很直观的:它是说从给定方向到达一个点的辐射亮度等于从该点出发的光线上所有点对该点的累加辐射亮度。沿光线上每点上到光线原点的辐射亮度增加值要按从原点到该点的总光束透光率的比例减少。如图:在更一般的情况下,如果场景中有反射或放射表面,光线长度就不是无限的,被光线碰到的表面会对其辐射亮度产生影响,使得该点上出离该表面的辐射亮度增加,并致使交点后面的光线上的点对光线原点的辐射亮度没有什么贡献值。如果光线(p,ω)在距离t处跟表面相交于点p0,那么传输积分方程是(17.1):其中p0=p+tω是表面上的点,而p'=p+tω是沿光线上的点,如图:这个方程描述了沿光线的辐射亮度的两个贡献值:第一项是沿光线从表面上反射回来的辐射亮度,由Lo项给出,它包括表面上的放射辐射亮度和反射辐射亮度。这个辐射亮度可以被参与介质所衰减。第二项代表了由于体积散射和放射所产生的增加出来的辐射亮度,但到光线和表面的交点为止,过了这个交点对辐射亮度就没有什么影响了。为了简明起见,这里不再过多地讨论传输方程。然而,就像光传输方程可以写成不同路径的累加和并引入重要性函数,我们也可以对传输方程做如此处理。这里只列出几个VolumeIntegrator的实现,其它诸如路径追踪、双向路径追踪、光子映射等等用于表面积分的算法也可以应用于体积分。17.2体积分器接口VolumeIntegrator接口继承于Interator,将其中的Preprocess()、RequestSample()和Li()函数包括进来。体积分器的前两个函数的用法跟表面积分器的用法相同。Li()函数跟表面积分器的版本相似,都返回沿给定光线的辐射亮度,而体积分器应该假定光线已经跟场景中几何体相交,如果光线确实跟某个表面相交,Ray::maxt要设置成交点。这样,体积分器应该只计算参数范围[mint,maxt]中的体积散射效果。VolumeIntegrator接口还增加了一个函数,Transmittance(),它负责计算从Ray::mint到Ray::maxt的光线上的光束透光率。VolumeScatteringDeclarations+=classVolumeIntegrator:publicIntegrator{public:virtualSpectrumTransmittance(constScene*scene,constRay&ray,constSample*sample,float*alpha)const=0;};有了这个背景知识之后,我们就可以完全理解Scene::Li()函数了。它是对方程17.1的一个直接的实现。表面积分器计算在光线交点处的出射辐射亮度Lo,忽略了到光线原点的衰减效应。体积分器的Transmittance()函数计算到表面上点的光束透光率Tr,它的Li()函数给出参与介质所引起的沿光线的辐射亮度。LoTr跟来自参与介质的辐射亮度增加量的和给出了光线原点出的总辐射亮度值。17.3只有放射的积分器最简单的体积分器可能就是忽略了内散射而只考虑放射和衰减的积分器了。因为忽略了内散射,在源项中的球面积分就不见了,简化后的传输方程为:EmissionIntegrator用蒙特卡罗积分来解这个方程。EmissionlntegratorDeclarations=classEmissionIntegrator:publicVolumeIntegrator{public:EmissionIntegratorPublicMethodsprivate:EmissionIntegratorPrivateData};EmissionIntegrator的Transmittance()和Li()函数都要沿光线做一维积分求值。这里没有用固定数目的采样,而是根据光线在体积区域中的跑过的距离来决定采样数,距离越长,则采样越多。这个方法从直观意义上讲是很值得的:光线在介质的区间越长,所要求的精确度就越高,就需要越多的采样来捕捉沿光线的光学性质上的变化。采样个数由一个用户给定的参数(步长值)来间接地确定。光线被分成给定长度的线段,在每个线段中取一个采样。EmissionIntegratorPublicMethods=EmissionIntegrator(floatss){stepSize=ss;}EmissionIntegratorPrivateData=floatstepSize;Transmittance()和Li()函数各需一个一维采样值来做相应的积分求值计算。EmissionIntegratorMethodDefinitions=voidEmissionIntegrator::RequestSamples(Sample*sample,constScene*scene)tauSampleOffset=sample-Add1D(1);scatterSampleOffset=sample-Add1D(1);}EmissionIntegratorPrivateData+=inttauSampleOffset,scatterSampleOffset;Transmittance()函数还是很直接了当的。VolumeRegion的Tau()函数负责计算从光线起点到终点之间的光学厚度τ。这里积分器的工作只是选择一个步长大小,并将一个采样值传给函数,并返回e-τ。如果Tau()函数可以用解析法计算τ,就忽略这些额外的值。这个函数应用了这样一个事实:Sample值只对相机光线是非NULL的,它被用来为阴影和间接光线增加步长大小,这样就降低了计算需求(和精度)。对于这些光线而言,精度的降低通常不会产生什么影响。EmissionIntegratorMethodDefinitions+=SpectrumEmissionIntegrator::Transmittance(constScene*scene,constRay&ray,constSample*sample,float*alpha)const{if(!scene-volumeRegion)returnSpectrum(1.f);floatstep=sample?stepSize:4.f*stepSize;floatoffset=sample?sample-oneD[tauSampleOffset][0]:RandomFloat();Spectrumtau=scene-volumeRegion-Tau(ray,step,offset);returnExp(-tau);}Li()函数负责对方程17.2的和的第二项求值。如果光线在t=t0进入体区域,那么从光线起点一直到点t0都不会有放射或衰减现象发生,Li()函数可以考虑在t0到t1进行积分,其中t1是光线出离体区域或碰上一个表面的最小参数偏置量。这个积分的估计值如下:该值可以通过在t0到t1的光线上均匀选择采样点pi,然后求下列值:其中p(pi)=1/(t1-t0)。在估计值公式中的Lve项可以直接用相应的volumeRegion函数来求得,用来求Tr的τ也可以直接求得(均质型天气或指数型天气),或通过15.7节中的蒙特卡罗积分求得。为了做这个计算,Li()的实现先求积分的t的范围,并对t0,t1分别初始化:EmissionIntegratorMethodDefinitions+=SpectrumEmissionlntegrator::Li(constScene*scene,constRayDifferential&ray,constSample*sample,float*alpha)const{VolumeRegion*vr=scene-volumeRegion;floatt0,t1;if(!vr||!vr-IntersectP(ray,&t0,&t1))returnO.f;Doemission-onlyvolumeintegrationinvr}这里使用了两个另外两项技术。第一,就像第15.7节中VolumeRegion::Tau()函数使用均匀步长的采样点,这里的实现基于类似的理由也使用均匀的步长。第二,如果将点pi按照离光线原点p的距离排好序,就可以高效地对光束透光率Tr求值。然后,我们可以利用Tr的连乘的性质来逐步地从前一个点的值来计算当前点的值:因为Tr(pj-pi-1)所覆盖的距离比Tr(pj-p)所覆盖的要短,如果使用蒙特卡罗求值,就可以使用更少的采样。Doemission-onlyvolumeintegrationinvr=SpectrumLv(0.);Prepareforvolumeintegrationsteppingfor(inti=0;inSamples;++i,t0+=step){Advancetosampleatt0andupdateTComputeemission-onlysourcetermatp}*T=Tr;returnLv*step;Prepareforvolumeintegrationstepping=intN=Ceil2Int((t1-t0)/stepSize);floatstep=(t1-t0)/N;SpectrumTr(1.f);Pointp=ray(t0),pPrev;Vectorw=-ray.d;if(sample)t0+=sample-oneD[scatterSampleOffset][0]*step;elset-0+=RandomFloat()*step;为了求当前点上的总透光率,需要求前前一个点到当前点之间的透光率,在乘上从光线原点到前一个点的透光率。Advancetosampleatt0andupdateT=pPrev=p;p=ray(t0);SpectrumstepTau=vr-Tau(Ray(pPrev,p-pPrev,0,1),.5f*stepSize,RandomFloat());Tr*=Exp(-stepTau);Possiblyterminateraymarchingiftran