Appendix A. Creating a Node

SoNode. It discusses enabling elements in the state, constructing a node, and implementing actions for it. Chapter 1 in The Inventor Toolmaker provides important background material on these concepts, and this appendix assumes you are familiar with the material presented there.


Note: This appendix provides an update of Chapter 2, “Creating a Node,” of The Inventor Toolmaker. Its main purpose is to help developers who are new to Inventor work with Inventor 2.1 without being hindered by outdated information. Developers familiar with Inventor may prefer to look at Chapter 5, “Incompatible Extender API Changes” and at the revised example programs instead of reading this complete appendix.

The first part of this appendix offers an overview of the steps required to create a new node. When necessary, additional sections explain key concepts in further detail and list the relevant macros. Next, the appendix examples show how to create three new node classes:

Sections at the end of the chapter discuss the following:

Overview

SoSubNode.hSO_NODE_HEADER() macro declares type identifier and naming variables and methods that all node classes must support. The SO_NODE_SOURCE()macro defines the static variables and methods declared in the SO_NODE_HEADER()classes are mentioned in the following sections.

Creating a new node requires these steps:

  1. Select a name for the new node class and determine what class it is derived from.

  2. Define and name each field in the node.

  3. Define an initClass() method to initialize the type information and to ensure that the required elements are enabled in the state (see “Initializing the Node Class”).

  4. Define a constructor (see “Defining the Constructor”).

  5. Implement the actions supported by the node (see “Implementing Actions”).

    • For a property node, you usually need to implement the GLRender() and callback() methods (see “Creating a Property Node”). You may also need to implement getBoundingBox(), getMatrix(), and other methods.

    • For a shape node, you need to implement the generatePrimitives() method for the SoCallbackAction as well as the getBoundingBox() method. You may want to implement a specific GLRender() or rayPick() method as well (see “Creating a Shape Node”). For vertex-based shapes, you may need to implement a generateDefaultNormals() method.

    • For a group node, you need to implement all actions to ensure that the children are traversed (see “Creating a Group Node”).

  6. Implement a copyContents() method if the node contains any non-field instance data (see “The copyContents() Method”).

  7. Implement an affectsState() method if it cannot be inherited from the parent class (see “The affectsState() Method”).

Initializing the Node Class

The Inventor Toolmaker, the initClass() method sets up the type identifier and file format name information for the class. The initialization macro for nodes, SO_NODE_INIT_CLASS(), work for you. One additional task for you as the node writer is to enable each of the elements in the state for each action the node supports. The following subsections provide additional information about enabling elements in the state.

Enabling Elements in the State

initClass() SoAction.ha simple example, SoDrawStyle enables these elements in the SoGLRenderAction:

SO_ENABLE(SoGLRenderAction, SoGLDrawStyleElement);
SO_ENABLE(SoGLRenderAction, SoGLLinePatternElement);
SO_ENABLE(SoGLRenderAction, SoGLLineWidthElement);
SO_ENABLE(SoGLRenderAction, SoGLPointSizeElement);

SoDrawStyle also implements the SoCallbackAction. It enables these elements in the SoCallbackAction:

SO_ENABLE(SoCallbackAction, SoDrawStyleElement);
SO_ENABLE(SoCallbackAction, SoLinePatternElement);
SO_ENABLE(SoCallbackAction, SoLineWidthElement);
SO_ENABLE(SoCallbackAction, SoPointSizeElement);


Tip: If you know that the element is already enabled by another node or action, you can skip this step.

Now that these elements have been enabled, their values can be set and queried. (The access or set an element that has not been enabled.)

Inheritance Within the Element Stack

SoDrawStyle elements in the previous section brings up another feature of the element stack: Some elements have corresponding GL versions that are derived from them. The SoGL version of an element typically sends its value to SoGLDrawStyleElement is derived from SoDrawStyleElement, and SoGLLinePatternElement is derived from SoLinePatternElement. The parent element class and its derived class share the same stack index.

If you try to enable two classes that share a stack index (for example, SoGLDrawStyleElement and SoDrawStyleElement), only the more derived class is actually enabled (in this case, SoGLDrawStyleElement). However, you can always use the base class static method to set or get the value for either the parent or the derived class. (You cannot, however, enable only the parent version and then try to treat it as the derived GL version.)Defining the Constructor

values. If the fields contain enumerated values, their names and values are defined in the constructor as well. Use the macro to perform the basic work.

can be tested in constructors. If your class requires considerable overhead when it is initialized, you may want to perform this work only once when the first instance of the class is created. For example, the SoCube class sets up the coordinates and normals of the cube faces during construction of its first instance. (You could put this code in the initClass() method, but putting it in the constructor guarantees that someone is actually using your node class first.)

Setting Up the Node's Fields

up their default values. The first parameter is the name of the field. The second parameter is the default field value, in parentheses. Using SoDrawStyle as an example:

SO_NODE_ADD_FIELD(style, (SoDrawStyleElement::getDefault()));
SO_NODE_ADD_FIELD(lineWidth,
                 (SoLineWidthElement::getDefault()));
SO_NODE_ADD_FIELD(linePattern,
                 (SoLinePatternElement::getDefault()));
SO_NODE_ADD_FIELD(pointSize,
                 (SoPointSizeElement::getDefault()));

To add a field with a vector value, the syntax is as follows:

SO_NODE_ADD_FIELD(translation, (0.0, 0.0, 0.0));

Defining Enumerated Values for a Field

style field contains an enumerated value: FILLED, LINES, POINTS, or INVISIBLE. Use the SO_NODE_DEFINE_-
ENUM_VALUE() macro to define the enumerated values. The first parameter is the type of the enumerated value. The second parameter is its value, as shown here:

SO_NODE_DEFINE_ENUM_VALUE(Style, FILLED);
SO_NODE_DEFINE_ENUM_VALUE(Style, LINES);
SO_NODE_DEFINE_ENUM_VALUE(Style, POINTS);
SO_NODE_DEFINE_ENUM_VALUE(Style, INVISIBLE);

style field of the SoDrawStyle node, use the SO_NODE_SET_SF_ENUM_TYPE() macro:O_NODE_SET_SF_ENUM_TYPE(style, Style);

Implementing Actions

The SoDrawStyle node, as you have already seen, supports two actions, the SoGLRenderAction and the SoCallbackAction, in addition to the SoSearchAction and the SoWriteAction, which it inherits from SoNode.


Tip: Do not apply a new action within another action (because caching will not function properly). Also, if you are creating a new node, do not modify the node (for example, call setValue() on a field) within an action method.


The doAction() Method

SoDrawStyle node changes the values of 4 elements in the state based on the value of the corresponding fields. For example, if its style field has a value of INVISIBLE, it changes the value of the SoGLDrawStyleElement in the state to INVISIBLE. First it must check if another node has overridden the draw style. The code to set the element's value is:

if (! style.isIgnored() &&
    ! SoOverrideElement::getDrawStyleOverride(state)) {
    if (isOverride()) {
        SoOverrideElement::setDrawStyleOverride(state, this,
                                               TRUE);
    }
    SoDrawStyleElement::set(state,
          (SoDrawStyleElement::Style) style.getValue());
}

For the callback action, SoDrawStyle does the same thing: It sets the value of the element based on the value of the corresponding field in the node. Since the two actions perform exactly the same tasks, the common code is put into a separate method that both the GL render and the callback actions can call. By convention, this shared method used by property nodes is called doAction(), which is a virtual method on SoNode. The code for the draw-style node's callback action followed by the node's GL render action:

void
SoDrawStyle::callback(SoCallbackAction *action)
(
   doAction(action);
}
void
SoDrawStyle::GLRender(SoGLRenderAction *action)
(
   doAction(action);
}

This is the draw-style node's doAction() method:

SoDrawStyle::doAction(SoAction *action)
{
    SoState     *state = action->getState();

    if (! style.isIgnored() && 
        ! SoOverrideElement::getDrawStyleOverride(state)) {
        if (isOverride()) {
            SoOverrideElement::setDrawStyleOverride( 
                                         state, this, TRUE);}
        SoDrawStyleElement::set(state,
                               (SoDrawStyleElement::Style) 
                                style.getValue());
    }

    if (! pointSize.isIgnored() && 
        ! SoOverrideElement::getPointSizeOverride(state)) {
        if (isOverride()) {
            SoOverrideElement::setPointSizeOverride(
                                         state, this, TRUE);}
        SoPointSizeElement::set(state, pointSize.getValue());
    }

    if (! lineWidth.isIgnored() && 
       ! SoOverrideElement::getLineWidthOverride(state)) {
        if (isOverride()) {
            SoOverrideElement::setLineWidthOverride(state, 
                                         state, this, TRUE);}
        SoLineWidthElement::set(state, lineWidth.getValue());
    }

    if (! linePattern.isIgnored() &&  
       !SoOverrideElement::getLinePatternOverride(state)) {
        if (isOverride()) {
            SoOverrideElement::setLinePatternOverride(
                                         state, this, TRUE);}
        SoLinePatternElement::set(state, 
                                     linePattern.getValue());
    }
}

The advantage of this scheme becomes apparent when you consider extending the set of actions (see Chapter 4 of The Inventor Toolmaker). You can define a new action class and implement a static method for SoNode that calls doAction(). Then all properties that implement doAction() will perform the appropriate operation without needing any static methods for them.

Changing and Examining State Elements

The Inventor Toolmaker, each element class provides methods for setting and querying its value. The static set() method usually has two parameters, as shown in the previous section:

  • The state from which to retrieve the element (which the element obtains from the given action)

  • The new value for the element

get() method that returns the current value stored in an element instance. For example, to obtain the current draw style, use:

style = SoDrawStyleElement::get(action->getState());

Elements that have multiple values may define a different sequence of get() methods. For example, the material color elements and coordinate element can contain many values. In these cases, the element class defines three methods:

getInstance()  

returns the top instance of the element in the state as a const pointer getNum() returns the number of values in the element

get(n) returns the nth value in the element Element Bundles

Elements are designed to be small and specific, for two reasons. The first is that it should be possible for a node to change one aspect of the state without having to change any of the rest, including related elements. For example, the SoMaterialBinding node changes only the SoMaterialBindingElement without affecting any other material elements. The second reason has to do with caching. It is easy to determine when any element's value has changed, since (typically) the whole element changes at once. Therefore, determining which nodes affect a cache is a straightforward task.

However, some elements are related to each other, and it's good to deal with them together for convenience and efficiency. Classes called bundles provide simple interfaces to collections of related elements.

Supported Inventor bundle classes are:

  • SoMaterialBundleSoNormalBundle

  • SoTextureCoordinateBundle

The SoMaterialBundle class accesses all elements having to do with surface materials. In Inventor 2.1, these functions are more efficiently performed by using the SoLazyElement (see the next section for more information). The following examples use the SoLazyElement instead of material bundles.

Methods on the SoMaterialBundle allow shapes to step easily through sequential materials and to send the current material to OpenGL. SoNormalBundle lets you step easily through sequential normals and provides functions for generating default normals. SoTextureCoordinateBundle lets you step through texture coordinates and provides methods for generating texture coordinates if the shape is using SoTextureCoordinatePlane or SoTextureCoordinateEnvironment.

The SoLazyElement

Several elements that were responsible for material properties in Inventor 2.0 have been combined into two elements: SoLazyElement and SoGLLazyElement, its derived OpenGL version.

  • SoLazyElement is responsible for setting and getting material state.

  • SoGLLazyElement is responsible for tracking the OpenGL state, invalidating caches, and so on.

See “Changes in Material Elements” for more information.

Creating a Property Node

example creates a new property class called Glow, which modifies the emissive color of the current material to make objects appear to glow. It has a field called color, which is the color of the glow, and a float field called brightness, ranging from 0 to 1, indicating how much the object should glow and a transparency field, indicating the transparency of the glowing material. Its value is between 0 (opaque) and 1 (invisible).

In Inventor 2.1, the transparency and diffuse color are combined in the state into a packed color. To facilitate dealing with diffuse colors and transparency independently, a class SoColorPacker is provided. In the example that follows, the SoColorPacker is constructed in the Glow node constructor, and is used whenever the transparency is set in the state.

The doAction() method of the Glow node must check whether the emissive color or transparency is already being overridden by another material node. The SoOverrideElement determines what is overridden.

materials: GLRender() and callback(). It uses doAction() (see “The doAction() Method”) since it performs the same operation for both actions. The doAction() method for the Glow class updates the lazy color element based on the values of the color, transparency, and brightness fields of the node.

Glow.h#include <Inventor/SbColor.h>
#include <Inventor/fields/SoSFColor.h>
#include <Inventor/fields/SoSFFloat.h>
#include <Inventor/nodes/SoSubNode.h>

// Need SoColorPacker to put the transparency into the state:
class SoColorPacker;

class Glow : public SoNode {

   SO_NODE_HEADER(Glow);

 public:
   // Fields:
   SoSFColor  color;            // Color of glow
   SoSFFloat  brightness;       // Amount of glow (0-1)
   SoSFFloat	  transparency;	 // transparency of glowing object

   // Initializes this class for use in scene graphs. This
   // should be called after database initialization and 
   // before any instance of this node is constructed.
   static void    initClass();

   // Constructor
   Glow();

 protected:
   // These implement supported actions. The only actions     
   // dealing with materials are the callback and GL render
   // actions. We will inherit all other action methods from
   // SoNode.
   virtual void   GLRender(SoGLRenderAction *action);
   virtual void   callback(SoCallbackAction *action);

   // This implements generic traversal of Glow node, used in
   // both of the above methods.
   virtual void   doAction(SoAction *action);

 private:
   // Destructor. Private to keep people from trying to 
   // delete nodes instead of using reference count 
   // mechanism.
   virtual ~Glow();

   // A pointer to the color packer is required in nodes 
   // that set diffuse or transparency.  This will
   // be initialized in constructor, deleted in destructor:
   SoColorPacker	 *colorPacker;
   
