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.