这个系列是Physically Based Rendering: From Theory To Implementation的读书笔记,本节主要介绍PBRT中的材质。
BSDFs
在PBRT中使用BSDF
类来代表BRDF和BTDF,这样整个渲染器的其它部件可以直接调用BSDF
类而不是分别考虑BRDF和BTDF两种情况。还需要说明的是PBRT在渲染时会区分几何法线$\mathbf{n}_g$和着色法线$\mathbf{n}_s$:几何法线是指由几何体表面所定义的法线,而着色法线则是由顶点法线或是凹凸贴图来定义。
在BSDF
类的实现中还需要存储有限个BxDF
对象用来表示不同的散射模型,默认最多存储8
个BxDF
。添加BxDF
对象需要调用Add()
函数并提供相应的指针:
void Material::Add(BxDF *b) {
Assert(nBxDFs < MaxBxDFs);
bxdfs[nBxDFs++] = b;
}
由于BxDF
在计算着色的时需要使用的着色坐标系,BSDF
类中也因此需要提供世界坐标系到着色坐标系的变换公式。在Geometric Setting中我们介绍过可以通过三个正交的方向向量$(\mathbf{s}, \mathbf{t}, \mathbf{n})$来定义着色坐标系,这样世界坐标系到着色坐标系的变换矩阵为:
由于变换矩阵$\mathbf{M}$是一个正交矩阵,着色坐标系到世界坐标系的变换矩阵为$\mathbf{M}^T$。
在渲染时着色法线可能会导致一些问题,比如说漏光(light leak)和黑点(dark spot):漏光是指入射光线在几何法线以下,但对于着色法线仍然是可见的;而黑点则是指入射光线对几何法线是可见的,但由于它在着色法线以下而不会计算着色。
为了解决这样的问题,在PBRT中在计算着色时同时使用了两个法线:如果$\omega_i$和$\omega_o$都位于几何法线$\mathbf{n}_g$的同一个半球上则计算BRDF,否则计算BTDF;而在计算散射函数时则使用着色法线$\mathbf{n}_s$进行计算。
对于给定的入射和出射方向,BSDF
类会调用自身的f()
方法来计算反射光线的radiance:
Spectrum BSDF::f(const Vector3f &woW, const Vector3f &wiW,
BxDFType flags) const {
Vector3f wi = WorldToLocal(wiW), wo = WorldToLocal(woW);
bool reflect = Dot(wiW, ng) * Dot(woW, ng) > 0;
Spectrum f(0.f);
for (int i = 0; i < nBxDFs; ++i)
if (bxdfs[i]->MatchesFlags(flags) &&
((reflect && (bxdfs[i]->type & BSDF_REFLECTION)) ||
(!reflect && (bxdfs[i]->type & BSDF_TRANSMISSION))))
f += bxdfs[i]->f(wo, wi);
return f;
}
Material Interface and Implementations
在PBRT中使用Material
类来抽象出不同材质的通用接口,它的定义为
class Material {
public:
virtual void ComputeScatteringFunctions(SurfaceInteraction *si,
MemoryArena &arena, TransportMode mode,
bool allowMultipleLobes) const = 0;
virtual ~Material();
static void Bump(const std::shared_ptr<Texture<Float>> &d, SurfaceInteraction *si);
};
其中最重要的函数是Material::ComputeScatteringFunctions()
,它会为SurfaceInteraction
中的BSDF
对象添加BxDF
实例。其它参数的作用为:
MemoryArena
对象用来分配内存TransportMode
参数用来表示光线是从相机出发还是从光源出发,某些渲染算法需要这个信息allowMultipleLobes
参数用来表示在渲染时否考虑不同类型的BSDF
,它由不同的积分器来控制
进行渲染时由积分器实例化SurfaceInteraction
对象,然后不断调用ComputeScatteringFunctions()
的同名函数直到Material
对象添加BSDF
。
void SurfaceInteraction::ComputeScatteringFunctions(
const RayDifferential &ray, MemoryArena &arena,
bool allowMultipleLobes, TransportMode mode) {
ComputeDifferentials(ray);
primitive->ComputeScatteringFunctions(this, arena, mode,
allowMultipleLobes);
}
void GeometricPrimitive::ComputeScatteringFunctions(
SurfaceInteraction *isect, MemoryArena &arena, TransportMode mode,
bool allowMultipleLobes) const {
if (material)
material->ComputeScatteringFunctions(isect, arena, mode,
allowMultipleLobes);
}
Matte Material
MatteMaterial
材质是最基本的材质模型,它对应理想漫反射材质。MatteMaterial
包含2个参数:漫反射系数Kd
以及粗糙度系数sigma
。
class MatteMaterial : public Material {
public:
MatteMaterial(const std::shared_ptr<Texture<Spectrum>> &Kd,
const std::shared_ptr<Texture<Float>> &sigma,
const std::shared_ptr<Texture<Float>> &bumpMap)
: Kd(Kd), sigma(sigma), bumpMap(bumpMap) { }
void ComputeScatteringFunctions(SurfaceInteraction *si, MemoryArena &arena,
TransportMode mode, bool allowMultipleLobes) const;
private:
std::shared_ptr<Texture<Spectrum>> Kd;
std::shared_ptr<Texture<Float>> sigma, bumpMap;
};
如果sigma
为0则使用Lambertian反射模型,否则使用Oren–Nayar漫反射模型:
void MatteMaterial::ComputeScatteringFunctions(SurfaceInteraction *si,
MemoryArena &arena, TransportMode mode,
bool allowMultipleLobes) const {
if (bumpMap)
Bump(bumpMap, si);
si->bsdf = ARENA_ALLOC(arena, BSDF)(*si);
Spectrum r = Kd->Evaluate(*si).Clamp();
Float sig = Clamp(sigma->Evaluate(*si), 0, 90);
if (!r.IsBlack()) {
if (sig == 0)
si->bsdf->Add(ARENA_ALLOC(arena, LambertianReflection)(r));
else
si->bsdf->Add(ARENA_ALLOC(arena, OrenNayar)(r, sig));
}
}
Plastic Material
PlasticMaterial
是一种常见的材质模型,它包含2个反射率Kd
和Ks
分别对应漫反射和表面光泽镜面反射。
class PlasticMaterial : public Material {
public:
PlasticMaterial(const std::shared_ptr<Texture<Spectrum>> &Kd,
const std::shared_ptr<Texture<Spectrum>> &Ks,
const std::shared_ptr<Texture<Float>> &roughness,
const std::shared_ptr<Texture<Float>> &bumpMap,
bool remapRoughness)
: Kd(Kd), Ks(Ks), roughness(roughness), bumpMap(bumpMap),
remapRoughness(remapRoughness) { }
void ComputeScatteringFunctions(SurfaceInteraction *si, MemoryArena &arena,
TransportMode mode, bool allowMultipleLobes) const;
private:
std::shared_ptr<Texture<Spectrum>> Kd, Ks;
std::shared_ptr<Texture<Float>> roughness, bumpMap;
const bool remapRoughness;
};
void PlasticMaterial::ComputeScatteringFunctions(
SurfaceInteraction *si, MemoryArena &arena, TransportMode mode,
bool allowMultipleLobes) const {
if (bumpMap)
Bump(bumpMap, si);
si->bsdf = ARENA_ALLOC(arena, BSDF)(*si);
Spectrum kd = Kd->Evaluate(*si).Clamp();
if (!kd.IsBlack())
si->bsdf->Add(ARENA_ALLOC(arena, LambertianReflection)(kd));
Spectrum ks = Ks->Evaluate(*si).Clamp();
if (!ks.IsBlack()) {
Fresnel *fresnel = ARENA_ALLOC(arena, FresnelDielectric)(1.f, 1.5f);
Float rough = roughness->Evaluate(*si);
if (remapRoughness)
rough = TrowbridgeReitzDistribution::RoughnessToAlpha(rough);
MicrofacetDistribution *distrib =
ARENA_ALLOC(arena, TrowbridgeReitzDistribution)(rough, rough);
BxDF *spec =
ARENA_ALLOC(arena, MicrofacetReflection)(ks, distrib, fresnel);
si->bsdf->Add(spec);
}
}
Mix Material
在很多情况下需要考虑将两种材质按一定的比例组合起来,因此在PBRT中使用MixMaterial
来描述这种组合材料。
class MixMaterial : public Material {
public:
MixMaterial(const std::shared_ptr<Material> &m1,
const std::shared_ptr<Material> &m2,
const std::shared_ptr<Texture<Spectrum>> &scale)
: m1(m1), m2(m2), scale(scale) { }
void ComputeScatteringFunctions(SurfaceInteraction *si, MemoryArena &arena,
TransportMode mode, bool allowMultipleLobes) const;
private:
std::shared_ptr<Material> m1, m2;
std::shared_ptr<Texture<Spectrum>> scale;
};
同样的道理,在MixMaterial::ComputeScatteringFunctions()
函数中需要添加ScaledBxDF
来将两种BSDF组合起来:
void MixMaterial::ComputeScatteringFunctions(SurfaceInteraction *si,
MemoryArena &arena, TransportMode mode,
bool allowMultipleLobes) const {
Spectrum s1 = scale->Evaluate(*si).Clamp();
Spectrum s2 = (Spectrum(1.f) - s1).Clamp();
SurfaceInteraction si2 = *si;
m1->ComputeScatteringFunctions(si, arena, mode, allowMultipleLobes);
m2->ComputeScatteringFunctions(&si2, arena, mode, allowMultipleLobes);
int n1 = si->bsdf->NumComponents(), n2 = si2.bsdf->NumComponents();
for (int i = 0; i < n1; ++i)
si->bsdf->bxdfs[i] =
ARENA_ALLOC(arena, ScaledBxDF)(si->bsdf->bxdfs[i], s1);
for (int i = 0; i < n2; ++i)
si->bsdf->Add(ARENA_ALLOC(arena, ScaledBxDF)(si2.bsdf->bxdfs[i], s2));
}
Fourier Material
FourierMaterial
对应经过测量的FourierBSDF
材质。
class FourierMaterial : public Material {
public:
FourierMaterial(const std::string &filename, const std::shared_ptr<Texture<Float>> &bump);
void ComputeScatteringFunctions(SurfaceInteraction *si, MemoryArena &arena,
TransportMode mode, bool allowMultipleLobes) const;
private:
FourierBSDFTable bsdfTable;
std::shared_ptr<Texture<Float>> bumpMap;
};
void FourierMaterial::ComputeScatteringFunctions(SurfaceInteraction *si,
MemoryArena &arena, TransportMode mode,
bool allowMultipleLobes) const {
if (bumpMap)
Bump(bumpMap, si);
si->bsdf = ARENA_ALLOC(arena, BSDF)(*si);
si->bsdf->Add(ARENA_ALLOC(arena, FourierBSDF)(bsdfTable, mode));
}
Additional Materials
除了上面介绍过的这些材质外,在PBRT中还包含GlassMaterial
、MetalMaterial
、MirrorMaterial
、SubstrateMaterial
(基于FresnelBlend
)等其它材质,它们的定义和实现可参考src/materials
路径下的对应文件。
Bump Mapping
凹凸贴图(bump mapping)在渲染中是一种非常实用的技术。我们可以通过凹凸贴图在材质表面定义一个距离函数,从而渲染出更加真实的场景。具体来说凹凸贴图在每个$p$点定义了一个偏移量$d(p)$,这样在渲染时$p$点的真实坐标为:
\[p' = p + d(p) \mathbf{n}(p)\]在PBRT中Material
基类实现了Material::Bump()
方法来计算凹凸贴图。对于曲面上的点$p$,我们可以通过参数化平面的坐标$(u, v)$来描述偏移后的位置:
其中法向可以表示为$\mathbf{n}(u, v) = \frac{\partial p}{\partial u} \times \frac{\partial p}{\partial v}$。类似地,我们可以利用偏导来计算凹凸贴图后的着色法向。以$u$方向为例,等式两边对$u$求偏导可以得到:
\[\frac{\partial p'}{\partial u} = \frac{\partial p}{\partial u} + \frac{\partial d(u, v)}{\partial u} \mathbf{n}(u, v) + d(u, v) \frac{\partial \mathbf{n}(u, v)}{\partial u}\]其中$\frac{\partial p}{\partial u}$和$\frac{\partial \mathbf{n}(u, v)}{\partial u}$项已经由SurfaceInteraction
类进行保存,因此这里只需要考虑偏移函数的偏导数$\frac{\partial d(u, v)}{\partial u}$。在PBRT中使用差分来近似它:
除此之外在计算凹凸贴图时还需要提供材质的纹理指针来计算偏移量$\Delta u$。通过调用Material::Bump()
方法凹凸贴图的相关信息就写入到SurfaceInteraction
实例中。
void Material::Bump(const std::shared_ptr<Texture<Float>> &d,
SurfaceInteraction *si) {
SurfaceInteraction siEval = *si;
// Shift siEval du in the u direction
Float du = .5f * (std::abs(si->dudx) + std::abs(si->dudy));
if (du == 0) du = .01f;
siEval.p = si->p + du * si->shading.dpdu;
siEval.uv = si->uv + Vector2f(du, 0.f);
siEval.n = Normalize((Normal3f)Cross(si->shading.dpdu,
si->shading.dpdv) + du * si->dndu);
Float uDisplace = d->Evaluate(siEval);
// Shift siEval dv in the v direction
Float dv = .5f * (std::abs(si->dvdx) + std::abs(si->dvdy));
if (dv == 0) dv = .01f;
siEval.p = si->p + dv * si->shading.dpdv;
siEval.uv = si->uv + Vector2f(0.f, dv);
siEval.n = Normalize((Normal3f)Cross(si->shading.dpdu,
si->shading.dpdv) + dv * si->dndv);
Float vDisplace = d->Evaluate(siEval);
// Compute bump-mapped differential geometry
Float displace = d->Evaluate(*si);
Vector3f dpdu = si->shading.dpdu +
(uDisplace - displace) / du * Vector3f(si->shading.n) +
displace * Vector3f(si->shading.dndu);
Vector3f dpdv = si->shading.dpdv +
(vDisplace - displace) / dv * Vector3f(si->shading.n) +
displace * Vector3f(si->shading.dndv);
si->SetShadingGeometry(dpdu, dpdv, si->shading.dndu, si->shading.dndv, false);
}