Irrlicht Engine logo

Tutorial: Ageia PhysX & Irrlicht by Andrew J Santa Maria
Original tutorial can be found at: http://www.twilightstar.net/~andrew/works/tutorial/irrlicht_physx/physxtutorial.htm

Basic Integration of Ageia PhysX (formerly called Novodex) with Irrlicht by Andrew J. "andyZER0" Santa Maria

I'm assuming you have:

  • Irrlicht SDK 0.14.0
  • Ageia PhysX SDK 2.3.1
  • Enough knowledge of C++ :) I won't spend time describing every single line of code.  (That's what comments are for right? :)  I'm hoping all the function names and variables are self explanatory.
  • Your paths set up correctly pointing to the proper PhysX and Irrlicht headers etc.
You can download the sourcecode/binaries used in this tutorial here.  Be aware that the binaries provided in the tutorial rely on .NET Framework 2.0, so you may wanna compile it for yourself if you don't have it.  I successfully compiled this under Microsoft Visual C++ 2005 Express Edition.  If you experience problems with your compiler, you can email me and I'll see what I can do :)  Otherwise, the forums are a great resource too.

This tutorial uses some existing and modified code from the PhysX SDK 2.3.1 Training Program Beginner Lesson 101 written by Bob Schade.  
This tutorial uses a Quake3 map from Mercior's "Integrating Newton Game Dynamics with Irrlicht" tutorial.
It also uses a free wood texture from http://www.mayang.com/

Now let's begin!

We'll be using this libraries and including these headers

#pragma comment(lib, "PhysXLoader.lib")
#pragma comment(lib, "NxCooking.lib")
#pragma comment(lib, "Irrlicht.lib")

#include <NxPhysics.h>
#include <NxCooking.h>
#include <Stream.h>
#include <irrlicht.h>

Custom Scene Node

Using code from the original Irrlicht engine SDK (in source.zip), we create a new scene node similar to CAnimatedMeshSceneNode

class CPhysXAnimatedMeshSceneNode : public IAnimatedMeshSceneNode;

Give it some special variables

private:
    // NEW STUFF
    NxActor *Actor;
    bool PhysXControlled;

Along with special functions...

virtual bool isPhysXControlled() const { return PhysXControlled; }
virtual void setPhysXControlled(const bool &controlled) { PhysXControlled = controlled; }

The only difference between CPhysXAnimatedMeshSceneNode and the original CAnimatedMeshSceneNode is the pointer to an NxActor and a bool declaring whether or not the scenenode should be controlled by PhysX.  Thus, you can assume the only other difference is the constructor and a modified post render function to dynamically reposition the scenenode.

CPhysXAnimatedMeshSceneNode(IAnimatedMesh* mesh, NxActor* actor, ISceneNode* parent, ISceneManager* mgr,    s32 id,
        const core::vector3df& position = core::vector3df(0,0,0),
        const core::vector3df& rotation = core::vector3df(0,0,0),
        const core::vector3df& scale = core::vector3df(1.0f, 1.0f, 1.0f));

/***** moving along... you can find the rest of the code in the source :) ****/

