9.2 Material Interface and Implementations

The abstract Material class defines the interface that material implementations must provide. The Material class is defined in the files core/material.h and core/material.cpp.

<<Material Declarations>>= 
class Material { public: <<Material Interface>> 
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);
};

A single method must be implemented by Materials: ComputeScatteringFunctions(). This method is given a SurfaceInteraction object that contains geometric properties at an intersection point on the surface of a shape. The method’s implementation is responsible for determining the reflective properties at the point and initializing the SurfaceInteraction::bsdf member variable with a corresponding BSDF class instance. If the material includes subsurface scattering, then the SurfaceInteraction::bssrdf member should be initialized as well. (It should otherwise be left unchanged from its default nullptr value.) The BSSRDF class that represents subsurface scattering functions is defined later, in Section 11.4, after the foundations of volumetric scattering have been introduced.

Three additional parameters are passed to this method:

  • A MemoryArena, which should be used to allocate memory for BSDFs and BSSRDFs.
  • The TransportMode parameter, which indicates whether the surface intersection was found along a path starting from the camera or one starting from a light source; this detail has implications for how BSDFs and BSSRDFs are evaluated—see Section 16.1.
  • Finally, the allowMultipleLobes parameter indicates whether the material should use BxDFs that aggregate multiple types of scattering into a single BxDF when such BxDFs are available. (An example of such a BxDF is FresnelSpecular, which includes both specular reflection and transmission.) These BxDFs can improve the quality of final results when used with Monte Carlo light transport algorithms but can introduce noise in images when used with the DirectLightingIntegrator and WhittedIntegrator. Therefore, the Integrator is allowed to control whether such BxDFs are used via this parameter.

<<Material Interface>>= 
virtual void ComputeScatteringFunctions(SurfaceInteraction *si, MemoryArena &arena, TransportMode mode, bool allowMultipleLobes) const = 0;

Since the usual interface to the intersection point used by Integrators is through an instance of the SurfaceInteraction class, we will add a convenience method ComputeScatteringFunctions() to that class. Its implementation first calls the SurfaceInteraction’s ComputeDifferentials() method to compute information about the projected size of the surface area around the intersection on the image plane for use in texture antialiasing. Next, it forwards the request to the Primitive, which in turn will call the corresponding ComputeScatteringFunctions() method of its Material. (See, for example, the GeometricPrimitive::ComputeScatteringFunctions() implementation.)

<<SurfaceInteraction Method Definitions>>+=  
void SurfaceInteraction::ComputeScatteringFunctions( const RayDifferential &ray, MemoryArena &arena, bool allowMultipleLobes, TransportMode mode) { ComputeDifferentials(ray); primitive->ComputeScatteringFunctions(this, arena, mode, allowMultipleLobes); }

9.2.1 Matte Material

The MatteMaterial material is defined in materials/matte.h and materials/matte.cpp. It is the simplest material in pbrt and describes a purely diffuse surface.

<<MatteMaterial Declarations>>= 
class MatteMaterial : public Material { public: <<MatteMaterial Public Methods>> 
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: <<MatteMaterial Private Data>> 
std::shared_ptr<Texture<Spectrum>> Kd; std::shared_ptr<Texture<Float>> sigma, bumpMap;
};

This material is parameterized by a spectral diffuse reflection value, Kd, and a scalar roughness value, sigma. If sigma has the value zero at the point on a surface, MatteMaterial creates a LambertianReflection BRDF; otherwise, the OrenNayar model is used. Like all of the other Material implementations in this chapter, it also takes an optional scalar texture that defines an offset function over the surface. If its value is not nullptr, this texture is used to compute a shading normal at each point based on the function it defines. (Section 9.3 discusses the implementation of this computation.) Figure 8.14 in the previous chapter shows the MatteMaterial material with the dragon model.

<<MatteMaterial Public Methods>>= 
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) { }

<<MatteMaterial Private Data>>= 
std::shared_ptr<Texture<Spectrum>> Kd; std::shared_ptr<Texture<Float>> sigma, bumpMap;

The ComputeScatteringFunctions() method puts the pieces together, determining the bump map’s effect on the shading geometry, evaluating the textures, and allocating and returning the appropriate BSDF.

<<MatteMaterial Method Definitions>>= 
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)); }
}

If a bump map was provided to the MatteMaterial constructor, the Material::Bump() method is called to calculate the shading normal at the point. This method will be defined in the next section.

<<Perform bump mapping with bumpMap, if present>>= 
if (bumpMap) Bump(bumpMap, si);

