How to implement a node into

Open ActiveWrl

 

by Malte Weiß

 

 

1         Introduction

 

This documents explains how to extend the Open ActiveWrl framework by implementing a (native) node. First of all you have to learn where to place new node files:

 

·        The general node class must be part of the project ctk.

 

The source file must be saved in /develop/CC/lib/OActiveWrl/src/ctk.

The header file should be stored in /develop/CC/lib/OActiveWrl/include/ctk.

 

·        If your node supports OpenGL rendering a special class is to be derived and implemented in the project glctk.

 

The source file must be saved in /develop/CC/lib/OActiveWrl/src/glctk.

The header file has to be stored in /develop/CC/lib/OActiveWrl/include/glctk.

 

By default node classes are named in the following format:

 

            Cyb” [ + “GL” ] + Name of the node + “C”

 

Remind that “GL” is inserted for OpenGL support classes only. The names of the source (.cpp) and the header files (.h) equal this format but they are written in small letters.

 

Examples:

 

Name of the node

Source file

Header file

CybWeatherC

cybweatherc.cpp

cybweatherc.h

CybGLWeatherC

cybglweatherc.cpp

cybglweatherc.h

 

In the following the process of implementing the node is illustrated by an example node, which we call WeatherBox. It allows the user to use simple weather simulation in VRML environments.

 

 

2       Basic class framework

 

2.1       Decide the node type

 

First you have to decide, which kind of node you want to implement. By default nodes are directly derived from the class CybNodeC, so they support the standard functionality and can be implemented standalone into a VRML file.

But if you want to insert a new geometry node it has to be derived from CybGeometryC, which is a part of the Shape node.

 

Our example WeatherBox is a geometry node.

 

2.2       Define VRML specification

 

The second step you have to do is thinking about the VRML specifications of your node. You must exactly define, which fields it contains and which events are received or sent. The specification consists of a text, where each line declares another field or event in the following format:

 

·        ( “field” | “exposedField” | “eventIn” | “eventOut”)

·        Data type

·        Name

·        [ Default value ]

 

Our WeatherBox node shall own these abilities:

 

·        The node supports two types of weather, which can be either rain or snow.

·        A box can be defined, the weather takes place in.

·        The user can determine a direction, the particles move to.

·        The maximum number of particles can be specified.

·        Least but not last the speed which the particles are falling with may be set.

 

This is a possible specification, which results from the list above:

 

      exposedField  SFString  type          "RAIN"

      exposedField  SFVec3f   size          1.0 1.0 1.0

      exposedField  SFVec3f   direction     0.05 -0.5 0.05

      exposedField  SFInt32   maxParticles  1000

      exposedField  SFFloat   speedFactor   1.0

 

As you can see all parameter are realized as exposed fields, because all fields shall be controlled dynamically; e.g. the direction can be changed during run-time to simulate wind. The last field (speedFactor) defines a factor the speed of the particles is multiplied with. If you enter 2.0 all particles fall two times faster than the default speed.

 

2.3       Using NodeClassCreator

 

The third step is performed by executing the NodeClassCreator, an application that creates the basic class framework. When the program is started, there are a couple of fields which have to filled first. These are explained in the following:

 

·        Node name – The name of your node with a capital letter in the beginning. In our example we are entering “WeatherBox” here. Remind that you only enter the name of the node, which means no prefix (“Cyb”) or postfix (“C”).

·        Author – Enter your name here. It will appear in the comment in the head of the source and the header file.

·        Base class – The super class of the new node. This depends on the kind of node (see 2.1) you are implementing. Our WeatherBox is a geometry node so we enter “CybGeometryC”.

·        Base header – This is the header the super class is included from. It is automatically set if you edit the name of the base class. You can specify another header file but by default you do not have to do so. Our example uses the proposition of the NodeClassCreator: “ctk/cybgeometryc.h”.

·        Field specifications  – Enter the VRML specification of your node here as explained in 2.2. In our example we use the specifications shown above.

 

