Tutorials

From H3D.org

Jump to: navigation, search

Contents

C++

Creating new node types

Here we will show you some examples of how to extend H3D API with new nodes with new or specialized behaviour which can then easily be used in your x3d scene descriptions. The source code can be obtained from the downloads section named "Various useful nodes".

Torus (new geometry node)

The Torus node
Enlarge
The Torus node

In this example we will create a new geometry node to use, more specifically a torus geometry. Typically when you create a new geometry node you will have to do three things:

  • Perform the OpenGL calls to render the geometry.
  • Calculate an axis-aligned bounding box encompassing geometry.
  • Specify the X3D/Python interface for your node.

I will go through the source code line by line and explain what each part does. So first we need to create a new class for our new node. Since we are creating a geometry node the base class to inherit from is X3DGeometryNode.

 
 class Torus : public X3DGeometryNode {
 public:
 

Next we have the calculation of the bounding box. We want to calculate the bounding box based on the outerRadius and innerRadius fields and the resulting bounding box will reside in the bound member of the node(inherited from X3DGeometryNode). The bounding box should be automatically updated when any of these fields change. In order to do this we define a new specialized field class based on the SFBound field, accepting the radius fields as input and having a BoxBound object as output.

 
/// SFBound is specialized update itself from the innerRadius and
/// outerRadius field of the Torus node.
///
/// routes_in[0] is the innerRadius field of the Torus node.  
/// routes_in[1] is the outerRadius field of the Torus node.  
 
class SFBound: public TypedField< X3DGeometryNode::SFBound,
                                  Types< SFFloat, SFFloat > > {
   /// Update the bound from the radius field. 
   virtual void update() {
      H3DFloat inner_radius = 
          static_cast< SFFloat * >( routes_in[0] )->getValue();
      H3DFloat outer_radius = 
          static_cast< SFFloat * >( routes_in[1] )->getValue();
      BoxBound *bb = new BoxBound;
      bb->size->setValue( Vec3f( (outer_radius+inner_radius)*2, 
                                 (outer_radius+inner_radius)*2,
                                 inner_radius * 2 ) );
      value = bb;
   }
};
 

The TypedField template defines the inputs and outputs of the field. The first argument tells what the base class of the field will be (which is the same as output). In our case it is an SFBound field. We want the value of this field (which is the bounding box) to depend on the two fields innerRadius and outerRadius. Both of these fields are of type SFFloat. The second argument (Types< SFFloat, SFFloat>) specifies a list of field types that have to be routed to it. In our case two SFFloat fields. By specifying the field like this we require two SFFloat fields to be routed to it or there will be an error. In the update()-function that calculates the new value depending on the radius fields we can then assume that the incoming fields are of tye SFFloat and do a static_cast. Now all we have to do is to set up these routes properly and we have a bounding box field that updates automatically.

After this we have the constructor.

 
/// Constructor.
Torus( Inst< SFNode      > _metadata        = 0,
       Inst< SFBound     > _bound           = 0,
       Inst< DisplayList > _displayList     = 0,
       Inst< MFBool      > _isTouched       = 0,
       Inst< MFVec3f     > _force           = 0,
       Inst< MFVec3f     > _contactPoint    = 0,
       Inst< MFVec3f     > _contactNormal   = 0,
       Inst< MFVec3f     > _contactTexCoord = 0,
       Inst< SFBoundTree > _boundTree       = 0,
       Inst< SFFloat     > _innerRadius     = 0,
       Inst< SFFloat     > _outerRadius     = 0,
       Inst< SFInt32     > _nrSides         = 0,
       Inst< SFInt32     > _nrRings         = 0,
       Inst< SFBool      > _solid           = 0 );
 

The first 9 arguments are arguments to the base class, while the rest are for new fields specified in this nodes. For an closer explanation about the Inst<> template and the uses of it see this page. Normally you can just copy the constructor arguments from the base class and add your new fields below. One important thing to notice though is that in the second argument we are using our newly defined SFBound class in the Inst<>-template. This will make the bound member of the node be an instance of our new field type instead of the default type of the X3DGeometryNode base class.

Next are a couple of function definitions. The render() and traverseSG() function are available in all nodes and are used during traversal of the scene-graph. The render() function is used to render the node graphically using OpenGL while the traverseSG() function is used for anything else you want to do while traversing the scene-graph. You have access to things like transformation matrices from and to global/local coordinate spaces and haptics devices among other things.

 
/// Renders the Torus using OpenGL.
virtual void render();
 
/// Traverse the scenegraph. 
virtual void traverseSG( TraverseInfo &ti );  
 


Next comes the specification of all the fields that are specific for this new node. Any values that you want to make publicly available must be declared here as field.

 
    /// The innerRadius field define the inner radius of the torus. 
    ///
    /// <b>Access type:</b> inputOutput
    /// <b> Default value:</b> 0.5
    auto_ptr< SFFloat >  innerRadius;
 
    /// The outerRadius field define the inner radius of the torus. 
    ///
    /// <b>Access type:</b> inputOutput
    /// <b> Default value:</b> 1
    auto_ptr< SFFloat >  outerRadius;
 
    /// The nrSides field define the number of sides for each radial section
    /// of the torus.
    ///
    /// <b>Access type:</b> inputOutput
    /// <b> Default value:</b> 0.5
    auto_ptr< SFInt32 >  nrSides;
 
    /// The nrRings field define the number of radial divisions of the 
    /// torus.
    ///
    /// <b>Access type:</b> inputOutput
    /// <b> Default value:</b> 0.5
    auto_ptr< SFInt32 >  nrRings;
 
    /// The solid field determines whether the Torus is visible when viewed from
    /// the inside. 
    /// <b>Access type:</b>  inputOutput
    auto_ptr< SFBool  >  solid;
 

And finally we have a member that should be part of any node. This is an entry into the internal node database of H3D API, which is a database of all nodes and its fields that are available.

 
    static H3DNodeDatabase database;
 

Ok, that is the header file. Let's move on to the cpp-file.

First we have the node database entry. It will add this node to the node data base, making it possible to use it from X3D and Python. The first argument is the name of the node as a string. This is the name it will have when using it in X3D. The next two arguments are values that are needed for the node database system. When creating a new node, just copy the two arguments and replace the Torus node name with your node name. The last argument is optional and if specifies mean that you will inherit the public fields from the database of a base class. If you do not only the fields that are added to the database in this field is available.

 
// Add this node to the H3DNodeDatabase system.
H3DNodeDatabase Torus::database( "Torus", 
                                  &(newInstance<Torus>), 
                                  typeid( Torus ),
                                  &X3DGeometryNode::database );
 

To specify the fields that are to be available from X3D/Python they have to be added to the database of the node. The FIELDDB_ELEMENT is a macro for doing just that. The first argument is the name of the node, the second the name of the field and the third the access type of the field. So now we add the fields we want.

 
// Define the x3d interface
namespace TorusInternals {
  FIELDDB_ELEMENT( Torus, innerRadius, INPUT_OUTPUT );
  FIELDDB_ELEMENT( Torus, outerRadius, INPUT_OUTPUT );
  FIELDDB_ELEMENT( Torus, nrSides, INPUT_OUTPUT );
  FIELDDB_ELEMENT( Torus, nrRings, INPUT_OUTPUT );
  FIELDDB_ELEMENT( Torus, solid, INPUT_OUTPUT );
}
 

The beginning of the constructor is just a call to the base class and initialization of the member field variables.

 
Torus::Torus( 
         Inst< SFNode  >  _metadata,
         Inst< SFBound >  _bound,
	 Inst< DisplayList > _displayList,
	 Inst< MFBool      > _isTouched,
	 Inst< MFVec3f     > _force,
	 Inst< MFVec3f     > _contactPoint,
	 Inst< MFVec3f     > _contactNormal,
	 Inst< MFVec3f     > _contactTexCoord,
	 Inst< SFBoundTree > _boundTree,
         Inst< SFFloat >  _innerRadius,
         Inst< SFFloat >  _outerRadius,
         Inst< SFInt32 >  _nrSides,
         Inst< SFInt32 >  _nrRings,
         Inst< SFBool  >  _solid ) :
  X3DGeometryNode( _metadata, _bound, _displayList, _isTouched, _force, 
		   _contactPoint, _contactNormal, _contactTexCoord, _boundTree ),
  innerRadius    ( _innerRadius ),
  outerRadius    ( _outerRadius ),
  nrSides        ( _nrSides ),
  nrRings        ( _nrRings ),
  solid          ( _solid    ) {
 

Then we initialize the fields of the node and set the type_name. This should be done in all nodes. The type_name is used to give better warning and error messages.

 
  type_name = "Torus";
  database.initFields( this );
 

Now we set the default values of the fields.

 
  // set default values
  innerRadius->setValue( 0.5 );
  outerRadius->setValue( 1 );
  nrSides->setValue( 40 );
  nrRings->setValue( 40 );
  solid->setValue( true );
 

Time to set up internal routes. We need to set up the routes to the bound field as we specified earlier in the SFBound class. We route the innerRadius and outerRadius field.

 
  // set up bound routes
  innerRadius->route( bound );
  outerRadius->route( bound );
 

We now set up routes to the displayList field. All fields that should trigger a re-rendering of the node using the render()-function should be routed to this field. If it is not, the node might not be rendered properly. If you experience that the graphical rendering of a node is not changed even though you change the values of its field, the problem is probably that you have forgotten to set up a route from the field you change to the displayList field.

 
  // set up display list routes(each field that if changed 
  // requires a rerendering of the torus should be routed in here)
  innerRadius->route( displayList );
  outerRadius->route( displayList );
  nrSides->route( displayList );
  nrRings->route( displayList );
  solid->route( displayList );
 


The render function uses glut to render the torus.

 
void Torus::render() {
  glutSolidTorus( innerRadius->getValue(), 
                  outerRadius->getValue(), 
                  nrSides->getValue(),
                  nrRings->getValue() );
}
 


Finally we have the traverseSG function. All it does in our case is to set if back face culling should be enabled or not.

 
void Torus::traverseSG( TraverseInfo &ti ) {
  if( solid->getValue() ) {
    useBackFaceCulling( true );
  } else {
    useBackFaceCulling( false );
  }
  X3DGeometryNode::traverseSG( ti );
}
 

And that is all there is to it. Compile it as part of a shared library or in a viewer as described here and you are ready to use the new node. Here is an example x3d-file you can try

 
 <Shape>
    <Appearance>
	<Material diffuseColor="1 0 0"  shininess="0.4" specularColor="1 1 1"/>
    </Appearance>
    <Torus innerRadius="0.05" outerRadius="0.1" />
 </Shape>
 

PaintableTexture (new texture node)

PaintableTexture node
Enlarge
PaintableTexture node

In this tutorial we want to make it possible to draw on a geometry using the haptics device. In order to do this we will create a new texture node that supports dynamic updates of the texture data. The normally available texture nodes(such as ImageTexture) just display an image loaded from a url. What we want is to use the texture as a canvas and draw whereever the haptics device touches the texture. All geometry nodes (nodes inherited from X3DGeometryNode) have four output fields related to the contact point. They are:

  • isTouched(MFBool) - true if device in contact with geometry, aflse otherwise
  • contactPoint(MFVec3f) - the last contact point of each haptics device
  • contactNormal(MFVec3f) - the normal of the geometry at the last contact point.
  • contactTexCoord(MFVec3f) - the texture coordinate at the last contact point.

Each of these are MFields, i.e. they can multiple values. There will be one entry for each haptics device available with the contact points etc for each device. This means that the size of the MField depends on the number of haptics devices. Of these values it is the contactTexCoord that is of interest to us. We know that we can get the texture coordinate of the point we touch. Now we just need a node that will change the color of the texture at a given texture coordinate. In order to do this(efficiently) we need to create a new node. First we have to consider what fields we want to be available in the node. A simple case would be to have fields for:

  • width, height, depth - resolution of the texture in pixels in each dimension
  • backgroundColor - the color of the "canvas" from the beginning
  • paintColor - the color with what to change
  • paintAtTexCoord - the texture coordinate at which to paint

Now that we know what kind of interface we want we can create a new node. We know that it is a texture node we want to make and to make it as general as possible we use a 3d texture by inheriting from X3DTexture3DNode.

 
  class PaintableTexture : 
    public X3DTexture3DNode {
  public:
 

We need a way to update the texture depending on a texture coordinate. In order to do this we create a new field class that accepts input routes of texture coordinates(SFVec3f). We make it an AutoUpdate field because we want it to update every time an event, i.e. every time we get a new event we want to draw a pixel in the texture. The OnNewValueSField template is just a convenience template defining the virtual function onNewValue which will be called as soon as the value of the field has a new value, which can happen when the update function is called or the setValue function is called. It lets us override just one function instead of both the setValue and update functions.

 
    /// The PainterField class performs the update to the texture 
    /// each time an event is sent to the field.
    class PainterField: public OnNewValueSField< AutoUpdate< SFVec3f > > {
      virtual void onNewValue( const Vec3f &v );
    };
 


Nothing special with the constructor. The seven first arguments are from the base class and the rest are new fields in this node. Note that the last argument is an instance of the PainterField class that we just defined.

 
    /// Constructor.
    PaintableTexture(  Inst< DisplayList > _displayList   = 0,
		       Inst< SFNode   >  _metadata        = 0,
		       Inst< SFBool   >  _repeatS         = 0,
		       Inst< SFBool   >  _repeatT         = 0,
		       Inst< SFBool   >  _scaleToP2       = 0,
		       Inst< SFImage  > _image            = 0,
		       Inst< SFTextureProperties > _textureProperties = 0,
		       Inst< SFInt32      > _width           = 0,
		       Inst< SFInt32      > _height          = 0,
		       Inst< SFInt32      > _depth           = 0,
		       Inst< SFColorRGBA  > _backgroundColor = 0,
		       Inst< SFColorRGBA  > _paintColor      = 0,
		       Inst< PainterField > _paintAtTexCoord = 0 );
 

We need to set the inital color of the texture somewhere. We do this in the initialize function of the node. The initialize function is a virtual function in all nodes that is called once at the first reference of each node.

 
    /// Initializes the texture to the specified resolution and background 
    /// color.
    virtual void initialize();
 

Specify the field members of the node. All fields that we want to be publicly available should be declared here.

 
    /// The width of the image to paint(number of pixels).
    ///
    /// <b>Default value:</b> 128 \n
    auto_ptr< SFInt32 > width;
 
    /// The height of the image to paint(number of pixels).
    ///
    /// <b>Default value:</b> 128 \n
    auto_ptr< SFInt32 > height;
 
    /// The depth of the image to paint(number of pixels).
    ///
    /// <b>Default value:</b> 128 \n
    auto_ptr< SFInt32 > depth;
 
    /// The original color of each pixel in the image.
    ///
    /// Default value:</b> RGBA( 1,1,1,1 ) \n
    auto_ptr< SFColorRGBA > backgroundColor;
 
    /// The color with which to paint.
    ///
    /// <b>Default value:</b> RGBA( 0, 0, 0, 1 ) \n
    auto_ptr< SFColorRGBA > paintColor;
 
    /// When an event is received on this field the pixel with the
    /// given texture coordinate will be painted with the color
    /// in the paintColor field.
    ///
    /// <b>Default value:</b> RGBA( 0, 0, 0, 1 ) \n
    auto_ptr< PainterField > paintAtTexCoord;
 

Finally we have a database entry in the H3D API node database, the database of all nodes available and its fields.

 
    /// The H3DNodeDatabase for this node.
    static H3DNodeDatabase database;
 

Moving on to the cpp-file!


First we have the node database entry. It will add this node to the node data base, making it possible to use it from X3D and Python. The first argument is the name of the node as a string. This is the name it will have when using it in X3D. The next two arguments are values that are needed for the node database system. When creating a new node, just copy the two arguments and replace the Torus node name with your node name. The last argument is optional and if specifies mean that you will inherit the public fields from the database of a base class. If you do not only the fields that are added to the database in this field is available.

 
// Add this node to the H3DNodeDatabase system.
H3DNodeDatabase PaintableTexture::database( "PaintableTexture", 
                                            &(newInstance<PaintableTexture>), 
                                            typeid( PaintableTexture ),
                                            &X3DTexture3DNode::database );
 

To specify the fields that are to be available from X3D/Python they have to be added to the database of the node. The FIELDDB_ELEMENT is a macro for doing just that. The first argument is the name of the node, the second the name of the field and the third the access type of the field. So now we add the fields we want.

 
/// Add the x3d field interface.
namespace PaintableTextureInternals {
  FIELDDB_ELEMENT( PaintableTexture, width, INITIALIZE_ONLY );
  FIELDDB_ELEMENT( PaintableTexture, height, INITIALIZE_ONLY );
  FIELDDB_ELEMENT( PaintableTexture, depth, INITIALIZE_ONLY );
  FIELDDB_ELEMENT( PaintableTexture, backgroundColor, INITIALIZE_ONLY );
  FIELDDB_ELEMENT( PaintableTexture, paintColor, INPUT_OUTPUT );
  FIELDDB_ELEMENT( PaintableTexture, paintAtTexCoord, INPUT_ONLY );
}
 

The beginning of the constructor is just a call to the base class and initialization of the member field variables.

 
PaintableTexture::PaintableTexture( 
				   Inst< DisplayList > _displayList,
				   Inst< SFNode   >  _metadata,
				   Inst< SFBool   >  _repeatS,
				   Inst< SFBool   >  _repeatT,
				   Inst< SFBool   >  _scaleToP2,
				   Inst< SFImage  > _image,
				   Inst< SFTextureProperties > _textureProperties,
				   Inst< SFInt32     > _width,
				   Inst< SFInt32     > _height,
				   Inst< SFInt32     > _depth,
				   Inst< SFColorRGBA > _backgroundColor,
				   Inst< SFColorRGBA > _paintColor,
				   Inst< PainterField > _paintAtTexCoord ) :
  X3DTexture3DNode( _displayList, _metadata, _repeatS, _repeatT,
                    _scaleToP2, _image, _textureProperties ),
  width( _width ),
  height( _height ),
  depth( _depth ),
  backgroundColor( _backgroundColor ),
  paintColor( _paintColor ),
  paintAtTexCoord( _paintAtTexCoord ) {
 

Then we initialize the fields of the node and set the type_name. This should be done in all nodes. The type_name is used to give better warning and error messages.

 
  type_name = "PaintableTexture";
  database.initFields( this );
 

Then we set the default values of our fields.

 
  width->setValue( 128 );
  height->setValue( 128 );
  depth->setValue( 1 );
  backgroundColor->setValue( RGBA( 1, 1, 1, 1 ) );
  paintColor->setValue( RGBA( 0, 0, 0, 1 ) );
}
 

Ok, so time for the initialize function. We want to create a new image with all pixels set the the background color. The color we get from the field is in the range [0,1] and we want to create a 32-bit RGBA image. This means that for each component we have 256 values, so we multiply the color value with 255 to get it in this range.

 
void PaintableTexture::initialize() {
  H3DInt32 w = width->getValue();
  H3DInt32 h = height->getValue();
  H3DInt32 d = depth->getValue();
  const RGBA &bg_color = backgroundColor->getValue();
 
  // initialize a new PixelImage with the color for each pixel set
  // to the background color.
  unsigned int data_size = 4 * w * h * d;
  unsigned char *data = new unsigned char[ data_size ];
  for( unsigned int i = 0; i < data_size; i+=4 ) {
    data[i]   = (unsigned char) ( bg_color.r * 255 );
    data[i+1] = (unsigned char) ( bg_color.g * 255 );
    data[i+2] = (unsigned char) ( bg_color.b * 255 );
    data[i+3] = (unsigned char) ( bg_color.a * 255 );
  }
 
  image->setValue( new PixelImage( w,
                                   h,
                                   d,
                                   32,
                                   PixelImage::RGBA,
                                   PixelImage::UNSIGNED,
                                   data ) );
  X3DTexture3DNode::initialize();
}
 

Finally we need to implement the field that actually does the painting. There are several convenience functions available for changing image data in the field. One way would be to take out the actual image from the field, change the image and then send an event on the image field. This would however cause the entire texture to be reloaded into texture memory, even though maybe only one pixel has been modified. If you use the functions in the SFImage field the pixels that have changed will be tracked and only a small part of the texture have to be replaced. This requires all the calls to function that change the texture to be encapsulated within beginEditing() and endEditing(). The beginEditing() will reset the tracking of pixels and start recording which pixels have changed while the endEditing will send an event on the image field, which will later result in an update in the texture of only the modified area.

 
void PaintableTexture::PainterField::onNewValue( const Vec3f &tc ) {
  PaintableTexture *pt = static_cast< PaintableTexture * >( getOwner() );
  pt->image->beginEditing();
  pt->image->setPixel( tc, pt->paintColor->getValue() );
  pt->image->endEditing();
}
 

And that is it! Now we have the node we need. All we have to do now is create a scene with our texture and connect the fields properly. So let's make one.

We create a simple scene with a rectangle geometry to paint on and our new texture applied. As explained above every geometry node has a contactTexCoord field with the texture coordinate of the contact point. However we can not use this directly since it is an MFVec3f and the PaintableTexture we just created only accept an SFVec3f of the texture coordinate to paint on. This means that we will have to convert the MFVec3f somehow to a SFVec3f. We will do this by writing a little python script that will do this by using the texture coordinate of the contact point of the first haptics device. First we add a PythonScript node and add routes from the contactTexCoord field of the geometry to our field in the python script that will do the conversion. The output of this field will then be routed to the paintAtTexCoord field of the texture.

 
<Group>
 <Shape>
     <Appearance>
       <Material />
       <PaintableTexture DEF="TEXTURE" />
       <SmoothSurface />
     </Appearance>
     <Rectangle2D DEF="GEOM" size="0.3 0.3" />
  </Shape>
  <PythonScript DEF="PS" url="paintabletexture.py" />
  <ROUTE fromNode="GEOM" fromField="contactTexCoord" toNode="PS" toField="firstTexCoord" />
  <ROUTE_NO_EVENT fromNode="PS" fromField="firstTexCoord" toNode="TEXTURE" toField="paintAtTexCoord" />
</Group>
 

All we have to do now is to write the simple python script that takes an MFVec3f and contains an SFVec3f.

 
#import the H3DInterface module that contains all the H3D API python bindings
from H3DInterface import *
 
# This field class just takes the first value of its input MField 
# and uses it as output
class FirstTouchTexCoord( TypedField( SFVec3f, MFVec3f ) ):
  def update( self, event ):
    tex_coords = event.getValue()
    # if we have a texture coordinate return that
    if( len(tex_coords) > 0 ):
      return tex_coords[0]
    else:
      return Vec3f( 0, 0, 0 )
 
# create an instance of our class that we route to in the x3d file
firstTexCoord = FirstTouchTexCoord()
 
 

And now we can load the x3d file and start painting. This node can of course easily be extended to support more advanced painting modes, have a reset function to clear the image etc, but that is left as an exercise to the user. If you do this however, or make any other node that you thing others can benefit from, please share it in the downloads section and write an entry in the wiki describing your node. If we all add nodes we will soon have a large library of nodes that we can all benefit from.

Creating new loaders

Personal tools
go to