5.1 Spectral Representation
The SPDs of real-world objects can be quite complicated; Figure 5.1 shows graphs of the spectral distribution of emission from a fluorescent light and the spectral distribution of the reflectance of lemon skin. A renderer doing computations with SPDs needs a compact, efficient, and accurate way to represent functions like these. In practice, some trade-off needs to be made between these qualities.
A general framework for investigating these issues can be developed based on the problem of finding good basis functions to represent SPDs. The idea behind basis functions is to map the infinite-dimensional space of possible SPD functions to a low-dimensional space of coefficients . For example, a trivial basis function is the constant function . An arbitrary SPD would be represented in this basis by a single coefficient equal to its average value, so that its approximation would be . This is obviously a poor approximation, since most SPDs are much more complex than this single basis function is capable of representing accurately.
Many different basis functions have been investigated for spectral representation in computer graphics; the “Further Reading” section cites a number of papers and further resources on this topic. Different sets of basis functions can offer substantially different trade-offs in the complexity of the key operations like converting an arbitrary SPD into a set of coefficients (projecting it into the basis), computing the coefficients for the SPD given by the product of two SPDs expressed in the basis, and so on. In this chapter, we’ll introduce two representations that can be used for spectra in pbrt: RGBSpectrum, which follows the typical computer graphics practice of representing SPDs with coefficients representing a mixture of red, green, and blue colors, and SampledSpectrum, which represents the SPD as a set of point samples over a range of wavelengths.
5.1.1 The Spectrum Type
Throughout pbrt, we have been careful to implement all computations involving SPDs in terms of the Spectrum type, using a specific set of built-in operators (addition, multiplication, etc.). The Spectrum type hides the details of the particular spectral representation used, so that changing this detail of the system only requires changing the Spectrum implementation; other code can remain unchanged. The implementations of the Spectrum type are in the files core/spectrum.h and core/spectrum.cpp.
The selection of which spectrum representation is used for the Spectrum type in pbrt is done with a typedef in the file core/pbrt.h. By default, pbrt uses the more efficient but less accurate RGB representation.
We have not written the system such that the selection of which Spectrum implementation to use could be resolved at run time; to switch to a different representation, the entire system must be recompiled. One advantage to this design is that many of the various Spectrum methods can be implemented as short functions that can be inlined by the compiler, rather than being left as stand-alone functions that have to be invoked through the relatively slow virtual method call mechanism. Inlining frequently used short functions like these can give a substantial improvement in performance. A second advantage is that structures in the system that hold instances of the Spectrum type can hold them directly rather than needing to allocate them dynamically based on the spectral representation chosen at run time.
5.1.2 CoefficientSpectrum Implementation
Both of the representations implemented in this chapter are based on storing a fixed number of samples of the SPD. Therefore, we’ll start by defining the CoefficientSpectrum template class, which represents a spectrum as a particular number of samples given as the nSpectrumSamples template parameter. Both RGBSpectrum and SampledSpectrum are partially implemented by inheriting from CoefficientSpectrum.
One CoefficientSpectrum constructor is provided; it initializes a spectrum with a constant value across all wavelengths.
A variety of arithmetic operations on Spectrum objects are needed; the implementations in CoefficientSpectrum are all straightforward. First, we define operations to add pairs of spectral distributions. For the sampled representation, it’s easy to show that each sample value for the sum of two SPDs is equal to the sum of the corresponding sample values.
Similarly, subtraction, multiplication, division, and unary negation are defined component-wise. These methods are very similar to the ones already shown, so we won’t include them here. pbrt also provides equality and inequality tests, also not included here.
It is often useful to know if a spectrum represents an SPD with value zero everywhere. If, for example, a surface has zero reflectance, the light transport routines can avoid the computational cost of casting reflection rays that have contributions that would eventually be multiplied by zeros and thus do not need to be traced.
The Spectrum implementation (and thus the CoefficientSpectrum implementation) must also provide implementations of a number of slightly more esoteric methods, including those that take the square root of an SPD or raise the function it represents to a given power. These are needed for some of the computations performed by the Fresnel classes in Chapter 8, for example. The implementation of Sqrt() takes the square root of each component to give the square root of the SPD. The implementations of Pow() and Exp() are analogous and won’t be included here.
It’s frequently useful to be able to linearly interpolate between two SPDs with a parameter .
Some portions of the image processing pipeline will want to clamp a spectrum to ensure that the function it represents is within some allowable range.
Finally, we provide a debugging routine to check if any of the sample values of the SPD is the not-a-number (NaN floating-point value). This situation can happen due to an accidental division by 0; Assert()s throughout the system use this method to catch this case close to where it happens.
Most of the spectral computations in pbrt can be implemented using the basic operations we have defined so far. However, in some cases it’s necessary to be able to iterate over a set of spectral samples that represent an SPD—for example to perform a spectral sample-based table lookup or to evaluate a piecewise function over wavelengths. Classes that need this functionality in pbrt include the TabulatedBSSRDF class, which is used for subsurface scattering, and the HomogeneousMedium and GridDensityMedium classes.
For these uses, CoefficientSpectrum provides a public constant, nSamples, that gives the number of samples used to represent the SPD and an operator[] method to access individual sample values.
Note that the presence of this sample accessor imposes the implicit assumption that the spectral representation is a set of coefficients that linearly scale a fixed set of basis functions. If, for example, a Spectrum implementation instead represented SPDs as a sum of Gaussians where the coefficients alternatingly scaled the Gaussians and set their width,
then the code that currently uses this accessor would need to be modified, perhaps to instead operate on a version of the SPD that had been converted to a set of linear coefficients. While this crack in the Spectrum abstraction is not ideal, it simplifies other parts of the current system and isn’t too hard to clean up if one adds spectral representations, where this assumption isn’t correct.