Bullet教程 Bullet大开发 第一章——解读Bullet的HelloWorld程序

Bullet教程 Bullet大开发 第一章——解读Bullet的HelloWorld程序

HelloWorld程序介绍

Bullet官方的HelloWorld程序是入门Bullet物理引擎的必须了解的程序。作为程序员,应该知道自己的第一个程序就是从HelloWorld程序开始,Bullet物理引擎亦是如此,只不过我真的没有在程序里看到所谓的“HelloWorld”……
由于Bullet引擎是由AMD公司开发的,所以程序的注释内容相应的,全都是英文。这里我只是做一下注释翻译及一点点的说明。
现在废话不多说,开始解说HelloWorld程序!

当我们配置好Bullet的链接库并包含HelloWorld程序所需的<btBulletDynamicsCommon.h>头文件之后,程序的开端是对Bullet的一些初始化:

/// 冲突配置包含内存的默认设置,冲突设置。高级用户可以创建自己的配置。
btDefaultCollisionConfiguration* collisionConfiguration = new btDefaultCollisionConfiguration();

/// 使用默认的冲突调度程序。对于并行处理,您可以使用不同的分派器(参见Extras/BulletMultiThreaded)
btCollisionDispatcher* dispatcher = new	btCollisionDispatcher(collisionConfiguration);

/// btDbvtBroadphase是一种很好的通用的两步法碰撞检测。你也可以尝试btAxis3Sweep。
btBroadphaseInterface* overlappingPairCache = new btDbvtBroadphase();

/// 默认约束求解器。对于并行处理,您可以使用不同的解决程序(参见Extras/BulletMultiThreaded)
btSequentialImpulseConstraintSolver* solver = new btSequentialImpulseConstraintSolver;
	
btDiscreteDynamicsWorld* dynamicsWorld = 
new btDiscreteDynamicsWorld(dispatcher,overlappingPairCache,solver,collisionConfiguration);
	
dynamicsWorld->setGravity(btVector3(0,-10,0));

这里的程序有点鱼龙混杂,而我们真正需要的是指向btDiscreteDynamicsWorld的指针,它用于表示刚体碰撞的世界。他的公有成员函数setGravity用于设置重力系数及方向,这里取的是g = 10 N/kg,然而精确值是g = 9.801 N/kg(这是北京地区的重力系数值,不同地区的重力系数值不一样)。由于我们默认3D世界中正y值就是向上的方向,这就是为什么重力系数只设置在y轴上。
如果你想模拟月球或火星上的物理场景,你可以适当地调小或调大重力系数值。

初始化完Bullet之后,就是创建一些基本的刚体了:

// 跟踪形状,以便于我们在退出时释放内存。
// 只要有可能,一定要在刚体中重复使用碰撞形状!
btAlignedObjectArray<btCollisionShape*> collisionShapes;

// 地面是在y=-56的位置上的一个立方体。
// 球在y=-6处击中,中心为-5
{
btCollisionShape* groundShape = new btBoxShape(btVector3(btScalar(50.),btScalar(50.),btScalar(50.)));
collisionShapes.push_back(groundShape);

btTransform groundTransform;
groundTransform.setIdentity();
groundTransform.setOrigin(btVector3(0,-56,0));

btScalar mass(0.);

	// 刚体是动态的如果且仅当质量为非零时,否则是静止的
	bool isDynamic = (mass != 0.f);

	btVector3 localInertia(0,0,0);
	if (isDynamic)
		groundShape->calculateLocalInertia(mass, localInertia);

	// 使用MotionState是可选的,它提供了插值功能,并且只同步“活动”对象
	btDefaultMotionState* myMotionState = new btDefaultMotionState(groundTransform);
	btRigidBody::btRigidBodyConstructionInfo rbInfo(mass,myMotionState, groundShape, localInertia);
	btRigidBody* body = new btRigidBody(rbInfo);

	// 将物体添加到动力学世界
	dynamicsWorld->addRigidBody(body);
}

这里,我们首先创建了一个btCollisionShape*的btAlignedObjectArray数组用于跟踪形状,作用如注释所示。实际上你可以用C++的std::vector来替代btAlignedObjectArray,但对于btAlignedObjectArray,Bullet用户手册说用它有一些好处(貌似),姑且就用着吧。
接着,我们创建了一个盒子形状的50 x 50 x 50大小的碰撞形状作为地面。你也可以尝试用其他形状作为地面,具体内容我们会放在后面来讲,你也可以参照Bullet用户手册的第4节Bullet Collision Detection(碰撞检测)的Convex Primitives (凸原语(有道的奇葩翻译))小节来了解Bullet所支持的碰撞形状。回过正题,在我们创建完碰撞形状之后,将其附加至上面所创建的btAlignedObjectArray数组,以进行跟踪。
然后,我们创建一个btTransform用于表示该碰撞对象在世界坐标系中的变换。btTransform的方法setIdentity官方解释为”Set this transformation to the identity.”(有道翻译:将这个转换设置为identity。),其实我真不懂这个的意思,但这个方法常常用在对btTransform进行变换操作之前。方法setOrigin顾名思义,就是进行位移变换。需要注意的是,Bullet用户手册里描述道:“btTransform是一个位置和一个方向的组合。……不允许缩放或剪切。”(有道翻译)。
紧接着,就是对该碰撞对象的质量(mass)进行设置。质量越大,碰撞对象就越重,就像实际生活一样子。如果质量为0,则该碰撞对象为静态对象。你可能不希望地面坍塌吧,所以将不必进行运动的碰撞对象的质量设置为0是个明智的选择。
注释说MotionState是可选,实际上我并不知道怎么对MotionState进行省略。其实MotionState有一些功能,我会放到后面来讲。
最后就是创建刚体了。想进行物理模拟,就必须创建刚体(或软体)。刚体对象用btRigidBody表示。创建完刚体对象之后,记得把刚体对象附加到动力学世界中哦。

