KODE+Ogre3D Tutorial

From KODE Wiki
Jump to: navigation, search
KODEManual.png
This article is part
of the KODE Manual
Chapters
Introduction
Install and Use
Concepts
Quick Tutorial
API Reference
World
Body
Joints
Geoms
Spaces
Vector3

If Ogre3D is installed on your system, and the build system detected its presence, you should have a set of sample programs built under:

samples/KODEOgre

First make sure you can run the sample programs.

You'll find one named "sample", which is fairly empty; it only shows a simplistic "skybox", with minimal camera interaction. If you check the corresponding sample.cpp source code you'll find this:

#include <stdexcept>
#include <iostream>

#include <kode/kode.hpp>

#include "App.hpp"

using namespace kode;
using namespace kode::ogre;

struct Demo : public App {

    bool paused = false;

    Demo()
    {
        // setup App::world here
        // also, create all your objects
    }


    void step()
    {
        // update the simulation state
        if (!paused) {
            world.step();
        }
    }


    bool keyPressed(const OIS::KeyEvent& evt) override
    {
        if (App::keyPressed(evt))
            return true;

        // Handle keys that you want. For example:
        if (evt.key == OIS::KC_SPACE) {
            paused = !paused;
            return true;
        }

        return false;
    }
};

int main()
{
    try {
        Demo app;
        app.run();
    }
    catch (std::exception& e) {
        std::cout << e.what() << std::endl;
    }
}

This basic skeleton already takes care of basic user interaction and drawing of the scene. Furthermore, the main loop is hidden in the base class, App. We will only be concerned with:

  • adding some member variables to the class,
  • setting up some parameters inside the constructor,
  • handling input for interaction,
  • and customizing the stpe() method to do more advanced things.

Note that we will use convenience classes from the kode::ogre namespace, which will automatically be in charge of displaying objects; unless noted otherwise, they are equivalent to the kode class with the same name.


Worlds and Bodies[edit]

The entire simulation happens in a World object; We don't have to create one explicitly since the App class already has a World member variable, called world. A World keeps track of physical bodies (once they are added to the world), so calling World::step() will simulate all bodies int he world at once. A World also has global parameters, such as gravity and the size of time steps. Let's tweak those from inside the constructor:

struct Demo : public App {

    // ...

    Demo()
    {
        world.setGravity(0, -9.8, 0); // negative since it's pointing down the Y axis
        world.setStepSize(0.01);
    }

     // ,,,
};

Now each time we invoke world.step() we will step the simulation forward by 0.01 units of time. There's no requirement for that to be interpreted as "0.01 seconds", but it helps making sense of all the other units.

Now let's create a physical body and place it "4 units up"; we must also tell the world to keep track of this body, by calling world.add():

struct Demo : public App {

    // ...
    ogre::Body body;

    Demo()
    {
        // ...
        world.add(body);
        body.setPosition(0, 4, 0);
    }

    // ...
};


Here's the full source code:

Ogre-tutorial-body.png
#include <stdexcept>
#include <iostream>

#include <kode/kode.hpp>

#include "KODEOgre.hpp"

using namespace kode;
using namespace kode::ogre;

struct Demo : public App {

    bool paused = false;

    ogre::Body body;

    Demo()
    {
        world.setGravity(0, -9.8, 0); // negative since it's pointing down the Y axis
        world.setStepSize(0.01);

        world.add(body);
        body.setPosition(0, 4, 0);
    }


    void step()
    {
        // update the simulation state
        if (!paused) {
            world.step();
        }
    }


    bool keyPressed(const OIS::KeyEvent& evt) override
    {
        if (App::keyPressed(evt))
            return true;

        // Handle keys that you want. For example:
        if (evt.key == OIS::KC_SPACE) {
            paused = !paused;
            return true;
        }

        return false;
    }
};

int main()
{
    try {
        Demo app;
        app.run();
    }
    catch (std::exception& e) {
        std::cout << e.what() << std::endl;
    }
}

When you run this code you should see a rounded, colored box falling. That's how the utility Body class represents a physical body visually.


Multiple Bodies[edit]

Let's add more bodies to our program. We remove the body member, and create a std::vector of Body. Here are the changes:

#include <vector>
#include <utility> // for std::move

// ...

struct Demo : public App {

    // ...

    std::vector<ogre::Body> bodies;

    Demo()
    {
        // ...

        for (int i=0; i<10; ++i) {
            ogre::Body b{world};
            b.setPosition(-9 + 2*i, i+2, 0);
            bodies.push_back( std::move(b) );
        }
    }

