2.6. Creating a Shape Node

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( C++ | Java | .NET ), 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( C++ | Java | .NET ), 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( C++ | Java ). 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( C++ | Java | .NET ). For example, if the shape generates triangles, the triangle callback function is invoked for every triangle generated. Filled shapes, such as SoCone( C++ | Java | .NET ) and SoQuadMesh( C++ | Java | .NET ), generate triangles (regardless of draw style), line shapes (such as SoLineSet( C++ | Java | .NET ) and SoIndexedLineSet( C++ | Java | .NET )) generate line segments, and point shapes (such as SoPointSet( C++ | Java | .NET )) generate points.

You may want your shape to store additional information in an SoDetail( C++ | Java | .NET )—for example, what part of the shape each vertex belongs to. In this case, you can use an existing subclass of SoDetail( C++ | Java | .NET ) (see the section called “Using an SoDetail”), or you can create a new SoDetail( C++ | Java | .NET ) subclass to hold the appropriate information. By default, the pointer to the detail in SoPrimitiveVertex( C++ | Java ) is NULL.

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

For rendering, you may be able to inherit the GLRender() method from the SoShape( C++ | Java | .NET ) 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( C++ | Java | .NET ) 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( C++ | Java | .NET ). SoShape( C++ | Java | .NET ) 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( C++ | Java | .NET ) 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 example from the SoSphere( C++ | Java | .NET ) class shows how to implement your own rayPick() method:


C++
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( C++ | Java | .NET ) 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( C++ | Java | .NET ), 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( C++ | Java | .NET ), you can inherit computeBBox() from the base SoIndexedShape( C++ | Java | .NET ) 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.

OpenInventor provides a way to implement a custom node using one or several “implementation scenegraph”. The idea is to delegate rendering, but also picking, computeBbox, and any other actions to a lower level SceneGraph. For example, SoCube( C++ | Java | .NET ) nodes could be implemented using an SoFaceSet( C++ | Java | .NET ) by composition, which is actually the case. In simplest case, you can redirect every action to your implementation scenegraph by just redefining the doAction() method. Let’s see an example with the Pyramid Node:

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( C++ | Java | .NET ), 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( C++ | Java | .NET ). 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.

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.

Example 2.3.  Pyramid.h


C++
#include <Inventor/SbLinear.h>
#include <Inventor/fields/SoSFBitMask.h>
#include <Inventor/fields/SoSFFloat.h>
#include <Inventor/nodes/SoShape.h>
#include <Inventor/nodes/SoIndexedTriangleSet.h>
#include <Inventor/caches/SoNodeDependencies.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();
  static void exitClass();

  // 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& center );

  virtual void doAction( SoAction* action );

  // Destructor
  virtual ~Pyramid();

private:
  // Static data representing the geometry of the pyramid.
  // Static members are used in order to minimize memory consumption.

  // Vertex positions
  static std::vector<SbVec3f> s_vertices;

  // Vertex normals
  static std::vector<SbVec3f> s_normals;

  // Texture coordinates
  static std::vector<SbVec2f> s_texCoords;

  // Vertex indices
  static std::vector<int32_t> s_vertexIndices;

  // Normal indices
  static std::vector<int32_t> s_normalIndices;

  // Texture Coordinates indices
  static std::vector<int32_t> s_texCoordIndices;

  // Material indices
  static std::vector<int32_t> s_materialIndices;

  // static vertex property node containing the geometry of the pyramid
  static SoRef<SoVertexProperty> s_vertexProperty;

  // Internal shape representing the pyramid
  SoRef<SoIndexedTriangleSet> m_triangleSet;

  // Retrieve internal shape representing the pyramid
  SoNode* getInternalShape( SoState* state );

  // Update internal shape geometry depending on the value of the #parts field.
  void updateInternalShape();

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

  // This cache handler is used as a flag to check whether or not the internal shape needs to be rebuilt.
  // It is needed to add a dependency on the shape's fields, so that the cache automatically invalidates
  // itself when one of the fields is changed.
  // In our case, we need a dependancy on the #parts field.
  SoRef<SoNodeDependencies> m_internalShapeCache;
};

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

