[ Previous Section | Next Section | Chapter Index | Main Index ]

Subsections
The Attribute Stack
Lights in Scene Graphs

Section 4.4

Lights and Materials in Scenes


In this section, we turn to some of the practicalities of using lights and materials in a scene and, in particular, in a scene graph.


4.4.1  The Attribute Stack

OpenGL is a state machine with dozens or hundreds of state variables. It can be easy for a programmer to lose track of the current state. In the case of transformation matrices, the OpenGL commands glPushMatrix and glPopMatrix offer some help for managing state. Typically, these methods are used when temporary changes are made to the transformation matrix. Using a stack with push and pop is a neat way to save and restore state. OpenGL extends this idea beyond the matrix stacks.

Material and light properties are examples of attributes. OpenGL has an attribute stack that can be used for saving and restoring attribute values. The push and pop methods for the attribute stack are defined in class GL:

public void glPushAttrib(int mask)
public void glPopAttrib()

There are many attributes that can be stored on the attribute stack. The attributes are divided into attribute groups, and one or more groups of attributes can be pushed onto the stack with a single call to glPushAttrib. The mask parameter tells which group or groups of attributes are to be pushed. Each call to glPushAttrib must be matched by a later call to glPopAttrib. Note that glPopAttrib has no parameters, and it is not necessary to tell it which attributes to pop -- it will simply restore the values of all attributes that were saved onto the stack by the matching call to glPushAttrib.

The attribute that most concerns us here is the one identified by the constant GL.GL_LIGHTING_BIT. A call to

gl.glPushAttrib( GL.GL_LIGHTING_BIT );

will push onto the attribute stack all the OpenGL state variables associated with lights and materials. The command saves material properties, properties of lights, settings for the global light model, the enable state for lighting and for each individual light, the settings for GL_COLOR_MATERIAL, and the shading model (GL_FLAT or GL_SMOOTH). All the saved values can be restored later by the single matching call to gl.glPopAttrib().

The parameter to glPushAttrib is a bit mask, which means that you can "or" together several attribute group names in order to save the values in several groups in one step. For example,

gl.glPushAttrib( GL.GL_LIGHTING_BIT | GL.GL_CURRENT_BIT );

will save values in the GL_CURRENT_BIT group as well as in the GL_LIGHTING_BIT group. You might want to do this since the current color is not included in the "lighting" group but is included in the "current" group. It is better to combine groups in this way rather than to use separate calls to glPushAttrib, since the attribute stack has a limited size. (The size is guaranteed to be at least 16, which should be sufficient for most purposes. However, you might have to be careful when rendering very complex scenes.)

There are other useful attribute groups. GL.GL_POLYGON_BIT, GL.GL_LINE_BIT, and GL.GL_POINT_BIT can be used to save attributes relevant to the rendering of polygons, lines, and points, such as the point size, the line width, the settings for line stippling, and the settings relevant to polygon offset. GL.GL_TEXTURE_BIT can be used to save many settings that control how textures are processed (although not the texture images). GL.GL_ENABLE_BIT will save the values of boolean properties that can be enabled and disabled with gl.glEnable and gl.glDisable.

Since glPushAttrib can be used to push large groups of attribute values, you might think that it would be more efficient to use the glGet family of commands to read the values of just those properties that you are planning to modify, and to save the old values in variables in your program so that you can restore them later. (See Subsection 3.1.4.) But in fact, a glGet command can require your program to communicate with the graphics card -- and wait for the response, which is the kind of thing that can hurt performance. In contrast, calls to glPushAttrib and glPopAttrib can be queued with other OpenGL commands and sent to the graphics card in batches, where they can be executed efficiently by the graphics hardware. In fact, you should always prefer using glPushAttrib/glPopAttrib instead of a glGet command when you can.

