JavaScript EditorFree JavaScript Editor     Ajax Editor 

Main Page
Previous Page
Next Page

12.4. The ÜberLight Shader

So far in this chapter we've discussed lighting algorithms that simulate the effect of global illumination for more realistic lighting effects. Traditional point, directional, and spotlights can be used in conjunction with these global illumination effects. However, the traditional light sources leave a lot to be desired in terms of their flexibility and ease of use.

Ronen Barzel of Pixar Animation Studios wrote a paper in 1997 that described a much more versatile lighting model specifically tailored for the creation of computer-generated films. This lighting model has so many features and controls compared to the traditional graphics hardware light source types that its RenderMan implementation became known as the "überlight" shader (i.e., the lighting shader that has everything in it except the proverbial kitchen sink). Larry Gritz wrote a public domain version of this shader that was published in Advanced RenderMan: Creating CGI for Motion Pictures, which he coauthored with Tony Apodaca. A Cg version of this shader was published by Fabio Pellacini and Kiril Vidimice of Pixar in the book GPU Gems, edited by Randima Fernando. The full-blown überlight shader has been used successfully in a variety of computer-generated films, including Toy Story, Monsters, Inc., and Finding Nemo. Because of the proven usefulness of the überlight shader, this section looks at how to implement its essential features in the OpenGL Shading Language.

12.4.1. ÜberLight Controls

In movies, lighting helps to tell the story in several different ways. Sharon Calahan gives a good overview of this process in the book Advanced RenderMan: Creating CGI for Motion Pictures. This description includes five important fundamentals of good lighting design that were derived from the book Matters of Light & Depth by Ross Lowell:

  • Directing the viewer's eye

  • Creating depth

  • Conveying time of day and season

  • Enhancing mood, atmosphere, and drama

  • Revealing character personality and situation

Because of the importance of lighting to the final product, movies have dedicated lighting designers. To light computer graphics scenes, lighting designers must have an intuitive and versatile lighting model to use.

For the best results in lighting a scene, it is crucial to make proper decisions about the shape and placement of the lights. For the überlight lighting model, lights are assigned a position in world coordinates. The überlight shader uses a pair of superellipses to determine the shape of the light. A superellipse is a function that varies its shape from an ellipse to a rectangle, based on the value of a roundness parameter. By varying the roundness parameter, we can shape the beam of illumination in a variety of ways (see Figure 12.3 for some examples). The superellipse function is defined as

Figure 12.3. A variety of light beam shapes produced with the überlight shader. We enabled barn shaping and varied the roundness and edge width parameters of the superellipse shaping function. The top row uses edge widths of 0 and the bottom row uses 0.3. From left to right, the roundness parameter is set to 0.2, 0.5, 1.0, 2.0, and 4.0.

As the value for d nears 0, this function becomes the equation for a rectangle, and when d is equal to 1, the function becomes the equation for an ellipse. Values in between create shapes in between a rectangle and an ellipse, and these shapes are also useful for lighting. This is referred to in the shader as barn shaping since devices used in the theater for shaping light beams are referred to as barn doors.

It is also desirable to have a soft edge to the light, in other words, a gradual drop-off from full intensity to zero intensity. We accomplish this by defining a pair of nested superellipses. Inside the innermost superellipse, the light has full intensity. Outside the outermost superellipse, the light has zero intensity. In between, we can apply a gradual transition by using the smoothstep function. See Figure 12.3 for examples of lights with and without such soft edges.

Two more controls that add to the versatility of this lighting model are the near and far distance parameters, also known as the cuton and cutoff values. These define the region of the beam that actually provides illumination (see Figure 12.4). Again, smooth transition zones are desired so that the lighting designer can control the transition. Of course, this particular control has no real-world analogy, but it has proved to be useful for softening the lighting in a scene and preventing the light from reaching areas where no light is desired. See Figure 12.5 for an example of the effect of modifying these parameters.

Figure 12.4. Effects of the near and far distance parameters for the überlight shader

Figure 12.5. Dramatic lighting effects achieved by alteration of the depth cutoff parameters of the überlight shader. In the first frame, the light barely reaches the elephant model. By simply adjusting the far depth edge value, we can gradually bathe our model in light.

12.4.2. Vertex Shader

Listing 12.6 shows the code for the vertex shader for the überlight model. The main purpose of the vertex shader is to transform vertex positions, surface normals, and the viewing (camera) position into the lighting coordinate system. In this coordinate system the light is at the origin and the z axis is pointed toward the origin of the world coordinate system. This allows us to more easily perform the lighting computations in the fragment shader. The computed values are passed to the fragment shader in the form of the varying variables LCpos, LCnorm, and LCcamera.

To perform these calculations, the application must provide ViewPosition, the position of the camera in world space, and WCLightPos, the position of the light source in world coordinates.

To do the necessary transformations, we need matrices that transform points from modeling coordinates to world coordinates (MCtoWC) and from world coordinates to the light coordinate system (WCtoLC). The corresponding matrices for transforming normals between the same coordinate systems are the inverse transpose matrices (MCtoWCit and WCtoLCit).

Listing 12.6. Überlight vertex shader

uniform vec3 WCLightPos;       // Position of light in world coordinates
uniform vec4 ViewPosition;     // Position of camera in world space
uniform mat4 WCtoLC;           // World to light coordinate transform
uniform mat4 WCtoLCit;         // World to light inverse transpose
uniform mat4 MCtoWC;           // Model to world coordinate transform
uniform mat4 MCtoWCit;         // Model to world inverse transpose