Example 2.4.  Pyramid.c++


C++
#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/SoTextureCoordinateElement.h>
#include <Inventor/elements/SoTextureEnabledElement.h>
#include <Inventor/elements/SoModelMatrixElement.h>
#include <Inventor/misc/SoState.h>

#include "Pyramid.h"

SO_NODE_SOURCE( Pyramid );

// Vertex positions
std::vector<SbVec3f> Pyramid::s_vertices;

// Vertex normals
std::vector<SbVec3f> Pyramid::s_normals;
// Texture coordinates
std::vector<SbVec2f> Pyramid::s_texCoords;

// Vertex indices
std::vector<int32_t> Pyramid::s_vertexIndices;

// Normal indices
std::vector<int32_t> Pyramid::s_normalIndices;

// Texture Coordinates indices
std::vector<int32_t> Pyramid::s_texCoordIndices;

// Material indices
std::vector<int32_t> Pyramid::s_materialIndices;

// static vertex property node containing the geometry of the pyramid
SoRef<SoVertexProperty> Pyramid::s_vertexProperty;

// This initializes the Pyramid class.
void
Pyramid::initClass()
{
  // Initialize type id variables
  getClassRenderEngineMode().setRenderMode( SbRenderEngineMode::OIV_OPENINVENTOR_RENDERING );
  SO__NODE_INIT_CLASS( Pyramid, "Pyramid", SoShape );
}

void
Pyramid::exitClass()
{
  // Clear static data
  s_vertexProperty = NULL;
  s_vertices.clear();
  s_normals.clear();
  s_texCoords.clear();
  s_vertexIndices.clear();
  s_normalIndices.clear();
  s_texCoordIndices.clear();
  s_materialIndices.clear();

  // Deinit type id variables
  SO__NODE_EXIT_CLASS( Pyramid );
}

