前言
上一章的BRDFs和BTDFs描述的是光在表面的一点上如何发散,但渲染器需要知道表面上的一点对应的是哪些BRDFs和BTDFs,以及它们的参数是什么。本章讨论的就是如何解决这些问题。
基本的思路是对场景中的每个primitive(几何图元)绑定一个surface shader。这个surface shader用一个Material接口类,它有一个方法,给定该方法一个suface的指针,该方法就会创建一个描述该点的光如何scatting的BsDF对象。BsDF class持有一系列BxDFs,它们的作用会被累加起来加到full scattering function中。而Materials对象,使用Texture class的实例(下一章介绍)来决定在每一个点的材质的参数。
9.1 BsDFs
BsDFs class 是BRDFs和BTDFs的集合。并且它会隐藏一些shading normals(着色法线)的细节,无论是来自三角形网格中的per-vertex normals(逐顶点法线)或者从bump mapping,shading normals都可以显著提高渲染表面的丰富性。
BSDF构造函数
BSDF的构造函数需要一个SurfaceInterfact对象(包含有一个表面某一点的几何信息),以及一个表示边界折射率的参数eta。对于不透明的物体,eta是不会使用的(默认为1)。
BSDF会计算出一个坐标系统(也就是上一章BxDF使用的local坐标系统!),该坐标系统以shading normal作为其中一个坐标轴(z轴)。为了方便,我们用ns标记shading normal,用ng标记geometric normal(如图9.1)。
BSDF构造函数及相关成员变量如下:
BSDF(const SurfaceInteraction &si, Float eta = 1)
: eta(eta), ns(si.shading.n), ng(si.n),
ss(Normalize(si.shading.dpdu)), ts(Cross(ns, ss)) { }
//BSDF Public Data
const Float eta;
//BSDF Private Data
const Normal3f ns, ng; const Vector3f ss, ts;
bxdfs数组
BSDF内部存储了有限数量的BxDF组件,目前限制8个,如需要可以扩展。相关代码如下:
void Add(BxDF *b) {
Assert(nBxDFs < MaxBxDFs);
bxdfs[nBxDFs++] = b;
}
//BSDF Private Data
int nBxDFs = 0;
static constexpr int MaxBxDFs = 8;
BxDF *bxdfs[MaxBxDFs];
int NumComponents(BxDFType flags = BSDF_ALL) const;
WorldToLocal()和LocalToWorld()
BSDF提供将世界坐标系转换为Local reflection坐标系(或反过来)。回忆第8章,因为在local reflection坐标系中,法线正好是z轴((0, 0, 1),表面上的一条切线是x轴(1, 0, 0),另一条切线是y轴(0, 1, 0),如果给定了在世界坐标系的这三个向量n,s,t,那么从世界坐标系到local reflection坐标系的变换矩阵M就是:
可以验证一下上面的推论是否正确,例如,用M乘以表面法线n,Mn=(s·n, t·n, n·n)。因为s,t和n是相互垂直的,因此Mn的x和y向量都是0,因为n是normalized的,因此nn = 1。因此,Mn= (0,0,1)。
当从Local reflection坐标系转换到世界坐标系时,我们不需要计算M的逆矩阵,因为M是正交矩阵,它的逆矩阵等于它的转置矩阵。对应的代码如下:
Vector3f WorldToLocal(const Vector3f &v) const {
return Vector3f(Dot(v, ss), Dot(v, ts), Dot(v, ns));
}
Vector3f LocalToWorld(const Vector3f &v) const {
return Vector3f(ss.x * v.x + ts.x * v.y + ns.x * v.z,
ss.y * v.x + ts.y * v.y + ns.y * v.z,
ss.z * v.x + ts.z * v.y + ns.z * v.z);
}
Shading normals带来的问题
Shading normals可能会引起不希望发生的错误。如图9.2,图a展示了一个 light leak(光线泄露):如果用geometric normal计算,ωi和ωo处在表面的不同侧,因此如果表面没有透射的话,这条光线是没有贡献的。然而,如果用shading normal的话,ωi和ωo处在表面的同一侧,会把ωi的影响计算进去。这是情况告诉我们,不能直接用ns替换ng。
图b展示了类似的问题:shading normal指示没有光会反射到观察者那里,因为观察者和输入光不在表面的同一个半球,但shading normal指示它们在同一个半球。当这种情况发生是,直接用ns会导致表面上有难看的黑点。
幸运的是,有一个优雅的方法解决这些问题。当计算BSDF的时候,我们可以用geometric normal来决定我们应该计算反射还是透射:在以ng为准的表面上,如果ωi and ωo实在同一个半球,则只计算BRDFs,如果不在同一半球,则值计算BTDFs。
BSDF::f()接口
BSDF::f()接口先通过上面的方法判断是反射还是透射,然后把多个bxdf是累加起来。
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; }
Spectrum BSDF::rho()接口
主要用在蒙特卡洛采样算法,后面再分析。
9.1.1 BsDF Memory Management
内存管理相关,略过...
9.2 Material Interface and Implementations
Material class是一个虚基类,如下:
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> &d,
SurfaceInteraction *si);
};
Material的派生类只有一个接口必须要实现:Materials: ComputeScatteringFunctions()。输入参数是一个SurfaceInteraction对象,它包含了在表面上的一个交点的集合属性。该方法的任务是,确定在该point的反射属性,以及用一个BSDF实例初始化SurfaceInteraction::bsdf成员变量。(如果材质包含次表面散射, SurfaceInteraction::bssrdf成员变量也应该被初始化,在此先不管)。
另外三个参数,arena,mode,allowMultipleLobes先不管。
ComputeScatteringFunctions()的调用堆栈
因为上层的积分器(如WhittedIntegrator)只能访问到SurfaceInteraction对象,这里介绍一下积分器是如何调用到Material::ComputeScatteringFunctions()接口的,直接贴下相关代码:
Spectrum WhittedIntegrator::Li(const RayDifferential &ray, const Scene &scene,
Sampler &sampler, MemoryArena &arena,
int depth) const {
SurfaceInteraction isect;
if (!scene.Intersect(ray, &isect)) {
for (const auto &light : scene.lights) L += light->Le(ray);
return L;
}
// Compute scattering functions for surface interaction
isect.ComputeScatteringFunctions(ray, arena);
if (!isect.bsdf)
return Li(isect.SpawnRay(ray.d), scene, sampler, arena, depth);
//........
}
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);
}
因为现在还没介绍积分器以及整个渲染流程,这个大致了解一下就可以了吧。
9.2.1 Matte Material(无光泽的材质)
MatteMaterial class如下:
class MatteMaterial : public Material {
public:
// MatteMaterial Public Methods
MatteMaterial(const std::shared_ptr> &Kd,
const std::shared_ptr> &sigma,
const std::shared_ptr> &bumpMap)
: Kd(Kd), sigma(sigma), bumpMap(bumpMap) {}
void ComputeScatteringFunctions(SurfaceInteraction *si, MemoryArena &arena,
TransportMode mode,
bool allowMultipleLobes) const;
private:
// MatteMaterial Private Data
std::shared_ptr> Kd;
std::shared_ptr> sigma, bumpMap;
};
MatteMaterial需要一个漫反射光谱参数Kd,以及一个代表粗糙度的参数sigma。如果sigma为0,则MatteMaterial会创建一个LambertianReflection BRDF;否则,使用OrenNayar模型。还有一个可选的参数bumpMap,它可以改变表面的法线(也就是shading normal),在9.3节介绍。
MatteMaterial::ComputeScatteringFunctions()
代码如下:
void MatteMaterial::ComputeScatteringFunctions(SurfaceInteraction *si,
MemoryArena &arena,
TransportMode mode,
bool allowMultipleLobes) const {
// Perform bump mapping with _bumpMap_, if present
if (bumpMap) Bump(bumpMap, si);
// Evaluate textures for _MatteMaterial_ material and allocate BRDF
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));
}
}
从代码可以看出,ComputeScatteringFunctions()先判断是否有bumpMap,如果有则根据它计算该点的shading normal(9.3节介绍)。然后从Kd纹理中读取该点的漫反射光谱r,以及从sigma纹理中读出粗糙度sig(Texture相关在下一章介绍)。根据sig来对BSDF添加LambertianReflection或OrenNayar,并返回给si的bsdf参数。
Plastic Material (塑料材质)
Plastic可以用一个diffuse(漫反射)和glossy scattering(高光散射)的混合来建模,通过参数控制特定的颜色(diffuse参数)和镜面高光的大小(glossy scattering参数)。代码如下:
class PlasticMaterial : public Material {
public:
// PlasticMaterial Public Methods
PlasticMaterial(const std::shared_ptr> &Kd,
const std::shared_ptr> &Ks,
const std::shared_ptr> &roughness,
const std::shared_ptr> &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:
// PlasticMaterial Private Data
std::shared_ptr> Kd, Ks;
std::shared_ptr> roughness, bumpMap;
const bool remapRoughness;
};
Kd and Ks参数分别控制diffuse reflection和glossy specular reflection的大小。
roughness参数控制specular highlight的大小。有两种方式。第一种,如果remapRoughness参数为true,那么roughness的范围是0到1,roughness越大,则高光的范围越大。另一种,如果remapRoughness为false,roughness参数就直接做为microfacet distribution的α的参数(8.4.2节)。
PlasticMaterial::ComputeScatteringFunctions()
代码还是比较简单,直接贴代码,不分析了:
void PlasticMaterial::ComputeScatteringFunctions(
SurfaceInteraction *si, MemoryArena &arena, TransportMode mode,
bool allowMultipleLobes) const {
// Perform bump mapping with _bumpMap_, if present
if (bumpMap) Bump(bumpMap, si);
si->bsdf = ARENA_ALLOC(arena, BSDF)(*si);
// Initialize diffuse component of plastic material
Spectrum kd = Kd->Evaluate(*si).Clamp();
if (!kd.IsBlack())
si->bsdf->Add(ARENA_ALLOC(arena, LambertianReflection)(kd));
// Initialize specular component of plastic material
Spectrum ks = Ks->Evaluate(*si).Clamp();
if (!ks.IsBlack()) {
Fresnel *fresnel = ARENA_ALLOC(arena, FresnelDielectric)(1.5f, 1.f);
// Create microfacet distribution _distrib_ for plastic material
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);
}
}