    // ...

};

We are now using a slightly different constructor (highlighted in the code) for the bodies: this one automatically calls World:add(*this), so we don't have to do it ourselves.

One important aspect is that Body is non-copyable, but it's movable. So we can either construct it in-place in the vector (if we wanted to use .emplace_back()), or we call std::move() on it to allow it to be moved into the vector. All high-level classes in KODE are non-copyable but are movable.

Here's the full source code:

Ogre-tutorial-10-bodies.png
#include <stdexcept>
#include <iostream>
#include <vector>
#include <utility> // for std::move

#include <kode/kode.hpp>

#include "KODEOgre.hpp"

using namespace kode;
using namespace kode::ogre;

struct Demo : public App {

    bool paused = false;

    std::vector<ogre::Body> bodies;

    Demo()
    {
        world.setGravity(0, -9.8, 0); // negative since it's pointing down the Y axis
        world.setStepSize(0.01);

        for (int i=0; i<10; ++i) {
            ogre::Body b{world};
            b.setPosition(-9 + 2*i, i+2, 0);
            bodies.push_back( std::move(b) );
        }
    }


    void step()
    {
        // update the simulation state
        if (!paused) {
            world.step();
        }
    }


    bool keyPressed(const OIS::KeyEvent& evt) override
    {
        if (App::keyPressed(evt))
            return true;

        // Handle keys that you want. For example:
        if (evt.key == OIS::KC_SPACE) {
            paused = !paused;
            return true;
        }

        return false;
    }
};

int main()
{
    try {
        Demo app;
        app.run();
    }
    catch (std::exception& e) {
        std::cout << e.what() << std::endl;
    }
}


Collisions[edit]

In KODE a physical body has no shape. Shapes are a distinct concept, we call them Geoms. A Geom can be used alone, or it can be attached to a body. Let's add a plane geom to represent the ground; we won't use a body for the ground, we want it to be immovable, so we add the member variable:

class Demo : public App {
    // ...

    ogre::Plane ground = {0, 1, 0, 2};

    // ...
};

A Plane geom is a half-space; everything below the plane is considered "inside", for collision purposes. Here we are constructing it from the coefficients a, b, c, d from the general plane equation, a x + b y + c z + d = 0; the normal (a,b,c) = (0,1,0) is pointing up, and d = 2 so the plane is 2 units below zero. We could construct the same plane with the point-and-normal constructor:

    ogre::Plane ground = {{0, -2, 0}, {0, 1, 0}};


Let's now attach spheres to the bodies, so they can collide with the ground plane. We will store the spheres in another vector, and append to it whenever we append a new body:

class Demo : public App {

    // ...

    std::vector<Sphere> spheres;

    // ...

    Demo()
    {
        // ...

        for (int i=0; i<10; ++i) {
            ogre::Body b{world};
            ogre::Sphere s{world, 0.3}; // radius = 0.3

            b.attach(s);
            b.setPosition(-9 + 2*i, i+2, 0);

            b.getNode()->setVisible(false); // hide the Body mesh

            bodies.push_back( std::move(b) );
            spheres.push_back( std::move(s) );
        }
    }

    // ...
};

We attach the sphere to the body first, and only after that we move the body to its initial position; this will ensure that the sphere is moved to the same place. A geom can have any arbitrary offset in relation with a body; if the body is at position (3,4,5), and the geom is at (5,7,9), the offset vector (2,3,4) is stored, so the difference is maintained the same. The offset vector is recomputed whenever the geom is attached to the body, or setPosition() is called on the geom. The offset can be set directly by calling setOffsetPosition() (in which case the position is the one that is recomputed).

The OGRE samples have a more convenient way to create a body with a single geom attached, to save some typing: the BodyFoo classes construct a Body and a Foo geom, and attach them. The code can instead be written as:

class Demo : public App {

    // ...

    std::vector<BodySphere> balls;

    // ...

    Demo()
    {
        // ...

        for (int i=0; i<10; ++i) {
            ogre::BodySphere b{world, 0.3}; // radius = 0.3
            b.setPosition(-9 + 2*i, i+2, 0);
            balls.push_back( std::move(b) );
        }
    }

    // ...
};


The full source code is now:

#include <stdexcept>
#include <iostream>
#include <vector>
#include <utility> // for std::move

#include <kode/kode.hpp>

#include "KODEOgre.hpp"

using namespace kode;
using namespace kode::ogre;

struct Demo : public App {

