Irrlicht Engine logo

Tutorial: ODE & Irrlicht
This tutorial was written by Thomas Suter (thanks a lot!), and published on this website. This is simply a copy of the page. Please take a look at the original page, if you think this tutorial may be out of date.


Lets start!

Using ODE With Irrlicht

new Source and Binaries for Irrlicht v0.6
 
 
This Tutorial will explain how to integrate the Open Dynamics Engine (ODE) with Irrlicht.
I will not describe the steps to get ODE running, instead I'll point out some of the tricky stuff needed to start with ODE and Irrlicht. You will however find some detailed comment about ODE compilation etc. in the sources of ODE.
 
 
You will need to download and compile ODE version 0.039 or higher with OPCODE support. If you are developing with MSVC 7 you could also use the libraries which comes with this tutorial, although I strongly recommend to take the time and compile your own version of ODE preferably the CVS version which is always more uptodate than the packaged one.

Further more you need of course the Irrlicht SDK, make shure you have the newest version. Although the code described in the tutorial was developed using irrlicht 0.6 it should be runnable with only minor changes in higher versions.

 
 
Before we start with the description of the demo application I'd like every body to read the ODE documentation. If You really want to use ODE You will have to get accustomed to the C API. Read the documentation once or twice :-)  
 

Bounce

We will write an application with a terrain and some cubes that fall down, hopefully bouncing off the terrain.
The user (player) will be able to travel the world with a flying first person camera using the keys W, S, D and A to go forward, backward, left and right. The mouse will be used to look around in all directions.The TAB key will pause/unpause the simulation and with the ENTER/RETURN key we will lift the cubes to the height of the camera.Pressing the ESCAPE key will end the application.
 
 
  Bounce
 
Project Setup

I use Visual Studio .Net 2003 so I will describe what I did to setup the project in MSVC
  • First You start a new Win32 Project. You don't need a console so stick to the project template without console. Make an empty project without any skeleton files.

  • Then You add the ODE include and lib directories to the project paths using the Menu

    Tools-Options-Projects-VC++Directories.

    Select 'Show directories for' - Include files and add the path to Your ODE/include directory.

    Then select 'Show directories for' - Library files and add the path to Your ODE/lib directory. Make sure that this directory contains the files ode.lib and OPCODE.lib

  • Do the corresponding steps for the irrlicht/inlcude directory and the irrlicht.lib file. The Irrlicht DLL must be in a path known by the system if you want to run the application and for debugging.
That's all for the setup.
 
 
Main Method

Because we don't write a console application we must write a special 'main' method as entrypoint for the application.
 
 
 
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine,int nCmdShow)
{
	Bounce::Bounceable::RunApplication();
	return 0;
}
BounceMain.cpp, line 12-15
 
Every thing else should pretty much be the same as with programming in linux or writing a windows console application. You don't need to open a window because irrlicht will do that for us.  
 
Bounceable

The class holding all the information for ODE and Irrlicht is the Bounceable class. The static method RunApplication serves as initialisation code and main loop. For a simulation with ODE we need the following members and methods:
 
 
Members:
  • dWorldID world; a handle to the world to which the object will belong to

  • dSpaceID space; a handle to the objects space used for collision detection

  • dBodyID body; a handle to the body data of the object, the main handle for physical interactions

  • dGeomID geom; a handle to the geometry data of the object, the main handle for collision interactions

  • dMass mass; the mass of the object
 
 
Methods:
  • static void nearCollisionCallback(void* data, dGeomID o1, dGeomID o2); The Collision callback gets called every time ODE decides that 2 geoms are close enough to check for intersection points

  • static void QuaternionToEuler(const dQuaternion quaternion,irr::core::vector3df &euler); The conversion method to get irrlicht rotation vectors from ODE quaternions

  • void setGeomData(irr::scene::IMesh* m); Convert the irrlicht mesh structure to an OPCODE collision structure for TriMeshes

  • void setGeomData(); Convert the irrlicht boundingbox structure to an OPCODE collision structure, and define the physical body of the object
 
 

Running an ODE Simulation

The basic simulation loop looks as follows:
  1. Apply forces, position changes and rotations to the ODE bodies and geometries
  2. Calculate the collisions and insert one contact joint between the corresponding bodies for each collision.
  3. Let the simulation calculate one step
  4. Clear all contact joint informations
  5. Apply the new positions and rotations to the visual objects
  6. Loop to point 1
 
 
Since the only object that's controlled by the user is the camera we don't need to apply forces etc. to the ODE bodies, so we just skip step 1. Our simulation loop looks like this:

 
 
 
if(simulate){
   //updateEntitiesBeforePhysics(); 
   // build the collision joints for all objects in 'theSpace'
   dSpaceCollide(theSpace,0,&nearCollisionCallback);
   // make a simulation step for 'theWorld'
   dWorldStep(theWorld,0.1f); 
   //optionally use dWorldStepFast1 instead of dWorldStep
   //dWorldStepFast1(theWorld,0.1,1);
   // clean up joints from collisions
   dJointGroupEmpty(theJointGroup);
   // apply the new positions and rotations to the scenenodes
   updateEntitiesAfterPhysics();
}
bounce.cpp, line 136-151
 
