转载:http://www.opengpu.org/forum.php?mod=viewthread&tid=5428&fromuid=10107
第九章 反射模型
本章定义了一组类来描述光源是如何在表面上散射的。回忆一下,在第5.4节中我们介绍了双向反射分布函数(BRDF),来描述光在某个表面上的反射;又介绍了BTDF来描述在表面上的透射,而BSDF把这两种效果综合起来。描述真实表面上的散射的最好方法常常是多个BRDF和BTDF的综合;在第10章里,我们将介绍一个BSDF对象,将多个BRDF和BTDF组合在一起来表示光在表面上的总体上的散射效果。本章不考虑在表面上反射性质和透射性质发生变化的情况,在第11章中,描述纹理的类会处理这个问题。
表面反射模型有几个来源:
1. 实验数据:许多真实世界中的表面的反射分布性质是在实验室里测量出来的。这些数据可以直接放在表格里使用,或者用来计算一组基函数的系数。
2.现象学模型:建立描述真实世界里表面的性质的方程,并且这些方程可以很有效地模拟出这些性质。像BSDF这类模型就非常好用,因为它们可以用直观的参数来改变模型的行为(例如“粗糙度”)。计算机图形学中的许多反射函数都属于这个范畴。
3.仿真:有时我们知道一个表面的组成部分的底层信息。例如,我们可能知道一种颜料是由悬浮在介质中的大小大致相同的彩色颗粒组成的,或者知道一种编织物是有两种类型的线构成,每种线都有已知的反射特性。在这样的情况下,我们可以通过模拟微观几何的散射来得到反射数据。这个模拟过程可以是在渲染中进行的,也可以是一种预处理过程,并由此拟合出一组基函数以备后用。
4. 物理(波动)光学: 有些反射模型是由一种非常精细的光模型推衍出来的,其中光被视为一种波,并通过解麦克斯韦方程来研究光在表面上的散射过程。这些模型通常需要耗时的计算,却并不一定会得到比几何光学模型更精确的结果。
5,几何光学:跟仿真方法相似,如果知道了表面的底层的散射和几何性质,就可以从这些性质中推导出有解析表达式的反射模型。几何光学模型使得光对表面的交互作用更容易处理,因为诸如偏振现象的复杂效应被忽略了。
基本术语
为了比较不同反射模型的视觉效果,我们介绍一些关于表面反射的基本术语。
表面反射可以分为四大类:漫反射(diffuse),光泽镜面反射(glossy specular),理想镜面反射(perfect specular),逆反射(retro-reflective)。大多数表面的反射是这四类反射的混合。漫发射将光线均等地向所有方向上散射。虽然很难见到理想的漫反射表面,但接近漫反射的例子有无光泽的黑板,无光涂料等等。象塑料、高光泽涂料这样的光泽镜面表面向一组特定的方向进行散射--它们可以映照出其它物体的模糊的映像。理想镜面表面只向一个单一的方向散射。镜子和玻璃是理想镜面表面的例子。最后,象天鹅绒或在地球上看到的月亮这样的逆反射表面主要向入射光方向对光线进行散射。(如图: a -漫反射, b -光泽镜面反射, c -理想镜面反射, d -逆反射)。
给定了一个反射类型,反射分布函数可以是各向同性(isotropic)的或者是各向异性(anisotropic)的。大多数物体是各向同性的:如果选定表面上的一个点绕着该点的法向量旋转它,所反射的光的总量不变。相反地,对各向异性材料做这样的旋转,就会反射出不同量的光。各向异性表面的例子有被擦亮的金属、唱机唱片和CD盘片。
几何设置
在pbrt中的反射计算是在一个反射坐标系中进行的,在该坐标系中,被着色的点上的两个切向量和法向量分别跟x,y,z轴对齐,所有BRDF和BTDF函数的传入向量和返回向量都是在这个坐标系下定义的。理解好这个坐标系对后面理解BRDF和BTDF的实现非常重要。
着色坐标系还给出了一个用球面坐标(θ,Φ)来表示方向的标架;角度θ是给定方向跟z轴的夹角,Φ是给定方向在xy平面上的投影跟x轴的夹角。(如图)
给定了在该坐标系中的一个方向,就可以很容易地计算出它与法向量夹角的余弦,等等:
cos θ = (n . ω) = ( (0, 0, 0) . ω) = ωz
下面就是求这个余弦值的工具函数:
<BSDF Inline Function> =
inline float CosTheta (const Vector &w) { return w.z;}
利用 sin2θ + cos2θ = 1, 可得到相应的正弦值:
<BSDF Inline Functions> +=
inline float SinTheta(const Vector &w) {
return sqrtf(max(0.f, 1.f - w.z * w.z));
}
下面是求sin2θ的工具函数:
<BSDF Inline Functions> +=
inline float SinTheta2(const Vector &w) {
return 1.f - CosTheta(w) * CosTheta(w);
}
类似地我们可以用着色坐标系统来简化关于Φ的正弦和余弦计算。在着色点所在的平面上方向ω有坐标(x, y),分别为 r cosΦ 和 r sinΦ。而半径r即是sin θ, 因此:
cos Φ = x / r = x / sin θ
sin Φ = y / r = y / sin θ
<BSDF Inline Functions> +=
inline float CosPhi(const Vector &w) {
return w.x / SinTheta(w);
}
inline float SinPhi(const Vector &w) {
return w.y / SinTheta(w);
}
我们要遵守的一个约定是,入射光ωi和向外的观察方向ωo 在被变换到表面上的局部坐标系以后,都是被正规化的,且方向都朝外。根据约定,表面法向量总是指向物体之外的,这就很容易确定光线是进入还离开透光物体:如果入射光方向ωi跟n在同一个半球,则光线是在进入,否则就是在离开。
因此,需要记住的这一点:表面法向量可能跟ωi或者ωo(或者两者都有)不在物体的同一侧。跟许多其它渲染器不同的是,pbrt并不为了保持ωo和法向量在表面同一侧而翻转法向量。所以,在实现BRDF和BTDF时就不能做这样的假定。
还有,应该注意用于着色的局部坐标系可能并不等同于Shape::Intersect()所返回的坐标系,它们在求交过程和着色过程之间可以做些改动以达到象凸凹纹理映射(bump mapping)这类的效果。
在阅读本章时还要注意的一点是,BRDF和BTDF的实现不应该关心ωi和ωo是否位于同一个半球。例如,虽然一个反射BRDF应该探测是否入射光方向在表面的上面并且出射方向在表面的下面,从而可判定在这种情况下不存在反射,但是我们希望反射函数能够利用反射模型中的相应公式来计算光的反射量,而忽略它们是否在同一个半球这个细节。更高层的pbrt代码会保证反射例程或透射的散射例程能在恰当的时机被调用。
9.1 基本接口
首先我们定义BRDF和BTDF函数的接口。BRDF和BTDF共享一个基类,BxDF,它定义了要实现的基本接口。两类函数共用一个接口,共享一个基类,可以减少重复性代码,也可以是系统的某些部分可以使用BxDF而不必区分BRDF和BTDF。
<BxDF Declarations> =
class COREDLL BxDF {
public:
<BxDF Interface>
<BxDF Public Data>
};
将要在第10.1节介绍BSDF类存放一组BxDF对象来共同地描述表面上一个点的散射情况。虽然我们通过使用一个共同接口而隐藏了关于反射材质和透射材质的实现细节,但是第16章所介绍的一些光传输算法需要区分这两种类型。因此,所有的BxDF要有一个BxDF::type成员还存放BxDFType标志。对于每个BxDF而言,标志必须在BSDF_REFLECTION或BSDF_TRANSMISSION集合中取一个值,而且必须是漫反射、光泽反射和镜面反射标志之一。注意这里没有逆反射标志,这里的分类将逆反射视为光泽反射。
<BSDF Declarations> =
enum BxDFType {
BSDF_REFLECTION = 1 << 0,
BSDF_TRASNMISSION = 1 << 1,
BSDF_DIFFUSE = 1 << 2,
BSDF_GLOSSY = 1 << 3,
BSDF_SPECULAR = 1 << 4,
BSDF_ALL_TYPES = BSDF_DIFFUSE | BSDF_GLOSSY | BSDF_SPECULAR,
BSDF_ALL_REFLECTION = BSDF_REFLECTION | BSDF_ALL_TYPES,
BSDF_ALL_TRASNMISSION = BSDF_TRASNMISSION | BSDF_ALL_TYPES,
BSDF_ALL = BSDF_ALL_REFLECTION | BSDF_ALL_TRASNMISSION
};
<BxDF Public Data> =
const BxDFType type;
<BxDF Interface> =
BxDF(BxDFType t) : type (t) { }
MatchesFlags()工具函数用来确定BxDF是否具备用户提供的标志:
<BxDF Interface> +=
bool MatchesFlag(BxDFType flags) const {
return (type & flags) == type;
}
BxDF的关键函数是BxDF::f()。它返回给定的一对方向所对应的分布函数值。这个接口隐性地假定不同波长上的光是不相干(decoupled)的,即一个波长上的能量不会以不同的波长被反射出去。这样一来,就不会支持萤光材料了。有了这个假定,反射函数的结果就可以直接用一个Spectrum来表示:
<BxDF Interface> +=
virtual Spectrum f(const Vector &wo, const Vector &wi) const = 0;
并不是所有的BxDF都用该函数求值。例如,象镜子、玻璃或水这样的全镜面反射物体只在单一出射方向上对单一方向的入射光进行散射。描述这样的BxDF的最好方法是delta分布函数:除了出射方向之外,其它方向的值都为0.
在pbrt中,这些BxDF需要特殊的处理,所以我们提供BxDF::Sample_f()函数。我们用这个函数处理由delta分布来描述的散射,也用它来对BxDF所散射出的多个光线的方向进行随机采样(见第15章)。给定了出射方向之后,BxDF::Sample_f()计算入射光的方向ωi,并根据这一对方向来计算BxDF值。对于delta分布而言,需要利用这种方式来生成适当的ωi方向,因为调用者无法生成相应的ωi方向。利用delta分布的BxDF并不需要参数u1,u2和pdf,在我们介绍非镜面反射函数时再介绍它们。
<BxDF Interface> +=
virtual Spectrum Sample_f(const Vector &wo, Vector *wi, float u1, float u2, float *pdf) const;
9.1.1 反射率
有时我们需要将4D的BRDF或BTDF(定义为一个方向对的函数)的聚合行为缩减为单一方向上的2D函数,甚至缩减为一个常量来描述总体上的散射行为。
半球-方向反射率函数(hemispherical-directional reflectance)是一个2D函数,它给出了对半球上的恒定照明下的在某个给定方向上的总反射量,也可以被等价地认为是来自某个给定方向上的光在半球上的总反射量。定义如下:
BxDF::rho()函数用来计算上面的ρhd。某些BxDF可以用解析表达式来计算这个值,虽然大多数的BxDF用Monte Carlo积分来计算其近似值。对于这些BxDF而言,参数nSamples和samples用来控制Monte Carlo算法的行为(见15.5.5节)。
<BxDF Interface> +=
virtual Spectrum rho(const Vector &wo, int nSamples = 16, float *samples = NULL) const;
表面的半球-半球反射率(hemispherical- hemispherical reflectance)是一个光谱值常量,记作ρhh,它给出了在所有方向有相同的入射光的情况下一个表面上被反射的入射光的比率。
当用户没有提供方向ωo时,我们重载函数BxDF::rho()来计算ρhh,其余的参数在计算Monte Carlo近似值时会用到。
<BxDF Interface> +=
virtual Spectrum rho(int nSamples = 16, float *samples = NULL) const;
9.1.2 BRDF->BTDF适配器
为了将已经定义的BRDF当作BTDF使用,特别是当BRDF的现象学模型对描述透射现象也很不错的情况下,我们定义一个适配器类就很便利地做到这一点。BRDFToBTDF类用一个BRDF的指针作为构造器的参数,并利用它来实现一个BTDF。特别地,这需要将函数调用传给BRDF,并翻转ωi方向使之处于另一个半球。
<BxDF Declarations> +=
class COREDLL BRDFToBTDF : public BxDF {
public:
<BRDFToBTDF Public Methods>
private:
BxDF *brdf;
};
适配器的构造器很简单,只需将BxDF::type中的反射标志和透射标志对调一下。
<BRDFToBTDF Public Methods> =
BRDFToBTDF(BxDF *b)
: BxDF(BxDFType(b->type ^ ( BSDF_REFLECTION | BSDF_TRANSMISSION))) {
brdf = b;
}
适配器需要将入射方向转换为另一半球上相应的方向。这在着色坐标系下很容易做到,只需将向量的z值取负值即可。
<BRDFToBTDF Public Methods> +=
static Vector otherHemisphere(const Vector &w) {
return Vector(w.x, w.y, -w.z);
}
在调用BRDF的BxDF::rho(), BxDF::f(), 和BxDF::Sample_f()函数时,我们调用BRDFToBTDF:: otherHemisphere()来将光线翻转到另一个半球上:
BRDFToBTDF Public Methods> +=
Specturm rho(const Vector &w, int nSamples, float *samples) const {
return brdf->rho(otherHemisphere(w), nSamples, samples);
}
Specturm rho(int nSamples, float *samples) const {
return brdf->rho(nSamples, samples);
}
<BxDF Method Definitions> =
Specturm BRDFToBTDF::f(const Vector &wo, const Vector &wi) const {
return brdf->f(wo, otherHemisphere(wi));
}
<BxDF Method Definitions> +=
Specturm BRDFToBTDF::Sample_f (const Vector &wo, const Vector &wi,
float u1, float u2, float *pdf) const {
Spectrum f = brdf->Sample_f(wo, wi, u1, u2, pdf);
*wi = otherHermisphere(*wi);
return f;
}
9.2 镜面反射和透射
对于光在完全光滑的表面上的行为,我们可以比较容易地用物理模型和几何模型进行解析上的分析。这些表面上的入射光表现出理想镜面反射和透射;对于给定的方向ωi,所有的光线被散射到单一的出射方向ωo。对于镜面反射,这个方向跟法向量的夹角等于入射光跟法向量的夹角:
θi = θo
对于透射,出射方向由斯涅耳(Snell)定律决定,该定律给出了透射方向和法向量n的夹角θt跟入射方向和法向量n的夹角θi的关系式。Snell定律基于入射光所在的介质的折射率(index of refraction)和光线所要进入的介质的折射率。折射率表述了光线在特定介质中传播跟在真空中传播相比较下的减慢程度。我们用希腊字母η(eta)来表示折射率,Snell定律可表示如下:
ηi sin θi = ηt sin θt
在一般情况下,折射率随光的波长的不同而有所变化。这样,在两种不同介质的交界处,入射光通常被散射到多个方向上,即色散现象(dispersion)。当入射的白光穿过一个棱镜被分离出不同的光谱成分时,就可以看到这种现象。在计算机图形学的实际应用中,我们忽略了这种波长的相关性,因为这种现象对视觉上的精确性并不重要,对它的忽略可以极大地简化光线传输的计算。
下图是用一个理想镜面反射的BRDF(图a)和一个镜面透射的BTDF(图b)对Killeroo模型进行渲染的效果。注意通过透明物体的折射光线将背后的场景变形了。
9.2.1菲涅耳(Fresnel) 反射率
除了计算反射方向和透射方向之外,还需要计算入射光被反射或透射的比率。在简单的光线追踪器中,这些比率常常被称为“反射率”或“透射率”,它们在整个表面上被视为常量。然而,对于物理上的反射或折射而言,这些量是跟视角相关的,不可以用一个常量的比例因子来表达。菲涅耳(Fresnel)方程描述了光在表面上被反射的量;它实际上是麦克斯韦方程组在光滑表面上的解。
有两种Fresnel方程,一种用于绝缘体(如玻璃),一种用于导体(如金属)。对于这两种情况,Fresnel方程又根据入射光的偏振情况分两种形式。在渲染中恰当地处理偏振现象是很复杂的,所以在pbrt中我们通常假定光是没有偏振的,也就是说,光波的朝向是随机的。有了这个假定,Fresnel反射率就等于平行偏振项的平方和垂直偏振项的平方的平均值。
为了计算绝缘体的Fresnel反射率,我们需要知道两种介质的折射率。表9.1列出了几个常见的绝缘材质的折射率
下面是对绝缘体Fresnel反射率公式的近似表达式:
r|| = (ηt cosθi - ηi cosθt) / (ηt cosθi + ηi cosθt)
rT = (ηi cosθi - ηt cosθt) / (ηi cosθi + ηt cosθt)
其中r||是平行偏振光的Fresnel反射率,rT是垂直偏振光的Fresnel反射率,ηi和ηt分别是入射光所在的介质的折射率和透射介质的折射率,ωo和ωt分别是入射方向和透射方向,其中ωt是用Snell定律计算出来的。
对于无偏振的光,Fresnel反射率为:
Fr = (r||2 + rT2) / 2
函数FrDiel()计算绝缘材质和圆偏振光所使用的Fresnel反射公式。cosθi和cosθt分别用参数cosi和cost传入:
<BxDF Utility Functions> =
COREDLL Spectrum FrDiel(float cosi, float cost, const Spectrum &etai, const Spectrum &etat)
{
Specturm Rparl = ((etat * cosi) - (etai * cost)) /
((etat * cosi) + (etai * cost));
Specturm RparP = ((etai * cosi) - (etat * cost)) /
((etai * cosi) + (etat * cost));
return (Rparl * Rparl + RparP * RparP) / 2.f;
}
根据能量守恒定律,绝缘体所透射的能量为 1 - Fr。
与绝缘体不同的是,导体对光不产生透射,但一些入射光会被材料吸收并转化为热量。我们用Fresnel公式可以得出有多少光被反射了。该公式要用到导体的折射率η和吸收系数k。表9.2列出了一些导体的η和k值:
许多导体的η和k值是未知的,因为已做的关于导体的测量要比绝缘体的测量要少得多。但是有两个近似方法可以计算出令人满意的值。这两种方法都假设物体的反射率是沿法向量的入射方向上测量出的,即:观察者和光源都是沿法向量直视/直照到表面上的。将η或k值中的一个固定,代入到导体Fresnel公式,就可以确定另一个值,也就可以计算出沿法向量的入射方向的反射率(Cook and Torrance 1982)。
这个方法的第一个版本就是在假定吸收系数为0的情况下计算η的相似值。如果k=0且cosθi = 1(入射方向为法向量),则前面提到的两个公式就简化为:
r|| 2= rT2 = (η2 - 2 η + 1) /(η2 +2 η + 1) = ((η -1)/( η+1))2
因为在入射方向为法向量的情况下Fresnel反射率是已知的,就可以解出η:
η = (1 + Fr1/2) / (1-Fr1/2)
<BxDF Utility Functions> +=
COREDLL Specturm FresnelApproxEta(const Spectrum &Fr) {
Spectrum reflectance = Fr.Clamp(0.f, .999f);
return (Spectrum(1.) + reflectance.Sqrt()) / (Spectrum(1.) - reflectance.Sqrt());
}
我们可以用相同的方法计算吸收系数k,这里假定η=1。Fresnel就简化为:
r|| 2= rT2 = k2 / (k2+4)
我们就可以很容易地解出k:
k = 2 ( Fr / (1 - Fr))1/2
<BxDF Utility Functions> +=
COREDLL Specturm FresnelApproxK(const Spectrum &Fr) {
Spectrum reflectance = Fr.Clamp(0.f, .999f);
return 2.f * (reflectance / (Spectrum(1.) - refletance)).Sqert();
}
为了方便,我们定义一个抽象类Fresnel来提供计算Fresnel反射系数的接口。FresnelConductor和FresnelDislectric实现类有助于简化BRDF的实现。
<BxDF Declarations> +=
class COREDLL Fresnel {
public:
<Fresnel Interface>
};
Fresnel接口所提供的唯一函数是Fresnel::Evaluate()。给定了入射方向和法向量的夹角余弦cosi,该函数返回表面反射光的量值:
<Fresnel Interface> =
virtual Spectrum Evaluate (float cosi) const = 0;
Fresnel导体
<BxDF Declarations> +=
class COREDLL FresnelConductor : public Fresnel {
public:
<FresnelConductor Public Methods>
private:
<FresnelConductor Private Data>
};
FresnelConductor构造器只是简单地存放给定的折射率η和吸收系数k:
<FresnelConductor Private Data>
Spectrum eta, k;
<FresnelConductor Public Methods>
FresnelConductor(const Spectrum &e, const Spectrum &kk)
: eta(e), k(kk) {
}
FresnelConductor的求值例程很简单,只是调用前面讲过的FrCond()函数而已。注意这里取cosi的绝对值,因为FrCond()要求法向量和入射方向ωi在同一侧:
<BxDF Method Definitions> +=
Spectrum FresnelConductor::Evaluate(float cosi) const {
return FrCond(fabsf(cosi), eta, k);
}
Fresnel Dielectrics
<BxDF Declarations> +=
class COREDLL FresnelDielectric : public Fresnel {
public:
<FresnelDielectric Public Methods>
private:
<FresnelDielectric Private Data>
};
FresnelDielectric构造器只是存放表面两侧的折射率:
<FresnelDielectric Private Data> =
float eta_i, eta_t;
<FresnelDielectric Public Methods> =
FresnelDielectric(float ei, float et) {
eta_i = ei;
eta_t = et;
}
<BxDF Method Definitions> +=
Spectrum FresnelDielectric::Evaluate(float cosi) const {
<Compute Fresnel reflectance for dielectric>
}
对绝缘体Fresnel公式求值要比导体复杂些。首先,需要确定入射方向是在介质的内部还是外部,这样才能正确地使用两个折射率。其次,需要使用Snell定律来计算透射方向和法向量的夹角。最后,用cos2θ + sin2θ = 1计算出该角度的余弦值来。
<Compute Fresnel reflectance for dielectric> =
cosi = Clamp(cosi, -1.f, 1.f);
<Compute indices of refraction for dielectric>
<Compute sint using Snell's law>
if (sint > 1.) {
<Handle total internal reflection>
}
else {
float cost = sqrtf(max(0.f, 1.f - sint*sint));
return FrDiel(fabsf(cosi), cost, ei, et);
}
我们用入射角的余弦符号可以判定光线在介质的哪一侧。如果余弦值在0到1之间,则光线在介质外面,否则就在外面。变量ei和et分别被设置为入射介质和透射介质的折射率。
<Compute indices of refraction for dielectric>
bool entering = cosi > 0.;
float ei = eta_i, et = eta_t;
if (!entering)
swap(ei, et);
一旦设置好折射率,就可以直截了当地用Snell定律计算sinθt:
<Compute sint using Snell's law>
float sint = ei/et * sqrtf(max(0.f, l.f - cosi*cosi));
当光线从高折射率的介质进入低折射率的介质时,如果入射角大于临界角(critical angle),就产生全内反射(total internal reflection),即所有光线都被反射回来。如果sinθt大于1,就是这种情况。在这种情况下,就没有必要使用Fresnel方程了。
<Handle total internal reflection> =
return 1.;
一个特殊的Fresnel接口
FresnelNoOp实现了Fresnel接口,它只是对所有的入射方向返回100%的反射率。
<BxDF Declarations> +=
class COREDLL FresnelNoOp : public Fresnel {
public:
Spectrum Evaluate(float) const { return Spectrum(l.); }
};
9.2.2 镜面反射
现在我们可以实现SpecularReflection类了,该类使用Fresnel类描述了物理效果上颇令人信服的镜面反射。首先,我们要推导出描述镜面反射的BRDF。由于Fresnel方程给出了光线反射率,Fr(ωi),我们需要一个满足下面关系的BRDF:
Lo(ωo) = fr(ωo, ωi) Li(ωi) = Fr(ωi)Li(ωi)
其中ωi是ωo关于法向量的反射向量。(回忆一下对于镜面反射θi = θo, 故Fr(ωo) = Fr(ωi))。
该BRDF可以用狄拉克delta分布构造出来。第7.1节介绍过delta分布有个很重要的性质:
? f(x) δ(x - x0) dx = f(x0)
跟标准函数相比较,delta分布函数需要特殊的处理。特别地,对delta分布的积分必须显式地按照delta分布的本身的意义来求值,否则它们的值就无法计算。例如,考虑上面的式子,如果用梯形面积规则或其它数值积分技术来求值,那么根据delta分布的定义,任何求值点xi都不会有非零的δ(xi)值。所以,我们必须允许delta分布自己来确定求值点。
直观地讲,我们希望BRDF在除了理想反射方向之外的任何地方都是0,所以我们就要求助于delta分布。最容易想到的方法是简单地使用delta函数,将入射方向限制在反射角度ωr。这样就产生如下的BRDF:
其中R(ωo, n)是ωo关于法向量n的镜面反射向量。
<BxDF Declarations> +=
class COREDLL SpecularReflection : public BxDF {
public:
<SpecularReflection Public Methods>
private:
<SpecularReflection Private Data>
};
SpecularReflection使用了一个Fresnel对象来描述绝缘体或导体的Fresnel特性,还使用了一个Spectrum对象,用于对反射的颜色进行比例变换。
<SpecularReflection Public Methods> =
SpecularReflection(const Spectrum &r, Fresnel *f)
: BxDF(BxDFType(BSDF_REFLECTION | BSDF_SPECULAR)),
R(r), fresnel(f) {
}
<SpecularReflection Private Data> =
Spectrum R;
Fresnel *fresnel;
其它的实现还是很直截了当的。SpecularReflection::f()不产生散射,因为对于任意的一对方向,delta函数不产生散射。
<SpecularReflection Public Methods> +=
Spectrum f(const Vector &, const Vector &) const {
return Spectrum(0.);
}
现在我们实现Sample_f()函数,它根据delta分布来选择适当的方向。它将输出变量wi设置为wo关于法向量的反射方向。*pdf值被设置为1,因为在此情况下没有Monte Carlo采样。
<BxDF Method Definitions> +=
Spectrum SpecularReflection::Sample_f(const Vector &wo,
Vector *wi, float u1, float u2, float *pdf) const {
<Compute perfect specular reflection direction>
*pdf = l.f;
return fresnel->Evaluate(CosTheta(wo)) * R / fabsf(CosTheta(*wi));
}
(如图)我们要计算的方向是关于ωo法向量的反射方向。因为所有计算是在着色坐标系下进行的,其中法向量是(0,0,1),我们只需绕法向量将ωi旋转180度。在第2章我们讲到了绕z轴的选择矩阵,如果旋转角度为π,则矩阵如下:
bzm_square
std::cout << "hello world" ;
PBRT阅读: 第九章 反射模型 第9.1-9.2节
2012-05-17 23:28:31| 分类: Ray|字号 订阅
第九章 反射模型
本章定义了一组类来描述光源是如何在表面上散射的。回忆一下,在第5.4节中我们介绍了双向反射分布函数(BRDF),来描述光在某个表面上的反射;又介绍了BTDF来描述在表面上的透射,而BSDF把这两种效果综合起来。描述真实表面上的散射的最好方法常常是多个BRDF和BTDF的综合;在第10章里,我们将介绍一个BSDF对象,将多个BRDF和BTDF组合在一起来表示光在表面上的总体上的散射效果。本章不考虑在表面上反射性质和透射性质发生变化的情况,在第11章中,描述纹理的类会处理这个问题。
表面反射模型有几个来源:
1. 实验数据:许多真实世界中的表面的反射分布性质是在实验室里测量出来的。这些数据可以直接放在表格里使用,或者用来计算一组基函数的系数。
2.现象学模型:建立描述真实世界里表面的性质的方程,并且这些方程可以很有效地模拟出这些性质。像BSDF这类模型就非常好用,因为它们可以用直观的参数来改变模型的行为(例如“粗糙度”)。计算机图形学中的许多反射函数都属于这个范畴。
3.仿真:有时我们知道一个表面的组成部分的底层信息。例如,我们可能知道一种颜料是由悬浮在介质中的大小大致相同的彩色颗粒组成的,或者知道一种编织物是有两种类型的线构成,每种线都有已知的反射特性。在这样的情况下,我们可以通过模拟微观几何的散射来得到反射数据。这个模拟过程可以是在渲染中进行的,也可以是一种预处理过程,并由此拟合出一组基函数以备后用。
4. 物理(波动)光学: 有些反射模型是由一种非常精细的光模型推衍出来的,其中光被视为一种波,并通过解麦克斯韦方程来研究光在表面上的散射过程。这些模型通常需要耗时的计算,却并不一定会得到比几何光学模型更精确的结果。
5,几何光学:跟仿真方法相似,如果知道了表面的底层的散射和几何性质,就可以从这些性质中推导出有解析表达式的反射模型。几何光学模型使得光对表面的交互作用更容易处理,因为诸如偏振现象的复杂效应被忽略了。
基本术语
为了比较不同反射模型的视觉效果,我们介绍一些关于表面反射的基本术语。
表面反射可以分为四大类:漫反射(diffuse),光泽镜面反射(glossy specular),理想镜面反射(perfect specular),逆反射(retro-reflective)。大多数表面的反射是这四类反射的混合。漫发射将光线均等地向所有方向上散射。虽然很难见到理想的漫反射表面,但接近漫反射的例子有无光泽的黑板,无光涂料等等。象塑料、高光泽涂料这样的光泽镜面表面向一组特定的方向进行散射--它们可以映照出其它物体的模糊的映像。理想镜面表面只向一个单一的方向散射。镜子和玻璃是理想镜面表面的例子。最后,象天鹅绒或在地球上看到的月亮这样的逆反射表面主要向入射光方向对光线进行散射。(如图: a -漫反射, b -光泽镜面反射, c -理想镜面反射, d -逆反射)。
给定了一个反射类型,反射分布函数可以是各向同性(isotropic)的或者是各向异性(anisotropic)的。大多数物体是各向同性的:如果选定表面上的一个点绕着该点的法向量旋转它,所反射的光的总量不变。相反地,对各向异性材料做这样的旋转,就会反射出不同量的光。各向异性表面的例子有被擦亮的金属、唱机唱片和CD盘片。
几何设置
在pbrt中的反射计算是在一个反射坐标系中进行的,在该坐标系中,被着色的点上的两个切向量和法向量分别跟x,y,z轴对齐,所有BRDF和BTDF函数的传入向量和返回向量都是在这个坐标系下定义的。理解好这个坐标系对后面理解BRDF和BTDF的实现非常重要。
着色坐标系还给出了一个用球面坐标(θ,Φ)来表示方向的标架;角度θ是给定方向跟z轴的夹角,Φ是给定方向在xy平面上的投影跟x轴的夹角。(如图)
给定了在该坐标系中的一个方向,就可以很容易地计算出它与法向量夹角的余弦,等等:
cos θ = (n . ω) = ( (0, 0, 0) . ω) = ω
z
下面就是求这个余弦值的工具函数:
<BSDF Inline Function> =
inline float CosTheta (const Vector &w) { return w.z;}
利用 sin
2
θ + cos
2
θ = 1, 可得到相应的正弦值:
<BSDF Inline Functions> +=
inline float SinTheta(const Vector &w) {
return sqrtf(max(0.f, 1.f - w.z * w.z));
}
下面是求sin
2
θ的工具函数:
<BSDF Inline Functions> +=
inline float SinTheta2(const Vector &w) {
return 1.f - CosTheta(w) * CosTheta(w);
}
类似地我们可以用着色坐标系统来简化关于Φ的正弦和余弦计算。在着色点所在的平面上方向ω有坐标(x, y),分别为 r cosΦ 和 r sinΦ。而半径r即是sin θ, 因此:
cos Φ = x / r = x / sin θ
sin Φ = y / r = y / sin θ
<BSDF Inline Functions> +=
inline float CosPhi(const Vector &w) {
return w.x / SinTheta(w);
}
inline float SinPhi(const Vector &w) {
return w.y / SinTheta(w);
}
我们要遵守的一个约定是,入射光ωi和向外的观察方向ωo 在被变换到表面上的局部坐标系以后,都是被正规化的,且方向都朝外。根据约定,表面法向量总是指向物体之外的,这就很容易确定光线是进入还离开透光物体:如果入射光方向ωi跟n在同一个半球,则光线是在进入,否则就是在离开。
因此,需要记住的这一点:表面法向量可能跟ωi或者ωo(或者两者都有)不在物体的同一侧。跟许多其它渲染器不同的是,pbrt并不为了保持ωo和法向量在表面同一侧而翻转法向量。所以,在实现BRDF和BTDF时就不能做这样的假定。
还有,应该注意用于着色的局部坐标系可能并不等同于Shape::Intersect()所返回的坐标系,它们在求交过程和着色过程之间可以做些改动以达到象凸凹纹理映射(bump mapping)这类的效果。
在阅读本章时还要注意的一点是,BRDF和BTDF的实现不应该关心ωi和ωo是否位于同一个半球。例如,虽然一个反射BRDF应该探测是否入射光方向在表面的上面并且出射方向在表面的下面,从而可判定在这种情况下不存在反射,但是我们希望反射函数能够利用反射模型中的相应公式来计算光的反射量,而忽略它们是否在同一个半球这个细节。更高层的pbrt代码会保证反射例程或透射的散射例程能在恰当的时机被调用。
9.1 基本接口
首先我们定义BRDF和BTDF函数的接口。BRDF和BTDF共享一个基类,BxDF,它定义了要实现的基本接口。两类函数共用一个接口,共享一个基类,可以减少重复性代码,也可以是系统的某些部分可以使用BxDF而不必区分BRDF和BTDF。
<BxDF Declarations> =
class COREDLL BxDF {
public:
<BxDF Interface>
<BxDF Public Data>
};
将要在第10.1节介绍BSDF类存放一组BxDF对象来共同地描述表面上一个点的散射情况。虽然我们通过使用一个共同接口而隐藏了关于反射材质和透射材质的实现细节,但是第16章所介绍的一些光传输算法需要区分这两种类型。因此,所有的BxDF要有一个BxDF::type成员还存放BxDFType标志。对于每个BxDF而言,标志必须在BSDF_REFLECTION或BSDF_TRANSMISSION集合中取一个值,而且必须是漫反射、光泽反射和镜面反射标志之一。注意这里没有逆反射标志,这里的分类将逆反射视为光泽反射。
<BSDF Declarations> =
enum BxDFType {
BSDF_REFLECTION = 1 << 0,
BSDF_TRASNMISSION = 1 << 1,
BSDF_DIFFUSE = 1 << 2,
BSDF_GLOSSY = 1 << 3,
BSDF_SPECULAR = 1 << 4,
BSDF_ALL_TYPES = BSDF_DIFFUSE | BSDF_GLOSSY | BSDF_SPECULAR,
BSDF_ALL_REFLECTION = BSDF_REFLECTION | BSDF_ALL_TYPES,
BSDF_ALL_TRASNMISSION = BSDF_TRASNMISSION | BSDF_ALL_TYPES,
BSDF_ALL = BSDF_ALL_REFLECTION | BSDF_ALL_TRASNMISSION
};
<BxDF Public Data> =
const BxDFType type;
<BxDF Interface> =
BxDF(BxDFType t) : type (t) { }
MatchesFlags()工具函数用来确定BxDF是否具备用户提供的标志:
<BxDF Interface> +=
bool MatchesFlag(BxDFType flags) const {
return (type & flags) == type;
}
BxDF的关键函数是BxDF::f()。它返回给定的一对方向所对应的分布函数值。这个接口隐性地假定不同波长上的光是不相干(decoupled)的,即一个波长上的能量不会以不同的波长被反射出去。这样一来,就不会支持萤光材料了。有了这个假定,反射函数的结果就可以直接用一个Spectrum来表示:
<BxDF Interface> +=
virtual Spectrum f(const Vector &wo, const Vector &wi) const = 0;
并不是所有的BxDF都用该函数求值。例如,象镜子、玻璃或水这样的全镜面反射物体只在单一出射方向上对单一方向的入射光进行散射。描述这样的BxDF的最好方法是delta分布函数:除了出射方向之外,其它方向的值都为0.
在pbrt中,这些BxDF需要特殊的处理,所以我们提供BxDF::Sample_f()函数。我们用这个函数处理由delta分布来描述的散射,也用它来对BxDF所散射出的多个光线的方向进行随机采样(见第15章)。给定了出射方向之后,BxDF::Sample_f()计算入射光的方向ωi,并根据这一对方向来计算BxDF值。对于delta分布而言,需要利用这种方式来生成适当的ωi方向,因为调用者无法生成相应的ωi方向。利用delta分布的BxDF并不需要参数u1,u2和pdf,在我们介绍非镜面反射函数时再介绍它们。
<BxDF Interface> +=
virtual Spectrum Sample_f(const Vector &wo, Vector *wi, float u1, float u2, float *pdf) const;
9.1.1 反射率
有时我们需要将4D的BRDF或BTDF(定义为一个方向对的函数)的聚合行为缩减为单一方向上的2D函数,甚至缩减为一个常量来描述总体上的散射行为。
半球-方向反射率函数(hemispherical-directional reflectance)是一个2D函数,它给出了对半球上的恒定照明下的在某个给定方向上的总反射量,也可以被等价地认为是来自某个给定方向上的光在半球上的总反射量。定义如下:
BxDF::rho()函数用来计算上面的ρ
hd
。某些BxDF可以用解析表达式来计算这个值,虽然大多数的BxDF用Monte Carlo积分来计算其近似值。对于这些BxDF而言,参数nSamples和samples用来控制Monte Carlo算法的行为(见15.5.5节)。
<BxDF Interface> +=
virtual Spectrum rho(const Vector &wo, int nSamples = 16, float *samples = NULL) const;
表面的半球-半球反射率(hemispherical- hemispherical reflectance)是一个光谱值常量,记作ρ
hh
,它给出了在所有方向有相同的入射光的情况下一个表面上被反射的入射光的比率。
当用户没有提供方向ωo时,我们重载函数BxDF::rho()来计算ρ
hh
,其余的参数在计算Monte Carlo近似值时会用到。
<BxDF Interface> +=
virtual Spectrum rho(int nSamples = 16, float *samples = NULL) const;
9.1.2 BRDF->BTDF适配器
为了将已经定义的BRDF当作BTDF使用,特别是当BRDF的现象学模型对描述透射现象也很不错的情况下,我们定义一个适配器类就很便利地做到这一点。BRDFToBTDF类用一个BRDF的指针作为构造器的参数,并利用它来实现一个BTDF。特别地,这需要将函数调用传给BRDF,并翻转ωi方向使之处于另一个半球。
<BxDF Declarations> +=
class COREDLL BRDFToBTDF : public BxDF {
public:
<BRDFToBTDF Public Methods>
private:
BxDF *brdf;
};
适配器的构造器很简单,只需将BxDF::type中的反射标志和透射标志对调一下。
<BRDFToBTDF Public Methods> =
BRDFToBTDF(BxDF *b)
: BxDF(BxDFType(b->type ^ ( BSDF_REFLECTION | BSDF_TRANSMISSION))) {
brdf = b;
}
适配器需要将入射方向转换为另一半球上相应的方向。这在着色坐标系下很容易做到,只需将向量的z值取负值即可。
<BRDFToBTDF Public Methods> +=
static Vector otherHemisphere(const Vector &w) {
return Vector(w.x, w.y, -w.z);
}
在调用BRDF的BxDF::rho(), BxDF::f(), 和BxDF::Sample_f()函数时,我们调用BRDFToBTDF:: otherHemisphere()来将光线翻转到另一个半球上:
BRDFToBTDF Public Methods> +=
Specturm rho(const Vector &w, int nSamples, float *samples) const {
return brdf->rho(otherHemisphere(w), nSamples, samples);
}
Specturm rho(int nSamples, float *samples) const {
return brdf->rho(nSamples, samples);
}
<BxDF Method Definitions> =
Specturm BRDFToBTDF::f(const Vector &wo, const Vector &wi) const {
return brdf->f(wo, otherHemisphere(wi));
}
<BxDF Method Definitions> +=
Specturm BRDFToBTDF::Sample_f (const Vector &wo, const Vector &wi,
float u1, float u2, float *pdf) const {
Spectrum f = brdf->Sample_f(wo, wi, u1, u2, pdf);
*wi = otherHermisphere(*wi);
return f;
}
9.2 镜面反射和透射
对于光在完全光滑的表面上的行为,我们可以比较容易地用物理模型和几何模型进行解析上的分析。这些表面上的入射光表现出理想镜面反射和透射;对于给定的方向ωi,所有的光线被散射到单一的出射方向ωo。对于镜面反射,这个方向跟法向量的夹角等于入射光跟法向量的夹角:
θ
i
= θ
o
对于透射,出射方向由斯涅耳(Snell)定律决定,该定律给出了透射方向和法向量n的夹角θt跟入射方向和法向量n的夹角θi的关系式。Snell定律基于入射光所在的介质的折射率(index of refraction)和光线所要进入的介质的折射率。折射率表述了光线在特定介质中传播跟在真空中传播相比较下的减慢程度。我们用希腊字母η(eta)来表示折射率,Snell定律可表示如下:
η
i
sin θ
i
= η
t
sin θ
t
在一般情况下,折射率随光的波长的不同而有所变化。这样,在两种不同介质的交界处,入射光通常被散射到多个方向上,即色散现象(dispersion)。当入射的白光穿过一个棱镜被分离出不同的光谱成分时,就可以看到这种现象。在计算机图形学的实际应用中,我们忽略了这种波长的相关性,因为这种现象对视觉上的精确性并不重要,对它的忽略可以极大地简化光线传输的计算。
下图是用一个理想镜面反射的BRDF(图a)和一个镜面透射的BTDF(图b)对Killeroo模型进行渲染的效果。注意通过透明物体的折射光线将背后的场景变形了。
9.2.1菲涅耳(Fresnel) 反射率
除了计算反射方向和透射方向之外,还需要计算入射光被反射或透射的比率。在简单的光线追踪器中,这些比率常常被称为“反射率”或“透射率”,它们在整个表面上被视为常量。然而,对于物理上的反射或折射而言,这些量是跟视角相关的,不可以用一个常量的比例因子来表达。菲涅耳(Fresnel)方程描述了光在表面上被反射的量;它实际上是麦克斯韦方程组在光滑表面上的解。
有两种Fresnel方程,一种用于绝缘体(如玻璃),一种用于导体(如金属)。对于这两种情况,Fresnel方程又根据入射光的偏振情况分两种形式。在渲染中恰当地处理偏振现象是很复杂的,所以在pbrt中我们通常假定光是没有偏振的,也就是说,光波的朝向是随机的。有了这个假定,Fresnel反射率就等于平行偏振项的平方和垂直偏振项的平方的平均值。
为了计算绝缘体的Fresnel反射率,我们需要知道两种介质的折射率。表9.1列出了几个常见的绝缘材质的折射率。
下面是对绝缘体Fresnel反射率公式的近似表达式:
r
||
= (ηt cosθi - ηi cosθt) / (ηt cosθi + ηi cosθt)
r
T
= (ηi cosθi - ηt cosθt) / (ηi cosθi + ηt cosθt)
其中r
||
是平行偏振光的Fresnel反射率,r
T
是垂直偏振光的Fresnel反射率,ηi和ηt分别是入射光所在的介质的折射率和透射介质的折射率,ωo和ωt分别是入射方向和透射方向,其中ωt是用Snell定律计算出来的。
对于无偏振的光,Fresnel反射率为:
Fr = (r
||
2
+ r
T
2
) / 2
函数FrDiel()计算绝缘材质和圆偏振光所使用的Fresnel反射公式。cosθi和cosθt分别用参数cosi和cost传入:
<BxDF Utility Functions> =
COREDLL Spectrum FrDiel(float cosi, float cost, const Spectrum &etai, const Spectrum &etat)
{
Specturm Rparl = ((etat * cosi) - (etai * cost)) /
((etat * cosi) + (etai * cost));
Specturm RparP = ((etai * cosi) - (etat * cost)) /
((etai * cosi) + (etat * cost));
return (Rparl * Rparl + RparP * RparP) / 2.f;
}
根据能量守恒定律,绝缘体所透射的能量为 1 - Fr。
与绝缘体不同的是,导体对光不产生透射,但一些入射光会被材料吸收并转化为热量。我们用Fresnel公式可以得出有多少光被反射了。该公式要用到导体的折射率η和吸收系数k。表9.2列出了一些导体的η和k值:
导体Fresnel反射率公式的一个常用的近似公式为:
<BxDF Utility Functions> +=
COREDLL Spectrum FrCond (float cosi, const Spectrum &eta, const Spectrum &k) {
Spectrum tmp = (eta * eta + k * k) * cosi * cosi;
Spectrum Rparl2 = (tmp - (2.f * eta * cosi ) + 1) /
(tmp + (2.f * eta * cosi) + 1);
Spectrum tmp_f = eta * eta + k *k;
Spectrum Rperp2 = (tmp_f - (2.f * eta * cosi) + cosi * cosi) /
(tmp_f + (2.f *eta * cosi) + cosi * cosi);
return (Rparl2 + Rperp2) / 2.f;
}
许多导体的η和k值是未知的,因为已做的关于导体的测量要比绝缘体的测量要少得多。但是有两个近似方法可以计算出令人满意的值。这两种方法都假设物体的反射率是沿法向量的入射方向上测量出的,即:观察者和光源都是沿法向量直视/直照到表面上的。将η或k值中的一个固定,代入到导体Fresnel公式,就可以确定另一个值,也就可以计算出沿法向量的入射方向的反射率(Cook and Torrance 1982)。
这个方法的第一个版本就是在假定吸收系数为0的情况下计算η的相似值。如果k=0且cosθi = 1(入射方向为法向量),则前面提到的两个公式就简化为:
r
||
2
= r
T
2
= (η2 - 2 η + 1) /(η2 +2 η + 1) = ((η -1)/( η+1))2
因为在入射方向为法向量的情况下Fresnel反射率是已知的,就可以解出η:
η = (1 + F
r
1/2
) / (1-F
r
1/2
)
<BxDF Utility Functions> +=
COREDLL Specturm FresnelApproxEta(const Spectrum &Fr) {
Spectrum reflectance = Fr.Clamp(0.f, .999f);
return (Spectrum(1.) + reflectance.Sqrt()) / (Spectrum(1.) - reflectance.Sqrt());
}
我们可以用相同的方法计算吸收系数k,这里假定η=1。Fresnel就简化为:
r
||
2
= r
T
2
= k
2
/ (k
2
+4)
我们就可以很容易地解出k:
k = 2 ( Fr / (1 - Fr))
1/2
<BxDF Utility Functions> +=
COREDLL Specturm FresnelApproxK(const Spectrum &Fr) {
Spectrum reflectance = Fr.Clamp(0.f, .999f);
return 2.f * (reflectance / (Spectrum(1.) - refletance)).Sqert();
}
为了方便,我们定义一个抽象类Fresnel来提供计算Fresnel反射系数的接口。FresnelConductor和FresnelDislectric实现类有助于简化BRDF的实现。
<BxDF Declarations> +=
class COREDLL Fresnel {
public:
<Fresnel Interface>
};
Fresnel接口所提供的唯一函数是Fresnel::Evaluate()。给定了入射方向和法向量的夹角余弦cosi,该函数返回表面反射光的量值:
<Fresnel Interface> =
virtual Spectrum Evaluate (float cosi) const = 0;
Fresnel导体
<BxDF Declarations> +=
class COREDLL FresnelConductor : public Fresnel {
public:
<FresnelConductor Public Methods>
private:
<FresnelConductor Private Data>
};
FresnelConductor构造器只是简单地存放给定的折射率η和吸收系数k:
<FresnelConductor Private Data>
Spectrum eta, k;
<FresnelConductor Public Methods>
FresnelConductor(const Spectrum &e, const Spectrum &kk)
: eta(e), k(kk) {
}
FresnelConductor的求值例程很简单,只是调用前面讲过的FrCond()函数而已。注意这里取cosi的绝对值,因为FrCond()要求法向量和入射方向ωi在同一侧:
<BxDF Method Definitions> +=
Spectrum FresnelConductor::Evaluate(float cosi) const {
return FrCond(fabsf(cosi), eta, k);
}
Fresnel Dielectrics
<BxDF Declarations> +=
class COREDLL FresnelDielectric : public Fresnel {
public:
<FresnelDielectric Public Methods>
private:
<FresnelDielectric Private Data>
};
FresnelDielectric构造器只是存放表面两侧的折射率:
<FresnelDielectric Private Data> =
float eta_i, eta_t;
<FresnelDielectric Public Methods> =
FresnelDielectric(float ei, float et) {
eta_i = ei;
eta_t = et;
}
<BxDF Method Definitions> +=
Spectrum FresnelDielectric::Evaluate(float cosi) const {
<Compute Fresnel reflectance for dielectric>
}
对绝缘体Fresnel公式求值要比导体复杂些。首先,需要确定入射方向是在介质的内部还是外部,这样才能正确地使用两个折射率。其次,需要使用Snell定律来计算透射方向和法向量的夹角。最后,用cos
2
θ + sin
2
θ = 1计算出该角度的余弦值来。
<Compute Fresnel reflectance for dielectric> =
cosi = Clamp(cosi, -1.f, 1.f);
<Compute indices of refraction for dielectric>
<Compute sint using Snell's law>
if (sint > 1.) {
<Handle total internal reflection>
}
else {
float cost = sqrtf(max(0.f, 1.f - sint*sint));
return FrDiel(fabsf(cosi), cost, ei, et);
}
我们用入射角的余弦符号可以判定光线在介质的哪一侧。如果余弦值在0到1之间,则光线在介质外面,否则就在外面。变量ei和et分别被设置为入射介质和透射介质的折射率。
<Compute indices of refraction for dielectric>
bool entering = cosi > 0.;
float ei = eta_i, et = eta_t;
if (!entering)
swap(ei, et);
一旦设置好折射率,就可以直截了当地用Snell定律计算sinθt:
<Compute sint using Snell's law>
float sint = ei/et * sqrtf(max(0.f, l.f - cosi*cosi));
当光线从高折射率的介质进入低折射率的介质时,如果入射角大于临界角(critical angle),就产生全内反射(total internal reflection),即所有光线都被反射回来。如果sinθ
t
大于1,就是这种情况。在这种情况下,就没有必要使用Fresnel方程了。
<Handle total internal reflection> =
return 1.;
一个特殊的Fresnel接口
FresnelNoOp实现了Fresnel接口,它只是对所有的入射方向返回100%的反射率。
<BxDF Declarations> +=
class COREDLL FresnelNoOp : public Fresnel {
public:
Spectrum Evaluate(float) const { return Spectrum(l.); }
};
9.2.2 镜面反射
现在我们可以实现SpecularReflection类了,该类使用Fresnel类描述了物理效果上颇令人信服的镜面反射。首先,我们要推导出描述镜面反射的BRDF。由于Fresnel方程给出了光线反射率,Fr(ωi),我们需要一个满足下面关系的BRDF:
L
o
(ωo) = fr(ωo, ωi) Li(ωi) = Fr(ωi)L
i
(ωi)
其中ωi是ωo关于法向量的反射向量。(回忆一下对于镜面反射θi = θo, 故Fr(ωo) = Fr(ωi))。
该BRDF可以用狄拉克delta分布构造出来。第7.1节介绍过delta分布有个很重要的性质:
? f(x) δ(x - x
0
) dx = f(x0)
跟标准函数相比较,delta分布函数需要特殊的处理。特别地,对delta分布的积分必须显式地按照delta分布的本身的意义来求值,否则它们的值就无法计算。例如,考虑上面的式子,如果用梯形面积规则或其它数值积分技术来求值,那么根据delta分布的定义,任何求值点xi都不会有非零的δ(xi)值。所以,我们必须允许delta分布自己来确定求值点。
直观地讲,我们希望BRDF在除了理想反射方向之外的任何地方都是0,所以我们就要求助于delta分布。最容易想到的方法是简单地使用delta函数,将入射方向限制在反射角度ωr。这样就产生如下的BRDF:
虽然看上去很不错,但是代入第5章的散射方程(5.6),就会发现问题:
这是不正确的,因为它包含了一个额外的因子cosθi。但是我们可以简单地除去这个因子,就得到正确的理想镜面反射的BRDF:
其中R(ωo, n)是ωo关于法向量n的镜面反射向量。
<BxDF Declarations> +=
class COREDLL SpecularReflection : public BxDF {
public:
<SpecularReflection Public Methods>
private:
<SpecularReflection Private Data>
};
SpecularReflection使用了一个Fresnel对象来描述绝缘体或导体的Fresnel特性,还使用了一个Spectrum对象,用于对反射的颜色进行比例变换。
<SpecularReflection Public Methods> =
SpecularReflection(const Spectrum &r, Fresnel *f)
: BxDF(BxDFType(BSDF_REFLECTION | BSDF_SPECULAR)),
R(r), fresnel(f) {
}
<SpecularReflection Private Data> =
Spectrum R;
Fresnel *fresnel;
其它的实现还是很直截了当的。SpecularReflection::f()不产生散射,因为对于任意的一对方向,delta函数不产生散射。
<SpecularReflection Public Methods> +=
Spectrum f(const Vector &, const Vector &) const {
return Spectrum(0.);
}
现在我们实现Sample_f()函数,它根据delta分布来选择适当的方向。它将输出变量wi设置为wo关于法向量的反射方向。*pdf值被设置为1,因为在此情况下没有Monte Carlo采样。
<BxDF Method Definitions> +=
Spectrum SpecularReflection::Sample_f(const Vector &wo,
Vector *wi, float u1, float u2, float *pdf) const {
<Compute perfect specular reflection direction>
*pdf = l.f;
return fresnel->Evaluate(CosTheta(wo)) * R / fabsf(CosTheta(*wi));
}
(如图)我们要计算的方向是关于ωo法向量的反射方向。因为所有计算是在着色坐标系下进行的,其中法向量是(0,0,1),我们只需绕法向量将ωi旋转180度。在第2章我们讲到了绕z轴的选择矩阵,如果旋转角度为π,则矩阵如下:
所以,就有:
<Compute perfect specular reflection direction> =
*wi = Vector(-wo.x, -wo.y, wo.z);
9.2.3 镜面透射
现在我们推导镜面透射的BTDF公式。Snell定律是这个推导过程的基础。它不仅给出了透射光线的方向,而且也可以显示出光线进入不同介质时辐射亮度所发生的变化。
设两个介质有不同的折射率ηi和ηo,考虑一下入射方向的在两种介质边界上的辐射亮度(如图):
我们记入射能量被送入到出射方向的能量比率为τ,那么根据Fresnel方程,有τ = 1 - Fr(ωi)。透射微分辐射通量为:
dΦo = τ dΦi
根据辐射亮度的定义公式5.1,我们有:
Lo cosθo dA dωo = τ ( Li cosθi dA dωi)
将立体角展开为球面角,则有:
Lo cosθo dA sinθo dθo dΦo = τ ( Li cosθi dA sinθi dθi dΦi) (9.4)
我们对Snell定律进行关于θ的微分,有下列关系式:
ηo cosθo dθo = ηi cosθi dθi
通过整理,得到:
cosθo dθo / cosθi dθi = ηi / ηo
将该式和Snell定律代入(9.4),得到:
Lo ηi2 dΦo = τ Li ηo2 dΦi
因为Φi = Φo + π,所以dΦi = dΦo。这样就得到最后的关系式:
Lo = τ Li (ηo2 / ηi2)
因此,镜面透射的BTDF是:
bzm_square
std::cout << "hello world" ;
PBRT阅读: 第九章 反射模型 第9.1-9.2节
2012-05-17 23:28:31| 分类: Ray|字号 订阅
第九章 反射模型
本章定义了一组类来描述光源是如何在表面上散射的。回忆一下,在第5.4节中我们介绍了双向反射分布函数(BRDF),来描述光在某个表面上的反射;又介绍了BTDF来描述在表面上的透射,而BSDF把这两种效果综合起来。描述真实表面上的散射的最好方法常常是多个BRDF和BTDF的综合;在第10章里,我们将介绍一个BSDF对象,将多个BRDF和BTDF组合在一起来表示光在表面上的总体上的散射效果。本章不考虑在表面上反射性质和透射性质发生变化的情况,在第11章中,描述纹理的类会处理这个问题。
表面反射模型有几个来源:
1. 实验数据:许多真实世界中的表面的反射分布性质是在实验室里测量出来的。这些数据可以直接放在表格里使用,或者用来计算一组基函数的系数。
2.现象学模型:建立描述真实世界里表面的性质的方程,并且这些方程可以很有效地模拟出这些性质。像BSDF这类模型就非常好用,因为它们可以用直观的参数来改变模型的行为(例如“粗糙度”)。计算机图形学中的许多反射函数都属于这个范畴。
3.仿真:有时我们知道一个表面的组成部分的底层信息。例如,我们可能知道一种颜料是由悬浮在介质中的大小大致相同的彩色颗粒组成的,或者知道一种编织物是有两种类型的线构成,每种线都有已知的反射特性。在这样的情况下,我们可以通过模拟微观几何的散射来得到反射数据。这个模拟过程可以是在渲染中进行的,也可以是一种预处理过程,并由此拟合出一组基函数以备后用。
4. 物理(波动)光学: 有些反射模型是由一种非常精细的光模型推衍出来的,其中光被视为一种波,并通过解麦克斯韦方程来研究光在表面上的散射过程。这些模型通常需要耗时的计算,却并不一定会得到比几何光学模型更精确的结果。
5,几何光学:跟仿真方法相似,如果知道了表面的底层的散射和几何性质,就可以从这些性质中推导出有解析表达式的反射模型。几何光学模型使得光对表面的交互作用更容易处理,因为诸如偏振现象的复杂效应被忽略了。
基本术语
为了比较不同反射模型的视觉效果,我们介绍一些关于表面反射的基本术语。
表面反射可以分为四大类:漫反射(diffuse),光泽镜面反射(glossy specular),理想镜面反射(perfect specular),逆反射(retro-reflective)。大多数表面的反射是这四类反射的混合。漫发射将光线均等地向所有方向上散射。虽然很难见到理想的漫反射表面,但接近漫反射的例子有无光泽的黑板,无光涂料等等。象塑料、高光泽涂料这样的光泽镜面表面向一组特定的方向进行散射--它们可以映照出其它物体的模糊的映像。理想镜面表面只向一个单一的方向散射。镜子和玻璃是理想镜面表面的例子。最后,象天鹅绒或在地球上看到的月亮这样的逆反射表面主要向入射光方向对光线进行散射。(如图: a -漫反射, b -光泽镜面反射, c -理想镜面反射, d -逆反射)。
给定了一个反射类型,反射分布函数可以是各向同性(isotropic)的或者是各向异性(anisotropic)的。大多数物体是各向同性的:如果选定表面上的一个点绕着该点的法向量旋转它,所反射的光的总量不变。相反地,对各向异性材料做这样的旋转,就会反射出不同量的光。各向异性表面的例子有被擦亮的金属、唱机唱片和CD盘片。
几何设置
在pbrt中的反射计算是在一个反射坐标系中进行的,在该坐标系中,被着色的点上的两个切向量和法向量分别跟x,y,z轴对齐,所有BRDF和BTDF函数的传入向量和返回向量都是在这个坐标系下定义的。理解好这个坐标系对后面理解BRDF和BTDF的实现非常重要。
着色坐标系还给出了一个用球面坐标(θ,Φ)来表示方向的标架;角度θ是给定方向跟z轴的夹角,Φ是给定方向在xy平面上的投影跟x轴的夹角。(如图)
给定了在该坐标系中的一个方向,就可以很容易地计算出它与法向量夹角的余弦,等等:
cos θ = (n . ω) = ( (0, 0, 0) . ω) = ω
z
下面就是求这个余弦值的工具函数:
<BSDF Inline Function> =
inline float CosTheta (const Vector &w) { return w.z;}
利用 sin
2
θ + cos
2
θ = 1, 可得到相应的正弦值:
<BSDF Inline Functions> +=
inline float SinTheta(const Vector &w) {
return sqrtf(max(0.f, 1.f - w.z * w.z));
}
下面是求sin
2
θ的工具函数:
<BSDF Inline Functions> +=
inline float SinTheta2(const Vector &w) {
return 1.f - CosTheta(w) * CosTheta(w);
}
类似地我们可以用着色坐标系统来简化关于Φ的正弦和余弦计算。在着色点所在的平面上方向ω有坐标(x, y),分别为 r cosΦ 和 r sinΦ。而半径r即是sin θ, 因此:
cos Φ = x / r = x / sin θ
sin Φ = y / r = y / sin θ
<BSDF Inline Functions> +=
inline float CosPhi(const Vector &w) {
return w.x / SinTheta(w);
}
inline float SinPhi(const Vector &w) {
return w.y / SinTheta(w);
}
我们要遵守的一个约定是,入射光ωi和向外的观察方向ωo 在被变换到表面上的局部坐标系以后,都是被正规化的,且方向都朝外。根据约定,表面法向量总是指向物体之外的,这就很容易确定光线是进入还离开透光物体:如果入射光方向ωi跟n在同一个半球,则光线是在进入,否则就是在离开。
因此,需要记住的这一点:表面法向量可能跟ωi或者ωo(或者两者都有)不在物体的同一侧。跟许多其它渲染器不同的是,pbrt并不为了保持ωo和法向量在表面同一侧而翻转法向量。所以,在实现BRDF和BTDF时就不能做这样的假定。
还有,应该注意用于着色的局部坐标系可能并不等同于Shape::Intersect()所返回的坐标系,它们在求交过程和着色过程之间可以做些改动以达到象凸凹纹理映射(bump mapping)这类的效果。
在阅读本章时还要注意的一点是,BRDF和BTDF的实现不应该关心ωi和ωo是否位于同一个半球。例如,虽然一个反射BRDF应该探测是否入射光方向在表面的上面并且出射方向在表面的下面,从而可判定在这种情况下不存在反射,但是我们希望反射函数能够利用反射模型中的相应公式来计算光的反射量,而忽略它们是否在同一个半球这个细节。更高层的pbrt代码会保证反射例程或透射的散射例程能在恰当的时机被调用。
9.1 基本接口
首先我们定义BRDF和BTDF函数的接口。BRDF和BTDF共享一个基类,BxDF,它定义了要实现的基本接口。两类函数共用一个接口,共享一个基类,可以减少重复性代码,也可以是系统的某些部分可以使用BxDF而不必区分BRDF和BTDF。
<BxDF Declarations> =
class COREDLL BxDF {
public:
<BxDF Interface>
<BxDF Public Data>
};
将要在第10.1节介绍BSDF类存放一组BxDF对象来共同地描述表面上一个点的散射情况。虽然我们通过使用一个共同接口而隐藏了关于反射材质和透射材质的实现细节,但是第16章所介绍的一些光传输算法需要区分这两种类型。因此,所有的BxDF要有一个BxDF::type成员还存放BxDFType标志。对于每个BxDF而言,标志必须在BSDF_REFLECTION或BSDF_TRANSMISSION集合中取一个值,而且必须是漫反射、光泽反射和镜面反射标志之一。注意这里没有逆反射标志,这里的分类将逆反射视为光泽反射。
<BSDF Declarations> =
enum BxDFType {
BSDF_REFLECTION = 1 << 0,
BSDF_TRASNMISSION = 1 << 1,
BSDF_DIFFUSE = 1 << 2,
BSDF_GLOSSY = 1 << 3,
BSDF_SPECULAR = 1 << 4,
BSDF_ALL_TYPES = BSDF_DIFFUSE | BSDF_GLOSSY | BSDF_SPECULAR,
BSDF_ALL_REFLECTION = BSDF_REFLECTION | BSDF_ALL_TYPES,
BSDF_ALL_TRASNMISSION = BSDF_TRASNMISSION | BSDF_ALL_TYPES,
BSDF_ALL = BSDF_ALL_REFLECTION | BSDF_ALL_TRASNMISSION
};
<BxDF Public Data> =
const BxDFType type;
<BxDF Interface> =
BxDF(BxDFType t) : type (t) { }
MatchesFlags()工具函数用来确定BxDF是否具备用户提供的标志:
<BxDF Interface> +=
bool MatchesFlag(BxDFType flags) const {
return (type & flags) == type;
}
BxDF的关键函数是BxDF::f()。它返回给定的一对方向所对应的分布函数值。这个接口隐性地假定不同波长上的光是不相干(decoupled)的,即一个波长上的能量不会以不同的波长被反射出去。这样一来,就不会支持萤光材料了。有了这个假定,反射函数的结果就可以直接用一个Spectrum来表示:
<BxDF Interface> +=
virtual Spectrum f(const Vector &wo, const Vector &wi) const = 0;
并不是所有的BxDF都用该函数求值。例如,象镜子、玻璃或水这样的全镜面反射物体只在单一出射方向上对单一方向的入射光进行散射。描述这样的BxDF的最好方法是delta分布函数:除了出射方向之外,其它方向的值都为0.
在pbrt中,这些BxDF需要特殊的处理,所以我们提供BxDF::Sample_f()函数。我们用这个函数处理由delta分布来描述的散射,也用它来对BxDF所散射出的多个光线的方向进行随机采样(见第15章)。给定了出射方向之后,BxDF::Sample_f()计算入射光的方向ωi,并根据这一对方向来计算BxDF值。对于delta分布而言,需要利用这种方式来生成适当的ωi方向,因为调用者无法生成相应的ωi方向。利用delta分布的BxDF并不需要参数u1,u2和pdf,在我们介绍非镜面反射函数时再介绍它们。
<BxDF Interface> +=
virtual Spectrum Sample_f(const Vector &wo, Vector *wi, float u1, float u2, float *pdf) const;
9.1.1 反射率
有时我们需要将4D的BRDF或BTDF(定义为一个方向对的函数)的聚合行为缩减为单一方向上的2D函数,甚至缩减为一个常量来描述总体上的散射行为。
半球-方向反射率函数(hemispherical-directional reflectance)是一个2D函数,它给出了对半球上的恒定照明下的在某个给定方向上的总反射量,也可以被等价地认为是来自某个给定方向上的光在半球上的总反射量。定义如下:
BxDF::rho()函数用来计算上面的ρ
hd
。某些BxDF可以用解析表达式来计算这个值,虽然大多数的BxDF用Monte Carlo积分来计算其近似值。对于这些BxDF而言,参数nSamples和samples用来控制Monte Carlo算法的行为(见15.5.5节)。
<BxDF Interface> +=
virtual Spectrum rho(const Vector &wo, int nSamples = 16, float *samples = NULL) const;
表面的半球-半球反射率(hemispherical- hemispherical reflectance)是一个光谱值常量,记作ρ
hh
,它给出了在所有方向有相同的入射光的情况下一个表面上被反射的入射光的比率。
当用户没有提供方向ωo时,我们重载函数BxDF::rho()来计算ρ
hh
,其余的参数在计算Monte Carlo近似值时会用到。
<BxDF Interface> +=
virtual Spectrum rho(int nSamples = 16, float *samples = NULL) const;
9.1.2 BRDF->BTDF适配器
为了将已经定义的BRDF当作BTDF使用,特别是当BRDF的现象学模型对描述透射现象也很不错的情况下,我们定义一个适配器类就很便利地做到这一点。BRDFToBTDF类用一个BRDF的指针作为构造器的参数,并利用它来实现一个BTDF。特别地,这需要将函数调用传给BRDF,并翻转ωi方向使之处于另一个半球。
<BxDF Declarations> +=
class COREDLL BRDFToBTDF : public BxDF {
public:
<BRDFToBTDF Public Methods>
private:
BxDF *brdf;
};
适配器的构造器很简单,只需将BxDF::type中的反射标志和透射标志对调一下。
<BRDFToBTDF Public Methods> =
BRDFToBTDF(BxDF *b)
: BxDF(BxDFType(b->type ^ ( BSDF_REFLECTION | BSDF_TRANSMISSION))) {
brdf = b;
}
适配器需要将入射方向转换为另一半球上相应的方向。这在着色坐标系下很容易做到,只需将向量的z值取负值即可。
<BRDFToBTDF Public Methods> +=
static Vector otherHemisphere(const Vector &w) {
return Vector(w.x, w.y, -w.z);
}
在调用BRDF的BxDF::rho(), BxDF::f(), 和BxDF::Sample_f()函数时,我们调用BRDFToBTDF:: otherHemisphere()来将光线翻转到另一个半球上:
BRDFToBTDF Public Methods> +=
Specturm rho(const Vector &w, int nSamples, float *samples) const {
return brdf->rho(otherHemisphere(w), nSamples, samples);
}
Specturm rho(int nSamples, float *samples) const {
return brdf->rho(nSamples, samples);
}
<BxDF Method Definitions> =
Specturm BRDFToBTDF::f(const Vector &wo, const Vector &wi) const {
return brdf->f(wo, otherHemisphere(wi));
}
<BxDF Method Definitions> +=
Specturm BRDFToBTDF::Sample_f (const Vector &wo, const Vector &wi,
float u1, float u2, float *pdf) const {
Spectrum f = brdf->Sample_f(wo, wi, u1, u2, pdf);
*wi = otherHermisphere(*wi);
return f;
}
9.2 镜面反射和透射
对于光在完全光滑的表面上的行为,我们可以比较容易地用物理模型和几何模型进行解析上的分析。这些表面上的入射光表现出理想镜面反射和透射;对于给定的方向ωi,所有的光线被散射到单一的出射方向ωo。对于镜面反射,这个方向跟法向量的夹角等于入射光跟法向量的夹角:
θ
i
= θ
o
对于透射,出射方向由斯涅耳(Snell)定律决定,该定律给出了透射方向和法向量n的夹角θt跟入射方向和法向量n的夹角θi的关系式。Snell定律基于入射光所在的介质的折射率(index of refraction)和光线所要进入的介质的折射率。折射率表述了光线在特定介质中传播跟在真空中传播相比较下的减慢程度。我们用希腊字母η(eta)来表示折射率,Snell定律可表示如下:
η
i
sin θ
i
= η
t
sin θ
t
在一般情况下,折射率随光的波长的不同而有所变化。这样,在两种不同介质的交界处,入射光通常被散射到多个方向上,即色散现象(dispersion)。当入射的白光穿过一个棱镜被分离出不同的光谱成分时,就可以看到这种现象。在计算机图形学的实际应用中,我们忽略了这种波长的相关性,因为这种现象对视觉上的精确性并不重要,对它的忽略可以极大地简化光线传输的计算。
下图是用一个理想镜面反射的BRDF(图a)和一个镜面透射的BTDF(图b)对Killeroo模型进行渲染的效果。注意通过透明物体的折射光线将背后的场景变形了。
9.2.1菲涅耳(Fresnel) 反射率
除了计算反射方向和透射方向之外,还需要计算入射光被反射或透射的比率。在简单的光线追踪器中,这些比率常常被称为“反射率”或“透射率”,它们在整个表面上被视为常量。然而,对于物理上的反射或折射而言,这些量是跟视角相关的,不可以用一个常量的比例因子来表达。菲涅耳(Fresnel)方程描述了光在表面上被反射的量;它实际上是麦克斯韦方程组在光滑表面上的解。
有两种Fresnel方程,一种用于绝缘体(如玻璃),一种用于导体(如金属)。对于这两种情况,Fresnel方程又根据入射光的偏振情况分两种形式。在渲染中恰当地处理偏振现象是很复杂的,所以在pbrt中我们通常假定光是没有偏振的,也就是说,光波的朝向是随机的。有了这个假定,Fresnel反射率就等于平行偏振项的平方和垂直偏振项的平方的平均值。
为了计算绝缘体的Fresnel反射率,我们需要知道两种介质的折射率。表9.1列出了几个常见的绝缘材质的折射率。
下面是对绝缘体Fresnel反射率公式的近似表达式:
r
||
= (ηt cosθi - ηi cosθt) / (ηt cosθi + ηi cosθt)
r
T
= (ηi cosθi - ηt cosθt) / (ηi cosθi + ηt cosθt)
其中r
||
是平行偏振光的Fresnel反射率,r
T
是垂直偏振光的Fresnel反射率,ηi和ηt分别是入射光所在的介质的折射率和透射介质的折射率,ωo和ωt分别是入射方向和透射方向,其中ωt是用Snell定律计算出来的。
对于无偏振的光,Fresnel反射率为:
Fr = (r
||
2
+ r
T
2
) / 2
函数FrDiel()计算绝缘材质和圆偏振光所使用的Fresnel反射公式。cosθi和cosθt分别用参数cosi和cost传入:
<BxDF Utility Functions> =
COREDLL Spectrum FrDiel(float cosi, float cost, const Spectrum &etai, const Spectrum &etat)
{
Specturm Rparl = ((etat * cosi) - (etai * cost)) /
((etat * cosi) + (etai * cost));
Specturm RparP = ((etai * cosi) - (etat * cost)) /
((etai * cosi) + (etat * cost));
return (Rparl * Rparl + RparP * RparP) / 2.f;
}
根据能量守恒定律,绝缘体所透射的能量为 1 - Fr。
与绝缘体不同的是,导体对光不产生透射,但一些入射光会被材料吸收并转化为热量。我们用Fresnel公式可以得出有多少光被反射了。该公式要用到导体的折射率η和吸收系数k。表9.2列出了一些导体的η和k值:
导体Fresnel反射率公式的一个常用的近似公式为:
<BxDF Utility Functions> +=
COREDLL Spectrum FrCond (float cosi, const Spectrum &eta, const Spectrum &k) {
Spectrum tmp = (eta * eta + k * k) * cosi * cosi;
Spectrum Rparl2 = (tmp - (2.f * eta * cosi ) + 1) /
(tmp + (2.f * eta * cosi) + 1);
Spectrum tmp_f = eta * eta + k *k;
Spectrum Rperp2 = (tmp_f - (2.f * eta * cosi) + cosi * cosi) /
(tmp_f + (2.f *eta * cosi) + cosi * cosi);
return (Rparl2 + Rperp2) / 2.f;
}
许多导体的η和k值是未知的,因为已做的关于导体的测量要比绝缘体的测量要少得多。但是有两个近似方法可以计算出令人满意的值。这两种方法都假设物体的反射率是沿法向量的入射方向上测量出的,即:观察者和光源都是沿法向量直视/直照到表面上的。将η或k值中的一个固定,代入到导体Fresnel公式,就可以确定另一个值,也就可以计算出沿法向量的入射方向的反射率(Cook and Torrance 1982)。
这个方法的第一个版本就是在假定吸收系数为0的情况下计算η的相似值。如果k=0且cosθi = 1(入射方向为法向量),则前面提到的两个公式就简化为:
r
||
2
= r
T
2
= (η2 - 2 η + 1) /(η2 +2 η + 1) = ((η -1)/( η+1))2
因为在入射方向为法向量的情况下Fresnel反射率是已知的,就可以解出η:
η = (1 + F
r
1/2
) / (1-F
r
1/2
)
<BxDF Utility Functions> +=
COREDLL Specturm FresnelApproxEta(const Spectrum &Fr) {
Spectrum reflectance = Fr.Clamp(0.f, .999f);
return (Spectrum(1.) + reflectance.Sqrt()) / (Spectrum(1.) - reflectance.Sqrt());
}
我们可以用相同的方法计算吸收系数k,这里假定η=1。Fresnel就简化为:
r
||
2
= r
T
2
= k
2
/ (k
2
+4)
我们就可以很容易地解出k:
k = 2 ( Fr / (1 - Fr))
1/2
<BxDF Utility Functions> +=
COREDLL Specturm FresnelApproxK(const Spectrum &Fr) {
Spectrum reflectance = Fr.Clamp(0.f, .999f);
return 2.f * (reflectance / (Spectrum(1.) - refletance)).Sqert();
}
为了方便,我们定义一个抽象类Fresnel来提供计算Fresnel反射系数的接口。FresnelConductor和FresnelDislectric实现类有助于简化BRDF的实现。
<BxDF Declarations> +=
class COREDLL Fresnel {
public:
<Fresnel Interface>
};
Fresnel接口所提供的唯一函数是Fresnel::Evaluate()。给定了入射方向和法向量的夹角余弦cosi,该函数返回表面反射光的量值:
<Fresnel Interface> =
virtual Spectrum Evaluate (float cosi) const = 0;
Fresnel导体
<BxDF Declarations> +=
class COREDLL FresnelConductor : public Fresnel {
public:
<FresnelConductor Public Methods>
private:
<FresnelConductor Private Data>
};
FresnelConductor构造器只是简单地存放给定的折射率η和吸收系数k:
<FresnelConductor Private Data>
Spectrum eta, k;
<FresnelConductor Public Methods>
FresnelConductor(const Spectrum &e, const Spectrum &kk)
: eta(e), k(kk) {
}
FresnelConductor的求值例程很简单,只是调用前面讲过的FrCond()函数而已。注意这里取cosi的绝对值,因为FrCond()要求法向量和入射方向ωi在同一侧:
<BxDF Method Definitions> +=
Spectrum FresnelConductor::Evaluate(float cosi) const {
return FrCond(fabsf(cosi), eta, k);
}
Fresnel Dielectrics
<BxDF Declarations> +=
class COREDLL FresnelDielectric : public Fresnel {
public:
<FresnelDielectric Public Methods>
private:
<FresnelDielectric Private Data>
};
FresnelDielectric构造器只是存放表面两侧的折射率:
<FresnelDielectric Private Data> =
float eta_i, eta_t;
<FresnelDielectric Public Methods> =
FresnelDielectric(float ei, float et) {
eta_i = ei;
eta_t = et;
}
<BxDF Method Definitions> +=
Spectrum FresnelDielectric::Evaluate(float cosi) const {
<Compute Fresnel reflectance for dielectric>
}
对绝缘体Fresnel公式求值要比导体复杂些。首先,需要确定入射方向是在介质的内部还是外部,这样才能正确地使用两个折射率。其次,需要使用Snell定律来计算透射方向和法向量的夹角。最后,用cos
2
θ + sin
2
θ = 1计算出该角度的余弦值来。
<Compute Fresnel reflectance for dielectric> =
cosi = Clamp(cosi, -1.f, 1.f);
<Compute indices of refraction for dielectric>
<Compute sint using Snell's law>
if (sint > 1.) {
<Handle total internal reflection>
}
else {
float cost = sqrtf(max(0.f, 1.f - sint*sint));
return FrDiel(fabsf(cosi), cost, ei, et);
}
我们用入射角的余弦符号可以判定光线在介质的哪一侧。如果余弦值在0到1之间,则光线在介质外面,否则就在外面。变量ei和et分别被设置为入射介质和透射介质的折射率。
<Compute indices of refraction for dielectric>
bool entering = cosi > 0.;
float ei = eta_i, et = eta_t;
if (!entering)
swap(ei, et);
一旦设置好折射率,就可以直截了当地用Snell定律计算sinθt:
<Compute sint using Snell's law>
float sint = ei/et * sqrtf(max(0.f, l.f - cosi*cosi));
当光线从高折射率的介质进入低折射率的介质时,如果入射角大于临界角(critical angle),就产生全内反射(total internal reflection),即所有光线都被反射回来。如果sinθ
t
大于1,就是这种情况。在这种情况下,就没有必要使用Fresnel方程了。
<Handle total internal reflection> =
return 1.;
一个特殊的Fresnel接口
FresnelNoOp实现了Fresnel接口,它只是对所有的入射方向返回100%的反射率。
<BxDF Declarations> +=
class COREDLL FresnelNoOp : public Fresnel {
public:
Spectrum Evaluate(float) const { return Spectrum(l.); }
};
9.2.2 镜面反射
现在我们可以实现SpecularReflection类了,该类使用Fresnel类描述了物理效果上颇令人信服的镜面反射。首先,我们要推导出描述镜面反射的BRDF。由于Fresnel方程给出了光线反射率,Fr(ωi),我们需要一个满足下面关系的BRDF:
L
o
(ωo) = fr(ωo, ωi) Li(ωi) = Fr(ωi)L
i
(ωi)
其中ωi是ωo关于法向量的反射向量。(回忆一下对于镜面反射θi = θo, 故Fr(ωo) = Fr(ωi))。
该BRDF可以用狄拉克delta分布构造出来。第7.1节介绍过delta分布有个很重要的性质:
? f(x) δ(x - x
0
) dx = f(x0)
跟标准函数相比较,delta分布函数需要特殊的处理。特别地,对delta分布的积分必须显式地按照delta分布的本身的意义来求值,否则它们的值就无法计算。例如,考虑上面的式子,如果用梯形面积规则或其它数值积分技术来求值,那么根据delta分布的定义,任何求值点xi都不会有非零的δ(xi)值。所以,我们必须允许delta分布自己来确定求值点。
直观地讲,我们希望BRDF在除了理想反射方向之外的任何地方都是0,所以我们就要求助于delta分布。最容易想到的方法是简单地使用delta函数,将入射方向限制在反射角度ωr。这样就产生如下的BRDF:
虽然看上去很不错,但是代入第5章的散射方程(5.6),就会发现问题:
这是不正确的,因为它包含了一个额外的因子cosθi。但是我们可以简单地除去这个因子,就得到正确的理想镜面反射的BRDF:
其中R(ωo, n)是ωo关于法向量n的镜面反射向量。
<BxDF Declarations> +=
class COREDLL SpecularReflection : public BxDF {
public:
<SpecularReflection Public Methods>
private:
<SpecularReflection Private Data>
};
SpecularReflection使用了一个Fresnel对象来描述绝缘体或导体的Fresnel特性,还使用了一个Spectrum对象,用于对反射的颜色进行比例变换。
<SpecularReflection Public Methods> =
SpecularReflection(const Spectrum &r, Fresnel *f)
: BxDF(BxDFType(BSDF_REFLECTION | BSDF_SPECULAR)),
R(r), fresnel(f) {
}
<SpecularReflection Private Data> =
Spectrum R;
Fresnel *fresnel;
其它的实现还是很直截了当的。SpecularReflection::f()不产生散射,因为对于任意的一对方向,delta函数不产生散射。
<SpecularReflection Public Methods> +=
Spectrum f(const Vector &, const Vector &) const {
return Spectrum(0.);
}
现在我们实现Sample_f()函数,它根据delta分布来选择适当的方向。它将输出变量wi设置为wo关于法向量的反射方向。*pdf值被设置为1,因为在此情况下没有Monte Carlo采样。
<BxDF Method Definitions> +=
Spectrum SpecularReflection::Sample_f(const Vector &wo,
Vector *wi, float u1, float u2, float *pdf) const {
<Compute perfect specular reflection direction>
*pdf = l.f;
return fresnel->Evaluate(CosTheta(wo)) * R / fabsf(CosTheta(*wi));
}
(如图)我们要计算的方向是关于ωo法向量的反射方向。因为所有计算是在着色坐标系下进行的,其中法向量是(0,0,1),我们只需绕法向量将ωi旋转180度。在第2章我们讲到了绕z轴的选择矩阵,如果旋转角度为π,则矩阵如下:
所以,就有:
<Compute perfect specular reflection direction> =
*wi = Vector(-wo.x, -wo.y, wo.z);
9.2.3 镜面透射
现在我们推导镜面透射的BTDF公式。Snell定律是这个推导过程的基础。它不仅给出了透射光线的方向,而且也可以显示出光线进入不同介质时辐射亮度所发生的变化。
设两个介质有不同的折射率ηi和ηo,考虑一下入射方向的在两种介质边界上的辐射亮度(如图):
bzm_square
std::cout << "hello world" ;
PBRT阅读: 第九章 反射模型 第9.1-9.2节
2012-05-17 23:28:31| 分类: Ray|字号 订阅
第九章 反射模型
本章定义了一组类来描述光源是如何在表面上散射的。回忆一下,在第5.4节中我们介绍了双向反射分布函数(BRDF),来描述光在某个表面上的反射;又介绍了BTDF来描述在表面上的透射,而BSDF把这两种效果综合起来。描述真实表面上的散射的最好方法常常是多个BRDF和BTDF的综合;在第10章里,我们将介绍一个BSDF对象,将多个BRDF和BTDF组合在一起来表示光在表面上的总体上的散射效果。本章不考虑在表面上反射性质和透射性质发生变化的情况,在第11章中,描述纹理的类会处理这个问题。
表面反射模型有几个来源:
1. 实验数据:许多真实世界中的表面的反射分布性质是在实验室里测量出来的。这些数据可以直接放在表格里使用,或者用来计算一组基函数的系数。
2.现象学模型:建立描述真实世界里表面的性质的方程,并且这些方程可以很有效地模拟出这些性质。像BSDF这类模型就非常好用,因为它们可以用直观的参数来改变模型的行为(例如“粗糙度”)。计算机图形学中的许多反射函数都属于这个范畴。
3.仿真:有时我们知道一个表面的组成部分的底层信息。例如,我们可能知道一种颜料是由悬浮在介质中的大小大致相同的彩色颗粒组成的,或者知道一种编织物是有两种类型的线构成,每种线都有已知的反射特性。在这样的情况下,我们可以通过模拟微观几何的散射来得到反射数据。这个模拟过程可以是在渲染中进行的,也可以是一种预处理过程,并由此拟合出一组基函数以备后用。
4. 物理(波动)光学: 有些反射模型是由一种非常精细的光模型推衍出来的,其中光被视为一种波,并通过解麦克斯韦方程来研究光在表面上的散射过程。这些模型通常需要耗时的计算,却并不一定会得到比几何光学模型更精确的结果。
5,几何光学:跟仿真方法相似,如果知道了表面的底层的散射和几何性质,就可以从这些性质中推导出有解析表达式的反射模型。几何光学模型使得光对表面的交互作用更容易处理,因为诸如偏振现象的复杂效应被忽略了。
基本术语
为了比较不同反射模型的视觉效果,我们介绍一些关于表面反射的基本术语。
表面反射可以分为四大类:漫反射(diffuse),光泽镜面反射(glossy specular),理想镜面反射(perfect specular),逆反射(retro-reflective)。大多数表面的反射是这四类反射的混合。漫发射将光线均等地向所有方向上散射。虽然很难见到理想的漫反射表面,但接近漫反射的例子有无光泽的黑板,无光涂料等等。象塑料、高光泽涂料这样的光泽镜面表面向一组特定的方向进行散射--它们可以映照出其它物体的模糊的映像。理想镜面表面只向一个单一的方向散射。镜子和玻璃是理想镜面表面的例子。最后,象天鹅绒或在地球上看到的月亮这样的逆反射表面主要向入射光方向对光线进行散射。(如图: a -漫反射, b -光泽镜面反射, c -理想镜面反射, d -逆反射)。
给定了一个反射类型,反射分布函数可以是各向同性(isotropic)的或者是各向异性(anisotropic)的。大多数物体是各向同性的:如果选定表面上的一个点绕着该点的法向量旋转它,所反射的光的总量不变。相反地,对各向异性材料做这样的旋转,就会反射出不同量的光。各向异性表面的例子有被擦亮的金属、唱机唱片和CD盘片。
几何设置
在pbrt中的反射计算是在一个反射坐标系中进行的,在该坐标系中,被着色的点上的两个切向量和法向量分别跟x,y,z轴对齐,所有BRDF和BTDF函数的传入向量和返回向量都是在这个坐标系下定义的。理解好这个坐标系对后面理解BRDF和BTDF的实现非常重要。
着色坐标系还给出了一个用球面坐标(θ,Φ)来表示方向的标架;角度θ是给定方向跟z轴的夹角,Φ是给定方向在xy平面上的投影跟x轴的夹角。(如图)
给定了在该坐标系中的一个方向,就可以很容易地计算出它与法向量夹角的余弦,等等:
cos θ = (n . ω) = ( (0, 0, 0) . ω) = ω
z
下面就是求这个余弦值的工具函数:
<BSDF Inline Function> =
inline float CosTheta (const Vector &w) { return w.z;}
利用 sin
2
θ + cos
2
θ = 1, 可得到相应的正弦值:
<BSDF Inline Functions> +=
inline float SinTheta(const Vector &w) {
return sqrtf(max(0.f, 1.f - w.z * w.z));
}
下面是求sin
2
θ的工具函数:
<BSDF Inline Functions> +=
inline float SinTheta2(const Vector &w) {
return 1.f - CosTheta(w) * CosTheta(w);
}
类似地我们可以用着色坐标系统来简化关于Φ的正弦和余弦计算。在着色点所在的平面上方向ω有坐标(x, y),分别为 r cosΦ 和 r sinΦ。而半径r即是sin θ, 因此:
cos Φ = x / r = x / sin θ
sin Φ = y / r = y / sin θ
<BSDF Inline Functions> +=
inline float CosPhi(const Vector &w) {
return w.x / SinTheta(w);
}
inline float SinPhi(const Vector &w) {
return w.y / SinTheta(w);
}
我们要遵守的一个约定是,入射光ωi和向外的观察方向ωo 在被变换到表面上的局部坐标系以后,都是被正规化的,且方向都朝外。根据约定,表面法向量总是指向物体之外的,这就很容易确定光线是进入还离开透光物体:如果入射光方向ωi跟n在同一个半球,则光线是在进入,否则就是在离开。
因此,需要记住的这一点:表面法向量可能跟ωi或者ωo(或者两者都有)不在物体的同一侧。跟许多其它渲染器不同的是,pbrt并不为了保持ωo和法向量在表面同一侧而翻转法向量。所以,在实现BRDF和BTDF时就不能做这样的假定。
还有,应该注意用于着色的局部坐标系可能并不等同于Shape::Intersect()所返回的坐标系,它们在求交过程和着色过程之间可以做些改动以达到象凸凹纹理映射(bump mapping)这类的效果。
在阅读本章时还要注意的一点是,BRDF和BTDF的实现不应该关心ωi和ωo是否位于同一个半球。例如,虽然一个反射BRDF应该探测是否入射光方向在表面的上面并且出射方向在表面的下面,从而可判定在这种情况下不存在反射,但是我们希望反射函数能够利用反射模型中的相应公式来计算光的反射量,而忽略它们是否在同一个半球这个细节。更高层的pbrt代码会保证反射例程或透射的散射例程能在恰当的时机被调用。
9.1 基本接口
首先我们定义BRDF和BTDF函数的接口。BRDF和BTDF共享一个基类,BxDF,它定义了要实现的基本接口。两类函数共用一个接口,共享一个基类,可以减少重复性代码,也可以是系统的某些部分可以使用BxDF而不必区分BRDF和BTDF。
<BxDF Declarations> =
class COREDLL BxDF {
public:
<BxDF Interface>
<BxDF Public Data>
};
将要在第10.1节介绍BSDF类存放一组BxDF对象来共同地描述表面上一个点的散射情况。虽然我们通过使用一个共同接口而隐藏了关于反射材质和透射材质的实现细节,但是第16章所介绍的一些光传输算法需要区分这两种类型。因此,所有的BxDF要有一个BxDF::type成员还存放BxDFType标志。对于每个BxDF而言,标志必须在BSDF_REFLECTION或BSDF_TRANSMISSION集合中取一个值,而且必须是漫反射、光泽反射和镜面反射标志之一。注意这里没有逆反射标志,这里的分类将逆反射视为光泽反射。
<BSDF Declarations> =
enum BxDFType {
BSDF_REFLECTION = 1 << 0,
BSDF_TRASNMISSION = 1 << 1,
BSDF_DIFFUSE = 1 << 2,
BSDF_GLOSSY = 1 << 3,
BSDF_SPECULAR = 1 << 4,
BSDF_ALL_TYPES = BSDF_DIFFUSE | BSDF_GLOSSY | BSDF_SPECULAR,
BSDF_ALL_REFLECTION = BSDF_REFLECTION | BSDF_ALL_TYPES,
BSDF_ALL_TRASNMISSION = BSDF_TRASNMISSION | BSDF_ALL_TYPES,
BSDF_ALL = BSDF_ALL_REFLECTION | BSDF_ALL_TRASNMISSION
};
<BxDF Public Data> =
const BxDFType type;
<BxDF Interface> =
BxDF(BxDFType t) : type (t) { }
MatchesFlags()工具函数用来确定BxDF是否具备用户提供的标志:
<BxDF Interface> +=
bool MatchesFlag(BxDFType flags) const {
return (type & flags) == type;
}
BxDF的关键函数是BxDF::f()。它返回给定的一对方向所对应的分布函数值。这个接口隐性地假定不同波长上的光是不相干(decoupled)的,即一个波长上的能量不会以不同的波长被反射出去。这样一来,就不会支持萤光材料了。有了这个假定,反射函数的结果就可以直接用一个Spectrum来表示:
<BxDF Interface> +=
virtual Spectrum f(const Vector &wo, const Vector &wi) const = 0;
并不是所有的BxDF都用该函数求值。例如,象镜子、玻璃或水这样的全镜面反射物体只在单一出射方向上对单一方向的入射光进行散射。描述这样的BxDF的最好方法是delta分布函数:除了出射方向之外,其它方向的值都为0.
在pbrt中,这些BxDF需要特殊的处理,所以我们提供BxDF::Sample_f()函数。我们用这个函数处理由delta分布来描述的散射,也用它来对BxDF所散射出的多个光线的方向进行随机采样(见第15章)。给定了出射方向之后,BxDF::Sample_f()计算入射光的方向ωi,并根据这一对方向来计算BxDF值。对于delta分布而言,需要利用这种方式来生成适当的ωi方向,因为调用者无法生成相应的ωi方向。利用delta分布的BxDF并不需要参数u1,u2和pdf,在我们介绍非镜面反射函数时再介绍它们。
<BxDF Interface> +=
virtual Spectrum Sample_f(const Vector &wo, Vector *wi, float u1, float u2, float *pdf) const;
9.1.1 反射率
有时我们需要将4D的BRDF或BTDF(定义为一个方向对的函数)的聚合行为缩减为单一方向上的2D函数,甚至缩减为一个常量来描述总体上的散射行为。
半球-方向反射率函数(hemispherical-directional reflectance)是一个2D函数,它给出了对半球上的恒定照明下的在某个给定方向上的总反射量,也可以被等价地认为是来自某个给定方向上的光在半球上的总反射量。定义如下:
BxDF::rho()函数用来计算上面的ρ
hd
。某些BxDF可以用解析表达式来计算这个值,虽然大多数的BxDF用Monte Carlo积分来计算其近似值。对于这些BxDF而言,参数nSamples和samples用来控制Monte Carlo算法的行为(见15.5.5节)。
<BxDF Interface> +=
virtual Spectrum rho(const Vector &wo, int nSamples = 16, float *samples = NULL) const;
表面的半球-半球反射率(hemispherical- hemispherical reflectance)是一个光谱值常量,记作ρ
hh
,它给出了在所有方向有相同的入射光的情况下一个表面上被反射的入射光的比率。
当用户没有提供方向ωo时,我们重载函数BxDF::rho()来计算ρ
hh
,其余的参数在计算Monte Carlo近似值时会用到。
<BxDF Interface> +=
virtual Spectrum rho(int nSamples = 16, float *samples = NULL) const;
9.1.2 BRDF->BTDF适配器
为了将已经定义的BRDF当作BTDF使用,特别是当BRDF的现象学模型对描述透射现象也很不错的情况下,我们定义一个适配器类就很便利地做到这一点。BRDFToBTDF类用一个BRDF的指针作为构造器的参数,并利用它来实现一个BTDF。特别地,这需要将函数调用传给BRDF,并翻转ωi方向使之处于另一个半球。
<BxDF Declarations> +=
class COREDLL BRDFToBTDF : public BxDF {
public:
<BRDFToBTDF Public Methods>
private:
BxDF *brdf;
};
适配器的构造器很简单,只需将BxDF::type中的反射标志和透射标志对调一下。
<BRDFToBTDF Public Methods> =
BRDFToBTDF(BxDF *b)
: BxDF(BxDFType(b->type ^ ( BSDF_REFLECTION | BSDF_TRANSMISSION))) {
brdf = b;
}
适配器需要将入射方向转换为另一半球上相应的方向。这在着色坐标系下很容易做到,只需将向量的z值取负值即可。
<BRDFToBTDF Public Methods> +=
static Vector otherHemisphere(const Vector &w) {
return Vector(w.x, w.y, -w.z);
}
在调用BRDF的BxDF::rho(), BxDF::f(), 和BxDF::Sample_f()函数时,我们调用BRDFToBTDF:: otherHemisphere()来将光线翻转到另一个半球上:
BRDFToBTDF Public Methods> +=
Specturm rho(const Vector &w, int nSamples, float *samples) const {
return brdf->rho(otherHemisphere(w), nSamples, samples);
}
Specturm rho(int nSamples, float *samples) const {
return brdf->rho(nSamples, samples);
}
<BxDF Method Definitions> =
Specturm BRDFToBTDF::f(const Vector &wo, const Vector &wi) const {
return brdf->f(wo, otherHemisphere(wi));
}
<BxDF Method Definitions> +=
Specturm BRDFToBTDF::Sample_f (const Vector &wo, const Vector &wi,
float u1, float u2, float *pdf) const {
Spectrum f = brdf->Sample_f(wo, wi, u1, u2, pdf);
*wi = otherHermisphere(*wi);
return f;
}
9.2 镜面反射和透射
对于光在完全光滑的表面上的行为,我们可以比较容易地用物理模型和几何模型进行解析上的分析。这些表面上的入射光表现出理想镜面反射和透射;对于给定的方向ωi,所有的光线被散射到单一的出射方向ωo。对于镜面反射,这个方向跟法向量的夹角等于入射光跟法向量的夹角:
θ
i
= θ
o
对于透射,出射方向由斯涅耳(Snell)定律决定,该定律给出了透射方向和法向量n的夹角θt跟入射方向和法向量n的夹角θi的关系式。Snell定律基于入射光所在的介质的折射率(index of refraction)和光线所要进入的介质的折射率。折射率表述了光线在特定介质中传播跟在真空中传播相比较下的减慢程度。我们用希腊字母η(eta)来表示折射率,Snell定律可表示如下:
η
i
sin θ
i
= η
t
sin θ
t
在一般情况下,折射率随光的波长的不同而有所变化。这样,在两种不同介质的交界处,入射光通常被散射到多个方向上,即色散现象(dispersion)。当入射的白光穿过一个棱镜被分离出不同的光谱成分时,就可以看到这种现象。在计算机图形学的实际应用中,我们忽略了这种波长的相关性,因为这种现象对视觉上的精确性并不重要,对它的忽略可以极大地简化光线传输的计算。
下图是用一个理想镜面反射的BRDF(图a)和一个镜面透射的BTDF(图b)对Killeroo模型进行渲染的效果。注意通过透明物体的折射光线将背后的场景变形了。
9.2.1菲涅耳(Fresnel) 反射率
除了计算反射方向和透射方向之外,还需要计算入射光被反射或透射的比率。在简单的光线追踪器中,这些比率常常被称为“反射率”或“透射率”,它们在整个表面上被视为常量。然而,对于物理上的反射或折射而言,这些量是跟视角相关的,不可以用一个常量的比例因子来表达。菲涅耳(Fresnel)方程描述了光在表面上被反射的量;它实际上是麦克斯韦方程组在光滑表面上的解。
有两种Fresnel方程,一种用于绝缘体(如玻璃),一种用于导体(如金属)。对于这两种情况,Fresnel方程又根据入射光的偏振情况分两种形式。在渲染中恰当地处理偏振现象是很复杂的,所以在pbrt中我们通常假定光是没有偏振的,也就是说,光波的朝向是随机的。有了这个假定,Fresnel反射率就等于平行偏振项的平方和垂直偏振项的平方的平均值。
为了计算绝缘体的Fresnel反射率,我们需要知道两种介质的折射率。表9.1列出了几个常见的绝缘材质的折射率。
下面是对绝缘体Fresnel反射率公式的近似表达式:
r
||
= (ηt cosθi - ηi cosθt) / (ηt cosθi + ηi cosθt)
r
T
= (ηi cosθi - ηt cosθt) / (ηi cosθi + ηt cosθt)
其中r
||
是平行偏振光的Fresnel反射率,r
T
是垂直偏振光的Fresnel反射率,ηi和ηt分别是入射光所在的介质的折射率和透射介质的折射率,ωo和ωt分别是入射方向和透射方向,其中ωt是用Snell定律计算出来的。
对于无偏振的光,Fresnel反射率为:
Fr = (r
||
2
+ r
T
2
) / 2
函数FrDiel()计算绝缘材质和圆偏振光所使用的Fresnel反射公式。cosθi和cosθt分别用参数cosi和cost传入:
<BxDF Utility Functions> =
COREDLL Spectrum FrDiel(float cosi, float cost, const Spectrum &etai, const Spectrum &etat)
{
Specturm Rparl = ((etat * cosi) - (etai * cost)) /
((etat * cosi) + (etai * cost));
Specturm RparP = ((etai * cosi) - (etat * cost)) /
((etai * cosi) + (etat * cost));
return (Rparl * Rparl + RparP * RparP) / 2.f;
}
根据能量守恒定律,绝缘体所透射的能量为 1 - Fr。
与绝缘体不同的是,导体对光不产生透射,但一些入射光会被材料吸收并转化为热量。我们用Fresnel公式可以得出有多少光被反射了。该公式要用到导体的折射率η和吸收系数k。表9.2列出了一些导体的η和k值:
导体Fresnel反射率公式的一个常用的近似公式为:
<BxDF Utility Functions> +=
COREDLL Spectrum FrCond (float cosi, const Spectrum &eta, const Spectrum &k) {
Spectrum tmp = (eta * eta + k * k) * cosi * cosi;
Spectrum Rparl2 = (tmp - (2.f * eta * cosi ) + 1) /
(tmp + (2.f * eta * cosi) + 1);
Spectrum tmp_f = eta * eta + k *k;
Spectrum Rperp2 = (tmp_f - (2.f * eta * cosi) + cosi * cosi) /
(tmp_f + (2.f *eta * cosi) + cosi * cosi);
return (Rparl2 + Rperp2) / 2.f;
}
许多导体的η和k值是未知的,因为已做的关于导体的测量要比绝缘体的测量要少得多。但是有两个近似方法可以计算出令人满意的值。这两种方法都假设物体的反射率是沿法向量的入射方向上测量出的,即:观察者和光源都是沿法向量直视/直照到表面上的。将η或k值中的一个固定,代入到导体Fresnel公式,就可以确定另一个值,也就可以计算出沿法向量的入射方向的反射率(Cook and Torrance 1982)。
这个方法的第一个版本就是在假定吸收系数为0的情况下计算η的相似值。如果k=0且cosθi = 1(入射方向为法向量),则前面提到的两个公式就简化为:
r
||
2
= r
T
2
= (η2 - 2 η + 1) /(η2 +2 η + 1) = ((η -1)/( η+1))2
因为在入射方向为法向量的情况下Fresnel反射率是已知的,就可以解出η:
η = (1 + F
r
1/2
) / (1-F
r
1/2
)
<BxDF Utility Functions> +=
COREDLL Specturm FresnelApproxEta(const Spectrum &Fr) {
Spectrum reflectance = Fr.Clamp(0.f, .999f);
return (Spectrum(1.) + reflectance.Sqrt()) / (Spectrum(1.) - reflectance.Sqrt());
}
我们可以用相同的方法计算吸收系数k,这里假定η=1。Fresnel就简化为:
r
||
2
= r
T
2
= k
2
/ (k
2
+4)
我们就可以很容易地解出k:
k = 2 ( Fr / (1 - Fr))
1/2
<BxDF Utility Functions> +=
COREDLL Specturm FresnelApproxK(const Spectrum &Fr) {
Spectrum reflectance = Fr.Clamp(0.f, .999f);
return 2.f * (reflectance / (Spectrum(1.) - refletance)).Sqert();
}
为了方便,我们定义一个抽象类Fresnel来提供计算Fresnel反射系数的接口。FresnelConductor和FresnelDislectric实现类有助于简化BRDF的实现。
<BxDF Declarations> +=
class COREDLL Fresnel {
public:
<Fresnel Interface>
};
Fresnel接口所提供的唯一函数是Fresnel::Evaluate()。给定了入射方向和法向量的夹角余弦cosi,该函数返回表面反射光的量值:
<Fresnel Interface> =
virtual Spectrum Evaluate (float cosi) const = 0;
Fresnel导体
<BxDF Declarations> +=
class COREDLL FresnelConductor : public Fresnel {
public:
<FresnelConductor Public Methods>
private:
<FresnelConductor Private Data>
};
FresnelConductor构造器只是简单地存放给定的折射率η和吸收系数k:
<FresnelConductor Private Data>
Spectrum eta, k;
<FresnelConductor Public Methods>
FresnelConductor(const Spectrum &e, const Spectrum &kk)
: eta(e), k(kk) {
}
FresnelConductor的求值例程很简单,只是调用前面讲过的FrCond()函数而已。注意这里取cosi的绝对值,因为FrCond()要求法向量和入射方向ωi在同一侧:
<BxDF Method Definitions> +=
Spectrum FresnelConductor::Evaluate(float cosi) const {
return FrCond(fabsf(cosi), eta, k);
}
Fresnel Dielectrics
<BxDF Declarations> +=
class COREDLL FresnelDielectric : public Fresnel {
public:
<FresnelDielectric Public Methods>
private:
<FresnelDielectric Private Data>
};
FresnelDielectric构造器只是存放表面两侧的折射率:
<FresnelDielectric Private Data> =
float eta_i, eta_t;
<FresnelDielectric Public Methods> =
FresnelDielectric(float ei, float et) {
eta_i = ei;
eta_t = et;
}
<BxDF Method Definitions> +=
Spectrum FresnelDielectric::Evaluate(float cosi) const {
<Compute Fresnel reflectance for dielectric>
}
对绝缘体Fresnel公式求值要比导体复杂些。首先,需要确定入射方向是在介质的内部还是外部,这样才能正确地使用两个折射率。其次,需要使用Snell定律来计算透射方向和法向量的夹角。最后,用cos
2
θ + sin
2
θ = 1计算出该角度的余弦值来。
<Compute Fresnel reflectance for dielectric> =
cosi = Clamp(cosi, -1.f, 1.f);
<Compute indices of refraction for dielectric>
<Compute sint using Snell's law>
if (sint > 1.) {
<Handle total internal reflection>
}
else {
float cost = sqrtf(max(0.f, 1.f - sint*sint));
return FrDiel(fabsf(cosi), cost, ei, et);
}
我们用入射角的余弦符号可以判定光线在介质的哪一侧。如果余弦值在0到1之间,则光线在介质外面,否则就在外面。变量ei和et分别被设置为入射介质和透射介质的折射率。
<Compute indices of refraction for dielectric>
bool entering = cosi > 0.;
float ei = eta_i, et = eta_t;
if (!entering)
swap(ei, et);
一旦设置好折射率,就可以直截了当地用Snell定律计算sinθt:
<Compute sint using Snell's law>
float sint = ei/et * sqrtf(max(0.f, l.f - cosi*cosi));
当光线从高折射率的介质进入低折射率的介质时,如果入射角大于临界角(critical angle),就产生全内反射(total internal reflection),即所有光线都被反射回来。如果sinθ
t
大于1,就是这种情况。在这种情况下,就没有必要使用Fresnel方程了。
<Handle total internal reflection> =
return 1.;
一个特殊的Fresnel接口
FresnelNoOp实现了Fresnel接口,它只是对所有的入射方向返回100%的反射率。
<BxDF Declarations> +=
class COREDLL FresnelNoOp : public Fresnel {
public:
Spectrum Evaluate(float) const { return Spectrum(l.); }
};
9.2.2 镜面反射
现在我们可以实现SpecularReflection类了,该类使用Fresnel类描述了物理效果上颇令人信服的镜面反射。首先,我们要推导出描述镜面反射的BRDF。由于Fresnel方程给出了光线反射率,Fr(ωi),我们需要一个满足下面关系的BRDF:
L
o
(ωo) = fr(ωo, ωi) Li(ωi) = Fr(ωi)L
i
(ωi)
其中ωi是ωo关于法向量的反射向量。(回忆一下对于镜面反射θi = θo, 故Fr(ωo) = Fr(ωi))。
该BRDF可以用狄拉克delta分布构造出来。第7.1节介绍过delta分布有个很重要的性质:
? f(x) δ(x - x
0
) dx = f(x0)
跟标准函数相比较,delta分布函数需要特殊的处理。特别地,对delta分布的积分必须显式地按照delta分布的本身的意义来求值,否则它们的值就无法计算。例如,考虑上面的式子,如果用梯形面积规则或其它数值积分技术来求值,那么根据delta分布的定义,任何求值点xi都不会有非零的δ(xi)值。所以,我们必须允许delta分布自己来确定求值点。
直观地讲,我们希望BRDF在除了理想反射方向之外的任何地方都是0,所以我们就要求助于delta分布。最容易想到的方法是简单地使用delta函数,将入射方向限制在反射角度ωr。这样就产生如下的BRDF:
虽然看上去很不错,但是代入第5章的散射方程(5.6),就会发现问题:
这是不正确的,因为它包含了一个额外的因子cosθi。但是我们可以简单地除去这个因子,就得到正确的理想镜面反射的BRDF:
其中R(ωo, n)是ωo关于法向量n的镜面反射向量。
<BxDF Declarations> +=
class COREDLL SpecularReflection : public BxDF {
public:
<SpecularReflection Public Methods>
private:
<SpecularReflection Private Data>
};
SpecularReflection使用了一个Fresnel对象来描述绝缘体或导体的Fresnel特性,还使用了一个Spectrum对象,用于对反射的颜色进行比例变换。
<SpecularReflection Public Methods> =
SpecularReflection(const Spectrum &r, Fresnel *f)
: BxDF(BxDFType(BSDF_REFLECTION | BSDF_SPECULAR)),
R(r), fresnel(f) {
}
<SpecularReflection Private Data> =
Spectrum R;
Fresnel *fresnel;
其它的实现还是很直截了当的。SpecularReflection::f()不产生散射,因为对于任意的一对方向,delta函数不产生散射。
<SpecularReflection Public Methods> +=
Spectrum f(const Vector &, const Vector &) const {
return Spectrum(0.);
}
现在我们实现Sample_f()函数,它根据delta分布来选择适当的方向。它将输出变量wi设置为wo关于法向量的反射方向。*pdf值被设置为1,因为在此情况下没有Monte Carlo采样。
<BxDF Method Definitions> +=
Spectrum SpecularReflection::Sample_f(const Vector &wo,
Vector *wi, float u1, float u2, float *pdf) const {
<Compute perfect specular reflection direction>
*pdf = l.f;
return fresnel->Evaluate(CosTheta(wo)) * R / fabsf(CosTheta(*wi));
}
(如图)我们要计算的方向是关于ωo法向量的反射方向。因为所有计算是在着色坐标系下进行的,其中法向量是(0,0,1),我们只需绕法向量将ωi旋转180度。在第2章我们讲到了绕z轴的选择矩阵,如果旋转角度为π,则矩阵如下:
所以,就有:
<Compute perfect specular reflection direction> =
*wi = Vector(-wo.x, -wo.y, wo.z);
9.2.3 镜面透射
现在我们推导镜面透射的BTDF公式。Snell定律是这个推导过程的基础。它不仅给出了透射光线的方向,而且也可以显示出光线进入不同介质时辐射亮度所发生的变化。
设两个介质有不同的折射率ηi和ηo,考虑一下入射方向的在两种介质边界上的辐射亮度(如图):