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.
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.
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.