9.1 BSDF Representation

There are two components of pbrt’s representation of BSDFs: the BxDF interface and its implementations (described in Section 9.1.2) and the BSDF class (described in Section 9.1.5). The former models specific types of scattering at surfaces, while the latter provides a convenient wrapper around a pointer to a specific BxDF implementation. The BSDF class also centralizes general functionality so that BxDF implementations do not individually need to handle it, and it records information about the local geometric properties of the surface.

9.1.1 Geometric Setting and Conventions

Reflection computations in pbrt are performed in a reflection coordinate system where the two tangent vectors and the normal vector at the point being shaded are aligned with the x , y , and z axes, respectively (Figure 9.2). All direction vectors passed to and returned from the BxDF evaluation and sampling routines will be defined with respect to this coordinate system. It is important to understand this coordinate system in order to understand the BxDF implementations in this chapter.

Section 3.8 introduced a range of utility functions—like SinTheta(), CosPhi(), etc.—that efficiently evaluate trigonometric functions of unit vectors expressed in Cartesian coordinates matching the convention used here. They will be used extensively in this chapter, as quantities like the cosine of the elevation angle play a central role in most reflectance models.

Figure 9.2: The Basic BSDF Coordinate Setting. The shading coordinate system is defined by the orthonormal basis vectors left-parenthesis bold s comma bold t comma bold n right-parenthesis . We will orient these vectors such that they lie along the x , y , and z axes in this coordinate system. Direction vectors omega Subscript in rendering space are transformed into the shading coordinate system before any of the BRDF or BTDF methods are called.

We will frequently find it useful to check whether two direction vectors lie in the same hemisphere with respect to the surface normal in the BSDF coordinate system; the SameHemisphere() function performs this check.

<<Spherical Geometry Inline Functions>>+= 
bool SameHemisphere(Vector3f w, Vector3f wp) { return w.z * wp.z > 0; }

There are some additional conventions that are important to keep in mind when reading the code in this chapter and when adding BRDFs and BTDFs to pbrt:

  • The incident light direction omega Subscript normal i and the outgoing viewing direction omega Subscript normal o will both be normalized and outward facing after being transformed into the local coordinate system at the surface. In other words, the directions will not model the physical propagation of light, which is helpful in bidirectional rendering algorithms that generate light paths in reverse order.
  • In pbrt, the surface normal bold n Subscript always points to the “outside” of the object, which makes it easy to determine if light is entering or exiting transmissive objects: if the incident light direction omega Subscript normal i is in the same hemisphere as bold n Subscript , then light is entering; otherwise, it is exiting. Therefore, the normal may be on the opposite side of the surface than one or both of the omega Subscript normal i and omega Subscript normal o direction vectors. Unlike many other renderers, pbrt does not flip the normal to lie on the same side as omega Subscript normal o .
  • The local coordinate system used for shading may not be exactly the same as the coordinate system returned by the Shape::Intersect() routines from Chapter 6; it may have been modified between intersection and shading to achieve effects like bump mapping. See Chapter 10 for examples of this kind of modification.

9.1.2 BxDF Interface

The interface for the individual BRDF and BTDF functions is defined by BxDF, which is in the file base/bxdf.h.

<<BxDF Definition>>= 
class BxDF : public TaggedPointer< DiffuseBxDF, CoatedDiffuseBxDF, CoatedConductorBxDF, DielectricBxDF, ThinDielectricBxDF, HairBxDF, MeasuredBxDF, ConductorBxDF, NormalizedFresnelBxDF> { public: <<BxDF Interface>> 
BxDFFlags Flags() const; using TaggedPointer::TaggedPointer; std::string ToString() const; SampledSpectrum f(Vector3f wo, Vector3f wi, TransportMode mode) const; pstd::optional<BSDFSample> Sample_f(Vector3f wo, Float uc, Point2f u, TransportMode mode = TransportMode::Radiance, BxDFReflTransFlags sampleFlags = BxDFReflTransFlags::All) const; Float PDF(Vector3f wo, Vector3f wi, TransportMode mode, BxDFReflTransFlags sampleFlags = BxDFReflTransFlags::All) const; SampledSpectrum rho(Vector3f wo, pstd::span<const Float> uc, pstd::span<const Point2f> u2) const; SampledSpectrum rho(pstd::span<const Point2f> u1, pstd::span<const Float> uc2, pstd::span<const Point2f> u2) const; void Regularize();
};

The BxDF interface provides a method to query the material type following the earlier categorization, which some light transport algorithms in Chapters 13 through 15 use to specialize their behavior.

<<BxDF Interface>>= 
BxDFFlags Flags() const;

The BxDFFlags enumeration lists the previously mentioned categories and also distinguishes reflection from transmission. Note that retroreflection is treated as glossy reflection in this list.

<<BxDFFlags Definition>>= 
enum BxDFFlags { Unset = 0, Reflection = 1 << 0, Transmission = 1 << 1, Diffuse = 1 << 2, Glossy = 1 << 3, Specular = 1 << 4, <<Composite BxDFFlags definitions>> 
DiffuseReflection = Diffuse | Reflection, DiffuseTransmission = Diffuse | Transmission, GlossyReflection = Glossy | Reflection, GlossyTransmission = Glossy | Transmission, SpecularReflection = Specular | Reflection, SpecularTransmission = Specular | Transmission, All = Diffuse | Glossy | Specular | Reflection | Transmission
};

These constants can also be combined via a binary or operation to characterize materials that simultaneously exhibit multiple traits. A number of commonly used combinations are provided with their own names for convenience:

<<Composite BxDFFlags definitions>>= 
DiffuseReflection = Diffuse | Reflection, DiffuseTransmission = Diffuse | Transmission, GlossyReflection = Glossy | Reflection, GlossyTransmission = Glossy | Transmission, SpecularReflection = Specular | Reflection, SpecularTransmission = Specular | Transmission, All = Diffuse | Glossy | Specular | Reflection | Transmission

A few utility functions encapsulate the logic for testing various flag characteristics.

<<BxDFFlags Inline Functions>>= 
bool IsReflective(BxDFFlags f) { return f & BxDFFlags::Reflection; } bool IsTransmissive(BxDFFlags f) { return f & BxDFFlags::Transmission; } bool IsDiffuse(BxDFFlags f) { return f & BxDFFlags::Diffuse; } bool IsGlossy(BxDFFlags f) { return f & BxDFFlags::Glossy; } bool IsSpecular(BxDFFlags f) { return f & BxDFFlags::Specular; } bool IsNonSpecular(BxDFFlags f) { return f & (BxDFFlags::Diffuse | BxDFFlags::Glossy); }

The key method that BxDFs provide is f(), which returns the value of the distribution function for the given pair of directions. The provided directions must be expressed in the local reflection coordinate system introduced in the previous section.

This interface implicitly assumes that light in different wavelengths is decoupled—energy at one wavelength will not be reflected at a different wavelength. In this case, the effect of reflection can be described by a per-wavelength factor returned in the form of a SampledSpectrum. Fluorescent materials that redistribute energy between wavelengths would require that this method return an n times n matrix to encode the transfer between the n spectral samples of SampledSpectrum.

Neither constructors nor methods of BxDF implementations will generally be informed about the specific wavelengths associated with SampledSpectrum entries, since they do not require this information.

<<BxDF Interface>>+=  
SampledSpectrum f(Vector3f wo, Vector3f wi, TransportMode mode) const;

The function also takes a TransportMode enumerator that indicates whether the outgoing direction is toward the camera or toward a light source (and the corresponding opposite for the incident direction). This is necessary to handle cases where scattering is non-symmetric; this subtle aspect is discussed further in Section 9.5.2.

BxDFs must also provide a method that uses importance sampling to draw a direction from a distribution that approximately matches the scattering function’s shape. Not only is this operation crucial for efficient Monte Carlo integration of the light transport equation (1.1), it is the only way to evaluate some BSDFs. For example, perfect specular objects like a mirror, glass, or water only scatter light from a single incident direction into a single outgoing direction. Such BxDFs are best described with Dirac delta distributions (covered in more detail in Section 9.1.4) that are zero except for the single direction where light is scattered. Their f() and PDF() methods always return zero.

Implementations of the Sample_f() method should determine the direction of incident light omega Subscript normal i given an outgoing direction omega Subscript normal o and return the value of the BxDF for the pair of directions. They take three uniform samples in the range left-bracket 0 comma 1 right-parenthesis squared via the uc and u parameters. Implementations can use these however they wish, though it is generally best if they use the 1D sample uc to choose between different types of scattering (e.g., reflection or transmission) and the 2D sample to choose a specific direction. Using uc and u[0] to choose a direction, for example, would likely give inferior results to using u[0] and u[1], since uc and u[0] are not necessarily jointly well distributed. Not all the sample values need be used, and BxDFs that need additional sample values must generate them themselves. (The LayeredBxDF described in Section 14.3 is one such example.)

Note the potentially counterintuitive direction convention: the outgoing direction omega Subscript normal o is given, and the implementation then samples an incident direction omega Subscript normal i . The Monte Carlo methods in this book construct light paths in reverse order—that is, counter to the propagation direction of the transported quantity (radiance or importance)—motivating this choice.

Callers of this method must be prepared for the possibility that sampling fails, in which case an unset optional value will be returned.

<<BxDF Interface>>+=  
pstd::optional<BSDFSample> Sample_f(Vector3f wo, Float uc, Point2f u, TransportMode mode = TransportMode::Radiance, BxDFReflTransFlags sampleFlags = BxDFReflTransFlags::All) const;

The sample generation can optionally be restricted to the reflection or transmission component via the sampleFlags parameter. A sampling failure will occur in invalid cases—for example, if the caller requests a transmission sample on an opaque surface.

<<BxDFReflTransFlags Definition>>= 
enum class BxDFReflTransFlags { Unset = 0, Reflection = 1 << 0, Transmission = 1 << 1, All = Reflection | Transmission };

If sampling succeeds, the method returns a BSDFSample that includes the value of the BSDF f, the sampled direction wi, its probability density function (PDF) measured with respect to solid angle, and a BxDFFlags instance that describes the characteristics of the particular sample. BxDFs should specify the direction wi with respect to the local reflection coordinate system, though BSDF::Sample_f() will transform this direction to rendering space before returning it.

Some BxDF implementations (notably, the LayeredBxDF described in Section 14.3) generate samples via simulation, following a random light path. The distribution of paths that escape is the BxDF’s exact (probabilistic) distribution, but the returned f and pdf are only proportional to their true values. (Fortunately, by the same proportion!) This case needs special handling in light transport algorithms, and is indicated by the pdfIsProportional field. For all the BxDFs in this chapter, it can be left set to its default false value.

<<BSDFSample Definition>>= 
struct BSDFSample { <<BSDFSample Public Methods>> 
BSDFSample(SampledSpectrum f, Vector3f wi, Float pdf, BxDFFlags flags, Float eta = 1, bool pdfIsProportional = false) : f(f), wi(wi), pdf(pdf), flags(flags), eta(eta), pdfIsProportional(pdfIsProportional) {} bool IsReflection() const { return pbrt::IsReflective(flags); } bool IsTransmission() const { return pbrt::IsTransmissive(flags); } bool IsDiffuse() const { return pbrt::IsDiffuse(flags); } bool IsGlossy() const { return pbrt::IsGlossy(flags); } bool IsSpecular() const { return pbrt::IsSpecular(flags); }
SampledSpectrum f; Vector3f wi; Float pdf = 0; BxDFFlags flags; Float eta = 1; bool pdfIsProportional = false; };

<<BSDFSample Public Methods>>= 
BSDFSample(SampledSpectrum f, Vector3f wi, Float pdf, BxDFFlags flags, Float eta = 1, bool pdfIsProportional = false) : f(f), wi(wi), pdf(pdf), flags(flags), eta(eta), pdfIsProportional(pdfIsProportional) {}

Several convenience methods can be used to query characteristics of the sample using previously defined functions like BxDFFlags::IsReflective(), etc.

<<BSDFSample Public Methods>>+= 
bool IsReflection() const { return pbrt::IsReflective(flags); } bool IsTransmission() const { return pbrt::IsTransmissive(flags); } bool IsDiffuse() const { return pbrt::IsDiffuse(flags); } bool IsGlossy() const { return pbrt::IsGlossy(flags); } bool IsSpecular() const { return pbrt::IsSpecular(flags); }

