Posts
Search
Contact
Cookies
About
RSS

using Irrlicht and ODE together

Added 4 Feb 2017, 10:01 a.m. edited 18 Jun 2023, 1:12 a.m.
Both Irrlicht and ODE (Open Dynamics Engine) are long standing and mature projects, ideal if you just want things to work without drama! Of course you have to put up with some C++ coding but at least the Irrlicht API doesn't force any of the vagaries of "advanced" C++ on you, and you can keep your code reasonably readable despite the usual sprinkling of ASCII soup, and of course ODE's API is a robust no nonsense C API (I believe there is a C++ API for ODE but I've never actually seen it used anywhere!) For ease of development, I've split even small bits of boiler plate out into their own file, for example the event receiver is a great little self contained unit and not something you need visit so why have it in your main source file, however when it gets to things like callbacks, you do need to start using the odd global and extern - while usually frowned on, globals do have their place, and especially as we are not using multiple threads, there is a lot less complication. One class that probably does need work its the physics object class (physObj) it defiantly needs a destructor (currently the OS cleans up Irrlicht and ODE resources!) and things like the dynamics body is just publicly exposed (ooh err!) - That said do you really want to encapsulate every ODE function that you can do with a dynamics body (sometimes public fields are not always evil!) Obviously like globals, public fields will not result in the world ending but you do need to take responsibility for how you use them... initialising ODE is simple
    dInitODE2(0); 
    dAllocateODEDataForThread(dAllocateMaskAll); 
 
    world = dWorldCreate(); 
    space = dHashSpaceCreate(0); 
    contactgroup = dJointGroupCreate(0); 
    dWorldSetGravity(world,0,-9.98,0);
The contact group is of interest as this is what ODE uses to arbitrate collisions and will be used in our collision callback but more of that later... Because a lot of the dynamics and visual stuff has been munged together we can create an object without lots of boiler plate for both libraries for each different instance, don't forget potentially we could be creating at start, and also later on in response to certain events...
     for (int i=0;i<200;i++) { // 400 shapes 
        new physObj(physObj::ShapeType::box, 
                core::vector3df(randf(0.5f,1.f), randf(0.5f,1.f), randf(0.5f,1.f)), 
                core::vector3df(randf(-4.f,4.f), randf(1.5f,15.f), randf(-4.f,4.f))); 
        new physObj(physObj::ShapeType::sphere, 
                core::vector3df(randf(0.25f, 0.5f),0.f,0.f),   // sphere size only uses .X 
                core::vector3df(randf(-4.f,4.f), randf(1.5f,15.f), randf(-4.f,4.f))); 
    }
The shape type only allows us to create two different objects and most of the size vector is wasted for spheres but you should be able to see how this could later be extended to create a wide range of different types of objects, including compound geometries, ideally you'd want to add some kind of visual template (an Irrlicht scene node) rather than have all the shapes of the same type look identical, there are many different ways to go, but the basic welding of the visual and dynamic is well in hand (we'll look closer later)
        dSpaceCollide(space, 0, &nearCallback); 
        dWorldQuickStep(world, frameDeltaTime);
        dJointGroupEmpty(contactgroup); 
 
        physObj::updateAll(); 
 
        driver->beginScene(true, true, video::SColor(0,0,0,0)); 
        smgr->drawAll(); // draw the 3d scene 
        driver->endScene();
This is the meat of our render loop, the dynamics library first checks for collisions within the space, all the potentially colliding object pairs are passed to the our near callback function. At this point all colliding objects will have temporary contact joints and the world can be "stepped". Having advanced our dynamics objects, we run a static method in physObj which updates all Irrlicht's visual objects that we have representing the dynamic objects. The only bit left of interest in our main unit (of compilation) is our dynamic interaction, lets look at the code that is run when the space bar is held down...
            for (physObj* o : physObj::objects) { 
                pos = dBodyGetPosition(o->body); 
                v.set(0.f, 10.f, 0.f); 
                v=v-core::vector3df(pos[0],pos[1],pos[2]);  
                v.normalize();  //  vector to target 
                dBodyGetMass(o->body, &m); 
                f = m.mass * 25.0;   // force * mass = same acceleration for each one 
                v=v*f; 
                dBodyEnable(o->body); // adding a force won't enable a disabled body (a collision will) 
                dBodyAddForce(o->body, v.X, v.Y, v.Z); 
            }
looping through each object we first subtract the objects position from a target position, normalising this gives us the direction from the object to a target, but importantly at a unit magnitude. Multiplying this vector by mass and scaling this up, gives us a force to add that will end up giving all our differently massed objects the same velocity (collisions not withstanding). When you break it down like that the effect this gives looks like it should be quite complex to code, but this is one of the many nice things about a dynamics engine, shove stuff in a particular direction and it all behaves (almost!) like you'd expect it to in the real world, often complex behaviours will arise from the simplest rules, for example given the right circumstances you'll see some objects orbiting the big bait ball of objects. Moving on to out collision callback, there is little to be said here other than to say that we get the opportunity to specify how bodies interact, for example if we set a bodies user data (to its physObj for example) we can identify which are say ice and wood or ice and metal, given different combinations of bodies we can change the collision properties like friction and so forth. Object creation is in the physObj constructor
    physObj::objects.push_back(this);
    body = dBodyCreate(world);
    dBodySetPosition(body,pos.X, pos.Y,pos.Z);
    if (st==physObj::ShapeType::sphere) {
         ... (similar to box)
    }
    if (st==physObj::ShapeType::box) {
        node = smgr->addCubeSceneNode(1.f,0,-1, pos,
              core::vector3df(0.f, 0.f, 0.f), size);
        node->setMaterialTexture(0, driver->getTexture("./data/compCube.png"));
        dMassSetBox(&m,1,size.X,size.Y,size.Z);
        geom = dCreateBox(space,size.X,size.Y,size.Z);
    }
    node->setMaterialFlag(video::EMF_LIGHTING, false);
    dGeomSetBody(geom,body);
    dBodySetMass(body,&m);
phyObj maintains a static list of all the created object, which is a common and handy pattern which allows us to later iterate them when we want to update them all. Each object's mass is calculated from a fixed density which gives us a sensible mass for different sized objects. When updating our phyObj position and rotation, we just query the dynamics position and rotation, the rotation is converted from a quat to eular angles, while eular rotations are naive seeming, as its a single absolute rotation, its not a problem. The bulk of what code there is, is purely to calculate eular angles from a given quaternion. So there's the outline, and while defiantly not perfect, its plenty to get you going... You can get the source here