前言:最近在接触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