为何子步数会影响简易Verlet物理模拟的运动速度?
为何子步数会影响简易Verlet物理模拟的运动速度?
我开发了一个能正常运行的简易Verlet求解器,但调整模拟的子步数(substeps)时发现,子步数越多,物体运动速度越快,仿佛作用力被放大了。
重要伪代码片段
// Every frame let subDeltaTime = deltaTime / substeps;// In seconds for (let i = 0; i < substeps; i++) { for (let body of simulation.bodies) { // Gravity body.acceleration.y += 9.8 * body.mass; // Solve constraints and collisions // Update position using Verlet let displacement = body.position - body.oldPosition; body.oldPosition = body.position; body.position += displacement + body.acceleration * (subDeltaTime * subDeltaTime); body.acceleration *= 0; // Reset acceleration } }
已尝试的解决方法
- 将施加的力除以substeps的数量
- 仅在单次子步中累积加速度
- 固定帧率为60FPS、delta time为16ms
- 参考其他Verlet实现但未找到有效解决办法
问题根源分析
你的核心错误出在重力的施加逻辑与Verlet公式的结合上:
- 混淆力与加速度:重力的本质是加速度(9.8m/s²),而非力。代码中
9.8 * body.mass计算的是重力的大小(F=mg),但Verlet公式需要的是加速度,正确取值应为9.8(无需乘以质量)。 - 子步中重复错误施加重力:每个子步都对加速度累加一次重力相关值,再乘以
subDeltaTime²,结合你最小示例中额外将重力乘以delta*delta、更新时又重复乘一次的操作,导致重力贡献被错误放大,最终运动速度随子步数异常提升。
修正方案
调整重力施加逻辑,确保每个子步仅施加正确的加速度值,且仅在Verlet更新阶段引入时间因子:
修正后的核心伪代码
// Every frame let subDeltaTime = deltaTime / substeps;// In seconds for (let i = 0; i < substeps; i++) { for (let body of simulation.bodies) { // Gravity:直接设置加速度为重力加速度(无需乘质量或时间) body.acceleration.y = 9.8; // Solve constraints and collisions // Update position using Verlet let displacement = body.position - body.oldPosition; body.oldPosition = body.position; // 仅在此处乘以子步时间的平方 body.position += displacement + body.acceleration * (subDeltaTime * subDeltaTime); body.acceleration *= 0; // Reset acceleration } }
针对最小示例的修正代码
// Bodies - update let delta = dt / substeps; for (let i = 0; i < substeps; i++) { for (const body of bodies) { // Apply constraints(保留原有逻辑) if (body.x < body.radius) body.x = body.radius; if (body.y < body.radius) body.y = body.radius; if (body.x > canvas.width - body.radius) body.x = canvas.width - body.radius; if (body.y > canvas.height - body.radius) body.y = canvas.height - body.radius; // Collision(保留原有逻辑) for (const other of bodies) { if (other === body) continue; const distance = Math.sqrt(Math.pow(body.x - other.x, 2) + Math.pow(body.y - other.y, 2)); const max = other.radius + body.radius; if (distance < max) { let normalX = (body.x - other.x) / distance; let normalY = (body.y - other.y) / distance; let overlap = max - distance; body.x += normalX * overlap * 0.5; body.y += normalY * overlap * 0.5; other.x -= normalX * overlap * 0.5; other.y -= normalY * overlap * 0.5; } } // Apply gravity:仅设置加速度为9.8,不乘时间因子 body.accY = 9.8; // Apply verlet let displacementX = body.x - body.lastX; let displacementY = body.y - body.lastY; body.lastX = body.x; body.lastY = body.y; // 仅在此处乘以子步时间的平方 body.x += displacementX + body.accX * (delta * delta); body.y += displacementY + body.accY * (delta * delta); body.accX = 0; body.accY = 0; } }
修正逻辑说明
- 重力加速度是恒定值,每个子步只需直接赋值给加速度,无需累加或提前乘以时间因子。
- Verlet公式中的
dt²仅在位置更新阶段使用,对应当前子步的时间间隔,确保位移计算与时间尺度一致。 - 无论子步数多少,总时间跨度都是
dt,因此总位移不会随子步数变化,运动速度保持稳定。
内容的提问来源于stack exchange,提问作者LeandroSQ
相关产品推荐
相关产品推荐

