天天看點

Bullet實體引擎不完全指南(Bullet Physics Engine not complete Guide)

write by 九天雁翎(JTianLing) -- blog.csdn.net/vagrxie

讨論新聞討論區及檔案

前言

    Bullet據稱為遊戲世界占有率為第三的實體引擎,也是前幾大引擎目前唯一能夠找到的支援iPhone,開源,免費(Zlib協定,非常自由,且商業免費)的實體引擎,但是文檔資料并不是很好,Demo雖然多,但是主要出于特性測試/展示的目的,會讓初學者無從看起,一頭霧水。我剛學習Bullet的時候困于沒有好的文檔及資料,非常沒有頭緒,折騰了很久,是以就發揮沒有就創造的精神,寫作及整理此文,(以整理資料為主,自己寫 為輔)希望大家在學習Bullet的時候不要再像我開始一樣沒有頭緒。因為我實在沒有精力去完成一個包含Bullet方方面面的完全指南,是以本文隻能是不完全版本,這個就請大家諒解了,但是期望能夠真正的完成一個簡單的由淺入深的教程,并提供盡量詳盡的額外資訊連結,隻能說讓初學者比看官方的WIKI和Demo效果更好,大家有好的資訊和資料而本文沒有包含的,也請告訴我,我可以在新版中添加進來。因為我學習Bullet的時間也比較短,有不對的地方請高人指點。

    前段時間簡單的學習了一下Bullet,牽涉到圖形部分的時候主要都是研究Bullet與Ogre的結合,是以使用了OgreBullet這個Ogre的Addon,其實真正的學習當然還是直接利用Bullet本身附帶的簡單的debug OpenGL繪制就好了。本文就完全以Bullet本身的Debug功能來學習,雖然簡陋,但是可以排除幹擾,專注于bullet。也許除了本文,會有個額外的文章,稍微研究下Ogre與Bullet的整合和分析一下OgreBullet的源碼。

Bullet介紹

    Bullet的首頁 。最新版本在這裡下載下傳 。簡單的中文介紹見百度百科 。一些也許可以促使你選擇Bullet的小故事在以前的文章中有提及,參考這裡 的開頭--為什麼選擇Bullet。很遺憾的是前幾天看到的一篇很詳細的bullet中文介紹找不到了,将來也許補上。

安裝

    Bullet作為一款開源實體引擎,你可以選擇作者編譯好的SDK ,或者直接從源碼編譯自己的版本(Windows版本自帶VS工程)。得益于CMake,在其他平台從源碼自己編譯也非常簡單,參考這裡 。iPhone版本的話參考這裡 。想要更詳細點的圖文教程可以參考Creating_a_project_from_scratch 。

Hello World Application

    在學習之前,沒有接觸過實體引擎的可以參考一下這個術語表 。

    這裡 有個較為詳細的教程。也包含在Bullet本身的一個名叫 AppHelloWorld 的Demo中。(注釋也很詳細,但是和WIKI上的版本略有不同)可以大概的對Bullet有個感覺。

    其實Bullet與Ogre走的一條路線,為了靈活,增加了很多使用的複雜性。(真懷念Box2D和Irrlicht的簡單啊)其實即使希望通過strategy模式來增加靈活度,讓使用者可以自由的選擇各類算法和解決方案,但是我還是感覺首先提供預設解決方案,使用者需要不同方案的時候通過Set方式改變(甚至也可以new的時候修改)但是大牛們研究這些東西那麼透,總是會覺得這個世界上不存在預設方案。。。。。因為沒有方案是最優的,是适合大多數情況的,是以導緻Bullet的HelloWorld程式源代碼都已經超過100行。。。。。。。。。。-_-!發了點牢騷。。。。。

    通過HelloWorld程式,我們大概可以知道一些東西,比如建立一個Bullet實體世界的步驟,比如Bullet的類以bt(變态-_-!)開頭,比如Bullet與Box2D這樣的2D實體引擎一樣,專注于資料的計算,本身沒有圖形輸出,比如建立一個實體實體的時候也有shape的概念,然後通過一個結構作為參數(BodyConstructionInfo)來建立真實的物體,大概的熟悉一下就好,具體的細節還不懂,沒有關系,一步一步來。

    另外,建議趁這個機會,确定自己機器使用Bullet的環境,特别是Win32下,我的使用方法是,利用BULLET_HOME環境變量指明Bullet安裝的位置,BULLTE_LIBS指明最後編譯完的靜态庫的位置,工程中利用這兩個環境變量來确定位置。(這種用法很适合屏蔽各機器的環境不同)最後的Hello World工程見https://bullet-sample.jtianling.googlecode.com/hg/中的Bullet-HelloWorld。

    請確定該Hello World程式能夠運作(無論是你自己的還是用我的)然後才繼續下面的内容。

