5.3 RGBSpectrum Implementation

The RGBSpectrum implementation here represents SPDs with a weighted sum of red, green, and blue components. Recall that this representation is ill defined: given two different computer displays, having them display the same RGB value won’t cause them to emit the same SPD. Thus, in order for a set of RGB values to specify an actual SPD, we must know the monitor primaries that they are defined in terms of; this information is generally not provided along with RGB values.

The RGB representation is nevertheless convenient: almost all 3D modeling and design tools use RGB colors, and most 3D content is specified in terms of RGB. Furthermore, it’s computationally and storage efficient, requiring just three floating-point values to represent. Our implementation of RGBSpectrum inherits from CoefficientSpectrum, specifying three components to store. Thus, all of the arithmetic operations defined earlier are automatically available for the RGBSpectrum.

<<Spectrum Declarations>>+= 
class RGBSpectrum : public CoefficientSpectrum<3> { public: <<RGBSpectrum Public Methods>> 
RGBSpectrum(Float v = 0.f) : CoefficientSpectrum<3>(v) { } RGBSpectrum(const CoefficientSpectrum<3> &v) : CoefficientSpectrum<3>(v) { } RGBSpectrum(const RGBSpectrum &s, SpectrumType type = SpectrumType::Reflectance) { *this = s; } static RGBSpectrum FromRGB(const Float rgb[3], SpectrumType type = SpectrumType::Reflectance) { RGBSpectrum s; s.c[0] = rgb[0]; s.c[1] = rgb[1]; s.c[2] = rgb[2]; return s; } void ToRGB(Float *rgb) const { rgb[0] = c[0]; rgb[1] = c[1]; rgb[2] = c[2]; } const RGBSpectrum &ToRGBSpectrum() const { return *this; } void ToXYZ(Float xyz[3]) const { RGBToXYZ(c, xyz); } static RGBSpectrum FromXYZ(const Float xyz[3], SpectrumType type = SpectrumType::Reflectance) { RGBSpectrum r; XYZToRGB(xyz, r.c); return r; } Float y() const { const Float YWeight[3] = { 0.212671f, 0.715160f, 0.072169f }; return YWeight[0] * c[0] + YWeight[1] * c[1] + YWeight[2] * c[2]; } static RGBSpectrum FromSampled(const Float *lambda, const Float *v, int n) { <<Sort samples if unordered, use sorted for returned spectrum>> 
if (!SpectrumSamplesSorted(lambda, v, n)) { std::vector<Float> slambda(&lambda[0], &lambda[n]); std::vector<Float> sv(&v[0], &v[n]); SortSpectrumSamples(&slambda[0], &sv[0], n); return FromSampled(&slambda[0], &sv[0], n); }
Float xyz[3] = { 0, 0, 0 }; for (int i = 0; i < nCIESamples; ++i) { Float val = InterpolateSpectrumSamples(lambda, v, n, CIE_lambda[i]); xyz[0] += val * CIE_X[i]; xyz[1] += val * CIE_Y[i]; xyz[2] += val * CIE_Z[i]; } Float scale = Float(CIE_lambda[nCIESamples-1] - CIE_lambda[0]) / Float(CIE_Y_integral * nCIESamples); xyz[0] *= scale; xyz[1] *= scale; xyz[2] *= scale; return FromXYZ(xyz); }
};

<<RGBSpectrum Public Methods>>= 
RGBSpectrum(Float v = 0.f) : CoefficientSpectrum<3>(v) { } RGBSpectrum(const CoefficientSpectrum<3> &v) : CoefficientSpectrum<3>(v) { }

Beyond the basic arithmetic operators, the RGBSpectrum needs to provide methods to convert to and from XYZ and RGB representations. For the RGBSpectrum these are trivial. Note that FromRGB() takes a SpectrumType parameter like the SampledSpectrum instance of this method. Although it’s unused here, the FromRGB() methods of these two classes must have matching signatures so that the rest of the system can call them consistently regardless of which spectral representation is being used.

<<RGBSpectrum Public Methods>>+=  
static RGBSpectrum FromRGB(const Float rgb[3], SpectrumType type = SpectrumType::Reflectance) { RGBSpectrum s; s.c[0] = rgb[0]; s.c[1] = rgb[1]; s.c[2] = rgb[2]; return s; }

Similarly, spectrum representations must be able to convert themselves to RGB values. For the RGBSpectrum, the implementation can sidestep the question of what particular RGB primaries are used to represent the spectral distribution and just return the RGB coefficients directly, assuming that the primaries are the same as the ones already being used to represent the color.

<<RGBSpectrum Public Methods>>+=  
void ToRGB(Float *rgb) const { rgb[0] = c[0]; rgb[1] = c[1]; rgb[2] = c[2]; }

All spectrum representations must also be able to convert themselves to an RGBSpectrum object. This is again trivial here.

<<RGBSpectrum Public Methods>>+=  
const RGBSpectrum &ToRGBSpectrum() const { return *this; }

The implementations of the RGBSpectrum::ToXYZ(), RGBSpectrum::FromXYZ(), and RGBSpectrum::y() methods are based on the RGBToXYZ() and XYZToRGB() functions defined above and are not included here.

To create an RGB spectrum from an arbitrary sampled SPD, FromSampled() converts the spectrum to XYZ and then to RGB. It evaluates the piecewise linear sampled spectrum at 1-nm steps, using the InterpolateSpectrumSamples() utility function, at each of the wavelengths where there is a value for the CIE matching functions. It then uses this value to compute the Riemann sum to approximate the XYZ integrals.

<<RGBSpectrum Public Methods>>+= 
static RGBSpectrum FromSampled(const Float *lambda, const Float *v, int n) { <<Sort samples if unordered, use sorted for returned spectrum>> 
if (!SpectrumSamplesSorted(lambda, v, n)) { std::vector<Float> slambda(&lambda[0], &lambda[n]); std::vector<Float> sv(&v[0], &v[n]); SortSpectrumSamples(&slambda[0], &sv[0], n); return FromSampled(&slambda[0], &sv[0], n); }
Float xyz[3] = { 0, 0, 0 }; for (int i = 0; i < nCIESamples; ++i) { Float val = InterpolateSpectrumSamples(lambda, v, n, CIE_lambda[i]); xyz[0] += val * CIE_X[i]; xyz[1] += val * CIE_Y[i]; xyz[2] += val * CIE_Z[i]; } Float scale = Float(CIE_lambda[nCIESamples-1] - CIE_lambda[0]) / Float(CIE_Y_integral * nCIESamples); xyz[0] *= scale; xyz[1] *= scale; xyz[2] *= scale; return FromXYZ(xyz); }

InterpolateSpectrumSamples() takes a possibly irregularly sampled set of wavelengths and SPD values left-parenthesis lamda Subscript i Baseline comma v Subscript i Baseline right-parenthesis and returns the value of the SPD at the given wavelength lamda , linearly interpolating between the two sample values that bracket lamda . The FindInterval() function defined in Appendix A performs a binary search through the sorted wavelength array lambda to find the interval containing l.

<<Spectrum Method Definitions>>+=  
Float InterpolateSpectrumSamples(const Float *lambda, const Float *vals, int n, Float l) { if (l <= lambda[0]) return vals[0]; if (l >= lambda[n - 1]) return vals[n - 1]; int offset = FindInterval(n, [&](int index) { return lambda[index] <= l; }); Float t = (l - lambda[offset]) / (lambda[offset+1] - lambda[offset]); return Lerp(t, vals[offset], vals[offset + 1]); }