RDC Shadows


This document is a short tutorial on how to add shadows to a scene using RenderDotC. At the time of writing, RenderDotC was up to version 3.1b1. You can find RenderDotC at the DotC Software homepage.

It is assumed in this guide that the reader has a copy of RenderDotC and some experience with writing RIBs. A working knowledge of your system is required and a C compiler will come in very handy later.

Although care has been taken to ensure that all of the information in this guide is correct, no-one can be held liable for any errors or omissions.

An Example Scene

To demonstrate how shadows work in RDC, we'll use a very simple scene. We'll use a sphere on a flat plane:
scene.jpg
scene.tiff 
Here is the RIB code for this scene:


version 3.03
Display "scene.tiff" "file" "rgba"
Format 400 300 1

Projection "perspective" "fov" 45
Translate 0 0 5
Rotate -20 1 0 0

WorldBegin
        LightSource "ambientlight" 0 "intensity" [0.2]
        LightSource "spotlight" 1 "intensity" [75] "from" [5 5 -5] "to" [0 0 0]
                    "coneangle" [0.5]

        Color [1 1 1]
        Surface "matte"
        Sphere 1 -1 1 360

        Patch "bilinear" "P"    [-10 -1 -10
                                  10 -1 -10
                                 -10 -1  10
                                  10 -1  10]

WorldEnd

scene.rib

The Light Becomes the Camera

To create a shadow we need a shadow map. A shadow map is simply an image which, instead of red, green, blue and alpha channels, only contains z-buffer data. The purpose of this is to have a record of the "depth" or Z value of the surface hit at every pixel.

This is very simple, all we have to do is change the line:

Display "scene.tiff" "file" "rgba"
to:
Display "shadow.map" "file" "z"
And we will get a shadow map containing Z values. That was the easy part, the hard part is moving the camera into the same position as the light. Pixar have a neat little snippet of code (which can also be found in the RenderMan Companion) called placecam which makes this part a lot easier.

This is a slightly modified version of Pixar's placecam.c: 


/*
 * PlaceCamera(): establish a viewpoint, viewing direction and orientation
 *  for a scene. This routine must be called before RiWorldBegin().
 *  position: a point giving the camera position
 *  aim: a point giving the location at which the camera is aimed
 *  roll: an optional rotation of the camera about its direction axis
 *  coneangle: an optional spotlight shader cone angle
 */

#include <math.h>
#include <stdio.h>
#include <stdlib.h>

#define PI 3.1415926535897932

void usage(void)
{
        printf("usage: placecam pos_x pos_y pos_z aim_x aim_y aim_z\n");
        printf(" [coneangle] [roll_angle]\n");
        printf(" Calculate RenderMan transforms needed for camera transform\n");
        printf(" from light position to aim point with the given roll angle.\n");
        exit(1);
}

typedef double RtPoint[3];

void RiRotate(double angle, double x, double y, double z)
{
        if (fabs(angle) > 0.001)
                printf("Rotate %0.2f %0.2f %0.2f %0.2f\n", angle, x, y, z);
}

void RiTranslate(double dx, double dy, double dz)
{
        printf("Translate %0.2f %0.2f %0.2f\n", dx, dy, dz);
}

/*
 * AimZ(): rotate the world so the direction vector points in
 *  positive z by rotating about the y axis, then x. The cosine
 *  of each rotation is given by components of the normalized
 *  direction vector. Before the y rotation the direction vector
 *  might be in negative z, but not afterward.
 */