讓你坐在司機的位置上

    該怎麼學習的問題,向來都是各執一詞,有人認為該從最基礎的學起,就像建房子一樣打好地基,有人會更加推崇自上而下的學習(Top-Down Approach),我屬于後一派,能先寫有用的可以摸到的程式,然後一層一層的向下學習,這樣會更加有趣味性,并且學習曲線也會更加平緩,假如你是前一派,那麼推薦你先看完Bullet的User Manual,然後是Bullet所有的Tutorial Articles ,然後再自己一個一個看Demo。

    在Hello World的例子中你已經可以看到文本資料的輸出,能夠看到球/Box的落下了,但是很明顯太不直覺了,得益于Bullet良好的debug輸出支援,我們要先能直覺的通過圖形看到球的落下!先坐在司機的位置上才能學會開車^^你也不至于被乏味的汽車/交通理論悶死。

    Bullet像Ogre一樣,提供了一個DemoApplication類,友善我們學習,我們先看看Bullet的DemoApplication是怎麼樣的。先看看Bullet自己提供的AppBasicDemo這個Demo。忽略那些作者用#if 0關閉的内容和hashmap的測試内容,看看DemoApplication的用法。

首先是BasicDemo類,從class BasicDemo : public PlatformDemoApplication可以看到,DemoApplication是給你繼承使用的,這裡的PlatformDemoApplication實際是GlutDemoApplication。(Win32那個作者好像隻是預留的)

怎麼去實作這個類先放一邊,看看整個類的使用:

    GLDebugDrawer    gDebugDrawer;

    BasicDemo ccdDemo;

    ccdDemo.initPhysics();

    ccdDemo.getDynamicsWorld()->setDebugDrawer(&gDebugDrawer);

    glutmain(argc, argv,640,480,"Bullet Physics Demo. http://bulletphysics.com",&ccdDemo);

實際就這5句,很簡單,構造debug,BasicDemo,調用initPhysics函數,設定debug,調用glutmain這個函數,參數也一目了然。這裡就不看了。看實作一個有用的DemoApplication的過程。

    大概看看DemoApplication這個基類和GlutDemoApplication知道必須要實作的兩個純虛函數是

virtual    void initPhysics() = 0;

virtual void clientMoveAndDisplay() = 0;

看BasicDemo的實作後,知道還需要實作displayCallback這個現實回調,基本上就沒有其他東西了,了解起來也還算容易。

initPhysics的部分,一看就知道,與HelloWorld中過程幾乎一緻,也就是實際建構實體世界的過程。隻是多了   

setTexturing(true);

setShadows(true);

setCameraDistance(btScalar(SCALING*50.));

這三個與顯示有關的東西(其實這些代碼放到myinit中去也可以,畢竟與實體無關)

最後還多了個clientResetScene的調用,我們知道這個過程就好,具體函數的實作先不管。

clientMoveAndDisplay和displayCallback部分

其實非常簡單,幾乎可以直接放到glutExampleApplication中去。(事實上不從靈活性考慮,我覺得放到glutExampleApplication中更好)

原來的程式有些代碼重複,其實隻要下列代碼就夠了:(一般的程式也不需要修改)

void  BasicDemo::clientMoveAndDisplay()

{

    //simple dynamics world doesn't handle fixed-time-stepping

    float  ms = getDeltaTimeMicroseconds();

    ///step the simulation

    if  (m_dynamicsWorld)

    {

        m_dynamicsWorld->stepSimulation(ms / 1000000.f );

    }

    displayCallback();

}

void  BasicDemo::displayCallback(void ) {

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    renderme();

    //optional but useful: debug drawing to detect problems

    if  (m_dynamicsWorld)

        m_dynamicsWorld->debugDrawWorld();

    glFlush();

    swapBuffers();

}

運作該程式能夠看到中間一個很多Box堆起來的大方塊,點選滑鼠右鍵還能發射一個方塊出去。

了解這個Demo以後,我們就可以直接來用Bullet建構我們自己的實體世界了,暫時不用考慮圖形的問題,甚至不用知道Bullet使用GLUT 作為debug圖形的輸出,GLUI 做界面,都不用知道,隻需要知道上面demoApplication的使用和在initPhysics函數中完成建構實體世界的代碼。另外,你願意的話,也可以先看看exitPhysics的内容,用于配置設定資源的釋放,作為C++程式,一開始就關注資源的釋放問題是個好習慣。雖然對于我們這樣簡單的demo程式來說是無所謂的。

看過上面Demo後,也許你已經有些了解,也許你還是一頭霧水,不管怎麼樣,Bullet的Demo畢竟還是别人的東西,現在,從零開始,建構一個HelloWorld程式描述的世界。先自己嘗試一下!假如你成功了,那麼直接跳過一下的内容,失敗了,再回頭了看看,提醒你步驟:

1.繼承DemoApplication,拷貝上面clientMoveAndDisplay和displayCallback部分的代碼,實作這兩個函數。

2.在initPhysics函數中完成建構實體世界的代碼。(建構過程參考HelloWorld)

3.Main中的使用代碼:

    GLDebugDrawer    gDebugDrawer;

    BasicDemo ccdDemo;

    ccdDemo.initPhysics();

    ccdDemo.getDynamicsWorld()->setDebugDrawer(&gDebugDrawer);

    glutmain(argc, argv,640,480,"Bullet Physics Demo. http://bulletphysics.com",&ccdDemo);

4.注意工程需要多包含$(BULLET_HOME)/Demos/OpenGL的頭檔案目錄

和庫:

$(BULLET_HOME)/Glut/glut32.lib

opengl32.lib

glu32.lib

麻煩點的是glut是個動态庫,你需要将dll檔案拷貝到你工程的運作目錄。

現在應該成功了吧?

我實作的工程見https://bullet-sample.jtianling.googlecode.com/hg/中的Bullet-WithGL。

于是乎,現在你已經可以看到Hello World中那個不曾顯示的電腦憑空現象的球了。大概是下面這個樣子滴:(因為現在Google Docs寫完文章後很久就不能夠直接post到CSDN的部落格中去了,是以每次寫完文章後都得到建立文章中去複制粘貼,圖檔還需要重新上傳然後插入,非常麻煩,是以最近的文章都盡量的減少了圖檔的使用,見諒。其實說來,隻有看熱鬧的人才需要截圖,真的看教程的人估計自己的程式都已經運作起來了,也沒有必要看我的截圖了)

Bullet實體引擎不完全指南(Bullet Physics Engine not complete Guide)

到目前為止,你已經有了可以自己happy一下的程式了,你可以立即深入的學習,去研究ExampleApplication的源代碼,去了解Bullet是怎麼與圖形互動的,但是在這個之前,特别是對于以前沒有使用過其他實體引擎的人,先多在這個圖形版的HelloWorld的程式的基礎上玩玩,比如現在球很明顯沒有彈性,調整一下反彈的系數看看,比如多生成幾個球,比如加上速度,示範兩個球的碰撞,比如因為現在沒有設定debugmode,是以實際沒有debug資訊輸出,嘗試輸出aabb等debug資訊,有助于進一步學習。有了圖形,就有了豐富的世界,先對Bullet的各個概念先熟悉一下(特别是btRigidBodyConstructionInfo中的各個變量,還有各個shape)然後再前進吧。事實上,得益于ExampleApplication,現在甚至可以用滑鼠左鍵拖拽實體,右鍵發射箱子的功能也還在,還能按左右鍵調整camera。(其實還有一堆功能,自己嘗試一下吧)因為比較簡單,在本教程中不會再有關于這些基礎資訊的内容,隻能自己去找資料或者嘗試了。其實就我使用實體引擎的經驗,學習并使用一款實體引擎并不會太難,最麻煩的地方在于後期遊戲中各個參數的調整,盡量使遊戲的效果變得真實,自然,或者達到你想要的效果,這種調整很多時候需要靠你自己的感覺,而這個感覺的建立,那就是多多嘗試一個實體引擎中的在各個參數下呈現的不同效果了,這種感覺的建立,經驗的獲得,不是任何教程,文檔或者示範程式能夠給你的。

