diff --git a/.github/workflows/rapier-ci-build.yml b/.github/workflows/rapier-ci-build.yml index 6b793fc52..9115cbb48 100644 --- a/.github/workflows/rapier-ci-build.yml +++ b/.github/workflows/rapier-ci-build.yml @@ -117,21 +117,6 @@ jobs: run: cd crates/rapier2d && cargo build --verbose --target wasm32-unknown-unknown; - name: build rapier3d run: cd crates/rapier3d && cargo build --verbose --target wasm32-unknown-unknown; - build-wasm-emscripten: - runs-on: ubuntu-latest - env: - RUSTFLAGS: -D warnings - steps: - - uses: actions/checkout@v4 - - run: rustup target add wasm32-unknown-emscripten - - name: build rapier2d - run: cd crates/rapier2d && cargo build --verbose --target wasm32-unknown-emscripten; - - name: build rapier3d - run: cd crates/rapier3d && cargo build --verbose --target wasm32-unknown-emscripten; - - name: build rapier2d --features simd-stable - run: cd crates/rapier2d && cargo build --verbose --target wasm32-unknown-emscripten --features simd-stable; - - name: build rapier3d --features simd-stable - run: cd crates/rapier3d && cargo build --verbose --target wasm32-unknown-emscripten --features simd-stable; # If this fails, consider changing your text or adding something to .typos.toml # You can find typos here: https://crates.io/crates/typos' typos: diff --git a/Cargo.toml b/Cargo.toml index 6fe8ad6f8..7a98057dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,10 +73,10 @@ num-traits = { version = "0.2", default-features = false } approx = { version = "0.5", default-features = false } # Parry (each crate picks its own variant) -parry2d = { version = "0.28", default-features = false, features = ["required-features"] } -parry3d = { version = "0.28", default-features = false, features = ["required-features"] } -parry2d-f64 = { version = "0.28", default-features = false, features = ["required-features"] } -parry3d-f64 = { version = "0.28", default-features = false, features = ["required-features"] } +parry2d = { version = "0.29", default-features = false, features = ["required-features"] } +parry3d = { version = "0.29", default-features = false, features = ["required-features"] } +parry2d-f64 = { version = "0.29", default-features = false, features = ["required-features"] } +parry3d-f64 = { version = "0.29", default-features = false, features = ["required-features"] } # Utilities bitflags = "2" @@ -98,7 +98,6 @@ serde_json = "1" oorandom = { version = "11", default-features = false } [patch.crates-io] -#wrapped2d = { git = "https://github.com/Bastacyclop/rust_box2d.git" } #xurdf = { path = "../xurdf/xurdf" } #simba = { path = "../simba" } diff --git a/README.md b/README.md index 527273df1..fb4d1184e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- crates.io + crates.io

diff --git a/assets/rapier-logo-vertical.png b/assets/rapier-logo-vertical.png new file mode 100644 index 000000000..311572420 Binary files /dev/null and b/assets/rapier-logo-vertical.png differ diff --git a/assets/rapier-logo.jpg b/assets/rapier-logo.jpg new file mode 100644 index 000000000..1a15f1897 Binary files /dev/null and b/assets/rapier-logo.jpg differ diff --git a/assets/rapier-logo.png b/assets/rapier-logo.png new file mode 100644 index 000000000..b884b0806 Binary files /dev/null and b/assets/rapier-logo.png differ diff --git a/crates/rapier3d-mjcf/src/loader/conversion.rs b/crates/rapier3d-mjcf/src/loader/conversion.rs index c84b54dd4..378ea1c88 100644 --- a/crates/rapier3d-mjcf/src/loader/conversion.rs +++ b/crates/rapier3d-mjcf/src/loader/conversion.rs @@ -9,8 +9,8 @@ use std::path::Path; use mjcf_rs::Pose as MPose; use mjcf_rs::body as mb; -use mjcf_rs::equality::{Equality as MjcfEquality, EqualityConnect, EqualityWeld}; use mjcf_rs::equality::EqualityJoint as MjcfEqualityJointDef; +use mjcf_rs::equality::{Equality as MjcfEquality, EqualityConnect, EqualityWeld}; use mjcf_rs::extras::Sensor as MjcfSensor; use mjcf_rs::glam::DVec3; use mjcf_rs::model::{BodyEntry, BodyId, Model}; @@ -663,8 +663,8 @@ impl<'a> Conversion<'a> { fn resolve_tendon_joints(&self, t: &mjcf_rs::tendon::FixedTendon) -> Vec<(usize, Real)> { t.joints .iter() - .filter_map(|term| { - match self.robot.joint_name_to_idx.get(&term.joint).copied() { + .filter_map( + |term| match self.robot.joint_name_to_idx.get(&term.joint).copied() { Some(idx) => Some((idx, term.coef as Real)), None => { log::warn!( @@ -674,8 +674,8 @@ impl<'a> Conversion<'a> { ); None } - } - }) + }, + ) .collect() } @@ -712,8 +712,7 @@ impl<'a> Conversion<'a> { } for &(idxk, ck) in &joints[1..] { let already_coupled = self.robot.joint_couplings.iter().any(|c| { - (c.joint1 == idx0 && c.joint2 == idxk) - || (c.joint1 == idxk && c.joint2 == idx0) + (c.joint1 == idx0 && c.joint2 == idxk) || (c.joint1 == idxk && c.joint2 == idx0) }); if already_coupled { continue; diff --git a/crates/rapier3d-mjcf/src/loader/mass.rs b/crates/rapier3d-mjcf/src/loader/mass.rs index b8feee5d7..fa48274d8 100644 --- a/crates/rapier3d-mjcf/src/loader/mass.rs +++ b/crates/rapier3d-mjcf/src/loader/mass.rs @@ -327,12 +327,12 @@ pub(super) fn add_springdamper_to_multibody( let mut offset = 0; let mut ndofs = 0; for (i, link) in multibody.links().enumerate() { - let nd = link.joint().ndofs(); + let link_ndofs = link.joint().ndofs(); if i == link_id { - ndofs = nd; + ndofs = link_ndofs; break; } - offset += nd; + offset += link_ndofs; } if ndofs == 0 { return; diff --git a/crates/rapier3d-mjcf/src/loader/runtime.rs b/crates/rapier3d-mjcf/src/loader/runtime.rs index d8ef2f4ed..8bb76838a 100644 --- a/crates/rapier3d-mjcf/src/loader/runtime.rs +++ b/crates/rapier3d-mjcf/src/loader/runtime.rs @@ -264,7 +264,8 @@ impl MjcfRobotHandles> { rb.set_position(free_pose(qp, dof.scale, robot.base_shift), true); } if let Some(qv) = qvel { - let lin = Vector::new(qv[0] as Real, qv[1] as Real, qv[2] as Real) * dof.scale; + let lin = + Vector::new(qv[0] as Real, qv[1] as Real, qv[2] as Real) * dof.scale; let ang = Vector::new(qv[3] as Real, qv[4] as Real, qv[5] as Real); rb.set_linvel(lin, true); rb.set_angvel(ang, true); @@ -279,7 +280,9 @@ impl MjcfRobotHandles> { // Articulated DoF (hinge / slide / ball): write to the multibody's // generalized coordinates. let Some(jidx) = dof.joint else { continue }; - let Some(jh) = self.joints.get(jidx) else { continue }; + let Some(jh) = self.joints.get(jidx) else { + continue; + }; // `joint` is `None` for a joint that was dropped as a loop closure. let Some(handle) = jh.joint else { continue }; if let Some(link) = multibody_joints.rigid_body_link(jh.link2) { @@ -292,7 +295,9 @@ impl MjcfRobotHandles> { // Compute the displacement (target − current) under an immutable // borrow, then apply it under a mutable one. let (assembly_id, ndofs, disp) = { - let Some(link) = mb.link(link_id) else { continue }; + let Some(link) = mb.link(link_id) else { + continue; + }; let joint = link.joint(); let coords = joint.coords(); let disp = qpos.map(|qp| match dof.kind { @@ -375,7 +380,8 @@ impl MjcfRobotHandles { world[dof.body] = free_pose(qp, dof.scale, robot.base_shift); } if let Some(qv) = qvel { - let lin = Vector::new(qv[0] as Real, qv[1] as Real, qv[2] as Real) * dof.scale; + let lin = + Vector::new(qv[0] as Real, qv[1] as Real, qv[2] as Real) * dof.scale; let ang = Vector::new(qv[3] as Real, qv[4] as Real, qv[5] as Real); free_vels.push((dof.body, lin, ang)); } @@ -402,8 +408,10 @@ impl MjcfRobotHandles { // joint's parent body is already positioned. This mirrors rapier's // multibody `body_to_parent`: world₂ = world₁ · frame1 · q · frame2⁻¹. for (jidx, j) in robot.joints.iter().enumerate() { - world[j.link2] = - world[j.link1] * j.joint.local_frame1 * joint_tf[jidx] * j.joint.local_frame2.inverse(); + world[j.link2] = world[j.link1] + * j.joint.local_frame1 + * joint_tf[jidx] + * j.joint.local_frame2.inverse(); } for (i, handle) in self.bodies.iter().enumerate() { @@ -445,7 +453,7 @@ impl MjcfRobotHandles { /// Like [`apply_controls`](Self::apply_controls) but uniformly scales every /// actuator's strength by `gain_scale` (gains and force limits; see - /// [`configure_actuator_motor`]). `gain_scale < 1` softens the actuation — + /// `configure_actuator_motor`). `gain_scale < 1` softens the actuation — /// e.g. to ease servo-driven moves that would otherwise saturate and snap. pub fn apply_controls_scaled( &self, @@ -471,7 +479,7 @@ impl MjcfRobotHandles> { /// [`apply_controls`](MjcfRobotHandles::::apply_controls): /// it reproduces MuJoCo's "actuation" by writing each actuator's motor /// onto the multibody link it drives. The per-actuator interpretation is - /// identical (see [`configure_actuator_motor`]); call it once per frame + /// identical (see `configure_actuator_motor`); call it once per frame /// before `pipeline.step`. /// /// `ctrl` is a flat array of one scalar per actuator, in the order of @@ -493,7 +501,7 @@ impl MjcfRobotHandles> { /// Like [`apply_controls_multibody`](Self::apply_controls_multibody) but /// uniformly scales every actuator's strength by `gain_scale` (gains and - /// force limits; see [`configure_actuator_motor`]). `gain_scale < 1` softens + /// force limits; see `configure_actuator_motor`). `gain_scale < 1` softens /// the actuation, so a servo-driven move (e.g. easing between keyframes) /// ramps in instead of saturating to its force limit and arriving instantly. pub fn apply_controls_multibody_scaled( @@ -507,7 +515,9 @@ impl MjcfRobotHandles> { // `H` is `Option` here, so `ah.joint` is // `Option>`: the outer `None` means "no actuator joint", // the inner `None` means "joint dropped as a loop closure". - let Some(Some(handle)) = ah.joint else { continue }; + let Some(Some(handle)) = ah.joint else { + continue; + }; let Some((mb, link_id)) = multibody_joints.get_mut(handle) else { continue; }; diff --git a/crates/rapier3d-mjcf/tests/actuator_gain.rs b/crates/rapier3d-mjcf/tests/actuator_gain.rs new file mode 100644 index 000000000..8f7107a88 --- /dev/null +++ b/crates/rapier3d-mjcf/tests/actuator_gain.rs @@ -0,0 +1,105 @@ +//! `apply_controls_multibody_scaled`'s `gain_scale` softens actuation: a lower +//! scale makes a position servo approach its target more slowly (no snap). + +use rapier3d::prelude::*; +use rapier3d_mjcf::{MjcfLoaderOptions, MjcfMultibodyOptions, MjcfRobot}; + +// A strong position servo on one hinge — at full strength it slams to target. +const SERVO: &str = r#" + + + + + + + + + + + + +"#; + +fn angle(rb: &RigidBody) -> f32 { + let x = rb.rotation() * Vector::X; + (-x.z).atan2(x.x) +} + +/// Drive `j` toward 1.0 for `steps` steps at the given strength scale; return +/// the angle reached. +fn drive_to(steps: usize, gain_scale: Real) -> f32 { + let (robot, _) = MjcfRobot::from_str(SERVO, MjcfLoaderOptions::default(), ".").unwrap(); + let mut bodies = RigidBodySet::new(); + let mut colliders = ColliderSet::new(); + let mut ij = ImpulseJointSet::new(); + let mut mbj = MultibodyJointSet::new(); + let handles = robot.clone().insert_using_multibody_joints( + &mut bodies, + &mut colliders, + &mut mbj, + &mut ij, + MjcfMultibodyOptions::default(), + ); + let link = robot.body_name_to_idx["link"]; + let mut pipeline = PhysicsPipeline::new(); + let ip = IntegrationParameters::default(); + let (mut isl, mut bp, mut np, mut ccd) = ( + IslandManager::new(), + DefaultBroadPhase::new(), + NarrowPhase::new(), + CCDSolver::new(), + ); + for _ in 0..steps { + handles.apply_controls_multibody_scaled(&mut bodies, &mut mbj, &[1.0_f32], gain_scale); + pipeline.step( + Vector::new(0.0, 0.0, 0.0), // no gravity: isolate the servo dynamics + &ip, + &mut isl, + &mut bp, + &mut np, + &mut bodies, + &mut colliders, + &mut ij, + &mut mbj, + &mut ccd, + &(), + &(), + ); + } + angle( + bodies + .get(handles.bodies[link].as_ref().unwrap().body) + .unwrap(), + ) +} + +#[test] +fn lower_gain_scale_approaches_target_more_slowly() { + let steps = 15; + let full = drive_to(steps, 1.0); + let soft = drive_to(steps, 0.1); + println!("full={full} soft={soft}"); + + // Force-limited, so the approach rate scales with strength: at 0.1 the + // servo moves roughly an order of magnitude less far in the same time. + assert!( + full > 0.2, + "full-strength servo should be well underway: {full}" + ); + assert!( + soft < full * 0.3, + "0.1x servo should lag well behind: soft={soft} full={full}" + ); + assert!( + soft > 0.0, + "0.1x servo should still be moving toward target: {soft}" + ); +} + +#[test] +fn zero_ish_gain_does_not_nan() { + // The minimum the example slider allows is 0.02; make sure a tiny scale (and + // the INFINITY force cap on a forcerange-less actuator) stays finite. + let a = drive_to(20, 0.02); + assert!(a.is_finite()); +} diff --git a/crates/rapier3d-mjcf/tests/equality_joint.rs b/crates/rapier3d-mjcf/tests/equality_joint.rs index 54e72c245..0d8bd0645 100644 --- a/crates/rapier3d-mjcf/tests/equality_joint.rs +++ b/crates/rapier3d-mjcf/tests/equality_joint.rs @@ -100,7 +100,10 @@ fn equality_joint_couples_multibody_joints() { let q1 = read_q(&multibody_joints, h1); let q2 = read_q(&multibody_joints, h2); - assert!((q1 - 0.3).abs() < 0.05, "driven joint q1 = {q1}, expected ~0.3"); + assert!( + (q1 - 0.3).abs() < 0.05, + "driven joint q1 = {q1}, expected ~0.3" + ); assert!( (q2 - 2.0 * q1).abs() < 0.02, "coupling q2 = 2·q1 not enforced: q1 = {q1}, q2 = {q2}" diff --git a/crates/rapier3d-mjcf/tests/fixed_tendon.rs b/crates/rapier3d-mjcf/tests/fixed_tendon.rs new file mode 100644 index 000000000..0c63f0065 --- /dev/null +++ b/crates/rapier3d-mjcf/tests/fixed_tendon.rs @@ -0,0 +1,170 @@ +//! Fixed-tendon support: parsing, actuator binding to the tendon's primary +//! joint, and the co-actuation coupling that drags the other joints along. + +use rapier3d::prelude::*; +use rapier3d_mjcf::{MjcfLoaderOptions, MjcfMultibodyOptions, MjcfRobot}; + +// Two serial hinges joined by a fixed tendon `j1 + j2`, driven by one position +// actuator on the tendon — the shadow-hand finger pattern in miniature. +const TENDON_ARM: &str = r#" + + + + + + + + + + + + + + + + + + + + + + + +"#; + +#[test] +fn fixed_tendon_parses_and_binds() { + let (robot, model) = + MjcfRobot::from_str(TENDON_ARM, MjcfLoaderOptions::default(), ".").unwrap(); + + // Parser kept the fixed tendon with its two terms. + assert_eq!(model.tendons.len(), 1); + assert_eq!(model.tendons[0].name.as_deref(), Some("t")); + assert_eq!(model.tendons[0].joints.len(), 2); + + // The tendon actuator binds to the tendon's primary (first) joint, `j1`. + let j1 = robot.joint_name_to_idx["j1"]; + let j2 = robot.joint_name_to_idx["j2"]; + assert_eq!(robot.actuators.len(), 1); + assert_eq!(robot.actuators[0].joint_index, Some(j1)); + + // A co-actuation coupling was added between the tendon's joints (coeff 1). + let coupling = robot + .joint_couplings + .iter() + .find(|c| (c.joint1 == j1 && c.joint2 == j2) || (c.joint1 == j2 && c.joint2 == j1)) + .expect("expected a co-actuation coupling between j1 and j2"); + assert!((coupling.coeff - 1.0).abs() < 1e-6); +} + +fn hinge_angle(rb: &RigidBody) -> f32 { + let x = rb.rotation() * Vector::X; + (-x.z).atan2(x.x) +} + +#[test] +fn fixed_tendon_actuator_drives_both_joints() { + let (robot, _) = MjcfRobot::from_str(TENDON_ARM, MjcfLoaderOptions::default(), ".").unwrap(); + + let mut bodies = RigidBodySet::new(); + let mut colliders = ColliderSet::new(); + let mut ij = ImpulseJointSet::new(); + let mut mbj = MultibodyJointSet::new(); + let handles = robot.clone().insert_using_multibody_joints( + &mut bodies, + &mut colliders, + &mut mbj, + &mut ij, + MjcfMultibodyOptions::default(), + ); + + let b1 = robot.body_name_to_idx["b1"]; + let b2 = robot.body_name_to_idx["b2"]; + + // Drive the single tendon actuator toward 0.5. + let ctrl = [0.5_f32]; + let mut pipeline = PhysicsPipeline::new(); + let ip = IntegrationParameters::default(); + let (mut isl, mut bp, mut np, mut ccd) = ( + IslandManager::new(), + DefaultBroadPhase::new(), + NarrowPhase::new(), + CCDSolver::new(), + ); + for _ in 0..400 { + handles.apply_controls_multibody(&mut bodies, &mut mbj, &ctrl); + pipeline.step( + Vector::new(0.0, 0.0, -9.81), + &ip, + &mut isl, + &mut bp, + &mut np, + &mut bodies, + &mut colliders, + &mut ij, + &mut mbj, + &mut ccd, + &(), + &(), + ); + } + + // j1 is actuated (proximal world angle ≈ 0.5); j2 follows via the coupling, + // so the distal body's *world* angle is ≈ j1 + j2 ≈ 1.0. + let a1 = hinge_angle( + bodies + .get(handles.bodies[b1].as_ref().unwrap().body) + .unwrap(), + ); + let a2_world = hinge_angle( + bodies + .get(handles.bodies[b2].as_ref().unwrap().body) + .unwrap(), + ); + let j2 = a2_world - a1; // relative (coupled) angle + assert!( + (a1 - 0.5).abs() < 0.05, + "proximal didn't reach target: {a1}" + ); + assert!( + (j2 - 0.5).abs() < 0.05, + "distal didn't follow via coupling: j2 = {j2}" + ); +} + +/// The real shadow_hand: its four `*J0` finger tendons should now bind their +/// actuators and couple each finger's middle+distal segments. Skipped without +/// the menagerie checkout. +#[test] +fn shadow_hand_fixed_tendons_bind_and_couple() { + let scene = "../../../mujoco_menagerie/shadow_hand/scene_right.xml"; + if !std::path::Path::new(scene).exists() { + eprintln!("mujoco_menagerie not found; skipping"); + return; + } + let (robot, model) = MjcfRobot::from_file(scene, MjcfLoaderOptions::default()).unwrap(); + // Four fixed tendons (FF/MF/RF/LF J0), all two-joint, equal-coef. + assert_eq!(model.tendons.len(), 4); + + // The four `rh_A_*J0` tendon actuators now bind (previously unbound). + let bound = robot + .actuators + .iter() + .filter(|a| a.joint_index.is_some()) + .count(); + assert_eq!( + bound, 20, + "all 20 actuators (incl. the 4 tendon ones) should bind" + ); + + // Each tendon added a coupling between its middle and distal joints — + // FFJ1 (distal) follows FFJ2 (middle). + let ffj2 = robot.joint_name_to_idx["rh_FFJ2"]; + let ffj1 = robot.joint_name_to_idx["rh_FFJ1"]; + assert!( + robot.joint_couplings.iter().any(|c| { + (c.joint1 == ffj2 && c.joint2 == ffj1) || (c.joint1 == ffj1 && c.joint2 == ffj2) + }), + "expected a coupling between rh_FFJ1 and rh_FFJ2" + ); +} diff --git a/crates/rapier3d-mjcf/tests/keyframe_controls.rs b/crates/rapier3d-mjcf/tests/keyframe_controls.rs new file mode 100644 index 000000000..cec8d9a17 --- /dev/null +++ b/crates/rapier3d-mjcf/tests/keyframe_controls.rs @@ -0,0 +1,144 @@ +//! `MjcfRobot::keyframe_controls` lets an actuated model hold a keyframe pose +//! under the controls callback, instead of the zero control vector dragging +//! every position servo back to the zero configuration. + +use rapier3d::prelude::*; +use rapier3d_mjcf::{MjcfLoaderOptions, MjcfMultibodyOptions, MjcfRobot}; + +// A hinge held by a `` servo, with a keyframe that bends it to 1.0. +const SERVO_ARM: &str = r#" + + + + + + + + + + + + + + + +"#; + +fn hinge_angle(rb: &RigidBody) -> f32 { + let x = rb.rotation() * Vector::X; + (-x.z).atan2(x.x) +} + +fn build() -> ( + MjcfRobot, + RigidBodySet, + ColliderSet, + ImpulseJointSet, + MultibodyJointSet, + rapier3d_mjcf::MjcfRobotHandles>, +) { + let (robot, _) = MjcfRobot::from_str(SERVO_ARM, MjcfLoaderOptions::default(), ".").unwrap(); + let mut bodies = RigidBodySet::new(); + let mut colliders = ColliderSet::new(); + let mut ij = ImpulseJointSet::new(); + let mut mbj = MultibodyJointSet::new(); + let handles = robot.clone().insert_using_multibody_joints( + &mut bodies, + &mut colliders, + &mut mbj, + &mut ij, + MjcfMultibodyOptions::default(), + ); + (robot, bodies, colliders, ij, mbj, handles) +} + +fn run( + robot: &MjcfRobot, + bodies: &mut RigidBodySet, + colliders: &mut ColliderSet, + ij: &mut ImpulseJointSet, + mbj: &mut MultibodyJointSet, + handles: &rapier3d_mjcf::MjcfRobotHandles>, + ctrl: &[Real], +) -> f32 { + let mut pipeline = PhysicsPipeline::new(); + let ip = IntegrationParameters::default(); + let (mut isl, mut bp, mut np, mut ccd) = ( + IslandManager::new(), + DefaultBroadPhase::new(), + NarrowPhase::new(), + CCDSolver::new(), + ); + for _ in 0..200 { + handles.apply_controls_multibody(bodies, mbj, ctrl); + pipeline.step( + Vector::new(0.0, 0.0, -9.81), + &ip, + &mut isl, + &mut bp, + &mut np, + bodies, + colliders, + ij, + mbj, + &mut ccd, + &(), + &(), + ); + } + let link = robot.body_name_to_idx["link"]; + hinge_angle( + bodies + .get(handles.bodies[link].as_ref().unwrap().body) + .unwrap(), + ) +} + +#[test] +fn zero_ctrl_drags_keyframe_to_zero_config() { + let (robot, mut bodies, mut colliders, mut ij, mut mbj, handles) = build(); + let key = robot.keyframe_by_name("bent").unwrap().clone(); + handles.apply_keyframe(&mut bodies, &mut mbj, &robot, &key); + // The naive zero control vector: the servo pulls the joint back to 0. + let angle = run( + &robot, + &mut bodies, + &mut colliders, + &mut ij, + &mut mbj, + &handles, + &vec![0.0; handles.actuators.len()], + ); + assert!( + angle.abs() < 0.05, + "expected joint dragged to ~0, got {angle}" + ); +} + +#[test] +fn keyframe_controls_hold_the_pose() { + let (robot, mut bodies, mut colliders, mut ij, mut mbj, handles) = build(); + let key = robot.keyframe_by_name("bent").unwrap().clone(); + handles.apply_keyframe(&mut bodies, &mut mbj, &robot, &key); + // Driving the servo with the keyframe-derived control holds the pose. + let ctrl = robot.keyframe_controls(&key); + assert_eq!(ctrl.len(), 1); + assert!( + (ctrl[0] - 1.0).abs() < 1e-6, + "derived control = {}", + ctrl[0] + ); + let angle = run( + &robot, + &mut bodies, + &mut colliders, + &mut ij, + &mut mbj, + &handles, + &ctrl, + ); + assert!( + (angle - 1.0).abs() < 0.05, + "expected joint held at ~1.0, got {angle}" + ); +} diff --git a/crates/rapier3d-mjcf/tests/keyframe_qpos.rs b/crates/rapier3d-mjcf/tests/keyframe_qpos.rs new file mode 100644 index 000000000..a0d85154e --- /dev/null +++ b/crates/rapier3d-mjcf/tests/keyframe_qpos.rs @@ -0,0 +1,177 @@ +//! Keyframe `qpos` / `qvel` application on both insertion paths. + +use rapier3d::prelude::*; +use rapier3d_mjcf::{MjcfLoaderOptions, MjcfMultibodyOptions, MjcfRobot}; + +/// A two-link arm hinged to the world: a shoulder hinge about Z then an +/// elbow hinge about Z, with a "home" keyframe that bends both joints. +const ARM: &str = r#" + + + + + + + + + + + + + + + + + +"#; + +fn world_z_angle(rb: &RigidBody) -> f32 { + // The arm rotates in the XY plane about Z; recover the angle from the + // body's rotation applied to the +X axis. + let x = rb.rotation() * Vector::X; + x.y.atan2(x.x) +} + +#[test] +fn qpos_applied_via_multibody() { + let (robot, _) = MjcfRobot::from_str(ARM, MjcfLoaderOptions::default(), ".").unwrap(); + assert_eq!(robot.qpos_dofs.len(), 2, "expected two 1-DoF hinge slots"); + + let mut bodies = RigidBodySet::new(); + let mut colliders = ColliderSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let mut multibody_joints = MultibodyJointSet::new(); + let handles = robot.clone().insert_using_multibody_joints( + &mut bodies, + &mut colliders, + &mut multibody_joints, + &mut impulse_joints, + MjcfMultibodyOptions::default(), + ); + + let key = robot.keyframe_by_name("home").unwrap().clone(); + handles.apply_keyframe(&mut bodies, &mut multibody_joints, &robot, &key); + + let upper = bodies + .get(handles.bodies[1].as_ref().unwrap().body) + .unwrap(); + let lower = bodies + .get(handles.bodies[2].as_ref().unwrap().body) + .unwrap(); + // Shoulder = 0.5 (world angle); elbow = -0.3 relative → 0.2 world. + assert!( + (world_z_angle(upper) - 0.5).abs() < 1e-4, + "shoulder angle = {}", + world_z_angle(upper) + ); + assert!( + (world_z_angle(lower) - 0.2).abs() < 1e-4, + "lower world angle = {}", + world_z_angle(lower) + ); +} + +#[test] +fn qpos_applied_via_impulse_joints() { + let (robot, _) = MjcfRobot::from_str(ARM, MjcfLoaderOptions::default(), ".").unwrap(); + + let mut bodies = RigidBodySet::new(); + let mut colliders = ColliderSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let handles = + robot + .clone() + .insert_using_impulse_joints(&mut bodies, &mut colliders, &mut impulse_joints); + + let key = robot.keyframe_by_name("home").unwrap().clone(); + handles.apply_keyframe(&mut bodies, &robot, &key); + + let upper = bodies + .get(handles.bodies[1].as_ref().unwrap().body) + .unwrap(); + let lower = bodies + .get(handles.bodies[2].as_ref().unwrap().body) + .unwrap(); + assert!( + (world_z_angle(upper) - 0.5).abs() < 1e-4, + "shoulder angle = {}", + world_z_angle(upper) + ); + assert!( + (world_z_angle(lower) - 0.2).abs() < 1e-4, + "lower world angle = {}", + world_z_angle(lower) + ); + // The lower body's anchor (the elbow) must coincide on both sides — i.e. + // forward-kinematics kept the joint satisfied. Elbow world point is the + // tip of the upper link. + let elbow = upper.translation() + upper.rotation() * Vector::new(1.0, 0.0, 0.0); + let lower_origin = lower.translation(); + assert!( + (elbow - lower_origin).length() < 1e-4, + "elbow joint violated: {elbow:?} vs {lower_origin:?}" + ); +} + +/// A free-floating base + one hinge: the "home" keyframe lifts and rotates the +/// base (qpos[0..7]) and bends the hinge (qpos[7]). +const FLOATER: &str = r#" + + + + + + + + + + + + + + + + + +"#; + +#[test] +fn free_base_qpos_sets_world_pose() { + let (robot, _) = MjcfRobot::from_str(FLOATER, MjcfLoaderOptions::default(), ".").unwrap(); + assert_eq!(robot.qpos_dofs.len(), 2); + + let mut bodies = RigidBodySet::new(); + let mut colliders = ColliderSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let mut multibody_joints = MultibodyJointSet::new(); + let handles = robot.clone().insert_using_multibody_joints( + &mut bodies, + &mut colliders, + &mut multibody_joints, + &mut impulse_joints, + MjcfMultibodyOptions::default(), + ); + + let key = robot.keyframe_by_name("home").unwrap().clone(); + handles.apply_keyframe(&mut bodies, &mut multibody_joints, &robot, &key); + + let base = bodies + .get(handles.bodies[1].as_ref().unwrap().body) + .unwrap(); + let t = base.translation(); + assert!( + (t - Vector::new(1.0, 2.0, 3.0)).length() < 1e-4, + "base translation = {t:?}" + ); + + // The child arm rides along: its origin sits at base + R_base*(0.2,0,0), + // which with an identity base orientation is (1.2, 2, 3). + let arm = bodies + .get(handles.bodies[2].as_ref().unwrap().body) + .unwrap(); + let a = arm.translation(); + assert!( + (a - Vector::new(1.2, 2.0, 3.0)).length() < 1e-4, + "arm origin = {a:?}" + ); +} diff --git a/crates/rapier3d-mjcf/tests/loader_regressions.rs b/crates/rapier3d-mjcf/tests/loader_regressions.rs index 03893b326..acda37d63 100644 --- a/crates/rapier3d-mjcf/tests/loader_regressions.rs +++ b/crates/rapier3d-mjcf/tests/loader_regressions.rs @@ -575,7 +575,10 @@ fn mesh_scale_inherits_default_when_asset_precedes_default() { "#; let (_, model) = MjcfRobot::from_str(xml, MjcfLoaderOptions::default(), ".").unwrap(); - let mesh = model.assets.mesh("part").expect("mesh `part` should be parsed"); + let mesh = model + .assets + .mesh("part") + .expect("mesh `part` should be parsed"); assert_eq!( mesh.scale, [0.001, 0.001, 0.001], diff --git a/crates/rapier3d-mjcf/tests/material_defaults.rs b/crates/rapier3d-mjcf/tests/material_defaults.rs index 47e6e3e7e..3785eb5b8 100644 --- a/crates/rapier3d-mjcf/tests/material_defaults.rs +++ b/crates/rapier3d-mjcf/tests/material_defaults.rs @@ -76,7 +76,11 @@ fn material_inherits_class_defaults() { // `shiny` default class: metallic 1, reflectance 0.9, roughness 1 − 0.8. let m = material_named(MODEL, "g_class"); assert!((m.metallic - 1.0).abs() < 1e-6, "metallic = {}", m.metallic); - assert!((m.reflectance - 0.9).abs() < 1e-6, "reflectance = {}", m.reflectance); + assert!( + (m.reflectance - 0.9).abs() < 1e-6, + "reflectance = {}", + m.reflectance + ); assert!( (m.roughness - 0.2).abs() < 1e-6, "roughness = {} (expected 1 − shininess(0.8))", @@ -102,7 +106,11 @@ fn material_without_class_uses_mujoco_defaults() { // `plain` has no class: MuJoCo defaults specular/shininess 0.5, dielectric. let m = material_named(MODEL, "g_plain"); assert!((m.metallic - 0.0).abs() < 1e-6, "metallic = {}", m.metallic); - assert!((m.reflectance - 0.5).abs() < 1e-6, "reflectance = {}", m.reflectance); + assert!( + (m.reflectance - 0.5).abs() < 1e-6, + "reflectance = {}", + m.reflectance + ); assert!( (m.roughness - 0.5).abs() < 1e-6, "default shininess(0.5) → roughness = {}", diff --git a/crates/rapier3d-mjcf/tests/phase5.rs b/crates/rapier3d-mjcf/tests/phase5.rs index 3a153f222..385b3dd40 100644 --- a/crates/rapier3d-mjcf/tests/phase5.rs +++ b/crates/rapier3d-mjcf/tests/phase5.rs @@ -129,7 +129,10 @@ const AFFINE_SERVO_XML: &str = r#" "#; /// The rapier handle of the `arm` body in the model above. -fn arm_handle(robot: &MjcfRobot, handles: &rapier3d_mjcf::MjcfRobotHandles) -> RigidBodyHandle { +fn arm_handle( + robot: &MjcfRobot, + handles: &rapier3d_mjcf::MjcfRobotHandles, +) -> RigidBodyHandle { for (i, bh) in handles.bodies.iter().enumerate() { if robot.bodies[i].name.as_deref() == Some("arm") { return bh.as_ref().unwrap().body; @@ -140,7 +143,8 @@ fn arm_handle(robot: &MjcfRobot, handles: &rapier3d_mjcf::MjcfRobotHand #[test] fn affine_general_actuator_parses_as_servo() { - let (robot, _) = MjcfRobot::from_str(AFFINE_SERVO_XML, MjcfLoaderOptions::default(), ".").unwrap(); + let (robot, _) = + MjcfRobot::from_str(AFFINE_SERVO_XML, MjcfLoaderOptions::default(), ".").unwrap(); let a = &robot.actuators[0].actuator; assert_eq!(a.bias_type.as_deref(), Some("affine")); assert_eq!(a.gainprm.first().copied(), Some(10.0)); @@ -155,7 +159,8 @@ fn affine_general_actuator_parses_as_servo() { /// across the whole run. fn drive_servo_to(target: Real, multibody: bool) -> Real { use rapier3d_mjcf::MjcfMultibodyOptions; - let (robot, _) = MjcfRobot::from_str(AFFINE_SERVO_XML, MjcfLoaderOptions::default(), ".").unwrap(); + let (robot, _) = + MjcfRobot::from_str(AFFINE_SERVO_XML, MjcfLoaderOptions::default(), ".").unwrap(); let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); let mut impulse_joints = ImpulseJointSet::new(); @@ -176,8 +181,11 @@ fn drive_servo_to(target: Real, multibody: bool) -> Real { let arm = arm_handle(&robot, &handles); (Ctl::Multibody(handles), arm) } else { - let handles = - robot.clone().insert_using_impulse_joints(&mut bodies, &mut colliders, &mut impulse_joints); + let handles = robot.clone().insert_using_impulse_joints( + &mut bodies, + &mut colliders, + &mut impulse_joints, + ); let arm = arm_handle(&robot, &handles); (Ctl::Impulse(handles), arm) }; @@ -216,13 +224,19 @@ fn drive_servo_to(target: Real, multibody: bool) -> Real { #[test] fn affine_general_actuator_drives_hinge_impulse_path() { let angle = drive_servo_to(0.5, false); - assert!((angle - 0.5).abs() < 0.1, "servo didn't reach target: angle = {angle}"); + assert!( + (angle - 0.5).abs() < 0.1, + "servo didn't reach target: angle = {angle}" + ); } #[test] fn affine_general_actuator_drives_hinge_multibody_path() { let angle = drive_servo_to(0.5, true); - assert!((angle - 0.5).abs() < 0.1, "multibody servo didn't reach target: angle = {angle}"); + assert!( + (angle - 0.5).abs() < 0.1, + "multibody servo didn't reach target: angle = {angle}" + ); } #[test] diff --git a/crates/rapier3d-mjcf/tests/shadow_hand_keyframes.rs b/crates/rapier3d-mjcf/tests/shadow_hand_keyframes.rs new file mode 100644 index 000000000..d1642c67f --- /dev/null +++ b/crates/rapier3d-mjcf/tests/shadow_hand_keyframes.rs @@ -0,0 +1,195 @@ +//! Keyframes whose `qpos` is shorter than the model's full generalized +//! coordinate vector. This is what lets the menagerie's standalone +//! `keyframes.xml` snippets (authored against the bare robot) apply to a scene +//! that *appends* free props after the robot: the keyframe drives the leading +//! DoFs and leaves the trailing ones untouched. + +use rapier3d::prelude::*; +use rapier3d_mjcf::{MjcfLoaderOptions, MjcfMultibodyOptions, MjcfRobot}; + +/// A hinged arm declared before a free-floating prop, with a keyframe that only +/// specifies the hinge's single `qpos` — the prop's 7 `qpos` are omitted. +const ARM_THEN_PROP: &str = r#" + + + + + + + + + + + + + + + + + +"#; + +fn arm_y_angle(rb: &RigidBody) -> f32 { + // The hinge is about +Y; recover the angle from where +X lands. + let x = rb.rotation() * Vector::X; + (-x.z).atan2(x.x) +} + +#[test] +fn short_qpos_applies_to_leading_dofs_multibody() { + let (robot, _) = MjcfRobot::from_str(ARM_THEN_PROP, MjcfLoaderOptions::default(), ".").unwrap(); + // Two DoF slots: the hinge, then the prop's free joint (in that order). + assert_eq!(robot.qpos_dofs.len(), 2); + + let mut bodies = RigidBodySet::new(); + let mut colliders = ColliderSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let mut mbj = MultibodyJointSet::new(); + let handles = robot.clone().insert_using_multibody_joints( + &mut bodies, + &mut colliders, + &mut mbj, + &mut impulse_joints, + MjcfMultibodyOptions::default(), + ); + + let arm = robot.body_name_to_idx["arm"]; + let prop = robot.body_name_to_idx["prop"]; + let prop_before = bodies + .get(handles.bodies[prop].as_ref().unwrap().body) + .unwrap() + .translation(); + + let key = robot.keyframe_by_name("bent").unwrap().clone(); + handles.apply_keyframe(&mut bodies, &mut mbj, &robot, &key); + + let arm_rb = bodies + .get(handles.bodies[arm].as_ref().unwrap().body) + .unwrap(); + let prop_after = bodies + .get(handles.bodies[prop].as_ref().unwrap().body) + .unwrap() + .translation(); + + assert!( + (arm_y_angle(arm_rb) - 0.6).abs() < 1e-4, + "hinge angle = {}", + arm_y_angle(arm_rb) + ); + // The prop has no qpos in the keyframe, so it stays where it loaded. + assert!( + (prop_before - prop_after).length() < 1e-6, + "prop moved: {prop_before:?} -> {prop_after:?}" + ); +} + +#[test] +fn short_qpos_applies_to_leading_dofs_impulse() { + let (robot, _) = MjcfRobot::from_str(ARM_THEN_PROP, MjcfLoaderOptions::default(), ".").unwrap(); + + let mut bodies = RigidBodySet::new(); + let mut colliders = ColliderSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let handles = + robot + .clone() + .insert_using_impulse_joints(&mut bodies, &mut colliders, &mut impulse_joints); + + let arm = robot.body_name_to_idx["arm"]; + let prop = robot.body_name_to_idx["prop"]; + let prop_before = bodies + .get(handles.bodies[prop].as_ref().unwrap().body) + .unwrap() + .translation(); + + let key = robot.keyframe_by_name("bent").unwrap().clone(); + handles.apply_keyframe(&mut bodies, &robot, &key); + + let arm_rb = bodies + .get(handles.bodies[arm].as_ref().unwrap().body) + .unwrap(); + let prop_after = bodies + .get(handles.bodies[prop].as_ref().unwrap().body) + .unwrap() + .translation(); + + assert!( + (arm_y_angle(arm_rb) - 0.6).abs() < 1e-4, + "hinge angle = {}", + arm_y_angle(arm_rb) + ); + assert!( + (prop_before - prop_after).length() < 1e-6, + "prop moved: {prop_before:?} -> {prop_after:?}" + ); +} + +/// End-to-end check against the real shadow_hand model + its standalone +/// `keyframes.xml`, mirroring what the menagerie example does. Skipped when the +/// menagerie isn't cloned next to the rapier repo. +#[test] +fn shadow_hand_sibling_keyframes_align() { + let base = "../../../mujoco_menagerie/shadow_hand"; + let scene = format!("{base}/scene_right.xml"); + let kf = format!("{base}/keyframes.xml"); + if !std::path::Path::new(&scene).exists() { + eprintln!("mujoco_menagerie not found; skipping shadow_hand integration test"); + return; + } + + let (mut robot, _) = MjcfRobot::from_file(&scene, MjcfLoaderOptions::default()).unwrap(); + let (kfr, _) = MjcfRobot::from_file(&kf, MjcfLoaderOptions::default()).unwrap(); + robot.keyframes = kfr.keyframes; // the scene declares none; merge them in. + assert_eq!(robot.keyframes.len(), 13); + + let mut bodies = RigidBodySet::new(); + let mut colliders = ColliderSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let mut mbj = MultibodyJointSet::new(); + let handles = robot.clone().insert_using_multibody_joints( + &mut bodies, + &mut colliders, + &mut mbj, + &mut impulse_joints, + MjcfMultibodyOptions::default(), + ); + + let obj = robot.body_name_to_idx["object"]; + let ff = robot.body_name_to_idx["rh_ffdistal"]; + let obj_before = bodies + .get(handles.bodies[obj].as_ref().unwrap().body) + .unwrap() + .translation(); + let ff_before = *bodies + .get(handles.bodies[ff].as_ref().unwrap().body) + .unwrap() + .rotation(); + + let key = robot.keyframe_by_name("rock").unwrap().clone(); + handles.apply_keyframe(&mut bodies, &mut mbj, &robot, &key); + + let obj_after = bodies + .get(handles.bodies[obj].as_ref().unwrap().body) + .unwrap() + .translation(); + let ff_after = *bodies + .get(handles.bodies[ff].as_ref().unwrap().body) + .unwrap() + .rotation(); + + // The "rock" pose curls the fingers; the free object (declared after the + // hand, so its qpos sits past the keyframe's 24 values) stays put. + assert!( + ff_before.angle_between(ff_after) > 0.5, + "finger didn't curl: {}", + ff_before.angle_between(ff_after) + ); + assert!( + (obj_before - obj_after).length() < 1e-6, + "object moved: {obj_before:?} -> {obj_after:?}" + ); + assert!( + (obj_after - Vector::new(0.3, 0.0, 0.1)).length() < 1e-3, + "object not at its scene pose: {obj_after:?}" + ); +} diff --git a/crates/rapier3d/tests/multibody_armature.rs b/crates/rapier3d/tests/multibody_armature.rs index 60a4db0f6..4d9f8f780 100644 --- a/crates/rapier3d/tests/multibody_armature.rs +++ b/crates/rapier3d/tests/multibody_armature.rs @@ -113,11 +113,20 @@ fn armature_does_not_change_spatial_inertia() { let rev = RevoluteJointBuilder::new(Vector::Z).local_anchor2(Vector::new(-1.0, 0.0, 0.0)); let handle = multibody_joints.insert(base, link, rev, true).unwrap(); - let before = bodies[link].mass_properties().local_mprops.principal_inertia(); + let before = bodies[link] + .mass_properties() + .local_mprops + .principal_inertia(); let (mb, _) = multibody_joints.get_mut(handle).unwrap(); mb.armature_mut()[0] = 5.0; - let after = bodies[link].mass_properties().local_mprops.principal_inertia(); - assert_eq!(before, after, "armature must not alter the spatial inertia tensor"); + let after = bodies[link] + .mass_properties() + .local_mprops + .principal_inertia(); + assert_eq!( + before, after, + "armature must not alter the spatial inertia tensor" + ); } diff --git a/crates/rapier3d/tests/multibody_coupling.rs b/crates/rapier3d/tests/multibody_coupling.rs index cbcb78c43..eb8fd5ef9 100644 --- a/crates/rapier3d/tests/multibody_coupling.rs +++ b/crates/rapier3d/tests/multibody_coupling.rs @@ -17,12 +17,18 @@ fn run_coupling(coeff: Real, offset: Real, target: Real) -> (Real, Real) { let inertia = Vector::new(0.1, 0.1, 0.1); let base = bodies.insert(RigidBodyBuilder::fixed()); let link1 = bodies.insert( - RigidBodyBuilder::dynamic() - .additional_mass_properties(MassProperties::new(Vector::ZERO, 1.0, inertia)), + RigidBodyBuilder::dynamic().additional_mass_properties(MassProperties::new( + Vector::ZERO, + 1.0, + inertia, + )), ); let link2 = bodies.insert( - RigidBodyBuilder::dynamic() - .additional_mass_properties(MassProperties::new(Vector::ZERO, 1.0, inertia)), + RigidBodyBuilder::dynamic().additional_mass_properties(MassProperties::new( + Vector::ZERO, + 1.0, + inertia, + )), ); let h1 = multibody_joints @@ -85,8 +91,14 @@ fn run_coupling(coeff: Real, offset: Real, target: Real) -> (Real, Real) { fn one_to_one_coupling_makes_joints_track() { // q2 = q1: the robotiq case. Drive q1 to 0.5; q2 must follow. let (q1, q2) = run_coupling(1.0, 0.0, 0.5); - assert!((q1 - 0.5).abs() < 0.05, "driven joint didn't reach target: q1 = {q1}"); - assert!((q2 - q1).abs() < 0.02, "coupling not enforced: q1 = {q1}, q2 = {q2}"); + assert!( + (q1 - 0.5).abs() < 0.05, + "driven joint didn't reach target: q1 = {q1}" + ); + assert!( + (q2 - q1).abs() < 0.02, + "coupling not enforced: q1 = {q1}, q2 = {q2}" + ); } #[test] @@ -94,7 +106,10 @@ fn scaled_and_offset_coupling() { // q2 = -0.5·q1 + 0.2: a gear ratio with an offset. let (q1, q2) = run_coupling(-0.5, 0.2, 0.6); let expected = -0.5 * q1 + 0.2; - assert!((q1 - 0.6).abs() < 0.05, "driven joint didn't reach target: q1 = {q1}"); + assert!( + (q1 - 0.6).abs() < 0.05, + "driven joint didn't reach target: q1 = {q1}" + ); assert!( (q2 - expected).abs() < 0.02, "coupling q2 = -0.5·q1 + 0.2 not enforced: q1 = {q1}, q2 = {q2}, expected {expected}" diff --git a/crates/rapier3d/tests/multibody_dof_inertia.rs b/crates/rapier3d/tests/multibody_dof_inertia.rs index e0ba2a324..72bae555a 100644 --- a/crates/rapier3d/tests/multibody_dof_inertia.rs +++ b/crates/rapier3d/tests/multibody_dof_inertia.rs @@ -15,13 +15,9 @@ fn leaf_joint_inverse_inertia_is_link_inertia() { let mut multibody_joints = MultibodyJointSet::new(); let base = bodies.insert(RigidBodyBuilder::fixed()); let izz = 0.3; - let link = bodies.insert( - RigidBodyBuilder::dynamic().additional_mass_properties(MassProperties::new( - Vector::ZERO, - 1.0, - Vector::new(0.1, 0.2, izz), - )), - ); + let link = bodies.insert(RigidBodyBuilder::dynamic().additional_mass_properties( + MassProperties::new(Vector::ZERO, 1.0, Vector::new(0.1, 0.2, izz)), + )); let h = multibody_joints .insert(base, link, RevoluteJointBuilder::new(Vector::Z), true) .unwrap(); @@ -43,7 +39,12 @@ fn leaf_joint_inverse_inertia_is_link_inertia() { let inv = mb.dof_inverse_inertia(&bodies); // Before setting armature: 1 / Izz. assert_eq!(inv.len(), 1); - assert!((inv[0] - 1.0 / izz).abs() < 1e-4, "inv = {}, want {}", inv[0], 1.0 / izz); + assert!( + (inv[0] - 1.0 / izz).abs() < 1e-4, + "inv = {}, want {}", + inv[0], + 1.0 / izz + ); // Now set armature and recheck. let n = mb.armature().len(); mb.armature_mut()[n - 1] = armature; @@ -71,20 +72,12 @@ fn parent_joint_inverse_inertia_accounts_for_children() { let mut multibody_joints = MultibodyJointSet::new(); let base = bodies.insert(RigidBodyBuilder::fixed()); let (i1, i2) = (0.4, 0.25); - let link1 = bodies.insert( - RigidBodyBuilder::dynamic().additional_mass_properties(MassProperties::new( - Vector::ZERO, - 1.0, - Vector::new(0.1, 0.1, i1), - )), - ); - let link2 = bodies.insert( - RigidBodyBuilder::dynamic().additional_mass_properties(MassProperties::new( - Vector::ZERO, - 1.0, - Vector::new(0.1, 0.1, i2), - )), - ); + let link1 = bodies.insert(RigidBodyBuilder::dynamic().additional_mass_properties( + MassProperties::new(Vector::ZERO, 1.0, Vector::new(0.1, 0.1, i1)), + )); + let link2 = bodies.insert(RigidBodyBuilder::dynamic().additional_mass_properties( + MassProperties::new(Vector::ZERO, 1.0, Vector::new(0.1, 0.1, i2)), + )); multibody_joints .insert(base, link1, RevoluteJointBuilder::new(Vector::Z), true) .unwrap(); diff --git a/crates/rapier3d/tests/multibody_spring.rs b/crates/rapier3d/tests/multibody_spring.rs index d5a73f5f6..71d0e7f74 100644 --- a/crates/rapier3d/tests/multibody_spring.rs +++ b/crates/rapier3d/tests/multibody_spring.rs @@ -19,13 +19,9 @@ fn settle_angle(stiffness: Real, rest: Real, damping: Real, inertia: Real, steps let base = bodies.insert(RigidBodyBuilder::fixed()); // Link rotates in place about the shared origin (COM on the hinge axis), so // the effective joint inertia is exactly `inertia` — no lever-arm term. - let link = bodies.insert( - RigidBodyBuilder::dynamic().additional_mass_properties(MassProperties::new( - Vector::ZERO, - 1.0, - Vector::new(inertia, inertia, inertia), - )), - ); + let link = bodies.insert(RigidBodyBuilder::dynamic().additional_mass_properties( + MassProperties::new(Vector::ZERO, 1.0, Vector::new(inertia, inertia, inertia)), + )); let rev = RevoluteJointBuilder::new(Vector::Z); let handle = multibody_joints.insert(base, link, rev, true).unwrap(); diff --git a/crates/rapier_testbed2d-f64/Cargo.toml b/crates/rapier_testbed2d-f64/Cargo.toml index 516613f71..5f4425b5c 100644 --- a/crates/rapier_testbed2d-f64/Cargo.toml +++ b/crates/rapier_testbed2d-f64/Cargo.toml @@ -34,7 +34,6 @@ clippy = { needless_lifetimes = "allow" } default = ["dim2"] dim2 = [] parallel = ["rapier/parallel", "num_cpus"] -other-backends = ["wrapped2d"] profiler_ui = ["dep:puffin_egui", "profiling/profile-with-puffin"] # See https://github.com/dimforge/rapier/issues/760. unstable-puffin-pr-235 = [] @@ -50,7 +49,6 @@ rand_pcg = "0.10" web-time = { version = "1.1" } bitflags = "2" num_cpus = { version = "1", optional = true } -wrapped2d = { version = "0.4", optional = true } bincode = "1" Inflector = "0.11" md5 = "0.8" diff --git a/crates/rapier_testbed2d/Cargo.toml b/crates/rapier_testbed2d/Cargo.toml index aa4517107..216c73cca 100644 --- a/crates/rapier_testbed2d/Cargo.toml +++ b/crates/rapier_testbed2d/Cargo.toml @@ -34,13 +34,12 @@ clippy = { needless_lifetimes = "allow" } default = ["dim2"] dim2 = [] parallel = ["rapier/parallel", "num_cpus"] -other-backends = ["wrapped2d"] profiler_ui = ["dep:puffin_egui", "profiling/profile-with-puffin"] # See https://github.com/dimforge/rapier/issues/760. unstable-puffin-pr-235 = [] [package.metadata.docs.rs] -features = ["parallel", "other-backends", "profiler_ui"] +features = ["parallel", "profiler_ui"] [dependencies] nalgebra = { version = "0.35", features = ["rand"] } @@ -50,7 +49,6 @@ rand_pcg = "0.10" web-time = { version = "1.1" } bitflags = "2" num_cpus = { version = "1", optional = true } -wrapped2d = { version = "0.4", optional = true } bincode = "1" Inflector = "0.11" md5 = "0.8" diff --git a/crates/rapier_testbed3d-f64/Cargo.toml b/crates/rapier_testbed3d-f64/Cargo.toml index 7cdf64f32..b4c72cdca 100644 --- a/crates/rapier_testbed3d-f64/Cargo.toml +++ b/crates/rapier_testbed3d-f64/Cargo.toml @@ -27,9 +27,6 @@ required-features = ["dim3"] [lints] rust.unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(feature, values("dim2", "f32"))', - # The `other-backends` feature isn't in the tested3d-f64 - # but easier to just ignore it here. - 'cfg(feature, values("other-backends"))', ] } clippy = { needless_lifetimes = "allow" } @@ -61,7 +58,7 @@ serde_json = "1" profiling = "1.0" puffin_egui = { version = "0.29", optional = true } indexmap = { version = "2", features = ["serde"] } -kiss3d = { version = "0.43.0", features = ["egui", "serde"] } +kiss3d = { version = "0.44.0", features = ["egui", "serde"] } log = "0.4" env_logger = "0.11" diff --git a/crates/rapier_testbed3d/Cargo.toml b/crates/rapier_testbed3d/Cargo.toml index 90872ee1d..367c2aed0 100644 --- a/crates/rapier_testbed3d/Cargo.toml +++ b/crates/rapier_testbed3d/Cargo.toml @@ -34,13 +34,12 @@ clippy = { needless_lifetimes = "allow" } default = ["dim3"] dim3 = [] parallel = ["rapier/parallel", "num_cpus"] -other-backends = ["physx", "physx-sys", "glam"] profiler_ui = ["dep:puffin_egui", "profiling/profile-with-puffin"] # See https://github.com/dimforge/rapier/issues/760. unstable-puffin-pr-235 = [] [package.metadata.docs.rs] -features = ["parallel", "other-backends", "profiler_ui"] +features = ["parallel", "profiler_ui"] [dependencies] nalgebra = { version = "0.35", features = ["rand"] } @@ -49,10 +48,7 @@ rand = "0.10" rand_pcg = "0.10" web-time = { version = "1.1" } bitflags = "2" -glam = { version = "0.33", optional = true } # For Physx num_cpus = { version = "1", optional = true } -physx = { version = "0.19", features = ["glam"], optional = true } -physx-sys = { version = "0.11", optional = true } bincode = "1" md5 = "0.8" Inflector = "0.11" diff --git a/examples2d/Cargo.toml b/examples2d/Cargo.toml index 8923d447e..fab9601dd 100644 --- a/examples2d/Cargo.toml +++ b/examples2d/Cargo.toml @@ -10,10 +10,10 @@ publish = false parallel = ["rapier2d/parallel", "rapier_testbed2d/parallel"] simd-stable = ["rapier2d/simd-stable"] simd-nightly = ["rapier2d/simd-nightly"] -other-backends = ["rapier_testbed2d/other-backends"] enhanced-determinism = ["rapier2d/enhanced-determinism"] [dependencies] +anyhow = "1" rand = "0.10" lyon = "0.17" dot_vox = "5" diff --git a/examples2d/add_remove2.rs b/examples2d/add_remove2.rs index c3fcc485d..57041b8b6 100644 --- a/examples2d/add_remove2.rs +++ b/examples2d/add_remove2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { let mut world = PhysicsWorld::new(); let rad = 0.5; @@ -18,54 +18,56 @@ pub fn init_world(testbed: &mut Testbed) { }) .collect::>(); - // Callback that will be executed on the main loop to handle proximities. - testbed.add_callback(move |mut graphics, physics, _, state| { - let rot = -state.time; - for rb_handle in &platform_handles { - let rb = physics.bodies.get_mut(*rb_handle).unwrap(); - rb.set_next_kinematic_rotation(Rotation::new(rot)); - } + /* + * Set up the testbed. + */ + viewer.set_world(&mut world); + viewer.look_at(Vec2::ZERO, 20.0); - if state.timestep_id % 10 == 0 { - let x = rand::random::() * 10.0 - 5.0; - let y = rand::random::() * 10.0 + 10.0; - let rigid_body = RigidBodyBuilder::dynamic().translation(Vector::new(x, y)); - let handle = physics.bodies.insert(rigid_body); - let collider = ColliderBuilder::cuboid(rad, rad); - physics - .colliders - .insert_with_parent(collider, handle, &mut physics.bodies); + let mut step_id = 0usize; + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + step_id += 1; - if let Some(graphics) = &mut graphics { - graphics.add_body(handle, &physics.bodies, &physics.colliders); + let rot = -(step_id as f32 * world.integration_parameters.dt); + for rb_handle in &platform_handles { + let rb = world.bodies.get_mut(*rb_handle).unwrap(); + rb.set_next_kinematic_rotation(Rotation::new(rot)); } - } - let to_remove: Vec<_> = physics - .bodies - .iter() - .filter(|(_, b)| b.position().translation.y < -10.0) - .map(|e| e.0) - .collect(); - for handle in to_remove { - physics.bodies.remove( - handle, - &mut physics.islands, - &mut physics.colliders, - &mut physics.impulse_joints, - &mut physics.multibody_joints, - true, - ); + if step_id.is_multiple_of(10) { + let x = rand::random::() * 10.0 - 5.0; + let y = rand::random::() * 10.0 + 10.0; + let rigid_body = RigidBodyBuilder::dynamic().translation(Vector::new(x, y)); + let handle = world.bodies.insert(rigid_body); + let collider = ColliderBuilder::cuboid(rad, rad); + world + .colliders + .insert_with_parent(collider, handle, &mut world.bodies); - if let Some(graphics) = &mut graphics { - graphics.remove_body(handle); + viewer.add_body(handle, &world); } - } - }); - /* - * Set up the testbed. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::ZERO, 20.0); + let to_remove: Vec<_> = world + .bodies + .iter() + .filter(|(_, b)| b.position().translation.y < -10.0) + .map(|e| e.0) + .collect(); + for handle in to_remove { + world.bodies.remove( + handle, + &mut world.islands, + &mut world.colliders, + &mut world.impulse_joints, + &mut world.multibody_joints, + true, + ); + + viewer.remove_body(handle); + } + } + } + Ok(()) } diff --git a/examples2d/all_examples2.rs b/examples2d/all_examples2.rs index 41fb38919..b01fcbb6c 100644 --- a/examples2d/all_examples2.rs +++ b/examples2d/all_examples2.rs @@ -1,7 +1,9 @@ #![allow(dead_code)] #![allow(clippy::type_complexity)] -use rapier_testbed2d::{Example, TestbedApp}; +use rapier_testbed2d::{ExampleEntry, TestbedViewer}; +use std::future::Future; +use std::pin::Pin; mod add_remove2; mod ccd2; @@ -41,93 +43,103 @@ mod s2d_joint_grid; mod s2d_pyramid; mod sensor2; mod stress_tests; -#[cfg(not(target_arch = "wasm32"))] mod trimesh2; mod voxels2; mod utils; +/// A registered example: a fn pointer running the example's owned loop. +/// (A non-capturing closure coerces to this higher-ranked fn pointer.) +type ExampleFn = + for<'a> fn(&'a mut TestbedViewer) -> Pin> + 'a>>; + +/// `(group, name, run-fn)` -> `(ExampleEntry, ExampleFn)`. +macro_rules! examples { + ($($group:expr, $name:expr, $run:path);* $(;)?) => { + vec![ $( (ExampleEntry::new($group, $name), (|v| Box::pin($run(v))) as ExampleFn) ),* ] + }; +} + #[kiss3d::main] pub async fn main() { const COLLISIONS: &str = "Collisions"; const DYNAMICS: &str = "Dynamics"; - const COMPLEX: &str = "Complex Shapes"; const JOINTS: &str = "Joints"; const CONTROLS: &str = "Controls"; const DEBUG: &str = "Debug"; const S2D: &str = "Inspired by Solver 2D"; + const STRESS: &str = "Stress tests"; - let mut builders: Vec = vec![ - // ── Demos ────────────────────────────────────────────────────────── - Example::new(COLLISIONS, "Add remove", add_remove2::init_world), - Example::new(COLLISIONS, "Drum", drum2::init_world), - Example::new(COLLISIONS, "Inv pyramid", inv_pyramid2::init_world), - Example::new(COLLISIONS, "Platform", platform2::init_world), - Example::new(COLLISIONS, "Pyramid", pyramid2::init_world), - Example::new(COLLISIONS, "Sensor", sensor2::init_world), - Example::new(COLLISIONS, "Convex polygons", convex_polygons2::init_world), - Example::new(COLLISIONS, "Heightfield", heightfield2::init_world), - Example::new(COLLISIONS, "Polyline", polyline2::init_world), - #[cfg(not(target_arch = "wasm32"))] - Example::new(COLLISIONS, "Trimesh", trimesh2::init_world), - Example::new(COLLISIONS, "Voxels", voxels2::init_world), - Example::new( - COLLISIONS, - "Collision groups", - collision_groups2::init_world, - ), - Example::new( - COLLISIONS, - "One-way platforms", - one_way_platforms2::init_world, - ), - // ── Dynamics ────────────────────────────────────────────────────────── - Example::new(DYNAMICS, "Locked rotations", locked_rotations2::init_world), - Example::new(DYNAMICS, "Restitution", restitution2::init_world), - Example::new(DYNAMICS, "Damping", damping2::init_world), - Example::new(DYNAMICS, "CCD", ccd2::init_world), - // ── Joints ───────────────────────────────────────────────────────── - Example::new(JOINTS, "Joints", joints2::init_world), - Example::new(JOINTS, "Rope Joints", rope_joints2::init_world), - Example::new(JOINTS, "Pin Slot Joint", pin_slot_joint2::init_world), - Example::new( - JOINTS, - "Joint motor position", - joint_motor_position2::init_world, - ), - Example::new( - JOINTS, - "Inverse kinematics", - inverse_kinematics2::init_world, - ), - // ── Characters ───────────────────────────────────────────────────── - Example::new( - CONTROLS, - "Character controller", - character_controller2::init_world, - ), - // ── Debug ────────────────────────────────────────────────────────── - Example::new(DEBUG, "Box ball", debug_box_ball2::init_world), - Example::new(DEBUG, "Compression", debug_compression2::init_world), - Example::new(DEBUG, "Intersection", debug_intersection2::init_world), - Example::new(DEBUG, "Total overlap", debug_total_overlap2::init_world), - Example::new(DEBUG, "Vertical column", debug_vertical_column2::init_world), - // ── Demos inspired by Solver2D ─────────────────────────────────────────────────── - Example::new(S2D, "High mass ratio 1", s2d_high_mass_ratio_1::init_world), - Example::new(S2D, "High mass ratio 2", s2d_high_mass_ratio_2::init_world), - Example::new(S2D, "High mass ratio 3", s2d_high_mass_ratio_3::init_world), - Example::new(S2D, "Confined", s2d_confined::init_world), - Example::new(S2D, "Pyramid", s2d_pyramid::init_world), - Example::new(S2D, "Card house", s2d_card_house::init_world), - Example::new(S2D, "Arch", s2d_arch::init_world), - Example::new(S2D, "Bridge", s2d_bridge::init_world), - Example::new(S2D, "Ball and chain", s2d_ball_and_chain::init_world), - Example::new(S2D, "Joint grid", s2d_joint_grid::init_world), - Example::new(S2D, "Far pyramid", s2d_far_pyramid::init_world), + let examples: Vec<(ExampleEntry, ExampleFn)> = examples![ + // ── Collisions ────────────────────────────────────────────────────── + COLLISIONS, "Add remove", add_remove2::run; + COLLISIONS, "Drum", drum2::run; + COLLISIONS, "Inv pyramid", inv_pyramid2::run; + COLLISIONS, "Platform", platform2::run; + COLLISIONS, "Pyramid", pyramid2::run; + COLLISIONS, "Sensor", sensor2::run; + COLLISIONS, "Convex polygons", convex_polygons2::run; + COLLISIONS, "Heightfield", heightfield2::run; + COLLISIONS, "Polyline", polyline2::run; + COLLISIONS, "Trimesh", trimesh2::run; + COLLISIONS, "Voxels", voxels2::run; + COLLISIONS, "Collision groups", collision_groups2::run; + COLLISIONS, "One-way platforms", one_way_platforms2::run; + // ── Dynamics ──────────────────────────────────────────────────────── + DYNAMICS, "Locked rotations", locked_rotations2::run; + DYNAMICS, "Restitution", restitution2::run; + DYNAMICS, "Damping", damping2::run; + DYNAMICS, "CCD", ccd2::run; + // ── Joints ────────────────────────────────────────────────────────── + JOINTS, "Joints", joints2::run; + JOINTS, "Rope Joints", rope_joints2::run; + JOINTS, "Pin Slot Joint", pin_slot_joint2::run; + JOINTS, "Joint motor position", joint_motor_position2::run; + JOINTS, "Inverse kinematics", inverse_kinematics2::run; + // ── Controls ──────────────────────────────────────────────────────── + CONTROLS, "Character controller", character_controller2::run; + // ── Debug ─────────────────────────────────────────────────────────── + DEBUG, "Box ball", debug_box_ball2::run; + DEBUG, "Compression", debug_compression2::run; + DEBUG, "Intersection", debug_intersection2::run; + DEBUG, "Total overlap", debug_total_overlap2::run; + DEBUG, "Vertical column", debug_vertical_column2::run; + // ── Inspired by Solver2D ──────────────────────────────────────────── + S2D, "High mass ratio 1", s2d_high_mass_ratio_1::run; + S2D, "High mass ratio 2", s2d_high_mass_ratio_2::run; + S2D, "High mass ratio 3", s2d_high_mass_ratio_3::run; + S2D, "Confined", s2d_confined::run; + S2D, "Pyramid", s2d_pyramid::run; + S2D, "Card house", s2d_card_house::run; + S2D, "Arch", s2d_arch::run; + S2D, "Bridge", s2d_bridge::run; + S2D, "Ball and chain", s2d_ball_and_chain::run; + S2D, "Joint grid", s2d_joint_grid::run; + S2D, "Far pyramid", s2d_far_pyramid::run; + // ── Stress tests ──────────────────────────────────────────────────── + STRESS, "Balls", stress_tests::balls2::run; + STRESS, "Boxes", stress_tests::boxes2::run; + STRESS, "Capsules", stress_tests::capsules2::run; + STRESS, "Convex polygons", stress_tests::convex_polygons2::run; + STRESS, "Heightfield", stress_tests::heightfield2::run; + STRESS, "Pyramid", stress_tests::pyramid2::run; + STRESS, "Verticals stacks", stress_tests::vertical_stacks2::run; + STRESS, "(Stress test) joint ball", stress_tests::joint_ball2::run; + STRESS, "(Stress test) joint fixed", stress_tests::joint_fixed2::run; + STRESS, "(Stress test) joint prismatic", stress_tests::joint_prismatic2::run; ]; - let mut benches = stress_tests::builders(); - builders.append(&mut benches); - let testbed = TestbedApp::from_builders(builders); - testbed.run().await + let (entries, run_fns): (Vec<_>, Vec) = examples.into_iter().unzip(); + let mut viewer = TestbedViewer::new(entries).await; + + loop { + viewer.clear_scene(); + let idx = viewer.selected(); + if let Err(e) = run_fns[idx](&mut viewer).await { + eprintln!("example #{idx} failed: {e:?}"); + } + if viewer.quitting() { + break; + } + } } diff --git a/examples2d/ccd2.rs b/examples2d/ccd2.rs index 44b6f75a0..8656254c8 100644 --- a/examples2d/ccd2.rs +++ b/examples2d/ccd2.rs @@ -1,8 +1,8 @@ use kiss3d::color::Color; -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -80,42 +80,49 @@ pub fn init_world(testbed: &mut Testbed) { } } - // Callback that will be executed on the main loop to handle proximities. - testbed.add_callback(move |mut graphics, physics, events, _| { - while let Ok(prox) = events.collision_events.try_recv() { - let color = if prox.started() { - Color::new(1.0, 1.0, 0.0, 1.0) - } else { - Color::new(0.5, 0.5, 1.0, 1.0) - }; - - let parent_handle1 = physics - .colliders - .get(prox.collider1()) - .unwrap() - .parent() - .unwrap(); - let parent_handle2 = physics - .colliders - .get(prox.collider2()) - .unwrap() - .parent() - .unwrap(); - if let Some(graphics) = &mut graphics { + /* + * Set up the testbed. + */ + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 2.5), 20.0); + + let (collision_send, collision_recv) = std::sync::mpsc::channel(); + let (contact_force_send, _contact_force_recv) = std::sync::mpsc::channel(); + let event_handler = ChannelEventCollector::new(collision_send, contact_force_send); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step_with_events(&(), &event_handler); + + // Handle proximities. + while let Ok(prox) = collision_recv.try_recv() { + let color = if prox.started() { + Color::new(1.0, 1.0, 0.0, 1.0) + } else { + Color::new(0.5, 0.5, 1.0, 1.0) + }; + + let parent_handle1 = world + .colliders + .get(prox.collider1()) + .unwrap() + .parent() + .unwrap(); + let parent_handle2 = world + .colliders + .get(prox.collider2()) + .unwrap() + .parent() + .unwrap(); if parent_handle1 != ground_handle && prox.collider1() != sensor_handle { - graphics.set_body_color(parent_handle1, color, false); + viewer.set_body_color(parent_handle1, color, false); } if parent_handle2 != ground_handle && prox.collider2() != sensor_handle { - graphics.set_body_color(parent_handle2, color, false); + viewer.set_body_color(parent_handle2, color, false); } } } - }); - - /* - * Set up the testbed. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 2.5), 20.0); + } + Ok(()) } diff --git a/examples2d/character_controller2.rs b/examples2d/character_controller2.rs index 6b529008c..8041fdaad 100644 --- a/examples2d/character_controller2.rs +++ b/examples2d/character_controller2.rs @@ -1,12 +1,12 @@ use crate::utils::character; use crate::utils::character::CharacterControlMode; use kiss3d::color::Color; -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::control::{KinematicCharacterController, PidController}; use rapier2d::prelude::*; use std::f32::consts::PI; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -33,7 +33,7 @@ pub fn init_world(testbed: &mut Testbed) { .soft_ccd_prediction(10.0); let collider = ColliderBuilder::capsule_y(0.3, 0.15); let (character_handle, _) = world.insert(rigid_body, collider); - testbed.set_initial_body_color(character_handle, Color::new(0.8, 0.1, 0.1, 1.0)); + viewer.set_initial_body_color(character_handle, Color::new(0.8, 0.1, 0.1, 1.0)); /* * Create the cubes @@ -143,24 +143,6 @@ pub fn init_world(testbed: &mut Testbed) { let joint = RevoluteJointBuilder::new().limits([-0.3, 0.3]); world.insert_impulse_joint(ground_handle, handle, joint); - /* - * Setup a callback to move the platform. - */ - testbed.add_callback(move |_, physics, _, run_state| { - let linvel = Vector::new( - (run_state.time * 2.0).sin() * 2.0, - (run_state.time * 5.0).sin() * 1.5, - ); - // let angvel = run_state.time.sin() * 0.5; - - // Update the velocity-based kinematic body by setting its velocity. - if let Some(platform) = physics.bodies.get_mut(platform_handle) { - platform.set_linvel(linvel, true); - // NOTE: interaction with rotating platforms isn't handled very well yet. - // platform.set_angvel(angvel, true); - } - }); - /* * Callback to update the character based on user inputs. */ @@ -173,22 +155,43 @@ pub fn init_world(testbed: &mut Testbed) { }; let mut pid = PidController::default(); - testbed.add_callback(move |graphics, physics, _, _| { - if let Some(graphics) = graphics { + /* + * Set up the testbed. + */ + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 1.0), 100.0); + + let mut step_id = 0usize; + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + step_id += 1; + + // Move the platform. + let linvel = Vector::new( + ((step_id as f32 * world.integration_parameters.dt) * 2.0).sin() * 2.0, + ((step_id as f32 * world.integration_parameters.dt) * 5.0).sin() * 1.5, + ); + // let angvel = (step_id as f32 * world.integration_parameters.dt as f32).sin() * 0.5; + + // Update the velocity-based kinematic body by setting its velocity. + if let Some(platform) = world.bodies.get_mut(platform_handle) { + platform.set_linvel(linvel, true); + // NOTE: interaction with rotating platforms isn't handled very well yet. + // platform.set_angvel(angvel, true); + } + + // Update the character based on user inputs. character::update_character( - graphics, - physics, + viewer, + &mut world, &mut control_mode, &mut controller, &mut pid, character_handle, ); } - }); + } - /* - * Set up the testbed. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 1.0), 100.0); + Ok(()) } diff --git a/examples2d/collision_groups2.rs b/examples2d/collision_groups2.rs index d543698ab..53e043ede 100644 --- a/examples2d/collision_groups2.rs +++ b/examples2d/collision_groups2.rs @@ -1,8 +1,8 @@ use kiss3d::color::{BLUE, GREEN}; -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -34,7 +34,7 @@ pub fn init_world(testbed: &mut Testbed) { .collision_groups(GREEN_GROUP); let green_collider_handle = world.insert_collider(green_floor, Some(floor_handle)); - testbed.set_initial_collider_color(green_collider_handle, GREEN); + viewer.set_initial_collider_color(green_collider_handle, GREEN); /* * A blue floor that will collide with the BLUE group only. @@ -44,7 +44,7 @@ pub fn init_world(testbed: &mut Testbed) { .collision_groups(BLUE_GROUP); let blue_collider_handle = world.insert_collider(blue_floor, Some(floor_handle)); - testbed.set_initial_collider_color(blue_collider_handle, BLUE); + viewer.set_initial_collider_color(blue_collider_handle, BLUE); /* * Create the cubes @@ -72,13 +72,20 @@ pub fn init_world(testbed: &mut Testbed) { let collider = ColliderBuilder::cuboid(rad, rad).collision_groups(group); let (handle, _) = world.insert(rigid_body, collider); - testbed.set_initial_body_color(handle, color); + viewer.set_initial_body_color(handle, color); } } /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 1.0), 100.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 1.0), 100.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/convex_polygons2.rs b/examples2d/convex_polygons2.rs index ea8c350f3..f5679002a 100644 --- a/examples2d/convex_polygons2.rs +++ b/examples2d/convex_polygons2.rs @@ -1,9 +1,9 @@ use rand::distr::{Distribution, StandardUniform}; use rand::{SeedableRng, rngs::StdRng}; -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -64,8 +64,15 @@ pub fn init_world(testbed: &mut Testbed) { } /* - * Set up the testbed. + * Set up the viewer. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 50.0), 10.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 50.0), 10.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/damping2.rs b/examples2d/damping2.rs index 480b25856..4f952da4d 100644 --- a/examples2d/damping2.rs +++ b/examples2d/damping2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -34,6 +34,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(3.0, 2.0), 50.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(3.0, 2.0), 50.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/debug_box_ball2.rs b/examples2d/debug_box_ball2.rs index 710603d88..f30e51a54 100644 --- a/examples2d/debug_box_ball2.rs +++ b/examples2d/debug_box_ball2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -27,6 +27,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::ZERO, 50.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::ZERO, 50.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/debug_compression2.rs b/examples2d/debug_compression2.rs index 5138af518..27770e310 100644 --- a/examples2d/debug_compression2.rs +++ b/examples2d/debug_compression2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -47,23 +47,28 @@ pub fn init_world(testbed: &mut Testbed) { let mut force = Vector::new(0.0, 0.0); - testbed.add_callback(move |_, physics, _, _| { - let left_plank = &mut physics.bodies[handles[0]]; - left_plank.reset_forces(true); - left_plank.add_force(force, true); + /* + * Set up the testbed. + */ + viewer.set_world(&mut world); + viewer.look_at(Vec2::ZERO, 50.0); - let right_plank = &mut physics.bodies[handles[1]]; - right_plank.reset_forces(true); - right_plank.add_force(-force, true); + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); - force.x += 10000.0; + let left_plank = &mut world.bodies[handles[0]]; + left_plank.reset_forces(true); + left_plank.add_force(force, true); - println!("force: {}", force.x); - }); + let right_plank = &mut world.bodies[handles[1]]; + right_plank.reset_forces(true); + right_plank.add_force(-force, true); - /* - * Set up the testbed. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::ZERO, 50.0); + force.x += 10000.0; + + println!("force: {}", force.x); + } + } + Ok(()) } diff --git a/examples2d/debug_intersection2.rs b/examples2d/debug_intersection2.rs index 10c2b7027..6770480b5 100644 --- a/examples2d/debug_intersection2.rs +++ b/examples2d/debug_intersection2.rs @@ -1,8 +1,8 @@ use kiss3d::color::{Color, GREY, RED}; -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -19,7 +19,7 @@ pub fn init_world(testbed: &mut Testbed) { (y as f32 - count as f32 / 2.0) * rad * 3.0, )); let (handle, _) = world.insert(rigid_body, collider.clone()); - testbed.set_initial_body_color( + viewer.set_initial_body_color( handle, Color::new( x as f32 / count as f32, @@ -34,33 +34,46 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::ZERO, 50.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::ZERO, 50.0); - testbed.add_callback(move |mut graphics, physics, _, run| { - let slow_time = run.timestep_id as f32 / 3.0; + let mut step_id = 0usize; + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + step_id += 1; - let query_pipeline = physics.broad_phase.as_query_pipeline( - physics.narrow_phase.query_dispatcher(), - &physics.bodies, - &physics.colliders, - QueryFilter::default(), - ); + let slow_time = step_id as f32 / 3.0; - for intersection in query_pipeline.intersect_shape( - Pose::from_translation(Vector::new(slow_time.cos() * 10.0, slow_time.sin() * 10.0)), - &Ball::new(rad / 2.0), - ) { - if let Some(graphics) = graphics.as_deref_mut() { - for (handle, _) in physics.bodies.iter() { - graphics.set_body_color(handle, GREY, false); + let query_pipeline = world.broad_phase.as_query_pipeline( + world.narrow_phase.query_dispatcher(), + &world.bodies, + &world.colliders, + QueryFilter::default(), + ); + + let ball = Ball::new(rad / 2.0); + let intersections: Vec<_> = query_pipeline + .intersect_shape( + Pose::from_translation(Vector::new( + slow_time.cos() * 10.0, + slow_time.sin() * 10.0, + )), + &ball, + ) + .collect(); + + for intersection in intersections { + for (handle, _) in world.bodies.iter() { + viewer.set_body_color(handle, GREY, false); } - let collider = physics.colliders.get(intersection.0).unwrap(); + let collider = world.colliders.get(intersection.0).unwrap(); let body_handle = collider.parent().unwrap(); - graphics.set_body_color(body_handle, RED, false); + viewer.set_body_color(body_handle, RED, false); } } - }); + } + Ok(()) } diff --git a/examples2d/debug_total_overlap2.rs b/examples2d/debug_total_overlap2.rs index d53d4b0f5..600f9453b 100644 --- a/examples2d/debug_total_overlap2.rs +++ b/examples2d/debug_total_overlap2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -19,6 +19,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::ZERO, 50.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::ZERO, 50.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/debug_vertical_column2.rs b/examples2d/debug_vertical_column2.rs index a2e60a750..ac662cdbf 100644 --- a/examples2d/debug_vertical_column2.rs +++ b/examples2d/debug_vertical_column2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -36,7 +36,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - // testbed.harness_mut().physics.gravity.y = -981.0; - testbed.look_at(Vec2::new(0.0, 2.5), 5.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 2.5), 5.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/drum2.rs b/examples2d/drum2.rs index 2114b896d..aa9f790c2 100644 --- a/examples2d/drum2.rs +++ b/examples2d/drum2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -57,19 +57,20 @@ pub fn init_world(testbed: &mut Testbed) { world.insert_collider(collider, Some(velocity_based_platform_handle)); } - /* - * Setup a callback to control the platform. - */ - testbed.add_callback(move |_, physics, _, _| { - // Update the velocity-based kinematic body by setting its velocity. - if let Some(platform) = physics.bodies.get_mut(velocity_based_platform_handle) { - platform.set_angvel(-0.15, true); - } - }); - /* * Run the simulation. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 1.0), 40.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 1.0), 40.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + // Update the velocity-based kinematic body by setting its velocity. + if let Some(platform) = world.bodies.get_mut(velocity_based_platform_handle) { + platform.set_angvel(-0.15, true); + } + } + } + Ok(()) } diff --git a/examples2d/heightfield2.rs b/examples2d/heightfield2.rs index f23736431..bbe6232bc 100644 --- a/examples2d/heightfield2.rs +++ b/examples2d/heightfield2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -58,6 +58,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::ZERO, 10.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::ZERO, 10.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/inv_pyramid2.rs b/examples2d/inv_pyramid2.rs index f2ed2cff0..ff73d9072 100644 --- a/examples2d/inv_pyramid2.rs +++ b/examples2d/inv_pyramid2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -37,6 +37,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 2.5), 20.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 2.5), 20.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/inverse_kinematics2.rs b/examples2d/inverse_kinematics2.rs index 99a7aaccc..5450a049e 100644 --- a/examples2d/inverse_kinematics2.rs +++ b/examples2d/inverse_kinematics2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -50,43 +50,47 @@ pub fn init_world(testbed: &mut Testbed) { let mut displacements = DVector::zeros(0); - testbed.add_callback(move |graphics, physics, _, _| { - let Some(graphics) = graphics else { return }; - if let Some((multibody, link_id)) = physics.multibody_joints.get_mut(last_link) { - // Ensure our displacement vector has the right number of elements. - if displacements.nrows() < multibody.ndofs() { - displacements = DVector::zeros(multibody.ndofs()); - } else { - displacements.fill(0.0); - } - - let Some(mouse_point) = graphics.mouse().point else { - return; - }; - - // We will have the endpoint track the mouse position. - let target_point = Vector::new(mouse_point.x, mouse_point.y); - - let options = InverseKinematicsOption { - constrained_axes: JointAxesMask::LIN_AXES, - ..Default::default() - }; - - multibody.inverse_kinematics( - &physics.bodies, - link_id, - &options, - &Pose::from_translation(target_point), - |_| true, - &mut displacements, - ); - multibody.apply_displacements(displacements.as_slice()); - } - }); - /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::ZERO, 300.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::ZERO, 300.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + + if let Some((multibody, link_id)) = world.multibody_joints.get_mut(last_link) { + // Ensure our displacement vector has the right number of elements. + if displacements.nrows() < multibody.ndofs() { + displacements = DVector::zeros(multibody.ndofs()); + } else { + displacements.fill(0.0); + } + + let Some(mouse_point) = viewer.mouse().point else { + continue; + }; + + // We will have the endpoint track the mouse position. + let target_point = Vector::new(mouse_point.x, mouse_point.y); + + let options = InverseKinematicsOption { + constrained_axes: JointAxesMask::LIN_AXES, + ..Default::default() + }; + + multibody.inverse_kinematics( + &world.bodies, + link_id, + &options, + &Pose::from_translation(target_point), + |_| true, + &mut displacements, + ); + multibody.apply_displacements(displacements.as_slice()); + } + } + } + Ok(()) } diff --git a/examples2d/joint_motor_position2.rs b/examples2d/joint_motor_position2.rs index e5308ce2a..9af199fc2 100644 --- a/examples2d/joint_motor_position2.rs +++ b/examples2d/joint_motor_position2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -63,6 +63,13 @@ pub fn init_world(testbed: &mut Testbed) { * Set up the testbed. */ world.gravity = Vector::new(0.0, 0.0); - testbed.set_physics_world(world); - testbed.look_at(Vec2::ZERO, 40.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::ZERO, 40.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/joints2.rs b/examples2d/joints2.rs index e2be4d3cb..f6d321993 100644 --- a/examples2d/joints2.rs +++ b/examples2d/joints2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -10,7 +10,7 @@ pub fn init_world(testbed: &mut Testbed) { /* * Enable/disable softness. */ - let settings = testbed.example_settings_mut(); + let settings = viewer.example_settings_mut(); let variable_softness = settings.get_or_set_bool("Variable softness", false); /* @@ -79,6 +79,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(numk as f32 * rad, numi as f32 * -rad), 20.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(numk as f32 * rad, numi as f32 * -rad), 20.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/locked_rotations2.rs b/examples2d/locked_rotations2.rs index fc41a5bbf..d62c23f03 100644 --- a/examples2d/locked_rotations2.rs +++ b/examples2d/locked_rotations2.rs @@ -1,10 +1,10 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; // This shows a bug when a cylinder is in contact with a very large // but very thin cuboid. In this case the EPA returns an incorrect // contact normal, resulting in the cylinder falling through the floor. -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -42,6 +42,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::ZERO, 40.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::ZERO, 40.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/one_way_platforms2.rs b/examples2d/one_way_platforms2.rs index d72665f33..da38c56ee 100644 --- a/examples2d/one_way_platforms2.rs +++ b/examples2d/one_way_platforms2.rs @@ -1,4 +1,4 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; struct OneWayPlatformHook { @@ -53,7 +53,7 @@ impl PhysicsHooks for OneWayPlatformHook { } } -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -81,37 +81,42 @@ pub fn init_world(testbed: &mut Testbed) { }; /* - * Spawn cubes at regular intervals and apply a custom gravity - * depending on their position. + * Set up the testbed. */ - testbed.add_callback(move |graphics, physics, _, run_state| { - if run_state.timestep_id % 200 == 0 && physics.bodies.len() <= 7 { - // Spawn a new cube. - let collider = ColliderBuilder::cuboid(1.5, 2.0); - let body = RigidBodyBuilder::dynamic().translation(Vector::new(20.0, 10.0)); - let handle = physics.bodies.insert(body); - physics - .colliders - .insert_with_parent(collider, handle, &mut physics.bodies); + viewer.set_world(&mut world); + viewer.look_at(Vec2::ZERO, 20.0); + + let mut step_id = 0usize; + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step_with_events(&physics_hooks, &()); + step_id += 1; + + /* + * Spawn cubes at regular intervals and apply a custom gravity + * depending on their position. + */ + if step_id.is_multiple_of(200) && world.bodies.len() <= 7 { + // Spawn a new cube. + let collider = ColliderBuilder::cuboid(1.5, 2.0); + let body = RigidBodyBuilder::dynamic().translation(Vector::new(20.0, 10.0)); + let handle = world.bodies.insert(body); + world + .colliders + .insert_with_parent(collider, handle, &mut world.bodies); - if let Some(graphics) = graphics { - graphics.add_body(handle, &physics.bodies, &physics.colliders); + viewer.add_body(handle, &world); } - } - for handle in physics.islands.active_bodies() { - let body = &mut physics.bodies[handle]; - if body.position().translation.y > 1.0 { - body.set_gravity_scale(1.0, false); - } else if body.position().translation.y < -1.0 { - body.set_gravity_scale(-1.0, false); + for handle in world.islands.active_bodies() { + let body = &mut world.bodies[handle]; + if body.position().translation.y > 1.0 { + body.set_gravity_scale(1.0, false); + } else if body.position().translation.y < -1.0 { + body.set_gravity_scale(-1.0, false); + } } } - }); - - /* - * Set up the testbed. - */ - testbed.set_physics_world_with_hooks(world, physics_hooks); - testbed.look_at(Vec2::ZERO, 20.0); + } + Ok(()) } diff --git a/examples2d/pin_slot_joint2.rs b/examples2d/pin_slot_joint2.rs index 265b39254..251e21b57 100644 --- a/examples2d/pin_slot_joint2.rs +++ b/examples2d/pin_slot_joint2.rs @@ -1,10 +1,10 @@ use crate::utils::character; use crate::utils::character::CharacterControlMode; -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::control::{KinematicCharacterController, PidController}; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -73,22 +73,24 @@ pub fn init_world(testbed: &mut Testbed) { let mut controller = KinematicCharacterController::default(); let mut pid = PidController::default(); - testbed.add_callback(move |graphics, physics, _, _| { - if let Some(graphics) = graphics { + /* + * Set up the testbed. + */ + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 1.0), 100.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); character::update_character( - graphics, - physics, + viewer, + &mut world, &mut control_mode, &mut controller, &mut pid, character_handle, ); } - }); - - /* - * Set up the testbed. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 1.0), 100.0); + } + Ok(()) } diff --git a/examples2d/platform2.rs b/examples2d/platform2.rs index d96b70e02..61fd38379 100644 --- a/examples2d/platform2.rs +++ b/examples2d/platform2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -56,27 +56,34 @@ pub fn init_world(testbed: &mut Testbed) { let (position_based_platform_handle, _) = world.insert(platform_body, collider); /* - * Setup a callback to control the platform. + * Run the simulation. */ - testbed.add_callback(move |_, physics, _, run_state| { - let velocity = Vector::new(run_state.time.sin() * 5.0, (run_state.time * 5.0).sin()); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 1.0), 40.0); - // Update the velocity-based kinematic body by setting its velocity. - if let Some(platform) = physics.bodies.get_mut(velocity_based_platform_handle) { - platform.set_linvel(velocity, true); - } + let mut step_id = 0usize; + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + step_id += 1; - // Update the position-based kinematic body by setting its next position. - if let Some(platform) = physics.bodies.get_mut(position_based_platform_handle) { - let mut next_tra = platform.translation(); - next_tra += velocity * physics.integration_parameters.dt; - platform.set_next_kinematic_translation(next_tra); - } - }); + let velocity = Vector::new( + (step_id as f32 * world.integration_parameters.dt).sin() * 5.0, + ((step_id as f32 * world.integration_parameters.dt) * 5.0).sin(), + ); - /* - * Run the simulation. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 1.0), 40.0); + // Update the velocity-based kinematic body by setting its velocity. + if let Some(platform) = world.bodies.get_mut(velocity_based_platform_handle) { + platform.set_linvel(velocity, true); + } + + // Update the position-based kinematic body by setting its next position. + if let Some(platform) = world.bodies.get_mut(position_based_platform_handle) { + let mut next_tra = platform.translation(); + next_tra += velocity * world.integration_parameters.dt; + platform.set_next_kinematic_translation(next_tra); + } + } + } + Ok(()) } diff --git a/examples2d/polyline2.rs b/examples2d/polyline2.rs index f21aba479..9641d24da 100644 --- a/examples2d/polyline2.rs +++ b/examples2d/polyline2.rs @@ -1,8 +1,8 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::na::ComplexField; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -59,6 +59,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::ZERO, 10.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::ZERO, 10.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/pyramid2.rs b/examples2d/pyramid2.rs index 342d80bd3..498c4569e 100644 --- a/examples2d/pyramid2.rs +++ b/examples2d/pyramid2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -44,6 +44,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 2.5), 20.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 2.5), 20.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/restitution2.rs b/examples2d/restitution2.rs index 4dcfb8d1f..4b60f22ae 100644 --- a/examples2d/restitution2.rs +++ b/examples2d/restitution2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -33,6 +33,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 1.0), 25.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 1.0), 25.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/rope_joints2.rs b/examples2d/rope_joints2.rs index e8939688d..82f93988a 100644 --- a/examples2d/rope_joints2.rs +++ b/examples2d/rope_joints2.rs @@ -1,10 +1,10 @@ use crate::utils::character; use crate::utils::character::CharacterControlMode; -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::control::{KinematicCharacterController, PidController}; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -58,22 +58,26 @@ pub fn init_world(testbed: &mut Testbed) { let mut controller = KinematicCharacterController::default(); let mut pid = PidController::default(); - testbed.add_callback(move |graphics, physics, _, _| { - if let Some(graphics) = graphics { + /* + * Set up the testbed. + */ + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 1.0), 100.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + character::update_character( - graphics, - physics, + viewer, + &mut world, &mut control_mode, &mut controller, &mut pid, character_handle, ); } - }); + } - /* - * Set up the testbed. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 1.0), 100.0); + Ok(()) } diff --git a/examples2d/s2d_arch.rs b/examples2d/s2d_arch.rs index ccba54c09..2c539e27e 100644 --- a/examples2d/s2d_arch.rs +++ b/examples2d/s2d_arch.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -98,6 +98,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 2.5), 20.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 2.5), 20.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/s2d_ball_and_chain.rs b/examples2d/s2d_ball_and_chain.rs index e3536db2b..f9768de74 100644 --- a/examples2d/s2d_ball_and_chain.rs +++ b/examples2d/s2d_ball_and_chain.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -71,6 +71,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 2.5), 20.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 2.5), 20.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/s2d_bridge.rs b/examples2d/s2d_bridge.rs index 318743271..372d6f687 100644 --- a/examples2d/s2d_bridge.rs +++ b/examples2d/s2d_bridge.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -55,6 +55,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 2.5), 20.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 2.5), 20.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/s2d_card_house.rs b/examples2d/s2d_card_house.rs index 953796874..291f12ca7 100644 --- a/examples2d/s2d_card_house.rs +++ b/examples2d/s2d_card_house.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -69,6 +69,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 2.5), 20.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 2.5), 20.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/s2d_confined.rs b/examples2d/s2d_confined.rs index 7ddf6ac72..5760c6191 100644 --- a/examples2d/s2d_confined.rs +++ b/examples2d/s2d_confined.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -72,6 +72,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 2.5), 20.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 2.5), 20.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/s2d_far_pyramid.rs b/examples2d/s2d_far_pyramid.rs index 03f5ff3a2..ba6151846 100644 --- a/examples2d/s2d_far_pyramid.rs +++ b/examples2d/s2d_far_pyramid.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -40,6 +40,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(origin.x + 0.0, origin.y + 2.5), 20.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(origin.x + 0.0, origin.y + 2.5), 20.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/s2d_high_mass_ratio_1.rs b/examples2d/s2d_high_mass_ratio_1.rs index 7471222da..11713ad97 100644 --- a/examples2d/s2d_high_mass_ratio_1.rs +++ b/examples2d/s2d_high_mass_ratio_1.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -55,6 +55,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 2.5), 20.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 2.5), 20.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/s2d_high_mass_ratio_2.rs b/examples2d/s2d_high_mass_ratio_2.rs index 2a1117b15..ce3d5db25 100644 --- a/examples2d/s2d_high_mass_ratio_2.rs +++ b/examples2d/s2d_high_mass_ratio_2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -44,6 +44,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 2.5), 20.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 2.5), 20.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/s2d_high_mass_ratio_3.rs b/examples2d/s2d_high_mass_ratio_3.rs index e6a5cc137..0fcb2f65b 100644 --- a/examples2d/s2d_high_mass_ratio_3.rs +++ b/examples2d/s2d_high_mass_ratio_3.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -38,6 +38,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 2.5), 20.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 2.5), 20.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/s2d_joint_grid.rs b/examples2d/s2d_joint_grid.rs index 233c17bfa..b440ecee1 100644 --- a/examples2d/s2d_joint_grid.rs +++ b/examples2d/s2d_joint_grid.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -54,6 +54,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 2.5), 20.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 2.5), 20.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/s2d_pyramid.rs b/examples2d/s2d_pyramid.rs index 4b4bf5c59..2dec73a67 100644 --- a/examples2d/s2d_pyramid.rs +++ b/examples2d/s2d_pyramid.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -18,7 +18,7 @@ pub fn init_world(testbed: &mut Testbed) { * Create the cubes */ const BASE_COUNT_SETTING: &str = "# of basis cubes"; - let settings = testbed.example_settings_mut(); + let settings = viewer.example_settings_mut(); let base_count = settings.get_or_set_u32(BASE_COUNT_SETTING, 100, 2..=200); let h = 0.5; @@ -39,6 +39,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 2.5), 20.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 2.5), 20.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/sensor2.rs b/examples2d/sensor2.rs index 848cdb173..3bbe2bd6a 100644 --- a/examples2d/sensor2.rs +++ b/examples2d/sensor2.rs @@ -1,8 +1,8 @@ use kiss3d::color::Color; -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -36,7 +36,7 @@ pub fn init_world(testbed: &mut Testbed) { let collider = ColliderBuilder::cuboid(rad, rad); let (handle, _) = world.insert(rigid_body, collider); - testbed.set_initial_body_color(handle, Color::new(0.5, 0.5, 1.0, 1.0)); + viewer.set_initial_body_color(handle, Color::new(0.5, 0.5, 1.0, 1.0)); } /* @@ -59,34 +59,40 @@ pub fn init_world(testbed: &mut Testbed) { .active_events(ActiveEvents::COLLISION_EVENTS); world.insert_collider(sensor_collider, Some(sensor_handle)); - testbed.set_initial_body_color(sensor_handle, Color::new(0.5, 1.0, 1.0, 1.0)); + viewer.set_initial_body_color(sensor_handle, Color::new(0.5, 1.0, 1.0, 1.0)); - // Callback that will be executed on the main loop to handle proximities. - testbed.add_callback(move |mut graphics, physics, events, _| { - while let Ok(prox) = events.collision_events.try_recv() { - let color = if prox.started() { - Color::new(1.0, 1.0, 0.0, 1.0) - } else { - Color::new(0.5, 0.5, 1.0, 1.0) - }; - - let parent_handle1 = physics.colliders[prox.collider1()].parent().unwrap(); - let parent_handle2 = physics.colliders[prox.collider2()].parent().unwrap(); + /* + * Set up the testbed. + */ + viewer.set_world(&mut world); + let (collision_send, collision_recv) = std::sync::mpsc::channel(); + let (contact_force_send, _contact_force_recv) = std::sync::mpsc::channel(); + let event_handler = ChannelEventCollector::new(collision_send, contact_force_send); + viewer.look_at(Vec2::new(0.0, 1.0), 100.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step_with_events(&(), &event_handler); + + // Callback that handles proximities. + while let Ok(prox) = collision_recv.try_recv() { + let color = if prox.started() { + Color::new(1.0, 1.0, 0.0, 1.0) + } else { + Color::new(0.5, 0.5, 1.0, 1.0) + }; + + let parent_handle1 = world.colliders[prox.collider1()].parent().unwrap(); + let parent_handle2 = world.colliders[prox.collider2()].parent().unwrap(); - if let Some(graphics) = &mut graphics { if parent_handle1 != ground_handle && parent_handle1 != sensor_handle { - graphics.set_body_color(parent_handle1, color, false); + viewer.set_body_color(parent_handle1, color, false); } if parent_handle2 != ground_handle && parent_handle2 != sensor_handle { - graphics.set_body_color(parent_handle2, color, false); + viewer.set_body_color(parent_handle2, color, false); } } } - }); - - /* - * Set up the testbed. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 1.0), 100.0); + } + Ok(()) } diff --git a/examples2d/stress_tests/balls2.rs b/examples2d/stress_tests/balls2.rs index 77c83feb1..9e9eda1cf 100644 --- a/examples2d/stress_tests/balls2.rs +++ b/examples2d/stress_tests/balls2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -54,6 +54,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 2.5), 5.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 2.5), 5.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/stress_tests/boxes2.rs b/examples2d/stress_tests/boxes2.rs index adc843d7d..3266c3351 100644 --- a/examples2d/stress_tests/boxes2.rs +++ b/examples2d/stress_tests/boxes2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -51,8 +51,15 @@ pub fn init_world(testbed: &mut Testbed) { } /* - * Set up the testbed. + * Set up the viewer. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 50.0), 10.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 50.0), 10.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/stress_tests/capsules2.rs b/examples2d/stress_tests/capsules2.rs index 7eb11f6fc..cb5d21a6f 100644 --- a/examples2d/stress_tests/capsules2.rs +++ b/examples2d/stress_tests/capsules2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -55,6 +55,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 50.0), 10.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 50.0), 10.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/stress_tests/convex_polygons2.rs b/examples2d/stress_tests/convex_polygons2.rs index 3bc013f55..4ac8b3627 100644 --- a/examples2d/stress_tests/convex_polygons2.rs +++ b/examples2d/stress_tests/convex_polygons2.rs @@ -1,9 +1,9 @@ use rand::distr::{Distribution, StandardUniform}; use rand::{SeedableRng, rngs::StdRng}; -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -66,6 +66,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 50.0), 10.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 50.0), 10.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/stress_tests/heightfield2.rs b/examples2d/stress_tests/heightfield2.rs index 15a9f9a35..393403a9f 100644 --- a/examples2d/stress_tests/heightfield2.rs +++ b/examples2d/stress_tests/heightfield2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -56,8 +56,15 @@ pub fn init_world(testbed: &mut Testbed) { } /* - * Set up the testbed. + * Set up the viewer. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 50.0), 10.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 50.0), 10.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/stress_tests/joint_ball2.rs b/examples2d/stress_tests/joint_ball2.rs index 704a6afec..32bcddaf4 100644 --- a/examples2d/stress_tests/joint_ball2.rs +++ b/examples2d/stress_tests/joint_ball2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -54,8 +54,15 @@ pub fn init_world(testbed: &mut Testbed) { } /* - * Set up the testbed. + * Set up the viewer. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(numk as f32 * rad, numi as f32 * -rad), 5.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(numk as f32 * rad, numi as f32 * -rad), 5.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/stress_tests/joint_fixed2.rs b/examples2d/stress_tests/joint_fixed2.rs index 6b8e07569..c3bcbf1d2 100644 --- a/examples2d/stress_tests/joint_fixed2.rs +++ b/examples2d/stress_tests/joint_fixed2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -65,6 +65,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(50.0, 50.0), 5.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(50.0, 50.0), 5.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/stress_tests/joint_prismatic2.rs b/examples2d/stress_tests/joint_prismatic2.rs index 9de2e658b..6abe7326c 100644 --- a/examples2d/stress_tests/joint_prismatic2.rs +++ b/examples2d/stress_tests/joint_prismatic2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -51,6 +51,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(80.0, 80.0), 15.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(80.0, 80.0), 15.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/stress_tests/mod.rs b/examples2d/stress_tests/mod.rs index de8c3a87f..3736fdb83 100644 --- a/examples2d/stress_tests/mod.rs +++ b/examples2d/stress_tests/mod.rs @@ -1,37 +1,12 @@ -use rapier_testbed2d::Example; - -mod balls2; -mod boxes2; -mod capsules2; -mod convex_polygons2; -mod heightfield2; -mod joint_ball2; -mod joint_fixed2; -mod joint_prismatic2; -mod pyramid2; -mod vertical_stacks2; - -pub fn builders() -> Vec { - const STRESS: &str = "Stress Tests"; - - vec![ - Example::new(STRESS, "Balls", balls2::init_world), - Example::new(STRESS, "Boxes", boxes2::init_world), - Example::new(STRESS, "Capsules", capsules2::init_world), - Example::new(STRESS, "Convex polygons", convex_polygons2::init_world), - Example::new(STRESS, "Heightfield", heightfield2::init_world), - Example::new(STRESS, "Pyramid", pyramid2::init_world), - Example::new(STRESS, "Verticals stacks", vertical_stacks2::init_world), - Example::new(STRESS, "(Stress test) joint ball", joint_ball2::init_world), - Example::new( - STRESS, - "(Stress test) joint fixed", - joint_fixed2::init_world, - ), - Example::new( - STRESS, - "(Stress test) joint prismatic", - joint_prismatic2::init_world, - ), - ] -} +// Stress-test scenes. The registry (group/name) lives in the main file; this +// module just exposes each scene's `run` entry point. +pub mod balls2; +pub mod boxes2; +pub mod capsules2; +pub mod convex_polygons2; +pub mod heightfield2; +pub mod joint_ball2; +pub mod joint_fixed2; +pub mod joint_prismatic2; +pub mod pyramid2; +pub mod vertical_stacks2; diff --git a/examples2d/stress_tests/pyramid2.rs b/examples2d/stress_tests/pyramid2.rs index b5b5bbf40..52bed1292 100644 --- a/examples2d/stress_tests/pyramid2.rs +++ b/examples2d/stress_tests/pyramid2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -44,6 +44,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 2.5), 5.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 2.5), 5.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/stress_tests/vertical_stacks2.rs b/examples2d/stress_tests/vertical_stacks2.rs index c317de00b..608a85928 100644 --- a/examples2d/stress_tests/vertical_stacks2.rs +++ b/examples2d/stress_tests/vertical_stacks2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -51,6 +51,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 2.5), 5.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 2.5), 5.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/trimesh2.rs b/examples2d/trimesh2.rs index 605b1311b..fd3fffc5a 100644 --- a/examples2d/trimesh2.rs +++ b/examples2d/trimesh2.rs @@ -1,7 +1,7 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -47,6 +47,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 20.0), 17.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 20.0), 17.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples2d/utils/character.rs b/examples2d/utils/character.rs index ae88bc409..2bfc4044c 100644 --- a/examples2d/utils/character.rs +++ b/examples2d/utils/character.rs @@ -1,6 +1,6 @@ use kiss3d::color::Color; use rapier_testbed2d::{ - KeyCode, PhysicsState, TestbedGraphics, + KeyCode, TestbedViewer, egui::{Align2, ComboBox, Slider, Ui, Window}, }; use rapier2d::{ @@ -17,8 +17,8 @@ pub enum CharacterControlMode { } pub fn update_character( - graphics: &mut TestbedGraphics, - physics: &mut PhysicsState, + graphics: &mut TestbedViewer, + physics: &mut PhysicsWorld, control_mode: &mut CharacterControlMode, controller: &mut KinematicCharacterController, pid: &mut PidController, @@ -48,7 +48,7 @@ pub fn update_character( } fn character_movement_from_inputs( - gfx: &TestbedGraphics, + gfx: &TestbedViewer, mut speed: Real, artificial_gravity: bool, ) -> Vector { @@ -85,8 +85,8 @@ fn character_movement_from_inputs( } fn update_pid_controller( - gfx: &mut TestbedGraphics, - phx: &mut PhysicsState, + gfx: &mut TestbedViewer, + phx: &mut PhysicsWorld, character_handle: RigidBodyHandle, pid: &mut PidController, speed: Real, @@ -123,8 +123,8 @@ fn update_pid_controller( } fn update_kinematic_controller( - gfx: &mut TestbedGraphics, - phx: &mut PhysicsState, + gfx: &mut TestbedViewer, + phx: &mut PhysicsWorld, character_handle: RigidBodyHandle, controller: &KinematicCharacterController, speed: Real, @@ -174,7 +174,7 @@ fn update_kinematic_controller( } fn character_control_ui( - gfx: &mut TestbedGraphics, + gfx: &mut TestbedViewer, character_controller: &mut KinematicCharacterController, pid_controller: &mut PidController, control_mode: &mut CharacterControlMode, diff --git a/examples2d/voxels2.rs b/examples2d/voxels2.rs index f64f5d39c..2e9ec5f16 100644 --- a/examples2d/voxels2.rs +++ b/examples2d/voxels2.rs @@ -1,16 +1,16 @@ -use rapier_testbed2d::Testbed; +use rapier_testbed2d::TestbedViewer; use rapier2d::parry::transformation::voxelization::FillMode; use rapier2d::prelude::*; const VOXEL_SIZE: Real = 0.1; // 0.25; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * Voxel geometry type selection. */ // TODO: make the testbed support custom enums (or at least a list of option from strings and // associated constants). - let settings = testbed.example_settings_mut(); + let settings = viewer.example_settings_mut(); let falling_objects = settings.get_or_set_string( "Falling objects", 3, // Defaults to Mixed. @@ -100,6 +100,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec2::new(0.0, 20.0), 17.0); + viewer.set_world(&mut world); + viewer.look_at(Vec2::new(0.0, 20.0), 17.0); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d-f64/Cargo.toml b/examples3d-f64/Cargo.toml index a44e81c0b..a46c02018 100644 --- a/examples3d-f64/Cargo.toml +++ b/examples3d-f64/Cargo.toml @@ -13,6 +13,7 @@ parallel = ["rapier3d-f64/parallel", "rapier_testbed3d-f64/parallel"] enhanced-determinism = ["rapier3d-f64/enhanced-determinism"] [dependencies] +anyhow = "1" rand = "0.10" getrandom = { version = "0.2", features = ["js"] } wasm-bindgen = "0.2" diff --git a/examples3d-f64/all_examples3-f64.rs b/examples3d-f64/all_examples3-f64.rs index aaaece0dd..cd1659df8 100644 --- a/examples3d-f64/all_examples3-f64.rs +++ b/examples3d-f64/all_examples3-f64.rs @@ -1,26 +1,47 @@ #![allow(dead_code)] +#![allow(clippy::type_complexity)] #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; extern crate rapier3d_f64 as rapier3d; extern crate rapier_testbed3d_f64 as rapier_testbed3d; -use rapier_testbed3d::{Example, TestbedApp}; +use rapier_testbed3d::{ExampleEntry, TestbedViewer}; +use std::future::Future; +use std::pin::Pin; mod debug_serialized3; mod trimesh3_f64; +type ExampleFn = + for<'a> fn(&'a mut TestbedViewer) -> Pin> + 'a>>; + +macro_rules! examples { + ($($group:expr, $name:expr, $run:path);* $(;)?) => { + vec![ $( (ExampleEntry::new($group, $name), (|v| Box::pin($run(v))) as ExampleFn) ),* ] + }; +} + #[kiss3d::main] pub async fn main() { - let builders: Vec<_> = vec![ - Example::new("Demos f64", "Trimesh", trimesh3_f64::init_world), - Example::new( - "Demos f64", - "(Debug) serialized", - debug_serialized3::init_world, - ), + const DEMOS: &str = "Demos f64"; + + let examples: Vec<(ExampleEntry, ExampleFn)> = examples![ + DEMOS, "Trimesh", trimesh3_f64::run; + DEMOS, "(Debug) serialized", debug_serialized3::run; ]; - let testbed = TestbedApp::from_builders(builders); - testbed.run().await + let (entries, run_fns): (Vec<_>, Vec) = examples.into_iter().unzip(); + let mut viewer = TestbedViewer::new(entries).await; + + loop { + viewer.clear_scene(); + let idx = viewer.selected(); + if let Err(e) = run_fns[idx](&mut viewer).await { + eprintln!("example #{idx} failed: {e:?}"); + } + if viewer.quitting() { + break; + } + } } diff --git a/examples3d-f64/debug_serialized3.rs b/examples3d-f64/debug_serialized3.rs index 6fb0d996e..36fc84f72 100644 --- a/examples3d-f64/debug_serialized3.rs +++ b/examples3d-f64/debug_serialized3.rs @@ -1,4 +1,4 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; #[derive(serde::Deserialize)] @@ -13,27 +13,36 @@ struct State { pub ccd_solver: CCDSolver, } -pub fn init_world(testbed: &mut Testbed) { - /* - * Set up the testbed. - */ +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { let Ok(bytes) = std::fs::read("state.bin") else { println!("Failed to load serialized world state."); - return; + return Ok(()); }; let state: State = bincode::deserialize(&bytes).unwrap(); - testbed.set_world( - state.bodies, - state.colliders, - state.impulse_joints, - state.multibody_joints, - ); - testbed.harness_mut().physics.islands = state.islands; - testbed.harness_mut().physics.broad_phase = state.broad_phase; - testbed.harness_mut().physics.narrow_phase = state.narrow_phase; - testbed.harness_mut().physics.ccd_solver = state.ccd_solver; - - testbed.set_graphics_shift([-541.0, -6377257.0, -61.0].into()); - testbed.look_at([10.0, 10.0, 10.0].into(), [0.0, 0.0, 0.0].into()); + let mut world = PhysicsWorld::new(); + world.bodies = state.bodies; + world.colliders = state.colliders; + world.impulse_joints = state.impulse_joints; + world.multibody_joints = state.multibody_joints; + + viewer.set_world(&mut world); + + // Preserve the deserialized acceleration structures (set_world resets the + // broad-phase to the UI-selected default). + world.islands = state.islands; + world.broad_phase = state.broad_phase; + world.narrow_phase = state.narrow_phase; + world.ccd_solver = state.ccd_solver; + + viewer.set_graphics_shift([-541.0, -6377257.0, -61.0].into()); + viewer.look_at([10.0, 10.0, 10.0].into(), [0.0, 0.0, 0.0].into()); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + + Ok(()) } diff --git a/examples3d-f64/trimesh3_f64.rs b/examples3d-f64/trimesh3_f64.rs index d8a976360..4b02c8cb9 100644 --- a/examples3d-f64/trimesh3_f64.rs +++ b/examples3d-f64/trimesh3_f64.rs @@ -1,16 +1,13 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::glamx::{DVec3, Vec3}; use rapier3d::na::ComplexField; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ - let mut bodies = RigidBodySet::new(); - let mut colliders = ColliderSet::new(); - let impulse_joints = ImpulseJointSet::new(); - let multibody_joints = MultibodyJointSet::new(); + let mut world = PhysicsWorld::new(); /* * Ground @@ -38,9 +35,11 @@ pub fn init_world(testbed: &mut Testbed) { let (vertices, indices) = heightfield.to_trimesh(); let rigid_body = RigidBodyBuilder::fixed(); - let handle = bodies.insert(rigid_body); + let handle = world.bodies.insert(rigid_body); let collider = ColliderBuilder::trimesh(vertices, indices).unwrap(); - colliders.insert_with_parent(collider, handle, &mut bodies); + world + .colliders + .insert_with_parent(collider, handle, &mut world.bodies); /* * Create the cubes @@ -62,22 +61,34 @@ pub fn init_world(testbed: &mut Testbed) { // Build the rigid body. let rigid_body = RigidBodyBuilder::dynamic().translation(DVec3::new(x, y, z)); - let handle = bodies.insert(rigid_body); + let handle = world.bodies.insert(rigid_body); if j % 2 == 0 { let collider = ColliderBuilder::cuboid(rad, rad, rad); - colliders.insert_with_parent(collider, handle, &mut bodies); + world + .colliders + .insert_with_parent(collider, handle, &mut world.bodies); } else { let collider = ColliderBuilder::ball(rad); - colliders.insert_with_parent(collider, handle, &mut bodies); + world + .colliders + .insert_with_parent(collider, handle, &mut world.bodies); } } } } /* - * Set up the testbed. + * Set up the viewer. */ - testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + + Ok(()) } diff --git a/examples3d/Cargo.toml b/examples3d/Cargo.toml index 6b8a1571f..b57aa8fbd 100644 --- a/examples3d/Cargo.toml +++ b/examples3d/Cargo.toml @@ -10,10 +10,10 @@ publish = false parallel = ["rapier3d/parallel", "rapier_testbed3d/parallel"] simd-stable = ["rapier3d/simd-stable"] simd-nightly = ["rapier3d/simd-nightly"] -other-backends = ["rapier_testbed3d/other-backends"] enhanced-determinism = ["rapier3d/enhanced-determinism"] [dependencies] +anyhow = "1" rand = "0.10" getrandom = { version = "0.2", features = ["js"] } wasm-bindgen = "0.2" diff --git a/examples3d/all_examples3.rs b/examples3d/all_examples3.rs index cffe5b661..7ae9e00e8 100644 --- a/examples3d/all_examples3.rs +++ b/examples3d/all_examples3.rs @@ -1,7 +1,9 @@ #![allow(dead_code)] #![allow(clippy::type_complexity)] -use rapier_testbed3d::{Example, TestbedApp}; +use rapier_testbed3d::{ExampleEntry, TestbedViewer}; +use std::future::Future; +use std::pin::Pin; mod utils; @@ -64,159 +66,129 @@ mod vehicle_controller3; mod vehicle_joints3; mod voxels3; +/// A registered example: a fn pointer running the example's owned loop. +/// (A non-capturing closure coerces to this higher-ranked fn pointer.) +type ExampleFn = + for<'a> fn(&'a mut TestbedViewer) -> Pin> + 'a>>; + +/// `(group, name, run-fn)` -> `(ExampleEntry, ExampleFn)`. +macro_rules! examples { + ($($group:expr, $name:expr, $run:path);* $(;)?) => { + vec![ $( (ExampleEntry::new($group, $name), (|v| Box::pin($run(v))) as ExampleFn) ),* ] + }; +} + #[kiss3d::main] pub async fn main() { const COLLISIONS: &str = "Collisions"; const DYNAMICS: &str = "Dynamics"; - const COMPLEX: &str = "Complex Shapes"; const JOINTS: &str = "Joints"; const CONTROLS: &str = "Controls"; const DEBUG: &str = "Debug"; const ROBOTICS: &str = "Robotics"; + const STRESS: &str = "Stress tests"; - let mut builders: Vec = vec![ - // ── Collisions ────────────────────────────────────────────────────────── - Example::new(COLLISIONS, "Fountain", fountain3::init_world), - Example::new(COLLISIONS, "Primitives", primitives3::init_world), - Example::new(COLLISIONS, "Keva tower", keva3::init_world), - Example::new(COLLISIONS, "Newton cradle", newton_cradle3::init_world), - Example::new(COLLISIONS, "Domino", domino3::init_world), - Example::new(COLLISIONS, "Platform", platform3::init_world), - Example::new(COLLISIONS, "Sensor", sensor3::init_world), - Example::new(COLLISIONS, "Compound", compound3::init_world), - #[cfg(not(target_arch = "wasm32"))] - Example::new( - COLLISIONS, - "Convex decomposition", - convex_decomposition3::init_world, - ), - Example::new( - COLLISIONS, - "Convex polyhedron", - convex_polyhedron3::init_world, - ), - Example::new(COLLISIONS, "TriMesh", trimesh3::init_world), - #[cfg(not(target_arch = "wasm32"))] - Example::new( - COLLISIONS, - "Dynamic trimeshes", - dynamic_trimesh3::init_world, - ), - Example::new(COLLISIONS, "Heightfield", heightfield3::init_world), - Example::new(COLLISIONS, "Voxels", voxels3::init_world), - Example::new( - COLLISIONS, - "Collision groups", - collision_groups3::init_world, - ), - Example::new( - COLLISIONS, - "One-way platforms", - one_way_platforms3::init_world, - ), - // ── Dynamics ───────────────────────────────────────────────────────── - Example::new(DYNAMICS, "Locked rotations", locked_rotations3::init_world), - Example::new(DYNAMICS, "Restitution", restitution3::init_world), - Example::new(DYNAMICS, "Damping", damping3::init_world), - Example::new(DYNAMICS, "Gyroscopic", gyroscopic3::init_world), - Example::new(DYNAMICS, "CCD", ccd3::init_world), - // ── Joints ───────────────────────────────────────────────────────── - Example::new(JOINTS, "Impulse Joints", joints3::init_world_with_joints), - Example::new( - JOINTS, - "Multibody Joints", - joints3::init_world_with_articulations, - ), - Example::new(JOINTS, "Rope Joints", rope_joints3::init_world), - Example::new(JOINTS, "Spring Joints", spring_joints3::init_world), - Example::new( - JOINTS, - "Joint Motor Position", - joint_motor_position3::init_world, - ), - Example::new( - JOINTS, - "Inverse kinematics", - inverse_kinematics3::init_world, - ), - // ── Controls ───────────────────────────────────────────────────── - Example::new( - CONTROLS, - "Character controller", - character_controller3::init_world, - ), - Example::new( - CONTROLS, - "Vehicle controller", - vehicle_controller3::init_world, - ), - Example::new(CONTROLS, "Vehicle joints", vehicle_joints3::init_world), - // ── Robotics ─────────────────────────────────────────────────────── - #[cfg(not(target_arch = "wasm32"))] - Example::new(ROBOTICS, "URDF", urdf3::init_world), - #[cfg(not(target_arch = "wasm32"))] - Example::new(ROBOTICS, "MJCF", mjcf3::init_world), - #[cfg(not(target_arch = "wasm32"))] - Example::new(ROBOTICS, "Mujoco Menagerie", mujoco_menagerie3::init_world), - // ── Debug ────────────────────────────────────────────────────────── - Example::new(DEBUG, "Multibody joints", debug_articulations3::init_world), - Example::new( - DEBUG, - "Add/rm collider", - debug_add_remove_collider3::init_world, - ), - Example::new(DEBUG, "Big colliders", debug_big_colliders3::init_world), - Example::new(DEBUG, "Boxes", debug_boxes3::init_world), - Example::new(DEBUG, "Balls", debug_balls3::init_world), - Example::new(DEBUG, "Disabled", debug_disabled3::init_world), - Example::new(DEBUG, "Two cubes", debug_two_cubes3::init_world), - Example::new(DEBUG, "Pop", debug_pop3::init_world), - Example::new( - DEBUG, - "Dyn. collider add", - debug_dynamic_collider_add3::init_world, - ), - Example::new(DEBUG, "Friction", debug_friction3::init_world), - Example::new(DEBUG, "Internal edges", debug_internal_edges3::init_world), - Example::new(DEBUG, "Long chain", debug_long_chain3::init_world), - Example::new( - DEBUG, - "High mass ratio: chain", - debug_chain_high_mass_ratio3::init_world, - ), - Example::new( - DEBUG, - "High mass ratio: cube", - debug_cube_high_mass_ratio3::init_world, - ), - Example::new(DEBUG, "Triangle", debug_triangle3::init_world), - Example::new(DEBUG, "Trimesh", debug_trimesh3::init_world), - Example::new(DEBUG, "Thin cube", debug_thin_cube_on_mesh3::init_world), - Example::new(DEBUG, "Cylinder", debug_cylinder3::init_world), - Example::new(DEBUG, "Infinite fall", debug_infinite_fall3::init_world), - Example::new(DEBUG, "Prismatic", debug_prismatic3::init_world), - Example::new(DEBUG, "Rollback", debug_rollback3::init_world), - Example::new( - DEBUG, - "Shape modification", - debug_shape_modification3::init_world, - ), - Example::new( - DEBUG, - "Sleeping kinematics", - debug_sleeping_kinematic3::init_world, - ), - #[cfg(not(target_arch = "wasm32"))] - Example::new(DEBUG, "Deserialize", debug_deserialize3::init_world), - Example::new( - DEBUG, - "Multibody ang. motor pos.", - debug_multibody_ang_motor_pos3::init_world, - ), + let examples: Vec<(ExampleEntry, ExampleFn)> = examples![ + // ── Collisions ────────────────────────────────────────────────────── + COLLISIONS, "Fountain", fountain3::run; + COLLISIONS, "Primitives", primitives3::run; + COLLISIONS, "Keva tower", keva3::run; + COLLISIONS, "Newton cradle", newton_cradle3::run; + COLLISIONS, "Domino", domino3::run; + COLLISIONS, "Platform", platform3::run; + COLLISIONS, "Sensor", sensor3::run; + COLLISIONS, "Compound", compound3::run; + COLLISIONS, "Convex decomposition", convex_decomposition3::run; + COLLISIONS, "Convex polyhedron", convex_polyhedron3::run; + COLLISIONS, "TriMesh", trimesh3::run; + COLLISIONS, "Dynamic trimeshes", dynamic_trimesh3::run; + COLLISIONS, "Heightfield", heightfield3::run; + COLLISIONS, "Voxels", voxels3::run; + COLLISIONS, "Collision groups", collision_groups3::run; + COLLISIONS, "One-way platforms", one_way_platforms3::run; + // ── Dynamics ──────────────────────────────────────────────────────── + DYNAMICS, "Locked rotations", locked_rotations3::run; + DYNAMICS, "Restitution", restitution3::run; + DYNAMICS, "Damping", damping3::run; + DYNAMICS, "Gyroscopic", gyroscopic3::run; + DYNAMICS, "CCD", ccd3::run; + // ── Joints ────────────────────────────────────────────────────────── + JOINTS, "Impulse Joints", joints3::run_impulse_joints; + JOINTS, "Multibody Joints", joints3::run_multibody_joints; + JOINTS, "Rope Joints", rope_joints3::run; + JOINTS, "Spring Joints", spring_joints3::run; + JOINTS, "Joint Motor Position", joint_motor_position3::run; + JOINTS, "Inverse kinematics", inverse_kinematics3::run; + // ── Controls ──────────────────────────────────────────────────────── + CONTROLS, "Character controller", character_controller3::run; + CONTROLS, "Vehicle controller", vehicle_controller3::run; + CONTROLS, "Vehicle joints", vehicle_joints3::run; + // ── Robotics ──────────────────────────────────────────────────────── + ROBOTICS, "URDF", urdf3::run; + ROBOTICS, "MJCF", mjcf3::run; + ROBOTICS, "Mujoco Menagerie", mujoco_menagerie3::run; + // ── Debug ─────────────────────────────────────────────────────────── + DEBUG, "Multibody joints", debug_articulations3::run; + DEBUG, "Add/rm collider", debug_add_remove_collider3::run; + DEBUG, "Big colliders", debug_big_colliders3::run; + DEBUG, "Boxes", debug_boxes3::run; + DEBUG, "Balls", debug_balls3::run; + DEBUG, "Disabled", debug_disabled3::run; + DEBUG, "Two cubes", debug_two_cubes3::run; + DEBUG, "Pop", debug_pop3::run; + DEBUG, "Dyn. collider add", debug_dynamic_collider_add3::run; + DEBUG, "Friction", debug_friction3::run; + DEBUG, "Internal edges", debug_internal_edges3::run; + DEBUG, "Long chain", debug_long_chain3::run; + DEBUG, "High mass ratio: chain", debug_chain_high_mass_ratio3::run; + DEBUG, "High mass ratio: cube", debug_cube_high_mass_ratio3::run; + DEBUG, "Triangle", debug_triangle3::run; + DEBUG, "Trimesh", debug_trimesh3::run; + DEBUG, "Thin cube", debug_thin_cube_on_mesh3::run; + DEBUG, "Cylinder", debug_cylinder3::run; + DEBUG, "Infinite fall", debug_infinite_fall3::run; + DEBUG, "Prismatic", debug_prismatic3::run; + DEBUG, "Rollback", debug_rollback3::run; + DEBUG, "Shape modification", debug_shape_modification3::run; + DEBUG, "Sleeping kinematics", debug_sleeping_kinematic3::run; + DEBUG, "Deserialize", debug_deserialize3::run; + DEBUG, "Multibody ang. motor pos.", debug_multibody_ang_motor_pos3::run; + // ── Stress tests ──────────────────────────────────────────────────── + STRESS, "Balls", stress_tests::balls3::run; + STRESS, "Boxes", stress_tests::boxes3::run; + STRESS, "Capsules", stress_tests::capsules3::run; + STRESS, "CCD", stress_tests::ccd3::run; + STRESS, "Compound", stress_tests::compound3::run; + STRESS, "Convex polyhedron", stress_tests::convex_polyhedron3::run; + STRESS, "Many kinematics", stress_tests::many_kinematics3::run; + STRESS, "Many static", stress_tests::many_static3::run; + STRESS, "Many sleep", stress_tests::many_sleep3::run; + STRESS, "Heightfield", stress_tests::heightfield3::run; + STRESS, "Stacks", stress_tests::stacks3::run; + STRESS, "Pyramid", stress_tests::pyramid3::run; + STRESS, "Trimesh", stress_tests::trimesh3::run; + STRESS, "ImpulseJoint ball", stress_tests::joint_ball3::run; + STRESS, "ImpulseJoint fixed", stress_tests::joint_fixed3::run; + STRESS, "ImpulseJoint revolute", stress_tests::joint_revolute3::run; + STRESS, "ImpulseJoint prismatic", stress_tests::joint_prismatic3::run; + STRESS, "Many pyramids", stress_tests::many_pyramids3::run; + STRESS, "Keva tower", stress_tests::keva3::run; + STRESS, "Ray cast", stress_tests::ray_cast3::run; ]; - let mut benches = stress_tests::builders(); - builders.append(&mut benches); - let testbed = TestbedApp::from_builders(builders); - testbed.run().await + let (entries, run_fns): (Vec<_>, Vec) = examples.into_iter().unzip(); + let mut viewer = TestbedViewer::new(entries).await; + + // The example owns its physics state and render loop; this outer loop just + // (re)dispatches the example the UI has selected, nexus-style. + loop { + viewer.clear_scene(); + let idx = viewer.selected(); + if let Err(e) = run_fns[idx](&mut viewer).await { + eprintln!("example #{idx} failed: {e:?}"); + } + if viewer.quitting() { + break; + } + } } diff --git a/examples3d/ccd3.rs b/examples3d/ccd3.rs index 63ec89105..20243e724 100644 --- a/examples3d/ccd3.rs +++ b/examples3d/ccd3.rs @@ -1,9 +1,9 @@ use kiss3d::color::Color; -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; fn create_wall( - testbed: &mut Testbed, + viewer: &mut TestbedViewer, world: &mut PhysicsWorld, offset: Vector, stack_height: usize, @@ -26,17 +26,17 @@ fn create_wall( let (handle, _) = world.insert(rigid_body, collider); k += 1; if k % 2 == 0 { - testbed + viewer .set_initial_body_color(handle, Color::new(1., 131. / 255., 244.0 / 255., 1.0)); } else { - testbed + viewer .set_initial_body_color(handle, Color::new(131. / 255., 1., 244.0 / 255., 1.0)); } } } } -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -63,7 +63,7 @@ pub fn init_world(testbed: &mut Testbed) { for i in 0..num_x { let x = i as f32 * 6.0; create_wall( - testbed, + viewer, &mut world, Vector::new(x, shift_y, 0.0), num_z, @@ -71,7 +71,7 @@ pub fn init_world(testbed: &mut Testbed) { ); create_wall( - testbed, + viewer, &mut world, Vector::new(x, shift_y, shift_z), num_z, @@ -101,44 +101,50 @@ pub fn init_world(testbed: &mut Testbed) { .translation(Vector::new(-20.0, shift_y + 2.0, shift_z)) .ccd_enabled(true); let (handle, _) = world.insert(rigid_body, collider.clone()); - testbed.set_initial_body_color(handle, Color::new(0.2, 0.2, 1.0, 1.0)); + viewer.set_initial_body_color(handle, Color::new(0.2, 0.2, 1.0, 1.0)); - // Callback that will be executed on the main loop to handle proximities. - testbed.add_callback(move |mut graphics, physics, events, _| { - while let Ok(prox) = events.collision_events.try_recv() { - let color = if prox.started() { - Color::new(1.0, 1.0, 0.0, 1.0) - } else { - Color::new(0.5, 0.5, 1.0, 1.0) - }; + /* + * Set up the testbed. + */ + viewer.set_world(&mut world); + let (collision_send, collision_recv) = std::sync::mpsc::channel(); + let (contact_force_send, _contact_force_recv) = std::sync::mpsc::channel(); + let event_handler = ChannelEventCollector::new(collision_send, contact_force_send); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step_with_events(&(), &event_handler); + + // Callback that will be executed on the main loop to handle proximities. + while let Ok(prox) = collision_recv.try_recv() { + let color = if prox.started() { + Color::new(1.0, 1.0, 0.0, 1.0) + } else { + Color::new(0.5, 0.5, 1.0, 1.0) + }; - let parent_handle1 = physics - .colliders - .get(prox.collider1()) - .unwrap() - .parent() - .unwrap(); - let parent_handle2 = physics - .colliders - .get(prox.collider2()) - .unwrap() - .parent() - .unwrap(); + let parent_handle1 = world + .colliders + .get(prox.collider1()) + .unwrap() + .parent() + .unwrap(); + let parent_handle2 = world + .colliders + .get(prox.collider2()) + .unwrap() + .parent() + .unwrap(); - if let Some(graphics) = &mut graphics { if parent_handle1 != ground_handle && parent_handle1 != sensor_handle { - graphics.set_body_color(parent_handle1, color, false); + viewer.set_body_color(parent_handle1, color, false); } if parent_handle2 != ground_handle && parent_handle2 != sensor_handle { - graphics.set_body_color(parent_handle2, color, false); + viewer.set_body_color(parent_handle2, color, false); } } } - }); - - /* - * Set up the testbed. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + } + Ok(()) } diff --git a/examples3d/character_controller3.rs b/examples3d/character_controller3.rs index de6418f07..8a0229989 100644 --- a/examples3d/character_controller3.rs +++ b/examples3d/character_controller3.rs @@ -1,12 +1,12 @@ use crate::utils::character::{self, CharacterControlMode}; use kiss3d::color::Color; -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::{ control::{KinematicCharacterController, PidController}, prelude::*, }; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -48,7 +48,7 @@ pub fn init_world(testbed: &mut Testbed) { .soft_ccd_prediction(10.0); let collider = ColliderBuilder::capsule_y(0.3 * scale, 0.15 * scale); // 0.15, 0.3, 0.15); let (character_handle, _) = world.insert(rigid_body, collider); - testbed.set_initial_body_color(character_handle, Color::new(0.8, 0.1, 0.1, 1.0)); + viewer.set_initial_body_color(character_handle, Color::new(0.8, 0.1, 0.1, 1.0)); /* * Create the cubes @@ -158,25 +158,6 @@ pub fn init_world(testbed: &mut Testbed) { let joint = RevoluteJointBuilder::new(Vector::Z).limits([-0.3, 0.3]); world.insert_impulse_joint(ground_handle, handle, joint); - /* - * Setup a callback to move the platform. - */ - testbed.add_callback(move |_, physics, _, run_state| { - let linvel = Vector::new( - (run_state.time * 2.0).sin() * 2.0, - (run_state.time * 5.0).sin() * 1.5, - 0.0, - ) * scale; - // let angvel = run_state.time.sin() * 0.5; - - // Update the velocity-based kinematic body by setting its velocity. - if let Some(platform) = physics.bodies.get_mut(platform_handle) { - platform.set_linvel(linvel, true); - // NOTE: interaction with rotating platforms isn’t handled very well yet. - // platform.set_angvel(angvel, true); - } - }); - /* * Callback to update the character based on user inputs. */ @@ -189,22 +170,47 @@ pub fn init_world(testbed: &mut Testbed) { }; let mut pid = PidController::default(); - testbed.add_callback(move |graphics, physics, _, _| { - if let Some(graphics) = graphics { + /* + * Set up the viewer. + */ + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + + let mut step_id = 0usize; + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + step_id += 1; + + /* + * Move the platform. + */ + let linvel = Vector::new( + ((step_id as f32 * world.integration_parameters.dt) * 2.0).sin() * 2.0, + ((step_id as f32 * world.integration_parameters.dt) * 5.0).sin() * 1.5, + 0.0, + ) * scale; + // let angvel = (step_id as f32 * world.integration_parameters.dt as f32).sin() * 0.5; + + // Update the velocity-based kinematic body by setting its velocity. + if let Some(platform) = world.bodies.get_mut(platform_handle) { + platform.set_linvel(linvel, true); + // NOTE: interaction with rotating platforms isn’t handled very well yet. + // platform.set_angvel(angvel, true); + } + + /* + * Update the character based on user inputs. + */ character::update_character( - graphics, - physics, + viewer, + &mut world, &mut control_mode, &mut controller, &mut pid, character_handle, ); } - }); - - /* - * Set up the testbed. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + } + Ok(()) } diff --git a/examples3d/collision_groups3.rs b/examples3d/collision_groups3.rs index 51eb2fa56..9b5b45008 100644 --- a/examples3d/collision_groups3.rs +++ b/examples3d/collision_groups3.rs @@ -1,8 +1,8 @@ use kiss3d::color::{BLUE, GREEN}; -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -34,7 +34,7 @@ pub fn init_world(testbed: &mut Testbed) { .collision_groups(GREEN_GROUP); let green_collider_handle = world.insert_collider(green_floor, Some(floor_handle)); - testbed.set_initial_collider_color(green_collider_handle, BLUE); + viewer.set_initial_collider_color(green_collider_handle, BLUE); /* * A blue floor that will collide with the BLUE group only. @@ -44,7 +44,7 @@ pub fn init_world(testbed: &mut Testbed) { .collision_groups(BLUE_GROUP); let blue_collider_handle = world.insert_collider(blue_floor, Some(floor_handle)); - testbed.set_initial_collider_color(blue_collider_handle, GREEN); + viewer.set_initial_collider_color(blue_collider_handle, GREEN); /* * Create the cubes @@ -75,7 +75,7 @@ pub fn init_world(testbed: &mut Testbed) { let collider = ColliderBuilder::cuboid(rad, rad, rad).collision_groups(group); let (handle, _) = world.insert(rigid_body, collider); - testbed.set_initial_body_color(handle, color); + viewer.set_initial_body_color(handle, color); } } } @@ -83,6 +83,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/compound3.rs b/examples3d/compound3.rs index 99f0e3af6..0c088e2a9 100644 --- a/examples3d/compound3.rs +++ b/examples3d/compound3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -77,6 +77,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/convex_decomposition3.rs b/examples3d/convex_decomposition3.rs index eeaa682bc..d50e3dcf1 100644 --- a/examples3d/convex_decomposition3.rs +++ b/examples3d/convex_decomposition3.rs @@ -1,5 +1,5 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; -pub fn init_world(testbed: &mut Testbed) { - crate::dynamic_trimesh3::do_init_world(testbed, true); +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { + crate::dynamic_trimesh3::run_impl(viewer, true).await } diff --git a/examples3d/convex_polyhedron3.rs b/examples3d/convex_polyhedron3.rs index 703d27dd2..7111b06ac 100644 --- a/examples3d/convex_polyhedron3.rs +++ b/examples3d/convex_polyhedron3.rs @@ -1,9 +1,9 @@ use rand::distr::{Distribution, StandardUniform}; use rand::{SeedableRng, rngs::StdRng}; -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -58,6 +58,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(30.0, 30.0, 30.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(30.0, 30.0, 30.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/damping3.rs b/examples3d/damping3.rs index 12f966798..8253a5196 100644 --- a/examples3d/damping3.rs +++ b/examples3d/damping3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -34,6 +34,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(2.0, 2.5, 20.0), Vec3::new(2.0, 2.5, 0.0)); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(2.0, 2.5, 20.0), Vec3::new(2.0, 2.5, 0.0)); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_add_remove_collider3.rs b/examples3d/debug_add_remove_collider3.rs index e79a2beed..339cf4656 100644 --- a/examples3d/debug_add_remove_collider3.rs +++ b/examples3d/debug_add_remove_collider3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -25,27 +25,32 @@ pub fn init_world(testbed: &mut Testbed) { let collider = ColliderBuilder::ball(ball_rad).density(100.0); let (_ball_handle, _) = world.insert(rb, collider); - testbed.add_callback(move |_, physics, _, _| { - // Remove then re-add the ground collider. - let removed_collider_handle = ground_collider_handle; - let coll = physics - .colliders - .remove( - removed_collider_handle, - &mut physics.islands, - &mut physics.bodies, - true, - ) - .unwrap(); - ground_collider_handle = - physics - .colliders - .insert_with_parent(coll, ground_handle, &mut physics.bodies); - }); - /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + + // Remove then re-add the ground collider. + let removed_collider_handle = ground_collider_handle; + let coll = world + .colliders + .remove( + removed_collider_handle, + &mut world.islands, + &mut world.bodies, + true, + ) + .unwrap(); + ground_collider_handle = + world + .colliders + .insert_with_parent(coll, ground_handle, &mut world.bodies); + } + } + Ok(()) } diff --git a/examples3d/debug_articulations3.rs b/examples3d/debug_articulations3.rs index abcf92d4a..6bcdd4bcb 100644 --- a/examples3d/debug_articulations3.rs +++ b/examples3d/debug_articulations3.rs @@ -1,4 +1,4 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; fn create_ball_articulations(world: &mut PhysicsWorld, num: usize) { @@ -54,7 +54,7 @@ fn create_ball_articulations(world: &mut PhysicsWorld, num: usize) { } } -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -76,6 +76,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(15.0, 5.0, 42.0), Vec3::new(13.0, 1.0, 1.0)); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(15.0, 5.0, 42.0), Vec3::new(13.0, 1.0, 1.0)); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_balls3.rs b/examples3d/debug_balls3.rs index 5647a2a57..6029ca742 100644 --- a/examples3d/debug_balls3.rs +++ b/examples3d/debug_balls3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -50,6 +50,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_big_colliders3.rs b/examples3d/debug_big_colliders3.rs index de159cc70..01d2025dc 100644 --- a/examples3d/debug_big_colliders3.rs +++ b/examples3d/debug_big_colliders3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -32,6 +32,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_boxes3.rs b/examples3d/debug_boxes3.rs index be6f10e56..978f98a67 100644 --- a/examples3d/debug_boxes3.rs +++ b/examples3d/debug_boxes3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -33,6 +33,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_chain_high_mass_ratio3.rs b/examples3d/debug_chain_high_mass_ratio3.rs index 997fa935d..3ce4eb060 100644 --- a/examples3d/debug_chain_high_mass_ratio3.rs +++ b/examples3d/debug_chain_high_mass_ratio3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -62,8 +62,15 @@ pub fn init_world(testbed: &mut Testbed) { } /* - * Set up the testbed. + * Set up the viewer. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_cube_high_mass_ratio3.rs b/examples3d/debug_cube_high_mass_ratio3.rs index f9f3fc841..c1b2fa7f9 100644 --- a/examples3d/debug_cube_high_mass_ratio3.rs +++ b/examples3d/debug_cube_high_mass_ratio3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -87,6 +87,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_cylinder3.rs b/examples3d/debug_cylinder3.rs index 44175fe94..3612b2855 100644 --- a/examples3d/debug_cylinder3.rs +++ b/examples3d/debug_cylinder3.rs @@ -1,10 +1,10 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; // This shows a bug when a cylinder is in contact with a very large // but very thin cuboid. In this case the EPA returns an incorrect // contact normal, resulting in the cylinder falling through the floor. -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -47,6 +47,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_deserialize3.rs b/examples3d/debug_deserialize3.rs index c87729225..2b7251281 100644 --- a/examples3d/debug_deserialize3.rs +++ b/examples3d/debug_deserialize3.rs @@ -1,4 +1,4 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; #[derive(serde::Deserialize)] @@ -14,9 +14,9 @@ struct PhysicsState { pub multibody_joints: MultibodyJointSet, } -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { // Deserialize - let setting = testbed.example_settings_mut(); + let setting = viewer.example_settings_mut(); let frame_id = setting.get_or_set_u32("frame", 0, 0..=1400); let frame_dirs = "/Users/sebcrozet/work/hytopia/sdk/examples/bug-demo"; let path = format!("{frame_dirs}/snapshot{frame_id}.bincode"); @@ -24,10 +24,10 @@ pub fn init_world(testbed: &mut Testbed) { Ok(bytes) => bytes, Err(err) => { println!("Failed to open the serialized scene file {path:?}: {err}"); - return; + return Ok(()); } }; - match bincode::deserialize(&bytes) { + let mut world = match bincode::deserialize(&bytes) { Ok(state) => { let state: PhysicsState = state; println!("World state deserialized successfully:"); @@ -56,10 +56,22 @@ pub fn init_world(testbed: &mut Testbed) { world.narrow_phase = state.narrow_phase; world.integration_parameters = state.integration_parameters; world.gravity = state.gravity; - testbed.set_physics_world(world); + world + } + Err(err) => { + println!("Failed to deserialize the world state: {err}"); + return Ok(()); + } + }; + + viewer.set_world(&mut world); + + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::new(0.0, 0.0, 0.0)); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::new(0.0, 0.0, 0.0)); + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); } - Err(err) => println!("Failed to deserialize the world state: {err}"), } + Ok(()) } diff --git a/examples3d/debug_disabled3.rs b/examples3d/debug_disabled3.rs index c43aa61b7..f94a106a7 100644 --- a/examples3d/debug_disabled3.rs +++ b/examples3d/debug_disabled3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { let mut world = PhysicsWorld::new(); let rad = 0.5; @@ -23,32 +23,37 @@ pub fn init_world(testbed: &mut Testbed) { let collider = ColliderBuilder::cuboid(5.0, 1.0, 5.0); let (_handle, handle_to_disable) = world.insert(rigid_body, collider); - // Callback that will be executed on the main loop to handle proximities. - testbed.add_callback(move |mut graphics, physics, _, run_state| { - if run_state.timestep_id % 250 == 0 { - let co = &mut physics.colliders[handle_to_disable]; - let enabled = co.is_enabled(); - co.set_enabled(!enabled); - println!("Platform is now enabled: {}", co.is_enabled()); - } + /* + * Set up the testbed. + */ + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(30.0, 4.0, 30.0), Vec3::new(0.0, 1.0, 0.0)); + + let mut step_id = 0usize; + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + step_id += 1; + + if step_id.is_multiple_of(250) { + let co = &mut world.colliders[handle_to_disable]; + let enabled = co.is_enabled(); + co.set_enabled(!enabled); + println!("Platform is now enabled: {}", co.is_enabled()); + } - if run_state.timestep_id % 25 == 0 { - let rigid_body = RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 20.0, 0.0)); - let handle = physics.bodies.insert(rigid_body); - let collider = ColliderBuilder::cuboid(rad, rad, rad); - physics - .colliders - .insert_with_parent(collider, handle, &mut physics.bodies); + if step_id.is_multiple_of(25) { + let rigid_body = + RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 20.0, 0.0)); + let handle = world.bodies.insert(rigid_body); + let collider = ColliderBuilder::cuboid(rad, rad, rad); + world + .colliders + .insert_with_parent(collider, handle, &mut world.bodies); - if let Some(graphics) = &mut graphics { - graphics.add_body(handle, &physics.bodies, &physics.colliders); + viewer.add_body(handle, &world); } } - }); - - /* - * Set up the testbed. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(30.0, 4.0, 30.0), Vec3::new(0.0, 1.0, 0.0)); + } + Ok(()) } diff --git a/examples3d/debug_dynamic_collider_add3.rs b/examples3d/debug_dynamic_collider_add3.rs index 05a360f5a..c9138ad0a 100644 --- a/examples3d/debug_dynamic_collider_add3.rs +++ b/examples3d/debug_dynamic_collider_add3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -37,74 +37,74 @@ pub fn init_world(testbed: &mut Testbed) { let mut extra_colliders = Vec::new(); let snapped_frame = 51; - testbed.add_callback(move |mut graphics, physics, _, _| { - step += 1; + /* + * Set up the testbed. + */ + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); - // Add a bigger ball collider - let collider = ColliderBuilder::ball(ball_rad + 0.01 * (step as f32)).density(100.0); - let new_ball_collider_handle = - physics - .colliders - .insert_with_parent(collider, ball_handle, &mut physics.bodies); + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); - if let Some(graphics) = &mut graphics { - graphics.add_collider(new_ball_collider_handle, &physics.colliders); - } + step += 1; + + // Add a bigger ball collider + let collider = ColliderBuilder::ball(ball_rad + 0.01 * (step as f32)).density(100.0); + let new_ball_collider_handle = + world + .colliders + .insert_with_parent(collider, ball_handle, &mut world.bodies); - extra_colliders.push(new_ball_collider_handle); + viewer.add_collider(new_ball_collider_handle, &world); - // Snap the ball velocity or restore it. - let ball = physics.bodies.get_mut(ball_handle).unwrap(); + extra_colliders.push(new_ball_collider_handle); - if step == snapped_frame { - linvel = ball.linvel(); - angvel = ball.angvel(); - pos = *ball.position(); - } + // Snap the ball velocity or restore it. + let ball = world.bodies.get_mut(ball_handle).unwrap(); + + if step == snapped_frame { + linvel = ball.linvel(); + angvel = ball.angvel(); + pos = *ball.position(); + } + + if step == 100 { + ball.set_linvel(linvel, true); + ball.set_angvel(angvel, true); + ball.set_position(pos, true); + step = snapped_frame; - if step == 100 { - ball.set_linvel(linvel, true); - ball.set_angvel(angvel, true); - ball.set_position(pos, true); - step = snapped_frame; + for handle in &extra_colliders { + viewer.remove_collider(*handle); - for handle in &extra_colliders { - if let Some(graphics) = &mut graphics { - graphics.remove_collider(*handle); + world + .colliders + .remove(*handle, &mut world.islands, &mut world.bodies, true); } - physics - .colliders - .remove(*handle, &mut physics.islands, &mut physics.bodies, true); + extra_colliders.clear(); } - extra_colliders.clear(); - } - - // Remove then re-add the ground collider. - // let ground = physics.bodies.get_mut(ground_handle).unwrap(); - // ground.set_position(Pose::from_translation(Vector::new(0.0, step as f32 * 0.001, 0.0)), false); - // let coll = physics - // .colliders - // .remove(ground_collider_handle, &mut physics.bodies, true) - // .unwrap(); - let coll = ColliderBuilder::cuboid(ground_size, ground_height + step as f32 * 0.01, 0.4) - .friction(0.15); - let new_ground_collider_handle = - physics - .colliders - .insert_with_parent(coll, ground_handle, &mut physics.bodies); - - if let Some(graphics) = &mut graphics { - graphics.add_collider(new_ground_collider_handle, &physics.colliders); - } + // Remove then re-add the ground collider. + // let ground = world.bodies.get_mut(ground_handle).unwrap(); + // ground.set_position(Pose::from_translation(Vector::new(0.0, step as f32 * 0.001, 0.0)), false); + // let coll = world + // .colliders + // .remove(ground_collider_handle, &mut world.bodies, true) + // .unwrap(); + let coll = + ColliderBuilder::cuboid(ground_size, ground_height + step as f32 * 0.01, 0.4) + .friction(0.15); + let new_ground_collider_handle = + world + .colliders + .insert_with_parent(coll, ground_handle, &mut world.bodies); - extra_colliders.push(new_ground_collider_handle); - }); + viewer.add_collider(new_ground_collider_handle, &world); - /* - * Set up the testbed. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + extra_colliders.push(new_ground_collider_handle); + } + } + Ok(()) } diff --git a/examples3d/debug_eccentric_boxes3.rs b/examples3d/debug_eccentric_boxes3.rs index 377c46d21..f63a89145 100644 --- a/examples3d/debug_eccentric_boxes3.rs +++ b/examples3d/debug_eccentric_boxes3.rs @@ -1,7 +1,7 @@ use rapier3d::prelude::*; -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -34,6 +34,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_friction3.rs b/examples3d/debug_friction3.rs index f48e3fd30..e278eb0ae 100644 --- a/examples3d/debug_friction3.rs +++ b/examples3d/debug_friction3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -31,6 +31,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_infinite_fall3.rs b/examples3d/debug_infinite_fall3.rs index 17d448ca2..bcdf4c8d0 100644 --- a/examples3d/debug_infinite_fall3.rs +++ b/examples3d/debug_infinite_fall3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -34,6 +34,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.look_at(Vec3::new(100.0, -10.0, 100.0), Vec3::ZERO); - testbed.set_physics_world(world); + viewer.look_at(Vec3::new(100.0, -10.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_internal_edges3.rs b/examples3d/debug_internal_edges3.rs index 67f232401..db99fdacb 100644 --- a/examples3d/debug_internal_edges3.rs +++ b/examples3d/debug_internal_edges3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -58,6 +58,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_long_chain3.rs b/examples3d/debug_long_chain3.rs index a1ee05b5c..96eaf17bc 100644 --- a/examples3d/debug_long_chain3.rs +++ b/examples3d/debug_long_chain3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -55,6 +55,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_multibody_ang_motor_pos3.rs b/examples3d/debug_multibody_ang_motor_pos3.rs index 7ae5ea497..0dd748d6c 100644 --- a/examples3d/debug_multibody_ang_motor_pos3.rs +++ b/examples3d/debug_multibody_ang_motor_pos3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { let mut world = PhysicsWorld::new(); let ground = RigidBodyBuilder::fixed().translation(Vector::new(0.0, 0.0, 0.0)); @@ -22,6 +22,13 @@ pub fn init_world(testbed: &mut Testbed) { world.insert_multibody_joint(body, body_part, joint); - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(20.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 0.0)); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(20.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 0.0)); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_pop3.rs b/examples3d/debug_pop3.rs index 29b5c851e..9f231d6f3 100644 --- a/examples3d/debug_pop3.rs +++ b/examples3d/debug_pop3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -33,6 +33,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_prismatic3.rs b/examples3d/debug_prismatic3.rs index 5fd358f86..15dbf5847 100644 --- a/examples3d/debug_prismatic3.rs +++ b/examples3d/debug_prismatic3.rs @@ -1,4 +1,4 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; fn prismatic_repro(world: &mut PhysicsWorld, box_center: Vector) { @@ -41,7 +41,7 @@ fn prismatic_repro(world: &mut PhysicsWorld, box_center: Vector) { ); } -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -62,6 +62,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_rollback3.rs b/examples3d/debug_rollback3.rs index 2c73271dc..a71779c65 100644 --- a/examples3d/debug_rollback3.rs +++ b/examples3d/debug_rollback3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -36,29 +36,34 @@ pub fn init_world(testbed: &mut Testbed) { let mut step = 0; let snapped_frame = 51; - testbed.add_callback(move |_, physics, _, _| { - step += 1; + /* + * Set up the testbed. + */ + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); - // Snap the ball velocity or restore it. - let ball = physics.bodies.get_mut(ball_handle).unwrap(); + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); - if step == snapped_frame { - linvel = ball.linvel(); - angvel = ball.angvel(); - pos = *ball.position(); - } + step += 1; - if step == 100 { - ball.set_linvel(linvel, true); - ball.set_angvel(angvel, true); - ball.set_position(pos, true); - step = snapped_frame; - } - }); + // Snap the ball velocity or restore it. + let ball = world.bodies.get_mut(ball_handle).unwrap(); - /* - * Set up the testbed. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + if step == snapped_frame { + linvel = ball.linvel(); + angvel = ball.angvel(); + pos = *ball.position(); + } + + if step == 100 { + ball.set_linvel(linvel, true); + ball.set_angvel(angvel, true); + ball.set_position(pos, true); + step = snapped_frame; + } + } + } + Ok(()) } diff --git a/examples3d/debug_shape_modification3.rs b/examples3d/debug_shape_modification3.rs index 2b224e1c5..58dc96ca4 100644 --- a/examples3d/debug_shape_modification3.rs +++ b/examples3d/debug_shape_modification3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -55,43 +55,6 @@ pub fn init_world(testbed: &mut Testbed) { let mut step = 0; let snapped_frame = 51; - testbed.add_callback(move |mut gfx, physics, _, _| { - step += 1; - - // Snap the ball velocity or restore it. - let ball = physics.bodies.get_mut(ball_handle).unwrap(); - - if step == snapped_frame { - linvel = ball.linvel(); - angvel = ball.angvel(); - pos = *ball.position(); - } - - let shapeshifting_coll = physics - .colliders - .get_mut(shapeshifting_coll_handle) - .unwrap(); - if step % 50 == 0 { - shape_idx = (shape_idx + 1) % 4; - shapeshifting_coll.set_shape(shapes[shape_idx].clone()) - } - - if step == 100 { - ball.set_linvel(linvel, true); - ball.set_angvel(angvel, true); - ball.set_position(pos, true); - step = snapped_frame; - } - - let ball_coll = physics.colliders.get_mut(ball_coll_handle).unwrap(); - ball_coll.set_shape(SharedShape::ball(ball_rad * step as f32 * 2.0)); - - if let Some(gfx) = &mut gfx { - gfx.update_collider(ball_coll_handle, &physics.colliders); - gfx.update_collider(shapeshifting_coll_handle, &physics.colliders); - } - }); - /* * Create the primitives */ @@ -137,6 +100,43 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(40.0, 40.0, 40.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(40.0, 40.0, 40.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + + step += 1; + + // Snap the ball velocity or restore it. + let ball = world.bodies.get_mut(ball_handle).unwrap(); + + if step == snapped_frame { + linvel = ball.linvel(); + angvel = ball.angvel(); + pos = *ball.position(); + } + + let shapeshifting_coll = world.colliders.get_mut(shapeshifting_coll_handle).unwrap(); + if step % 50 == 0 { + shape_idx = (shape_idx + 1) % 4; + shapeshifting_coll.set_shape(shapes[shape_idx].clone()) + } + + if step == 100 { + ball.set_linvel(linvel, true); + ball.set_angvel(angvel, true); + ball.set_position(pos, true); + step = snapped_frame; + } + + let ball_coll = world.colliders.get_mut(ball_coll_handle).unwrap(); + ball_coll.set_shape(SharedShape::ball(ball_rad * step as f32 * 2.0)); + + viewer.update_collider(ball_coll_handle, &world); + viewer.update_collider(shapeshifting_coll_handle, &world); + } + } + Ok(()) } diff --git a/examples3d/debug_sleeping_kinematic3.rs b/examples3d/debug_sleeping_kinematic3.rs index dcb18d8e3..8b7e978fb 100644 --- a/examples3d/debug_sleeping_kinematic3.rs +++ b/examples3d/debug_sleeping_kinematic3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -22,62 +22,68 @@ pub fn init_world(testbed: &mut Testbed) { let (slow_platform_handle, _) = world.insert(slow_platform_body, collider); /* - * Setup a callback to control the platform. + * Setup the platform control parameters. */ let start_tick = 500; let stop_tick = 1000; - testbed.add_callback(move |_, physics, _, run_state| { - if run_state.timestep_id == stop_tick { - println!("Both platforms should stop moving now and eventually fall asleep."); - // The platforms moved until this time. They must not be sleeping. - assert!(!physics.bodies[platform_handle].is_sleeping()); - assert!(!physics.bodies[slow_platform_handle].is_sleeping()); + /* + * Run the simulation. + */ + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 5.0, 10.0), Vec3::ZERO); - if let Some(slow_platform) = physics.bodies.get_mut(slow_platform_handle) { - slow_platform.set_linvel(Vector::ZERO, true); - } - if let Some(platform) = physics.bodies.get_mut(platform_handle) { - platform.set_linvel(Vector::ZERO, true); - } - } + let mut step_id = 0usize; + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + step_id += 1; - if run_state.timestep_id > stop_tick + 500 { - // Platforms should fall asleep shortly after we stopped moving them. - assert!(physics.bodies[platform_handle].is_sleeping()); - assert!(physics.bodies[slow_platform_handle].is_sleeping()); - } + let timestep_id = step_id; + let time = step_id as f32 * world.integration_parameters.dt; - if run_state.timestep_id < start_tick || run_state.timestep_id >= stop_tick { - return; - } else if run_state.timestep_id == start_tick { - println!("Platforms should start moving now and never stop."); - println!("The slow platform should move up and not sleep."); - // Platforms should have had plenty of time to fall asleep before we start moving them. - assert!(physics.bodies[platform_handle].is_sleeping()); - assert!(physics.bodies[slow_platform_handle].is_sleeping()); + if timestep_id == stop_tick { + println!("Both platforms should stop moving now and eventually fall asleep."); + // The platforms moved until this time. They must not be sleeping. + assert!(!world.bodies[platform_handle].is_sleeping()); + assert!(!world.bodies[slow_platform_handle].is_sleeping()); - let slow_velocity = Vector::new(0.0, 0.01, 0.0); - if let Some(slow_platform) = physics.bodies.get_mut(slow_platform_handle) { - slow_platform.set_linvel(slow_velocity, true); + if let Some(slow_platform) = world.bodies.get_mut(slow_platform_handle) { + slow_platform.set_linvel(Vector::ZERO, true); + } + if let Some(platform) = world.bodies.get_mut(platform_handle) { + platform.set_linvel(Vector::ZERO, true); + } } - } - let velocity = Vector::new( - 0.0, - (run_state.time * 2.0).cos(), - run_state.time.sin() * 2.0, - ); + if timestep_id > stop_tick + 500 { + // Platforms should fall asleep shortly after we stopped moving them. + assert!(world.bodies[platform_handle].is_sleeping()); + assert!(world.bodies[slow_platform_handle].is_sleeping()); + } - // Update the velocity-based kinematic body by setting its velocity. - if let Some(platform) = physics.bodies.get_mut(platform_handle) { - platform.set_linvel(velocity, true); - } - }); + if !(timestep_id < start_tick || timestep_id >= stop_tick) { + if timestep_id == start_tick { + println!("Platforms should start moving now and never stop."); + println!("The slow platform should move up and not sleep."); + // Platforms should have had plenty of time to fall asleep before we start moving them. + assert!(world.bodies[platform_handle].is_sleeping()); + assert!(world.bodies[slow_platform_handle].is_sleeping()); - /* - * Run the simulation. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 5.0, 10.0), Vec3::ZERO); + let slow_velocity = Vector::new(0.0, 0.01, 0.0); + if let Some(slow_platform) = world.bodies.get_mut(slow_platform_handle) { + slow_platform.set_linvel(slow_velocity, true); + } + } + + let velocity = Vector::new(0.0, (time * 2.0).cos(), time.sin() * 2.0); + + // Update the velocity-based kinematic body by setting its velocity. + if let Some(platform) = world.bodies.get_mut(platform_handle) { + platform.set_linvel(velocity, true); + } + } + } + } + Ok(()) } diff --git a/examples3d/debug_thin_cube_on_mesh3.rs b/examples3d/debug_thin_cube_on_mesh3.rs index f8e120c60..fe8882b10 100644 --- a/examples3d/debug_thin_cube_on_mesh3.rs +++ b/examples3d/debug_thin_cube_on_mesh3.rs @@ -1,10 +1,10 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; // This shows a bug when a cylinder is in contact with a very large // but very thin cuboid. In this case the EPA returns an incorrect // contact normal, resulting in the cylinder falling through the floor. -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -47,6 +47,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_triangle3.rs b/examples3d/debug_triangle3.rs index e3fda6799..5b16cbf03 100644 --- a/examples3d/debug_triangle3.rs +++ b/examples3d/debug_triangle3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -29,6 +29,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_trimesh3.rs b/examples3d/debug_trimesh3.rs index fc8d140f1..fc4c88f47 100644 --- a/examples3d/debug_trimesh3.rs +++ b/examples3d/debug_trimesh3.rs @@ -1,8 +1,8 @@ use kiss3d::color::LIGHT_GRAY; -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -46,11 +46,18 @@ pub fn init_world(testbed: &mut Testbed) { let rigid_body = RigidBodyBuilder::fixed().translation(Vector::new(0.0, 0.0, 0.0)); let collider = ColliderBuilder::trimesh(vtx, idx).expect("Could not create trimesh collider."); let (handle, _) = world.insert(rigid_body, collider); - testbed.set_initial_body_color(handle, LIGHT_GRAY); + viewer.set_initial_body_color(handle, LIGHT_GRAY); /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/debug_two_cubes3.rs b/examples3d/debug_two_cubes3.rs index ea16685c2..58d93fc4b 100644 --- a/examples3d/debug_two_cubes3.rs +++ b/examples3d/debug_two_cubes3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -19,6 +19,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/domino3.rs b/examples3d/domino3.rs index 34b3be5ed..25e793be6 100644 --- a/examples3d/domino3.rs +++ b/examples3d/domino3.rs @@ -1,8 +1,8 @@ use kiss3d::color::Color; -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -57,7 +57,7 @@ pub fn init_world(testbed: &mut Testbed) { let rigid_body = RigidBodyBuilder::dynamic().pose(position); let collider = ColliderBuilder::cuboid(thickness, width * 2.0, width); let (handle, _) = world.insert(rigid_body, collider); - testbed.set_initial_body_color(handle, colors[i % 2]); + viewer.set_initial_body_color(handle, colors[i % 2]); } else { skip -= 1; } @@ -72,6 +72,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/dynamic_trimesh3.rs b/examples3d/dynamic_trimesh3.rs index a0192f78e..eb63431a8 100644 --- a/examples3d/dynamic_trimesh3.rs +++ b/examples3d/dynamic_trimesh3.rs @@ -1,15 +1,18 @@ use obj::raw::object::Polygon; -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::parry::bounding_volume; use rapier3d::prelude::*; use std::fs::File; use std::io::BufReader; -pub fn init_world(testbed: &mut Testbed) { - do_init_world(testbed, false); +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { + run_impl(viewer, false).await } -pub fn do_init_world(testbed: &mut Testbed, use_convex_decomposition: bool) { +pub async fn run_impl( + viewer: &mut TestbedViewer, + use_convex_decomposition: bool, +) -> anyhow::Result<()> { /* * World */ @@ -121,8 +124,15 @@ pub fn do_init_world(testbed: &mut Testbed, use_convex_decomposition: bool) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } fn models() -> Vec { diff --git a/examples3d/fountain3.rs b/examples3d/fountain3.rs index 628683f74..bce8ffddb 100644 --- a/examples3d/fountain3.rs +++ b/examples3d/fountain3.rs @@ -1,9 +1,9 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; const MAX_NUMBER_OF_BODIES: usize = 400; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { let mut world = PhysicsWorld::new(); let rad = 0.5; @@ -21,64 +21,65 @@ pub fn init_world(testbed: &mut Testbed) { let (_handle, _) = world.insert(rigid_body, collider); } - // Callback that will be executed on the main loop to handle proximities. - testbed.add_callback(move |mut graphics, physics, _, run_state| { - let rigid_body = RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 10.0, 0.0)); - let handle = physics.bodies.insert(rigid_body); - let collider = match run_state.timestep_id % 3 { - 0 => ColliderBuilder::round_cylinder(rad, rad, rad / 10.0), - 1 => ColliderBuilder::cone(rad, rad), - _ => ColliderBuilder::cuboid(rad, rad, rad), - }; + /* + * Set up the viewer. + */ + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(30.0, 4.0, 30.0), Vec3::new(0.0, 1.0, 0.0)); - physics - .colliders - .insert_with_parent(collider, handle, &mut physics.bodies); + /* + * Run the simulation, spawning bodies from the fountain each step and + * pruning the oldest ones once the cap is reached (was an `add_callback`). + */ + let mut step_id = 0usize; + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + step_id += 1; - if let Some(graphics) = &mut graphics { - graphics.add_body(handle, &physics.bodies, &physics.colliders); - } + let rigid_body = RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 10.0, 0.0)); + let handle = world.bodies.insert(rigid_body); + let collider = match step_id % 3 { + 0 => ColliderBuilder::round_cylinder(rad, rad, rad / 10.0), + 1 => ColliderBuilder::cone(rad, rad), + _ => ColliderBuilder::cuboid(rad, rad, rad), + }; - if physics.bodies.len() > MAX_NUMBER_OF_BODIES { - let mut to_remove: Vec<(RigidBodyHandle, Vector)> = physics - .bodies - .iter() - .filter(|e| e.1.is_dynamic()) - .map(|e| (e.0, e.1.translation())) - .collect(); + world + .colliders + .insert_with_parent(collider, handle, &mut world.bodies); + viewer.add_body(handle, &world); - to_remove.sort_by(|a, b| { - (a.1.x.abs() + a.1.z.abs()) - .partial_cmp(&(b.1.x.abs() + b.1.z.abs())) - .unwrap() - .reverse() - }); + if world.bodies.len() > MAX_NUMBER_OF_BODIES { + let mut to_remove: Vec<(RigidBodyHandle, Vector)> = world + .bodies + .iter() + .filter(|e| e.1.is_dynamic()) + .map(|e| (e.0, e.1.translation())) + .collect(); - let num_to_remove = to_remove.len().saturating_sub(MAX_NUMBER_OF_BODIES); - for (handle, _) in &to_remove[..num_to_remove] { - physics.bodies.remove( - *handle, - &mut physics.islands, - &mut physics.colliders, - &mut physics.impulse_joints, - &mut physics.multibody_joints, - true, - ); + to_remove.sort_by(|a, b| { + (a.1.x.abs() + a.1.z.abs()) + .partial_cmp(&(b.1.x.abs() + b.1.z.abs())) + .unwrap() + .reverse() + }); - if let Some(graphics) = &mut graphics { - graphics.remove_body(*handle); + let num_to_remove = to_remove.len().saturating_sub(MAX_NUMBER_OF_BODIES); + for (handle, _) in &to_remove[..num_to_remove] { + world.bodies.remove( + *handle, + &mut world.islands, + &mut world.colliders, + &mut world.impulse_joints, + &mut world.multibody_joints, + true, + ); + viewer.remove_body(*handle); } } } - }); + } - /* - * Set up the testbed. - */ - testbed.set_physics_world(world); - // testbed - // .physics_state_mut() - // .integration_parameters - // .erp = 0.2; - testbed.look_at(Vec3::new(30.0, 4.0, 30.0), Vec3::new(0.0, 1.0, 0.0)); + Ok(()) } diff --git a/examples3d/gyroscopic3.rs b/examples3d/gyroscopic3.rs index ea85336b7..4dc7de649 100644 --- a/examples3d/gyroscopic3.rs +++ b/examples3d/gyroscopic3.rs @@ -1,9 +1,9 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; // Simulate the the Dzhanibekov effect: // https://en.wikipedia.org/wiki/Tennis_racket_theorem -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { let mut world = PhysicsWorld::new(); let shapes = vec![ @@ -21,6 +21,13 @@ pub fn init_world(testbed: &mut Testbed) { let collider = ColliderBuilder::compound(shapes); let (_body_handle, _) = world.insert(body, collider); - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(8.0, 0.0, 8.0), Vec3::new(0.0, 0.0, 0.0)); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(8.0, 0.0, 8.0), Vec3::new(0.0, 0.0, 0.0)); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/harness_capsules3.rs b/examples3d/harness_capsules3.rs index ad659f2f8..752ae5b2b 100644 --- a/examples3d/harness_capsules3.rs +++ b/examples3d/harness_capsules3.rs @@ -1,10 +1,8 @@ -use rapier_testbed3d::harness::{Harness, RapierBroadPhaseType}; +//! Headless benchmark: builds a capsule stack and steps it without any window +//! or testbed, printing the number of steps executed. use rapier3d::prelude::*; -pub fn init_world(harness: &mut Harness) { - /* - * World - */ +fn build_world() -> PhysicsWorld { let mut world = PhysicsWorld::new(); /* @@ -48,16 +46,14 @@ pub fn init_world(harness: &mut Harness) { offset -= 0.05 * rad * (num as f32 - 1.0); } - /* - * Set up the harness. - */ - harness.set_physics_world(world, RapierBroadPhaseType::default()); + world } fn main() { - let harness = &mut Harness::new_empty(); - init_world(harness); - harness.set_max_steps(10000); - harness.run(); - println!("{}", harness.state.timestep_id); + let mut world = build_world(); + let num_steps = 10000; + for _ in 0..num_steps { + world.step(); + } + println!("{num_steps}"); } diff --git a/examples3d/heightfield3.rs b/examples3d/heightfield3.rs index 110bbffcf..7bd0cb7d3 100644 --- a/examples3d/heightfield3.rs +++ b/examples3d/heightfield3.rs @@ -1,8 +1,8 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::na::ComplexField; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -89,6 +89,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/inverse_kinematics3.rs b/examples3d/inverse_kinematics3.rs index 60b3c349c..c11f2c3f2 100644 --- a/examples3d/inverse_kinematics3.rs +++ b/examples3d/inverse_kinematics3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -50,54 +50,58 @@ pub fn init_world(testbed: &mut Testbed) { let mut displacements = DVector::zeros(0); - testbed.add_callback(move |graphics, physics, _, _| { - let Some(graphics) = graphics else { return }; - if let Some((multibody, link_id)) = physics.multibody_joints.get_mut(last_link) { - // Ensure our displacement vector has the right number of elements. - if displacements.nrows() < multibody.ndofs() { - displacements = DVector::zeros(multibody.ndofs()); - } else { - displacements.fill(0.0); - } - - let Some(mouse_ray) = graphics.mouse().ray else { - return; - }; - - // Cast a ray on a plane aligned with the camera passing through the origin. - let fwd = graphics.camera_fwd_dir(); - let fwd_glam = Vector::new(-fwd.x, -fwd.y, -fwd.z).normalize(); - let halfspace = HalfSpace { normal: fwd_glam }; - let (mouse_orig, mouse_dir) = mouse_ray; - let ray = Ray::new( - Vector::new(mouse_orig.x, mouse_orig.y, mouse_orig.z), - Vector::new(mouse_dir.x, mouse_dir.y, mouse_dir.z), - ); - let Some(hit) = halfspace.cast_local_ray(&ray, f32::MAX, false) else { - return; - }; - let target_point = ray.point_at(hit); - - let options = InverseKinematicsOption { - constrained_axes: JointAxesMask::LIN_AXES, - ..Default::default() - }; - - multibody.inverse_kinematics( - &physics.bodies, - link_id, - &options, - &Pose::from_translation(target_point), - |_| true, - &mut displacements, - ); - multibody.apply_displacements(displacements.as_slice()); - } - }); - /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(0.0, 0.5, 2.5), Vec3::new(0.0, 0.5, 0.0)); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(0.0, 0.5, 2.5), Vec3::new(0.0, 0.5, 0.0)); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + + if let Some((multibody, link_id)) = world.multibody_joints.get_mut(last_link) { + // Ensure our displacement vector has the right number of elements. + if displacements.nrows() < multibody.ndofs() { + displacements = DVector::zeros(multibody.ndofs()); + } else { + displacements.fill(0.0); + } + + let Some(mouse_ray) = viewer.mouse().ray else { + continue; + }; + + // Cast a ray on a plane aligned with the camera passing through the origin. + let fwd = viewer.camera_fwd_dir(); + let fwd_glam = Vector::new(-fwd.x, -fwd.y, -fwd.z).normalize(); + let halfspace = HalfSpace { normal: fwd_glam }; + let (mouse_orig, mouse_dir) = mouse_ray; + let ray = Ray::new( + Vector::new(mouse_orig.x, mouse_orig.y, mouse_orig.z), + Vector::new(mouse_dir.x, mouse_dir.y, mouse_dir.z), + ); + let Some(hit) = halfspace.cast_local_ray(&ray, f32::MAX, false) else { + continue; + }; + let target_point = ray.point_at(hit); + + let options = InverseKinematicsOption { + constrained_axes: JointAxesMask::LIN_AXES, + ..Default::default() + }; + + multibody.inverse_kinematics( + &world.bodies, + link_id, + &options, + &Pose::from_translation(target_point), + |_| true, + &mut displacements, + ); + multibody.apply_displacements(displacements.as_slice()); + } + } + } + Ok(()) } diff --git a/examples3d/joint_motor_position3.rs b/examples3d/joint_motor_position3.rs index b4ad97f61..98a788134 100644 --- a/examples3d/joint_motor_position3.rs +++ b/examples3d/joint_motor_position3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -57,24 +57,30 @@ pub fn init_world(testbed: &mut Testbed) { target_angles.push(max_angle_limit); } - testbed.add_callback(move |_, physics, _, state| { - for ((_, joint), target) in physics.impulse_joints.iter().zip(target_angles.iter()) { - let rb1 = &physics.bodies[joint.body1()]; - let rb2 = &physics.bodies[joint.body2()]; - let revolute = joint.data.as_revolute().unwrap(); - println!( - "[Step {}] rev angle: {} (target = {})", - state.timestep_id, - revolute.angle(rb1.rotation(), rb2.rotation()), - target - ); - } - }); - /* * Set up the testbed. */ world.gravity = Vector::new(0.0, 0.0, 0.0); - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(15.0, 5.0, 42.0), Vec3::new(13.0, 1.0, 1.0)); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(15.0, 5.0, 42.0), Vec3::new(13.0, 1.0, 1.0)); + + let mut step_id = 0usize; + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + step_id += 1; + for ((_, joint), target) in world.impulse_joints.iter().zip(target_angles.iter()) { + let rb1 = &world.bodies[joint.body1()]; + let rb2 = &world.bodies[joint.body2()]; + let revolute = joint.data.as_revolute().unwrap(); + println!( + "[Step {}] rev angle: {} (target = {})", + step_id, + revolute.angle(rb1.rotation(), rb2.rotation()), + target + ); + } + } + } + Ok(()) } diff --git a/examples3d/joints3.rs b/examples3d/joints3.rs index 362247ea5..69e91ab44 100644 --- a/examples3d/joints3.rs +++ b/examples3d/joints3.rs @@ -1,4 +1,4 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; fn create_coupled_joints(world: &mut PhysicsWorld, origin: Vector, use_articulations: bool) { @@ -530,64 +530,59 @@ fn create_actuated_spherical_joints( } } -fn do_init_world(testbed: &mut Testbed, use_articulations: bool) { +fn build_scene(world: &mut PhysicsWorld, use_articulations: bool) { + create_prismatic_joints(world, Vector::new(20.0, 5.0, 0.0), 4, use_articulations); + create_actuated_prismatic_joints(world, Vector::new(25.0, 5.0, 0.0), 4, use_articulations); + create_revolute_joints(world, Vector::new(20.0, 0.0, 0.0), 3, use_articulations); + create_revolute_joints_with_limits(world, Vector::new(34.0, 0.0, 0.0), use_articulations); + create_fixed_joints(world, Vector::new(0.0, 10.0, 0.0), 10, use_articulations); + create_actuated_revolute_joints(world, Vector::new(20.0, 10.0, 0.0), 6, use_articulations); + create_actuated_spherical_joints(world, Vector::new(13.0, 10.0, 0.0), 3, use_articulations); + create_spherical_joints(world, 15, use_articulations); + create_spherical_joints_with_limits(world, Vector::new(-5.0, 0.0, 0.0), use_articulations); + create_coupled_joints(world, Vector::new(0.0, 20.0, 0.0), use_articulations); +} + +pub async fn run_impulse_joints(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ let mut world = PhysicsWorld::new(); - create_prismatic_joints( - &mut world, - Vector::new(20.0, 5.0, 0.0), - 4, - use_articulations, - ); - create_actuated_prismatic_joints( - &mut world, - Vector::new(25.0, 5.0, 0.0), - 4, - use_articulations, - ); - create_revolute_joints( - &mut world, - Vector::new(20.0, 0.0, 0.0), - 3, - use_articulations, - ); - create_revolute_joints_with_limits(&mut world, Vector::new(34.0, 0.0, 0.0), use_articulations); - create_fixed_joints( - &mut world, - Vector::new(0.0, 10.0, 0.0), - 10, - use_articulations, - ); - create_actuated_revolute_joints( - &mut world, - Vector::new(20.0, 10.0, 0.0), - 6, - use_articulations, - ); - create_actuated_spherical_joints( - &mut world, - Vector::new(13.0, 10.0, 0.0), - 3, - use_articulations, - ); - create_spherical_joints(&mut world, 15, use_articulations); - create_spherical_joints_with_limits(&mut world, Vector::new(-5.0, 0.0, 0.0), use_articulations); - create_coupled_joints(&mut world, Vector::new(0.0, 20.0, 0.0), use_articulations); + build_scene(&mut world, false); /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(15.0, 5.0, 42.0), Vec3::new(13.0, 1.0, 1.0)); -} + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(15.0, 5.0, 42.0), Vec3::new(13.0, 1.0, 1.0)); -pub fn init_world_with_joints(testbed: &mut Testbed) { - do_init_world(testbed, false) + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } -pub fn init_world_with_articulations(testbed: &mut Testbed) { - do_init_world(testbed, true) +pub async fn run_multibody_joints(viewer: &mut TestbedViewer) -> anyhow::Result<()> { + /* + * World + */ + let mut world = PhysicsWorld::new(); + + build_scene(&mut world, true); + + /* + * Set up the testbed. + */ + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(15.0, 5.0, 42.0), Vec3::new(13.0, 1.0, 1.0)); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/keva3.rs b/examples3d/keva3.rs index acd82026d..c1daa5e9c 100644 --- a/examples3d/keva3.rs +++ b/examples3d/keva3.rs @@ -1,9 +1,9 @@ use kiss3d::color::Color; -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; pub fn build_block( - testbed: &mut Testbed, + viewer: &mut TestbedViewer, world: &mut PhysicsWorld, half_extents: Vector, shift: Vector, @@ -47,7 +47,7 @@ pub fn build_block( let collider = ColliderBuilder::cuboid(dim.x, dim.y, dim.z); let (handle, _) = world.insert(rigid_body, collider); - testbed.set_initial_body_color(handle, color0); + viewer.set_initial_body_color(handle, color0); std::mem::swap(&mut color0, &mut color1); } } @@ -66,13 +66,13 @@ pub fn build_block( )); let collider = ColliderBuilder::cuboid(dim.x, dim.y, dim.z); let (handle, _) = world.insert(rigid_body, collider); - testbed.set_initial_body_color(handle, color0); + viewer.set_initial_body_color(handle, color0); std::mem::swap(&mut color0, &mut color1); } } } -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -104,7 +104,7 @@ pub fn init_world(testbed: &mut Testbed) { let numz = numx * 3 + 1; let block_width = numx as f32 * half_extents.z * 2.0; build_block( - testbed, + viewer, &mut world, half_extents, Vector::new(-block_width / 2.0, block_height, -block_width / 2.0), @@ -117,8 +117,15 @@ pub fn init_world(testbed: &mut Testbed) { println!("Num keva blocks: {num_blocks_built}"); /* - * Set up the testbed. + * Set up the viewer. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/locked_rotations3.rs b/examples3d/locked_rotations3.rs index 6909a541e..5da5d496c 100644 --- a/examples3d/locked_rotations3.rs +++ b/examples3d/locked_rotations3.rs @@ -1,10 +1,10 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; // This shows a bug when a cylinder is in contact with a very large // but very thin cuboid. In this case the EPA returns an incorrect // contact normal, resulting in the cylinder falling through the floor. -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -45,6 +45,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 3.0, 0.0), Vec3::new(0.0, 3.0, 0.0)); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 3.0, 0.0), Vec3::new(0.0, 3.0, 0.0)); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/mjcf3.rs b/examples3d/mjcf3.rs index a8c0441ed..069b21baf 100644 --- a/examples3d/mjcf3.rs +++ b/examples3d/mjcf3.rs @@ -1,8 +1,8 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; use rapier3d_mjcf::{MjcfLoaderOptions, MjcfMultibodyOptions, MjcfRobot}; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -41,6 +41,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(2.0, 2.0, 2.0), Vec3::new(0.0, 0.0, 0.0)); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(2.0, 2.0, 2.0), Vec3::new(0.0, 0.0, 0.0)); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/mujoco_menagerie3.rs b/examples3d/mujoco_menagerie3.rs index f14e82b0c..9a51ad93b 100644 --- a/examples3d/mujoco_menagerie3.rs +++ b/examples3d/mujoco_menagerie3.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicUsize, Ordering}; use kiss3d::color::Color; -use rapier_testbed3d::{RenderMaterial, Testbed, settings::StringDisplayMode}; +use rapier_testbed3d::{RenderMaterial, TestbedViewer, settings::StringDisplayMode}; use rapier3d::prelude::*; use rapier3d_mjcf::mjcf_rs::extras::Keyframe; use rapier3d_mjcf::{MjcfLoaderOptions, MjcfMultibodyOptions, MjcfRobot, MjcfRobotHandles}; @@ -14,7 +14,7 @@ use rapier3d_mjcf::{MjcfLoaderOptions, MjcfMultibodyOptions, MjcfRobot, MjcfRobo const SCENE_ROOTS: &[&str] = &["../mujoco_menagerie"]; static LAST_FRAMED_SCENE: AtomicUsize = AtomicUsize::new(usize::MAX); -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { let scenes = discover_scenes(SCENE_ROOTS); let labels: Vec = scenes .iter() @@ -30,23 +30,23 @@ pub fn init_world(testbed: &mut Testbed) { // Insert the checkboxes FIRST so they appear above the (taller) scene // list in the Example Settings panel — IndexMap preserves insertion // order on the first init_world call after the panel is fresh. - let use_multibody = testbed + let use_multibody = viewer .example_settings_mut() .get_or_set_bool("Use multibody joints", true); - let render_colliders = testbed + let render_colliders = viewer .example_settings_mut() .get_or_set_bool("Render colliders", false); - let render_visual_meshes = testbed + let render_visual_meshes = viewer .example_settings_mut() .get_or_set_bool("Render visual meshes", true); // Some MJCF files (apptronik_apollo for example) declare every geom // with `contype=conaffinity=0`, so even their "collision class" // capsules end up in the visual channel. This toggle lets you hide // those non-mesh visual geoms while keeping the .obj-derived ones. - let render_visual_primitives = testbed + let render_visual_primitives = viewer .example_settings_mut() .get_or_set_bool("Render visual primitives", false); - let disable_collisions = testbed + let disable_collisions = viewer .example_settings_mut() .get_or_set_bool("Disable collisions", true); // Drives every actuator each frame with a zero control input — MuJoCo's @@ -54,7 +54,7 @@ pub fn init_world(testbed: &mut Testbed) { // position servo) this holds each joint at its neutral pose, e.g. keeping // the flybody legs spread instead of letting them retract. Only effective // on the multibody path (controls are applied to multibody joints). - let enable_controls = testbed + let enable_controls = viewer .example_settings_mut() .get_or_set_bool("Enable joint controls", true); // Uniformly scales actuator strength (gains + force limits) when driving the @@ -62,13 +62,13 @@ pub fn init_world(testbed: &mut Testbed) { // (below). Turn it down to make a servo-driven move between keyframes ease in // instead of snapping for models whose actuators are otherwise strong enough // to arrive almost instantly. - testbed + viewer .example_settings_mut() .get_or_set_f32("Actuator strength", 1.0, 0.02..=2.0); // MJCF `` passive springs (integrated implicitly on the // multibody path). Unchecking removes them — useful to see a model without // its return springs (e.g. robotiq's gripper preload, cassie's leg springs). - let enable_springs = testbed + let enable_springs = viewer .example_settings_mut() .get_or_set_bool("Enable joint springs", true); // The keyframe to apply right after loading, so the robot starts in a @@ -76,7 +76,7 @@ pub fn init_world(testbed: &mut Testbed) { // all-zeros configuration). The real option list is filled once the model // is loaded (it depends on which keyframes the model declares); reserve the // slot here so it renders above the taller scene list. - testbed + viewer .example_settings_mut() .get_or_set_string("Keyframe", 0, vec!["(none)".to_string()]); // When the actuators drive toward the keyframe (multibody + controls on), @@ -85,15 +85,15 @@ pub fn init_world(testbed: &mut Testbed) { // a keyframe change in that mode. Without controls there's nothing to track // the target, so the keyframe is applied by reloading (a restart) instead. let keyframe_is_live = use_multibody && enable_controls; - testbed + viewer .example_settings_mut() .set_restart_on_change("Keyframe", !keyframe_is_live); // The controls callback reads "Actuator strength" live each step, so don't // restart the sim when the slider moves. - testbed + viewer .example_settings_mut() .set_restart_on_change("Actuator strength", false); - let selected = testbed.example_settings_mut().get_or_set_string_with( + let selected = viewer.example_settings_mut().get_or_set_string_with( "Scene", default_idx, labels, @@ -105,7 +105,7 @@ pub fn init_world(testbed: &mut Testbed) { let scene_changed = LAST_FRAMED_SCENE.swap(selected, Ordering::Relaxed) != selected; if scenes.is_empty() { - testbed.example_settings_mut().set_label("NO MODEL FOUND", "Consider cloning `google-deepmind/mujoco_menagerie`\ninto the same parent directory as the rapier repo.") + viewer.example_settings_mut().set_label("NO MODEL FOUND", "Consider cloning `google-deepmind/mujoco_menagerie`\ninto the same parent directory as the rapier repo.") } let mut world = PhysicsWorld::new(); @@ -118,7 +118,7 @@ pub fn init_world(testbed: &mut Testbed) { use_multibody, disable_collisions, enable_springs, - testbed, + viewer, scene_changed, ) { Ok((robot, body_handles, mb)) => { @@ -129,7 +129,7 @@ pub fn init_world(testbed: &mut Testbed) { } add_floor(&mut world); } - testbed.set_physics_world(world); + viewer.set_world(&mut world); // "Enable joint controls" → keep the model at the *currently selected* // keyframe every step. Re-registered on each `init_world` (the testbed @@ -145,7 +145,7 @@ pub fn init_world(testbed: &mut Testbed) { // barely move under servo force alone). Each keyframe's hold control is its // `ctrl` (or a target derived from its `qpos`), precomputed here; "(none)" // falls back to zeros (hold the neutral pose). - if enable_controls + let mut controls_state = if enable_controls && let Some(handles) = mb_handles && let Some(robot) = loaded.as_ref().map(|(robot, _)| robot.clone()) { @@ -154,73 +154,84 @@ pub fn init_world(testbed: &mut Testbed) { .iter() .map(|k| robot.keyframe_controls(k)) .collect(); - let neutral = vec![0.0 as Real; handles.actuators.len()]; + let neutral = vec![0.0_f32; handles.actuators.len()]; // Seed with the keyframe already applied at load so we don't re-apply // it on the first step. - let mut last_selected = testbed + let last_selected = viewer .example_settings_mut() .get_string_id("Keyframe") .unwrap_or(0); - testbed.add_callback(move |graphics, physics, _, _| { - let settings = graphics.and_then(|g| g.settings.as_deref()); - // Selection index 0 is "(none)"; index `i + 1` is keyframe `i`. - let selected = settings - .and_then(|s| s.get_string_id("Keyframe")) - .unwrap_or(last_selected); - // Live actuator-strength scale (1.0 = as authored; lower = softer). - let gain: Real = settings - .and_then(|s| s.get_f32("Actuator strength")) - .unwrap_or(1.0); - let keyframe = selected.checked_sub(1).and_then(|i| robot.keyframes.get(i)); - if selected != last_selected { - // if let Some(key) = keyframe { - // handles.apply_keyframe( - // &mut physics.bodies, - // &mut physics.multibody_joints, - // &robot, - // key, - // ); - // } - last_selected = selected; - } - let ctrl = selected - .checked_sub(1) - .and_then(|i| per_keyframe_ctrl.get(i)) - .unwrap_or(&neutral); - handles.apply_controls_multibody_scaled( - &mut physics.bodies, - &mut physics.multibody_joints, - ctrl, - gain, - ); - }); - } + Some((handles, robot, per_keyframe_ctrl, neutral, last_selected)) + } else { + None + }; // MJCF is Z-up by default — keep the camera convention consistent // with the model so orbit controls feel right. - testbed.set_up_axis(Vec3::Z); + viewer.set_up_axis(Vec3::Z); if !use_multibody { - testbed.integration_parameters_mut().dt = 1.0 / 240.0; - testbed.integration_parameters_mut().num_solver_iterations = 12; + world.integration_parameters.dt = 1.0 / 240.0; + world.integration_parameters.num_solver_iterations = 12; } else { - testbed.integration_parameters_mut().num_internal_pgs_iterations = 4; + world.integration_parameters.num_internal_pgs_iterations = 4; } // Refit the camera only on actual scene changes, otherwise a // checkbox toggle in the Example Settings panel would snap the // camera out from under the user every time. if scene_changed { - testbed.request_frame_all(); + viewer.request_frame_all(); } if let Some((robot, body_handles)) = loaded && render_visual_meshes { - register_visual_meshes(testbed, &robot, &body_handles, render_visual_primitives); + register_visual_meshes(viewer, &robot, &body_handles, render_visual_primitives); } - testbed.set_colliders_visible(render_colliders); - testbed.set_body_render_meshes_visible(render_visual_meshes); + viewer.set_colliders_visible(render_colliders); + viewer.set_body_render_meshes_visible(render_visual_meshes); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + + if let Some((handles, robot, per_keyframe_ctrl, neutral, last_selected)) = + controls_state.as_mut() + { + let settings = viewer.example_settings(); + // Selection index 0 is "(none)"; index `i + 1` is keyframe `i`. + let selected = settings.get_string_id("Keyframe").unwrap_or(*last_selected); + // Live actuator-strength scale (1.0 = as authored; lower = softer). + let gain: Real = settings.get_f32("Actuator strength").unwrap_or(1.0); + let keyframe = selected.checked_sub(1).and_then(|i| robot.keyframes.get(i)); + let _ = keyframe; + if selected != *last_selected { + // if let Some(key) = keyframe { + // handles.apply_keyframe( + // &mut world.bodies, + // &mut world.multibody_joints, + // &robot, + // key, + // ); + // } + *last_selected = selected; + } + let ctrl = selected + .checked_sub(1) + .and_then(|i| per_keyframe_ctrl.get(i)) + .map(|v| v.as_slice()) + .unwrap_or(neutral.as_slice()); + handles.apply_controls_multibody_scaled( + &mut world.bodies, + &mut world.multibody_joints, + ctrl, + gain, + ); + } + } + } + Ok(()) } fn loader_options() -> MjcfLoaderOptions { @@ -293,15 +304,21 @@ fn merge_sibling_keyframes(robot: &mut MjcfRobot, scene_path: &Path) { } match MjcfRobot::from_file(&kf_path, loader_options()) { Ok((kf_robot, _)) => { - let existing: std::collections::HashSet = - robot.keyframes.iter().filter_map(|k| k.name.clone()).collect(); + let existing: std::collections::HashSet = robot + .keyframes + .iter() + .filter_map(|k| k.name.clone()) + .collect(); for k in kf_robot.keyframes { if k.name.as_ref().is_none_or(|n| !existing.contains(n)) { robot.keyframes.push(k); } } } - Err(e) => eprintln!("Failed to load sibling keyframes `{}`: {e}", kf_path.display()), + Err(e) => eprintln!( + "Failed to load sibling keyframes `{}`: {e}", + kf_path.display() + ), } } @@ -314,7 +331,7 @@ fn merge_sibling_keyframes(robot: &mut MjcfRobot, scene_path: &Path) { /// preserved by name across re-inits (toggling another setting re-runs /// `init_world`, and a stale index could point at a different keyframe). fn configure_keyframe_setting( - testbed: &mut Testbed, + viewer: &mut TestbedViewer, robot: &MjcfRobot, scene_changed: bool, ) -> Option { @@ -334,14 +351,14 @@ fn configure_keyframe_setting( default_idx } else { // Preserve the prior selection by name (the index may have shifted). - testbed + viewer .example_settings_mut() .get_string("Keyframe") .and_then(|prev| names.iter().position(|n| n == prev)) .unwrap_or(default_idx) }; - testbed + viewer .example_settings_mut() .set_string("Keyframe", selected_idx, names); @@ -357,10 +374,14 @@ fn load_into_world( use_multibody: bool, disable_collisions: bool, enable_springs: bool, - testbed: &mut Testbed, + viewer: &mut TestbedViewer, scene_changed: bool, ) -> Result< - (MjcfRobot, Vec>, Option), + ( + MjcfRobot, + Vec>, + Option, + ), Box, > { let (mut robot, model) = MjcfRobot::from_file(path, loader_options())?; @@ -408,7 +429,7 @@ fn load_into_world( // Now that the model's keyframes are known, populate the "Keyframe" picker // and resolve the user's selection (`None` ⇒ "(none)" ⇒ don't apply one). - let keyframe = configure_keyframe_setting(testbed, &robot, scene_changed); + let keyframe = configure_keyframe_setting(viewer, &robot, scene_changed); let (body_handles, mb_handles) = if use_multibody { let handles = robot.clone().insert_using_multibody_joints( @@ -419,12 +440,7 @@ fn load_into_world( mb_options, ); if let Some(key) = &keyframe { - handles.apply_keyframe( - &mut world.bodies, - &mut world.multibody_joints, - &robot, - key, - ); + handles.apply_keyframe(&mut world.bodies, &mut world.multibody_joints, &robot, key); } let body_handles = handles .bodies @@ -460,7 +476,7 @@ fn load_into_world( /// texture file path flow through to the testbed when both are /// available. fn register_visual_meshes( - testbed: &mut Testbed, + viewer: &mut TestbedViewer, robot: &MjcfRobot, body_handles: &[Option], include_primitives: bool, @@ -501,7 +517,7 @@ fn register_visual_meshes( reflectance: m.reflectance, emissive: m.emissive, }); - testbed.add_body_render_mesh( + viewer.add_body_render_mesh( *handle, &vm.shape, vm.local_pose, diff --git a/examples3d/newton_cradle3.rs b/examples3d/newton_cradle3.rs index 0a5ff1829..f39300aaf 100644 --- a/examples3d/newton_cradle3.rs +++ b/examples3d/newton_cradle3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -38,6 +38,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/one_way_platforms3.rs b/examples3d/one_way_platforms3.rs index d6d0c140b..3a14f98ce 100644 --- a/examples3d/one_way_platforms3.rs +++ b/examples3d/one_way_platforms3.rs @@ -1,4 +1,4 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; struct OneWayPlatformHook { @@ -53,7 +53,7 @@ impl PhysicsHooks for OneWayPlatformHook { } } -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -81,37 +81,40 @@ pub fn init_world(testbed: &mut Testbed) { }; /* - * Spawn cubes at regular intervals and apply a custom gravity - * depending on their position. + * Set up the testbed. */ - testbed.add_callback(move |graphics, physics, _, run_state| { - if run_state.timestep_id % 200 == 0 && physics.bodies.len() <= 7 { - // Spawn a new cube. - let collider = ColliderBuilder::cuboid(1.0, 2.0, 1.5); - let body = RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 6.0, 20.0)); - let handle = physics.bodies.insert(body); - physics - .colliders - .insert_with_parent(collider, handle, &mut physics.bodies); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 0.0, 0.0), Vec3::ZERO); + + let mut step_id = 0usize; + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step_with_events(&physics_hooks, &()); + step_id += 1; + + // Spawn cubes at regular intervals and apply a custom gravity + // depending on their position. + if step_id.is_multiple_of(200) && world.bodies.len() <= 7 { + // Spawn a new cube. + let collider = ColliderBuilder::cuboid(1.0, 2.0, 1.5); + let body = RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 6.0, 20.0)); + let handle = world.bodies.insert(body); + world + .colliders + .insert_with_parent(collider, handle, &mut world.bodies); - if let Some(graphics) = graphics { - graphics.add_body(handle, &physics.bodies, &physics.colliders); + viewer.add_body(handle, &world); } - } - for handle in physics.islands.active_bodies() { - let body = physics.bodies.get_mut(handle).unwrap(); - if body.position().translation.y > 1.0 { - body.set_gravity_scale(1.0, false); - } else if body.position().translation.y < -1.0 { - body.set_gravity_scale(-1.0, false); + for handle in world.islands.active_bodies() { + let body = world.bodies.get_mut(handle).unwrap(); + if body.position().translation.y > 1.0 { + body.set_gravity_scale(1.0, false); + } else if body.position().translation.y < -1.0 { + body.set_gravity_scale(-1.0, false); + } } } - }); - - /* - * Set up the testbed. - */ - testbed.set_physics_world_with_hooks(world, physics_hooks); - testbed.look_at(Vec3::new(100.0, 0.0, 0.0), Vec3::ZERO); + } + Ok(()) } diff --git a/examples3d/platform3.rs b/examples3d/platform3.rs index 037cc43be..ce4cdfc24 100644 --- a/examples3d/platform3.rs +++ b/examples3d/platform3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -63,35 +63,40 @@ pub fn init_world(testbed: &mut Testbed) { let (position_based_platform_handle, _) = world.insert(platform_body, collider); /* - * Setup a callback to control the platform. + * Run the simulation. */ - testbed.add_callback(move |_, physics, _, run_state| { - let velocity = Vector::new( - 0.0, - (run_state.time * 2.0).cos(), - run_state.time.sin() * 2.0, - ); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 5.0, 10.0), Vec3::ZERO); - // Update the velocity-based kinematic body by setting its velocity. - if let Some(platform) = physics.bodies.get_mut(velocity_based_platform_handle) { - platform.set_linvel(velocity, true); - platform.set_angvel(Vector::new(0.0, 1.0, 0.0), true); - } + let mut step_id = 0usize; + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + step_id += 1; - // Update the position-based kinematic body by setting its next position. - if let Some(platform) = physics.bodies.get_mut(position_based_platform_handle) { - let next_tra = platform.translation() + (-velocity * physics.integration_parameters.dt); - let next_rot = platform.rotation(); - let delta_rot = Rotation::from_rotation_y(-0.5 * physics.integration_parameters.dt); - let next_rot = delta_rot * next_rot; - platform.set_next_kinematic_translation(next_tra); - platform.set_next_kinematic_rotation(next_rot); - } - }); + let velocity = Vector::new( + 0.0, + ((step_id as f32 * world.integration_parameters.dt) * 2.0).cos(), + (step_id as f32 * world.integration_parameters.dt).sin() * 2.0, + ); - /* - * Run the simulation. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 5.0, 10.0), Vec3::ZERO); + // Update the velocity-based kinematic body by setting its velocity. + if let Some(platform) = world.bodies.get_mut(velocity_based_platform_handle) { + platform.set_linvel(velocity, true); + platform.set_angvel(Vector::new(0.0, 1.0, 0.0), true); + } + + // Update the position-based kinematic body by setting its next position. + if let Some(platform) = world.bodies.get_mut(position_based_platform_handle) { + let next_tra = + platform.translation() + (-velocity * world.integration_parameters.dt); + let next_rot = platform.rotation(); + let delta_rot = Rotation::from_rotation_y(-0.5 * world.integration_parameters.dt); + let next_rot = delta_rot * next_rot; + platform.set_next_kinematic_translation(next_tra); + platform.set_next_kinematic_rotation(next_rot); + } + } + } + Ok(()) } diff --git a/examples3d/primitives3.rs b/examples3d/primitives3.rs index b8f2be1e2..7e10696e9 100644 --- a/examples3d/primitives3.rs +++ b/examples3d/primitives3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -60,8 +60,19 @@ pub fn init_world(testbed: &mut Testbed) { } /* - * Set up the testbed. + * Set up the viewer. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + /* + * Run the simulation. + */ + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + + Ok(()) } diff --git a/examples3d/restitution3.rs b/examples3d/restitution3.rs index 3af841f91..90c090c84 100644 --- a/examples3d/restitution3.rs +++ b/examples3d/restitution3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -36,6 +36,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(0.0, 3.0, 30.0), Vec3::new(0.0, 3.0, 0.0)); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(0.0, 3.0, 30.0), Vec3::new(0.0, 3.0, 0.0)); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/rope_joints3.rs b/examples3d/rope_joints3.rs index 36868ad01..9c81eeb35 100644 --- a/examples3d/rope_joints3.rs +++ b/examples3d/rope_joints3.rs @@ -1,10 +1,10 @@ use crate::utils::character::{self, CharacterControlMode}; use kiss3d::color::Color; -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::control::{KinematicCharacterController, PidController}; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -61,7 +61,7 @@ pub fn init_world(testbed: &mut Testbed) { let collider = ColliderBuilder::cuboid(0.15, 0.3, 0.15); let (character_handle, _) = world.insert(rigid_body, collider); - testbed.set_initial_body_color( + viewer.set_initial_body_color( character_handle, Color::new(1., 131. / 255., 244.0 / 255., 1.0), ); @@ -80,28 +80,31 @@ pub fn init_world(testbed: &mut Testbed) { world.insert_impulse_joint(character_handle, child_handle, joint); /* - * Callback to update the character based on user inputs. + * State to update the character based on user inputs. */ let mut control_mode = CharacterControlMode::Kinematic(0.1); let mut controller = KinematicCharacterController::default(); let mut pid = PidController::default(); - testbed.add_callback(move |graphics, physics, _, _| { - if let Some(graphics) = graphics { + /* + * Set up the testbed. + */ + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::new(0.0, 0.0, 0.0)); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + character::update_character( - graphics, - physics, + viewer, + &mut world, &mut control_mode, &mut controller, &mut pid, character_handle, ); } - }); - - /* - * Set up the testbed. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::new(0.0, 0.0, 0.0)); + } + Ok(()) } diff --git a/examples3d/sensor3.rs b/examples3d/sensor3.rs index 83c8377a5..257180d2e 100644 --- a/examples3d/sensor3.rs +++ b/examples3d/sensor3.rs @@ -1,8 +1,8 @@ use kiss3d::color::Color; -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -39,7 +39,7 @@ pub fn init_world(testbed: &mut Testbed) { let collider = ColliderBuilder::cuboid(rad, rad, rad); let (handle, _) = world.insert(rigid_body, collider); - testbed.set_initial_body_color(handle, Color::new(0.5, 0.5, 1.0, 1.0)); + viewer.set_initial_body_color(handle, Color::new(0.5, 0.5, 1.0, 1.0)); } } @@ -63,34 +63,45 @@ pub fn init_world(testbed: &mut Testbed) { .active_events(ActiveEvents::COLLISION_EVENTS); world.insert_collider(sensor_collider, Some(sensor_handle)); - testbed.set_initial_body_color(sensor_handle, Color::new(0.5, 1.0, 1.0, 1.0)); + viewer.set_initial_body_color(sensor_handle, Color::new(0.5, 1.0, 1.0, 1.0)); - // Callback that will be executed on the main loop to handle proximities. - testbed.add_callback(move |mut graphics, physics, events, _| { - while let Ok(prox) = events.collision_events.try_recv() { - let color = if prox.started() { - Color::new(1.0, 1.0, 0.0, 1.0) - } else { - Color::new(0.5, 0.5, 1.0, 1.0) - }; + /* + * Set up the viewer. + */ + viewer.set_world(&mut world); + let (collision_send, collision_recv) = std::sync::mpsc::channel(); + let (contact_force_send, _contact_force_recv) = std::sync::mpsc::channel(); + let event_handler = ChannelEventCollector::new(collision_send, contact_force_send); + viewer.look_at(Vec3::new(6.0, 4.0, 6.0), Vec3::new(0.0, 1.0, 0.0)); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step_with_events(&(), &event_handler); + + // Handle proximities. + let mut proximities = Vec::new(); + while let Ok(prox) = collision_recv.try_recv() { + proximities.push(prox); + } + + for prox in proximities { + let color = if prox.started() { + Color::new(1.0, 1.0, 0.0, 1.0) + } else { + Color::new(0.5, 0.5, 1.0, 1.0) + }; - let parent_handle1 = physics.colliders[prox.collider1()].parent().unwrap(); - let parent_handle2 = physics.colliders[prox.collider2()].parent().unwrap(); + let parent_handle1 = world.colliders[prox.collider1()].parent().unwrap(); + let parent_handle2 = world.colliders[prox.collider2()].parent().unwrap(); - if let Some(graphics) = &mut graphics { if parent_handle1 != ground_handle && parent_handle1 != sensor_handle { - graphics.set_body_color(parent_handle1, color, false); + viewer.set_body_color(parent_handle1, color, false); } if parent_handle2 != ground_handle && parent_handle2 != sensor_handle { - graphics.set_body_color(parent_handle2, color, false); + viewer.set_body_color(parent_handle2, color, false); } } } - }); - - /* - * Set up the testbed. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(6.0, 4.0, 6.0), Vec3::new(0.0, 1.0, 0.0)); + } + Ok(()) } diff --git a/examples3d/spring_joints3.rs b/examples3d/spring_joints3.rs index 85c0ba32f..c0fffa332 100644 --- a/examples3d/spring_joints3.rs +++ b/examples3d/spring_joints3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -46,6 +46,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(15.0, 5.0, 42.0), Vec3::new(13.0, 1.0, 1.0)); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(15.0, 5.0, 42.0), Vec3::new(13.0, 1.0, 1.0)); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/stress_tests/balls3.rs b/examples3d/stress_tests/balls3.rs index c31b1b0e6..e4026bf73 100644 --- a/examples3d/stress_tests/balls3.rs +++ b/examples3d/stress_tests/balls3.rs @@ -1,8 +1,8 @@ use glam::Vec3; -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -44,6 +44,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/stress_tests/boxes3.rs b/examples3d/stress_tests/boxes3.rs index 20139dfa1..5e12add18 100644 --- a/examples3d/stress_tests/boxes3.rs +++ b/examples3d/stress_tests/boxes3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -50,6 +50,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/stress_tests/capsules3.rs b/examples3d/stress_tests/capsules3.rs index e953fff61..3b33d447e 100644 --- a/examples3d/stress_tests/capsules3.rs +++ b/examples3d/stress_tests/capsules3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -51,6 +51,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/stress_tests/ccd3.rs b/examples3d/stress_tests/ccd3.rs index 004055ec3..65ce31f5a 100644 --- a/examples3d/stress_tests/ccd3.rs +++ b/examples3d/stress_tests/ccd3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -66,6 +66,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/stress_tests/compound3.rs b/examples3d/stress_tests/compound3.rs index 8fd656706..1c53d5742 100644 --- a/examples3d/stress_tests/compound3.rs +++ b/examples3d/stress_tests/compound3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -56,6 +56,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/stress_tests/convex_polyhedron3.rs b/examples3d/stress_tests/convex_polyhedron3.rs index 1ac1b27a1..3f153c4fa 100644 --- a/examples3d/stress_tests/convex_polyhedron3.rs +++ b/examples3d/stress_tests/convex_polyhedron3.rs @@ -1,9 +1,9 @@ use rand::distr::{Distribution, StandardUniform}; use rand::{SeedableRng, rngs::StdRng}; -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -63,6 +63,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/stress_tests/heightfield3.rs b/examples3d/stress_tests/heightfield3.rs index 7a04f0935..bd8db520a 100644 --- a/examples3d/stress_tests/heightfield3.rs +++ b/examples3d/stress_tests/heightfield3.rs @@ -1,8 +1,8 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::na::ComplexField; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -67,6 +67,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/stress_tests/joint_ball3.rs b/examples3d/stress_tests/joint_ball3.rs index 03c895e40..f669d2bc5 100644 --- a/examples3d/stress_tests/joint_ball3.rs +++ b/examples3d/stress_tests/joint_ball3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -51,9 +51,16 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at( + viewer.set_world(&mut world); + viewer.look_at( Vec3::new(-110.0, -46.0, 170.0), Vec3::new(54.0, -38.0, 29.0), ); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/stress_tests/joint_fixed3.rs b/examples3d/stress_tests/joint_fixed3.rs index 00ba5bb9c..3e370b4b3 100644 --- a/examples3d/stress_tests/joint_fixed3.rs +++ b/examples3d/stress_tests/joint_fixed3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -72,6 +72,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(-38.0, 14.0, 108.0), Vec3::new(46.0, 12.0, 23.0)); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(-38.0, 14.0, 108.0), Vec3::new(46.0, 12.0, 23.0)); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/stress_tests/joint_prismatic3.rs b/examples3d/stress_tests/joint_prismatic3.rs index f149f4027..6de245264 100644 --- a/examples3d/stress_tests/joint_prismatic3.rs +++ b/examples3d/stress_tests/joint_prismatic3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -51,6 +51,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(262.0, 63.0, 124.0), Vec3::new(101.0, 4.0, -3.0)); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(262.0, 63.0, 124.0), Vec3::new(101.0, 4.0, -3.0)); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/stress_tests/joint_revolute3.rs b/examples3d/stress_tests/joint_revolute3.rs index 8e3405d70..be0077af9 100644 --- a/examples3d/stress_tests/joint_revolute3.rs +++ b/examples3d/stress_tests/joint_revolute3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -64,9 +64,16 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at( + viewer.set_world(&mut world); + viewer.look_at( Vec3::new(478.0, 83.0, 228.0), Vec3::new(134.0, 83.0, -116.0), ); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/stress_tests/keva3.rs b/examples3d/stress_tests/keva3.rs index 5d35eb6a6..3588bddb2 100644 --- a/examples3d/stress_tests/keva3.rs +++ b/examples3d/stress_tests/keva3.rs @@ -1,10 +1,10 @@ use kiss3d::color::Color; -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::glamx::Vec3Swizzles; use rapier3d::prelude::*; pub fn build_block( - testbed: &mut Testbed, + viewer: &mut TestbedViewer, bodies: &mut RigidBodySet, colliders: &mut ColliderSet, half_extents: Vec3, @@ -47,7 +47,7 @@ pub fn build_block( let collider = ColliderBuilder::cuboid(dim.x, dim.y, dim.z); colliders.insert_with_parent(collider, handle, bodies); - testbed.set_initial_body_color(handle, color0); + viewer.set_initial_body_color(handle, color0); std::mem::swap(&mut color0, &mut color1); } } @@ -67,13 +67,13 @@ pub fn build_block( let handle = bodies.insert(rigid_body); let collider = ColliderBuilder::cuboid(dim.x, dim.y, dim.z); colliders.insert_with_parent(collider, handle, bodies); - testbed.set_initial_body_color(handle, color0); + viewer.set_initial_body_color(handle, color0); std::mem::swap(&mut color0, &mut color1); } } } -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -104,7 +104,7 @@ pub fn init_world(testbed: &mut Testbed) { let numz = numx * 3 + 1; let block_width = numx as f32 * half_extents.z * 2.0; build_block( - testbed, + viewer, &mut world.bodies, &mut world.colliders, half_extents, @@ -117,6 +117,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/stress_tests/many_kinematics3.rs b/examples3d/stress_tests/many_kinematics3.rs index 75c794919..9fc8aaf52 100644 --- a/examples3d/stress_tests/many_kinematics3.rs +++ b/examples3d/stress_tests/many_kinematics3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -40,25 +40,31 @@ pub fn init_world(testbed: &mut Testbed) { } } - testbed.add_callback(move |_, physics, _, _| { - for (_, rb) in physics.bodies.iter_mut() { - let mut linvel = rb.linvel(); + /* + * Set up the testbed. + */ + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); - for dim in 0..3 { - if (linvel[dim] > 0.0 && rb.translation()[dim] > (shift * num as f32) / 2.0) - || (linvel[dim] < 0.0 && rb.translation()[dim] < -(shift * num as f32) / 2.0) - { - linvel[dim] = -linvel[dim]; + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + + for (_, rb) in world.bodies.iter_mut() { + let mut linvel = rb.linvel(); + + for dim in 0..3 { + if (linvel[dim] > 0.0 && rb.translation()[dim] > (shift * num as f32) / 2.0) + || (linvel[dim] < 0.0 + && rb.translation()[dim] < -(shift * num as f32) / 2.0) + { + linvel[dim] = -linvel[dim]; + } } - } - rb.set_linvel(linvel, false); + rb.set_linvel(linvel, false); + } } - }); - - /* - * Set up the testbed. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + } + Ok(()) } diff --git a/examples3d/stress_tests/many_pyramids3.rs b/examples3d/stress_tests/many_pyramids3.rs index edcb9e033..f4f6e0414 100644 --- a/examples3d/stress_tests/many_pyramids3.rs +++ b/examples3d/stress_tests/many_pyramids3.rs @@ -1,4 +1,4 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; fn create_pyramid( @@ -26,7 +26,7 @@ fn create_pyramid( } } -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -69,8 +69,15 @@ pub fn init_world(testbed: &mut Testbed) { } /* - * Set up the testbed. + * Set up the viewer. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/stress_tests/many_sleep3.rs b/examples3d/stress_tests/many_sleep3.rs index f7d66e7b6..21f5b64b1 100644 --- a/examples3d/stress_tests/many_sleep3.rs +++ b/examples3d/stress_tests/many_sleep3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -45,6 +45,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/stress_tests/many_static3.rs b/examples3d/stress_tests/many_static3.rs index 83a50bf8a..4c383c2ad 100644 --- a/examples3d/stress_tests/many_static3.rs +++ b/examples3d/stress_tests/many_static3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -43,6 +43,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/stress_tests/mod.rs b/examples3d/stress_tests/mod.rs index 1af42a7e4..daa7609fa 100644 --- a/examples3d/stress_tests/mod.rs +++ b/examples3d/stress_tests/mod.rs @@ -1,53 +1,22 @@ -use rapier_testbed3d::Example; - -mod balls3; -mod boxes3; -mod capsules3; -mod ccd3; -mod compound3; -mod convex_polyhedron3; -mod heightfield3; -mod joint_ball3; -mod joint_fixed3; -mod joint_prismatic3; -mod joint_revolute3; -mod keva3; -mod many_kinematics3; -mod many_pyramids3; -mod many_sleep3; -mod many_static3; -mod pyramid3; -mod ray_cast3; -mod stacks3; -mod trimesh3; - -pub fn builders() -> Vec { - const STRESS: &str = "Stress Tests"; - - vec![ - Example::new(STRESS, "Balls", balls3::init_world), - Example::new(STRESS, "Boxes", boxes3::init_world), - Example::new(STRESS, "Capsules", capsules3::init_world), - Example::new(STRESS, "CCD", ccd3::init_world), - Example::new(STRESS, "Compound", compound3::init_world), - Example::new(STRESS, "Convex polyhedron", convex_polyhedron3::init_world), - Example::new(STRESS, "Many kinematics", many_kinematics3::init_world), - Example::new(STRESS, "Many static", many_static3::init_world), - Example::new(STRESS, "Many sleep", many_sleep3::init_world), - Example::new(STRESS, "Heightfield", heightfield3::init_world), - Example::new(STRESS, "Stacks", stacks3::init_world), - Example::new(STRESS, "Pyramid", pyramid3::init_world), - Example::new(STRESS, "Trimesh", trimesh3::init_world), - Example::new(STRESS, "ImpulseJoint ball", joint_ball3::init_world), - Example::new(STRESS, "ImpulseJoint fixed", joint_fixed3::init_world), - Example::new(STRESS, "ImpulseJoint revolute", joint_revolute3::init_world), - Example::new( - STRESS, - "ImpulseJoint prismatic", - joint_prismatic3::init_world, - ), - Example::new(STRESS, "Many pyramids", many_pyramids3::init_world), - Example::new(STRESS, "Keva tower", keva3::init_world), - Example::new(STRESS, "Ray cast", ray_cast3::init_world), - ] -} +// Stress-test scenes. The registry (group/name) lives in the main file; this +// module just exposes each scene's `run` entry point. +pub mod balls3; +pub mod boxes3; +pub mod capsules3; +pub mod ccd3; +pub mod compound3; +pub mod convex_polyhedron3; +pub mod heightfield3; +pub mod joint_ball3; +pub mod joint_fixed3; +pub mod joint_prismatic3; +pub mod joint_revolute3; +pub mod keva3; +pub mod many_kinematics3; +pub mod many_pyramids3; +pub mod many_sleep3; +pub mod many_static3; +pub mod pyramid3; +pub mod ray_cast3; +pub mod stacks3; +pub mod trimesh3; diff --git a/examples3d/stress_tests/pyramid3.rs b/examples3d/stress_tests/pyramid3.rs index 0e660e847..a0246d2c6 100644 --- a/examples3d/stress_tests/pyramid3.rs +++ b/examples3d/stress_tests/pyramid3.rs @@ -1,4 +1,4 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; fn create_pyramid( @@ -33,7 +33,7 @@ fn create_pyramid( } } -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -66,6 +66,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/stress_tests/ray_cast3.rs b/examples3d/stress_tests/ray_cast3.rs index 80ef6fd19..13bf46381 100644 --- a/examples3d/stress_tests/ray_cast3.rs +++ b/examples3d/stress_tests/ray_cast3.rs @@ -1,21 +1,57 @@ -use crate::{Example, stress_tests}; -use kiss3d::prelude::*; -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { - let settings = testbed.example_settings_mut(); +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { + /* + * World + */ + let mut world = PhysicsWorld::new(); - // NOTE: this demo is a bit special. It takes as a setting the builder of another demo, - // builds it, and add a ton of rays into it. This gives us an easy way to check - // ray-casting in a wide variety of situations. - let demos: Vec = stress_tests::builders() - .into_iter() - .filter(|demo| !std::ptr::fn_addr_eq(demo.builder, self::init_world as fn(&mut Testbed))) - .collect(); - let demo_names: Vec<_> = demos.iter().map(|demo| demo.name.to_string()).collect(); - let selected = settings.get_or_set_string("Scene", 0, demo_names); - (demos[selected].builder)(testbed); + /* + * Ground + */ + let ground_size = 200.1; + let ground_height = 0.1; + + let rigid_body = RigidBodyBuilder::fixed().translation(Vec3::new(0.0, -ground_height, 0.0)); + let collider = ColliderBuilder::cuboid(ground_size, ground_height, ground_size); + world.insert(rigid_body, collider); + + /* + * Create the cubes + */ + let num = 10; + let rad = 1.0; + + let shift = rad * 2.0; + let centerx = shift * (num / 2) as f32; + let centery = shift / 2.0; + let centerz = shift * (num / 2) as f32; + + let mut offset = -(num as f32) * (rad * 2.0) * 0.5; + + for j in 0usize..num { + for i in 0..num { + for k in 0usize..num { + let x = i as f32 * shift - centerx + offset; + let y = j as f32 * shift + centery; + let z = k as f32 * shift - centerz + offset; + + // Build the rigid body. + let rigid_body = RigidBodyBuilder::dynamic().translation(Vec3::new(x, y, z)); + let collider = ColliderBuilder::cuboid(rad, rad, rad); + world.insert(rigid_body, collider); + } + } + + offset -= 0.05 * rad * (num as f32 - 1.0); + } + + /* + * Set up the testbed. + */ + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); /* * Cast rays at each frame. @@ -29,54 +65,53 @@ pub fn init_world(testbed: &mut Testbed) { .collect(); let mut centered_rays = rays.clone(); - testbed.add_callback(move |graphics, physics, _, _| { - let Some(graphics) = graphics else { - return; - }; - - // Re-center the ray relative to the current position of all objects. - // This ensures demos with falling objects don’t end up with a boring situation - // where all the rays point into the void. - let mut center = Vec3::ZERO; - for (_, b) in physics.bodies.iter() { - center += b.translation(); - } - center /= physics.bodies.len() as Real; + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); - for (centered, ray) in centered_rays.iter_mut().zip(rays.iter()) { - centered.origin = center + ray.origin; - } + // Re-center the ray relative to the current position of all objects. + // This ensures demos with falling objects don’t end up with a boring situation + // where all the rays point into the void. + let mut center = Vec3::ZERO; + for (_, b) in world.bodies.iter() { + center += b.translation(); + } + center /= world.bodies.len() as Real; - // Cast the rays. - let t1 = std::time::Instant::now(); - let max_toi = ray_ball_radius - 1.0; - - let query_pipeline = physics.broad_phase.as_query_pipeline( - physics.narrow_phase.query_dispatcher(), - &physics.bodies, - &physics.colliders, - Default::default(), - ); - - for ray in ¢ered_rays { - if let Some((_, toi)) = query_pipeline.cast_ray(ray, max_toi, true) { - let a = ray.origin; - let b = ray.point_at(toi); - graphics.window.draw_line(a, b, GREEN, 100.0, true); - } else { - let a = ray.origin; - let b = ray.point_at(max_toi); - graphics.window.draw_line(a, b, RED, 100.0, true); + for (centered, ray) in centered_rays.iter_mut().zip(rays.iter()) { + centered.origin = center + ray.origin; } - } - let main_check_time = t1.elapsed().as_secs_f32(); - if let Some(settings) = &mut graphics.settings { - settings.set_label("Ray count:", format!("{}", rays.len())); - settings.set_label( + // Cast the rays. + let t1 = std::time::Instant::now(); + let max_toi = ray_ball_radius - 1.0; + + let query_pipeline = world.broad_phase.as_query_pipeline( + world.narrow_phase.query_dispatcher(), + &world.bodies, + &world.colliders, + Default::default(), + ); + + let mut hits = 0; + for ray in ¢ered_rays { + if query_pipeline.cast_ray(ray, max_toi, true).is_some() { + hits += 1; + } + } + let main_check_time = t1.elapsed().as_secs_f32(); + + viewer + .example_settings_mut() + .set_label("Ray count:", format!("{}", rays.len())); + viewer + .example_settings_mut() + .set_label("Ray hits:", format!("{}", hits)); + viewer.example_settings_mut().set_label( "Ray-cast time", format!("{:.2}ms", main_check_time * 1000.0,), ); } - }); + } + Ok(()) } diff --git a/examples3d/stress_tests/stacks3.rs b/examples3d/stress_tests/stacks3.rs index 44fe55df8..69ea66fbb 100644 --- a/examples3d/stress_tests/stacks3.rs +++ b/examples3d/stress_tests/stacks3.rs @@ -1,4 +1,4 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; fn create_tower_circle( @@ -88,7 +88,7 @@ fn create_pyramid( } } -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -169,8 +169,15 @@ pub fn init_world(testbed: &mut Testbed) { ); /* - * Set up the testbed. + * Set up the viewer. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/stress_tests/trimesh3.rs b/examples3d/stress_tests/trimesh3.rs index 2903eaef6..8da953186 100644 --- a/examples3d/stress_tests/trimesh3.rs +++ b/examples3d/stress_tests/trimesh3.rs @@ -1,8 +1,8 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::na::ComplexField; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -70,8 +70,15 @@ pub fn init_world(testbed: &mut Testbed) { } /* - * Set up the testbed. + * Set up the viewer. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/trimesh3.rs b/examples3d/trimesh3.rs index 1df0046e5..901e965ce 100644 --- a/examples3d/trimesh3.rs +++ b/examples3d/trimesh3.rs @@ -1,8 +1,8 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::na::ComplexField; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -99,6 +99,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/urdf3.rs b/examples3d/urdf3.rs index 54dc2ca0b..b3041ed54 100644 --- a/examples3d/urdf3.rs +++ b/examples3d/urdf3.rs @@ -1,8 +1,8 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::prelude::*; use rapier3d_urdf::{UrdfLoaderOptions, UrdfMultibodyOptions, UrdfRobot}; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -42,6 +42,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(20.0, 20.0, 20.0), Vec3::new(5.0, 0.0, 0.0)); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(20.0, 20.0, 20.0), Vec3::new(5.0, 0.0, 0.0)); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + } + } + Ok(()) } diff --git a/examples3d/utils/character.rs b/examples3d/utils/character.rs index 4b273a95d..afce05a57 100644 --- a/examples3d/utils/character.rs +++ b/examples3d/utils/character.rs @@ -1,6 +1,6 @@ use kiss3d::color::Color; use rapier_testbed3d::{ - KeyCode, PhysicsState, TestbedGraphics, + KeyCode, TestbedViewer, egui::{Align2, ComboBox, Slider, Ui, Window}, }; use rapier3d::{ @@ -18,8 +18,8 @@ pub enum CharacterControlMode { } pub fn update_character( - graphics: &mut TestbedGraphics, - physics: &mut PhysicsState, + graphics: &mut TestbedViewer, + physics: &mut PhysicsWorld, control_mode: &mut CharacterControlMode, controller: &mut KinematicCharacterController, pid: &mut PidController, @@ -49,7 +49,7 @@ pub fn update_character( } fn character_movement_from_inputs( - gfx: &TestbedGraphics, + gfx: &TestbedViewer, mut speed: Real, artificial_gravity: bool, ) -> Vector { @@ -100,8 +100,8 @@ fn character_movement_from_inputs( } fn update_pid_controller( - gfx: &mut TestbedGraphics, - phx: &mut PhysicsState, + gfx: &mut TestbedViewer, + phx: &mut PhysicsWorld, character_handle: RigidBodyHandle, pid: &mut PidController, speed: Real, @@ -139,8 +139,8 @@ fn update_pid_controller( } fn update_kinematic_controller( - gfx: &mut TestbedGraphics, - phx: &mut PhysicsState, + gfx: &mut TestbedViewer, + phx: &mut PhysicsWorld, character_handle: RigidBodyHandle, controller: &KinematicCharacterController, speed: Real, @@ -190,7 +190,7 @@ fn update_kinematic_controller( } fn character_control_ui( - gfx: &mut TestbedGraphics, + gfx: &mut TestbedViewer, character_controller: &mut KinematicCharacterController, pid_controller: &mut PidController, control_mode: &mut CharacterControlMode, diff --git a/examples3d/vehicle_controller3.rs b/examples3d/vehicle_controller3.rs index 7600af778..848494709 100644 --- a/examples3d/vehicle_controller3.rs +++ b/examples3d/vehicle_controller3.rs @@ -1,8 +1,8 @@ -use rapier_testbed3d::Testbed; +use rapier_testbed3d::{Key, TestbedViewer}; use rapier3d::control::{DynamicRayCastVehicleController, WheelTuning}; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -112,9 +112,49 @@ pub fn init_world(testbed: &mut Testbed) { world.insert_collider(collider, None); /* - * Set up the testbed. + * Set up the viewer. */ - testbed.set_physics_world(world); - testbed.set_vehicle_controller(vehicle); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + /* + * Vehicle controls. + */ + let keys = viewer.keys(); + let mut engine_force = 0.0; + let mut steering_angle = 0.0; + + if keys.pressed(Key::Right) { + steering_angle += -0.7; + } + if keys.pressed(Key::Left) { + steering_angle += 0.7; + } + if keys.pressed(Key::Up) { + engine_force += 30.0; + } + if keys.pressed(Key::Down) { + engine_force += -30.0; + } + + let wheels = vehicle.wheels_mut(); + wheels[0].engine_force = engine_force; + wheels[0].steering = steering_angle; + wheels[1].engine_force = engine_force; + wheels[1].steering = steering_angle; + + let q = world.broad_phase.as_query_pipeline_mut( + world.narrow_phase.query_dispatcher(), + &mut world.bodies, + &mut world.colliders, + QueryFilter::exclude_dynamic().exclude_rigid_body(vehicle_handle), + ); + vehicle.update_vehicle(world.integration_parameters.dt, q); + + world.step(); + } + } + Ok(()) } diff --git a/examples3d/vehicle_joints3.rs b/examples3d/vehicle_joints3.rs index 5f355e7a4..44c297866 100644 --- a/examples3d/vehicle_joints3.rs +++ b/examples3d/vehicle_joints3.rs @@ -1,7 +1,7 @@ -use rapier_testbed3d::{KeyCode, Testbed}; +use rapier_testbed3d::{KeyCode, TestbedViewer}; use rapier3d::prelude::*; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * World */ @@ -130,80 +130,6 @@ pub fn init_world(testbed: &mut Testbed) { } } - /* - * Callback to control the wheels motors. - */ - testbed.add_callback(move |gfx, physics, _, _| { - let Some(gfx) = gfx else { return }; - - let mut thrust = 0.0; - let mut steering = 0.0; - let mut boost = 1.0; - - for key in gfx.keys().get_pressed() { - match *key { - KeyCode::Right => { - steering = -1.0; - } - KeyCode::Left => { - steering = 1.0; - } - KeyCode::Up => { - thrust = -drive_strength; - } - KeyCode::Down => { - thrust = drive_strength; - } - KeyCode::RShift => { - boost = 1.5; - } - _ => {} - } - } - let mut should_wake_up = false; - if thrust != 0.0 || steering != 0.0 { - should_wake_up = true; - } - - // Apply steering to the axles. - for steering_handle in &steering_joints { - let steering_joint = physics - .impulse_joints - .get_mut(*steering_handle, should_wake_up) - .unwrap(); - steering_joint.data.set_motor_position( - JointAxis::AngY, - max_steering_angle * steering, - 1.0e4, - 1.0e3, - ); - } - - // Apply thrust. - // Pseudo-differential adjusting speed of engines depending on steering arc - // Higher values result in more drifty behavior. - let differential_strength = 0.5; - let sideways_shift = (max_steering_angle * steering).sin() * differential_strength; - let speed_diff = if sideways_shift > 0.0 { - f32::hypot(1.0, sideways_shift) - } else { - 1.0 / f32::hypot(1.0, sideways_shift) - }; - - let ms = [1.0 / speed_diff, speed_diff]; - for (motor_handle, &ms) in motor_joints.iter().copied().zip(ms.iter()) { - let motor_joint = physics - .impulse_joints - .get_mut(motor_handle, should_wake_up) - .unwrap(); - motor_joint.data.set_motor_velocity( - JointAxis::AngX, - -30.0 * thrust * ms * boost, - 1.0e2, - ); - } - }); - /* * Create some cubes on the ground. */ @@ -232,6 +158,83 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + + /* + * Control the wheels motors. + */ + let mut thrust = 0.0; + let mut steering = 0.0; + let mut boost = 1.0; + + for key in viewer.keys().get_pressed() { + match *key { + KeyCode::Right => { + steering = -1.0; + } + KeyCode::Left => { + steering = 1.0; + } + KeyCode::Up => { + thrust = -drive_strength; + } + KeyCode::Down => { + thrust = drive_strength; + } + KeyCode::RShift => { + boost = 1.5; + } + _ => {} + } + } + let mut should_wake_up = false; + if thrust != 0.0 || steering != 0.0 { + should_wake_up = true; + } + + // Apply steering to the axles. + for steering_handle in &steering_joints { + let steering_joint = world + .impulse_joints + .get_mut(*steering_handle, should_wake_up) + .unwrap(); + steering_joint.data.set_motor_position( + JointAxis::AngY, + max_steering_angle * steering, + 1.0e4, + 1.0e3, + ); + } + + // Apply thrust. + // Pseudo-differential adjusting speed of engines depending on steering arc + // Higher values result in more drifty behavior. + let differential_strength = 0.5; + let sideways_shift = (max_steering_angle * steering).sin() * differential_strength; + let speed_diff = if sideways_shift > 0.0 { + f32::hypot(1.0, sideways_shift) + } else { + 1.0 / f32::hypot(1.0, sideways_shift) + }; + + let ms = [1.0 / speed_diff, speed_diff]; + for (motor_handle, &ms) in motor_joints.iter().copied().zip(ms.iter()) { + let motor_joint = world + .impulse_joints + .get_mut(motor_handle, should_wake_up) + .unwrap(); + motor_joint.data.set_motor_velocity( + JointAxis::AngX, + -30.0 * thrust * ms * boost, + 1.0e2, + ); + } + } + } + Ok(()) } diff --git a/examples3d/voxels3.rs b/examples3d/voxels3.rs index 443fa95e7..0c3c3ea90 100644 --- a/examples3d/voxels3.rs +++ b/examples3d/voxels3.rs @@ -1,19 +1,19 @@ use kiss3d::color::Color; use obj::raw::object::Polygon; use rapier_testbed3d::KeyCode; -use rapier_testbed3d::Testbed; +use rapier_testbed3d::TestbedViewer; use rapier3d::parry::bounding_volume; use rapier3d::parry::transformation::voxelization::FillMode; use rapier3d::prelude::*; use std::fs::File; use std::io::BufReader; -pub fn init_world(testbed: &mut Testbed) { +pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { /* * Voxel geometry type selection. */ - let settings = testbed.example_settings_mut(); + let settings = viewer.example_settings_mut(); let falling_objects = settings.get_or_set_string( "Falling objects", @@ -203,108 +203,116 @@ pub fn init_world(testbed: &mut Testbed) { ColliderBuilder::cuboid(0.51, 0.51, 0.51).collision_groups(InteractionGroups::none()), None, ); - testbed.set_initial_collider_color(hit_indicator_handle, Color::new(0.5, 0.5, 0.1, 1.0)); - testbed.set_initial_collider_color(hit_highlight_handle, Color::new(0.1, 0.5, 0.1, 1.0)); - - testbed.add_callback(move |graphics, physics, _, _| { - let Some(graphics) = graphics else { return }; - let Some((mouse_orig, mouse_dir)) = graphics.mouse().ray else { - return; - }; - - let ray = Ray::new( - Vector::new(mouse_orig.x, mouse_orig.y, mouse_orig.z), - Vector::new(mouse_dir.x, mouse_dir.y, mouse_dir.z), - ); - let filter = QueryFilter { - predicate: Some(&|_, co: &Collider| co.shape().as_voxels().is_some()), - ..Default::default() - }; - let query_pipeline = physics.broad_phase.as_query_pipeline( - physics.narrow_phase.query_dispatcher(), - &physics.bodies, - &physics.colliders, - filter, - ); - - if let Some((handle, hit)) = query_pipeline.cast_ray_and_get_normal(&ray, Real::MAX, true) { - // Highlight the voxel. - let hit_collider = &physics.colliders[handle]; - let hit_pos = hit_collider.position(); - let hit_local_normal = hit_pos.rotation.inverse() * hit.normal; - let voxels = hit_collider.shape().as_voxels().unwrap(); - let FeatureId::Face(id) = hit.feature else { - unreachable!() + viewer.set_initial_collider_color(hit_indicator_handle, Color::new(0.5, 0.5, 0.1, 1.0)); + viewer.set_initial_collider_color(hit_highlight_handle, Color::new(0.1, 0.5, 0.1, 1.0)); + + /* + * Set up the testbed. + */ + viewer.set_world(&mut world); + viewer.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + + while viewer.render_frame(&mut world).await { + if viewer.simulating() { + world.step(); + + let Some((mouse_orig, mouse_dir)) = viewer.mouse().ray else { + continue; }; - let voxel_key = voxels.voxel_at_flat_id(id).unwrap(); - let voxel_center_local = voxels.voxel_center(voxel_key); - let voxel_center = hit_pos.rotation * voxel_center_local + hit_pos.translation; - let voxel_size = voxels.voxel_size(); - let hit_highlight = physics.colliders.get_mut(hit_highlight_handle).unwrap(); - hit_highlight.set_translation(voxel_center); - hit_highlight - .shape_mut() - .as_cuboid_mut() - .unwrap() - .half_extents = voxel_size / 2.0 + Vector::splat(0.001); - graphics.update_collider(hit_highlight_handle, &physics.colliders); - - // Show the hit point. - let hit_pt = ray.point_at(hit.time_of_impact); - let hit_indicator = physics.colliders.get_mut(hit_indicator_handle).unwrap(); - hit_indicator.set_translation(hit_pt); - hit_indicator.shape_mut().as_ball_mut().unwrap().radius = voxel_size.length() / 3.5; - graphics.update_collider(hit_indicator_handle, &physics.colliders); - - // If a relevant key was pressed, edit the shape. - if graphics.keys().pressed(KeyCode::Space) { - let removal_mode = graphics.keys().pressed(KeyCode::LShift); - let voxels = physics - .colliders - .get_mut(handle) - .unwrap() + + let ray = Ray::new( + Vector::new(mouse_orig.x, mouse_orig.y, mouse_orig.z), + Vector::new(mouse_dir.x, mouse_dir.y, mouse_dir.z), + ); + let filter = QueryFilter { + predicate: Some(&|_, co: &Collider| co.shape().as_voxels().is_some()), + ..Default::default() + }; + let query_pipeline = world.broad_phase.as_query_pipeline( + world.narrow_phase.query_dispatcher(), + &world.bodies, + &world.colliders, + filter, + ); + + if let Some((handle, hit)) = + query_pipeline.cast_ray_and_get_normal(&ray, Real::MAX, true) + { + // Highlight the voxel. + let hit_collider = &world.colliders[handle]; + let hit_pos = hit_collider.position(); + let hit_local_normal = hit_pos.rotation.inverse() * hit.normal; + let voxels = hit_collider.shape().as_voxels().unwrap(); + let FeatureId::Face(id) = hit.feature else { + unreachable!() + }; + let voxel_key = voxels.voxel_at_flat_id(id).unwrap(); + let voxel_center_local = voxels.voxel_center(voxel_key); + let voxel_center = hit_pos.rotation * voxel_center_local + hit_pos.translation; + let voxel_size = voxels.voxel_size(); + let hit_highlight = world.colliders.get_mut(hit_highlight_handle).unwrap(); + hit_highlight.set_translation(voxel_center); + hit_highlight .shape_mut() - .as_voxels_mut() - .unwrap(); - let mut affected_key = voxel_key; - - if !removal_mode { - // Find index of max absolute value component - let abs_normal = hit_local_normal.abs(); - let imax = if abs_normal.x >= abs_normal.y && abs_normal.x >= abs_normal.z { - 0 - } else if abs_normal.y >= abs_normal.z { - 1 - } else { - 2 - }; - let normal_arr = [hit_local_normal.x, hit_local_normal.y, hit_local_normal.z]; - if normal_arr[imax] >= 0.0 { - affected_key[imax] += 1; - } else { - affected_key[imax] -= 1; + .as_cuboid_mut() + .unwrap() + .half_extents = voxel_size / 2.0 + Vector::splat(0.001); + viewer.update_collider(hit_highlight_handle, &world); + + // Show the hit point. + let hit_pt = ray.point_at(hit.time_of_impact); + let hit_indicator = world.colliders.get_mut(hit_indicator_handle).unwrap(); + hit_indicator.set_translation(hit_pt); + hit_indicator.shape_mut().as_ball_mut().unwrap().radius = voxel_size.length() / 3.5; + viewer.update_collider(hit_indicator_handle, &world); + + // If a relevant key was pressed, edit the shape. + if viewer.keys().pressed(KeyCode::Space) { + let removal_mode = viewer.keys().pressed(KeyCode::LShift); + let voxels = world + .colliders + .get_mut(handle) + .unwrap() + .shape_mut() + .as_voxels_mut() + .unwrap(); + let mut affected_key = voxel_key; + + if !removal_mode { + // Find index of max absolute value component + let abs_normal = hit_local_normal.abs(); + let imax = if abs_normal.x >= abs_normal.y && abs_normal.x >= abs_normal.z { + 0 + } else if abs_normal.y >= abs_normal.z { + 1 + } else { + 2 + }; + let normal_arr = + [hit_local_normal.x, hit_local_normal.y, hit_local_normal.z]; + if normal_arr[imax] >= 0.0 { + affected_key[imax] += 1; + } else { + affected_key[imax] -= 1; + } } - } - voxels.set_voxel(affected_key, !removal_mode); - graphics.update_collider(handle, &physics.colliders); + voxels.set_voxel(affected_key, !removal_mode); + viewer.update_collider(handle, &world); + } + } else { + // When there is no hit, move the indicators behind the camera. + let behind_camera = mouse_orig - mouse_dir * 1000.0; + let behind_camera_vect = + Vector::new(behind_camera.x, behind_camera.y, behind_camera.z); + let hit_indicator = world.colliders.get_mut(hit_indicator_handle).unwrap(); + hit_indicator.set_translation(behind_camera_vect); + let hit_highlight = world.colliders.get_mut(hit_highlight_handle).unwrap(); + hit_highlight.set_translation(behind_camera_vect); } - } else { - // When there is no hit, move the indicators behind the camera. - let behind_camera = mouse_orig - mouse_dir * 1000.0; - let behind_camera_vect = Vector::new(behind_camera.x, behind_camera.y, behind_camera.z); - let hit_indicator = physics.colliders.get_mut(hit_indicator_handle).unwrap(); - hit_indicator.set_translation(behind_camera_vect); - let hit_highlight = physics.colliders.get_mut(hit_highlight_handle).unwrap(); - hit_highlight.set_translation(behind_camera_vect); } - }); - - /* - * Set up the testbed. - */ - testbed.set_physics_world(world); - testbed.look_at(Vec3::new(100.0, 100.0, 100.0), Vec3::ZERO); + } + Ok(()) } fn models() -> Vec { diff --git a/src/dynamics/joint/multibody_joint/multibody.rs b/src/dynamics/joint/multibody_joint/multibody.rs index 536318b31..34df022d8 100644 --- a/src/dynamics/joint/multibody_joint/multibody.rs +++ b/src/dynamics/joint/multibody_joint/multibody.rs @@ -565,8 +565,7 @@ impl Multibody { if k != 0.0 { let q = self.links[li].joint.coords[a]; let rest = self.links[li].joint.spring_ref[a]; - self.accelerations[idx] += - -k * (q - rest) - k * dt * self.velocities[idx]; + self.accelerations[idx] += -k * (q - rest) - k * dt * self.velocities[idx]; } idx += 1; } @@ -994,7 +993,10 @@ impl Multibody { /// by `link1` (which always has a free DoF and so is an active link in the /// solver island, unlike a possibly-fixed root). pub(crate) fn num_couplings_owned_by(&self, owner_link: usize) -> usize { - self.couplings.iter().filter(|c| c.link1 == owner_link).count() + self.couplings + .iter() + .filter(|c| c.link1 == owner_link) + .count() } /// Generates the velocity constraints for the DoF couplings owned by @@ -1524,8 +1526,8 @@ impl Multibody { let jb2 = &self.body_jacobians[link2.internal_id]; // Use the (overwritten below) W·J slot as scratch for J1ᵀ·f1. - let (mut out_j, mut scratch) = jacobians - .rows_range_pair_mut(*j_id..*j_id + self.ndofs, wj_id..wj_id + self.ndofs); + let (mut out_j, mut scratch) = + jacobians.rows_range_pair_mut(*j_id..*j_id + self.ndofs, wj_id..wj_id + self.ndofs); jb2.tr_mul_to(force2.as_vector(), &mut out_j); jb1.tr_mul_to(force1.as_vector(), &mut scratch); out_j.axpy(-1.0, &scratch, 1.0); diff --git a/src/dynamics/joint/multibody_joint/multibody_link.rs b/src/dynamics/joint/multibody_joint/multibody_link.rs index b2d67f950..f2d7d4401 100644 --- a/src/dynamics/joint/multibody_joint/multibody_link.rs +++ b/src/dynamics/joint/multibody_joint/multibody_link.rs @@ -1,6 +1,8 @@ use crate::alloc_prelude::*; use core::ops::{Deref, DerefMut}; +#[cfg(doc)] +use crate::dynamics::Multibody; use crate::dynamics::{MultibodyJoint, RigidBodyHandle}; use crate::math::{Pose, Real, Vector}; use crate::prelude::RigidBodyVelocity; diff --git a/src/dynamics/solver/joint_constraint/generic_joint_constraint_builder.rs b/src/dynamics/solver/joint_constraint/generic_joint_constraint_builder.rs index 74a5a9349..ff024d9fc 100644 --- a/src/dynamics/solver/joint_constraint/generic_joint_constraint_builder.rs +++ b/src/dynamics/solver/joint_constraint/generic_joint_constraint_builder.rs @@ -279,8 +279,8 @@ impl JointGenericInternalConstraintBuilder { ) { let multibody = &multibodies[link_id.multibody]; let link = multibody.link(link_id.id).unwrap(); - let num_constraints = link.joint().num_velocity_constraints() - + multibody.num_couplings_owned_by(link_id.id); + let num_constraints = + link.joint().num_velocity_constraints() + multibody.num_couplings_owned_by(link_id.id); if num_constraints == 0 { return; diff --git a/src_testbed/debug_render.rs b/src_testbed/debug_render.rs index 7a4d65480..ae5aac18a 100644 --- a/src_testbed/debug_render.rs +++ b/src_testbed/debug_render.rs @@ -1,10 +1,10 @@ #![allow(clippy::unnecessary_cast)] // Casts are needed for switching between f32/f64. -use crate::harness::Harness; use kiss3d::window::Window; use rapier::math::Vector; use rapier::pipeline::{ DebugColor, DebugRenderBackend, DebugRenderMode, DebugRenderObject, DebugRenderPipeline, + PhysicsWorld, }; pub struct DebugRenderPipelineResource { @@ -60,17 +60,17 @@ impl<'a> DebugRenderBackend for Kiss3dLinesRenderBackend<'a> { pub fn debug_render_scene( window: &mut Window, debug_render: &mut DebugRenderPipelineResource, - harness: &Harness, + world: &PhysicsWorld, ) { if debug_render.enabled { let mut backend = Kiss3dLinesRenderBackend { window }; debug_render.pipeline.render( &mut backend, - &harness.physics.bodies, - &harness.physics.colliders, - &harness.physics.impulse_joints, - &harness.physics.multibody_joints, - &harness.physics.narrow_phase, + &world.bodies, + &world.colliders, + &world.impulse_joints, + &world.multibody_joints, + &world.narrow_phase, ); } } diff --git a/src_testbed/harness/mod.rs b/src_testbed/harness/mod.rs deleted file mode 100644 index be79c55e4..000000000 --- a/src_testbed/harness/mod.rs +++ /dev/null @@ -1,343 +0,0 @@ -#![allow(clippy::unnecessary_cast)] // Casts are needed for switching between f32/f64. - -use crate::{ - TestbedGraphics, - physics::{PhysicsEvents, PhysicsState}, -}; -use plugin::HarnessPlugin; -use rapier::dynamics::{ - CCDSolver, ImpulseJointSet, IntegrationParameters, IslandManager, MultibodyJointSet, - RigidBodySet, -}; -use rapier::geometry::{BroadPhaseBvh, BvhOptimizationStrategy, ColliderSet, NarrowPhase}; -use rapier::pipeline::{ChannelEventCollector, PhysicsHooks, PhysicsPipeline, PhysicsWorld}; - -pub mod plugin; - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)] -pub enum RapierBroadPhaseType { - #[default] - BvhSubtreeOptimizer, - BvhWithoutOptimization, -} - -impl RapierBroadPhaseType { - pub fn init_broad_phase(self) -> BroadPhaseBvh { - match self { - RapierBroadPhaseType::BvhSubtreeOptimizer => { - BroadPhaseBvh::with_optimization_strategy(BvhOptimizationStrategy::SubtreeOptimizer) - } - RapierBroadPhaseType::BvhWithoutOptimization => { - BroadPhaseBvh::with_optimization_strategy(BvhOptimizationStrategy::None) - } - } - } -} - -pub struct RunState { - #[cfg(feature = "parallel")] - pub thread_pool: rapier::rayon::ThreadPool, - #[cfg(feature = "parallel")] - num_threads: usize, - pub timestep_id: usize, - pub time: f32, -} - -impl Default for RunState { - fn default() -> Self { - Self::new() - } -} - -impl RunState { - #[cfg(feature = "parallel")] - pub fn new() -> Self { - let num_threads = num_cpus::get_physical(); - - let thread_pool = rapier::rayon::ThreadPoolBuilder::new() - .num_threads(num_threads) - .build() - .unwrap(); - - Self { - thread_pool, - num_threads, - timestep_id: 0, - time: 0.0, - } - } - - #[cfg(not(feature = "parallel"))] - pub fn new() -> Self { - Self { - timestep_id: 0, - time: 0.0, - } - } - - #[cfg(feature = "parallel")] - pub fn num_threads(&self) -> usize { - self.num_threads - } - - #[cfg(not(feature = "parallel"))] - pub fn num_threads(&self) -> usize { - 1 - } - - #[cfg(feature = "parallel")] - pub fn set_num_threads(&mut self, num_threads: usize) { - if self.num_threads != num_threads { - self.thread_pool = rapier::rayon::ThreadPoolBuilder::new() - .num_threads(num_threads) - .build() - .unwrap(); - self.num_threads = num_threads; - } - } -} - -pub struct Harness { - pub physics: PhysicsState, - max_steps: usize, - callbacks: Callbacks, - plugins: Vec>, - events: PhysicsEvents, - event_handler: ChannelEventCollector, - pub state: RunState, -} - -type Callbacks = - Vec, &mut PhysicsState, &PhysicsEvents, &RunState)>>; - -#[allow(dead_code)] -impl Harness { - pub fn new_empty() -> Self { - let collision_event_channel = std::sync::mpsc::channel(); - let contact_force_event_channel = std::sync::mpsc::channel(); - let event_handler = - ChannelEventCollector::new(collision_event_channel.0, contact_force_event_channel.0); - let events = PhysicsEvents { - collision_events: collision_event_channel.1, - contact_force_events: contact_force_event_channel.1, - }; - let physics = PhysicsState::new(); - let state = RunState::new(); - - Self { - physics, - max_steps: 1000, - callbacks: Vec::new(), - plugins: Vec::new(), - events, - event_handler, - state, - } - } - - pub fn new( - bodies: RigidBodySet, - colliders: ColliderSet, - impulse_joints: ImpulseJointSet, - multibody_joints: MultibodyJointSet, - broad_phase_type: RapierBroadPhaseType, - ) -> Self { - let mut res = Self::new_empty(); - res.set_world( - bodies, - colliders, - impulse_joints, - multibody_joints, - broad_phase_type, - ); - res - } - - pub fn set_max_steps(&mut self, max_steps: usize) { - self.max_steps = max_steps - } - - pub fn integration_parameters_mut(&mut self) -> &mut IntegrationParameters { - &mut self.physics.integration_parameters - } - - pub fn clear_callbacks(&mut self) { - self.callbacks.clear(); - } - - pub fn physics_state_mut(&mut self) -> &mut PhysicsState { - &mut self.physics - } - - pub fn set_world( - &mut self, - bodies: RigidBodySet, - colliders: ColliderSet, - impulse_joints: ImpulseJointSet, - multibody_joints: MultibodyJointSet, - broad_phase_type: RapierBroadPhaseType, - ) { - self.set_world_with_params( - bodies, - colliders, - impulse_joints, - multibody_joints, - broad_phase_type, - rapier::math::Vector::Y * -9.81, - (), - ) - } - - pub fn set_world_with_params( - &mut self, - bodies: RigidBodySet, - colliders: ColliderSet, - impulse_joints: ImpulseJointSet, - multibody_joints: MultibodyJointSet, - broad_phase_type: RapierBroadPhaseType, - gravity: rapier::math::Vector, - hooks: impl PhysicsHooks + 'static, - ) { - self.physics.gravity = gravity; - self.physics.bodies = bodies; - self.physics.colliders = colliders; - self.physics.impulse_joints = impulse_joints; - self.physics.multibody_joints = multibody_joints; - self.physics.hooks = Box::new(hooks); - - self.physics.islands = IslandManager::new(); - self.physics.broad_phase = broad_phase_type.init_broad_phase(); - self.physics.narrow_phase = NarrowPhase::new(); - self.state.timestep_id = 0; - self.state.time = 0.0; - self.physics.ccd_solver = CCDSolver::new(); - self.physics.pipeline = PhysicsPipeline::new(); - self.physics.pipeline.counters.enable(); - } - - /// Replaces the world state with the contents of a [`PhysicsWorld`]. - /// - /// This is a convenience method equivalent to [`set_world_with_params`](Self::set_world_with_params) - /// but takes a single [`PhysicsWorld`] and respects its gravity and integration parameters. - /// No physics hooks are installed; use [`set_physics_world_with_hooks`](Self::set_physics_world_with_hooks) - /// if you need hooks. - pub fn set_physics_world( - &mut self, - world: PhysicsWorld, - broad_phase_type: RapierBroadPhaseType, - ) { - self.set_physics_world_with_hooks(world, broad_phase_type, ()); - } - - /// Replaces the world state with the contents of a [`PhysicsWorld`] and installs the given - /// physics hooks. - pub fn set_physics_world_with_hooks( - &mut self, - world: PhysicsWorld, - broad_phase_type: RapierBroadPhaseType, - hooks: impl PhysicsHooks + 'static, - ) { - self.physics.gravity = world.gravity; - self.physics.integration_parameters = world.integration_parameters; - self.physics.bodies = world.bodies; - self.physics.colliders = world.colliders; - self.physics.impulse_joints = world.impulse_joints; - self.physics.multibody_joints = world.multibody_joints; - self.physics.hooks = Box::new(hooks); - - self.physics.islands = IslandManager::new(); - self.physics.broad_phase = broad_phase_type.init_broad_phase(); - self.physics.narrow_phase = NarrowPhase::new(); - self.state.timestep_id = 0; - self.state.time = 0.0; - self.physics.ccd_solver = CCDSolver::new(); - self.physics.pipeline = PhysicsPipeline::new(); - self.physics.pipeline.counters.enable(); - } - - pub fn add_plugin(&mut self, plugin: impl HarnessPlugin + 'static) { - self.plugins.push(Box::new(plugin)); - } - - pub fn add_callback< - F: FnMut(Option<&mut TestbedGraphics>, &mut PhysicsState, &PhysicsEvents, &RunState) + 'static, - >( - &mut self, - callback: F, - ) { - self.callbacks.push(Box::new(callback)); - } - - pub fn step(&mut self) { - self.step_with_graphics(None); - } - - #[profiling::function] - pub fn step_with_graphics(&mut self, mut graphics: Option<&mut TestbedGraphics>) { - #[cfg(feature = "parallel")] - { - let physics = &mut self.physics; - let event_handler = &self.event_handler; - self.state.thread_pool.install(|| { - physics.pipeline.step( - physics.gravity, - &physics.integration_parameters, - &mut physics.islands, - &mut physics.broad_phase, - &mut physics.narrow_phase, - &mut physics.bodies, - &mut physics.colliders, - &mut physics.impulse_joints, - &mut physics.multibody_joints, - &mut physics.ccd_solver, - &*physics.hooks, - event_handler, - ); - }); - } - - #[cfg(not(feature = "parallel"))] - self.physics.pipeline.step( - self.physics.gravity, - &self.physics.integration_parameters, - &mut self.physics.islands, - &mut self.physics.broad_phase, - &mut self.physics.narrow_phase, - &mut self.physics.bodies, - &mut self.physics.colliders, - &mut self.physics.impulse_joints, - &mut self.physics.multibody_joints, - &mut self.physics.ccd_solver, - &*self.physics.hooks, - &self.event_handler, - ); - - for plugin in &mut self.plugins { - plugin.step(&mut self.physics, &self.state) - } - - for f in &mut self.callbacks { - f( - graphics.as_deref_mut(), - &mut self.physics, - &self.events, - &self.state, - ); - } - - for plugin in &mut self.plugins { - plugin.run_callbacks(&mut self.physics, &self.events, &self.state) - } - - self.events.poll_all(); - - self.state.time += self.physics.integration_parameters.dt as f32; - self.state.timestep_id += 1; - } - - pub fn run(&mut self) { - for _ in 0..self.max_steps { - self.step(); - } - } -} diff --git a/src_testbed/harness/plugin.rs b/src_testbed/harness/plugin.rs deleted file mode 100644 index 4f6129dcc..000000000 --- a/src_testbed/harness/plugin.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::PhysicsState; -use crate::harness::RunState; -use crate::physics::PhysicsEvents; - -pub trait HarnessPlugin { - fn run_callbacks( - &mut self, - physics: &mut PhysicsState, - events: &PhysicsEvents, - harness_state: &RunState, - ); - fn step(&mut self, physics: &mut PhysicsState, run_state: &RunState); - fn profiling_string(&self) -> String; -} diff --git a/src_testbed/lib.rs b/src_testbed/lib.rs index 21af98740..388434714 100644 --- a/src_testbed/lib.rs +++ b/src_testbed/lib.rs @@ -3,13 +3,11 @@ extern crate nalgebra as na; pub use crate::graphics::{GraphicsManager, RenderMaterial}; -pub use crate::harness::plugin::HarnessPlugin; -pub use crate::physics::PhysicsState; -pub use crate::plugin::TestbedPlugin; +pub use crate::physics::RapierBroadPhaseType; pub use crate::testbed::{ - Example, KeysState, RunMode, Testbed, TestbedActionFlags, TestbedApp, TestbedGraphics, - TestbedState, TestbedStateFlags, + ExampleEntry, KeysState, RunMode, TestbedActionFlags, TestbedState, TestbedStateFlags, }; +pub use crate::viewer::TestbedViewer; // Re-export kiss3d types that users might need pub use kiss3d::event::{Action, Key, MouseButton, WindowEvent}; @@ -23,16 +21,13 @@ pub use egui; mod debug_render; mod graphics; -pub mod harness; mod mouse; pub mod physics; -#[cfg(all(feature = "dim3", feature = "other-backends"))] -mod physx_backend; -mod plugin; mod save; pub mod settings; mod testbed; pub mod ui; +mod viewer; #[cfg(feature = "dim3")] pub use kiss3d::camera::OrbitCamera3d as Camera; diff --git a/src_testbed/physics/mod.rs b/src_testbed/physics/mod.rs index 21864eb58..2566072ef 100644 --- a/src_testbed/physics/mod.rs +++ b/src_testbed/physics/mod.rs @@ -1,13 +1,61 @@ -use rapier::dynamics::{ - CCDSolver, ImpulseJointSet, IntegrationParameters, IslandManager, MultibodyJointSet, - RigidBodySet, -}; +use rapier::dynamics::{ImpulseJointSet, IslandManager, MultibodyJointSet, RigidBodySet}; use rapier::geometry::{ - BroadPhaseBvh, ColliderSet, CollisionEvent, ContactForceEvent, DefaultBroadPhase, NarrowPhase, + BroadPhaseBvh, BvhOptimizationStrategy, ColliderSet, CollisionEvent, ContactForceEvent, + DefaultBroadPhase, NarrowPhase, }; -use rapier::pipeline::{PhysicsHooks, PhysicsPipeline}; +use rapier::pipeline::PhysicsWorld; use std::sync::mpsc::Receiver; +/// Which broad-phase acceleration structure the testbed builds for a scene. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)] +pub enum RapierBroadPhaseType { + #[default] + BvhSubtreeOptimizer, + BvhWithoutOptimization, +} + +impl RapierBroadPhaseType { + pub fn init_broad_phase(self) -> BroadPhaseBvh { + match self { + RapierBroadPhaseType::BvhSubtreeOptimizer => { + BroadPhaseBvh::with_optimization_strategy(BvhOptimizationStrategy::SubtreeOptimizer) + } + RapierBroadPhaseType::BvhWithoutOptimization => { + BroadPhaseBvh::with_optimization_strategy(BvhOptimizationStrategy::None) + } + } + } +} + +/// Snapshots the full simulation state of a [`PhysicsWorld`]. +pub fn snapshot_world(world: &PhysicsWorld, timestep_id: usize) -> PhysicsSnapshot { + PhysicsSnapshot::new( + timestep_id, + &world.broad_phase, + &world.narrow_phase, + &world.islands, + &world.bodies, + &world.colliders, + &world.impulse_joints, + &world.multibody_joints, + ) + .expect("Failed to create physics snapshot") +} + +/// Restores a [`PhysicsWorld`] from a snapshot produced by [`snapshot_world`]. +pub fn restore_world(world: &mut PhysicsWorld, snapshot: &PhysicsSnapshot) { + let restored = snapshot + .restore() + .expect("Failed to restore physics snapshot"); + world.broad_phase = restored.broad_phase; + world.narrow_phase = restored.narrow_phase; + world.islands = restored.island_manager; + world.bodies = restored.bodies; + world.colliders = restored.colliders; + world.impulse_joints = restored.impulse_joints; + world.multibody_joints = restored.multibody_joints; +} + #[derive(Clone)] pub struct PhysicsSnapshot { timestep_id: usize, @@ -87,75 +135,6 @@ impl PhysicsSnapshot { } } -pub struct PhysicsState { - pub islands: IslandManager, - pub broad_phase: BroadPhaseBvh, - pub narrow_phase: NarrowPhase, - pub bodies: RigidBodySet, - pub colliders: ColliderSet, - pub impulse_joints: ImpulseJointSet, - pub multibody_joints: MultibodyJointSet, - pub ccd_solver: CCDSolver, - pub pipeline: PhysicsPipeline, - pub integration_parameters: IntegrationParameters, - pub gravity: rapier::math::Vector, - pub hooks: Box, -} - -impl Default for PhysicsState { - fn default() -> Self { - Self::new() - } -} - -impl PhysicsState { - pub fn new() -> Self { - Self { - islands: IslandManager::new(), - broad_phase: DefaultBroadPhase::default(), - narrow_phase: NarrowPhase::new(), - bodies: RigidBodySet::new(), - colliders: ColliderSet::new(), - impulse_joints: ImpulseJointSet::new(), - multibody_joints: MultibodyJointSet::new(), - ccd_solver: CCDSolver::new(), - pipeline: PhysicsPipeline::new(), - integration_parameters: IntegrationParameters::default(), - gravity: rapier::math::Vector::Y * -9.81, - hooks: Box::new(()), - } - } - - /// Create a snapshot of the current physics state. - pub fn snapshot(&self) -> PhysicsSnapshot { - PhysicsSnapshot::new( - 0, // timestep_id - could be tracked if needed - &self.broad_phase, - &self.narrow_phase, - &self.islands, - &self.bodies, - &self.colliders, - &self.impulse_joints, - &self.multibody_joints, - ) - .expect("Failed to create physics snapshot") - } - - /// Restore physics state from a snapshot. - pub fn restore_snapshot(&mut self, snapshot: PhysicsSnapshot) { - let restored = snapshot - .restore() - .expect("Failed to restore physics snapshot"); - self.broad_phase = restored.broad_phase; - self.narrow_phase = restored.narrow_phase; - self.islands = restored.island_manager; - self.bodies = restored.bodies; - self.colliders = restored.colliders; - self.impulse_joints = restored.impulse_joints; - self.multibody_joints = restored.multibody_joints; - } -} - pub struct PhysicsEvents { pub collision_events: Receiver, pub contact_force_events: Receiver, diff --git a/src_testbed/physx_backend.rs b/src_testbed/physx_backend.rs deleted file mode 100644 index 975d0f7a9..000000000 --- a/src_testbed/physx_backend.rs +++ /dev/null @@ -1,817 +0,0 @@ -#![allow(dead_code)] - -use crate::ui::egui::emath::OrderedFloat; -use glamx::{Pose3, Vec3}; -use na::{Isometry3, Matrix4, Point3, Quaternion, Unit, UnitQuaternion, Vector3}; -use physx::cooking::{ - ConvexMeshCookingResult, PxConvexMeshDesc, PxCookingParams, PxHeightFieldDesc, - PxTriangleMeshDesc, TriangleMeshCookingResult, -}; -use physx::foundation::DefaultAllocator; -use physx::prelude::*; -use physx::scene::{FrictionType, SceneFlags}; -use physx::traits::Class; -use physx_sys::PxFilterFlags; -use physx_sys::{ - FilterShaderCallbackInfo, PxBitAndByte, PxConvexFlags, PxConvexMeshGeometryFlags, - PxHeightFieldSample, PxMeshGeometryFlags, PxMeshScale_new, PxRigidActor, -}; -use rapier::counters::Counters; -use rapier::dynamics::{ - ImpulseJointSet, IntegrationParameters, MultibodyJointSet, RigidBodyHandle, RigidBodySet, -}; -use rapier::geometry::{Collider, ColliderSet}; -use rapier::prelude::JointAxesMask; -use std::collections::HashMap; - -trait IntoNa { - type Output; - fn into_na(self) -> Self::Output; -} - -impl IntoNa for glamx::Mat4 { - type Output = Matrix4; - fn into_na(self) -> Self::Output { - self.to_cols_array_2d().into() - } -} - -impl IntoNa for PxVec3 { - type Output = Vector3; - fn into_na(self) -> Self::Output { - Vector3::new(self.x(), self.y(), self.z()) - } -} - -impl IntoNa for PxQuat { - type Output = Quaternion; - fn into_na(self) -> Self::Output { - Quaternion::new(self.w(), self.x(), self.y(), self.z()) - } -} - -impl IntoNa for PxTransform { - type Output = Isometry3; - fn into_na(self) -> Self::Output { - let tra = self.translation().into_na(); - let quat = self.rotation().into_na(); - let unit_quat = Unit::new_unchecked(quat); - Isometry3::from_parts(tra.into(), unit_quat) - } -} - -trait IntoPhysx { - type Output; - fn into_physx(self) -> Self::Output; -} - -impl IntoPhysx for Vector3 { - type Output = PxVec3; - fn into_physx(self) -> Self::Output { - PxVec3::new(self.x, self.y, self.z) - } -} - -impl IntoPhysx for Point3 { - type Output = PxVec3; - fn into_physx(self) -> Self::Output { - PxVec3::new(self.x, self.y, self.z) - } -} - -impl IntoPhysx for UnitQuaternion { - type Output = PxQuat; - fn into_physx(self) -> Self::Output { - PxQuat::new(self.i, self.j, self.k, self.w) - } -} - -impl IntoPhysx for glamx::Vec3 { - type Output = PxVec3; - fn into_physx(self) -> Self::Output { - PxVec3::new(self.x, self.y, self.z) - } -} - -impl IntoPhysx for glamx::Quat { - type Output = PxQuat; - fn into_physx(self) -> Self::Output { - PxQuat::new(self.x, self.y, self.z, self.w) - } -} - -impl IntoPhysx for Isometry3 { - type Output = PxTransform; - fn into_physx(self) -> Self::Output { - PxTransform::from_translation_rotation( - &self.translation.vector.into_physx(), - &self.rotation.into_physx(), - ) - } -} - -impl IntoPhysx for glamx::Pose3 { - type Output = PxTransform; - fn into_physx(self) -> Self::Output { - PxTransform::from_translation_rotation( - &self.translation.into_physx(), - &self.rotation.into_physx(), - ) - } -} - -trait IntoGlam { - type Output; - fn into_glam(self) -> Self::Output; -} - -impl IntoGlam for Vector3 { - type Output = glamx::Vec3; - fn into_glam(self) -> Self::Output { - glamx::vec3(self.x, self.y, self.z) - } -} - -impl IntoGlam for Point3 { - type Output = glamx::Vec3; - fn into_glam(self) -> Self::Output { - glamx::vec3(self.x, self.y, self.z) - } -} - -impl IntoGlam for Matrix4 { - type Output = glamx::Mat4; - fn into_glam(self) -> Self::Output { - glamx::Mat4::from_cols_array_2d(&self.into()) - } -} - -impl IntoGlam for Isometry3 { - type Output = glamx::Mat4; - fn into_glam(self) -> Self::Output { - glamx::Mat4::from_cols_array_2d(&self.to_homogeneous().into()) - } -} - -thread_local! { -pub static FOUNDATION: std::cell::RefCell = std::cell::RefCell::new(PhysicsFoundation::default()); -} - -pub struct PhysxWorld { - // physics: Physics, - // materials: Vec>, - shapes: Vec>, - scene: Option>, -} - -impl Drop for PhysxWorld { - fn drop(&mut self) { - let scene = self.scene.take(); - // FIXME: we get a segfault if we don't leak the scene. - std::mem::forget(scene); - } -} - -impl PhysxWorld { - #[profiling::function] - pub fn from_rapier( - gravity: Vec3, - integration_parameters: &IntegrationParameters, - bodies: &RigidBodySet, - colliders: &ColliderSet, - impulse_joints: &ImpulseJointSet, - multibody_joints: &MultibodyJointSet, - use_two_friction_directions: bool, - num_threads: usize, - ) -> Self { - FOUNDATION.with(|physics| { - let mut physics = physics.borrow_mut(); - let mut shapes = Vec::new(); - - let friction_type = if use_two_friction_directions { - FrictionType::TwoDirectional - } else { - FrictionType::Patch - }; - - let mut scene_desc = SceneDescriptor { - gravity: gravity.into_physx(), - thread_count: num_threads as u32, - broad_phase_type: BroadPhaseType::Abp, - solver_type: SolverType::Tgs, - friction_type, - ccd_max_passes: integration_parameters.max_ccd_substeps as u32, - ..SceneDescriptor::new(()) - }; - - let ccd_enabled = bodies.iter().any(|(_, rb)| rb.is_ccd_enabled()); - - if ccd_enabled { - scene_desc.simulation_filter_shader = - FilterShaderDescriptor::CallDefaultFirst(ccd_filter_shader); - scene_desc.flags.insert(SceneFlags::EnableCcd); - } - - let mut scene: Owner = physics.create(scene_desc).unwrap(); - let mut rapier2dynamic = HashMap::new(); - let mut rapier2static = HashMap::new(); - let mut rapier2link = HashMap::new(); - - /* - * - * Rigid bodies - * - */ - for (rapier_handle, rb) in bodies.iter() { - if multibody_joints.rigid_body_link(rapier_handle).is_some() { - continue; - }; - - let pos = rb.position().into_physx(); - if rb.is_dynamic() { - let mut actor = physics.create_dynamic(&pos, rapier_handle).unwrap(); - let linvel = rb.linvel().into_physx(); - let angvel = rb.angvel().into_physx(); - actor.set_linear_velocity(&linvel, true); - actor.set_angular_velocity(&angvel, true); - actor.set_solver_iteration_counts( - // Use our number of solver iterations as their number of position iterations. - integration_parameters.num_solver_iterations as u32, - 1, - ); - - rapier2dynamic.insert(rapier_handle, actor); - } else { - let actor = physics.create_static(pos, ()).unwrap(); - rapier2static.insert(rapier_handle, actor); - } - } - - /* - * Articulations. - */ - /* - for multibody in multibody_joints.multibodies() { - let mut articulation: Owner = - physics.create_articulation_reduced_coordinate(()).unwrap(); - let mut parent = None; - - for link in multibody.links() { - let is_root = parent.is_none(); - let rb_handle = link.rigid_body_handle(); - let rb = bodies.get(rb_handle).unwrap(); - - if is_root && rb.is_fixed() { - articulation.set_articulation_flag(ArticulationFlag::FixBase, true); - } - - let link_pose = rb.position().into_physx(); - let px_link = articulation - .create_link(parent.take(), &link_pose, rb_handle) - .unwrap(); - - // TODO: there is no get_inbound_joint_mut? - if let Some(px_inbound_joint) = - unsafe { (PxArticulationLink_getInboundJoint(px_link.as_ptr())).as_mut() } - { - let frame1 = link.joint().data.local_frame1.into_physx(); - let frame2 = link.joint().data.local_frame2.into_physx(); - - px_inbound_joint.set_parent_pose(&frame1); - px_inbound_joint.set_child_pose(&frame2); - - /* - - let px_joint = px_inbound_joint - .as_articulation_joint_reduced_coordinate() - .unwrap(); - - if let Some(_) = link - .articulation() - .downcast_ref::() - { - px_joint.set_joint_type(ArticulationJointType::Spherical); - px_joint.set_motion(ArticulationAxis::Swing1, ArticulationMotion::Free); - px_joint.set_motion(ArticulationAxis::Swing2, ArticulationMotion::Free); - px_joint.set_motion(ArticulationAxis::Twist, ArticulationMotion::Free); - } else if let Some(_) = - link.articulation().downcast_ref::() - { - px_joint.set_joint_type(ArticulationJointType::Revolute); - px_joint.set_motion(ArticulationAxis::Swing1, ArticulationMotion::Free); - px_joint.set_motion(ArticulationAxis::Swing2, ArticulationMotion::Free); - px_joint.set_motion(ArticulationAxis::Twist, ArticulationMotion::Free); - } - - */ - } - - // FIXME: we are using transmute here in order to erase the lifetime of - // the &mut ref behind px_link (which is tied to the lifetime of the - // multibody_joint). This looks necessary because we need - // that mutable ref to create the next link. Yet, the link creation - // methods also requires a mutable ref to the multibody_joint. - rapier2link.insert(rb_handle, px_link as *mut PxArticulationLink); - parent = Some(unsafe { std::mem::transmute(px_link as *mut _) }); - } - - scene.add_articulation(articulation); - } - - */ - - /* - * - * Colliders - * - */ - let mut materials_cache = HashMap::new(); - for (_, collider) in colliders.iter() { - if let Some((mut px_shape, collider_pos)) = physx_collider_from_rapier_collider( - &mut *physics, - &collider, - &mut materials_cache, - ) { - if let Some(parent_handle) = collider.parent() { - let parent_body = &bodies[parent_handle]; - - if let Some(link) = rapier2link.get_mut(&parent_handle) { - unsafe { - physx_sys::PxRigidActor_attachShape_mut( - *link as *mut PxRigidActor, - px_shape.as_mut_ptr(), - ); - } - } else if !parent_body.is_dynamic() { - let actor = rapier2static.get_mut(&parent_handle).unwrap(); - actor.attach_shape(&mut px_shape); - } else { - let actor = rapier2dynamic.get_mut(&parent_handle).unwrap(); - actor.attach_shape(&mut px_shape); - } - - unsafe { - let pose = collider_pos.into_physx(); - physx_sys::PxShape_setLocalPose_mut( - px_shape.as_mut_ptr(), - &pose.into(), - ); - } - - shapes.push(px_shape); - } - } - } - - // Update mass properties and CCD flags. - for (rapier_handle, _rb) in bodies.iter() { - let rb = &bodies[rapier_handle]; - let densities: Vec<_> = rb - .colliders() - .iter() - .map(|h| colliders[*h].density()) - .collect(); - - unsafe { - let actor = if let Some(actor) = rapier2dynamic.get_mut(&rapier_handle) { - std::mem::transmute(actor.as_mut()) - } else if let Some(actor) = rapier2link.get_mut(&rapier_handle) { - *actor as *mut _ - } else { - continue; - }; - - physx_sys::PxRigidBodyExt_updateMassAndInertia( - actor, - densities.as_ptr(), - densities.len() as u32, - std::ptr::null(), - false, - ); - - if rb.is_ccd_enabled() { - physx_sys::PxRigidBody_setRigidBodyFlag_mut( - actor, - RigidBodyFlag::EnableCcd, - true, - ); - } - } - } - - /* - * - * Joints - * - */ - Self::setup_joints( - &mut physics, - impulse_joints, - &mut rapier2static, - &mut rapier2dynamic, - &mut rapier2link, - ); - - for (_, actor) in rapier2static { - scene.add_static_actor(actor); - } - - for (_, actor) in rapier2dynamic { - scene.add_dynamic_actor(actor); - } - - Self { - scene: Some(scene), - shapes, - } - }) - } - - fn setup_joints( - physics: &mut PxPhysicsFoundation, - impulse_joints: &ImpulseJointSet, - rapier2static: &mut HashMap>, - rapier2dynamic: &mut HashMap>, - rapier2link: &mut HashMap, - ) { - unsafe { - for joint in impulse_joints.iter() { - let actor1 = rapier2static - .get_mut(&joint.1.body1()) - .map(|act| &mut **act as *mut PxRigidStatic as *mut PxRigidActor) - .or(rapier2dynamic - .get_mut(&joint.1.body1()) - .map(|act| &mut **act as *mut PxRigidDynamic as *mut PxRigidActor)) - .or(rapier2link - .get_mut(&joint.1.body1()) - .map(|lnk| *lnk as *mut PxRigidActor)) - .unwrap(); - let actor2 = rapier2static - .get_mut(&joint.1.body2()) - .map(|act| &mut **act as *mut PxRigidStatic as *mut PxRigidActor) - .or(rapier2dynamic - .get_mut(&joint.1.body2()) - .map(|act| &mut **act as *mut PxRigidDynamic as *mut PxRigidActor)) - .or(rapier2link - .get_mut(&joint.1.body2()) - .map(|lnk| *lnk as *mut PxRigidActor)) - .unwrap(); - - let px_frame1 = joint.1.data.local_frame1.into_physx(); - let px_frame2 = joint.1.data.local_frame2.into_physx(); - - let px_joint = physx_sys::phys_PxD6JointCreate( - physics.as_mut_ptr(), - actor1, - px_frame1.as_ptr(), - actor2, - px_frame2.as_ptr(), - ); - - let motion_x = if joint.1.data.limit_axes.contains(JointAxesMask::LIN_X) { - physx_sys::PxD6Motion::Limited - } else if !joint.1.data.locked_axes.contains(JointAxesMask::LIN_X) { - physx_sys::PxD6Motion::Free - } else { - physx_sys::PxD6Motion::Locked - }; - let motion_y = if joint.1.data.limit_axes.contains(JointAxesMask::LIN_Y) { - physx_sys::PxD6Motion::Limited - } else if !joint.1.data.locked_axes.contains(JointAxesMask::LIN_Y) { - physx_sys::PxD6Motion::Free - } else { - physx_sys::PxD6Motion::Locked - }; - let motion_z = if joint.1.data.limit_axes.contains(JointAxesMask::LIN_Z) { - physx_sys::PxD6Motion::Limited - } else if !joint.1.data.locked_axes.contains(JointAxesMask::LIN_Z) { - physx_sys::PxD6Motion::Free - } else { - physx_sys::PxD6Motion::Locked - }; - let motion_ax = if joint.1.data.limit_axes.contains(JointAxesMask::ANG_X) { - physx_sys::PxD6Motion::Limited - } else if !joint.1.data.locked_axes.contains(JointAxesMask::ANG_X) { - physx_sys::PxD6Motion::Free - } else { - physx_sys::PxD6Motion::Locked - }; - let motion_ay = if joint.1.data.limit_axes.contains(JointAxesMask::ANG_Y) { - physx_sys::PxD6Motion::Limited - } else if !joint.1.data.locked_axes.contains(JointAxesMask::ANG_Y) { - physx_sys::PxD6Motion::Free - } else { - physx_sys::PxD6Motion::Locked - }; - let motion_az = if joint.1.data.limit_axes.contains(JointAxesMask::ANG_Z) { - physx_sys::PxD6Motion::Limited - } else if !joint.1.data.locked_axes.contains(JointAxesMask::ANG_Z) { - physx_sys::PxD6Motion::Free - } else { - physx_sys::PxD6Motion::Locked - }; - - physx_sys::PxD6Joint_setMotion_mut(px_joint, physx_sys::PxD6Axis::X, motion_x); - physx_sys::PxD6Joint_setMotion_mut(px_joint, physx_sys::PxD6Axis::Y, motion_y); - physx_sys::PxD6Joint_setMotion_mut(px_joint, physx_sys::PxD6Axis::Z, motion_z); - physx_sys::PxD6Joint_setMotion_mut(px_joint, physx_sys::PxD6Axis::Twist, motion_ax); - physx_sys::PxD6Joint_setMotion_mut( - px_joint, - physx_sys::PxD6Axis::Swing1, - motion_ay, - ); - physx_sys::PxD6Joint_setMotion_mut( - px_joint, - physx_sys::PxD6Axis::Swing2, - motion_az, - ); - } - } - } - - pub fn step(&mut self, counters: &mut Counters, params: &IntegrationParameters) { - let mut scratch = unsafe { ScratchBuffer::new(4) }; - - counters.step_started(); - self.scene - .as_mut() - .unwrap() - .step( - params.dt, - None::<&mut physx_sys::PxBaseTask>, - Some(&mut scratch), - true, - ) - .expect("error occurred during PhysX simulation"); - counters.step_completed(); - } - - pub fn sync(&mut self, bodies: &mut RigidBodySet, colliders: &mut ColliderSet) { - let mut sync_pos = |handle: &RigidBodyHandle, pos: Pose3| { - let rb = &mut bodies[*handle]; - rb.set_position(pos, false); - - for coll_handle in rb.colliders() { - let collider = &mut colliders[*coll_handle]; - collider.set_position( - pos * collider - .position_wrt_parent() - .copied() - .unwrap_or(Pose3::IDENTITY), - ); - } - }; - - for actor in self.scene.as_mut().unwrap().get_dynamic_actors() { - let handle = actor.get_user_data(); - let pos = actor.get_global_pose().into_na(); - sync_pos(handle, pos.into()); - } - - /* - for articulation in self.scene.as_mut().unwrap().get_articulations() { - if let Some(articulation) = articulation.as_articulation_reduced_coordinate() { - for link in articulation.get_links() { - let handle = link.get_user_data(); - let pos = link.get_global_pose().into_na(); - sync_pos(handle, pos); - } - } - } - - */ - } -} - -fn physx_collider_from_rapier_collider( - physics: &mut PxPhysicsFoundation, - collider: &Collider, - materials_cache: &mut HashMap<[OrderedFloat; 2], Owner>, -) -> Option<(Owner, Pose3)> { - let mut local_pose = collider - .position_wrt_parent() - .copied() - .unwrap_or(Pose3::IDENTITY); - let cooking_params = PxCookingParams::new(physics).unwrap(); - let shape = collider.shape(); - let shape_flags = if collider.is_sensor() { - ShapeFlags::TriggerShape - } else { - ShapeFlags::SimulationShape - }; - - let material = materials_cache - .entry([ - OrderedFloat(collider.material().friction), - OrderedFloat(collider.material().restitution), - ]) - .or_insert_with(|| { - physics - .create_material( - collider.material().friction, - collider.material().friction, - collider.material().restitution, - (), - ) - .unwrap() - }); - let materials = &mut [material.as_mut()][..]; - - let shape = if let Some(cuboid) = shape.as_cuboid() { - let geometry = PxBoxGeometry::new( - cuboid.half_extents.x, - cuboid.half_extents.y, - cuboid.half_extents.z, - ); - physics.create_shape(&geometry, materials, true, shape_flags, ()) - } else if let Some(ball) = shape.as_ball() { - let geometry = PxSphereGeometry::new(ball.radius); - physics.create_shape(&geometry, materials, true, shape_flags, ()) - } else if let Some(capsule) = shape.as_capsule() { - let center = capsule.center(); - let mut dir = capsule.segment.b - capsule.segment.a; - - if dir.x < 0.0 { - dir = -dir; - } - - let rot = glamx::Quat::from_rotation_arc(Vec3::X, dir); - local_pose = local_pose.prepend_translation(center) * rot; - let geometry = PxCapsuleGeometry::new(capsule.radius, capsule.half_height()); - physics.create_shape(&geometry, materials, true, shape_flags, ()) - } else if let Some(heightfield) = shape.as_heightfield() { - let heights = heightfield.heights(); - let scale = heightfield.scale(); - local_pose = local_pose.prepend_translation(Vec3::new(-scale.x / 2.0, 0.0, -scale.z / 2.0)); - const Y_FACTOR: f32 = 1_000f32; - let mut heightfield_desc; - unsafe { - let samples: Vec<_> = heights - .data() - .iter() - .map(|h| PxHeightFieldSample { - height: (*h * Y_FACTOR) as i16, - materialIndex0: PxBitAndByte { - structgen_pad0: [0; 1], - }, - materialIndex1: PxBitAndByte { - structgen_pad0: [0; 1], - }, - }) - .collect(); - heightfield_desc = physx_sys::PxHeightFieldDesc_new(); - heightfield_desc.nbRows = heights.nrows() as u32; - heightfield_desc.nbColumns = heights.ncols() as u32; - heightfield_desc.samples.stride = std::mem::size_of::() as u32; - heightfield_desc.samples.data = samples.as_ptr() as *const std::ffi::c_void; - } - - let heightfield_desc = PxHeightFieldDesc { - obj: heightfield_desc, - }; - let heightfield = physx::cooking::create_height_field(physics, &heightfield_desc); - - if let Some(mut heightfield) = heightfield { - let flags = PxMeshGeometryFlags::DoubleSided; - let geometry = PxHeightFieldGeometry::new( - &mut *heightfield, - flags, - scale.y / Y_FACTOR, - scale.x / (heights.nrows() as f32 - 1.0), - scale.z / (heights.ncols() as f32 - 1.0), - ); - physics.create_shape(&geometry, materials, true, shape_flags, ()) - } else { - eprintln!("PhysX heightfield construction failed."); - return None; - } - } else if let Some(convex) = shape - .as_convex_polyhedron() - .or(shape.as_round_convex_polyhedron().map(|c| &c.inner_shape)) - { - let vertices = convex.points(); - let mut convex_desc; - unsafe { - convex_desc = physx_sys::PxConvexMeshDesc_new(); - convex_desc.points.count = vertices.len() as u32; - convex_desc.points.stride = (3 * std::mem::size_of::()) as u32; - convex_desc.points.data = vertices.as_ptr() as *const std::ffi::c_void; - convex_desc.flags = PxConvexFlags::ComputeConvex; - } - - let convex_desc = PxConvexMeshDesc { obj: convex_desc }; - let convex = physx::cooking::create_convex_mesh(physics, &cooking_params, &convex_desc); - - if let ConvexMeshCookingResult::Success(mut convex) = convex { - let flags = PxConvexMeshGeometryFlags::empty(); - let scaling = unsafe { PxMeshScale_new() }; - let geometry = PxConvexMeshGeometry::new(&mut convex, &scaling, flags); - physics.create_shape(&geometry, materials, true, shape_flags, ()) - } else { - eprintln!("PhysX convex mesh construction failed."); - return None; - } - } else if let Some(trimesh) = shape.as_trimesh() { - let vertices = trimesh.vertices(); - let indices = trimesh.flat_indices(); - - let mut mesh_desc; - unsafe { - mesh_desc = physx_sys::PxTriangleMeshDesc_new(); - - mesh_desc.points.count = trimesh.vertices().len() as u32; - mesh_desc.points.stride = (3 * std::mem::size_of::()) as u32; - mesh_desc.points.data = vertices.as_ptr() as *const std::ffi::c_void; - - mesh_desc.triangles.count = (indices.len() as u32) / 3; - mesh_desc.triangles.stride = (3 * std::mem::size_of::()) as u32; - mesh_desc.triangles.data = indices.as_ptr() as *const std::ffi::c_void; - } - - let mesh_desc = PxTriangleMeshDesc { obj: mesh_desc }; - let trimesh = physx::cooking::create_triangle_mesh(physics, &cooking_params, &mesh_desc); - - if let TriangleMeshCookingResult::Success(mut trimesh) = trimesh { - let flags = PxMeshGeometryFlags::DoubleSided; - let scaling = unsafe { PxMeshScale_new() }; - let geometry = PxTriangleMeshGeometry::new(&mut trimesh, &scaling, flags); - physics.create_shape(&geometry, materials, true, shape_flags, ()) - } else { - eprintln!("PhysX triangle mesh construction failed."); - return None; - } - } else { - eprintln!("Creating a shape unknown to the PhysX backend."); - return None; - }; - - shape.map(|s| (s, local_pose)) -} - -type PxPhysicsFoundation = PhysicsFoundation; -type PxMaterial = physx::material::PxMaterial<()>; -type PxShape = physx::shape::PxShape<(), PxMaterial>; -type PxArticulationLink = physx::articulation_link::PxArticulationLink; -type PxRigidStatic = physx::rigid_static::PxRigidStatic<(), PxShape>; -type PxRigidDynamic = physx::rigid_dynamic::PxRigidDynamic; -type PxArticulationReducedCoordinate = - physx::articulation_reduced_coordinate::PxArticulationReducedCoordinate<(), PxArticulationLink>; -type PxScene = physx::scene::PxScene< - (), - PxArticulationLink, - PxRigidStatic, - PxRigidDynamic, - PxArticulationReducedCoordinate, - OnCollision, - OnTrigger, - OnConstraintBreak, - OnWakeSleep, - OnAdvance, ->; - -/// Next up, the simulation event callbacks need to be defined, and possibly an -/// allocator callback as well. -struct OnCollision; -impl CollisionCallback for OnCollision { - fn on_collision( - &mut self, - _header: &physx_sys::PxContactPairHeader, - _pairs: &[physx_sys::PxContactPair], - ) { - } -} -struct OnTrigger; -impl TriggerCallback for OnTrigger { - fn on_trigger(&mut self, _pairs: &[physx_sys::PxTriggerPair]) {} -} - -struct OnConstraintBreak; -impl ConstraintBreakCallback for OnConstraintBreak { - fn on_constraint_break(&mut self, _constraints: &[physx_sys::PxConstraintInfo]) {} -} -struct OnWakeSleep; -impl WakeSleepCallback for OnWakeSleep { - fn on_wake_sleep( - &mut self, - _actors: &[&physx::actor::ActorMap], - _is_waking: bool, - ) { - } -} - -struct OnAdvance; -impl AdvanceCallback for OnAdvance { - fn on_advance( - &self, - _actors: &[&physx::rigid_body::RigidBodyMap], - _transforms: &[PxTransform], - ) { - } -} - -unsafe extern "C" fn ccd_filter_shader(data: *mut FilterShaderCallbackInfo) -> PxFilterFlags { - unsafe { - (*(*data).pairFlags) |= physx_sys::PxPairFlags::DetectCcdContact; - } - PxFilterFlags::empty() -} diff --git a/src_testbed/plugin.rs b/src_testbed/plugin.rs deleted file mode 100644 index 87c3b76f1..000000000 --- a/src_testbed/plugin.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::GraphicsManager; -use crate::harness::Harness; -use crate::physics::PhysicsState; -use kiss3d::window::Window; - -pub trait TestbedPlugin { - fn init_plugin(&mut self); - fn init_graphics( - &mut self, - graphics: &mut GraphicsManager, - window: &mut Window, - harness: &mut Harness, - ); - fn clear_graphics(&mut self, graphics: &mut GraphicsManager, window: &mut Window); - fn run_callbacks(&mut self, harness: &mut Harness); - fn step(&mut self, physics: &mut PhysicsState); - fn draw(&mut self, graphics: &mut GraphicsManager, window: &mut Window, harness: &mut Harness); - fn update_ui( - &mut self, - ui_context: &egui::Context, - harness: &mut Harness, - graphics: &mut GraphicsManager, - window: &mut Window, - ); - fn profiling_string(&self) -> String; -} diff --git a/src_testbed/save.rs b/src_testbed/save.rs index 729f58d1b..5eb085bbc 100644 --- a/src_testbed/save.rs +++ b/src_testbed/save.rs @@ -8,9 +8,7 @@ pub struct SerializableTestbedState { pub running: RunMode, pub flags: TestbedStateFlags, pub selected_example: usize, - pub selected_backend: usize, pub example_settings: ExampleSettings, - pub physx_use_two_friction_directions: bool, pub camera: Camera, } @@ -20,9 +18,7 @@ impl TestbedState { running: self.running, flags: self.flags, selected_example: self.selected_display_index, - selected_backend: self.selected_backend, example_settings: self.example_settings.clone(), - physx_use_two_friction_directions: self.physx_use_two_friction_directions, camera, } } @@ -32,9 +28,7 @@ impl TestbedState { self.running = state.running; self.flags = state.flags; self.selected_display_index = state.selected_example; - self.selected_backend = state.selected_backend; self.example_settings = state.example_settings; - self.physx_use_two_friction_directions = state.physx_use_two_friction_directions; *camera = state.camera; } } diff --git a/src_testbed/testbed/app.rs b/src_testbed/testbed/app.rs deleted file mode 100644 index 2a513ad1a..000000000 --- a/src_testbed/testbed/app.rs +++ /dev/null @@ -1,780 +0,0 @@ -//! TestbedApp - the main application runner. - -use crate::Camera; -use crate::debug_render::{DebugRenderPipelineResource, debug_render_scene}; -use crate::graphics::GraphicsManager; -use crate::harness::Harness; -use crate::mouse::SceneMouse; -use crate::save::SerializableTestbedState; -use crate::testbed::hover::highlight_hovered_body; -use crate::ui; -use kiss3d::color::Color; -use kiss3d::event::{Action, Key, WindowEvent}; -use kiss3d::window::Window; -use rapier::dynamics::RigidBodyActivation; -use std::mem; - -use super::Plugins; -use super::graphics_context::TestbedGraphics; -use super::keys::KeysState; -use super::state::{RAPIER_BACKEND, RunMode, TestbedActionFlags, TestbedState, TestbedStateFlags}; -use super::testbed::{SimulationBuilders, Testbed}; - -#[cfg(feature = "other-backends")] -use super::OtherBackends; - -#[cfg(all(feature = "dim3", feature = "other-backends"))] -use super::state::{PHYSX_BACKEND_PATCH_FRICTION, PHYSX_BACKEND_TWO_FRICTION_DIR}; - -/// The main testbed application -pub struct TestbedApp { - builders: SimulationBuilders, - graphics: GraphicsManager, - state: TestbedState, - harness: Harness, - #[cfg(feature = "other-backends")] - other_backends: OtherBackends, - plugins: Plugins, -} - -impl TestbedApp { - pub fn save_file_path() -> String { - format!("testbed_state_{}.autosave.json", env!("CARGO_CRATE_NAME")) - } - - pub fn new_empty() -> Self { - let graphics = GraphicsManager::new(); - let state = TestbedState::default(); - let harness = Harness::new_empty(); - #[cfg(feature = "other-backends")] - let other_backends = OtherBackends { - #[cfg(feature = "dim3")] - physx: None, - }; - - TestbedApp { - builders: Vec::new(), - plugins: Plugins(Vec::new()), - graphics, - state, - harness, - #[cfg(feature = "other-backends")] - other_backends, - } - } - - pub fn from_builders(builders: SimulationBuilders) -> Self { - let mut res = TestbedApp::new_empty(); - res.set_builders(builders); - res - } - - pub fn set_builders(&mut self, builders: SimulationBuilders) { - use super::state::ExampleEntry; - use indexmap::IndexSet; - - // Collect unique groups in order of first appearance - let mut groups: IndexSet<&'static str> = IndexSet::new(); - for example in &builders { - groups.insert(example.group); - } - - // Build the display order: group by group, preserving original order within each group - let mut examples = Vec::new(); - for group in &groups { - for (builder_index, example) in builders.iter().enumerate() { - if example.group == *group { - examples.push(ExampleEntry { - name: example.name, - group: example.group, - builder_index, - }); - } - } - } - - self.state.example_groups = groups.into_iter().collect(); - self.state.examples = examples; - self.builders = builders; - } - - pub async fn run(self) { - self.run_with_init(|_| {}).await - } - - pub async fn run_with_init(mut self, init: impl FnMut(&mut Testbed)) { - // Install a default logger so warnings emitted by the example - // builders (e.g. the MJCF loader complaining about missing mesh - // files or features) actually print. Uses RUST_LOG if set, falls - // back to `warn` level otherwise. `try_init` yields silently to a - // user-installed logger so embedders aren't forced into this - // backend. - let _ = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")) - .try_init(); - - #[cfg(feature = "profiler_ui")] - profiling::puffin::set_scopes_on(true); - - // Check for benchmark mode - let args: Vec = std::env::args().collect(); - if args.iter().any(|a| a == "--bench") { - self.run_benchmark(); - return; - } - - self.run_async(init).await - } - - fn run_benchmark(&mut self) { - use std::fs::File; - use std::io::{BufWriter, Write}; - - let num_bench_iters = 1000u32; - let builders = mem::take(&mut self.builders); - let backend_names = self.state.backend_names.clone(); - - for builder in &builders { - let mut results = Vec::new(); - println!("Running benchmark for {}", builder.name); - - for (backend_id, backend) in backend_names.iter().enumerate() { - println!("|_ using backend {backend}"); - self.state.selected_backend = backend_id; - self.harness = Harness::new_empty(); - - let mut testbed = Testbed { - graphics: None, - state: &mut self.state, - harness: &mut self.harness, - #[cfg(feature = "other-backends")] - other_backends: &mut self.other_backends, - plugins: &mut self.plugins, - }; - (builder.builder)(&mut testbed); - - let mut timings = Vec::new(); - for k in 0..num_bench_iters { - if self.state.selected_backend == RAPIER_BACKEND { - self.harness.step(); - } - - #[cfg(all(feature = "dim3", feature = "other-backends"))] - { - if self.state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION - || self.state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR - { - self.other_backends.physx.as_mut().unwrap().step( - &mut self.harness.physics.pipeline.counters, - &self.harness.physics.integration_parameters, - ); - self.other_backends.physx.as_mut().unwrap().sync( - &mut self.harness.physics.bodies, - &mut self.harness.physics.colliders, - ); - } - } - - if k > 0 { - timings.push(self.harness.physics.pipeline.counters.step_time.time_ms()); - } - } - results.push(timings); - } - - use inflector::Inflector; - let filename = format!("{}.csv", builder.name.to_camel_case()); - let mut file = BufWriter::new(File::create(filename).unwrap()); - - write!(file, "{}", backend_names[0]).unwrap(); - for backend in &backend_names[1..] { - write!(file, ",{backend}").unwrap(); - } - writeln!(file).unwrap(); - - for i in 0..results[0].len() { - write!(file, "{}", results[0][i]).unwrap(); - for result in &results[1..] { - write!(file, ",{}", result[i]).unwrap(); - } - writeln!(file).unwrap(); - } - } - } - - async fn run_async(mut self, mut init: impl FnMut(&mut Testbed)) { - let title = if cfg!(feature = "dim2") { - "Rapier: 2D demos" - } else { - "Rapier: 3D demos" - }; - - let mut window = Window::new_with_size(title, 1280, 720).await; - window.set_background_color(Color::new(245.0 / 255.0, 245.0 / 255.0, 236.0 / 255.0, 1.0)); - window.set_ambient(0.1); - - let mut debug_render = DebugRenderPipelineResource::default(); - let mut camera = Camera::default(); - let mut scene_mouse = SceneMouse::new(); - let mut keys = KeysState::default(); - - // User init - let testbed_gfx = TestbedGraphics { - graphics: &mut self.graphics, - window: &mut window, - camera: &mut camera, - mouse: &mut scene_mouse, - keys: &mut keys, - settings: None, - }; - - let mut testbed = Testbed { - graphics: Some(testbed_gfx), - state: &mut self.state, - harness: &mut self.harness, - #[cfg(feature = "other-backends")] - other_backends: &mut self.other_backends, - plugins: &mut self.plugins, - }; - - init(&mut testbed); - - // Main render loop - #[cfg(feature = "dim3")] - while window - .render_3d(self.graphics.scene_mut(), &mut camera) - .await - { - self.run_frame( - &mut window, - &mut debug_render, - &mut camera, - &mut scene_mouse, - &mut keys, - ); - } - - #[cfg(feature = "dim2")] - while window - .render_2d(self.graphics.scene_mut(), &mut camera) - .await - { - self.run_frame( - &mut window, - &mut debug_render, - &mut camera, - &mut scene_mouse, - &mut keys, - ); - } - } - - fn run_frame( - &mut self, - window: &mut Window, - debug_render: &mut DebugRenderPipelineResource, - camera: &mut Camera, - scene_mouse: &mut SceneMouse, - keys: &mut KeysState, - ) { - profiling::finish_frame!(); - - // Handle input events - self.handle_events(window, keys); - - // Handle the vehicle controller if there is one. - #[cfg(feature = "dim3")] - { - self.update_vehicle_controller(keys); - } - - // Update mouse state - let cursor_pos = window.cursor_pos(); - scene_mouse.update_from_window(cursor_pos, window.size().into(), camera); - - // Handle action flags - self.handle_action_flags(window, camera, scene_mouse, keys); - - // Handle sleep settings - self.handle_sleep_settings(); - - // Run simulation - if self.state.running != RunMode::Stop { - for _ in 0..self.state.nsteps { - if self.state.selected_backend == RAPIER_BACKEND { - let mut testbed_gfx = TestbedGraphics { - graphics: &mut self.graphics, - window, - camera, - mouse: scene_mouse, - keys, - settings: Some(&mut self.state.example_settings), - }; - self.harness.step_with_graphics(Some(&mut testbed_gfx)); - - for plugin in &mut self.plugins.0 { - plugin.step(&mut self.harness.physics); - } - } - - #[cfg(all(feature = "dim3", feature = "other-backends"))] - { - if self.state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION - || self.state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR - { - self.other_backends.physx.as_mut().unwrap().step( - &mut self.harness.physics.pipeline.counters, - &self.harness.physics.integration_parameters, - ); - self.other_backends.physx.as_mut().unwrap().sync( - &mut self.harness.physics.bodies, - &mut self.harness.physics.colliders, - ); - } - } - - for plugin in &mut self.plugins.0 { - plugin.run_callbacks(&mut self.harness); - } - } - - if self.state.running == RunMode::Step { - self.state.running = RunMode::Stop; - } - } - - // Autosave state. - #[cfg(not(target_arch = "wasm32"))] - { - let new_save_data = self.state.save_data(*camera); - if self.state.prev_save_data != new_save_data { - // Save the data in a file. - let data = serde_json::to_string_pretty(&new_save_data).unwrap(); - if let Err(e) = std::fs::write(Self::save_file_path(), &data) { - eprintln!("Failed to write autosave file: {}", e); - } - self.state.prev_save_data = new_save_data; - } - } - - highlight_hovered_body(&mut self.graphics, scene_mouse, &self.harness.physics); - - // Update graphics - self.graphics.draw( - self.state.flags, - &self.harness.physics.bodies, - &self.harness.physics.colliders, - ); - - // Draw debug render - debug_render_scene(window, debug_render, &self.harness); - - // Draw UI - window.draw_ui(|ctx| { - ui::update_ui(ctx, &mut self.state, &mut self.harness, debug_render); - }); - - self.state.prev_flags = self.state.flags; - } - - fn handle_events(&mut self, window: &mut Window, keys: &mut KeysState) { - for event in window.events().iter() { - match event.value { - WindowEvent::Key(key, Action::Press, _) => { - // Track pressed keys - if !keys.pressed_keys.contains(&key) { - keys.pressed_keys.push(key); - } - // Update modifier states - match key { - Key::LShift | Key::RShift => keys.shift = true, - Key::LControl | Key::RControl => keys.ctrl = true, - Key::LAlt | Key::RAlt => keys.alt = true, - _ => {} - } - } - WindowEvent::Key(key, Action::Release, _) => { - // Remove from pressed keys - keys.pressed_keys.retain(|k| *k != key); - // Handle special keys - match key { - Key::T => { - if self.state.running == RunMode::Stop { - self.state.running = RunMode::Running; - } else { - self.state.running = RunMode::Stop; - } - } - Key::S => { - self.state.running = RunMode::Step; - } - Key::R => { - self.state - .action_flags - .set(TestbedActionFlags::EXAMPLE_CHANGED, true); - } - Key::LShift | Key::RShift => keys.shift = false, - Key::LControl | Key::RControl => keys.ctrl = false, - Key::LAlt | Key::RAlt => keys.alt = false, - _ => {} - } - } - _ => {} - } - } - } - - #[cfg(feature = "dim3")] - fn update_vehicle_controller(&mut self, keys: &mut KeysState) { - use rapier::prelude::QueryFilter; - - if self.state.running == RunMode::Stop { - return; - } - - if let Some(vehicle) = &mut self.state.vehicle_controller { - let mut engine_force = 0.0; - let mut steering_angle = 0.0; - - println!("Pressed: {:?}", keys); - if keys.pressed(Key::Right) { - steering_angle += -0.7; - } - if keys.pressed(Key::Left) { - steering_angle += 0.7; - } - if keys.pressed(Key::Up) { - engine_force += 30.0; - } - if keys.pressed(Key::Down) { - engine_force += -30.0; - } - - let wheels = vehicle.wheels_mut(); - wheels[0].engine_force = engine_force; - wheels[0].steering = steering_angle; - wheels[1].engine_force = engine_force; - wheels[1].steering = steering_angle; - - let query_pipeline = self.harness.physics.broad_phase.as_query_pipeline_mut( - self.harness.physics.narrow_phase.query_dispatcher(), - &mut self.harness.physics.bodies, - &mut self.harness.physics.colliders, - QueryFilter::exclude_dynamic().exclude_rigid_body(vehicle.chassis), - ); - - vehicle.update_vehicle( - self.harness.physics.integration_parameters.dt, - query_pipeline, - ); - } - } - - fn handle_action_flags( - &mut self, - window: &mut Window, - camera: &mut Camera, - scene_mouse: &mut SceneMouse, - keys: &mut KeysState, - ) { - #[cfg(not(target_arch = "wasm32"))] - { - let app_started = self - .state - .action_flags - .contains(TestbedActionFlags::APP_STARTED); - - if app_started { - self.state - .action_flags - .set(TestbedActionFlags::APP_STARTED, false); - if let Some(saved_state) = std::fs::read(Self::save_file_path()) - .ok() - .and_then(|data| serde_json::from_slice::(&data).ok()) - { - self.state.apply_saved_data(saved_state, camera); - self.state.camera_locked = true; - } - } - } - - let backend_changed = self - .state - .action_flags - .contains(TestbedActionFlags::BACKEND_CHANGED); - if backend_changed { - self.state - .action_flags - .set(TestbedActionFlags::BACKEND_CHANGED, false); - self.state - .action_flags - .set(TestbedActionFlags::EXAMPLE_CHANGED, true); - self.state.camera_locked = true; - } - - let restarted = self - .state - .action_flags - .contains(TestbedActionFlags::RESTART); - if restarted { - self.state - .action_flags - .set(TestbedActionFlags::RESTART, false); - self.state.camera_locked = true; - self.state - .action_flags - .set(TestbedActionFlags::EXAMPLE_CHANGED, true); - } - - let example_changed = self - .state - .action_flags - .contains(TestbedActionFlags::EXAMPLE_CHANGED); - if example_changed { - self.state - .action_flags - .set(TestbedActionFlags::EXAMPLE_CHANGED, false); - self.clear(window); - self.harness.clear_callbacks(); - - if !self.state.camera_locked { - *camera = Camera::default(); - } - - if !restarted { - self.state.example_settings.clear(); - } - - // Clamp selected_display_index to valid range - let max_index = self.state.examples.len().saturating_sub(1); - self.state.selected_display_index = self.state.selected_display_index.min(max_index); - - if !self.builders.is_empty() { - let builder_index = self.state.selected_builder_index(); - let builder = self.builders[builder_index].builder; - let testbed_gfx = TestbedGraphics { - graphics: &mut self.graphics, - window, - camera, - mouse: scene_mouse, - keys, - settings: None, - }; - - let mut testbed = Testbed { - graphics: Some(testbed_gfx), - state: &mut self.state, - harness: &mut self.harness, - #[cfg(feature = "other-backends")] - other_backends: &mut self.other_backends, - plugins: &mut self.plugins, - }; - builder(&mut testbed); - } - - self.state.camera_locked = false; - } - - if self - .state - .action_flags - .contains(TestbedActionFlags::RESET_WORLD_GRAPHICS) - { - self.state - .action_flags - .set(TestbedActionFlags::RESET_WORLD_GRAPHICS, false); - for (handle, _) in self.harness.physics.bodies.iter() { - self.graphics.add_body_colliders( - window, - handle, - &self.harness.physics.bodies, - &self.harness.physics.colliders, - ); - } - - for (handle, co) in self.harness.physics.colliders.iter() { - if co.parent().is_none() { - self.graphics - .add_collider(window, handle, &self.harness.physics.colliders); - } - } - } - - if self - .state - .action_flags - .contains(TestbedActionFlags::TAKE_SNAPSHOT) - { - self.state - .action_flags - .set(TestbedActionFlags::TAKE_SNAPSHOT, false); - self.state.snapshot = Some(self.harness.physics.snapshot()); - } - - if self - .state - .action_flags - .contains(TestbedActionFlags::RESTORE_SNAPSHOT) - { - self.state - .action_flags - .set(TestbedActionFlags::RESTORE_SNAPSHOT, false); - if let Some(snapshot) = &self.state.snapshot { - self.harness.physics.restore_snapshot(snapshot.clone()); - self.state - .action_flags - .set(TestbedActionFlags::RESET_WORLD_GRAPHICS, true); - } - } - - if example_changed - || self.state.prev_flags.contains(TestbedStateFlags::WIREFRAME) - != self.state.flags.contains(TestbedStateFlags::WIREFRAME) - { - self.graphics.toggle_wireframe_mode( - &self.harness.physics.colliders, - self.state.flags.contains(TestbedStateFlags::WIREFRAME), - ); - } - - if self - .state - .action_flags - .contains(TestbedActionFlags::FRAME_SCENE) - { - self.state - .action_flags - .set(TestbedActionFlags::FRAME_SCENE, false); - frame_scene( - &self.harness.physics.colliders, - &self.harness.physics.bodies, - &self.graphics, - camera, - ); - } - } - - fn handle_sleep_settings(&mut self) { - if self.state.prev_flags.contains(TestbedStateFlags::SLEEP) - != self.state.flags.contains(TestbedStateFlags::SLEEP) - { - if self.state.flags.contains(TestbedStateFlags::SLEEP) { - for (_, body) in self.harness.physics.bodies.iter_mut() { - body.activation_mut().normalized_linear_threshold = - RigidBodyActivation::default_normalized_linear_threshold(); - body.activation_mut().angular_threshold = - RigidBodyActivation::default_angular_threshold(); - } - } else { - for (_, body) in self.harness.physics.bodies.iter_mut() { - body.wake_up(true); - body.activation_mut().normalized_linear_threshold = -1.0; - } - } - } - } - - fn clear(&mut self, window: &mut Window) { - self.state.can_grab_behind_ground = false; - self.graphics.clear(); - - for mut plugin in self.plugins.0.drain(..) { - plugin.clear_graphics(&mut self.graphics, window); - } - } -} - -/// Recenter the camera so the AABB of all the collider and visual-mesh -/// shapes in the scene fills the viewport. Acts as a "frame all" / -/// "best view" command. -fn frame_scene( - colliders: &rapier::geometry::ColliderSet, - bodies: &rapier::dynamics::RigidBodySet, - graphics: &crate::graphics::GraphicsManager, - camera: &mut Camera, -) { - use rapier::parry::bounding_volume::BoundingVolume; - use rapier::parry::math::Real; - - // Helper: skip AABBs with non-finite extents (half-spaces, broken - // shapes) so they don't blow up the union. - fn is_finite_aabb(a: &rapier::parry::bounding_volume::Aabb) -> bool { - if !a.mins.x.is_finite() - || !a.mins.y.is_finite() - || !a.maxs.x.is_finite() - || !a.maxs.y.is_finite() - { - return false; - } - #[cfg(feature = "dim3")] - if !a.mins.z.is_finite() || !a.maxs.z.is_finite() { - return false; - } - true - } - - let mut aabb: Option = None; - let mut merge = |a: rapier::parry::bounding_volume::Aabb| { - if !is_finite_aabb(&a) { - return; - } - aabb = Some(match aabb { - None => a, - Some(prev) => prev.merged(&a), - }); - }; - // 1. Physical colliders. - for (_, co) in colliders.iter() { - merge(co.compute_aabb()); - } - // 2. Render-only meshes attached to a body — they're not in the - // collider set but still visible, and their extent can exceed - // the colliders' (e.g. simplified primitive colliders + full - // visual mesh). Compute each one's AABB at its current world - // pose: `body.position * vm.delta`. - for vm in graphics.body_attached_nodes() { - let Some(rb) = bodies.get(vm.body) else { - continue; - }; - let world_pose = *rb.position() * vm.delta; - merge(vm.shape.compute_aabb(&world_pose)); - } - let Some(aabb) = aabb else { - return; - }; - - let center = aabb.center(); - let extents = aabb.extents(); - - #[cfg(feature = "dim3")] - { - // Bounding-sphere radius around the AABB center. - let half = extents * 0.5 as Real; - let radius = (half.x * half.x + half.y * half.y + half.z * half.z) - .sqrt() - .max(0.5); - let fov = camera.fov(); - // Distance such that the bounding sphere subtends the camera's - // vertical FoV exactly. A small margin (1.1×) prevents the AABB - // edges from being clipped by the frustum / window aspect. - let dist = radius / (fov as Real * 0.5).sin().max(1e-3) * 1.1; - - let kiss_center = - kiss3d::glamx::Vec3::new(center.x as f32, center.y as f32, center.z as f32); - // Approach from along +X / +Y / +Z roughly equally so any axis-up - // convention reads naturally. - let dir = kiss3d::glamx::Vec3::new(1.0, 1.0, 1.0).normalize(); - let eye = kiss_center + dir * dist as f32; - camera.look_at(eye, kiss_center); - } - - #[cfg(feature = "dim2")] - { - let kiss_center = kiss3d::glamx::Vec2::new(center.x as f32, center.y as f32); - // PanZoomCamera2d's "zoom" is pixels per world unit. Pick a zoom - // that fits the larger of the two extents in the smallest framebuffer - // dimension we're aware of (assume 800×600 default). - let max_extent = extents.x.max(extents.y).max(0.5); - let zoom = (600.0 as Real / max_extent / 1.2) as f32; - camera.look_at(kiss_center, zoom); - } -} diff --git a/src_testbed/testbed/graphics_context.rs b/src_testbed/testbed/graphics_context.rs deleted file mode 100644 index 3439bede7..000000000 --- a/src_testbed/testbed/graphics_context.rs +++ /dev/null @@ -1,138 +0,0 @@ -//! Graphics context for a frame. - -use kiss3d::color::Color; -use kiss3d::window::Window; -use rapier::dynamics::RigidBodyHandle; -use rapier::dynamics::RigidBodySet; -use rapier::geometry::{ColliderHandle, ColliderSet, SharedShape}; - -use crate::Camera; -use crate::graphics::GraphicsManager; -use crate::mouse::SceneMouse; -use crate::settings::ExampleSettings; - -use super::keys::KeysState; - -/// Context for graphics operations during a frame -pub struct TestbedGraphics<'a> { - pub graphics: &'a mut GraphicsManager, - pub window: &'a mut Window, - pub camera: &'a mut Camera, - pub mouse: &'a SceneMouse, - pub keys: &'a KeysState, - pub settings: Option<&'a mut ExampleSettings>, -} - -impl<'a> TestbedGraphics<'a> { - pub fn set_body_color(&mut self, body: RigidBodyHandle, color: Color, tmp_color: bool) { - self.graphics.set_body_color(body, color, tmp_color); - } - - pub fn add_body( - &mut self, - handle: RigidBodyHandle, - bodies: &RigidBodySet, - colliders: &ColliderSet, - ) { - self.graphics - .add_body_colliders(self.window, handle, bodies, colliders); - } - - pub fn remove_body(&mut self, handle: RigidBodyHandle) { - self.graphics.remove_body_nodes(handle); - } - - pub fn add_collider(&mut self, handle: ColliderHandle, colliders: &ColliderSet) { - self.graphics.add_collider(self.window, handle, colliders); - } - - pub fn remove_collider(&mut self, handle: ColliderHandle) { - self.graphics.remove_collider_nodes(handle); - } - - /// Attach a render-only mesh to `body`. The mesh follows the body's - /// pose offset by `local_pose` and does not participate in physics. - /// - /// `uvs` is an optional per-vertex UV buffer (used only when the - /// shape is a `TriMesh` *and* `texture` is also set). `normals` is an - /// optional per-vertex normal buffer parallel to the shape's vertices; - /// when supplied the renderer uses it verbatim instead of recomputing - /// flat per-face normals. `texture` is an optional path to a 2D color - /// image to apply. - pub fn add_body_render_mesh( - &mut self, - body: RigidBodyHandle, - shape: &SharedShape, - local_pose: rapier::math::Pose, - color: Color, - uvs: Option<&[[f32; 2]]>, - normals: Option<&[[f32; 3]]>, - texture: Option<&std::path::Path>, - material: Option, - ) { - self.graphics.add_body_render_mesh( - self.window, - body, - shape, - local_pose, - color, - uvs, - normals, - texture, - material, - ); - } - - /// Show or hide every collider-derived render node. - pub fn set_colliders_visible(&mut self, visible: bool) { - self.graphics.set_colliders_visible(visible); - } - - /// Show or hide every body-attached render-only mesh (registered via - /// [`Self::add_body_render_mesh`]). - pub fn set_body_render_meshes_visible(&mut self, visible: bool) { - self.graphics.set_body_render_meshes_visible(visible); - } - - pub fn keys(&self) -> &KeysState { - self.keys - } - - pub fn mouse(&self) -> &SceneMouse { - self.mouse - } - - /// Update collider graphics after shape modification - pub fn update_collider(&mut self, handle: ColliderHandle, colliders: &ColliderSet) { - // Remove and re-add the collider to update its graphics - self.graphics.remove_collider_nodes(handle); - self.graphics.add_collider(self.window, handle, colliders); - } - - /// Get the camera rotation as a unit quaternion (3D only) - #[cfg(feature = "dim3")] - pub fn camera_rotation(&self) -> na::UnitQuaternion { - // Calculate rotation from orbit camera angles - let rot_x = na::UnitQuaternion::from_axis_angle(&na::Vector3::y_axis(), self.camera.at().x); - let rot_y = - na::UnitQuaternion::from_axis_angle(&(-na::Vector3::x_axis()), self.camera.at().y); - rot_x * rot_y - } - - /// Get the camera forward direction (3D only) - #[cfg(feature = "dim3")] - pub fn camera_fwd_dir(&self) -> na::Vector3 { - let rot = self.camera_rotation(); - rot * na::Vector3::z() - } - - /// Get mutable access to the egui context for custom UI - pub fn egui_context(&self) -> &egui::Context { - self.window.egui_context() - } - - /// Get mutable access to the egui context for custom UI - pub fn egui_context_mut(&mut self) -> &mut egui::Context { - self.window.egui_context_mut() - } -} diff --git a/src_testbed/testbed/hover.rs b/src_testbed/testbed/hover.rs index e036eac8b..960f9320b 100644 --- a/src_testbed/testbed/hover.rs +++ b/src_testbed/testbed/hover.rs @@ -1,8 +1,9 @@ #![allow(clippy::useless_conversion)] // Conversions are needed for switching between f32/f64. +use crate::GraphicsManager; use crate::mouse::SceneMouse; -use crate::{GraphicsManager, PhysicsState}; use kiss3d::prelude::*; +use rapier::pipeline::PhysicsWorld; use rapier::prelude::QueryFilter; #[cfg(feature = "dim3")] @@ -12,7 +13,7 @@ use rapier::prelude::{Ray, Real}; pub fn highlight_hovered_body( graphics_manager: &mut GraphicsManager, mouse: &SceneMouse, - physics: &PhysicsState, + world: &PhysicsWorld, ) { use rapier::math::Vector; @@ -20,15 +21,15 @@ pub fn highlight_hovered_body( // Convert from kiss3d Vec2 (f32) to rapier Vector (may be f64) let pt = Vector::new(pt.x as _, pt.y as _); - let query_pipeline = physics.broad_phase.as_query_pipeline( - physics.narrow_phase.query_dispatcher(), - &physics.bodies, - &physics.colliders, + let query_pipeline = world.broad_phase.as_query_pipeline( + world.narrow_phase.query_dispatcher(), + &world.bodies, + &world.colliders, QueryFilter::only_dynamic(), ); for (handle, _) in query_pipeline.intersect_point(pt) { - let collider = &physics.colliders[handle]; + let collider = &world.colliders[handle]; if let Some(parent_handle) = collider.parent() { graphics_manager.set_body_color(parent_handle, RED, true); } @@ -40,21 +41,21 @@ pub fn highlight_hovered_body( pub fn highlight_hovered_body( graphics_manager: &mut GraphicsManager, mouse: &SceneMouse, - physics: &PhysicsState, + world: &PhysicsWorld, ) { if let Some((ray_origin, ray_dir)) = mouse.ray { let ray = Ray::new(ray_origin.into(), ray_dir.into()); - let query_pipeline = physics.broad_phase.as_query_pipeline( - physics.narrow_phase.query_dispatcher(), - &physics.bodies, - &physics.colliders, + let query_pipeline = world.broad_phase.as_query_pipeline( + world.narrow_phase.query_dispatcher(), + &world.bodies, + &world.colliders, QueryFilter::only_dynamic(), ); let hit = query_pipeline.cast_ray(&ray, Real::MAX, true); if let Some((handle, _)) = hit { - let collider = &physics.colliders[handle]; + let collider = &world.colliders[handle]; if let Some(parent_handle) = collider.parent() { graphics_manager.set_body_color(parent_handle, RED, true); diff --git a/src_testbed/testbed/mod.rs b/src_testbed/testbed/mod.rs index e41698b60..6c2df49ff 100644 --- a/src_testbed/testbed/mod.rs +++ b/src_testbed/testbed/mod.rs @@ -1,35 +1,15 @@ -//! Testbed module - visual debugging and example runner for Rapier physics. +//! Testbed support types: UI/run state, keyboard state, and hover highlighting. #![allow(clippy::bad_bit_mask)] #![allow(clippy::unnecessary_cast)] #![allow(clippy::module_inception)] -mod app; -mod graphics_context; -mod hover; -mod keys; -mod state; -mod testbed; +pub(crate) mod hover; +pub(crate) mod keys; +pub(crate) mod state; -#[cfg(all(feature = "dim3", feature = "other-backends"))] -use crate::physx_backend::PhysxWorld; - -// Re-export all public types -pub use app::TestbedApp; -pub use graphics_context::TestbedGraphics; +// Re-export the public testbed-support types. pub use keys::KeysState; -pub use state::{RunMode, TestbedActionFlags, TestbedState, TestbedStateFlags, UiTab}; -pub use testbed::{Example, Testbed}; - -// Internal re-exports for other modules -pub(crate) use state::{PHYSX_BACKEND_PATCH_FRICTION, PHYSX_BACKEND_TWO_FRICTION_DIR}; - -/// Backend implementations for other physics engines -#[cfg(feature = "other-backends")] -pub struct OtherBackends { - #[cfg(feature = "dim3")] - pub physx: Option, -} - -/// Container for testbed plugins -pub struct Plugins(pub Vec>); +pub use state::{ + ExampleEntry, RunMode, TestbedActionFlags, TestbedState, TestbedStateFlags, UiTab, +}; diff --git a/src_testbed/testbed/state.rs b/src_testbed/testbed/state.rs index 919aea2c9..65cd6acc3 100644 --- a/src_testbed/testbed/state.rs +++ b/src_testbed/testbed/state.rs @@ -1,13 +1,8 @@ //! Testbed state types and flags. use bitflags::bitflags; -use na::Point3; -#[cfg(feature = "dim3")] -use rapier::control::DynamicRayCastVehicleController; - -use crate::harness::RapierBroadPhaseType; -use crate::physics::PhysicsSnapshot; +use crate::physics::{PhysicsSnapshot, RapierBroadPhaseType}; use crate::save::SerializableTestbedState; use crate::settings::ExampleSettings; @@ -20,6 +15,17 @@ pub enum RunMode { Step, } +/// A loop transition requested from the UI: stop entirely, or switch to another +/// example (or re-run the current one). The target is carried in +/// [`TestbedState::selected_display_index`]; this only signals the +/// example-owned `while viewer.render_frame()` loop to exit so the outer demo +/// runner can dispatch the next example. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum Transition { + Quit, + Switch, +} + bitflags! { /// Flags for controlling what is displayed in the testbed #[derive(Copy, Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)] @@ -45,13 +51,14 @@ impl Default for TestbedStateFlags { } bitflags! { - /// Flags for testbed actions that need to be processed + /// Flags for in-frame testbed actions applied to the borrowed world. + /// + /// Example switching / restart / backend changes are no longer flags — they + /// are handled by [`Transition`], which makes the example's render loop exit + /// so the outer demo runner re-dispatches. #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub struct TestbedActionFlags: u32 { const RESET_WORLD_GRAPHICS = 1 << 0; - const EXAMPLE_CHANGED = 1 << 1; - const RESTART = 1 << 2; - const BACKEND_CHANGED = 1 << 3; const TAKE_SNAPSHOT = 1 << 4; const RESTORE_SNAPSHOT = 1 << 5; const APP_STARTED = 1 << 6; @@ -61,10 +68,6 @@ bitflags! { } } -pub(crate) const RAPIER_BACKEND: usize = 0; -pub(crate) const PHYSX_BACKEND_PATCH_FRICTION: usize = 1; -pub(crate) const PHYSX_BACKEND_TWO_FRICTION_DIR: usize = 2; - /// Which tab is currently selected in the UI #[derive(Default, Copy, Clone, PartialEq, Eq, Debug)] pub enum UiTab { @@ -74,45 +77,46 @@ pub enum UiTab { Performance, } -/// Information about an example for UI display +/// Information about an example for UI display. #[derive(Clone, Debug)] pub struct ExampleEntry { pub name: &'static str, pub group: &'static str, - /// Index in the original builders array - pub builder_index: usize, +} + +impl ExampleEntry { + pub fn new(group: &'static str, name: &'static str) -> Self { + Self { name, group } + } } /// State for the testbed application pub struct TestbedState { pub running: RunMode, - pub draw_colls: bool, - #[cfg(feature = "dim3")] - pub vehicle_controller: Option, - pub grabbed_object_plane: (Point3, na::Vector3), pub can_grab_behind_ground: bool, - pub drawing_ray: Option>, pub prev_flags: TestbedStateFlags, pub flags: TestbedStateFlags, pub action_flags: TestbedActionFlags, - pub backend_names: Vec<&'static str>, + /// Pending loop transition (example switch / quit) requested from the UI. + pub transition: Option, + /// `true` while a restart / solver-parameter change is the reason for a + /// pending [`Transition::Switch`]; such switches preserve the user's + /// example-setting edits, whereas selecting a different example clears them. + pub preserve_settings_on_switch: bool, /// Examples in display order (grouped, then by original order within group) pub examples: Vec, /// Unique group names in order of first appearance pub example_groups: Vec<&'static str>, /// Currently selected position in the display order pub selected_display_index: usize, - pub selected_backend: usize, pub example_settings: ExampleSettings, pub broad_phase_type: RapierBroadPhaseType, - pub physx_use_two_friction_directions: bool, pub snapshot: Option, - pub nsteps: usize, pub camera_locked: bool, pub selected_tab: UiTab, pub prev_save_data: SerializableTestbedState, /// Unit up-vector kept in sync with the camera (see - /// [`crate::Testbed::set_up_axis`]). The gravity slider in the + /// [`crate::TestbedViewer::set_up_axis`]). The gravity slider in the /// testbed UI reads this so it can keep gravity aligned with "down" /// (`-up_axis`) instead of the hard-coded Y-axis it used to assume. /// Defaults to `Vector::Y`. @@ -121,35 +125,21 @@ pub struct TestbedState { impl Default for TestbedState { fn default() -> Self { - #[allow(unused_mut)] - let mut backend_names = vec!["rapier"]; - #[cfg(all(feature = "dim3", feature = "other-backends"))] - backend_names.push("physx (patch friction)"); - #[cfg(all(feature = "dim3", feature = "other-backends"))] - backend_names.push("physx (two friction dir)"); - let flags = TestbedStateFlags::default(); Self { running: RunMode::Running, - draw_colls: false, - #[cfg(feature = "dim3")] - vehicle_controller: None, - grabbed_object_plane: (Point3::origin(), na::zero()), can_grab_behind_ground: false, - drawing_ray: None, snapshot: None, prev_flags: flags, flags, - action_flags: TestbedActionFlags::APP_STARTED | TestbedActionFlags::EXAMPLE_CHANGED, - backend_names, + action_flags: TestbedActionFlags::APP_STARTED, + transition: None, + preserve_settings_on_switch: false, examples: Vec::new(), example_groups: Vec::new(), example_settings: ExampleSettings::default(), selected_display_index: 0, - selected_backend: RAPIER_BACKEND, broad_phase_type: RapierBroadPhaseType::default(), - physx_use_two_friction_directions: true, - nsteps: 1, camera_locked: false, selected_tab: UiTab::default(), prev_save_data: SerializableTestbedState::default(), @@ -159,11 +149,25 @@ impl Default for TestbedState { } impl TestbedState { - /// Get the builder index for the currently selected example - pub fn selected_builder_index(&self) -> usize { - self.examples - .get(self.selected_display_index) - .map(|e| e.builder_index) - .unwrap_or(0) + /// Builds the grouped display order from a flat list of examples. + pub fn set_examples(&mut self, examples: Vec) { + use indexmap::IndexSet; + + let mut groups: IndexSet<&'static str> = IndexSet::new(); + for example in &examples { + groups.insert(example.group); + } + + let mut ordered = Vec::new(); + for group in &groups { + for example in &examples { + if example.group == *group { + ordered.push(example.clone()); + } + } + } + + self.example_groups = groups.into_iter().collect(); + self.examples = ordered; } } diff --git a/src_testbed/testbed/testbed.rs b/src_testbed/testbed/testbed.rs deleted file mode 100644 index f6259c6ed..000000000 --- a/src_testbed/testbed/testbed.rs +++ /dev/null @@ -1,361 +0,0 @@ -//! Testbed struct for building examples. - -use kiss3d::color::Color; -use rapier::dynamics::{ - ImpulseJointSet, IntegrationParameters, MultibodyJointSet, RigidBodyHandle, RigidBodySet, -}; -use rapier::geometry::{ColliderHandle, ColliderSet, SharedShape}; -use rapier::pipeline::{PhysicsHooks, PhysicsWorld}; - -#[cfg(feature = "dim3")] -use {glamx::Vec3, rapier::control::DynamicRayCastVehicleController}; - -use crate::harness::Harness; -use crate::physics::PhysicsState; -use crate::settings::ExampleSettings; - -use super::graphics_context::TestbedGraphics; -use super::state::{TestbedActionFlags, TestbedState}; - -#[cfg(all(feature = "dim3", feature = "other-backends"))] -use super::OtherBackends; - -#[cfg(all(feature = "dim3", feature = "other-backends"))] -use super::state::{PHYSX_BACKEND_PATCH_FRICTION, PHYSX_BACKEND_TWO_FRICTION_DIR}; - -#[cfg(all(feature = "dim3", feature = "other-backends"))] -use crate::physx_backend::PhysxWorld; - -use super::Plugins; - -/// An example/demo that can be run in the testbed -#[derive(Clone)] -pub struct Example { - /// Display name of the example - pub name: &'static str, - /// Group/category for organizing in the UI (e.g., "Demos", "Joints", "Debug") - pub group: &'static str, - /// The builder function that initializes the example - pub builder: fn(&mut Testbed), -} - -impl Example { - /// Create a new example with a group - pub fn new(group: &'static str, name: &'static str, builder: fn(&mut Testbed)) -> Self { - Self { - name, - group, - builder, - } - } - - /// Create a new example in the default "Demos" group - pub fn demo(name: &'static str, builder: fn(&mut Testbed)) -> Self { - Self::new("Demos", name, builder) - } -} - -/// Allow constructing Example from a tuple (group, name, builder) for convenience -impl From<(&'static str, &'static str, fn(&mut Testbed))> for Example { - fn from((group, name, builder): (&'static str, &'static str, fn(&mut Testbed))) -> Self { - Self::new(group, name, builder) - } -} - -/// Type alias for simulation builder functions -pub type SimulationBuilders = Vec; - -/// The main testbed struct passed to example builders -pub struct Testbed<'a> { - pub graphics: Option>, - pub harness: &'a mut Harness, - pub state: &'a mut TestbedState, - #[cfg(all(feature = "dim3", feature = "other-backends"))] - pub other_backends: &'a mut OtherBackends, - pub plugins: &'a mut Plugins, -} - -impl Testbed<'_> { - pub fn set_number_of_steps_per_frame(&mut self, nsteps: usize) { - self.state.nsteps = nsteps; - } - - #[cfg(feature = "dim3")] - pub fn set_vehicle_controller(&mut self, controller: DynamicRayCastVehicleController) { - self.state.vehicle_controller = Some(controller); - } - - pub fn allow_grabbing_behind_ground(&mut self, allow: bool) { - self.state.can_grab_behind_ground = allow; - } - - pub fn integration_parameters_mut(&mut self) -> &mut IntegrationParameters { - &mut self.harness.physics.integration_parameters - } - - pub fn physics_state_mut(&mut self) -> &mut PhysicsState { - &mut self.harness.physics - } - - pub fn harness(&self) -> &Harness { - self.harness - } - - pub fn harness_mut(&mut self) -> &mut Harness { - self.harness - } - - pub fn example_settings_mut(&mut self) -> &mut ExampleSettings { - &mut self.state.example_settings - } - - pub fn set_world( - &mut self, - bodies: RigidBodySet, - colliders: ColliderSet, - impulse_joints: ImpulseJointSet, - multibody_joints: MultibodyJointSet, - ) { - self.set_world_with_params( - bodies, - colliders, - impulse_joints, - multibody_joints, - rapier::math::Vector::Y * -9.81, - (), - ) - } - - pub fn set_world_with_params( - &mut self, - bodies: RigidBodySet, - colliders: ColliderSet, - impulse_joints: ImpulseJointSet, - multibody_joints: MultibodyJointSet, - gravity: rapier::math::Vector, - hooks: impl PhysicsHooks + 'static, - ) { - self.harness.set_world_with_params( - bodies, - colliders, - impulse_joints, - multibody_joints, - self.state.broad_phase_type, - gravity, - hooks, - ); - - self.post_set_world(); - } - - /// Installs a [`PhysicsWorld`] into the testbed, using its gravity and integration - /// parameters. This is the most ergonomic way to set up a scene. - /// - /// Use [`set_physics_world_with_hooks`](Self::set_physics_world_with_hooks) if you need - /// custom physics hooks. - pub fn set_physics_world(&mut self, world: PhysicsWorld) { - self.set_physics_world_with_hooks(world, ()); - } - - /// Same as [`set_physics_world`](Self::set_physics_world) but also installs the given - /// physics hooks. - pub fn set_physics_world_with_hooks( - &mut self, - world: PhysicsWorld, - hooks: impl PhysicsHooks + 'static, - ) { - self.harness - .set_physics_world_with_hooks(world, self.state.broad_phase_type, hooks); - - self.post_set_world(); - } - - fn post_set_world(&mut self) { - self.state - .action_flags - .set(TestbedActionFlags::RESET_WORLD_GRAPHICS, true); - - #[cfg(feature = "dim3")] - { - self.state.vehicle_controller = None; - } - - #[cfg(all(feature = "dim3", feature = "other-backends"))] - { - if self.state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION - || self.state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR - { - self.other_backends.physx = Some(PhysxWorld::from_rapier( - self.harness.physics.gravity, - &self.harness.physics.integration_parameters, - &self.harness.physics.bodies, - &self.harness.physics.colliders, - &self.harness.physics.impulse_joints, - &self.harness.physics.multibody_joints, - self.state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR, - self.harness.state.num_threads(), - )); - } - } - } - - pub fn set_graphics_shift(&mut self, shift: rapier::math::Vector) { - if !self.state.camera_locked - && let Some(graphics) = &mut self.graphics - { - graphics.graphics.gfx_shift = shift; - } - } - - #[cfg(feature = "dim2")] - pub fn look_at(&mut self, at: glamx::Vec2, zoom: f32) { - if !self.state.camera_locked - && let Some(graphics) = &mut self.graphics - { - graphics.camera.set_at(at); - graphics.camera.set_zoom(zoom); - } - } - - #[cfg(feature = "dim3")] - pub fn look_at(&mut self, eye: Vec3, at: Vec3) { - if !self.state.camera_locked - && let Some(graphics) = &mut self.graphics - { - graphics.camera.look_at(eye, at); - } - } - - /// Set the world-up direction the orbit camera uses for its yaw / - /// pitch axis. The testbed defaults to Y-up; call this with - /// [`Vec3::Z`] (for example) when loading a model that uses a - /// different convention so the orbit controls feel natural. - /// - /// Call this **before** [`Self::look_at`] — the camera's - /// pitch/yaw are computed against the up axis. - #[cfg(feature = "dim3")] - pub fn set_up_axis(&mut self, up_axis: Vec3) { - // Mirror what the camera does internally so the gravity slider - // (which reads `state.up_axis`) and the orbit camera stay in - // sync. Stored normalized — magnitude is meaningless for an - // axis and the camera normalizes too. - // `up_axis` is an `f32` `Vec3` (matching the camera); the stored - // axis uses the math `Vector`, which is `DVec3` in double-precision - // builds. The conversion is a no-op when `Real` is `f32`. - #[allow(clippy::useless_conversion)] - { - self.state.up_axis = up_axis.normalize().into(); - } - if !self.state.camera_locked - && let Some(graphics) = &mut self.graphics - { - graphics.camera.set_up_axis(up_axis); - } - } - - /// Programmatic equivalent of clicking the testbed's "Frame all" - /// button — recenters the orbit camera so the union AABB of every - /// collider fills the viewport. The recentering is done on the - /// next frame (it needs the inserted collider AABBs, which aren't - /// available until after the scene-graphics-reset pass), so call - /// this anywhere in `init_world` after `set_physics_world`. - pub fn request_frame_all(&mut self) { - self.state - .action_flags - .set(TestbedActionFlags::FRAME_SCENE, true); - } - - pub fn set_initial_body_color(&mut self, body: RigidBodyHandle, color: Color) { - if let Some(graphics) = &mut self.graphics { - graphics.graphics.set_initial_body_color(body, color); - } - } - - pub fn set_initial_collider_color(&mut self, collider: ColliderHandle, color: Color) { - if let Some(graphics) = &mut self.graphics { - graphics - .graphics - .set_initial_collider_color(collider, color); - } - } - - pub fn set_body_wireframe(&mut self, body: RigidBodyHandle, wireframe_enabled: bool) { - if let Some(graphics) = &mut self.graphics { - graphics - .graphics - .set_body_wireframe(body, wireframe_enabled); - } - } - - /// Attach a render-only mesh to `body`. The mesh follows the body's - /// pose offset by `local_pose` and does not participate in physics — - /// it is purely a visualization. Use this to render MJCF-style - /// `` visual meshes without - /// inserting them as colliders. - /// - /// `uvs` is an optional per-vertex UV buffer (used only when the - /// shape is a `TriMesh` *and* `texture` is also set). `normals` is an - /// optional per-vertex normal buffer parallel to the shape's vertices; - /// when supplied the renderer uses it verbatim (preserving authored - /// smooth shading) instead of recomputing flat per-face normals. - /// `texture` is an optional path to a 2D color image to apply. - pub fn add_body_render_mesh( - &mut self, - body: RigidBodyHandle, - shape: &SharedShape, - local_pose: rapier::math::Pose, - color: Color, - uvs: Option<&[[f32; 2]]>, - normals: Option<&[[f32; 3]]>, - texture: Option<&std::path::Path>, - material: Option, - ) { - if let Some(graphics) = &mut self.graphics { - graphics.graphics.add_body_render_mesh( - graphics.window, - body, - shape, - local_pose, - color, - uvs, - normals, - texture, - material, - ); - } - } - - /// Show or hide every collider-derived render node. - pub fn set_colliders_visible(&mut self, visible: bool) { - if let Some(graphics) = &mut self.graphics { - graphics.graphics.set_colliders_visible(visible); - } - } - - /// Show or hide every body-attached render-only mesh (registered via - /// [`Self::add_body_render_mesh`]). - pub fn set_body_render_meshes_visible(&mut self, visible: bool) { - if let Some(graphics) = &mut self.graphics { - graphics.graphics.set_body_render_meshes_visible(visible); - } - } - - pub fn add_callback< - F: FnMut( - Option<&mut TestbedGraphics>, - &mut PhysicsState, - &crate::physics::PhysicsEvents, - &crate::harness::RunState, - ) + 'static, - >( - &mut self, - callback: F, - ) { - self.harness.add_callback(callback); - } - - pub fn add_plugin(&mut self, mut plugin: impl crate::plugin::TestbedPlugin + 'static) { - plugin.init_plugin(); - self.plugins.0.push(Box::new(plugin)); - } -} diff --git a/src_testbed/ui.rs b/src_testbed/ui.rs index 85533ab4e..9af5e11cc 100644 --- a/src_testbed/ui.rs +++ b/src_testbed/ui.rs @@ -1,15 +1,13 @@ use rapier::math::Real; +use rapier::pipeline::PhysicsWorld; use crate::debug_render::DebugRenderPipelineResource; -use crate::harness::{Harness, RapierBroadPhaseType}; -use crate::testbed::{ - PHYSX_BACKEND_PATCH_FRICTION, PHYSX_BACKEND_TWO_FRICTION_DIR, RunMode, TestbedActionFlags, - TestbedState, TestbedStateFlags, UiTab, -}; +use crate::physics::RapierBroadPhaseType; +use crate::testbed::state::Transition; +use crate::testbed::{RunMode, TestbedActionFlags, TestbedState, TestbedStateFlags, UiTab}; pub use egui; -use crate::PhysicsState; use crate::settings::SettingValue; use egui::{ComboBox, RichText, Slider, Ui, Window}; use web_time::Instant; @@ -107,7 +105,7 @@ fn setup_custom_theme(ctx: &egui::Context) { pub(crate) fn update_ui( ui_context: &egui::Context, state: &mut TestbedState, - harness: &mut Harness, + world: &mut PhysicsWorld, debug_render: &mut DebugRenderPipelineResource, ) { setup_custom_theme(ui_context); @@ -143,10 +141,10 @@ pub(crate) fn update_ui( examples_tab(ui, state); } UiTab::Settings => { - settings_tab(ui, state, harness, debug_render); + settings_tab(ui, state, world, debug_render); } UiTab::Performance => { - performance_tab(ui, harness); + performance_tab(ui, world); } }); @@ -182,7 +180,8 @@ pub(crate) fn update_ui( .on_hover_text("Restart example (R)") .clicked() { - state.action_flags.set(TestbedActionFlags::RESTART, true); + state.preserve_settings_on_switch = true; + state.transition = Some(Transition::Switch); } ui.separator(); @@ -220,32 +219,7 @@ pub(crate) fn update_ui( } fn examples_tab(ui: &mut Ui, state: &mut TestbedState) { - // Backend selector (if multiple backends available) - if state.backend_names.len() > 1 { - ui.horizontal(|ui| { - ui.label("Backend:"); - let mut backend_changed = false; - ComboBox::from_id_salt("backend_combo") - .width(150.0) - .selected_text(state.backend_names[state.selected_backend]) - .show_ui(ui, |ui| { - for (id, name) in state.backend_names.iter().enumerate() { - backend_changed = ui - .selectable_value(&mut state.selected_backend, id, *name) - .changed() - || backend_changed; - } - }); - - if backend_changed { - state - .action_flags - .set(TestbedActionFlags::BACKEND_CHANGED, true); - } - }); - } - - // Navigation and backend selector row + // Navigation row ui.horizontal(|ui| { // Previous/Next buttons (navigate in display order) if ui @@ -254,9 +228,7 @@ fn examples_tab(ui: &mut Ui, state: &mut TestbedState) { .clicked() { state.selected_display_index -= 1; - state - .action_flags - .set(TestbedActionFlags::EXAMPLE_CHANGED, true); + state.transition = Some(Transition::Switch); } if ui @@ -268,9 +240,7 @@ fn examples_tab(ui: &mut Ui, state: &mut TestbedState) { .clicked() { state.selected_display_index += 1; - state - .action_flags - .set(TestbedActionFlags::EXAMPLE_CHANGED, true); + state.transition = Some(Transition::Switch); } // Current example name @@ -320,9 +290,7 @@ fn examples_tab(ui: &mut Ui, state: &mut TestbedState) { && !is_selected { state.selected_display_index = display_idx; - state - .action_flags - .set(TestbedActionFlags::EXAMPLE_CHANGED, true); + state.transition = Some(Transition::Switch); } } }); @@ -332,12 +300,10 @@ fn examples_tab(ui: &mut Ui, state: &mut TestbedState) { fn settings_tab( ui: &mut Ui, state: &mut TestbedState, - harness: &mut Harness, + world: &mut PhysicsWorld, debug_render: &mut DebugRenderPipelineResource, ) { - let integration_parameters = &mut harness.physics.integration_parameters; - let is_physx = state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION - || state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR; + let integration_parameters = &mut world.integration_parameters; // ───────────────────────────────────────────────────────────────── // RENDERING @@ -379,24 +345,20 @@ fn settings_tab( integration_parameters.set_inv_dt(frequency as Real); } - // Steps per frame - ui.add(Slider::new(&mut state.nsteps, 1..=100).text("Steps/frame")) - .on_hover_text("Physics steps per rendered frame."); - // Gravity slider — operates along the current up-axis so the // slider's "down" matches the camera's "down". Scenes that switch // to Z-up (e.g. the MJCF demo) get gravity along -Z instead of the // hard-coded -Y. Any orthogonal components of gravity are // preserved when the slider moves. let up = state.up_axis; - let mut gravity_along_up = harness.physics.gravity.dot(up); + let mut gravity_along_up = world.gravity.dot(up); if ui .add(Slider::new(&mut gravity_along_up, 0.0..=-200.0).text("Gravity")) .on_hover_text("Gravity (m/s^2) along the up-axis. Default: -9.81") .changed() { - let current = harness.physics.gravity.dot(up); - harness.physics.gravity += up * (gravity_along_up - current); + let current = world.gravity.dot(up); + world.gravity += up * (gravity_along_up - current); } // Sleep @@ -420,7 +382,7 @@ fn settings_tab( ui.add(Slider::new(&mut integration_parameters.num_solver_iterations, 1..=10).text("Substeps")) .on_hover_text("Main solver iterations. Higher = more stable stacking."); - if !is_physx { + { ui.add( Slider::new( &mut integration_parameters.num_internal_pgs_iterations, @@ -451,7 +413,7 @@ fn settings_tab( // ───────────────────────────────────────────────────────────────── // CONTACTS (Rapier only) // ───────────────────────────────────────────────────────────────── - if !is_physx { + { ui.label(RichText::new("Contacts").strong()); ui.add_space(2.0); @@ -513,7 +475,7 @@ fn settings_tab( // ───────────────────────────────────────────────────────────────── // ADVANCED (Rapier only) // ───────────────────────────────────────────────────────────────── - if !is_physx { + { ui.label(RichText::new("Advanced").strong()); ui.add_space(2.0); @@ -540,8 +502,9 @@ fn settings_tab( }); if bp_changed { - harness.physics.broad_phase = state.broad_phase_type.init_broad_phase(); - state.action_flags.set(TestbedActionFlags::RESTART, true); + world.broad_phase = state.broad_phase_type.init_broad_phase(); + state.preserve_settings_on_switch = true; + state.transition = Some(Transition::Switch); } }); @@ -556,23 +519,11 @@ fn settings_tab( ) .on_hover_text("Minimum bodies per simulation island."); - #[cfg(feature = "parallel")] - { - let mut num_threads = harness.state.num_threads(); - if ui - .add(Slider::new(&mut num_threads, 1..=num_cpus::get_physical()).text("Threads")) - .on_hover_text("Parallel solver threads.") - .changed() - { - harness.state.set_num_threads(num_threads); - } - } - ui.add_space(8.0); } } -fn performance_tab(ui: &mut Ui, harness: &Harness) { +fn performance_tab(ui: &mut Ui, world: &PhysicsWorld) { // ───────────────────────────────────────────────────────────────── // SCENE INFO // ───────────────────────────────────────────────────────────────── @@ -584,15 +535,15 @@ fn performance_tab(ui: &mut Ui, harness: &Harness) { .spacing([20.0, 2.0]) .show(ui, |ui| { ui.label("Bodies:"); - ui.label(format!("{}", harness.physics.bodies.len())); + ui.label(format!("{}", world.bodies.len())); ui.end_row(); ui.label("Colliders:"); - ui.label(format!("{}", harness.physics.colliders.len())); + ui.label(format!("{}", world.colliders.len())); ui.end_row(); ui.label("Joints:"); - ui.label(format!("{}", harness.physics.impulse_joints.len())); + ui.label(format!("{}", world.impulse_joints.len())); ui.end_row(); }); @@ -603,7 +554,7 @@ fn performance_tab(ui: &mut Ui, harness: &Harness) { // ───────────────────────────────────────────────────────────────── // TIMING - Full details // ───────────────────────────────────────────────────────────────── - let counters = &harness.physics.pipeline.counters; + let counters = &world.physics_pipeline.counters; let total_ms = counters.step_time_ms(); let fps = if total_ms > 0.0 { (1000.0 / total_ms).round() @@ -728,23 +679,20 @@ fn performance_tab(ui: &mut Ui, harness: &Harness) { .default_open(false) .show(ui, |ui| { ui.label( - egui::RichText::new(serialization_string( - harness.state.timestep_id, - &harness.physics, - )) - .small() - .monospace(), + egui::RichText::new(serialization_string(world)) + .small() + .monospace(), ); }); } -fn serialization_string(timestep_id: usize, physics: &PhysicsState) -> String { +fn serialization_string(world: &PhysicsWorld) -> String { let t = Instant::now(); - let bf = bincode::serialize(&physics.broad_phase).unwrap(); - let nf = bincode::serialize(&physics.narrow_phase).unwrap(); - let bs = bincode::serialize(&physics.bodies).unwrap(); - let cs = bincode::serialize(&physics.colliders).unwrap(); - let js = bincode::serialize(&physics.impulse_joints).unwrap(); + let bf = bincode::serialize(&world.broad_phase).unwrap(); + let nf = bincode::serialize(&world.narrow_phase).unwrap(); + let bs = bincode::serialize(&world.bodies).unwrap(); + let cs = bincode::serialize(&world.colliders).unwrap(); + let js = bincode::serialize(&world.impulse_joints).unwrap(); let serialization_time = Instant::now() - t; let hash_bf = md5::compute(&bf); let hash_nf = md5::compute(&nf); @@ -752,13 +700,12 @@ fn serialization_string(timestep_id: usize, physics: &PhysicsState) -> String { let hash_colliders = md5::compute(&cs); let hash_joints = md5::compute(&js); format!( - "Frame: {} ({:.1}ms)\n\ + "Serialized ({:.1}ms)\n\ Broad: {:.1}KB {}\n\ Narrow: {:.1}KB {}\n\ Bodies: {:.1}KB {}\n\ Colliders: {:.1}KB {}\n\ Joints: {:.1}KB {}", - timestep_id, serialization_time.as_secs_f64() * 1000.0, bf.len() as f32 / 1000.0, format!("{hash_bf:?}").split_at(8).0, @@ -852,13 +799,14 @@ fn example_settings_ui(ui_context: &egui::Context, state: &mut TestbedState) { // the screen edge (which otherwise hides all // but the first couple of entries when the // settings window sits low on screen). - egui::ScrollArea::vertical() - .max_height(1600.0) - .show(ui, |ui| { + egui::ScrollArea::vertical().max_height(1600.0).show( + ui, + |ui| { for (id, option) in range.iter().enumerate() { ui.selectable_value(value, id, option); } - }); + }, + ); }); }); } @@ -921,7 +869,10 @@ fn example_settings_ui(ui_context: &egui::Context, state: &mut TestbedState) { } if any_changed { - state.action_flags.set(TestbedActionFlags::RESTART, true); + // A restart-triggering setting changed: re-run the example, + // preserving the user's setting edits. + state.preserve_settings_on_switch = true; + state.transition = Some(Transition::Switch); } }); } diff --git a/src_testbed/viewer.rs b/src_testbed/viewer.rs new file mode 100644 index 000000000..a9ef45218 --- /dev/null +++ b/src_testbed/viewer.rs @@ -0,0 +1,664 @@ +//! TestbedViewer — the example-owned-loop testbed. +//! +//! The viewer owns the window, graphics, cameras, input and UI. The *example* +//! owns the [`PhysicsWorld`] and drives the loop: +//! +//! ```ignore +//! pub async fn run(viewer: &mut TestbedViewer) -> anyhow::Result<()> { +//! let mut world = PhysicsWorld::new(); +//! /* build scene */ +//! viewer.set_world(&mut world); +//! viewer.look_at(eye, at); +//! while viewer.render_frame(&mut world).await { +//! if viewer.simulating() { +//! world.step(); +//! } +//! } +//! Ok(()) +//! } +//! ``` +//! +//! The example calls `world.step()` (or `world.step_with_events(&hooks, &events)` +//! with its own [`ChannelEventCollector`]) directly — the viewer is not involved +//! in stepping or event collection. + +#![allow(clippy::unnecessary_cast)] // Casts are needed for switching between f32/f64. + +use kiss3d::color::Color; +use kiss3d::event::{Action, Key, WindowEvent}; +use kiss3d::window::Window; +use rapier::dynamics::{RigidBodyActivation, RigidBodyHandle}; +use rapier::geometry::{ColliderHandle, SharedShape}; +use rapier::pipeline::PhysicsWorld; + +#[cfg(feature = "dim3")] +use glamx::Vec3; + +use crate::Camera; +use crate::debug_render::{DebugRenderPipelineResource, debug_render_scene}; +use crate::graphics::{GraphicsManager, RenderMaterial}; +use crate::mouse::SceneMouse; +use crate::physics::{restore_world, snapshot_world}; +use crate::settings::ExampleSettings; +use crate::testbed::hover::highlight_hovered_body; +use crate::testbed::keys::KeysState; +use crate::testbed::state::{ + ExampleEntry, RunMode, TestbedActionFlags, TestbedState, TestbedStateFlags, Transition, +}; +use crate::ui; + +/// The example-owned-loop testbed viewer. +/// +/// The viewer renders the world and drives the UI, but does **not** step the +/// simulation or collect events — the example does that itself via +/// [`PhysicsWorld::step`] / [`PhysicsWorld::step_with_events`]. +pub struct TestbedViewer { + window: Window, + graphics: GraphicsManager, + camera: Camera, + scene_mouse: SceneMouse, + keys: KeysState, + debug_render: DebugRenderPipelineResource, + state: TestbedState, +} + +fn save_file_path() -> String { + format!("testbed_state_{}.autosave.json", env!("CARGO_CRATE_NAME")) +} + +impl TestbedViewer { + /// Creates the viewer window and registers the list of examples shown in the + /// UI (used by the outer demo runner to dispatch the selected example). + pub async fn new(examples: Vec) -> Self { + // Install a default logger so warnings emitted by the examples (e.g. the + // MJCF loader complaining about missing meshes) actually print. + let _ = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")) + .try_init(); + + #[cfg(feature = "profiler_ui")] + profiling::puffin::set_scopes_on(true); + + let title = if cfg!(feature = "dim2") { + "Rapier: 2D demos" + } else { + "Rapier: 3D demos" + }; + + let mut window = Window::new_with_size(title, 1280, 720).await; + window.set_background_color(Color::new(245.0 / 255.0, 245.0 / 255.0, 236.0 / 255.0, 1.0)); + window.set_ambient(0.1); + + let mut camera = Camera::default(); + + let mut state = TestbedState::default(); + state.set_examples(examples); + + // Restore the autosaved UI state + camera (selected example, flags, ...). + #[cfg(not(target_arch = "wasm32"))] + { + if let Some(saved) = std::fs::read(save_file_path()) + .ok() + .and_then(|data| serde_json::from_slice(&data).ok()) + { + state.apply_saved_data(saved, &mut camera); + // Keep the restored camera: the first example's `look_at` must + // not override it. + state.camera_locked = true; + } + } + state.action_flags.remove(TestbedActionFlags::APP_STARTED); + + Self { + window, + graphics: GraphicsManager::new(), + camera, + scene_mouse: SceneMouse::new(), + keys: KeysState::default(), + debug_render: DebugRenderPipelineResource::default(), + state, + } + } + + // ───────────────────────────── loop driving ───────────────────────────── + + /// Renders one frame (presents the scene built last frame, processes input + /// and UI). Returns `false` when the example's loop should end — either the + /// window was closed or a [`Transition`] (example switch / quit) was + /// requested from the UI. + pub async fn render_frame(&mut self, world: &mut PhysicsWorld) -> bool { + profiling::finish_frame!(); + + #[cfg(feature = "dim3")] + let keep_open = self + .window + .render_3d(self.graphics.scene_mut(), &mut self.camera) + .await; + #[cfg(feature = "dim2")] + let keep_open = self + .window + .render_2d(self.graphics.scene_mut(), &mut self.camera) + .await; + + if !keep_open { + self.state.transition = Some(Transition::Quit); + return false; + } + + self.handle_events(); + + let cursor_pos = self.window.cursor_pos(); + self.scene_mouse + .update_from_window(cursor_pos, self.window.size().into(), &self.camera); + + self.handle_action_flags(world); + self.handle_sleep_settings(world); + self.autosave(); + + highlight_hovered_body(&mut self.graphics, &self.scene_mouse, world); + self.graphics + .draw(self.state.flags, &world.bodies, &world.colliders); + debug_render_scene(&mut self.window, &mut self.debug_render, world); + + // Disjoint field borrows: the closure captures `state`/`debug` while + // `self.window` is the receiver. + let state = &mut self.state; + let debug = &mut self.debug_render; + self.window + .draw_ui(|ctx| ui::update_ui(ctx, state, world, debug)); + + self.state.prev_flags = self.state.flags; + self.state.transition.is_none() + } + + /// Whether the simulation should advance this frame, honoring run/pause/step. + /// A pending single-step is consumed (returns `true` once, then pauses). + pub fn simulating(&mut self) -> bool { + match self.state.running { + RunMode::Stop => false, + RunMode::Running => true, + RunMode::Step => { + self.state.running = RunMode::Stop; + true + } + } + } + + // ───────────────────────── scene registration ─────────────────────────── + + /// Registers render nodes for the world the example just built, configures + /// the selected broad-phase, and enables the profiling counters. + /// + /// Render-node creation is deferred to the next frame so example code can + /// still set initial body/collider colors after calling this. + pub fn set_world(&mut self, world: &mut PhysicsWorld) { + world.broad_phase = self.state.broad_phase_type.init_broad_phase(); + world.physics_pipeline.counters.enable(); + self.state + .action_flags + .set(TestbedActionFlags::RESET_WORLD_GRAPHICS, true); + } + + /// Clears the scene and resets per-example viewer state. Called by the outer + /// demo runner between examples. A switch caused by a restart / backend / + /// solver-parameter change preserves the camera and the user's setting + /// edits; selecting a different example resets both. + pub fn clear_scene(&mut self) { + self.graphics.clear(); + self.state.transition = None; + self.state.snapshot = None; + self.state.action_flags = TestbedActionFlags::empty(); + + if self.state.preserve_settings_on_switch { + self.state.camera_locked = true; + } else { + self.camera = Camera::default(); + self.state.camera_locked = false; + self.state.example_settings.clear(); + } + self.state.preserve_settings_on_switch = false; + } + + // ──────────────────────────── camera / scene ──────────────────────────── + + pub fn allow_grabbing_behind_ground(&mut self, allow: bool) { + self.state.can_grab_behind_ground = allow; + } + + pub fn set_graphics_shift(&mut self, shift: rapier::math::Vector) { + if !self.state.camera_locked { + self.graphics.gfx_shift = shift; + } + } + + #[cfg(feature = "dim2")] + pub fn look_at(&mut self, at: glamx::Vec2, zoom: f32) { + if !self.state.camera_locked { + self.camera.set_at(at); + self.camera.set_zoom(zoom); + } + } + + #[cfg(feature = "dim3")] + pub fn look_at(&mut self, eye: Vec3, at: Vec3) { + if !self.state.camera_locked { + self.camera.look_at(eye, at); + } + } + + /// Sets the world-up direction the orbit camera uses (call before + /// [`Self::look_at`]). Keeps the gravity slider aligned with "down". + #[cfg(feature = "dim3")] + pub fn set_up_axis(&mut self, up_axis: Vec3) { + #[allow(clippy::useless_conversion)] + { + self.state.up_axis = up_axis.normalize().into(); + } + if !self.state.camera_locked { + self.camera.set_up_axis(up_axis); + } + } + + /// Camera orientation as a unit quaternion (3D only). + #[cfg(feature = "dim3")] + pub fn camera_rotation(&self) -> na::UnitQuaternion { + let rot_x = na::UnitQuaternion::from_axis_angle(&na::Vector3::y_axis(), self.camera.at().x); + let rot_y = + na::UnitQuaternion::from_axis_angle(&(-na::Vector3::x_axis()), self.camera.at().y); + rot_x * rot_y + } + + /// Camera forward direction (3D only). + #[cfg(feature = "dim3")] + pub fn camera_fwd_dir(&self) -> na::Vector3 { + self.camera_rotation() * na::Vector3::z() + } + + /// Recenters the camera so the whole scene fills the viewport (deferred to + /// the next frame, when collider AABBs are available). + pub fn request_frame_all(&mut self) { + self.state + .action_flags + .set(TestbedActionFlags::FRAME_SCENE, true); + } + + // ─────────────────────────── render helpers ───────────────────────────── + + pub fn set_initial_body_color(&mut self, body: RigidBodyHandle, color: Color) { + self.graphics.set_initial_body_color(body, color); + } + + pub fn set_initial_collider_color(&mut self, collider: ColliderHandle, color: Color) { + self.graphics.set_initial_collider_color(collider, color); + } + + pub fn set_body_wireframe(&mut self, body: RigidBodyHandle, wireframe_enabled: bool) { + self.graphics.set_body_wireframe(body, wireframe_enabled); + } + + /// Attaches a render-only mesh to `body` (does not participate in physics). + #[allow(clippy::too_many_arguments)] + pub fn add_body_render_mesh( + &mut self, + body: RigidBodyHandle, + shape: &SharedShape, + local_pose: rapier::math::Pose, + color: Color, + uvs: Option<&[[f32; 2]]>, + normals: Option<&[[f32; 3]]>, + texture: Option<&std::path::Path>, + material: Option, + ) { + self.graphics.add_body_render_mesh( + &mut self.window, + body, + shape, + local_pose, + color, + uvs, + normals, + texture, + material, + ); + } + + pub fn set_colliders_visible(&mut self, visible: bool) { + self.graphics.set_colliders_visible(visible); + } + + pub fn set_body_render_meshes_visible(&mut self, visible: bool) { + self.graphics.set_body_render_meshes_visible(visible); + } + + // ──────────────────────── runtime graphics (in-loop) ───────────────────── + // Examples that add/remove bodies or change colors while the simulation runs + // (formerly inside an `add_callback` closure) call these from their loop. + + /// Sets a body's render color. With `tmp_color`, the color is transient (it is + /// reset to the body's base color next frame — used for hover/transient cues); + /// otherwise it becomes the body's persistent color. + pub fn set_body_color(&mut self, body: RigidBodyHandle, color: Color, tmp_color: bool) { + self.graphics.set_body_color(body, color, tmp_color); + } + + /// Creates render nodes for a body inserted into `world` after [`Self::set_world`]. + pub fn add_body(&mut self, handle: RigidBodyHandle, world: &PhysicsWorld) { + self.graphics + .add_body_colliders(&mut self.window, handle, &world.bodies, &world.colliders); + } + + /// Removes the render nodes of a body removed from the world. + pub fn remove_body(&mut self, handle: RigidBodyHandle) { + self.graphics.remove_body_nodes(handle); + } + + /// Creates render nodes for a parentless collider inserted after [`Self::set_world`]. + pub fn add_collider(&mut self, handle: ColliderHandle, world: &PhysicsWorld) { + self.graphics + .add_collider(&mut self.window, handle, &world.colliders); + } + + /// Removes the render nodes of a collider removed from the world. + pub fn remove_collider(&mut self, handle: ColliderHandle) { + self.graphics.remove_collider_nodes(handle); + } + + /// Refreshes a collider's render nodes after its shape was modified in place. + pub fn update_collider(&mut self, handle: ColliderHandle, world: &PhysicsWorld) { + self.graphics.remove_collider_nodes(handle); + self.graphics + .add_collider(&mut self.window, handle, &world.colliders); + } + + // ───────────────────────────── input / events ─────────────────────────── + + pub fn keys(&self) -> &KeysState { + &self.keys + } + + pub fn mouse(&self) -> &SceneMouse { + &self.scene_mouse + } + + pub fn egui_context(&self) -> &egui::Context { + self.window.egui_context() + } + + pub fn egui_context_mut(&mut self) -> &mut egui::Context { + self.window.egui_context_mut() + } + + // ───────────────────────────── registry / loop ────────────────────────── + + /// Display index of the example the UI currently has selected, clamped to a + /// valid range (a stale autosave may carry an index past the current list). + pub fn selected(&self) -> usize { + self.state + .selected_display_index + .min(self.state.examples.len().saturating_sub(1)) + } + + /// Name of the example the UI currently has selected (for dispatch). + pub fn selected_name(&self) -> Option<&'static str> { + self.state + .examples + .get(self.state.selected_display_index) + .map(|e| e.name) + } + + /// Whether the user requested the whole testbed to quit. + pub fn quitting(&self) -> bool { + matches!(self.state.transition, Some(Transition::Quit)) + } + + /// Read-only access to the underlying example settings (for examples that + /// read live-tunable values declared via [`Self::example_settings_mut`]). + pub fn example_settings(&self) -> &ExampleSettings { + &self.state.example_settings + } + + pub fn example_settings_mut(&mut self) -> &mut ExampleSettings { + &mut self.state.example_settings + } + + // ───────────────────────────── internals ──────────────────────────────── + + fn handle_events(&mut self) { + // Collect first so we can mutate `self` freely inside the loop. + let events: Vec = self.window.events().iter().map(|e| e.value).collect(); + + for value in events { + match value { + WindowEvent::Key(key, Action::Press, _) => { + if !self.keys.pressed_keys.contains(&key) { + self.keys.pressed_keys.push(key); + } + match key { + Key::LShift | Key::RShift => self.keys.shift = true, + Key::LControl | Key::RControl => self.keys.ctrl = true, + Key::LAlt | Key::RAlt => self.keys.alt = true, + _ => {} + } + } + WindowEvent::Key(key, Action::Release, _) => { + self.keys.pressed_keys.retain(|k| *k != key); + match key { + Key::T => { + self.state.running = if self.state.running == RunMode::Stop { + RunMode::Running + } else { + RunMode::Stop + }; + } + Key::S => self.state.running = RunMode::Step, + Key::R => self.request_restart(), + Key::LShift | Key::RShift => self.keys.shift = false, + Key::LControl | Key::RControl => self.keys.ctrl = false, + Key::LAlt | Key::RAlt => self.keys.alt = false, + _ => {} + } + } + _ => {} + } + } + } + + fn request_restart(&mut self) { + self.state.preserve_settings_on_switch = true; + self.state.transition = Some(Transition::Switch); + } + + fn handle_action_flags(&mut self, world: &mut PhysicsWorld) { + if self + .state + .action_flags + .contains(TestbedActionFlags::TAKE_SNAPSHOT) + { + self.state + .action_flags + .set(TestbedActionFlags::TAKE_SNAPSHOT, false); + self.state.snapshot = Some(snapshot_world(world, 0)); + } + + if self + .state + .action_flags + .contains(TestbedActionFlags::RESTORE_SNAPSHOT) + { + self.state + .action_flags + .set(TestbedActionFlags::RESTORE_SNAPSHOT, false); + if let Some(snapshot) = &self.state.snapshot { + restore_world(world, snapshot); + self.state + .action_flags + .set(TestbedActionFlags::RESET_WORLD_GRAPHICS, true); + } + } + + if self + .state + .action_flags + .contains(TestbedActionFlags::RESET_WORLD_GRAPHICS) + { + self.state + .action_flags + .set(TestbedActionFlags::RESET_WORLD_GRAPHICS, false); + for (handle, _) in world.bodies.iter() { + self.graphics.add_body_colliders( + &mut self.window, + handle, + &world.bodies, + &world.colliders, + ); + } + for (handle, co) in world.colliders.iter() { + if co.parent().is_none() { + self.graphics + .add_collider(&mut self.window, handle, &world.colliders); + } + } + + // Honor the current wireframe flag on freshly created nodes. + if self.state.flags.contains(TestbedStateFlags::WIREFRAME) { + self.graphics.toggle_wireframe_mode(&world.colliders, true); + } + } + + if self.state.prev_flags.contains(TestbedStateFlags::WIREFRAME) + != self.state.flags.contains(TestbedStateFlags::WIREFRAME) + { + self.graphics.toggle_wireframe_mode( + &world.colliders, + self.state.flags.contains(TestbedStateFlags::WIREFRAME), + ); + } + + if self + .state + .action_flags + .contains(TestbedActionFlags::FRAME_SCENE) + { + self.state + .action_flags + .set(TestbedActionFlags::FRAME_SCENE, false); + frame_scene( + &world.colliders, + &world.bodies, + &self.graphics, + &mut self.camera, + ); + } + } + + fn handle_sleep_settings(&mut self, world: &mut PhysicsWorld) { + if self.state.prev_flags.contains(TestbedStateFlags::SLEEP) + != self.state.flags.contains(TestbedStateFlags::SLEEP) + { + if self.state.flags.contains(TestbedStateFlags::SLEEP) { + for (_, body) in world.bodies.iter_mut() { + body.activation_mut().normalized_linear_threshold = + RigidBodyActivation::default_normalized_linear_threshold(); + body.activation_mut().angular_threshold = + RigidBodyActivation::default_angular_threshold(); + } + } else { + for (_, body) in world.bodies.iter_mut() { + body.wake_up(true); + body.activation_mut().normalized_linear_threshold = -1.0; + } + } + } + } + + fn autosave(&mut self) { + #[cfg(not(target_arch = "wasm32"))] + { + let new_save_data = self.state.save_data(self.camera); + if self.state.prev_save_data != new_save_data { + let data = serde_json::to_string_pretty(&new_save_data).unwrap(); + if let Err(e) = std::fs::write(save_file_path(), &data) { + eprintln!("Failed to write autosave file: {e}"); + } + self.state.prev_save_data = new_save_data; + } + } + } +} + +/// Recenter the camera so the AABB of all the collider and visual-mesh shapes in +/// the scene fills the viewport ("frame all" / "best view"). +fn frame_scene( + colliders: &rapier::geometry::ColliderSet, + bodies: &rapier::dynamics::RigidBodySet, + graphics: &GraphicsManager, + camera: &mut Camera, +) { + use rapier::parry::bounding_volume::BoundingVolume; + use rapier::parry::math::Real; + + fn is_finite_aabb(a: &rapier::parry::bounding_volume::Aabb) -> bool { + if !a.mins.x.is_finite() + || !a.mins.y.is_finite() + || !a.maxs.x.is_finite() + || !a.maxs.y.is_finite() + { + return false; + } + #[cfg(feature = "dim3")] + if !a.mins.z.is_finite() || !a.maxs.z.is_finite() { + return false; + } + true + } + + let mut aabb: Option = None; + let mut merge = |a: rapier::parry::bounding_volume::Aabb| { + if !is_finite_aabb(&a) { + return; + } + aabb = Some(match aabb { + None => a, + Some(prev) => prev.merged(&a), + }); + }; + for (_, co) in colliders.iter() { + merge(co.compute_aabb()); + } + for vm in graphics.body_attached_nodes() { + let Some(rb) = bodies.get(vm.body) else { + continue; + }; + let world_pose = *rb.position() * vm.delta; + merge(vm.shape.compute_aabb(&world_pose)); + } + let Some(aabb) = aabb else { + return; + }; + + let center = aabb.center(); + let extents = aabb.extents(); + + #[cfg(feature = "dim3")] + { + let half = extents * 0.5 as Real; + let radius = (half.x * half.x + half.y * half.y + half.z * half.z) + .sqrt() + .max(0.5); + let fov = camera.fov(); + let dist = radius / (fov as Real * 0.5).sin().max(1e-3) * 1.1; + + let kiss_center = + kiss3d::glamx::Vec3::new(center.x as f32, center.y as f32, center.z as f32); + let dir = kiss3d::glamx::Vec3::new(1.0, 1.0, 1.0).normalize(); + let eye = kiss_center + dir * dist as f32; + camera.look_at(eye, kiss_center); + } + + #[cfg(feature = "dim2")] + { + let kiss_center = kiss3d::glamx::Vec2::new(center.x as f32, center.y as f32); + let max_extent = extents.x.max(extents.y).max(0.5); + let zoom = (600.0 as Real / max_extent / 1.2) as f32; + camera.look_at(kiss_center, zoom); + } +}