引言

前置阅读:建议先阅读运动学基础向量(点积、归一化)。

检测到碰撞只是第一步(我们在几何章节讨论了检测),更重要的是响应 (Response)。 物体是反弹?是停下?还是滑行?这取决于物理材质的属性:弹力 (Bounciness) 和摩擦力 (Friction)。

上一篇我们学习了运动学 (Kinematics)——描述物体如何运动。 本篇将进入动力学 (Dynamics)——回答物体为什么这样运动,并用它来构建完整的碰撞响应系统。

1. 反射向量 (Reflection Vector)

最常见的响应是”反弹”。例如光线反射、台球撞击。 我们需要根据入射向量 \(\vec{v}\) 和碰撞表面的法线 \(\vec{n}\) 计算出反射向量 \(\vec{r}\)

Collision Response

公式如下: \[ \vec{r} = \vec{v} - 2(\vec{v} \cdot \vec{n})\vec{n} \]

推导: 1. \(\vec{v}\) 在法线方向的分量是 \((\vec{v} \cdot \vec{n})\vec{n}\)。 2. 要反转这个分量,我们需要减去它两次(一次抵消,一次反向)。

代码实现:

// Unity 内置函数
Vector3 reflection = Vector3.Reflect(velocity, hitNormal);

// 手动实现
Vector3 Reflect(Vector3 v, Vector3 n) {
    return v - 2f * Vector3.Dot(v, n) * n;
}

2. 恢复系数 (Coefficient of Restitution)

现实世界中,能量总会损耗。我们引入一个恢复系数 (Coefficient of Restitution, \(e\)),通常称为 Bounciness。

恢复系数的严格定义基于碰撞前后的相对速度在法线方向上的比值:

\[ e = -\frac{v_{rel}' \cdot \vec{n}}{v_{rel} \cdot \vec{n}} \]

  • \(e = 1\): 完全弹性碰撞(超级弹力球,永远不停止)。
  • \(e = 0\): 完全非弹性碰撞(一坨泥巴贴在墙上)。
  • \(0 < e < 1\): 真实的物理模拟。

对于简单的墙壁反弹,法线方向的速度分量取反并乘以 \(e\)

\[ v_n' = -e \cdot v_n \]

3. 动力学基础 (Dynamics Fundamentals)

在深入碰撞冲量之前,我们需要从运动学过渡到动力学。运动学告诉我们位置、速度和加速度的关系,而动力学则引入了 (Force) 和质量 (Mass) 来解释运动的原因。

3.1 牛顿第二定律 (Newton’s Second Law)

力是使物体加速的原因:

\[ \vec{F} = m \vec{a} \]

其中 \(m\) 是质量 (Mass),\(\vec{a}\) 是加速度 (Acceleration)。 质量越大的物体,同样的力产生的加速度越小——这就是惯性 (Inertia)。

3.2 动量 (Momentum)

动量是物体运动状态的量度,定义为质量与速度的乘积:

\[ \vec{p} = m\vec{v} \]

动量是一个矢量,方向与速度相同。 一辆 2000 kg 的卡车以 10 m/s 行驶,动量为 \(20000 \text{ kg·m/s}\); 一个 0.17 kg 的台球以 5 m/s 滚动,动量仅为 \(0.85 \text{ kg·m/s}\)

3.3 动量守恒 (Conservation of Momentum)

当两个物体组成的系统不受外力时,总动量守恒

\[ m_1\vec{v}_1 + m_2\vec{v}_2 = m_1\vec{v}_1' + m_2\vec{v}_2' \]

这是碰撞响应最核心的约束条件——碰撞前后,系统的总动量不变。

3.4 冲量-动量定理 (Impulse-Momentum Theorem)

牛顿第二定律可以改写为动量的变化率形式:

\[ \vec{F} = \frac{d\vec{p}}{dt} \]

对时间积分得到冲量 (Impulse) \(\vec{J}\)

\[ \vec{J} = \int \vec{F} \, dt = \Delta \vec{p} = m\vec{v}' - m\vec{v} \]

碰撞发生在极短的时间内(\(\Delta t \to 0\)),力非常大但持续时间极短。 我们不关心力的具体大小,只关心它对速度的总效果——这正是冲量。

3.5 牛顿第三定律 (Newton’s Third Law)

碰撞时物体 A 对物体 B 施加的冲量与 B 对 A 施加的冲量大小相等、方向相反

\[ \vec{J}_A = -\vec{J}_B \]

这保证了总动量的守恒。

4. 碰撞冲量求解 (Collision Impulse)

有了动力学工具,我们可以正式推导两个刚体碰撞时的冲量公式。

4.1 问题建模

设两个球体质量分别为 \(m_1\), \(m_2\),碰撞前速度为 \(\vec{v}_1\), \(\vec{v}_2\),碰撞法线为 \(\vec{n}\)(从物体 1 指向物体 2),恢复系数为 \(e\)

相对速度 (Relative Velocity): \[ \vec{v}_{rel} = \vec{v}_1 - \vec{v}_2 \]

如果 \(\vec{v}_{rel} \cdot \vec{n} > 0\),说明两物体正在分离,无需响应。

4.2 冲量大小推导

由冲量-动量定理,碰撞后速度为:

\[ \vec{v}_1' = \vec{v}_1 + \frac{j}{m_1}\vec{n} \] \[ \vec{v}_2' = \vec{v}_2 - \frac{j}{m_2}\vec{n} \]

其中 \(j\) 是冲量的标量大小(沿法线方向),正值表示将物体 1 推离物体 2。

代入恢复系数的定义 \(e = -\frac{\vec{v}_{rel}' \cdot \vec{n}}{\vec{v}_{rel} \cdot \vec{n}}\),展开碰撞后的相对速度:

\[ \vec{v}_{rel}' \cdot \vec{n} = (\vec{v}_1' - \vec{v}_2') \cdot \vec{n} = \vec{v}_{rel} \cdot \vec{n} + j\left(\frac{1}{m_1} + \frac{1}{m_2}\right) \]

代入恢复系数方程并求解 \(j\)

\[ \boxed{j = \frac{-(1 + e) \, \vec{v}_{rel} \cdot \vec{n}}{\frac{1}{m_1} + \frac{1}{m_2}}} \]

这就是碰撞冲量公式,是所有刚体物理引擎的核心。

4.3 特殊情况

  • 撞墙:当一方质量无穷大(\(m_2 \to \infty\))时,\(1/m_2 \to 0\),公式退化为 \(j = -(1+e) \, m_1 (\vec{v}_{rel} \cdot \vec{n})\),等价于单体反射。
  • 等质量碰撞\(m_1 = m_2 = m\) 时,\(j = -\frac{(1+e) \, m}{2}(\vec{v}_{rel} \cdot \vec{n})\),在 \(e=1\) 时两物体交换速度分量。

4.4 代码实现

struct RigidBody {
    public Vector3 velocity;
    public float mass;
    public float inverseMass; // 1/mass,静态物体设为 0
}

void ResolveCollision(ref RigidBody a, ref RigidBody b,
                      Vector3 normal, float restitution)
{
    Vector3 vRel = a.velocity - b.velocity;
    float vAlongNormal = Vector3.Dot(vRel, normal);

    // 正在分离则跳过
    if (vAlongNormal > 0f) return;

    float j = -(1f + restitution) * vAlongNormal
              / (a.inverseMass + b.inverseMass);

    Vector3 impulse = j * normal;
    a.velocity += a.inverseMass * impulse;
    b.velocity -= b.inverseMass * impulse;
}

提示: 使用 inverseMass 而非 mass 是物理引擎的常见做法。 静态/不可移动物体的 inverseMass = 0,无需特殊判断就自然不会受力移动。

5. 摩擦力 (Friction)

碰撞不仅在法线方向产生冲量,在切线方向 (Tangential Direction) 也会因摩擦产生冲量。 真实的摩擦模型遵循库仑摩擦定律 (Coulomb Friction Law)。

5.1 分离法线与切线分量

碰撞点的相对速度可以分解为法线分量和切线分量:

\[ v_n = (\vec{v}_{rel} \cdot \vec{n}) \vec{n} \] \[ \vec{v}_t = \vec{v}_{rel} - v_n \]

切线方向的单位向量为: \[ \vec{t} = \frac{\vec{v}_t}{|\vec{v}_t|} \]

如果 \(|\vec{v}_t| \approx 0\),说明没有滑动,可跳过摩擦计算。

5.2 库仑摩擦模型

库仑摩擦将摩擦分为静摩擦 (Static Friction) 和动摩擦 (Kinetic Friction):

  • 静摩擦系数 \(\mu_s\):物体静止时阻止其开始滑动的最大摩擦力。
  • 动摩擦系数 \(\mu_k\)\(\mu_k \leq \mu_s\)):物体滑动时的摩擦力。

切线方向的摩擦冲量大小 \(j_t\) 首先按无摩擦约束求解:

\[ j_t = \frac{-\vec{v}_{rel} \cdot \vec{t}}{\frac{1}{m_1} + \frac{1}{m_2}} \]

然后应用库仑约束:

\[ j_t^{final} = \begin{cases} j_t & \text{if } |j_t| \leq \mu_s |j| \quad \text{(静摩擦,完全停止滑动)} \\ -\mu_k |j| & \text{if } |j_t| > \mu_s |j| \quad \text{(动摩擦,部分减速)} \end{cases} \]

其中 \(j\) 是法线方向的冲量大小(上一节求出的值)。

5.3 代码实现

void ApplyFriction(ref RigidBody a, ref RigidBody b,
                   Vector3 normal, float jNormal,
                   float staticFriction, float kineticFriction)
{
    Vector3 vRel = a.velocity - b.velocity;
    // 切线分量
    Vector3 vt = vRel - Vector3.Dot(vRel, normal) * normal;
    if (vt.sqrMagnitude < 1e-6f) return;

    Vector3 tangent = vt.normalized;
    float jt = -Vector3.Dot(vRel, tangent)
               / (a.inverseMass + b.inverseMass);

    Vector3 frictionImpulse;
    if (Mathf.Abs(jt) <= staticFriction * Mathf.Abs(jNormal)) {
        // 静摩擦:完全抵消切线速度
        frictionImpulse = jt * tangent;
    } else {
        // 动摩擦:施加固定大小的摩擦冲量
        frictionImpulse = -kineticFriction * Mathf.Abs(jNormal) * tangent;
    }

    a.velocity += a.inverseMass * frictionImpulse;
    b.velocity -= b.inverseMass * frictionImpulse;
}

6. 实战示例:台球碰撞 (Billiard Ball Collision)

让我们用一个完整的台球碰撞例子把前面的知识串联起来。

场景设定: - 球 A:\(m = 0.17\) kg,\(\vec{v}_A = (2, 0)\) m/s(向右运动)。 - 球 B:\(m = 0.17\) kg,\(\vec{v}_B = (0, 0)\) m/s(静止)。 - 碰撞法线:\(\vec{n} = (1, 0)\)(A 正面撞 B)。 - 恢复系数:\(e = 0.95\)(台球接近完全弹性)。 - 动摩擦系数:\(\mu_k = 0.05\)

第一步:检查碰撞

\[ \vec{v}_{rel} = (2, 0) - (0, 0) = (2, 0) \] \[ \vec{v}_{rel} \cdot \vec{n} = 2 \times 1 + 0 \times 0 = 2 > 0 \]

等等——结果大于零?这是因为我们的法线从 A 指向 B,相对速度沿法线正方向意味着 A 正在靠近 B。在本文的约定中,法线从物体 1 指向物体 2,所以 \(\vec{v}_{rel} \cdot \vec{n} < 0\) 才表示靠近。让我们修正法线方向:\(\vec{n} = (-1, 0)\)(从 B 指向 A,即碰撞面法线指向物体 1)。

\[ \vec{v}_{rel} \cdot \vec{n} = 2 \times (-1) = -2 < 0 \quad \checkmark \text{(正在靠近)} \]

第二步:计算法线冲量

\[ j = \frac{-(1 + 0.95) \times (-2)}{\frac{1}{0.17} + \frac{1}{0.17}} = \frac{3.9}{11.76} \approx 0.3316 \text{ N·s} \]

第三步:更新速度

\[ \vec{v}_A' = (2, 0) + \frac{0.3316}{0.17} \times (-1, 0) = (2 - 1.95, 0) = (0.05, 0) \] \[ \vec{v}_B' = (0, 0) - \frac{0.3316}{0.17} \times (-1, 0) = (1.95, 0) \]

验证动量守恒: - 碰撞前:\(0.17 \times 2 + 0.17 \times 0 = 0.34\) - 碰撞后:\(0.17 \times 0.05 + 0.17 \times 1.95 = 0.34\)

验证恢复系数: \[ e = -\frac{(0.05 - 1.95) \cdot (-1)}{(2 - 0) \cdot (-1)} = -\frac{1.9}{-2} = 0.95 \quad \checkmark \]

球 A 几乎停下,球 B 获得了大部分速度——这正是台球中”定杆”的效果。

第四步:摩擦(侧旋场景)

如果球 A 带有侧向速度 \(\vec{v}_A = (2, 0.5)\),碰撞后切线分量 \(\vec{v}_t\) 不为零:

\[ \vec{v}_t = (0, 0.5) \quad \Rightarrow \quad \vec{t} = (0, 1) \]

摩擦冲量约束:\(\mu_k |j| = 0.05 \times 0.3316 = 0.0166\) N·s, 切线方向的速度变化很小,球会保留大部分侧向运动——这就是台球中”偏杆”产生的走位效果。

工程提示:在实际物理引擎中,静摩擦与动摩擦的切换需要加入 Hysteresis(迟滞)死区。如果不加死区,当切线速度在静/动摩擦阈值附近振荡时,物体会出现抖动。常见做法是:当切线力超过 \(\mu_s \cdot |j|\) 时切换到动摩擦,但要等切线速度降到某个更低阈值时才切回静摩擦。

总结

  • 反射向量是实现反弹效果的核心,依赖于点积运算。
  • 恢复系数通过法线方向的相对速度比来控制能量损耗。
  • 动量守恒冲量-动量定理是从运动学过渡到碰撞响应的桥梁。
  • 碰撞冲量公式 \(j = -(1+e)(\vec{v}_{rel} \cdot \vec{n}) / (1/m_1 + 1/m_2)\) 是刚体物理引擎的核心。
  • 库仑摩擦模型区分静摩擦和动摩擦,比简单的速度衰减更真实。
  • 使用 inverseMass 可以统一处理静态物体和动态物体。

< 上一篇: 运动学基础 | 回到目录 | 下一篇: 渲染管线 >

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。