Blog Archive About

Psychopath Renderer

a slightly psychotic path tracer

2015 - 07 - 16

Subdivision Surfaces

I recently implemented Catmull-Clark subdivision surfaces in Psychopath.

One of my goals with Psychopath is for all curved surfaces to actually render as curved surfaces rather than as tessellated triangle meshes. To accomplish this for subdivision surfaces I am leveraging OpenSubdiv to pre-process the cage mesh into b-spline patches. Psychopath then converts the b-spline patches into bezier patches which it already knows how to render.

The end result is this:

Pontiac GTO '67

(Model by 'thecali', available on BlendSwap—please note that the version in the render above has some parts missing, as some bits were modeled with curves which Psychopath can't render yet.)

In other words, I can finally render interesting models! Yay! Most things are modeled with Catmull-Clark subdivision surfaces these days, so this a big step.

The only down-side so far is that OpenSubdiv seems to be a bit on the slow side when converting the mesh to b-spline patches—it takes around 45 seconds for this model. It's possible I'm not using the library optimally. It's certainly something I'll need to investigate. But I'm quite pleased for now!

2015 - 07 - 13

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.

2015 - 06 - 03

Basic Shading System

Psychopath now has a basic shading system! Yay!

As you can see in this artistically-awful image, different objects can now have different shaders assigned to them:

Multiple Materials

This was actually fairly easy to get up and running. It just required some design work. I did indeed go with shading being done at intersection time, as described in my last post.

This basic shading system does not use OSL, and doesn't even support texturing of any kind. Each shader just produces a single fixed BSDF. But building this basic version of a shading system helped me work out the machinery behind how shaders get assigned to objects and how they are processed during rendering.

One of the cool features of this shading system is that shaders are assigned hierarchically. Any node in the scene graph (more-or-less) can have a shader assigned to it, and all nodes under such a node will inherit that shader—unless they themselves have been assigned a different shader. This makes it easy to assign a single shader to the entire scene, for example.

2015 - 05 - 26

Designing a Shading System

One of Psychopath's current limitations is that every surface has to have the same shader. This is obviously not workable if it's going to be used as a "real" renderer, so I need to design a shading system that allows different shaders to be assigned to different objects.

Eventually I want to use Open Shading Language for shading in Psychopath, but as a first pass I'm going to do something a little simpler to get my feet wet. I've never written a shading system, after all.

But even so, since Psychopath traces many rays at once there are actually some interesting choices to make here which will carry over to how Psychopath utilizes OSL. There are two broad approaches I can take:

  1. Do the shading when a ray hits something. This would require storing the result of the shading in the intersection structure, which would be a BSDF closure.

  2. Do the shading after all of the ray intersection tests have been completed. This would require storing all information necessary to do the shading in the intersection structure, which would be things such as a reference to the shader and any required differential geometry data.

The biggest benefit of the first approach is that it keeps the architecture a bit simpler. Displacement shaders will have to be evaluated during ray traversal anyway, so the first approach would keep all of the shading code in the same general part of Psychopath.

The biggest benefit of the second approach is that the shading evaluations can be reordered to maximize e.g. coherent texture access.

I've been going back-and-forth trying to decide which approach to take, but I think I've finally settled on the first approach. The first approach gives me more flexibility in how much data I store, and since Psychopath is using breadth first ray tracing there are already strong guarantees about accessing shaders and their associated data in a coherent way.

The main problem with the second approach is that the amount of data that needs to be stored is unbounded. If a mesh has e.g. multiple UV sets, then Psychopath has to store intersection data about all of those UV sets to be able to do the shading later. And I'd also like Psychopath to be able to support arbitrary geometry data for use in shaders, which would exacerbate the problem. It won't be possible to predict ahead of time how much data needs to be stored.

With the first approach, on the other hand, the amount of data can be bounded. There is still the possibility (even likelihood) of multiple mixed closures. But because they are closures, I have more flexibility. I can take a monte carlo approach and, after shading, select a subset of them to be stored along with the appropriate weighting information. The downside to that, of course, would be increased noise. But the max number of closures to store could be a render setting, which would allow the user to decide what to do with that memory/noise tradeoff.

The biggest problem with the first approach, however, is that the shader will need to be evaluated at every ray hit during traversal, rather than just the final hit. And especially for non-trivial shaders in a complex scene, that could have a non-negligible performance impact.

But in the end, I think it's a reasonable tradeoff. I would rather keep memory usage predictably bounded, as one of Psychopath's goals is to be able to render very large scenes.