Bullet物理引擎在OpenGL中的应用

在开发OpenGL的应用之时, 难免要遇到使用物理来模拟OpenGL中的场景内容. 由于OpenGL仅仅是一个关于图形的开发接口, 因此需要通过第三方库来实现场景的物理模拟. 目前我选择 Bullet 物理引擎, 其官方网站为 Bullet, 开发库的下载地址则在 github 上.


1. OpenGL 环境

首先我们需要搭建框架, OpenGL 的基本框架这里不详述, 我个人是在几何着色器内实现光照, 这是由于我实现的是面法线. 另外用到的其他三方库有 GLFW 和 GLM库, 前者有助于管理OpenGL窗口, 后者省却了自己写数学公式代码的过程. 另外实现了立方体模型和球体的创建, 满足学习 Bullet 的需要即可.

立方体模型


2. 物理环境的初始化

对于 Bullet 物理库而言, 它的搭建也很简单, 在初始化 OpenGL 的上下文的时候, 也可以初始化我们的物理环境, 参考物理库自带的 HelloWorld 即可, 相关代码为:

    ///collision configuration contains default setup for memory, collision setup
    m_collisionConfiguration = new btDefaultCollisionConfiguration();
    //m_collisionConfiguration->setConvexConvexMultipointIterations();

    ///use the default collision dispatcher. For parallel processing you can use a diffent dispatcher (see Extras/BulletMultiThreaded)
    m_dispatcher = new  btCollisionDispatcher(m_collisionConfiguration);

    m_broadphase = new btDbvtBroadphase();

    ///the default constraint solver. For parallel processing you can use a different solver (see Extras/BulletMultiThreaded)
    btSequentialImpulseConstraintSolver* sol = new btSequentialImpulseConstraintSolver;
    m_solver = sol;

    m_dynamicsWorld = new btDiscreteDynamicsWorld(m_dispatcher, m_broadphase, m_solver, m_collisionConfiguration);

    m_dynamicsWorld->setGravity(btVector3(0, -10, 0));

3. 物理环境的销毁

相应的, 还有清理代码

    std::vector<ModelInfo>::iterator it = m_models.begin();
    while(it != m_models.end())
    {
        ModelInfo info = *it;
        delete info.model;
        m_dynamicsWorld->removeRigidBody(info.obj);
        it++;
    }
    m_models.clear();

    for (int j=0;j<m_collisionShapes.size();j++)
    {
        btCollisionShape* shape = m_collisionShapes[j];
        m_collisionShapes[j] = 0;
        delete shape;
    }
    //delete dynamics world
    delete m_dynamicsWorld;

    //delete solver
    delete m_solver;

    delete m_broadphase;

    delete m_dispatcher;

    delete m_collisionConfiguration;

    //next line is optional: it will be cleared by the destructor when the array goes out of scope
    m_collisionShapes.clear();

在我的实现内, 结构体 ModelInfo 是一个自定义的结构体(struct), 我通过该结构体的容器保存了所有物体的物理模型以及其对应OpenGL模型的关系, 这样在物理库更新一个物体的位置和方向时, 我们就可以在 OpenGL 内更新物体的位置和方向.

4. 物理世界的渲染

下面是我渲染物理世界中所有模型的代码, 计算出所有模型的变换矩阵, 而后通知其相关代码进行渲染.

void PhysicsBaseWorld::render()
{
    std::vector<ModelInfo>::iterator it = m_models.begin();
    while(it != m_models.end())
    {
        ModelInfo info = *it;
        btRigidBody* obj = info.obj;
        btRigidBody* body = btRigidBody::upcast(obj);

        btTransform trans;
        if (body && body->getMotionState())
        {
            body->getMotionState()->getWorldTransform(trans);

        } else
        {
            trans = obj->getWorldTransform();
        }
        glm::vec3 position = glm::vec3(float(trans.getOrigin().getX()),float(trans.getOrigin().getY()),float(trans.getOrigin().getZ()));
        btQuaternion rot = trans.getRotation();
        glm::quat q = glm::quat(rot.getW(), rot.getX(), rot.getY(), rot.getZ());
        glm::mat4 rot4 = glm::toMat4(q);
        glm::mat4 m = glm::translate(glm::mat4(1.0), position) * rot4;

        Model* model = info.model;
        model->setModelMat(m);
        model->render();        
        it++;
    }
}

5. 绘制静态的物体

在创建物理世界的过程中, 物理库中主要使用函数 createRigidBody() 来创建刚体模型, 其主要有三个参数, 分别表示质量, 变换, 形状. 其中质量为 0 的物体为静止物体, 可以用来创建地面或者路边的石头之类的物体模型. 创建静止立方体模型的代码

    ///create a few basic rigid bodies
    btCollisionShape* shape = new btBoxShape(btVector3(halfsize[0],halfsize[1],halfsize[2]));

    m_collisionShapes.push_back(shape);

    btTransform transform;
    transform.setIdentity();
    transform.setOrigin(btVector3(pos[0], pos[1], pos[2]));

    {
        btScalar mass(0.);
        btRigidBody* body = createRigidBody(mass,transform,shape);

        Cube* cube = new Cube(halfsize[0],halfsize[1],halfsize[2]);
        cube->setColor(col);
        ModelInfo info = {body, cube};
        m_models.push_back(info);
    }

上面的代码创建的过程中, 我同时创建了一个对应的 OpenGL 立方体.

6. 绘制可活动的物体

创建可活动模型时需要设置相关的运动状态信息

    btAssert((!shape || shape->getShapeType() != INVALID_SHAPE_PROXYTYPE));

    //rigidbody is dynamic if and only if mass is non zero, otherwise static
    bool isDynamic = (mass != 0.f);

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

    btDefaultMotionState* myMotionState = new btDefaultMotionState(startTransform);

    btRigidBody::btRigidBodyConstructionInfo cInfo(mass, myMotionState, shape, localInertia);

    btRigidBody* body = new btRigidBody(cInfo);

    body->setUserIndex(-1);
    m_dynamicsWorld->addRigidBody(body);

    btBoxShape* s = dynamic_cast<btBoxShape*>(shape);
    if(s != 0)
    {
        btVector3 size = s->getHalfExtentsWithMargin();
        Cube* cube = new Cube(size.getX(), size.getY(), size.getZ());
        cube->setColor(col);
        ModelInfo info = {body, cube};
        m_models.push_back(info);
    }

    return body;

7. 更新物理世界

最后我们需要时刻更新物理世界中的模型位置和方位

m_dynamicsWorld->stepSimulation(elpasedTime, 0);

演示模型