use rapier3d::prelude::*;
#[test]
fn woken_body_is_supported_by_recycled_contacts() {
let mut bodies = RigidBodySet::new();
let mut colliders = ColliderSet::new();
let mut impulse_joints = ImpulseJointSet::new();
let mut multibody_joints = MultibodyJointSet::new();
let mut pipeline = PhysicsPipeline::new();
let mut bf = BroadPhaseBvh::new();
let mut nf = NarrowPhase::new();
let mut islands = IslandManager::new();
let mut ccd = CCDSolver::new();
let params = IntegrationParameters::default();
let gravity = Vector::Y * -9.81;
let ground = bodies.insert(RigidBodyBuilder::fixed().translation(Vector::new(0.0, -0.5, 0.0)));
colliders.insert_with_parent(
ColliderBuilder::cuboid(10.0, 0.5, 10.0),
ground,
&mut bodies,
);
let cube = bodies.insert(RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 0.6, 0.0)));
colliders.insert_with_parent(ColliderBuilder::cuboid(0.5, 0.5, 0.5), cube, &mut bodies);
let step = |bodies: &mut RigidBodySet,
colliders: &mut ColliderSet,
islands: &mut IslandManager,
bf: &mut BroadPhaseBvh,
nf: &mut NarrowPhase,
impulse_joints: &mut ImpulseJointSet,
multibody_joints: &mut MultibodyJointSet,
ccd: &mut CCDSolver,
pipeline: &mut PhysicsPipeline| {
pipeline.step(
gravity,
¶ms,
islands,
bf,
nf,
bodies,
colliders,
impulse_joints,
multibody_joints,
ccd,
&(),
&(),
);
};
let mut slept = false;
for _ in 0..400 {
step(
&mut bodies,
&mut colliders,
&mut islands,
&mut bf,
&mut nf,
&mut impulse_joints,
&mut multibody_joints,
&mut ccd,
&mut pipeline,
);
if bodies[cube].is_sleeping() {
slept = true;
break;
}
}
assert!(slept, "the cube never fell asleep");
bodies[cube].apply_impulse(Vector::new(0.5, 0.0, 0.0), true);
assert!(!bodies[cube].is_sleeping());
for _ in 0..120 {
step(
&mut bodies,
&mut colliders,
&mut islands,
&mut bf,
&mut nf,
&mut impulse_joints,
&mut multibody_joints,
&mut ccd,
&mut pipeline,
);
let y = bodies[cube].translation().y;
assert!(
y > 0.4,
"woken cube sank into the ground (y = {y}): its contacts were not solved"
);
}
}
struct World {
bodies: RigidBodySet,
colliders: ColliderSet,
impulse_joints: ImpulseJointSet,
multibody_joints: MultibodyJointSet,
pipeline: PhysicsPipeline,
bf: BroadPhaseBvh,
nf: NarrowPhase,
islands: IslandManager,
ccd: CCDSolver,
params: IntegrationParameters,
}
impl World {
fn new() -> Self {
let mut world = Self {
bodies: RigidBodySet::new(),
colliders: ColliderSet::new(),
impulse_joints: ImpulseJointSet::new(),
multibody_joints: MultibodyJointSet::new(),
pipeline: PhysicsPipeline::new(),
bf: BroadPhaseBvh::new(),
nf: NarrowPhase::new(),
islands: IslandManager::new(),
ccd: CCDSolver::new(),
params: IntegrationParameters::default(),
};
let ground = world
.bodies
.insert(RigidBodyBuilder::fixed().translation(Vector::new(0.0, -0.5, 0.0)));
world.colliders.insert_with_parent(
ColliderBuilder::cuboid(50.0, 0.5, 50.0),
ground,
&mut world.bodies,
);
world
}
fn add_cube(&mut self, builder: RigidBodyBuilder) -> RigidBodyHandle {
let handle = self.bodies.insert(builder);
self.colliders.insert_with_parent(
ColliderBuilder::cuboid(0.5, 0.5, 0.5),
handle,
&mut self.bodies,
);
handle
}
fn step(&mut self) {
self.pipeline.step(
Vector::Y * -9.81,
&self.params,
&mut self.islands,
&mut self.bf,
&mut self.nf,
&mut self.bodies,
&mut self.colliders,
&mut self.impulse_joints,
&mut self.multibody_joints,
&mut self.ccd,
&(),
&(),
);
}
}
#[test]
fn non_sleeping_neighbor_keeps_touching_row_awake() {
let mut w = World::new();
let n = 8;
let mut row = Vec::new();
for i in 0..n {
let builder = RigidBodyBuilder::dynamic()
.translation(Vector::new(i as f32, 0.5, 0.0))
.can_sleep(i != 0);
row.push(w.add_cube(builder));
}
let lone = w.add_cube(RigidBodyBuilder::dynamic().translation(Vector::new(30.0, 0.5, 0.0)));
for _ in 0..400 {
w.step();
}
for (i, handle) in row.iter().enumerate() {
assert!(
!w.bodies[*handle].is_sleeping(),
"row cube {i} must stay awake: its island contains a non-sleeping body"
);
}
assert!(
w.bodies[lone].is_sleeping(),
"the disconnected cube must sleep on its own"
);
for (i, handle) in row.iter().enumerate() {
let pos = w.bodies[*handle].translation();
assert!(
(pos.y - 0.5).abs() < 0.1 && (pos.x - i as f32).abs() < 0.1,
"row cube {i} drifted to {pos:?}"
);
}
}
#[test]
fn impact_wakes_sleeping_region() {
let mut w = World::new();
let n = 6;
let mut row = Vec::new();
for i in 0..n {
row.push(
w.add_cube(RigidBodyBuilder::dynamic().translation(Vector::new(i as f32, 0.5, 0.0))),
);
}
for _ in 0..400 {
w.step();
}
for (i, handle) in row.iter().enumerate() {
assert!(
w.bodies[*handle].is_sleeping(),
"row cube {i} should be asleep before the impact"
);
}
let bullet = w.add_cube(
RigidBodyBuilder::dynamic()
.translation(Vector::new(-3.0, 0.5, 0.0))
.linvel(Vector::new(20.0, 0.0, 0.0)),
);
for _ in 0..30 {
w.step();
}
assert!(
!w.bodies[row[0]].is_sleeping(),
"the impacted cube must be awake"
);
assert!(
w.bodies[row[0]].linvel().length() > 0.05
|| (w.bodies[row[0]].translation().x - 0.0) > 0.05,
"the impacted cube must have physically responded to the hit"
);
let _ = bullet;
}
#[test]
fn joint_keeps_both_sides_awake() {
let mut w = World::new();
let mover = w.add_cube(
RigidBodyBuilder::dynamic()
.translation(Vector::new(0.0, 0.5, 0.0))
.can_sleep(false),
);
let b = w.add_cube(RigidBodyBuilder::dynamic().translation(Vector::new(1.0, 0.5, 0.0)));
let a = w.add_cube(RigidBodyBuilder::dynamic().translation(Vector::new(2.5, 0.5, 0.0)));
let control = w.add_cube(RigidBodyBuilder::dynamic().translation(Vector::new(10.0, 0.5, 0.0)));
let joint = FixedJointBuilder::new()
.local_anchor1(Vector::new(1.5, 0.0, 0.0))
.local_anchor2(Vector::new(0.0, 0.0, 0.0));
w.impulse_joints.insert(b, a, joint, true);
for _ in 0..400 {
w.step();
}
assert!(!w.bodies[mover].is_sleeping());
assert!(
!w.bodies[b].is_sleeping(),
"halo neighbor of the non-sleeping body must stay awake"
);
assert!(
!w.bodies[a].is_sleeping(),
"a body jointed to an awake body must not sleep"
);
assert!(
w.bodies[control].is_sleeping(),
"the control cube should sleep normally"
);
assert!((w.bodies[a].translation().x - 2.5).abs() < 0.1);
assert!((w.bodies[b].translation().x - 1.0).abs() < 0.1);
}
#[test]
fn corner_velocity_sleep_metric() {
let mut w = World::new();
let beam = w.bodies.insert(
RigidBodyBuilder::dynamic()
.translation(Vector::new(0.0, 30.0, 0.0))
.angvel(Vector::new(0.0, 0.0, 0.3)) .gravity_scale(0.0),
);
w.colliders
.insert_with_parent(ColliderBuilder::cuboid(10.0, 0.1, 0.1), beam, &mut w.bodies);
let pebble = w.bodies.insert(
RigidBodyBuilder::dynamic()
.translation(Vector::new(0.0, 30.0, 20.0))
.angvel(Vector::new(0.0, 0.0, 0.55))
.gravity_scale(0.0),
);
w.colliders.insert_with_parent(
ColliderBuilder::cuboid(0.05, 0.05, 0.05),
pebble,
&mut w.bodies,
);
for _ in 0..200 {
w.step();
}
assert!(
!w.bodies[beam].is_sleeping(),
"a slowly pivoting long beam (tips at ~3 m/s) must not sleep mid-motion"
);
assert!(
w.bodies[pebble].is_sleeping(),
"a small spinner whose surface moves at ~0.05 m/s should be allowed to sleep"
);
}
#[test]
fn sliding_support_wakes_sleeping_rider() {
let mut w = World::new();
let pusher = w.bodies.insert(
RigidBodyBuilder::kinematic_velocity_based()
.translation(Vector::new(-1.55, 0.5, 0.0))
.linvel(Vector::new(0.15, 0.0, 0.0)),
);
w.colliders.insert_with_parent(
ColliderBuilder::cuboid(0.5, 0.5, 0.5),
pusher,
&mut w.bodies,
);
let support = w.add_cube(RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 0.5, 0.0)));
let rider = w.add_cube(RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 1.5, 0.0)));
let mut rider_slept = false;
let mut rider_woke_after_sleep = false;
for _ in 0..400 {
w.step();
if w.bodies[rider].is_sleeping() {
rider_slept = true;
} else if rider_slept {
rider_woke_after_sleep = true;
}
}
assert!(
rider_slept,
"the rider should fall asleep mid-slide (its velocity is below the sleep threshold)"
);
assert!(
rider_woke_after_sleep,
"the frontier drift monitor must wake the frozen rider as its support slides away"
);
let rider_pos = w.bodies[rider].translation();
let support_pos = w.bodies[support].translation();
assert!(
rider_pos.x > 0.3,
"the rider must keep being dragged along overall (x = {})",
rider_pos.x
);
assert!(
(rider_pos.y - 1.5).abs() < 0.2 && (rider_pos.x - support_pos.x).abs() < 0.75,
"the rider must still ride its support (rider {rider_pos:?}, support {support_pos:?})"
);
}
#[test]
fn slow_kinematic_wakes_sleeping_body_on_contact() {
let mut w = World::new();
let cube = w.add_cube(RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 0.5, 0.0)));
let wall = w.bodies.insert(
RigidBodyBuilder::kinematic_velocity_based()
.translation(Vector::new(-2.5, 0.5, 0.0))
.linvel(Vector::new(0.3, 0.0, 0.0)),
);
w.colliders
.insert_with_parent(ColliderBuilder::cuboid(0.5, 0.5, 0.5), wall, &mut w.bodies);
for _ in 0..120 {
w.step();
}
assert!(w.bodies[cube].is_sleeping());
for _ in 0..320 {
w.step();
}
assert!(
!w.bodies[cube].is_sleeping(),
"the slow kinematic wall must wake the sleeping cube on contact"
);
assert!(
w.bodies[cube].translation().x > 0.2,
"the cube must have been pushed, not tunneled into (x = {})",
w.bodies[cube].translation().x
);
}
#[test]
fn huge_slow_platform_wakes_small_sleeping_body() {
let mut w = World::new();
let cube = w.add_cube(RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 0.5, 0.0)));
let platform = w.bodies.insert(
RigidBodyBuilder::kinematic_velocity_based()
.translation(Vector::new(-11.0, 0.5, 0.0))
.linvel(Vector::new(0.3, 0.0, 0.0)),
);
w.colliders.insert_with_parent(
ColliderBuilder::cuboid(10.0, 0.4, 10.0),
platform,
&mut w.bodies,
);
for _ in 0..90 {
w.step();
}
assert!(w.bodies[cube].is_sleeping());
for _ in 0..60 {
w.step();
}
assert!(
!w.bodies[cube].is_sleeping(),
"the huge slow platform must wake the small sleeping cube shortly after contact"
);
assert!(
w.bodies[cube].translation().x > 0.02,
"the cube must have been pushed by the platform (x = {})",
w.bodies[cube].translation().x
);
}
#[test]
fn slow_dynamic_intruder_wakes_sleeping_body() {
let mut w = World::new();
let n = 10;
let mut dominoes = Vec::new();
for i in 0..n {
let rot = if i == 0 {
Rotation::from_rotation_z(-0.2)
} else {
Rotation::IDENTITY
};
let handle = w.bodies.insert(
RigidBodyBuilder::dynamic()
.pose(Pose::from_parts(Vector::new(i as f32 * 0.4, 2.0, 0.0), rot)),
);
w.colliders.insert_with_parent(
ColliderBuilder::cuboid(0.1, 2.0, 1.0),
handle,
&mut w.bodies,
);
dominoes.push(handle);
}
let tilt = |bodies: &RigidBodySet, h: RigidBodyHandle| {
let up = bodies[h].rotation() * Vector::Y;
up.y.clamp(-1.0, 1.0).acos()
};
for _ in 0..900 {
w.step();
}
for (i, handle) in dominoes.iter().enumerate() {
let t = tilt(&w.bodies, *handle);
assert!(
t > 0.5,
"domino {i} did not fall (tilt = {t}): the chain stalled against a sleeping domino"
);
}
for _ in 0..600 {
w.step();
}
for (i, handle) in dominoes.iter().enumerate() {
assert!(
w.bodies[*handle].is_sleeping(),
"fallen domino {i} never went back to sleep"
);
}
}
#[test]
fn grazing_wedge_wakes_sleeping_chain() {
let mut w = World::new();
let mut curr_angle = 0.0f32;
let mut curr_rad = 10.0f32;
let mut dominoes = Vec::new();
for i in 0..187 {
let perimeter = 2.0 * std::f32::consts::PI * curr_rad;
let spacing = 0.4;
let prev_angle = curr_angle;
curr_angle += 2.0 * std::f32::consts::PI * spacing / perimeter;
let (x, z) = curr_angle.sin_cos();
let two_pi = 2.0 * std::f32::consts::PI;
let nudged = curr_angle % two_pi < prev_angle % two_pi;
let tilt = if nudged { 0.2 } else { 0.0 };
if i >= 150 {
let rot = Rotation::from_rotation_y(curr_angle);
let tilt_axis = rot * Vector::Z;
let tilt_rot = Rotation::from_axis_angle(tilt_axis, tilt);
let handle = w
.bodies
.insert(RigidBodyBuilder::dynamic().pose(Pose::from_parts(
Vector::new(x * curr_rad, 2.1, z * curr_rad),
tilt_rot * rot,
)));
w.colliders.insert_with_parent(
ColliderBuilder::cuboid(0.1, 2.0, 1.0),
handle,
&mut w.bodies,
);
dominoes.push(handle);
}
curr_rad += 1.5 / perimeter;
}
for _ in 0..1800 {
w.step();
}
for (i, handle) in dominoes.iter().enumerate() {
let up = w.bodies[*handle].rotation() * Vector::Y;
let tilt = up.y.clamp(-1.0, 1.0).acos();
assert!(
tilt > 0.5,
"segment domino {i} did not fall (tilt = {tilt}): the wedge froze against a sleeping domino"
);
}
}
#[test]
fn sub_gate_impact_wakes_and_transfers_momentum() {
let mut w = World::new();
let ground_friction0 = w
.bodies
.insert(RigidBodyBuilder::fixed().translation(Vector::new(0.0, -0.5, 70.0)));
w.colliders.insert_with_parent(
ColliderBuilder::cuboid(20.0, 0.5, 10.0).friction(0.0),
ground_friction0,
&mut w.bodies,
);
let target = w
.bodies
.insert(RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 0.5, 70.0)));
w.colliders.insert_with_parent(
ColliderBuilder::cuboid(0.5, 0.5, 0.5).friction(0.0),
target,
&mut w.bodies,
);
for _ in 0..120 {
w.step();
}
assert!(w.bodies[target].is_sleeping());
let mover = w.bodies.insert(
RigidBodyBuilder::dynamic()
.translation(Vector::new(-3.0, 0.5, 70.0))
.linvel(Vector::new(0.6, 0.0, 0.0)),
);
w.colliders.insert_with_parent(
ColliderBuilder::cuboid(0.5, 0.5, 0.5).friction(0.0),
mover,
&mut w.bodies,
);
for _ in 0..240 {
w.step();
}
let target_v = w.bodies[target].linvel().x;
assert!(
!w.bodies[target].is_sleeping(),
"the sub-gate impact never woke the sleeping target"
);
assert!(
target_v > 0.25,
"the impact was partially absorbed by the sleeping target \
(target vx = {target_v}, expected ~0.3 as in the awake-vs-awake case)"
);
}
#[test]
fn floating_region_does_not_sleep_partially() {
let mut w = World::new();
let mover = w.add_cube(
RigidBodyBuilder::dynamic()
.translation(Vector::new(0.0, 0.5, 0.0))
.can_sleep(false),
);
let b = w.add_cube(RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 1.5, 0.0)));
let c = w.add_cube(RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 2.5, 0.0)));
let d = w.add_cube(RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 3.5, 0.0)));
let control: Vec<_> = (0..3)
.map(|i| {
w.add_cube(RigidBodyBuilder::dynamic().translation(Vector::new(
10.0,
0.5 + i as f32,
0.0,
)))
})
.collect();
for _ in 0..600 {
w.step();
}
assert!(!w.bodies[mover].is_sleeping());
for (name, handle) in [("b", b), ("c", c), ("d", d)] {
assert!(
!w.bodies[handle].is_sleeping(),
"cube {name} floats above a live support and must not sleep"
);
}
for (i, handle) in control.iter().enumerate() {
assert!(
w.bodies[*handle].is_sleeping(),
"grounded control cube {i} should be asleep"
);
}
for (i, handle) in [mover, b, c, d].iter().enumerate() {
let pos = w.bodies[*handle].translation();
let expected_y = 0.5 + i as f32;
assert!(
(pos.y - expected_y).abs() < 0.1 && pos.x.abs() < 0.1,
"stacked cube {i} drifted to {pos:?}"
);
}
}
#[test]
fn whole_floating_island_sleeps() {
let mut w = World::new();
let a = w.bodies.insert(
RigidBodyBuilder::dynamic()
.translation(Vector::new(0.0, 30.0, 0.0))
.gravity_scale(0.0),
);
w.colliders
.insert_with_parent(ColliderBuilder::cuboid(0.5, 0.5, 0.5), a, &mut w.bodies);
let b = w.bodies.insert(
RigidBodyBuilder::dynamic()
.translation(Vector::new(0.999, 30.0, 0.0))
.gravity_scale(0.0),
);
w.colliders
.insert_with_parent(ColliderBuilder::cuboid(0.5, 0.5, 0.5), b, &mut w.bodies);
for _ in 0..400 {
w.step();
}
assert!(
w.bodies[a].is_sleeping() && w.bodies[b].is_sleeping(),
"a fully floating island must still sleep as a whole"
);
}
#[test]
fn slow_drift_does_not_sleep_mid_motion() {
let mut w = World::new();
let drifter = w.bodies.insert(
RigidBodyBuilder::dynamic()
.translation(Vector::new(0.0, 30.0, 0.0))
.linvel(Vector::new(0.3, 0.0, 0.0))
.gravity_scale(0.0),
);
w.colliders.insert_with_parent(
ColliderBuilder::cuboid(0.5, 0.5, 0.5),
drifter,
&mut w.bodies,
);
let creeper = w.bodies.insert(
RigidBodyBuilder::dynamic()
.translation(Vector::new(0.0, 30.0, 20.0))
.linvel(Vector::new(0.03, 0.0, 0.0))
.gravity_scale(0.0),
);
w.colliders.insert_with_parent(
ColliderBuilder::cuboid(0.5, 0.5, 0.5),
creeper,
&mut w.bodies,
);
for _ in 0..400 {
w.step();
}
assert!(
!w.bodies[drifter].is_sleeping(),
"a body moving at 0.3 length-units/s must not sleep mid-motion"
);
assert!(
w.bodies[creeper].is_sleeping(),
"a body creeping below the sleep threshold may sleep"
);
}