原本计划06章是一个碰撞检测的小demo,上手之后才发现,碰撞反馈也是一个非常复杂的话题,所以就单拎出来一章,详细说明。碰撞反馈是基于碰撞检测的结果,将发生接触的物体分离开,同时应用上物理效果,使碰撞表现的接近于自然情况。
本文作者游蓝海。原创不易,未经许可,禁止任何形式的转载。
1. 刚体运动
1.1 刚体的概念
刚体,英文称作Rigidbody,是物理世界中的一个基本运动单元,包含了位移、旋转、质量、速度、力等物理属性。在物理引擎中,通常会把刚体看做是一个不可见的点,通过附加上不同的形状,才能与其他物体发生交互。从代码设计的角度而言,就是一种组合模式:一个刚体,包含了若干个形状。
public class Rigidbody
{
/// 位移
public Vector2 position;
/// 移动速度
public Vector2 velocity;
/// 质量
public float mass;
/// 质量的倒数。方便除法计算,对于静态物体,可以认为质量无限大,invMass=0,所有作用力相乘结果都是0。
public float invMass;
/// 持续作用力。持续影响移动速度
public Vector2 force;
/// 脉冲力。仅影响移动速度一次
public Vector2 forceImpulse;
/// 旋转角度
public float rotation;
/// 角速度。单位是度,计算作用力的时候,切记要转换成弧度。
public float angleVelocity;
/// 角动量。相当于旋转质量
public float inertial;
/// 角动量倒数。方便除法计算
public float invInertial;
/// 扭矩力。持续的影响旋转速度
public float torque;
/// 扭矩脉冲力。仅影响旋转速度一次
public float torqueImpulse;
/// 摩擦力
public float fraction = 0.3f;
/// 形状集合
public List<Shape> shapes;
}
1.2 形状的概念
形状(Shape),在一些物理引擎里也叫碰撞体(Collider),描述了物体的轮廓信息。常用的2D形状有圆形、线段、多边形,三角形和矩形也可以归为多边形。
1.3 牛顿三大定律
详细内容可以参考百科-牛顿运动定律,这里仅简单回顾一下。
- 匀速运动。
S = S0 + v * t
- 加速度。
a = F / m; v = v0 + a * t
- 力是相互的。
F' = -F
1.3 线性运动
主要影响物体的位移和位移的速度。
velocity += force * invMass * dt;
position += velocity * dt;
1.4 旋转运动
主要影响物体的角度和旋转速度。
angleVelocity += torque * invInertial * dt;
rotation += angleVelocity * dt;
2. 碰撞反馈
2.1 碰撞接触信息
两个形状是否发生碰撞,可以使用GJK算法进行检测。当检测到碰撞后,再使用EPA算法,计算穿透方向和穿透深度。至于碰撞接触点,简单起见,可以使用求最近距离的方式,从闵可夫斯基差集多边形的边上计算得到最近点。
if (!isCollision)
{
computeClosetPoint(simplex.getSupport(0), simplex.getSupport(1));
}
else
{
queryEPA();
computeClosetPoint(currentEpaEdge.a, currentEpaEdge.b);
}
...
void getContacts(List<ContactInfo> list)
{
Edge e = gjk.currentEpaEdge;
ContactInfo a = new ContactInfo
{
point = gjk.closestOnA, // 接触点
normal = e.normal, // 穿透方向
penetration = e.distance, // 穿透深度
};
list.Clear();
list.Add(a);
}
2.2 反作用力
两个物体发生碰撞后,会产生反作用力将两个物体分离开。作用力和反作用力是相互的,两者大小相等,方向相反。碰撞分离的关键点,就是如何求得作用力,然后改变物体的运动速度和旋转角度。
已知两个物体碰撞后在某时刻的速度和质量,可以根据动量定律,求得该时刻的动量dF。以下公式是box2d中采用的动量计算公式:
Δ F ⃗ = m Δ v ⃗ Δ v ⃗ = v ⃗ 1 − v ⃗ 2 + u ⃗ 1 − u ⃗ 2 u ⃗ = r ⃗ t w m = 1 k k = 1 m 1 + 1 m 2 + 1 I 1 q 1 + 1 I 2 q 2 q n = r 2 − ∣ r ⃗ ⋅ n ⃗ ∣ 2 q t = r 2 − ∣ r ⃗ ⋅ t ⃗ ∣ 2 \begin{aligned} & \Delta \vec F = m \Delta \vec v \\ & \Delta \vec v = \vec v_1 - \vec v_2 + \vec u_1 - \vec u_2 \\ & \vec u = \vec r_t \, w \\ & m = \frac{1}{k} \\ & k = \frac{1}{m_1} + \frac{1}{m_2} + \frac{1}{I_1} q_1 + \frac{1}{I_2} q_2 \\ & q_n = r^2 - |\vec r \cdot \vec n|^2 \\ & q_t = r^2 - |\vec r \cdot \vec t|^2 \end{aligned} ΔF=mΔvΔv=v1−v2+u1−u2u=rtwm=k1k=m11+m21+I11q1+I21q2qn=r2−∣r⋅n∣2qt=r2−∣r⋅t∣2
- dv是该时刻两个物体上的碰撞点的相对速度,包含线性移动速度v和旋转速度u之和。
- u是碰撞点的旋转速度,而不是刚体的旋转速度。点的旋转速度为点在单位时间沿着圆周转过的弧长,设点到中心的半径为r,刚体转速为w弧度/秒,则单位时间转过的弧长为:
u = r * w
。而旋转方向,就是点到中心向量的切线方向; - m是该时刻的有效质量。这个我还没有弄清楚,如果有懂的朋友,不妨留言说明一下;
- q是点的旋转惯性。也是没有理解。注意,旋转惯性在法线方向和切线方向不一样,需要分开计算。
为了方便计算,沿着穿透向量方向,可以将作用力分解为平行和垂直的两个分量,分别记作法线方向n和切线方向t。法线方向主要体现为反弹力,切线方向主要体现为摩擦力。
Δ F n = max ( m n Δ v n , 0 ) Δ v n = Δ v ⃗ ⋅ n ⃗ Δ F t = c l a m p ( m t Δ v t , − μ Δ F n , μ Δ F n ) Δ v t = Δ v ⃗ ⋅ t ⃗ \begin{aligned} & \Delta F_n = \max(m_n \Delta v_n, 0) \\ & \Delta v_n = \Delta \vec v \cdot \vec n \\ & \Delta F_t = clamp(m_t \Delta v_t, -\mu \Delta F_n, \mu \Delta F_n) \\ & \Delta v_t = \Delta \vec v \cdot \vec t \\ \end{aligned} \\ ΔFn=max(mnΔvn,0)Δvn=Δv⋅nΔFt=clamp(mtΔvt,−μΔFn,μΔFn)Δvt=Δv⋅t
作用力Fn初始的时候是指向穿透方向的,也就是正数,反作用力是-Fn。碰撞的过程中受力是连续发生变化的,作用力会由大变小,直到变为0。发起碰撞的刚体,其速度会逐步减小至0,然后开始反向运动,当反作用力为0的时候,其速度也就达到了最大值。整个碰撞过程中,Fn的值是需要限定到0以上的,否则又会把物体拉回来。
作用力Ft会让物体侧滑,而滑动就要受到摩擦力的影响。u = 1 - fraction
,当摩擦系数fraction=0的时候,侧滑作用力达到最大;当摩擦系数fraction = 1的时候,没有侧滑作用力。
2.3 速度改变
发起碰撞的刚体body1,受到反向作用力;被碰撞的刚体body2,受到正向作用力。
// 法线和切线方向力之和
Vector2 F = normal * Fn + tangent * Ft;
// 刚体中心点到碰撞接触点的向量
Vector2 r1 = point - body1.position;
Vector2 r2 = point - body2.position;
body1.velocity += -F / body1.m;
body1.angleVelocity += cross(-F, r1) * body1.angleVelocity / body1.I;
body2.velocity += F / body2.m;
body2.angleVelocity += cross(F, r2) * body2.angleVelocity / body2.I;
2.4 重叠分离
当两个物体发生重叠,在没有运动速度,或者速度很小的时候,是无法依靠碰撞反馈来将两个物体分开的。比如,通过代码直接设置了物体坐标,导致物体间发生了重叠。针对这种情况,需要增加一个分离速度,将发生重叠的物体“挤”出去。
设重叠的深度为s,则分离速度为:v = s / dt
,在计算碰撞作用力的时候,叠加上这样一个偏移速度,就可以产生一个挤出力。但是,当时间dt很小的时候,这个分离速度将会变的很大,看起来很不真实。可以通过给公式增加一些系数,来灵活控制挤出速度。
Δ
F
n
=
max
(
m
n
(
Δ
v
n
+
v
b
)
,
0
)
v
b
=
β
max
(
0
,
s
−
δ
)
Δ
t
,
(
β
是
缩
放
系
数
,
δ
是
可
容
忍
的
穿
透
深
度
)
\Delta F_n = \max(m_n(\Delta v_n + v_b), 0) \\ v_b = \frac{ \beta \max(0, s - \delta)}{\Delta t}, \\ (\beta是缩放系数,\delta是可容忍的穿透深度)
ΔFn=max(mn(Δvn+vb),0)vb=Δtβmax(0,s−δ),(β是缩放系数,δ是可容忍的穿透深度)
3. 范例
本章Demo使用Unity3D引擎开发,Demo工程已上传github: https://github.com/youlanhai/learn-physics/tree/master/Assets/06-seperation
4. 参考
- Erin Catto, GDC 2006 Physics Tutorial
- Erin Catto, How Do Physics Engines Work
- box2d-lite 轻量级的物理引擎
- box2d物理引擎文档
- chipmunk物理引擎文档
本系列文章会和我的个人公众号同步更新,感兴趣的朋友可以关注下我的公众号:游戏引擎学习。扫下面的二维码加关注: