Open Scene Graph States and StateSets part 1

Everything You Always Wanted to Know About StateSets (But Were Afraid to Ask)

by Tim Moore

One of the most important optimizations that Open Scene Graph performs is minimizing the number of changes to OpenGL graphics state. OpenGL is defined as a complex state machine, and its state contains everything that affects how polygons are rendered. The values that make up the state range from very simple on/off switches, such as enabling the depth test or lighting, to complex structures, like textures and shader programs, that may occupy thousands or millions of bytes of memory on the graphics processor. Even though the OpenGL driver may not need to retransfer all that data to the graphics card to change state, it can still be very expensive. Generally all rendering on the GPU has to come to a halt when the state changes. The setup time for changing a shader or texture can be quite long.

A 3D scene can contain thousands of different objects, but many of them will share the same graphics state. If objects with the same state can be rendered together, then redundant state changes will be avoided and the number of state changes will be minimized. The objects could be organized this way by the program, but in a scene graph system like Open Scene Graph the objects are grouped in a logical and spatial hierarchy that does not necessarily have anything to do with the graphics state used by the objects.

In part 1 of this two-part series we'll look at how Open Scene Graph represents OpenGL graphics state. Part 2 will explore some of the ways that Open Scene Graph optimizes rendering to minimize the number of state changes.

Drawable and StateSet

In Open Scene Graph, geometry is stored in the leaves of the scene graph in Drawable objects. For rendering, Drawable presents a simple interface: a draw() function. When draw() is called, the OpenGL model-view matrix and all other rendering state must be set up correctly. At every video frame, Open Scene Graph traverses the scene graph. In a process called "culling," it determines what Drawable objects are visible, records the model-view matrix and the graphics state, and stores them in lists called RenderBins. The contents of some RenderBins will be sorted by distance from the eye point, but most Drawables will be grouped by graphics state. Then, in the "draw" process, all the Drawables will be rendered in order.

How does OSG represent the graphics state and recognize that objects have the same state? If you have done any OSG programming you know that you don't specify the entire graphics state for an object in the scene graph; instead, you create an object called a StateSet that specifies a very small part of the OpenGL state and attach it to a node in the scene graph. Figure 1 shows a representation of a scene graph. Drawable objects containing geometry are represented by the polyhedra at the bottom. They are help in Geode objects, which are in turn the children of Transform objects. StateSets that set textures and material properties are attached to several of the nodes in the scene graph. The StateSet containing the roof tile texture is shared by two nodes in the graph.

Figure 1. A simple scene graph.

The StateSet might specify textures, material properties for lighting, uniform parameters for shaders, and some OpenGL mode changes such as enabling blending and alpha testing. A StateSet can contain two kinds of values, modes and pointers to StateAttribute objects. A mode is an integer constant that controls an OpenGL capability when passed to the glEnable() and glDisable() functions. A StateAttribute is a value or collection of values in the OpenGL state. Attributes correspond directly to parameters in the OpenGL state machine, but some OpenGL parameters may be grouped into a single StateAttribute, and the StateAttribute may have members that don't correspond to any real OpenGL parameters. An example of this is the Material state attribute, which groups the ambient, diffuse, specular, and emission material properties into one object, even though each of these parameters can be changed independently in OpenGL. Material also has a setAlpha() convenience method to change the alpha values of all the material parameters in one call; this must be done by hand when using raw OpenGL. The internal representation of a StateSet is quite dynamic; from the point of view of a StateSet, there aren't any predefined modes, and only a general system of state attribute types is known. Texture modes and attributes are stored in the StateSet according to the texture unit number.

Figure 2. osg::StateSet
Any node in the scene graph, from the root of the scene down to the Drawables at the leaves of the graph, may have an associated StateSet. The OpenGL state in effect when a Drawable is actually rendered is the composition of the default state and the modes and attributes set in the StateSet objects in the Drawable's ancestors in the scene graph. Attributes set lower in the tree take precedence over those set in StateSet objects higher in the tree, although there is a system whereby an attribute can override the value of an attribute that is set lower in the graph and, conversely, an attribute can protect itself from being overridden.

osg::State

OSG manages the global OpenGL state with a State object. You will not normally encounter this object unless you need to write custom Drawable classes with their own drawImplementation(). methods, but Open Scene Graph uses it extensively when rendering the scene. State uses the same modes and state attributes as StateSet does in order to make OpenGL calls to set the graphics state. The current state is changed using StateSet objects in a way designed to minimize the number of low-level OpenGL calls that will be made. It is important to note that state attribute equality is only based on the identity, or pointer value, of the StateAttribute objects. State doesn't "look inside" the state attributes or call a compare() method on them.

StateSets and the StateSet Stack

As mentioned before, a StateSet. is a small collection of modes and attributes that should be applied to the current graphics state. State maintains a stack of StateSet objects, and the main public interface for changing the graphics state are pushStateSet(), popStateSet(), and apply() methods. Internally, State maintains stacks for all the modes and attribute types. When pushStateSet() or popStateSet() are called, the StateSet is traversed and the individual mode and attribute stacks are pushed and popped. It's important to note that pushStateSet() and popStateSet() don't actually change the OpenGL state. You must call apply() to force the OpenGL state to be consistent with the current tops of all the mode and attribute stacks. As individual modes and attributes are pushed and popped, the State object does some book-keeping to note whether the values on the tops of the stacks have really changed. The apply() method then updates only the OpenGL modes and attributes that have actually changed. This lazy updating allows several StateSet objects to be pushed and popped before actually changing the OpenGL state, which very well might be similar or identical to the original state. You might imagine the StateSet stack modifications happening as the scene graph is traversed. The Open Scene Graph rendering traversal doesn't actually do this directly, but it does do a similar series of "movements" from State to State, pushing and popping StateSet objects in a stack-like manner.

Figure 3. osg::State
There are some convenience methods for dealing with StateSet objects that you might see in OSG code. State::apply(const StateSet*) pushes a StateSet, calls apply() and then pops the StateSet all in one go. State::insertStateSet() pops a number of StateSets, inserts a new StateSet, then pushes the old ones back. This and other similar methods are used for various effects in the rendering traversal. There are also applyMode() and applyAttribute() methods that make the change and mark the parameter's stack as changed, even though nothing is pushed or popped. This will force that parameter to be properly updated on the next call to apply.

Using State in Custom Drawables

As stated earlier, you probably won't use the State object in normal OSG code, but you will if you write your own Drawable classes with custom drawImplementation() methods. For examples of using State, look at the source code to an Open Scene Graph class like osgText::Text, which makes many OpenGL calls in its draw method. In addition to managing the graphics state held in StateSet objects, State also controls lower level OpenGL state, such as the use of vertex attribute arrays. State also has member functions for calling OpenGL functions that might be implemented as extensions.

You should use the state management functions in your own code instead of the lower level OpenGL equivalents; otherwise, the OpenGL state will become inconsistent with the State object's model, resulting in visual artifacts or even a crash. The alternative is to save all OpenGL state in your function using the glPushAttrib() and glPushClientAttrib() functions, run your graphics code and restore the state using glPopClientAttrib() and glPopAttrib(). This is slow, but there might not be an alternative if wrapping legacy code.

Next Time...

Part 2 describes the StateGraph class. This class is used by Open Scene Graph when rendering the scene and actually pushes and pops StateSet objects as we've described here.