Getting Started With Bullet

by Punong Bisyonaryo

Introduction

Bullet Physics is a professional open source collision detection, rigid body and soft body dynamics library. The library is free for commercial use under the Zlib license. It can be easily integrated into Irrlicht to provide a degree of realism in your game or simulation.

Note: This tutorial is based on the Irrlicht test demo by Jacky_J in this thread

Setting Up

Before we get started on the code, we need to make sure our IDE is set up with the Irrlicht header files and linked to the Irrlicht libraries. Check out the tutorials page for instructions on setting up Irrlicht on your OS and IDE.

You need to specify to your IDE's linker the libBulletDynamics, libBulletCollision, and libLinearMath libraries.

Finally, in our main.cpp, we include the Irrlicht library, the main Bullet Dynamics library, and also cstdlib

#include <irrlicht.h>
#include "btBulletDynamicsCommon.h"
#include <cstdlib>

We set the namespace for our Irrlicht library. You can also set the namespace for its 5 subnamespaces, but for this tutorial they are not set to show which stuff belongs to which namespace.

using namespace irr;

We then declare our functions and global variables

// Functions
//For now, there's not much here. Just add them in as we create them
static int GetRandInt(int TMax) { return rand() % TMax; }

// Globals
//For now, there's not much here. Just add them in as we create them
static IrrlichtDevice *irrDevice;
static video::IVideoDriver *irrDriver;
static scene::ISceneManager *irrScene;
static gui::IGUIEnvironment* irrGUI;
static io::IFileSystem *irrFile;
static ITimer *irrTimer;
static ILogger *irrLog;

Create a Basic Irrlicht Shell

We start off with our basic Irrlicht structure by getting the driver, adding a camera and running the Irrlicht engine

int main()
{
	irrDevice = createDevice(video::EDT_OPENGL, core::dimension2d<u32>(640, 480), 32, false, false, false, 0);
	if (irrDevice == 0)
		return 1; // could not create selected driver.
	irrGUI = irrDevice->getGUIEnvironment();
	irrTimer = irrDevice->getTimer();
	irrDriver = irrDevice->getVideoDriver();
	irrScene = irrDevice->getSceneManager();

	irrDevice->setWindowCaption(L"Irrlicht with Bullet Physics Demo");

	//Add an FPS camera move it up and back and point it at the origin
	scene::ICameraSceneNode *Camera = irrScene->addCameraSceneNodeFPS(0, 100, 10);
	Camera->setPosition(core::vector3df(0, 5, -5));
	Camera->setTarget(core::vector3df(0, 0, 0));

	//Make the mouse cursor invisible
	irrDevice->getCursorControl()->setVisible(false);

	while(irrDevice->run())
	{
		if (irrDevice->isWindowActive())
		{
			irrDriver->beginScene(true, true, video::SColor(255,200,200,200));
			irrScene->drawAll();
			irrDriver->endScene();
		}
		else
			irrDevice->yield();
	}

	//Delete the Irrlicht device
	irrDevice->drop();
	return 0;
}

Adding Bullet

At this point, there are no objects in our world yet so you won't be able to see anything. Let's start adding in some rigid bodies so we can finally start seeing some Bullet action.

First we initialize Bullet. Before we can create btDiscreteDynamicsWorld, we must choose a broadphase algorithm to use, a collision configuration, and a constraint solver. To learn more about these, check out the Hello World Tutorial at the Bullet Wiki.

Add the following global variables. The first variable is our "physical world", and the second one will hold all of our objects in it.

static btDiscreteDynamicsWorld *World;
static core::list<btRigidBody *> Objects;

Now place the following code after you declare the irrGui, irrDriver, irrScene, etc.

	// Initialize bullet
	btBroadphaseInterface *BroadPhase = new btAxisSweep3(btVector3(-1000, -1000, -1000), btVector3(1000, 1000, 1000));
	btDefaultCollisionConfiguration *CollisionConfiguration = new btDefaultCollisionConfiguration();
	btCollisionDispatcher *Dispatcher = new btCollisionDispatcher(CollisionConfiguration);
	btSequentialImpulseConstraintSolver *Solver = new btSequentialImpulseConstraintSolver();
	World = new btDiscreteDynamicsWorld(Dispatcher, BroadPhase, Solver, CollisionConfiguration);

