1169 字
6 分钟
Bevy 下利用 Verlet 积分算法实现 2d 绳索物理效果的模拟
简单描述 Verlet 算法
Verlet 积分算法是一种数值积分方法,常用于物理模拟中,特别适合处理粒子系统和刚体动力学。它通过跟踪粒子的位置和前一位置来计算新的位置,从而避免了速度的直接计算。这种方法在模拟绳子、布料等柔性物体时表现出色,因为它能够自然地处理约束条件。
简单来说,我们可以把绳子看作一系列质点,每两个相邻质点之间有一条约束,可以把这条约束看作一根杆子,用来确保相邻两个质点之间的距离不变,这样我们就可以实现“牵一发而动全身”。至于质点的运动主要是靠两个位置来决定的——当前位置和旧位置。杆子的约束用来更新质点的当前位置,而质点可以根据verlet算法通过旧位置和当前位置来推出新位置,从而模拟出真实的物理效果。
Bevy 中实现
完整项目请见 👇
Waiting for api.github.com...
定义组件
这是质点组件
#[derive(Component)]struct Particle { current_pos: Vec2, old_pos: Vec2, is_locked: bool, // 这里方便后续确定是否是固定点}这是代表杆子的组件
#[derive(Component)]struct Stick { p1: Entity, p2: Entity, length: f32,}这个是代表锁定的组件,这样后续筛选锁住的质点的时候就不需要遍历整个质点了,而是通过 ECS 系统直接找到包含 Locked 组件的质点,有利于性能的提升。
#[derive(Component)]struct Locked;初始化
fn setup(mut commands: Commands) { // 生成一个 2D 摄像机 commands.spawn(Camera2d::default());
// 初始化一系列质点 let mut prev_particle = None; for i in 0..30 { let particle = commands .spawn((Particle { current_pos: Vec2::new(0.0, 300.0 - i as f32 * 10.0), // 每两个质点之间间隔 10 个像素 old_pos: Vec2::new(0.0, 300.0 - i as f32 * 10.0), is_locked: if i == 0 { true } else { false }, // 在这个 demo 里我把第一个点标记为了固定点跟随鼠标移动 },)) .id(); if i == 0 { commands.entity(particle).insert(Locked); } if let Some(prev_particle) = prev_particle { // 在这里初始化 “杆子” commands.spawn(Stick { p1: prev_particle, p2: particle, length: 10.0, }); } prev_particle = Some(particle); }}设置固定点跟随鼠标
fn follow_mouse( mut query: Query<&mut Particle, With<Locked>>, q_window: Query<&Window>, q_camera: Query<(&Camera, &GlobalTransform)>,) { let Ok(window) = q_window.single() else { return; }; let Ok((camera, camera_transform)) = q_camera.single() else { return; }; if let Some(cursor_pos) = window.cursor_position() { if let Ok(world_pos) = camera.viewport_to_world_2d(camera_transform, cursor_pos) { let Ok(mut locked_particle) = query.single_mut() else { return; }; locked_particle.current_pos = world_pos; } }}更新质点位置
这里就是利用了 verlet 算法的地方
Verlet 积分的核心在于利用位置函数 在时间点 附近的两个方向的 泰勒级数(Taylor Series) 展开。
1. 向前展开(预测未来)
我们预测下一时刻 的位置:
使用物理符号简化表示( 为速度, 为加速度):
2. 向后展开(回溯过去)
同理,我们可以写出上一时刻 的位置:
3. 相加消去速度项
将 (式1) 和 (式2) 相加,奇数阶导数项(包括速度 和加加速度 )由于正负抵消而消失:
4. 最终公式
移项解出 ,得到 Verlet 积分的标准形式:
为了对应代码逻辑,将其拆解为:
fn update_particles(time: Res<Time>, mut q_particles: Query<&mut Particle, Without<Locked>>) { let dt = time.delta_secs(); let dt_square = dt * dt; let gravity = Vec2::new(0.0, -1500.0);
for mut particle in q_particles.iter_mut() { let velocity = particle.current_pos - particle.old_pos; let new_pos = particle.current_pos + velocity + gravity * dt_square; particle.old_pos = particle.current_pos; particle.current_pos = new_pos; }}处理约束
fn handle_sticks(q_sticks: Query<&Stick>, mut q_particles: Query<&mut Particle>) { for _ in 0..50 { for stick in q_sticks.iter() { if let Ok([mut p1, mut p2]) = q_particles.get_many_mut([stick.p1, stick.p2]) { let delta = p2.current_pos - p1.current_pos; let distance = delta.length(); if distance == 0.0 { continue; } let diff = (distance - stick.length) / distance; let offset = delta * diff;
match (p1.is_locked, p2.is_locked) { (true, true) => {} (true, false) => { p2.current_pos -= offset; } (false, true) => { p1.current_pos += offset; } (false, false) => { p1.current_pos += offset * 0.5; p2.current_pos -= offset * 0.5; } } } } }}最后用 gizmos 画出来
fn draw_particles(mut gizmos: Gizmos, query: Query<(&Particle, Has<Locked>)>) { for (particle, is_locked) in query.iter() { if is_locked { gizmos.circle_2d(particle.current_pos, 3.0, Color::srgb(1.0, 0.0, 0.0)); } else { gizmos.circle_2d(particle.current_pos, 3.0, Color::srgb(0.0, 1.0, 0.0)); } }}主函数
fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems( FixedUpdate, (follow_mouse, update_particles, handle_sticks).chain(), ) .add_systems(Update, draw_particles) .run();}项目演示

Bevy 下利用 Verlet 积分算法实现 2d 绳索物理效果的模拟
https://blog.mengh04.top/posts/verlet积分算法模拟绳索/