void CPhysXAnimatedMeshSceneNode::OnPreRender()
{
    if (IsVisible)
    {
        // reorient/reposition the scene node every frame
        if (Actor && !Actor->isSleeping() && PhysXControlled)
        {
            // this code's from the Beginner Lesson 101 tutorial
            /* Ageia and PhysX, both stylized and non-stylized, are trademarks or registered trademarks
            of Ageia Technologies Inc. Copyright 2006 Ageia Technologies Inc. */
            // modified to use for a irrlicht matrix4
            NxMat34 pose = Actor->getGlobalPose();

            const NxVec3 pos = pose.t;
            const NxMat33 orient = pose.M;
            core::matrix4 irrMat; // a 4x4 matrix in the irrlicht engine
            orient.getColumnMajorStride4(&irrMat.M[0]);
            pos.get(&irrMat.M[12]);
            //clear the elements we don't need:
            irrMat.M[3] = irrMat.M[7] = irrMat.M[11] = 0.0f;
            irrMat.M[15] = 1.0f;

            // with that newly made matrix, let's use it to transform/rotate the node
            setPosition(irrMat.getTranslation());
            setRotation(irrMat.getRotationDegrees());
        }
        // because this node supports rendering of mixed mode meshes consisting of
        // transparent and solid material at the same time, we need to go through all
        // materials, check of what type they are and register this node for the right
        // render pass according to that.

        video::IVideoDriver* driver = SceneManager->getVideoDriver();

        PassCount = 0;
        int transparentCount = 0;
        int solidCount = 0;

        // count transparent and solid materials in this scene node
        for (u32 i=0; i<Materials.size(); ++i)
        {
            video::IMaterialRenderer* rnd =
                driver->getMaterialRenderer(Materials[i].MaterialType);

            if (rnd && rnd->isTransparent())
                ++transparentCount;
            else
                ++solidCount;

            if (solidCount && transparentCount)
                break;
        }   

        // register according to material types counted
        //! but first, check if it's in our camera's frustum before we decide we want to register for rendering
        if (SceneManager->getActiveCamera()->getViewFrustrum()->getBoundingBox().isPointInside(getPosition()))
        {
            if (solidCount)
                SceneManager->registerNodeForRendering(this, scene::ESNRP_SOLID);

            if (transparentCount)
                SceneManager->registerNodeForRendering(this, scene::ESNRP_TRANSPARENT);
        }
    }

    ISceneNode::OnPreRender();

    if (IsVisible)
        for (s32 i=0; i<(s32)JointChildSceneNodes.size(); ++i)
            if (JointChildSceneNodes[i])
                JointChildSceneNodes[i]->OnPreRender();
}

Then, to make things easier on ourselves, let's create a function similar to addAnimatedMeshSceneNode.

IAnimatedMeshSceneNode* addPhysXAnimatedMeshSceneNode(IAnimatedMesh* mesh, NxActor* actor, ISceneNode* parent=0, s32 id=-1,
                                                      const core::vector3df& position = core::vector3df(0,0,0),    const core::vector3df& rotation = core::vector3df(0,0,0),    const core::vector3df& scale = core::vector3df(1.0f, 1.0f, 1.0f))
{
    if (!mesh)
        return 0;

    if (!parent)
        parent = smgr->getRootSceneNode();

    IAnimatedMeshSceneNode* node =
        new CPhysXAnimatedMeshSceneNode(mesh, actor, parent, smgr, id, position, rotation, scale);
    node->drop();

    return node;   
}

Initialization and Shuttin' Down functions

Now that we have our new, dynamic scene node class ready to use, let's declare some variables and create some functions that that'll make everything work together. (pretty much the use for...every function)

// Physics SDK globals
NxPhysicsSDK*     gPhysicsSDK = NULL;            // pointer to the SDK
NxScene*          gScene = NULL;                // pointer to the scene
NxVec3            gDefaultGravity(0,-9.81f,0);    // NxVec3 representing gravity

// Force globals
NxVec3 gForceVec(0,0,0);
NxReal gForceStrength = 75000000.0f; // some force big enough to toss them balls and boxes around


IAnimatedMesh *cubeMesh, *sphereMesh; // pointers to the actual cube and sphere meshes
ITexture *texture0, *texture1; // pointers to their textures

////////////////////////////////////////////////////////

// function to initialize PhysX
bool InitNx()
{
    /* Ageia and PhysX, both stylized and non-stylized, are trademarks or registered trademarks
    of Ageia Technologies Inc. Copyright 2006 Ageia Technologies Inc. */

    // Create the physics SDK
    gPhysicsSDK = NxCreatePhysicsSDK(NX_PHYSICS_SDK_VERSION);
    if (!gPhysicsSDK)  return false;

    // Set the physics parameters
    gPhysicsSDK->setParameter(NX_SKIN_WIDTH, 0.0f); // usually 0.02

    // Set the debug visualization parameters
    gPhysicsSDK->setParameter(NX_VISUALIZATION_SCALE, 1);
    gPhysicsSDK->setParameter(NX_VISUALIZE_COLLISION_SHAPES, 1);
    gPhysicsSDK->setParameter(NX_VISUALIZE_ACTOR_AXES, 1);

    // Create the scene
    NxSceneDesc sceneDesc;
    sceneDesc.gravity               = gDefaultGravity;
    sceneDesc.broadPhase            = NX_BROADPHASE_COHERENT;
    sceneDesc.collisionDetection    = true;
    gScene = gPhysicsSDK->createScene(sceneDesc);
    if (!gScene)
        return false;

    // Create the default material
    NxMaterial* defaultMaterial = gScene->getMaterialFromIndex(0);
    defaultMaterial->setRestitution(0.125f); //! 0.5
    defaultMaterial->setStaticFriction(0.5f); //! 0.5
    defaultMaterial->setDynamicFriction(0.5f); //! 0.5

    // Get the current time
    UpdateTime();

    // Start the first frame of the simulation
    StartPhysics();

    return true;
}