The PDF() method returns the value of the PDF for a given pair of directions, which is useful for techniques like multiple importance sampling that compare probabilities of multiple strategies for obtaining a given sample.

<<BxDF Interface>>+=  
Float PDF(Vector3f wo, Vector3f wi, TransportMode mode, BxDFReflTransFlags sampleFlags = BxDFReflTransFlags::All) const;

9.1.3 Hemispherical Reflectance

With the BxDF methods described so far, it is possible to implement methods that compute the reflectance of a BxDF by applying the Monte Carlo estimator to the definitions of reflectance from Equations (4.12) and (4.13).

A first variant of BxDF::rho() computes the reflectance function rho Subscript normal h normal d . Its caller is responsible for determining how many samples should be taken and for providing the uniform sample values to be used in computing the estimate. Thus, depending on the context, callers have control over sampling and the quality of the returned estimate.

<<BxDF Method Definitions>>= 
SampledSpectrum BxDF::rho(Vector3f wo, pstd::span<const Float> uc, pstd::span<const Point2f> u2) const { SampledSpectrum r(0.); for (size_t i = 0; i < uc.size(); ++i) { <<Compute estimate of rho Subscript normal h normal d >> 
pstd::optional<BSDFSample> bs = Sample_f(wo, uc[i], u2[i]); if (bs) r += bs->f * AbsCosTheta(bs->wi) / bs->pdf;
} return r / uc.size(); }

Each term of the estimator

StartFraction 1 Over n EndFraction sigma-summation Underscript j Overscript n Endscripts StartFraction f Subscript normal r Baseline left-parenthesis omega Subscript Baseline comma omega Subscript Baseline Subscript j Baseline right-parenthesis StartAbsoluteValue cosine theta Subscript j Baseline EndAbsoluteValue Over p left-parenthesis omega Subscript Baseline Subscript j Baseline right-parenthesis EndFraction

is easily evaluated.

<<Compute estimate of rho Subscript normal h normal d >>= 
pstd::optional<BSDFSample> bs = Sample_f(wo, uc[i], u2[i]); if (bs) r += bs->f * AbsCosTheta(bs->wi) / bs->pdf;

The hemispherical-hemispherical reflectance is found in the second BxDF::rho() method that evaluates Equation (4.13). As with the first rho() method, the caller is responsible for passing in uniform sample values—in this case, five dimensions’ worth of them.

<<BxDF Method Definitions>>+= 
SampledSpectrum BxDF::rho(pstd::span<const Point2f> u1, pstd::span<const Float> uc, pstd::span<const Point2f> u2) const { SampledSpectrum r(0.f); for (size_t i = 0; i < uc.size(); ++i) { <<Compute estimate of rho Subscript normal h normal h >> 
Vector3f wo = SampleUniformHemisphere(u1[i]); if (wo.z == 0) continue; Float pdfo = UniformHemispherePDF(); pstd::optional<BSDFSample> bs = Sample_f(wo, uc[i], u2[i]); if (bs) r += bs->f * AbsCosTheta(bs->wi) * AbsCosTheta(wo) / (pdfo * bs->pdf);
} return r / (Pi * uc.size()); }

Our implementation samples the first direction wo uniformly over the hemisphere. Given this, the second direction can be sampled using BxDF::Sample_f().

<<Compute estimate of rho Subscript normal h normal h >>= 
Vector3f wo = SampleUniformHemisphere(u1[i]); if (wo.z == 0) continue; Float pdfo = UniformHemispherePDF(); pstd::optional<BSDFSample> bs = Sample_f(wo, uc[i], u2[i]); if (bs) r += bs->f * AbsCosTheta(bs->wi) * AbsCosTheta(wo) / (pdfo * bs->pdf);

9.1.4 Delta Distributions in BSDFs

Several BSDF models in this chapter make use of Dirac delta distributions to represent interactions with perfect specular materials like smooth metal or glass surfaces. They represent a curious corner case in implementations, and we therefore establish a few important conventions.

Recall from Section 8.1.1 that the Dirac delta distribution is defined such that

delta left-parenthesis x right-parenthesis equals 0 for all x not-equals 0

and

integral Subscript negative normal infinity Superscript normal infinity Baseline delta left-parenthesis x right-parenthesis normal d x equals 1 period

