在C++图形程序里让物体掉下来、撞开、堆叠,并不是简单写几个重力公式就能稳定的。真实交互需要连续碰撞、摩擦、约束求解,这些底层数值工作如果自己造轮子,很容易出现抖动、穿透和性能崩塌。物理引擎正是为解决这类问题而生,它提供刚体、软体、关节等抽象,并以固定步长迭代保证可复现的仿真结果。

主流C++物理引擎特性对比
当前C++生态中常见的物理引擎有Bullet、Box2D、PhysX和ODE。Bullet是开源BSD协议,支持三维刚体与软体,跨Windows、Linux、macOS及移动端,社区示例丰富,适合独立游戏与仿真工具。Box2D专注二维,接口极简,移动端跑起来非常省电,很多二维手机游戏靠它做平台跳跃和道具碰撞。
PhysX由NVIDIA维护,在PC和主机上利用SIMD与GPU加速,大规模破碎和布料效果突出,但授权与集成复杂度高于Bullet。ODE是比较老的开源引擎,目前维护放缓,新项目很少首选。下表列出关键差异,帮助你在图形项目中快速判断。
| 引擎 | 维度 | 协议 | 典型场景 |
|---|---|---|---|
| Bullet | 3D | BSD | 跨平台三维仿真 |
| Box2D | 2D | MIT | 二维休闲游戏 |
| PhysX | 3D | 专有 | 主机大作、特效 |
选型时还要看构建系统。Bullet提供CMake,能直接链进你的C++工程;Box2D也易集成。若项目只用OpenGL画立方体,Bullet的最小依赖就能满足。不要因为PhysX名字响就盲目引入,它的SDK体积和线程模型会增加图形层以外的工作量。
Bullet引擎的基础接入流程
以Bullet为例,接入分四步:创建btDiscreteDynamicsWorld世界、注册碰撞配置与调度器、加入地面与刚体形状、在渲染循环里调用stepSimulation。世界对象持有所有物体并负责Broadphase,碰撞对在这里被筛出再做窄相检测。
下面代码展示最小可运行骨架。注意btDefaultCollisionConfiguration和btSequentialImpulseConstraintSolver是必选组件,缺少会导致链接或运行崩溃。形状用btBoxShape时半边长要按米制给,否则重力感觉会怪异。
#include <btBulletDynamicsCommon.h>
int main() {
btDefaultCollisionConfiguration config;
btCollisionDispatcher dispatcher(&config);
btDbvtBroadphase broadphase;
btSequentialImpulseConstraintSolver solver;
btDiscreteDynamicsWorld world(&dispatcher, &broadphase, &solver, &config);
world.setGravity(btVector3(0, -9.8, 0));
btBoxShape* groundShape = new btBoxShape(btVector3(50, 1, 50));
btDefaultMotionState* groundMotion = new btDefaultMotionState(btTransform(btQuaternion(0,0,0,1), btVector3(0,-1,0)));
btRigidBody::btRigidBodyConstructionInfo groundInfo(0, groundMotion, groundShape, btVector3(0,0,0));
btRigidBody groundBody(groundInfo);
world.addRigidBody(&groundBody);
btBoxShape* boxShape = new btBoxShape(btVector3(1,1,1));
btDefaultMotionState* boxMotion = new btDefaultMotionState(btTransform(btQuaternion(0,0,0,1), btVector3(0,5,0)));
btScalar mass = 1;
btVector3 inertia(0,0,0);
boxShape->calculateLocalInertia(mass, inertia);
btRigidBody::btRigidBodyConstructionInfo boxInfo(mass, boxMotion, boxShape, inertia);
btRigidBody boxBody(boxInfo);
world.addRigidBody(&boxBody);
for (int i = 0; i < 60; i++) {
world.stepSimulation(1.0f / 60.0f, 10);
}
return 0;
}
代码里stepSimulation第一参数是固定步长,第二参数是最大子步。固定步长非常关键,图形帧率波动时若直接传可变dt,求解器会失稳。很多新手把deltaTime从GLFW直接塞进去,结果盒子高速穿过地面,这正是隧道效应。
刚体变换的同步也需注意。Bullet的btMotionState在stepSimulation后更新世界矩阵,你在OpenGL里应读取getWorldTransform并转成glm::mat4送到着色器。不要把图形坐标和物理坐标混用同一套缩放,物理世界以米为单位更不容易出数值问题。
图形层与物理层的数据同步策略
物理引擎只算位置旋转,画面靠图形API画。常见做法是每帧先stepSimulation固定步,再遍历所有btRigidBody取变换写回渲染对象。若图形端用场景树,可让每个节点持有一个btRigidBody*指针,更新时直接映射。
当物体数量大时,避免每帧new矩阵。预分配std::vector<glm::mat4>缓冲,按索引写入,渲染时绑定缓冲即可。Bullet自身不依赖图形库,这种解耦让你随时换Vulkan或软件光栅化而不动物理代码。
另一个易错点是静态物体也走MotionState。地面质量设0即为静态,但依然要加进世界,否则动态体下落会无视它。调试阶段可开Bullet的btIDebugDraw接口,把线框画到OpenGL,确认碰撞体和你看到的网格一致,很多视觉穿模其实只是图形模型原点偏了。
最后谈性能。Broadphase用btDbvtBroadphase已能应付数千物体;若做粒子或碎块过万,考虑PhysX或并行求解器。C++多线程下Bullet的世界对象不要跨线程step,可开多个世界做分区。物理频率通常设60Hz,图形可更高,中间用插值补帧,这样既能稳仿真又不卡画面。