You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何子步数会影响简易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公式的结合上:

  1. 混淆力与加速度:重力的本质是加速度(9.8m/s²),而非力。代码中9.8 * body.mass计算的是重力的大小(F=mg),但Verlet公式需要的是加速度,正确取值应为9.8(无需乘以质量)。
  2. 子步中重复错误施加重力:每个子步都对加速度累加一次重力相关值,再乘以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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 03:37:05