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.
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.
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.)
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.
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.
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.
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.
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.
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 parameter (recall Section 8.4.2).
Figure 9.3 shows a plastic dragon. PlasticMaterial is defined in materials/plastic.h and materials/plastic.cpp.
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.
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.
As with the diffuse component, the glossy specular component is skipped if it’s not going to make a contribution to the overall BSDF.
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::ComputeScatteringFunctions() starts with its two constituent Materials initializing their respective BSDFs.
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.
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.
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.
The constructor is responsible for reading the BSDF from a file and initializing the FourierBSDFTable.
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.
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 and absorption coefficients 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.5 demonstrates the KdSubsurfaceMaterial.