Recently we had to face this interesting problem: extracting a constant pass out of an arbitrarely complex rendertree. In short, we received scenes set up for rendering, with a given number of passes and channels already set up. However, we needed an extra constant pass which was not planned in advance.
The rendertrees have all kinds of materials, with bump, transparency and reflection in place.
The most obvious approach to solve the problem is brute force: writing a script that would traverse all the materials, and substitute each material with a constant one, using as color the diffuse color of the original material, and inheriting all the subtrees for the transparency and reflection mixing. This would have required a couple of days of scripting, and, as any brute force approach, is not really elegant.
Another idea we tried was tuning the normal. We replaced all the lights with a single directional white light, pointing downward. Then, we connected a constant vector to the bump port of each material, pointing upward (0,1,0). The light has no specular contribution and the ambience is set to black. This way, only the diffuse component of the materials is returned, and since the normal always points upward, the incidence with the light is always 1, so all the materials behave like constant.
Unfortunately this method has a major disadvantage: since the direction of the reflected and refracted rays leaving a (reflecting/refracting) material depends on the normal, the result is completely wrong for secondary rays, because we are moving the normal to an arbitrary (up) direction. So, using the bump port was not a solution.
However, this failure pointed us towards a better solution.
Again, we just had to fix the diffuse component. In all the material shaders (except a few exceptions like hair, which we treat separately), the way the diffuse component is computed is as follows:
- Get the light color
- Multiply it by the incidence
- Multiply the result by the diffuse color
So, if you want to get back the plain diffuse color, you just have to tell the light to return 1 (white) divided by the incidence. This way, the material shader returns diffuse * incidence * (1 / incidence) == diffuse.
This the starting case (a phong sphere – top), and our first result (bottom)
And this is the rendertree for the light
Also in light shaders, the normal can be used (it’s the lit point normal). The dot product of the normal with the light direction L0 is the incidence, which we then use to divide 1, and return the result as the intensity of the light.
That was ok for a half dome, but what about the rest? A possibility is simply to double the light, specularly to the xz plane, cloning the rendertree (and L0 becoming 0,-1,0 for the other light). That would cover 99% of the cases, being any oriented surface sample being lit only by one of the two lights at most.
The “at most” is the remaining problem. If you have a cube, its vertical faces won’t be lit by any!
So, we have to expand a bit this setup. We start setting up a minimal light rig, ensuring that al least one of the lights will always hit any oriented surface. The minimum number of lights for such a rig is 4, equally distributed in space.
So, we take a tetrahedron of radius 1, centered at the origin, and cluster constrain a directional white light to each vertex, all pointing toward the origin.
Each light has the following rendertree (with a small change for each of them)
This is the tree for light L0: the left side vectors are the four lights positions, set by expression. Since The tetrahedron has radius one, they also represent the direction toward each lights. All their incidence with the normal is computed and sent to a positivity check, and this check drives a switcher between zero and one. So, the sum of these 4 switchers is the number of lights (1 to 3) visible from the rendered point.
This number is multiplied by the light incidence and the result divides 1, as in the single light case.
Why do we need the number of visible light? Since we have 4 lights, the total amount of light that gets received by the point is
L = L0*I0 + L1*I1 + L2*I2 + L3*I3, for each positive Iight. In fact, the material shaders won’t even query a light if its incidence is negative.
So, as before, we still return 1 divided by the light incidence, but also divided by the number of visible lights, so that the overall sum of the visible lights is always 1.
The other 3 lights have the same rendertree, except the dividing incidence comes from I1, I2, I3 respectively. For instance, this is the tree for light L1.
And finally, we have our constant result. You can download the rig here.