# Does a constant reflection over the light spectrum lead to grayscale value?

I'm currently fighting with the spectrum->RGB color conversions, my algorithm seems to make an error somewhere, e.g. I get values >1 for some spectral responses.

Now there is a way to calculate an rgb value for normal incidence from index of refraction:

F(0°) = $(\frac{n-1}{n+1})^2$

Where n is the index of refraction (Real-Time Rendering 3rd, p.234 f.) And if $n$ is constant, you can use it for all RGB channels.

However, if I calculate the spectral value with my spectrum->RGB conversion and just use a constant $n$, I get different values for R, G and B.

Specifically, $n = 1.52$ would lead to $RGB_{single} = (0.04257999496;0.04257999496;0.04257999496)$ whereas my program calculates $RGB_{spectrum} = 0.054223;0.042692;0.041095$ (evaluated at $390nm - 780nm$ with $\Delta\lambda = 10nm$).

So my question now is: would a constant index of refraction really lead to a grayscale (i.e. all channels the same) value? Because the conversion functions depend on the CIE functions, taking the integral of those and clearly the integrals are different for $x, y, z$.

Also: since I seem to have a problem with my program in the first place: is there a step by step tutorial for creating the conversion function somewhere?

Edit 01.09.2017 10:25 After being asked to share my code, here goes (in a somewhat top-down approach): First I read the CIE functions from file and store them in a std::vector> with wavelength as key and function value as value. the vector just encapsulates x, y and z independently.

std::vector<std::map<float, float>> cieFunctions = readCieFunctions(ciePath);


Now cieFunctions.at(0) is the x function and so forth. I read in the spectral values basically the same way, into a map with key being the wavelength and value being the reflectivity. Here I take care to match the units, wavelength as $nm$ and reflectivity in values $\in [0, 1]$ (rather than $\in [0, 100]$%).

Having reached this, I call my calculateRgbValue(std::map<float, float>spectralValue) function, where I use the matrix for a D65 white point monitor. I calculate the coordinate, divide it by the integral of Y over the visible wavelength spectrum (constant taken from Physically Based Rendering book) and use that in a multiplcation with said matrix to get the RGB value:

QString SpectralToRGBConverter::calculateRgbValue(std::map<float, float> spectralValues)
{
QString result;
Vec3 rgb;

Mat3 conversionMatrix(
3.240479, -1.537150, -0.498535,
-0.969256, 1.875992, 0.041556,
0.055648, -0.204043, 1.057311);

float X = calcCieCoordinate(spectralValues, cieFunctions.at(0));
float Y = calcCieCoordinate(spectralValues, cieFunctions.at(1));
float Z = calcCieCoordinate(spectralValues, cieFunctions.at(2));

float yIntegral = 106.856895;   //from Phyiscally Based Rendering 3rd, p. 325

Vec3 XYZ(X, Y, Z);
XYZ /= yIntegral;

rgb = conversionMatrix*XYZ;

result = (std::to_string(rgb.x) + ";" + std::to_string(rgb.y) + ";" + std::to_string(rgb.z)).c_str();
return result;
}


The idea of calculating the cie coordinate is best described with the mathematical formula: $coord = \sum_{i=0}^{n-1}x_i c_i \Delta\lambda$ where $n$ is the number of samples in the spectrum, $x_i$ is the $i-th$ cie function value for the wavelength of the $i-th$ sample and $c_i$ is the $i-th$ function value of the spectrum and $\Delta\lambda$ is the stride between two samples $\Delta\lambda = \lambda_{i+1} - \lambda_i$