Now we have to decide which methods our class implements beside the standard ones. If you want to add one of the “special” methods click on its checkbox to activate it. These methods are now described:

 

·        <virtual> FinishInit – This virtual method is called when the node is completely loaded. It is useful to activate it if you have to do some pre-calculations.

·        <virtual> ToChannel is called when the node is sent to a channel. It is normally used if you do not render geometry.

·        <virtual> ToChannelDefault and <virtual> ToChannelTexture are called if the node is sent to a specific (graphic) channel, where “Default” means the standard (non-textured) channel and “Texture” means the channel, where textures are used for rendering.

·        ToIsectLineChannel is only available if ToChannelDefault is implemented. This method is responsible for intersection tests between the input device and your geometry.

·        PrepareRender is used to prepare the render process, e.g. for pre-calculations or setting up memory lists. PrepareRender is called after start-up and then only if the member variable m_bUpdateRender is set to 1 (true).

 

We only need ToChannelDefault in our example so you should activate its checkbox, to learn more about its implementation.

 

Now click on the flag to create the header and the source code. Both are now shown in the bottom part of the window. To implement them just copy the code out of the fields (“header” or “source”) and save them to the specific files (see 1 for more information). Then you can insert these files into the project ctk.

 

 

3         Activating the node

 

Now the node has a right syntax and is part of your project, but it will neither compile nor read the node from a VRML file. Some handwork is to do.

 

3.1       Update #define directives

 

At the first line of the class constructor you will find a line with an unknown #define-constant. In our sample it is

 

      m_nodeType.push_back(CYBNT_WEATHERBOX);

 

The format of the constant name is

 

CYBNT_” + Name of the node in capital letters

 

where “NT” means “Node type”.

 

This numeric constant has to be defined in the header file

 

/develop/CC/lib/OActiveWrl/include/ctk/cybnodec.h

 

The number identifies your node, so it does not matter which number you choose as long as it is unique. In our example we added this line:

 

      #define  CYBNT_WEATHERBOX  1069

 

Additionally our field names also use constants which you could have to define. Their name format is

 

CYBFN_” + Name of the node in capital letters

 

where “FN” is the short version of “Field name”. These constants must be defined in the file

 

/develop/CC/lib/OActiveWrl/include/ctk/cybwrlfields.h

 

Back to our sample – because “type”, “size” and “direction” are known names we only have to define the constants for “maxParticles”, “speedFactor” and “particleSize”:

 

      #define  CYBFN_MAXPARTICLES  177

      #define  CYBFN_SPEEDFACTOR   178

      #define  CYBFN_PARTICLESIZE  179

 

3.2            Register node

 

Now the project will compile but it will surely not detect your node in a VRML file. Next we have to register the node, which is done in the method CybWorldC::InitKeyList(). You must add a line in the format:

 

  m_kVRMLNodeKeyStr.push_back( NodeInfo("<Node name>"), <Id constant>) );

 

Our example is registered by the line

 

  m_kVRMLNodeKeyStr.push_back( NodeInfo("WeatherBox", CYBNT_WEATHERBOX) );

 

Now we have to advise the program to create an object of our node if it is necessary. This is done in the method CybWorldC::CreateNode(long). Just add another case to the switch-statement in the following format:

 

  case <Node id constant>:

    return new <Node class name>(this);

 

In our example we add

 

  case CYBNT_WEATHERBOX :

    return new CybWeatherBoxC(this);

 

3.3       Return name for JavaScript

 

The last thing you have to do to make your node working is telling the JavaScript engine the name of your node. This in done in the function GetSFNodeName(SFNode*), which is found in the file

 

/develop/CC/lib/OActiveWrl/include/ctk/jssfnode.cpp

 

Just add another case to the switch-statement in the format

 

    case <Id constant>: name = "<Node name>"; break;

 

In our example this line is

 

    case CYBNT_WEATHERBOX: name = "WeatherBox"; break;

 