According to these equations, delta can be interpreted as a normalized density function that is zero for all x not-equals 0 . Generating a sample from such a distribution is trivial, since there is only one value that it can take. In this sense, the forthcoming implementations of Sample_f() involving delta functions naturally fit into the Monte Carlo sampling framework.

However, sampling alone is not enough: two methods (Sample_f() and PDF) also provide sampling densities, and it is considerably less clear what values should be returned here. Strictly speaking, the delta distribution is not a true function but constitutes the limit of a sequence of functions—for example, one describing a box of unit area whose width approaches 0; see Chapter 5 of Bracewell (2000) for details. In the limit, the value of delta left-parenthesis 0 right-parenthesis must then necessarily tend toward infinity. This important theoretical realization does not easily translate into C++ code: certainly, returning an infinite or very large PDF value is not going to lead to correct results from the renderer.

To resolve this conflict, BSDFs may only contain matched pairs of delta functions in their f Subscript normal r function and PDF. For example, suppose that the PDF factors into a remainder term and a delta function involving a particular direction omega prime Subscript :

p left-parenthesis omega Subscript normal i Baseline right-parenthesis equals p Superscript normal r normal e normal m Baseline left-parenthesis omega Subscript normal i Baseline right-parenthesis delta left-parenthesis omega prime Subscript Baseline minus omega Subscript normal i Baseline right-parenthesis period

If the same holds true for f Subscript normal r , then a Monte Carlo estimator that divides f Subscript normal r by the PDF will never require evaluation of the delta function:

StartFraction f Subscript normal r Baseline left-parenthesis normal p Subscript Baseline comma omega Subscript normal o Baseline comma omega Subscript normal i Baseline right-parenthesis Over p left-parenthesis omega Subscript normal i Baseline right-parenthesis EndFraction equals StartFraction delta left-parenthesis omega prime Subscript Baseline minus omega Subscript normal i Baseline right-parenthesis f Subscript normal r Superscript normal r normal e normal m Baseline left-parenthesis normal p comma omega Subscript normal o Baseline comma omega Subscript normal i Baseline right-parenthesis Over delta left-parenthesis omega prime Subscript Baseline minus omega Subscript normal i Baseline right-parenthesis p Superscript normal r normal e normal m Baseline left-parenthesis omega Subscript normal i Baseline right-parenthesis EndFraction equals StartFraction f Subscript normal r Superscript normal r normal e normal m Baseline left-parenthesis normal p comma omega Subscript normal o Baseline comma omega Subscript normal i Baseline right-parenthesis Over p Superscript normal r normal e normal m Baseline left-parenthesis omega Subscript normal i Baseline right-parenthesis EndFraction period

Implementations of perfect specular materials will thus return a constant PDF of 1 when Sample_f() generates a direction associated with a delta function, with the understanding that the delta function will cancel in the estimator.

In contrast, the respective PDF() methods should return 0 for all directions, since there is zero probability that another sampling method will randomly find the direction from a delta distribution.

9.1.5 BSDFs

BxDF class implementations perform all computation in a local shading coordinate system that is most appropriate for this task. In contrast, rendering algorithms operate in rendering space (Section 5.1.1); hence a transformation between these two spaces must be performed somewhere. The BSDF is a small wrapper around a BxDF that handles this transformation.

