Complex functionality can not always be achieved by concatenating engines and assembling nodes in a scene graph. Therefore Studierstube and the underlying Open Inventor library allow for extensions in a variety of ways. Typically such extensions take the form of new C++ classes which are derived from classes defined in the framework. Thus such extensions must be developed in C++.
Studierstube is a set of extensions classes to Open Inventor. To add new functionality to it can be done by writing new Open Inventor extensions. The basic API to do so is covered by two very good books on Open Inventor, The Inventor Mentor and The Inventor Toolmaker. Details on writing new extensions can be found there, we give only a short introduction into the most common types here.
To get in-depth information on what nodes, nodekits and engines are read the Inventor Mentor chapters 2 (omit pp 70-71), 3, 9, 12, 13, 14. To know more about extending Open Inventor read the Inventor Toolmaker chapters 1, 2, 6, 7.
The basic blocks of Open Inventor are nodes which in turn are assembled into scene graphs. Therefore, writing new nodes is a common way to add new behavior or rendering to an Open Inventor or Studierstube application.
To define a new node, you need to be clear about its functionality and the parameters required to control the functions. Nodes are parametrized using fields, therefore you should map the parameters to a set of fields of the node. Moreover, a node is traversed by actions as part of a scene graph. Any special behavior of the node is usually triggered through these actions. For example the rendering code of a node is triggered by a callback from the SoGLRenderAction. Interaction with user input is triggered by an SoHandleEventAction or SoHandle3DEventAction. For each action a node defines a virtual method which is called by that action. To specify a uniform behavior across all nodes use the virtual method doAction.
To define a node a set of macros have to be used. A template for a new node looks as follows:
#include <Inventor/nodes/SoSubNode.h>
#include <Inventor/fields/SoSFBool.h>
class ButtonPress : public SoNode
{
SO_NODE_HEADER(ButtonPress)
public:
ButtonPress(void);
static void initClass(void);
SoSFBool left;
SoSFBool middle;
SoSFBool right;
protected:
virtual ~ButtonPress();
virtual void handleEvent( SoHandleEventAction *action);
};
The node ButtonPress will react to input events and report if the left, middle or right mouse button is pressed. Some notes on the header file:
Lets have a look at the source code to define the methods of our new node:
#include "ButtonPress.h"
SO_NODE_SOURCE(ButtonPress);
void ButtonPress::initClass(void)
{
SO_NODE_INIT_CLASS(ButtonPress, SoNode, "Node");
}
ButtonPress::ButtonPress(void)
{
SO_NODE_CONSTRUCTOR(ButtonPress);
SO_NODE_ADD_FIELD(left, (FALSE));
SO_NODE_ADD_FIELD(middle, (FALSE));
SO_NODE_ADD_FIELD(right, (FALSE));
}
ButtonPress::~ButtonPress()
{}
void ButtonPress::handleEvent( SoHandleEventAction * action )
{
SoEvent * event = action->getEvent();
if( event->isOfType( SoMouseButtonEvent::getClassTypeId())
{
SoMouseButtonEvent * mbevent = (SoMouseButtonEvent *) event;
switch( mbevent->getButton())
{
case SoMouseButtonEvent::BUTTON1:
left->setValue(mbevent->getState());
break;
case SoMouseButtonEvent::BUTTON2:
middle->setValue(mbevent->getState());
break;
case SoMouseButtonEvent::BUTTON3:
right->setValue(mbevent->getState());
break;
}
}
}
Note the following things in the source file:
Some more complex behavior will require to act on fields of the node. In such a case, use a SoFieldSensor object to observe the field and be called back whenever it changes. A SoFieldSensor takes a function or a static method and will call it whenever a field value has changed. It can be attached to any field, but only one field at a time. Because it is not part of the reference counting scheme of Open Inventor, you have to take care of it yourself. The typical code looks like that:
...
#include <Inventor/sensors/SoFieldSensor.h>
class ButtonPress: public SoNode
{
...
protected:
static void fieldChanged( void * data, SoSensor * sensor);
SoFieldSensor fieldSensor;
}
We defined the SoFieldSensor as a simple member, this ensures that
it is available in the constructor and will be deleted, when the node
is deleted. This is exactly what we want, so we do not need any further
bookkeeping. The static method will be called by the SoFieldSensor and
will implement any special behavior.
ButtonPress::PuttonPress()
{
...
fieldSensor.setData( this ); // set data pointer passed to callback
fieldSensor.setFunction( ButtonPress::fieldChanged ); // set callback function
fieldSensor.attach( &left ); // attach to field to observe (requires a pointer)
}
void ButtonPress::fieldChanged( void * data, SoSensor * sensor )
{
assert( data != NULL );
assert( ((SoNode *)data)->isOfType( ButtonPress::getClassTypeId()));
ButtonPress * self = (ButtonPress *) data;
printf("Button was pressed\n");
}
To setup the SoFieldSensor we have to add a few lines to the constructor. We need to set the function to be called, then any additional data we want to pass into the function and finally tell the SoFieldSensor object which field to observe by attaching it to the field. We pass in the this pointer as additional data to be able to access the node from within the static callback function.
In the callback function we simply cast the data pointer to a class pointer and can now implement any advanced functionality. It is always a good idea to use a couple of assert statements to make sure that the data passed in is valid to catch possible bugs in your code.
Engines can be created in a similar way as nodes. They are somewhat simpler because they do not deal with actions at all. They also provide a standard way of observing field values and recalculating the outputs.
An engine has fields for input values and defines outputs to write the result of its specific calculation. Open Inventor optimizes the evaluation of such engines so that it is only triggered if the value of an output is actually required. To support this optimization the implementation of an action must follow a certain pattern.
Computation of the output state has to be implemented in the abstract virtual method evaluate(). Open Inventor calls it automatically, if it requires an update of the engine outputs. The implementation typically reads out the input fields of the engine, computes the results and writes them into the outputs.
Engine outputs do not hold any values, but only references to connected fields. A set of macros are used to directly write the results into the connected fields. These make the engine outputs appear as simple objects.
The following example engine takes the values of the string input fields and returns the concatenation of the individual string values.
#include <Inventor/engines/SoSubEngine.h>
#include <Inventor/fields/SoMFString.h>
class Concat: public SoEngine
{
SO_ENGINE_HEADER(Concat);
public:
Concat(void);
static void initClass(void);
SoMFString a;
SoMFString b;
SoEngineOutput aCb;
SoEngineOutput bCa;
protected:
virtual ~Concat();
virtual void evaluate(void);
}
The header looks simple enough. Again there are similar macros to define an engine. The source code implements the evaluate function to calculate the two concatenations of the strings a and b. Note, that the two engine outputs are not typed. This will be defined in the implementation in the source file:
#include "Concat.h"
SO_ENGINE_SOURCE(Concat);
void Concat::initClass(void)
{
SO_ENGINE_INIT_CLASS(Concat, SoEngine, "Engine");
}
Concat::Concat(void)
{
SO_ENGINE_CONSTRUCTOR(Concat);
SO_ENGINE_ADD_INPUT(a, (""));
SO_ENGINE_ADD_INPUT(b, (""));
SO_ENGINE_ADD_OUTPUT(aCb, SoMFString);
SO_ENGINE_ADD_OUTPUT(bCa, SoMFString);
}
void Concat::evaluate(void)
{
unsigned int max = (a.getNum() > b.getNum()) ? a.getNum() : b.getNum();
SO_ENGINE_OUTPUT(aCb, SoMFString, setNum(max));
SO_ENGINE_OUTPUT(bCa, SoMFString, setNum(max));
for( unsigned int i = 0; i < max; i ++ )
{
SbString va = (a.getNum() > i) ? a[i] : "";
SbString vb = (b.getNum() > i) ? b[i] : "";
SbString t1 = va + vb;
SbString t2 = vb + va;
SO_ENGINE_OUTPUT(aCb, SoMFString, set1Value(i,t1));
SO_ENGINE_OUTPUT(bCa, SoMFString, set1Value(i,t2));
}
}
Again, macros similar to the ones used in the node source are required to create an engine. Also the method initClass uses the macro SO_ENGINE_INIT_CLASS with the same arguments as the corresponding macro for nodes. Within the constructor two macros are used to define the inputs and outputs of the engine. The SO_ENGINE_ADD_INPUT macro corresponds to the SO_NODE_ADD_FIELD macro and takes the same parameters. The SO_ENGINE_ADD_OUTPUT macro, however, only takes the name and the type of the output, but no default value.
The method evaluate implements the functionality of the engine. It is called whenever the outputs of the engine need to be re-evaluated. This happens only, if the inputs have changed and a field connected to an output is used. For example, if the engine output is connected to the string field of an SoText2 node, the field will be read to render the correct text. Then the engine will be evaluated to update it's output values.
Output values are set with a special macro SO_ENGINE_OUTPUT. It takes three parameters: the name of the output, the type of the output and the method to call on the connected fields. The macro will actually expand to code that calls all connected fields, therefore it requires the type of the field and the method invocation with all parameters. The method will be called for each field that is connected, therefore you should not use any expressions in the parameters, as they will be evaluated as well for each field.
In our example we use the macro twice. First we set the number of values in the connected fields to the number of values we are going to compute using setNum(). This resizes the output fields to hold the right number of values. Then we set each value in the connected fields using the call set1Value() with the necessary parameters.
Why did we define our engine's input and outputs to use multiple value fields ? Open Inventor provides default conversions between single and multiple value fields. Therefore, if we implement an engine using multiple value fields, we get one for single value fields for free. Note, that we also create as many results as the maximum number of values in any input field and simply reuse the last values for inputs that have less values.Most buildin Open Inventor engines work that way and it is therefore the most appropriate way to deal with such situations.
Nodekits are a special variation of nodes. They are part of the scene graph and also encapsulate a small sub scene graph itself. Thus they can be used to build reusable scene graph components and to create fixed connections and dependencies between the nodes of the sub scene graph. Nodekits add the concept of a part to the concept of nodes. A part is a node within the sub scene graph controlled by the nodekit. The parts are arranged in a fixed graph when the nodekit is initialized. The resulting sub scene graph is traversed by all actions in the standard way.