    bool paused = false;

    std::vector<ogre::BodySphere> balls;

    ogre::Plane ground = {0, 1, 0, 2};

    Demo()
    {
        world.setGravity(0, -9.8, 0); // negative since it's pointing down the Y axis
        world.setStepSize(0.01);

        for (int i=0; i<10; ++i) {
            ogre::BodySphere b{world, 0.3}; // radius = 0.3
            b.setPosition(-9 + 2*i, i+2, 0);
            balls.push_back( std::move(b) );
        }
    }


    void step()
    {
        // update the simulation state
        if (!paused) {
            world.step();
        }
    }


    bool keyPressed(const OIS::KeyEvent& evt) override
    {
        if (App::keyPressed(evt))
            return true;

        // Handle keys that you want. For example:
        if (evt.key == OIS::KC_SPACE) {
            paused = !paused;
            return true;
        }

        return false;
    }
};

int main()
{
    try {
        Demo app;
        app.run();
    }
    catch (std::exception& e) {
        std::cout << e.what() << std::endl;
    }
}

If you run this code you'll see spheres falling through the ground; that's because we are not really checking for collisions. KODE has the Collider class with the collide() method, which calls the appropriate collision detection routine depending on the type of geoms. Here's how we could use it:

class Demo : public App {

    // ...

    Collider collider;

    // ...

    void step()
    {
        // update the simulation state
        if (!paused) {
 
            // check collisions against the ground
            for (ogre::Sphere& s : balls) {
                std::vector<ContactPoint> points;
                collider.collide(ground, s, points);
                for (ContactPoint& p : points) {
                    // p is a contact point between sphere s and the ground
                    // let's do something with it later
                }
            }
            // check collisions between the spheres
            for (unsigned i=0; i<balls.size(); ++i) {
                for (unsigned j=i+1; j<balls.size(); ++j) {
                    // do the same thing as above
                    std::vector<ContactPoint> points;
                    collider.collide(balls[i], balls[j], points);
                    for (ContactPoint& p : points) {
                        // p is a contact point between two spheres
                    }
                }
            }

            world.step();
        }
    }

    // ...
};

It's very verbose to write out the loops explicitly to check each pair of geoms. It also might not be very fast. Instead, we'll use the Space classes to do the same task.


Spaces[edit]

just like a World keeps track of bodies to simulate, a Space keeps track of geoms to check for collisions. A Space can be specialized to employ spatial data structures to find collisions more quickly than checking all pairs. Here's how we would incorporate the simplest Space implementation, SimpleSpace:

struct Demo : public App {

    // ...

    SimpleSpace space;

    Demo()
    {
        // ...

        space.add(ground);

        for (int i=0; i<10; ++i) {
            ogre::BodySphere b{0.3}; // radius = 0.3
            b.setPosition(-9 + 2*i, i+2, 0);
            space.add(b);
            balls.push_back( std::move(b) );
        }
    }

    // ...
};

Checking for potential collisions now is just a matter of calling the space's findPairs(). Spaces don't really check for the actual shapes, they only care about the axis-aligned bounding boxes (AABBs); so the space might find two AABBs that are overlapping, but the actual geoms might be separated. Here's how we use findPairs():

struct Demo : public App {

    // ...

    void step()
    {
        // update the simulation state
        if (!paused) {
            space.findPairs([&] (Geom& a, Geom& b)
                            {
                                // geoms a and b are probably colliding
                                std::vector<ContactPoint> points;
                                collider.collide(a, b, points);
                                for (ContactPoint& p : points) {
                                    // let's do something with it later
                                }
                            });
            world.step();
        }
    }

    // ...
};

The argument for findPairs() is any callable object: a function, a functor, or in this case, a lambda, that can take two geoms by reference. The final step is to do something about the contact points that are found, so they actually affect the simulation.


Contacts[edit]

A Contact is something that makes bodies behave as if they were in contact at a given point. Because bodies usually move around, we don't want to keep contacts alive for very long. We create contacts when we know bodies are colliding, then we advanced the simulation (that now knows the contacts exist), then immediately destroy the contacts. One easy way to do this is to keep the contacts in a vector, that's local to the step() function; at the end of the function, all contacts will be destroyed:

struct Demo : public App {

    // ...

    void step()
    {
        std::vector<Contact> contacts;
        // update the simulation state
        if (!paused) {
            space.findPairs([&] (Geom& a, Geom& b)
                            {
                                // geoms a and b are probably colliding
                                std::vector<ContactPoint> points;
                                collider.collide(a, b, points);
                                for (ContactPoint& p : points) {
                                    Contact con{a.getBody(), b.getBody(), p};
                                    contacts.push_back( std::move(con) );
                                }
                            });
            world.step();
        }
    }

