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

Rapier2D零重力场景中弹性球垂直速度归零的原因及解决方法

问题

在零重力环境下,用4个完全弹性壁构建盒子,内部放置完全弹性球,初始速度设为(100., 100.),理论上球应在盒内永久反弹。但实际每次反弹后,速度的y分量会逐渐趋近于零,最终仅在左右壁间往返运动。以下是使用rapier2d和macroquad实现的最小可复现代码:

use rapier2d::prelude as rapier;
use macroquad::prelude as mq;

struct PhysicsWorld {
    gravity: rapier::Vector<f32>,
    integration_parameters: rapier::IntegrationParameters,
    pipeline: rapier::PhysicsPipeline,
    island_manager: rapier::IslandManager,
    broad_phase: rapier::BroadPhase,
    narrow_phase: rapier::NarrowPhase,
    rigid_body_set: rapier::RigidBodySet,
    collider_set: rapier::ColliderSet,
    impulse_joint_set: rapier::ImpulseJointSet,
    multibody_joint_set: rapier::MultibodyJointSet,
    ccd_solver: rapier::CCDSolver,
    query_pipeline: rapier::QueryPipeline,
    physics_hooks: (),
    event_handler: (),
}

impl Default for PhysicsWorld {
    fn default() -> Self {
        let gravity = rapier::Vector::zeros();
        let integration_parameters = rapier::IntegrationParameters{
            num_solver_iterations: std::num::NonZeroUsize::new(16).unwrap(),
            prediction_distance: 0.001,
            ..Default::default()
        
        };
        let pipeline = rapier::PhysicsPipeline::new();
        let island_manager = rapier::IslandManager::new();
        let broad_phase = rapier::BroadPhase::new();
        let narrow_phase = rapier::NarrowPhase::new();
        let rigid_body_set = rapier::RigidBodySet::new();
        let collider_set = rapier::ColliderSet::new();
        let impulse_joint_set = rapier::ImpulseJointSet::new();
        let multibody_joint_set = rapier::MultibodyJointSet::new();
        let ccd_solver = rapier::CCDSolver::new();
        let query_pipeline = rapier::QueryPipeline::new();
        let physics_hooks = ();
        let event_handler = ();
        Self {
            gravity,
            integration_parameters,
            pipeline,
            island_manager,
            broad_phase,
            narrow_phase,
            rigid_body_set,
            collider_set,
            impulse_joint_set,
            multibody_joint_set,
            ccd_solver,
            query_pipeline,
            physics_hooks,
            event_handler,
        }
    }
}

impl PhysicsWorld {
    fn update(&mut self) {
        self.pipeline.step(
            &self.gravity,
            &self.integration_parameters,
            &mut self.island_manager,
            &mut self.broad_phase,
            &mut self.narrow_phase,
            &mut self.rigid_body_set,
            &mut self.collider_set,
            &mut self.impulse_joint_set,
            &mut self.multibody_joint_set,
            &mut self.ccd_solver,
            Some(&mut self.query_pipeline),
            &self.physics_hooks,
            &self.event_handler,
        );
    }

    pub fn add_ball_with_velocity(&mut self, radius: f32, x: f32, y: f32, vx: f32, vy: f32) -> (rapier::RigidBodyHandle, rapier::ColliderHandle) {
        let rigid_body = rapier::RigidBodyBuilder::dynamic().translation(rapier::vector![x, y]).linvel(rapier::vector![vx, vy]).build();
        let collider = rapier::ColliderBuilder::ball(radius).restitution(1.0).build();        
        let ball_r_handle = self.rigid_body_set.insert(rigid_body);
        let ball_c_handle = self.collider_set.insert_with_parent(collider, ball_r_handle, &mut self.rigid_body_set);
        return (ball_r_handle, ball_c_handle);
    }

    pub fn add_fixed_cuboid(&mut self, x: f32, y: f32, hx: f32, hy: f32) -> (rapier::RigidBodyHandle, rapier::ColliderHandle) {
        let rigid_body = rapier::RigidBodyBuilder::fixed().translation(rapier::vector![x, y]).build();
        let collider = rapier::ColliderBuilder::cuboid(hx, hy).restitution(1.0).build();
        let cuboid_r_handle = self.rigid_body_set.insert(rigid_body);
        let cuboid_c_handle = self.collider_set.insert_with_parent(collider, cuboid_r_handle, &mut self.rigid_body_set);
        return (cuboid_r_handle, cuboid_c_handle);
    }

    pub fn add_window_borders(&mut self) {
        let width = mq::screen_width();
        let height = mq::screen_height();
        let thickness = 10.0;
        let h_center_x = width / 2.0;
        let v_center_y = height / 2.0;
        let h_top_y = height - thickness / 2.0; //(center of top border)
        let h_bottom_y = thickness / 2.0; //(center of bottom border)
        let v_left_x = thickness / 2.0; //(center of left border)
        let v_right_x = width - thickness / 2.0; //(center of right border)
        let _top = self.add_fixed_cuboid(h_center_x, h_top_y, width / 2.0, thickness / 2.0);
        let _bottom = self.add_fixed_cuboid(h_center_x, h_bottom_y, width / 2.0, thickness / 2.0);
        let _left = self.add_fixed_cuboid(v_left_x, v_center_y, thickness / 2.0, height / 2.0);
        let _right = self.add_fixed_cuboid(v_right_x, v_center_y, thickness / 2.0, height / 2.0);
    }

