http://69.163.227.177/forum.php?mod=viewthread&tid=5626
第10章 材质
前一章所介绍的底层BRDF和BTDF只解决了关于光在表面上如何散射的部分问题。虽然它们能够描述在表面上某个点上的散射情况,但是渲染器需要确定在该点上使用哪一种BRDF和BTDF,并设置怎样的参数。在本章里,我们将描述一个过程式着色机制,用它来确定在表面上的某个点上使用哪一种BRDF和BTDF。
其基本思想是将场景中每个体素绑定上一个Material接口类的实例,该接口的一个函数以一个点为参数,返回一个BSDF对象。BSDF类有一组BxDF来共同描述该点的散射情况。Material又使用Texture类的实例来确定特定点上的材质特性。例如,一个ImageTexture对象可以被用来调节表面上的漫反射的颜色。这跟许多渲染系统的着色机制有所不同,因为通常的方式是将表面着色器和光照积分器结合成一个模块,并且令着色器返回该点的反射光的颜色。然而,pbrt将这两个部分分离开,令Material返回一个BSDF,就可以更好地处理许多类型的光线传输算法。
10.1BSDF
BSDF类代表了一组BRDF和BTDF。将它们以这种方式组织在一起,目的是允许系统的其它部分能够直接跟复合型的BSDF打交道,而不是必须考虑其中每一个组成部分。同等重要的是,BSDF类隐藏了着色法向量(shading normal)的细节。着色法向量(来自三角形网格的顶点法向量或者凹凸贴图)可以极大地提高场景渲染的视觉效果的丰富性,但是由于它们是一种特制的结构,就很难融入基于物理的渲染器。这里的BSDF实现将处理它们所引起的问题。
<BSDF Declarations> +=
class COREDLL BSDF {
public:
<BSDF Public Methods>
<BSDF Public Data>
private:
<BSDF Private Methods>
<BSDF Private Data>
};
BSDF构造器的参数包括一个表示着色微分几何信息的DifferentialGeometry对象,原表面法向量ngeom,还有一个选项:表面所包围的介质的折射率。它将这些值存放到成员变量中,并构造出一个正交坐标系,其中一个轴为着色法向量;这有助于将向量变换到BxDF坐标系中(9.1节)。在本节中,我们用ns表示着色法向量,用ng代表几何法向量。
<BSDF Method Definitions> =
BSDF::BSDF(const DifferentialGeometry &dg, const Normal &ngeom,float e)
: dgShading(dg), eta(e) {
ng= ngeom;
nn= dgShading.nn;
sn= Normalize(dgShading.dpdu);
tn= Cross(nn, sn);
nBxDFs= 0;
}
<BSDF Public Data> =
const DifferentialGeometry dgShading;
const float eta;
<BSDF Private Data>
Normal nn, ng;
Vector sn, tn;
BSDF的实现存放固定数目的BxDF。虽然很容易动态地分配空间来容纳更多的BxDF,但目前的限制(个数为8)已经足够应付大多数的实际应用。
<BSDF Inline Method Definitions> =
inline voidBSDF::Add(BxDF *b) {
Assert(nBxDFs< MAX_BxDFS);
bxdfs[nBxDFs++] = b;
}
<BSDF Private Data> +=
intnBxDFs;
#define MAX_BxDFS 8
BxDF*bxdfs[MAX_BxDFS];
为了方便系统其它部分对BSDF的使用,该类定义了两个不同的方法,第一个返回BSDF所存放的BxDF数目,第二个只返回匹配给定的一组BxDFType标志的BxDF数目。虽然我们可以用一个函数来代替这两个函数(用缺省的标志BSDF_ALL代替第一个),但由于对无参数的BSDF::NumComponents()调用十分频繁,所以我们用两个函数来避免了对各个BxDF标志逐个检查。
<BSDF Public Methods> =
intNumComponents() const { return nBxDFs; }
intNumComponents(BxDFType flags) const;
HasShadingGeometry()函数用来确定BSDF是否有跟其几何法向量不同的着色法向量。例如,PhotonIntegrator就需要调用这个函数。
<BSDF Public Methods> +=
boolHasShadingGeometry() const {
return(nn.x != ng.x || nn.y != ng.y || nn.z != ng.z);
}
BSDF还有一个函数来进行局部坐标系的向量变换。回忆一下:在这个坐标系中,表面法向量是沿z轴(0,0,1)的,主切向量是(1,0,0),第二切向量是(0,1,0)。将方向变换到“着色空间”(shading space)可以简化许多BxDF的实现。有了世界空间的三个正交向量s,t,n,将向量变换到局部反射空间的矩阵M是:
我们做一下验证:例如,将M乘以法向量n, 则 M n = (s.n, t.n, n.n)。由于s,t,n是正交向量,则M n 的x,y分量为0。并且由于n是正规化了的向量,则n.n=1。所以, M n = (0, 0, 1),跟我们所期望的结果一致。
在这里,我们不需要计算M的逆阵来对向量进行变换(见2.8.3),因为M是正交矩阵(行与行,列与列都是相互正交的),其逆阵就是其转置阵。
<BSDF Public Methods> +=
VectorWorldToLocal(const Vector &v) const {
returnVector(Dot(v, sn), Dot(v, tn), Dot(v, nn));
}
下列方法将向量从局部空间变换回世界空间,在做点积运算时,实际上是用了M的转置阵(即逆阵):
<BSDF Public Methods> +=
VectorLocalToWorld(const Vector &v) const {
returnVector(sn.x * v.x + tn.x * v.y + nn.x * v.z,
sn.y* v.x + tn.y * v.y + nn.y * v.z,
sn.z* v.x + tn.z * v.y + nn.z * v.z);
}
在实际应用中,着色法向量可能会引起不同的人为缺陷(图10.2)。
图(a)显示了一个光泄漏(light leak):由几何法向量可以看出光源在表面的背面,如果表面不透光,那么光源就不会有贡献值。然而,如果我们调用散射方程(5.6)计算一下,就会错误地将方向ωi上的光包括进来。这个情况表明ns不可以直接代替ng做着色运算。
图(b)显示了类似的麻烦:着色法向量表明了不应该有光反射到观察者那里去,因为它跟照明方向并不在同一个半球,而几何法向量则表明了它们在同一个半球之内。直接使用ns就会在表面上显示很难看的黑色斑点。
幸运地是,有一个优美的方法可以解决诸如此类的问题。在对BSDF求值时,我们用几何法向量来确定是否计算反射或透射:如果ωi和ωo在以ng为中心的同一个半球,则计算BRDF,否则计算BTDF。在计算散射方程时,计算法向量跟入射方向的点积时,仍需使用着色向量而不是几何向量。
现在应该明白了为什么pbrt不管ωi和ωo是否在同一个半球都要对BxDF进行求值计算。这样做可以避免光泄漏,因为在如图10.2(a)的情况下,只对BTDF求值,这样对于纯反射的表面而言,就不会产生反射。类似地,对于如图10.2(b)的情况,由于我们只对BRTF求值,就不会出现黑色斑点。
有了以上的约定,就很容易得出对给定的一对方向进行BSDF求值的函数。首先,要把世界空间的方向向量变换到局部BSDF空间,然后再确定是用BRDF还是用BTDF。然后对其中相关的BxDF进行循环求得它们贡献值的总和。
<BSDF Method Definitions> +=
SpectrumBSDF::f(const Vector &woW, const Vector &wiW,
BxDFTypeflags) const {
Vectorwi = WorldToLocal(wiW), wo = WorldToLocal(woW);
if(Dot(wiW, ng) * Dot(woW, ng) > 0) // ignore BTDFs
flags= BxDFType(flags & -BSDFTRANSMISSION);
else// ignore BRDFs
flags= BxDFType(flags & -BSDF_REFLECTION);
Spectrumf = 0.;
for(int i = 0 ; i < nBxDFs; ++i)
if(bxdfs->MatchesFlags(flags))
f+= bxdfs->f(wo, wi);
returnf;
}
pbrt也提供对BSDF的BxDF求反射率总和的BSDF函数,这些函数只是对这些BxDF循环,调用BxDF::rho()函数。这里就不一一列出了。
<BSDF Public Methods> +=
Spectrumrho(BxDFType flags = BSDF_ALL) const;
Spectrumrho(const Vector &wo, BxDFType flags = BSDF_ALL) const;
10.1.1BSDF内存管理
对于每条跟场景中几何体相交的相机光线,SurfaceIntegrator在计算交点处的辐射亮度过程中,要创建一个或多个BSDF对象(负责处理多重交互反射的积分器要沿着光线创建多个BSDF)。在每个BSDF中,又包含多个BxDF,并通过在交点处的Material对象返回给积分器。一个简单的实现可能会用new 和delete函数动态地分配为BSDF和其中的BxDF来分配内存。
不幸地是,这样的方法是很没有效率的 -- 会有太多的时间花费到动态内存管理例程上。所以,我们要用A.2.4节的MemoryArena类,建立专用的内存分配机制。MemoryArena申请一大块内存,并通过Memory::Alloc()来返回该内存块中的内存段。它不支持对单个内存分配的释放,而是用MemoryArena::FreeAll()来同时释放所有的内存。这个方法对内存的分配和释放都是非常高效的。
BSDF类有一个静态的MemoryArena对象用于BSDF和BxDF的内存分配。它提供了内存分配和释放函数,这些函数只是调用MemoryArena相应的函数。每当计算好一条相机光线的贡献值,Scene::Render()函数就会调用BSDF::FreeAll()来释放为该光线分配的所有BSDF内存。在这个时候,系统不应该有BSDF指针(因为相关的内存释放了,任何指针都失效)。如果为了去掉这个约束而修改pbrt,就必须使用其它的内存管理方法来管理BSDF内存。
<BSDF Public Methods> +=
staticvoid *Alloc(u_int sz) { return arena.Alloc(sz); }
staticvoid FreeAl1() { arena.FreeAl1(); }
<BSDF Private Data> +=
staticMemoryArena arena;
为了使用方便,我们用一个宏定义BSDF_ALLOC,这样就隐藏了这个内存管理的某些零乱的细节。
如果我们用new操作符,就有诸如下列的代码:
BSDF*b = new BSDF;
BxDF*lam = new Lambertian(Spectrum(1.0));
我们用宏定义,就应该写成:
BSDF*b = BSDF_ALLOC(BSDF);
BxDF*lam = BSDF_ALLOC(Lambertian)(Spectrum(1.0));
这个宏调用了BSDF::Alloc(),为相应的对象申请适当大小的内存,然后用placementnew操作符在给定的内存位置上运行对象的构造器。
<BSDF Declarations> +=
#defineBSDF ALLOC(T) new (BSDF::Alloc(sizeof(T))) T
我们还要把BSDF析构器声明为私有的,以防对其不恰当的调用(例如,企图用delete删除一个BSDF)。如果有对析构器的调用,就会在编译时出错。企图调用delete来释放由MemoryArena所管理的内存会产生很难预测的错误,因为指向由MemoryArena所管理的内存的某个中间位置的指针被传给了系统的内存释放函数。为了屏蔽因私有析构器而产生的编译警告,我们将BSDF声明为某个不存在的类的friend。
<BSDF Private Methods> =
~BSDF(){ }
friendclass NoSuchClass;
10.2材质类的接口和实现
抽象类Material只定义了一个函数, Material::GetBSDF()。该函数负责确定表面上点的反射特性并返回相应的一个BSDF对象。
<Material Class Declarations> =
classCOREDLL Material : public ReferenceCounted {
public:
<MaterialInterface>
}
Material::GetBSDF()用两个微分几何信息类的对象作为参数,第一个dgGeom,是交点处的实际微分几何信息,第二个dgShading,是可能经过扰动过的着色几何信息(例如,来自三角形网格顶点向量)。材质类可以用GetBSDF()函数中的凹凸贴图对着色几何信息进行进一步的扰动;所返回的BSDF含有该点最终的着色几何信息和相关的BRDF和BTDF。
<Material Interface> =
virtualBSDF *GetBSDF(const Differential Geometry &dgGeom,
constDifferential Geometry &dgShading) const = 0;
由于积分器是通过Intersection类的某个对象来使用交点的,所以为了方便起见,我们在Intersection类中加上一个函数,用来返回交点处的BSDF。为了进行纹理反走样,该函数调用Differentialgeometry::ComputeDifferentials()来计算交点处表面面积在图像平面上的投影尺寸,再调用Primitive类的GetBSDF(),其中再调用Material类的GetBSDF()。
<IntersectionMethod Definitions> =
BSDF *Intersection::GetBSDF(constRayDifferential &ray) const {
dg.ComputeDifferentials(ray);
returnprimitive->GetBSDF(dg, WorldToObject);
}
10.2.1 无光材质
Matte材质是pbrt中最简单的一种材质,它描述了一个纯漫反射的表面。它的参数包括一个光谱值形式的漫反射的反射率Matte::kd,还有一个纯量形式的粗糙度值,Matte::sigma。如果Matte::sigma为零,则返回一个Lambert BRDF,否则,就使用OrenNayar模型的BRDF。跟其它的Material的实现一样,它还有一个选项,该选项定义了一个在表面上的偏置函数的纯量纹理(scalar texture)。如果该纹理为非NULL值,就用它所定义的函数来计算每个点上的着色向量。
<Matte ClassDeclarations> =
class Matte : public Material {
public:
<Matte PublicMethods>
private:
<MattePrivate Data>
};
<Matte PublicMethods> =
Matte(Reference<Texture<Spectrum>> kd,
Reference<Texture<float>> sig,
Reference<Texture<float>> bump) {
Kd = kd;
sigma = sig;
bumpMap = bump;
}
<Matte PrivateData> =
Reference<Texture<Spectrum>> Kd;
Reference<Texture<float>> sigma, bumpMap;
GetBSDF()函数将这些功能串在一起,确定凹凸贴图的效果,对纹理求值,申请BSDF内存并将之返回。
<Matte MethodDefinitions> =
BSDF *Matte::GetBSDF(constDifferential Geometry &dgGeom,
constDifferential Geometry &dgShading) const {
<AllocateBSDF, possibly doing bump mapping with bumpMap>
<Evaluatetextures for Matte material and allocate BRDF>
return bsdf;
}
如果我们传给Matte构造器一个凹凸贴图,Meterial::Bump()将被调用,计算相应点的着色向量。我们将在下一节定义这个函数。
<Allocate BSDF,possibly doing bump mapping with bumpMap> =
Differential Geometry dgs;
if (bumpMap)
Bump(bumpMap,dgGeom, dgShading, &dgs);
else
dgs = dgShading;
BSDF *bsdf =BSDF_ALLOC(BSDF)(dgs, dgGeom.nn);
下一步是对给出了漫反射系数和粗糙度的Textures进行求值;它们可能返回常量,或者从图像贴图中查值,或者做复杂的过程性着色计算来计算这些值。有了这些值,剩下的工作就是申请BSDF内存并返回结果。由于Textures可能返回负值或者超出所允许的范围,所以要将这些值修正到正常范围中再传给BRDF构造器。
<Evaluate texturesfor Matte material and allocate BRDF> =
Spectrum r =Kd->Evaluate(dgs).Clamp();
float sig =Clamp(sigma->Evaluate(dgs), 0.f, 90.f);
if (sig == 0.)
bsdf->Add(BSDF_ALLOC(Lambertian)(r));
else
bsdf->Add(BSDF_ALLOC(OrenNayar)(r,sig));
return bsdf;
10.2.2 塑料材质
塑料材质可以被模型化为一个混合型的漫反射和光泽散射函数,并有参数控制特定的颜色和镜面高光的大小。Plastic的参数包括两个反射率:kd, ks,分别控制漫反射和光泽镜面反射。另外还有一个粗糙度参数(范围是从0到1)用来控制高光的大小:粗糙度越大,则高光越大。
<Plastic ClassDeclarations> =
class Plastic : public Material{
public:
<PlasticPublic Methods>
private:
<PlasticPrivate Data>
};
<Plastic PublicMethods> =
Plastic(Reference<Texture<Spectrum>> kd,
Reference<Texture<Spectrum>> ks,
Reference<Texture<float>> rough,
Reference<Texture<float>> bump) {
Kd = kd;
Ks = ks;
roughness =rough;
bumpMap = bump;
}
<Plastic PrivateData> =
Reference<Texture<Spectrum>> Kd, Ks;
Reference<Texture<float>> roughness, bumpMap;
Plastic::GetBSDF()跟Mate::GetBSDF()的结构基本相同:对纹理求值,调用凹凸贴图函数,申请BxDF内存,初始化BSDF。
<Plastic MethodDefinitions> =
BSDF *Plastic::GetBSDF(constDifferentialGeometry &dgGeom,
constDifferentialGeometry &dgShading) const {
<AllocateBSDF, possibly doing bump mapping with bumpMap>
Spectrum kd =Kd->Evaluate(dgs).Clamp();
BxDF * diff =BSDF_ALLOC(Lambertian)(kd);
Fresnel *fresnel= BSDF_ALLOC(FresnelDielectric)(1.5f, 1.f);
Spectrum ks =Ks->Evaluate(dgs).Clamp();
float rough =roughness->Evaluate(dgs);
BxDF *spec =BSDF_ALLOC(Microfacet)(ks, fresnel,
BSDF_ALLOC(Blinn)(1.f/ rough));
bsdf->Add(diff);
bsdf->Add(spec);
return bsdf;
}
10.2.3 其它的材质
除了以上所介绍的基本材质,pbrt还定义了12个材质插件。我们对它们的实现不再一一进行介绍,因为它们不过是Matte和Plastic的各种变形。所有这些材质的构造器使用定义散射参数的纹理,GetBSDF()函数对这些纹理求值,创建适当的BxDF并返回。(参见附录C的文件格式,其中有关于这些材质参数的概括)。
这些材质包括:
· 半透明材质(Translucent):穿透表面的光泽透射,跟透过雾化玻璃看到的一样。
· 镜面(Mirror):简单的镜子,用理想镜面反射来模拟。
· 玻璃(Glass):反射和透射,用Fresnel项来做跟视角相关的权值。
· 光亮的金属(ShinyMetal):用理想镜面反射的金属表面
· 衬底(Substrate):一种分层模型,根据视角不同有不同的光泽镜面反射和漫反射(见前一章的FresnelBlend BRDF)。
· 粘土(Clay),毛粘( Felt), 底漆(Primer),蓝漆( BluePaint), 擦过的金属(BrushedMetal): 都是测量型BRDF,没有参数,要用Lafortune模型来近似。
· Uber:一种由前面的材质组合而成的“厨房槽盆”(kitchensink),这是一个高度参数化的材质,在进行其它文件格式的转换时特别有用。
10.3 凹凸贴图
前一节所介绍的所有材质都有一个选项,即一个float型的纹理贴图,它定义了在表面上每个点上的一个位移:每个点p对应着一个点p',并且p' = p + d(p) n(p),其中d(p)就是位移纹理(displacementtexture)所返回的关于点p的位移量,而n(p)是在p的表面法向量(如图)。
我们用这个纹理来计算着色法向量,使得表面上看上去是被位移函数移动了,但是实际上并没有改变其几何形状。这个过程被称为凹凸贴图(bump mapping)。对于位移相对小的函数而言,凹凸贴图的视觉效果还是非常令人满意的。这个思想和相关的着色法向量计算技术是由Jim Blinn(1978)开发出来的。
图10.7显示了一个应用到球上的纹理贴图,该贴图是由一个网格线图像贴图定义的。
Material::Bump()是一个Material类实现的一个工具函数。它负责计算某个着色点在给定的位移纹理下的凹凸贴图效果。为了保留将来对Material的实现的灵活性,我们将这个方法放在固定的材质求值管线之外,单独地提供了这个可被选择性调用的函数。
Material::Bump()的实现基于一个关于被移位后的表面上的偏导数∂p/∂u和∂p/∂v的近似公式,我们用它代替实际的表面偏导数来计算着色法向量(回忆一下:表面法向量n = ∂p/∂u x ∂p/∂v)。假定原表面的参数形式是p(u,v),凹凸偏置函数是一个纯量函数d(u,v),那么偏置后的表面为:
p'(u,v) = p(u,v) + d(u,v) n(u,v)
其中n(u,v)是(u,v)处的表面法向量。
这个函数的偏导数可以用链式法则来求得,例如关于u的偏导数为:
∂p'/∂u = ∂p(u,v)/ ∂u + (∂d(u,v)/∂u )n(u,v) + d(u,v) (∂n(u,v)/ ∂u)
我们已经知道∂p(u,v)/∂u的值(存放在DifferentialGeometry类里,该类还存放了表面法向量n(u,v)和(∂n(u,v)/ ∂u)。我们可以在需要时求得d(u,v),所以唯一的未知项是∂d(u,v)/ ∂u。
有两个可能的方法来计算∂d(u,v)/∂u和∂d(u,v)/∂v。第一个方法是为Texture接口提供一个计算纹理函数偏导数的函数。例如,对于直接使用表面(u,v)参数方程的图像纹理贴图,可以通过求u,v方向上的纹理差值来计算偏导数。然而,这个方法很难被扩展,无法用于复杂的过程性的纹理。所以,pbrt在Material::Bump()函数中直接用前向差分来计算这些值,而无需改变Texture接口。
回忆一下偏导数的定义:
前向差分法使用一个Δu有限值,并在两个位置上求d(u,v)的值。故有:
有趣的是,大多数的凹凸贴图的实现假定d(u,v)很小而忽略了最后一项(因为凹凸贴图模拟小扰动时最有用,所以这个假设还是有道理的)。还有许多渲染器根本不计算∂n/∂u,∂n/∂v,故不得不忽略这一项。对最后一项的忽略意味着位移函数值的大小不影响凹凸贴图后的偏导数,加上一个全局常量也不会影响最终结果,因为只有凹凸函数的差值能够产生影响。
由于pbrt有∂n/∂u和∂n/∂v值,所以以上三项都会计算,虽然最后一项不产生什么实际效果。
<Material MethodDefinitions> =
voidMaterial::Bump(Reference<Texture<float> > d,
constDifferentialGeometry &dgGeom,
constDifferentialGeometry &dgs,
DifferentialGeometry*dgBump) {
<Computeoffset positions and evaluate displacement texture>
<Computebump-mapped differential geometry>
<Orientshading normal to match geometric normal>
}
<Compute offsetpositions and evaluate displacement texture> =
DifferentialGeometry dgEval = dgs;
<Shift dgEval du in the udirection>
float uDisplace =d->Evaluate(dgEval);
<Shift dgEval dv in the vdirection>
float vDisplace =d->Evaluate(dgEval);
float displace =d->Evaluate(dgs);
还有一个问题是如何选择偏置值Δu和Δv进行有限差分计算。这些值要足够小,使得d(u,v)的微小的变化能够被捕捉到;但也要足够大,使得现有的浮点精度足以给出良好的结果。我们将这样选择Δu和Δv:在图像空间所产生的偏置量为半个像素宽,然后用它们修改DifferentialGeometry相应的成员变量。
<Shift dgEval du inthe u direction> =
float du = .5f *(fabsf(dgs.dudx) + fabsf(dgs.dudy));
if (du == 0.f) du = .01f;
dgEval.p = dgs.p + du * dgs.dpdu;
dgEval.u = dgs.u + du;
dgEval.nn =Normal(Normalize(Cross(dgs.dpdu, dgs.dpdv) +
du* dgs.dndu));
<Shift dgEval dv inthe v direction> =
float dv = .5f *(fabsf(dgs.dvdx) + fabsf(dgs.dvdy));
if (dv == 0.f) dv = .01f;
dgEval.p = dgs.p + dv * dgs.dpdv;
dgEval.u = dgs.u;
dgEval.v = dgs.v + dv;
dgEval.nn =Normal(Normalize(Cross(dgs.dpdu, dgs.dpdv) +
dv* dgs.dndv));
有了新位置和相关的位移纹理值,就可以用前面的公式计算偏导数了:
<Computebump-mapped differential geometry> =
*dgBump = dgs;
dgBump->dpdu = dgs.dpdu + (uDisplace- displace) / du * Vector(dgs.nn) +
displace* dgs.dndu;
dgBump->dpdv = dgs.dpdv +(vDisplace - displace) / dv * Vector(dgs.nn) +
displace* dgs.dndv;
dgBump->nn =Normal(Normalize(Cross(dgBump->dpdu, dgBump->dpdv)));
if (dgs.shape->reverseOrientation^ dgs.shape->transformSwapsHandedness)
dgBump->nn *=- 1.f;
最后,该函数在需要的情况下反转着色坐标系,使得着色法向量位于关于几何法向量的半球之内。因为着色法向量代表几何法向量的一个相对很小的扰动,两个法向量应该在同一个半球之内。
<Orient shadingnormal to match geometric normal> =
if (Dot(dgGeom.nn,dgBump->nn) < 0.f)
dgBump->nn *=-1.f;