Because we can set the simulation to pause the whole code is surrounded by an if statement.
The function dSpaceCollide calculates all collisions for the space with id theSpace. If two geomertries get near enough the function calls our callback method nearCollisionCallback.
The function dWorldStep advances the simulation of the world with id theWorld by 0.1 time steps.
dJointGroupEmpty clears the joints that were generated in our callback method.
And updateEntitiesAfterPhysics transfers the new positions and rotations to the visual objects.
 
 
 
void Bounceable::nearCollisionCallback(void* data, dGeomID o1, dGeomID o2){
  int i=0;
  dBodyID b1=dGeomGetBody(o1);
  dBodyID b2=dGeomGetBody(o2);
  if(b1 && b2 && dAreConnectedExcluding(b1,b2,dJointTypeContact))return;
  dContact contact[MAX_CONTACTS];
  for(i=0;i<MAX_CONTACTS;i++){
    contact[i].surface.mode=dContactBounce | dContactSoftCFM;
    contact[i].surface.mu=dInfinity;
    contact[i].surface.mu2=0;
    contact[i].surface.bounce=1e-5f;
    contact[i].surface.bounce_vel=1e-9f;
    contact[i].surface.soft_cfm=1e-6f;
  }
  int numc=dCollide(o1,o2,MAX_CONTACTS,&contact[0].geom,sizeof(dContact));
  if(numc>0){			
    for(i=0;i<numc;i++){		
      dJointID c=dJointCreateContact(theWorld,theJointGroup,&contact[i]);
      dJointAttach(c,b1,b2);
    }
  }
}
bounce.cpp, line 326-358
 
The callback method check first if the two bodies that collide are connected. If they are connected we don't build any extra contact information. Otherwise, we prepare the contact structures. The values of the surface parameters must be set, of course You can do a lot of adjustments and improvements here.
After that we call the collision function which inspects the geometries in great detail and sets the corresponding collision point into our contact structures. Then we only need to build a new contact joint for each contact that was set. We could of course add some more things here, like extra forces, or play some collision sounds, or start some extra animations...
If we need a reference to our object, there is the possibility to store such a pointer in the geom structure.Calling

dGeomSetData(geomid,(void*)mypointer)

will store the pointer and with

mypointer=(MyOBJClass*)dGeomGetData(geomid)

we can get the pointer back.
 
 
 
void Bounceable::updateEntitiesAfterPhysics(){
  irr::core::vector3df pos;
  irr::core::vector3df rot;
  std::list<BOUNCEABLE>*::iterator iter=NULL;
  for(iter=bounceables.begin();iter!=bounceables.end();++iter){
    Bounceable* entity=(*iter);
    dGeomID geom=entity->geom;
    if(geom!=0){
      // get the new position of the ODE geometry
      dReal* ode_pos=(dReal*)dGeomGetPosition(geom);
      // set the position at the scenenode
      pos.set((irr::f32)ode_pos[0],(irr::f32)ode_pos[1],(irr::f32)ode_pos[2]);
      entity->node->setPosition(pos);
      // get the rotation quaternion
      dQuaternion result;
      dGeomGetQuaternion(geom, result);
      // convert it to eulerangles
      QuaternionToEuler(result,rot);
      // set the rotation 
      entity->node->setRotation(rot);			
    }
  }
}
bounce.cpp, line 174-196
 
At last in the simulation loop we transfer the new position and rotation values to the scenenodes. The method updateEntitiesAfterPhysics will do the job. Propagating the position is straight forward but the rotation needs to be converted from quaternions to euler angles.  
 
 
void Bounceable::QuaternionToEuler(const dQuaternion quaternion, vector3df &euler){
  dReal w,x,y,z;
  w=quaternion[0];
  x=quaternion[1];
  y=quaternion[2];
  z=quaternion[3];
  double sqw = w*w;    
  double sqx = x*x;    
  double sqy = y*y;    
  double sqz = z*z; 
  
  euler.Z = (irr::f32) (atan2(2.0 * (x*y + z*w),(sqx - sqy - sqz + sqw))
                        *irr::core::GRAD_PI);
  
  euler.X = (irr::f32) (atan2(2.0 * (y*z + x*w),(-sqx - sqy + sqz + sqw))
                        *irr::core::GRAD_PI);  
  
  euler.Y = (irr::f32) (asin(-2.0 * (x*z - y*w))
                        *irr::core::GRAD_PI);
}
bounce.cpp, line 359-375
 

Converting Irrlicht Meshes to ODE TriMesh

The mesh conversion isn't difficult all we have to do is keep track of the vertex indices if we have meshes with multiple meshbuffers.
 
 
 