// Constructor
Pyramid::Pyramid()
: m_internalShapeCache( new SoNodeDependencies )
{
  SO_NODE_CONSTRUCTOR( Pyramid );
  SO_NODE_ADD_FIELD( parts,     (ALL)  );
  SO_NODE_ADD_FIELD( baseWidth, (2.0f) );
  SO_NODE_ADD_FIELD( baseDepth, (2.0f) );
  SO_NODE_ADD_FIELD( height,    (2.0f) );
  
  // 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
  // Set up the static buffers representing the geometry of the pyramid
  if ( SO_NODE_IS_FIRST_INSTANCE() )
  {
    // Vertex positions
    s_vertices.resize( 5 );
    s_vertices[0].setValue(  0.0f,  1.0f,  0.0f );
    s_vertices[1].setValue( -1.0f, -1.0f,  1.0f );
    s_vertices[2].setValue(  1.0f, -1.0f,  1.0f );
    s_vertices[3].setValue(  1.0f, -1.0f, -1.0f );
    s_vertices[4].setValue( -1.0f, -1.0f, -1.0f );

    // Vertex normals
    s_normals.resize( 5 );
    float invRoot5 = static_cast<float>( 1.0 / sqrt(5.0) );
    float invRoot5Twice = 2.0f * invRoot5;
    s_normals[0].setValue( 0.0f, invRoot5,  invRoot5Twice ); // front normal
    s_normals[1].setValue( invRoot5Twice, invRoot5, 0.0f );  // right normal
    s_normals[2].setValue( 0.0F, invRoot5, -invRoot5Twice ); // rear normal
    s_normals[3].setValue( -invRoot5Twice, invRoot5, 0.0f ); // left normal
    s_normals[4].setValue( 0.0f, -1.0f, 0.0f );              // base normal

    // Vertex texture coordinates
    s_texCoords.resize( 11 );
    s_texCoords[0].setValue( 0.0f,   0.0f );
    s_texCoords[1].setValue( 0.25f,  0.0f );
    s_texCoords[2].setValue( 0.5f,   0.0f );
    s_texCoords[3].setValue( 0.75f,  0.0f );
    s_texCoords[4].setValue( 1.0f,   0.0f );
    s_texCoords[5].setValue( 0.0f,   1.0f );
    s_texCoords[6].setValue( 0.125f, 1.0f );
    s_texCoords[7].setValue( 0.325f, 1.0f );
    s_texCoords[8].setValue( 0.625f, 1.0f );
    s_texCoords[9].setValue( 0.825f, 1.0f );
    s_texCoords[10].setValue( 1.0f,  1.0f );

    // Vertex indices (the shape is indexed)
    // 3 indices for each of the 6 triangles => 18 indices
    s_vertexIndices.resize( 18 );
    s_vertexIndices[0] = 1;
    s_vertexIndices[1] = 2;
    s_vertexIndices[2] = 0;
    s_vertexIndices[3] = 2;
    s_vertexIndices[4] = 3;
    s_vertexIndices[5] = 0;
    s_vertexIndices[6] = 3;
    s_vertexIndices[7] = 4;
    s_vertexIndices[8] = 0;
    s_vertexIndices[9] = 4;
    s_vertexIndices[10] = 1;
    s_vertexIndices[11] = 0;
    s_vertexIndices[12] = 4;
    s_vertexIndices[13] = 3;
    s_vertexIndices[14] = 1;
    s_vertexIndices[15] = 1;
    s_vertexIndices[16] = 3;
    s_vertexIndices[17] = 2;

    // Normals indices (1 for each triangle)
    s_normalIndices.resize( 6 );
    s_normalIndices[0] = 0;
    s_normalIndices[1] = 1;
    s_normalIndices[2] = 2;
    s_normalIndices[3] = 3;
    s_normalIndices[4] = 4;
    s_normalIndices[5] = 4;

    // Texture coordinates indices
    s_texCoordIndices.resize( 18 );
    s_texCoordIndices[0] = 1;
    s_texCoordIndices[1] = 2;
    s_texCoordIndices[2] = 7;
    s_texCoordIndices[3] = 2;
    s_texCoordIndices[4] = 3;
    s_texCoordIndices[5] = 8;
    s_texCoordIndices[6] = 3;
    s_texCoordIndices[7] = 4;
    s_texCoordIndices[8] = 9;
    s_texCoordIndices[9] = 0;
    s_texCoordIndices[10] = 1;
    s_texCoordIndices[11] = 6;
    s_texCoordIndices[12] = 0;
    s_texCoordIndices[13] = 4;
    s_texCoordIndices[14] = 5;
    s_texCoordIndices[15] = 5;
    s_texCoordIndices[16] = 4;
    s_texCoordIndices[17] = 10;

    // Material indices (1 for each triangle)
    // Set a material index for the first 4 triangles (the sides)
    // and another one for the last 2 triangles (the base)
    s_materialIndices.resize( 6 );
    for ( size_t i = 0; i < 4; i++ )
      s_materialIndices[i] = 0;
    for ( size_t i = 4; i < s_materialIndices.size(); i++ )
      s_materialIndices[i] = 1;

    // Initialize static vertex property with the static buffers.
    s_vertexProperty = new SoVertexProperty;
    s_vertexProperty->normalBinding.setValue( SoVertexProperty::PER_FACE_INDEXED );
    s_vertexProperty->materialBinding.setValue( SoVertexProperty::PER_PART_INDEXED );

    s_vertexProperty->vertex.setValuesPointer( static_cast<int>(s_vertices.size()), &s_vertices[0] );
    s_vertexProperty->normal.setValuesPointer( static_cast<int>(s_normals.size()), &s_normals[0] );
    s_vertexProperty->texCoord.setValuesPointer( static_cast<int>(s_texCoords.size()), &s_texCoords[0] );
  }

  // Add dependency to the parts field. This allows the cache handler
  // to automatically invalidate itself when the field is changed.
  m_internalShapeCache->addDependency( parts );
}