比如,下面是Bullet支援的5種基本物體形狀:

Bullet實體引擎不完全指南(Bullet Physics Engine not complete Guide)

其實上面的内容是最關鍵的,此時學開車的你已經在司機的位置了,學遊泳的你已經在水裡了,剩下的Bullet相關的内容雖然還有很多,但是其實已經完全可以自己獨立折騰了,因為每個折騰的成果你都已經能夠實時的看到,能夠很哈皮的去折騰了。

與顯示的整合,MotionState

    一個隻有資料運算的實體引擎,一般而言隻能為顯示引擎提供資料,這就牽涉到與圖形引擎整合的問題,像Box2D這樣的實體引擎就是直接需要直接向各個實體實體去查詢位置,然後更新顯示,這種方式雖然簡單,但是我感覺非常不好,因為難免在update中去更新這種東西,導緻遊戲邏輯部分需要處理實體引擎+圖形引擎兩部分的内容。(可以參考Box2D與Cocos2D for iPhone的整合)而且,對于完全沒有移動的物體也會進行一次查詢和移動操作。(即使優化,對不移動物體也是進行了兩次查詢)

    Bullet為了解決此問題,提供了新的解決方案,MotionState。其實就是當活動物體狀态改變時提供一種回調,而且就Bullet的文檔中說明,此種回調還帶有适當的插值以優化顯示。通過這種方法,在MotionState部分就已經可以完成顯示的更新,不用再需要在update中添加這種更新的代碼。而且,注意,僅僅對活動物體狀态改變時才會進行回調,這樣完全避免了不活動物體的性能損失。

    首先看看ExampleApplication中是怎麼利用default的MotionState來顯示上面的圖形的,然後再看看複雜點的例子,與Ogre的整合。

先看看回調接口:

///The btMotionState interface class allows the dynamics world to synchronize and interpolate the updated world transforms with graphics

///For optimizations, potentially only moving objects get synchronized (using setWorldPosition/setWorldOrientation)

class    btMotionState

{

    public :

        virtual  ~btMotionState()

        {

        }

        virtual  void   getWorldTransform(btTransform& worldTrans ) const  =0 ;

        //Bullet only calls the update of worldtransform for active objects

        virtual  void   setWorldTransform(const  btTransform& worldTrans)=0 ;

};

很簡單,一個get接口,用于bullet擷取物體的初始狀态,一個set接口,用于活動物體位置改變時調用以設定新的狀态。

下面看看btDefaultMotionState這個bullet中帶的預設的MotionState類。

///The btDefaultMotionState provides a common implementation to synchronize world transforms with offsets.

struct   btDefaultMotionState : public  btMotionState

{

    btTransform m_graphicsWorldTrans;

    btTransform m_centerOfMassOffset;

    btTransform m_startWorldTrans;

    void *      m_userPointer;

    btDefaultMotionState(const  btTransform& startTrans = btTransform::getIdentity(),const  btTransform& centerOfMassOffset = btTransform::getIdentity())

        : m_graphicsWorldTrans(startTrans),

        m_centerOfMassOffset(centerOfMassOffset),

        m_startWorldTrans(startTrans),

        m_userPointer(0 )

    {

    }

    ///synchronizes world transform from user to physics

    virtual  void    getWorldTransform(btTransform& centerOfMassWorldTrans ) const  

    {

            centerOfMassWorldTrans =    m_centerOfMassOffset.inverse() * m_graphicsWorldTrans ;

    }

    ///synchronizes world transform from physics to user

    ///Bullet only calls the update of worldtransform for active objects

    virtual  void    setWorldTransform(const  btTransform& centerOfMassWorldTrans)

    {

            m_graphicsWorldTrans = centerOfMassWorldTrans * m_centerOfMassOffset ;

    }

};

這個預設的MotionState實作了這兩個接口,但是還引入了質心 (center Of Mass應該是指質心 吧)的概念,與外部互動時,以質心位置表示實際物體所在位置。

在一般rigitBody的構造函數中可以看到下列代碼:

    if  (m_optionalMotionState)

    {

        m_optionalMotionState->getWorldTransform(m_worldTransform);

    } else

    {

        m_worldTransform = constructionInfo.m_startWorldTransform;

    }

