从零实现C++物理引擎:刚体模拟与碰撞检测核心原理详解

📅 2026/7/22 13:39:44
从零实现C++物理引擎:刚体模拟与碰撞检测核心原理详解
1. 项目概述为什么物理引擎是游戏开发的基石在游戏开发、动画制作乃至虚拟仿真领域一个流畅、真实且高效的物理世界是沉浸感的核心来源。玩家推动一个箱子它应该沿着地面滑动遇到墙壁会停下角色从高处跳下会有一个符合预期的加速下落过程两个物体高速相撞应该产生令人信服的反弹和旋转。这些看似简单的交互背后是一套复杂的数学和物理计算在支撑而承载这套计算的核心就是物理引擎。刚体模拟作为物理引擎中最基础也最关键的组成部分专门处理那些形状和大小在运动过程中不会发生变化的物体即“刚体”的运动规律。刚体模拟的核心挑战在于“高效”与“真实”之间的平衡。我们既希望物理世界能尽可能逼真模拟重力、摩擦力、碰撞、关节约束等多种效应又必须保证在每帧16.6毫秒对应60FPS甚至更短的时间内完成所有计算以确保游戏的流畅性。这其中碰撞检测与响应是计算最密集、逻辑最复杂也最容易出现性能瓶颈的环节。碰撞检测需要回答“两个物体是否接触”以及“接触的细节如接触点、穿透深度、法线方向是什么”碰撞响应则基于这些细节计算并施加正确的冲量改变物体的速度和角速度从而模拟出碰撞后的反弹、滑动或静止。选择C来实现这个核心模块几乎是业内的必然选择。C提供了无与伦比的性能控制能力从内存布局到指令执行开发者都能进行精细优化。这对于需要每帧处理成百上千个刚体、进行数万次几何测试的物理引擎来说至关重要。同时现代CC11/14/17及以后的标准库和语言特性如智能指针、移动语义、Lambda表达式等又能帮助我们构建出既高效又相对安全、易于维护的代码结构避免陷入传统C风格代码的泥潭。这个项目就是一次深入物理引擎腹地的编程实践。我们将从零开始用C构建一个专注于刚体模拟的轻量级物理引擎核心并重点攻克碰撞检测与响应这两个硬骨头。目标不是复现一个像PhysX或Bullet那样功能齐全的工业级引擎而是通过亲手实现彻底理解其中的原理、算法和优化技巧为后续更复杂的游戏开发或引擎研究打下坚实基础。无论你是对游戏引擎底层感兴趣的程序员还是希望在自己的独立游戏中加入定制物理特性的开发者这次实践都将是一次宝贵的旅程。2. 核心架构与数学基础构建物理世界的语言在动手写代码之前我们必须先搭建好物理引擎的数学模型和程序架构。这就像盖房子前要先打好地基和绘制蓝图。2.1 刚体状态描述位置、朝向与运动一个刚体在三维空间中的状态需要用几个关键量来描述位置Position一个三维向量Vec3通常表示刚体质心Center of Mass在世界坐标系中的坐标。朝向Orientation在三维中我们通常用四元数Quaternion来表示旋转。相比欧拉角四元数能有效避免万向节死锁并且插值更平滑相比旋转矩阵它更紧凑4个标量 vs 9个标量。在二维项目中一个标量旋转角度就足够了。线速度Linear Velocity和角速度Angular Velocity分别是描述位置变化率和朝向变化率的三维向量。质量Mass和转动惯量Inertia Tensor质量抵抗线加速度转动惯量抵抗角加速度。转动惯量是一个3x3的矩阵但对于主轴对齐的简单形状可以简化为一个三维向量对角矩阵。在代码中我们会定义一个RigidBody类来封装这些状态以及质量、摩擦力、恢复系数弹性等物理属性。class RigidBody { public: // 状态量 Vec3 position; Quaternion orientation; Vec3 linearVelocity; Vec3 angularVelocity; // 物理属性 float mass; float inverseMass; // 存储倒数计算冲量时直接使用乘法避免除法开销 Mat3 inertiaTensor; Mat3 inverseInertiaTensor; // 同上存储逆矩阵 // 力与冲量 Vec3 forceAccumulator; Vec3 torqueAccumulator; // 几何形状指向一个Shape基类支持多种形状 std::shared_ptrCollisionShape shape; // 每帧更新状态 void integrate(float deltaTime); };integrate函数是运动学的核心它根据当前受到的合力和合力矩更新速度和位置。这里通常使用显式欧拉法或更稳定的辛欧拉法Semi-Implicit Euler也叫Symplectic Euler。void RigidBody::integrate(float deltaTime) { if (inverseMass 0.0f) return; // 静态物体不更新 // 计算加速度 Vec3 linearAcceleration forceAccumulator * inverseMass; Vec3 angularAcceleration inverseInertiaTensor.transform(torqueAccumulator); // 更新速度辛欧拉法先更新速度再用新速度更新位置 linearVelocity linearAcceleration * deltaTime; angularVelocity angularAcceleration * deltaTime; // 应用阻尼模拟微小的速度损失如空气阻力 linearVelocity * std::pow(linearDamping, deltaTime); angularVelocity * std::pow(angularDamping, deltaTime); // 更新位置和朝向 position linearVelocity * deltaTime; // 四元数积分q q 0.5 * deltaTime * [ω] * q orientation.addScaledVector(angularVelocity, deltaTime * 0.5f); orientation.normalize(); // 防止四元数长度漂移 // 清空累加器 forceAccumulator.clear(); torqueAccumulator.clear(); }注意四元数积分是一个容易出错的地方。这里使用的是近似的积分方法对于高速旋转或长时间模拟可能会积累误差。工业级引擎会使用更精确的积分方法但对于大多数游戏场景这个近似已经足够好且非常高效。务必记得在积分后对四元数进行归一化normalize。2.2 碰撞检测的层次结构从快到精直接对场景中每两个物体都进行精确的几何相交测试如三角形与三角形测试其时间复杂度是O(N²)是完全不可接受的。因此所有物理引擎都采用分层Broad Phase - Narrow Phase的检测策略。Broad Phase粗略检测目标是快速找出所有可能发生碰撞的物体对Pair并剔除明显不可能碰撞的对。常用算法有包围盒Bounding Volume为每个物体计算一个简单的几何体如轴对齐包围盒AABB、包围球来近似表示其范围。如果两个物体的包围盒都不相交那它们肯定不相交。空间分割Spatial Partitioning如均匀网格Grid、四叉树/八叉树Quadtree/Octree、BVHBounding Volume Hierarchy。这些数据结构能快速将空间相邻的物体分组只对同组内的物体进行碰撞检测。扫描与剪裁Sweep and Prune利用物体在轴上投影的连续性通过维护排序的端点列表来快速更新重叠对特别适合物体连续运动的场景。在实现上我推荐从基于动态AABB的扫描与剪裁开始。它的实现相对直观能很好地处理大量动态物体并且是许多成熟引擎如Box2D的选择。我们可以为每个物体在X、Y、Z轴上维护一个区间[min, max]并在每帧更新这些区间。通过维护三个轴X, Y, Z上所有区间端点的排序列表可以高效地找出在所有轴上区间都重叠的物体对。Narrow Phase精确检测对Broad Phase产生的候选物体对进行精确的几何相交测试并计算出碰撞信息接触流形Contact Manifold。这是我们接下来要重点实现的部分。3. 核心碰撞检测算法实现从理论到代码Narrow Phase是碰撞检测的精华所在。我们将实现两种最基础但极其重要的形状碰撞检测球体-球体以及凸包-凸包使用分离轴定理SAT。理解它们是理解更复杂形状如网格检测的基础。3.1 球体与球体碰撞最简单的起点球体碰撞检测是最简单的因为球体具有完美的对称性。判断两个球体是否碰撞只需要比较两球心之间的距离与两球半径之和。struct Contact { Vec3 pointOnA; // 物体A上的接触点世界坐标 Vec3 pointOnB; // 物体B上的接触点世界坐标 Vec3 normal; // 碰撞法线从A指向B float penetration; // 穿透深度 // ... 其他信息如摩擦力方向、恢复系数等 }; bool collideSphereSphere(const Sphere sphereA, const Transform transformA, const Sphere sphereB, const Transform transformB, Contact outContact) { // 获取世界空间中的球心 Vec3 centerA transformA.transformPoint(sphereA.center); Vec3 centerB transformB.transformPoint(sphereB.center); Vec3 ab centerB - centerA; float distanceSquared ab.lengthSquared(); // 先计算平方距离避免开方 float radiusSum sphereA.radius sphereB.radius; if (distanceSquared radiusSum * radiusSum) { return false; // 距离平方大于半径和平方未碰撞 } float distance std::sqrt(distanceSquared); outContact.normal ab / distance; // 单位法线从A指向B outContact.penetration radiusSum - distance; // 接触点位于两球心连线上根据半径比例计算 outContact.pointOnA centerA outContact.normal * sphereA.radius; outContact.pointOnB centerB - outContact.normal * sphereB.radius; // 更精确的做法是取中点但上述方法对于响应计算足够好 // outContact.pointOnA centerA outContact.normal * (sphereA.radius - outContact.penetration * 0.5f); // outContact.pointOnB centerB - outContact.normal * (sphereB.radius - outContact.penetration * 0.5f); return true; }实操心得在碰撞检测中应尽量避免使用开销较大的数学运算如开方sqrt、三角函数。这里我们先比较距离的平方只有确定碰撞后才进行开方这是一个常见的优化。在性能敏感的代码中这类“延迟计算”或“使用平方值”的技巧非常有用。3.2 凸包与凸包碰撞分离轴定理SAT详解对于盒子矩形、凸多边形、凸多面体等形状分离轴定理Separating Axis Theorem, SAT是2D和3D碰撞检测的利器。其核心思想非常直观如果能找到一条直线轴使得两个凸形状在该轴上的投影区间不重叠那么这两个形状就一定没有碰撞。反之如果在所有可能的候选轴上投影都重叠则它们碰撞。对于两个凸多边形候选轴集包括每个多边形的每条边的法线方向。 对于两个凸多面体如立方体候选轴集包括每个多面体的每个面的法线方向以及每条来自A的边与每条来自B的边的叉积方向即边的组合方向。我们以2D的矩形OBB定向包围盒为例因为它包含了SAT的所有关键步骤且易于可视化。假设矩形由中心位置c、两个互相垂直的本地轴u、v代表朝向以及在半轴上的延伸e半宽半高定义。struct OBB2D { Vec2 center; Vec2 axes[2]; // 必须是单位向量且正交 Vec2 extents; // 半宽半高 };碰撞检测函数如下bool collideOBBOBB(const OBB2D a, const OBB2D b, Contact2D outContact) { float bestOverlap FLT_MAX; Vec2 bestAxis; // 存储最小穿透深度的轴即碰撞法线 // 候选轴a的两个本地轴b的两个本地轴 Vec2 testAxes[4] { a.axes[0], a.axes[1], b.axes[0], b.axes[1] }; for (int i 0; i 4; i) { Vec2 axis testAxes[i]; // 归一化轴如果输入轴可能不是单位向量 axis.normalize(); // 计算形状a和b在轴上的投影区间 Projection projA getProjection(a, axis); Projection projB getProjection(b, axis); // 检查投影是否重叠 float overlap getOverlap(projA, projB); if (overlap 0.0f) { return false; // 找到分离轴无碰撞 } // 记录最小穿透深度及其对应的轴 if (overlap bestOverlap) { bestOverlap overlap; bestAxis axis; } } // 如果所有轴都重叠则发生碰撞 // 此时 bestAxis 是穿透深度最小的轴我们将其作为碰撞法线 // 但需要确定法线的方向从A指向B Vec2 centerDiff b.center - a.center; if (centerDiff.dot(bestAxis) 0.0f) { bestAxis -bestAxis; // 确保法线从A指向B } outContact.normal bestAxis; outContact.penetration bestOverlap; // 计算接触点对于矩形可以找最深的顶点或使用Clipping算法求多边形交集 // 这里简化处理取矩形A在反法线方向上的最远顶点作为参考点 outContact.point findSupportPoint(a, -outContact.normal); // 更精确的接触流形生成需要更复杂的算法如 Sutherland-Hodgman 多边形裁剪 return true; }getProjection函数的任务是计算一个凸形状在给定轴上的投影区间[min, max]。对于OBB可以将其顶点投影到轴上并找出最小和最大值。更高效的方法是使用支持函数Support Function直接计算在轴方向上的最远点。Projection getProjection(const OBB2D obb, const Vec2 axis) { // 计算半长在轴上的投影长度 float projExtent obb.extents.x * std::abs(axis.dot(obb.axes[0])) obb.extents.y * std::abs(axis.dot(obb.axes[1])); // 中心点在轴上的投影 float centerProj obb.center.dot(axis); return Projection{centerProj - projExtent, centerProj projExtent}; }注意事项在3D中对于两个凸多面体如两个定向盒子候选轴的数量会急剧增加6个面法线 9*2? 实际上是边的组合最多15条独特轴。实现时务必注意效率。此外SAT只告诉我们是否碰撞以及最小穿透轴要生成用于碰撞响应的接触点集合接触流形通常还需要额外的步骤如寻找接触特征面-面、边-边、点-面等并使用裁剪算法如 Sutherland-Hodgman来生成多边形接触面。对于刚入门可以先使用一个接触点如穿透最深点来实现基础的响应虽然这可能在物体卡住时不够稳定。4. 碰撞响应让世界动起来检测到碰撞并生成接触信息后下一步就是让物体对这些碰撞做出符合物理规律的反应。碰撞响应的目标是解决穿透让物体分开并模拟碰撞过程中的能量交换反弹、摩擦。我们主要使用冲量法Impulse Method它直接修改物体的速度计算高效适合实时模拟。4.1 冲量法基础原理冲量J是力在时间上的积分它直接改变物体的动量。对于碰撞我们假设碰撞发生在瞬间计算一个瞬时冲量来改变两物体的速度使其满足碰撞后的约束条件主要是法线方向上的分离和切向的摩擦力。核心公式来自于动量守恒和恢复系数Coefficient of Restitution,e的定义。恢复系数e描述了碰撞前后相对速度在法线方向上的比例关系e1为完全弹性碰撞e0为完全非弹性碰撞。对于一个接触点计算法线方向冲量j的公式如下推导过程略可参考物理引擎相关书籍j -(1 e) * (v_rel · n) / [ (1/m_a 1/m_b) ( (r_a × n) · (I_a^{-1} (r_a × n)) ) ( (r_b × n) · (I_b^{-1} (r_b × n)) ) ]其中v_rel接触点处的相对速度B相对于A。n碰撞法线从A指向B。e恢复系数取两个物体恢复系数的较小值min(e_a, e_b)。m_a,m_b物体A和B的质量。I_a,I_b物体A和B的转动惯量世界坐标系下的逆矩阵。r_a,r_b从物体质心指向接触点的向量。这个公式看起来复杂但分解开来就是分子是期望改变的速度大小考虑了弹性分母是一个等效质量项包含了平动惯性和转动惯性的影响。4.2 代码实现计算并应用冲量首先我们需要计算接触点处的相对速度。这个速度包含了线速度和由于旋转产生的速度。void resolveCollision(RigidBody bodyA, RigidBody bodyB, const Contact contact) { // 1. 计算接触点相对于各自质心的向量 Vec3 rA contact.pointOnA - bodyA.position; Vec3 rB contact.pointOnB - bodyB.position; // 2. 计算接触点处的相对速度 Vec3 velA bodyA.linearVelocity cross(bodyA.angularVelocity, rA); Vec3 velB bodyB.linearVelocity cross(bodyB.angularVelocity, rB); Vec3 relativeVel velB - velA; // B相对于A的速度 // 3. 计算法线方向上的相对速度 float contactVel dot(relativeVel, contact.normal); // 如果相对速度在分离为正说明碰撞已经正在解决可以跳过 if (contactVel 0) { return; } // 4. 计算恢复系数 float e std::min(bodyA.restitution, bodyB.restitution); // 5. 计算分母项等效质量 float invMassSum bodyA.inverseMass bodyB.inverseMass; Vec3 crossRAxN cross(rA, contact.normal); Vec3 crossRBxN cross(rB, contact.normal); float angularEffectA dot(crossRAxN, bodyA.inverseInertiaTensorWorld.transform(crossRAxN)); float angularEffectB dot(crossRBxN, bodyB.inverseInertiaTensorWorld.transform(crossRBxN)); float denominator invMassSum angularEffectA angularEffectB; // 防止除零静态物体invMass为0 if (denominator 0.0f) return; // 6. 计算法线冲量大小 float j -(1.0f e) * contactVel / denominator; // 确保冲量是正向的推动物体分开 if (j 0.0f) return; // 7. 应用冲量 Vec3 impulse contact.normal * j; bodyA.applyImpulse(-impulse, rA); bodyB.applyImpulse( impulse, rB); // 8. 摩擦力处理简化版库仑摩擦 applyFriction(bodyA, bodyB, contact, rA, rB, j); } void RigidBody::applyImpulse(const Vec3 impulse, const Vec3 point) { linearVelocity impulse * inverseMass; // 角速度变化Δω I^{-1} * (r × impulse) Vec3 torque cross(point, impulse); angularVelocity inverseInertiaTensorWorld.transform(torque); }4.3 摩擦力的模拟摩擦力总是与接触面平行方向与相对切向速度相反。一个常用的简化模型是库仑摩擦模型。我们首先计算切向方向t然后计算消除切向速度所需的冲量但其大小受限于法向冲量乘以摩擦系数μ。void applyFriction(RigidBody bodyA, RigidBody bodyB, const Contact contact, const Vec3 rA, const Vec3 rB, float normalImpulseMagnitude) { // 1. 计算切向方向 Vec3 relativeVel calculateRelativeVelocityAtPoint(bodyA, bodyB, contact.pointOnA, rA, rB); Vec3 tangent relativeVel - contact.normal * dot(relativeVel, contact.normal); // 如果切向速度很小忽略摩擦力 float tangentLen tangent.length(); if (tangentLen 1e-6f) return; tangent.normalize(); // 2. 计算为消除切向速度所需的冲量大小公式与法向类似但恢复系数为0 float frictionVel dot(relativeVel, tangent); float invMassSum bodyA.inverseMass bodyB.inverseMass; Vec3 crossRAxT cross(rA, tangent); Vec3 crossRBxT cross(rB, tangent); float angularEffectA dot(crossRAxT, bodyA.inverseInertiaTensorWorld.transform(crossRAxT)); float angularEffectB dot(crossRBxT, bodyB.inverseInertiaTensorWorld.transform(crossRBxT)); float denominator invMassSum angularEffectA angularEffectB; if (denominator 0.0f) return; float jt -frictionVel / denominator; // 3. 库仑定律摩擦力大小不能超过 μ * |法向力| float mu sqrtf(bodyA.friction * bodyB.friction); // 合并摩擦系数 Vec3 frictionImpulse; if (std::abs(jt) normalImpulseMagnitude * mu) { // 静摩擦或动摩擦未滑动 frictionImpulse tangent * jt; } else { // 滑动摩擦大小固定为 μ * |法向力| frictionImpulse tangent * (-normalImpulseMagnitude * mu * sign(jt)); } // 4. 应用摩擦力冲量 bodyA.applyImpulse(-frictionImpulse, rA); bodyB.applyImpulse( frictionImpulse, rB); }实操心得碰撞响应特别是摩擦力和多个接触点的处理是物理引擎稳定性的关键。上面的代码是单点接触的简化版本。在真实场景中一个碰撞可能产生多个接触点如一个盒子立在平面上有四个接触点。简单地独立处理每个点会导致“抖动”或能量异常。工业级引擎会使用顺序冲量法Sequential Impulse或更高级的约束求解器如基于拉格朗日乘子的速度级或位置级求解器将一帧内所有接触约束组成一个方程组进行迭代求解以获得更稳定、更符合物理的结果。对于入门可以先实现单点接触观察效果再逐步研究更高级的求解器。5. 系统集成与性能优化实战将各个模块组合成一个可运行的物理世界并确保其高效运行是项目成功的关键。5.1 主循环与时间积分物理引擎的主循环通常遵循“收集力 - 积分运动 - 检测碰撞 - 解析碰撞”的模式。一个简单的固定时间步长Fixed Timestep循环如下class PhysicsWorld { public: std::vectorRigidBody bodies; float fixedDeltaTime 1.0f / 60.0f; // 固定时间步长 int velocityIterations 8; // 速度迭代次数用于约束求解 int positionIterations 3; // 位置迭代次数用于修正穿透 void step(float deltaTime) { // 1. 力生成如重力 for (auto body : bodies) { body.forceAccumulator Vec3(0, -9.8f * body.mass, 0); // 重力 } // 2. 积分速度与位置运动学更新 for (auto body : bodies) { body.integrate(fixedDeltaTime); } // 3. 碰撞检测Broad Phase Narrow Phase std::vectorContact contacts; broadPhaseDetection(contacts); narrowPhaseDetection(contacts); // 填充contacts详细信息 // 4. 碰撞响应迭代求解 for (int i 0; i velocityIterations; i) { for (auto contact : contacts) { resolveCollision(*contact.bodyA, *contact.bodyB, contact); } } // 5. 位置修正可选用于处理深度穿透防止物体“陷进去” for (int i 0; i positionIterations; i) { for (auto contact : contacts) { correctPositions(*contact.bodyA, *contact.bodyB, contact); } } // 6. 清空所有临时力 for (auto body : bodies) { body.clearAccumulators(); } } };注意使用固定时间步长是为了保证物理模拟的稳定性不受渲染帧率波动的影响。更高级的做法是使用“累积时间”配合固定步长更新即while (accumulatedTime fixedDeltaTime) { updatePhysics(fixedDeltaTime); accumulatedTime - fixedDeltaTime; }。5.2 关键性能优化技巧空间分割优化Broad Phase当物体数量N很大时O(N²)的Broad Phase无法接受。实现一个动态AABB树Dynamic Bounding Volume Tree或均匀网格Uniform Grid可以大幅提升性能。动态AABB树在物体移动时更新效率较高是许多引擎的选择。惰性计算与缓存物体的世界空间AABB、世界空间的转动惯量矩阵等每帧都可能需要多次使用。应在物体状态位置、朝向改变时标记为“脏”Dirty并在首次需要时计算并缓存结果避免重复计算。SIMD指令集现代CPU支持SIMD如SSE, AVX可以同时对多个浮点数进行运算。将向量Vec3、矩阵Mat3的运算点积、叉积、变换用SIMD指令重写能获得数倍的性能提升。许多数学库如GLM Eigen都提供了SIMD支持。内存布局优化对于需要频繁遍历的数组如所有刚体使用结构数组Array of Structures, AoS可能导致缓存利用率低。可以考虑使用数组结构Structure of Arrays, SoA即将所有刚体的位置放在一个连续数组速度放在另一个数组等。这样在积分步骤中连续访问同类型数据对缓存更友好。多线程碰撞检测特别是Broad Phase和不同物体对之间的Narrow Phase和约束求解可以很好地并行化。可以使用任务系统如Intel TBB或简单的线程池将工作分解到多个核心。5.3 调试与可视化让问题无所遁形物理引擎的Bug往往难以通过肉眼观察。一个强大的调试渲染器至关重要。绘制形状轮廓用线条画出每个刚体的世界空间形状球体、盒子等。绘制包围盒用不同颜色如红色绘制Broad Phase使用的AABB。绘制接触点与法线在碰撞点画一个小点并沿碰撞法线方向画一条短线可以直观看到碰撞发生的点和方向。绘制速度向量从物体质心画一条箭头代表其线速度和角速度。暂停与单步执行允许暂停模拟并一帧一帧地前进观察每一刻的状态变化。我习惯使用简单的OpenGL立即模式或现代图形API如Vulkan/D3D12的线条绘制功能来实现这些调试视图。它们是你理解引擎内部状态、定位诡异物理现象如物体抖动、飞走的最强工具。6. 常见问题与排查技巧实录在开发过程中你一定会遇到各种奇怪的问题。以下是一些典型问题及其排查思路问题现象可能原因排查与解决思路物体轻微抖动或高频振动1. 碰撞响应迭代次数不足。2. 恢复系数e设置过高接近1导致能量无法耗散。3. 时间步长deltaTime不稳定或过大。4. 接触点生成不稳定如SAT在边缘情况返回的法线方向帧间跳动。1. 增加velocityIterations如从6增加到10。2. 适当降低恢复系数或引入速度阈值低于该阈值时设为0。3. 使用固定时间步长并确保其值合理如1/60s。4. 在接触点计算中增加容差epsilon或使用更稳定的接触流形生成算法如GJK/EPA算法。物体碰撞后“粘”在一起或穿透1. 碰撞检测漏报Broad Phase或Narrow Phase有bug。2. 冲量计算错误特别是分母为0或负值导致未应用冲量。3. 摩擦力冲量过大阻碍了法向分离。4. 未处理静止接触Resting Contact即物体叠放时每帧仍有微小穿透需要持续施加力防止下沉。1. 开启调试绘制检查包围盒和精确形状是否匹配碰撞检测函数是否返回正确结果。2. 检查invMass和inverseInertiaTensor计算静态物体质量无穷大的倒数应为0。3. 检查摩擦系数mu的计算确保其值合理通常在0~1之间。4. 实现位置修正Positional Correction或引入“伪速度”Baumgarte Stabilization来处理持续穿透。物体获得异常大的速度并飞走1. 数值不稳定如除以一个极小的数。2. 碰撞法线方向计算错误如方向反了。3. 在应用冲量时对静态物体inverseMass 0也错误地修改了其速度。4. 角速度积分错误导致四元数非归一化进而引起变换矩阵错误。1. 在除法前检查分母绝对值小于某个阈值如1e-6时直接跳过或钳制。2. 确保碰撞法线单位化且方向是从物体A指向物体B或根据相对速度方向确定。3. 在applyImpulse函数开头检查inverseMass若为0则直接返回。4. 在四元数积分后务必调用normalize()。性能随物体数量增加急剧下降1. Broad Phase算法是O(N²)的朴素检测。2. Narrow Phase中进行了不必要的昂贵运算如每对物体都进行SAT全轴测试。3. 内存访问模式不友好导致大量缓存未命中。1. 实现空间分割数据结构如动态AABB树、网格。这是提升性能最有效的一步。2. 在SAT前先用简单的包围球测试进行快速剔除。对特定形状对如球-球使用特化函数。3. 审视数据布局考虑对热点数据如位置、速度使用SoA。使用性能分析工具如VTune, Very Sleepy定位瓶颈。堆叠的物体不稳定容易倒塌1. 单点接触无法为堆叠提供稳定支撑。2. 迭代求解器次数不足无法在一帧内解决所有接触约束。3. 恢复系数不为0导致物体在接触时仍有微小弹跳积累能量。1. 必须实现接触流形Contact Manifold为面接触提供多个接触点如盒子-地面提供4个点。2. 大幅增加velocityIterations和positionIterations如分别增加到20和10。3. 对于静止接触相对速度很小将恢复系数设置为0。我个人在实际开发中最深刻的体会是物理引擎的调试五分靠代码五分靠视觉。当你看到物体行为异常时第一反应不应该是盲目修改参数而是打开你的调试绘制看看包围盒是否贴合碰撞法线方向是否合理接触点位置是否正确。很多时候问题就赤裸裸地画在屏幕上。另一个习惯是为每一个物理实体赋予一个唯一的ID并在日志中输出关键步骤的信息如“物体ID1与ID2碰撞法线(0,1,0)穿透0.1”结合单步执行可以像侦探一样追踪整个模拟过程。从最简单的两个球开始逐步增加盒子、复杂的凸包再到堆叠和关节每一步都确保基础稳固这样构建起来的引擎才会健壮可靠。