// 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
{
  int curParts = ( parts.isIgnored() ? ALL : parts.getValue() );
  return ( (curParts & part) != 0 );
}

// Setup internal shape index buffers depending on the value of the #parts field.
void
Pyramid::updateInternalShape()
{
  // There are 6 triangles in the pyramid's geometry.
  // The first 4 triangles represent the side triangles,
  // while the last 2 triangles represent the base.
  // minTriangle and maxTriangle represent the indices of
  // the first and the last triangle that need to be drawn.
  // Their values are used to get an offset and a size in order
  // to set the index buffers correctly.
  int minTriangle = 0;
  int maxTriangle = static_cast<int>( s_normalIndices.size() );

  // If parts doesn't contain SIDES, skip the first 4 triangles
  if ( !hasPart( SIDES ) )
    minTriangle = 4;

  // If parts doesn't contain BASE, skip the last triangles
  if ( !hasPart( BASE ) )
    maxTriangle = 4;

  int numTriangles = maxTriangle - minTriangle;

  if ( numTriangles == 0 )
  {
    // Set the buffers empty if no triangle need to be drawn.
    m_triangleSet->coordIndex.setNum( 0 );
    m_triangleSet->normalIndex.setNum( 0 );
    m_triangleSet->textureCoordIndex.setNum( 0 );
    m_triangleSet->materialIndex.setNum( 0 );
  }
  else
  {
    // Set the index buffers according to the offset and size that have been computed.
    // In our case, normal and material index are related to triangles.
    m_triangleSet->normalIndex.setValuesPointer( numTriangles, &s_normalIndices[minTriangle] );
    m_triangleSet->materialIndex.setValuesPointer( numTriangles, &s_materialIndices[minTriangle] );

    // However, vertex and texture coordinate indices are related to vertices.
    // We need to compute indices offset and number first, which are 3 times the triangle values.
    int numIndices = 3 * numTriangles;
    int minIndex   = 3 * minTriangle;
    m_triangleSet->coordIndex.setValuesPointer( numIndices, &s_vertexIndices[minIndex] );
    m_triangleSet->textureCoordIndex.setValuesPointer( numIndices, &s_texCoordIndices[minIndex] );
  }
}

// Retrieve internal shape representing the pyramid
// This method performs the shape's initialization during the first call
SoNode*
Pyramid::getInternalShape( SoState* state )
{
  // Initialize internal shape
  if ( m_triangleSet.ptr() == NULL )
  {
    m_triangleSet = new SoIndexedTriangleSet;
    m_triangleSet->vertexProperty.setValue( s_vertexProperty.ptr() );
  }

  // If the cache handler is invalid, we need an update
  if ( !m_internalShapeCache->isValid(state) )
  {
    updateInternalShape();
    m_internalShapeCache->updateCache( state ); // make the cache valid again after the update
  }

  return m_triangleSet.ptr();
}

// This method performs the "typical" operation of a node for any action.
void
Pyramid::doAction( SoAction* action )
{
  // Access the state from the action
  SoState* state = action->getState();

  // Do a push before modifying elements as we don't want the shape to leak.
  state->push();

  // Change the current GL model 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 );
  SbMatrix scaleMatrix = SbMatrix::identity();
  scaleMatrix.scale( SbVec3f( halfWidth, halfHeight, halfDepth ) );
  SoModelMatrixElement::mult( state, this, scaleMatrix );

  // Retrieve our implementation scenegraph
  SoNode* internalShape = getInternalShape( state );

  // Here comes the central part of the implementation. We delegate the rendering part to our internal sceneGraph.
  // The forwardTraversal() method enables the action to traverse a sub-scenegraph which is not
  // actually part of the whole inventor sceneGraph. Outside of this node, this sub-scenegraph won't
  // be visible. It won't appear in current path and won't be visible to a search action for instance.
  action->forwardTraversal( internalShape );

  // Do a pop after modifying elements as we don't want the shape to leak.
  state->pop();
}