這就是get函數的使用,也就是決定物體初始坐标的函數回調。

set函數的回調如下:

void     btDiscreteDynamicsWorld::synchronizeSingleMotionState(btRigidBody* body)

{

    btAssert(body);

    if  (body->getMotionState() && !body->isStaticOrKinematicObject())

    {

        //we need to call the update at least once, even for sleeping objects

        //otherwise the 'graphics' transform never updates properly

        ///@todo: add 'dirty' flag

        //if (body->getActivationState() != ISLAND_SLEEPING)

        {

            btTransform interpolatedTransform;

            btTransformUtil::integrateTransform(body->getInterpolationWorldTransform(),

                body->getInterpolationLinearVelocity(),body->getInterpolationAngularVelocity(),m_localTime*body->getHitFraction(),interpolatedTransform);

            body->getMotionState()->setWorldTransform(interpolatedTransform);

        }

    }

}

也就是同步狀态的時候調用。此過程發生在調用bullet的btDynamicsWorld::stepSimulation函數調用時。

然後可以參考DemoApplication的DemoApplication::renderscene(int pass)函數:

    btScalar    m[16 ];

    btMatrix3x3 rot;rot.setIdentity();

    const  int   numObjects=m_dynamicsWorld->getNumCollisionObjects();

    btVector3 wireColor(1 ,0 ,0 );

    for (int  i=0 ;i<numObjects;i++)

    {

        btCollisionObject*  colObj=m_dynamicsWorld->getCollisionObjectArray()[i];

        btRigidBody*        body=btRigidBody::upcast(colObj);

        if (body&&body->getMotionState())

        {

            btDefaultMotionState* myMotionState = (btDefaultMotionState*)body->getMotionState();

            myMotionState->m_graphicsWorldTrans.getOpenGLMatrix(m);

            rot=myMotionState->m_graphicsWorldTrans.getBasis();

        }

    }

}

實際也就是再通過擷取motionState然後擷取到圖形的位置了,這種defaultMotion的使用就類似Box2D中的使用了。

既然是回調,那麼就可以讓函數不僅僅做指派那麼簡單的事情,回頭來再做一次輪詢全部物體的查詢,官網的WIKI中為Ogre編寫的MotionState就比較合乎推薦的MotionState用法,代碼如下:

lass MyMotionState : public  btMotionState {

public :

    MyMotionState(const  btTransform &initialpos, Ogre::SceneNode *node) {

        mVisibleobj = node;

        mPos1 = initialpos;

    }

    virtual  ~MyMotionState() {

    }

    void  setNode(Ogre::SceneNode *node) {

        mVisibleobj = node;

    }

    virtual  void  getWorldTransform(btTransform &worldTrans) const  {

        worldTrans = mPos1;

    }

    virtual  void  setWorldTransform(const  btTransform &worldTrans) {

        if (NULL  == mVisibleobj) return ; // silently return before we set a node

        btQuaternion rot = worldTrans.getRotation();

        mVisibleobj->setOrientation(rot.w(), rot.x(), rot.y(), rot.z());

        btVector3 pos = worldTrans.getOrigin();

        mVisibleobj->setPosition(pos.x(), pos.y(), pos.z());

    }

protected :

    Ogre::SceneNode *mVisibleobj;

    btTransform mPos1;

};

注意,這裡的使用直接在set回調中直接設定了物體的位置。如此使用MotionState後,update隻需要關心邏輯即可,不用再去手動查詢物體的位置,然後更新物體的位置并重新整理顯示。

碰撞檢測

    實體引擎不僅僅包括模拟真實實體實作的一些運動,碰撞,應該還提供方式供檢測碰撞情況,bullet也不例外。

    AppCollisionInterfaceDemo展示了怎麼直接通過btCollisionWorld來檢測碰撞而不模拟實體。

