9.1 BSDFs

The BSDF class represents a collection of BRDFs and BTDFs. Grouping them in this manner allows the rest of the system to work with composite BSDFs directly, rather than having to consider all of the components they may have been built from. Equally important, the BSDF class hides some of the details of shading normals from the rest of the system. Shading normals, either from per-vertex normals in triangle meshes or from bump mapping, can substantially improve the visual richness of rendered scenes, but because they are an ad hoc construct, they are tricky to incorporate into a physically based renderer. The issues that they introduce are handled in the BSDF implementation.

<<BSDF Declarations>>+= 
class BSDF { public: <<BSDF Public Methods>> 
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)) { } void Add(BxDF *b) { Assert(nBxDFs < MaxBxDFs); bxdfs[nBxDFs++] = b; } int NumComponents(BxDFType flags = BSDF_ALL) const; 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); } Spectrum f(const Vector3f &woW, const Vector3f &wiW, BxDFType flags = BSDF_ALL) const; Spectrum rho(int nSamples, const Point2f *samples1, const Point2f *samples2, BxDFType flags = BSDF_ALL) const; Spectrum rho(const Vector3f &wo, int nSamples, const Point2f *samples, BxDFType flags = BSDF_ALL) const; Spectrum Sample_f(const Vector3f &wo, Vector3f *wi, const Point2f &u, Float *pdf, BxDFType type = BSDF_ALL, BxDFType *sampledType = nullptr) const; Float Pdf(const Vector3f &wo, const Vector3f &wi, BxDFType flags = BSDF_ALL) const;
<<BSDF Public Data>> 
const Float eta;
private: <<BSDF Private Methods>> 
~BSDF() { }
<<BSDF Private Data>> 
const Normal3f ns, ng; const Vector3f ss, ts; int nBxDFs = 0; static constexpr int MaxBxDFs = 8; BxDF *bxdfs[MaxBxDFs]; friend class MixMaterial;
};

The BSDF constructor takes a SurfaceInteraction object that contains information about the differential geometry at the point on a surface as well as a parameter eta that gives the relative index of refraction over the boundary. For opaque surfaces, eta isn’t used, and a value of one should be provided by the caller. (The default value of one for eta is for just this case.) The constructor computes an orthonormal coordinate system with the shading normal as one of the axes; this coordinate system will be useful for transforming directions to and from the BxDF coordinate system that is described in Figure 8.2. Throughout this section, we will use the convention that bold n Subscript normal s denotes the shading normal and bold n Subscript normal g the geometric normal (Figure 9.1).

Figure 9.1: The geometric normal, bold n Subscript normal g , defined by the surface geometry, and the shading normal, bold n Subscript normal s , given by per-vertex normals and/or bump mapping, will generally define different hemispheres for integrating incident illumination to compute surface reflection. This inconsistency is important to handle carefully since it can otherwise lead to artifacts in images.

<<BSDF Public Methods>>= 
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;

The BSDF implementation stores only a limited number of individual BxDF components. It could easily be extended to allocate more space if more components were given to it, although this isn’t necessary for any of the Material implementations in pbrt thus far, and the current limit of eight is plenty for almost all practical applications.

<<BSDF Public Methods>>+=  
void Add(BxDF *b) { Assert(nBxDFs < MaxBxDFs); bxdfs[nBxDFs++] = b; }

<<BSDF Private Data>>+=  
int nBxDFs = 0; static constexpr int MaxBxDFs = 8; BxDF *bxdfs[MaxBxDFs];

For other parts of the system that need additional information about the particular BRDFs and BTDFs that are present, a method returns the number of BxDFs stored by the BSDF that match a particular set of BxDFType flags.

<<BSDF Public Methods>>+=  
int NumComponents(BxDFType flags = BSDF_ALL) const;