   // Keep a copy of the transparency that this node puts 
   // into the state:
   float 		transpValue;
   

The Glow node is representative of most property nodes in that it is concerned solely with editing the current traversal state, regardless of the action being performed.

The source code for the Glow class is shown in Example A-2.

Example A-1. Glow.c++


#include <Inventor/actions/SoCallbackAction.h>
#include <Inventor/actions/SoGLRenderAction.h>
#include <Inventor/elements/SoLazyElement.h>
#include <Inventor/elements/SoOverrideElement.h>

#include "Glow.h"

SO_NODE_SOURCE(Glow);

//
// Initializes the Glow class. This is a one-time thing that 
// is done after database initialization and before any 
// instance of this class is constructed.
//

void
Glow::initClass()
{
   // Initialize type id variables. Arguments to the macro
   // are: the name of the node class, the class this is
   // derived from, and the name registered with the type of
   // the parent class.
   SO_NODE_INIT_CLASS(Glow, SoNode, "Node");
}

//
// Constructor
//

Glow::Glow()
{
   // Do standard constructor stuff:
   SO_NODE_CONSTRUCTOR(Glow);

   // Add "color" field to the field data. The default value 
   // for this field is R=G=B=1, which is white.
   SO_NODE_ADD_FIELD(color, (1.0, 1.0, 1.0));

   // Add "brightness" field to the field data. The default
   // value for this field is 0.
   SO_NODE_ADD_FIELD(brightness, (0.0));
   
   // Add "transparency" field to the field data.  Default 
   // value is 0 (opaque).
   SO_NODE_ADD_FIELD(transparency,  (0.0));
   
   // Initialize the color Packer (required of any property 
   // node that uses an SoColorPacker to set diffuse color 
   // or transparency):
   colorPacker = new SoColorPacker;
   
}

//
// Destructor
//

Glow::~Glow()
{
    //  Delete the color Packer:
    delete colorPacker;
}

//
// Implements GL render action.
//

void
Glow::GLRender(SoGLRenderAction *action)
{
   // Set the elements in the state correctly. Note that we
   // prefix the call to doAction() with the class name. This
   // avoids problems if someone derives a new class from the
   // Glow node and inherits the GLRender() method; Glow's
   // doAction() will still be called in that case.
   Glow::doAction(action);

   //  Note that in Inventor 2.1 the GLRender method only 
   //  sets the color in the lazy element; it does not send 
   //  it to GL.  This is for efficiency, since there is no
   //  need to send it until it is really used.

}

//
// Implements callback action.
//

void
Glow::callback(SoCallbackAction *action)
{
   // Set the elements in the state correctly.
   Glow::doAction(action);
}

//
// Typical action implementation; it sets the correct element
// in the action's traversal state. We assume that the lazy
// element has been enabled.
//

void
Glow::doAction(SoAction *action)
{
   // Make sure the "brightness" field is not ignored. If it
   // is, then we don't need to change anything in the state.
   
   // Also check that the emissive color is not being 
   // overridden; if it is, this node should not set it.
   
   if (! brightness.isIgnored() && 
        ! SoOverrideElement::getEmissiveColorOverride
                                      (action->getState())) {

       SbColor emissiveColor = color.getValue() * 
                                       brightness.getValue();

       //Use the Lazy element to set emissive color. 
       //Note that this won't actually send the color to GL. 
      
       SoLazyElement::setEmissive(action->getState(), 
                                             &emissiveColor);
       
   }
   
   // To send transparency we again check ignore flag and 
   // override element.
 
   if (! transparency.isIgnored() &&
        ! SoOverrideElement::getTransparencyOverride
                                      (action->getState())) {
   
        // Keep a copy of the transparency that we are 
        // putting in the state:
        transpValue = transparency.getValue();
        
        // The color packer must be provided when 
        // transparency is set, so that the transparency
        // will be merged with current diffuse color
        // in the state:
        SoLazyElement::setTransparency(action->getState(),
                        this, 1, &transpValue,  colorPacker);
    } 
}

Creating a Shape Node

because shape nodes need to access more of the state and implement different behaviors for different actions. For example, a shape needs to draw geometry during rendering, return intersection information during picking, and compute its extent when getting a bounding box.

All shapes need to define at least two methods: generatePrimitives() and getBoundingBox(). When you define the generatePrimitives() method for your new class, you can inherit the GLRender() and rayPick() methods from the base class, SoShape, because they use the generated primitives. This feature saves time at the prototyping stage, since you need to implement only the generatePrimitives() method, and rendering and picking are provided at no extra cost. When you are ready for fine-tuning, you can redefine these two methods to improve performance.

Generating Primitives

SoCallbackAction, it generates triangles, line segments, or points. The information for each vertex of the triangle, line segment, or point is stored in an instance of SoPrimitiveVertex. The shape fills in the information for each vertex. Then, for each primitive generated (that is, triangle, line segment, or point), an appropriate callback function is invoked by a method on SoShape. For example, if the shape generates triangles, the triangle callback function is invoked for every triangle generated. SoCone and SoQuadMesh, generate triangles (regardless of draw style), line shapes (such as SoLineSet and SoIndexedLineSet) generate line segments, and point shapes (such as SoPointSet) generate points.

SoPrimitiveVertex

The SoPrimitiveVertex contains all information for that vertex:

  • Point (coordinates, in object space)

  • Normal

  • Texture coordinates

  • Material index

  • A pointer to an instance of an SoDetail subclass (may be NULL)

The shape's generatePrimitives() method sets each of these values.

explicitly. If you want explicit control over when the callback function is invoked, you can use the following methods provided by the SoShape class:

  • invokeTriangleCallbacks()

  • invokeLineSegmentCallbacks()

  • invokePointCallbacks()

To take advantage of the automatic mechanism, use these three methods, provided by the SoShape base class as a convenience:

beginShape(action, shapeType) shapeVertex(&vertex) endShape() shapeType parameter is TRIANGLE_FAN, TRIANGLE_STRIP, TRIANGLES, or POLYGON. For example, if you choose TRIANGLE_FAN, this method performs the necessary triangulation and invokes the appropriate callbacks for each successive triangle of the shape. This mechanism is similar to OpenGL's geometry calls.

Creating Details

SoDetail—for example, what part of the shape each vertex belongs to. In this case, you can use an existing subclass of SoDetail (see The Inventor Mentor, Chapter 9), or you can create a new SoDetail subclass to hold the appropriate information. By default, the pointer to the detail in SoPrimitiveVertex is NULL.

If you decide to store information in an SoDetail, you create an instance of the subclass and store a pointer to it in the SoPrimitiveVertex by calling setDetail().

Rendering

For rendering, you may be able to inherit the GLRender() method from the SoShape class. In this case, you define a generatePrimitives() method as described in the previous sections. Each primitive is generated and then rendered separately.

In other cases, you may want to write your own render method for the new shape class, especially if it would be more efficient to send the vertex information to OpenGL in some other form, such as triangle strips. The Pyramid node created later in this appendix implements its own GLRender() method. Before rendering, the shape should test whether it needs to be rendered. You can use the SoShape::shouldGLRender() method, which checks for INVISIBLE draw style, BOUNDING_BOX complexity, delayed transparency, and render abort.

Inventor takes care of sending the draw-style value to OpenGL (where it is handled by glPolygonMode()). This means that filled shapes are drawn automatically as lines or points if the draw style indicates such. Note that if your object is composed of lines, but the draw style is POINTS, you need to handle that case explicitly. You need to check whether the draw-style element in the state is points or lines and render the shape accordingly.

The lazy elements are used in the GLRender() method of the Pyramid to send the colors to OpenGL. This has to be set up as follows:

  • SoLazyElement::getLightModel() is used to tell if lighting is on. If so, it is necessary to send normals to OpenGL.

  • SoLazyGLElement::sendAllMaterial() is needed prior to rendering the shape. This ensures that the OpenGL material state is in agreement with the current Inventor state.

  • SoGLLazyElement::sendDiffuseByIndex() is required if more than one diffuse color or transparency is sent. This issues the appropriate OpenGL commands to send the specified color and transparency to OpenGL.

  • SoGLLazyElement::reset() is required at the end of rendering, if colors other than the first color were sent to OpenGL. This informs the lazy element that the current color in the state is not the last one sent to GL.

Picking

For picking, you may also be able to inherit the rayPick() method from the SoShape class. In this case, you define a generatePrimitives() method, and the parent class rayPick() method tests the picking ray against each primitive that has been generated. If the ray intersects the primitive, it creates an SoPickedPoint. SoShape provides three virtual methods for creating details:

  • createTriangleDetail()

  • createLineDetail()

