ODE Object tutorial

From GPWiki

Files:GUITutorial_warn.gif The Game Programming Wiki has moved! Files:GUITutorial_warn.gif

The wiki is now hosted by GameDev.NET at wiki.gamedev.net. All gpwiki.org content has been moved to the new server.

However, the GPWiki forums are still active! Come say hello.

This page is far from complete, go ahead and write some more!
You can extend this page by editing it.

ODE The object game tutorial

Introduction

In the scope of my Combat game, I'm learning how to use the Open Dynamics Engine (ODE for friends) for physic simulation. Actually I had an hard time to find a good tutorial on the subject (many were too advanced, some were easy but not clear) so - as usual - the best way to learn it is writing a tutorial which can guide me and you through the use of this great piece of code.

What we're going to learn

  • Basic ODE concepts
  • How to setup ODE
  • How to create a world
  • How to handle the world

In detail :)

  • Linear movements
  • Angular movements
  • Masses
  • Forces
  • Collisions

The object game

In this simulation we'll create a little world with some obects in a box. The user can take the objects with the mouse and play with them as he likes :)

Basic ODE concepts

This is a brief description of ODE, I'm not going into detailed explanations about how it works: we just want to know the overall scheme.

ODE keeps worlds: you start by creating a world (or more), you insert objects in it and you give rules to handle events. Since it's dynamic you can generate events in this world.

ODE acts like the time. As you know (or should know) time is the measure of change. So, every time you give ODE some amount of time, it will make your world change. The act of making this world change is called integration: you provide a world to integrate and a step size (i.e. how much time has passed since last integration), and ODE will make things work.

As you can easily understand, in reality time is a continuous quantity, but in videogames/simulations time is a discrete quantity. So, the best way to get a good result from ODE is to provide a step as small as possible. This will help to get faster and more accurate results from ODE.

This dynamics engine is powerful, but quite simple for our purposes:

  1. we set gravity in our world
  2. we create objects with masses
  3. we create joints in our objects
  4. we apply forces
  5. ODE will move things using complicated physics rules
  6. ODE will generate collisions
  7. we are happy :)

ODE will sum forces, compute accelerations on masses, calculate collisions. We have only to setup a world, provide a dynamic source in this world (probabily the user interaction) and just benefit from ODE's computational work.

Quality

ODE is not exact. It behaves approximately. In spite of this, it can work in different modes: using single or double precision floating point, using fast or accurate integration functions. Probably, for the purpose of games, you'll prefer to use ODE in the fastest and less accurate mode to get a good result and little waste of CPU cycles.

C or C++ ?

ODE is written in C++, but it has public interfaces in C. Probably you can use the C++ classes for your project, but actually I didn't find anything about it (apart from the header files of the library). So if you want Pure C++, encapsulate ODE or try to figure out how to use object oriented interfaces from the source code :) It's FLOSS, you can take a look at it ;)

Writing a C++ wrapper around the C interface is pretty simple (although perhaps a tad inefficent). The only real trick is:

In Physics.h:
/* 
 * callback for ODE. friend of PhysicsEngine. _this is meant to be 
 * a this pointer at the PhysicsEngine object currently using the callback.
 */
void nearCollisionCallback( void* _this, dGeomID id1, dGeomID id2 );

class PhysicsEngine 
{
 public:
  bool doPhysics();
  .... wrapper stuff (setPosition,addItem,etc) ....
  friend void nearCollisionCallback( void* _this, dGeomID id1, dGeomID id2 );

 private:
    map<string,dBodyID> bodies;
    dWorldID worldId;
    dSpaceID spaceId;
    dJointGroupID contactJointGroup;
}

For Physics.cc, see the excellent tutorial below (I didn't write the tutorial, just added this snippet). But a relevant snippet for the above:

bool PhysicsEngine::doPhysics()
{
    dSpaceCollide( this->spaceId, this, &nearCollisionCallback );
    dWorldQuickStep( this->worldId, 1 );
    dJointGroupDestroy( this->contactJointGroup );
    return true;
}

I did implement it this way, and got it too work. But I'm in the process of a re-write. Once I do that, I'll link to the code. You may want to hide the callback in a namespace or some such thing. But I digress.

How to setup ODE

Ahem, what about dInitODE()?

ODE actually doesn't need setup: when you create a world everything gets set up. But I'm going to describe here how to create and destroy a world.

Creation is extremely simple: a world is identified by the dWorldID type, it's like a pointer, but actually user doesn't see it. You allocate it with dWorldCreate(), and when you finish with it, you can destroy it with dWorldDestroy(dWorldID).

dWorldID world = dWorldCreate();
// Use this world
dWorldDestroy(world);
// Other stuff
dCloseODE();

The latter is expecially useful to clean totally the memory from ODE information: some of it isn't destroyed with worlds, so you have to clean it by hand with dCloseODE(). It's intended to be used when you don't need the engine anymore.

Probably you'll need to correct the error caused from ODE. You can do this with dWorldSetERP(dWorldID, dReal), with typical values ranging from 0.1 to 0.8. Default is 0.2 and I assume it's ok for us, so for now we don't want to change it.

Also, you may want to change the CFM, but as above we don't need to change it: for now we assume that the value is okay. If you think you may need it, read the manual for details.

To integrate we can use the dWorldStep or dWorldQuickStep functions. We'll use the former, preferring speed over precision.

It's a really good idea to use constant step sizes, because ODE will produce jittering on variable step size. So: calculate once the updating time of your application and keep it constant.

int nextUpdate = 20; // millisec
dReal stepSize = nextUpdate/1000.0; // Step size, fixed
 
// Time correct loop
while (1) {
	registerTime();
	dWorldQuickStep(world, stepSize);
	drawThings();
	if (savedTime() + nextUpdate > actualTime())
		wait(savedTime() + nextUpdate - actualTime());
}

How to create a world

It's important to get this concept: one thing is a body, and one thing is its geometry. Actually a body is an entity with a mass, but it doesn't have a shape. Such shape is the geometry. This distinction is required because geometries are used for collision detection, but masses aren't. And a geometry doesn't need to have a mass; for example, if you build a terrain, probably you want it to be fixed, like have an infinite mass. In this tutorial, we'll use a plane as a fixed geometry, and some objects with mass and geometry.

So, for our purpose, we'll divide this job into 2 parts: creating bodies and creating the geometries. Let's start by initializing the world for our demo:

/* Application Initialization */
	dWorldID world;
 
	// Initialize the world
	world = dWorldCreate();
	dWorldSetGravity(world, 0.0, -4.0, 0.0);

Creating bodies

For this demo, we put in the game 3 rigid bodies:

  • A sphere, mass 1
  • A cube, mass 2
  • An irregular object, mass 4

For now, we absolutely don't care about shapes and collisions: we just want to define objects. First, let's define a data structure for our demo:

typedef struct {
	dBodyID body;
	dMass mass;
} Object;

As you see, we have a body identifier, and actually I'm putting in this structure also the mass data: these data are the mass quantity itself, the gravity centre and an inertia tensor. Since I really don't care of these two complicates things now, the initialization code is the same for every objects. They'll be all spheres.

/* Application Initialization */
	enum { oCube, oSphere, oComplex, objsCount };
	Object objs[objsCount];
	dReal masses[objsCount] = {1.0, 2.0, 4.0};
 
	// Initialize objects
	for (int i = 0; i < objsCount; ++i) {
		// Create an object in the world
		objs[i].body = dBodyCreate(world);
		// Create a mass with the distribution of a sphere
		dMassSetSphereTotal(&objs[i].mass, masses[i], 1.0);
		// Apply the mass to the cube
		dBodySetMass(objs[i].body, &objs[i].mass);
		// Translate the body somewhere
		dBodySetPosition(objs[i].body, (i - 1) * 4.0, 0.0, 0.0);
		// Set a starting speed
		dBodySetLinearVel(objs[i].body, (i - 1) * -1.0, 0.0, 0.0);
	}

As you see, we set here physic properties not related with geometry itself: position and mass. Actually we can set also velocity and other nice things for an object, but we don't care of it right now, because we are more interested in animathing the whole scene. It's raw code, but don't be scared, i'm sure i can do better :) Then we set a linear velocity on the object, just to see what happens.

Let's draw something to see what happens

void drawObject(Object *o) {
	int type = dGeomGetClass(o->geom); // Doesn't work
	switch (type) {
		case dSphereClass:
		case dBoxClass:
		case dTriMeshClass: {
			float rad = dGeomSphereGetRadius(o->geom);
			GLUquadric *q = gluNewQuadric();
			gluSphere(q, rad, 8, 8);
			break;
		}
		default:
			printf("        UNKNOWN OBJECT CLASS!\n");
	}
}
 
