http://69.163.227.177/forum.php?mod=viewthread&tid=6200
第12章 体积散射
到目前为止,我们都假定场景中的所有表面都在真空中,这意味者在表面之间的光线的辐射亮度是恒定的。然而,在许多真实世界中的情景中,这个假设并不精确:雾和烟对光产生了衰减和散射作用,大气中的粒子的散射作用使得蓝天呈蓝色,使得夕阳晚景呈红色。本章介绍相关的数学知识来描述参与介质(participating media--分布于三维空间区域中的粒子)对光的影响。有了对参与介质的效果的模拟,我们就可以渲染场景有雾霭的图像、穿透云层的光束、子表面的散射(即光线的出射点跟入射点不同)。
本章首先描述了光线的辐射亮度受参与介质影响的基本物理过程。然后,我们介绍VolumeRegion基本类,它是描述不同介质的接口。为了确定光在场景中的分布的整体效果,我们还需要第17章中所介绍的VolumeIntegrator。
12.1 体积散射过程
在有参与介质的环境中,有三个主要过程可以影响辐射亮度的分布:
吸收(Absorption):由于光能转变成其它形式的能量(如热)而减弱了辐射亮度。
放射(Emission):发光的粒子增强了环境中的能量。
散射(Scattering):由于光跟粒子的碰撞,在一个方向上传播的光被散射到其它方向上去。
所有这些特性既可以是均质的(homogeneous),也可以是非均质的(inhomogeneous)。均质的特性在给定的一个空间范围之内是一个恒量,而非均质的特性在空间内是变化的。下图显示了一个体积散射的例子:一个聚光灯照亮了参与介质,并投射出一个体积阴影(volumetric shadow)。
12.1.1 吸收
考虑一下火产生的浓烟:烟遮挡了后面的物体,因为烟的颗粒吸收了从物体到观察者的光线。烟越浓,被吸收的光就越多。下图显示了烟的这种特效,其体积密度是由一个模拟烟的精确物理模型生成的。注意地面上的阴影:参与介质已经吸收了光源和地面之间的光线,从而投射出一个阴影。
吸收是通过介质的吸收截面σa来表述的,它是光线在介质中单位距离之内被吸收的概率密度。通常情况下,吸收截面随着位置p和方向ω的变化而变化,虽然严格地讲它是关于位置的函数,但是它通常也是随光谱变化的量。σa的单位是距离的倒数(m-1)。这意味者σa可以取任意正值,并不需要限定在0到1之内。
下图显示了沿着很短一段光线上的吸收效果。
在点p上有一定的辐射亮度Li(p, -ω),我们希望求出在微分体积之内被吸收后的出射辐射亮度Lo(p, ω)。沿光线微分长度dt的辐射亮度变化值可以通过下面的微分方程来表示:
Lo(p, ω) − Li(p, −ω) = dLo(p, ω)= −σa(p, ω) Li(p, −ω) dt ,
也就是说,沿着光线的辐射亮度的微分减少值跟其初始值呈线性关系。
这个微分方程的解实际上是要给出描述光线上被吸收的光的总比率的积分方程。如果我们假定光线以p为起始点在介质中沿方向ω上行进了距离d,则总的吸收比率是:
12.1.2 放射
在介质中,吸收过程对沿光线的辐射亮度有减弱作用,而放射过程却有增强作用,原因是由于化学、热力学或核作用将能量转换为可见光。我们记在点p处的ω方向上单位距离上增加的出射辐射亮度为Lve(p, ω),那么有下列微分方程:
dLo(p, ω) = Lve(p, ω) dt .
注意这个方程基于这样的假定:即出射光Lve是不依赖于入射光Li的。在pbrt的线性光学的前提下这个假定总是成立的。
12.1.3 外散射(out-scattering)和消光 (extinction)
在参与介质中,第三个光的基本交互作用是散射。当一束光穿过一种介质中,它可以跟介质中的粒子碰撞并被反射到不同的方向上。这对光束的总辐射亮度有两个效果:它减弱了光束出离一个微分区域时的辐射亮度,因为部分光线被反射到其它方向上。这个效果被称为外散射(out-scattering),即本节要介绍的主题。然而,其它光线的辐射亮度也会被加到当前的光线的路径中,这是下一节要介绍的内散射(in-scattering)。
单位距离内的外散射事件发生的概率由散射系数给σs给出。跟衰减系数类似,外散射在微分长度dt上所产生的辐射亮度减少值可以通过下列方程给出:
dLo(p, ω)= −σs(p, ω) Li(p, −ω) dt
由吸收和外散射所引起的总辐射亮度的减少是根据σa + σs来决定的。这个综合效果被称为衰减或消光(extinction)。为了方便起见,我们用衰减系数σt来代表两个系数之和:
σt(p, ω) = σa(p, ω) + σs(p, ω)
有了衰减系数以后,我们可以得到描述总衰减效果的微分方程:
dLo(p, ω) / dt = −σt(p, ω) Li(p, −ω)
我们通过解这个方程可以得到光束透射率(beam transmittance),它给出了一条光线通过两个点所传输的辐射亮度的比率:
其中d是点p到点p'的距离,ω是两点之间的向量的正规化向量,Tr是两点之间的光束透射率。如果表面上给定方向ω上的出射辐射亮度是Lo(p, ω),那么,通过消光作用之后,在方向ω上的入射辐射亮度是:
Tr(p → p') Lo(p, ω)
光束透射率有两个很有用的性质:一个点到它自身的透射率为1,即Tr(p → p) = 1;在真空中对于所有点p'都有Tr(p → p') = 1。另外,对于所有的介质,在光线上各点上的透射率有相乘的关系(例如下面p,p',p''三个点):
Tr(p → p'') = Tr(p → p') Tr(p' → p'')
这个性质对于体积散射的实现而言非常有用,因为我们可以靠计算相继点上透射率的乘积逐步地得到沿光线上的许多点上的透射率。
在Tr中取负值的指数被称为两点之间的光学厚度(optical thickness), 用符号τ来表示:
对于均匀介质而言,σt为常量,很容易计算出τ的积分,从而得到Beer定律:
Tr(p→p') = e−σtd .
11.1.4 内散射(In-Scattering)
外散射由于向不同方向进行散射的缘故减弱了光线的辐射亮度;与之相反的是,由于其它方向上被散射的光线加入到当前光线的方向,内散射加强了光线的辐射亮度。假定粒子之间的间距是它们半径的数倍,这样我们就可以忽略粒子之间的交互作用。在这个假定之下,相函数p(ω→ω')表示了在某点上的散射辐射(scattered radiation)的角分布。它相当于体积上的BSDF。然而,这个比拟并不准确,因为相函数有一个归一化约束,即对于所有的ω,满足下列条件:
这个约束条件意味着相函数实际上定义了在特定方向上散射的概率分布。
内散射所增加的总辐射亮度可以由源项S给出,即:
dLo(p, ω) = S(p, ω) dt
它包括了体积放射和内散射:
源项的内散射部分是单位距离内的散射概率σs和在该点上所增加的辐射亮度的乘积,所增加的辐射亮度可以通过对入射辐射亮度和相函数的乘积进行球面积分得到。注意这个公式跟第5章的散射方程很相似,主要的不同是这里没有cosine项,因为相函数使用的是辐射亮度而不是微分辐射照度(differential irradiance)。
12.2 相函数
人们为了描述表面上散射研究了许多BSDF模型,同样地,人们也开发了许多相函数来描述体积散射。其中包括用少数参数来做拟合函数的参数化模型,还有基于粒子形状和材质(如球形水滴)而推导出的散射辐射分布的解析模型,等等。
在大多数的自然产生的介质中,相函数是关于两个方向ω和ω'的夹角θ的一维函数;这样的介质被称为各向同性的(isotropic),它们的相函数常常被写成p(cosθ)。在比较奇异的介质中,例如有水晶结构的介质,它的相函数是两个方向的4D函数。除了前面提到的归一化性质以外,自然生成的介质还有一个重要的性质,即互反性(reciprocal):两个方向相互交换后,相函数值保持不变。注意各向同性的介质的互反性很容易从cos(-θ) = cos(θ)得到。
相函数本身可以是各向同性的(isotropic),也可以是各向异性的(isotropic)的。因为各向同性的相函数描述的是在各个方向上的均匀散射,因此跟两个方向都无关。因为相函数是归一化的,因此只有唯一的各向同性函数,且为常量1/4π:
P isotropic(ω→ω') = 1 / 4 π
<Volume Scattering Definitions> =
float PhaseIsotropic(const Vector &, const Vector &) {
return 1.f / (4.f * M_PI);
}
本节下面所介绍的各向异性相函数描述的是各向同性介质,因此是用两个方向的夹角来定义的(如图)。
跟表面上散射的约定(两个方向都背离散射点)有所不同的是,这里入射方向是指向散射点的,而出射方向是背离散射点的,这跟相函数的约定一致。
pbrt实现了描述Rayleigh散射和Mie散射的相函数。Rayleigh模型表示了在地球大气中极小粒子(如分子)所产生的散射。如果粒子半径比光波长λ小(r/λ < 0.05),Rayleigh模型就可以很精确地描述散射光的分布。跟波长相关的Rayleigh散射也是蓝天之所以蓝、晚霞之所以红的原因。Mie散射基于更一般性的理论:它是从Maxwell方程推导出来的,可以描述的粒子半径范围更为宽广。例如,它可以很好地描述水雾中的天气。
<Volume Scattering Declarations> =
float PhaseRayleigh(const Vector &w, const Vector &wp);
float PhaseMieHazy(const Vector &w, const Vector &wp);
float PhaseMieMurky(const Vector &w, const Vector &wp);
在计算机图形学中得到广泛应用的一个相函数是由Henyey和Greenstein开发的。这个相函数很容易和测量出的散射数据相拟合。它有一个单一的参数g(称为非对称参数),用来控制散射光的分布:
pHG(cos θ) = (1/4π) (1− g2) / (1+ g2 − 2g(cos θ))3/2
下图显示了Henyey–Greenstein相函数,它具有可变的非对称参数g,变化范围是(-1, 1)。负的g值对应于向后散射(back-scattering),这时光大多数会在入射方向上散射(图中的实线部分对应g = -0.35);正的g值对应于向前散射(forword-scattering) (图中的虚线部分对应g = 0.67)。g的值越大,散射方向就越靠近- ω(向后散射)或ω(向前散射)方向。
<Volume Scattering Definitions> +=
float PhaseHG(const Vector &w, const Vector &wp, float g) {
float costheta = Dot(w, wp);
return 1.f / (4.f * M_PI) *
(1.f - g*g) / powf(1.f + g*g - 2.f * g * costheta, 1.5f);
}
非对称参数有很严格的意义。它是相函数和ω-ω'夹角余弦的乘积的平均值。给定一个任意的相函数,可以用下式计算g:
这样,对于各向同性相函数来说,g=0。
满足这个方程的相函数有任意多个,单独使用g值不足以唯一地描述一个散射分布。然而。我们将复杂的散射分布化简为一个只有一个参数的模型,这样做所带来的方便比可能的精度上的缺失更重要。
无法用单一的非对称参数描述的更复杂的相函数常常用相函数(如Henyey–Greenstein相函数)的加权和来表示,其中每个相函数带有不同的参数值:
其中所有的wi的和为1以保证正规性。
Blasi, Le Saec, 和Schlick开发出了一个Henyey–Greenstein相函数的很高效的近似表示,由于它的高效性而被广泛地应用于计算机图形学领域中:
pSchlick(cos θ) = (1/4π)(1− k2)/(1− k cos θ)2
其中参数k跟Henyey–Greenstein中的g项有类似的作用,-1对应于向后散射,0对应于各向同性散射,1对应于向前散射。两者关系如下:
k = 1.55g - .55g3
<Volume Scattering Definitions> +=
float PhaseSchlick(const Vector &w, const Vector &wp, float g) {
float k = 1.55f * g - .55f * g * g * g;
float kcostheta = k * Dot(w, wp);
return 1.f / (4.f * M_PI) *
(1.f - k*k) / ((1.f - kcostheta) * (1.f - kcostheta));
}
12.3 体积区域接口和均匀介质
在pbrt中描述体积散射的核心抽象类是VolumeRegion,它是描述场景中一个区域中的体积散射的接口。在场景中不同的部分,可以使用不同类型的VolumeRegion来描述不同类型的散射。
<Volume Scattering Declarations> +=
class COREDLL VolumeRegion {
public:
<VolumeRegion Interface>
};
所有的VolumeRegion必须可以计算和轴对齐的世界空间包围盒,它可以通过函数VolumeRegion::WorldBound()来得到。跟Shape和Primitive一样,这个包围盒可以用来将VolumeRegion放入加速结构。
<VolumeRegion Interface> =
virtual BBox WorldBound() const = 0;
因为VolumeIntegrator需要知道光线在世界空间中穿过体积区域的参数范围,而且世界包围盒并不一定能够紧包住体积区域,所以我们提供了一个单独的函数VolumeRegion::IntersectP(),用来返回光线跟该区域重叠的那部分光线的参数t值的范围。
<VolumeRegion Interface> +=
virtual bool IntersectP(const Ray &ray, float *t0, float *t1) const = 0;
这个接口有4个函数,分别对应于前面介绍的散射性质。给定了世界空间的一个点和方向,VolumeRegion::sigma_a(),VolumeRegion::sigma_s(),VolumeRegion::sigma_Lve()返回相应的吸收、散射和放射性质。给定一对方向后,VolumeRegion::p()函数返回给定点的相函数值。
<VolumeRegion Interface> +=
virtual Spectrum sigma_a(const Point &, const Vector &,
float time) const = 0;
virtual Spectrum sigma_s(const Point &, const Vector &,
float time) const = 0;
virtual Spectrum Lve(const Point &, const Vector &,
float time) const = 0;
virtual float p(const Point &, const Vector &,
const Vector &, float time) const = 0;
为了方便,还有一个函数VolumeRegion::sigma_t()用来返回给定点上的衰减系数。它的缺省实现是返回σa和σs的和,但大多数的实现会重载该函数,直接计算出σt。
<Volume Scattering Definitions> +=
Spectrum VolumeRegion::sigma_t(const Point &p, const Vector &w,
float time) const {
return sigma_a(p, w, time) + sigma_s(p, w, time);
}
最后,VolumeRegion::tau()函数根据点ray(ray.mint)和点ray(ray.maxt)来计算体积区域的光学厚度。有些实现可以之间计算出这个值,例如下节要介绍的HomogeneousVolume,而其它的实现需要Monte Carlo积分来计算。为了方便使用Monte Carlo方法,该函数使用了两个可选的参数,step和offset,对于使用封闭形式解的实现而言,可以忽略这两个参数。
<VolumeRegion Interface> +=
virtual Spectrum tau(const Ray &ray, float step = 1.f,
float offset = 0.5) const = 0;
11.3.1 均匀体积区域
最简单的体积表示是HomogeneousVolume,它描述了具有均匀散射性质的盒形区域。它的构造器使用了σa和σs,相函数的g值,还有放射量Lve。再加上从世界空间到体积区域的物体空间的变换还有一个轴对齐的物体空间的包围盒,就足够可以描述该区域的散射性质和空间范围。
<HomogeneousVolumeDensity Declarations> =
class HomogeneousVolumeDensity : public VolumeRegion {
public:
<HomogeneousVolumeDensity Public Methods>
private:
<HomogeneousVolumeDensity Private Data>
};
<HomogeneousVolumeDensity Private Data> =
Spectrum sig_a, sig_s, le;
float g;
BBox extent;
Transform WorldToVolume;
由于包围盒是定义在物体空间的,所以在WorldBound()函数中使用到世界空间的变换。
<HomogeneousVolumeDensity Public Methods> =
BBox WorldBound() const {
return Inverse(WorldToVolume)(extent);
}
如果区域的世界空间--物体空间变换包含了旋转操作,那么该区域在世界空间里就不是轴对齐的了,但我们可以将光线变换到区域的物体空间中,并求得它们的重叠部分,这样就可以得到更紧密的光线参数范围。
<HomogeneousVolumeDensity Public Methods> +=
bool IntersectP(const Ray &r, float *t0, float *t1) const {
Ray ray = WorldToVolume(r);
return extent.IntersectP(ray, t0, t1);
}
剩下的接口函数都很简单。它们都先要验证给定点是否是在区域中,如果是的话就返回相应的值。这里只列出了sigma_a(),其它的函数就略去了。
<HomogeneousVolumeDensity Public Methods> +=
Spectrum sigma_a(const Point &p, const Vector &, float) const {
return extent.Inside(WorldToVolume(p)) ? sig_a : 0.;
}
由于σa和σs在整个区域中是常量,我们可以通过Beer法则以封闭解的形式来计算光学厚度:
<HomogeneousVolumeDensity Public Methods> +=
Spectrum tau(const Ray &ray, float, float) const {
float t0, t1;
if (!IntersectP(ray, &t0, &t1)) return 0.;
return Distance(ray(t0), ray(t1)) * (sig_a + sig_s);
}
12.4密度可变的体积区域
本章所介绍的其它的体积区域基于这样的一个假设:介质中的粒子有相同的基本散射性质,但其密度是随空间位置变化的。这个假设所产生的结论之一就是我们可以用该点的密度乘以某个基准值来描述体积散射性质。例如,我们可以为衰减系数σt设置基准值为0.2。对于粒子密度为1的区域,σt返回0.2。如果粒子密度为3,则返回0.6。
为了减少重复性代码,并令不同的表示将重点放在定义粒子密度的方法上,我们定义了一个DensityRegion类,它定义了一个纯虚函数来获取一个点上的粒子密度。体积区域的表示可以继承DensityRegion,就无需实现相同的逻辑了。
<Volume Scattering Declarations> +=
class DensityRegion : public VolumeRegion {
public:
<DensityRegion Public Methods>
protected:
<DensityRegion Protected Data>
};
DensityRegion构造器使用了基本散射性质的值,并将它们放入相应的成员变量中。注意接口声明了体积区域---世界空间的变换,但该类存放的是世界空间---体积区域的变换。
<DensityRegion Protected Data> =
Transform WorldToVolume;
Spectrum sig_a, sig_s, le;
float g;
<DensityRegion Public Methods> =
DensityRegion(const Spectrum &sa, const Spectrum &ss, float gg,
const Spectrum &emit, const Transform &VolumeToWorld)
: sig_a(sa), sig_s(ss), le(emit), g(gg),
WorldToVolume(Inverse(VolumeToWorld)) { }
所有DensityRegion的继承类必须实现DensityRegion::Density(),它返回了体积区域在物体空间中给定点的密度。该密度用来乘以基本的散射参数,故在任何一点都应该是非负数。
<DensityRegion Public Methods> +=
virtual float Density(const Point &Pobj) const = 0;
这里只列出DensityRegion::sigma_a()函数的实现,其它类似,从略:
<DensityRegion Public Methods> +=
Spectrum sigma_a(const Point &p, const Vector &) const {
return Density(WorldToVolume(p)) * sig_a;
}
其中一个例外是DensityRegion::p()函数,它并不是用局部的密度乘以相函数值,因为从点到点的散射变化已经由σs负责处理了。
<DensityRegion Public Methods> +=
float p(const Point &p, const Vector &w, const Vector &wp) const {
return PhaseHG(w, wp, g);
}
DensityRegion无法实现VolumeRegion::Tau()函数,因为这个函数基于对VolumeRegion形状的总体的了解,还要知道其中的密度分布。因此这个方法应该由所有的继承类来实现。
12.4.1 三维网格
VolumeGrid类将密度存放在一个普通的三维的位置网格中,这跟用2D采样网格来表示图像的ImageTexture有些类似。我们通过对这些采样值进行插值来计算采样点之间的位置上的密度。它的构造器使用了用户提供的存放着密度的3D数组,这就可以允许使用很多类型的数据源(物理仿真,CT扫描,等等)。因为它是DensityRegion的子类,所以用户也要提供σa、σs、Lve和g的基准值。
<VolumeGrid Declarations> =
class VolumeGrid: public DensityRegion {
public:
<VolumeGridDensity Public Methods>
private:
<VolumeGridDensity Private Data>
};
构造器做基本散射参数的初始化,存储区域的物体空间中的包围盒,并做密度值的局部拷贝。
<VolumeGrid Public Methods> =
VolumeGrid (const Spectrum &sa, const Spectrum &ss, float gg,
const Spectrum &emit, const BBox &e, const Transform &v2w,
int x, int y, int z, const float *d)
: DensityRegion(sa, ss, gg, emit, v2w), nx(x), ny(y), nz(z), extent(e) {
density = new float[nx*ny*nz];
memcpy(density, d, nx*ny*nz*sizeof(float));
}
<VolumeGridPrivate Data>
float *density;
const int nx, ny, nz;
const BBox extent;
WorldBound()函数跟IntersectP()函数跟前面所介绍的HomogeneousVolume中的类似,从略。
VolumeGrid的Density()的任务是用采样来重构给定点上的密度。
<VolumeGridMethod Definitions> =
float VolumeGrid::Density(const Point &Pobj) const {
if (!extent.Inside(Pobj)) return 0;
<Compute voxel coordinates and offsets for Pobj>
<Trilinearly interpolate density values to compute local density>
}
有了3D空间中一个点的8个采样值之后,该函数用三线性插值来计算该点的密度函数值。首先,要找到整数坐标小于该采样位置的最近的体积区域采样,然后用沿每个轴的麦哈顿距离(Manhattan distance)(dx,dy,dz)来插值。
<Compute voxel coordinates and offsets for Pobj> =
float voxx = (Pobj.x - extent.pMin.x) /
(extent.pMax.x - extent.pMin.x) * nx - .5f;
float voxy = (Pobj.y - extent.pMin.y) /
(extent.pMax.y - extent.pMin.y) * ny - .5f;
float voxz = (Pobj.z - extent.pMin.z) /
(extent.pMax.z - extent.pMin.z) * nz - .5f;
int vx = Floor2Int(voxx);
int vy = Floor2Int(voxy);
int vz = Floor2Int(voxz);
float dx = voxx - vx, dy = voxy - vy, dz = voxz - vz;
我们用这些距离做一系列的Lerp()调用来估算出采样点的密度:
<Trilinearly interpolate density values to compute local density> =
float d00 = Lerp(dx, D(vx, vy, vz), D(vx+1, vy, vz));
float d10 = Lerp(dx, D(vx, vy+1, vz), D(vx+1, vy+1, vz));
float d01 = Lerp(dx, D(vx, vy, vz+1), D(vx+1, vy, vz+1));
float d11 = Lerp(dx, D(vx, vy+1, vz+1), D(vx+1, vy+1, vz+1));
float d0 = Lerp(dy, d00, d10);
float d1 = Lerp(dy, d01, d11);
return Lerp(dz, d0, d1);
工具函数D()返回给定采样位置上的密度。它的唯一任务是使用截取(clamping)方式处理越界的情况,并计算出给定采样的数组偏置值。跟MIPMaps不同的是,截取方式几乎总是我们想要的方式。因为所有的查找点都位于包围盒之内,只有在边界的情况会出现越界。
<VolumeGrid Public Methods> =
float D(int x, int y, int z) const {
x = Clamp(x, 0, nx-1);
y = Clamp(y, 0, ny-1);
z = Clamp(z, 0, nz-1);
return density[z*nx*ny + y*nx + x];
}
12.4.2 指数密度
另一个很有用的密度类是ExponentialDensity类,它描述了在给定的3D范围内按照关于高度h的指数函数变化的密度分布:
d(h) = a e-bh
其中a,b分别是控制总体密度的参数以及按高度进行衰减的快慢程度的参数。这个密度函数是对在地球表面上所观察到的地球大气是个很好的模拟(这里忽略了地球的弯曲)。它也可以用于模拟在地平面上的低沉的雾气。
<ExponentialDensity Declarations> =
class ExponentialDensity : public DensityRegion {
public:
<ExponentialDensity Public Methods>
private:
<ExponentialDensity Private Data>
};
ExponentialDensity的构造器使用所传入的变量来初始化成员函数。除了要传入DensityRegion构造器的体积散射性质外,还有体积的包围盒,参数a和b。这个构造器还要用到一个“向上”的向量对体积区域进行朝向定位,并用来计算点的高度。当然,向上向量并非必要(实际上使用世界--物体变换也可以做到这一点),但在概念上更容易为用户理解。
<ExponentialDensity Public Methods> =
ExponentialDensity(const Spectrum &sa, const Spectrum &ss,
float gg, const Spectrum &emit, const BBox &e,
const Transform &v2w, float aa, float bb,
const Vector &up)
: DensityRegion(sa, ss, gg, emit, v2w), extent(e), a(aa), b(bb) {
upDir = Normalize(up);
}
<ExponentialDensity Private Data> =
BBox extent;
float a, b;
Vector upDir;
WorldBound()函数跟IntersectP()函数跟前面所介绍的HomogeneousVolume中的类似,从略。
在物体空间中,给定点的沿“向上”向量的高度可以这样求得:将从包围盒的下角(lower corner)到p的向量投影到“向上”向量的方向上,然后求两个向量的点积即得到高度。如图:
<ExponentialDensity Public Methods> +=
float Density(const Point &Pobj) const {
if (!extent.Inside(Pobj)) return 0;
float height = Dot(Pobj - extent.pMin, upDir);
return a * expf(-b * height);
}
12.5 体积聚合体
跟可以存放一组Primitive的Aggregate类一样,AggregateVolume可以存放一个或多个VolumeRegion。这样做有两个原因:第一,简化场景以及对VolumeIntegrator的实现,因为我们可以只对一个AggregateVolume进行函数调用,而无需对场景中所有的区域进行循环;第二,AggeragateVolume的实现可以有可能利用3D空间数据结构来提升效率,因为我们可以剔除掉里特定光线或查找点太远的区域。
这里我们只实现一个简单的AggregateVolume,它有一个存放场景中所有体积区域的列表,并在具体的函数实现中对这些区域进行循环。当场景中有许多不同的区域时,这个实现不太高效,如何写一个高效的实现留做练习。
<Volume Scattering Declarations> +=
class AggregateVolume : public VolumeRegion {
public:
<AggregateVolume Public Methods>
private:
<AggregateVolume Private Data>
};
它的构造器很简单,只是拷贝传入的VolumeRegion的vector,并计算总体的包围盒。
<Volume Scattering Definitions> +=
AggregateVolume::AggregateVolume(const vector<VolumeRegion *> &r) {
regions = r;
for (uint32_t i = 0; i < regions.size(); ++i)
bound = Union(bound, regions->WorldBound());
}
<AggregateVolume Private Data> =
vector<VolumeRegion *> regions;
BBox bound;
如前所述,大多数的函数实现只是对每个区域循环,并做相应的函数调用,这里只列出了AggregateVolume::sigma_a()的实现,其它函数在此略去。
<Volume Scattering Definitions> +=
Spectrum AggregateVolume::sigma_a(const Point &p, const Vector &w,
float time) const {
Spectrum s(0.);
for (uint32_t i = 0; i < regions.size(); ++i)
s += regions->sigma_a(p, w, time);
return s;
}
有一个不太容易实现的是函数IntersectP()。t的参数范围是所有体积区域的tmin的最小值到所有区域tmax的最大值。
<Volume Scattering Definitions> +=
bool AggregateVolume::IntersectP(const Ray &ray,
float *t0, float *t1) const {
*t0 = INFINITY;
*t1 = -INFINITY;
for (uint32_t i = 0; i < regions.size(); ++i) {
float tr0, tr1;
if (regions->IntersectP(ray, &tr0, &tr1)) {
*t0 = min(*t0, tr0);
*t1 = max(*t1, tr1);
}
}
return (*t0 < *t1);
}