Writing VRML Processing Scripts in Python

This document is an introduction to using the mcf.vrml libraries for creating VRML processing scripts and systems. Because of the considerable flexibility of the system, this document only scratches the surface of possible uses. If you find anything in this document or the system which does not work properly, or which doesn't seem fast enough, please contact me, I can't fix what I don't know about. I also can't promise I can fix anything, but I do make an effort in most cases.

Loading and Dumping a File

mcf.vrml will only load syntactically correct VRML 97 files. Normally, to load a VRML file you will use the convenience functions for file access located in the mcf.vrml.loader module:

from mcf.loader import loader
sceneGraph = loader.load( 'c:\\temp\\testfile1.wrl' ) # note use of double backslashes
if sceneGraph:
	loader.save( sceneGraph, 'c:\\temp\\testfile2.wrl' ) # file will be overwritten without warning

The loader.load function provides for url download, and gzip decoding of a url or filename.  If an error is encountered during the parsing of the file, you may receive an exception. The most common errors are mcf.vrml.parser.UnfinisedError (a syntax error caused less than the full file to be read), AttributeError, and TypeError.

loader.save is the complement of loader.load.  It attempts to save a file to disk (currently there is no url support for this) in UTF-8 format. 

Identity Transformation

The code for creating an "identity transformation", the element-by-element re-creation of a sceneGraph is minimal:

from mcf.vrml.walker import customwalker, vrmlwalker
def process( sg ):
	c = customwalker.CustomWalker(  ) # default
	w = vrmlwalker.VRMLWalker( custom= c )
	return w.walk( sg )

The process function takes a sceneGraph object as it's input.
It creates a CustomWalker object which describes how we want to navigate the various nodes of the sceneGraph. It then creates a default sceneGraph walker which uses the CustomWalker object to control its processing. Having created the walker object, the function tells it to walk the sceneGraph and return the result to whatever called the function.

Default Rules

Processing scripts can override the default action of the walker by specifying a "defaultrule" argument (which must be a callable object (see below for details)) to the VRMLWalker contructor.

def printer( curnode, **namedargs):
	'''
	A trivial processing rule body
	which does not recreate the node
	'''
	print curnode.__gi__ # __gi__ is the node-type of the node...
	return 1, None
...
w = vrmlwalker.VRMLWalker( custom= c, defaultrule=printer )

In doing this, the processing script takes over the responsibility for recreating this node if it needs the node recreated. The processing rule body (the defaultrule in this case) can call the CustomWalker object's methods to assist in the task of recreation:

def print_and_recreate( curnode, gi, app, parentlist, CustomWalker, transform, **namedargs):
	'''
	Less trivial rule body which both prints
	the __gi__ of the node and asks the CustomWalker
	to recreate the object in the "default" way.
	'''
	print curnode.__gi__ # as for the truly trivial version
	processchildren, newnode = CustomWalker.rebuild_node( parentlist, curnode, gi, transform)
	return processchildren, newnode

The CustomWalker also defines methods for calculating an object's "gi", and "childlist" and a "defaultrule" method which figures out what type of rebuilding mechanism to use for a particular type of node.

Note: the new node is returned as the second value. The system will register transform[ id(curnode)] = newnode if this value is not the None object. Without that registration, the default rebuilding process will fail.

Head Rules

Head rules are (strangely enough), rules with heads. A head is a condition which says "if x is true: choose my body as the only processing script to run when we first encounter this node." The rule bodies are the same as the rules used for default rule bodies. The rule heads are a list of required results to some set of (arbitrary) test rules which will be applied to every node in the DAG (A DAG is a more general version of a tree structure). You define both the tests and the required results based on your task-at-hand.

def get_gi( curnode, gi, **namedargs ):
	'''
	A trivial, but very common test function
	which returns the node-type (gi) of the
	current node.
	'''
	return gi # Note that it returns only the value to be matched!
def depth_over_4( curnode, gi, parentlist, **namedargs ):
	'''
	Returns whether the recursion depth of this node is
	greater than 4
	'''
	return len( parentlist ) > 4