    // ...

};

Note that a contact happens between bodies (we call a.getBody(), b.getBody()); the physics code doesn't know or care about geoms, it needs to know something is affecting the movement of bodies. The Contact class has many methods for tweaking how the collision behaves:

                                Contact con{a.getBody(), b.getBody(), p};
                                con.setBounciness(0.7); // absorbs 30 % of the impact
                                con.setMu(5); // friction coefficient
                                contacts.push_back( move(con) );


Finally, here's the full source code of spheres that bounce:

#include <stdexcept>
#include <iostream>
#include <vector>
#include <utility> // for std::move

#include <kode/kode.hpp>

#include "KODEOgre.hpp"

using namespace kode;
using namespace kode::ogre;

struct Demo : public App {

    bool paused = false;

    std::vector<ogre::BodySphere> balls;

    ogre::Plane ground = {0, 1, 0, 2};

    Collider collider;

    SimpleSpace space;

    Demo()
    {
        world.setGravity(0, -9.8, 0); // negative since it's pointing down the Y axis
        world.setStepSize(0.01);

        space.add(ground);

        for (int i=0; i<10; ++i) {
            ogre::BodySphere b{world, 0.3}; // radius = 0.3
            b.setPosition(-9 + 2*i, i+2, 0);
            space.add(b);
            balls.push_back( std::move(b) );
        }
    }


    void step()
    {
        std::vector<Contact> contacts;
        // update the simulation state
        if (!paused) {
            space.findPairs([&] (Geom& a, Geom& b)
                            {
                                // geoms a and b are probably colliding
                                std::vector<ContactPoint> points;
                                collider.collide(a, b, points);
                                for (ContactPoint& p : points) {
                                    Contact con{a.getBody(), b.getBody(), p};
                                    con.setBounciness(0.7); // absorbs 30 % of the impact
                                    con.setMu(5); // friction coefficient
                                    contacts.push_back( std::move(con) );
                                }
                            });
            world.step();
        }
    }


    bool keyPressed(const OIS::KeyEvent& evt) override
    {
        if (App::keyPressed(evt))
            return true;

        // Handle keys that you want. For example:
        if (evt.key == OIS::KC_SPACE) {
            paused = !paused;
            return true;
        }

        return false;
    }
};

int main()
{
    try {
        Demo app;
        app.run();
    }
    catch (std::exception& e) {
        std::cout << e.what() << std::endl;
    }
}


Joints[edit]

Joints are entities that constrain the relative motion of bodies. A Contact is a Joint; a Hinge is a Joint; a BallSocket is a Joint.

Joints can be attached between pairs of bodies, or between a body and the "world". In the example from the previous section, sometimes one of the geoms supplied by findPairs() will be the ground, which is not attached to any body, so the .getBody() method will return a null pointer; the joints interpret a "null body" as being the immovable world.

This is an adaptation from the Concepts page, showing a simple pendulum:

Ogre-tutorial-pendulum.png
#include <stdexcept>
#include <iostream>
 
#include <kode/kode.hpp>
 
#include "KODEOgre.hpp"
 
using namespace kode;
using namespace kode::ogre;
 
struct Demo : public App {
 
    bool paused = false;

    ogre::Body b;
    ogre::Hinge h;
 
    Demo()
    {
        world.setGravity(0, -1, 0);
        world.setStepSize(0.02);

        b.setPosition(1, 1, 0);
        world.add(b);

        h.attach(b);
        h.setAnchor(0, 1, 0);
        h.setAxis(0, 0, 1);
    }
 
 
    void step()
    {
        // update the simulation state
        if (!paused) {
            world.step();
        }
    }
 
 
    bool keyPressed(const OIS::KeyEvent& evt) override
    {
        if (App::keyPressed(evt))
            return true;
 
        // Handle keys that you want. For example:
        if (evt.key == OIS::KC_SPACE) {
            paused = !paused;
            return true;
        }
 
        return false;
    }
};
 
int main()
{
    try {
        Demo app;
        app.run();
    }
    catch (std::exception& e) {
        std::cout << e.what() << std::endl;
    }
}

In general, joints require setting up various parameters before they can do something useful (setting up anchors, axes, etc). They require bodies to already attached; if bodies are attached afterwards, the parameters will not work properly.