獲取示例代碼
前言
本文將介紹3D物理引擎Bullet的基本使用方式以及如何將之前的OpenGL渲染代碼和Bullet相結合,制造一個符合物理運動規則的虛擬3D場景。下面是效果圖。
Bullet
Bullet是一個開源的物理引擎,使用C++編寫,可以前往它的Github地址查看它的最新源代碼。最新的Bullet是Bullet3,不過本文并沒有采用新的版本,而是使用的Bullet2的代碼。例子中通過源代碼集成的方式集成Bullet,將核心代碼直接拷貝到iOS項目中。
集成Bullet
首先clone Bullet的源代碼。將下面的部分拷貝到iOS項目中。
我將它拷貝到了項目的
Physics Engine/Bullet
目錄下。然后在Build Setting中添加用戶頭文件搜索路徑。這是為了讓Bullet的代碼可以include到正確的文件。
配置物理引擎
我創建了PhysicsEngine
類封裝Bullet的主要功能,因為Bullet是C++寫的,我不希望我的其他OC代碼直接使用C++代碼,這會使得我的項目里面到處都是mm文件。目前的例子很簡單,PhysicsEngine
做的事情就是初始化物理世界,向物理世界添加剛體和同步剛體的變換矩陣這幾件事。下面是構建一個物理世界需要的幾個重要變量,btDiscreteDynamicsWorld
是最終要生成的物理世界,btDbvtBroadphase
用來執行快速碰撞檢測, btDefaultCollisionConfiguration
將會執行更加細粒度的碰撞檢測。btSequentialImpulseConstraintSolver
會解決受力,約束等復雜問題,往往是性能的瓶頸,所以會有一些并行的版本。
btDefaultCollisionConfiguration *configration;
btCollisionDispatcher *dispatcher;
btSequentialImpulseConstraintSolver *solver;
btDbvtBroadphase *broadphase;
btDiscreteDynamicsWorld *world;
在init
中初始化這些變量。并且設置重力world->setGravity(btVector3(0,-9.8,0));
。我設置了一個比較仿真的值-9.8,方向是y軸向下的。
- (instancetype)init
{
self = [super init];
if (self) {
configration = new btDefaultCollisionConfiguration();
dispatcher = new btCollisionDispatcher(configration);
solver = new btSequentialImpulseConstraintSolver();
broadphase = new btDbvtBroadphase();
world = new btDiscreteDynamicsWorld(dispatcher,broadphase,solver,configration);
world->setGravity(btVector3(0,-9.8,0));
rigidBodies = [NSMutableSet new];
}
return self;
}
剛體(Rigidbody)
剛體是物理引擎中一個重要的概念,你可以把它當作一個不會形變,質量固定的物體,它有自己的形狀,比如一個木箱子。在Bullet中,使用btRigidBody
來表示剛體,將剛體加入物理世界,他就會在重力以及其他力的驅使下運動了。下面是加入新的剛體的代碼。
- (void)addRigidBody:(RigidBody *)rigidBody {
btTransform defaultTransform = btTransformFromGLK(rigidBody.rigidBodyTransform);
btDefaultMotionState *motionState = new btDefaultMotionState(defaultTransform);
btVector3 fallInertia(0,0,0);
btCollisionShape *collisionShape = [self buildCollisionShape: rigidBody];
collisionShape->calculateLocalInertia(rigidBody.mass, fallInertia);
btRigidBody *btrigidBody = new btRigidBody(rigidBody.mass, motionState, collisionShape, fallInertia);
btrigidBody->setRestitution(rigidBody.restitution);
btrigidBody->setFriction(rigidBody.friction);
world->addRigidBody(btrigidBody);
btrigidBody->setUserPointer((__bridge void *)rigidBody);
rigidBody.rawBtRigidBodyPointer = btrigidBody;
[rigidBodies addObject:rigidBody]; // 保證對rigidBody的持有
}
RigidBody
是我自己建立的OC類,目的也是為了避免OC代碼和C++代碼的過多交互。主要為了封裝btRigidBody
所需要的基本數據以及同步對應btRigidBody
的模型變換矩陣。這樣OpenGL繪制代碼只要和RigidBody
類交互即可。btRigidBody
的創建需要質量(mass),初始狀態(motionState),剛體的形狀(collisionShape),恢復系數(Restitution),摩擦系數(Friction)等等。本文只使用了Box這個形狀,在buildCollisionShape
中創建。
- (btCollisionShape *)buildCollisionShape:(RigidBody *)rigidBody {
if (rigidBody.rigidbodyShape.type == RigidBodyShapeTypeBox) {
GLKVector3 boxSize = rigidBody.rigidbodyShape.shapes.box.size;
return new btBoxShape(btVector3(boxSize.x / 2.0, boxSize.y / 2.0, boxSize.z / 2.0));
}
return new btSphereShape(1.0);
}
最后在update
方法中同步剛體的變換矩陣。
- (void)update:(NSTimeInterval)deltaTime {
// deltaTime is Seconds
world->stepSimulation((btScalar)deltaTime);
[self syncRigidBodies];
}
...
- (void)syncRigidBodies {
for (RigidBody * rigidBody in rigidBodies) {
btRigidBody *btrigidBody = (btRigidBody *)rigidBody.rawBtRigidBodyPointer;
rigidBody.rigidBodyTransform = glkTransformFromBT(btrigidBody->getWorldTransform());
}
}
結合物理和繪制
通過PhysicsEngine
已經可以在物理世界里模擬出任意個Box的運動情況了,接下來我們要將之前繪制的幾何體和剛體綁定起來。我創建了GameObject
類綁定兩者。
@interface GameObject : NSObject
@property (strong, nonatomic) GLObject * geometry;
@property (strong, nonatomic) RigidBody * rigidBody;
- (instancetype)initWithGeometry:(GLObject *)geometry rigidBody:(RigidBody *)rigidBody;
- (void)update:(NSTimeInterval)deltaTime;
這個類要做的就是將剛體的變換信息同步給幾何體的ModelMatrix
。不過剛體并不需要同步縮放變換,所以代碼中將它特別提取了出來。
- (instancetype)initWithGeometry:(GLObject *)geometry rigidBody:(RigidBody *)rigidBody
{
self = [super init];
if (self) {
self.geometry = geometry;
self.rigidBody = rigidBody;
self.rigidBody.rigidBodyTransform = geometry.modelMatrix;
// 提取出原始的縮放分量,平移和旋轉交給物理引擎去處理
originGeometryMatrix = GLKMatrix4MakeScale(self.geometry.modelMatrix.m00, self.geometry.modelMatrix.m11, self.geometry.modelMatrix.m22);
}
return self;
}
- (void)update:(NSTimeInterval)deltaTime {
if (self.rigidBody) {
if (self.geometry) {
self.geometry.modelMatrix = GLKMatrix4Multiply(self.rigidBody.rigidBodyTransform, originGeometryMatrix);
[self.geometry update:deltaTime];
}
}
}
最后一步
最后回到ViewController
,初始化一個物理引擎PhysicsEngine
,創建一個質量為0的Box作為地板,每次點擊屏幕創建一個新的質量為1的Box。
// Physics
self.physicsEngine = [PhysicsEngine new];
// Static Floor
[self createPhysicsCube: GLKVector3Make(8, 0.2, 8) mass:0.0 position:GLKVector3Make(0, 0, 0)];
...
- (void)createPhysicsCube:(GLKVector3)size mass:(float)mass position:(GLKVector3)position {
UIImage *diffuseImage = [UIImage imageNamed:@"texture.jpg"];
GLKTextureInfo *diffuseMap = [GLKTextureLoader textureWithCGImage:diffuseImage.CGImage options:nil error:nil];
Cube *cube = [[Cube alloc] initWithGLContext:self.glContext diffuseMap:diffuseMap normalMap:diffuseMap];
cube.modelMatrix = GLKMatrix4Multiply(GLKMatrix4MakeTranslation(position.x, position.y, position.z), GLKMatrix4MakeScale(size.x, size.y, size.z));
RigidBody *rigidBody = [[RigidBody alloc] initAsBox:size];
rigidBody.mass = mass;
GameObject *gameObject = [[GameObject alloc] initWithGeometry:cube rigidBody:rigidBody];
[self.physicsEngine addRigidBody:rigidBody];
[self.objects addObject:gameObject];
}
...
#pragma mark - Touch Event
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self createPhysicsCube: GLKVector3Make(0.5, 0.5, 0.5) mass:1.0 position:GLKVector3Make(0, 4, 0)];
}
當然不要忘了在update
中調用PhysicsEngine
的update
,這樣物理引擎才能運轉起來。
- (void)update {
[super update];
[self.physicsEngine update: self.timeSinceLastUpdate];
...
}
總結
本人對Bullet的了解目前還是比較淺顯的,所以本文只是介紹了Bullet物理引擎的冰山一角,想要了解更多可以去查閱官方文檔。多去使用一下其他游戲引擎里的物理引擎也是加深你對物理引擎概念了解的好方法。