void drawEverything() {
	const dReal *realP;
	// Drawing bodies
	for (int i = 0; i < objsCount; ++i) {
		glPushMatrix();
			glColor3fv(colors[i]);
			realP = dBodyGetPosition(objs[i].body);
			glTranslatef(realP[0], realP[1], realP[2]);
			drawObject(&objs[i]);
		glPopMatrix();
	}
}
 
	// Main loop
	while (1) {
		...
 
		glLoadIdentity();
		glTranslatef(sceneTraX, sceneTraY, -20.0);
		glRotatef(sceneRotX, 1.0, 0.0, 0.0);
		glRotatef(sceneRotY, 0.0, 1.0, 0.0);
		drawEverything();
 
		dWorldQuickStep(world, 0.05);
 
		SDL_GL_SwapBuffers();
		SDL_Delay(20);
	}

As probabily you can imagine, all the objects will fall in the deep. Of course ODE doesn't know anything about their shape and it doesn't know anything about the plane we'll put under them. They will simply fall under gravity action. And, well, the drawing code simply doesn't work :) Because it's based on geometry classes, that we're going to define in next section.

Creating geometries

Starting from a better explanation of "geometry" is a good idea. A geometry represent a rigid shape and it's the base for the collision system: defining a geometry is like to creating an "hard thing" in reality: a table, a book, something that you can touch. When geometries collide they produce contact points, which can be used by you to determining complicated things about collisions (if you mean to) like rotating an object to follow the slope of the surface under it. Geometries are placeable or non-placeable, i.e. they can be moved around or not. In our example, the plane is non-placeable, while the other objects are. So a geometry can be moved and rotated around like a physic object, and we'll see later that we can attach a geom to a body, so moving one will affect the other.

Geoms are actually objects in the code: they are instances of classes like sphere, cube, cylinder models. We'll use spherical and triangle classes. Actually you can even implement (or modify) a new class and use that for your objects, but for this tutorial we're not interested in doing it. If you're interested, I redirect you to library source code :) I love open source, don't you? Looking at the code is easy like drinking a coffee.

Before going into the details, it's important to understand the concept of space: it's like a geometry, but it can be composed by multiple geometries allowing internal collision between them. This is important because it allow you to keep little subsystems of geometries, and it also increase the computational speed. You can see the space as a world, but instead of handling bodies it handles geometries and collisions. A space is-a geometry, but in addition you can add other geometries to it, and fiew other things.

A geometry has a representing identifier dGeomID, you can destroy a geom with dGeomDestroy(dGeomID), and you can attach an amount of user-defined data to a geometry.

Let's code :) First we want to define the space, create a geometry for the plane and the spheres. After this we'll see collisions between them.

First we add the geometry info to the object structure

typedef struct {
	dBodyID body;
	dMass mass;
	dGeom geom;
} Object;

Then we define a space. There are different types of spaces, depending on your objects structures, but for our needs, a simple space is ok:

dSpaceID space;
...
space = dSimpleSpaceCreate(0);

As you know, one can create a hierarchy of spaces, so you can specify the parent space when creating a new one. In this case, this is the root (and the only space in our world), so we use 0 as parameter.

Finally we can initialize geometries. Let's review the proper loop:

// Initialize objects
	for (int i = 0; i < objsCount; ++i) {
		// Create an object in the world
		objs[i].body = dBodyCreate(world);
		// Create a mass with the distribution of a cube
		dMassSetBoxTotal(&objs[i].mass, masses[i], 1.0, 1.0, 1.0);
		// Apply the mass to the cube
		dBodySetMass(objs[i].body, &objs[i].mass);
		// Translate the body somewhere
		dBodySetPosition(objs[i].body, (i - 1) * 4.0, 0.0, 0.0);
		// Add some starting speed
//		dBodySetLinearVel(objs[i].body, (i - 1) * -1.0, 0.0, 0.0);
 
		// Create a geometry
		objs[i].geom = dCreateSphere(space, (i / 3.0) + 1.0);
		// And givin it some data
		dGeomSetData(objs[i].geom, names[i]);
		dGeomSetBody(objs[i].geom, objs[i].body);
	}

Defining a geometry is quite simple: first you create a shape for it, then you create a geometry and attach the data. Finally you can attach user-defined data and bind the geometry to a body. As you can easily see, dCreateSphere actually includes the shape and geometry creation: the dCreateSphere function creates the geometry objects and the spherical shape that is used. We also bind the geometry to physical objects. This is the way you actually make an effective rapresentation of the world with objects that can collide.