而官方的WIKI對于碰撞檢測的描述也過于簡單,隻給下列的示例代碼,但是卻沒有詳細的解釋。

    //Assume world->stepSimulation or world->performDiscreteCollisionDetection has been called

    int  numManifolds = world->getDispatcher()->getNumManifolds();

    for  (int  i=0 ;i<numManifolds;i++)

    {

        btPersistentManifold* contactManifold =  world->getDispatcher()->getManifoldByIndexInternal(i);

        btCollisionObject* obA = static_cast <btCollisionObject*>(contactManifold->getBody0());

        btCollisionObject* obB = static_cast <btCollisionObject*>(contactManifold->getBody1());

        int  numContacts = contactManifold->getNumContacts();

        for  (int  j=0 ;j<numContacts;j++)

        {

            btManifoldPoint& pt = contactManifold->getContactPoint(j);

            if  (pt.getDistance()<0.f )

            {

                const  btVector3& ptA = pt.getPositionWorldOnA();

                const  btVector3& ptB = pt.getPositionWorldOnB();

                const  btVector3& normalOnB = pt.m_normalWorldOnB;

            }

        }

    }

以上代碼的主要内容就是

int  numManifolds = world->getDispatcher()->getNumManifolds();

btPersistentManifold* contactManifold =  world->getDispatcher()->getManifoldByIndexInternal(i);

兩句。

    而btPersistentManifold類表示一個Manifold,其中包含了body0,body1表示Manifold的兩個物體。

這裡特别提及的是,Manifold并不直接表示碰撞,其真實的含義大概是重疊,在不同的情況下可能表示不同的含義,比如在Box2D中,手冊的描述大概是(憑記憶)為了快速的檢測碰撞,在2D中一般先經過AABB盒的檢測過濾,而隻有AABB盒重疊的才有可能碰撞,而Manifold在Box2D中就表示AABB盒重疊的兩個物體,而我看Bullet有不同的Broadphase,在實際中,也重疊也應該會有不同的情況,因為我沒有看源碼,是以不能确定,但是,總而言之,可以了解Manifold為接近碰撞的情況。

是以無論在Box2D還是Bullet中,都有額外的表示碰撞的概念,那就是contact(接觸)。上述示例代碼:

int  numContacts = contactManifold->getNumContacts();

就表示檢視接觸點的數量,假如接觸點為0,那麼自然表示兩個物體接近于碰撞,而實際沒有碰撞。而上述代碼中的Distance的判斷應該是防止誤差,因為我輸出了一個盒子和地面發生碰撞的全部過程的distance,發現絕大部分情況,隻要有contact,那麼距離就小于0,可是在一次盒子離開地面的過程中,distance還真有過一次0.00x的正值。。。。。。。

當你開始放心大膽的使用上述代碼後,也許你總是用來模拟物體的其他效果,也許都不會有問題,直到某一天你希望在碰撞檢測後删除掉發生碰撞的問題,你的程式crash了。。。。你卻不知道為什麼。用前面的demo來展示碰撞檢測的方法,并且删除掉發生碰撞的物體。一般先寫出的代碼都會類似下面這樣:

    int  numManifolds = m_dynamicsWorld->getDispatcher()->getNumManifolds();

    for  (int  i=0 ;i<numManifolds;i++)

    {

        btPersistentManifold* contactManifold =  m_dynamicsWorld->getDispatcher()->getManifoldByIndexInternal(i);

        btCollisionObject* obA = static_cast <btCollisionObject*>(contactManifold->getBody0());

        btCollisionObject* obB = static_cast <btCollisionObject*>(contactManifold->getBody1());

        int  numContacts = contactManifold->getNumContacts();

        for  (int  j=0 ;j<numContacts;j++)

        {

            btManifoldPoint& pt = contactManifold->getContactPoint(j);

            if  (pt.getDistance()<0.f )

            {

                RemoveObject(obA);

                RemoveObject(obB);

            }

        }

    }