Next, the Textures that give the values of the diffuse reflection spectrum and the roughness are evaluated; texture implementations may return constant values, look up values from image maps, or do complex procedural shading calculations to compute these values (the texture evaluation process is the subject of Chapter 10). Given these values, all that needs to be done is to allocate a BSDF and then allocate the appropriate type of Lambertian BRDF and provide it to the BSDF. Because Textures may return negative values or values otherwise outside of the expected range, these values are clamped to valid ranges before they are passed to the BRDF constructor.

<<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)); }

9.2.2 Plastic Material

Plastic can be modeled as a mixture of a diffuse and glossy scattering function with parameters controlling the particular colors and specular highlight size. The parameters to PlasticMaterial are two reflectivities, Kd and Ks, which respectively control the amounts of diffuse reflection and glossy specular reflection.

Next is a roughness parameter that determines the size of the specular highlight. It can be specified in two ways. First, if the remapRoughness parameter is true, then the given roughness should vary from zero to one, where the higher the roughness value, the larger the highlight. (This variant is intended to be fairly user-friendly.) Alternatively, if the parameter is false, then the roughness is used to directly initialize the microfacet distribution’s alpha parameter (recall Section 8.4.2).

Figure 9.3 shows a plastic dragon. PlasticMaterial is defined in materials/plastic.h and materials/plastic.cpp.

Figure 9.3: Dragon Rendered with a Plastic Material. Note the combination of diffuse and glossy specular reflection. (Model courtesy of Christian Schüller.)

<<PlasticMaterial Declarations>>= 
class PlasticMaterial : public Material { public: <<PlasticMaterial Public Methods>> 
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: <<PlasticMaterial Private Data>> 
std::shared_ptr<Texture<Spectrum>> Kd, Ks; std::shared_ptr<Texture<Float>> roughness, bumpMap; const bool remapRoughness;
};

<<PlasticMaterial Public Methods>>= 
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) { }

<<PlasticMaterial Private Data>>= 
std::shared_ptr<Texture<Spectrum>> Kd, Ks; std::shared_ptr<Texture<Float>> roughness, bumpMap; const bool remapRoughness;

The PlasticMaterial::ComputeScatteringFunctions() method follows the same basic structure as MatteMaterial::ComputeScatteringFunctions(): it calls the bump-mapping function, evaluates textures, and then allocates BxDFs to use to initialize the BSDF.

<<PlasticMaterial Method Definitions>>= 
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.f, 1.5f); <<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); }
}

In Material implementations, it’s worthwhile to skip creation of BxDF components that do not contribute to the scattering at a point. Doing so saves the renderer unnecessary work later when it’s computing reflected radiance at the point. Therefore, the Lambertian component is only created if kd is non-zero.

<<Initialize diffuse component of plastic material>>= 
Spectrum kd = Kd->Evaluate(*si).Clamp(); if (!kd.IsBlack()) si->bsdf->Add(ARENA_ALLOC(arena, LambertianReflection)(kd));

As with the diffuse component, the glossy specular component is skipped if it’s not going to make a contribution to the overall BSDF.

<<Initialize specular component of plastic material>>= 
Spectrum ks = Ks->Evaluate(*si).Clamp(); if (!ks.IsBlack()) { Fresnel *fresnel = ARENA_ALLOC(arena, FresnelDielectric)(1.f, 1.5f); <<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); }

<<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);

9.2.3 Mix Material

It’s useful to be able to combine two Materials with varying weights. The MixMaterial takes two other Materials and a Spectrum-valued texture and uses the Spectrum returned by the texture to blend between the two materials at the point being shaded. It is defined in the files materials/mixmat.h and materials/mixmat.cpp.

<<MixMaterial Declarations>>= 
class MixMaterial : public Material { public: <<MixMaterial Public Methods>> 
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: <<MixMaterial Private Data>> 
std::shared_ptr<Material> m1, m2; std::shared_ptr<Texture<Spectrum>> scale;
};

<<MixMaterial Public Methods>>= 
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) { }

<<MixMaterial Private Data>>= 
std::shared_ptr<Material> m1, m2; std::shared_ptr<Texture<Spectrum>> scale;

<<MixMaterial Method Definitions>>= 
void MixMaterial::ComputeScatteringFunctions(SurfaceInteraction *si, MemoryArena &arena, TransportMode mode, bool allowMultipleLobes) const { <<Compute weights and original BxDFs for mix material>> 
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);
<<Initialize si->bsdf with weighted mixture of BxDFs>> 
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));
}

MixMaterial::ComputeScatteringFunctions() starts with its two constituent Materials initializing their respective BSDFs.

<<Compute weights and original BxDFs for mix material>>= 
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);