<<BSDF Definition>>= 
class BSDF { public: <<BSDF Public Methods>> 
BSDF() = default; BSDF(Normal3f ns, Vector3f dpdus, BxDF bxdf) : bxdf(bxdf), shadingFrame(Frame::FromXZ(Normalize(dpdus), Vector3f(ns))) {} operator bool() const { return (bool)bxdf; } BxDFFlags Flags() const { return bxdf.Flags(); } Vector3f RenderToLocal(Vector3f v) const { return shadingFrame.ToLocal(v); } Vector3f LocalToRender(Vector3f v) const { return shadingFrame.FromLocal(v); } SampledSpectrum f(Vector3f woRender, Vector3f wiRender, TransportMode mode = TransportMode::Radiance) const { Vector3f wi = RenderToLocal(wiRender), wo = RenderToLocal(woRender); if (wo.z == 0) return {}; return bxdf.f(wo, wi, mode); } template <typename BxDF> SampledSpectrum f(Vector3f woRender, Vector3f wiRender, TransportMode mode = TransportMode::Radiance) const { Vector3f wi = RenderToLocal(wiRender), wo = RenderToLocal(woRender); if (wo.z == 0) return {}; const BxDF *specificBxDF = bxdf.CastOrNullptr<BxDF>(); return specificBxDF->f(wo, wi, mode); } pstd::optional<BSDFSample> Sample_f( Vector3f woRender, Float u, Point2f u2, TransportMode mode = TransportMode::Radiance, BxDFReflTransFlags sampleFlags = BxDFReflTransFlags::All) const { Vector3f wo = RenderToLocal(woRender); if (wo.z == 0 ||!(bxdf.Flags() & sampleFlags)) return {}; <<Sample bxdf and return BSDFSample>> 
pstd::optional<BSDFSample> bs = bxdf.Sample_f(wo, u, u2, mode, sampleFlags); if (!bs || !bs->f || bs->pdf == 0 || bs->wi.z == 0) return {}; bs->wi = LocalToRender(bs->wi); return bs;
} Float PDF(Vector3f woRender, Vector3f wiRender, TransportMode mode = TransportMode::Radiance, BxDFReflTransFlags sampleFlags = BxDFReflTransFlags::All) const { Vector3f wo = RenderToLocal(woRender), wi = RenderToLocal(wiRender); if (wo.z == 0) return 0; return bxdf.PDF(wo, wi, mode, sampleFlags); } template <typename BxDF> PBRT_CPU_GPU pstd::optional<BSDFSample> Sample_f(Vector3f woRender, Float u, Point2f u2, TransportMode mode = TransportMode::Radiance, BxDFReflTransFlags sampleFlags = BxDFReflTransFlags::All) const { Vector3f wo = RenderToLocal(woRender); if (wo.z == 0) return {}; const BxDF *specificBxDF = bxdf.Cast<BxDF>(); if (!(specificBxDF->Flags() & sampleFlags)) return {}; pstd::optional<BSDFSample> bs = specificBxDF->Sample_f(wo, u, u2, mode, sampleFlags); if (!bs || !bs->f || bs->pdf == 0 || bs->wi.z == 0) return {}; DCHECK_GT(bs->pdf, 0); bs->wi = LocalToRender(bs->wi); return bs; } template <typename BxDF> PBRT_CPU_GPU Float PDF(Vector3f woRender, Vector3f wiRender, TransportMode mode = TransportMode::Radiance, BxDFReflTransFlags sampleFlags = BxDFReflTransFlags::All) const { Vector3f wo = RenderToLocal(woRender), wi = RenderToLocal(wiRender); if (wo.z == 0) return 0; const BxDF *specificBxDF = bxdf.Cast<BxDF>(); return specificBxDF->PDF(wo, wi, mode, sampleFlags); } std::string ToString() const; SampledSpectrum rho(pstd::span<const Point2f> u1, pstd::span<const Float> uc, pstd::span<const Point2f> u2) const { return bxdf.rho(u1, uc, u2); } SampledSpectrum rho(Vector3f woRender, pstd::span<const Float> uc, pstd::span<const Point2f> u) const { Vector3f wo = RenderToLocal(woRender); return bxdf.rho(wo, uc, u); } void Regularize() { bxdf.Regularize(); }
private: <<BSDF Private Members>> 
BxDF bxdf; Frame shadingFrame;
};

In addition to an encapsulated BxDF, the BSDF holds a shading frame based on the Frame class.

<<BSDF Private Members>>= 
BxDF bxdf; Frame shadingFrame;

Figure 9.3: 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. It is important to handle this inconsistency carefully since it can otherwise lead to artifacts in images.

The constructor initializes the latter from the shading normal bold n Subscript normal s and partial-differential normal p slash partial-differential u using the shading coordinate system convention (Figure 9.3).

<<BSDF Public Methods>>= 
BSDF() = default; BSDF(Normal3f ns, Vector3f dpdus, BxDF bxdf) : bxdf(bxdf), shadingFrame(Frame::FromXZ(Normalize(dpdus), Vector3f(ns))) {}

The default constructor creates a BSDF with a nullptr-valued bxdf, which is useful to represent transitions between different media that do not themselves scatter light. An operator bool() method checks whether the BSDF represents a real material interaction, in which case the Flags() method provides further information about its high-level properties.

