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.
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.
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.
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.
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 and returns the value
of the SPD at the given wavelength , linearly interpolating
between the two sample values that bracket .
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]);
}