After this step the project will compile and your source code will read all fields from one of those nodes if it occurs in a VRML file. But it will actually not do anything more than “reading”. The next step is filling the code with life. We will extend the WeatherBox node class to demonstrate this.

 

 

4         WeatherBox: Implementing geometry

 

First of all we should have a look at the variables of our CybWeatherBoxC class. NodeClassCreator has defined the following variables:

 

  SFString    m_strType;              // field ‘type’

  SFVec3f     m_vSize;                      // field ‘size’

  SFVec3f     m_vDirection;                 // field ‘direction’

  SFInt32     m_nMaxParticles;        // field ‘maxParticles’

  SFFloat     m_fParticleSize;        // field ‘particleSize’

  SFFloat     m_fSpeedFactor;               // field ‘speedFactor’

 

Each variable corresponds to a field in the VRML specification as you can see in the comments beside.

 

4.1            Geometry data

 

Our weather consists of particles so we need some variables, which we add to the class definition:

 

protected:

  SFVec3f*    m_particles;      // Array of particles

  SFInt32     m_nNumParticles;  // Current number of particles

 

The particles are saved as vectors (Type SFVec3f) in an array which  m_particles  points to. m_nNumParticles contains the size of this array. m_nNumParticles does not need to be equal to m_nMaxParticles e.g. if m_nMaxParticles has just been changed (see 4.3).

 

In the constructor this data is initialised by zero:

 

  // Initialize graphical data

  m_particles     = 0;

  m_nNumParticles = 0;

 

4.2       Helper methods

 

Next we need two helper methods, which are used later:

 

protected:

  SFVec3f     GetNewParticle(SFBool bTop);

  SFBool      IsParticleDead(SFVec3f &v);

 

GetNewParticle creates a new random particle:

 

// Creates a random new particle and returns it

 

SFVec3f CybWeatherBoxC::GetNewParticle()

{

  // Calculate vector

  SFVec3f res;

  res.x = (rand() / double(RAND_MAX)) * m_vSize.x - m_vSize.x*0.5;

  res.y = (rand() / double(RAND_MAX)) * m_vSize.x - m_vSize.x*0.5;

  res.z = (rand() / double(RAND_MAX)) * m_vSize.z - m_vSize.z*0.5;

 

  return res;

}

 

IsParticleDead tells us if a particle is outside of the weather box:

 

// Checks weather a particle is outside of the bounding-box

 

SFBool CybWeatherBoxC::IsParticleDead(SFVec3f &v)

{

  return (v.x < -0.5*m_vSize.x || v.y < -0.5*m_vSize.y ||

          v.z < -0.5*m_vSize.z || v.x >  0.5*m_vSize.x ||

          v.y >  0.5*m_vSize.y || v.z >  0.5*m_vSize.z);

}

 

4.3            UpdateGeometry()

 

The most important function of the class is UpdateGeometry() because it is responsible for setting up all geometric data. It is defined as follows:

 

  public:

    void UpdateGeometry();

 

UpdateGeometry() is a method which is called each time when the WeatherBox is send to the channel (see 4.4), so the weather (rain or snow) is in a permanent move.

 

The implementation is separated into different parts. The head of the method looks like this:


 

// Update all geometry

 

void CybWeatherBoxC::UpdateGeometry()