创建了静态刚体对象(地面)之后,就要为世界增添一点“活力”了——我们创建一个动态的刚体对象,用于模拟物体下落运动:

{
	// 创建一个动态的刚体
	
	// btCollisionShape* colShape = new btBoxShape(btVector3(1,1,1));
	btCollisionShape* colShape = new btSphereShape(btScalar(1.));
	collisionShapes.push_back(colShape);

	/// 创建动态对象
	btTransform startTransform;
	startTransform.setIdentity();

	btScalar mass(1.f);

	// 刚体是动态的如果且仅当质量为非零时,否则是静止的
	bool isDynamic = (mass != 0.f);

	btVector3 localInertia(0,0,0);
	if (isDynamic)
		colShape->calculateLocalInertia(mass,localInertia);

	startTransform.setOrigin(btVector3(2,10,0));
		
	// 推荐使用motionstate,它提供插值功能,只同步“活动”对象
	btDefaultMotionState* myMotionState = new btDefaultMotionState(startTransform);
	btRigidBody::btRigidBodyConstructionInfo rbInfo(mass,myMotionState,colShape,localInertia);
	btRigidBody* body = new btRigidBody(rbInfo);

	dynamicsWorld->addRigidBody(body);
}

不知道大家有没有注意到,碰撞形状由btBoxShape变为btSphereShape,btSphereShape的碰撞形状为球体。

接下来,我们需要一个循环用于进行物理模拟,并打印出两个刚体的位置(相对于世界坐标系)

///-----开始分段模拟-----
	for (int i=0; i<150; i++)
	{
		dynamicsWorld->stepSimulation(1.f/60.f,10);
		
		// 打印所有对象的位置
		for (int j=dynamicsWorld->getNumCollisionObjects()-1; j>=0; j--)
		{
			btCollisionObject* obj = dynamicsWorld->getCollisionObjectArray()[j];
			btRigidBody* body = btRigidBody::upcast(obj);
			btTransform trans;
			if (body && body->getMotionState())
				body->getMotionState()->getWorldTransform(trans);
			else
				trans = obj->getWorldTransform();
			printf("world pos object %d = %f,%f,%f\n" 
				,j ,float(trans.getOrigin().getX()), float(trans.getOrigin().getY()), float(trans.getOrigin().getZ()));
		}

“dynamicsWorld->stepSimulation(1.f/60.f,10);”这条语句用于进行单步的物理模拟,并更新刚体对象的数据,它的参数为单步模拟的步长,用计算机图形学的话讲就是两个动画帧更新之间的时差(单位为:秒),用这个时差去除1秒就可以算出FPS。
方法getNumCollisionObjects()顾名思义,就是获取刚体世界的刚体数量。
方法getCollisionObjectArray()用于获取刚体的碰撞对象的数组。注意,碰撞对象(btCollisionObject)不同于碰撞形状(btCollisionShape)。我们在每次循环中逐个获取刚体的碰撞对象,并对它进行提升转换(upcast)。碰撞对象转换为刚体对象后,我们就可以大肆地操作刚体对象了。
我们定义了一个btTransform(矩阵)用于获取刚体对象相对于世界坐标系的变换。定义完矩阵之后,我们检查刚体对象是否附加了MotionState,如果有,则获取刚体对象的MotionState的变换;否则,就只获取刚体的碰撞对象的变换。
获取了变换之后,就对刚体对象的位置进行打印。btTransform的方法getOrigin用于获取该变换的位置信息(类型为btVector3),btVector3的方法getX/Y/Z用于获取相应的坐标轴值。

完成物理模拟后,在退出程序之前,我们需要对我们所犯下的罪行进行忏悔(其实是释放资源啦):

// 在创建/初始化的反向顺序中清理
	///-----开始清除-----
	// 从动力学世界中移除刚体,并删除它们
	for (int i=dynamicsWorld->getNumCollisionObjects()-1; i>=0 ;i--)
	{
		btCollisionObject* obj = dynamicsWorld->getCollisionObjectArray()[i];
		btRigidBody* body = btRigidBody::upcast(obj);
		if (body && body->getMotionState())
		{
			delete body->getMotionState();
		}
		dynamicsWorld->removeCollisionObject( obj );
		delete obj;
	}
	// 删除碰撞形状
	for (int j=0;j<collisionShapes.size();j++)
	{
		btCollisionShape* shape = collisionShapes[j];
		collisionShapes[j] = 0;
		delete shape;
	}
	// 删除动态世界
	delete dynamicsWorld;
	// 删除器
	delete solver;
	// 删除broadphase
	delete overlappingPairCache;
	// 删除调度程序
	delete dispatcher;
	delete collisionConfiguration;
	// 下一行是可选的:当阵列超出范围时,它将被析构函数清除。
	collisionShapes.clear();

释放资源就不用我说了,照着程序来就行了。需要注意的是“在创建/初始化的反向顺序中清理”!

对HelloWorld程序进行改进

Bullet的HelloWorld程序多无聊啊!就只是简简单单的文字输出而已,毫无视觉体验!我们接下来我们就来增添一点视觉体验吧!
由于篇幅问题,我们这里就省略全部的视觉创建过程,不过我有一个法宝要传授给大家。
btTransform有一个方法getOpenGLMatrix用于获取btTransform的数据数组,也就是我们可以这样使用它:

glm::mat4 model = glm::mat4(1.0f);
trans.getOpenGLMatrix(glm::value_ptr(model));

注意:这里使用了OpenGL的数学库GLM,对于其他数学库,只需要传递矩阵的数据数组的指针即可。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值