Bullet Physics 三维碰撞检测:从原理到工程实践

发布时间:2026/8/6 8:22:56
Bullet Physics 三维碰撞检测:从原理到工程实践 1. 从“撞上”到“检测”三维碰撞检测的工程意义在三维虚拟世界里一个角色能否走上台阶一颗子弹能否击中目标一辆赛车是否会撞上护栏这些看似简单的交互背后都依赖着一套复杂而精密的计算系统——三维碰撞检测。这不仅仅是游戏开发的核心更是工业仿真、虚拟现实、机器人路径规划等众多领域的基础设施。很多刚接触三维开发的朋友可能会觉得碰撞检测就是“两个物体碰上了”但实际工程中这远非一个简单的布尔判断。它涉及到空间划分、几何计算、性能优化等一系列问题处理不好轻则角色“穿墙”重则程序卡顿崩溃。今天要聊的Bullet Physics就是解决这些问题的行业标杆之一。它是一个开源的、跨平台的物理模拟库以其稳定、高效和功能全面而闻名被广泛应用于游戏如《侠盗猎车手V》、《荒野大镖客救赎2》的载具物理、电影特效如《阿凡达》的特效模拟以及各类仿真软件中。对于开发者而言Bullet 提供了一个强大的工具箱让我们不必从零开始推导复杂的物理公式就能实现逼真的刚体动力学、柔体、布料乃至车辆模拟。而这一切的起点就是碰撞检测。本文将从一个实践者的角度带你快速上手 Bullet并深入其碰撞检测的核心机制。我们会从最基础的场景搭建开始逐步拆解碰撞形状、世界管理、检测流程以及结果处理并分享在实际项目中容易踩到的“坑”和优化技巧。无论你是游戏开发者、仿真工程师还是对物理模拟感兴趣的技术爱好者这篇文章都将为你提供一个扎实的起点。2. 环境搭建与第一个碰撞世界在开始写代码之前我们需要先把 Bullet 库“请”到我们的项目中。Bullet 的官方维护非常活跃获取方式也很灵活。2.1 获取与编译 Bullet 库最推荐的方式是从其 GitHub 仓库获取源码并自行编译。这样做的好处是你可以根据目标平台Windows, Linux, macOS和编译环境MSVC, GCC, Clang进行最优化配置并且能方便地开启或关闭某些模块如柔体、多线程支持。git clone https://github.com/bulletphysics/bullet3.git cd bullet3Bullet 使用 CMake 作为构建系统。假设我们在 Linux/macOS 下创建一个构建目录并配置mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease -DBUILD_SHARED_LIBSON -DBUILD_BULLET2_DEMOSOFF -DBUILD_EXTRASOFF make -j4 sudo make install这里有几个关键参数-DCMAKE_BUILD_TYPERelease生成优化后的发布版本性能更好。-DBUILD_SHARED_LIBSON生成动态链接库.so 或 .dll方便多个项目共用。如果希望静态链接则设为OFF。-DBUILD_BULLET2_DEMOSOFF关闭官方示例的编译可以加快编译速度初学时可先关闭。-DBUILD_EXTRASOFF关闭一些额外工具和示例同样为了简化。在 Windows 下你可以使用 CMake GUI 工具生成 Visual Studio 解决方案文件.sln然后用 VS 打开并编译。编译安装完成后你会在系统库路径如/usr/local/lib找到libBulletCollision.so,libBulletDynamics.so等库文件头文件通常在/usr/local/include/bullet。在你的项目中需要正确链接这些库并包含头文件。注意如果你使用包管理器如 Ubuntu 的apt或 macOS 的Homebrew也可以直接安装预编译的包如apt install libbullet-dev但版本可能不是最新的且定制化选项较少。对于生产环境建议从源码编译以控制版本和特性。2.2 初始化物理世界一切的舞台物理世界btDiscreteDynamicsWorld是 Bullet 中所有物理实体刚体活动的舞台。它负责管理这些实体并逐步推进时间模拟物理过程。创建一个最简单的世界通常需要以下几个步骤#include btBulletDynamicsCommon.h int main() { // 1. 碰撞配置Collision Configuration // 它管理着碰撞算法的内存池和默认设置。对于大多数应用使用默认配置即可。 btDefaultCollisionConfiguration* collisionConfiguration new btDefaultCollisionConfiguration(); // 2. 碰撞调度器Collision Dispatcher // 它负责在碰撞对两个碰撞形状之间分配合适的碰撞检测算法。 // 例如一个盒子和一个球体碰撞它会自动选择 box-sphere 算法。 btCollisionDispatcher* dispatcher new btCollisionDispatcher(collisionConfiguration); // 3. 广相碰撞检测Broadphase // 这是性能优化的关键。它快速筛选出“可能”发生碰撞的物体对避免对所有物体两两进行精确计算窄相检测。 // btDbvtBroadphase 使用动态包围体层次树Dynamic Bounding Volume Hierarchy是通用且高效的选择。 btBroadphaseInterface* overlappingPairCache new btDbvtBroadphase(); // 4. 约束求解器Constraint Solver // 负责计算力、冲量并求解约束如关节、接触点以更新物体的位置和速度。 btSequentialImpulseConstraintSolver* solver new btSequentialImpulseConstraintSolver; // 5. 动力学世界Dynamics World // 将上述所有组件组合起来形成我们的物理世界。 btDiscreteDynamicsWorld* dynamicsWorld new btDiscreteDynamicsWorld( dispatcher, overlappingPairCache, solver, collisionConfiguration ); // 设置重力加速度单位米/秒²。Y轴向上负值表示向下。 dynamicsWorld-setGravity(btVector3(0, -9.8, 0)); // ... 在这里添加物体刚体 // 模拟循环通常放在游戏主循环中 for (int i 0; i 300; i) { // 模拟300步 dynamicsWorld-stepSimulation(1.f / 60.f, 10); // 每步模拟1/60秒最多10次子步 } // 程序结束时按创建顺序的逆序清理资源 delete dynamicsWorld; delete solver; delete overlappingPairCache; delete dispatcher; delete collisionConfiguration; return 0; }这段代码搭建了一个完整的物理模拟框架。stepSimulation函数是核心它驱动时间前进。其第一个参数是时间步长deltaTime通常与你的游戏帧时间一致如1/60秒。第二个参数是最大子步数Bullet 内部会将大的时间步拆分成更小的子步来保证模拟的稳定性尤其是在高速运动或复杂碰撞时。3. 碰撞形状定义物体的“物理边界”在 Bullet 中一个物体刚体的视觉模型渲染用的三角网格和它的物理边界是分离的。我们用一个更简单的几何形状碰撞形状btCollisionShape来近似表示这个物体的物理轮廓用于碰撞检测计算。这样做极大地提升了性能。3.1 基本碰撞形状Bullet 提供了多种基本形状适用于不同的物体盒子btBoxShape由半边长half extents定义。例如一个长宽高分别为2、1、1.5米的盒子其半边长向量为btVector3(1.0, 0.5, 0.75)。btCollisionShape* boxShape new btBoxShape(btVector3(1.0f, 0.5f, 0.75f));球体btSphereShape只需一个半径参数。btCollisionShape* sphereShape new btSphereShape(1.0f);胶囊体btCapsuleShape由半径和高度不含两端的半球定义。非常适合用于角色控制器因为它能平滑地滑过台阶和斜坡。btCollisionShape* capsuleShape new btCapsuleShape(0.5f, 2.0f); // 半径0.5米柱体高2米圆柱体btCylinderShape类似盒子由半边长定义但碰撞检测按圆柱处理。注意其朝向默认Y轴为高。圆锥体btConeShape、多面体btConvexHullShape等。3.2 复合碰撞形状与三角网格对于复杂物体基本形状可能无法准确匹配。这时有两种主要选择复合形状btCompoundShape将多个基本形状组合在一起。例如一把椅子可以由一个盒子的座面、四个圆柱体的腿组合而成。这比使用一个复杂的单一形状效率更高且更贴合实际。btCompoundShape* chairShape new btCompoundShape(); btTransform localTrans; localTrans.setIdentity(); // 添加座面盒子 localTrans.setOrigin(btVector3(0, 0.5, 0)); // 座面在本地坐标中的位置 chairShape-addChildShape(localTrans, new btBoxShape(btVector3(0.4, 0.05, 0.4))); // 添加一条腿圆柱 localTrans.setOrigin(btVector3(0.3, 0, 0.3)); chairShape-addChildShape(localTrans, new btCylinderShape(btVector3(0.03, 0.5, 0.03))); // ... 添加其他三条腿三角网格形状btBvhTriangleMeshShape用于完全静态的、细节复杂的场景如地形、建筑内部。它使用一个三角网格来精确表示碰撞表面。关键点btBvhTriangleMeshShape是不可移动的静态或运动学刚体且只能与凸形状如球、盒、胶囊进行碰撞检测不能与其他三角网格形状检测除非进行特殊配置但性能很差。btTriangleMesh* mesh new btTriangleMesh(); // 假设你有顶点数组 vertices 和索引数组 indices for (int i 0; i numTriangles; i) { int idx0 indices[i*3]; int idx1 indices[i*31]; int idx2 indices[i*32]; mesh-addTriangle( btVector3(vertices[idx0].x, vertices[idx0].y, vertices[idx0].z), btVector3(vertices[idx1].x, vertices[idx1].y, vertices[idx1].z), btVector3(vertices[idx2].x, vertices[idx2].y, vertices[idx2].z) ); } btBvhTriangleMeshShape* terrainShape new btBvhTriangleMeshShape(mesh, true); // true 表示使用量化AABB树优化查询实操心得形状选型原则能用简单不用复杂优先使用基本形状或其组合。一个胶囊体模拟的角色比用上千个三角面模拟的角色在碰撞检测上快几个数量级。动静分离静态且复杂的场景地形、房屋用btBvhTriangleMeshShape。动态物体用基本形状或复合形状。匹配视觉轮廓碰撞形状应尽量贴近视觉模型的轮廓尤其是玩家交互频繁的部位否则会产生“打不中”或“隔空碰撞”的糟糕体验。对于玩家角色胶囊体是黄金标准。4. 创建刚体赋予形状质量与运动有了碰撞形状我们还需要将其包装成刚体btRigidBody并为其设置质量、惯性、位置等属性它才能真正参与物理模拟。4.1 刚体创建流程// 1. 定义物体的初始变换位置和朝向 btTransform startTransform; startTransform.setIdentity(); startTransform.setOrigin(btVector3(0, 10, 0)); // 初始位置在 (0, 10, 0) // 2. 创建运动状态Motion State // 它负责同步物理引擎中刚体的变换与你渲染引擎中对应模型的变换。 // 这是一个可选但强烈推荐的组件它解耦了物理和渲染。 btDefaultMotionState* myMotionState new btDefaultMotionState(startTransform); // 3. 计算物体的局部惯性Local Inertia // 惯性决定了物体旋转的难易程度它依赖于物体的质量和碰撞形状。 btVector3 localInertia(0, 0, 0); float mass 1.0f; // 质量单位为千克 if (mass ! 0.f) { // 只有非零质量的动态物体才需要计算惯性 collisionShape-calculateLocalInertia(mass, localInertia); } // 4. 创建刚体构造信息 btRigidBody::btRigidBodyConstructionInfo rbInfo(mass, myMotionState, collisionShape, localInertia); // 5. 创建刚体 btRigidBody* body new btRigidBody(rbInfo); // 6. 可选设置附加属性 body-setRestitution(0.5f); // 恢复系数弹性0.0为完全非弹性1.0为完全弹性 body-setFriction(0.8f); // 摩擦系数 // 7. 将刚体添加到物理世界 dynamicsWorld-addRigidBody(body);4.2 刚体类型静态、动态与运动学通过设置质量mass我们可以定义刚体的类型静态刚体Static质量为零mass 0。它永远不会移动比如地面、墙壁。物理引擎会对其进行特殊优化。动态刚体Dynamic质量大于零。它会受到重力、力、碰撞的影响而运动。大部分可交互的物体都是动态的。运动学刚体Kinematic质量为零但你可以通过代码直接设置其位置和速度。它不会受力的影响但可以推动动态刚体。常用于电梯、移动平台或者由动画驱动的角色此时通常配合胶囊体形状。// 创建一个运动学刚体例如一个移动平台 btRigidBody::btRigidBodyConstructionInfo rbInfo(0, myMotionState, collisionShape, btVector3(0,0,0)); btRigidBody* kinematicBody new btRigidBody(rbInfo); kinematicBody-setCollisionFlags(kinematicBody-getCollisionFlags() | btCollisionObject::CF_KINEMATIC_OBJECT); kinematicBody-setActivationState(DISABLE_DEACTIVATION); // 防止休眠 // 在游戏循环中你可以手动更新它的位置 btTransform trans; kinematicBody-getMotionState()-getWorldTransform(trans); trans.setOrigin(newPosition); kinematicBody-getMotionState()-setWorldTransform(trans); kinematicBody-setWorldTransform(trans); // 同时更新物理世界的变换5. 碰撞检测结果获取与处理创建了世界和物体并让它们运动起来后我们如何知道两个物体发生了碰撞并获取碰撞的详细信息如碰撞点、法线呢Bullet 提供了几种方式。5.1 接触点检测Contact Point Processing最常用的是通过物理世界获取每帧的接触点信息。在stepSimulation之后我们可以遍历所有的“接触流形”Contact Manifold它包含了两个碰撞对象之间在当前帧的所有接触点。dynamicsWorld-stepSimulation(deltaTime, 10); // 获取碰撞世界的调度器 btDispatcher* dispatcher dynamicsWorld-getDispatcher(); int numManifolds dispatcher-getNumManifolds(); for (int i 0; i numManifolds; i) { btPersistentManifold* contactManifold dispatcher-getManifoldByIndexInternal(i); // 获取发生碰撞的两个物体 const btCollisionObject* obA static_castconst btCollisionObject*(contactManifold-getBody0()); const btCollisionObject* obB static_castconst btCollisionObject*(contactManifold-getBody1()); // 获取用户指针通常用于关联你的游戏对象 MyGameObject* objA static_castMyGameObject*(obA-getUserPointer()); MyGameObject* objB static_castMyGameObject*(obB-getUserPointer()); int numContacts contactManifold-getNumContacts(); for (int j 0; j numContacts; j) { btManifoldPoint pt contactManifold-getContactPoint(j); // 只处理“新的”或“持续的”接触点忽略已解决的 if (pt.getDistance() 0.f) { btVector3 ptA pt.getPositionWorldOnA(); // 在物体A上的碰撞点世界坐标 btVector3 ptB pt.getPositionWorldOnB(); // 在物体B上的碰撞点 btVector3 normalOnB pt.m_normalWorldOnB; // 从B指向A的碰撞法线单位向量 float impulse pt.getAppliedImpulse(); // 应用于此接触点的冲量大小 // 在这里处理你的游戏逻辑播放声音、造成伤害、触发事件等 if (objA objB) { objA-onCollision(objB, ptA, normalOnB, impulse); objB-onCollision(objA, ptB, -normalOnB, impulse); // 法线方向相反 } } } }5.2 光线投射Raycasting光线投射用于查询从一点沿某一方向第一个被击中的物体是什么。这在实现鼠标拾取、武器瞄准、视线检测等功能时非常有用。btVector3 rayFrom(0, 10, 0); // 射线起点 btVector3 rayTo(0, -10, 0); // 射线终点 btCollisionWorld::ClosestRayResultCallback rayCallback(rayFrom, rayTo); dynamicsWorld-rayTest(rayFrom, rayTo, rayCallback); if (rayCallback.hasHit()) { // 击中了某个物体 btVector3 hitPoint rayCallback.m_hitPointWorld; // 击中点 btVector3 hitNormal rayCallback.m_hitNormalWorld; // 击中点法线 const btCollisionObject* hitObject rayCallback.m_collisionObject; // 被击中的物体 MyGameObject* hitGameObj static_castMyGameObject*(hitObject-getUserPointer()); // ... 处理击中逻辑 }5.3 形状投射Sweep Test形状投射比光线投射更强大它允许你用一个形状如胶囊体沿着一条路径进行扫描检测是否会与场景中的物体碰撞。这是实现角色控制器移动、子弹体积检测的基石。// 假设我们用一个胶囊体形状进行扫描 btTransform startTrans, endTrans; startTrans.setIdentity(); startTrans.setOrigin(btVector3(0, 5, 0)); endTrans.setIdentity(); endTrans.setOrigin(btVector3(10, 5, 0)); btCollisionWorld::ClosestConvexResultCallback sweepCallback(btVector3(0,0,0), btVector3(0,0,0)); sweepCallback.m_collisionFilterGroup COL_PLAYER; // 碰撞过滤组 sweepCallback.m_collisionFilterMask COL_GROUND | COL_ENEMY; // 碰撞过滤掩码 dynamicsWorld-convexSweepTest(capsuleShape, startTrans, endTrans, sweepCallback); if (sweepCallback.hasHit()) { float hitFraction sweepCallback.m_closestHitFraction; // 在路径上的碰撞比例 (0~1) btVector3 hitPoint sweepCallback.m_hitPointWorld; // 根据 hitFraction你可以计算出安全的移动终点 btVector3 safeEnd startTrans.getOrigin() (endTrans.getOrigin() - startTrans.getOrigin()) * hitFraction; }6. 性能优化与常见陷阱直接使用 Bullet 而不加优化在物体数量稍多时几百个就可能遇到性能瓶颈。以下是一些关键的优化策略和避坑指南。6.1 碰撞过滤Collision Filtering不是所有物体都需要相互检测碰撞。比如子弹不需要和子弹碰撞特效粒子不需要和任何物体碰撞。Bullet 使用组Group和掩码Mask来实现过滤。每个刚体在添加到世界时可以指定其所属的碰撞组和碰撞掩码碰撞组group一个整数通常用2的幂次方1248...来定义不同的类别如COL_GROUND1,COL_PLAYER2,COL_ENEMY4,COL_BULLET8。碰撞掩码mask一个整数其二进制位表示该物体会与哪些组的物体进行碰撞检测。// 定义过滤组 enum CollisionGroups { COL_NOTHING 0, COL_GROUND 1 0, // 1 COL_PLAYER 1 1, // 2 COL_ENEMY 1 2, // 4 COL_BULLET 1 3, // 8 COL_TRIGGER 1 4 // 16 }; // 地面只和玩家、敌人碰撞 short groundGroup COL_GROUND; short groundMask COL_PLAYER | COL_ENEMY; dynamicsWorld-addRigidBody(groundBody, groundGroup, groundMask); // 玩家和地面、敌人、触发器碰撞 short playerGroup COL_PLAYER; short playerMask COL_GROUND | COL_ENEMY | COL_TRIGGER; dynamicsWorld-addRigidBody(playerBody, playerGroup, playerMask); // 子弹只和敌人碰撞 short bulletGroup COL_BULLET; short bulletMask COL_ENEMY; dynamicsWorld-addRigidBody(bulletBody, bulletGroup, bulletMask);6.2 物体休眠Activation State动态刚体在静止不动一段时间后Bullet 会将其置为“休眠”ISLAND_SLEEPING状态。休眠的物体将跳过大部分物理计算直到被外力碰撞、施加力等唤醒。这是非常重要的性能优化。常见陷阱运动学刚体不休眠如果你创建了一个运动学刚体如移动平台并希望它持续运动需要显式设置setActivationState(DISABLE_DEACTIVATION)否则它可能会在不该休眠的时候休眠导致物理模拟出错。手动唤醒如果你通过代码直接修改了一个动态刚体的位置setWorldTransform它不会自动唤醒。你需要调用activate()来强制唤醒它否则它可能“卡”在原地。强制激活对于玩家角色等关键物体可以设置setActivationState(DISABLE_DEACTIVATION)防止其休眠但会带来额外的性能开销需权衡。6.3 使用合适的碰撞形状与层次结构避免使用过于复杂的凸包形状btConvexHullShape虽然能包裹任意凸体但顶点数过多会严重影响性能。尽量用基本形状组合。对于大量静态小物体可以考虑使用btCompoundShape将它们合并成一个大的静态物体减少世界中的刚体数量。动态 AABB 树更新开销btDbvtBroadphase虽然高效但当大量动态物体每帧都在移动时更新其包围盒树也有开销。尽量减少不必要的物体运动。6.4 调试渲染与可视化Bullet 自带一个简单的调试绘制器btIDebugDraw可以将碰撞形状、接触点、法线等以线框形式绘制出来。这对于验证碰撞形状是否正确、检测是否生效至关重要。你需要实现btIDebugDraw接口将其与你图形 APIOpenGL/DirectX的绘制命令关联起来然后在物理世界中设置调试绘制器并开启调试模式。class MyDebugDrawer : public btIDebugDraw { // 实现 drawLine, drawContactPoint, reportErrorWarning 等方法... }; MyDebugDrawer debugDrawer; dynamicsWorld-setDebugDrawer(debugDrawer); // 在渲染循环中 dynamicsWorld-debugDrawWorld();开启调试后你会在场景中看到所有碰撞形状的轮廓线发生碰撞时会有接触点和法线显示这是排查碰撞问题最直观的工具。7. 从检测到响应构建一个简单的碰撞交互让我们把上面的知识串联起来实现一个经典场景一个球体从空中落下撞击一个盒子然后根据碰撞法线反弹。// 初始化世界、形状等略... // 创建静态地面一个巨大的盒子 btBoxShape* groundShape new btBoxShape(btVector3(50, 1, 50)); btTransform groundTransform; groundTransform.setIdentity(); groundTransform.setOrigin(btVector3(0, -1, 0)); btDefaultMotionState* groundMotionState new btDefaultMotionState(groundTransform); btRigidBody::btRigidBodyConstructionInfo groundRBCI(0, groundMotionState, groundShape, btVector3(0,0,0)); btRigidBody* groundBody new btRigidBody(groundRBCI); dynamicsWorld-addRigidBody(groundBody); // 创建动态球体 btSphereShape* sphereShape new btSphereShape(1.0); btTransform sphereTransform; sphereTransform.setIdentity(); sphereTransform.setOrigin(btVector3(0, 10, 0)); btDefaultMotionState* sphereMotionState new btDefaultMotionState(sphereTransform); btVector3 sphereInertia(0,0,0); float sphereMass 1.0f; sphereShape-calculateLocalInertia(sphereMass, sphereInertia); btRigidBody::btRigidBodyConstructionInfo sphereRBCI(sphereMass, sphereMotionState, sphereShape, sphereInertia); btRigidBody* sphereBody new btRigidBody(sphereRBCI); sphereBody-setRestitution(0.8f); // 设置较高的弹性 dynamicsWorld-addRigidBody(sphereBody); // 创建动态盒子 btBoxShape* boxShape new btBoxShape(btVector3(2, 0.5, 2)); btTransform boxTransform; boxTransform.setIdentity(); boxTransform.setOrigin(btVector3(5, 2, 0)); btDefaultMotionState* boxMotionState new btDefaultMotionState(boxTransform); btVector3 boxInertia(0,0,0); float boxMass 5.0f; // 盒子比球重 boxShape-calculateLocalInertia(boxMass, boxInertia); btRigidBody::btRigidBodyConstructionInfo boxRBCI(boxMass, boxMotionState, boxShape, boxInertia); btRigidBody* boxBody new btRigidBody(boxRBCI); boxBody-setRestitution(0.3f); dynamicsWorld-addRigidBody(boxBody); // 模拟循环 for (int i 0; i 600; i) { dynamicsWorld-stepSimulation(1.f / 60.f, 10); // 获取并更新球体的渲染位置 btTransform sphereCurrentTrans; sphereBody-getMotionState()-getWorldTransform(sphereCurrentTrans); // 将 sphereCurrentTrans 传递给你的渲染引擎... // 检测球与盒子的碰撞仅作示例实际应在接触点处理回调中做 // 这里使用手动检测来演示原理 int numManifolds dynamicsWorld-getDispatcher()-getNumManifolds(); for (int m 0; m numManifolds; m) { btPersistentManifold* manifold dynamicsWorld-getDispatcher()-getManifoldByIndexInternal(m); const btCollisionObject* obA manifold-getBody0(); const btCollisionObject* obB manifold-getBody1(); if ((obA sphereBody obB boxBody) || (obA boxBody obB sphereBody)) { if (manifold-getNumContacts() 0) { std::cout Sphere hit Box at frame: i std::endl; // 可以在这里播放碰撞音效、产生粒子等 } } } }在这个例子中球体因为设置了较高的恢复系数0.8撞击地面和盒子后会有明显的反弹。而较重的盒子被球撞击后只会轻微移动。通过调整质量、摩擦力和恢复系数你可以模拟出各种不同的物理材质感觉如橡胶球、木箱、钢铁等。8. 进阶话题与资源指引入门之后你可能还会遇到更复杂的需求。这里提供一些方向触发器Trigger Volumes有时我们只需要知道一个物体进入了某个区域而不需要物理碰撞响应如拾取物品、进入下一关。可以将刚体的碰撞标志设置为CF_NO_CONTACT_RESPONSE然后通过btGhostObject来检测重叠事件。连续碰撞检测CCD对于高速运动的物体如子弹可能会在单帧时间内“穿过”薄墙。Bullet 提供了 CCD 机制通过形状扫描来防止这类“隧道效应”。约束与关节Constraints/Joints用于连接两个刚体实现铰链、滑块、球窝、车辆悬挂等复杂运动。btHingeConstraint,btSliderConstraint,btGeneric6DofConstraint等是常用的关节类型。柔体与布料Soft Body ClothBullet 也包含柔体模拟模块可以模拟旗帜、布料、绳索等可变形物体。多线程Bullet 支持多线程模拟btParallelConstraintSolver可以充分利用多核 CPU 提升复杂场景的性能。学习资源官方资源Bullet 的 GitHub Wiki 和源码中的Demos文件夹是最好的学习材料。调试与可视化花时间搭建好调试绘制它能帮你直观理解碰撞形状和物理过程事半功倍。社区Bullet 的官方论坛和 Stack Overflow 上有大量历史问题和解答。从我个人的项目经验来看掌握 Bullet 的关键在于理解其“组件化”的设计思想世界、形状、刚体、约束各司其职。初期最常见的错误是混淆了视觉坐标与物理坐标、忽略了单位一致性建议全程使用米-千克-秒制以及没有正确设置碰撞过滤导致性能低下或逻辑错误。多动手写小 Demo 测试每个功能点善用调试绘制你会很快建立起三维物理世界的直觉。