Now i create the plane geometry: to begin we'll try to make spheres bounce on this plane. The geometry we're interested in, is the TriMesh geometry. It's a soup of triangles: you specify vertexes and indexes that compose the triangles. In this way you can actually store data in the way you prefer (triangle strip, triangle fan, separated triangles, etc). It seems that being able to handle correctly triangle meshes in ODE is an hard task for most users, so i'm trying to make it easy (since it was hard for me, too... I took a whole afternoon to get it working).

First, watch the code:

dGeomID plane;
dTriMeshDataID triMesh;
...
	// Plane geometry
	const int indexes[6] = {2, 1, 0, 3, 2, 0};
	const dVector3 triVert[4] = {
		{ 10.0,  0.0,  10.0},
		{-10.0,  0.0,  10.0},
		{-10.0,  0.0, -10.0},
		{ 10.0,  0.0, -10.0}
	};
 
	triMesh = dGeomTriMeshDataCreate();
	dGeomTriMeshDataBuildSimple(triMesh, (dReal*)triVert, 4, indexes, 6);
	plane = dCreateTriMesh(space, triMesh, NULL, NULL, NULL);
	dGeomSetData(plane, "Plane");
	dGeomSetPosition(plane, 0, -10.0, 0);

Documentation make it a bit hard to understand, but it isn't for real. To begin you must get how triangle meshes are managed in ODE: you define vertexes and indices, first identify the vertexes themself, then you use the indices to specify which vertexes are used to compose a triangle. In this case, we have 2 triangles: the first uses vertexes 2, 1 and 0; the second uses 3, 2, 0. It's important to understand also that triangles can have normals, and if you don't provide them, they'll be build, so you have to specify a correct order of your indices.

Infact, my first version of the indexes array was:

const int indexes[6] = {0, 1, 2, 0, 2, 3};

But inoticed that the plane was behaving a bit strange... More like an accelerator, than a plane :) You can invert the indices later to see what i mean ;)

Now that you should know well how to specify vertexes, just create a triangle mesh that use them. With dGeomTriMeshDataBuildSimple, it's easy: you specify the triangle mesh data structure to use, then the vertexes, the vertex count, the indixes and it's count. Fianlly i set some data (a name) for this geometry, and move it around.

Keep in mind that moving a geometry, immediately transform it's vertexes. I didn't knew it, so my demo resulted in vertexes positioned somewhere, while the plane was drawn in another place :P As you see, we don't bind any object to this plane, because this mesh will be fixed here. If you like to, you can use dCreatePlane to create an infinite plane geometry.

The NULL, NULL, NULL that you see in dCreateTriMesh, are callback pointers for handling triangles-specific collisions, but we don't care about that.

We have geometries now, and the drawing code actually works, because every geometry is a sphere. For the plane we use a different code:

void drawEverything() {
	const dReal *realP;
	// Drawing bodies
	for (int i = 0; i < objsCount; ++i) {
		glPushMatrix();
			glColor3fv(colors[i]);
			realP = dBodyGetPosition(objs[i].body);
			glTranslatef(realP[0], realP[1], realP[2]);
			drawObject(&objs[i]);
		glPopMatrix();
	}
 
	// Draw plane
	dVector3 v1, v2, v3;
	glPushMatrix();
		glColor3f(1.0, 1.0, 1.0);
		realP = dGeomGetPosition(plane);
//		glTranslatef(realP[0], realP[1], realP[2]); Remember? Moving a geometry transform it's vertexes.
		glBegin(GL_TRIANGLES);
			for (int i = 0; i < 2; ++i) {
				dGeomTriMeshGetTriangle(plane, i, v1, v2, v3);
				glVertex3fv(v1);
				glVertex3fv(v2);
				glVertex3fv(v3);
			}
		glEnd();
	glPopMatrix();
}

If you try the code now, you'll see objects falling in the deep, but without bouncing, colliding or something. To handle collisions, we need to implement a callback function: it'll be called everytime two objects are potentially colliding. In this demo we'll use two of these functions: one is built to show you how collisions are managed, the second is to actually manage the collisions and make the sphere bounce.

dReal contactPoints[20][3];
int contactPointsCount = 0;
dJointGroupID contactGroup;
 
