Bullet物理引擎不完全指南 Bullet Physics Engine not complete Guide
前言
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.libopengl32.libglu32.lib麻煩點的是glut是個動態庫,你需要將dll檔案拷貝到你工程的執行目錄。現在應該成功了吧?我實現的工程見https://bullet-sample.jtianling.googlecode.com/hg/中的Bullet-WithGL。於是乎,現在你已經可以看到Hello World中那個不曾顯示的電腦憑空現象的球了。大概是下面這個樣子滴:(因為現在Google Docs寫完文章後很久就不能夠直接post到CSDN的部落格中去了,所以每次寫完文章後都得到新建文章中去複製貼上,圖片還需要重新上傳然後插入,非常麻煩,所以最近的文章都儘量的減少了圖片的使用,見諒。其實說來,只有看熱鬧的人才需要截圖,真的看教程的人估計自己的程式都已經執行起來了,也沒有必要看我的截圖了)
到目前為止,你已經有了可以自己happy一下的程式了,你可以立即深入的學習,去研究ExampleApplication的原始碼,去了解Bullet是怎麼與圖形互動的,但是在這個之前,特別是對於以前沒有使用過其他物理引擎的人,先多在這個圖形版的HelloWorld的程式的基礎上玩玩,比如現在球很明顯沒有彈性,調整一下反彈的係數看看,比如多生成幾個球,比如加上速度,演示兩個球的碰撞,比如因為現在沒有設定debugmode,所以實際沒有debug資訊輸出,嘗試輸出aabb等debug資訊,有助於進一步學習。有了圖形,就有了豐富的世界,先對Bullet的各個概念先熟悉一下(特別是btRigidBodyConstructionInfo中的各個變數,還有各個shape)然後再前進吧。事實上,得益於ExampleApplication,現在甚至可以用滑鼠左鍵拖拽物理,右鍵發射箱子的功能也還在,還能按左右鍵調整camera。(其實還有一堆功能,自己嘗試一下吧)因為比較簡單,在本教程中不會再有關於這些基礎資訊的內容,只能自己去找資料或者嘗試了。其實就我使用物理引擎的經驗,學習並使用一款物理引擎並不會太難,最麻煩的地方在於後期遊戲中各個引數的調整,儘量使遊戲的效果變得真實,自然,或者達到你想要的效果,這種調整很多時候需要靠你自己的感覺,而這個感覺的建立,那就是多多嘗試一個物理引擎中的在各個引數下呈現的不同效果了,這種感覺的建立,經驗的獲得,不是任何教程,文件或者演示程式能夠給你的。比如,下面是Bullet支援的5種基本物體形狀:
其實上面的內容是最關鍵的,此時學開車的你已經在司機的位置了,學游泳的你已經在水裡了,剩下的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.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的文章
最後,因為本文較長,為方便檢視,提供了pdf版方便大家檢視。
原創文章作者保留版權 轉載請註明原作者 並給出連結