varying vec3 LCpos;            // Vertex position in light coordinates
varying vec3 LCnorm;           // Normal in light coordinates
varying vec3 LCcamera;         // Camera position in light coordinates

void main()
    gl_Position = ftransform();

    // Compute world space position and normal
    vec4 wcPos = MCtoWC * gl_Vertex;
    vec3 wcNorm = (MCtoWCit * vec4(gl_Normal, 0.0)).xyz;

    // Compute light coordinate system camera position,
    // vertex position and normal
    LCcamera = (WCtoLC * ViewPosition).xyz;
    LCpos = (WCtoLC * wcPos).xyz;
    LCnorm = (WCtoLCit * vec4(wcNorm, 0.0)).xyz;

12.4.3. Fragment Shader

With the key values transformed into the lighting coordinate system for the specified light source, the fragment shader (Listing 12.7) can perform the necessary lighting computations. One subroutine in this shader (superEllipseShape) computes the attenuation factor of the light across the cross section of the beam. This value is 1.0 for fragments within the inner superellipse, 0 for fragments outside the outer superellipse, and a value between 0 and 1.0 for fragments between the two superellipses. Another subroutine (distanceShape) computes a similar attenuation factor along the direction of the light beam. These two values are multiplied together to give us the illumination factor for the fragment.

The computation of the light reflection is done in a manner similar to shaders we've examined in previous chapters. Because the computed normals may become denormalized by linear interpolation, we must renormalize them in the fragment shader to obtain more accurate results. After the attenuation factors are computed, we perform a simple reflection computation that gives a plastic appearance. You could certainly modify these computations to simulate the reflection from some other type of material.

Listing 12.7. Überlight fragment shader

uniform vec3 SurfaceColor;

// Light parameters
uniform vec3 LightColor;
uniform vec3 LightWeights;

// Surface parameters
uniform vec3 SurfaceWeights;
uniform float SurfaceRoughness;
uniform bool AmbientClamping;

// Super ellipse shaping parameters
uniform bool BarnShaping;
uniform float SeWidth;
uniform float SeHeight;
uniform float SeWidthEdge;
uniform float SeHeightEdge;
uniform float SeRoundness;

// Distance shaping parameters
uniform float DsNear;
uniform float DsFar;
uniform float DsNearEdge;
uniform float DsFarEdge;

varying vec3 LCpos;          // Vertex position in light coordinates
varying vec3 LCnorm;         // Normal in light coordinates
varying vec3 LCcamera;       // Camera position in light coordinates

float superEllipseShape(vec3 pos)
   if (!BarnShaping)
       return 1.0;

       // Project the point onto the z = 1.0 plane
      vec2 ppos = pos.xy / pos.z;
      vec2 abspos = abs(ppos);

      float w = SeWidth;
      float W = SeWidth + SeWidthEdge;
      float h = SeHeight;
      float H = SeHeight + SeHeightEdge;

      float exp1 = 2.0 / SeRoundness;
      float exp2 = -SeRoundness / 2.0;

      float inner = w * h * pow(pow(h * abspos.x, exp1) +
                                pow(w * abspos.y, exp1), exp2);
      float outer = W * H * pow(pow(H * abspos.x, exp1) +
                                pow(W * abspos.y, exp1), exp2);

      return 1.0 - smoothstep(inner, outer, 1.0);
float distanceShape(vec3 pos)
   float depth;

   depth = abs(pos.z);

   float dist = smoothstep(DsNear - DsNearEdge, DsNear, depth) *
                (1.0 - smoothstep(DsFar, DsFar + DsFarEdge, depth));
   return dist;

void main()
      vec3 tmpLightColor = LightColor;
      vec3 N = normalize(LCnorm);
      vec3 L = -normalize(LCpos);
      vec3 V = normalize(LCcamera-LCpos);
      vec3 H = normalize(L + V);
      vec3 tmpColor = tmpLightColor;

      float attenuation = 1.0;
      attenuation *= superEllipseShape(LCpos);
      attenuation *= distanceShape(LCpos);

      float ndotl = dot(N, L);
      float ndoth = dot(N, H);

      vec3 litResult;

      litResult[0] = AmbientClamping ? max(ndotl, 0.0) : 1.0;
      litResult[1] = max(ndotl, 0.0);
      litResult[2] = litResult[1] * max(ndoth, 0.0) * SurfaceRoughness;
      litResult *= SurfaceWeights * LightWeights;

      vec3 ambient = tmpLightColor * SurfaceColor * litResult[0];
      vec3 diffuse = tmpColor * SurfaceColor * litResult[1];
      vec3 specular = tmpColor * litResult[2];
      gl_FragColor = vec4(attenuation *
                         (ambient + diffuse + specular), 1.0);

An example of using this shader is shown in Color Plate 20, along with a screen shot of a user interface designed by Philip Rideout for manipulating its controls. The überlight shader as described by Barzel and Gritz actually has several additional features. It can support multiple lights, but our example shader showed just one for simplicity. The key parameters can be defined as arrays, and a loop can be executed to perform the necessary computations for each light source. In the following chapter, we show how to add shadows to this shader.

Previous Page
Next Page

JavaScript EditorAjax Editor     JavaScript Editor