<<BSDF Public Methods>>+=  
operator bool() const { return (bool)bxdf; } BxDFFlags Flags() const { return bxdf.Flags(); }

The BSDF provides methods that perform transformations to and from the reflection coordinate system used by BxDFs.

<<BSDF Public Methods>>+=  
Vector3f RenderToLocal(Vector3f v) const { return shadingFrame.ToLocal(v); } Vector3f LocalToRender(Vector3f v) const { return shadingFrame.FromLocal(v); }

The f() function performs the required coordinate frame conversion and then queries the BxDF. The rare case in which the wo direction lies exactly in the surface’s tangent plane often leads to not-a-number (NaN) values in BxDF implementations that further propagate and may eventually contaminate the rendered image. The BSDF avoids this case by immediately returning a zero-valued SampledSpectrum.

<<BSDF Public Methods>>+=  
SampledSpectrum f(Vector3f woRender, Vector3f wiRender, TransportMode mode = TransportMode::Radiance) const { Vector3f wi = RenderToLocal(wiRender), wo = RenderToLocal(woRender); if (wo.z == 0) return {}; return bxdf.f(wo, wi, mode); }

The BSDF also provides a second templated f() method that can be parameterized by the underlying BxDF. If the caller knows the specific type of BSDF::bxdf, it can call this variant directly without involving the dynamic method dispatch used in the method above. This approach is used by pbrt’s wavefront rendering path, which groups evaluations based on the underlying BxDF to benefit from vectorized execution on the GPU. The implementation of this specialized version simply casts the BxDF to the provided type before invoking its f() method.

<<BSDF Public Methods>>+=  
template <typename BxDF> SampledSpectrum f(Vector3f woRender, Vector3f wiRender, TransportMode mode = TransportMode::Radiance) const { Vector3f wi = RenderToLocal(wiRender), wo = RenderToLocal(woRender); if (wo.z == 0) return {}; const BxDF *specificBxDF = bxdf.CastOrNullptr<BxDF>(); return specificBxDF->f(wo, wi, mode); }

The BSDF::Sample_f() method similarly forwards the sampling request on to the BxDF after transforming the omega Subscript normal o direction to the local coordinate system.

<<BSDF Public Methods>>+=  
pstd::optional<BSDFSample> Sample_f( Vector3f woRender, Float u, Point2f u2, TransportMode mode = TransportMode::Radiance, BxDFReflTransFlags sampleFlags = BxDFReflTransFlags::All) const { Vector3f wo = RenderToLocal(woRender); if (wo.z == 0 ||!(bxdf.Flags() & sampleFlags)) return {}; <<Sample bxdf and return BSDFSample>> 
pstd::optional<BSDFSample> bs = bxdf.Sample_f(wo, u, u2, mode, sampleFlags); if (!bs || !bs->f || bs->pdf == 0 || bs->wi.z == 0) return {}; bs->wi = LocalToRender(bs->wi); return bs;
}

If the BxDF implementation returns a sample that has a zero-valued BSDF or PDF or an incident direction in the tangent plane, this method nevertheless returns an unset sample value. This allows calling code to proceed without needing to check those cases.

<<Sample bxdf and return BSDFSample>>= 
pstd::optional<BSDFSample> bs = bxdf.Sample_f(wo, u, u2, mode, sampleFlags); if (!bs || !bs->f || bs->pdf == 0 || bs->wi.z == 0) return {}; bs->wi = LocalToRender(bs->wi); return bs;

BSDF::PDF() follows the same pattern.

<<BSDF Public Methods>>+=  
Float PDF(Vector3f woRender, Vector3f wiRender, TransportMode mode = TransportMode::Radiance, BxDFReflTransFlags sampleFlags = BxDFReflTransFlags::All) const { Vector3f wo = RenderToLocal(woRender), wi = RenderToLocal(wiRender); if (wo.z == 0) return 0; return bxdf.PDF(wo, wi, mode, sampleFlags); }

We have omitted the definitions of additional templated Sample_f() and PDF() variants that are parameterized by the BxDF type.

Finally, BSDF provides rho() methods to compute the reflectance that forward the call on to its underlying bxdf. They are trivial and therefore not included here.