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 |