前言:最近在接觸OpenGl和DX11的時候,順便學習了Bullet這個3D物理引擎的基本使用,記錄一下。

|BulletPhysics介紹
BulletPhysics是一個跨平台的開源物理引擎,也是三大主流3D物理引擎之一,支持三維碰撞檢測、柔體動力學和剛體動力學,多用於游戲開發和電影制作中。(GTA5,荒野大嫖客也使用了這個物理引擎)
為了更容易使用物理引擎,我們必須掌握它里面的幾個基本概念。
物理世界:
用來模擬各種剛體的運動。
物理世界有個重要的函數——stepSimulation模擬步長函數,它通過傳入的時間大小(float deltaTime),
來給世界里所有剛體進行一段時間(deltaTime長的時間)流逝的模擬。
剛體:
參與物理模擬的物體,例如一個球體,一個長方體,或者由多個復雜形狀組合成的物體。
包含形狀,摩擦系數,阻尼系數,彈性系數等屬性。
基本使用原理:
每幀調用物理世界的模擬步長函數,來使物理世界中模擬時間流逝。
每次模擬之后,每個剛體都會更新自己的位置及旋轉角度。
然后在模擬之后根據每個剛體更新后的相應位置及旋轉角度,來用圖形表現方法來繪制表現。
|1、下載Bullet庫,編譯,配置項目
可參考該篇博客: http://www.cnblogs.com/liangliangh/p/3575590.html
|2、初始化物理世界
Broadphase(粗測階段):
我們需要提前設置好世界大小和最大剛體數等參數傳遞用於構造BroadPhase。
BroadPhase的作用是在碰撞檢測的初測階段,通過基於重疊包圍盒的加速結構的三維掃描和裁剪,快速並粗略篩選掉許多不會發生碰撞的對象對。
tip:另外還有NarrowPhase(細測階段),只不過它不需要參數初始化,它負責碰撞檢測的最后一步測試,也是詳細的碰撞測試,比較耗費性能,所以才需要一個粗測階段粗略過濾掉大部分不會碰撞的形狀對。
CollisionConfiguration(碰撞配置):
則是規定哪些物體能和哪些物體碰撞的設置(例如一些多人射擊游戲中,隊友之間不會發生碰撞,但是和其他物體都能發生碰撞)
默認值是均能互相發生碰撞,本文使用了默認值。
創建並初始化物理世界代碼:
//設置世界的空間大小,限定剛體運動的空間范圍 btVector3 worldAabbMin(-10000, -10000, -10000); btVector3 worldAabbMax(10000, 10000, 10000); //設置最大剛體數 int maxProxies = 1024; //利用以上配置創建粗測階段所需參數 btAxisSweep3* broadphase = new btAxisSweep3(worldAabbMin, worldAabbMax, maxProxies); //創建好碰撞配置 btDefaultCollisionConfiguration* collisionConfiguration = new btDefaultCollisionConfiguration(); btCollisionDispatcher* dispatcher = new btCollisionDispatcher(collisionConfiguration); //創建求解器 btSequentialImpulseConstraintSolver* solver = new btSequentialImpulseConstraintSolver(); //使用以上創建的設置來創建物理世界 btDiscreteDynamicsWorld* dynamicsWorld = new btDiscreteDynamicsWorld(dispatcher, broadphase, solver, collisionConfiguration); //設置物理世界重力(這里在y軸上的重力設為10N/kg) dynamicsWorld->setGravity(btVector3(0, -10, 0));
這樣我們就成功創建了一個帶有重力的物理世界dynamicsWorld
(注意:new的東西要在不需要物理世界的時候delete掉回收內存,而且delete順序不妥則可能會出錯,下面提供一個釋放代碼參考)
#define SAFE_DELETE_PTR(ptr) do{if(ptr){delete ptr;ptr = nullptr;}}while(0); PhysicsWorld::~PhysicsWorld() { //必須先delete DynamicWorld SAFE_DELETE_PTR(mDynamicsWorld); //再delete其他相關資源 SAFE_DELETE_PTR(mBroadphase); SAFE_DELETE_PTR(mCollisionConfiguration); SAFE_DELETE_PTR(mDispatcher); SAFE_DELETE_PTR(mSolver); }
|3、創建剛體
靜態剛體
靜態剛體意思是固定不會動的物體,例如地面,或者堅硬的牆之類的。
動態剛體
動態剛體意思則是可以運動的物體,例如子彈,車, 足球之類的。
因為世界一般都有地面,所以第一個要生成的剛體往往是地面,以地面剛體的生成舉例:
地面一般是固定不變的,所以它是靜態剛體,我們設置mass時要設置為0
(密度為0時會被Bullet認為是靜態剛體,非0時則認為是動態剛體)
地面是平面形狀的,所以形狀要設置成 btStaticPlaneShape(即靜態平面形狀)。
創建一個平面狀的靜態剛體(作為地面)的代碼例子:
//創建 物體的初始位置旋轉角度信息:旋轉角度0,位置在Y軸-1距離 btDefaultMotionState* groundMotionState = new btDefaultMotionState(btTransform(btQuaternion(0, 0, 0, 1), btVector3(0, -1, 0))); //創建 靜態平面形狀 btCollisionShape* groundShape = new btStaticPlaneShape(btVector3(0, 1, 0), 1); //生成設置信息 btRigidBody::btRigidBodyConstructionInfo groundRigidBodyCI(0, groundMotionState, groundShape, btVector3(0, 0, 0)); //根據設置信息 創建剛體 btRigidBody* groundbody = new btRigidBody(groundRigidBodyCI); //設置摩擦系數0.5 groundbody->setFriction(0.5f); //將地面剛體添加到 物理世界 dynamicWorld->addRigidBody(groundbody);
創建一個球狀的動態剛體的代碼例子:
//創建 物體的初始位置旋轉角度信息:旋轉角度0,位置在Y軸10距離的高空 btDefaultMotionState* ballMotionState = new btDefaultMotionState(btTransform(btQuaternion(0, 0, 0, 1), btVector3(0, 10, 0))); //創建 半徑0.5的球體形狀 btCollisionShape* ballShape = new btSphereShape(0.5); //設置密度(特殊地,密度為0時會被認為靜態剛體,非0時則作為動態剛體) int mass = 10; //慣性 btVector3 inertia; //根據密度自動計算並設置慣性 ballShape->calculateLocalInertia(mass, inertia); //生成設置信息 btRigidBody::btRigidBodyConstructionInfo groundRigidBodyCI(mass, ballMotionState, ballShape, inertia); //根據設置信息 創建剛體 btRigidBody* ballBody = new btRigidBody(groundRigidBodyCI); //設置摩擦系數0.5 ballBody->setFriction(0.5f); //將該剛體添加到物理世界里 dynamicsWorld->addRigidBody(ballBody);
(Bullet還有其它很多基本三維形狀類,不同的形狀需要的構造參數也不一樣,了解更多可查閱官方文檔)
其它部分基本跟上面的代碼一樣。
|4、開始模擬
為了讓物理世界的模擬和畫面顯示的同步,
需要在程序的主循環函數(也就是每幀都會調用的一個主函數)里某個位置使用(一般是在渲染之前的位置)。
物理世界的模擬步長函數:
int btDiscreteDynamicsWorld::stepSimulationint stepSimulation(btScalar timeStep,int maxSubSteps=1);
timeStep也就是要模擬的時間段大小,maxSubSteps是指模擬的子步驟的數量,
簡單來說就是將時間段拆成maxSubSteps個子時間段,然后對每個子時間段依次進行模擬。
如果子步驟數量比較小,有些速度比較快的物體可能因為模擬的時間段比較大,容易穿透過其他物體模型。
將時間段拆成若干個更小的子時間段來依次模擬能夠更容易避免穿模現象,當然求解多若干次是會付出性能代價的。
(比較適中恰當的子步驟數量是10,最好根據自己程序性能和正確性的平衡來修改)
模擬完,還要更新各物體的渲染邏輯位置角度信息。
(物理引擎的位置角度信息和渲染邏輯的位置角度信息是分別獨立的,物理模擬后須將物理引擎的位置角度信息賦給渲染邏輯的位置角度信息)
本文假設主循環函數為void updateScene(float deltaTime);
void updateScene(float deltaTime) { //主循環函數的其它內容(一般是邏輯處理) //balabala..... //物理世界模擬 //通過10次子步驟求解,模擬出deltaTime后的物理世界變化。 dynamicsWorld->stepSimulation(deltaTime, 10); //更新物理世界每一個物體 auto & objectArray = dynamicsWorld->getCollisionObjectArray(); for (int i = 0; i < objectArray.size(); ++i) { //處於不活動狀態或者是靜態剛體的話,則不處理 if (!objectArray[i]->isActive() || objectArray[i]->isStaticObject())continue; Transform* object = reinterpret_cast<Transform*>(objectArray[i]->getUserPointer()); //沒有用戶指針的話,則不處理 if (!object)continue; //更新目標物體的位置 const auto & pos = objectArray[i]->getWorldTransform().getOrigin(); object->setPosition(pos.x(), pos.y(), pos.z()); //更新目標物體的旋轉角度 const auto & rotationM = objectArray[i]->getWorldTransform().getRotation(); object->setRotation(rotationM.getX(), rotationM.getY(), rotationM.getZ(), rotationM.getW()); } //主循環函數的其他內容(一般是渲染) //bala...... }
如果成功的話我們就能模擬出一個帶重力的物理世界,
生成好地板剛體,球剛體,並把球設置在高空,那么我們將通過圖形渲染方法會看到球受重力影響下落的物理效果。
|5、刪除剛體
此外,在游戲過程中,也存在可能中途刪除物體的情況。
由於物理引擎和渲染邏輯是分別獨立的,要刪除一個物體,則不僅需要在渲染邏輯上刪除,還要在物理引擎上刪除它的剛體。
一個值得參考的方法是在遍歷物理世界所有剛體的時候,檢測刪除標記並刪除相應的剛體:
void updateScene(float dt) { //主循環函數的其它內容(一般是邏輯處理) //balabala..... //模擬步長 m_dynamicsWorld->stepSimulation(dt,10); auto & objectArray = m_dynamicsWorld->getCollisionObjectArray(); //更新物理世界每一個物理物體 for(int i =0; i < objectArray.size();++i) { //清除待刪除物理剛體 int entityState = reinterpret_cast<int>(objectArray[i]->getUserPointer()); //本文將待刪除物理剛體的用戶指針指向Entity::NoEntity(-1值)作為待刪除標記,也可用其它來作為標記 if (entityState == Entity::NoEntity) { m_dynamicsWorld->removeCollisionObject(objectArray[i]); --i;//刪除后要退回一位 continue; } //不存在用戶指針或者睡眠中,則不處理 if (!objectArray[i]->isActive()|| objectArray[i]->isStaticObject()){ continue; } Transform* object= reinterpret_cast<Transform*>(entityState); if (!object){ continue; } //更新目標物體的位置 const auto & pos = objectArray[i]->getWorldTransform().getOrigin(); object->setPosition(pos.x(), pos.y(), pos.z()); //更新目標物體的旋轉角度 const auto & rotationM = objectArray[i]->getWorldTransform().getRotation(); object->setRotation(Vector4f(rotationM.getX(), rotationM.getY(), rotationM.getZ(),rotationM.getW())); } }
|參考
BulletPhysics 官網 https://pybullet.org/wordpress/
BulletPhysics Github https://github.com/bulletphysics/bullet3
BulletPhysics 快速入門文檔: https://docs.google.com/document/d/10sXEhzFRSnvFcl3XxNGhnD4N2SedqwdAvK3dsihxVUA/edit#heading=h.2ye70wns7io3