但是上面這樣的代碼是有問題的,這在Box2D的文檔中有較長的描述,Bullet文檔中沒有描述,那就是obA和obB可能重複删除的問題(也就相當于删除同一個對象多次,自然crash)在本例中有兩個問題會導緻重複,很明顯的一個,當兩個物體多餘一個Contact點的時候,在周遊Contacts點時會導緻obA,obB重複删除。另外,稍微隐晦點的情況是,當一個物體與兩個物體發生碰撞時,同一個物體也可能在不同的manifold中,是以,真正沒有問題的代碼是先記錄所有的碰撞,然後消除重複,再然後删除 。這是Bullet文檔中沒有提到,WIKI中也沒有說明的,初學者需要特别注意。。。。。。下面才是安全的代碼:

    int  numManifolds = m_dynamicsWorld->getDispatcher()->getNumManifolds();

    for  (int  i=0 ;i<numManifolds;i++)

    {

        btPersistentManifold* contactManifold =  m_dynamicsWorld->getDispatcher()->getManifoldByIndexInternal(i);

        btCollisionObject* obA = static_cast <btCollisionObject*>(contactManifold->getBody0());

        btCollisionObject* obB = static_cast <btCollisionObject*>(contactManifold->getBody1());

        int  numContacts = contactManifold->getNumContacts();

        for  (int  j=0 ;j<numContacts;j++)

        {

            btManifoldPoint& pt = contactManifold->getContactPoint(j);

            if  (pt.getDistance()<0.f )

            {

                m_collisionObjects.push_back(obA);

                m_collisionObjects.push_back(obB);

            }

        }

    }

    m_collisionObjects.sort();

    m_collisionObjects.unique();

    for  (CollisionObjects_t::iterator itr = m_collisionObjects.begin();

        itr != m_collisionObjects.end();

        ++itr) {

        RemoveObject(*itr);

    }

    m_collisionObjects.clear();

上述m_collisionObjects是std::list類型的成員變量。

碰撞過濾

    Bullet的wiki 提到了3個方法,這裡隻講述最簡單的mask(掩碼)過濾方法。

    mask的使用相信大家基本都接觸過,無非就是通過一個整數各個2進制位來表示一些bool值。比如Unix/Linux中檔案權限的掩碼。在bullet中的碰撞mask的使用非常簡單,主要在addRigidBody時候指定。(需要注意的是,隻有btDiscreteDynamicsWorld類才有這個函數,btDynamicsWorld并沒有,是以demoApplication中的成員變量dynamicWorld不能直接使用。)

WIKI中的代碼已經很能說明問題了:

#define BIT(x) ( 1 <<(x))

enum  collisiontypes {

    COL_NOTHING = 0 , //<Collide with nothing

    COL_SHIP = BIT(1 ), //<Collide with ships

    COL_WALL = BIT(2 ), //<Collide with walls

    COL_POWERUP = BIT(3 ) //<Collide with powerups

}

int  shipCollidesWith = COL_WALL;

int  wallCollidesWith = COL_NOTHING;

int  powerupCollidesWith = COL_SHIP | COL_WALL;

btRigidBody ship; // Set up the other ship stuff

btRigidBody wall; // Set up the other wall stuff

btRigidBody powerup; // Set up the other powerup stuff

mWorld->addRigidBody(ship, COL_SHIP, shipCollidesWith);

mWorld->addRigidBody(wall, COL_WALL, wallCollidesWith);

mWorld->addRigidBody(powerup, COL_POWERUP, powerupCollidesWith);

特别是那個#define BIT(x) ( 1 <<(x)) 宏用的很有意思。

不要特别注意的是,兩個物體要發生碰撞,那麼,兩個物體的collidesWith參數必須要互相指定對方,假如A指定碰撞B,但是B沒有指定碰撞A,那麼還是沒有碰撞。就上面的例子而言,雖然ship和powerup想要撞牆,但是牆不想撞它們,那麼事實上,上面的例子就相當于過濾了所有牆的碰撞,其實僅僅隻有ship和power的碰撞,這真所謂強扭的瓜不甜啊,等雙方都情願。

仿照上面的例子,假如你希望在碰撞檢測的時候過濾掉地闆,隻讓物體間發生碰撞然後删除物體,為demo添加下列代碼:

#define BIT(x) ( 1 <<(x))

    enum  collisiontypes {

        COL_NOTHING = 0 , //<Collide with nothing

        COL_GROUND = BIT(1 ), //<Collide with ships

        COL_OBJECTS = BIT(2 ), //<Collide with walls

    };

    short  GroundCollidesWith = COL_OBJECTS;

    short  ObjectsCollidesWith = COL_GROUND;

但是當你将上述方法應用到demo中,想要過濾掉你想要的碰撞,你會發現碰撞檢測的确是過濾掉了,同時過濾掉的還有碰撞,球直接傳地闆而過,掉進了無底的深淵。注意,這裡的過濾是指碰撞過濾,而不是碰撞檢測的過濾,假如希望實作碰撞檢測的過濾,你可以在碰撞檢測中直接進行。比如前面地闆的例子,因為地闆是靜态物體,你可以通過調用rigidBody的isStaticObject來判斷是否是地闆,然後進行删除,就如下面的代碼這樣:

            if  (pt.getDistance()<0.f ) {

                if  (!obA->isStaticObject()) {

                    m_collisionObjects.push_back(obA);

                }

                if  (!obB->isStaticObject()) {

                    m_collisionObjects.push_back(obB);

                }

            }