// run this to close it
void ReleaseNx()
{
    /* Ageia and PhysX, both stylized and non-stylized, are trademarks or registered trademarks
    of Ageia Technologies Inc. Copyright 2006 Ageia Technologies Inc. */
    if (gScene)
    {
        //GetPhysicsResults();  // Make sure to fetchResults() before shutting down
        gPhysicsSDK->releaseScene(*gScene);
    }
    if (gPhysicsSDK)  gPhysicsSDK->release();
}

Per Frame Operations

Next, we create functions to execute every frame to
perform physical simulation, get some time based movement, and what not.

NxReal UpdateTime()
{
    /* Ageia and PhysX, both stylized and non-stylized, are trademarks or registered trademarks
               of Ageia Technologies Inc. Copyright 2006 Ageia Technologies Inc. */
    NxReal deltaTime;
#ifndef LINUX
    static __int64 gTime,gLastTime;
    __int64 freq;
    QueryPerformanceCounter((LARGE_INTEGER *)&gTime);  // Get current count
    QueryPerformanceFrequency((LARGE_INTEGER *)&freq); // Get processor freq
    deltaTime = (double)(gTime - gLastTime)/(double)freq;
#else
    static clock_t gTime, gLastTime;
    gTime = clock();
    deltaTime = (NxReal)((double)(gTime - gLastTime) / 1000000.0f);
#endif
    gLastTime = gTime;
    return deltaTime;
}

// run this every frame
void StartPhysics()
{
    /* Ageia and PhysX, both stylized and non-stylized, are trademarks or registered trademarks
    of Ageia Technologies Inc. Copyright 2006 Ageia Technologies Inc. */
    // Update the time step
    NxReal deltaTime = UpdateTime();

    // Start collision and dynamics for delta time since the last frame
    gScene->simulate(deltaTime * 5.0f); // i multiply by five for more faster, realistic looking dynamics
    gScene->flushStream();
}

// this too
void GetPhysicsResults()
{
    /* Ageia and PhysX, both stylized and non-stylized, are trademarks or registered trademarks
    of Ageia Technologies Inc. Copyright 2006 Ageia Technologies Inc. */
    // Get results from gScene->simulate(deltaTime)
    while (!gScene->fetchResults(NX_RIGID_BODY_FINISHED, false));
}

Entity Creation

Then, we use these functions to create either boxes or cubes to throw around in the environment.

NxActor* CreateBox(const core::vector3df &pos, const core::vector3df &scale = core::vector3df(1.0f,1.0f,1.0f))
{
    /* Ageia and PhysX, both stylized and non-stylized, are trademarks or registered trademarks
    of Ageia Technologies Inc. Copyright 2006 Ageia Technologies Inc. */
    // function slightly modified to modify dimensions/scale

    // Add a single-shape actor to the scene
    NxActorDesc actorDesc;
    NxBodyDesc bodyDesc;
    bodyDesc.angularDamping = 0.5f;

    // The actor has one shape, a box, 1m on a side
    NxBoxShapeDesc boxDesc;
    boxDesc.dimensions.set(scale.X,scale.Y,scale.Z); // should be 0.5
    actorDesc.shapes.pushBack(&boxDesc);

    actorDesc.body = &bodyDesc;
    actorDesc.density = 1.0f;
    actorDesc.globalPose.t = NxVec3(pos.X,pos.Y,pos.Z);
    return gScene->createActor(actorDesc);   
}

NxActor* CreateSphere(const core::vector3df &pos, const f32 &radius = 1.0f)
{
    /* Ageia and PhysX, both stylized and non-stylized, are trademarks or registered trademarks
    of Ageia Technologies Inc. Copyright 2006 Ageia Technologies Inc. */
    // function slightly modified to create spheres

    // Add a single-shape actor to the scene
    NxActorDesc actorDesc;
    NxBodyDesc bodyDesc;
    bodyDesc.angularDamping = 0.5f;

    // The actor has one shape, a box, 1m on a side
    NxSphereShapeDesc sphereDesc;
    sphereDesc.radius = radius; // should be 0.5
    actorDesc.shapes.pushBack(&sphereDesc);

    actorDesc.body = &bodyDesc;
    actorDesc.density = 1.0f;
    actorDesc.globalPose.t = NxVec3(pos.X,pos.Y,pos.Z);
    return gScene->createActor(actorDesc);   
}

Then, we have a function to load a Quake 3 map and properly convert it into a PMAP to use for PhysX.  This way, efficient collisions can be performed with the entities against the actual map.

NxActor *CreateQuake3Map(IAnimatedMesh *q3map, const vector3df &pos = vector3df(0.0f,0.0f,0.0f), const core::vector3df &scale = core::vector3df(1.0f,1.0f,1.0f))
{
    if (!q3map)
        return NULL;

    // retrieve how many meshes there are
    irr::s32 meshBufferCount = q3map->getMesh(0)->getMeshBufferCount();

    core::array<NxVec3> vertices; // used for allocating vertices
    core::array<NxU32> indices; // used for allocating indices

    NxU32 tempIndexCount = 0; // used for offsetting indices

    for (int i = 0; i < meshBufferCount; ++i)
    {
        // pointer to the map's mesh buffer
        IMeshBuffer *mb = q3map->getMesh(0)->getMeshBuffer(i); // for every mesh buffer

        s32 numVertices = mb->getVertexCount(); // get vertex num every mesh buffer
        s32 numIndices = mb->getIndexCount(); // get index num every mesh buffer

        video::S3DVertex2TCoords *mbVertices = (irr::video::S3DVertex2TCoords*)mb->getVertices(); // get pointer to vertices in the mesh buffer
        irr::u16 *mbIndices = mb->getIndices(); // get pointer to indices in the mesh buffer

        for (int j = 0; j < numVertices; ++j) // push vertices into an array
            vertices.push_back(NxVec3(mbVertices[j].Pos.X * scale.X, mbVertices[j].Pos.Y * scale.Y, mbVertices[j].Pos.Z * scale.Z));

        for (int j = 0; j < numIndices; ++j) // push indices into an array
            indices.push_back(NxU32(mbIndices[j]) + tempIndexCount);

        // the q3 map when loaded into irrlicht, is divided into multiply mesh buffers.
        // we want the sum of all mesh buffer indices.  this way, when it's loaded into physx
        // it's offsetted correctly instead of the bug i previously had where it indices only
        // pointed to the first 400 or whatever vertices because each set of indices pointed to only
        // its own pair of vertices in the mesh buffer.
        tempIndexCount += numIndices;
    }

    NxPMap q3mapPMap;
    q3mapPMap.dataSize    = 0;
    q3mapPMap.data        = NULL;
    NxTriangleMeshDesc mapMeshDesc; //  mesh description
    // Build physical model
    mapMeshDesc.numVertices = vertices.size();   
    mapMeshDesc.numTriangles = indices.size() / 3;
    mapMeshDesc.pointStrideBytes = sizeof(NxVec3);  
    mapMeshDesc.triangleStrideBytes = 3*sizeof(NxU32);   
    mapMeshDesc.points = vertices.const_pointer();   
    mapMeshDesc.triangles = indices.const_pointer();   
    mapMeshDesc.flags = 0;
    cout << endl << mapMeshDesc.numVertices << " vertices.\n";
    cout << indices.size() << " indices.\n";
    cout << mapMeshDesc.numTriangles << " triangles.\n";

    MemoryWriteBuffer buf;
    NxCookTriangleMesh(mapMeshDesc, buf);
    MemoryReadBuffer readBuffer(buf.data);
    NxTriangleMesh *q3mapTriangleMesh = gPhysicsSDK->createTriangleMesh(readBuffer);

    // PMap stuff
    // Try loading PMAP from disk
    fstream file("q3map.pmap",ios::in|ios::binary|ios::ate);
    if (!file.good()) // if the file doesn't exist, we write ourselves a new PMAP
    {
        file.close(); // close it.  it failed :(

        file.clear();// CLEAR THEM FLAGS!!  (so we can attempt opening again properly)

        cout << "Please wait while precomputing pmap...\n";
        file.open("q3map.pmap",ios::out|ios::binary|ios::trunc);
        if(NxCreatePMap(q3mapPMap, *q3mapTriangleMesh, 64))
        {
            // The pmap has successfully been created, save it to disk for later use
            if (file.good())
            {
                cout << "writing data size:\t" << q3mapPMap.dataSize << endl;
                file.write((char*)q3mapPMap.data, q3mapPMap.dataSize);
            }
            else
                cout << "Unable to write to file.\n";

            //assign pmap to mesh
            q3mapTriangleMesh->loadPMap(q3mapPMap);

            // sdk created data => sdk deletes it
            NxReleasePMap(q3mapPMap);
        }

        file.close();
    }
    else
    {
        cout << "Found pmap and using it...\n";
        // Found pmap file
        q3mapPMap.dataSize    = file.tellg(); //getFileSize("q3map.pmap");
        file.seekg(0,ios::beg);
        cout << "reading data size:\t" << q3mapPMap.dataSize << endl;
        q3mapPMap.data        = new NxU8[q3mapPMap.dataSize];
        file.read((char*)q3mapPMap.data, q3mapPMap.dataSize);//fread(q3mapPMap.data, q3mapPMap.dataSize, 1, fp);
        file.close();

        //assign pmap to mesh
        q3mapTriangleMesh->loadPMap(q3mapPMap);

        //we created data => we delete it
        delete [] q3mapPMap.data;
    }

    NxTriangleMeshShapeDesc mapMeshShapeDesc;
    mapMeshShapeDesc.meshData = q3mapTriangleMesh;
    NxActorDesc actorDesc;
    actorDesc.shapes.pushBack(&mapMeshShapeDesc);
    actorDesc.globalPose.t = NxVec3(pos.X,pos.Y,pos.Z);
    indices.clear();
    vertices.clear();

    return gScene->createActor(actorDesc);
}

