Verlet积分需严格对齐初始位置与旧位置、固定时间步、统一累加力并乘dt²更新位移;结构体打包粒子数据提升缓存效率;碰撞后须镜像修正old_position以保持隐含速度;重力单位应匹配像素尺度。

Verlet 积分在 C++ 里怎么写才不飘、不炸
Verlet 积分本身很简单,但直接照公式抄代码,大概率跑几帧就 NaN 或粒子飞出屏幕。核心问题不是公式错,而是初始条件没对齐、时间步没归一、力没正确累加。
关键点:Verlet 不显式存速度,所以 position 和 old_position 必须严格对应上一帧和上上帧的状态;力必须在每个积分步开始前计算并应用一次,不能漏、不能重复加。
-
old_position初始化不能等于position,否则第一帧位移为 0,后续全错;推荐用position + velocity * dt反推(哪怕只用一次) - 时间步
dt必须固定,Verlet 对变步长极其敏感;如果用帧率驱动,务必做dt = clamp(dt, 0.01, 0.02)类似的保护 - 所有作用力(重力、弹簧、碰撞冲量)必须统一加到
acceleration上,再乘dt * dt算位移增量,别直接改position
用 std::vector 存粒子时,内存布局影响性能吗
影响明显,尤其粒子数超 1000 后。Verlet 需要频繁读写 position 和 old_position,如果两个数组分开存(比如 std::vector),CPU 缓存行会反复换入换出。
更稳的做法是结构体打包:struct Particle { Vec2 pos; Vec2 old_pos; float mass; };,然后单个 std::vector。这样每次访问一个粒子的全部状态,大概率落在同一缓存行内。
- 避免
std::vector这类“SOA 拆分”——Verlet 根本不用positions, velocities velocity - 如果要用 SIMD 加速(如 AVX),才考虑 AoS 改 SOA,但那是进阶优化,先确保单粒子逻辑正确再说
- 别用
new逐个分配Particle,std::vector的连续内存 +reserve()足够快
碰撞检测后怎么安全更新 old_position
碰撞响应后若直接改 position,下帧 Verlet 会误算位移,导致反弹过猛或卡墙。正确做法是:碰撞修正 position 后,同步反推修正 old_position,保持“隐含速度”合理。
例如墙碰撞把 pos.x 设为 boundary,那就该让 old_pos.x = 2 * boundary - pos.x(即镜像旧位置)。这样下帧的位移差就自然产生反弹效果,不需要额外加速度。
- 弹簧约束也一样:每次迭代后拉回粒子,同时按比例调整
old_position,而不是只动position - 如果用了阻尼(如
pos = lerp(pos, old_pos, 0.99)),必须在碰撞修正之后做,否则阻尼会削弱碰撞响应 - 别在碰撞中直接设
old_position = position,这等于清零速度,物体会突然“粘住”
为什么加了重力后粒子还是往下掉得慢
常见原因是重力单位没对齐:物理引擎常用 m/s²,而你的坐标系可能是像素,dt 是秒,但 1 px = 1 m 没声明,结果重力数值小了 100 倍。
Verlet 位移更新是 pos += (pos - old_pos) + acceleration * dt * dt,其中 acceleration * dt * dt 项对数值极其敏感。假设 dt = 0.016(60fps),那 dt² ≈ 0.00026,此时重力取 9.8 就只贡献约 0.0025 px 每帧——肉眼根本看不出动。
- 快速验证:临时把重力设成
1000.0f,看是否明显加速;如果是,说明单位缩放没调好 - 建议统一用“像素每秒平方”作单位,比如
gravity = 300.0f(即每秒向下加速 300px) - 别依赖全局
const float GRAVITY = 9.8f,它只在真实尺度模拟里有意义
Verlet 表面简单,但每个环节都咬着上一个环节的输出;最容易被忽略的是:它不给你“速度”这个中间态来调试,出问题只能靠打印 pos - old_pos 看位移向量是否合理。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











