Verlet积分通过位置历史差分隐式表达加速度,核心公式为pos_new = 2pos_current - pos_old + accelerationdt*dt;需固定dt、缓存两帧位置、力累积后统一处理,并配合PBD约束求解。

Verlet 积分在 C++ 中的核心实现逻辑
Verlet 积分不显式存储速度,而是靠位置的历史差分来隐式表达加速度和惯性——这正是它稳定、抗漂移、适合约束系统(如布料、绳子)的关键。你不需要写 velocity += acceleration * dt 这类易累积误差的欧拉步骤。
最简形式的位移 Verlet 是:
pos_new = 2 * pos_current - pos_old + acceleration * dt * dt
其中 pos_old 是上一帧的位置,pos_current 是当前帧位置,dt 是固定时间步长(必须固定!可变 dt 会严重破坏稳定性)。
实操建议:
-
dt建议设为0.01f或0.016f(60Hz),避免用glfwGetTime()等返回的不规则 delta - 每个质点需缓存两个位置:用
std::vector<:vec3> pos和std::vector<:vec3> pos_old,别试图只存一个velocity向量去“还原” - 加速度项不能直接写成
force / mass,而要先累积所有力(重力、弹簧力、碰撞冲量等),再统一除以质量后乘dt * dt
怎么用 Verlet 模拟弹簧连接的质点链(绳子/布料边)
Verlet 本身不处理约束;真正让绳子“绷直”、布料“不穿透”的是后续的位置约束求解(Position-Based Dynamics, PBD)。Verlet 只负责预测位置,PBD 负责把预测后违反约束的位置“拉回来”。
例如一根长度为 L 的弹簧连接质点 i 和 j,约束函数是 ||x_i - x_j|| == L。一次 Jacobi 迭代的修正如下:
glm::vec3 delta = pos[i] - pos[j];
float current_len = glm::length(delta);
if (current_len > 1e-5f) {
float diff = (current_len - L) / current_len;
float shift = diff * 0.5f; // 0.5 是权重,用于软化(可调)
pos[i] -= delta * shift;
pos[j] += delta * shift;
}
注意:shift 乘了 0.5f 是因为两端各承担一半修正量;若质点质量不同,应按质量反比分配(比如 inv_mass[i] / (inv_mass[i] + inv_mass[j]))。
常见错误现象:
- 绳子“抖动发散”:没做足够轮数的约束迭代(通常 3–8 次,布料建议 ≥5)
- 绳子“过软塌陷”:约束迭代次数太少,或
L初始值设得比静止长度小 - 质点穿模穿透刚体:碰撞约束没插在弹簧约束之后执行,或法向修正量没乘
restitution和阻尼因子
为什么不要在 Verlet 里手动维护 velocity
很多初学者想从 pos 和 pos_old 算出 velocity 用于风力、阻尼或渲染拖尾——这是可行的,但必须清楚:这个速度是中心差分估算值,不是状态变量。
计算方式是:velocity ≈ (pos_current - pos_old) / (2.0f * dt)。但它不能反向用于更新位置(否则退化为 Velocity Verlet,失去原生 Verlet 的约束友好性)。
实操中更安全的做法是:
- 仅在需要时临时算
velocity(比如施加空气阻力:drag = -k * velocity),且确保该力被加入到下一帧的加速度累加中 - 完全避免在约束求解阶段使用
velocity——PBD 全程只操作pos和pos_old - 如果真需要高精度速度(如粒子特效),用 Velocity Verlet 变体,但要同步维护
velocity并在每次约束后重新投影速度(复杂度陡增)
性能关键点:如何避免每帧 O(n²) 的碰撞检测
布料节点数一过几百,朴素的两两距离检测(for(i) for(j))立刻卡死。Verlet 物理对延迟敏感,必须降复杂度。
推荐方案是空间哈希(Spatial Hash)或网格划分(Grid-based Broad Phase):
- 把世界划分为边长 ≈ 最大弹簧长度 × 1.2 的三维格子(2D 布料用二维格子)
- 每帧清空哈希表,遍历所有质点,将其
grid_index = floor(pos / cell_size)作为 key 插入,value 是质点索引列表 - 对每个非空格子,只检查它自身及 26 个邻格(2D 是 8 个)内的质点对——复杂度降到近似 O(n)
容易踩的坑:
- 格子尺寸设得太小 → 碎片化严重,哈希冲突多;太大 → 邻格遍历量爆炸
- 忘记在约束迭代后更新质点位置对应的格子索引(导致下一帧漏检)
- 用
std::unordered_map存格子,但 key 是glm::ivec3未自定义 hash 函数 → 编译失败或行为未定义
边界处理比想象中麻烦:布料边缘质点可能被拉出格子范围,floor 后坐标溢出,得提前 clamp 或用带符号整型哈希。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