float SpectralToRGBConverter::calcCieCoordinate(std::map<float, float> spectralValues, std::map<float, float> function)
{
//http://www.brucelindbloom.com/index.html?Eqn_Spect_to_XYZ.html
//Coordinate = Int_lambda function(lambda) * spectralValue(lambda) dlambda
//for discrete values:
//Coordinate = Sum_i function(i) * spectalValue(i) * Delta lambda
//where Delta lambda is the spacing between two measurements
float deltaLambda;// in nm
auto first = spectralValues.begin();
auto second = std::next(first, 1);

if (ui.radioButtonMicroMeter->isChecked())
{
deltaLambda = (second->first - first->first) * 1000;
}
else
{
deltaLambda = second->first - first->first;
}

float coordinate = 0;
for (auto pairWavelengthReflectance : spectralValues)
{
coordinate += getFunctionValue(function, pairWavelengthReflectance.first) * pairWavelengthReflectance.second * deltaLambda;
}

return coordinate;
}


getFunctionValue is basically a nearest neighbor sampling:

float SpectralToRGBConverter::getFunctionValue(std::map<float, float> function, float x)
{
float conversionFactorWaveLength;
if (ui.radioButtonMicroMeter->isChecked())
{
//if x is not in nm like function.first
conversionFactorWaveLength = 1000;
}
else
{
conversionFactorWaveLength = 1;
}

float conversionFactorReflectivityScale;
if (ui.radioButtonPercentage->isChecked())
{
//if the reflectivity is given in percentages [0, 100] rather than proportions [0, 1]
conversionFactorReflectivityScale = 0.01;
}
else
{
conversionFactorReflectivityScale = 1;
}

float xLookUp = x * conversionFactorWaveLength;
float previous = 0;
for (auto pairXY : function)
{   //maps are sorted
float current = pairXY.first;
if (current == xLookUp)
{
return function[current] * conversionFactorReflectivityScale;
}
else
if (current > xLookUp)
{
//get the closest value to x
float min;
if (xLookUp - previous < current - xLookUp)
{
min = previous;
}
else
{
min = current;
}
return function[min] * conversionFactorReflectivityScale;
}
previous = current;
}
return -1;
}

• Are you able to share your code? Sep 1, 2017 at 6:43
• I will try to make it readable later and then share it (or at least the important parts). It is (or should be) what they do in Physically Based Rendering Third (p.324), if by chance you have that book.
– Tare
Sep 1, 2017 at 7:57
• No the result is probably not grayscale. What happens is there is a very sophisticated white balance in the system. I mean put tinted glasses on and pretty soon you nolonger see the tint. Sep 1, 2017 at 17:19
• @joojaa thanks but I was looking more for a proof, i.e. someone who has a working spectrum->rgb converter who would calculate the rgb response you get for using an $ior = 1.52 \iff reflection intensity = 0.04257999496$ on the wavelengths $390-780nm$
– Tare
Sep 2, 2017 at 13:46

## 1 Answer

I know this is an old question. I did write a spectrum->rgb converter, however all of its relevant parts for this question come from pbr-book source code.

Specifically though, the answer is no. Constant index of refraction (which is actually a wave length dependent number, so taking it as a constant is more of an approximation really) does not produce a grayscale color value. Since the final spectral distribution of your object, aka its color, is determined by the spectral distribution of the light source and the spectral reflectance factor of the surface. If your surface is transmissive, there should also be a spectral transmittance factor. Though IOR is constant the value of from the factor and the light source should produce enough change to create a perceived color difference.

• Thanks for the hint. I was calculating albedo values for textures though, so this would be "how much of light at a wavelength / colour channel would be reflected, if there was any?" rather than "how much do I see reflected in actual lighting conditions?". If you were to use an actual lighting, I guess you'd need to use pure white light and assume a perfect diffuse reflection from the object with the calculated albedo (i.e. no transmittance, no specular reflection). I know that a constant IOR is an ideal assumption though :) for some materials it's close enough in visible light wavelengths
– Tare
Jan 7, 2021 at 7:13
• Aah okay then. Well spectral reflectance factor is also a spectral power distribution if you know that material property, you should be able to produce albedo values with color matching functions of CIE using standard illuminants spd. Jan 7, 2021 at 7:38