void AimZ(RtPoint direction)
{
        double xzlen, yzlen, yrot, xrot;

        if (direction[0]==0 && direction[1]==0 && direction[2]==0)
                return;

        /*
         * The initial rotation about the y axis is given by the projection of
         * the direction vector onto the x,z plane: the x and z components
         * of the direction.
         */
         xzlen = sqrt(direction[0]*direction[0]+direction[2]*direction[2]);
         if (xzlen == 0)
                 yrot = (direction[1] < 0) ? 180 : 0;
         else
                 yrot = 180*acos(direction[2]/xzlen)/PI;

        /*
         * The second rotation, about the x axis, is given by the projection on
         * the y,z plane of the y-rotated direction vector: the original y
         * component, and the rotated x,z vector from above.
         */
         yzlen = sqrt(direction[1]*direction[1]+xzlen*xzlen);
         xrot = 180*acos(xzlen/yzlen)/PI; /* yzlen should never be 0 */

         if (direction[1] > 0)
                 RiRotate(xrot, 1.0, 0.0, 0.0);
         else
                 RiRotate(-xrot, 1.0, 0.0, 0.0);

        /* The last rotation declared gets performed first */
        if (direction[0] > 0)
                RiRotate(-yrot, 0.0, 1.0, 0.0);
        else
                RiRotate(yrot, 0.0, 1.0, 0.0);
}

void PlaceCamera(RtPoint position, RtPoint direction, float roll)
{
        RiRotate(-roll, 0.0, 0.0, 1.0);
        AimZ(direction);
        RiTranslate(-position[0], -position[1], -position[2]);
}

int main(int argc, char *argv[])
{
        RtPoint pos, aim, dir;
        double roll;
        double coneangle, fov;

        if (argc < 7)
                usage();

        pos[0] = atof(argv[1]);
        pos[1] = atof(argv[2]);
        pos[2] = atof(argv[3]);
        aim[0] = atof(argv[4]);
        aim[1] = atof(argv[5]);
        aim[2] = atof(argv[6]);

        if (argc > 7) coneangle = atof(argv[7]);
                else coneangle = 0.0;

        if (argc > 8) roll = atof(argv[8]);
                else roll = 0.0;

        printf("position: %0.2f, %0.2f, %0.2f\n", pos[0], pos[1], pos[2]);
        printf("aim: %0.2f, %0.2f, %0.2f\n", aim[0], aim[1], aim[2]);
        printf("coneangle: %0.4f\n", coneangle);
        printf("roll: %0.2f\n\n", roll);

        if (coneangle != 0.0) {
                fov = coneangle * 360.0 / PI;
                printf("Projection \"perspective\" \"fov\" [%0.2f]\n", fov);
        }

        dir[0] = aim[0] - pos[0];
        dir[1] = aim[1] - pos[1];
        dir[2] = aim[2] - pos[2];

        PlaceCamera(pos, dir, roll);
        return 0;
}

placecam.c

Setting up the Shadow

Now we need to set up the camera so that it "becomes" the light. With the example scene, the shadow-casting spotlight is defined like so:
LightSource "spotlight" 1 "intensity" [75] "from" [5 5 -5] "to" [0 0 0] "coneangle" [0.5]
To get the camera transform code we'll need to run placecam. Just running it without any parameters will produce this:
usage: placecam pos_x pos_y pos_z aim_x aim_y aim_z
[coneangle] [roll_angle]
Calculate RenderMan transforms needed for camera transform
from light position to aim point with the given roll angle.
This means that to get the transform code we need, all we have to do is run:
placecam 5 5 -5 0 0 0 0.5 1
Can you see where the values came from?
Remember that all of this is assuming that we are using a spot light.

placecam will now give us a little chunk of RIB code which we need to put into shadow.rib. This is what placecam returns from the above call:

position: 5.00, 5.00, -5.00
aim: 0.00, 0.00, 0.00
coneangle: 0.5000
roll: 0.00

Projection "perspective" "fov" [57.30]
Rotate -35.26 1.00 0.00 0.00
Rotate 45.00 0.00 1.00 0.00
Translate -5.00 -5.00 5.00
We need those last four lines which form the necessary RIB code to place the camera in the same position as the light. We also need to change the output resolution - it's usually a good idea to define a square image with dimensions which are powers of 2. 256x256 and 512x512 are good choices. Finally, we need to kick out any objects which do not cast shadows or which don't really affect the scene along with all surface and color statements and lights.