  • createPointDetail()

The default methods return NULL, but your shape can override this to set up and return a detail instance.

The Pyramid node created later in this appendix inherits the rayPick() method from SoShape in this manner.

For some shapes, such as spheres and cylinders, it is more efficient to check whether the picking ray intersects the object without tessellating the object into primitives. In such cases, you can implement your own rayPick() method and use the SoShape::shouldRayPick() method, which first checks to see if the object is pickable.

The following excerpt from the SoSphere class shows how to implement your own rayPick() method:void
SoSphere::rayPick(SoRayPickAction *action)
{
   SbVec3f             enterPoint, exitPoint, normal;
   SbVec4f             texCoord(0.0, 0.0, 0.0, 1.0);
   SoPickedPoint       *pp;

   // First see if the object is pickable.
   if (! shouldRayPick(action))
       return;

   // Compute the picking ray in our current object space.
   computeObjectSpaceRay(action);

   // Create SbSphere with correct radius, centered at zero.
   float       rad = (radius.isIgnored() ? 1.0 :
                       radius.getValue());
   SbSphere    sph(SbVec3f(0., 0., 0.), rad);

   // Intersect with pick ray. If found, set up picked    
   // point(s).
   if (sph.intersect(action->getLine(), enterPoint, 
                                       exitPoint)) {
      if (action->isBetweenPlanes(enterPoint) &&
          (pp = action->addIntersection(enterPoint)) != 
                                                    NULL) {

         normal = enterPoint;
         normal.normalize();
         pp->setObjectNormal(normal);
         // This macro computes the s and t texture 
         // coordinates for the shape.
         COMPUTE_S_T(enterPoint, texCoord[0], texCoord[1]);
         pp->setObjectTextureCoords(texCoord);
      }

      if (action->isBetweenPlanes(exitPoint) &&
          (pp = action->addIntersection(exitPoint)) != NULL){

         normal = exitPoint;
         normal.normalize();
         pp->setObjectNormal(normal);
         COMPUTE_S_T(exitPoint, texCoord[0], texCoord[1]);
         texCoord[2] = texCoord[3] = 0.0;
         pp->setObjectTextureCoords(texCoord);
      }
   }

}Getting a Bounding Box

SoShape provides a getBoundingBox() method that your new shape class can inherit. This method calls a virtual computeBBox() method, which you need to define. (The computeBBox() method is also used during rendering when bounding-box complexity is specified.)

If you are deriving a class from SoNonIndexedShape, you can use the computeCoordBBox() method within your computeBBox() routine. This method computes the bounding box by looking at the specified number of vertices, starting at startIndex. It uses the minimum and maximum coordinate values to form the diagonal for the bounding box and uses the average of the vertices as the center of the object.

If you are deriving a class from SoIndexedShape, you can inherit computeBBox() from the base SoIndexedShape class. This method uses all nonnegative indices in the coordinates list to find the minimum and maximum coordinate values. It uses the average of the coordinate values as the center of the object.

For improved picking performance, the rayPick() action assumes that the bounding boxes include only surfaces; not lines or points. If your shape object is composed of lines and/or points, you must implement a getBoundingBox() method similar to the following method from the SoLineSet implementation:

void
SoLineSet::getBoundingBox(SoGetBoundingBoxAction *action)
{
    // Let our parent class do the real work
    SoNonIndexedShape::getBoundingBox(action);

    // If there are any open bounding box caches, tell them 
    // that they contain lines
    SoBoundingBoxCache::setHasLinesOrPoints
                                     (action->getState());
}

Pyramid Node

Pyramid node, which has a square base at y = -1 and its apex at (0.0, 1.0, 0.0). The code presented here is similar to that used for other primitive (nonvertex-based) shapes, such as cones and cylinders. The pyramid behaves like an SoCone, except that it always has four sides. And, instead of a bottomRadius field, the Pyramid class has baseWidth and baseDepth fields in addition to the parts and height fields.

Some of the work for all shapes can be done by methods on the base shape class, SoShape. For example, SoShape::shouldGLRender() checks for INVISIBLE draw style when rendering. SoShape::shouldRayPick() checks for UNPICKABLE pick style when picking. This means that shape subclasses can concentrate on their specific behaviors.

To define a vertex-based shape subclass, you probably want to derive your class from either SoNonIndexedShape or SoIndexedShape. These classes define some methods and macros that can make your job easier.

You may notice in this example that there are macros (defined in SoSFEnum.h) that make it easy to deal with fields containing enumerated types, such as the parts field of our node. Similar macros are found in SoMFEnum.h and in the header files for the bitmask fields.

The class header for the Pyramid node is shown in Example A-3.

Example A-2. Pyramid.h


#include <Inventor/SbLinear.h>
#include <Inventor/fields/SoSFBitMask.h>
#include <Inventor/fields/SoSFFloat.h>
#include <Inventor/nodes/SoShape.h>
// SoShape.h includes SoSubNode.h; no need to include it here

// Pyramid texture coordinates are defined on the sides so 
// that the seam is along the left rear edge, wrapping
// counterclockwise around the sides. The texture 
// coordinates on the base are set up so the texture is 
// right side up when the pyramid is tilted back.

class Pyramid : public SoShape {

   SO_NODE_HEADER(Pyramid);

 public:

   enum Part {                  // Pyramid parts:
      SIDES = 0x01,             // The 4 side faces
      BASE  = 0x02,             // The bottom square face
      ALL   = 0x03,             // All parts
   };

   // Fields
   SoSFBitMask   parts;         // Visible parts
   SoSFFloat     baseWidth;     // Width of base
   SoSFFloat     baseDepth;     // Depth of base
   SoSFFloat     height;        // Height, base to apex 

   // Initializes this class
   static void   initClass();

   // Constructor
   Pyramid();

   // Turns on/off a part of the pyramid. (Convenience)
   void          addPart(Part part);
   void          removePart(Part part);

   // Returns whether a part is on or off. (Convenience)
   SbBool        hasPart(Part part) const;

 protected:
   // This implements the GL rendering action. We inherit
   // all other action behavior, including rayPick(), which 
   // is defined by SoShape to pick against all of the
   // triangles created by generatePrimitives.
   virtual void  GLRender(SoGLRenderAction *action);

   // Generates triangles representing a pyramid
   virtual void  generatePrimitives(SoAction *action);
   // This computes the bounding box and center of a pyramid.
   // It is used by SoShape for the SoGetBoundingBoxAction; 
   // also to compute the correct box to render or pick when
   // complexity is BOUNDING_BOX. Note that we do not have to
   // define a getBoundingBox() method, since SoShape already
   // takes care of that (using this method).
   virtual void   computeBBox(SoAction *action,
                              SbBox3f &box, SbVec3f &center);
 private:
   // Face normals. These are static because they are 
   // computed once and are shared by all instances
   static SbVec3f frontNormal, rearNormal;
   static SbVec3f leftNormal,  rightNormal; 
   static SbVec3f baseNormal;

   // Destructor
   virtual ~Pyramid();

