把五个箱子摞起来并稳住
目标
通过位置校正恢复已经穿透的重叠,通过迭代次数调整堆叠的刚度,并用图像记录五个箱子堆起并稳定的过程。本实验是课程 2 的收尾。
为什么重要
冲量只能修正速度,无法恢复已经穿透的重叠,因为发现重叠时物体已经互相穿透。因此,需要单独推动位置,而这里有两个调节项:每一步恢复多少百分比(percent),以及忽略多大余量(slop)。一次全部推出会发生弹跳,不留余量则会产生细微抖动。这一层的核心在于:不尝试把重叠精确降到 0,反而更加稳定。
此外,多个接触会相互耦合。修正上方会压迫下方,修正下方又会抬起上方。精确求解需要联立方程,成本很高,因此引擎会多次重复相同遍历来近似求解。迭代次数直接决定堆叠看起来有多坚固。
步骤
- 在
/root/stack中放置工具箱。 /root/stack/correct.py——位置校正。/root/stack/out/03-sink.txt——没有校正时的下沉深度。/root/stack/out/04-fixed.txt——启用校正后的深度。/root/stack/out/05-iters.txt——迭代次数 1、4、10。/root/stack/out/06-stack.txt和stack.png——堆叠五个箱子。/root/stack/out/f00.png~f11.png和index.html——堆叠过程。
参考
- 使用
nohup python3 -m http.server 8080 -d /root/stack/out &提供图像,并在网页预览中打开http://localhost:8080/。如果十二张图按顺序播放,就能看到箱子落下并堆叠。 - 请严格遵守单步顺序——重力、迭代速度求解、位置更新、位置校正。顺序不同,结果值也会不同。
- 常见错误一:在位置校正时连速度也一起改变。这样校正会变成一种力,使堆叠自行弹起。
- 常见错误二:不使用 slop,试图将重叠降到 0。每一步都会持续进行极小的校正,导致箱子抖动。
放置绘图工具箱
将示例中的 /root/stack/gfxlib.py 原样保存,并使用 /root/stack/check.py 绘制测试图案,生成 /root/stack/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/stack/correct.py 中创建 correct(depth, slop, percent, inv_m1, inv_m2)。计算 보정량 = max(depth - slop, 0) / (inv_m1 + inv_m2) * percent,并返回 (보정량*inv_m1, 보정량*inv_m2) 两个浮点数。这表示物体 1 沿 -n 方向移动相应距离,物体 2 沿 +n 方向移动相应距离。如果逆质量之和为 0,则返回 (0.0, 0.0)。
max(depth - slop, 0) 就是 slop 的全部含义。重叠浅于余量时,什么也不做。没有它,为了把重叠精确降到 0,校正会在每一步以极小值持续发生,使物体轻微抖动。
percent 表示每一步恢复百分之多少。设为 1.0 会一次全部推出,从而让物体弹开,并在下一步再次碰撞,形成振荡。通常使用 0.2 到 0.8。
先除以逆质量之和,再乘以各自逆质量,是为了让较轻的一方移动得更多。如果一侧是不动的物体(逆质量为 0),另一侧会承担全部移动。
没有校正时会在下沉状态下固定
将一个半高为 0.5 的箱子从 y=1.5、v=0 处落下,使用 g=-9.8、dt=1/60 模拟 600 步。每一步为:(1)v += g*dt;(2)迭代 10 次,如果 0.5 - y > 0 且 v < 0,则令 v = 0;(3)y += v*dt,并且不进行位置校正。将最终穿透深度 0.5 - y 写入 /root/stack/out/03-sink.txt,格式为 depth_final=(如果为负则写 0)。
即使将速度设为 0,已经穿透的部分仍会保留,因为发现重叠时已经发生了穿透。
而且在该状态下,下一步重力又会赋予速度,位置稍微下降,再次被设为 0。深度会停在某个值,这就是本步骤的答案。
600 步后的深度大于 0.02。它占箱子高度的若干百分比;堆叠多个箱子时,这种误差会累积,使整堆明显变矮。
令 v = 0 等同于恢复系数为 0 的冲量。地面的逆质量为 0,因此只改变箱子一侧。
启用校正后会恢复到 slop
在相同条件下增加(4)位置校正步骤——位置更新后计算 depth = 0.5 - y,如果 depth > slop,则执行 y += percent*(depth - slop)(slop=0.01、percent=0.2)。将 600 步后的深度写入 /root/stack/out/04-fixed.txt,格式为 depth_final=,并将与第 3 步结果的差以 improved= 写入。
校正只移动位置。如果同时改变速度,校正就会成为推动物体的力,使堆叠自行弹起。
深度会停在 slop 附近。没有降到 0 才是正常结果——保持极薄的重叠反而更加稳定。
improved 是 3단계 깊이 - 4단계 깊이。它必须为正,并且大约超过第 3 步深度的一半。
地面不移动,因此只移动箱子一侧。箱子之间则应按逆质量比例分配,向两侧推动。
迭代次数决定堆叠刚度
将三个半高为 0.5 的箱子从 y = 0.5 + i + 1.0(i=0,1,2)处落下并模拟 600 步,仅将迭代次数分别改为 1、4、10,然后把各自最大穿透深度写入 /root/stack/out/05-iters.txt,格式为 iters1=、iters4=、iters10=。启用位置校正(slop=0.01、percent=0.2)。
共有三个接触——地面与 0 号、0 号与 1 号、1 号与 2 号。在每次迭代中按此顺序求解。
箱子之间的接触处理如下。如果重叠 1.0 - (y[i+1] - y[i]) 大于 0,且相对速度 v[i+1] - v[i] 为负,则计算 j = -(v[i+1]-v[i]) / (inv_i + inv_{i+1}),并执行 v[i] -= j*inv_i、v[i+1] += j*inv_{i+1}。所有箱子的逆质量均为 1。
对三个接触都应用位置校正。箱子之间计算 m = percent*(depth-slop)/(inv_i+inv_{i+1}),使下方箱子向下移动 m*inv_i,上方箱子向上移动 m*inv_{i+1}。
最大穿透深度是三个接触深度中的最大值。迭代越多,它必须越小——这正是本步骤要观察的现象。
堆叠五个箱子
将五个箱子从 y = 0.5 + 2.0 + i*1.6(i=0..4)处落下,以 15 次迭代模拟 900 步,并将最后五个高度和最大穿透深度写入 /root/stack/out/06-stack.txt,格式为 y1= ~ y5=、max_depth=。然后将最终状态绘制到 /root/stack/out/stack.png(256x256,背景为 (20,20,28))——箱子的水平半宽为 1.5,屏幕坐标为 sx = int(x*25.6 + 128)、sy = int(255 - y*25.6),颜色自下而上为 (230,120,110)、(230,190,110)、(150,220,120)、(120,190,230)、(190,140,230),并在最后用白色绘制 sy=255 线表示地面。
让初始高度错开,箱子就会依次落下并堆叠。如果采用相同间距,它们会一起落下,没有可观察的过程。
一个箱子在屏幕上的水平范围为 89 到 166,垂直范围从 int(255-(y+0.5)*25.6) 到 int(255-(y-0.5)*25.6) 的矩形。使用 Canvas 的 fill_rect,一行即可完成。
最下方箱子会穿透一个 slop,因此矩形会延伸到 sy=255。如果先画地面线,箱子会把它覆盖。请最后绘制地面。
最终高度大约为 0.47、1.44、2.41、3.38、4.37。不是精确的 0.5、1.5、2.5,是因为它们在重叠一个 slop 的状态下稳定。
最大穿透深度不能超过 0.05。超过时,说明迭代次数不足或遗漏了位置校正。
用十二张图记录堆叠过程
在相同模拟中,从第 0 步开始每隔 15 步记录一次状态,生成从 /root/stack/out/f00.png 到 f11.png 的十二张图,并创建包含全部十二个名称的 /root/stack/out/index.html,使用 python3 -m http.server 8080 -d /root/stack/out 检查。绘图规则与第 6 步相同。
保存第 0、15、30、...、165 步的状态,之后一次性绘制即可。无需重新运行模拟十二次。
最后两张图几乎相同才是正常结果。这正是用数字确认堆叠已经稳定的方法——帧之间的差异是否收敛到 0。
相反,第一张和最后一张必须明显不同。第一张中箱子分散在空中,最后一张中箱子堆在地面上。
HTML 与前一实验相同,使用一个 <img> 和 setInterval 即可。