Computer Graphics Workshop '97 Lecture Notes


Today's topics
Reading/writing Open Inventor scene graphs

One of the other reasons for using Open Inventor-style fields in nodes is that they provide a consistent way of reading and writing the contents of nodes to data files. Open Inventor provides relatively straightforward facilities for reading and writing scene graphs to files in the Open Inventor file format, which have the suffix ".iv". however, performing these functions still requires a few lines of boilerplate code, so we have written simple wrapper functions to assist you in doing this.

Assume you have a scene graph rooted at the node "root". Then to write this scene graph out to the file "scene.iv", you would do the following:

(write-to-inventor-file root "scene.iv")
To read in a file named "scene-in.iv" and define the root node to be "root-in", you would do the following:
(define root-in (read-from-inventor-file "scene-in.iv"))
Let's examine what these procedures do to get a feeling for how functions like this would be performed in Open Inventor.
(define read-from-inventor-file
 (let ((my-scene-input (new-SoInput)))
  (lambda (filename)
   (if (= 0 (-> my-scene-input 'openFile filename))
	 (display "Cannot open file ")
	 (display filename)
	 (display "\n")
	 (SoSeparator-cast (void-null)))
	 (let ((my-graph (SoDB::readAll my-scene-input)))
	   (if (equal? my-graph (SoSeparator-cast (void-null)))
		 (display "Problem reading file\n")
		 (SoSeparator-cast (void-null)))
		 (-> my-scene-input 'closeFile)
When the function read-from-inventor-file is defined, it creates a local variable my-scene-input which is an object of type SoInput. This object is used to open the file for reading, and is passed as argument in the call to SoDB::readAll. This is a function of the scene database which does the following: if one scene graph is defined in the .iv file and its root node is a separator, it reads in this scene graph and returns a pointer to the root node; otherwise, it reads in all the scene graphs defined inside the file and places them under a newly created root separator node, which is then returned. Note the several levels of error checking; if the function fails, it will return a NULL pointer of type SoSeparator. You can check programmatically whether the call to read-from-inventor-file has failed by writing
(define root (read-from-inventor-file "scene.iv"))
(if (equal? root (SoSeparator-cast (void-null)))
    (display "Read failed!")
    (display "Read succeeded."))
Note that this function reuses the same SoInput object over and over again; this saves time and space that would be consumed by memory allocation and garbage collection.

As you might expect, there is an analogue of SoInput used for file output, which is called SoOutput. The procedure for writing a scene graph is as follows: construct the output object and open the destination file, create a write action object, tell this action to use the output object as its destination, and apply the action to the root node of the scene graph. The implementation is then straightforward:

(define (write-to-inventor-file root filename)
  (let* ((out (new-sooutput))
	 (wa (new-sowriteaction out)))
    (if (= 1 (-> out 'openfile filename))
	  (-> wa 'apply root)
	  (-> out 'closefile)
	  (display "Error opening file for output")
Note that this is the general procedure for performing actions on a scene graph: construct the action, set up the destination for its side-effects, if any, and apply the action to a certain node. (Note also that we were less careful about avoiding creating garbage when writing this procedure.)

Let's look at an example to compare programmatic creation of scene graphs with the Inventor file format:

;; Blue sphere, red cone. 
(define root (new-SoSeparator))
(-> root 'ref)

;; Sphere group
(define sep1 (new-SoSeparator))
(-> root 'addChild sep1)
(define xf1 (new-SoTransform))
(-> (-> xf1 'translation) 'setValue -1 0 0)
(-> sep1 'addChild xf1)
(define mat1 (new-SoMaterial))
(-> (-> mat1 'diffuseColor) 'setValue 0.8 0.2 0.2)
(-> sep1 'addChild mat1)
(-> sep1 'addChild (new-SoSphere))

;; Cone group
(define sep2 (new-SoSeparator))
(-> root 'addChild sep2)
(define xf2 (new-SoTransform))
(-> (-> xf2 'translation) 'setValue 2 0 0)
(-> sep2 'addChild xf2)
(define mat2 (new-SoMaterial))
(-> (-> mat2 'diffuseColor) 'setValue 0.2 0.2 0.8)
(-> sep2 'addChild mat2)
(-> sep2 'addChild (new-SoCone))

;; Viewer
(define v (examiner root))
This scene looks like this in the .iv file format:
#Inventor V2.1 ascii

Separator {
    Separator {
	Transform {
	    translation	-1 0 0
	Material {
	    diffuseColor	0.8 0.2 0.2
	Sphere {
    Separator {
	Transform {
	    translation	2 0 0
	Material {
	    diffuseColor	0.2 0.2 0.8
	Cone {
Structuring scene graphs

We have already seen how the placement of nodes in the scene graph can change the result during rendering; recall the placement of SoMaterial nodes in the first problem set's scene graph and how they affected all suceeding shape nodes in the graph. We will now examine how Inventor determines which information in which nodes is used to render the scene, in preparation for developing more structure and functionality in the scene graph.

Traversal state

When Open Inventor renders a scene in a viewer, it applies a render action to the root of the viewer's current scene graph. This action keeps track of the traversal state; fundamentally, nodes modify elements in this state. For example, an SoMaterial node may replace several of the color elements in the traversal state. An SoTransform adds its effects to the current transformation matrix; note that transformations are cumulative, while changes in material properties (for example) are not.

Group nodes

There are several types of group nodes which allow more structure to be enforced in the scene graph; the two primary classes are SoGroup and SoSeparator. SoGroup simply allows grouping of several nodes under one parent node, and makes sure they are traversed in the proper order, that is, from top down and from left to right. SoSeparator implements a "push/pop" on the traversal state; that is, no node under an SoSeparator node can have any effect on any node above it.

Let's look at an example of this. Consider the following scene graph:

                     |           |
                   Group        Cone
               Material (blue)
Keep in mind that the default material for all shapes is white. During rendering of this scene graph, the nodes will be visited in the following order: Separator, Group, Material, and Cone. The Material node will change the current color in the rendering state to blue, and this color will then be used when rendering the cone. However, if we replaced the Group node with another Separator, the blue material would be "popped" off the stack when we finished traversing all of the children of this second separator. For this reason, the cone would appear white in this case. The last type of separator node is called a TransformSeparator; this implements a push/pop as described above, but only for the current transformation matrix. That is, if there is a transform node under a transform separator which moves geometry under that separator around, this transformation will not affect objects above and to the right of the transform separator. However, changes to other elements such as the current color will be propagated past the transform separator.

There are several other subclassses of group nodes which provide specialized functions; two of these nodes are SoSwitch and SoBlinker. A switch node traverses all, none, or exactly one of its children. It is subclassed from SoGroup, so it doesn't implement a push/pop of the traversal state; that is, you could add several material nodes to a switch node and use them to change the color of an object on demand:

(define root (new-SoSeparator))
(-> root 'ref)
(define switch (new-SoSwitch))
(define red-mat (new-SoMaterial))
(-> (-> red-mat 'diffuseColor) 'setValue 0.8 0.2 0.2)
(define blue-mat (new-SoMaterial))
(-> (-> blue-mat 'diffuseColor) 'setValue 0.2 0.2 0.8)
(addChildren switch red-mat blue-mat) ;; addChildren defined in CGWInit.scm
(addChildren root switch (new-SoCone))
(define viewer (examiner root))

(-> (-> switch 'whichChild) 'setValue 1) ;; blue child. Index starts at 0
(-> (-> switch 'whichChild) 'setValue 0) ;; red child.
Switch nodes are very useful for building scene graph "machines" in which you can turn on and off individual parts of a large structure.

A blinker node traverses one of its children during each render cycle, but changes which child it traverses a specified number of times per second. An example of the use of a blinker node is in 13.8.Blinker.

All of the types of group node simplement the same base-level functionality for adding children to and removing children from a group:

SoGroup - base class for all group nodes
SoBase > SoFieldContainer > SoNode > SoGroup
Methods from class SoGroup:

void addChild(SoNode *child)
void insertChild(SoNode *child, int newChildIndex)
void removeChild(int index)
void removeChild(SoNode *child)

The addChild method takes a node as an argument, and adds this node as the last child of this group. The insertChild method takes a node and an index as arguments, and adds this node so that it becomes the one with the given index. (The first child of a group has index 0.) The removeChild method can take either a node or index as an argument, and attempts to remove the indicated node grom the group. Keep in mind the issues of reference counting described in the last lecture; if you are not careful, removing a child of a group will cause this child to be deleted.

Vertex based shapes

So far we have been using only geometric primitives built into Inventor as our shapes: sphere, cone, cylinder, and cube. Inventor supports many other types of geometry than just these primitives; most fall into the general category of vertex based shapes.

A vertex based shape in general is created by first specifying the coordinates of the vertices in the shape, and then specifying how these coordinates will be used. As an example, let's create a face set, which is a collection of planar polygons.

The structure of our scene graph will look like this:

            |                 |                |            |
          coords             bind             mat       face-set
      (SoCoordinate3) (SoMaterialBinding) (SoMaterial) (SoFaceSet)
The "coords" node replaces the current coordinates in the rendering state with those in the node. The "bind" node specifies how the colors to come will affect the resulting polygons; that is, whether the colors will affect each vertex, each polygon, or the entire set of polygons. The "mat" node specifies the colors for the face set. The "face-set" node groups the current coordinates and material properties into appropriately colored polygons. (Note that it is valid to change the ordering of the first three nodes, but that the FaceSet node must come last, because traversal of this node causes a shape to be drawn immediately.)

Now let's build a sample scene graph using this structure.

(define root (new-SoSeparator))
(-> root 'ref)
(define coords (new-SoCoordinate3))
(-> root 'addChild coords)
(define bind (new-SoMaterialBinding))
(-> root 'addChild bind)
(define mat (new-SoMaterial))
(-> root 'addChild mat)
(define face-set (new-SoFaceSet))
(-> root 'addChild face-set)
;; set up the coordinates for our polygons
(set-mfield-values! (-> coords 'point) 0
  '(#(1 0 0)
    #(2 0 0)
    #(1.5 1 0)
    #(0.5 1 0)
    #(0 0 0)
    #(1 0 0)))
;; make the material binding per face for now
(-> (-> bind 'value) 'setValue SoMaterialBinding::PER_FACE)
;; set up colors -- enough for per vertex binding
(set-mfield-values! (-> mat 'diffuseColor) 0
  '(#(0 0 1)
    #(1 0 1)
    #(1 0 0)
    #(1 1 0)
    #(0 1 0)
    #(0 1 1)))
;; set up the polygons -- two triangles
(set-mfield-values! (-> face-set 'numVertices) 0
  '(3 3))
When we view the resulting scene graph, we see two adjacent triangles of different colors. The face set tells the renderer how many of the specified coordinates to use for each polygon; the first polygon uses the first three coordinates, and the second uses the second three.

Now let's see what happens if we change this specification:

(-> (-> face-set 'numVertices) 'setValue 5)
We see a polygon which uses the first five specified coordinates as the vertices of a polygon. Note that since the first and last points overlap, we do not use both of them.

As a final example, let's try changing the material binding of the scene:

(-> (-> bind 'value) 'setValue SoMaterialBinding::PER_VERTEX)
Now each vertex has a color specified, and the renderer smoothly interpolates the color of the polygon to match the vertex colors.

The third problem set consists of the creation of a torus as a vertex based shape.

Next lecture

Next time we will complete our coverage of vertex based shapes, review and present some more material regarding fields, and answer any questions you may have. The following lecture we will begin presenting the tools you will need to build a fully interactive 3D application using Open Inventor.

Back to the CGW '97 home page

$Id: index.html,v 1.9 1997/01/07 23:53:57 kbrussel Exp $