Skip to content

Commit 936b228

Browse files
authored
feat: testbed rework to avoid the callback patterns (#956)
1 parent 7149ef3 commit 936b228

185 files changed

Lines changed: 4619 additions & 4863 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/rapier-ci-build.yml

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -117,21 +117,6 @@ jobs:
117117
run: cd crates/rapier2d && cargo build --verbose --target wasm32-unknown-unknown;
118118
- name: build rapier3d
119119
run: cd crates/rapier3d && cargo build --verbose --target wasm32-unknown-unknown;
120-
build-wasm-emscripten:
121-
runs-on: ubuntu-latest
122-
env:
123-
RUSTFLAGS: -D warnings
124-
steps:
125-
- uses: actions/checkout@v4
126-
- run: rustup target add wasm32-unknown-emscripten
127-
- name: build rapier2d
128-
run: cd crates/rapier2d && cargo build --verbose --target wasm32-unknown-emscripten;
129-
- name: build rapier3d
130-
run: cd crates/rapier3d && cargo build --verbose --target wasm32-unknown-emscripten;
131-
- name: build rapier2d --features simd-stable
132-
run: cd crates/rapier2d && cargo build --verbose --target wasm32-unknown-emscripten --features simd-stable;
133-
- name: build rapier3d --features simd-stable
134-
run: cd crates/rapier3d && cargo build --verbose --target wasm32-unknown-emscripten --features simd-stable;
135120
# If this fails, consider changing your text or adding something to .typos.toml
136121
# You can find typos here: https://crates.io/crates/typos'
137122
typos:

Cargo.toml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,10 @@ num-traits = { version = "0.2", default-features = false }
7373
approx = { version = "0.5", default-features = false }
7474

7575
# Parry (each crate picks its own variant)
76-
parry2d = { version = "0.28", default-features = false, features = ["required-features"] }
77-
parry3d = { version = "0.28", default-features = false, features = ["required-features"] }
78-
parry2d-f64 = { version = "0.28", default-features = false, features = ["required-features"] }
79-
parry3d-f64 = { version = "0.28", default-features = false, features = ["required-features"] }
76+
parry2d = { version = "0.29", default-features = false, features = ["required-features"] }
77+
parry3d = { version = "0.29", default-features = false, features = ["required-features"] }
78+
parry2d-f64 = { version = "0.29", default-features = false, features = ["required-features"] }
79+
parry3d-f64 = { version = "0.29", default-features = false, features = ["required-features"] }
8080

8181
# Utilities
8282
bitflags = "2"
@@ -98,7 +98,6 @@ serde_json = "1"
9898
oorandom = { version = "11", default-features = false }
9999

100100
[patch.crates-io]
101-
#wrapped2d = { git = "https://github.com/Bastacyclop/rust_box2d.git" }
102101
#xurdf = { path = "../xurdf/xurdf" }
103102

104103
#simba = { path = "../simba" }

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<p align="center">
2-
<img src="https://www.rapier.rs/img/rapier_logo_color_textpath_dark.svg" alt="crates.io">
2+
<img src="./assets/rapier-logo.jpg" alt="crates.io">
33
</p>
44
<p align="center">
55
<a href="https://discord.gg/vt9DJSW">

assets/rapier-logo-vertical.png

16.7 MB
Loading

assets/rapier-logo.jpg

2.58 MB
Loading

assets/rapier-logo.png

23.4 MB
Loading

crates/rapier3d-mjcf/src/loader/conversion.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ use std::path::Path;
99

1010
use mjcf_rs::Pose as MPose;
1111
use mjcf_rs::body as mb;
12-
use mjcf_rs::equality::{Equality as MjcfEquality, EqualityConnect, EqualityWeld};
1312
use mjcf_rs::equality::EqualityJoint as MjcfEqualityJointDef;
13+
use mjcf_rs::equality::{Equality as MjcfEquality, EqualityConnect, EqualityWeld};
1414
use mjcf_rs::extras::Sensor as MjcfSensor;
1515
use mjcf_rs::glam::DVec3;
1616
use mjcf_rs::model::{BodyEntry, BodyId, Model};
@@ -663,8 +663,8 @@ impl<'a> Conversion<'a> {
663663
fn resolve_tendon_joints(&self, t: &mjcf_rs::tendon::FixedTendon) -> Vec<(usize, Real)> {
664664
t.joints
665665
.iter()
666-
.filter_map(|term| {
667-
match self.robot.joint_name_to_idx.get(&term.joint).copied() {
666+
.filter_map(
667+
|term| match self.robot.joint_name_to_idx.get(&term.joint).copied() {
668668
Some(idx) => Some((idx, term.coef as Real)),
669669
None => {
670670
log::warn!(
@@ -674,8 +674,8 @@ impl<'a> Conversion<'a> {
674674
);
675675
None
676676
}
677-
}
678-
})
677+
},
678+
)
679679
.collect()
680680
}
681681

@@ -712,8 +712,7 @@ impl<'a> Conversion<'a> {
712712
}
713713
for &(idxk, ck) in &joints[1..] {
714714
let already_coupled = self.robot.joint_couplings.iter().any(|c| {
715-
(c.joint1 == idx0 && c.joint2 == idxk)
716-
|| (c.joint1 == idxk && c.joint2 == idx0)
715+
(c.joint1 == idx0 && c.joint2 == idxk) || (c.joint1 == idxk && c.joint2 == idx0)
717716
});
718717
if already_coupled {
719718
continue;

crates/rapier3d-mjcf/src/loader/mass.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -327,12 +327,12 @@ pub(super) fn add_springdamper_to_multibody(
327327
let mut offset = 0;
328328
let mut ndofs = 0;
329329
for (i, link) in multibody.links().enumerate() {
330-
let nd = link.joint().ndofs();
330+
let link_ndofs = link.joint().ndofs();
331331
if i == link_id {
332-
ndofs = nd;
332+
ndofs = link_ndofs;
333333
break;
334334
}
335-
offset += nd;
335+
offset += link_ndofs;
336336
}
337337
if ndofs == 0 {
338338
return;

crates/rapier3d-mjcf/src/loader/runtime.rs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,8 @@ impl MjcfRobotHandles<Option<MultibodyJointHandle>> {
264264
rb.set_position(free_pose(qp, dof.scale, robot.base_shift), true);
265265
}
266266
if let Some(qv) = qvel {
267-
let lin = Vector::new(qv[0] as Real, qv[1] as Real, qv[2] as Real) * dof.scale;
267+
let lin =
268+
Vector::new(qv[0] as Real, qv[1] as Real, qv[2] as Real) * dof.scale;
268269
let ang = Vector::new(qv[3] as Real, qv[4] as Real, qv[5] as Real);
269270
rb.set_linvel(lin, true);
270271
rb.set_angvel(ang, true);
@@ -279,7 +280,9 @@ impl MjcfRobotHandles<Option<MultibodyJointHandle>> {
279280
// Articulated DoF (hinge / slide / ball): write to the multibody's
280281
// generalized coordinates.
281282
let Some(jidx) = dof.joint else { continue };
282-
let Some(jh) = self.joints.get(jidx) else { continue };
283+
let Some(jh) = self.joints.get(jidx) else {
284+
continue;
285+
};
283286
// `joint` is `None` for a joint that was dropped as a loop closure.
284287
let Some(handle) = jh.joint else { continue };
285288
if let Some(link) = multibody_joints.rigid_body_link(jh.link2) {
@@ -292,7 +295,9 @@ impl MjcfRobotHandles<Option<MultibodyJointHandle>> {
292295
// Compute the displacement (target − current) under an immutable
293296
// borrow, then apply it under a mutable one.
294297
let (assembly_id, ndofs, disp) = {
295-
let Some(link) = mb.link(link_id) else { continue };
298+
let Some(link) = mb.link(link_id) else {
299+
continue;
300+
};
296301
let joint = link.joint();
297302
let coords = joint.coords();
298303
let disp = qpos.map(|qp| match dof.kind {
@@ -375,7 +380,8 @@ impl MjcfRobotHandles<ImpulseJointHandle> {
375380
world[dof.body] = free_pose(qp, dof.scale, robot.base_shift);
376381
}
377382
if let Some(qv) = qvel {
378-
let lin = Vector::new(qv[0] as Real, qv[1] as Real, qv[2] as Real) * dof.scale;
383+
let lin =
384+
Vector::new(qv[0] as Real, qv[1] as Real, qv[2] as Real) * dof.scale;
379385
let ang = Vector::new(qv[3] as Real, qv[4] as Real, qv[5] as Real);
380386
free_vels.push((dof.body, lin, ang));
381387
}
@@ -402,8 +408,10 @@ impl MjcfRobotHandles<ImpulseJointHandle> {
402408
// joint's parent body is already positioned. This mirrors rapier's
403409
// multibody `body_to_parent`: world₂ = world₁ · frame1 · q · frame2⁻¹.
404410
for (jidx, j) in robot.joints.iter().enumerate() {
405-
world[j.link2] =
406-
world[j.link1] * j.joint.local_frame1 * joint_tf[jidx] * j.joint.local_frame2.inverse();
411+
world[j.link2] = world[j.link1]
412+
* j.joint.local_frame1
413+
* joint_tf[jidx]
414+
* j.joint.local_frame2.inverse();
407415
}
408416

409417
for (i, handle) in self.bodies.iter().enumerate() {
@@ -445,7 +453,7 @@ impl MjcfRobotHandles<ImpulseJointHandle> {
445453

446454
/// Like [`apply_controls`](Self::apply_controls) but uniformly scales every
447455
/// actuator's strength by `gain_scale` (gains and force limits; see
448-
/// [`configure_actuator_motor`]). `gain_scale < 1` softens the actuation —
456+
/// `configure_actuator_motor`). `gain_scale < 1` softens the actuation —
449457
/// e.g. to ease servo-driven moves that would otherwise saturate and snap.
450458
pub fn apply_controls_scaled(
451459
&self,
@@ -471,7 +479,7 @@ impl MjcfRobotHandles<Option<MultibodyJointHandle>> {
471479
/// [`apply_controls`](MjcfRobotHandles::<ImpulseJointHandle>::apply_controls):
472480
/// it reproduces MuJoCo's "actuation" by writing each actuator's motor
473481
/// onto the multibody link it drives. The per-actuator interpretation is
474-
/// identical (see [`configure_actuator_motor`]); call it once per frame
482+
/// identical (see `configure_actuator_motor`); call it once per frame
475483
/// before `pipeline.step`.
476484
///
477485
/// `ctrl` is a flat array of one scalar per actuator, in the order of
@@ -493,7 +501,7 @@ impl MjcfRobotHandles<Option<MultibodyJointHandle>> {
493501

494502
/// Like [`apply_controls_multibody`](Self::apply_controls_multibody) but
495503
/// uniformly scales every actuator's strength by `gain_scale` (gains and
496-
/// force limits; see [`configure_actuator_motor`]). `gain_scale < 1` softens
504+
/// force limits; see `configure_actuator_motor`). `gain_scale < 1` softens
497505
/// the actuation, so a servo-driven move (e.g. easing between keyframes)
498506
/// ramps in instead of saturating to its force limit and arriving instantly.
499507
pub fn apply_controls_multibody_scaled(
@@ -507,7 +515,9 @@ impl MjcfRobotHandles<Option<MultibodyJointHandle>> {
507515
// `H` is `Option<MultibodyJointHandle>` here, so `ah.joint` is
508516
// `Option<Option<_>>`: the outer `None` means "no actuator joint",
509517
// the inner `None` means "joint dropped as a loop closure".
510-
let Some(Some(handle)) = ah.joint else { continue };
518+
let Some(Some(handle)) = ah.joint else {
519+
continue;
520+
};
511521
let Some((mb, link_id)) = multibody_joints.get_mut(handle) else {
512522
continue;
513523
};
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//! `apply_controls_multibody_scaled`'s `gain_scale` softens actuation: a lower
2+
//! scale makes a position servo approach its target more slowly (no snap).
3+
4+
use rapier3d::prelude::*;
5+
use rapier3d_mjcf::{MjcfLoaderOptions, MjcfMultibodyOptions, MjcfRobot};
6+
7+
// A strong position servo on one hinge — at full strength it slams to target.
8+
const SERVO: &str = r#"
9+
<mujoco>
10+
<worldbody>
11+
<body name="link" pos="0 0 1">
12+
<joint name="j" type="hinge" axis="0 1 0"/>
13+
<inertial mass="5" diaginertia="2 2 2"/>
14+
<geom type="capsule" fromto="0 0 0 0.5 0 0" size="0.03"/>
15+
</body>
16+
</worldbody>
17+
<actuator>
18+
<position name="a" joint="j" kp="2000" kv="200" forcerange="-20 20"/>
19+
</actuator>
20+
</mujoco>
21+
"#;
22+
23+
fn angle(rb: &RigidBody) -> f32 {
24+
let x = rb.rotation() * Vector::X;
25+
(-x.z).atan2(x.x)
26+
}
27+
28+
/// Drive `j` toward 1.0 for `steps` steps at the given strength scale; return
29+
/// the angle reached.
30+
fn drive_to(steps: usize, gain_scale: Real) -> f32 {
31+
let (robot, _) = MjcfRobot::from_str(SERVO, MjcfLoaderOptions::default(), ".").unwrap();
32+
let mut bodies = RigidBodySet::new();
33+
let mut colliders = ColliderSet::new();
34+
let mut ij = ImpulseJointSet::new();
35+
let mut mbj = MultibodyJointSet::new();
36+
let handles = robot.clone().insert_using_multibody_joints(
37+
&mut bodies,
38+
&mut colliders,
39+
&mut mbj,
40+
&mut ij,
41+
MjcfMultibodyOptions::default(),
42+
);
43+
let link = robot.body_name_to_idx["link"];
44+
let mut pipeline = PhysicsPipeline::new();
45+
let ip = IntegrationParameters::default();
46+
let (mut isl, mut bp, mut np, mut ccd) = (
47+
IslandManager::new(),
48+
DefaultBroadPhase::new(),
49+
NarrowPhase::new(),
50+
CCDSolver::new(),
51+
);
52+
for _ in 0..steps {
53+
handles.apply_controls_multibody_scaled(&mut bodies, &mut mbj, &[1.0_f32], gain_scale);
54+
pipeline.step(
55+
Vector::new(0.0, 0.0, 0.0), // no gravity: isolate the servo dynamics
56+
&ip,
57+
&mut isl,
58+
&mut bp,
59+
&mut np,
60+
&mut bodies,
61+
&mut colliders,
62+
&mut ij,
63+
&mut mbj,
64+
&mut ccd,
65+
&(),
66+
&(),
67+
);
68+
}
69+
angle(
70+
bodies
71+
.get(handles.bodies[link].as_ref().unwrap().body)
72+
.unwrap(),
73+
)
74+
}
75+
76+
#[test]
77+
fn lower_gain_scale_approaches_target_more_slowly() {
78+
let steps = 15;
79+
let full = drive_to(steps, 1.0);
80+
let soft = drive_to(steps, 0.1);
81+
println!("full={full} soft={soft}");
82+
83+
// Force-limited, so the approach rate scales with strength: at 0.1 the
84+
// servo moves roughly an order of magnitude less far in the same time.
85+
assert!(
86+
full > 0.2,
87+
"full-strength servo should be well underway: {full}"
88+
);
89+
assert!(
90+
soft < full * 0.3,
91+
"0.1x servo should lag well behind: soft={soft} full={full}"
92+
);
93+
assert!(
94+
soft > 0.0,
95+
"0.1x servo should still be moving toward target: {soft}"
96+
);
97+
}
98+
99+
#[test]
100+
fn zero_ish_gain_does_not_nan() {
101+
// The minimum the example slider allows is 0.02; make sure a tiny scale (and
102+
// the INFINITY force cap on a forcerange-less actuator) stays finite.
103+
let a = drive_to(20, 0.02);
104+
assert!(a.is_finite());
105+
}

0 commit comments

Comments
 (0)