Here is what shadow.rib should look like: 


version 3.03

# Output Z data only
Display "shadow.map" "shadow" "z"

# Shadow is bigger than output image, this should give good resolution
Format 512 512 1

# Clip it for better quality
Clipping 5 10

# No antialiasing
ShadingRate 1
PixelSamples 1 1
PixelFilter "box" 1 1

# Placecam camera transform
Projection "perspective" "fov" [57.30]
Rotate -35.26 1.00 0.00 0.00
Rotate 45.00 0.00 1.00 0.00
Translate -5.00 -5.00 5.00

WorldBegin

        # Don't need any light declarations
        # LightSource "ambientlight" 0 "intensity" [0.2]

        # The sphere casts a shadow
        Surface "null"
        Sphere 1 -1 1 360

        # The ground doesn't. Kill it.
        # Patch "bilinear" "P" [-10 -1 -10...

WorldEnd

shadow.rib

Shadow Map Quality

There are a few things which contribute to the quality of a shadow map. Firstly, the resolution. The larger your shadow map is the more data there is to define edges. If you use a low resolution shadow map you can get blocky and blurry edges.

Secondly, there is clipping. You don't have to worry too much about clipping with RenderDotC but if you get some unexpected results from your shadowmap you should look at your clipping. Clipping is easy to use, just add a line like this into your RIB code:

Clipping <nearest point> <farthest point>
Finally, the angle of the projection in your shadow map, since it is directly connected with the size of the projected objectsi in the final renderd map, the projection angle contributes to shadow map quality.

In the example shadow map, we use the following projection:

Projection "perspective" "fov" [57.30]
If you render this to the framebuffer (yes, you can render Z-buffer data to the screen - it will look strange but you'll still be able to tell what's going on) and fiddle with the projection angle you will find that you can turn the angle down to 20 and you will get a much nicer shadow map. The projection angle in the shadow map does not need to match the light's cone angle. As long as the shadow map can see all of the objects which cast shadows everything will be fine.

Using the Shadow Map

After you render shadow.rib you will have a shadow map. Now all you have to do is modify scene.rib to use it. This is very simple. All you have to do is change the line:
LightSource "spotlight" 1 "intensity" [75] "from" [5 5 -5] "to" [0 0 0] "coneangle" [0.5]
to:
LightSource "shadowspot" 1 "intensity" [75] "from" [5 5 -5] "to" [0 0 0] "coneangle" [0.5] "shadowname" ["shadow.map"] "width" [3]
So you'll be left with a final RIB which looks like this: 
version 3.03
Display "final.tiff" "file" "rgba"
Format 400 300 1

Projection "perspective" "fov" 45
Translate 0 0 5
Rotate -20 1 0 0

WorldBegin

        LightSource "ambientlight" 0 "intensity" [0.2]
        LightSource "shadowspot" 1 "intensity" [75] "from" [5 5 -5] "to" [0 0 0]
                    "coneangle" [0.5] "shadowname" ["shadow.map"] "width" [3]

        Color [1 1 1]
        Surface "matte"
        Sphere 1 -1 1 360

        Patch "bilinear" "P"    [-10 -1 -10
                                  10 -1 -10
                                 -10 -1  10
                                  10 -1  10]

WorldEnd

final.rib

When you render the final you will have a much nicer scene. Shadows can add a lot of depth, realism and believability to a scene - even a simple one like this:

final.jpg
final.tiff 

This guide was written by Emil Mikulic <darkmoon@connexus.net.au>

Copyright © 1999-2006 Dot C Software, Inc. All rights reserved.
Dot C Software, Inc., 182 Kuuhale St., Kailua, HI 96734
(808) 262-6715 (voice) (808) 261-2039 (fax)
The RenderMan® Interface Procedure and RIB Protocol are:
Copyright © 1988, 1989, Pixar. All rights reserved.
RenderMan® is a registered trademark of Pixar.