|
|
|
| |
| |
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. |
|
|
|
| |
| |
| |
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:
- Apply forces, position
changes and rotations to
the ODE bodies and geometries
- Calculate the collisions
and insert one contact joint
between the corresponding
bodies for each collision.
- Let the simulation calculate
one step
- Clear all contact joint
informations
- Apply the new positions
and rotations to the visual
objects
- 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 ;-) |
|
|
|
| |