This chapter explains how you can create new subclasses of SoNode. It discusses enabling elements in the state, constructing a node, and implementing actions for it. Chapter 1 provides important background material on these concepts, and this chapter assumes you are familiar with the material presented there.
The first part of this chapter 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 chapter examples show how to create three new node classes:
A property node class called Glow
A shape node class called Pyramid
A group node class called Alternate
Sections at the end of the chapter discuss the following:
Using new node classes
Creating abstract classes
The copy() method
The affectsState() method
Creating nodes that cannot be cached
Creating an alternate representation for a new node class
The file SoSubNode.h contains the macros for defining new node classes. The SO_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() macro. Other macros useful in creating new node classes are mentioned in the following sections.
Creating a new node requires these steps:
Select a name for the new node class and determine what class it is derived from.
Define and name each field in the node.
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”).
Define a constructor (see “Defining the Constructor”).
Implement the actions supported by the node (see “Implementing Actions”).
a. 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.
b. For a shape node, you need to implement the generate-
Primitives() 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
(see “Generating Default Normals”).
c. For a group node, you need to implement all actions to ensure that
the children are traversed
(see “Creating a Group Node”).
Implement a copy() method if the node contains any non-field instance data (see “The copy() Method”).
Implement an affectsState() method if it cannot be inherited from the parent class (see “The affectsState() Method”).
As discussed in Chapter 1, 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(), does most of the 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.
In the initClass() method, use the SO_ENABLE() macro (defined in SoAction.h) to enable the elements required by your node in the state. To use a 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. (See Table 1-1.) |
Now that these elements have been enabled, their values can be set and inquired. (The debugging version of Inventor generates an error if you try to access or set an element that has not been enabled.)
The previous example using SoDrawStyle elements 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 OpenGL when it is set. As you might guess, 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.)
The constructor defines the fields for the node and sets up their default values. If the fields contain enumerated values, their names and values are defined in the constructor as well. Use the SO_NODE_CONSTRUCTOR() macro to perform the basic work.
The SO_NODE_IS_FIRST_INSTANCE() macro returns a Boolean value that 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.)
The SO_NODE_ADD_FIELD() macro defines the fields in the node and sets 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())); |
To add a field with a vector value, the syntax is as follows:
SO_NODE_ADD_FIELD(translation, (0.0, 0.0, 0.0)); |
In the preceding example, the 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); |
Then, to specify that these enumerated values can be used in the style field of the SoDrawStyle node, use the SO_NODE_SET_SF_ENUM_TYPE() macro:
SO_NODE_SET_SF_ENUM_TYPE(style, Style); |
Your next task is to implement each of the actions your new node supports. 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. |
For the GL render action, the SoDrawStyle node changes the values of four 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. The corresponding code to set the element's value is
if (! style.isIgnored()) SoDrawStyleElement::set(state, this, (SoDrawStyleElement::Style) style.getValue()); |
For the callback action, the SoDrawStyle node 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, this common code is put into a separate method that can be called by both the GL render and the callback actions. 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 is
void SoDrawStyle::callback(SoCallbackAction *action) ( doAction(action); } |
The code for the draw-style node's GL render action is also simple (and familiar):
void SoDrawStyle::GLRender(SoGLRenderAction *action) ( doAction(action); } |
To complete the story, here is the complete code for the draw-style node's doAction() method:
void SoDrawStyle::doAction(SoAction *action) { SoState *state = action->getState(); if (! style.isIgnored()) SoDrawStyleElement::set(state, this, (SoDrawStyleElement::Style) style.getValue()); if (! lineWidth.isIgnored()) SoLineWidthElement::set(state, this, lineWidth.getValue()); if (! linePattern.isIgnored()) SoLinePatternElement::set(state, this, linePattern.getValue()); if (! pointSize.isIgnored()) SoPointSizeElement::set(state, this, pointSize.getValue()); } |
The advantage of this scheme becomes apparent when you consider extending the set of actions (see Chapter 4). 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.
As discussed in Chapter 1, each element class provides methods for setting and inquiring its value. The static set() method usually has three parameters, as shown in the previous section:
Most element classes also define a static get() method that returns the current value stored in an element instance. For example, to obtain the current draw style:
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:
Elements are designed to be small and specific, for two reasons. The first reason 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 SoBaseColor node changes only the SoDiffuseColorElement 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
The SoMaterialBundle class accesses all elements having to do with surface materials. Methods on the bundle allow shapes to step easily through sequential materials and to send the current material to OpenGL. The SoNormalBundle allows you to step easily through sequential normals and provides routines for generating default normals. The SoTextureCoordinateBundle allows you to step through texture coordinates and provides methods for generating texture coordinates if the shape is using SoTextureCoordinatePlane or SoTextureCoordinateEnvironment.
The easiest way to learn how to add a node class is by example. The first example creates a new property node 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.
For this class, we need to implement actions that deal with materials: GLRender() and callback(). We will use doAction() (see “The doAction() Method”), since it performs the same operation for both actions. The doAction() method for the Glow class updates the emissive color element based on the values of the color and brightness fields of the node.
The class header for our new node is shown in Example 2-1.
#include <Inventor/SbColor.h> #include <Inventor/fields/SoSFColor.h> #include <Inventor/fields/SoSFFloat.h> #include <Inventor/nodes/SoSubNode.h> class Glow : public SoNode { SO_NODE_HEADER(Glow); public: // Fields: SoSFColor color; // Color of glow SoSFFloat brightness; // Amount of glow (0-1) // 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 that // deal 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, rather than using the reference count mechanism. virtual ~Glow(); // Holds emissive color. A pointer to this is stored in the // state. SbColor emissiveColor; }; |
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 use of the element in the example is also typical; most elements have simple set() methods to store values.
The source code for the Glow class is shown in Example 2-2.
#include <Inventor/actions/SoCallbackAction.h> #include <Inventor/actions/SoGLRenderAction.h> #include <Inventor/bundles/SoMaterialBundle.h> #include <Inventor/elements/SoEmissiveColorElement.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. The 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 tasks 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)); } // Destructor Glow::~Glow() { } // 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); // For efficiency, Inventor nodes make sure that the first // defined material is always in GL, so shapes do not have to // send the first material each time. (This keeps caches from // being dependent on material values in many cases.) The // SoMaterialBundle class allows us to do this easily. SoMaterialBundle mb(action); mb.forceSend(0); } // 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 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. if (! brightness.isIgnored()) { // Define the emissive color as the product of the // "brightness" and "color" fields. "emissiveColor" is an // instance variable. Since material elements contain // pointers to the actual values, we need to store the // value in the instance. (We could have defined the // fields to contain multiple values, in which case we // would have to store an array of emissive colors.) emissiveColor = color.getValue() * brightness.getValue(); // Set the value of the emissive color element to our one // new emissive color. "this" is passed in to let the // caching mechanism know who set this element and to // handle overriding. (Note that this call will have no // effect if another node with a TRUE Override flag set // the element previously.) SoEmissiveColorElement::set(action->getState(), this, 1, &emissiveColor); } } |
This next example is more complicated than the property-node example, 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.
When it is traversed to generate primitives for the SoCallbackAction, each shape 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. Filled shapes, such as 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.
The SoPrimitiveVertex contains all information for that vertex:
The shape's generatePrimitives() method sets each of these values.
The appropriate callback function can be invoked either automatically or explicitly. If you want explicit control over when the callback function is invoked, you can use the following methods provided by the SoShape class:
To take advantage of the automatic mechanism, use these three methods, provided by the SoShape base class as a convenience:
The 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.
You may want your shape to store additional information in an 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().
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 will be 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 chapter 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 will be 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.
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 it intersects the primitive, it creates an SoPickedPoint. SoShape provides three virtual methods for creating details:
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 chapter 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); } } } |
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.
This example creates a 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 bit-mask fields.
The class header for the Pyramid node is shown in Example 2-3.
#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 again. // 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 given part is on or off. (Convenience) SbBool hasPart(Part part) const; protected: // This implements the GL rendering action. We will 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 and 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 ¢er); 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 2-4.
#include <GL/gl.h> #include <Inventor/SbBox.h> #include <Inventor/SoPickedPoint.h> #include <Inventor/SoPrimitiveVertex.h> #include <Inventor/actions/SoGLRenderAction.h> #include <Inventor/bundles/SoMaterialBundle.h> #include <Inventor/elements/SoGLTextureCoordinateElement.h> #include <Inventor/elements/SoGLTextureEnabledElement.h> #include <Inventor/elements/SoLightModelElement.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; // Make sure things are set up correctly for a solid object. // We are solid if all parts are on. beginSolidShape() is a // method on SoShape that sets up backface culling and other // optimizations. if (curParts == ALL) beginSolidShape(action); // 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. SbBool doTextures = (SoGLTextureEnabledElement::get(state) && SoTextureCoordinateElement::getType(state) != SoTextureCoordinateElement::NONE); // Determine if we need to send normals. Normals are // necessary if we are not doing BASE_COLOR lighting. SbBool sendNormals = (SoLightModelElement::get(state) != SoLightModelElement::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); // Make sure first material is sent if necessary. We'll use // the SoMaterialBundle class because it makes things very // easy. SoMaterialBundle mb(action); mb.sendFirst(); // 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-tessellated 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. if (materialPerPart) mb.send(1, FALSE); 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(); // Terminate the effects of rendering a solid shape if // necessary. if (curParts == ALL) endSolidShape(action); } // 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, we are ignoring 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'll 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() / // endShape() 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 were, 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 ¢er) { // Figure out what parts are active. int curParts = (parts.isIgnored() ? ALL : parts.getValue()); // If no parts are 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 the 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). |
This example 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 will 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.
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) |
| |
traverse(action, childIndex) |
|
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:
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 being 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) |
Figure 2-1 shows five group nodes. Assume the render action is being applied to the path shown. Groups A and C are considered IN_PATH. Group B is OFF_PATH, and Groups D and E are BELOW_PATH.
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 will 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.
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.
The class header for the Alternate node is shown in Example 2-5.
#include <Inventor/nodes/SoGroup.h> // SoGroup.h includes SoSubNode.h; no need to include it again. 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 2-6.
#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, however, 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); } |
Node classes you have created must be initialized in every application that uses them. Example 2-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 2-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 2-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.
#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); myViewer->setTitle("NewNodes"); myViewer->show(); myViewer->viewAll(); SoXt::show(myWindow); SoXt::mainLoop(); } |
#Inventor V2.0 ascii # # Input file for "newNodes" example program # Separator { Separator { Transform { translation 0 -1.1 0 } Cube { width 10 height .1 depth 10 } } Material { diffuseColor .3 .6 .9 shininess .5 } # Skip every other child Alternate { fields [] Pyramid { fields [SFBitMask parts, SFFloat baseWidth, SFFloat baseDepth, SFFloat height ] } Cube {} # This child is skipped Separator { Glow { fields [SFColor color, SFFloat brightness ] brightness .6 color 1 .3 .3 } Transform { translation 3 .6 0 } Pyramid { fields [SFBitMask parts, SFFloat baseWidth, SFFloat baseDepth, SFFloat height ] height 3.2 } } Sphere {} # This child is skipped } } |
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 copy() method defined for SoNode creates a copy of an instance of a node. If your node has no data other than fields and public children, then the copy() methods defined for SoNode and SoGroup should suffice.
However, if you have extra instance data in your node that needs to be copied, you will have to override the copy method. For example, if our Pyramid node class defined earlier contained a private integer member variable called count (for some private reason), the copy() method would look like this:
SoNode * Pyramid::copy(SbBool copyConnections) const { // Use the standard version of the copy method to create a // copy of this instance, including its field data Pyramid *newPyramid = (Pyramid *) SoNode::copy(copyConnections); // Copy the "count" field explicitly newPyramid->count = count; return newPyramid; } |
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.
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.
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. 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 will be able to 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.
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, which you can override for use when normal binding is DEFAULT. 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'll 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). |