# Computing sRGB color from SPDs

I'm trying to write my own spectral path tracer and a bit stuck with converting computed SPDs to LDR RGB values. What I'm doing right now is convert sampled SPDs (in W/m^-1) to XYZ using CIE 1931 color matching functions:

Vec3 xyz(0, 0, 0);
float norm = 0;

for (int i = 0; i < SPD_SAMPLE_COUNT; ++i) {
const float wavelength =
lerp(MIN_WAVELENGTH, MAX_WAVELENGTH, i / SPD_SAMPLE_COUNT);
const Vec3 cieMatchingFunctionValue = cie1931(wavelength);
xyz += cieMatchingFunctionValue * spd[i];
norm += cieMatchingFunctionValue.y;
}

xyz /= norm;


Here implementation of cie1931 is an anylitic approximation from Wyman et al.

Then XYZ to xyY (i.e. chromaticity and luminance), normalise luminance to be in [0, 1] range across the whole image (for now by dividing my max luminance just to keep it simple):

float maxLuminance = 0;

for (auto &xyz : image) {
maxLuminance = std::max(maxLuminance, xyz.y);
}

for (auto &xyz : image) {
Vec2 chromaticity = Vec2(xyz.x, xyz.y) / (xyz.x + xyz.y + xyz.z);
float luminance = xyz.y / maxLuminance;

// then to "scaled" XYZ and RGB, see below
}


Also I've tried Reinhard tone mapping operator:

float logSum = 0;

for (auto &xyz : image) {
logSum += std::log(xyz.y);
}

float logAvgLuminance = std::exp(logSum / image.size());

for (auto &xyz : image) {
Vec2 chromaticity = Vec2(xyz.x, xyz.y) / (xyz.x + xyz.y + xyz.z);
const float a = 0.18; // constant from Reinhard's paper.
float luminance = a * xyz.y / logAvgLuminance;
luminance /= luminance + 1;

// then to "scaled" XYZ and RGB, see below
}


Then back to XYZ, than to linear RGB (wia this transform):

for (auto &xyz : image) {
// XYZ to chromaticity and "scaled luminance", see above

Vec3 scaledXyz(
chromaticity.x * luminance / chromaticity.y,
luminance,
(1 - chromaticity.x - chromaticity.y) * luminance / chromaticity.y
);

float r = dot(Vec3( 3.2406, -1.5372, -0.4986), scaledXyz);
float g = dot(Vec3(-0.9689,  1.8758,  0.0415), scaledXyz);
float b = dot(Vec3( 0.0557, -0.2040,  1.0570), scaledXyz);

// Output RGB, i.e. std::cout << Vec3(r, g, b);
}


After that I still get RGB values outside of [0..1]. What am I doing wrong? It seems that "max" luminance should depend on chromaticities of the color. Is there an algorithm to account for that?

• Hello and welcome to the site. You are using quite a few equations there which means not many people know all of them well enough to answer. I suggest you flesh out the question more by adding the relevant equations and/or code. Jan 23, 2019 at 19:54
• @bernie Thank for that comment! Added code to illustrate what I've tried so far. Jan 24, 2019 at 10:53

• To preserve both hue and luminance, you can edit the xy coordinates before calculating scaledXyz. If the initial xy coordinates are outside the sRGB triangle, find the line segment from the xy coordinates to the white point (e.g. D65); then find the intersection of that line segement with the sRGB triangle. That's the closest sRGB point that has the same luminance and hue as the original point, but lower saturation. (The vertices of the sRGB triangle can be obtained by converting RGB(1, 0, 0), RGB(0, 1, 0), and RGB(0, 0, 1) to CIE xy space.)