Simulating Reflections in a Flat Surface

Simulating Reflections in a Flat Surface

May 1990

Introduction

To make a realistic computer-generated picture of a shiny surface it is usually necessary to simulate the reflections in the surface. Ray tracing can provide accurate reflections, but requires a great deal of CPU time. There are a number of less time-consuming ways to simulate reflections using PRMan. None of these methods are effective and efficient in all situations. For good results, it's important that you choose the most appropriate method for your application.

This application note describes a method to simulate reflections in flat surfaces. The method uses a texture map and requires an additional rendering step to create the texture map from the scene description.

There are less expensive methods of simulating reflections with a lower degree of realism. In outdoor scenes, the sky is usually the dominant source of reflections. A simple shader that selects sky and ground colors based on the "up" component of the reflected vector gives an inexpensive impression of reflections without any additional rendering steps or texture files. This method works well for curved surfaces, but may give unconvincing results for large flat surfaces.

If the reflective surfaces are not flat, the technique described in this application note will not work. In such a case, reflections can be simulated using environment textures. Additional rendering steps are needed to create the environment texture, and it takes longer to render the final image. Reflections in curved surfaces may be simulated quite accurately using environment maps, particularly if the reflected objects are not too close to the reflecting object. The environment map technique is far more realistic than the simple "sky and ground" reflection technique, but is far more expensive.

The criteria for selecting an appropriate method of simulating reflections are summarized in the table below.


Reflections in a Plane

Simulating reflections is particularly easy when the reflecting surface is flat (planar).

Imagine that you point a camera at a mirror and take a picture of the image reflected in the mirror (Figure 1a). Now imagine that the mirror is replaced with a clear glass window, and the camera is moved to an exactly opposite position on the other side of the window (Figure 1b). Take a second picture from the new vantage point with the camera looking into the room through the window. Remarkably enough, when you compare the two pictures, one is the "mirror image" of the other, that is, the same image with left and right reversed. This thought experiment suggests a technique for simulating reflections in a mirror or other flat surfaces in a computer-generated picture.

Surface Type Level of Realism Required Best Method Cost
All types Low Sky and ground shader Low
A single flat surface High Method described here Moderate
Curved, complex High Environment maps High

In the mathematical world of computer graphics, we can simulate a reflection exactly (including the left-right reversal) by reflecting the camera through the mirror, instead of simply moving it to the other side of the mirror.

Let's consider a particular example. The C program in the Appendix generates a RenderMan image of a simple room consisting of a floor and two adjacent walls. A mirror hangs on one wall and there is a teapot in front of the mirror. This scene is deliberately simplified to make the program easier to understand.

If the program is compiled and executed, it generates RenderMan calls that produce a picture of the room (called the "scene image"). Unfortunately, the mirror will not contain a reflection unless we do a little more work.

A reflected image is placed on the mirror by first rendering the reflection image using a reflected camera on the other side of the mirror, and then combining the reflection image with the scene image.

The steps of this procedure are as follows:

  1. Reflect the camera to the other side of the mirror surface.
  2. Modify the scene model by removing the reflecting surface and other surfaces that block the view of the reflected camera.
  3. Render the reflection image.
  4. Convert the reflection image into a reflection texture map.
  5. Apply a suitable shader to the reflecting surface; this shader uses the reflection texture to simulate the reflections.

Each of these steps is described in detail in the remainder of this application note.


Reflecting the Camera

The camera used to render the reflection image is simply the scene camera reflected through the reflection plane. In our example program, this is quite easy, since the mirror lies in the plane z=-0.05. (Figure 2 shows the example scene rendered without a reflection in the mirror.) If the reflection plane were z=0 in world space, the camera could be reflected by adding the command:

RiScale(1., 1., -1.);

This scale operation does nothing except negate all of the z coordinates of the camera coordinate system. If the camera was positioned at (x, y, z) before the scale operation, it will be positioned at (x, y, -z) after the scale; this is, of course, the reflection of the original position with respect to the z=0 plane.

The case of reflection in the z=-0.05 plane is only slightly more difficult. First we translate the z=0 plane to the position of the actual reflection plane using an RiTranslate call, then do the scale operation, which reflects through z=0, and then translate the z=0 plane back to its original position.

RiTranslate(0., 0., -0.05);
RiScale(1., 1., -1.);
RiTranslate(0., 0., 0.05);

Notice that points which lie on the z=-0.05 plane in world space are unaffected by this sequence of transformations. In the example program, this sequence of transformations is performed in the SetupCamera routine when the preprocessor symbol REFLECTION is defined.

