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.
The SoPrimitiveVertex( C++ | Java ) 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( C++ | Java | .NET ) class:
To take advantage of the automatic mechanism, use these three methods, provided by the SoShape( C++ | Java | .NET ) 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( 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:
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
#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++
#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
Memory used by TriangleSet vertices may have a cost, especially if your shape is aimed to be present thousands of time in your scenegraph. In this case, you can pre-compute vertices corresponding to a unit pyramid and use this singleton by aggregation in all instances of the Pyramid. Only indices are stored per instance. To adjust size of the pyramid instance, we apply a scale before rendering it by using SoModelMatrixElement( C++ | Java | .NET ).
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.
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. |
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
#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++
#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.
In the Pyramid example, we redefine the doAction() and redirect all actions to internal sceneGraph. This can be sub-optimal, especially for picking or bounding box computation. In the torus example, if computeBbox() had called doAction, bounding box would have been computed by internal SoFaceSet( C++ | Java | .NET ) by expanding a bounding box with each point of geometry. This would have been quite long, especially with large tessellation.