冲量、回弹、摩擦
目标
使用冲量计算碰撞响应,并通过数字和图像确认恢复系数与摩擦分别决定什么。完成本实验后,你将能够亲手构建物理引擎的响应层。
为什么重要
刚性物体的接触时间甚至不到 1 毫秒,因此以每秒 60 帧运行的模拟无法重现接触过程。物理引擎会跳过过程,只计算结果:建立碰撞前后速度关系的方程,一次求出满足方程的速度变化量并应用。
这里需要掌握两个约定。第一是保存逆质量。把静止物体的逆质量设为 0,无需特殊处理就能继续使用同一公式。第二是物体正在远离时不要干预。如果遗漏这项检查,就会把正要离开的物体再次拉回,使其黏在表面上。
步骤
- 在
/root/impulse中放置工具箱。 /root/impulse/impulse.py——冲量公式。/root/impulse/out/03-conserve.txt——动量与能量。/root/impulse/out/bounce.csv、04-bounce.txt、bounce.png——反弹高度。/root/impulse/out/05-friction.txt——滑动的箱子。/root/impulse/out/06-tangent.txt——切线方向摩擦。/root/impulse/out/balls.png与07-balls.txt——箱子里的五个球。
参考
- 使用
nohup python3 -m http.server 8080 -d /root/impulse/out &提供图像,并在网页预览中打开http://localhost:8080/。 - 常见错误 1:相对速度已经为正(正在远离),却仍应用冲量。物体会黏在表面上。
- 常见错误 2:摩擦计算中不把速度截断在 0。速度会降为负数,箱子反向移动。
- 常见错误 3:使用应用法向冲量之后的速度计算切线方向。这样会得到不同结果。
放置绘图工具箱
将示例中的 /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*n、v2 += 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.0、v=0 落下,以 g=-9.8、dt=1/240 模拟 2400 步。每一步执行 v += g*dt、y += v*dt;若 y < 0,则用 y = -y、v = -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/peak1 与 peak3/peak2 都应接近 e² = 0.49。因为速度缩小为 e 倍时,上升高度会缩小为 e 的平方倍。
图中会出现熟悉的、峰值逐渐降低的形状。这一张图就能说明恢复系数的含义。
用 y = -y 反弹,是把穿透量镜像回去。与直接截断为 0 相比,能量损失更少。
滑动箱子会在哪里停止
对初速度 5.0 的滑动箱子施加库仑摩擦(mu=0.3、g=9.8),以 dt=1/240 模拟。每一步执行 v = max(0, v - mu*g*dt)、x += v*dt;v 变为 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=1、n=(0,1)、e=0.5、mu=0.4,在 /root/impulse/out/06-tangent.txt 中写入 vout=vx,vy、jn=、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.8、dt=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 = r、vx = -e*vx;另一侧同理,y 轴也相同。
每一步的顺序是应用重力、更新位置、处理墙面。改变顺序会使球穿过墙。
能量必须减少。恢复系数小于 1,因此每次撞墙都会损失少量能量。如果能量增加,说明墙面处理中缺少位置恢复或符号错误。
轨迹会保留在图中,可以直观看到由一段段抛物线组成的弹跳路径。