假如希望與地面碰撞并不删除物體,隻有物體與物體的碰撞才删除物體,這也簡單:

                if  (!obA->isStaticObject() && !obB->isStaticObject()) {

                    m_collisionObjects.push_back(obA);

                    m_collisionObjects.push_back(obB);

至于更加複雜的情況,還可以借助于rigidBody的UserPointer,這在WIKI中沒有提及,

    ///users can point to their objects, userPointer is not used by Bullet

    void *  getUserPointer() const

    {

        return  m_userObjectPointer;

    }

    ///users can point to their objects, userPointer is not used by Bullet

    void    setUserPointer(void * userPointer)

    {

        m_userObjectPointer = userPointer;

    }

但是就我的經驗,這兩個函數的作用是巨大的,你可以将你需要的一切都設定進去。。。。。。。。然後取出來,就上面的碰撞檢測過濾而言,你完全可以實作自己的一套碰撞檢測mask,隻要你想,一切皆有可能。這些例子的完整源代碼見https://bullet-sample.jtianling.googlecode.com/hg/中的Bullet-CollideDetection工程。

限制(Constraints)和連接配接(Joints)

    一個一個單獨的實體實體已經可以建構一個有意思的實體世界了,但是顯示世界有很多東西(最典型的就是繩子連接配接的物體)不是單獨的實體實體可以模拟的,實體引擎中使用限制來模拟類似的東西/現象。

    (待補充)

軟體

   因為我的使用暫時不需要用到軟體,暫時未學習此部分内容,歡迎大家補充。

有用的工具

1. MAYA,3D Max的插件

2. Blender ,開源的3D模組化工具,内建的Game Engine有直接的Bullet支援,還有Erwin提供的改版 可以直接導出.bullet檔案。

使用了Bullet的其他有用工程

1. GameKit ,Erwin Coumans自己發起的整合Ogre/Irrlicht和Bullet的遊戲引擎,與Blender結合的也很好。

2. oolongengine ,烏龍引擎,wolfgang.engel (他的部落格 )這個大牛(到底有多牛可以參考這裡 )發起的iPhone平台引擎項目,使用了Bullet,也為Bullet能夠流暢的運作于iPhone平台做出了很大貢獻。(優化了浮點運算)為什麼寫烏龍引擎?wolfgang自己有個解釋,見這裡 。

3. Dynamica , Erwin建立的工程,開發bullet的Maya,3D Max插件。

4. bullet-physics-editor , Erwin自己發起的一個bullet編輯器工程,目前還處于前期開發階段。但是項目中同時包含一些能夠到處.Bullet檔案的Blender改版 。

Bullet的實作原理

1.Physics Pipeline Implementation ,應該是一個在愛爾蘭的中國人寫的,并釋出在WIKI上,這裡是他的部落格 。

2.SIGGRAPH 2010 course slides, high-level overview on Bullet collision detection

3.GDC 2010 presentation about contact generation

4.最大的資料自然就是Bullet的源代碼 啦。。。。。。慢慢研究吧。

參考資料

1.Bullet 2.76 Physics SDK Manual ,Bullet的項目發起人,目前的負責人Erwin Coumans所寫,(就WIKI資料顯示,這哥們現在在SONY)算是官方的Manual了,源碼包中就有pdf。

2.Bullet WIKI Tutorial Articles ,算是第2個能夠找到的稍微好點的關于Bullet的東西了,就是有點散亂。

3.Bullet Bullet Documentation ,Bullet的文檔,自動生成的,也就隻能在寫代碼的時候可能有些用,很難靠這個學習。

我寫的關于Bullet的文章

1.Ogre與Bullet的整合(on iPhone)

2.Ogre的3D Max插件介紹

最後,因為本文較長,為友善檢視,提供了pdf版 友善大家檢視。

原創文章作者保留版權 轉載請注明原作者 并給對外連結接

write by 九天雁翎(JTianLing) -- blog.csdn.net/vagrxie

繼續閱讀