dimforge / rapier

2D and 3D physics engines focused on performance.
https://rapier.rs
Apache License 2.0
3.77k stars 235 forks source link

Perfectly Elastic Ball Loses Velocity After Bounces in Zero Gravity Environment #615

Closed CraigBuilds closed 2 months ago

CraigBuilds commented 2 months ago

I have set up a simulation using Rapier2D within a macroquad environment, where a perfectly elastic ball bounces indefinitely within a box formed by four perfectly elastic walls, in zero-gravity. The ball is given an initial velocity vector of (100, 100), and according to the setup, it should bounce off the walls without losing energy, maintaining its velocity magnitude due to the elasticity settings.

However, I observe that after several bounces, the ball's velocity decreases gradually (particularly the y velocity) until the ball no longer bounces vertically, effectively moving only horizontally between the left and right walls.

Minimal Working Example

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
    }
}

Expected Behavior

The ball should bounce indefinitely within the box, with its velocity vector's magnitude remaining constant, reflecting perfectly elastic collisions with the walls.

Observed Behavior

The x and y component of the ball's velocity decreases after each bounce. The y component decreases faster, leading to a situation where the ball eventually only moves horizontally.

CraigBuilds commented 2 months ago

I have tried restoring the energy by calling this every loop:

    //rescale the velocities of all bodies to correct the total energy to its expected value 
    pub fn restore_energy(&mut self, handle: rapier::RigidBodyHandle, expected_energy: f32) {
        let body = self.rigid_body_set.get_mut(handle).unwrap();
        let current_energy = body.linvel().norm_squared() * body.mass();
        let scale = (expected_energy / current_energy).sqrt();
        body.set_linvel(body.linvel() * scale, true);
    }

And this has worked to some extent, as the total energy now stays the same. However, the y velocity still goes towards zero.

CraigBuilds commented 2 months ago

I've solved this myself. I needed to set friction to zero. Closing issue.