use rapier3d::prelude::*;
struct Harness {
bodies: RigidBodySet,
colliders: ColliderSet,
impulse_joints: ImpulseJointSet,
multibody_joints: MultibodyJointSet,
pipeline: PhysicsPipeline,
bf: BroadPhaseBvh,
nf: NarrowPhase,
islands: IslandManager,
ccd: CCDSolver,
params: IntegrationParameters,
gravity: Vector,
}
impl Harness {
fn new() -> Self {
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(),
gravity: Vector::ZERO,
}
}
fn step(&mut self) {
self.pipeline.step(
self.gravity,
&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,
&(),
&(),
);
}
fn run(&mut self, steps: usize) {
for _ in 0..steps {
self.step();
}
}
fn x(&self, h: RigidBodyHandle) -> Real {
self.bodies[h].translation().x
}
}
fn insert_thin_fixed_wall(h: &mut Harness) {
let wall = h.bodies.insert(RigidBodyBuilder::fixed());
h.colliders
.insert_with_parent(ColliderBuilder::cuboid(0.05, 5.0, 5.0), wall, &mut h.bodies);
}
fn insert_fast_dynamic(h: &mut Harness, ccd_enabled: bool) -> RigidBodyHandle {
let body = h.bodies.insert(
RigidBodyBuilder::dynamic()
.translation(Vector::new(-3.0, 0.0, 0.0))
.linvel(Vector::new(200.0, 0.0, 0.0))
.ccd_enabled(ccd_enabled),
);
h.colliders
.insert_with_parent(ColliderBuilder::cuboid(0.1, 0.1, 0.1), body, &mut h.bodies);
body
}
#[test]
fn default_ccd_vs_fixed_no_tunnel() {
let mut h = Harness::new();
insert_thin_fixed_wall(&mut h);
let body = insert_fast_dynamic(&mut h, false);
h.run(120);
assert!(
h.x(body) < 0.0,
"body tunneled through the fixed wall (x = {})",
h.x(body)
);
}
#[test]
fn default_tier_ignores_dynamic() {
let mut h = Harness::new();
let a = h.bodies.insert(
RigidBodyBuilder::dynamic()
.translation(Vector::new(-3.0, 0.0, 0.0))
.linvel(Vector::new(200.0, 0.0, 0.0)),
);
h.colliders
.insert_with_parent(ColliderBuilder::cuboid(0.1, 0.1, 0.1), a, &mut h.bodies);
let b = h.bodies.insert(
RigidBodyBuilder::dynamic()
.translation(Vector::new(3.0, 0.0, 0.0))
.linvel(Vector::new(-200.0, 0.0, 0.0)),
);
h.colliders
.insert_with_parent(ColliderBuilder::cuboid(0.1, 0.1, 0.1), b, &mut h.bodies);
h.run(5);
assert!(
h.x(a) > 0.0 && h.x(b) < 0.0,
"default-tier dynamic bodies should tunnel through each other (a.x = {}, b.x = {})",
h.x(a),
h.x(b)
);
}
#[test]
fn bullet_still_hits_dynamic() {
let mut h = Harness::new();
let bullet = insert_fast_dynamic(&mut h, true);
let target = h
.bodies
.insert(RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 0.0, 0.0)));
h.colliders.insert_with_parent(
ColliderBuilder::cuboid(0.2, 0.2, 0.2),
target,
&mut h.bodies,
);
h.run(60);
assert!(
h.x(target) > 0.05,
"bullet did not hit the dynamic target (target.x = {})",
h.x(target)
);
assert!(
h.x(bullet) < h.x(target),
"bullet passed through its target (bullet.x = {}, target.x = {})",
h.x(bullet),
h.x(target)
);
}
#[test]
fn global_ccd_off_tunnels() {
let mut h = Harness::new();
h.params.max_ccd_substeps = 0;
insert_thin_fixed_wall(&mut h);
let body = insert_fast_dynamic(&mut h, false);
h.run(60);
assert!(
h.x(body) > 1.0,
"body should tunnel through the wall when CCD is globally disabled (x = {})",
h.x(body)
);
}
#[test]
fn trimesh_fast_body_is_never_swept() {
use rapier3d::parry::shape::Ball;
let mut h = Harness::new();
insert_thin_fixed_wall(&mut h);
let (vtx, idx) = Ball::new(0.1).to_trimesh(8, 8);
let shape = SharedShape::new(TriMesh::new(vtx, idx).unwrap());
let body = h.bodies.insert(
RigidBodyBuilder::dynamic()
.translation(Vector::new(-3.0, 0.0, 0.0))
.linvel(Vector::new(200.0, 0.0, 0.0)),
);
h.colliders
.insert_with_parent(ColliderBuilder::new(shape), body, &mut h.bodies);
h.step();
assert!(
!h.bodies[body].is_ccd_active(),
"a trimesh-only body must never be flagged ccd_active"
);
h.run(59);
assert!(
h.x(body) > 1.0,
"a fast trimesh body is not swept and should tunnel through the thin wall (x = {})",
h.x(body)
);
}
#[test]
fn compound_fast_body_no_tunnel() {
let mut h = Harness::new();
insert_thin_fixed_wall(&mut h);
let shape = SharedShape::compound(vec![
(
Pose::from_translation(Vector::new(0.0, 0.15, 0.0)),
SharedShape::cuboid(0.1, 0.1, 0.1),
),
(
Pose::from_translation(Vector::new(0.0, -0.15, 0.0)),
SharedShape::cuboid(0.1, 0.1, 0.1),
),
]);
let body = h.bodies.insert(
RigidBodyBuilder::dynamic()
.translation(Vector::new(-3.0, 0.0, 0.0))
.linvel(Vector::new(200.0, 0.0, 0.0)),
);
h.colliders
.insert_with_parent(ColliderBuilder::new(shape), body, &mut h.bodies);
h.run(120);
assert!(
h.x(body) < 0.0,
"compound body tunneled through the fixed wall (x = {})",
h.x(body)
);
}
#[test]
fn compound_fast_body_vs_trimesh_wall_no_tunnel() {
let mut h = Harness::new();
let vtx = vec![
Vector::new(0.0, -5.0, -5.0),
Vector::new(0.0, 5.0, -5.0),
Vector::new(0.0, 5.0, 5.0),
Vector::new(0.0, -5.0, 5.0),
];
let idx = vec![[0, 1, 2], [0, 2, 3]];
let wall = SharedShape::new(TriMesh::new(vtx, idx).unwrap());
h.colliders.insert(ColliderBuilder::new(wall));
let shape = SharedShape::compound(vec![(Pose::IDENTITY, SharedShape::cuboid(0.1, 0.1, 0.1))]);
let body = h.bodies.insert(
RigidBodyBuilder::dynamic()
.translation(Vector::new(-3.0, 0.0, 0.0))
.linvel(Vector::new(200.0, 0.0, 0.0)),
);
h.colliders
.insert_with_parent(ColliderBuilder::new(shape), body, &mut h.bodies);
h.run(120);
assert!(
h.x(body) < 0.0,
"compound body tunneled through the trimesh wall (x = {})",
h.x(body)
);
}
#[test]
#[ignore = "benchmark"]
fn bench_dynamic_trimeshes_on_trimesh_ground() {
use rapier3d::parry::shape::Ball;
let mut h = Harness::new();
h.gravity = Vector::new(0.0, -9.81, 0.0);
let nsubdivs = 100;
let heights = Array2::from_fn(nsubdivs + 1, nsubdivs + 1, |i, j| {
-(i as Real * 40.0 / (nsubdivs as Real) / 2.0).cos()
- (j as Real * 40.0 / (nsubdivs as Real) / 2.0).cos()
});
let heightfield = HeightField::new(heights, Vector::new(100.0, 2.0, 100.0));
let mut ground = TriMesh::from(heightfield);
let _ = ground.set_flags(TriMeshFlags::FIX_INTERNAL_EDGES);
h.colliders
.insert(ColliderBuilder::new(SharedShape::new(ground)));
let (vtx, idx) = Ball::new(1.5).to_trimesh(10, 10);
let shape = SharedShape::new(TriMesh::new(vtx, idx).unwrap());
for k in 0..60 {
let (i, j) = (k % 8, k / 8);
let body = h
.bodies
.insert(RigidBodyBuilder::dynamic().translation(Vector::new(
i as Real * 5.0 - 17.5,
6.0 + (k % 3) as Real * 4.0,
j as Real * 5.0 - 17.5,
)));
h.colliders
.insert_with_parent(ColliderBuilder::new(shape.clone()), body, &mut h.bodies);
}
let t0 = std::time::Instant::now();
let steps = 300;
h.run(steps);
let elapsed = t0.elapsed().as_secs_f64() * 1000.0;
println!(
"{steps} steps: {elapsed:.1} ms ({:.3} ms/step)",
elapsed / steps as f64
);
}
#[test]
fn kinematic_not_a_default_target() {
let mut h = Harness::new();
let wall = h
.bodies
.insert(RigidBodyBuilder::kinematic_position_based());
h.colliders
.insert_with_parent(ColliderBuilder::cuboid(0.05, 5.0, 5.0), wall, &mut h.bodies);
let body = insert_fast_dynamic(&mut h, false);
h.run(60);
assert!(
h.x(body) > 1.0,
"default-tier body should tunnel through a kinematic wall (x = {})",
h.x(body)
);
}