   // Computes and returns half-width, half-height, and
   // half-depth based on current field values
   void           getSize(float &halfWidth,
                          float &halfHeight,
                          float &halfDepth) const;
};

The source code for the Pyramid node is shown in Example A-4.

Example A-3. Pyramid.c++


/*-----------------------------------------------------------
 *  This is an example from the Inventor Toolmaker,
 *  chapter 2, example 4.
 *
 *  Source file for "Pyramid" shape node.
*----------------------------------------------------------*/

#include <GL/gl.h>
#include <Inventor/SbBox.h>
#include <Inventor/SoPickedPoint.h>
#include <Inventor/SoPrimitiveVertex.h>
#include <Inventor/actions/SoGLRenderAction.h>
#include <Inventor/actions/SoRayPickAction.h>
#include <Inventor/elements/SoGLLazyElement.h> 
#include <Inventor/elements/SoGLTextureCoordinateElement.h>
#include <Inventor/elements/SoGLTextureEnabledElement.h>
#include <Inventor/elements/SoMaterialBindingElement.h>
#include <Inventor/elements/SoModelMatrixElement.h>
#include <Inventor/misc/SoState.h>
#include "Pyramid.h"

// Shorthand macro for testing whether the current parts 
// field value (parts) includes a given part (part)
#define HAS_PART(parts, part) (((parts) & (part)) != 0)

SO_NODE_SOURCE(Pyramid);

// Normals to four side faces and to base
SbVec3f Pyramid::frontNormal, Pyramid::rearNormal;
SbVec3f Pyramid::leftNormal,  Pyramid::rightNormal;
SbVec3f Pyramid::baseNormal;

//
// This initializes the Pyramid class.
//

void
Pyramid::initClass()
{
   // Initialize type id variables
   SO_NODE_INIT_CLASS(Pyramid, SoShape, "Shape");
}

//
// Constructor
//

Pyramid::Pyramid()
{
   SO_NODE_CONSTRUCTOR(Pyramid);
   SO_NODE_ADD_FIELD(parts,     (ALL));
   SO_NODE_ADD_FIELD(baseWidth, (2.0));
   SO_NODE_ADD_FIELD(baseDepth, (2.0));
   SO_NODE_ADD_FIELD(height,    (2.0));

   // Set up static values and strings for the "parts"
   // enumerated type field. This allows the SoSFEnum class
   // to read values for this field. For example, the first
   // line below says that the first value (index 0) has the 
   // value SIDES (defined in the header file) and is 
   // represented in the file format by the string "SIDES".
   SO_NODE_DEFINE_ENUM_VALUE(Part, SIDES);
   SO_NODE_DEFINE_ENUM_VALUE(Part, BASE);
   SO_NODE_DEFINE_ENUM_VALUE(Part, ALL);

   // Copy static information for "parts" enumerated type 
   // field into this instance. 
   SO_NODE_SET_SF_ENUM_TYPE(parts, Part);

   // If this is the first time the constructor is called, 
   // set up the static normals.
   if (SO_NODE_IS_FIRST_INSTANCE()) {
      float invRoot5      = 1.0 / sqrt(5.0);
      float invRoot5Twice = 2.0 * invRoot5;

      frontNormal.setValue(0.0, invRoot5,  invRoot5Twice);
      rearNormal.setValue( 0.0, invRoot5, -invRoot5Twice);
      leftNormal.setValue( -invRoot5Twice, invRoot5, 0.0);
      rightNormal.setValue( invRoot5Twice, invRoot5, 0.0);
      baseNormal.setValue(0.0, -1.0, 0.0);
   }
}

//
// Destructor
//

Pyramid::~Pyramid()
{
}

//
// Turns on a part of the pyramid. (Convenience function.)
//

void
Pyramid::addPart(Part part)
{
   parts.setValue(parts.getValue() | part);
}

//
// Turns off a part of the pyramid. (Convenience function.)
//

void
Pyramid::removePart(Part part)
{
   parts.setValue(parts.getValue() & ~part);
}

//
// Returns whether a given part is on or off. (Convenience
// function.)
//

SbBool
Pyramid::hasPart(Part part) const
{
   return HAS_PART(parts.getValue(), part);
}

//
// Implements the SoGLRenderAction for the Pyramid node.
//

void
Pyramid::GLRender(SoGLRenderAction *action)
{
    
   // Access the state from the action.
   SoState  *state = action->getState();

   // See which parts are enabled.
   int curParts = (parts.isIgnored() ? ALL :     
                                           parts.getValue());

   // First see if the object is visible and should be 
   // rendered now. This is a method on SoShape that checks 
   // for INVISIBLE draw style, BOUNDING_BOX complexity, and 
   // delayed transparency.
   if (! shouldGLRender(action))
      return;

   // Declare a pointer to a GLLazyElement.  This will be 
   // used if we send multiple colors.
   SoGLLazyElement* lazyElt;
   
   // In Inventor 2.1 we do not use beginSolidShape and
   // endSolidShape.  Instead, this information should be
   // provided in shape hints.
   // Change the current GL matrix to draw the pyramid with 
   // the correct size. This is easier than modifying all of 
   // the coordinates and normals of the pyramid. (For extra
   // efficiency, you can check if the field values are all 
   // set to default values - if so, then you can skip this 
   // step.) Scale world if necessary.
   float         halfWidth, halfHeight, halfDepth;
   getSize(halfWidth, halfHeight, halfDepth);
   glPushMatrix();
   glScalef(halfWidth, halfHeight, halfDepth);

   // See if texturing is enabled. If so, we will have to
   // send explicit texture coordinates. The "doTextures" 
   // flag will indicate if we care about textures at all.
   
   // Note this has changed slightly in Inventor version 2.1.
   // The texture coordinate type now is either FUNCTION or
   // DEFAULT. Texture coordinates are needed only for 
   // DEFAULT textures.

   SbBool doTextures =
      (SoGLTextureEnabledElement::get(state) &&
       SoTextureCoordinateElement::getType(state) !=
       SoTextureCoordinateElement::FUNCTION);

   // Determine if we need to send normals. Normals are
   // necessary if we are not doing BASE_COLOR lighting.
         
   // Use the lazy element to get the light model.   
   SbBool sendNormals = (SoLazyElement::getLightModel(state) 
            != SoLazyElement::BASE_COLOR);      

   // Determine if there's a material bound per part.
   SoMaterialBindingElement::Binding binding = 
      SoMaterialBindingElement::get(state);
   SbBool materialPerPart =
      (binding == SoMaterialBindingElement::PER_PART ||
       binding ==
                 SoMaterialBindingElement::PER_PART_INDEXED);
       
   // Issue a lazy element send.
   // The send ensures all material state in GL is current.
 
   SoGLLazyElement::sendAllMaterial(state);

   // Render the parts of the pyramid. We don't have to worry
   // about whether to render filled regions, lines, or 
   // points, since that is already taken care of. We are 
   // also ignoring complexity, which we could use to render
   // a more finely tesselated version of the pyramid.

   // We'll use this macro to make the code easier. It uses
   // the "point" variable to store the vertex point to send.
   SbVec3f  point;

#define SEND_VERTEX(x, y, z, s, t)   \
      point.setValue(x, y, z);       \
      if (doTextures)                \
         glTexCoord2f(s, t);         \
      glVertex3fv(point.getValue())

   if (HAS_PART(curParts, SIDES)) {

      // Draw each side separately, so that normals are 
      // correct. If sendNormals is TRUE, send face normals 
      // with the polygons. Make sure the vertex order obeys
      // the right-hand rule.

      glBegin(GL_TRIANGLES);

      // Front face: left front, right front, apex
      if (sendNormals)
         glNormal3fv(frontNormal.getValue());
      SEND_VERTEX(-1.0, -1.0,  1.0, .25,  0.0);
      SEND_VERTEX( 1.0, -1.0,  1.0, .50,  0.0);
      SEND_VERTEX( 0.0,  1.0,  0.0, .325, 1.0);

      // Right face: right front, right rear, apex
      if (sendNormals)
         glNormal3fv(rightNormal.getValue());
      SEND_VERTEX( 1.0, -1.0,  1.0, .50,  0.0);
      SEND_VERTEX( 1.0, -1.0, -1.0, .75,  0.0);
      SEND_VERTEX( 0.0,  1.0,  0.0, .625, 1.0);

      // Rear face: right rear, left rear, apex
      if (sendNormals)
         glNormal3fv(rearNormal.getValue());
      SEND_VERTEX( 1.0, -1.0, -1.0, .75,  0.0);
      SEND_VERTEX(-1.0, -1.0, -1.0, 1.0,  0.0);
      SEND_VERTEX( 0.0,  1.0,  0.0, .875, 1.0);
      // Left face: left rear, left front, apex
      if (sendNormals)
         glNormal3fv(leftNormal.getValue());
      SEND_VERTEX(-1.0, -1.0, -1.0, 0.0,  0.0);
      SEND_VERTEX(-1.0, -1.0,  1.0, .25,  0.0);
      SEND_VERTEX( 0.0,  1.0,  0.0, .125, 1.0);

      glEnd();
   }

   if (HAS_PART(curParts, BASE)) {

      // Send the next material if it varies per part. 
      // Use SoGLLazyElement::sendDiffuseByIndex().
      // This also sends transparency, so that if 
      // transparency type is not SCREEN_DOOR, there can be
      // a change of transparency across the shape.
      
      if (materialPerPart){
          //Obtain a current copy of the SoGLLazyElement; 
          //use this for the send.
          lazyElt = 
         (SoGLLazyElement*)SoLazyElement::getInstance(state);
          lazyElt->sendDiffuseByIndex(1);
      }

      if (sendNormals)
         glNormal3fv(baseNormal.getValue());

      // Base: left rear, right rear, right front, left front
      glBegin(GL_QUADS);
      SEND_VERTEX(-1.0, -1.0, -1.0, 0.0,  0.0);
      SEND_VERTEX( 1.0, -1.0, -1.0, 1.0,  0.0);
      SEND_VERTEX( 1.0, -1.0,  1.0, 1.0,  1.0);
      SEND_VERTEX(-1.0, -1.0,  1.0, 0.0,  1.0);
      glEnd();
   }

   // Restore the GL matrix.
   glPopMatrix();

   // Reset the diffuse color, if we sent it twice.
   if (materialPerPart)
      lazyElt->reset(state, SoLazyElement::DIFFUSE_MASK);

}

//
// Generates triangles representing a pyramid.
//

void
Pyramid::generatePrimitives(SoAction *action)
{
   // The pyramid will generate 6 triangles: 1 for each side
   // and 2 for the base. (Again, ignore complexity.)
   // This variable is used to store each vertex.
   SoPrimitiveVertex   pv;

   // Access the state from the action.
   SoState  *state = action->getState();

   // See which parts are enabled.
   int curParts = (parts.isIgnored() ? ALL : 
                                         parts.getValue());

   // We need the size to adjust the coordinates.
   float halfWidth, halfHeight, halfDepth;
   getSize(halfWidth, halfHeight, halfDepth);

   // See if we have to use a texture coordinate function,
   // rather than generating explicit texture coordinates.
   SbBool useTexFunc = 
      (SoTextureCoordinateElement::getType(state) ==
       SoTextureCoordinateElement::FUNCTION);

   // If we need to generate texture coordinates with a
   // function, we'll need an SoGLTextureCoordinateElement.
   // Otherwise, we'll set up the coordinates directly.
   const SoTextureCoordinateElement *tce;
   SbVec4f texCoord;
   if (useTexFunc)
      tce = SoTextureCoordinateElement::getInstance(state);
   else {
      texCoord[2] = 0.0;
      texCoord[3] = 1.0;
   }

   // Determine if there's a material bound per part.
   SoMaterialBindingElement::Binding binding = 
      SoMaterialBindingElement::get(state);
   SbBool materialPerPart =
      (binding == SoMaterialBindingElement::PER_PART ||
       binding == 
                 SoMaterialBindingElement::PER_PART_INDEXED);

   // We use this macro to make the code easier. It uses the
   // "point" variable to store the primitive vertex's point.
   SbVec3f  point;

#define GEN_VERTEX(pv, x, y, z, s, t, normal)   \
      point.setValue(halfWidth  * x,            \
                     halfHeight * y,            \
                     halfDepth  * z);           \
      if (useTexFunc)                           \
         texCoord = tce->get(point, normal);    \
      else {                                    \
         texCoord[0] = s;                       \
         texCoord[1] = t;                       \
      }                                         \
      pv.setPoint(point);                       \
      pv.setNormal(normal);                     \
      pv.setTextureCoords(texCoord);            \
      shapeVertex(&pv)

   if (HAS_PART(curParts, SIDES)) {

      // We will generate 4 triangles for the sides of the
      // pyramid. We can use the beginShape(), shapeVertex(),
      // andendShape() convenience functions on SoShape to 
      // make the triangle generation easier and clearer.
      // The shapeVertex() call is built into the macro.

      // Note that there is no detail information for the
      // Pyramid. If there was, we would create an instance 
      // of the correct subclass of SoDetail (such as
      // PyramidDetail) and call pv.setDetail(&detail) once.

      beginShape(action, TRIANGLES);

      // Front face: left front, right front, apex
      GEN_VERTEX(pv, -1.0, -1.0,  1.0, .25,  0.0, 
                                       frontNormal);
      GEN_VERTEX(pv,  1.0, -1.0,  1.0, .50,  0.0,
                                       frontNormal);
      GEN_VERTEX(pv,  0.0,  1.0,  0.0, .325, 1.0,
                                       frontNormal);

      // Right face: right front, right rear, apex
      GEN_VERTEX(pv,  1.0, -1.0,  1.0, .50,  0.0,
                                       rightNormal);
      GEN_VERTEX(pv,  1.0, -1.0, -1.0, .75,  0.0,
                                       rightNormal);
      GEN_VERTEX(pv,  0.0,  1.0,  0.0, .625, 1.0,
                                       rightNormal);

      // Rear face: right rear, left rear, apex
      GEN_VERTEX(pv,  1.0, -1.0, -1.0, .75,  0.0,  
                                       rearNormal);
      GEN_VERTEX(pv, -1.0, -1.0, -1.0, 1.0,  0.0,
                                       rearNormal);
      GEN_VERTEX(pv,  0.0,  1.0,  0.0, .875, 1.0,
                                       rearNormal);

      // Left face: left rear, left front, apex
      GEN_VERTEX(pv, -1.0, -1.0, -1.0, 0.0,  0.0,
                                        leftNormal);
      GEN_VERTEX(pv, -1.0, -1.0,  1.0, .25,  0.0, 
                                        leftNormal);
      GEN_VERTEX(pv,  0.0,  1.0,  0.0, .125, 1.0,
                                        leftNormal);

      endShape();
   }

   if (HAS_PART(curParts, BASE)) {

      // Increment the material index in the vertex if
      // necessary. (The index is set to 0 by default.)
      if (materialPerPart)
         pv.setMaterialIndex(1);

      // We will generate two triangles for the base, as a
      // triangle strip.
      beginShape(action, TRIANGLE_STRIP);

      // Base: left front, left rear, right front, right rear
      GEN_VERTEX(pv, -1.0, -1.0,  1.0, 0.0,  1.0, 
                                      baseNormal);
      GEN_VERTEX(pv, -1.0, -1.0, -1.0, 0.0,  0.0,
                                      baseNormal);
      GEN_VERTEX(pv,  1.0, -1.0,  1.0, 1.0,  1.0,
                                       baseNormal);
      GEN_VERTEX(pv,  1.0, -1.0, -1.0, 1.0,  0.0,
                                       baseNormal);

      endShape();
   }
}

//
// Computes the bounding box and center of a pyramid.
//

void
Pyramid::computeBBox(SoAction *, SbBox3f &box, SbVec3f 
                                                 &center)
{
   // Figure out what parts are active.
   int curParts = (parts.isIgnored() ? ALL :
                                           parts.getValue());

   // If no part is active, set the bounding box to be tiny.
   if (curParts == 0)
      box.setBounds(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);

   else {
      // These points define min and max extents of the box.
      SbVec3f min, max;

      // Compute the half-width, half-height, and half-depth 
      // of the pyramid. We'll use this info to set the min 
      // and max points.
      float   halfWidth, halfHeight, halfDepth;
      getSize(halfWidth, halfHeight, halfDepth);

      min.setValue(-halfWidth, -halfHeight, -halfDepth);

      // The maximum point depends on whether the SIDES are
      // active. If not, only the base is present.
      if (HAS_PART(curParts, SIDES))
         max.setValue(halfWidth, halfHeight, halfDepth);
      else
         max.setValue(halfWidth, -halfHeight, halfDepth);

      // Set the box to bound the two extreme points
      box.setBounds(min, max);
   }

   // This defines the "natural center" of the pyramid. We 
   // could define it to be the center of the base, if we 
   // want, but let's just make it the center of the
   // bounding box.
   center.setValue(0.0, 0.0, 0.0);
}

//
// Computes and returns half-width, half-height, and 
// half-depth based on current field values.
//

void
Pyramid::getSize(float &halfWidth,
                 float &halfHeight,
                 float &halfDepth) const
{
   halfWidth  = (baseWidth.isIgnored() ? 1.0 :
                 baseWidth.getValue()  / 2.0);
   halfHeight = (height.isIgnored()    ? 1.0 :
                 height.getValue()     / 2.0);
   halfDepth  = (baseDepth.isIgnored() ? 1.0 :
                 baseDepth.getValue()  / 2.0);
}


Tip: The easiest way to make sure your generatePrimitives() method is working is to use it for rendering, by temporarily commenting out your shape's GLRender() method (if it has one).


Creating a Group Node

The example discussed in this section illustrates how to create a group node subclass. (It is unlikely, however, that you'll need to create a new group class.) Our example class, Alternate, traverses every other child (that is, child 0, then child 2, and so on). Since, like the base SoGroup class, it has no fields, this example also illustrates how to create a node with no fields.

If you do create a new group class, you probably need to define a new traversal behavior for it. You may be able to inherit some of the traversal behavior from the parent class. Most groups define a protected traverseChildren() method that implements their “typical” traversal behavior. Your new group can probably inherit the read() and write() methods from SoGroup.

Child List

SoGroup, and all classes derived from it, store their children in an instance of SoChildList. This extender class provides useful methods for group classes, including the traverse() method, which has three forms:

traverse(action) 

traverses all children in the child list

traverse(action, firstChild, lastChild) 


traverses the children from first child to last child, inclusive

traverse(action, childIndex) 


traverses one child with the specified index

Hidden Children

If you want your new node to have children, but you don't want to grant public access to the child list, you can implement the node to have hidden children. Node kits are an example of groups within the Inventor library that have hidden children. Because node kits have a specific internal structure, access to the children needs to be restricted. If you want the node to have hidden children, it should not be derived from SoGroup, which has public children only.

SoNode provides a virtual getChildren() method that returns NULL by default. To implement a new node with hidden children, you need to do the following:

  1. Maintain an SoChildList for the node. This list can be a hierarchy of nodes.

  2. Implement a getChildren() method that returns a pointer to the child list. (SoPath uses getChildren() to maintain paths.)

Using the Path Code

Recall that an action can be applied to a node, a single path, or a path list. Before a group can traverse its children, it needs to know what the action has been applied to. The getPathCode() method of SoAction returns an enumerated value that indicates whether the action is being applied to a path and, if so, where this group node is in relation to the path or paths. The values returned by getPathCode() are as follows:

NO_PATH  

The action is not applied to a path (that is, the action is applied to a node).

BELOW_PATH  

This node is at or below the last node in the path chain.

OFF_PATH  

This node is not on the path chain (the node is to the left of the path; it needs to be traversed if it affects the nodes in the path).

IN_PATH  

The node is in the chain of the path (but is not the last node).

For SoGroup, if the group's path code is NO_PATH, BELOW_PATH, or OFF_PATH, it traverses all of its children. (Even if a node is OFF_PATH, you need to traverse it because it affects the nodes in the path to its right. Note, though, that if an SoSeparator is OFF_PATH, you do not need to traverse it because it does not have any effect on the path.) If a node is IN_PATH, you may not need to traverse all children in the group, since children to the right of the action path do not affect the nodes in the path. In this case, getPathCode() returns the indices of the children that need to be traversed. The traverseChildren() method for SoGroup looks like this:

void
SoGroup::traverseChildren(SoAction *action)
{
   int           numIndices;
   const   int   *indices;

   if (action->getPathCode(numIndices, indices) 
       == SoAction::IN_PATH) 
       children.traverse(action, 0, indices[numIndices - 1]);
           // Traverse all children up to and including the 
           // last child to traverse.

   else
      children.traverse(action);   // traverse all children
}

The GL render, callback, handle event, pick, and search methods for SoGroup all use traverseChildren(). The write method for SoGroup, which can be inherited by most subclasses, tests each node in the group before writing it out. The get matrix method does not use traverseChildren() because it doesn't need to traverse as much. If the path code for a group is NO_PATH or BELOW_PATH, it does not traverse the children. Here is the code for SoGroup::getMatrix():

void
SoGroup::getMatrix(SoGetMatrixAction *action)
{
   int         numIndices;
   const int   *indices;

   switch (action->getPathCode(numIndices, indices)) {
      case SoAction::NO_PATH:
      case SoAction::BELOW_PATH:
         break;
      case SoAction::IN_PATH:
         children.traverse(action, 0, 
                                   indices[numIndices - 1]);
         break;
      case SoAction::OFF_PATH:
         children.traverse(action);
         break;
      }
}

If a node is IN_PATH, the getMatrix() method traverses all the children in the group up to and including the last child in the action path (but not the children to the right of the path). If a node is OFF_PATH, the getMatrix() method traverses all the children in the group, since they can affect what is in the path.

What Happens If an Action Is Terminated?

Some actions, such as the GL render, handle event, and search actions, can terminate prematurely—for example, when the node to search for has been found. The SoAction class has a flag that indicates whether the action has terminated. The SoChildList class checks this flag automatically, so this termination is built into the SoChildList::traverse() methods, and the group traversal methods do not need to check the flag.

The new Alternate class can inherit the read and write methods from SoGroup. We just have to define the traversal behavior for the other actions. Most of the other actions can be handled by the traverseChildren() method.

If your group subclass is derived from SoSeparator, you must write special rendering methods that correspond to the different path nodes. See “The GLRender() Method”.

Alternate Node

The class header for the Alternate node is shown in Example A-5.

Example A-4. Alternate.h


#include <Inventor/nodes/SoGroup.h>
// SoGroup.h includes SoSubNode.h; no need to include it.

class Alternate : public SoGroup {

   SO_NODE_HEADER(Alternate);

 public:
   // Initializes this class.
   static void   initClass();

   // Default constructor
   Alternate();

   // Constructor that takes approximate number of children 
   // as a hint
   Alternate::Alternate(int numChildren);

 protected:
   // Generic traversal of children for any action.
   virtual void  doAction(SoAction *action);

   // These implement supported actions.
   virtual void  getBoundingBox(SoGetBoundingBoxAction
                                           *action);
   virtual void  GLRender(SoGLRenderAction *action);
   virtual void  handleEvent(SoHandleEventAction *action);
   virtual void  pick(SoPickAction *action);
   virtual void  getMatrix(SoGetMatrixAction *action);
   virtual void  search(SoSearchAction *action);

 private:
   // Destructor
   virtual ~Alternate();
};

The Alternate class source code is shown in Example A-6.

Example A-5. Alternate.c++


#include <Inventor/misc/SoChildList.h>
#include <Inventor/actions/SoGLRenderAction.h>
#include <Inventor/actions/SoGetBoundingBoxAction.h>
#include <Inventor/actions/SoGetMatrixAction.h>
#include <Inventor/actions/SoHandleEventAction.h>
#include <Inventor/actions/SoPickAction.h>
#include <Inventor/actions/SoSearchAction.h>
#include "Alternate.h"

SO_NODE_SOURCE(Alternate);

// This initializes the Alternate class.

void
Alternate::initClass()
{
   // Initialize type id variables
   SO_NODE_INIT_CLASS(Alternate, SoGroup, "Group"); 
}

// Constructor
Alternate::Alternate()
{
   SO_NODE_CONSTRUCTOR(Alternate);
}
// Constructor that takes approximate number of children.

Alternate::Alternate(int numChildren) : SoGroup(numChildren)
{
   SO_NODE_CONSTRUCTOR(Alternate);
}

// Destructor

Alternate::~Alternate()
{
}

// Each of these implements an action by calling the standard
// traversal method. Note that (as in the Glow node source) 
// we prefix the call to doAction() with the name of the
// class to avoid problems with derived classes.

void
Alternate::getBoundingBox(SoGetBoundingBoxAction *action)
{
   Alternate::doAction(action);
}

void
Alternate::GLRender(SoGLRenderAction *action)
{
   Alternate::doAction(action);
}

void
Alternate::handleEvent(SoHandleEventAction *action)
{
   Alternate::doAction(action);
}

void
Alternate::pick(SoPickAction *action)
{
   Alternate::doAction(action);
}

// This implements the traversal for the SoGetMatrixAction,
// which is handled a little differently: it does not 
// traverse below the root node or tail of the path it is
// applied to. Therefore, we need to compute the matrix only 
// if this group is in the middle of the current path chain 
// or is off the path chain (since the only way this could 
// be true is if the group is under a group that affects the // chain).

void
Alternate::getMatrix(SoGetMatrixAction *action)
{
   int         numIndices;
   const int   *indices;

   // Use SoAction::getPathCode() to determine where this 
   // group is in relation to the path being applied to (if 
   // any).
   switch (action->getPathCode(numIndices, indices)) {

    case SoAction::NO_PATH:
    case SoAction::BELOW_PATH:
     // If there's no path, or we're off the end, do nothing.
     break;

    case SoAction::OFF_PATH:
    case SoAction::IN_PATH:
     // If we are in the path chain or we affect nodes in the
     // path chain, traverse the children.
     Alternate::doAction(action);
     break;
   }
}

// This implements the traversal for the SoSearchAction, 
// which is also a little different. The search action is 
// set to either traverse all nodes in the graph or just 
// those that would be traversed during normal traversal. We 
// need to check that flag before traversing our children.
void
Alternate::search(SoSearchAction *action)
{
   // If the action is searching everything, then traverse 
   // all of our children as SoGroup would.
   if (action->isSearchingAll())
     SoGroup::search(action);

   else {
     // First, make sure this node is found if we are 
     // searching for Alternate (or group) nodes.
     SoNode::search(action);

     // Traverse the children in our usual way.
     Alternate::doAction(action);
   }
}

// This implements typical action traversal for an Alternate
// node, skipping every other child.

void
Alternate::doAction(SoAction *action)
{
   int         numIndices;
   const int   *indices;

   // This will be set to the index of the last (rightmost)
   // child to traverse.
   int         lastChildIndex;

   // If this node is in a path, see which of our children 
   // are in paths, and traverse up to and including the 
   // rightmost of these nodes (the last one in the 
   // "indices" array).
   if (action->getPathCode(numIndices, indices) ==
      SoAction::IN_PATH)
     lastChildIndex = indices[numIndices - 1];

   // Otherwise, consider all of the children.
   else
     lastChildIndex = getNumChildren() - 1;

   // Now we are ready to traverse the children, skipping 
   // every other one. For the SoGetBoundingBoxAction, we
   // have to do some extra work in between each pair of
   // children - we have to make sure the center points get
   // averaged correctly.
   if (action->isOfType(
         SoGetBoundingBoxAction::getClassTypeId())) {
     SoGetBoundingBoxAction *bba =
       (SoGetBoundingBoxAction *) action;
     SbVec3f  totalCenter(0.0, 0.0, 0.0);
     int      numCenters = 0;

     for (int i = 0; i <= lastChildIndex; i += 2) {
       children->traverse(bba, i);

       // If the traversal set a center point in the action,
       // add it to the total and reset for the next child.
       if (bba->isCenterSet()) {
         totalCenter += bba->getCenter();
         numCenters++;
         bba->resetCenter();
       }
     }
     // Now, set the center to be the average. Since the
     // centers were already transformed, there's no need to
     // transform the average.
     if (numCenters != 0)
       bba->setCenter(totalCenter / numCenters, FALSE);
   }

   // For all other actions, just traverse every other child.
   else
     for (int i = 0; i <= lastChildIndex; i += 2)
       children->traverse(action, i);
}

Using New Node Classes

Node classes you have created must be initialized in every application that uses them. Example A-7 shows how this is done, using the Glow, Pyramid, and Alternate node classes defined in the previous examples. The program reads a file (newNodes.iv, shown in Example A-8) that has a scene graph containing instances of these nodes. It writes the scene graph to standard output and then opens an examiner viewer to display the graph.

You can see from this example that extender node classes should be initialized after standard classes, which are initialized by SoDB::init(). In this program, SoDB::init() is called by SoXt::init(). Also, base classes must be initialized before any classes derived from them, since the initialization macros for a node class refer to the parent class.

Notice in Example A-8 that the Pyramid and Glow nodes, because they are not built into the Inventor library, write out their field names and types. (The Alternate class has no fields.) See the discussion of the file format for new (unknown) nodes in The Inventor Mentor, Chapter 11.

The isBuiltIn flag is a protected variable in SoFieldContainer, from which SoNode is derived. If this flag is FALSE, field types are written out along with the field values. By default, this flag is FALSE, but all Inventor classes set it to TRUE. If you are building a toolkit that uses Inventor and want your new classes to appear the same as Inventor classes, be sure to set this flag to TRUE.

Example A-6. NewNodes.c++


#include <Inventor/SoDB.h>
#include <Inventor/SoInput.h>
#include <Inventor/Xt/SoXt.h>
#include <Inventor/Xt/viewers/SoXtExaminerViewer.h>
#include <Inventor/actions/SoWriteAction.h>
#include <Inventor/nodes/SoSeparator.h>

// Header files for new node classes
#include "Glow.h"
#include "Pyramid.h"
#include "Alternate.h"

main(int, char **argv)
{
   SoInput      myInput;
   SoSeparator  *root;

   // Initialize Inventor and Xt.
   Widget myWindow = SoXt::init(argv[0]);
   if (myWindow == NULL) exit(1);

   // Initialize the new node classes.
   Glow::initClass();
   Pyramid::initClass();
   Alternate::initClass();

   if (! myInput.openFile("newNodes.iv")) {
      fprintf(stderr, "Can't open \"newNodes.iv\"\n");
      return 1;
   }

   root = SoDB::readAll(&myInput);
   if (root == NULL) {
      printf("File \"newNodes.iv\" contains bad data\n");
      return 2;
   }

   root->ref();

   // Write the graph to stdout.
   SoWriteAction wa;
   wa.apply(root);

   // Render it.
   SoXtExaminerViewer *myViewer =
      new SoXtExaminerViewer(myWindow);
   myViewer->setSceneGraph(root);
   
   //The following results in high-quality transparency and 
   //will permit PER_PART material binding to vary 
   //transparency.  Delete it and the pyramid will be 
   //rendered with the same SCREEN_DOOR transparency
   //overall.
   myViewer->setTransparencyType
                   (SoGLRenderAction::SORTED_OBJECT_BLEND);
   myViewer->setTitle("NewNodes");
   myViewer->show();
   myViewer->viewAll();

   SoXt::show(myWindow);
   SoXt::mainLoop();

   return 0;
}


Example A-7. newNodes.iv


#Inventor V2.0 ascii

#
# Input file for "newNodes" example program
#

Separator {
   MaterialBinding { value PER_PART }
   Material {
      diffuseColor   [.3 .6 .3, .9 .3 .2] 
      transparency   [ 0.8 , 0.0]
      shininess      .5
   }

   # Skip every other child.
   Alternate {
      fields         []

      Pyramid {
         fields         []
      }

      Cube {}           # This child is skipped

      Separator {
         MaterialBinding { value OVERALL }
         Glow {
            fields [ SFColor color , SFFloat brightness , 
                                     SFFloat transparency]
            brightness  .6
            color       .8 .3 .3
	            transparency .9
         }
         Transform {
            translation 3 .6 0
         }
         Pyramid {
            fields      [SFFloat   height ]
            height      3.2
         }
      }

      Sphere {}         # This child is skipped.
   }
}

Creating an Abstract Node Class

Creating an abstract node class is slightly different from creating a nonabstract one. Examples of abstract node classes are SoCamera, SoLight, and SoShape.

First, abstract classes should use the ABSTRACT versions of the macros described in SoSubNode.h. For example, the SoLight class makes this call in its initClass() method:

SO_NODE_INIT_ABSTRACT_CLASS(SoLight, "Light", SoNode);

Second, the constructor for an abstract class should be protected, meaning that it is impossible to create an instance of it.

The copyContents() Method

The copy() method defined for SoNode creates a copy of an instance of a node. It invokes the virtual copyContents() method. If your node has no data other than fields and public children, then the copyContents() methods defined for SoNode and SoGroup should suffice.

However, if you have extra instance data in your node that needs to be copied, you have to override the copyContents() method. For example, if the Pyramid node class defined earlier contained a private integer member variable called count (for some private reason), the copyContents() method would look like this:

void 
Pyramid::copyContents(const SoFieldContainer *fromFC,
                      SbBool copyConnections)
{
    // Copy the usual stuff by calling the base class method.
    SoShape::copyContents(fromFC, copyConnections);

    // Copy the "count" field explicitly.
    count = ((Pyramid *)fromFC)->count;
}

The affectsState() Method

The affectsState() method on SoNode indicates whether a node has a net effect on the state. (For example, SoSeparator changes the state, but it restores the state, so there's no net effect.) The default value for this method is TRUE, but some node classes such as SoSeparator, SoShape, SoArray, and SoMultipleCopy define it to be FALSE. When you define a new node class, you may need to redefine its affectsState() method if it differs from that of the parent class.

Uncacheable Nodes

You may create a new node whose effects should not be cached during rendering or bounding-box computation. For example, the SoCallback node allows a user to change the effect of the callback function, such as drawing a cube instead of a sphere, without ever making an Inventor call.

Uncacheable nodes such as SoCallback should call:

SoCacheElement::invalidate(state)

which aborts construction of the current cache. This call can be made during the render or bounding box action (the two actions that support caching). The invalidate() method also turns off auto-caching on any SoSeparator nodes over the uncacheable node.

Creating an Alternate Representation

When you create a new node, you probably also want to create an alternate representation for it that can be written to a file. For example, a good alternate representation for the Glow node would be an SoMaterial node with all fields ignored except for emissiveColor and transparency. The alternate representation is in the form of a field called alternateRep, of type SoSFNode. If your node is later read into an Inventor application that is not linked with this new node, Inventor can render the node using this alternate representation even though the node has not been initialized with the database. (See Chapter 11 in The Inventor Mentor on reading in extender nodes and engines.)

Within your program, when a change is made to the original node, you may want the alternate representation to change as well. In this case, override the write() method on SoNode to update the alternate representation, and then have it call the write() method of the base class.

Generating Default Normals

If you define your own vertex-based shape class and the parent class does not generate default normals, you need to generate default normals for rendering with the Phong lighting model and for generating primitives. SoVertexShape provides the generateDefaultNormals() method. Although the specifics depend on the shape itself, SoNormalBundle provides methods to facilitate this process.


Tip: If you define a node class that creates a node sensor attached to itself or a field sensor attached to one of its fields, you need to redefine readInstance() so that the sensor doesn't fire when the node is read from a file. Your readInstance() method needs to detach the sensor, call the readInstance() method of the parent class, and then reattach the sensor. Node kits provide the setUpConnections() method to make and break these connections (see Chapter 7 in The Inventor Toolmaker).