This document describes the process OpenGLContext uses to render a screen. It describes the RenderPass, RenderVisitor, and Visitor nodes, as well as their interactions with the Context and scenegraph-package nodes.
OpenGLContext 2.0 uses a modified "visitor"/traversal pattern (the
Visitor pattern is a common Computer Science pattern) to implement the
rendering process. This pattern allows us to replace the entire
rendering mechanism merely by replacing or specialising the classes
which implement this pattern. It also localises most of the code
executed during a rendering pass in a small number of modules, which
makes understanding the code easier.
The idea of a visitor pattern (I will only discuss the OpenGLContext
pattern, not the C.S. one) is that a visitor object is created which
traverses a graph (a scenegraph in our case). For each node in the
graph the visitor determines the appropriate methods to run for the
given node, and then determines what set of children (if any) should be
visited for the node.
In concrete terms, the visitor pattern is implemented by the
OpenGLContext.visitor module. The Visitor class has two points of
customization which are involved in the normal rendering pass
implementation. The vmethods system is used to determine the set
of methods to be run for a given node, while the childrenMethods system
is used to register methods for determining the children of a given node.
The primary difference between the two systems is that a registered
vmethod is called for every registered class of the node's __mro__
field. To give an example, the __mro__ of the standard Transform
node is:
and the standard RenderVisitor class registers vmethods for
vrml.vrml97.nodetypes.Transforming and vrml.vrml97.nodetypes.Grouping.
So that a Transform node encountered during traversal will have
both registered methods called (in the method resolution order).
In contrast, the childrenMethod system only calls the first registered method found in the
method resolution order. This distinction is not central to the design
of the visitor pattern (it would be just as possible to create a
super-like function which specifies whether or not to call the
super-class vmethod), but is, rather a historic artifact.
The difference between the classic computer
science pattern and the OpenGLContext pattern is that in the computer
science pattern, the particular note is responsible for dispatching the
visitor to each of its children. Because OpenGLContext generally
tries to minimize and/or eliminate rendering-specific code in the
scenegraph nodes, I introduced the concept of "virtual methods" for any
given scenegraph node which would be held by the visitor (primarily for
convenience).
Because there are a number of different possible traversal patterns (for instance, searching for a particular node in the hierarchy may wish to ignore Switch node's whichChoice value to allow for finding "switched off" content), I added "virtual children" methods as well. This resulted in completely eliminating the purpose of the original pattern (a node accepts visitors and decides how to dispatch them), bringing us much closer to a pure "traversal" pattern. Refactoring to use a simpler traversal pattern may occur in the future.
For most scenegraph nodes, there is a core functionality which is
common across almost all rendering modes. For instance:
This common rendering code is localized in the
OpenGLContext.rendervisitor module. This module defines a
sub-class of the visitor.Visitor class with registered functions for
implementing an "default" rendering pass (loosely an opaque rendering
pass, but with specializations based on the attributes of the
RenderPass node which allow for common optimizations (such as disabling
lighting on non-visible rendering passes), and support other common
rendering modes, such as Selection).
Let's take a look at a concrete example from the RenderVisitor
module:
def Transform( self, node ):
"""Render a transform object, return a finalisation token"""
assert hasattr( node, "transform"), """Transforming node %s does not have a transform method"""%( self.__class__ )
if self.transform:
glMatrixMode(GL_MODELVIEW)
try:
glPushMatrix() # should do checks here to make sure we're not going over limit
except GLerror, error:
matrix = glGetDouble( GL_MODELVIEW_MATRIX )
node.transform()
return TransformOverflowToken( matrix)
else:
node.transform ()
return TransformPopToken
return None
This method is registered to be called for all
vrml.vrml97.nodetypes.Transforming nodes. The single argument to
the function is the node being processed. The method checks
whether the current RenderPass (RenderVisitor) node's attribute
"transform" is true, and if it is, sets about applying the node's
transform() method within a child matrix. The return value from
the function is a "token" which is a callable object which will be
called in a "finally" block after the processing of the Transforming
node, and all of its children, has completed. In this case, the
tokens restore the previous matrix, either by calling glPopMatrix() or
manually restoring the matrix if the model view matrix stack has
overflowed.
The same general pattern is used throughout the RenderVisitor module. The registered methods determine whether they are applicable to the current rendering mode/pass, and if so, apply code which is as generic as possible to allow as much reuse as possible. As you will note from the example, there are cases where I've introduced methods to the nodes to perform particular operations (such as the .transform() method). Again, I've tried to keep these methods as general as possible to allow for reuse.
In the case of the Context and/or Scenegraph, there is more logic
required. OpenGLContext has two major modes of operation.
One question which is sure to come up, is how to Render scenegraph nodes while operating in the first mode. You can mode.visit( node ) on the rendering mode/pass passed to Context.Render( mode ). Note, however, that you should not do this for Scenegraph nodes, as they will likely interfere with the Context's already-processed Lights, Background, and Viewpoint customization points.
You'll find throughout the OpenGLContext project that the RenderPass objects are referred to as "modes" or "mode". This is because the original OpenGLContext project had two separate objects, the mode (implementation of a particular rendering algorithm) and the pass (data storage for a particular iteration of the mode). When version 2.0 unified two objects into the single RenderPass class, I considered changing all instances of "mode", but as the obvious equivalent "pass" is a Python keyword, I decided to leave the original name. When reading, you can substitute mode and pass for each other with no loss of meaning under OpenGLContext 2.0.
The OpenGLContext.renderpass module defines the default rendering
passes used by OpenGLContext. There are two major sub-types of
rendering pass:
All of the rendering passes derive a set of meta-data attributes
from the RenderPass class, these include:
In addition, there is the concept of an overall rendering pass,
rather unimaginatively named "OverallPass". Individual rendering passes
will defer to the overall rendering pass for attribute lookups which
fail. The OverallPass object manages dispatching to its sub-passes.
This allows us to replace the OverallPass's logic with custom code if
we need, for instance, specialized handling of particular sub-passes.
This flexibility is used to good effect in the stencil-shadow-rendering sub-package, where it is necessary to create new rendering passes for every active Light node. See OpenGLContext.shadow.passes for the code.
As a result, each individual rendering pass inherits the following
meta-meta-data:
Note: the Context object provides facilities not explicitly enumerated above, which allows for considerable flexibility during the rendering process.
The PassSet object is a simple specialization of the list type which
instantiates a given OverallPass with a list of sub-passes and returns
the value obtained by calling the new instance with the passed context
as argument. Each Context object has an associated PassSet object which
it uses to render itself.
The default OverallPass Set includes the following rendering passes
(in this order):
Together these define a fairly decent subset of the "normal"
rendering processes for a (naive*) OpenGL rendering environment.
* Naive because the implementation does nothing to optimize the rendering of the scenegraph, and is as a result, not particularly usable for complex content
So let's take a look at how the rendering process is triggered, from
the moment the GUI library sends the "OnPaint" or equivalent event to
the Context through to the calling of an individual RenderPass.
So, at this point, the RenderPass has been called, we are within the Context thread (with the lock held), and the OverallPass is initialised. Each RenderPass defines it's own __call__ method, but the ones likely of the most interest are the ones derived from VisitingRenderPass.