It then scales BxDFs in the BSDF from the first material, m1, using the ScaledBxDF adapter class, and then scales the BxDFs from the second BSDF, adding all of these BxDF components to si->bsdf.

It may appear that there’s a lurking memory leak in this code, in that the BxDF *s in si->bxdfs are clobbered by newly allocated ScaledBxDFs. However, recall that those BxDFs, like the new ones here, were allocated through a MemoryArena and thus their memory will be freed when the MemoryArena frees its entire block of memory.

<<Initialize si->bsdf with weighted mixture of BxDFs>>= 
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));

The implementation of MixMaterial::ComputeScatteringFunctions() needs direct access to the bxdfs member variables of the BSDF class. Because this is the only class that needs this access, we’ve just made MixMaterial a friend of BSDF rather than adding a number of accessor and setting methods.

<<BSDF Private Data>>+= 
friend class MixMaterial;

9.2.4 Fourier Material

The FourierMaterial class supports measured or synthetic BSDF data that has been tabulated into the directional basis that was introduced in Section 8.6. It is defined in the files materials/fourier.h and materials/fourier.cpp.

<<FourierMaterial Declarations>>= 
class FourierMaterial : public Material { public: <<FourierMaterial Public Methods>> 
FourierMaterial(const std::string &filename, const std::shared_ptr<Texture<Float>> &bump); void ComputeScatteringFunctions(SurfaceInteraction *si, MemoryArena &arena, TransportMode mode, bool allowMultipleLobes) const;
private: <<FourierMaterial Private Data>> 
FourierBSDFTable bsdfTable; std::shared_ptr<Texture<Float>> bumpMap;
};

The constructor is responsible for reading the BSDF from a file and initializing the FourierBSDFTable.

<<FourierMaterial Method Definitions>>= 
FourierMaterial::FourierMaterial(const std::string &filename, const std::shared_ptr<Texture<Float>> &bumpMap) : bumpMap(bumpMap) { FourierBSDFTable::Read(filename, &bsdfTable); }

<<FourierMaterial Private Data>>= 
FourierBSDFTable bsdfTable; std::shared_ptr<Texture<Float>> bumpMap;

Once the data is in memory, the ComputeScatteringFunctions() method’s task is straightforward. After the usual bump-mapping computation, it just has to allocate a FourierBSDF and provide it access to the data in the table.

<<FourierMaterial Method Definitions>>+= 
void FourierMaterial::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); si->bsdf->Add(ARENA_ALLOC(arena, FourierBSDF)(bsdfTable, mode)); }

9.2.5 Additional Materials

Beyond these materials, there are eight more Material implementations available in pbrt, all in the materials/ directory. We will not show all of their implementations here, since they are all just variations on the basic themes introduced in the material implementations above. All take Textures that define scattering parameters, these textures are evaluated in the materials’ respective ComputeScatteringFunctions() methods, and appropriate BxDFs are created and returned in a BSDF. See the documentation on pbrt’s file format for a summary of the parameters that these materials take.

These materials include:

  • GlassMaterial: Perfect or glossy specular reflection and transmission, weighted by Fresnel terms for accurate angular-dependent variation.
  • MetalMaterial: Metal, based on the Fresnel equations for conductors and the Torrance–Sparrow model. Unlike plastic, metal includes no diffuse component. See the files in the directory scenes/spds/metals/ for measured spectral data for the indices of refraction eta and absorption coefficients k for a variety of metals.
  • MirrorMaterial: A simple mirror, modeled with perfect specular reflection.
  • SubstrateMaterial: A layered model that varies between glossy specular and diffuse reflection depending on the viewing angle (based on the FresnelBlend BRDF).
  • SubsurfaceMaterial and KdSubsurfaceMaterial: Materials that return BSSRDFs that describe materials that exhibit subsurface scattering.
  • TranslucentMaterial: A material that describes diffuse and glossy specular reflection and transmission through the surface.
  • UberMaterial: A “kitchen sink” material representing the union of many of the preceding materials. This is a highly parameterized material that is particularly useful when converting scenes from other file formats into pbrt’s.

Figure 8.10 in the previous chapter shows the dragon model rendered with GlassMaterial, and Figure 9.4 shows it with the MetalMaterial.

Figure 9.4: Dragon rendered with the MetalMaterial, based on realistic measured gold scattering data. (Model courtesy of Christian Schüller.)

Figure 9.5 demonstrates the KdSubsurfaceMaterial.

Figure 9.5: Head model rendered with the KdSubsurfaceMaterial, which models subsurface scattering (in conjunction with the subsurface scattering light transport techniques from Section 15.5). (Model courtesy of Infinite Realities, Inc.)