As a sample for integrating a third party library that is not built into Panda3D into a game, this recipe dives into the C++ side of Panda3D to create a scene that is driven by the excellent and free Bullet physics engine.
This recipe builds on the project setup described in Creating a scene using C++ found in Chapter 2. Follow the steps of this recipe before proceeding!
You also need a copy of the Bullet source code. The latest version can be retrieved from this website: http://code.google.com/p/bullet/downloads/list.
Integrating the Bullet physics engine into a Panda3D program involves these tasks:
Lib
. Lib
directory so that the top-level directory, containing the file AUTHORS
and the directory msvc
, of Bullet is Libullet-2.77
. msvc2008
subdirectory of the Bullet source tree and open the file BULLET_PHYSICS.sln
. PandaSettings.vsprops
file and replace its content with the following configuration data:<?xml version="1.0" encoding="Windows-1252"?> <VisualStudioPropertySheet ProjectType="Visual C++" Version="8.00" Name="PandaSettings" > <Tool Name="VCCLCompilerTool" AdditionalIncludeDirectories=""..Libullet-2.77src";"C:Panda3D-1.7.0pythoninclude";"C:Panda3D-1.7.0include"" /> <Tool Name="VCLinkerTool" AdditionalDependencies="BulletDynamics.lib BulletCollision.lib LinearMath.lib libp3framework.lib libpanda.lib libpandafx.lib libpandaexpress.lib libp3dtool.lib libp3dtoolconfig.lib libp3pystub.lib libp3direct.lib" AdditionalLibraryDirectories=""..Libullet-2.77msvc2008libRelease";"C:Panda3D-1.7.0pythonlibs";"C:Panda3D-1.7.0lib"" /> </VisualStudioPropertySheet>
main.cpp
and add the main
function:#include "Application.h" PandaFramework framework; int main(int argc, char* argv[]) { Application app(argc, argv); app.run(); return 0; }
Application.h
and add the declaration of the Application
and BulletTask
classes:#pragma once #include <pandaFramework.h> #include <pandaSystem.h> #include <asyncTask.h> #include <btBulletDynamicsCommon.h> class Application { public: Application(int argc, char* argv[]); ~Application(); void run(); private: void init(); void setupBullet(); void addGround(); void updateBullet(); private: NodePath render; NodePath cam; NodePath smiley; WindowFramework* win; PandaFramework framework; btBroadphaseInterface* broadphase; btCollisionDispatcher* dispatcher; btConstraintSolver* solver; btDefaultCollisionConfiguration* collisionConfiguration; btDynamicsWorld* btWorld; }; class BulletTask { public: static AsyncTask::DoneStatus updateBullet(GenericAsyncTask* task, void* data); };
SmileyMotionState.h
. Insert the following code for declaring the SmileyMotionState
class:#pragma once #include <pandaFramework.h> #include <pandaSystem.h> #include <btBulletDynamicsCommon.h> class SmileyMotionState : public btMotionState { public: SmileyMotionState(const btTransform& start, const NodePath& sm); virtual ~SmileyMotionState() {} virtual void getWorldTransform(btTransform& trans) const; virtual void setWorldTransform(const btTransform& trans); protected: btTransform transform; NodePath smiley; };
SmileyTask.h
and add another class declaration:#pragma once #include <pandaFramework.h> #include <pandaSystem.h> #include <asyncTask.h> #include <randomizer.h> #include <btBulletDynamicsCommon.h> class SmileyTask { public: SmileyTask(NodePath& rndr, NodePath& sm, btDynamicsWorld* world); static AsyncTask::DoneStatus addSmiley(GenericAsyncTask* task, void* data); NodePath render; NodePath smiley; btDynamicsWorld* btWorld; int smileyCount; };
SmileyTask.cpp
and populate it with the following code:#include "SmileyTask.h" #include "SmileyMotionState.h" SmileyTask::SmileyTask(NodePath& rndr, NodePath& sm, btDynamicsWorld* world) { smileyCount = 0; smiley = sm; render = rndr; btWorld = world; } AsyncTask::DoneStatus SmileyTask::addSmiley(GenericAsyncTask* task, void* data) { SmileyTask* add = reinterpret_cast<SmileyTask*>(data); NodePath render = add->render; NodePath smiley = add->smiley; btDynamicsWorld* btWorld = add->btWorld; NodePath sm = render.attach_new_node("smiley-instance"); Randomizer rnd; smiley.instance_to(sm); btCollisionShape* shape = new btSphereShape(btScalar(1)); btTransform trans; trans.setIdentity(); trans.setOrigin(btVector3(rnd.random_real(40) - 20, rnd.random_real(20) + 10, rnd.random_real(60) - 30)); btScalar mass(10); btVector3 inertia(0, 0, 0); shape->calculateLocalInertia(mass, inertia); SmileyMotionState* ms = new SmileyMotionState(trans, sm); btRigidBody::btRigidBodyConstructionInfo info(mass, ms, shape, inertia); info.m_restitution = btScalar(0.5f); info.m_friction = btScalar(0.7f); btRigidBody* body = new btRigidBody(info); btWorld->addRigidBody(body); add->smileyCount++; if (add->smileyCount == 100) return AsyncTask::DS_done; return AsyncTask::DS_again; }
SmileyMotionState.cpp
and fill it with this following snippet:#include "SmileyMotionState.h" SmileyMotionState::SmileyMotionState(const btTransform& start, const NodePath& sm) { transform = start; smiley = sm; } void SmileyMotionState::getWorldTransform(btTransform& trans) const { trans = transform; } void SmileyMotionState::setWorldTransform(const btTransform& trans) { transform = trans; btQuaternion rot = trans.getRotation(); LQuaternionf prot(rot.w(), -rot.x(), -rot.z(), -rot.y()); smiley.set_hpr(prot.get_hpr()); btVector3 pos = trans.getOrigin(); smiley.set_pos(pos.x(), pos.z(), pos.y()); }
Application.cpp
and add the code below:#include <cardMaker.h> #include "Application.h" #include "SmileyTask.h" Application::Application(int argc, char* argv[]) { framework.open_framework(argc, argv); win = framework.open_window(); cam = win->get_camera_group(); render = win->get_render(); } Application::~Application() { } void Application::run() { init(); framework.main_loop(); framework.close_framework(); } void Application::init() { setupBullet(); Bullet physics engineintegratingPT(AsyncTaskManager) taskMgr = AsyncTaskManager::get_global_ptr(); smiley = win->load_model(framework.get_models(), "frowney"); SmileyTask* add = new SmileyTask(render, smiley, btWorld); PT(GenericAsyncTask) addSmiley = new GenericAsyncTask("AddSmiley", &SmileyTask::addSmiley, add); addSmiley->set_delay(0.01); taskMgr->add(addSmiley); PT(GenericAsyncTask) bt = new GenericAsyncTask("UpdateBullet", &BulletTask::updateBullet, btWorld); taskMgr->add(bt); addGround(); cam.set_pos(0, -100, 10); } void Application::setupBullet() { collisionConfiguration = new btDefaultCollisionConfiguration(); dispatcher = new btCollisionDispatcher(collisionConfiguration); broadphase = new btDbvtBroadphase(); btSequentialImpulseConstraintSolver* sol = new btSequentialImpulseConstraintSolver; solver = sol; btWorld = new btDiscreteDynamicsWorld(dispatcher, broadphase, solver, collisionConfiguration); btWorld->setGravity(btVector3(0, -9.81f, 0)); } void Application::addGround() { CardMaker cm("ground"); cm.set_frame(-500, 500, -500, 500); NodePath ground = render.attach_new_node(cm.generate()); ground.look_at(0, 0, -1); ground.set_color(0.2f, 0.6f, 0.2f); btCollisionShape* shape = new btBoxShape(btVector3(btScalar(500), btScalar(0.5f), btScalar(500))); Bullet physics engineintegratingbtTransform trans; trans.setIdentity(); trans.setOrigin(btVector3(0, -0.5f, 0)); btDefaultMotionState* ms = new btDefaultMotionState(trans); btScalar mass(0); btVector3 inertia(0, 0, 0); btRigidBody::btRigidBodyConstructionInfo info(mass, ms, shape, inertia); info.m_restitution = btScalar(0.5f); info.m_friction = btScalar(0.7f); btRigidBody* body = new btRigidBody(info); btWorld->addRigidBody(body); } AsyncTask::DoneStatus BulletTask::updateBullet(GenericAsyncTask* task, void* data) { btScalar dt(ClockObject::get_global_clock()->get_dt()); btDynamicsWorld* btWorld = reinterpret_cast<btDynamicsWorld*>(data); btWorld->stepSimulation(dt); return AsyncTask::DS_cont; }
First, we need to set up our environment: We unpack Bullet to the right location and compile it. We also modify the property file to add the search paths for Bullet's libraries and header files.
After we completely rewrite main.cpp
, we go on to define the interfaces of the classes for our little sample program. The Application
class, just like in the Python code samples, keeps everything together and controls the application behavior. BulletTask
only contains the static method that is used to perform simulation steps. A SmileyMotionState
is created for every new smiley created throughout the program's runtime. Motion state objects are a convenient way for Bullet to keep object transformations up-to-date as they provide a per-object callback mechanism that allows Bullet to inject new and updated data. Lastly, we define the interface of SmileyTask
, which is responsible for creating new smileys.
The next thing we add is the implementation of SmileyTask
. In the static member function, we first retrieve the necessary objects from the context data we pass into the function using the data
parameter.
Additionally, we can observe how Bullet's interface for adding a new object to the simulation works. We need to combine a shape, mass, inertia, and motion state into a btRigidBodyConstructionInfo
object that we can also use to set the object's material parameters for bounciness and friction. This info is then used to construct the new object, which is then added to our physics world.
In the implementation of SmileyMotionState
, nothing too obscure is happening. The getter and setter methods return and set transformation data and where necessary convert between the data types of Bullet and Panda3D.
Bullet uses a different coordinate system than Panda3D. In Bullet's representation of the scene, the y-axis points up, while in Panda3D, the z-axis points up. Since they are different, in the sample code, the z- and y-axes are flipped to accommodate this situation.
The code in Application.cpp
is responsible for opening the application window and initializing the Panda3D framework. It also sets up Bullet's btDiscreteDynamicsWorld
and sets up the scene.