I should note that there is another stack, for "client attributes." These are attributes that are stored on the client side (that is, in your computer's main memory) rather than in the graphics card. There is really only one group of client attributes that concerns us:

gl.glPushClientAttrib(GL.GL_CLIENT_VERTEX_ARRAY_BIT);

will store the values of settings relevant to using vertex arrays, such as the values for GL_VERTEX_POINTER and GL_NORMAL_POINTER and the enabled states for the various arrays. This can be useful when drawing primitives using glDrawArrays and glDrawElements. (Subsection 3.4.2.) The values saved on the stack can be restored with

gl.glPopClientAttrib();

Now, how can we use the attribute stack in practice? It can be used any time complex scenes are drawn when you want to limit state changes to just the part of the program where they are needed. When working with complex scenes, you need to keep careful control over the state. It's not a good idea for any part of the program to simply change some state variable and leave it that way, because any such change affects all the rendering that is done down the road. The typical pattern is to set state variables in the init() method to their most common values. Other parts of the program should not make permanent changes to the state. The easiest way to do this is to enclose the code that changes the state between push and pop operations.

Complex scenes are often represented as scene graphs. A scene graph is a data structure that represents the contents of a scene. We have seen how a screen graph can contain basic nodes representing geometric primitives, complex nodes representing groups of sub-nodes, and nodes representing viewers. In our scene graphs so far, represented by the package simplescenegraph3d, a node can have an associated color and if that color is null then the color is inherited from the parent of the node in the scene graph or from the OpenGL environment if it has no parent.

A more realistic package for scene graphs, scenegraph3d, adds material properties to the nodes and adds a node type for representing lights. All the nodes in a scene graph are represented by sub-classes of the class SceneNode3D from the scenegraph3d package. This base class defines and implements both the transforms and the material properties for the nodes in the graph. Now, instead of just the current drawing color, there are instance variables to represent all the material properties -- ambient color, diffuse color, specular color, emission color, and shininess. The SceneNode3D class has a draw() method that manages the transforms and the material properties (and calls another method, basicDraw() to do the actual drawing). Here is the method:

final public void draw(GL gl) {
    boolean hasColor = (color != null) || (specularColor != null) 
               || ambientColor != null || diffuseColor != null 
               || emissionColor != null || shininess >= 0;
    if (hasColor) {
       gl.glPushAttrib(GL.GL_LIGHTING_BIT | GL.GL_CURRENT_BIT);
       if (color != null)
          gl.glGetFloatv(GL.GL_CURRENT_COLOR, color, 0);
       if (ambientColor != null)
          gl.glMaterialfv(GL.GL_FRONT_AND_BACK, GL.GL_AMBIENT, ambientColor, 0);
       if (diffuseColor != null)
          gl.glMaterialfv(GL.GL_FRONT_AND_BACK, GL.GL_DIFFUSE, diffuseColor, 0);
       if (specularColor != null)
          gl.glMaterialfv(GL.GL_FRONT_AND_BACK, GL.GL_SPECULAR, specularColor, 0);
       if (emissionColor != null)
          gl.glMaterialfv(GL.GL_FRONT_AND_BACK, GL.GL_EMISSION, emissionColor, 0);
       if (shininess >= 0)
          gl.glMateriali(GL.GL_FRONT_AND_BACK, GL.GL_SHININESS, shininess);
    }
    gl.glPushMatrix();
    gl.glTranslated(translateX, translateY, translateZ);
    gl.glRotated(rotationAngle, rotationAxisX, rotationAxisY, rotationAxisZ);
    gl.glScaled(scaleX, scaleY, scaleZ);
    drawBasic(gl);  // Render the part of the scene represented by this node.
    gl.glPopMatrix();
    if (hasColor)
       gl.glPopAttrib();
 }

This method uses glPushAttrib and glPopAttrib to make sure that any changes that are made to the color and material properties are limited just to this node. The variable hasColor is used to test whether any such changes will actually be made, in order to avoid doing an unnecessary push and pop. The method also uses glPushMatrix and glPopMatrix to limit changes made to the transformation matrix. The effect is that the material properties and transform are guaranteed to have the same values when the method ends as they did when it began.


4.4.2  Lights in Scene Graphs

We have seen that the position of a light is transformed by the modelview matrix that is in effect when the position is set, and a similar statement holds for the direction of a spotlight. In this way, lights are like other objects in a scene. The contents of a scene are often represented by a scene graph, and it would be nice to be able to add lights to a scene graph in the same way that we add geometric objects. We should be able to animate the position of a light in a scene graph by applying transformations to the graph node that represents the light, in exactly the same way that we animate geometric objects.

We could, for example, place a light at the same location as a sun or a streetlight, and the illumination from the light would appear to come from the visible object that is associated with the light. If the sun moves during an animation, the light can move right along with it.

There is one way that lights differ from geometric objects: Geometric objects can be rendered in any order, but lights have to be set up and enabled before any of the geometry that they illuminate is rendered. So we have to work in two stages: Set up the lighting, then render the geometry. When lights are represented by nodes in a scene graph, we can do this by traversing the scene graph twice. During the first traversal, the properties and positions of all the lights in the scene graph are set, and geometry is ignored. During the second traversal, the geometry is rendered and the light nodes are ignored.

This is the approach taken in the scene graph package, scenegraph3d. This package contains a LightNode class. A LightNode represents an OpenGL light. The draw() method for a scene graph, shown above, does a traversal of the graph for the purpose of rendering geometry. There is also a turnOnLights() method that does a traversal of the scene graph for the purpose of turning on lights and setting their properties, while making sure that they are subject to all the transformations that are applied in the scene graph. Here is an applet that demonstrates the use of LightNodes in scene graphs.

There are four lights in the applet, one inside a sun that rotates around the "world," one in a moon that does likewise, and two spotlights in the headlights of a cart. The headlights come on only at night (when the sun is on the other side of the world). There is also a light that illuminates the scene from the position of the viewer and that can be turned on and off using a control beneath the display area. The viewpoint light is not part of the scene graph. The source code can be found in MovingLightDemo.java.

One issue when working with lights in OpenGL is managing the limited number of lights. In fact, the scene graphs defined by the package scenegraph3d can handle no more than eight lights; any lights in the scene graph beyond that number are ignored. Lights in the scene graph are assigned one of the light constants (GL_LIGHT0, GL_LIGHT1, and so on) automatically, as the graph is traversed, so that the user of the scene graph does not have to worry about managing the light constants. One interesting point is that the light constants are not simply assigned to LightNodes, and indeed it would be impossible to do so since the same LightNode can be encountered more than once during a single traversal of the graph. For example, in the MovingLightDemo program, the two headlights of the cart are represented by just one LightNode. There are two lights because the node is encountered twice during a graph traversal; the lights are in different locations because different transformations are in effect during the two encounters. A different light constant is used for each encounter, and each adds a new OpenGL light to the scene.

For more details about how lights are managed, see the methods turnOnLights and turnOnLightsBasic in scenegraph3d/SceneNode3D.java and the implementation of turnOnLightsBasic in scenegraph3d/LightNode.java. It's also worth looking at how scene graphs are used in the sample program MovingLightDemo.java.

By the way, that example also demonstrates some of the limitations of OpenGL lighting. One big limitation is that lights in OpenGL do not cast shadows. This means that light simply shines through objects. This is why is is possible to place a light inside a sphere and still have that light illuminate other objects: The light simply shines through the surface of the sphere. That's not such a bad thing, but when the sun in the applet is below the world, it shines up through the ground and illuminates objects in the scene from below. The effect is not all that obvious. You have to look for it, but even if you don't notice it, it makes things look subtly wrong. Problems with spotlights are also apparent in the example. As mentioned previously, you don't get a neat circle of light from a spotlight unless the polygons that it illuminates are very small. As a result, the illumination from the headlights flickers a bit as the cart moves. And if you look at the boundary of the circle of illumination from a headlight on an object made up of large polygons, you'll see that the boundary can be very jagged indeed. For example, consider this screenshot of a cylinder whose left side is illuminated by one of the spotlights in the program:


[ Previous Section | Next Section | Chapter Index | Main Index ]