// Implements the SoGLRenderAction for the Pyramid node.
void
Pyramid::GLRender( SoGLRenderAction* action )
{
  // 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;

  doAction( action );
}

// Generates triangles representing a pyramid.
void
Pyramid::generatePrimitives( SoAction* action )
{
  // Redirect action to the doAction() method because
  // all the work is done by the internal shape.
  doAction( action );
}

// Computes the bounding box and center of a pyramid.
void
Pyramid::computeBBox( SoAction* action, SbBox3f& /*box*/, SbVec3f& /*center*/ )
{
     (SoTextureEnabledElement::get(state) &&
  // all the work is done by the internal shape.
  doAction( action );
}

// 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.0f : baseWidth.getValue() / 2.0f );
  halfHeight = ( height.isIgnored()    ? 1.0f : height.getValue()    / 2.0f );
  halfDepth  = ( baseDepth.isIgnored() ? 1.0f : baseDepth.getValue() / 2.0f );
}

In the above example, the most important methods are the doAction() and getInternalShape() method. The getInternalShape() method creates an SoIndexedTriangleSet( C++ | Java | .NET ) corresponding to our pyramid. The doAction() just call the getInternalShape() and redirects action to generated shape. This example also shows how to limit memory footprint and using efficiently a precomputed shape. Text

To avoid recreating SoIndexedTriangleSet( C++ | Java | .NET ) at each traversal and to do this just when needed, SoNodeDependencies( C++ | Java | .NET ) can be used. In the example the m_internalShapeCache member is an object of this type. It makes easy to determine if the shape is valid and when it should be regenerated. In Pyramid constructor, we add a dependency of m_internalShapeCache to the part field. If part field is modify, m_internalShapeCache will be automatically invalidate so that, in getInternalShape(), we know if we can directly return cache shape or if we should update it.

[Tip]

In OpenInventor, you should not modify a sceneGraph during traversal. But your implementation graph is not really a part of OIV scenegraph so you can modify it during traversal.

[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).

In the previous example, geometry of shape only depended on field. In the next example, we will see how to track elements on state, like SoComplexityElement( C++ | Java | .NET ), and modify shape according to these elements.

SoComplexityElement( C++ | Java | .NET ) is set by the SoComplexity( C++ | Java | .NET ) node and is use to adjust complexity of shapes below. In our example, we will generate a torus with more or less triangles depending on complexity. As for Pyramid node, we will generate an SoFaceSet( C++ | Java | .NET ) and update it only when complexity on state will have changed. The class header for the Torus node is shown in Example 2-5.

The class header for the Torus node is shown in Example 2-5.

Example 2.5.  Torus.h


C++
#include <Inventor/SbLinear.h>
#include <Inventor/fields/SoSFFloat.h>
#include <Inventor/nodes/SoShape.h>
#include <Inventor/nodes/SoTriangleStripSet.h>
#include <Inventor/caches/SoNodeDependencies.h>

// Shape class representing a torus.
// The #radius field controls the major radius of the torus (from the center).
// The #minorRadius field controls the radius of the cross section of the torus.
// The torus geometry is affected by the value of the SoComplexity Inventor element :
// Higher complexity values will result in a more refined shape with more polygons.
// The default torus has radius 2.0 and minorRadius 1.0.
// The torus lies in the x-y plane and is centered about the origin.
class Torus : public SoShape
{
  SO_NODE_HEADER( Torus );

public:

  // Fields
  SoSFFloat radius;      // Radius of the torus from center
  SoSFFloat minorRadius; // Radius of cross-section

  // Initializes this class
  static void initClass();

  // Exits this class
  static void exitClass();

  // Constructor
  Torus();

protected:
  // These implement supported actions
  virtual void GLRender( SoGLRenderAction* action );
  virtual void generatePrimitives( SoAction* action );
  virtual void computeBBox( SoAction* action, SbBox3f& box, SbVec3f& center );
  virtual void doAction( SoAction* action );

  // Destructor
  virtual ~Torus();

private:

  // Structure used to pass around information about how to draw the torus
  struct TorusInfo
  {
    int numc; // number of vertices around cross-section
    int numt; // number of vertices around torus
  };

  // These methods are used to compute the different vertex properties given
  // the current torus subdivision we are working on during shape construction.
  SbVec3f getVertex( int minorSubdiv, int numMinorSubdivs, int subdiv, int numSubdivs );
  SbVec2f getTexCoord( int minorSubdiv, int numMinorSubdivs, int subdiv, int numSubdivs );
  SbVec3f getNormal( const SbVec3f& vert, int subdiv, int numSubdivs );

  // Uses the complexity element from the state to compute geometrical information
  // needed to build the internal shape.
  void computeComplexityInfo( SoState* state, TorusInfo& info );

  // Retrieve internal shape representing the torus
  SoNode* getInternalShape( SoState* state );

  // Update internal shape geometry depending on the Torus field values.
  void updateInternalShape( SoState* state );

  // vertex property node containing the geometry of the torus
  SoRef<SoVertexProperty> m_vertexProperty;

  // Internal shape representing the torus
  SoRef<SoTriangleStripSet> m_internalShape;

  // This cache handler is used as a flag to check whether or not the internal shape needs to be rebuilt.
  // It is also needed to add a dependency on one or more inventor elements, so that the cache
  // automatically invalidates itself when the element is changed.
  // In our case, we need a dependancy on the Complexity element and on both radii fields.
  SoRef<SoNodeDependencies> m_internalShapeCache;
};

The source code for the Torus node is shown in Example 2-6.

Example 2.6.  Torus.c++


C++
#include <Inventor/SoDB.h>
#include <Inventor/actions/SoGLRenderAction.h>
#include <Inventor/actions/SoGetBoundingBoxAction.h>
#include <Inventor/elements/SoComplexityElement.h>
#include <Inventor/misc/SoState.h>

#include "Torus.h"

SO_NODE_SOURCE( Torus );

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
#define TWOPI (2.0 * M_PI)

//
// This initializes the Torus class.
//
void
Torus::initClass()
{
  SO__NODE_INIT_CLASS( Torus, "Torus", SoShape );
}

//
// This exits the Torus class.
//
void
Torus::exitClass()
{
  SO__NODE_EXIT_CLASS( Torus );
}

//
// Constructor
//
Torus::Torus()
: m_internalShapeCache( new SoNodeDependencies )
{
  SO_NODE_CONSTRUCTOR( Torus );
  SO_NODE_ADD_FIELD( radius,      (2.0f) );
  SO_NODE_ADD_FIELD( minorRadius, (1.0f) );

  // Add dependency to the torus fields. This allows the cache handler
  // to automatically invalidate itself when the fields are changed.
  m_internalShapeCache->addDependency( radius );
  m_internalShapeCache->addDependency( minorRadius );

  // This is where we add the dependancy to the complexity element.
   // function, we'll need an SoTextureCoordinateElement.
  // This allows the cache handler to automatically invalidate
  // itself when the complexity element is changed.
  m_internalShapeCache->addDependency<SoComplexityElement>();
}

//
// Destructor
//
Torus::~Torus()
{
}

