OpenGLContext Rendering Process

This document describes the original process OpenGLContext used to render a screen. It describes the RenderPass, RenderVisitor, and Visitor nodes, as well as their interactions with the Context and scenegraph-package nodes.

Note: This discussion is of the "legacy" rendering pattern in OpenGLContext, this mechanism has been replaced by a new "flat" rendering pattern which does not rely on OpenGL transformations or select render modes.

The Visitor Pattern in OpenGLContext

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.  Nodes may define a renderedChildren( nodetypes) method to specify what nodes should be considered their children for the purposes of visitation.

The registered vmethod (if it exists) 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).

The difference between the classic computer science pattern and the OpenGLContext pattern is that in the computer science pattern, the particular node 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).  At the moment, this functionality has been disabled, as providing it was introducing too much of a slowdown in the rendering process.  The new, simpler, renderedChildren API provides considerably better performance.

RenderVisitor -- Default Rendering Implementation

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.

RenderPass -- Rendering Passes/Modes

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

Triggering the RenderPass

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.

  1. event handler for the Context object, such as wxOnPaint for the wxPython Context sub-classes calls self.triggerRedraw(1) to force a redraw of the Context
  2. Context.triggerRedraw sets the "alreadyDrawn" flag to false, which tells the context that it needs to be redrawn at the next available opportunity, if not able to immediately draw, sets the redrawRequest event.
    1. at the next available opportunity (which may be within the triggerRedraw method, depending on the threading status and/or whether or not we are currently in the middle of rendering), the context's OnDraw method will be called
  3. Context.OnDraw
    1. performs an event cascade (calls the DoEventCascade customization point while the scenegraph lock is held (by default this does nothing))
    2. sets this Context instance as the current context
      1. acquires the OpenGLContext contextLock
      2. does the appropriate GUI library set current call
    3. clears the redrawRequest event
    4. calls the Context's renderPasses attribute (normally an instance of the PassSet object discussed above) with the Context object as argument, receiving a flag specifying whether there was a visible change
      1. if there was a change, swaps buffers
    5. finally, un-sets the current context
  4. PassSet.__call__
    1. instantiates a new OverallPass object with pointers to the three sub-passes as an argument
    2. returns the result of calling the new OverallPass with the passed Context object
  5. OverallPass.__init__
    1. stores various matrices
    2. sets up global structures (see above)
    3. instantiates each sub-pass (with a pointer to the OverallPass, and the passCount for the given sub-pass)
  6. OverallPass.__call__
    1. iterates through the list of sub-pass instances calling each one in turn and tracking whether it reports a visible change
    2. returns the sum of all the "changed" values (which is in turn returned by the PassSet to the Context's OnDraw method)
  7. (Visiting)RenderPass.__call__
    1. generally checks whether the pass should be rendered or not (shouldDraw method)
    2. if not, returns false (no visible change)
    3. otherwise performs the pass-specific rendering and returns it's "visible" flag

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.