The BSDF also has methods that perform transformations to and from the local coordinate system used by BxDFs. Recall that, in this coordinate system, the surface normal is along the z axis left-parenthesis 0 comma 0 comma 1 right-parenthesis , the primary tangent is left-parenthesis 1 comma 0 comma 0 right-parenthesis , and the secondary tangent is left-parenthesis 0 comma 1 comma 0 right-parenthesis . The transformation of directions into “shading space” simplifies many of the BxDF implementations in Chapter 8. Given three orthonormal vectors bold s , bold t , and bold n in world space, the matrix bold upper M that transforms vectors in world space to the local reflection space is

bold upper M equals Start 3 By 3 Matrix 1st Row 1st Column bold s Subscript x Baseline 2nd Column bold s Subscript y Baseline 3rd Column bold s Subscript z Baseline 2nd Row 1st Column bold t Subscript x Baseline 2nd Column bold t Subscript y Baseline 3rd Column bold t Subscript z Baseline 3rd Row 1st Column bold n Subscript x Baseline 2nd Column bold n Subscript y Baseline 3rd Column bold n Subscript z Baseline EndMatrix equals Start 3 By 1 Matrix 1st Row bold s 2nd Row bold t 3rd Row bold n EndMatrix period

To confirm this yourself, consider, for example, the value of bold upper M times the surface normal bold n Subscript , bold upper M bold n equals left-parenthesis bold s dot bold n comma bold t dot bold n comma bold n dot bold n right-parenthesis . Since bold s , bold t , and bold n are all orthonormal, the x and y components of bold upper M bold n are zero. Since bold n is normalized, bold n dot bold n equals 1 . Thus, bold upper M bold n equals left-parenthesis 0 comma 0 comma 1 right-parenthesis , as expected.

In this case, we don’t need to compute the inverse transpose of bold upper M to transform normals (recall the discussion of transforming normals in Section 2.8.3). Because bold upper M is an orthonormal matrix (its rows and columns are mutually orthogonal and unit length), its inverse is equal to its transpose, so it is its own inverse transpose already.

<<BSDF Public Methods>>+=  
Vector3f WorldToLocal(const Vector3f &v) const { return Vector3f(Dot(v, ss), Dot(v, ts), Dot(v, ns)); }

The method that takes vectors back from local space to world space transposes bold upper M to find its inverse before doing the appropriate dot products.

<<BSDF Public Methods>>+=  
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 can cause a variety of undesirable artifacts in practice (Figure 9.2). Figure 9.2(a) shows a light leak: the geometric normal indicates that omega Subscript normal i and omega Subscript normal o lie on opposite sides of the surface, so if the surface is not transmissive, the light should have no contribution. However, if we directly evaluate the scattering equation, Equation (5.9), about the hemisphere centered around the shading normal, we will incorrectly incorporate the light from omega Subscript normal i . This case demonstrates that bold n Subscript bold s can’t just be used as a direct replacement for bold n Subscript bold g in rendering computations.

Figure 9.2: The Two Types of Errors That Result from Using Shading Normals. (a) A light leak: the geometric normal indicates that the light is on the back side of the surface, but the shading normal indicates the light is visible (assuming a reflective and not transmissive surface). (b) A dark spot: the geometric normal indicates that the surface is illuminated, but the shading normal indicates that the viewer is behind the lit side of the surface.

Figure 9.2(b) shows a similar tricky situation: the shading normal indicates that no light should be reflected to the viewer, since it is not in the same hemisphere as the illumination, while the geometric normal indicates that they are in the same hemisphere. Direct use of bold n Subscript bold s would cause ugly black spots on the surface where this situation happens.

Fortunately, there is an elegant solution to these problems. When evaluating the BSDF, we can use the geometric normal to decide if we should be evaluating reflection or transmission: if omega Subscript normal i and omega Subscript normal o lie in the same hemisphere with respect to bold n Subscript normal g , we evaluate the BRDFs, and otherwise we evaluate the BTDFs. In evaluating the scattering equation, however, the dot product of the normal and the incident direction is still taken with the shading normal rather than the geometric normal.