// Build the internal shape depending on the elements from the current state
void
Torus::updateInternalShape( SoState* state )
{
  // Use this structure to hold info about how to draw the torus
  TorusInfo info;

  // Figure out how many polygons to use
  computeComplexityInfo( state, info );

  // Each triangle strip goes around the top view
  // Number of vertices per strip = twice the number of (major subdivisions + 1)
  // because we want to loop back to the beginning
  const int stripSize = 2 * (info.numt + 1);

  // Number of triangle strips = number of minor subdivisions
  const int numStrips = info.numc;

  // Set the numVertices field of the TriangleStripSet accordingly
  m_internalShape->numVertices.setNum( numStrips );
  int32_t* numVertices = m_internalShape->numVertices.startEditing();
  for ( int strip = 0; strip < numStrips; strip++ )
    numVertices[strip] = stripSize;
  m_internalShape->numVertices.finishEditing();

  // Set the size of the VertexProperty buffers
  const int numVerticesTotal = stripSize * numStrips;
  m_vertexProperty->vertex.setNum( numVerticesTotal );
  m_vertexProperty->normal.setNum( numVerticesTotal );
  m_vertexProperty->texCoord.setNum( numVerticesTotal );

  SbVec3f* vertices  = m_vertexProperty->vertex.startEditing();
  SbVec3f* normals   = m_vertexProperty->normal.startEditing();
  SbVec2f* texCoords = m_vertexProperty->texCoord.startEditing();

  // Now fill the buffers
  int vertexIndex = 0;
  // go around cross section
  for ( int strip = 0; strip < numStrips; strip++ )
  {
    // go around top view
    for ( int stripVertex = 0; stripVertex <= info.numt; stripVertex++ )
    {
      for ( int offset = 1; offset >= 0; offset-- )
      {
        const int crossSection = strip + offset;

        vertices[vertexIndex]  = getVertex( crossSection, info.numc, stripVertex, info.numt );
        normals[vertexIndex]   = getNormal( vertices[vertexIndex], stripVertex, info.numt );
        texCoords[vertexIndex] = getTexCoord( crossSection, info.numc, stripVertex, info.numt );

        vertexIndex++;
      }
    }
  }

  m_vertexProperty->vertex.finishEditing();
  m_vertexProperty->normal.finishEditing();
  m_vertexProperty->texCoord.finishEditing();
}

// Retrieve internal shape representing the torus
SoNode*
Torus::getInternalShape( SoState* state )
{
  // Initialize internal shape
  if ( m_internalShape.ptr() == NULL )
  {
    m_vertexProperty = new SoVertexProperty;
    m_vertexProperty->normalBinding.setValue( SoVertexProperty::PER_VERTEX );
    m_vertexProperty->materialBinding.setValue( SoVertexProperty::OVERALL );

    m_internalShape = new SoTriangleStripSet;
    m_internalShape->vertexProperty.setValue( m_vertexProperty.ptr() );
  }

  // If the cache handler is invalid, we need an update
  if ( !m_internalShapeCache->isValid(state) )
  {
    updateInternalShape( state );
    m_internalShapeCache->updateCache( state ); // make the cache valid again after the update
  }

  return m_internalShape.ptr();
}

void
Torus::doAction( SoAction* action )
{
  // Access the state from the action
  SoState* state = action->getState();

  // Do a push before modifying elements as we don't want the shape to leak.
  state->push();

  // Retrieve our implementation scenegraph
  SoNode* internalShape = getInternalShape( state );

  // Here comes the central part of the implementation. We delegate the rendering part to our internal sceneGraph.
  // The forwardTraversal() method enables the action to traverse a sub-scenegraph which is not
  // actually part of the whole inventor sceneGraph. Outside of this node, this sub-scenegraph won't
  // be visible. It won't appear in current path and won't be visible to a search action for instance.
  action->forwardTraversal( internalShape );

  // Do a pop after modifying elements as we don't want the shape to leak.
  state->pop();
}

//
// Implements the computeBBox for the Torus node.
//
void
Torus::computeBBox( SoAction* action, SbBox3f& box, SbVec3f& center )
{
  // Here, we could redirect #action to the doAction() method so that
  // all the work is done by the internal shape, but by
  // knowing the values of the radius and minorRadius, we can compute the
  // bounding box of the torus much more efficiently.

  float radiiSum = radius.getValue() + minorRadius.getValue();
  float minRad = minorRadius.getValue();
  box = SbBox3f( -radiiSum, -radiiSum, -minRad, radiiSum, radiiSum, minRad );
  center = SbVec3f( 0.0f, 0.0f, 0.0f );
}

//
// Implements the SoGLRenderAction for the Torus node.
//
void
Torus::GLRender( SoGLRenderAction* action )
{
  // 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;

  // Redirect action to the doAction() method because
  // all the work is done by the internal shape.
  doAction( action );
}

