Wednesday 21 August 2013

Getting Started : Open Dynamics Engine (ODE)

After spending considerable time trying to get a simple Newton Dynamics example working, I switched over to the Open Dynamics Engine with much more success. To start, ODE has a wiki page with a very informative manual even if you simply skim it. After generating the Visual Studio solutions as explained in the INSTALL file.

    >premake4.exe --with-tests --with-demos vs2008

Which is really more than necessary since the only project needed to be built is the 'ode'.  If you fail to build the correct configuration of ode you will get a link error like this:

    fatal error LNK1104: cannot open file 
        '..\..\..\..\ode-0.12\lib\DebugDoubleLib\ode_doubled.lib'

I intend to use the static library with high precision so I will build under the DebugDoubleLib configuration. Just like in other examples, I am currently using premake to generate my Visual Studio solutions, and so you will need a .lua file with these lines modified to match your own directory hierarchy.

0) Setting up the Environment

    includedirs { odeSDK .. '/include' }

    links { odeSDK .. '/lib/DebugDoubleLib/ode_doubled' }

    configuration "Debug"
        flags { "Symbols" }
        defines { "_DEBUG", "dDOUBLE" }

1) Including the Headers

As recommended by the manual, this is all I added.

    #include <ode/ode.h>

2) Adding the namespace

No namespace necessary

3) Initialization

    dInitODE2(0);

Apparently dInitODE() is obsolete, and this initialization call can take a bitmask of flags which never seems to be used in any examples. 

4) Creating the World

We'll need two things to begin, a world and at least one space. Spaces are a way of controlling collision, by grouping together bodies that will collide, and separating sets of bodies that will never collide. At the very minimum we will need a world and some gravity.

    static dWorldID gWorldID = dWorldCreate();
    dWorldSetGravity(gWorldID,0.0f, 0.0f, -9.8f);

4i) Creating the Space

    static dSpaceID gSpaceID = dSimpleSpaceCreate(0);

With that we are ready to get started adding some shapes to the space. There are, of course, some nice world settings to improve the quality of the simulation which I will include in any code samples provided.

5) Creating Bodies and Geometry

Similar to any other physics engine, there is a larger 'body' which can contain multiple shapes with their corresponding geometry. There are a few exceptions here: plane shapes are not allowed to have a body.

    dGeomID kFloor = dCreatePlane(gSpaceID, 0,0,1,0);

all the other fundamental shapes are created in a similar way.

    dGeomID kBoxGeom = dCreateBox(gSpaceID, 1, 1, 1);
    dGeomSetPosition(kBoxGeom, 0, 0, 0);

I center the box at the origin because this will become the origin of the body containing the box, and not the origin of the world.

We can now see the connection between everything so far, that the shapes are what is used for collision detection so they are created in the space, but bodies are only containers for the shapes and are created in the world.

    dBodyID kBoxBody = dBodyCreate(worldID);
    dBodySetPosition(kBoxBody, 0, 0, 5);

    dGeomSetBody(kBoxGeom, kBoxBody );

Also the mass is optional of this is to be a kinematic/static body, but adding mass will make it's default setting dynamic. Although the geometry is only a single box, the mass distribution may be of a different shape or even different dimensions.

    dMass kMass
    dReal density = 1.0;
    dMassSetBox(&kMass, density, 1, 1, 1);


    dBodySetMass(kBoxBody , &kMass);

6) Simulation

Everything until now has been relatively simple, despite the 'C' style interface where everything is a function call and pointers are masked as 'IDs'. However, the simulation step can be a little tricky but looks like this:

    static dJointGroupID gJointGroup = dJointGroupCreate(0); //done at initilization

    dSpaceCollide(gSpaceID, 0, &NearCallback);
    dWorldQuickStep(gWorldID, myTimestep); 
    dJointGroupEmpty(gJointGroup);

First we check for any collisions, or even near collisions in any spaces we have. The results of which will be collected and organized using a user-implemented 'NearCallback'. The purpose of which is to take for every colliding geometry pair determine if the need to be resolved and then associate them together in pairs through the global joint group. I strongly recommend using the dWorldQuickStep, to avoid any run-time failures when bodies collide too quickly resulting in a normalization failure. Lastly, the joint group will necessarily need to be emptied before the next step takes place. 

void NearCallback(void *data, dGeomID geom1, dGeomID geom2)
{
   dBodyID body1 = dGeomGetBody(geom1);
   dBodyID body2 = dGeomGetBody(geom2);
   if (body1 && body2) //only shapes with a body may collide.
   {
      const int maxPts = 4;
      dContact kContact[maxPts]
      nbPts = dCollide (geom1,geom2, maxPts, &kContact[0].geom, sizeof(dContact));
      if (nbPts  > 0)
      {
         for (int i=0; i<nbPts ; i++)
         {
            kContact[i].surface.mode = dContactBounce;
            kContact[i].surface.mu = 0.5; //friction
            kContact[i].surface.bounce = 0.5; //restitution
            kContact[i].surface.bounce_vel = 1.0; //minimum speed to incur bounce
            dJointID kJoint = dJointCreateContact (gWorldID, gJointGroup, &kContact[i]);
            dJointAttach (kJoint, body1, body2);
         }
      }
   }
}

7) Shutdown/Cleanup

    dBodyDestroy(kBoxBody);
    dJointGroupDestroy(gJointGroup);
    dSpaceDestroy(gSpaceID);
    dWorldDestroy(gWorldID);
    dCloseODE();

Looking through the demo code was very helpful for me to understand how collision detection, and then collision resolution worked in ODE. Also, the manual is very good at times, but mostly at supplementing the documentation in the code. All the function style calls make for more book-work, but very interesting otherwise, and not terribly difficult to setup. Best of luck and Happy Coding.