    //draw the colliders (using the rigid body they are attached to)
    pub fn draw_colliders(&self) {
        for (_handle, collider) in self.collider_set.iter() {
            let rigid_body_handle = collider.parent().expect("Collider has no parent");
            let body = self.rigid_body_set.get(rigid_body_handle).unwrap();
            let pos = body.position().translation.vector;
            let shape: &dyn rapier::Shape = collider.shape();
            match (shape.as_ball(), shape.as_cuboid()) {
                (Some(ball), None) => {
                    mq::draw_circle(pos.x, pos.y, ball.radius, mq::WHITE);
                }
                (None, Some(cuboid)) => {
                    mq::draw_rectangle(pos.x - cuboid.half_extents.x, pos.y - cuboid.half_extents.y, cuboid.half_extents.x * 2.0, cuboid.half_extents.y * 2.0, mq::WHITE);
                }
                _ => {unreachable!()}
            }
        }
    }

    pub fn draw_ball_velocity_vector(&self, handle: rapier::RigidBodyHandle) {
        let body = self.rigid_body_set.get(handle).unwrap();
        let vel = body.linvel();
        mq::draw_text(&format!("vx: {:.2}, vy: {:.2}", vel.x, vel.y), 10., 30., 20.0, mq::WHITE);
    }
}

const ASPECT_RATIO: f32 = 16.0 / 9.0;
const WINDOW_WIDTH: f32 = 300.0;
const WINDOW_HEIGHT: f32 = WINDOW_WIDTH * ASPECT_RATIO;

fn main() {
    macroquad::Window::from_config(
        mq::Conf {
            window_width: WINDOW_WIDTH as i32,
            window_height: WINDOW_HEIGHT as i32,
            ..Default::default()
        },
        amain(),
    );
}


async fn amain() {
    let mut world = PhysicsWorld::default();
    let (ball, _) = world.add_ball_with_velocity(10.0, 200.0, 200.0, 100.0, 100.0);
    world.add_window_borders();
    loop {
        world.update();
        mq::clear_background(mq::BLACK);   
        world.draw_ball_velocity_vector(ball);     
        world.draw_colliders();
        mq::next_frame().await
    }
}
成因
  1. 默认速度阻尼的影响:Rapier的IntegrationParameters默认带有linear_damping = 0.01和angular_damping = 0.01配置,该参数会在每帧更新时对刚体速度进行微小衰减。单次衰减幅度虽小,但长期积累后,垂直方向的速度会逐渐被消耗至零。
  2. 数值精度误差积累:浮点数运算本身存在精度限制,多次碰撞后,速度计算的微小误差会逐步积累,加速垂直方向动能的损失。
修复方案

要实现无能量损失的永久反弹,需要关闭速度阻尼,并确保求解器参数足够减少数值误差:

修改PhysicsWorld的Default实现中的integration_parameters部分,添加关闭阻尼的配置,并设置最小求解器迭代次数与最大次数一致:

let integration_parameters = rapier::IntegrationParameters{
    num_solver_iterations: std::num::NonZeroUsize::new(16).unwrap(),
    min_island_solver_iterations: std::num::NonZeroUsize::new(16).unwrap(), // 确保每次都使用足够的迭代次数
    prediction_distance: 0.001,
    linear_damping: 0.0, // 关闭线性速度阻尼
    angular_damping: 0.0, // 关闭角速度阻尼
    ..Default::default()
};

完整修改后的代码

use rapier2d::prelude as rapier;
use macroquad::prelude as mq;

struct PhysicsWorld {
    gravity: rapier::Vector<f32>,
    integration_parameters: rapier::IntegrationParameters,
    pipeline: rapier::PhysicsPipeline,
    island_manager: rapier::IslandManager,
    broad_phase: rapier::BroadPhase,
    narrow_phase: rapier::NarrowPhase,
    rigid_body_set: rapier::RigidBodySet,
    collider_set: rapier::ColliderSet,
    impulse_joint_set: rapier::ImpulseJointSet,
    multibody_joint_set: rapier::MultibodyJointSet,
    ccd_solver: rapier::CCDSolver,
    query_pipeline: rapier::QueryPipeline,
    physics_hooks: (),
    event_handler: (),
}