void Bounceable::setGeomData(irr::scene::IMesh* m){
  // do nothing if the mesh or node is NULL
  if(mesh==NULL || node==NULL) return; 
    int i,j,ci,cif,cv;
    indexcount=0;
    vertexcount=0;
    // count vertices and indices
    for(i=0;igetMeshBufferCount();i++){
      irr::scene::IMeshBuffer* mb=mesh->getMeshBuffer(i);
      indexcount+=mb->getIndexCount();
      vertexcount+=mb->getVertexCount();
    }
    // build structure for ode trimesh geom
    vertices=new dVector3[vertexcount];
    indices=new int[indexcount];
    // fill trimesh geom
    ci=0; // current index in the indices array
    cif=0; // offset of the irrlicht-vertex-index in the vetices array 
    cv=0; // current index in the vertices array
    for(i=0;i<m->getMeshBufferCount();i++){
      irr::scene::IMeshBuffer* mb=mesh->getMeshBuffer(i);
      // fill indices
      irr::u16* mb_indices=mb->getIndices();
      for(j=0;jgetIndexCount();j++){
        // scale the indices from multiple meshbuffers to single index array
        indices[ci]=cif+mb_indices[j];
        ci++;
      }
      // update the offset for the next meshbuffer
      cif=cif+mb->getVertexCount();
      // fill vertices
      if(mb->getVertexType()==irr::video::EVT_STANDARD){
       irr::video::S3DVertex* mb_vertices=
              (irr::video::S3DVertex*)mb->getVertices();
       for(j=0;j<mb->getVertexCount();j++){
         vertices[cv][0]=mb_vertices[j].Pos.X;
         vertices[cv][1]=mb_vertices[j].Pos.Y;
         vertices[cv][2]=mb_vertices[j].Pos.Z;
         cv++;
       } 
     }else if(mb->getVertexType()==irr::video::EVT_2TCOORDS){
       irr::video::S3DVertex2TCoords* mb_vertices=
               (irr::video::S3DVertex2TCoords*)mb->getVertices();
       for(j=0;j<mb->getVertexCount();j++){
         vertices[cv][0]=mb_vertices[j].Pos.X;
         vertices[cv][1]=mb_vertices[j].Pos.Y;
         vertices[cv][2]=mb_vertices[j].Pos.Z;
         cv++;
       }    
     } 
  }
  irr::core::vector3df pos=node->getPosition();
  // build the trimesh data
  dTriMeshDataID data=dGeomTriMeshDataCreate();
  dGeomTriMeshDataBuildSimple(data,(dReal*)vertices, 
                      vertexcount, indices, indexcount);
  // build the trimesh geom 
  geom=dCreateTriMesh(space,data,0,0,0);
  // set the geom position 
  dGeomSetPosition(geom,pos.X,pos.Y,pos.Z);
  // lets have a pointer to our bounceable
  // we could need this in the collision callback
  dGeomSetData(geom,(void*)this); 
  // in our application we don't want geoms 
  // converted from meshes to have a body
  dGeomSetBody(geom,0); 
}
bounce.cpp, line 261-319
 

Using Irrlicht Boundignboxes to Define ODE Body and Geom

If we need to simulate objects with physical behaviour then it is good practice to use geometric primitives for the simulation because they are faster and often accurate enough. In case we should really need collision at mesh level we could always have special ODE-collision-spaces for these meshes and do tranformations and tests only when needed.
 
 
 
void Bounceable::setGeomData(){
  // get the boundingbox
  irr::core::aabbox3d box=node->getBoundingBox();
  irr::core::vector3df extend=box.getExtend();
  // get the position of the scenenode
  irr::core::vector3df pos=node->getPosition();
  // build a box shaped geometry for ODE
  geom=dCreateBox(space,(dReal)extend.X,(dReal)extend.Y,(dReal)extend.Z);
  // set the position of the ODE geom
  dGeomSetPosition(geom,pos.X,pos.Y,pos.Z);
  // set a pointer to our Bounceable, 
  // this will come in handy when we do more complicated collisions
  dGeomSetData(geom,(void*)this);
  // create a body for this object
  body=dBodyCreate(world);
  // setup the mass
  dMassSetBox(&mass,5.0,(dReal)extend.X,(dReal)extend.Y,(dReal)extend.Z);
  // combine body and mass
  dBodySetMass(body,&mass);
  // add the body to the geom
  dGeomSetBody(geom,body);
  // set the bodys position (same as geom position)
  dBodySetPosition(body,pos.X,pos.Y,pos.Z);
  dBodySetData(body,(void*)this);
}
bounce.cpp, line 262-277
 
You will get more details if You read the source code. The file bounce.h gives some information about the aims of the methods and in the file bounce.cpp You'll find the implementation which shouldn't be too hard to read.

Thank You for reading this tutorial and good luck with Your future games, hopefully with ODE support ;-)

 
 

Files
Filename Description Size Date
Bounce (irr v 0.6) Win32 binaries and sources, compiles with irrlicht v 0.6 and msvc .net 2003, ODE library not included! 676 Kb  16.03.2004 

 

 

 


Valid XHTML 1.0! Valid CSS!


Irrlicht Engine and Irrlicht Engine webpage © 2003-2005 by Nikolaus Gebhardt