void collisionHandler(void *dataUnused, dGeomID o1, dGeomID o2) {
	// Since here there is just one space, we don't check for geometry type:
	// they are not spaces
	dContactGeom contacts[MAX_CONTACTS];
	// Check for collisions
	int collisions = dCollide(o1, o2, MAX_CONTACTS, contacts, sizeof(dContactGeom));
	printf("%d collision points\n", collisions);
 
	// Save contact points
	for (int i = 0; i < collisions; ++i) {
		dGeomID g1 = contacts[i].g1,
				g2 = contacts[i].g2;
 
		if (g1 == g2)
			continue;
 
		float *pos = contacts[i].pos;
		printf("Copoint %d: %f %f %f\n", i, pos[0], pos[1], pos[2]);
 
		contactPoints[contactPointsCount][0] = pos[0];
		contactPoints[contactPointsCount][1] = pos[1];
		contactPoints[contactPointsCount][2] = pos[2];
		contactPointsCount++;
 
		char *o1Name = dGeomGetData(g1);
		char *o2Name = dGeomGetData(g2);
		const dReal *o1Pos = dGeomGetPosition(g1);
		const dReal *o2Pos = dGeomGetPosition(g2);
 
		printf("Collision between %s (%x) and %s (%x)\n", o1Name, g1, o2Name, g2);
	}
}

In this code we set a global vector of points, contactPoints. When two objects collide, we add a point in this array. It is used by drawEverything() to show you where are contact points. To see them in action is userful, because you get immediately an idea about how collisions works.

As you see, the first step is to compute collisions: dCollide takes 2 geometries and calculate collisions points between them. The MAX_CONTACTS parameter is necessary! If you set it to 0, it will always assume max 1 contact points. Then you pass an array of contacts points and a stride value (distance between two dContactGeom in the array).

To use this function, you should say ODE that you want to use it :) You can do this by calling dSpaceCollide(space, 0, collisionHandler) in your main loop, before drawing. dSpaceCollide will check for collisions between pairs of objects, and it takes an user defined value (set to 0 because we don't need it) and the function to call for every collision.

Collision points are a bit tricky, i think: when two geoms collide, a point is generated, and it stores some info like it's positions, a normal and a depth. The position is of course the collision point position, the normal is a vector that say in which direction the collision is directed, that is the direction between the two geometries colliding. Depth is how much a geometry is penetrating the other. From this, you can imagine that moving an object in the direction of the normal*depth, will make the objects tangent to each other, making the collision non-penetrating. ODE manual in section 10.1 (Contact Points) it's more clear about this data structure and how it works.

Take a look at collision points in this scene:

And what about physical reaction? Objects can collide, now, but actually they act like they aren't! For this, we need to create a special collision joint for each contact point. The joint in ode is usually used to attach two bodies to introduce a constrain between them, like a leg articulation or a piston. In addition to this, ODE have a Collision joint, used for handling physical reaction between colliding objects. Creating a contact joint requires the creation of a dContact structure, setting the parameters as you need in your simulation. These parameters are things like bounciness and friction of the contact surface. Actually i'm not going deeply into details, because all these are complicated physical variables that you should be able to tweak by yourself. For help, read the ODE manual in section 7.3.7 (Joint parameters, Contact).

To handle our collision, we create a new callback, so you can play with the code switching the callback as you like to watch what happens.

void nearCallback(void *unused, dGeomID o1, dGeomID o2) {
	dBodyID body1 = dGeomGetBody(o1);
	dBodyID body2 = dGeomGetBody(o2);
 
	dContact contact[MAX_CONTACTS];
 
	for (int i = 0; i < MAX_CONTACTS; i++) {
		contact[i].surface.mode = dContactBounce; // Bouncy surface
		contact[i].surface.bounce = 0.5;
		contact[i].surface.mu = 100.0; // Friction
	}
 
	int collisions = dCollide(o1, o2, MAX_CONTACTS, &contact[0].geom, sizeof(dContact));
	if (collisions) {
		for (int i = 0; i < collisions; ++i) {
			dJointID c = dJointCreateContact(world, contactGroup, contact[i]);
			dJointAttach(c, body1, body2);
		}
	}
}
 
...
	// Main loop
	dJointGroupEmpty(contactGroup);

In this example, we create contacts joints for a bouncy surface: it's necessary to allocate an array of contacts, set flags for them and finally it's possible to create joints and add them to a group. Finally we can attach this joint to bodies to make them interact.

Note: I didn't managed friction yet. It's important, but actually this code seems to produce 0 frictions: the object continue to slip over the plane until they fall. Friction value should be set with surface.mu value, and it should be between 0 and dInfinity, that is no friction and total friction.

The code developed until here

To be continued...