impl Default for PhysicsWorld {
    fn default() -> Self {
        let gravity = rapier::Vector::zeros();
        let integration_parameters = rapier::IntegrationParameters{
            num_solver_iterations: std::num::NonZeroUsize::new(16).unwrap(),
            min_island_solver_iterations: std::num::NonZeroUsize::new(16).unwrap(),
            prediction_distance: 0.001,
            linear_damping: 0.0,
            angular_damping: 0.0,
            ..Default::default()
        
        };
        let pipeline = rapier::PhysicsPipeline::new();
        let island_manager = rapier::IslandManager::new();
        let broad_phase = rapier::BroadPhase::new();
        let narrow_phase = rapier::NarrowPhase::new();
        let rigid_body_set = rapier::RigidBodySet::new();
        let collider_set = rapier::ColliderSet::new();
        let impulse_joint_set = rapier::ImpulseJointSet::new();
        let multibody_joint_set = rapier::MultibodyJointSet::new();
        let ccd_solver = rapier::CCDSolver::new();
        let query_pipeline = rapier::QueryPipeline::new();
        let physics_hooks = ();
        let event_handler = ();
        Self {
            gravity,
            integration_parameters,
            pipeline,
            island_manager,
            broad_phase,
            narrow_phase,
            rigid_body_set,
            collider_set,
            impulse_joint_set,
            multibody_joint_set,
            ccd_solver,
            query_pipeline,
            physics_hooks,
            event_handler,
        }
    }
}

impl PhysicsWorld {
    fn update(&mut self) {
        self.pipeline.step(
            &self.gravity,
            &self.integration_parameters,
            &mut self.island_manager,
            &mut self.broad_phase,
            &mut self.narrow_phase,
            &mut self.rigid_body_set,
            &mut self.collider_set,
            &mut self.impulse_joint_set,
            &mut self.multibody_joint_set,
            &mut self.ccd_solver,
            Some(&mut self.query_pipeline),
            &self.physics_hooks,
            &self.event_handler,
        );
    }

    pub fn add_ball_with_velocity(&mut self, radius: f32, x: f32, y: f32, vx: f32, vy: f32) -> (rapier::RigidBodyHandle, rapier::ColliderHandle) {
        let rigid_body = rapier::RigidBodyBuilder::dynamic().translation(rapier::vector![x, y]).linvel(rapier::vector![vx, vy]).build();
        let collider = rapier::ColliderBuilder::ball(radius).restitution(1.0).build();        
        let ball_r_handle = self.rigid_body_set.insert(rigid_body);
        let ball_c_handle = self.collider_set.insert_with_parent(collider, ball_r_handle, &mut self.rigid_body_set);
        return (ball_r_handle, ball_c_handle);
    }

    pub fn add_fixed_cuboid(&mut self, x: f32, y: f32, hx: f32, hy: f32) -> (rapier::RigidBodyHandle, rapier::ColliderHandle) {
        let rigid_body = rapier::RigidBodyBuilder::fixed().translation(rapier::vector![x, y]).build();
        let collider = rapier::ColliderBuilder::cuboid(hx, hy).restitution(1.0).build();
        let cuboid_r_handle = self.rigid_body_set.insert(rigid_body);
        let cuboid_c_handle = self.collider_set.insert_with_parent(collider, cuboid_r_handle, &mut self.rigid_body_set);
        return (cuboid_r_handle, cuboid_c_handle);
    }

    pub fn add_window_borders(&mut self) {
        let width = mq::screen_width();
        let height = mq::screen_height();
        let thickness = 10.0;
        let h_center_x = width / 2.0;
        let v_center_y = height / 2.0;
        let h_top_y = height - thickness / 2.0; //(center of top border)
        let h_bottom_y = thickness / 2.0; //(center of bottom border)
        let v_left_x = thickness / 2.0; //(center of left border)
        let v_right_x = width - thickness / 2.0; //(center of right border)
        let _top = self.add_fixed_cuboid(h_center_x, h_top_y, width / 2.0, thickness / 2.0);
        let _bottom = self.add_fixed_cuboid(h_center_x, h_bottom_y, width / 2.0, thickness / 2.0);
        let _left = self.add_fixed_cuboid(v_left_x, v_center_y, thickness / 2.0, height / 2.0);
        let _right = self.add_fixed_cuboid(v_right_x, v_center_y, thickness / 2.0, height / 2.0);
    }

    //draw the colliders (using the rigid body they are attached to)
    pub fn draw_colliders(&self) {
        for (_handle, collider) in self.collider_set.iter() {
            let rigid_body_handle = collider.parent().expect("Collider has no parent");
            let body = self.rigid_body_set.get(rigid_body_handle).unwrap();
            let pos = body.position().translation.vector;
            let shape: &dyn rapier::Shape = collider.shape();
            match (shape.as_ball(), shape.as_cuboid()) {
                (Some(ball), None) => {
                    mq::draw_circle(pos.x, pos.y, ball.radius, mq::WHITE);
                }
                (None, Some(cuboid)) => {
                    mq::draw_rectangle(pos.x - cuboid.half_extents.x, pos.y - cuboid.half_extents.y, cuboid.half_extents.x * 2.0, cuboid
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 05:07:22