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 '19 at 19:54
• @bernie Thank for that comment! Added code to illustrate what I've tried so far. Jan 24 '19 at 10:53