<p><em>(Disclaimer: I am not an expert in the terminology and practice of physically based rendering. This is more of a “physics” answer than a “raytracing” answer, and there may be elements of standard practice I am unaware of.)</em></p>
<p>You have discovered the effects of <a href="https://en.wikipedia.org/wiki/Color_temperature" rel="nofollow noreferrer">color temperature</a>.</p>
<p>When you “sample a wavelength uniformly between 360nm and 830nm” and then “average all those RGB values” you are making the assumption that the illumination of the scene has a uniform distribution of power per wavelength. No particular real-world light source has this flat <a href="https://en.wikipedia.org/wiki/Spectral_power_distribution" rel="nofollow noreferrer">spectral power distribution</a> (SPD), but we can refer to it as <a href="https://en.wikipedia.org/wiki/Standard_illuminant#Illuminant_E" rel="nofollow noreferrer">standard illuminant E</a>, which resembles (but is not, strictly) a color temperature of 5455 K.</p>
<p><a href="https://en.wikipedia.org/wiki/SRGB" rel="nofollow noreferrer">sRGB</a>, on the other hand, takes its <a href="https://en.wikipedia.org/wiki/White_point" rel="nofollow noreferrer">white point</a> as being that of <a href="https://en.wikipedia.org/wiki/Standard_illuminant#Illuminant_series_D" rel="nofollow noreferrer">standard illuminant D65</a>, 6500 K, which has a bluer appearance. Thus, by contrast, the rendering of your flat spectrum appears yellowish.</p>
<p>In order to resolve this inconsistency:</p>
<ol>
<li><p>Choose a SPD for your light source, and sample using that distribution when picking wavelengths. This will replace the yellowish color with whatever color you intentionally choose. If you pick D65, then you’re making the same lighting assumption as sRGB.</p>
<p>In principle, you can do the reverse, taking an illumination color specified in RGB and creating a SPD which would produce that RGB color. The problem with that strategy is that it is under-constrained — there are many spectra that produce the same color, but they interact with objects differently — the most obvious example is if your scene contains a rainbow produced from a prism; the intensity of the bands of the rainbow corresponds directly to the spectrum of the light. So, to actually set how your scene appears, you need to either define your lighting in terms of SPD, not only RGB, or pick a particular arbitrary mapping from RGB to SPD.</p>
</li>
<li><p>Convert that chosen SPD to RGB in exactly the same way you already did as your verification step, and use that as the RGB color of the light source when you are doing non-spectral (RGB) rendering. This will fix the problem where the background does not have the same white as the diamond. (Or, you can just use spectral rendering everywhere, but that of course requires more computation for the same image quality.)</p>
<p>These RGB colors <strong>will contain components which are less than zero</strong>, corresponding to monochromatic light which is out of the <a href="https://en.wikipedia.org/wiki/Gamut" rel="nofollow noreferrer">gamut</a> of sRGB (or other chosen RGB color space). Such components should <em>not</em> be clamped to be non-negative, because the out-of-gamut color of an individual ray will combine with other rays to form a color which is likely to be in-gamut (because it is more mixed with other colors and therefore less saturated).</p>
</li>
<li><p>Optionally, <a href="https://en.wikipedia.org/wiki/Color_balance" rel="nofollow noreferrer">white balance</a> the final image to match sRGB white. Whether this step is appropriate depends on your intent — whether you want the most accurate final visual stimulus as if the simulated scene were physically present (then don't balance), or whether you want the objects to appear naturally lit in context of other sRGB content (then do balance). You can also balance for artistic effect, of course.</p>
</li>
</ol>

CC BY-SA 4.0
2024-12-19T18:36:24.613Z
"This will fix the problem where the background does not have the same white as the diamond."
---
If I choose D65 as my distribution, it's rather the diamond that is going to match the background, correct?
Tom Clabault
CC BY-SA 4.0
2024-12-19T18:44:41.357Z
Also, is there a way to avoid "sampling" the distribution? This is expensive and error prone. I've seen [this implementation](https://github.com/nvpro-samples/nvpro_core/blob/master/nvvkhl/shaders/bsdf_functions.h#L496) on Github which uses a function to  convert directly from wavelength to RGB. That function what apparently fitted to already [contain the multiplication by the D65 illuminant] (that's what the doc of the function says). Can you make sense of that last bracketed part of the sentence?
Tom Clabault
CC BY-SA 4.0
2024-12-19T22:41:10.173Z
"If I choose D65 as my distribution, it's rather the diamond that is going to match the background, correct?" — Yes, that's correct. But if you hard-code just D65 then you will still have a problem if you want to work with non-white light sources. Starting with the SPD as the definition of the light’s color means everything is consistent. (I added a paragraph on why you shouldn’t start with RGB.)
Kevin Reid
CC BY-SA 4.0
2024-12-19T22:44:39.227Z
@TomClabault “Also, is there a way to avoid "sampling" the distribution?” — No, because each ray’s direction is wavelength-dependent, so you can’t use one ray for more than a single wavelength, so you’re always sampling in some sense.
Kevin Reid
CC BY-SA 4.0
2024-12-19T22:49:49.513Z
“a function to convert directly from wavelength to RGB” — That function is just combining the two operations “determine the intensity of D65 at that wavelength” and “find the RGB representation of this color of monochromatic light”. That’s a reasonable thing to do since they both depend on wavelength. The disadvantage is that it bakes in D65, so you can’t specify an arbitrary SPD for each light source, only change the RGB components of the result (which is basically doing the white-balance step I mentioned) and so you don't have control over the behavior within the scene of the full spectrum.
Kevin Reid
CC BY-SA 4.0
2024-12-20T07:50:58.603Z
On the sampling comment: by "sampling", did you mean importance sampling? i.e. sampling more often the wavelengths that have a high intensity? Or did you mean "reading the D65 intensity at that wavelength"?
Tom Clabault
CC BY-SA 4.0
2024-12-20T08:07:48.157Z
I think I understand better how things piece together but I can't get it to work yet. So to recap, (assuming D65): I pick wavelength uniformly in 360-830nm. Convert that wavelength to XYZ. Multiply that XYZ by D65's intensity at that wavelength. Convert to sRGB. Clamp to [0,1]. Average. Should this give me the value of the D65 illuminant in sRGB (which is going to be [1, 1, 1] since D65 is the white point of sRGB)? ----- I may have misunderstood how to account for the D65 SPD in all that.
Tom Clabault
CC BY-SA 4.0
2024-12-20T09:42:34.293Z
Let us [continue this discussion in chat](https://chat.stackexchange.com/rooms/156752/discussion-between-tom-clabault-and-kevin-reid).
Tom Clabault
CC BY-SA 4.0
2024-12-19T17:17:56.147Z
2024-12-20T20:06:58.893Z
2024-12-20T20:06:58.893Z
Kevin Reid
<p>I'm currently implementing dispersion in my RGB path tracer.</p>
<p><strong>How I do things:</strong></p>
<ul>
<li>When I hit a glass object, sample a wavelength uniformly between 360nm and 830nm and assign that wavelength to the ray</li>
<li>From now on, IORs of glass objects are now dependent on that wavelength. I compute the IORs for the sampled wavelength using Cauchy's equation</li>
<li>I sample reflections/refractions from glass objects using these new wavelength-dependent IORs</li>
<li>I tint the ray's throughput with the RGB color of that wavelength</li>
</ul>
<p><strong>How I compute the RGB color of a given wavelength:</strong></p>
<ul>
<li>Get the XYZ representation of that wavelength. I'm using the original <a href="https://cie.co.at/datatable/cie-1931-colour-matching-functions-2-degree-observer" rel="nofollow noreferrer">CIE 1931 2° CMF tables</a>. I simply index the wavelength in the table to get the XYZ value.</li>
<li>Directly convert that XYZ value from the table to sRGB with that matrix:</li>
</ul>
<pre><code>float r = 3.240479f * XYZ[0] + -1.537150f * XYZ[1] + -0.498535f * XYZ[2];
float g = -0.969256f * XYZ[0] + 1.875991f * XYZ[1] + 0.041556f * XYZ[2];
float b = 0.055648f * XYZ[0] + -0.204043f * XYZ[1] + 1.057311f * XYZ[2];
</code></pre>
<ul>
<li>Clamp the resulting RGB in [0, 1]</li>
</ul>
<p>With all this, I get a yellow tint on the diamond, any ideas why?</p>
<p><a href="https://i.sstatic.net/656EhGmB.webp" rel="nofollow noreferrer"><img src="https://i.sstatic.net/656EhGmB.webp" alt="The diamond should be completely transparent, not tinted slightly yellow like that" /></a></p>
<p>The diamond should be completely transparent, not tinted slightly yellow.</p>
<hr />
<p>Separately from all that, I also manually verified that:</p>
<ul>
<li>Taking evenly spaced wavelengths between 360nm and 830nm (spaced by 0.001)</li>
<li>Converting the wavelength to RGB (using the process described above)</li>
<li>Averaging all those RGB values</li>
<li>Yields <code>Average_RGB_of_wavelengths = [56.6118, 58.0125, 45.2291]</code>. Which is indeed yellow-ish.
From this simple test, I assume that my issue must be in my wavelength -&gt; RGB conversion, but where?</li>
</ul>
<p><a href="https://i.sstatic.net/8nbK5LTK.webp" rel="nofollow noreferrer"><img src="https://i.sstatic.net/8nbK5LTK.webp" alt="A white furnace test shows that the &quot;integration&quot; over wavelengths doesn't yield white but rather some yellow tint" /></a></p>
<p>A white furnace test shows that the &quot;integration&quot; over wavelengths doesn't yield white but rather some yellow tint. The non-tonemapped color of the sphere here is [57, 58 45] which matches what I found with my &quot;averaging test&quot; described above.</p>
<p>My code is <a href="https://github.com/TomClabault/HIPRT-Path-Tracer/blob/09e3a540ff1326cefdd8c3105a64701124c4764d/src/Device/includes/Dispersion.h#L395" rel="nofollow noreferrer">here</a> if needed.</p>

CC BY-SA 4.0
2024-12-19T17:32:54.613Z
I am confused. The conversion matrix presented is claimed to be 'sRGB', yet I've read conflicting materials. [Wikipedia](https://en.wikipedia.org/wiki/SRGB#The_sRGB_transfer_function_.28.22gamma.22.29) says this relationship is XYZ to linear RGB. If it is sRGB, I think the method is incorrect, since we can not accumulate radiance in sRGB space (and we should not clamp the linear RGB value before accumulation, either). It's obvious that sRGB applies gamma correction, which is non-linear. The order of "applying non-linear transform" and "accumulation" matters.
Enigmatisms
CC BY-SA 4.0
2024-12-19T18:25:09.037Z
@Enigmatisms Wikipedia says this is XYZ to linear sRGB though. Not XYZ to linear RGB. Second and third matrix equation [here](https://en.wikipedia.org/wiki/SRGB#Correspondence_to_CIE_XYZ_stimulus). I'm never gamma correcting anything in my tests (except the diamond screenshot which is tonemapped).
Tom Clabault
CC BY-SA 4.0
2024-12-19T23:03:55.747Z
“linear sRGB” is an imprecise phrase that, in my experience, usually refers to use of the primaries and white point specified by sRGB, but not the encoding (the nonlinear transform, loosely referred to also as “gamma”).
Kevin Reid
CC BY-SA 4.0
2024-12-20T07:08:09.027Z
So what do you mean? Did I have some misunderstanding regarding the color space? To me it appears that you are doing the accumulation in sRGB space, which seems strange to me.
Enigmatisms
CC BY-SA 4.0
2024-12-20T07:15:48.983Z
@Enigmatisms
I think Kevin corrected what I was trying to say. I'm accumulating in sRGB but I'm doing this in "linear sRGB" as Kevin explained. This means that I am accumulating linear values, without the non-linear gamma correction of the sRGB space so this should be fine?
Tom Clabault
<pathtracing><integral><spectral>
Wavelength to RGB conversion issue. Average RGB result is yellow tinted instead of white
Tom Clabault
# Wavelength to RGB conversion issue. Average RGB result is yellow tinted instead of white

(Asked by: username_0 on 2024-12-19)

I'm currently implementing dispersion in my RGB path tracer.

**How I do things:**

* When I hit a glass object, sample a wavelength uniformly between 360nm and 830nm and assign that wavelength to the ray
* From now on, IORs of glass objects are now dependent on that wavelength. I compute the IORs for the sampled wavelength using Cauchy's equation
* I sample reflections/refractions from glass objects using these new wavelength-dependent IORs
* I tint the ray's throughput with the RGB color of that wavelength

**How I compute the RGB color of a given wavelength:**

* Get the XYZ representation of that wavelength. I'm using the original [CIE 1931 2° CMF tables](https://cie.co.at/datatable/cie-1931-colour-matching-functions-2-degree-observer). I simply index the wavelength in the table to get the XYZ value.
* Directly convert that XYZ value from the table to sRGB with that matrix:

```
float r = 3.240479f * XYZ[0] + -1.537150f * XYZ[1] + -0.498535f * XYZ[2];
float g = -0.969256f * XYZ[0] + 1.875991f * XYZ[1] + 0.041556f * XYZ[2];
float b = 0.055648f * XYZ[0] + -0.204043f * XYZ[1] + 1.057311f * XYZ[2];

```

* Clamp the resulting RGB in [0, 1]

With all this, I get a yellow tint on the diamond, any ideas why?

[![The diamond should be completely transparent, not tinted slightly yellow like that](https://i.sstatic.net/656EhGmB.webp)](https://i.sstatic.net/656EhGmB.webp)

The diamond should be completely transparent, not tinted slightly yellow.

---

Separately from all that, I also manually verified that:

* Taking evenly spaced wavelengths between 360nm and 830nm (spaced by 0.001)
* Converting the wavelength to RGB (using the process described above)
* Averaging all those RGB values
* Yields `Average_RGB_of_wavelengths = [56.6118, 58.0125, 45.2291]`. Which is indeed yellow-ish.
  From this simple test, I assume that my issue must be in my wavelength -> RGB conversion, but where?

[![A white furnace test shows that the "integration" over wavelengths doesn't yield white but rather some yellow tint](https://i.sstatic.net/8nbK5LTK.webp)](https://i.sstatic.net/8nbK5LTK.webp)

A white furnace test shows that the "integration" over wavelengths doesn't yield white but rather some yellow tint. The non-tonemapped color of the sphere here is [57, 58 45] which matches what I found with my "averaging test" described above.

My code is [here](https://github.com/username_0/HIPRT-Path-Tracer/blob/09e3a540ff1326cefdd8c3105a64701124c4764d/src/Device/includes/Dispersion.h#L395) if needed.

**Tags:** pathtracing, integral, spectral

> **Comments:**
> **username_1**: I am confused. The conversion matrix presented is claimed to be 'sRGB', yet I've read conflicting materials. [Wikipedia](https://en.wikipedia.org/wiki/SRGB#The_sRGB_transfer_function_.28.22gamma.22.29) says this relationship is XYZ to linear RGB. If it is sRGB, I think the method is incorrect, since we can not accumulate radiance in sRGB space (and we should not clamp the linear RGB value before accumulation, either). It's obvious that sRGB applies gamma correction, which is non-linear. The order of "applying non-linear transform" and "accumulation" matters.  
> **username_0**: @username_1 Wikipedia says this is XYZ to linear sRGB though. Not XYZ to linear RGB. Second and third matrix equation [here](https://en.wikipedia.org/wiki/SRGB#Correspondence_to_CIE_XYZ_stimulus). I'm never gamma correcting anything in my tests (except the diamond screenshot which is tonemapped).  
> **username_2**: “linear sRGB” is an imprecise phrase that, in my experience, usually refers to use of the primaries and white point specified by sRGB, but not the encoding (the nonlinear transform, loosely referred to also as “gamma”).  
> **username_1**: So what do you mean? Did I have some misunderstanding regarding the color space? To me it appears that you are doing the accumulation in sRGB space, which seems strange to me.  
> **username_0**: @username_1 I think username_2 corrected what I was trying to say. I'm accumulating in sRGB but I'm doing this in "linear sRGB" as username_2 explained. This means that I am accumulating linear values, without the non-linear gamma correction of the sRGB space so this should be fine?  

---

## 1 answers

### Answer by username_2 on 2024-12-19. Score: 5 | Accepted answer

*(Disclaimer: I am not an expert in the terminology and practice of physically based rendering. This is more of a “physics” answer than a “raytracing” answer, and there may be elements of standard practice I am unaware of.)*

You have discovered the effects of [color temperature](https://en.wikipedia.org/wiki/Color_temperature).

When you “sample a wavelength uniformly between 360nm and 830nm” and then “average all those RGB values” you are making the assumption that the illumination of the scene has a uniform distribution of power per wavelength. No particular real-world light source has this flat [spectral power distribution](https://en.wikipedia.org/wiki/Spectral_power_distribution) (SPD), but we can refer to it as [standard illuminant E](https://en.wikipedia.org/wiki/Standard_illuminant#Illuminant_E), which resembles (but is not, strictly) a color temperature of 5455 K.

[sRGB](https://en.wikipedia.org/wiki/SRGB), on the other hand, takes its [white point](https://en.wikipedia.org/wiki/White_point) as being that of [standard illuminant D65](https://en.wikipedia.org/wiki/Standard_illuminant#Illuminant_series_D), 6500 K, which has a bluer appearance. Thus, by contrast, the rendering of your flat spectrum appears yellowish.

In order to resolve this inconsistency:

1. Choose a SPD for your light source, and sample using that distribution when picking wavelengths. This will replace the yellowish color with whatever color you intentionally choose. If you pick D65, then you’re making the same lighting assumption as sRGB.

   In principle, you can do the reverse, taking an illumination color specified in RGB and creating a SPD which would produce that RGB color. The problem with that strategy is that it is under-constrained — there are many spectra that produce the same color, but they interact with objects differently — the most obvious example is if your scene contains a rainbow produced from a prism; the intensity of the bands of the rainbow corresponds directly to the spectrum of the light. So, to actually set how your scene appears, you need to either define your lighting in terms of SPD, not only RGB, or pick a particular arbitrary mapping from RGB to SPD.
2. Convert that chosen SPD to RGB in exactly the same way you already did as your verification step, and use that as the RGB color of the light source when you are doing non-spectral (RGB) rendering. This will fix the problem where the background does not have the same white as the diamond. (Or, you can just use spectral rendering everywhere, but that of course requires more computation for the same image quality.)

   These RGB colors **will contain components which are less than zero**, corresponding to monochromatic light which is out of the [gamut](https://en.wikipedia.org/wiki/Gamut) of sRGB (or other chosen RGB color space). Such components should *not* be clamped to be non-negative, because the out-of-gamut color of an individual ray will combine with other rays to form a color which is likely to be in-gamut (because it is more mixed with other colors and therefore less saturated).
3. Optionally, [white balance](https://en.wikipedia.org/wiki/Color_balance) the final image to match sRGB white. Whether this step is appropriate depends on your intent — whether you want the most accurate final visual stimulus as if the simulated scene were physically present (then don't balance), or whether you want the objects to appear naturally lit in context of other sRGB content (then do balance). You can also balance for artistic effect, of course.

> **Comments:**
> **username_0**: "This will fix the problem where the background does not have the same white as the diamond." --- If I choose D65 as my distribution, it's rather the diamond that is going to match the background, correct?  
> **username_0**: Also, is there a way to avoid "sampling" the distribution? This is expensive and error prone. I've seen [this implementation](https://github.com/nvpro-samples/nvpro_core/blob/master/nvvkhl/shaders/bsdf_functions.h#L496) on Github which uses a function to  convert directly from wavelength to RGB. That function what apparently fitted to already [contain the multiplication by the D65 illuminant] (that's what the doc of the function says). Can you make sense of that last bracketed part of the sentence?  
> **username_2**: "If I choose D65 as my distribution, it's rather the diamond that is going to match the background, correct?" — Yes, that's correct. But if you hard-code just D65 then you will still have a problem if you want to work with non-white light sources. Starting with the SPD as the definition of the light’s color means everything is consistent. (I added a paragraph on why you shouldn’t start with RGB.)  
> **username_2**: @username_0 “Also, is there a way to avoid "sampling" the distribution?” — No, because each ray’s direction is wavelength-dependent, so you can’t use one ray for more than a single wavelength, so you’re always sampling in some sense.  
> **username_2**: “a function to convert directly from wavelength to RGB” — That function is just combining the two operations “determine the intensity of D65 at that wavelength” and “find the RGB representation of this color of monochromatic light”. That’s a reasonable thing to do since they both depend on wavelength. The disadvantage is that it bakes in D65, so you can’t specify an arbitrary SPD for each light source, only change the RGB components of the result (which is basically doing the white-balance step I mentioned) and so you don't have control over the behavior within the scene of the full spectrum.  
> **username_0**: On the sampling comment: by "sampling", did you mean importance sampling? i.e. sampling more often the wavelengths that have a high intensity? Or did you mean "reading the D65 intensity at that wavelength"?  
> **username_0**: I think I understand better how things piece together but I can't get it to work yet. So to recap, (assuming D65): I pick wavelength uniformly in 360-830nm. Convert that wavelength to XYZ. Multiply that XYZ by D65's intensity at that wavelength. Convert to sRGB. Clamp to [0,1]. Average. Should this give me the value of the D65 illuminant in sRGB (which is going to be [1, 1, 1] since D65 is the white point of sRGB)? ----- I may have misunderstood how to account for the D65 SPD in all that.  
> **username_0**: Let us [continue this discussion in chat](https://chat.stackexchange.com/rooms/156752/discussion-between-username_0-and-username_2).  

---

