LabHub
学习 学习路径 课程

物理引擎的骨架

冲量、回弹、摩擦

在 LabHub 中继续学习

目标

使用冲量计算碰撞响应,并通过数字和图像确认恢复系数与摩擦分别决定什么。完成本实验后,你将能够亲手构建物理引擎的响应层。

为什么重要

刚性物体的接触时间甚至不到 1 毫秒,因此以每秒 60 帧运行的模拟无法重现接触过程。物理引擎会跳过过程,只计算结果:建立碰撞前后速度关系的方程,一次求出满足方程的速度变化量并应用。

这里需要掌握两个约定。第一是保存逆质量。把静止物体的逆质量设为 0,无需特殊处理就能继续使用同一公式。第二是物体正在远离时不要干预。如果遗漏这项检查,就会把正要离开的物体再次拉回,使其黏在表面上。

步骤

  1. /root/impulse 中放置工具箱。
  2. /root/impulse/impulse.py——冲量公式。
  3. /root/impulse/out/03-conserve.txt——动量与能量。
  4. /root/impulse/out/bounce.csv04-bounce.txtbounce.png——反弹高度。
  5. /root/impulse/out/05-friction.txt——滑动的箱子。
  6. /root/impulse/out/06-tangent.txt——切线方向摩擦。
  7. /root/impulse/out/balls.png07-balls.txt——箱子里的五个球。

参考

放置绘图工具箱

将示例中的 /root/impulse/gfxlib.py 原样保存,并通过 /root/impulse/check.py 绘制测试图案,生成 /root/impulse/out/00-check.png。图案为 64x64 黑色背景:先从 (0,0) 到 (63,63) 画白色(255,255,255)对角线,再从 (0,32) 到 (63,32) 叠加红色(255,0,0)水平线。

从本实验开始,不再重新编写 PNG 编码器。系统把与第一个实验中手工编写的相同代码作为工具提供——因为这里要学习的不是文件格式。

实验 Pod 没有持久卷,因此前一个实验创建的文件不会保留。所以每个实验都从重新放置工具箱开始。

创建 Canvas(w, h, bg),使用 line(x0, y0, x1, y1, rgb) 绘制两条线,再用 write_png(path) 保存。必须后画水平线,交点 (32,32) 才会呈红色。

本实验会用它绘制反弹高度图和球的轨迹。

冲量公式

/root/impulse/impulse.py 中创建 resolve(v1, inv_m1, v2, inv_m2, n, e)。速度是二维元组,n 是从物体 1 指向物体 2 的单位向量。如果相对速度的法向分量大于等于 0,则不做任何处理并原样返回;若为负,则计算 j = -(1+e)*v_rel/(inv_m1+inv_m2),应用 v1 -= j*inv_m1*nv2 += j*inv_m2*n 后,以 (v1, v2) 返回结果。

v_rel = dot(v2 - v1, n)。该值为正表示两个物体已经在远离,因此不能干预。遗漏这项检查会把正要离开的物体再次拉回,使其黏在表面上。

接收逆质量而不是质量,是因为可以用 inv_m = 0 自然表示静止物体。如果使用无穷大,每次除法都需要特殊处理。

验证方法是让两个等质量物体正面碰撞。e=1 时两者交换速度,e=0 时两者得到相同速度。

什么被守恒,什么会消失

质量为 2 的物体 1(速度 (3,0))与质量为 1 的物体 2(速度 (-1,0))沿法向 (1,0) 碰撞。分别求解 e=1、0.5、0,并在 /root/impulse/out/03-conserve.txt 中写入 p_before=ke_before=p_e1=ke_e1=p_e05=ke_e05=p_e0=ke_e0= 八行。动量是 x 分量之和,动能是 0.5*m*v² 之和(小数点后六位)。

两个逆质量分别为 0.5 和 1.0。

**三种情况下动量都必须相同。**因为两个物体受到大小相等、方向相反的冲量,这在结构上必然成立。如果结果不同,说明符号错误。

只有 e=1 时动能守恒;e 越小,动能越少。现实中消失的能量转化为声音、热量和形变。

e=0 时两个物体速度相同,表示它们黏在一起共同运动。

反弹高度按 e 的平方衰减

让球从 y=5.0v=0 落下,以 g=-9.8dt=1/240 模拟 2400 步。每一步执行 v += g*dty += v*dt;若 y < 0,则用 y = -yv = -0.7*v 反弹。把结果写入 /root/impulse/out/bounce.csv,包含 step,y,v 表头和 2401 行;把前三次反弹后的最高点写入 /root/impulse/out/04-bounce.txt,格式为 peak1=peak2=peak3=。再在 /root/impulse/out/bounce.png(256x256,黑色背景)中按 px = int(step*255/2400)py = 255 - int(y/5.0*255) 绘制颜色为 (120,200,255) 的点。

反弹后的最高点是 y 大于等于相邻两个值的位置(局部最大值)。取最初三个即可。

计算比值。peak2/peak1peak3/peak2 都应接近 e² = 0.49因为速度缩小为 e 倍时,上升高度会缩小为 e 的平方倍。

图中会出现熟悉的、峰值逐渐降低的形状。这一张图就能说明恢复系数的含义。

y = -y 反弹,是把穿透量镜像回去。与直接截断为 0 相比,能量损失更少。

滑动箱子会在哪里停止

对初速度 5.0 的滑动箱子施加库仑摩擦(mu=0.3g=9.8),以 dt=1/240 模拟。每一步执行 v = max(0, v - mu*g*dt)x += v*dtv 变为 0 时停止。在 /root/impulse/out/05-friction.txt 中用 steps=stop_time=distance= 三行记录耗时和移动距离。

摩擦不与速度成正比,而是产生恒定减速度。因此速度线性下降,时间和距离都可解析计算。

t = v0 / (mu * g)        = 5 / 2.94 = 1.7007 s
d = v0² / (2 * mu * g)   = 25 / 5.88 = 4.2517 m

模拟值应与公式结果相差不超过 1%。不会完全相同,因为离散步进会截去最后一小段。

如果没有 max(0, ...),速度会降为负数,箱子会**向后移动。**这相当于摩擦推动了物体,也是实际引擎中的常见错误。

摩擦也是冲量

/root/impulse/tangent.py 中创建 bounce_with_friction(v, inv_m, n, e, mu),处理一个撞击静止表面(逆质量 0)的物体。计算并应用法向冲量 jn = -(1+e)*dot(v,n)/inv_m,然后针对切线方向 t = normalize(v - dot(v,n)*n) 计算 jt = -dot(v,t)/inv_m,将其截断到 [-mu*jn, +mu*jn] 后应用,并返回最终速度。返回 (v_out, jn, jt) 三个值;对于 v=(3,-3)inv_m=1n=(0,1)e=0.5mu=0.4,在 /root/impulse/out/06-tangent.txt 中写入 vout=vx,vyjn=jt=

切线方向是速度减去法向分量后的余量。如果余量为 0(正面碰撞),切线没有定义,应跳过摩擦。

截断 jt 就是库仑摩擦的全部。如果完全消除切向速度所需的值不超过 mu*jn,物体会停在原地(静摩擦);如果超过,只应用截断值,物体继续滑动(动摩擦)。一个条件分支即可产生两种摩擦。

在给定情况下,jn=4.5,截断前 jt=-3,上限为 0.4*4.5=1.8,因此 jt=-1.8。最终速度为 (1.2, 1.5)

**法向分量和切向分量必须使用应用 jn 之前的速度计算。**打乱顺序会得到不同结果。

箱子里的五个球

在边长 10 的箱子中模拟五个半径为 0.5 的球,设置 g=-9.8dt=1/240、墙面恢复系数 0.9,共 1800 步。初始位置依次为 (2,8)、(5,9)、(8,7)、(3,5)、(7,4),速度依次为 (3,0)、(-2,1)、(0,0)、(4,2)、(-3,-1)。每 10 步把各球位置绘制到 /root/impulse/out/balls.png(256x256,黑色背景,sx=int(x*25.6)sy=int(255-y*25.6)),颜色依次为 (255,100,100)、(100,255,100)、(100,150,255)、(255,220,80)、(220,120,255);并在 /root/impulse/out/07-balls.txt 中从 b1=x,y,vx,vy 写到 b5=,再写入 energy_start=energy_end=。能量公式为 sum(0.5*(vx²+vy²) + 9.8*y)

墙面碰撞按每个坐标轴分别处理。如果 x - r < 0,则设 x = rvx = -e*vx;另一侧同理,y 轴也相同。

每一步的顺序是应用重力、更新位置、处理墙面。改变顺序会使球穿过墙。

能量必须减少。恢复系数小于 1,因此每次撞墙都会损失少量能量。如果能量增加,说明墙面处理中缺少位置恢复或符号错误。

轨迹会保留在图中,可以直观看到由一段段抛物线组成的弹跳路径。