{

 

First the time difference to the last call is calculated in order to determine how “far” the particles will move this frame.

 

  // Calculate time difference to last call

  static TimeC timeLast = m_pWorld->GetSystemTime(); 

  TimeC timeCurrent = m_pWorld->GetSystemTime();

  TimeC timeDiff = timeCurrent - timeLast;

 

  long lDiff = timeDiff.GetSec() * 1000 + timeDiff.GetMsec();

 

Next the size of the particle array is managed if the method is called the first time or the field “maxParticles” has changed.

 

  /* +-------------------------------+

     | Update size of particle array |

     +-------------------------------+ */

 

  if(m_nMaxParticles == 0) return;

 

  int nNumParticles = m_nNumParticles;

 

  // Create particle array if it doesn't exist.

  if(m_particles == 0)

  {

    m_particles = (SFVec3f*) m_pWorld->Malloc(sizeof(SFVec3f) *

                               m_nMaxParticles);

    if(0 == m_particles)

    {

      msgError("[CybWeatherBoxC] Out of memory while creating geometry.");

      return;

    }

    m_nNumParticles = m_nMaxParticles;

  }

  // Resize particle array if maximum number has changed.

  else if(m_nNumParticles != m_nMaxParticles)

  {

    SFVec3f *pNewBuffer = (SFVec3f*) m_pWorld->Realloc(m_particles,

                            sizeof(SFVec3f) * m_nMaxParticles);

    if(0 == pNewBuffer)

    {

      msgError("[CybWeatherBoxC] Out of memory while creating geometry.");

      return;

    }

    m_particles = pNewBuffer;

    m_nNumParticles = m_nMaxParticles;

  }

 

Remind that CybWorldC::Malloc() and CybWorldC::Realloc() are used instead of malloc and realloc.

 

Now all new particles of the array are created:

 

  /* +------------------------------+

     | Create all missing particles |

     +------------------------------+ */

 

  SFVec3f *pVec = &m_particles[nNumParticles];

  for(int i = nNumParticles; i < m_nNumParticles; i++, pVec++)

    *pVec = GetNewParticle();

 

The last but actually most important task of this method is updating the particle list. “Updating” means moving the particles and “deleting” them if they have left the weather box. We use our helper functions (see 4.2) in this code block. As you can see “rain” and “snow” particles perform different movements. Rain falls faster and does not make other horizontal translations than defined by the directional vector, snow is slower and makes random horizontal moves additionally.

 

  /* +----------------------+

     | Update particle list |

     +----------------------+ */

 

  if(lDiff != 0)

  {

    // Seed randomize timer with system time (important for parallel

    // running!)

    TimeC t = m_pWorld->GetSystemTime();

    srand(t.GetSec() * 1000 + t.GetMsec());

 

    pVec = m_particles;

    for(i = 0; i < m_nNumParticles; i++, pVec++)

    {

      // Move particle

      if(0 == strcmp("RAIN", m_strType.c_str()))

      {

        if(lDiff != 0)

          *pVec += (double(lDiff) * 0.04 * m_fSpeedFactor) * m_vDirection;

      }

      else if(0 == strcmp("SNOW", m_strType.c_str()))

      {

        if(lDiff != 0)

        {

          *pVec += (double(lDiff) * 0.004 * m_fSpeedFactor) * m_vDirection;

          pVec->x += double(lDiff) * 0.004 * m_fSpeedFactor * ((rand() /

                     double(RAND_MAX)) - 0.5);

          pVec->z += double(lDiff) * 0.004 * m_fSpeedFactor * ((rand() /

                     double(RAND_MAX)) - 0.5);

        }

      }

 

      // Replace particle if it's dead.

      if(IsParticleDead(*pVec))

        *pVec = GetNewParticle();

    }

 

    timeLast = m_pWorld->GetSystemTime();

  }

}


4.4       Update each frame

 

At last we have to take care that this method is really called each frame. We only have to call UpdateGeometry() from the virtual method ToChannelDefault(CybChannelC*, long), which is already defined by the NodeClassCreator:

 

// Send weather box to default channel

 

void CybWeatherBoxC::ToChannelDefault(CybChannelC* pV, long lFlag)

{

  // Update the geometry

  UpdateGeometry();

}

 

Now our node really works! But it still does not show anything. The last but probably most interesting task is implementing an OpenGL support. And this is not too hard ;)

 

 

5         WeatherBox: OpenGL render support

 

In this last chapter I will show how to implement an OpenGL render support for a WeatherBox node.

 

As mentioned in chapter 1 we need two new files which must be inserted into the project glctk. We create these files:

 