Finally, whenever we left-click, we shoot out some spheres or boxes (depending if the variable sphereOrCube has a remainder when divided by 2).

irr::u16 sphereOrCube = 0;
bool createShape(const vector3df &position = camera->getPosition(), const vector3df &target = camera->getTarget(), const bool &applyForce = true)
{
    NxActor *newActor; // pointer to newly created object
    IAnimatedMeshSceneNode *newNode; // pointer to its scene node

    core::vector3df irrDir = (target - position).normalize(); // get and normalize the direction
    f32 dim = 20.0f; // the typical dimension/radius is 20

    // we're either shooting out cubes or spheres
    if (!(sphereOrCube % 2))
    {
        newActor = CreateBox(position, vector3df(dim,dim,dim));
        if (!newActor)
            return false;

        newNode = addPhysXAnimatedMeshSceneNode(cubeMesh,newActor);
        newNode->setMaterialTexture(0, texture0);
    }
    else
    {
        newActor = CreateSphere(position,dim);
        if (!newActor)
            return false;

        newNode = addPhysXAnimatedMeshSceneNode(sphereMesh,newActor);
        newNode->setMaterialTexture(0, texture1);
    }

    newNode->setMaterialFlag(EMF_LIGHTING, true);
    newNode->setScale(core::vector3df(dim,dim,dim));
    //newNode->addShadowVolumeSceneNode();

    if (applyForce)
        ApplyForceToActor(newActor,NxVec3(irrDir.X, irrDir.Y, irrDir.Z),gForceStrength); // apply force to "shoot" it

    return true;
}


So that's all you really need to use PhysX in your Irrlicht applications.  The full source/binaries is included here.  This is my first tutorial, so if you need any clarification, just email me or message me on AIM--my screenname is dr0wsy MANdrew.  Enjoy some physics in Irrlicht :)



Ageia and PhysX, both stylized and non-stylized, are trademarks or registered trademarks of Ageia Technologies Inc. Copyright 2006 Ageia Technologies Inc.

 

 

 

 


Valid XHTML 1.0! Valid CSS!


Irrlicht Engine and Irrlicht Engine webpage © 2003-2005 by Nikolaus Gebhardt