//
// Generates the triangles for the torus.
//
void
Torus::generatePrimitives( SoAction* action )
{
  // Redirect action to the doAction() method because
  // all the work is done by the internal shape.
  doAction( action );
}

// Computes vertex position given the current torus subdivision we are working on.
SbVec3f
Torus::getVertex( int minorSubdiv, int numMinorSubdivs, int subdiv, int numSubdivs )
{
  subdiv %= numSubdivs;
  minorSubdiv %= numMinorSubdivs;

  const double angle = M_PI_2 + TWOPI * static_cast<double>( subdiv ) / static_cast<double>( numSubdivs );
  const double minorAngle = TWOPI * static_cast<double>( minorSubdiv ) / static_cast<double>( numMinorSubdivs );
  const double minorAngleCos = radius.getValue() + minorRadius.getValue() * cos(minorAngle);

  return SbVec3f( static_cast<float>(minorAngleCos * cos(angle)),
                  static_cast<float>(minorAngleCos * sin(angle)),
                  static_cast<float>(minorRadius.getValue() * sin(minorAngle)) );
}

// Computes vertex texture coordinates given the current torus subdivision we are working on.
SbVec2f
Torus::getTexCoord( int minorSubdiv, int numMinorSubdivs, int subdiv, int numSubdivs )
{
  return SbVec2f( static_cast<float>(minorSubdiv) / static_cast<float>(numMinorSubdivs),
                  1.0f - static_cast<float>(subdiv) / static_cast<float>(numSubdivs) );
}

// Computes vertex normal given the current torus subdivision we are working on.
SbVec3f
Torus::getNormal( const SbVec3f& vert, int subdiv, int numSubdivs )
{
  subdiv %= numSubdivs;

  const double angle = M_PI_2 + TWOPI * static_cast<double>( subdiv ) / static_cast<double>( numSubdivs );

  SbVec3f norm( vert[0] - radius.getValue() * static_cast<float>(cos(angle)),
                vert[1] - radius.getValue() * static_cast<float>(sin(angle)),
                vert[2] );
  norm.normalize();

  return norm;
}

//
// Fills in numc and numt in the TorusInfo.
//
void
Torus::computeComplexityInfo( SoState* state, TorusInfo& info )
{
  // Here we retrieve the complexity value from the state and clamp it between 0.0 and 1.0
  const float complexity = SbMathHelper::Clamp( SoComplexityElement::get(state), 0.0f, 1.0f );

  // Based on the complexity, find the number of verts/normals
  // to use around the cross-section (numc) and torus (numt)
  // Bounding box complexity has already been taken care of by the
  // parent class.
  static const float complexityFactor = 70.0f;
  const float numVerts = complexity * complexityFactor;

  float minorMajorRatio = 0.0f;
  if ( radius.getValue() > 0.0f )
    minorMajorRatio = minorRadius.getValue() / radius.getValue();

  info.numt = 3 + static_cast<int>( numVerts );
  info.numc = 3 + static_cast<int>( numVerts * minorMajorRatio );
}

To generate SoFaceSet( C++ | Java | .NET ), we first retrieve complexity on state. We then use this complexity to tessellate more or less the geometry.

In a similar way as in Pyramid example, we use a SoNodeDependencies( C++ | Java | .NET ) to know when our cache is valid and when it should be regenerated. Here, we set dependencies on radius fileds, but also on the SoComplexityElement( C++ | Java | .NET ). During traversal, we check the validity of this cache regarding current state using the isValid() method. If the cache is not valid, we recreate our internal shape and reset SoNodeDependencies( C++ | Java | .NET ) using validate() method. If the SoNodeDependencies( C++ | Java | .NET )::isValid() returns true, it means associated elements, here the complexity, have not changed since last time we update the cache.

Custom nodes must not leak SoState( C++ | Java | .NET ). This means that it's recommended to wrap the doAction() implementation by a pair of calls: state->push() / state->pop()