Creating a new texture node

From H3D.org

Jump to: navigation, search

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 PaintableTexture node name with your node name. The last argument is optional and if specified means 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 are 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.

Personal tools
go to