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 SoShape 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 SoCallbackAction 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 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 SoShape SoShape . For example, if the shape generates triangles, the triangle callback function is invoked for every triangle generated. Filled shapes, such as SoCone SoCone SoCone and SoQuadMesh SoQuadMesh SoQuadMesh , generate triangles (regardless of draw style), line shapes (such as SoLineSet SoLineSet SoLineSet and SoIndexedLineSet SoIndexedLineSet SoIndexedLineSet ) generate line segments, and point shapes (such as SoPointSet SoPointSet SoPointSet ) generate points.
The SoPrimitiveVertex 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 SoShape SoShape class:
To take advantage of the automatic mechanism, use these three methods, provided by the SoShape SoShape 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 SoDetail SoDetail —for example, what part of the shape each vertex belongs to. In this case, you can use an existing subclass of SoDetail SoDetail SoDetail (see the section called “Using an SoDetail”), or you can create a new SoDetail SoDetail SoDetail subclass to hold the appropriate information. By default, the pointer to the detail in SoPrimitiveVertex SoPrimitiveVertex is NULL.
If you decide to store information in an SoDetail SoDetail SoDetail , you create an instance of the subclass and store a pointer to it in the SoPrimitiveVertex SoPrimitiveVertex by calling setDetail() .
For rendering, you may be able to inherit the GLRender() method from the SoShape SoShape 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 SoShape 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 SoPickedPoint SoPickedPoint . SoShape SoShape 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 SoShape 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 SoSphere 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 SoShape 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 SoNonIndexedShape 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 SoIndexedShape SoIndexedShape , you can inherit computeBBox() from the base SoIndexedShape SoIndexedShape 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 SoCone 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 SoShape 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 SoNonIndexedShape SoNonIndexedShape or SoIndexedShape SoIndexedShape 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.
Example 2.3. Pyramid.h
#include <Inventor/SbLinear.h> #include <Inventor/fields/SoSFBitMask.h> #include <Inventor/fields/SoSFFloat.h> #include <Inventor/nodes/SoShape.h> // SoShape.h includes SoSubNode.h; no need to include it 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.
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/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); }
![]() | |
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). |