ODE Tutorial
This tutorial explains the basics of ODE through the construction of a simple applications that displays cubes falling on the ground.
Preliminary setup
Discovering the QGLViewer
We are going to use ODE for the physics and a very handy library for OpenGL display of the physic simulation, named QGLViewer. This library is based on Qt and encapsulates a viewer for rendering and manipulating (flying through, rotating with a trackball,...) a 3D scene. There are excellent tutorials on the web site, so I just present here the very fundamentals of the library.
- Create a class Viewer that derives from QGLViewer
- Override the method QGLViewer::init() to indicate the OpenGL setup, such as enabling states (e.g. alpha blending) or creating context dependent objects (e.g. texture objects, frame buffers or vertex buffer objects).
- In the overidden QGLViewer::init(), you also specify the "size" of the scene, so that the viewer can optimally compute the near and far plane of the OpenGL camera used to render the scene. You can also tell the viewer to initially setup the camera so that the whole scene is encompassed by the camera frustum. Finally, you can position the camera to the position it occupied when the application was last quit (yes, the QGLViewer saves its state in a .qglviewer.xml file!).
- Overidde the method QGLViewer::draw() to indicate what OpenGL command must be issued to render the scene.
- Overidde the method QGLViewer::animate() to indicate how to animate the scene. This function is called regularly by the viewer, using a timer whose frequency is set with QGLViewer::setAnimationPeriod() (default is every 30ms).
Here is for example a simple viewer that shows a ball whose color is changing over the time.
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Compiling with qmake in a nutshell
Qt has a very nice tool to manage compilation, called qmake. You write a simple project file describing the sources and some configuration, and qmake generates the appropriate Makefile for your platform. Here is such a .pro for our simple example abobe:
| 1 | CONFIG *= qglviewer glut ode |
| 2 | |
| 3 | HEADERS *= viewer.h |
| 4 | SOURCES *= viewer.cpp main.cpp |
If you notice the first line, I indicate that I want the project to use qglviewer and glut configuration. These are non standard Qt configuration, so I need to tell Qt what to do when they are presents. For that, I define a qglviewer.prf and a glut.prf files that I put in some directory pointed by the environment variable QMAKEFEATURES. Here are what it looks like on my linux desktop:
|
|
I let you adapt this to you particular machine setup. The cool thing about the "feature" mechanism is that it separates machine-dependent configuration (where a particular library is installed) from project-dependent configuration (which file to compile, which library to link with).
Testing our example
Ok, now we can run our example. It displays a simple sphere. Use the three mouse button (+wheel) to turn around the sphere. This is the trackball metaphor. You can press space to switch to a fly metaphor. Press enter to toggle the animation. Quit and re-run the application: the viewpoint is restored! If you want to find more about the existing keybindings, press H, or (better) read the QGLViewer excellent documentation.
ODE in a nutshell
There are many things to know to use ODE correctly, because a lot of magical parameters have to be tuned for your particular application to run as you want. Fortunately, the documentation is quite verbose about this. Conversely (and unfortunately), it lacks a gentle introduction. Let's remedy to this. Here are what you need to know to begin.
Dynamics
ODE has a number of base concepts. Let's start with the two fundamental ones:
- Body: this is a solid rigid body whose dynamics will be computed by ODE during simulation. As so, it does not have a "shape" or a visual appearance. It is just a position and orientation (a "local frame" placed on the center of gravity if you will), a linear and angular velocity (how fast it moves and spins), and a mass together with an intertia tensor (how the mass is distributed in the object).
- World: this is what holds a bunch of bodies (dynamic and static) along with forces, and a current time. Running the physical simulation is just advancing the world's time by small steps and updating each bodies dynamics according to the existing forces and the fundamental law of dynamics.
So basically, here is how you create a physical simulation.
| 1 | dWorldID world = dWorldCreate(); |
| 2 | |
| 3 | // Create a body |
| 4 | dBodyID body = dBodyCreate(_world); |
| 5 | |
| 6 | dMass mass; |
| 7 | dMassSetBox(&mass,1,1,1,1); |
| 8 | dMassAdjust(&mass,0.2f); |
| 9 | |
| 10 | dBodySetMass(body,&mass); |
| 11 | dBodySetPosition(body,0,6,0); |
| 12 | |
| 13 | // Create another body |
| 14 | // ... |
| 15 |
Here, we create just a cube but you can add as many objects as you want. We'll see more about this later on. For the moment, notice how we specified the mass. We used helper functions. Indeed, what the dynamics need is not the mass as you usually think of it (I weight xxx), but the mass inertia matrix, which also gives you how the weight is distributed over the body (it drives how the body spins). This inertia matrix is inintuitive to specify in matrix form. Luckily, for specific cases, such as a uniformly dense cube or sphere, it is easy to compute from the box/sphere dimensions and density. This is what the helper functions do for you. You can find more in section 9.2.0 of the ODE manual.
Collision
I said above that dynamics simulation itself does not need to know the shape of each body. Well, that is true only if you consider that objects do not interact with each other. In reality, a physical simulation is interesting if bodies interact, namely if they collide, bounce and push each other. To compute such interactions, you know need to know the shape of objects, because this is what commands when/where/how bodies interact. Thus, ODE has two other concepts.
- Geom: this is what describe the shape (the geometry) of a body. It can be a canonical shape such as a cube or a sphere, or a polygonal object described by a triangulation of its surface (known as a B-rep).
- Space: this is what holds a bunch of geoms and manages the collision detection more or less intelligently. ODE comes with several flavor of space, as we will see in a moment.
So here is how we would define a geom in ODE, continuing the body example of above.
| 1 | dSpaceID space = dHashSpaceCreate(0); |
| 2 | |
| 3 | // Create a geom |
| 4 | dGeomID geom = dCreateBox(space,1,1,1); |
| 5 | // Bind together our previously created body and the geom |
| 6 | dGeomSetBody(geom,body); |
| 7 |
The important thing to notice is that we "bind" together a body and a geom. Once this is done, changing the position or orientation of one will change that of the other, that is we can call indifferently dBodySetPosition(body,0,6,0); or dGeomSetPosition(body,geom,6,0);.
The interest of separating the body from the (collision) geometry is that we can create "static" geometry. For that, we just create a geom in the space, and not associated body in the world. Typically, this is used to create a ground plane.
Joints
Bodies do not interact only because of collision. They can also interact because they are "attached" together. In ODE terminology, such an attachment is called a joint. I will not describe here the types of join and how to create them, because in this tutorial, we will only consider indpendent objects (e.g. falling cubes on a plane). Moreover, you will find detailed information in the ODE manual.
However, I will describe a particular type of joints: contact ones. Indeed, we have seen that the space manages the collision detection based on the given geoms. However, once collisions are detected, the dynamics must account for them. It turns out that collision/contact interaction can be described as temporary joints that exists only during the time of the contact. Thus, during simulation, we will:
- detect collision,
- create contact objects (indicating physical parameters such as friction coefficients or bouncyness),
- create joints from these contacts,
- attach this joint two the two bodies involved.
We will see more about how to do this in a moment.
Preparing for the show
Before we go to the full monthy and a working example, we should prepare the terrain a little bit, so that the code is organized cleanly and you can quickly identify where to put additional codes to extend the tutorial example. This involves some code engineering that I describe shortly here.
Object class
Since bodies and geoms are tightly linked in a normal ODE application, we will encapsulate them together in a bigger entity called object. We will also encapsulate how to render the object to make our application more sexy. For the moment, we will make a very crude encapsulation; we will see later how to make a better object based one.
An object owns a body and a geom, publicly accessible for simplicity. It can also be required to render itself (using OpenGL), and finally, it can be placed at a given position. The interface and implementation are:
|
|
The render() function does some fiddling to pass from ODE representation (a position and rotation) to OpenGL representation (a general 4x4 matrix) of transformations. After that matrix is added to OpenGL model view matrix, and object can render itself by using coordinates in its local frame. That's the purpose of the pure virtual function renderInLocalFrame().
We can now subclass Object for various shapes we want to handle. Here is what we could come up with for a cube, for example:
|
|
Bounding box
As we said earlier, the QGLViewer needs to know the size of the world to correclty setup the camera for rendering. Here is a helper function that takes a vector of objects and compute their axis aligned bounding box.
| 9 | namespace |
| 10 | { |
| 11 | void getAABB(const QVector<Object *>& objects,dReal aabb[6]) |
| 12 | { |
| 13 | dGeomGetAABB(objects[0]->geom,aabb); |
| 14 | foreach (Object *o,objects) |
| 15 | { |
| 16 | dReal aabb[6]; |
| 17 | dGeomGetAABB(o->geom,aabb); |
| 18 | for (int i=0;i<3;++i) |
| 19 | { |
| 20 | aabb[ i] = min(aabb[ i],aabb[ i]); |
| 21 | aabb[3+i] = max(aabb[3+i],aabb[3+i]); |
| 22 | } |
| 23 | } |
| 24 | } |
| ............................ | |
| 43 | } |
| ............................ | |
Note that the foreach is a Qt helper construction, very handy. If you are adapting this code to be Qt independent, just use a for loop.
Plane rendering
Finally, we need a little helper function to render an infinite plane, such as the ground plane.
| 9 | namespace |
| 10 | { |
| ............................ | |
| 25 | void renderPlane(dVector4 equation,float size) |
| 26 | { |
| 27 | Vec z = Vec(equation).unit(); |
| 28 | Vec o = equation[3]*z; |
| 29 | Vec x = z.orthogonalVec().unit(); |
| 30 | Vec y = (z^x).unit(); |
| 31 | glBegin(GL_QUADS); |
| 32 | glNormal3fv(z); |
| 33 | glVertex3fv(o-size*x-size*y); |
| 34 | glVertex3fv(o+size*x-size*y); |
| 35 | glVertex3fv(o+size*x+size*y); |
| 36 | glVertex3fv(o-size*x+size*y); |
| 37 | glEnd(); |
| 38 | } |
| ............................ | |
| 43 | } |
| ............................ | |
The full monthy
Viewer
We create our Viewer class that extends the QGLViewer. We make the world and space be private members. We also add members for storing objects, and additional static collision geoms. Here we will just use planes.
| 9 | class Viewer : public QGLViewer |
| 10 | { |
| 11 | Q_OBJECT; |
| 12 | public: |
| 13 | Viewer(QWidget *parent=NULL); |
| 14 | ~Viewer(); |
| ............................ | |
| 17 | protected: |
| 18 | virtual void init(); |
| 19 | virtual void startAnimation(); |
| 20 | virtual void animate(); |
| 21 | virtual void draw(); |
| 22 | private: |
| 23 | dWorldID _world; |
| 24 | dSpaceID _space; |
| ............................ | |
| 26 | QVector<Object*> _objects; |
| 27 | QVector<dGeomID> _planes; |
| 28 | dReal _aabb[6]; |
| ............................ | |
| 30 | }; |
| ............................ | |
In the constructor, we create the world, space, objects and static planes.
| 45 | Viewer::Viewer(QWidget *parent) |
| 46 | : QGLViewer(QGLFormat(QGL::SampleBuffers | |
| 47 | QGL::DoubleBuffer | |
| 48 | QGL::DepthBuffer | |
| 49 | QGL::Rgba | |
| 50 | QGL::AlphaChannel | |
| 51 | QGL::StencilBuffer),parent) |
| 52 | { |
| 53 | //////////////////////////////////////////////////////////// |
| 54 | // Create world, collision space and contact group |
| 55 | //////////////////////////////////////////////////////////// |
| 56 | _world = dWorldCreate(); |
| 57 | _space = dHashSpaceCreate(0); |
| 58 | // Set up gravity force |
| 59 | dWorldSetGravity (_world,0,0,-9.81); |
| 60 | // Create contact group |
| 61 | _contactgroup = dJointGroupCreate(0); |
| 62 | //////////////////////////////////////////////////////////// |
| 63 | // Creating objects |
| 64 | ////////////////////////////////////////////////////////////s |
| 65 | Cube *cube = new Cube(_world,_space,0.3,0.3,0.3,1); |
| 66 | cube->setPosition(0,0,1); |
| 67 | cube->color[0] = qrand()%255; |
| 68 | cube->color[1] = qrand()%255; |
| 69 | cube->color[2] = qrand()%255; |
| 70 | _objects.push_back(cube); |
| 71 | // Create static planes |
| 72 | _planes.push_back(dCreatePlane(_space,0,0,1,0)); |
| 73 | // Compute the bounding box |
| 74 | getAABB(_objects,_aabb); |
| 75 | } |
| ............................ | |
In the destructor, we clean up everything. Note that world and space take care of deleting the bodies and geoms create inside them. The qDeleteAll() is a handy Qt algorithm: it calls delete on each element of the container.
| 76 | Viewer::~Viewer() |
| 77 | { |
| 78 | ////////////////////////////////////////////////////////////// |
| 79 | // Deleting objects |
| 80 | ////////////////////////////////////////////////////////////// |
| 81 | qDeleteAll(_objects); |
| 82 | ////////////////////////////////////////////////////////////// |
| 83 | // Terminate with ODE |
| 84 | ////////////////////////////////////////////////////////////// |
| 85 | dSpaceDestroy(_space); |
| 86 | dWorldDestroy(_world); |
| 87 | dCloseODE(); |
| 88 | } |
| ............................ | |
For the viewer initialization, we use the computed bounding box to set up scene center and radius. We stick the center to the ground plane because it feels more natural for the trackball. We also setup OpenGL lighting.
| 89 | void Viewer::init() |
| 90 | { |
| 91 | Vec min(_aabb); |
| 92 | Vec max(_aabb+3); |
| 93 | Vec center = (min+max)/2.0f; |
| 94 | center.z = 0.0f; |
| 95 | setSceneRadius((max-min).norm()); |
| 96 | setSceneCenter(center); |
| 97 | // Try to restore previous state or focus the whole scene |
| 98 | if (!restoreStateFromFile()) |
| 99 | { |
| 100 | showEntireScene(); |
| 101 | } |
| 102 | // Setup OpenGL |
| 103 | GLfloat ambient[] = { 0.3f, 0.3f, 0.3f }; |
| 104 | GLfloat diffuse[] = { 0.1f, 0.1f, 0.1f , 1.0f}; |
| 105 | GLfloat specular[] = { 0.0f, 0.0f, 0.0f , 1.0f}; |
| 106 | glLightfv(GL_LIGHT0, GL_AMBIENT, ambient); |
| 107 | glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse); |
| 108 | glLightfv(GL_LIGHT0, GL_SPECULAR, specular); |
| 109 | } |
| ............................ | |
Finally, the drawing just traverses the objects and planes to render them.
| 110 | void Viewer::draw() |
| 111 | { |
| 112 | // Draw the objects |
| 113 | foreach (Object *o,_objects) |
| 114 | { |
| 115 | o->render(); |
| 116 | } |
| 117 | // Draw the planes |
| 118 | qglColor(Qt::white); |
| 119 | foreach (dGeomID g,_planes) |
| 120 | { |
| 121 | if (dGeomGetClass(g)) |
| 122 | { |
| 123 | dVector4 equation; |
| 124 | dGeomPlaneGetParams(g,equation); |
| 125 | renderPlane(equation,sceneRadius()); |
| 126 | } |
| 127 | } |
| 128 | } |
| ............................ | |
Simulation
For the simulation, we must be carreful with time management. Between two QGLViewer::animate() calls, a certain amount of time has elapsed. Naively, we would advance the world time of that amount. But because ODE uses a numerical integration of the underlying differential equation, we cannot make big steps, or the simulation will be innacurate, or even worse, unstable. So instead, we make "enough" small steps to cover the time delta. For measuring the elapsed time between two successive calls, we use the handy Qt class QTime, of which we store an occurence _time in the viewer.
| 9 | class Viewer : public QGLViewer |
| 10 | { |
| ............................ | |
| 22 | private: |
| 23 | dWorldID _world; |
| 24 | dSpaceID _space; |
| ............................ | |
| 26 | QVector<Object*> _objects; |
| 27 | QVector<dGeomID> _planes; |
| 28 | dReal _aabb[6]; |
| 29 | QTime _time; |
| 30 | }; |
| ............................ | |
Since the use can toggle the animation (by pressing the Return key), we need to "start" this time in the QGLViewe::startAnimation() function. Then, at the end of QGLViewer::animate(), we restart it, so that next time we are in animate(), the value _time.elapsed() is what we want.
| 129 | void Viewer::startAnimation() |
| 130 | { |
| 131 | _time.start(); |
| 132 | QGLViewer::startAnimation(); |
| 133 | } |
| 134 | void Viewer::animate() |
| 135 | { |
| 136 | static float nbSecondsByStep = 0.001f; |
| 137 | |
| 138 | // Find the time elapsed between last time |
| 139 | float nbSecsElapsed = _time.elapsed()/1000.0f; |
| 140 | // Find the corresponding number of steps that must be taken |
| 141 | int nbStepsToPerform = static_cast<int>(nbSecsElapsed/nbSecondsByStep); |
| 142 | // Make these steps to advance world time |
| 143 | for (int i=0;i<nbStepsToPerform;++i) |
| 144 | { |
| ............................ | |
| 147 | // Step world |
| 148 | dWorldQuickStep(_world, nbSecondsByStep); |
| 149 | // Remove all temporary collision joints now that the world has been stepped |
| 150 | dJointGroupEmpty(_contactgroup); |
| 151 | } |
| 152 | // Restart the elapsed time counter |
| 153 | _time.restart(); |
| 154 | } |
| ............................ | |
Collision handling
The last thing we need to do is to handle collision. The way ODE does it (in a nutshell, collision detection can be managed manually completely) is by calling dSpaceCollide. This function finds all collision between geoms of the space, and invoke a callback function on each pair of colliding geoms. Callbacks must be "global" functions. But the callback will need to access world and space, which are private members of the viewer. Luckily, the callback accept a void * data pointer, so we can pass a pointer to the viewer. We add the following public method to the viewer.
| 9 | class Viewer : public QGLViewer |
| 10 | { |
| 11 | Q_OBJECT; |
| 12 | public: |
| ............................ | |
| 16 | void handleCollisionBetween(dGeomID o0, dGeomID o1); |
| ............................ | |
| 30 | }; |
| ............................ | |
Then, we define the callback as a global function local to the viewer.cpp file.
| 9 | namespace |
| 10 | { |
| ............................ | |
| 39 | void nearCallback(void *data, dGeomID o0, dGeomID o1) |
| 40 | { |
| 41 | reinterpret_cast<Viewer*>(data)->handleCollisionBetween(o0,o1); |
| 42 | } |
| 43 | } |
| ............................ | |
Now we can invoke the collision detection prior to doing small step in the world simulation.
| 134 | void Viewer::animate() |
| 135 | { |
| 136 | static float nbSecondsByStep = 0.001f; |
| 137 | |
| 138 | // Find the time elapsed between last time |
| 139 | float nbSecsElapsed = _time.elapsed()/1000.0f; |
| 140 | // Find the corresponding number of steps that must be taken |
| 141 | int nbStepsToPerform = static_cast<int>(nbSecsElapsed/nbSecondsByStep); |
| 142 | // Make these steps to advance world time |
| 143 | for (int i=0;i<nbStepsToPerform;++i) |
| 144 | { |
| 145 | // Detect collision |
| 146 | dSpaceCollide(_space,this,&nearCallback); |
| 147 | // Step world |
| 148 | dWorldQuickStep(_world, nbSecondsByStep); |
| ............................ | |
| 151 | } |
| 152 | // Restart the elapsed time counter |
| 153 | _time.restart(); |
| 154 | } |
| ............................ | |
The next thing is to create a joint group to hold temporary joints. Remember that we said that ODE handles collision by creating temporary, local joints that "repulses" colliding objects. We add a private member to the viewer:
| 9 | class Viewer : public QGLViewer |
| 10 | { |
| ............................ | |
| 22 | private: |
| 23 | dWorldID _world; |
| 24 | dSpaceID _space; |
| ............................ | |
| 26 | QVector<Object*> _objects; |
| 27 | QVector<dGeomID> _planes; |
| 28 | dReal _aabb[6]; |
| 29 | QTime _time; |
| 30 | }; |
| ............................ | |
We create this member in the constructor.
| 45 | Viewer::Viewer(QWidget *parent) |
| 46 | : QGLViewer(QGLFormat(QGL::SampleBuffers | |
| 47 | QGL::DoubleBuffer | |
| 48 | QGL::DepthBuffer | |
| 49 | QGL::Rgba | |
| 50 | QGL::AlphaChannel | |
| 51 | QGL::StencilBuffer),parent) |
| 52 | { |
| ............................ | |
| 60 | // Create contact group |
| 61 | _contactgroup = dJointGroupCreate(0); |
| ............................ | |
And in the simulation loop, we empty this group after every step.
| 134 | void Viewer::animate() |
| 135 | { |
| 136 | static float nbSecondsByStep = 0.001f; |
| 137 | |
| 138 | // Find the time elapsed between last time |
| 139 | float nbSecsElapsed = _time.elapsed()/1000.0f; |
| 140 | // Find the corresponding number of steps that must be taken |
| 141 | int nbStepsToPerform = static_cast<int>(nbSecsElapsed/nbSecondsByStep); |
| 142 | // Make these steps to advance world time |
| 143 | for (int i=0;i<nbStepsToPerform;++i) |
| 144 | { |
| 145 | // Detect collision |
| 146 | dSpaceCollide(_space,this,&nearCallback); |
| 147 | // Step world |
| 148 | dWorldQuickStep(_world, nbSecondsByStep); |
| 149 | // Remove all temporary collision joints now that the world has been stepped |
| 150 | dJointGroupEmpty(_contactgroup); |
| 151 | } |
| 152 | // Restart the elapsed time counter |
| 153 | _time.restart(); |
| 154 | } |
| ............................ | |
The last thing we have to do is to implement handleCollisionBetween(). We borrow the code and comments of another tutorial on the web.
| ............................ |
Now we set the joint properties of each contact. Going into the full details here would require a tutorial of its own. I'll just say that the members of the dContact structure control the joint behaviour, such as friction, velocity and bounciness. See section 7.3.7 of the ODE manual and have fun experimenting to learn more.
| 161 | for (int i = 0; i < MAX_CONTACTS; i++) |
| 162 | { |
| 163 | contact[i].surface.mode = dContactBounce | dContactSoftCFM; |
| 164 | contact[i].surface.mu = dInfinity; |
| 165 | contact[i].surface.mu2 = 0; |
| 166 | contact[i].surface.bounce = 0.8; |
| 167 | contact[i].surface.bounce_vel = 0.1; |
| 168 | contact[i].surface.soft_cfm = 0.01; |
| 169 | } |
| ............................ | |
Here we do the actual collision test by calling dCollide. It returns the number of actual contact points or zero if there were none. As well as the geom IDs, max number of contacts we also pass the address of a dContactGeom as the fourth parameter. dContactGeom is a substructure of a dContact object so we simply pass the address of the first dContactGeom from our array of dContact objects and then pass the offset to the next dContactGeom as the fifth paramater, which is the size of a dContact structure.
| 170 | if (int numc = dCollide(o0, o1, MAX_CONTACTS, &contact[0].geom, sizeof(dContact))) |
| 171 | { |
| 172 | // Get the dynamics body for each geom |
| 173 | dBodyID b1 = dGeomGetBody(o0); |
| 174 | dBodyID b2 = dGeomGetBody(o1); |
| 175 | // To add each contact point found to our joint group we call dJointCreateContact which is just one of the many |
| 176 | // different joint types available. |
| 177 | for (int i = 0; i < numc; i++) |
| 178 | { |
| 179 | // dJointCreateContact needs to know which world and joint group to work with as well as the dContact |
| 180 | // object itself. It returns a new dJointID which we then use with dJointAttach to finally create the |
| 181 | // temporary contact joint between the two geom bodies. |
| 182 | dJointID c = dJointCreateContact(_world, _contactgroup, contact + i); |
| 183 | dJointAttach(c, b1, b2); |
| 184 | } |
| 185 | } |
| 186 | } |
| ............................ | |
Collision handling
That's it, we can run the program and we will see a single cube falling on the ground.
Going further
Now we have a skeleton to play with. For example, we can now add more cubes and a second plane to our scene.
| 45 | Viewer::Viewer(QWidget *parent) |
| 46 | : QGLViewer(QGLFormat(QGL::SampleBuffers | |
| 47 | QGL::DoubleBuffer | |
| 48 | QGL::DepthBuffer | |
| 49 | QGL::Rgba | |
| 50 | QGL::AlphaChannel | |
| 51 | QGL::StencilBuffer),parent) |
| 52 | { |
| ............................ | |
| 62 | //////////////////////////////////////////////////////////// |
| 63 | // Creating objects |
| 64 | ////////////////////////////////////////////////////////////s |
| 65 | static const int nb = 5; |
| 66 | for (int k=0;k<3;++k) |
| 67 | { |
| 68 | float z = 2.0+0.15*k; |
| 69 | for (int i=-nb;i<=nb;++i) |
| 70 | { |
| 71 | float x = 0.15*i+(k%2)*0.01; |
| 72 | for (int j=-nb;j<=nb;++j) |
| 73 | { |
| 74 | float y = 0.15*j+(k%2)*0.01; |
| 75 | Cube *cube = new Cube(_world,_space,0.1,0.1,0.1,1); |
| 76 | cube->setPosition(x,y,z); |
| 77 | cube->color[0] = qrand()%255; |
| 78 | cube->color[1] = qrand()%255; |
| 79 | cube->color[2] = qrand()%255; |
| 80 | _objects.push_back(cube); |
| 81 | } |
| 82 | } |
| 83 | } |
| 84 | // Create static planes |
| 85 | _planes.push_back(dCreatePlane(_space,0,0,1,0)); |
| 86 | _planes.push_back(dCreatePlane(_space,1,0,1,0.5)); |
| 87 | // Compute the bounding box |
| 88 | getAABB(_objects,_aabb); |
| 89 | } |
| ............................ | |
Running the simulation is a bit slower, but here is the result.
From now, it is probably time to read the ODE manual in more details. I suggest in particular the following sections.