·         Header file cybglweatherbox.h in /develop/CC/lib/OActiveWrl/include/glctk

·         Source file cybglweatherbox.cpp in /develop/CC/lib/OActiveWrl/src/glctk

 

We derive a new class named CybGLWeatherBox from CybWeatherBox and extend it by a render function.

 

5.1       The header file

 

The header file of our class is quite simple because we only need one method for rendering. We derive the ToChannelDefault method and extend it with new functionality.

 

#ifndef CYBGLWEATHERBOX_HC

#define CYBGLWEATHERBOX_HC

 

#include "ctk/cybweatherboxc.h"

 

// Class definition

 

class CybGLWeatherBoxC : public CybWeatherBoxC

{

public:

  CybGLWeatherBoxC(CybWorldC*);

 

  virtual void ToChannelDefault(CybChannelC*, long flag);

};

 

#endif

 

5.2       The source file

 

Now we get to the interesting part. To make use of OpenGL we must implement the header file of GLUT (OpenGL Utilities), which offers the entire palette of OpenGL functions. Additionally we implement an empty constructor. Its only task is transmitting the world pointer to the constructor of the super class, which will take care of it.

 

#include <GL/glut.h>

 

#include "glctk/cybglweatherboxc.h"

 

// Constructor

 

CybGLWeatherBoxC::CybGLWeatherBoxC(CybWorldC* pW) : CybWeatherBoxC(pW) {

}

 

Now ToChannelDefault is going to be implemented. First it checks whether the frameworks wants us to render the scene. If not we can abort here because our node does not perform any intersection test.

 

// Send it to channel (default)

 

void CybGLWeatherBoxC::ToChannelDefault(CybChannelC *pChannel, long flag)

{

  // Return if we don't have anything to do.

  if(flag & m_blindTrav || (flag != CYB_TRAV_DRAW &&

     flag != CYB_TRAV_SELECT))

     return;

 

After that ToChannelDefault of the super class is called. This is very important because it calls UpdateRender, which is the core of our node class.

 

  // Call superclass method which controls the geometry.

  CybWeatherBoxC::ToChannelDefault(pChannel, flag);

 

Now we can render, where the modes “rain” and “snow” have a different visualisation. Rain particles are rendered as lines, which the direction vector is used for, snow is rendered as a sum of dots. The field “particleSize” is supported in both cases.

 

  // Draw rain

  if(0 == strcmp("RAIN", m_strType.c_str()))

  {

    glLineWidth(m_fParticleSize);

 

    glBegin(GL_LINES);

    SFVec3f *pVec = m_particles;

    for(int i = 0; i < m_nNumParticles; i++, pVec++)

    {

      glVertex3f(pVec->x, pVec->y, pVec->z);

      glVertex3f(pVec->x - m_vDirection.x,

                 pVec->y - m_vDirection.y,

                 pVec->z - m_vDirection.z);

    }

    glEnd();

  }


  // Draw snow

  else if(0 == strcmp("SNOW", m_strType.c_str()))

  {

    glPointSize(m_fParticleSize);

 

    glBegin(GL_POINTS);

    SFVec3f *pVec = m_particles;

    for(int i = 0; i < m_nNumParticles; i++, pVec++)

      glVertex3f(pVec->x, pVec->y, pVec->z);

    glEnd();

  }

}

 

5.3            Register

 

Are we done? No, but nearly. Our system still does not know that we have derived a OpenGL class. The final step, which we have to perform, is adding another case into the switch-statement of CybGLWorldC::CreateNode(), as we did it before in CybWorldC (see 3.2). So we add the line

 

  case CYBNT_WEATHERBOX:

    return new CybGLWeatherBoxC(this);

 

Now the application framework always will create a CybGLWeatherBoxC object (instead of CybWeatherBoxC) if a OpenGL rendering engine is used.

 

 

 

 

Have fun with your own nodes!


The administrator of Open ActiveWrl is mailto:winkelholz@fgan.de SourceForge.net Logo