Spectral Rendering

Up to this point Psychopath has represented color with RGB values. This is a convenient way to handle color in a renderer because most color input to the renderer is going to be in some kind of RGB format (textures, hand-specified colors, etc.) and most color output the renderer produces is going to be in some kind of RGB format as well (final rendered images). So it makes sense that if your input is RGB, and your output is RGB, probably everything in-between should be RGB as well.

But RGB isn't how real light works. Real light is composed of a spectrum of wavelengths, much the same way that sound is. RGB is a bit like sampling audio at only three frequencies.

Even so, rendering with RGB usually works fine because human color vision is also more-or-less RGB. But there are side effects of spectrums that, although their causes are not visible to the naked eye, their results are. Fluorescent lighting is a good example: the effect it has on colors can't be accurately reproduced in RGB without also tweaking the colors of the materials. Prisms and rainbows are another example.

Admittedly, these examples are largely corner cases that can be faked, and I'm designing Psychopath with animation and VFX in mind—both of which are cases where artistic control is more important than strict accuracy. But nevertheless, being able to render spectral effects is cool! And it might come in handy when trying to match scene lighting for a VFX shot in a fluorescent-lit office building.

So I decided to make Psychopath a spectral renderer.

There are two broad approaches to spectral rendering: binning, and Monte Carlo spectral sampling.

Binning is basically RGB on crack. You can think of RGB color as three spectral samples: one in the reddish part of the spectrum, one in the greenish part, and one in the blueish part. Binning formalizes and generalizes this concept. Instead of three vaguely defined samples, you have N very specific ranges or "bins" over the visible spectrum. Anywhere from 9 to 30 bins is common with this approach.

Monte Carlo spectral sampling takes a completely different approach. Instead of dividing up the spectrum into predefined regions, each light path gets a randomly assigned wavelength. Then as more and more paths are traced, more and more points on the spectrum get sampled, and the image converges to the correct colors in much the same way as everything else in a Monte Carlo path tracer.

Both approaches have their pros and cons.

The biggest pro of the binning approach is that colors still have a clear canonical representation within the renderer: every color in the renderer is specified by a set of N values. Simple, easy, obvious.

In the Monte Carlo approach, colors don't have a single canonical way to be represented. However, that is also one of the pros of the Monte Carlo approach: you can have many different ways to represent color, and as long as they can all be sampled spectrally, they can all coexist effortlessly within the same renderer. Monte Carlo sampling is also more accurate than binning. For example, if you rendered a scene with a prism, the resulting image from a binning approach would only have as many colors as there are bins. But a Monte Carlo approach would, when converged, reveal all the colors of the rainbow in a smooth gradient. Finally, the Monte Carlo approach takes up less memory (only one value instead of 9-30), and might be faster depending on a variety of factors.

In the end, I decided to go with the Monte Carlo approach for Psychopath. The combination of being more accurate and taking less memory was very attractive. It also strikes me as more elegant: since I'm already solving the rest of the rendering equation with (Quasi-) Monte Carlo, why not just extend that into the color spectrum dimension as well?

However, the Monte Carlo approach has one big down side: color noise. Below is a spectral render of a solid medium-gray background with one sample per pixel.

One sample spectral render

As you can see, the resulting render is not a good representation of the underlying color in the scene. Instead of gray, we get rainbow. Even at 16 samples per pixel, the color noise is still quite visible:

Sixteen sample spectral render

With this approach it takes a lot of samples to make the color noise go away. And really, we aren't using our ray tracing very efficiently here: most visual phenomenon in the real world are not significantly spectral in nature, and yet we're taking the time to trace an entire light path for each single spectral sample.

There is a clever solution to this problem in the paper "Hero Wavelength Spectral Sampling" by Wilkie et al. The basic idea is simple: instead of just a single wavelength per light path, have several. One of the wavelengths is randomly chosen (the "hero" wavelength), and the others are evenly spaced apart from that to cover the visible spectrum.

The result of this method with four wavelengths per light path at one sample per pixel looks like this for the gray background:

One sample hero spectral render

And looks like this at sixteen samples per pixel:

Sixteen sample hero spectral render

The difference is enormous. Instead of unrecognizable color at one sample per pixel, we can clearly make out the intended color, albeit with noise. And at sixteen samples per pixel the color noise is all but gone.

The hero wavelength paper is very much worth a read, because it also covers how to properly handle cases with strong spectrally-dependant effects. But even just this simple idea makes a big difference, and makes Monte Carlo spectral sampling practical.

One last practical matter: given that artists aren't going to manually specify an entire spectrum for every color they use, the renderer needs a way to convert RGB input into a matching light spectrum automatically. There are a few approaches out there to do this, but the one I'm using in Psychopath is from the recent paper "Physically Meaningful Rendering Using Tristimulus Colours" by Meng et al. The awesome thing about the algorithm they develop in the paper is that it accurately covers (almost) the entire XYZ color space. This is significant because that means it can be used for converting colors from any color space: just convert to XYZ first. This is not the case for any of the other approaches I'm aware of.