Using VRML97 with OpenGLContext

This document describes the (partial) VRML97 implementation provided by the OpenGLContext and vrml packages.  The VRML97 ISO standard is a large and complex specification, and the OpenGLContext implementation supports only a small subset of its functionality, hopefully a useful subset.  You can load, render and re-save scenegraph's from VRML97 files, as well as render a number of the more common nodes.

What is Implemented?

OpenGLContext is only a partial implementation of VRML97.  It provides a subset targeted at the output of common 3-D modelers. In particular, the nodes which are implemented (at least partially) by OpenGLContext are as follows:

In addition, OpenGLContext provides a partial implementation of the VRML97 NURBs extension proposal by Blaxxun Interactive:

And OpenGLContext provides a number of specialized nodes which are not part of the VRML97 specification:

Despite the small number of implemented nodes in OpenGLContext, the parser and scenegraph structures of the vrml package support the entire VRML97 specification, so although particular nodes may not be rendered, many scenes may be loaded which include nodes other than those in the lists above.

Loading VRML97 Files

Basic operation of the VRML97 loader is as follows:

from OpenGLContext.loaders.loader import Loader
scenegraph = Loader.load( myurl, baseURL=None )

The URL can be either a single or multi-value string (i.e. a list of strings).  If the base URL is not None, then the urls in myurl will be interpreted relative to the base URL.  If the URL is a filename, the file will be opened, read, potentially un-gzipped, parsed, and converted to a scenegraph instance.

The VRML97 parser is based on SimpleParse 2.0.0, and uses the "cut" production to raise SyntaxError's when a file isn't properly formed.  You can catch those, as well as IOErrors around the call to "load".  Note, however, that for certain resources (image textures at the moment), the actual loading of the textures is done in separate worker threads, which will print messages if errors are encountered during loading.

You can register a new prototype for use by the VRML97 loader by calling the standardPrototype function in the loader module with your new prototype as an argument:

vrml97.standardPrototype( myNewPrototype )

The prototype's name, as reported by vrml.protofunctions.name(prototype) will then be bound to your prototype during loading.  Note that you can replace any built-in prototype with this mechanism as well.

Manipulating Nodes Programmatically

Basic operations with a scenegraph...

# Get a particular node-instance by it's VRML DEF name
myNamedNode = scenegraph.getDEF( "My-Named-Node" )
# Give an instance a new VRML DEF name
scenegraph.regDefName( "My-New-Name", myNamedNode )
# Get a prototype from the scenegraph's namespace
prototype = scenegraph.getProto( "Transform" )

The protofunctions module provides for "safe" manipulation and query of node and prototype structures.  Although you can normally access a node's DEF name as node.DEF, it's root scenegraph as node.root, and it's field definition as node.__class__.fieldname, it is possible for a prototype to define a field "DEF" or "root" which shadows those convenience properties.  And using node.__class__ just isn't recommended anywhere.

# get the root scenegraph for a node
scenegraph = protofunctions.root( node )
# get a particular field definition for a node/prototype by name
field = protofunctions.getField( node, 'name' )
field = protofunctions.getField( proto, 'name' )
# get all fields for a node/prototype
fields = protofunctions.getFields( node )
fields = protofunctions.getFields( proto )

Sample of interacting with a VRML node...

from OpenGLContext.scenegraph import basenodes
transform = basenodes.Transform (
translation = [0, 1,0],
rotation = [0, 1,0,3.14159],
children = [
basenodes.Group ()
],
)
print "Transform Translation", transform.translation
transform.translation = [2,3,4]
print "After Alteration", transform.translation
for child in transform.children:
print child

Field-type equivalents.  Note that there is coercian support...

VRML FieldType
SFField Python Data Type
MFField Python Data Type
[SF/MF]Int32
int
Numeric-Python int array
[SF/MF]Float
float
Numeric-Python double array
[SF/MF]String
string (interpreted as UTF-8 unicode)
list of strings
[SF/MF]Time
float (time-module float)
Numeric-Python double array
SFBool
int (0/1)

[SF/MF]Vec2f
2-item Numeric-Python double array x*2 Numeric-Python double array
[SF/MF]Color, [SF/MF]Vec3f
3-item Numeric-Python double array
x*3 Numeric-Python double array
[SF/MF]Rotation
4-item Numeric-Python double array
x*4 Numeric-Python double array
SFImage
Numeric-Python int array

The Scenegraph Cache

Most rendering nodes in OpenGLContext are written to take advantage of the built-in scenegraph cache.  The cache is used to store partial rendering solutions (for example, tessellations of vertex-based geometry) in order to allow per-frame operations to proceed as quickly as possible.  The cache operates by watching for changes to fields on nodes to invalidate the cached data.  Each node can have any number of cached pieces of data.

If you are wanting to write your own rendering node, the IndexedLineSet node provides a good example of using the cache for storing a display-list where the cache should be invalidated based on a number of different fields of two different nodes.

Creating New Prototypes

If you would like to create a new node-type (prototype), you can either define the prototype using a VRML97 prototype or create it directly in Python.  Prototypes are implemented as Python classes inheriting from the vrml.node.Node class.  Normally you will also want to inherit from one of the marker classes in vrml.vrml97.nodetypes, which is what tells the scenegraph what roles your node can play in the graph.

You can find a very straightforward example of defining new node-types in OpenGLContext.scenegraph.extrusions, where three (non-standard) geometric node-types are added to the system.

To make your new node-type available to the VRML97 loader, you can call OpenGLContext.loaders.vrml97.standardPrototype( cls ) on your prototype class.  This allows nodes in your VRML97 files to reference the nodes as built-ins.

There is a lot of sample code for creating your own scenegraph nodes in the scenegraph sub-package.  If you run into difficulties, feel free to contact Mike about them.

Event Model

OpenGLContext has a more limited event model than that specified in VRML97.  It allows for routing, and provides basic event setting/getting with the ability to define handlers for the events, but it does not attempt to implement the non-deterministic VRML behaviour.

Each field on each node sends a PyDispatcher event on setting, deletion or routing to an event.  The field and event classes are both descriptors.  They provide all sorts of support machinery for working with the field, including support for copying the node, and generating prototype descriptions.  The field class has a very small C accelerator function which replaces the fget method (when available), without this performance will noticeably suffer.

You can define a method to be called before an event (but not a field) is set simply by defining a method named on_fieldname where fieldname is the name of the field.  There are few examples of this in OpenGLContext, as this is a new feature in version 2.0.0 final.  You will see it in the OpenGLContext.scenegraph.interpolators module.

ROUTE objects can be constructed either in VRML97 files, or by constructing the routes directly.  ROUTEs are just regular nodes with 4 fields, source, sourceField, destination, and destinationField.  On creation they are bound (i.e. they start routing between the source and destination).

Note that a ROUTE does not do a regular set on a field to which it is routed.  It sets the value, but instead of sending a ('set',destinationField) message, it sends a ('route',destinationField) message.  This allows code to distinguish between a user sending a message by setting a field and an event cascade setting the field.  The ROUTE machinery watches for the ('route',destinationField) messages in order to do continued forwarding.