You are on page 1of 7

Irrlicht Engine wiki - Getting Started With Bullet Page 1 of 7

View Edit History Print


Irrlicht Engine Wiki
Irrlicht Manual Getting Started With Bullet
Tutorials by Punong Bisyonaryo
FAQ
Code Snippets
The IRRext Project Introduction
SceneNodes 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
OtherAdd-ons license. It can be easily integrated into Irrlicht to provide a degree of realism in your game or simulation.
RecentChanges
Note: This tutorial is based on the Irrlicht test demo by Jacky_J in this thread
Development
TODO List
Developers Setting Up
Design 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
Wiki for instructions on setting up Irrlicht on your OS and IDE.
Impressum
You need to specify to your IDE's linker the libBulletDynamics, libBulletCollision, and libLinearMath libraries.
Help
WikiSandbox
Finally, in our main.cpp, we include the Irrlicht library, the main Bullet Dynamics library, and also cstdlib
Irrlicht Home Page
Irrlicht Forums #include <irrlicht.h>
#include "btBulletDynamicsCommon.h"
edit SideBar #include <cstdlib>

Search:
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
Go 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

http://www.irrlicht3d.org/wiki/index.php?n=Main.GettingStartedWithBullet 8/12/2017
Irrlicht Engine wiki - Getting Started With Bullet Page 2 of 7

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.

http://www.irrlicht3d.org/wiki/index.php?n=Main.GettingStartedWithBullet 8/12/2017
Irrlicht Engine wiki - Getting Started With Bullet Page 3 of 7

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(GetRandIn
break;
case KEY_KEY_2:
CreateSphere(btVector3(GetRandInt(10) - 5.0f, 7.0f, GetRandInt(10) - 5.0f), GetRandInt(5) / 5.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 ra
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

http://www.irrlicht3d.org/wiki/index.php?n=Main.GettingStartedWithBullet 8/12/2017
Irrlicht Engine wiki - Getting Started With Bullet Page 4 of 7

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.

http://www.irrlicht3d.org/wiki/index.php?n=Main.GettingStartedWithBullet 8/12/2017
Irrlicht Engine wiki - Getting Started With Bullet Page 5 of 7

//***************************************************************
// 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) +
break;
case KEY_KEY_2:
CreateSphere(btVector3(GetRandInt(10) - 5.0f, 7.0f, GetRandInt(10) - 5.0f), GetRandInt(5) / 5.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();

http://www.irrlicht3d.org/wiki/index.php?n=Main.GettingStartedWithBullet 8/12/2017
Irrlicht Engine wiki - Getting Started With Bullet Page 6 of 7

// 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 ra
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

http://www.irrlicht3d.org/wiki/index.php?n=Main.GettingStartedWithBullet 8/12/2017
Irrlicht Engine wiki - Getting Started With Bullet Page 7 of 7

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();
}

Page last modified on September 07, 2011, at 03:49 AM


▲ Top ▲ Search Recent Changes All Recent Changes

http://www.irrlicht3d.org/wiki/index.php?n=Main.GettingStartedWithBullet 8/12/2017

You might also like