w = vrmlwalker.VRMLWalker( custom= c, \
	defaultrule=c.defaultrule, 
	testfuncs= (get_gi, depth_over_4 ),
	transformrulelist = [
		( 'PositionInterpolator', 0, print_and_recreate),
		( 'ScalarInterpolator', 1, printer ),
		( 'Group', None, print_and_recreate)
	]
)

What those two rules say is:

( 'PositionInterpolator', 0, print_and_recreate)
	-> PositionInterpolator nodes which are not more than 4 levels deep in the node hierarchy 
		will have their gi printed as they are recreated
( 'ScalarInterpolator', 1, printer )
	-> ScalarInterpolators more than 4 levels deep in the node hierarchy 
		will have their gi's printed, 
		but will not be recreated in the new sceneGraph 
		(they have, effectively, been deleted) *see note
( 'Group', None, print_and_recreate)
	-> All Groups, regardless of their nesting level will have their gis printed as they are recreated

NOTE: The function printer (defined above) returns (1, None) . The 1 signals to the system that this node's children should not be processed. If we had returned a 0 instead, the system would attempt to process the children of the (deleted) node, and would raise exceptions when the default recreation functions looked for the parents of those nodes. Your code must trap such situations and implement whatever action you want taken when a parent cannot be found.

Post Rules

A post rule is a head rule which is executed when the processor finishes processing a node and all of its children. This allows you to be sure that the results of children's processing is available to the functions processing the parent node. Note, however, that, unless you also define a pre-rule which creates a target object, the children's rules will not have anything to which to append themselves. In practice this is not a huge limitation, as we can easily use a Python structure such as a list or dictionary which is created by the pre-rule and used by the post-rule (which looks it up in the transformation table) then discarded by registering a new target node.

The name of the argument for post-processing rules is "posttransformrulelist".

A Non-trivial Example

The CustomWalker

VRML 2.0 Nodes

sceneGraphs

Generic Nodes

ROUTE "Nodes"

Prototype "Nodes"

NULL Nodes

__Attr__ Nodes

Common Tasks

Splicing files together

Eliminating nodes and moving children to another node

Swapping the prototype of a node

Processing Environment

The processing environment is designed to be easy-to-use. It allows you to declare only those arguments you require for a particular function.

variable

function

curnode Pointer to the node currently being processed
parentlist A combination list/mapping structure which lets you do efficient "last parent of type y" queries.
ancestorlist As for parentlist, but only nodes whose gi is in your hierinterestedin mapping.
Walker Pointer to the active walker object
CustomWalker Pointer to the active CustomWalker object
root The root node of the DAG. If a value is to be returned, it must be set as the transformed value of the root node. (see transform below) -> transform[ id(root) ] = returnvalue
app Pointer to an "application" object as passed in to the walk method of the walker object. This object can be anything at all, as it is never used or altered by the processing system.
transform A mapping of original-node-id's to pointers-to-recreated-nodes. In other words, if you are recreating the scenegraph during processing, this mapping lets you take a pointer to the original node and find out what node in the new scenegraph corresponds to it. ID is a built-in function in Python id( object ) which returns a number which is unique for the lifetime of the object. To register a transformation, create a new entry in the mapping like this: transform[ id( oldobject)] = newobject Note: If you return a second value from your processing scripts which is not None, that value will be registered as the transform of the current node..
get_gi Pointer to a callable object which can retrieve the gi of any node in the DAG
get_childlist Pointer to a callable object which can retrieve the childlist of any node in the DAG
**env Any number of application-specific objects passed in to the walk method of the walker object as the "env" argument. Each element of this environment can be accessed either as an argument or as part of the "namedargs" argument set.
generation The current node's generation (including the curnode)
childno The current node's index in the generation, i.e. generation[ childno] is curnode, while generation[ :childno] is the set of siblings preceding this node and generation[ childno+1:] is the set of siblings after this node.

Back to mcf.vrml home...