Creating Objects

Now we're going to create two functions: 1 for creating a box shape and another for creating a sphere. Both will be rigid bodies so they could interact using Bullet For each of the two functions, we'll start of by creating an object in Irrlicht (that's what gets rendered), and then we set the position of the object in a way for Bullet to figure out where it's at. We'll then give it a MotionState and describe to Bullet what that object is shaped like, give it some mass, and with all of those parameters, Bullet can figure out how to create that rigid body in space.

// Create a box rigid body
void CreateBox(const btVector3 &TPosition, const core::vector3df &TScale, btScalar TMass) {

	// Create an Irrlicht cube
	scene::ISceneNode *Node = irrScene->addCubeSceneNode(1.0f);
	Node->setScale(TScale);
	Node->setMaterialFlag(video::EMF_LIGHTING, 1);
	Node->setMaterialFlag(video::EMF_NORMALIZE_NORMALS, true);
	Node->setMaterialTexture(0, irrDriver->getTexture("rust0.jpg"));

	// Set the initial position of the object
	btTransform Transform;
	Transform.setIdentity();
	Transform.setOrigin(TPosition);

	// Give it a default MotionState
	btDefaultMotionState *MotionState = new btDefaultMotionState(Transform);

	// Create the shape
	btVector3 HalfExtents(TScale.X * 0.5f, TScale.Y * 0.5f, TScale.Z * 0.5f);
	btCollisionShape *Shape = new btBoxShape(HalfExtents);

	// Add mass
	btVector3 LocalInertia;
	Shape->calculateLocalInertia(TMass, LocalInertia);

	// Create the rigid body object
	btRigidBody *RigidBody = new btRigidBody(TMass, MotionState, Shape, LocalInertia);

	// Store a pointer to the irrlicht node so we can update it later
	RigidBody->setUserPointer((void *)(Node));

	// Add it to the world
	World->addRigidBody(RigidBody);
	Objects.push_back(RigidBody);
}

// Create a sphere rigid body
void CreateSphere(const btVector3 &TPosition, btScalar TRadius, btScalar TMass) {

	// Create an Irrlicht sphere
	scene::ISceneNode *Node = irrScene->addSphereSceneNode(TRadius, 32);
	Node->setMaterialFlag(video::EMF_LIGHTING, 1);
	Node->setMaterialFlag(video::EMF_NORMALIZE_NORMALS, true);
	Node->setMaterialTexture(0, irrDriver->getTexture("ice0.jpg"));

	// Set the initial position of the object
	btTransform Transform;
	Transform.setIdentity();
	Transform.setOrigin(TPosition);

	// Give it a default MotionState
	btDefaultMotionState *MotionState = new btDefaultMotionState(Transform);

	// Create the shape
	btCollisionShape *Shape = new btSphereShape(TRadius);

	// Add mass
	btVector3 LocalInertia;
	Shape->calculateLocalInertia(TMass, LocalInertia);

	// Create the rigid body object
	btRigidBody *RigidBody = new btRigidBody(TMass, MotionState, Shape, LocalInertia);

	// Store a pointer to the irrlicht node so we can update it later
	RigidBody->setUserPointer((void *)(Node));

	// Add it to the world
	World->addRigidBody(RigidBody);
	Objects.push_back(RigidBody);
}

You can grab the texture files from Jacky_J's post, or you could supply your own textures.

We're also going to need to create a function that creates the "ground" which our objects can interact with. The ground will basically be a large box we'll create with our createBox function

// Creates a base box
void CreateStartScene() {

	ClearObjects();
	CreateBox(btVector3(0.0f, 0.0f, 0.0f), core::vector3df(10.0f, 0.5f, 10.0f), 0.0f);
}

Don't forget to add their function declarations at the top of your code

static void CreateStartScene();
static void CreateBox(const btVector3 &TPosition, const core::vector3df &TScale, btScalar TMass);
static void CreateSphere(const btVector3 &TPosition, btScalar TRadius, btScalar TMass);

Listening in For Events

Now that we've created the functions for adding objects in our world, we need to assign them to map them to some keyboard keys. We do this through events and an event receiver. We need to create an event receiver class so that we could put in some cool interactions with our Irrlicht/Bullet engine. We extend the IEventReceiver class and overload the OnEvent method. There are three key events that we're gonna handle: the Escape, 1, 2, and X keys.

// Event receiver
class EventReceiverClass : public IEventReceiver  {

public:

	virtual bool OnEvent(const SEvent &TEvent) {

		if(TEvent.EventType == EET_KEY_INPUT_EVENT && !TEvent.KeyInput.PressedDown) {
			switch(TEvent.KeyInput.Key) {
				case KEY_ESCAPE:
					Done = true;
				break;
				case KEY_KEY_1:
					CreateBox(btVector3(GetRandInt(10) - 5.0f, 7.0f, GetRandInt(10) - 5.0f), core::vector3df(GetRandInt(3) + 0.5f, GetRandInt(3) + 0.5f, GetRandInt(3) + 0.5f), 1.0f);
				break;
				case KEY_KEY_2:
					CreateSphere(btVector3(GetRandInt(10) - 5.0f, 7.0f, GetRandInt(10) - 5.0f), GetRandInt(5) / 5.0f + 0.2f, 1.0f);
				break;
				case KEY_KEY_X:
					CreateStartScene();
				break;
				default:
					return false;
				break;
			}

			return true;
		}

		return false;
	}
};

For our Irrlicht engine to take note of these events, we have to instantiate this receiver and pass it along to our IrrlichtDevice. We're also going to have to change how we create our IrrlichtDevice. Look for the line where we create our IrrlichtDevice, instantiate our receiver class and pass it in the createDevice function.

	// Create Event Receiver
	EventReceiverClass Receiver;

	irrDevice = createDevice(video::EDT_OPENGL, core::dimension2d<u32>(640, 480), 32, false, false, false, &Receiver);

Notice that when we hit the Escape key, our receiver class sets a variable "Done" to true. We'll declare this variable with our other global variables.

static bool Done = false;

We'll also modify our main loop so that when Done is true, our program will terminate.

	while(irrDevice->run() && Done==false)

Physics Simulation

Now that we have our objects set up, we need to actually start applying the physics calculations like gravity and collisions to our objects. Bullet does this by "stepping" the simulation. A step is basically a discrete period of time and for each step, Bullet calculates how high or how fast an object falls, or how much a collision throws back an object since the last frame.

So what we have to do is before we call on Irrlicht to draw our objects onto the screen, we first have to call on Bullet to give us the updated position of each of our objects. For this, we create a new function called updatePhysics.

// Runs the physics simulation.
// - TDeltaTime tells the simulation how much time has passed since the last frame so the simulation can run independently of the frame rate.
void UpdatePhysics(u32 TDeltaTime) {

	World->stepSimulation(TDeltaTime * 0.001f, 60);

	btRigidBody *TObject;
	// Relay the object's orientation to irrlicht
	for(core::list<btRigidBody *>::Iterator it = Objects.begin(); it != Objects.end(); ++it) {

		//UpdateRender(*Iterator);
		scene::ISceneNode *Node = static_cast<scene::ISceneNode *>((*it)->getUserPointer());
		TObject = *it;

		// Set position
		btVector3 Point = TObject->getCenterOfMassPosition();
		Node->setPosition(core::vector3df((f32)Point[0], (f32)Point[1], (f32)Point[2]));

		// Set rotation
		btVector3 EulerRotation;
		QuaternionToEuler(TObject->getOrientation(), EulerRotation);
		Node->setRotation(core::vector3df(EulerRotation[0], EulerRotation[1], EulerRotation[2]));

	}
}

// Converts a quaternion to an euler angle
void QuaternionToEuler(const btQuaternion &TQuat, btVector3 &TEuler) {
	btScalar W = TQuat.getW();
	btScalar X = TQuat.getX();
	btScalar Y = TQuat.getY();
	btScalar Z = TQuat.getZ();
	float WSquared = W * W;
	float XSquared = X * X;
	float YSquared = Y * Y;
	float ZSquared = Z * Z;

	TEuler.setX(atan2f(2.0f * (Y * Z + X * W), -XSquared - YSquared + ZSquared + WSquared));
	TEuler.setY(asinf(-2.0f * (X * Z - Y * W)));
	TEuler.setZ(atan2f(2.0f * (X * Y + Z * W), XSquared - YSquared - ZSquared + WSquared));
	TEuler *= core::RADTODEG;
}

We then update our program's main loop to calculate the delta (the time since the last screen render), update the physics, and then render everything on screen. Our main loop should now look something like this.

	u32 TimeStamp = irrTimer->getTime(), DeltaTime = 0;
	while(irrDevice->run() && Done==false)
	{
		if (irrDevice->isWindowActive())
		{
            DeltaTime = irrTimer->getTime() - TimeStamp;
            TimeStamp = irrTimer->getTime();

            UpdatePhysics(DeltaTime);

			irrDriver->beginScene(true, true, video::SColor(255,200,200,200));
			irrScene->drawAll();
			irrGUI->drawAll();
			irrDriver->endScene();

			int fps = irrDriver->getFPS();

			if (lastFPS != fps)
			{
				core::stringw str = L"Irrlicht Engine - Quake 3 Map example [";
				str += irrDriver->getName();
				str += "] FPS:";
				str += fps;

				irrDevice->setWindowCaption(str.c_str());
				lastFPS = fps;
			}
		}
		else
			irrDevice->yield();
	}

Finishing Up

You can preload the textures you'll be using by getting the textures beforehand in your main(), before going into your main program loop.

	// Preload textures
	irrDriver->getTexture("ice0.jpg");
	irrDriver->getTexture("rust0.jpg");

As an added touch, we're going to add some GUI text on the upper-left corner of our screen explaining what keys the player can press.

	// Create text
	gui::IGUISkin* Skin = irrGUI->getSkin();
	Skin->setColor(gui::EGDC_BUTTON_TEXT, video::SColor(255, 255, 255, 255));
	irrGUI->addStaticText(L"Hit 1 to create a box\nHit 2 to create a sphere\nHit x to reset", core::rect<s32>(0, 0, 200, 100), false);

And finally, before our main loop we put in some final bits of code to initialize our scene.

	irrScene->addLightSceneNode(0, core::vector3df(2, 5, -2), video::SColorf(4, 4, 4, 1));
	CreateStartScene();

Other Notes

If you find a problem in this tutorial, contact me via punong_bisyonaryo _at_ yahoo_dot_com or at http://www.jplui.com/contact.php

To Compile on Linux

g++ example.cpp -o example -lIrrlicht -lGL -lGLU -lXrandr -lXext -lX11 `pkg-config --libs --cflags bullet`

Updated for recent Irrlicht and Bullet APIs

These are changes to Jacky_J's sample code to use newer versions of Irrlicht and Bullet. Also, we will be using Irrlicht's quaternion class to convert Bullet's quaternion to a rotation in Euler angles.

//***************************************************************
// Bullet/irrlicht demo by Alan Witkowski
// http://www.cs.utah.edu/~witkowsk
// http://code.google.com/p/irrlamb/
//***************************************************************
#include <irrlicht.h>
#include <btBulletCollisionCommon.h>
#include <btBulletDynamicsCommon.h>
#include <cstdlib>

using namespace irr;
using namespace core;
using namespace scene;
using namespace video;
using namespace io;
using namespace gui;

// Functions
static void CreateStartScene();
static void CreateBox(const btVector3 &TPosition, const vector3df &TScale, btScalar TMass);
static void CreateSphere(const btVector3 &TPosition, btScalar TRadius, btScalar TMass);
static void UpdatePhysics(u32 TDeltaTime);
static void UpdateRender(btRigidBody *TObject);
static void ClearObjects();
static int GetRandInt(int TMax) { return rand() % TMax; }

// Globals
static bool Done = false;
static btDiscreteDynamicsWorld *World;
static IrrlichtDevice *irrDevice;
static IVideoDriver *irrDriver;
static ISceneManager *irrScene;
static IGUIEnvironment *irrGUI;
static IFileSystem *irrFile;
static ITimer *irrTimer;
static ILogger *irrLog;
static list<btRigidBody *> Objects;

// Event receiver
class EventReceiverClass : public IEventReceiver  {

public:

	virtual bool OnEvent(const SEvent &TEvent) {

		if(TEvent.EventType == EET_KEY_INPUT_EVENT && !TEvent.KeyInput.PressedDown) {
			switch(TEvent.KeyInput.Key) {
				case KEY_ESCAPE:
					Done = true;
				break;
				case KEY_KEY_1:
					CreateBox(btVector3(GetRandInt(10) - 5.0f, 7.0f, GetRandInt(10) - 5.0f), vector3df(GetRandInt(3) + 0.5f, GetRandInt(3) + 0.5f, GetRandInt(3) + 0.5f), 1.0f);
				break;
				case KEY_KEY_2:
					CreateSphere(btVector3(GetRandInt(10) - 5.0f, 7.0f, GetRandInt(10) - 5.0f), GetRandInt(5) / 5.0f + 0.2f, 1.0f);
				break;
				case KEY_KEY_X:
					CreateStartScene();
				break;
				default:
					return false;
				break;
			}

			return true;
		}

		return false;
	}
};

int main() {

	// Initialize irrlicht
	EventReceiverClass Receiver;
	irrDevice = createDevice(video::EDT_OPENGL, dimension2d<u32>(800, 600), 32, false, false, false, &Receiver);
	irrGUI = irrDevice->getGUIEnvironment();
	irrTimer = irrDevice->getTimer();
	irrScene = irrDevice->getSceneManager();
	irrDriver = irrDevice->getVideoDriver();

	irrDevice->getCursorControl()->setVisible(0);

	// Initialize bullet
	btDefaultCollisionConfiguration *CollisionConfiguration = new btDefaultCollisionConfiguration();
	btBroadphaseInterface *BroadPhase = new btAxisSweep3(btVector3(-1000, -1000, -1000), btVector3(1000, 1000, 1000));
	btCollisionDispatcher *Dispatcher = new btCollisionDispatcher(CollisionConfiguration);
	btSequentialImpulseConstraintSolver *Solver = new btSequentialImpulseConstraintSolver();
	World = new btDiscreteDynamicsWorld(Dispatcher, BroadPhase, Solver, CollisionConfiguration);

	// Add camera
	ICameraSceneNode *Camera = irrScene->addCameraSceneNodeFPS(0, 100, 10);
	Camera->setPosition(vector3df(0, 5, -5));
	Camera->setTarget(vector3df(0, 0, 0));

	// Preload textures
	irrDriver->getTexture("ice0.jpg");
	irrDriver->getTexture("rust0.jpg");

	// Create text
	IGUISkin *Skin = irrGUI->getSkin();
	Skin->setColor(EGDC_BUTTON_TEXT, SColor(255, 255, 255, 255));
	irrGUI->addStaticText(L"Hit 1 to create a box\nHit 2 to create a sphere\nHit x to reset", rect<s32>(0, 0, 200, 100), false);

	// Create the initial scene
	irrScene->addLightSceneNode(0, core::vector3df(2, 5, -2), SColorf(4, 4, 4, 1));
	CreateStartScene();

	// Main loop
	u32 TimeStamp = irrTimer->getTime(), DeltaTime = 0;
	while(!Done) {

		DeltaTime = irrTimer->getTime() - TimeStamp;
		TimeStamp = irrTimer->getTime();

		UpdatePhysics(DeltaTime);

		irrDriver->beginScene(true, true, SColor(255, 20, 0, 0));
		irrScene->drawAll();
		irrGUI->drawAll();
		irrDriver->endScene();
		irrDevice->run();
	}

	ClearObjects();
	delete World;
	delete Solver;
	delete Dispatcher;
	delete BroadPhase;
	delete CollisionConfiguration;

	irrDevice->drop();

	return 0;
}

// Runs the physics simulation.
// - TDeltaTime tells the simulation how much time has passed since the last frame so the simulation can run independently of the frame rate.
void UpdatePhysics(u32 TDeltaTime) {

	World->stepSimulation(TDeltaTime * 0.001f, 60);

	// Relay the object's orientation to irrlicht
	for(list<btRigidBody *>::Iterator Iterator = Objects.begin(); Iterator != Objects.end(); ++Iterator) {

		UpdateRender(*Iterator);
	}	
}

// Creates a base box
void CreateStartScene() {

	ClearObjects();
	CreateBox(btVector3(0.0f, 0.0f, 0.0f), vector3df(10.0f, 0.5f, 10.0f), 0.0f);
}

// Create a box rigid body
void CreateBox(const btVector3 &TPosition, const vector3df &TScale, btScalar TMass) {

	ISceneNode *Node = irrScene->addCubeSceneNode(1.0f);
	Node->setScale(TScale);
	Node->setMaterialFlag(EMF_LIGHTING, 1);
	Node->setMaterialFlag(EMF_NORMALIZE_NORMALS, true);
	Node->setMaterialTexture(0, irrDriver->getTexture("rust0.jpg"));

	// Set the initial position of the object
	btTransform Transform;
	Transform.setIdentity();
	Transform.setOrigin(TPosition);

	btDefaultMotionState *MotionState = new btDefaultMotionState(Transform);

	// Create the shape
	btVector3 HalfExtents(TScale.X * 0.5f, TScale.Y * 0.5f, TScale.Z * 0.5f);
	btCollisionShape *Shape = new btBoxShape(HalfExtents);

	// Add mass
	btVector3 LocalInertia;
	Shape->calculateLocalInertia(TMass, LocalInertia);

	// Create the rigid body object
	btRigidBody *RigidBody = new btRigidBody(TMass, MotionState, Shape, LocalInertia);

	// Store a pointer to the irrlicht node so we can update it later
	RigidBody->setUserPointer((void *)(Node));

	// Add it to the world
	World->addRigidBody(RigidBody);
	Objects.push_back(RigidBody);
}

// Create a sphere rigid body
void CreateSphere(const btVector3 &TPosition, btScalar TRadius, btScalar TMass) {

	ISceneNode *Node = irrScene->addSphereSceneNode(TRadius, 32);
	Node->setMaterialFlag(EMF_LIGHTING, 1);
	Node->setMaterialFlag(EMF_NORMALIZE_NORMALS, true);
	Node->setMaterialTexture(0, irrDriver->getTexture("ice0.jpg"));

	// Set the initial position of the object
	btTransform Transform;
	Transform.setIdentity();
	Transform.setOrigin(TPosition);

	btDefaultMotionState *MotionState = new btDefaultMotionState(Transform);

	// Create the shape
	btCollisionShape *Shape = new btSphereShape(TRadius);

	// Add mass
	btVector3 LocalInertia;
	Shape->calculateLocalInertia(TMass, LocalInertia);

	// Create the rigid body object
	btRigidBody *RigidBody = new btRigidBody(TMass, MotionState, Shape, LocalInertia);

	// Store a pointer to the irrlicht node so we can update it later
	RigidBody->setUserPointer((void *)(Node));

	// Add it to the world
	World->addRigidBody(RigidBody);
	Objects.push_back(RigidBody);
}

// Passes bullet's orientation to irrlicht
void UpdateRender(btRigidBody *TObject) {
	ISceneNode *Node = static_cast<ISceneNode *>(TObject->getUserPointer());

	// Set position
	btVector3 Point = TObject->getCenterOfMassPosition();
	Node->setPosition(vector3df((f32)Point[0], (f32)Point[1], (f32)Point[2]));

	// Set rotation
	vector3df Euler;
	const btQuaternion& TQuat = TObject->getOrientation();
	quaternion q(TQuat.getX(), TQuat.getY(), TQuat.getZ(), TQuat.getW());
	q.toEuler(Euler);
	Euler *= RADTODEG;
	Node->setRotation(Euler);
}

// Removes all objects from the world
void ClearObjects() {

	for(list<btRigidBody *>::Iterator Iterator = Objects.begin(); Iterator != Objects.end(); ++Iterator) {
		btRigidBody *Object = *Iterator;

		// Delete irrlicht node
		ISceneNode *Node = static_cast<ISceneNode *>(Object->getUserPointer());
		Node->remove();

		// Remove the object from the world
		World->removeRigidBody(Object);

		// Free memory
		delete Object->getMotionState();
		delete Object->getCollisionShape();
		delete Object;
	}

	Objects.clear();
}