Now it should be clear why pbrt requires BxDFs to evaluate their values without regard to whether omega Subscript normal i and omega Subscript normal o are in the same or different hemispheres. This convention means that light leaks are avoided, since we will only evaluate the BTDFs for the situation in Figure 9.2(a), giving no reflection for a purely reflective surface. Similarly, black spots are avoided since we will evaluate the BRDFs for the situation in Figure 9.2(b), even though the shading normal would suggest that the directions are in different hemispheres.

Given these conventions, the method that evaluates the BSDF for a given pair of directions follows directly. It starts by transforming the world space direction vectors to local BSDF space and then determines whether it should use the BRDFs or the BTDFs. It then loops over the appropriate set and evaluates the sum of their contributions.

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

pbrt also provides BSDF methods that return the BSDF’s reflectances. (Recall the definition of reflectance in Section 8.1.1.) The two corresponding methods just loop over the BxDFs and sum the values returned by their BxDF::rho() methods; their straightforward implementations aren’t included here. These methods take arrays of samples for BxDFs for use in Monte Carlo sampling algorithms if needed (recall the BxDF::rho() interface defined in Section 8.1.1, which takes such samples as well.)

<<BSDF Public Methods>>+=  
Spectrum rho(int nSamples, const Point2f *samples1, const Point2f *samples2, BxDFType flags = BSDF_ALL) const; Spectrum rho(const Vector3f &wo, int nSamples, const Point2f *samples, BxDFType flags = BSDF_ALL) const;

9.1.1 BSDF Memory Management

For each ray that intersects geometry in the scene, one or more BSDF objects will be created by the Integrator in the process of computing the radiance carried along the ray. (Integrators that account for multiple interreflections of light will generally create a number of BSDFs along the way.) Each of these BSDFs in turn has a number of BxDFs stored inside it, as created by the Materials at the intersection points.

A naïve implementation would use new and delete to dynamically allocate storage for both the BSDF as well as each of the BxDFs that it holds. Unfortunately, such an approach would be unacceptably inefficient—too much time would be spent in the dynamic memory management routines for a series of small memory allocations. Instead, the implementation here uses a specialized allocation scheme based on the MemoryArena class described in Section A.4.3. A MemoryArena is passed into methods that allocate memory for BSDFs. For example, the SamplerIntegrator::Render() method creates a MemoryArena for each image tile and passes it to the integrators, which in turn pass it to the Material.

For the convenience of code that allocates BSDFs and BxDFs (e.g., the Materials in this chapter), there is a macro that hides some of the messiness of using the memory arena. Instead of using the new operator to allocate those objects like this:

BSDF *b = new BSDF; BxDF *lam = new LambertianReflection(Spectrum(0.5f));

code should instead be written with the ARENA_ALLOC() macro, like this:

BSDF *b = ARENA_ALLOC(arena, BSDF); BxDF *lam = ARENA_ALLOC(arena, LambertianReflection)(Spectrum(0.5f));

where arena is a MemoryArena.

The ARENA_ALLOC() macro uses the placement operator new to run the constructor for the object at the returned memory location.

<<Memory Declarations>>= 
#define ARENA_ALLOC(arena, Type) new (arena.Alloc(sizeof(Type))) Type

The BSDF destructor is a private method in order to ensure that it isn’t inadvertently called (e.g., due to an attempt to delete a BSDF). Making the destructor private ensures a compile time error if it is called. Trying to delete memory allocated by the MemoryArena could lead to errors or crashes, since a pointer to the middle of memory managed by the MemoryArena would be passed to the system’s dynamic memory freeing routine.

In turn, an implication of the allocation scheme here is that BSDF and BxDF destructors are never executed. This isn’t a problem for the ones currently implemented in the system.

<<BSDF Private Methods>>= 
~BSDF() { }