There is nothing special about z in the above procedure. The reflection through an x=k` or ``y=k plane (for some number k) is very similar, simply negating the x or y coordinates using an appropriate RiScale call.

If the reflection plane is not aligned with the coordinate system axes, it's a little harder to reflect the camera through the reflection plane. Instead of using just an RiTranslate call to move the z=0 (or x=0 or y=0) plane to coincide with the reflection plane, it is necessary to use both an RiRotate and an RiTranslate. The axis of rotation is the cross product of the normal vectors of the reflection plane and the z=0 plane. The direction of translation is along the normal vector of the reflection plane. The inverse RiTranslate and RiRotate must be used to transform the reflection plane back to z=0 after the RiScale is applied. If you prefer, all of the rotations, translations, and scales can be combined into a single transformation matrix that can be applied using the RiConcatTransform call.


Modifying the Model

In order to render the reflection image, the model must be modified to eliminate surfaces that would lie between the reflected camera and the interesting part of the scene. This must be done by the programmer based on an examination of the model. In the example, both the mirror itself and the wall on which it hangs would block the view of the reflected camera, and so both surfaces are removed from the model in the SetupModel routine when REFLECTION is defined.

Since the reflection camera will be "looking out of the mirror," it is safe to remove all surfaces from the scene that are at or behind the plane of the mirror (including the mirror itself). This will guarantee an unobstructed view for the reflection camera, and will increase the efficiency of rendering the reflection image.


Rendering the Reflection Image

If the example program is compiled with the preprocessor symbol REFLECTION defined, it will contain the RenderMan calls needed to render the reflection image that will appear reflected in the mirror when the final scene image is rendered. The reflection image is rendered into a TIFF file called refl.tif and then used to make a texture file (as described in the next section). It is most efficient to make the texture from a square image whose resolution is an integer power of 2. The scene image might not be square, so the pixel aspect ratio of the reflection image must be adjusted so that the reflection image covers the same screen window as the scene image. The screen window determines how much of the scene is visible in the image; since we will be combining the reflection image with the scene image later, the two images must cover the same screen window. In the example program, the calculation of the parameters for the RiFormat calls takes this effect into account.

Figure 3 shows the reflection texture rendered by the example program.


Making the Reflection Texture

Having rendered the reflection image, we can make it into a texture file called refl.tex using the RiMakeTexture call as follows:

RiMakeTexture("refl.tif", "refl.tex",
         RI_BLACK, RI_BLACK,
         RiBoxFilter, 1., 1., RI_NULL);

The RI_BLACK parameters specify that the texture values outside the 0:1 texture coordinate range of the reflection map should be zero. If the reflection texture is used properly, values outside the 0:1 texture coordinate range should never be accessed, but the RiMakeTexture call requires the specification of a wrap mode and RI_BLACK is a reasonable choice. The filtering parameters RiBoxFilter, 1., 1. are the default values for RiMakeTexture. Currently RiMakeTexture requires these parameters, but PRMan ignores them.

The equivalent txmake command is simply:

txmake refl.tif refl.tex

The Reflection Shader

The mirror or other reflecting surface must have a surface shader that uses the texture map created in the preceding step. Each pixel on the mirror is shaded using the texture map that was created from the reflection image. The pixel position in the scene image is used to look up the correct pixels from the texture map. In effect, the two images are being composited, but only at the pixels where the mirror is visible in the scene image. The shader below (Listing 1) shows what is required.

Each point P on the mirror is shaded with a color from the texture map, multiplied by the surface color and opacity so that colored and partially transparent mirrors are possible. The texture coordinates used to access the texture map are simply the x and y components of PNDC. PNDC is the point P expressed in the NDC (normalized device coordinate) system, in which the x and y coordinates range from 0 to 1 across and down the image. (Important note: the NDC coordinate system is an extension provided by PRMan; it is not described in the RenderMan Interface specification version 3.1.)

The simple shader shown here can be made more sophisticated by combining the reflection with a plastic shading model or a wood shading model. This would be appropriate to add reflections to a shiny floor or tabletop that is not a pure reflector like a mirror.

Figure 4 shows the example scene rendered with the reflection texture mapped onto the mirror.


Examples

Listing 1

A Reflection Shader

surface
refl(string reflname = "refl.tex")
{
    point PNDC;

    PNDC = transform("NDC", P);
    Ci = Os * Cs * color texture(reflname, xcomp(PNDC), ycomp(PNDC));
    Oi = Os;
}

Appendix A

A C Program that Generates a Scene

#include <stdio.h>
#include <ri.h>

#define ASPECT_RATIO  1.33333   /* define as 1.33333 for usual 4:3 image */
#define TEXTURE_ROWS  256        /* a power of two */
#define IMAGE_ROWS    300

main()
{
    RiBegin(RI_NULL);
    SetupOptions();

    RiFrameBegin(0);
    SetupCamera();

    RiWorldBegin();
    SetupModel();
    RiWorldEnd();

    RiFrameEnd();
    RiEnd();
}

/*
 * Set RenderMan options for medium quality/speed tradeoff.
 */

SetupOptions()
{
    static RtInt gsz = 32;
    static RtInt bktsz[2] = {12, 12};
    RtInt splits = 5;

    RiOption("limits", "gridsize", (RtPointer) &gsz,
              "eyesplits", (RtPointer) &splits,
              "bucketsize", (RtPointer) bktsz, RI_NULL);
    RiShadingRate(2.);
    RiPixelSamples(2., 2.);
    RiPixelFilter(RiBoxFilter, 1., 1.);

#ifdef REFLECTION
    RiDisplay("refl.tif", RI_FILE, RI_RGBA, RI_NULL);
    RiFormat(TEXTURE_ROWS, TEXTURE_ROWS, ASPECT_RATIO);
#else
    RiDisplay("foo", RI_FRAMEBUFFER, RI_RGBA, RI_NULL);
    RiFormat((RtInt)(IMAGE_ROWS*ASPECT_RATIO), IMAGE_ROWS, 1.0);
#endif
}

/*
 * Set RenderMan camera parameters.
 */

SetupCamera()

{
    static RtMatrix m = {             /* an arbitrary viewing matrix */
      .970143, -0.004705, -.24249, 0,
      0, .999812, -0.0193992, 0,
      .242536, 0.01882, .96996, 0,
      -7.27607, -1.96434, 53.406, 1
    };
    RtFloat fov = 25.;

    RiProjection(RI_PERSPECTIVE, (RtToken) "fov", (RtPointer) &fov, RI_NULL);
    RiTransform(m);

#ifdef REFLECTION
    /* Transform z=0 plane to coincide with reflection plane. */
    RiTranslate(0., 0., -0.05);

    /* Reflect camera through the reflection plane z=0. */
    RiScale(1., 1., -1.);

    /* Transform reflection plane back to z=0. */
    RiTranslate(0., 0., 0.05);
#endif
}

/*
 * The model consists of three "walls" of a room (actually two
 * walls and the floor) with a mirror on one of the walls.
 * When rendering the REFLECTION image, the mirror
 * and the wall it is hanging on are both removed so that
 * the camera can see into the room from the other side of
 * the wall.
 */

SetupModel()
{
    static RtColor c[4] = {           /* various colors */
      { 0.5, 0.4, 0.1}, { 0.3, 0.5, 0.5},
      { 1, 1, 1}, { 0.4, 0.2, 0.7}
    };
    int i;
#ifdef REFLECTION
#define NWALLS        2               /* Just do two walls. */
    static RtPoint walls[NWALLS][4] = {
      { 0, 0, -100}, { 0, 0, 0}, { 100, 0, -100}, {100, 0, 0},
      { 0, 100, -100}, {0, 100, 0}, { 0, 0, -100}, { 0, 0, 0},
    };
#else
#define NWALLS        3               /* Do three walls and mirror. */
    static RtPoint walls[NWALLS][4] = {
      { 0, 0, -100}, { 0, 0, 0}, { 100, 0, -100}, {100, 0, 0},
      { 0, 100, -100}, {0, 100, 0}, { 0, 0, -100}, { 0, 0, 0},
      { 0, 100, 0}, { 100, 100, 0}, { 0, 0, 0 }, { 100, 0, 0}
    };
    static RtPoint mirror[4] = {
      { 5, 10.5, -.05}, { 10, 10.5, -.05},
      {5, 0.5, -.05}, { 10, 0.5, -.05}
    };


    RiAttributeBegin();
    RiColor(c[2]);
    RiSurface("refl", RI_NULL);
    RiPatch(RI_BILINEAR, RI_P, (RtPointer) mirror, RI_NULL);
    RiAttributeEnd();
#endif

    RiColor(c[0]);
    for (i = 0; i < NWALLS; i++) {
      RiPatch(RI_BILINEAR, RI_P, (RtPointer) walls[i], RI_NULL);
      RiColor(c[1]);
    }

    /* Put a teapot in the room. */
    RiColor(c[3]);
    RiTransformBegin();
      RiTranslate(5., 1., -2.);
      RiRotate(90., 0., 1., 0.);
      RiRotate(-90., 1., 0., 0.);
      RiGeometry("teapot", RI_NULL);
    RiTransformEnd();
}