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.

Figure 5.1: (a) Spectral power distributions of a fluorescent light and (b) the reflectance of lemon skin. Wavelengths around 400 nm are bluish colors, greens and yellows are in the middle range of wavelengths, and reds have wavelengths around 700 nm. The fluorescent light’s SPD is even spikier than shown here, where the SPDs have been binned into 10-nm ranges; it actually emits much of its illumination at single discrete frequencies.

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 c Subscript i Baseline element-of bold upper R Superscript . For example, a trivial basis function is the constant function upper B left-parenthesis lamda right-parenthesis equals 1 . An arbitrary SPD would be represented in this basis by a single coefficient c equal to its average value, so that its approximation would be c upper B left-parenthesis lamda right-parenthesis equals c . 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.

<<Global Forward Declarations>>= 
typedef RGBSpectrum Spectrum; // typedef SampledSpectrum Spectrum;

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.

<<Spectrum Declarations>>= 
template <int nSpectrumSamples> class CoefficientSpectrum { public: <<CoefficientSpectrum Public Methods>> 
CoefficientSpectrum(Float v = 0.f) { for (int i = 0; i < nSpectrumSamples; ++i) c[i] = v; } #ifdef DEBUG CoefficientSpectrum(const CoefficientSpectrum &s) { for (int i = 0; i < nSpectrumSamples; ++i) c[i] = s.c[i]; } CoefficientSpectrum &operator=(const CoefficientSpectrum &s) { for (int i = 0; i < nSpectrumSamples; ++i) c[i] = s.c[i]; return *this; } #endif // DEBUG void Print(FILE *f) const { fprintf(f, "[ "); for (int i = 0; i < nSpectrumSamples; ++i) { fprintf(f, "%f", c[i]); if (i != nSpectrumSamples-1) fprintf(f, ", "); } fprintf(f, "]"); } CoefficientSpectrum &operator+=(const CoefficientSpectrum &s2) { for (int i = 0; i < nSpectrumSamples; ++i) c[i] += s2.c[i]; return *this; } CoefficientSpectrum operator+(const CoefficientSpectrum &s2) const { CoefficientSpectrum ret = *this; for (int i = 0; i < nSpectrumSamples; ++i) ret.c[i] += s2.c[i]; return ret; } CoefficientSpectrum operator-(const CoefficientSpectrum &s2) const { CoefficientSpectrum ret = *this; for (int i = 0; i < nSpectrumSamples; ++i) ret.c[i] -= s2.c[i]; return ret; } CoefficientSpectrum operator/(const CoefficientSpectrum &s2) const { CoefficientSpectrum ret = *this; for (int i = 0; i < nSpectrumSamples; ++i) ret.c[i] /= s2.c[i]; return ret; } CoefficientSpectrum operator*(const CoefficientSpectrum &sp) const { CoefficientSpectrum ret = *this; for (int i = 0; i < nSpectrumSamples; ++i) ret.c[i] *= sp.c[i]; return ret; } CoefficientSpectrum &operator*=(const CoefficientSpectrum &sp) { for (int i = 0; i < nSpectrumSamples; ++i) c[i] *= sp.c[i]; return *this; } CoefficientSpectrum operator*(Float a) const { CoefficientSpectrum ret = *this; for (int i = 0; i < nSpectrumSamples; ++i) ret.c[i] *= a; return ret; } CoefficientSpectrum &operator*=(Float a) { for (int i = 0; i < nSpectrumSamples; ++i) c[i] *= a; return *this; } friend inline CoefficientSpectrum operator*(Float a, const CoefficientSpectrum &s) { return s * a; } CoefficientSpectrum operator/(Float a) const { CoefficientSpectrum ret = *this; for (int i = 0; i < nSpectrumSamples; ++i) ret.c[i] /= a; Assert(!ret.HasNaNs()); return ret; } CoefficientSpectrum &operator/=(Float a) { for (int i = 0; i < nSpectrumSamples; ++i) c[i] /= a; return *this; } bool operator==(const CoefficientSpectrum &sp) const { for (int i = 0; i < nSpectrumSamples; ++i) if (c[i] != sp.c[i]) return false; return true; } bool operator!=(const CoefficientSpectrum &sp) const { return !(*this == sp); } bool IsBlack() const { for (int i = 0; i < nSpectrumSamples; ++i) if (c[i] != 0.) return false; return true; } friend CoefficientSpectrum Sqrt(const CoefficientSpectrum &s) { CoefficientSpectrum ret; for (int i = 0; i < nSpectrumSamples; ++i) ret.c[i] = std::sqrt(s.c[i]); return ret; } template <int n> friend inline CoefficientSpectrum<n> Pow(const CoefficientSpectrum<n> &s, Float e); CoefficientSpectrum operator-() const { CoefficientSpectrum ret; for (int i = 0; i < nSpectrumSamples; ++i) ret.c[i] = -c[i]; return ret; } friend CoefficientSpectrum Exp(const CoefficientSpectrum &s) { CoefficientSpectrum ret; for (int i = 0; i < nSpectrumSamples; ++i) ret.c[i] = std::exp(s.c[i]); return ret; } friend std::ostream& operator<<(std::ostream& os, const CoefficientSpectrum &s) { os << "["; for (int i = 0; i < nSpectrumSamples; ++i) { os << s.c[i]; if (i + 1 < nSpectrumSamples) os << ", "; } os << "]"; return os; } CoefficientSpectrum Clamp(Float low = 0, Float high = Infinity) const { CoefficientSpectrum ret; for (int i = 0; i < nSpectrumSamples; ++i) ret.c[i] = ::Clamp(c[i], low, high); return ret; } bool HasNaNs() const { for (int i = 0; i < nSpectrumSamples; ++i) if (std::isnan(c[i])) return true; return false; } bool Write(FILE *f) const { for (int i = 0; i < nSpectrumSamples; ++i) if (fprintf(f, "%f ", c[i]) < 0) return false; return true; } bool Read(FILE *f) { for (int i = 0; i < nSpectrumSamples; ++i) { double v; if (fscanf(f, "%lf ", &v) != 1) return false; c[i] = v; } return true; } Float &operator[](int i) { return c[i]; } Float operator[](int i) const { return c[i]; }
<<CoefficientSpectrum Public Data>> 
static const int nSamples = nSpectrumSamples;
protected: <<CoefficientSpectrum Protected Data>> 
Float c[nSpectrumSamples];
};

