Soft Shadows in PRMan

Soft Shadows in PRMan

August 1998


Figure 1: Soft Shadows


PhotoRealistic RenderMan has always supported shadow computations through the use of pre-rendered shadow maps, which can determine shadowing information for a single light source point. Unfortunately, such shadows have hard boundaries and are characteristic of computer graphics (see Figure 2). More realistic shadows come from light sources that aren't a single point, such as area lights (see the Area Light and Area Shadowing Support application note for changes introduced in PRMan 16).

Shadow computations from area light sources require visibility checks with more than one point on the light source. One way to approximate area shadows is by rendering multiple shadow maps from various points on the light source, and averaging their shadow contributions. (see Figure 3 and Figure 4). If we were able to render an infinite number of shadow maps, this would converge to the answer we seek. This is not feasible, of course, so we would like a solution that can approximate soft shadows without requiring a large number of shadow maps.

PRMan 3.8 introduced such a solution, wherein a form of the shadow() shadeop accepts multiple shadow maps and a specification of the "area light's" shape. Since this only affects shadow computations, you don't get other phenomena associated with area light sources (such as non-point highlights), but the softening of shadows goes a long way toward making illumination more realistic.

Using Soft Shadows

Before you can render an image with soft shadows, you must render the shadow maps that will provide the renderer with visibility information for the occluding objects. While normal shadows require only one shadow map rendered from the light source's location, soft shadows require multiple shadow maps from various points on the light source's area. The arrangement of the shadow maps is up to the user, but in general a few shadow maps from evenly-spaced points on the light source will produce the best results.

  • Important

    There are two restrictions that must be followed when rendering these shadow views:

    • Currently PRMan requires that all of the geometry that will cast shadows from a given light source must be contained in every shadow map that is supplied to the shadow() call. Clearly, each view will have different information (because of the shifted view position), but the volume of the occluding objects must be contained in the view frusta of every shadow map.
    • The shadow maps must be in a new, special shadow map format called minmax. This format contains a hierarchical min-max tree of occlusion information, which allows the soft shadow computation to be more efficient. These minmax shadow files are computed either by the RiMakeShadow call, with the separate utility txmake, or by using the shadow display driver with "int minmax" [1].

    (See the example below.)

Once you have multiple shadow maps providing various views of the occluding objects, you can use them in a shadow call to render soft shadows. There are a few differences between the soft-shadow form of shadow and the normal case:

  • The "name" parameter can be a comma-separated list of filenames rather than a single filename. This is where you list the various views you've rendered.
  • The new "source" parameter lets you specify the location and shape of the area light source. We never had to specify this before because the location was implicitly the origin of the shadow map's view; since we have multiple shadow maps now, we must be more explicit. The "source" keyword must be followed by one, two, three, or four points (which must be uniform and in current space), which will specify a point, line segment, triangle, or quadrilateral light source; if using a quadrilateral light source the points must be specified in "bowtie" order (in the same way that the points are ordered for a bilinear patch). The renderer will integrate its lighting computations over this area. It is generally advisable that your shadow maps are rendered from various points on this surface.
  • The new "gapbias" parameter tells the renderer how to infer connectivity information in the shadow maps. Adjacent pixels whose z values are within "gapbias" of each other will be assumed to be part of the same object. Sometimes experimentation may be necessary to find the right gapbias value, but a good starting point (and often a good enough value) is the shadow bias number.

As before, the bias parameter specifies how far to shift samples towards to the light source to prevent self-shadowing of objects.

The samples parameter still specifies how many samples to cast when performing the shadowmap computation. Larger values will produce less noisy output and smoother shadows, but are proportionally more expensive to compute.


You can render the four shadow views from Figure 3 into the .shad files light1.shad, light2.shad, light3.shad, and light4.shad using "int minmax" [1] in your Display call. For example:

Display "light1.shad" "shadow" "z" "int minmax" [1]
Display "light2.shad" "shadow" "z" "int minmax" [1]
Display "light3.shad" "shadow" "z" "int minmax" [1]
Display "light4.shad" "shadow" "z" "int minmax" [1]

Next, we must write a shader that will make use of multiple minmax shadow files. Here's an example:

light arealight(color lightcolor = color(1,1,1);
                float intensity = 1;
                string maplist = "";
                float numsamples = 1;
                point Pl1 = point(0, 0, 0);
                point Pl2 = point(0, 1, 0);
                point Pl3 = point(0, 0, 1);
                point Pl4 = point(0, 1, 1);
                float shadowBias = 0.001;
                float gapBias = 0.01) {
    varying float attenuation;

    illuminate ((Pl1+Pl2+Pl3+Pl4)*0.25) {  /* base illumination at average */
                                           /* of light positions           */

    attenuation = shadow(maplist,Ps,"source",Pl1,Pl2,Pl3,Pl4,
              "samples",numsamples, "bias",shadowBias, "gapbias", gapBias);

    Cl = lightcolor * intensity * (1-attenuation);

Note that this shader expects the comma-separated list of shadow views to be passed through the parameter maplist. To use this shader in a final render, we make a light source in RIB that uses the above shader. For this shader, we pass in the list of views and the four corners of the light source:

LightSource "arealight" 1 "intensity" [1]
            "maplist" ["light1.shad,light2.shad,light3.shad,light4.shad"],
            "Pl1" [1.15622 -8.83187 4.78469]
            "Pl2" [0.191448 -6.11682 8.94722]
            "Pl3" [5.9265 -7.13679 4.78469]
            "Pl4" [4.96173 -4.42175 8.94722]
            "gapBias" [0.03]
            "shadowBias" [0.3]
            "numsamples" [36]


Figure 2: Hard shadow

Figure 3: Four shadow maps
images/figures.26/softshadfig3a.gif images/figures.26/softshadfig3b.gif
images/figures.26/softshadfig3c.gif images/figures.26/softshadfig3d.gif


Figure 4: Averaging the results of four shadow maps