Thoughts on making Cycles into a spectral renderer

I see.
Manuka uses Renderman compatible materials but I’m not sure how spectra are exposed.

The difference between exposing a ‘wavelength ramp’ and a wavelength value seems insignificant. I think I need to understand more about the evaluation of materials in order to contribute intelligently to the conversation, but having a wavelength ramp already gives you all the access that the ordinary wavelength value would if it was plugged into a RGB curves node.

Conceptually, dealing with wavelengths in shader nodes is identical to dealing with colours. At this (channel or wavelength), this is how light interacts with the surface. There’s nothing different that I can see that differs between existing materials and spectral ones. Plug a wavelength driven value into the diffuse colour slot and you have a spectral defined diffuse shader, that’s all there is to it.

The user speficies the intensity over the whole domain regardless of the sampling technique. Rather than defining red, green and blue intensities, it is specified continuously over the domain. This is no different to the result of RGB upsampling.

Just to set the record straight, I don’t use lookups to generate spectral curves from RGB. They are algorithmically generated to produce the one unique curve that has some optimal property, such as minimum slope squared. I do compare them to existing “look-ups” of various paints on my website, but only to assess how realistic the generated ones are.

I am pretty sure a pure primaries approach would be more idealized for the sake of upsampling.

I suspect that is what Manuka is doing. Centre based on dominant wavelength, arrive at some useful width around it, then sample via piecewise cubic or Bezier etc. The results would be more idealized and accurate.

Once three primaries are deduced, the other two compliments can be added to obtain a precise CIE xyY position.

The only downside is that spectral gamuts such as REC.2020 can’t be obtained.

Primaries seems like the way to go, but there might be some improvements that can be made, I’ll see if I can have a look into it.

Generating the primaries is the hard part, considering these requirements:

  • Values bounded by 0 and 1
  • sum of RGB spectra = 1*
  • spectra’s XYZ matches source spaces primary
  • spectrum must be as smooth as possible based on some smoothness metric

(*) this is where I think some cleverness can be done. You can remove this limitation by mixing between the primaries and a separate white spectrum as values approach white. In this way, only the sum of any 2 primaries must be below 1.

Done.
http://scottburns.us/fast-rgb-to-spectrum-conversion-for-reflectances/
Published just today.

2 Likes

Well, that didn’t take long.

Are the primaries you created based on sRGB? If so, those spectra look perfect for this application, and doesn’t even need to be blended with a white in order to be bounded by 1

Yes, based on sRGB. But when combining the primaries, you should use linear rgb (un-companded sRGB) to preserve the linearity (and additivity) of the underlying CMFs (color matching functions).

This is true. This is why I feel having the ability to define materials spectrally is critical to the whole system. Manuka seems to agree. If the colour they want to represent lays outside of REC.709, they should be able to create the spectrum themselves. There’s just the one natural restriction of saturation vs luminosity in reflectances, but that’s a natural consequence of the system, not something that has to be enforced.

Yep, that’s what I’m doing already, so I am going to try swapping out the spectra and giving yours a try. Could I perhaps get the spectra in a more accessible format such as comma separated list?

Conveniently your spectra start and end at the same values I used for my implementation, but could this be extended to work with the far reds? There isn’t a whole lot of contribution there but having it undefined in that region leads to some difficulties.

Ask and ye shall receive…

rho_R
0.021592459, 0.020293111, 0.021807906, 0.023803297, 0.025208132, 0.025414957, 0.024621282, 0.020973705, 0.015752802, 0.01116804, 0.008578277, 0.006581877, 0.005171723, 0.004545205, 0.00414512, 0.004343112, 0.005238155, 0.007251939, 0.012543656, 0.028067132, 0.091342277, 0.484081092, 0.870378324, 0.939513128, 0.960926994, 0.968623763, 0.971263883, 0.972285819, 0.971898742, 0.972691859, 0.971734812, 0.97234454, 0.97150339, 0.970857997, 0.970553866, 0.969671404

rho_G
0.010542406, 0.010878976, 0.011063512, 0.010736566, 0.011681813, 0.012434719, 0.014986907, 0.020100392, 0.030356263, 0.063388962, 0.173423837, 0.568321142, 0.827791998, 0.916560468, 0.952002841, 0.964096452, 0.970590861, 0.972502542, 0.969148203, 0.955344651, 0.892637233, 0.5003641, 0.116236717, 0.047951391, 0.027873526, 0.020057963, 0.017382174, 0.015429109, 0.01543808, 0.014546826, 0.015197773, 0.014285896, 0.015069123, 0.015506263, 0.015545797, 0.016302839

rho_B
0.967865135, 0.968827912, 0.967128582, 0.965460137, 0.963110055, 0.962150324, 0.960391811, 0.958925903, 0.953890935, 0.925442998, 0.817997886, 0.42509696, 0.167036273, 0.078894327, 0.043852038, 0.031560435, 0.024170984, 0.020245519, 0.01830814, 0.016588218, 0.01602049, 0.015554808, 0.013384959, 0.012535491, 0.011199484, 0.011318274, 0.011353953, 0.012285073, 0.012663188, 0.012761325, 0.013067426, 0.013369566, 0.013427487, 0.01363574, 0.013893597, 0.014025757

The extend the primaries into the far red, just repeat the last value in each set as many times as needed.

Pardon the excessive number of digits. The last half or so of each is just noise, but I didn’t feel like editing each one by hand.:persevere:

1 Like

Thanks for that! How many digits do you think are useful? 0.xxxx? I’ll process them with a regex and post it back here

I guess I’d experiment. Add the three primaries together as you eliminate digits and decide how far from 1.00000 you’re willing to go. The underlying CMF I used had five signif digits. But why not just use all the digits in your software? It’s not like you’ll save bytes by truncating them and the extra digits won’t do any harm.

That’s true, the issue is, on this end, I also have to enter everything by hand. I’ve tried setting up a keyboard macro to copy it in but blender has a strange way of interpreting CTRL, so I’ve had to do everything by hand…

I wouldn’t call this idealized.

Idealized would mean taking the dominant wavelength and centering up on it with a perfectly symmetrical[1] sampling.

Should be more than possible.

[1] Relative to the spectral locus hull, not perfectly balanced spectral ratios.

Already done a few tests. Works fine.

Just haven’t arrived at a method to solve balancing the primaries such that the reference space primaries are precisely located.

My math is too weak on that front.

I consider “idealized” to be the result of some underlying optimization. Nature optimizes all the time. Equilibrium in structural mechanics can be thought of as the minimum of a total potential. I see no reason to why “perfectly symmetrical” would be a worthy goal, when there is nothing symmetrical about the physiology of human color perception.

I think that is the issue with this approach. As the primaries approach spectral primaries, the resultant spectra become further and further away from the goals of ‘bounded by 1 and adding up to 1 across the spectrum’. It neatly solves (if the spectra are positioned correctly) the XYZ locations of the primaries, but doesn’t fit the others.

Idealized in that you can generate perfectly smooth primaries, with compliments included, for any reference space, with fully tunable spectral widths to allow for a parametric approach.

I see no reason why this would be the case.

Reflectance sums aren’t that complex.

If, for example, you’re trying to create REC.2020 primaries, (which are already spectral), in order to get it to have the same reflected power as a perfect reflector (what most people would expect a perfect white material to behave like) the reflectance spectrum would have to be much greater than 1 at the three primary wavelengths, and would equal 0 everywhere else.

REC.709 seems about the largest space which can satisfy all these rules, though I haven’t confirmed that.