One CoefficientSpectrum constructor is provided; it initializes a spectrum with a constant value across all wavelengths.

<<CoefficientSpectrum Public Methods>>= 
CoefficientSpectrum(Float v = 0.f) { for (int i = 0; i < nSpectrumSamples; ++i) c[i] = v; }

<<CoefficientSpectrum Protected Data>>= 

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.

<<CoefficientSpectrum Public Methods>>+=  
CoefficientSpectrum &operator+=(const CoefficientSpectrum &s2) { for (int i = 0; i < nSpectrumSamples; ++i) c[i] += s2.c[i]; return *this; }

<<CoefficientSpectrum Public Methods>>+=  
CoefficientSpectrum operator+(const CoefficientSpectrum &s2) const { CoefficientSpectrum ret = *this; for (int i = 0; i < nSpectrumSamples; ++i) ret.c[i] += s2.c[i]; return ret; }

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.

<<CoefficientSpectrum Public Methods>>+=  
bool IsBlack() const { for (int i = 0; i < nSpectrumSamples; ++i) if (c[i] != 0.) return false; return true; }

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.

<<CoefficientSpectrum Public Methods>>+=  
friend CoefficientSpectrum Sqrt(const CoefficientSpectrum &s) { CoefficientSpectrum ret; for (int i = 0; i < nSpectrumSamples; ++i) ret.c[i] = std::sqrt(s.c[i]); return ret; }

It’s frequently useful to be able to linearly interpolate between two SPDs with a parameter  t .

<<Spectrum Inline Functions>>= 
inline Spectrum Lerp(Float t, const Spectrum &s1, const Spectrum &s2) { return (1 - t) * s1 + t * s2; }

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.

<<CoefficientSpectrum Public Methods>>+=  
CoefficientSpectrum Clamp(Float low = 0, Float high = Infinity) const { CoefficientSpectrum ret; for (int i = 0; i < nSpectrumSamples; ++i) ret.c[i] = ::Clamp(c[i], low, high); return ret; }

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.

<<CoefficientSpectrum Public Methods>>+=  
bool HasNaNs() const { for (int i = 0; i < nSpectrumSamples; ++i) if (std::isnan(c[i])) return true; return false; }

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.

<<CoefficientSpectrum Public Data>>= 
static const int nSamples = nSpectrumSamples;

<<CoefficientSpectrum Public Methods>>+= 
Float &operator[](int i) { return c[i]; }

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 c Subscript i alternatingly scaled the Gaussians and set their width,

upper S left-parenthesis lamda right-parenthesis equals sigma-summation Underscript i Overscript upper N Endscripts c Subscript 2 i Baseline normal e Superscript minus c Super Subscript 2 i plus 1 Superscript Baseline comma

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.