From f74b8401ad9ef50b8cdbf1f43a2b21f6c42b0ebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Sun, 2 Jan 2022 14:47:40 +0100 Subject: [PATCH 1/5] Implement multibody joints and the new solver --- .github/workflows/rapier-ci-build.yml | 24 +- Cargo.toml | 22 +- benchmarks2d/Cargo.toml | 6 +- benchmarks2d/balls2.rs | 6 +- benchmarks2d/boxes2.rs | 5 +- benchmarks2d/capsules2.rs | 5 +- benchmarks2d/convex_polygons2.rs | 5 +- benchmarks2d/heightfield2.rs | 5 +- benchmarks2d/joint_ball2.rs | 13 +- benchmarks2d/joint_fixed2.rs | 21 +- benchmarks2d/joint_prismatic2.rs | 15 +- benchmarks2d/pyramid2.rs | 5 +- benchmarks3d/Cargo.toml | 6 +- benchmarks3d/all_benchmarks3.rs | 8 +- benchmarks3d/balls3.rs | 5 +- benchmarks3d/boxes3.rs | 5 +- benchmarks3d/capsules3.rs | 5 +- benchmarks3d/ccd3.rs | 5 +- benchmarks3d/compound3.rs | 5 +- benchmarks3d/convex_polyhedron3.rs | 5 +- benchmarks3d/heightfield3.rs | 5 +- benchmarks3d/joint_ball3.rs | 13 +- benchmarks3d/joint_fixed3.rs | 19 +- benchmarks3d/joint_prismatic3.rs | 22 +- benchmarks3d/joint_revolute3.rs | 24 +- benchmarks3d/keva3.rs | 5 +- benchmarks3d/pyramid3.rs | 5 +- benchmarks3d/stacks3.rs | 5 +- benchmarks3d/trimesh3.rs | 5 +- {build => crates}/rapier2d-f64/Cargo.toml | 10 +- {build => crates}/rapier2d/Cargo.toml | 10 +- {build => crates}/rapier3d-f64/Cargo.toml | 10 +- {build => crates}/rapier3d/Cargo.toml | 10 +- {build => crates}/rapier_testbed2d/Cargo.toml | 12 +- {build => crates}/rapier_testbed3d/Cargo.toml | 12 +- examples2d/Cargo.toml | 6 +- examples2d/add_remove2.rs | 12 +- examples2d/ccd2.rs | 5 +- examples2d/collision_groups2.rs | 5 +- examples2d/convex_polygons2.rs | 5 +- examples2d/damping2.rs | 12 +- examples2d/debug_box_ball2.rs | 5 +- examples2d/heightfield2.rs | 5 +- examples2d/joints2.rs | 13 +- examples2d/locked_rotations2.rs | 5 +- examples2d/one_way_platforms2.rs | 6 +- examples2d/platform2.rs | 5 +- examples2d/polyline2.rs | 5 +- examples2d/pyramid2.rs | 5 +- examples2d/restitution2.rs | 5 +- examples2d/sensor2.rs | 5 +- examples2d/trimesh2.rs | 5 +- examples3d/Cargo.toml | 6 +- examples3d/all_examples3.rs | 11 +- examples3d/articulations3.rs | 678 +++++++++++ examples3d/ccd3.rs | 5 +- examples3d/collision_groups3.rs | 5 +- examples3d/compound3.rs | 5 +- examples3d/convex_decomposition3.rs | 6 +- examples3d/convex_polyhedron3.rs | 5 +- examples3d/damping3.rs | 12 +- examples3d/debug_add_remove_collider3.rs | 6 +- examples3d/debug_articulations3.rs | 98 ++ examples3d/debug_big_colliders3.rs | 5 +- examples3d/debug_boxes3.rs | 9 +- examples3d/debug_cylinder3.rs | 5 +- examples3d/debug_dynamic_collider_add3.rs | 6 +- examples3d/debug_friction3.rs | 5 +- examples3d/debug_infinite_fall3.rs | 5 +- examples3d/debug_prismatic3.rs | 24 +- examples3d/debug_rollback3.rs | 6 +- examples3d/debug_shape_modification3.rs | 6 +- examples3d/debug_triangle3.rs | 5 +- examples3d/debug_trimesh3.rs | 5 +- examples3d/domino3.rs | 5 +- examples3d/fountain3.rs | 11 +- examples3d/harness_capsules3.rs | 5 +- examples3d/heightfield3.rs | 5 +- examples3d/joints3.rs | 242 ++-- examples3d/keva3.rs | 5 +- examples3d/locked_rotations3.rs | 5 +- examples3d/one_way_platforms3.rs | 6 +- examples3d/platform3.rs | 5 +- examples3d/primitives3.rs | 19 +- examples3d/rapier.data | Bin 0 -> 2017666 bytes examples3d/restitution3.rs | 5 +- examples3d/sensor3.rs | 5 +- examples3d/trimesh3.rs | 5 +- publish-testbeds.sh | 10 +- src/counters/mod.rs | 4 +- src/data/coarena.rs | 12 +- src/data/graph.rs | 52 +- src/dynamics/integration_parameters.rs | 96 +- src/dynamics/island_manager.rs | 13 +- src/dynamics/joint/ball_joint.rs | 148 --- src/dynamics/joint/fixed_joint.rs | 73 +- src/dynamics/joint/generic_joint.rs | 144 --- .../joint/impulse_joint/impulse_joint.rs | 20 + .../impulse_joint_set.rs} | 73 +- src/dynamics/joint/impulse_joint/mod.rs | 6 + src/dynamics/joint/joint.rs | 143 --- src/dynamics/joint/joint_data.rs | 270 +++++ src/dynamics/joint/mod.rs | 29 +- .../joint/{spring_model.rs => motor_model.rs} | 36 +- src/dynamics/joint/multibody_joint/mod.rs | 15 + .../joint/multibody_joint/multibody.rs | 1021 +++++++++++++++++ .../joint/multibody_joint/multibody_joint.rs | 571 +++++++++ .../multibody_joint/multibody_joint_set.rs | 352 ++++++ .../joint/multibody_joint/multibody_link.rs | 173 +++ .../multibody_joint/multibody_workspace.rs | 27 + .../multibody_joint/unit_multibody_joint.rs | 122 ++ src/dynamics/joint/prismatic_joint.rs | 289 ++--- src/dynamics/joint/revolute_joint.rs | 220 ++-- src/dynamics/joint/spherical_joint.rs | 91 ++ src/dynamics/mod.rs | 15 +- src/dynamics/rigid_body_components.rs | 168 ++- src/dynamics/rigid_body_set.rs | 18 +- src/dynamics/solver/categorization.rs | 36 +- src/dynamics/solver/delta_vel.rs | 24 +- .../solver/generic_velocity_constraint.rs | 377 ++++++ .../generic_velocity_constraint_element.rs | 348 ++++++ src/dynamics/solver/interaction_groups.rs | 13 +- src/dynamics/solver/island_solver.rs | 151 +-- .../ball_position_constraint.rs | 266 ----- .../ball_position_constraint_wide.rs | 216 ---- .../ball_velocity_constraint.rs | 660 ----------- .../ball_velocity_constraint_wide.rs | 359 ------ .../fixed_position_constraint.rs | 151 --- .../fixed_position_constraint_wide.rs | 71 -- .../fixed_velocity_constraint.rs | 436 ------- .../fixed_velocity_constraint_wide.rs | 539 --------- .../generic_position_constraint.rs | 346 ------ .../generic_position_constraint_wide.rs | 60 - .../generic_velocity_constraint.rs | 706 ------------ .../generic_velocity_constraint_wide.rs | 464 -------- .../joint_constraint/joint_constraint.rs | 710 ++++++------ .../joint_generic_velocity_constraint.rs | 529 +++++++++ ...int_generic_velocity_constraint_builder.rs | 853 ++++++++++++++ .../joint_position_constraint.rs | 280 ----- .../joint_velocity_constraint.rs | 608 ++++++++++ .../joint_velocity_constraint_builder.rs | 699 +++++++++++ src/dynamics/solver/joint_constraint/mod.rs | 107 +- .../prismatic_position_constraint.rs | 182 --- .../prismatic_position_constraint_wide.rs | 71 -- .../prismatic_velocity_constraint.rs | 859 -------------- .../prismatic_velocity_constraint_wide.rs | 848 -------------- .../revolute_position_constraint.rs | 295 ----- .../revolute_position_constraint_wide.rs | 71 -- .../revolute_velocity_constraint.rs | 750 ------------ .../revolute_velocity_constraint_wide.rs | 527 --------- src/dynamics/solver/mod.rs | 27 +- src/dynamics/solver/parallel_island_solver.rs | 156 +-- .../solver/parallel_position_solver.rs | 107 -- .../solver/parallel_solver_constraints.rs | 101 +- .../solver/parallel_velocity_solver.rs | 111 +- src/dynamics/solver/position_constraint.rs | 168 --- .../solver/position_constraint_wide.rs | 157 --- .../solver/position_ground_constraint.rs | 121 -- .../solver/position_ground_constraint_wide.rs | 143 --- src/dynamics/solver/position_solver.rs | 57 - src/dynamics/solver/solver_constraints.rs | 317 +++-- src/dynamics/solver/velocity_constraint.rs | 131 +-- .../solver/velocity_constraint_element.rs | 114 +- .../solver/velocity_constraint_wide.rs | 110 +- .../solver/velocity_ground_constraint.rs | 77 +- .../velocity_ground_constraint_element.rs | 81 +- .../solver/velocity_ground_constraint_wide.rs | 93 +- src/dynamics/solver/velocity_solver.rs | 222 +++- .../broad_phase_multi_sap/broad_phase.rs | 6 +- src/geometry/contact_pair.rs | 55 +- src/geometry/narrow_phase.rs | 6 +- src/lib.rs | 50 +- src/pipeline/physics_pipeline.rs | 160 ++- src/utils.rs | 236 ++-- src_testbed/box2d_backend.rs | 19 +- src_testbed/harness/mod.rs | 45 +- src_testbed/lib.rs | 10 - src_testbed/nphysics_backend.rs | 248 ---- src_testbed/physics/mod.rs | 25 +- src_testbed/physx_backend.rs | 381 +++--- src_testbed/testbed.rs | 382 +++--- src_testbed/ui.rs | 84 +- 182 files changed, 9871 insertions(+), 12645 deletions(-) rename {build => crates}/rapier2d-f64/Cargo.toml (93%) rename {build => crates}/rapier2d/Cargo.toml (93%) rename {build => crates}/rapier3d-f64/Cargo.toml (92%) rename {build => crates}/rapier3d/Cargo.toml (93%) rename {build => crates}/rapier_testbed2d/Cargo.toml (81%) rename {build => crates}/rapier_testbed3d/Cargo.toml (82%) create mode 100644 examples3d/articulations3.rs create mode 100644 examples3d/debug_articulations3.rs create mode 100644 examples3d/rapier.data delete mode 100644 src/dynamics/joint/ball_joint.rs delete mode 100644 src/dynamics/joint/generic_joint.rs create mode 100644 src/dynamics/joint/impulse_joint/impulse_joint.rs rename src/dynamics/joint/{joint_set.rs => impulse_joint/impulse_joint_set.rs} (86%) create mode 100644 src/dynamics/joint/impulse_joint/mod.rs delete mode 100644 src/dynamics/joint/joint.rs create mode 100644 src/dynamics/joint/joint_data.rs rename src/dynamics/joint/{spring_model.rs => motor_model.rs} (66%) create mode 100644 src/dynamics/joint/multibody_joint/mod.rs create mode 100644 src/dynamics/joint/multibody_joint/multibody.rs create mode 100644 src/dynamics/joint/multibody_joint/multibody_joint.rs create mode 100644 src/dynamics/joint/multibody_joint/multibody_joint_set.rs create mode 100644 src/dynamics/joint/multibody_joint/multibody_link.rs create mode 100644 src/dynamics/joint/multibody_joint/multibody_workspace.rs create mode 100644 src/dynamics/joint/multibody_joint/unit_multibody_joint.rs create mode 100644 src/dynamics/joint/spherical_joint.rs create mode 100644 src/dynamics/solver/generic_velocity_constraint.rs create mode 100644 src/dynamics/solver/generic_velocity_constraint_element.rs delete mode 100644 src/dynamics/solver/joint_constraint/ball_position_constraint.rs delete mode 100644 src/dynamics/solver/joint_constraint/ball_position_constraint_wide.rs delete mode 100644 src/dynamics/solver/joint_constraint/ball_velocity_constraint.rs delete mode 100644 src/dynamics/solver/joint_constraint/ball_velocity_constraint_wide.rs delete mode 100644 src/dynamics/solver/joint_constraint/fixed_position_constraint.rs delete mode 100644 src/dynamics/solver/joint_constraint/fixed_position_constraint_wide.rs delete mode 100644 src/dynamics/solver/joint_constraint/fixed_velocity_constraint.rs delete mode 100644 src/dynamics/solver/joint_constraint/fixed_velocity_constraint_wide.rs delete mode 100644 src/dynamics/solver/joint_constraint/generic_position_constraint.rs delete mode 100644 src/dynamics/solver/joint_constraint/generic_position_constraint_wide.rs delete mode 100644 src/dynamics/solver/joint_constraint/generic_velocity_constraint.rs delete mode 100644 src/dynamics/solver/joint_constraint/generic_velocity_constraint_wide.rs create mode 100644 src/dynamics/solver/joint_constraint/joint_generic_velocity_constraint.rs create mode 100644 src/dynamics/solver/joint_constraint/joint_generic_velocity_constraint_builder.rs delete mode 100644 src/dynamics/solver/joint_constraint/joint_position_constraint.rs create mode 100644 src/dynamics/solver/joint_constraint/joint_velocity_constraint.rs create mode 100644 src/dynamics/solver/joint_constraint/joint_velocity_constraint_builder.rs delete mode 100644 src/dynamics/solver/joint_constraint/prismatic_position_constraint.rs delete mode 100644 src/dynamics/solver/joint_constraint/prismatic_position_constraint_wide.rs delete mode 100644 src/dynamics/solver/joint_constraint/prismatic_velocity_constraint.rs delete mode 100644 src/dynamics/solver/joint_constraint/prismatic_velocity_constraint_wide.rs delete mode 100644 src/dynamics/solver/joint_constraint/revolute_position_constraint.rs delete mode 100644 src/dynamics/solver/joint_constraint/revolute_position_constraint_wide.rs delete mode 100644 src/dynamics/solver/joint_constraint/revolute_velocity_constraint.rs delete mode 100644 src/dynamics/solver/joint_constraint/revolute_velocity_constraint_wide.rs delete mode 100644 src/dynamics/solver/parallel_position_solver.rs delete mode 100644 src/dynamics/solver/position_constraint.rs delete mode 100644 src/dynamics/solver/position_constraint_wide.rs delete mode 100644 src/dynamics/solver/position_ground_constraint.rs delete mode 100644 src/dynamics/solver/position_ground_constraint_wide.rs delete mode 100644 src/dynamics/solver/position_solver.rs delete mode 100644 src_testbed/nphysics_backend.rs diff --git a/.github/workflows/rapier-ci-build.yml b/.github/workflows/rapier-ci-build.yml index 08c5da5..b4bc538 100644 --- a/.github/workflows/rapier-ci-build.yml +++ b/.github/workflows/rapier-ci-build.yml @@ -28,13 +28,13 @@ jobs: - name: Build rapier3d run: cargo build --verbose -p rapier3d; - name: Build rapier2d SIMD - run: cd build/rapier2d; cargo build --verbose --features simd-stable; + run: cd crates/rapier2d; cargo build --verbose --features simd-stable; - name: Build rapier3d SIMD - run: cd build/rapier3d; cargo build --verbose --features simd-stable; + run: cd crates/rapier3d; cargo build --verbose --features simd-stable; - name: Build rapier2d SIMD Parallel - run: cd build/rapier2d; cargo build --verbose --features simd-stable --features parallel; + run: cd crates/rapier2d; cargo build --verbose --features simd-stable --features parallel; - name: Build rapier3d SIMD Parallel - run: cd build/rapier3d; cargo build --verbose --features simd-stable --features parallel; + run: cd crates/rapier3d; cargo build --verbose --features simd-stable --features parallel; - name: Run tests run: cargo test - name: Check rapier_testbed2d @@ -42,9 +42,9 @@ jobs: - name: Check rapier_testbed3d run: cargo check --verbose -p rapier_testbed3d; - name: Check rapier_testbed2d --features parallel - run: cd build/rapier_testbed2d; cargo check --verbose --features parallel; + run: cd crates/rapier_testbed2d; cargo check --verbose --features parallel; - name: Check rapier_testbed3d --features parallel - run: cd build/rapier_testbed3d; cargo check --verbose --features parallel; + run: cd crates/rapier_testbed3d; cargo check --verbose --features parallel; - name: Check rapier-examples-2d run: cargo check -j 1 --verbose -p rapier-examples-2d; - name: Check rapier-examples-3d @@ -57,9 +57,9 @@ jobs: - uses: actions/checkout@v2 - run: rustup target add wasm32-unknown-unknown - name: build rapier2d - run: cd build/rapier2d && cargo build --verbose --features wasm-bindgen --target wasm32-unknown-unknown; + run: cd crates/rapier2d && cargo build --verbose --features wasm-bindgen --target wasm32-unknown-unknown; - name: build rapier3d - run: cd build/rapier3d && cargo build --verbose --features wasm-bindgen --target wasm32-unknown-unknown; + run: cd crates/rapier3d && cargo build --verbose --features wasm-bindgen --target wasm32-unknown-unknown; build-wasm-emscripten: runs-on: ubuntu-latest env: @@ -68,10 +68,10 @@ jobs: - uses: actions/checkout@v2 - run: rustup target add wasm32-unknown-emscripten - name: build rapier2d - run: cd build/rapier2d && cargo build --verbose --target wasm32-unknown-emscripten; + run: cd crates/rapier2d && cargo build --verbose --target wasm32-unknown-emscripten; - name: build rapier3d - run: cd build/rapier3d && cargo build --verbose --target wasm32-unknown-emscripten; + run: cd crates/rapier3d && cargo build --verbose --target wasm32-unknown-emscripten; - name: build rapier2d --features simd-stable - run: cd build/rapier2d && cargo build --verbose --target wasm32-unknown-emscripten --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 build/rapier3d && cargo build --verbose --target wasm32-unknown-emscripten --features simd-stable; + run: cd crates/rapier3d && cargo build --verbose --target wasm32-unknown-emscripten --features simd-stable; diff --git a/Cargo.toml b/Cargo.toml index 7021ade..1bd8c67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,21 +1,17 @@ [workspace] -members = [ "build/rapier2d", "build/rapier2d-f64", "build/rapier_testbed2d", "examples2d", "benchmarks2d", - "build/rapier3d", "build/rapier3d-f64", "build/rapier_testbed3d", "examples3d", "benchmarks3d" ] +members = [ "crates/rapier2d", "crates/rapier2d-f64", "crates/rapier_testbed2d", "examples2d", "benchmarks2d", + "crates/rapier3d", "crates/rapier3d-f64", "crates/rapier_testbed3d", "examples3d", "benchmarks3d" ] resolver = "2" [patch.crates-io] #wrapped2d = { git = "https://github.com/Bastacyclop/rust_box2d.git" } #simba = { path = "../simba" } -#ncollide2d = { path = "../ncollide/build/ncollide2d" } -#ncollide3d = { path = "../ncollide/build/ncollide3d" } -#nphysics2d = { path = "../nphysics/build/nphysics2d" } -#nphysics3d = { path = "../nphysics/build/nphysics3d" } #kiss3d = { path = "../kiss3d" } -#parry2d = { path = "../parry/build/parry2d" } -#parry3d = { path = "../parry/build/parry3d" } -#parry2d-f64 = { path = "../parry/build/parry2d-f64" } -#parry3d-f64 = { path = "../parry/build/parry3d-f64" } +#parry2d = { path = "../parry/crates/parry2d" } +#parry3d = { path = "../parry/crates/parry3d" } +#parry2d-f64 = { path = "../parry/crates/parry2d-f64" } +#parry3d-f64 = { path = "../parry/crates/parry3d-f64" } #nalgebra = { path = "../nalgebra" } #kiss3d = { git = "https://github.com/sebcrozet/kiss3d" } @@ -24,14 +20,10 @@ resolver = "2" #parry3d = { git = "https://github.com/dimforge/parry", branch = "special_cases" } #parry2d-f64 = { git = "https://github.com/dimforge/parry", branch = "special_cases" } #parry3d-f64 = { git = "https://github.com/dimforge/parry", branch = "special_cases" } -#ncollide2d = { git = "https://github.com/dimforge/ncollide" } -#ncollide3d = { git = "https://github.com/dimforge/ncollide" } -#nphysics2d = { git = "https://github.com/dimforge/nphysics" } -#nphysics3d = { git = "https://github.com/dimforge/nphysics" } [profile.release] #debug = true -codegen-units = 1 +#codegen-units = 1 #opt-level = 1 #lto = true diff --git a/benchmarks2d/Cargo.toml b/benchmarks2d/Cargo.toml index c7c775d..bbab4d9 100644 --- a/benchmarks2d/Cargo.toml +++ b/benchmarks2d/Cargo.toml @@ -2,7 +2,7 @@ name = "rapier-benchmarks-2d" version = "0.1.0" authors = [ "Sébastien Crozet " ] -edition = "2018" +edition = "2021" [features] parallel = [ "rapier2d/parallel", "rapier_testbed2d/parallel" ] @@ -16,10 +16,10 @@ rand = "0.8" Inflector = "0.11" [dependencies.rapier_testbed2d] -path = "../build/rapier_testbed2d" +path = "../crates/rapier_testbed2d" [dependencies.rapier2d] -path = "../build/rapier2d" +path = "../crates/rapier2d" [[bin]] name = "all_benchmarks2" diff --git a/benchmarks2d/balls2.rs b/benchmarks2d/balls2.rs index ec55f24..168acaf 100644 --- a/benchmarks2d/balls2.rs +++ b/benchmarks2d/balls2.rs @@ -7,7 +7,9 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); + /* * Ground */ @@ -58,6 +60,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 2.5], 5.0); } diff --git a/benchmarks2d/boxes2.rs b/benchmarks2d/boxes2.rs index 2e4c5e4..c3d7445 100644 --- a/benchmarks2d/boxes2.rs +++ b/benchmarks2d/boxes2.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -63,6 +64,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 50.0], 10.0); } diff --git a/benchmarks2d/capsules2.rs b/benchmarks2d/capsules2.rs index e75afe4..3bfa6ab 100644 --- a/benchmarks2d/capsules2.rs +++ b/benchmarks2d/capsules2.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -65,6 +66,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 50.0], 10.0); } diff --git a/benchmarks2d/convex_polygons2.rs b/benchmarks2d/convex_polygons2.rs index 6c9792e..6678460 100644 --- a/benchmarks2d/convex_polygons2.rs +++ b/benchmarks2d/convex_polygons2.rs @@ -9,7 +9,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -76,6 +77,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 50.0], 10.0); } diff --git a/benchmarks2d/heightfield2.rs b/benchmarks2d/heightfield2.rs index 3178e60..04c56da 100644 --- a/benchmarks2d/heightfield2.rs +++ b/benchmarks2d/heightfield2.rs @@ -8,7 +8,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -63,6 +64,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 50.0], 10.0); } diff --git a/benchmarks2d/joint_ball2.rs b/benchmarks2d/joint_ball2.rs index 114fe85..e856556 100644 --- a/benchmarks2d/joint_ball2.rs +++ b/benchmarks2d/joint_ball2.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let mut joints = JointSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Create the balls @@ -41,16 +42,16 @@ pub fn init_world(testbed: &mut Testbed) { // Vertical joint. if i > 0 { let parent_handle = *body_handles.last().unwrap(); - let joint = BallJoint::new(Point::origin(), point![0.0, shift]); - joints.insert(parent_handle, child_handle, joint); + let joint = RevoluteJoint::new().local_anchor2(point![0.0, shift]); + impulse_joints.insert(parent_handle, child_handle, joint); } // Horizontal joint. if k > 0 { let parent_index = body_handles.len() - numi; let parent_handle = body_handles[parent_index]; - let joint = BallJoint::new(Point::origin(), point![-shift, 0.0]); - joints.insert(parent_handle, child_handle, joint); + let joint = RevoluteJoint::new().local_anchor2(point![-shift, 0.0]); + impulse_joints.insert(parent_handle, child_handle, joint); } body_handles.push(child_handle); @@ -60,6 +61,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![numk as f32 * rad, numi as f32 * -rad], 5.0); } diff --git a/benchmarks2d/joint_fixed2.rs b/benchmarks2d/joint_fixed2.rs index 8d6e8dc..690b8cb 100644 --- a/benchmarks2d/joint_fixed2.rs +++ b/benchmarks2d/joint_fixed2.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let mut joints = JointSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Create the balls @@ -46,22 +47,18 @@ pub fn init_world(testbed: &mut Testbed) { // Vertical joint. if i > 0 { let parent_handle = *body_handles.last().unwrap(); - let joint = FixedJoint::new( - Isometry::identity(), - Isometry::translation(0.0, shift), - ); - joints.insert(parent_handle, child_handle, joint); + let joint = + FixedJoint::new().local_frame2(Isometry::translation(0.0, shift)); + impulse_joints.insert(parent_handle, child_handle, joint); } // Horizontal joint. if k > 0 { let parent_index = body_handles.len() - num; let parent_handle = body_handles[parent_index]; - let joint = FixedJoint::new( - Isometry::identity(), - Isometry::translation(-shift, 0.0), - ); - joints.insert(parent_handle, child_handle, joint); + let joint = + FixedJoint::new().local_frame2(Isometry::translation(-shift, 0.0)); + impulse_joints.insert(parent_handle, child_handle, joint); } body_handles.push(child_handle); @@ -73,6 +70,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![50.0, 50.0], 5.0); } diff --git a/benchmarks2d/joint_prismatic2.rs b/benchmarks2d/joint_prismatic2.rs index c2b4bf3..4733088 100644 --- a/benchmarks2d/joint_prismatic2.rs +++ b/benchmarks2d/joint_prismatic2.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let mut joints = JointSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Create the balls @@ -46,12 +47,10 @@ pub fn init_world(testbed: &mut Testbed) { UnitVector::new_normalize(vector![-1.0, 1.0]) }; - let mut prism = - PrismaticJoint::new(Point::origin(), axis, point![0.0, shift], axis); - prism.limits_enabled = true; - prism.limits[0] = -1.5; - prism.limits[1] = 1.5; - joints.insert(curr_parent, curr_child, prism); + let mut prism = PrismaticJoint::new(axis) + .local_anchor2(point![0.0, shift]) + .limit_axis([-1.5, 1.5]); + impulse_joints.insert(curr_parent, curr_child, prism); curr_parent = curr_child; } @@ -61,6 +60,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![80.0, 80.0], 15.0); } diff --git a/benchmarks2d/pyramid2.rs b/benchmarks2d/pyramid2.rs index a557ff4..61636b9 100644 --- a/benchmarks2d/pyramid2.rs +++ b/benchmarks2d/pyramid2.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -50,6 +51,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 2.5], 5.0); } diff --git a/benchmarks3d/Cargo.toml b/benchmarks3d/Cargo.toml index 9b76df3..76d1911 100644 --- a/benchmarks3d/Cargo.toml +++ b/benchmarks3d/Cargo.toml @@ -2,7 +2,7 @@ name = "rapier-benchmarks-3d" version = "0.1.0" authors = [ "Sébastien Crozet " ] -edition = "2018" +edition = "2021" [features] parallel = [ "rapier3d/parallel", "rapier_testbed3d/parallel" ] @@ -16,10 +16,10 @@ rand = "0.8" Inflector = "0.11" [dependencies.rapier_testbed3d] -path = "../build/rapier_testbed3d" +path = "../crates/rapier_testbed3d" [dependencies.rapier3d] -path = "../build/rapier3d" +path = "../crates/rapier3d" [[bin]] name = "all_benchmarks3" diff --git a/benchmarks3d/all_benchmarks3.rs b/benchmarks3d/all_benchmarks3.rs index f61b80f..6a3f756 100644 --- a/benchmarks3d/all_benchmarks3.rs +++ b/benchmarks3d/all_benchmarks3.rs @@ -58,10 +58,10 @@ pub fn main() { ("Stacks", stacks3::init_world), ("Pyramid", pyramid3::init_world), ("Trimesh", trimesh3::init_world), - ("Joint ball", joint_ball3::init_world), - ("Joint fixed", joint_fixed3::init_world), - ("Joint revolute", joint_revolute3::init_world), - ("Joint prismatic", joint_prismatic3::init_world), + ("ImpulseJoint ball", joint_ball3::init_world), + ("ImpulseJoint fixed", joint_fixed3::init_world), + ("ImpulseJoint revolute", joint_revolute3::init_world), + ("ImpulseJoint prismatic", joint_prismatic3::init_world), ("Keva tower", keva3::init_world), ]; diff --git a/benchmarks3d/balls3.rs b/benchmarks3d/balls3.rs index 3f0bf36..b4d5102 100644 --- a/benchmarks3d/balls3.rs +++ b/benchmarks3d/balls3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Create the balls @@ -48,6 +49,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/benchmarks3d/boxes3.rs b/benchmarks3d/boxes3.rs index 9c7ed81..0250619 100644 --- a/benchmarks3d/boxes3.rs +++ b/benchmarks3d/boxes3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -58,6 +59,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/benchmarks3d/capsules3.rs b/benchmarks3d/capsules3.rs index 8565503..5b05f23 100644 --- a/benchmarks3d/capsules3.rs +++ b/benchmarks3d/capsules3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -59,6 +60,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/benchmarks3d/ccd3.rs b/benchmarks3d/ccd3.rs index 987aba6..d54ebc1 100644 --- a/benchmarks3d/ccd3.rs +++ b/benchmarks3d/ccd3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -73,6 +74,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/benchmarks3d/compound3.rs b/benchmarks3d/compound3.rs index a914ce9..872923c 100644 --- a/benchmarks3d/compound3.rs +++ b/benchmarks3d/compound3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -66,6 +67,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/benchmarks3d/convex_polyhedron3.rs b/benchmarks3d/convex_polyhedron3.rs index 7065cd5..cb834ea 100644 --- a/benchmarks3d/convex_polyhedron3.rs +++ b/benchmarks3d/convex_polyhedron3.rs @@ -9,7 +9,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -73,6 +74,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/benchmarks3d/heightfield3.rs b/benchmarks3d/heightfield3.rs index 32856fe..b95f1ee 100644 --- a/benchmarks3d/heightfield3.rs +++ b/benchmarks3d/heightfield3.rs @@ -8,7 +8,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -73,6 +74,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/benchmarks3d/joint_ball3.rs b/benchmarks3d/joint_ball3.rs index a1661f8..64128ba 100644 --- a/benchmarks3d/joint_ball3.rs +++ b/benchmarks3d/joint_ball3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let mut joints = JointSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); let rad = 0.4; let num = 100; @@ -36,16 +37,16 @@ pub fn init_world(testbed: &mut Testbed) { // Vertical joint. if i > 0 { let parent_handle = *body_handles.last().unwrap(); - let joint = BallJoint::new(Point::origin(), point![0.0, 0.0, -shift]); - joints.insert(parent_handle, child_handle, joint); + let joint = SphericalJoint::new().local_anchor2(point![0.0, 0.0, -shift]); + impulse_joints.insert(parent_handle, child_handle, joint); } // Horizontal joint. if k > 0 { let parent_index = body_handles.len() - num; let parent_handle = body_handles[parent_index]; - let joint = BallJoint::new(Point::origin(), point![-shift, 0.0, 0.0]); - joints.insert(parent_handle, child_handle, joint); + let joint = SphericalJoint::new().local_anchor2(point![-shift, 0.0, 0.0]); + impulse_joints.insert(parent_handle, child_handle, joint); } body_handles.push(child_handle); @@ -55,6 +56,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![-110.0, -46.0, 170.0], point![54.0, -38.0, 29.0]); } diff --git a/benchmarks3d/joint_fixed3.rs b/benchmarks3d/joint_fixed3.rs index 3d1e317..b3f4039 100644 --- a/benchmarks3d/joint_fixed3.rs +++ b/benchmarks3d/joint_fixed3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let mut joints = JointSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); let rad = 0.4; let num = 5; @@ -49,22 +50,16 @@ pub fn init_world(testbed: &mut Testbed) { // Vertical joint. if i > 0 { let parent_handle = *body_handles.last().unwrap(); - let joint = FixedJoint::new( - Isometry::identity(), - Isometry::translation(0.0, 0.0, -shift), - ); - joints.insert(parent_handle, child_handle, joint); + let joint = FixedJoint::new().local_anchor2(point![0.0, 0.0, -shift]); + impulse_joints.insert(parent_handle, child_handle, joint); } // Horizontal joint. if k > 0 { let parent_index = body_handles.len() - num; let parent_handle = body_handles[parent_index]; - let joint = FixedJoint::new( - Isometry::identity(), - Isometry::translation(-shift, 0.0, 0.0), - ); - joints.insert(parent_handle, child_handle, joint); + let joint = FixedJoint::new().local_anchor2(point![-shift, 0.0, 0.0]); + impulse_joints.insert(parent_handle, child_handle, joint); } body_handles.push(child_handle); @@ -77,6 +72,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![-38.0, 14.0, 108.0], point![46.0, 12.0, 23.0]); } diff --git a/benchmarks3d/joint_prismatic3.rs b/benchmarks3d/joint_prismatic3.rs index b011662..80839d7 100644 --- a/benchmarks3d/joint_prismatic3.rs +++ b/benchmarks3d/joint_prismatic3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let mut joints = JointSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); let rad = 0.4; let num = 5; @@ -47,19 +48,10 @@ pub fn init_world(testbed: &mut Testbed) { UnitVector::new_normalize(vector![-1.0, 1.0, 0.0]) }; - let z = Vector::z(); - let mut prism = PrismaticJoint::new( - Point::origin(), - axis, - z, - point![0.0, 0.0, -shift], - axis, - z, - ); - prism.limits_enabled = true; - prism.limits[0] = -2.0; - prism.limits[1] = 2.0; - joints.insert(curr_parent, curr_child, prism); + let prism = PrismaticJoint::new(axis) + .local_anchor2(point![0.0, 0.0, -shift]) + .limit_axis([-2.0, 0.0]); + impulse_joints.insert(curr_parent, curr_child, prism); curr_parent = curr_child; } @@ -70,6 +62,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![262.0, 63.0, 124.0], point![101.0, 4.0, -3.0]); } diff --git a/benchmarks3d/joint_revolute3.rs b/benchmarks3d/joint_revolute3.rs index d6dc06c..8bdf0e9 100644 --- a/benchmarks3d/joint_revolute3.rs +++ b/benchmarks3d/joint_revolute3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let mut joints = JointSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); let rad = 0.4; let num = 10; @@ -49,22 +50,21 @@ pub fn init_world(testbed: &mut Testbed) { colliders.insert_with_parent(collider, handles[k], &mut bodies); } - // Setup four joints. - let o = Point::origin(); + // Setup four impulse_joints. let x = Vector::x_axis(); let z = Vector::z_axis(); let revs = [ - RevoluteJoint::new(o, z, point![0.0, 0.0, -shift], z), - RevoluteJoint::new(o, x, point![-shift, 0.0, 0.0], x), - RevoluteJoint::new(o, z, point![0.0, 0.0, -shift], z), - RevoluteJoint::new(o, x, point![shift, 0.0, 0.0], x), + RevoluteJoint::new(z).local_anchor2(point![0.0, 0.0, -shift]), + RevoluteJoint::new(x).local_anchor2(point![-shift, 0.0, 0.0]), + RevoluteJoint::new(z).local_anchor2(point![0.0, 0.0, -shift]), + RevoluteJoint::new(x).local_anchor2(point![shift, 0.0, 0.0]), ]; - joints.insert(curr_parent, handles[0], revs[0]); - joints.insert(handles[0], handles[1], revs[1]); - joints.insert(handles[1], handles[2], revs[2]); - joints.insert(handles[2], handles[3], revs[3]); + impulse_joints.insert(curr_parent, handles[0], revs[0]); + impulse_joints.insert(handles[0], handles[1], revs[1]); + impulse_joints.insert(handles[1], handles[2], revs[2]); + impulse_joints.insert(handles[2], handles[3], revs[3]); curr_parent = handles[3]; } @@ -74,6 +74,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![478.0, 83.0, 228.0], point![134.0, 83.0, -116.0]); } diff --git a/benchmarks3d/keva3.rs b/benchmarks3d/keva3.rs index 38a0432..ad9e1ae 100644 --- a/benchmarks3d/keva3.rs +++ b/benchmarks3d/keva3.rs @@ -83,7 +83,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -132,6 +133,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/benchmarks3d/pyramid3.rs b/benchmarks3d/pyramid3.rs index 655ecb7..b378da4 100644 --- a/benchmarks3d/pyramid3.rs +++ b/benchmarks3d/pyramid3.rs @@ -41,7 +41,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -73,6 +74,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/benchmarks3d/stacks3.rs b/benchmarks3d/stacks3.rs index 39386ea..7fc9097 100644 --- a/benchmarks3d/stacks3.rs +++ b/benchmarks3d/stacks3.rs @@ -101,7 +101,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -183,6 +184,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/benchmarks3d/trimesh3.rs b/benchmarks3d/trimesh3.rs index 8621f19..1039a09 100644 --- a/benchmarks3d/trimesh3.rs +++ b/benchmarks3d/trimesh3.rs @@ -8,7 +8,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -78,6 +79,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/build/rapier2d-f64/Cargo.toml b/crates/rapier2d-f64/Cargo.toml similarity index 93% rename from build/rapier2d-f64/Cargo.toml rename to crates/rapier2d-f64/Cargo.toml index 5d85639..525e620 100644 --- a/build/rapier2d-f64/Cargo.toml +++ b/crates/rapier2d-f64/Cargo.toml @@ -8,9 +8,9 @@ homepage = "http://rapier.rs" repository = "https://github.com/dimforge/rapier" readme = "README.md" categories = [ "science", "game-development", "mathematics", "simulation", "wasm"] -keywords = [ "physics", "dynamics", "rigid", "real-time", "joints" ] +keywords = [ "physics", "dynamics", "rigid", "real-time", "impulse_joints" ] license = "Apache-2.0" -edition = "2018" +edition = "2021" [badges] maintenance = { status = "actively-developed" } @@ -47,9 +47,9 @@ required-features = [ "dim2", "f64" ] vec_map = { version = "0.8", optional = true } instant = { version = "0.1", features = [ "now" ]} num-traits = "0.2" -nalgebra = "0.29" -parry2d-f64 = "^0.7.1" -simba = "0.6" +nalgebra = "0.30" +parry2d-f64 = "0.8" +simba = "0.7" approx = "0.5" rayon = { version = "1", optional = true } crossbeam = "0.8" diff --git a/build/rapier2d/Cargo.toml b/crates/rapier2d/Cargo.toml similarity index 93% rename from build/rapier2d/Cargo.toml rename to crates/rapier2d/Cargo.toml index 9bc5f3b..64985d6 100644 --- a/build/rapier2d/Cargo.toml +++ b/crates/rapier2d/Cargo.toml @@ -8,9 +8,9 @@ homepage = "http://rapier.rs" repository = "https://github.com/dimforge/rapier" readme = "README.md" categories = [ "science", "game-development", "mathematics", "simulation", "wasm"] -keywords = [ "physics", "dynamics", "rigid", "real-time", "joints" ] +keywords = [ "physics", "dynamics", "rigid", "real-time", "impulse_joints" ] license = "Apache-2.0" -edition = "2018" +edition = "2021" [badges] maintenance = { status = "actively-developed" } @@ -47,9 +47,9 @@ required-features = [ "dim2", "f32" ] vec_map = { version = "0.8", optional = true } instant = { version = "0.1", features = [ "now" ]} num-traits = "0.2" -nalgebra = "0.29" -parry2d = "^0.7.1" -simba = "0.6" +nalgebra = "0.30" +parry2d = "0.8" +simba = "0.7" approx = "0.5" rayon = { version = "1", optional = true } crossbeam = "0.8" diff --git a/build/rapier3d-f64/Cargo.toml b/crates/rapier3d-f64/Cargo.toml similarity index 92% rename from build/rapier3d-f64/Cargo.toml rename to crates/rapier3d-f64/Cargo.toml index a6cdd00..77cf8d4 100644 --- a/build/rapier3d-f64/Cargo.toml +++ b/crates/rapier3d-f64/Cargo.toml @@ -8,9 +8,9 @@ homepage = "http://rapier.rs" repository = "https://github.com/dimforge/rapier" readme = "README.md" categories = [ "science", "game-development", "mathematics", "simulation", "wasm"] -keywords = [ "physics", "dynamics", "rigid", "real-time", "joints" ] +keywords = [ "physics", "dynamics", "rigid", "real-time", "impulse_joints" ] license = "Apache-2.0" -edition = "2018" +edition = "2021" [badges] maintenance = { status = "actively-developed" } @@ -47,9 +47,9 @@ required-features = [ "dim3", "f64" ] vec_map = { version = "0.8", optional = true } instant = { version = "0.1", features = [ "now" ]} num-traits = "0.2" -nalgebra = "0.29" -parry3d-f64 = "^0.7.1" -simba = "0.6" +nalgebra = "0.30" +parry3d-f64 = "0.8" +simba = "0.7" approx = "0.5" rayon = { version = "1", optional = true } crossbeam = "0.8" diff --git a/build/rapier3d/Cargo.toml b/crates/rapier3d/Cargo.toml similarity index 93% rename from build/rapier3d/Cargo.toml rename to crates/rapier3d/Cargo.toml index 2830c07..75ca2ba 100644 --- a/build/rapier3d/Cargo.toml +++ b/crates/rapier3d/Cargo.toml @@ -8,9 +8,9 @@ homepage = "http://rapier.rs" repository = "https://github.com/dimforge/rapier" readme = "README.md" categories = [ "science", "game-development", "mathematics", "simulation", "wasm"] -keywords = [ "physics", "dynamics", "rigid", "real-time", "joints" ] +keywords = [ "physics", "dynamics", "rigid", "real-time", "impulse_joints" ] license = "Apache-2.0" -edition = "2018" +edition = "2021" [badges] maintenance = { status = "actively-developed" } @@ -47,9 +47,9 @@ required-features = [ "dim3", "f32" ] vec_map = { version = "0.8", optional = true } instant = { version = "0.1", features = [ "now" ]} num-traits = "0.2" -nalgebra = "0.29" -parry3d = "^0.7.1" -simba = "0.6" +nalgebra = "0.30" +parry3d = "0.8" +simba = "0.7" approx = "0.5" rayon = { version = "1", optional = true } crossbeam = "0.8" diff --git a/build/rapier_testbed2d/Cargo.toml b/crates/rapier_testbed2d/Cargo.toml similarity index 81% rename from build/rapier_testbed2d/Cargo.toml rename to crates/rapier_testbed2d/Cargo.toml index 0ba4803..ea5db88 100644 --- a/build/rapier_testbed2d/Cargo.toml +++ b/crates/rapier_testbed2d/Cargo.toml @@ -6,9 +6,9 @@ description = "Testbed for the Rapier 2-dimensional physics engine in Rust." homepage = "http://rapier.org" repository = "https://github.com/dimforge/rapier" categories = [ "science", "game-development", "mathematics", "simulation", "wasm"] -keywords = [ "physics", "dynamics", "rigid", "real-time", "joints" ] +keywords = [ "physics", "dynamics", "rigid", "real-time", "impulse_joints" ] license = "Apache-2.0" -edition = "2018" +edition = "2021" [badges] maintenance = { status = "actively-developed" } @@ -22,20 +22,18 @@ required-features = [ "dim2" ] default = [ "dim2" ] dim2 = [ ] parallel = [ "rapier2d/parallel", "num_cpus" ] -other-backends = [ "wrapped2d", "nphysics2d", "ncollide2d" ] +other-backends = [ "wrapped2d" ] [dependencies] -nalgebra = { version = "0.29", features = [ "rand" ] } +nalgebra = { version = "0.30", features = [ "rand" ] } rand = "0.8" rand_pcg = "0.3" instant = { version = "0.1", features = [ "web-sys", "now" ]} bitflags = "1" num_cpus = { version = "1", optional = true } wrapped2d = { version = "0.4", optional = true } -parry2d = "0.7" -ncollide2d = { version = "0.32", optional = true } -nphysics2d = { version = "0.24", optional = true } +parry2d = "0.8" crossbeam = "0.8" bincode = "1" Inflector = "0.11" diff --git a/build/rapier_testbed3d/Cargo.toml b/crates/rapier_testbed3d/Cargo.toml similarity index 82% rename from build/rapier_testbed3d/Cargo.toml rename to crates/rapier_testbed3d/Cargo.toml index 1ec0084..8a8a1e4 100644 --- a/build/rapier_testbed3d/Cargo.toml +++ b/crates/rapier_testbed3d/Cargo.toml @@ -6,9 +6,9 @@ description = "Testbed for the Rapier 3-dimensional physics engine in Rust." homepage = "http://rapier.org" repository = "https://github.com/dimforge/rapier" categories = [ "science", "game-development", "mathematics", "simulation", "wasm"] -keywords = [ "physics", "dynamics", "rigid", "real-time", "joints" ] +keywords = [ "physics", "dynamics", "rigid", "real-time", "impulse_joints" ] license = "Apache-2.0" -edition = "2018" +edition = "2021" [badges] maintenance = { status = "actively-developed" } @@ -22,19 +22,17 @@ required-features = [ "dim3" ] default = [ "dim3" ] dim3 = [ ] parallel = [ "rapier3d/parallel", "num_cpus" ] -other-backends = [ "physx", "physx-sys", "glam", "nphysics3d", "ncollide3d" ] +other-backends = [ "physx", "physx-sys", "glam" ] [dependencies] -nalgebra = { version = "0.29", features = [ "rand" ] } +nalgebra = { version = "0.30", features = [ "rand" ] } rand = "0.8" rand_pcg = "0.3" instant = { version = "0.1", features = [ "web-sys", "now" ]} bitflags = "1" glam = { version = "0.12", optional = true } num_cpus = { version = "1", optional = true } -parry3d = "0.7" -ncollide3d = { version = "0.32", optional = true } -nphysics3d = { version = "0.24", optional = true } +parry3d = "0.8" physx = { version = "0.11", optional = true } physx-sys = { version = "0.4", optional = true } crossbeam = "0.8" diff --git a/examples2d/Cargo.toml b/examples2d/Cargo.toml index 3c065fd..415f994 100644 --- a/examples2d/Cargo.toml +++ b/examples2d/Cargo.toml @@ -2,7 +2,7 @@ name = "rapier-examples-2d" version = "0.1.0" authors = [ "Sébastien Crozet " ] -edition = "2018" +edition = "2021" default-run = "all_examples2" [features] @@ -19,10 +19,10 @@ lyon = "0.17" usvg = "0.14" [dependencies.rapier_testbed2d] -path = "../build/rapier_testbed2d" +path = "../crates/rapier_testbed2d" [dependencies.rapier2d] -path = "../build/rapier2d" +path = "../crates/rapier2d" [[bin]] name = "all_examples2" diff --git a/examples2d/add_remove2.rs b/examples2d/add_remove2.rs index f701a90..0919da9 100644 --- a/examples2d/add_remove2.rs +++ b/examples2d/add_remove2.rs @@ -4,12 +4,15 @@ use rapier_testbed2d::Testbed; pub fn init_world(testbed: &mut Testbed) { let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); + let rad = 0.5; let positions = [vector![5.0, -1.0], vector![-5.0, -1.0]]; - let platform_handles = std::array::IntoIter::new(positions) + let platform_handles = positions + .into_iter() .map(|pos| { let rigid_body = RigidBodyBuilder::new_kinematic_position_based() .translation(pos) @@ -57,7 +60,8 @@ pub fn init_world(testbed: &mut Testbed) { handle, &mut physics.islands, &mut physics.colliders, - &mut physics.joints, + &mut physics.impulse_joints, + &mut physics.multibody_joints, ); if let Some(graphics) = &mut graphics { @@ -69,6 +73,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 0.0], 20.0); } diff --git a/examples2d/ccd2.rs b/examples2d/ccd2.rs index 1f25586..808c2e5 100644 --- a/examples2d/ccd2.rs +++ b/examples2d/ccd2.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -123,6 +124,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 2.5], 20.0); } diff --git a/examples2d/collision_groups2.rs b/examples2d/collision_groups2.rs index 3f2a786..110c32a 100644 --- a/examples2d/collision_groups2.rs +++ b/examples2d/collision_groups2.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -89,6 +90,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 1.0], 100.0); } diff --git a/examples2d/convex_polygons2.rs b/examples2d/convex_polygons2.rs index 4373fcb..060e0c5 100644 --- a/examples2d/convex_polygons2.rs +++ b/examples2d/convex_polygons2.rs @@ -9,7 +9,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -76,6 +77,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 50.0], 10.0); } diff --git a/examples2d/damping2.rs b/examples2d/damping2.rs index e308de6..481fee2 100644 --- a/examples2d/damping2.rs +++ b/examples2d/damping2.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Create the balls @@ -38,6 +39,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world_with_params(bodies, colliders, joints, Vector::zeros(), ()); + testbed.set_world_with_params( + bodies, + colliders, + impulse_joints, + multibody_joints, + Vector::zeros(), + (), + ); testbed.look_at(point![3.0, 2.0], 50.0); } diff --git a/examples2d/debug_box_ball2.rs b/examples2d/debug_box_ball2.rs index 63b6f95..8f15b7a 100644 --- a/examples2d/debug_box_ball2.rs +++ b/examples2d/debug_box_ball2.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -33,6 +34,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 0.0], 50.0); } diff --git a/examples2d/heightfield2.rs b/examples2d/heightfield2.rs index a965ad8..09c604d 100644 --- a/examples2d/heightfield2.rs +++ b/examples2d/heightfield2.rs @@ -8,7 +8,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -63,6 +64,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 0.0], 10.0); } diff --git a/examples2d/joints2.rs b/examples2d/joints2.rs index 9333184..91d86cd 100644 --- a/examples2d/joints2.rs +++ b/examples2d/joints2.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let mut joints = JointSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Create the balls @@ -44,16 +45,16 @@ pub fn init_world(testbed: &mut Testbed) { // Vertical joint. if i > 0 { let parent_handle = *body_handles.last().unwrap(); - let joint = BallJoint::new(Point::origin(), point![0.0, shift]); - joints.insert(parent_handle, child_handle, joint); + let joint = RevoluteJoint::new().local_anchor2(point![0.0, shift]); + impulse_joints.insert(parent_handle, child_handle, joint); } // Horizontal joint. if k > 0 { let parent_index = body_handles.len() - numi; let parent_handle = body_handles[parent_index]; - let joint = BallJoint::new(Point::origin(), point![-shift, 0.0]); - joints.insert(parent_handle, child_handle, joint); + let joint = RevoluteJoint::new().local_anchor2(point![-shift, 0.0]); + impulse_joints.insert(parent_handle, child_handle, joint); } body_handles.push(child_handle); @@ -63,6 +64,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![numk as f32 * rad, numi as f32 * -rad], 20.0); } diff --git a/examples2d/locked_rotations2.rs b/examples2d/locked_rotations2.rs index a1f7ba5..32c528f 100644 --- a/examples2d/locked_rotations2.rs +++ b/examples2d/locked_rotations2.rs @@ -10,7 +10,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * The ground @@ -51,6 +52,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 0.0], 40.0); } diff --git a/examples2d/one_way_platforms2.rs b/examples2d/one_way_platforms2.rs index 6cbb2f0..2bed57d 100644 --- a/examples2d/one_way_platforms2.rs +++ b/examples2d/one_way_platforms2.rs @@ -62,7 +62,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -126,7 +127,8 @@ pub fn init_world(testbed: &mut Testbed) { testbed.set_world_with_params( bodies, colliders, - joints, + impulse_joints, + multibody_joints, vector![0.0, -9.81], physics_hooks, ); diff --git a/examples2d/platform2.rs b/examples2d/platform2.rs index 48129a0..a737e34 100644 --- a/examples2d/platform2.rs +++ b/examples2d/platform2.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground. @@ -89,6 +90,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Run the simulation. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 1.0], 40.0); } diff --git a/examples2d/polyline2.rs b/examples2d/polyline2.rs index 9f5241c..2cde60c 100644 --- a/examples2d/polyline2.rs +++ b/examples2d/polyline2.rs @@ -8,7 +8,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -65,6 +66,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 0.0], 10.0); } diff --git a/examples2d/pyramid2.rs b/examples2d/pyramid2.rs index 76616be..04a7953 100644 --- a/examples2d/pyramid2.rs +++ b/examples2d/pyramid2.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -50,6 +51,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 2.5], 20.0); } diff --git a/examples2d/restitution2.rs b/examples2d/restitution2.rs index 1be4298..2650ec2 100644 --- a/examples2d/restitution2.rs +++ b/examples2d/restitution2.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -44,6 +45,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 1.0], 25.0); } diff --git a/examples2d/sensor2.rs b/examples2d/sensor2.rs index 5dc5940..9ef5a6b 100644 --- a/examples2d/sensor2.rs +++ b/examples2d/sensor2.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground. @@ -98,6 +99,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 1.0], 100.0); } diff --git a/examples2d/trimesh2.rs b/examples2d/trimesh2.rs index a4b049a..1474b77 100644 --- a/examples2d/trimesh2.rs +++ b/examples2d/trimesh2.rs @@ -13,7 +13,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -103,7 +104,7 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 20.0], 17.0); } diff --git a/examples3d/Cargo.toml b/examples3d/Cargo.toml index 897c92b..bbab27e 100644 --- a/examples3d/Cargo.toml +++ b/examples3d/Cargo.toml @@ -2,7 +2,7 @@ name = "rapier-examples-3d" version = "0.1.0" authors = [ "Sébastien Crozet " ] -edition = "2018" +edition = "2021" default-run = "all_examples3" [features] @@ -20,10 +20,10 @@ wasm-bindgen = "0.2" obj-rs = { version = "0.6", default-features = false } [dependencies.rapier_testbed3d] -path = "../build/rapier_testbed3d" +path = "../crates/rapier_testbed3d" [dependencies.rapier3d] -path = "../build/rapier3d" +path = "../crates/rapier3d" [[bin]] name = "all_examples3" diff --git a/examples3d/all_examples3.rs b/examples3d/all_examples3.rs index a9e1456..0aad4c2 100644 --- a/examples3d/all_examples3.rs +++ b/examples3d/all_examples3.rs @@ -8,6 +8,7 @@ use inflector::Inflector; use rapier_testbed3d::{Testbed, TestbedApp}; use std::cmp::Ordering; +mod articulations3; mod ccd3; mod collision_groups3; mod compound3; @@ -15,6 +16,7 @@ mod convex_decomposition3; mod convex_polyhedron3; mod damping3; mod debug_add_remove_collider3; +mod debug_articulations3; mod debug_big_colliders3; mod debug_boxes3; mod debug_cylinder3; @@ -29,7 +31,7 @@ mod debug_trimesh3; mod domino3; mod fountain3; mod heightfield3; -mod joints3; +// mod joints3; mod keva3; mod locked_rotations3; mod one_way_platforms3; @@ -78,6 +80,10 @@ pub fn main() { let mut builders: Vec<(_, fn(&mut Testbed))> = vec![ ("Fountain", fountain3::init_world), ("Primitives", primitives3::init_world), + ( + "Multibody joints", + articulations3::init_world_with_articulations, + ), ("CCD", ccd3::init_world), ("Collision groups", collision_groups3::init_world), ("Compound", compound3::init_world), @@ -86,7 +92,7 @@ pub fn main() { ("Damping", damping3::init_world), ("Domino", domino3::init_world), ("Heightfield", heightfield3::init_world), - ("Joints", joints3::init_world), + ("Impulse Joints", articulations3::init_world_with_joints), ("Locked rotations", locked_rotations3::init_world), ("One-way platforms", one_way_platforms3::init_world), ("Platform", platform3::init_world), @@ -94,6 +100,7 @@ pub fn main() { ("Sensor", sensor3::init_world), ("TriMesh", trimesh3::init_world), ("Keva tower", keva3::init_world), + ("(Debug) multibody_joints", debug_articulations3::init_world), ( "(Debug) add/rm collider", debug_add_remove_collider3::init_world, diff --git a/examples3d/articulations3.rs b/examples3d/articulations3.rs new file mode 100644 index 0000000..0c87c41 --- /dev/null +++ b/examples3d/articulations3.rs @@ -0,0 +1,678 @@ +use rapier3d::prelude::*; +use rapier_testbed3d::Testbed; + +fn create_prismatic_joints( + bodies: &mut RigidBodySet, + colliders: &mut ColliderSet, + impulse_joints: &mut ImpulseJointSet, + multibody_joints: &mut MultibodyJointSet, + origin: Point, + num: usize, + use_articulations: bool, +) { + let rad = 0.4; + let shift = 2.0; + + let ground = RigidBodyBuilder::new_static() + .translation(vector![origin.x, origin.y, origin.z]) + .build(); + let mut curr_parent = bodies.insert(ground); + let collider = ColliderBuilder::cuboid(rad, rad, rad).build(); + colliders.insert_with_parent(collider, curr_parent, bodies); + + for i in 0..num { + let z = origin.z + (i + 1) as f32 * shift; + let rigid_body = RigidBodyBuilder::new_dynamic() + .translation(vector![origin.x, origin.y, z]) + .build(); + let curr_child = bodies.insert(rigid_body); + let collider = ColliderBuilder::cuboid(rad, rad, rad).build(); + colliders.insert_with_parent(collider, curr_child, bodies); + + let axis = if i % 2 == 0 { + UnitVector::new_normalize(vector![1.0f32, 1.0, 0.0]) + } else { + UnitVector::new_normalize(vector![-1.0f32, 1.0, 0.0]) + }; + + let prism = PrismaticJoint::new(axis) + .local_anchor1(point![0.0, 0.0, 0.0]) + .local_anchor2(point![0.0, 0.0, -shift]) + .limit_axis([-2.0, 2.0]); + + if use_articulations { + multibody_joints.insert(curr_parent, curr_child, prism); + } else { + impulse_joints.insert(curr_parent, curr_child, prism); + } + curr_parent = curr_child; + } +} + +fn create_actuated_prismatic_joints( + bodies: &mut RigidBodySet, + colliders: &mut ColliderSet, + impulse_joints: &mut ImpulseJointSet, + multibody_joints: &mut MultibodyJointSet, + origin: Point, + num: usize, + use_articulations: bool, +) { + let rad = 0.4; + let shift = 2.0; + + let ground = RigidBodyBuilder::new_static() + .translation(vector![origin.x, origin.y, origin.z]) + .build(); + let mut curr_parent = bodies.insert(ground); + let collider = ColliderBuilder::cuboid(rad, rad, rad).build(); + colliders.insert_with_parent(collider, curr_parent, bodies); + + for i in 0..num { + let z = origin.z + (i + 1) as f32 * shift; + let rigid_body = RigidBodyBuilder::new_dynamic() + .translation(vector![origin.x, origin.y, z]) + .build(); + let curr_child = bodies.insert(rigid_body); + let collider = ColliderBuilder::cuboid(rad, rad, rad).build(); + colliders.insert_with_parent(collider, curr_child, bodies); + + let axis = if i % 2 == 0 { + UnitVector::new_normalize(vector![1.0, 1.0, 0.0]) + } else { + UnitVector::new_normalize(vector![-1.0, 1.0, 0.0]) + }; + + let mut prism = PrismaticJoint::new(axis) + .local_anchor1(point![0.0, 0.0, 0.0]) + .local_anchor2(point![0.0, 0.0, -shift]); + + if i == 1 { + prism = prism + .limit_axis([-Real::MAX, 5.0]) + .motor_velocity(1.0, 1.0) + // We set a max impulse so that the motor doesn't fight + // the limits with large forces. + .motor_max_impulse(1.0); + } else if i > 1 { + prism = prism.motor_position(2.0, 0.01, 1.0); + } else { + prism = prism + .motor_velocity(1.0, 1.0) + // We set a max impulse so that the motor doesn't fight + // the limits with large forces. + .motor_max_impulse(0.7) + .limit_axis([-2.0, 5.0]); + } + + if use_articulations { + multibody_joints.insert(curr_parent, curr_child, prism); + } else { + impulse_joints.insert(curr_parent, curr_child, prism); + } + + curr_parent = curr_child; + } +} + +fn create_revolute_joints( + bodies: &mut RigidBodySet, + colliders: &mut ColliderSet, + impulse_joints: &mut ImpulseJointSet, + multibody_joints: &mut MultibodyJointSet, + origin: Point, + num: usize, + use_articulations: bool, +) { + let rad = 0.4; + let shift = 2.0; + + let ground = RigidBodyBuilder::new_static() + .translation(vector![origin.x, origin.y, 0.0]) + .build(); + let mut curr_parent = bodies.insert(ground); + let collider = ColliderBuilder::cuboid(rad, rad, rad).build(); + colliders.insert_with_parent(collider, curr_parent, bodies); + + for i in 0..num { + // Create four bodies. + let z = origin.z + i as f32 * shift * 2.0 + shift; + let positions = [ + Isometry::translation(origin.x, origin.y, z), + Isometry::translation(origin.x + shift, origin.y, z), + Isometry::translation(origin.x + shift, origin.y, z + shift), + Isometry::translation(origin.x, origin.y, z + shift), + ]; + + let mut handles = [curr_parent; 4]; + for k in 0..4 { + let rigid_body = RigidBodyBuilder::new_dynamic() + .position(positions[k]) + .build(); + handles[k] = bodies.insert(rigid_body); + let collider = ColliderBuilder::cuboid(rad, rad, rad).build(); + colliders.insert_with_parent(collider, handles[k], bodies); + } + + // Setup four impulse_joints. + let x = Vector::x_axis(); + let z = Vector::z_axis(); + let revs = [ + RevoluteJoint::new(z).local_anchor2(point![0.0, 0.0, -shift]), + RevoluteJoint::new(x).local_anchor2(point![-shift, 0.0, 0.0]), + RevoluteJoint::new(z).local_anchor2(point![0.0, 0.0, -shift]), + RevoluteJoint::new(x).local_anchor2(point![shift, 0.0, 0.0]), + ]; + + if use_articulations { + multibody_joints.insert(curr_parent, handles[0], revs[0]); + multibody_joints.insert(handles[0], handles[1], revs[1]); + multibody_joints.insert(handles[1], handles[2], revs[2]); + multibody_joints.insert(handles[2], handles[3], revs[3]); + } else { + impulse_joints.insert(curr_parent, handles[0], revs[0]); + impulse_joints.insert(handles[0], handles[1], revs[1]); + impulse_joints.insert(handles[1], handles[2], revs[2]); + impulse_joints.insert(handles[2], handles[3], revs[3]); + } + + curr_parent = handles[3]; + } +} + +fn create_revolute_joints_with_limits( + bodies: &mut RigidBodySet, + colliders: &mut ColliderSet, + impulse_joints: &mut ImpulseJointSet, + multibody_joints: &mut MultibodyJointSet, + origin: Point, + use_articulations: bool, +) { + let ground = bodies.insert( + RigidBodyBuilder::new_static() + .translation(origin.coords) + .build(), + ); + + let platform1 = bodies.insert( + RigidBodyBuilder::new_dynamic() + .translation(origin.coords) + .build(), + ); + colliders.insert_with_parent( + ColliderBuilder::cuboid(4.0, 0.2, 2.0).build(), + platform1, + bodies, + ); + + let shift = vector![0.0, 0.0, 6.0]; + let platform2 = bodies.insert( + RigidBodyBuilder::new_dynamic() + .translation(origin.coords + shift) + .build(), + ); + colliders.insert_with_parent( + ColliderBuilder::cuboid(4.0, 0.2, 2.0).build(), + platform2, + bodies, + ); + + let z = Vector::z_axis(); + let joint1 = RevoluteJoint::new(z).limit_axis([-0.2, 0.2]); + + if use_articulations { + multibody_joints.insert(ground, platform1, joint1); + } else { + impulse_joints.insert(ground, platform1, joint1); + } + + let joint2 = RevoluteJoint::new(z) + .local_anchor2(-Point::from(shift)) + .limit_axis([-0.2, 0.2]); + + if use_articulations { + multibody_joints.insert(platform1, platform2, joint2); + } else { + impulse_joints.insert(platform1, platform2, joint2); + } + + // Let’s add a couple of cuboids that will fall on the platforms, triggering the joint limits. + let cuboid_body1 = bodies.insert( + RigidBodyBuilder::new_dynamic() + .translation(origin.coords + vector![-2.0, 4.0, 0.0]) + .build(), + ); + colliders.insert_with_parent( + ColliderBuilder::cuboid(0.6, 0.6, 0.6).friction(1.0).build(), + cuboid_body1, + bodies, + ); + + let cuboid_body2 = bodies.insert( + RigidBodyBuilder::new_dynamic() + .translation(origin.coords + shift + vector![2.0, 16.0, 0.0]) + .build(), + ); + colliders.insert_with_parent( + ColliderBuilder::cuboid(0.6, 0.6, 0.6).friction(1.0).build(), + cuboid_body2, + bodies, + ); +} + +fn create_fixed_joints( + bodies: &mut RigidBodySet, + colliders: &mut ColliderSet, + impulse_joints: &mut ImpulseJointSet, + multibody_joints: &mut MultibodyJointSet, + origin: Point, + num: usize, + use_articulations: bool, +) { + let rad = 0.4; + let shift = 1.0; + + let mut body_handles = Vec::new(); + + for i in 0..num { + for k in 0..num { + let fk = k as f32; + let fi = i as f32; + + // NOTE: the num - 2 test is to avoid two consecutive + // fixed bodies. Because physx will crash if we add + // a joint between these. + let status = if i == 0 && (k % 4 == 0 && k != num - 2 || k == num - 1) { + RigidBodyType::Static + } else { + RigidBodyType::Dynamic + }; + + let rigid_body = RigidBodyBuilder::new(status) + .translation(vector![ + origin.x + fk * shift, + origin.y, + origin.z + fi * shift + ]) + .build(); + let child_handle = bodies.insert(rigid_body); + let collider = ColliderBuilder::ball(rad).build(); + colliders.insert_with_parent(collider, child_handle, bodies); + + // Vertical joint. + if i > 0 { + let parent_index = body_handles.len() - num; + let parent_handle = body_handles[parent_index]; + let joint = FixedJoint::new().local_anchor2(point![0.0, 0.0, -shift]); + + if use_articulations { + multibody_joints.insert(parent_handle, child_handle, joint); + } else { + impulse_joints.insert(parent_handle, child_handle, joint); + } + } + + // Horizontal joint. + if k > 0 { + let parent_index = body_handles.len() - 1; + let parent_handle = body_handles[parent_index]; + let joint = FixedJoint::new().local_anchor2(point![-shift, 0.0, 0.0]); + impulse_joints.insert(parent_handle, child_handle, joint); + } + + body_handles.push(child_handle); + } + } +} + +fn create_spherical_joints( + bodies: &mut RigidBodySet, + colliders: &mut ColliderSet, + impulse_joints: &mut ImpulseJointSet, + multibody_joints: &mut MultibodyJointSet, + num: usize, + use_articulations: bool, +) { + let rad = 0.4; + let shift = 1.0; + + let mut body_handles = Vec::new(); + + for k in 0..num { + for i in 0..num { + let fk = k as f32; + let fi = i as f32; + + let status = if i == 0 && (k % 4 == 0 || k == num - 1) { + RigidBodyType::Static + } else { + RigidBodyType::Dynamic + }; + + let rigid_body = RigidBodyBuilder::new(status) + .translation(vector![fk * shift, 0.0, fi * shift * 2.0]) + .build(); + let child_handle = bodies.insert(rigid_body); + let collider = ColliderBuilder::capsule_z(rad * 1.25, rad).build(); + colliders.insert_with_parent(collider, child_handle, bodies); + + // Vertical joint. + if i > 0 { + let parent_handle = *body_handles.last().unwrap(); + let joint = SphericalJoint::new().local_anchor2(point![0.0, 0.0, -shift * 2.0]); + + if use_articulations { + multibody_joints.insert(parent_handle, child_handle, joint); + } else { + impulse_joints.insert(parent_handle, child_handle, joint); + } + } + + // Horizontal joint. + if k > 0 { + let parent_index = body_handles.len() - num; + let parent_handle = body_handles[parent_index]; + let joint = SphericalJoint::new().local_anchor2(point![-shift, 0.0, 0.0]); + impulse_joints.insert(parent_handle, child_handle, joint); + } + + body_handles.push(child_handle); + } + } +} + +fn create_spherical_joints_with_limits( + bodies: &mut RigidBodySet, + colliders: &mut ColliderSet, + impulse_joints: &mut ImpulseJointSet, + multibody_joints: &mut MultibodyJointSet, + origin: Point, + use_articulations: bool, +) { + let shift = vector![0.0, 0.0, 3.0]; + + let ground = bodies.insert( + RigidBodyBuilder::new_static() + .translation(origin.coords) + .build(), + ); + + let ball1 = bodies.insert( + RigidBodyBuilder::new_dynamic() + .translation(origin.coords + shift) + .linvel(vector![20.0, 20.0, 0.0]) + .build(), + ); + colliders.insert_with_parent( + ColliderBuilder::cuboid(1.0, 1.0, 1.0).build(), + ball1, + bodies, + ); + + let ball2 = bodies.insert( + RigidBodyBuilder::new_dynamic() + .translation(origin.coords + shift * 2.0) + .build(), + ); + colliders.insert_with_parent( + ColliderBuilder::cuboid(1.0, 1.0, 1.0).build(), + ball2, + bodies, + ); + + let joint1 = SphericalJoint::new() + .local_anchor2(Point::from(-shift)) + .limit_axis(JointAxis::X, [-0.2, 0.2]) + .limit_axis(JointAxis::Y, [-0.2, 0.2]); + + let joint2 = SphericalJoint::new() + .local_anchor2(Point::from(-shift)) + .limit_axis(JointAxis::X, [-0.3, 0.3]) + .limit_axis(JointAxis::Y, [-0.3, 0.3]); + + if use_articulations { + multibody_joints.insert(ground, ball1, joint1); + multibody_joints.insert(ball1, ball2, joint2); + } else { + impulse_joints.insert(ground, ball1, joint1); + impulse_joints.insert(ball1, ball2, joint2); + } +} + +fn create_actuated_revolute_joints( + bodies: &mut RigidBodySet, + colliders: &mut ColliderSet, + impulse_joints: &mut ImpulseJointSet, + multibody_joints: &mut MultibodyJointSet, + origin: Point, + num: usize, + use_articulations: bool, +) { + let rad = 0.4; + let shift = 2.0; + + // We will reuse this base configuration for all the impulse_joints here. + let z = Vector::z_axis(); + let joint_template = RevoluteJoint::new(z).local_anchor2(point![0.0, 0.0, -shift]); + + let mut parent_handle = RigidBodyHandle::invalid(); + + for i in 0..num { + let fi = i as f32; + + // NOTE: the num - 2 test is to avoid two consecutive + // fixed bodies. Because physx will crash if we add + // a joint between these. + let status = if i == 0 { + RigidBodyType::Static + } else { + RigidBodyType::Dynamic + }; + + let shifty = (i >= 1) as u32 as f32 * -2.0; + + let rigid_body = RigidBodyBuilder::new(status) + .translation(vector![origin.x, origin.y + shifty, origin.z + fi * shift]) + // .rotation(Vector3::new(0.0, fi * 1.1, 0.0)) + .build(); + + let child_handle = bodies.insert(rigid_body); + let collider = ColliderBuilder::cuboid(rad * 2.0, rad * 6.0 / (fi + 1.0), rad).build(); + colliders.insert_with_parent(collider, child_handle, bodies); + + if i > 0 { + let mut joint = joint_template.clone(); + + if i % 3 == 1 { + joint = joint.motor_velocity(-20.0, 0.1); + } else if i == num - 1 { + let stiffness = 0.2; + let damping = 1.0; + joint = joint.motor_position(3.14 / 2.0, stiffness, damping); + } + + if i == 1 { + joint = joint + .local_anchor2(point![0.0, 2.0, -shift]) + .motor_velocity(-2.0, 0.1); + } + + if use_articulations { + multibody_joints.insert(parent_handle, child_handle, joint); + } else { + impulse_joints.insert(parent_handle, child_handle, joint); + } + } + + parent_handle = child_handle; + } +} + +fn create_actuated_spherical_joints( + bodies: &mut RigidBodySet, + colliders: &mut ColliderSet, + impulse_joints: &mut ImpulseJointSet, + multibody_joints: &mut MultibodyJointSet, + origin: Point, + num: usize, + use_articulations: bool, +) { + let rad = 0.4; + let shift = 2.0; + + // We will reuse this base configuration for all the impulse_joints here. + let joint_template = SphericalJoint::new().local_anchor1(point![0.0, 0.0, shift]); + + let mut parent_handle = RigidBodyHandle::invalid(); + + for i in 0..num { + let fi = i as f32; + + // NOTE: the num - 2 test is to avoid two consecutive + // fixed bodies. Because physx will crash if we add + // a joint between these. + let status = if i == 0 { + RigidBodyType::Static + } else { + RigidBodyType::Dynamic + }; + + let rigid_body = RigidBodyBuilder::new(status) + .translation(vector![origin.x, origin.y, origin.z + fi * shift]) + // .rotation(Vector3::new(0.0, fi * 1.1, 0.0)) + .build(); + + let child_handle = bodies.insert(rigid_body); + let collider = ColliderBuilder::capsule_y(rad * 2.0 / (fi + 1.0), rad).build(); + colliders.insert_with_parent(collider, child_handle, bodies); + + if i > 0 { + let mut joint = joint_template.clone(); + + if i == 1 { + joint = joint + .motor_velocity(JointAxis::AngX, 0.0, 0.1) + .motor_velocity(JointAxis::AngY, 0.5, 0.1) + .motor_velocity(JointAxis::AngZ, -2.0, 0.1); + } else if i == num - 1 { + let stiffness = 0.2; + let damping = 1.0; + joint = joint + .motor_position(JointAxis::AngX, 0.0, stiffness, damping) + .motor_position(JointAxis::AngY, 1.0, stiffness, damping) + .motor_position(JointAxis::AngZ, 3.14 / 2.0, stiffness, damping); + } + + if use_articulations { + multibody_joints.insert(parent_handle, child_handle, joint); + } else { + impulse_joints.insert(parent_handle, child_handle, joint); + } + } + + parent_handle = child_handle; + } +} + +fn do_init_world(testbed: &mut Testbed, use_articulations: bool) { + /* + * World + */ + let mut bodies = RigidBodySet::new(); + let mut colliders = ColliderSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let mut multibody_joints = MultibodyJointSet::new(); + + create_prismatic_joints( + &mut bodies, + &mut colliders, + &mut impulse_joints, + &mut multibody_joints, + point![20.0, 5.0, 0.0], + 4, + use_articulations, + ); + create_actuated_prismatic_joints( + &mut bodies, + &mut colliders, + &mut impulse_joints, + &mut multibody_joints, + point![25.0, 5.0, 0.0], + 4, + use_articulations, + ); + create_revolute_joints( + &mut bodies, + &mut colliders, + &mut impulse_joints, + &mut multibody_joints, + point![20.0, 0.0, 0.0], + 3, + use_articulations, + ); + create_revolute_joints_with_limits( + &mut bodies, + &mut colliders, + &mut impulse_joints, + &mut multibody_joints, + point![34.0, 0.0, 0.0], + use_articulations, + ); + create_fixed_joints( + &mut bodies, + &mut colliders, + &mut impulse_joints, + &mut multibody_joints, + point![0.0, 10.0, 0.0], + 10, + use_articulations, + ); + create_actuated_revolute_joints( + &mut bodies, + &mut colliders, + &mut impulse_joints, + &mut multibody_joints, + point![20.0, 10.0, 0.0], + 6, + use_articulations, + ); + create_actuated_spherical_joints( + &mut bodies, + &mut colliders, + &mut impulse_joints, + &mut multibody_joints, + point![13.0, 10.0, 0.0], + 3, + use_articulations, + ); + create_spherical_joints( + &mut bodies, + &mut colliders, + &mut impulse_joints, + &mut multibody_joints, + 15, + use_articulations, + ); + create_spherical_joints_with_limits( + &mut bodies, + &mut colliders, + &mut impulse_joints, + &mut multibody_joints, + point![-5.0, 0.0, 0.0], + use_articulations, + ); + + /* + * Set up the testbed. + */ + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); + testbed.look_at(point![15.0, 5.0, 42.0], point![13.0, 1.0, 1.0]); +} + +pub fn init_world_with_joints(testbed: &mut Testbed) { + do_init_world(testbed, false) +} + +pub fn init_world_with_articulations(testbed: &mut Testbed) { + do_init_world(testbed, true) +} diff --git a/examples3d/ccd3.rs b/examples3d/ccd3.rs index 8d676e3..2e00b86 100644 --- a/examples3d/ccd3.rs +++ b/examples3d/ccd3.rs @@ -44,7 +44,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -153,6 +154,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/examples3d/collision_groups3.rs b/examples3d/collision_groups3.rs index 8db86fa..d43a7fc 100644 --- a/examples3d/collision_groups3.rs +++ b/examples3d/collision_groups3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -93,6 +94,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point!(10.0, 10.0, 10.0), Point::origin()); } diff --git a/examples3d/compound3.rs b/examples3d/compound3.rs index f5f1de1..433210d 100644 --- a/examples3d/compound3.rs +++ b/examples3d/compound3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -90,6 +91,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/examples3d/convex_decomposition3.rs b/examples3d/convex_decomposition3.rs index 64c8802..98cf4a9 100644 --- a/examples3d/convex_decomposition3.rs +++ b/examples3d/convex_decomposition3.rs @@ -15,7 +15,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -52,7 +53,6 @@ pub fn init_world(testbed: &mut Testbed) { .iter() .map(|v| point![v.0, v.1, v.2]) .collect(); - use std::iter::FromIterator; let indices: Vec<_> = model .polygons .into_iter() @@ -104,7 +104,7 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/examples3d/convex_polyhedron3.rs b/examples3d/convex_polyhedron3.rs index 896eb03..7abcefb 100644 --- a/examples3d/convex_polyhedron3.rs +++ b/examples3d/convex_polyhedron3.rs @@ -9,7 +9,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -68,6 +69,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![30.0, 30.0, 30.0], Point::origin()); } diff --git a/examples3d/damping3.rs b/examples3d/damping3.rs index c634a0a..6f2edb9 100644 --- a/examples3d/damping3.rs +++ b/examples3d/damping3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Create the cubes @@ -38,6 +39,13 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world_with_params(bodies, colliders, joints, Vector::zeros(), ()); + testbed.set_world_with_params( + bodies, + colliders, + impulse_joints, + multibody_joints, + Vector::zeros(), + (), + ); testbed.look_at(point![2.0, 2.5, 20.0], point![2.0, 2.5, 0.0]); } diff --git a/examples3d/debug_add_remove_collider3.rs b/examples3d/debug_add_remove_collider3.rs index 12d58ec..dd680d5 100644 --- a/examples3d/debug_add_remove_collider3.rs +++ b/examples3d/debug_add_remove_collider3.rs @@ -7,7 +7,9 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); + /* * Ground. */ @@ -54,6 +56,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![10.0, 10.0, 10.0], Point::origin()); } diff --git a/examples3d/debug_articulations3.rs b/examples3d/debug_articulations3.rs new file mode 100644 index 0000000..26831ef --- /dev/null +++ b/examples3d/debug_articulations3.rs @@ -0,0 +1,98 @@ +use rapier3d::prelude::*; +use rapier_testbed3d::Testbed; + +fn create_ball_articulations( + bodies: &mut RigidBodySet, + colliders: &mut ColliderSet, + impulse_joints: &mut ImpulseJointSet, + multibody_joints: &mut MultibodyJointSet, + num: usize, +) { + let rad = 0.4; + let shift = 1.0; + + let mut body_handles = Vec::new(); + + for k in 0..num { + for i in 0..num { + let fk = k as f32; + let fi = i as f32; + + let status = if i == 0 { + // && (k % 4 == 0 || k == num - 1) { + RigidBodyType::Static + } else { + RigidBodyType::Dynamic + }; + + let rigid_body = RigidBodyBuilder::new(status) + .translation(vector![fk * shift, 0.0, fi * shift * 2.0]) + .build(); + let child_handle = bodies.insert(rigid_body); + let collider = ColliderBuilder::capsule_z(rad * 1.25, rad).build(); + colliders.insert_with_parent(collider, child_handle, bodies); + + // Vertical multibody_joint. + if i > 0 { + let parent_handle = *body_handles.last().unwrap(); + let joint = SphericalJoint::new().local_anchor2(point![0.0, 0.0, -shift * 2.0]); + multibody_joints.insert(parent_handle, child_handle, joint); + } + + // Horizontal multibody_joint. + if k > 0 && i > 0 { + let parent_index = body_handles.len() - num; + let parent_handle = body_handles[parent_index]; + let joint = SphericalJoint::new().local_anchor2(point![-shift, 0.0, 0.0]); + // let joint = + // PrismaticJoint::new(Vector::y_axis()).local_anchor2(point![-shift, 0.0, 0.0]); + // let joint = FixedJoint::new().local_anchor2(point![-shift, 0.0, 0.0]); + // let joint = + // RevoluteJoint::new(Vector::x_axis()).local_anchor2(point![-shift, 0.0, 0.0]); + impulse_joints.insert(parent_handle, child_handle, joint); + } + + body_handles.push(child_handle); + } + } +} + +pub fn init_world(testbed: &mut Testbed) { + /* + * World + */ + let mut bodies = RigidBodySet::new(); + let mut colliders = ColliderSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let mut multibody_joints = MultibodyJointSet::new(); + + let rigid_body = RigidBodyBuilder::new_dynamic().build(); + let collider = ColliderBuilder::cuboid(30.0, 0.01, 30.0) // Vector::y_axis()) + .translation(vector![0.0, -3.0, 0.0]) + .rotation(vector![0.1, 0.0, 0.1]) + .build(); + let handle = bodies.insert(rigid_body); + colliders.insert_with_parent(collider, handle, &mut bodies); + + let rigid_body = RigidBodyBuilder::new_static().build(); + let collider = ColliderBuilder::cuboid(30.0, 0.01, 30.0) // Vector::y_axis()) + .translation(vector![0.0, -3.02, 0.0]) + .rotation(vector![0.1, 0.0, 0.1]) + .build(); + let handle = bodies.insert(rigid_body); + colliders.insert_with_parent(collider, handle, &mut bodies); + + create_ball_articulations( + &mut bodies, + &mut colliders, + &mut impulse_joints, + &mut multibody_joints, + 15, + ); + + /* + * Set up the testbed. + */ + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); + testbed.look_at(point![15.0, 5.0, 42.0], point![13.0, 1.0, 1.0]); +} diff --git a/examples3d/debug_big_colliders3.rs b/examples3d/debug_big_colliders3.rs index 864782f..33460dc 100644 --- a/examples3d/debug_big_colliders3.rs +++ b/examples3d/debug_big_colliders3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -38,6 +39,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![10.0, 10.0, 10.0], Point::origin()); } diff --git a/examples3d/debug_boxes3.rs b/examples3d/debug_boxes3.rs index ea39a8a..ddf86db 100644 --- a/examples3d/debug_boxes3.rs +++ b/examples3d/debug_boxes3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -25,10 +26,10 @@ pub fn init_world(testbed: &mut Testbed) { } // Build the dynamic box rigid body. - for _ in 0..6 { + for _ in 0..2 { let rigid_body = RigidBodyBuilder::new_dynamic() .translation(vector![1.1, 0.0, 0.0]) - .rotation(vector![0.8, 0.2, 0.1]) + // .rotation(vector![0.8, 0.2, 0.1]) .can_sleep(false) .build(); let handle = bodies.insert(rigid_body); @@ -39,6 +40,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![10.0, 10.0, 10.0], Point::origin()); } diff --git a/examples3d/debug_cylinder3.rs b/examples3d/debug_cylinder3.rs index 88908c1..cb087be 100644 --- a/examples3d/debug_cylinder3.rs +++ b/examples3d/debug_cylinder3.rs @@ -10,7 +10,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -55,6 +56,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/examples3d/debug_dynamic_collider_add3.rs b/examples3d/debug_dynamic_collider_add3.rs index f66d8ce..27e1b0b 100644 --- a/examples3d/debug_dynamic_collider_add3.rs +++ b/examples3d/debug_dynamic_collider_add3.rs @@ -7,7 +7,9 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); + /* * Ground. */ @@ -114,6 +116,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![10.0, 10.0, 10.0], Point::origin()); } diff --git a/examples3d/debug_friction3.rs b/examples3d/debug_friction3.rs index 7d01986..44c9fae 100644 --- a/examples3d/debug_friction3.rs +++ b/examples3d/debug_friction3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -38,6 +39,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/examples3d/debug_infinite_fall3.rs b/examples3d/debug_infinite_fall3.rs index dcf4f12..4d950cf 100644 --- a/examples3d/debug_infinite_fall3.rs +++ b/examples3d/debug_infinite_fall3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -44,5 +45,5 @@ pub fn init_world(testbed: &mut Testbed) { * Set up the testbed. */ testbed.look_at(point![100.0, -10.0, 100.0], Point::origin()); - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); } diff --git a/examples3d/debug_prismatic3.rs b/examples3d/debug_prismatic3.rs index 43c6cc0..15176f3 100644 --- a/examples3d/debug_prismatic3.rs +++ b/examples3d/debug_prismatic3.rs @@ -4,7 +4,7 @@ use rapier_testbed3d::Testbed; fn prismatic_repro( bodies: &mut RigidBodySet, colliders: &mut ColliderSet, - joints: &mut JointSet, + impulse_joints: &mut ImpulseJointSet, box_center: Point, ) { let box_rb = bodies.insert( @@ -39,19 +39,12 @@ fn prismatic_repro( ); colliders.insert_with_parent(ColliderBuilder::ball(0.5).build(), wheel_rb, bodies); - let mut prismatic = rapier3d::dynamics::PrismaticJoint::new( - point![pos.x, pos.y, pos.z], - Vector::y_axis(), - Vector::zeros(), - Point::origin(), - Vector::y_axis(), - Vector::default(), - ); - prismatic.configure_motor_model(rapier3d::dynamics::SpringModel::VelocityBased); let (stiffness, damping) = (0.05, 0.2); - prismatic.configure_motor_position(0.0, stiffness, damping); - joints.insert(box_rb, wheel_rb, prismatic); + let prismatic = PrismaticJoint::new(Vector::y_axis()) + .local_anchor1(point![pos.x, pos.y, pos.z]) + .motor_position(0.0, stiffness, damping); + impulse_joints.insert(box_rb, wheel_rb, prismatic); } // put a small box under one of the wheels @@ -73,7 +66,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let mut joints = JointSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -91,13 +85,13 @@ pub fn init_world(testbed: &mut Testbed) { prismatic_repro( &mut bodies, &mut colliders, - &mut joints, + &mut impulse_joints, point![0.0, 5.0, 0.0], ); /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![10.0, 10.0, 10.0], Point::origin()); } diff --git a/examples3d/debug_rollback3.rs b/examples3d/debug_rollback3.rs index c5e5bde..b795434 100644 --- a/examples3d/debug_rollback3.rs +++ b/examples3d/debug_rollback3.rs @@ -7,7 +7,9 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); + /* * Ground. */ @@ -65,6 +67,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![10.0, 10.0, 10.0], Point::origin()); } diff --git a/examples3d/debug_shape_modification3.rs b/examples3d/debug_shape_modification3.rs index 6c27288..eb2966e 100644 --- a/examples3d/debug_shape_modification3.rs +++ b/examples3d/debug_shape_modification3.rs @@ -7,7 +7,9 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); + /* * Ground. */ @@ -113,6 +115,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![10.0, 10.0, 10.0], Point::origin()); } diff --git a/examples3d/debug_triangle3.rs b/examples3d/debug_triangle3.rs index 0c40882..8151cf9 100644 --- a/examples3d/debug_triangle3.rs +++ b/examples3d/debug_triangle3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); // Triangle ground. let vtx = [ @@ -36,6 +37,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![10.0, 10.0, 10.0], Point::origin()); } diff --git a/examples3d/debug_trimesh3.rs b/examples3d/debug_trimesh3.rs index d21d0d3..8e38719 100644 --- a/examples3d/debug_trimesh3.rs +++ b/examples3d/debug_trimesh3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); // Triangle ground. let width = 0.5; @@ -57,6 +58,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![10.0, 10.0, 10.0], Point::origin()); } diff --git a/examples3d/domino3.rs b/examples3d/domino3.rs index 7e9143f..067a86d 100644 --- a/examples3d/domino3.rs +++ b/examples3d/domino3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -73,6 +74,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/examples3d/fountain3.rs b/examples3d/fountain3.rs index 788d05d..4e47878 100644 --- a/examples3d/fountain3.rs +++ b/examples3d/fountain3.rs @@ -6,7 +6,9 @@ const MAX_NUMBER_OF_BODIES: usize = 400; pub fn init_world(testbed: &mut Testbed) { let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); + let rad = 0.5; /* @@ -63,7 +65,8 @@ pub fn init_world(testbed: &mut Testbed) { *handle, &mut physics.islands, &mut physics.colliders, - &mut physics.joints, + &mut physics.impulse_joints, + &mut physics.multibody_joints, ); if let Some(graphics) = &mut graphics { @@ -76,10 +79,10 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); // testbed // .physics_state_mut() // .integration_parameters - // .velocity_based_erp = 0.2; + // .erp = 0.2; testbed.look_at(point![-30.0, 4.0, -30.0], point![0.0, 1.0, 0.0]); } diff --git a/examples3d/harness_capsules3.rs b/examples3d/harness_capsules3.rs index e2f19d5..dae364a 100644 --- a/examples3d/harness_capsules3.rs +++ b/examples3d/harness_capsules3.rs @@ -7,7 +7,8 @@ pub fn init_world(harness: &mut Harness) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -59,7 +60,7 @@ pub fn init_world(harness: &mut Harness) { /* * Set up the harness. */ - harness.set_world(bodies, colliders, joints); + harness.set_world(bodies, colliders, impulse_joints, multibody_joints); } fn main() { diff --git a/examples3d/heightfield3.rs b/examples3d/heightfield3.rs index bb93667..f5a3ba6 100644 --- a/examples3d/heightfield3.rs +++ b/examples3d/heightfield3.rs @@ -8,7 +8,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -95,6 +96,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/examples3d/joints3.rs b/examples3d/joints3.rs index 11cd533..02aa5f9 100644 --- a/examples3d/joints3.rs +++ b/examples3d/joints3.rs @@ -4,7 +4,7 @@ use rapier_testbed3d::Testbed; fn create_prismatic_joints( bodies: &mut RigidBodySet, colliders: &mut ColliderSet, - joints: &mut JointSet, + impulse_joints: &mut ImpulseJointSet, origin: Point, num: usize, ) { @@ -28,25 +28,17 @@ fn create_prismatic_joints( colliders.insert_with_parent(collider, curr_child, bodies); let axis = if i % 2 == 0 { - UnitVector::new_normalize(vector![1.0, 1.0, 0.0]) + UnitVector::new_normalize(vector![1.0f32, 1.0, 0.0]) } else { - UnitVector::new_normalize(vector![-1.0, 1.0, 0.0]) + UnitVector::new_normalize(vector![-1.0f32, 1.0, 0.0]) }; - let z = Vector::z(); - let mut prism = PrismaticJoint::new( - point![0.0, 0.0, 0.0], - axis, - z, - point![0.0, 0.0, -shift], - axis, - z, - ); - prism.limits_enabled = true; - prism.limits[0] = -2.0; - prism.limits[1] = 2.0; + let mut prism = JointData::prismatic(axis) + .local_anchor1(point![0.0, 0.0, shift]) + .local_anchor2(point![0.0, 0.0, 0.0]) + .limit_axis(JointAxis::X, [-2.0, 2.0]); - joints.insert(curr_parent, curr_child, prism); + impulse_joints.insert(curr_parent, curr_child, prism); curr_parent = curr_child; } @@ -55,7 +47,7 @@ fn create_prismatic_joints( fn create_actuated_prismatic_joints( bodies: &mut RigidBodySet, colliders: &mut ColliderSet, - joints: &mut JointSet, + impulse_joints: &mut ImpulseJointSet, origin: Point, num: usize, ) { @@ -84,36 +76,29 @@ fn create_actuated_prismatic_joints( UnitVector::new_normalize(vector![-1.0, 1.0, 0.0]) }; - let z = Vector::z(); - let mut prism = PrismaticJoint::new( - point![0.0, 0.0, 0.0], - axis, - z, - point![0.0, 0.0, -shift], - axis, - z, - ); + let mut prism = JointData::prismatic(axis) + .local_anchor1(point![0.0, 0.0, 0.0]) + .local_anchor2(point![0.0, 0.0, -shift]); if i == 1 { - prism.configure_motor_velocity(1.0, 1.0); - prism.limits_enabled = true; - prism.limits[1] = 5.0; - // We set a max impulse so that the motor doesn't fight - // the limits with large forces. - prism.motor_max_impulse = 1.0; + prism = prism + .limit_axis(JointAxis::X, [-Real::MAX, 5.0]) + .motor_velocity(JointAxis::X, 1.0, 1.0) + // We set a max impulse so that the motor doesn't fight + // the limits with large forces. + .motor_max_impulse(JointAxis::X, 1.0); } else if i > 1 { - prism.configure_motor_position(2.0, 0.01, 1.0); + prism = prism.motor_position(JointAxis::X, 2.0, 0.01, 1.0); } else { - prism.configure_motor_velocity(1.0, 1.0); - // We set a max impulse so that the motor doesn't fight - // the limits with large forces. - prism.motor_max_impulse = 0.7; - prism.limits_enabled = true; - prism.limits[0] = -2.0; - prism.limits[1] = 5.0; + prism = prism + .motor_velocity(JointAxis::X, 1.0, 1.0) + // We set a max impulse so that the motor doesn't fight + // the limits with large forces. + .motor_max_impulse(JointAxis::X, 0.7) + .limit_axis(JointAxis::X, [-2.0, 5.0]); } - joints.insert(curr_parent, curr_child, prism); + impulse_joints.insert(curr_parent, curr_child, prism); curr_parent = curr_child; } @@ -122,7 +107,7 @@ fn create_actuated_prismatic_joints( fn create_revolute_joints( bodies: &mut RigidBodySet, colliders: &mut ColliderSet, - joints: &mut JointSet, + impulse_joints: &mut ImpulseJointSet, origin: Point, num: usize, ) { @@ -156,22 +141,20 @@ fn create_revolute_joints( colliders.insert_with_parent(collider, handles[k], bodies); } - // Setup four joints. - let o = Point::origin(); + // Setup four impulse_joints. let x = Vector::x_axis(); let z = Vector::z_axis(); - let revs = [ - RevoluteJoint::new(o, z, point![0.0, 0.0, -shift], z), - RevoluteJoint::new(o, x, point![-shift, 0.0, 0.0], x), - RevoluteJoint::new(o, z, point![0.0, 0.0, -shift], z), - RevoluteJoint::new(o, x, point![shift, 0.0, 0.0], x), + RevoluteJoint::new(x).local_anchor2(point![0.0, 0.0, -shift]), + RevoluteJoint::new(z).local_anchor2(point![-shift, 0.0, 0.0]), + RevoluteJoint::new(x).local_anchor2(point![0.0, 0.0, -shift]), + RevoluteJoint::new(z).local_anchor2(point![shift, 0.0, 0.0]), ]; - joints.insert(curr_parent, handles[0], revs[0]); - joints.insert(handles[0], handles[1], revs[1]); - joints.insert(handles[1], handles[2], revs[2]); - joints.insert(handles[2], handles[3], revs[3]); + impulse_joints.insert(curr_parent, handles[0], revs[0]); + impulse_joints.insert(handles[0], handles[1], revs[1]); + impulse_joints.insert(handles[1], handles[2], revs[2]); + impulse_joints.insert(handles[2], handles[3], revs[3]); curr_parent = handles[3]; } @@ -180,7 +163,7 @@ fn create_revolute_joints( fn create_revolute_joints_with_limits( bodies: &mut RigidBodySet, colliders: &mut ColliderSet, - joints: &mut JointSet, + impulse_joints: &mut ImpulseJointSet, origin: Point, ) { let ground = bodies.insert( @@ -212,25 +195,14 @@ fn create_revolute_joints_with_limits( bodies, ); - let mut joint1 = RevoluteJoint::new( - Point::origin(), - Vector::z_axis(), - Point::origin(), - Vector::z_axis(), - ); - joint1.limits_enabled = true; - joint1.limits = [-0.2, 0.2]; - joints.insert(ground, platform1, joint1); + let z = Vector::z_axis(); + let mut joint1 = RevoluteJoint::new(z).limit_axis(JointAxis::X, [-0.2, 0.2]); + impulse_joints.insert(ground, platform1, joint1); - let mut joint2 = RevoluteJoint::new( - Point::origin(), - Vector::z_axis(), - Point::from(-shift), - Vector::z_axis(), - ); - joint2.limits_enabled = true; - joint2.limits = [-0.3, 0.3]; - joints.insert(platform1, platform2, joint2); + let mut joint2 = RevoluteJoint::new(z) + .local_anchor2(shift.into()) + .limit_axis(JointAxis::Z, [-0.2, 0.2]); + impulse_joints.insert(platform1, platform2, joint2); // Let’s add a couple of cuboids that will fall on the platforms, triggering the joint limits. let cuboid_body1 = bodies.insert( @@ -259,7 +231,7 @@ fn create_revolute_joints_with_limits( fn create_fixed_joints( bodies: &mut RigidBodySet, colliders: &mut ColliderSet, - joints: &mut JointSet, + impulse_joints: &mut ImpulseJointSet, origin: Point, num: usize, ) { @@ -268,8 +240,8 @@ fn create_fixed_joints( let mut body_handles = Vec::new(); - for k in 0..num { - for i in 0..num { + for i in 0..num { + for k in 0..num { let fk = k as f32; let fi = i as f32; @@ -295,23 +267,18 @@ fn create_fixed_joints( // Vertical joint. if i > 0 { - let parent_handle = *body_handles.last().unwrap(); - let joint = FixedJoint::new( - Isometry::identity(), - Isometry::translation(0.0, 0.0, -shift), - ); - joints.insert(parent_handle, child_handle, joint); + let parent_index = body_handles.len() - num; + let parent_handle = body_handles[parent_index]; + let joint = JointData::fixed().local_anchor2(point![0.0, 0.0, -shift]); + impulse_joints.insert(parent_handle, child_handle, joint); } // Horizontal joint. if k > 0 { - let parent_index = body_handles.len() - num; + let parent_index = body_handles.len() - 1; let parent_handle = body_handles[parent_index]; - let joint = FixedJoint::new( - Isometry::identity(), - Isometry::translation(-shift, 0.0, 0.0), - ); - joints.insert(parent_handle, child_handle, joint); + let joint = JointData::fixed().local_anchor2(point![-shift, 0.0, 0.0]); + impulse_joints.insert(parent_handle, child_handle, joint); } body_handles.push(child_handle); @@ -322,7 +289,7 @@ fn create_fixed_joints( fn create_ball_joints( bodies: &mut RigidBodySet, colliders: &mut ColliderSet, - joints: &mut JointSet, + impulse_joints: &mut ImpulseJointSet, num: usize, ) { let rad = 0.4; @@ -351,16 +318,16 @@ fn create_ball_joints( // Vertical joint. if i > 0 { let parent_handle = *body_handles.last().unwrap(); - let joint = BallJoint::new(Point::origin(), point![0.0, 0.0, -shift * 2.0]); - joints.insert(parent_handle, child_handle, joint); + let joint = JointData::ball().local_anchor2(point![0.0, 0.0, -shift * 2.0]); + impulse_joints.insert(parent_handle, child_handle, joint); } // Horizontal joint. if k > 0 { let parent_index = body_handles.len() - num; let parent_handle = body_handles[parent_index]; - let joint = BallJoint::new(Point::origin(), point![-shift, 0.0, 0.0]); - joints.insert(parent_handle, child_handle, joint); + let joint = JointData::ball().local_anchor2(point![-shift, 0.0, 0.0]); + impulse_joints.insert(parent_handle, child_handle, joint); } body_handles.push(child_handle); @@ -371,7 +338,7 @@ fn create_ball_joints( fn create_ball_joints_with_limits( bodies: &mut RigidBodySet, colliders: &mut ColliderSet, - joints: &mut JointSet, + impulse_joints: &mut ImpulseJointSet, origin: Point, ) { let shift = vector![0.0, 0.0, 3.0]; @@ -405,38 +372,32 @@ fn create_ball_joints_with_limits( bodies, ); - let mut joint1 = BallJoint::new(Point::origin(), Point::from(-shift)); - joint1.limits_enabled = true; - joint1.limits_local_axis1 = Vector::z_axis(); - joint1.limits_local_axis2 = Vector::z_axis(); - joint1.limits_angle = 0.2; - joints.insert(ground, ball1, joint1); + let mut joint1 = JointData::ball() + .local_anchor2(Point::from(-shift)) + .limit_axis(JointAxis::X, [-0.2, 0.2]) + .limit_axis(JointAxis::Y, [-0.2, 0.2]); + impulse_joints.insert(ground, ball1, joint1); - let mut joint2 = BallJoint::new(Point::origin(), Point::from(-shift)); - joint2.limits_enabled = true; - joint2.limits_local_axis1 = Vector::z_axis(); - joint2.limits_local_axis2 = Vector::z_axis(); - joint2.limits_angle = 0.3; - joints.insert(ball1, ball2, joint2); + let mut joint2 = JointData::ball() + .local_anchor2(Point::from(-shift)) + .limit_axis(JointAxis::X, [-0.3, 0.3]) + .limit_axis(JointAxis::Y, [-0.3, 0.3]); + impulse_joints.insert(ball1, ball2, joint2); } fn create_actuated_revolute_joints( bodies: &mut RigidBodySet, colliders: &mut ColliderSet, - joints: &mut JointSet, + impulse_joints: &mut ImpulseJointSet, origin: Point, num: usize, ) { let rad = 0.4; let shift = 2.0; - // We will reuse this base configuration for all the joints here. - let joint_template = RevoluteJoint::new( - Point::origin(), - Vector::z_axis(), - point![0.0, 0.0, -shift], - Vector::z_axis(), - ); + // We will reuse this base configuration for all the impulse_joints here. + let z = Vector::z_axis(); + let joint_template = RevoluteJoint::new(z).local_anchor2(point![0.0, 0.0, -shift]); let mut parent_handle = RigidBodyHandle::invalid(); @@ -467,19 +428,19 @@ fn create_actuated_revolute_joints( let mut joint = joint_template.clone(); if i % 3 == 1 { - joint.configure_motor_velocity(-20.0, 0.1); + joint = joint.motor_velocity(JointAxis::AngX, -20.0, 0.1); } else if i == num - 1 { let stiffness = 0.2; let damping = 1.0; - joint.configure_motor_position(3.14 / 2.0, stiffness, damping); + joint = joint.motor_position(JointAxis::AngX, 3.14 / 2.0, stiffness, damping); } if i == 1 { - joint.local_anchor2.y = 2.0; - joint.configure_motor_velocity(-2.0, 0.1); + joint.local_frame2.translation.vector.y = 2.0; + joint = joint.motor_velocity(JointAxis::AngX, -2.0, 0.1); } - joints.insert(parent_handle, child_handle, joint); + impulse_joints.insert(parent_handle, child_handle, joint); } parent_handle = child_handle; @@ -489,15 +450,15 @@ fn create_actuated_revolute_joints( fn create_actuated_ball_joints( bodies: &mut RigidBodySet, colliders: &mut ColliderSet, - joints: &mut JointSet, + impulse_joints: &mut ImpulseJointSet, origin: Point, num: usize, ) { let rad = 0.4; let shift = 2.0; - // We will reuse this base configuration for all the joints here. - let joint_template = BallJoint::new(point![0.0, 0.0, shift], Point::origin()); + // We will reuse this base configuration for all the impulse_joints here. + let joint_template = JointData::ball().local_anchor1(point![0.0, 0.0, shift]); let mut parent_handle = RigidBodyHandle::invalid(); @@ -526,18 +487,20 @@ fn create_actuated_ball_joints( let mut joint = joint_template.clone(); if i == 1 { - joint.configure_motor_velocity(vector![0.0, 0.5, -2.0], 0.1); + joint = joint + .motor_velocity(JointAxis::AngX, 0.0, 0.1) + .motor_velocity(JointAxis::AngY, 0.5, 0.1) + .motor_velocity(JointAxis::AngZ, -2.0, 0.1); } else if i == num - 1 { let stiffness = 0.2; let damping = 1.0; - joint.configure_motor_position( - Rotation::new(vector![0.0, 1.0, 3.14 / 2.0]), - stiffness, - damping, - ); + joint = joint + .motor_position(JointAxis::AngX, 0.0, stiffness, damping) + .motor_position(JointAxis::AngY, 1.0, stiffness, damping) + .motor_position(JointAxis::AngZ, 3.14 / 2.0, stiffness, damping); } - joints.insert(parent_handle, child_handle, joint); + impulse_joints.insert(parent_handle, child_handle, joint); } parent_handle = child_handle; @@ -550,67 +513,68 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let mut joints = JointSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); create_prismatic_joints( &mut bodies, &mut colliders, - &mut joints, + &mut impulse_joints, point![20.0, 5.0, 0.0], 4, ); create_actuated_prismatic_joints( &mut bodies, &mut colliders, - &mut joints, + &mut impulse_joints, point![25.0, 5.0, 0.0], 4, ); create_revolute_joints( &mut bodies, &mut colliders, - &mut joints, + &mut impulse_joints, point![20.0, 0.0, 0.0], 3, ); create_revolute_joints_with_limits( &mut bodies, &mut colliders, - &mut joints, + &mut impulse_joints, point![34.0, 0.0, 0.0], ); create_fixed_joints( &mut bodies, &mut colliders, - &mut joints, + &mut impulse_joints, point![0.0, 10.0, 0.0], 10, ); create_actuated_revolute_joints( &mut bodies, &mut colliders, - &mut joints, + &mut impulse_joints, point![20.0, 10.0, 0.0], 6, ); create_actuated_ball_joints( &mut bodies, &mut colliders, - &mut joints, + &mut impulse_joints, point![13.0, 10.0, 0.0], 3, ); - create_ball_joints(&mut bodies, &mut colliders, &mut joints, 15); + create_ball_joints(&mut bodies, &mut colliders, &mut impulse_joints, 15); create_ball_joints_with_limits( &mut bodies, &mut colliders, - &mut joints, + &mut impulse_joints, point![-5.0, 0.0, 0.0], ); /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![15.0, 5.0, 42.0], point![13.0, 1.0, 1.0]); } diff --git a/examples3d/keva3.rs b/examples3d/keva3.rs index 38a0432..ad9e1ae 100644 --- a/examples3d/keva3.rs +++ b/examples3d/keva3.rs @@ -83,7 +83,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -132,6 +133,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/examples3d/locked_rotations3.rs b/examples3d/locked_rotations3.rs index 95dfae1..5e925b0 100644 --- a/examples3d/locked_rotations3.rs +++ b/examples3d/locked_rotations3.rs @@ -10,7 +10,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * The ground @@ -54,6 +55,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![10.0, 3.0, 0.0], point![0.0, 3.0, 0.0]); } diff --git a/examples3d/one_way_platforms3.rs b/examples3d/one_way_platforms3.rs index 3802563..4857ae6 100644 --- a/examples3d/one_way_platforms3.rs +++ b/examples3d/one_way_platforms3.rs @@ -62,7 +62,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -126,7 +127,8 @@ pub fn init_world(testbed: &mut Testbed) { testbed.set_world_with_params( bodies, colliders, - joints, + impulse_joints, + multibody_joints, vector![0.0, -9.81, 0.0], physics_hooks, ); diff --git a/examples3d/platform3.rs b/examples3d/platform3.rs index 6b3c234..d1126ec 100644 --- a/examples3d/platform3.rs +++ b/examples3d/platform3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground. @@ -103,6 +104,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Run the simulation. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![-10.0, 5.0, -10.0], Point::origin()); } diff --git a/examples3d/primitives3.rs b/examples3d/primitives3.rs index db9143b..891557e 100644 --- a/examples3d/primitives3.rs +++ b/examples3d/primitives3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -51,13 +52,13 @@ pub fn init_world(testbed: &mut Testbed) { let handle = bodies.insert(rigid_body); let collider = match j % 5 { - 0 => ColliderBuilder::cuboid(rad, rad, rad).build(), - 1 => ColliderBuilder::ball(rad).build(), - // Rounded cylinders are much more efficient that cylinder, even if the - // rounding margin is small. - 2 => ColliderBuilder::round_cylinder(rad, rad, rad / 10.0).build(), - 3 => ColliderBuilder::cone(rad, rad).build(), - _ => ColliderBuilder::capsule_y(rad, rad).build(), + _ => ColliderBuilder::cuboid(rad, rad, rad).build(), + // 1 => ColliderBuilder::ball(rad).build(), + // // Rounded cylinders are much more efficient that cylinder, even if the + // // rounding margin is small. + // 2 => ColliderBuilder::round_cylinder(rad, rad, rad / 10.0).build(), + // 3 => ColliderBuilder::cone(rad, rad).build(), + // _ => ColliderBuilder::capsule_y(rad, rad).build(), }; colliders.insert_with_parent(collider, handle, &mut bodies); @@ -70,6 +71,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/examples3d/rapier.data b/examples3d/rapier.data new file mode 100644 index 0000000000000000000000000000000000000000..33ddaa39622ac4024342c83beaf16d26b63dac25 GIT binary patch literal 2017666 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

MWYgy@{^c?+)`(9Z}SEV$52&lIj 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

    tx*f=y|Xpf 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∓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 +-jM{g{`wnL!KgEqE<%) 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
    ov-3-VYU>3Y~wz!oy{D$)xd$cFoe@@457Ms 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
    A-!EoXEuo-4=C6rif-}J?WJ03*gsnAhYA(F~-NT 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

    Gsri_cf{?Zt`-*l{PJ$1GHLK7Fhr#iP+ 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 literal 0 HcmV?d00001 diff --git a/examples3d/restitution3.rs b/examples3d/restitution3.rs index 4c08742..00462d1 100644 --- a/examples3d/restitution3.rs +++ b/examples3d/restitution3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -44,6 +45,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![0.0, 3.0, 30.0], point![0.0, 3.0, 0.0]); } diff --git a/examples3d/sensor3.rs b/examples3d/sensor3.rs index 0e2b489..ebd47f3 100644 --- a/examples3d/sensor3.rs +++ b/examples3d/sensor3.rs @@ -7,7 +7,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground. @@ -102,6 +103,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![-6.0, 4.0, -6.0], point![0.0, 1.0, 0.0]); } diff --git a/examples3d/trimesh3.rs b/examples3d/trimesh3.rs index f5810aa..31a532b 100644 --- a/examples3d/trimesh3.rs +++ b/examples3d/trimesh3.rs @@ -8,7 +8,8 @@ pub fn init_world(testbed: &mut Testbed) { */ let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let joints = JointSet::new(); + let impulse_joints = ImpulseJointSet::new(); + let multibody_joints = MultibodyJointSet::new(); /* * Ground @@ -100,6 +101,6 @@ pub fn init_world(testbed: &mut Testbed) { /* * Set up the testbed. */ - testbed.set_world(bodies, colliders, joints); + testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.look_at(point![100.0, 100.0, 100.0], Point::origin()); } diff --git a/publish-testbeds.sh b/publish-testbeds.sh index 2b2e068..24ef410 100755 --- a/publish-testbeds.sh +++ b/publish-testbeds.sh @@ -6,20 +6,20 @@ echo "$tmp" cp -r src "$tmp"/. cp -r src_testbed "$tmp"/. -cp -r build "$tmp"/. +cp -r crates "$tmp"/. cp -r LICENSE README.md "$tmp"/. ### Publish the 2D version. -sed 's#\.\./\.\./src#src#g' build/rapier_testbed2d/Cargo.toml > "$tmp"/Cargo.toml -sed -i 's#\.\./rapier#./build/rapier#g' "$tmp"/Cargo.toml +sed 's#\.\./\.\./src#src#g' crates/rapier_testbed2d/Cargo.toml > "$tmp"/Cargo.toml +sed -i 's#\.\./rapier#./crates/rapier#g' "$tmp"/Cargo.toml currdir=$(pwd) cd "$tmp" && cargo publish cd "$currdir" || exit ### Publish the 3D version. -sed 's#\.\./\.\./src#src#g' build/rapier_testbed3d/Cargo.toml > "$tmp"/Cargo.toml -sed -i 's#\.\./rapier#./build/rapier#g' "$tmp"/Cargo.toml +sed 's#\.\./\.\./src#src#g' crates/rapier_testbed3d/Cargo.toml > "$tmp"/Cargo.toml +sed -i 's#\.\./rapier#./crates/rapier#g' "$tmp"/Cargo.toml cp -r LICENSE README.md "$tmp"/. cd "$tmp" && cargo publish diff --git a/src/counters/mod.rs b/src/counters/mod.rs index 4d4d05d..324696f 100644 --- a/src/counters/mod.rs +++ b/src/counters/mod.rs @@ -14,7 +14,7 @@ mod solver_counters; mod stages_counters; mod timer; -/// Aggregation of all the performances counters tracked by nphysics. +/// Aggregation of all the performances counters tracked by rapier. #[derive(Clone, Copy)] pub struct Counters { /// Whether thi counter is enabled or not. @@ -34,7 +34,7 @@ pub struct Counters { } impl Counters { - /// Create a new set of counters initialized to wero. + /// Create a new set of counters initialized to zero. pub fn new(enabled: bool) -> Self { Counters { enabled, diff --git a/src/data/coarena.rs b/src/data/coarena.rs index 124d85a..1f01c05 100644 --- a/src/data/coarena.rs +++ b/src/data/coarena.rs @@ -13,6 +13,14 @@ impl Coarena { Self { data: Vec::new() } } + pub fn iter(&self) -> impl Iterator { + self.data + .iter() + .enumerate() + .filter(|(_, elt)| elt.0 != u32::MAX) + .map(|(i, elt)| (Index::from_raw_parts(i as u32, elt.0), &elt.1)) + } + /// Gets a specific element from the coarena without specifying its generation number. /// /// It is strongly encouraged to use `Coarena::get` instead of this method because this method @@ -23,12 +31,12 @@ impl Coarena { /// Deletes an element for the coarena and returns its value. /// - /// We can't really remove an element from the coarena. So instead of actually removing - /// it, this method will reset the value to the given `removed_value`. + /// This method will reset the value to the given `removed_value`. pub fn remove(&mut self, index: Index, removed_value: T) -> Option { let (i, g) = index.into_raw_parts(); let data = self.data.get_mut(i as usize)?; if g == data.0 { + data.0 = u32::MAX; // invalidate the generation number. Some(std::mem::replace(&mut data.1, removed_value)) } else { None diff --git a/src/data/graph.rs b/src/data/graph.rs index de958c3..2dc7fbf 100644 --- a/src/data/graph.rs +++ b/src/data/graph.rs @@ -644,19 +644,24 @@ impl<'a, E> Iterator for Edges<'a, E> { // For iterate_over, "both" is represented as None. // For reverse, "no" is represented as None. - let (iterate_over, reverse) = (None, Some(self.direction.opposite())); + let (iterate_over, _reverse) = (None, Some(self.direction.opposite())); if iterate_over.unwrap_or(Direction::Outgoing) == Direction::Outgoing { let i = self.next[0].index(); - if let Some(Edge { node, weight, next }) = self.edges.get(i) { + if let Some(Edge { + node: _node, + weight, + next, + }) = self.edges.get(i) + { self.next[0] = next[0]; return Some(EdgeReference { index: EdgeIndex(i as u32), - node: if reverse == Some(Direction::Outgoing) { - swap_pair(*node) - } else { - *node - }, + // node: if reverse == Some(Direction::Outgoing) { + // swap_pair(*node) + // } else { + // *node + // }, weight, }); } @@ -674,11 +679,11 @@ impl<'a, E> Iterator for Edges<'a, E> { return Some(EdgeReference { index: edge_index, - node: if reverse == Some(Direction::Incoming) { - swap_pair(*node) - } else { - *node - }, + // node: if reverse == Some(Direction::Incoming) { + // swap_pair(*node) + // } else { + // *node + // }, weight, }); } @@ -688,10 +693,10 @@ impl<'a, E> Iterator for Edges<'a, E> { } } -fn swap_pair(mut x: [T; 2]) -> [T; 2] { - x.swap(0, 1); - x -} +// fn swap_pair(mut x: [T; 2]) -> [T; 2] { +// x.swap(0, 1); +// x +// } impl<'a, E> Clone for Edges<'a, E> { fn clone(&self) -> Self { @@ -742,24 +747,11 @@ impl IndexMut for Graph { } } -/// A “walker” object that can be used to step through the edge list of a node. -/// -/// Created with [`.detach()`](struct.Neighbors.html#method.detach). -/// -/// The walker does not borrow from the graph, so it lets you step through -/// neighbors or incident edges while also mutating graph weights, as -/// in the following example: -#[derive(Clone)] -pub struct WalkNeighbors { - skip_start: NodeIndex, - next: [EdgeIndex; 2], -} - /// Reference to a `Graph` edge. #[derive(Debug)] pub struct EdgeReference<'a, E: 'a> { index: EdgeIndex, - node: [NodeIndex; 2], + // node: [NodeIndex; 2], weight: &'a E, } diff --git a/src/dynamics/integration_parameters.rs b/src/dynamics/integration_parameters.rs index 4725319..d998eec 100644 --- a/src/dynamics/integration_parameters.rs +++ b/src/dynamics/integration_parameters.rs @@ -18,18 +18,6 @@ pub struct IntegrationParameters { /// to numerical instabilities. pub min_ccd_dt: Real, - /// The Error Reduction Parameter in `[0, 1]` is the proportion of - /// the positional error to be corrected at each time step (default: `0.2`). - pub erp: Real, - /// The Error Reduction Parameter for joints in `[0, 1]` is the proportion of - /// the positional error to be corrected at each time step (default: `0.2`). - pub joint_erp: Real, - /// Each cached impulse are multiplied by this coefficient in `[0, 1]` - /// when they are re-used to initialize the solver (default `1.0`). - pub warmstart_coeff: Real, - /// Correction factor to avoid large warmstart impulse after a strong impact (default `10.0`). - pub warmstart_correction_slope: Real, - /// 0-1: how much of the velocity to dampen out in the constraint solver? /// (default `1.0`). pub velocity_solve_fraction: Real, @@ -40,23 +28,21 @@ pub struct IntegrationParameters { /// If non-zero, you do not need the positional solver. /// A good non-zero value is around `0.2`. /// (default `0.0`). - pub velocity_based_erp: Real, + pub erp: Real, - /// Amount of penetration the engine wont attempt to correct (default: `0.005m`). + /// Amount of penetration the engine wont attempt to correct (default: `0.001m`). pub allowed_linear_error: Real, /// The maximal distance separating two objects that will generate predictive contacts (default: `0.002`). pub prediction_distance: Real, - /// Amount of angular drift of joint limits the engine wont - /// attempt to correct (default: `0.001rad`). - pub allowed_angular_error: Real, - /// Maximum linear correction during one step of the non-linear position solver (default: `0.2`). - pub max_linear_correction: Real, - /// Maximum angular correction during one step of the non-linear position solver (default: `0.2`). - pub max_angular_correction: Real, - /// Maximum number of iterations performed by the velocity constraints solver (default: `4`). + /// Maximum number of iterations performed to solve non-penetration and joint constraints (default: `4`). pub max_velocity_iterations: usize, - /// Maximum number of iterations performed by the position-based constraints solver (default: `1`). - pub max_position_iterations: usize, + /// Maximum number of iterations performed to solve friction constraints (default: `8`). + pub max_velocity_friction_iterations: usize, + /// Maximum number of iterations performed to remove the energy introduced by penetration corrections (default: `1`). + pub max_stabilization_iterations: usize, + /// If `false`, friction and non-penetration constraints will be solved in the same loop. Otherwise, + /// non-penetration constraints are solved first, and friction constraints are solved after (default: `true`). + pub interleave_restitution_and_friction_resolution: bool, /// Minimum number of dynamic bodies in each active island (default: `128`). pub min_island_size: usize, /// Maximum number of substeps performed by the solver (default: `1`). @@ -64,46 +50,6 @@ pub struct IntegrationParameters { } impl IntegrationParameters { - /// Creates a set of integration parameters with the given values. - #[deprecated = "Use `IntegrationParameters { dt: 60.0, ..Default::default() }` instead"] - pub fn new( - dt: Real, - erp: Real, - joint_erp: Real, - warmstart_coeff: Real, - allowed_linear_error: Real, - allowed_angular_error: Real, - max_linear_correction: Real, - max_angular_correction: Real, - prediction_distance: Real, - max_velocity_iterations: usize, - max_position_iterations: usize, - max_ccd_substeps: usize, - ) -> Self { - IntegrationParameters { - dt, - erp, - joint_erp, - warmstart_coeff, - allowed_linear_error, - allowed_angular_error, - max_linear_correction, - max_angular_correction, - prediction_distance, - max_velocity_iterations, - max_position_iterations, - max_ccd_substeps, - ..Default::default() - } - } - - /// The current time-stepping length. - #[inline(always)] - #[deprecated = "You can just read the `IntegrationParams::dt` value directly"] - pub fn dt(&self) -> Real { - self.dt - } - /// The inverse of the time-stepping length, i.e. the steps per seconds (Hz). /// /// This is zero if `self.dt` is zero. @@ -136,10 +82,10 @@ impl IntegrationParameters { } } - /// Convenience: `velocity_based_erp / dt` + /// Convenience: `erp / dt` #[inline] - pub(crate) fn velocity_based_erp_inv_dt(&self) -> Real { - self.velocity_based_erp * self.inv_dt() + pub(crate) fn erp_inv_dt(&self) -> Real { + self.erp * self.inv_dt() } } @@ -148,20 +94,14 @@ impl Default for IntegrationParameters { Self { dt: 1.0 / 60.0, min_ccd_dt: 1.0 / 60.0 / 100.0, - // multithreading_enabled: true, - erp: 0.2, - joint_erp: 0.2, velocity_solve_fraction: 1.0, - velocity_based_erp: 0.0, - warmstart_coeff: 1.0, - warmstart_correction_slope: 10.0, - allowed_linear_error: 0.005, + erp: 0.8, + allowed_linear_error: 0.001, // 0.005 prediction_distance: 0.002, - allowed_angular_error: 0.001, - max_linear_correction: 0.2, - max_angular_correction: 0.2, max_velocity_iterations: 4, - max_position_iterations: 1, + max_velocity_friction_iterations: 8, + max_stabilization_iterations: 1, + interleave_restitution_and_friction_resolution: true, // Enabling this makes a big difference for 2D stability. // FIXME: what is the optimal value for min_island_size? // It should not be too big so that we don't end up with // huge islands that don't fit in cache. diff --git a/src/dynamics/island_manager.rs b/src/dynamics/island_manager.rs index 10ac0f8..55b47c9 100644 --- a/src/dynamics/island_manager.rs +++ b/src/dynamics/island_manager.rs @@ -1,7 +1,7 @@ use crate::data::{BundleSet, ComponentSet, ComponentSetMut, ComponentSetOption}; use crate::dynamics::{ - JointSet, RigidBodyActivation, RigidBodyColliders, RigidBodyHandle, RigidBodyIds, - RigidBodyType, RigidBodyVelocity, + ImpulseJointSet, MultibodyJointSet, RigidBodyActivation, RigidBodyColliders, RigidBodyHandle, + RigidBodyIds, RigidBodyType, RigidBodyVelocity, }; use crate::geometry::{ColliderParent, NarrowPhase}; use crate::math::Real; @@ -175,7 +175,8 @@ impl IslandManager { bodies: &mut Bodies, colliders: &Colliders, narrow_phase: &NarrowPhase, - joints: &JointSet, + impulse_joints: &ImpulseJointSet, + multibody_joints: &MultibodyJointSet, min_island_size: usize, ) where Bodies: ComponentSetMut @@ -302,11 +303,15 @@ impl IslandManager { // in contact or joined with this collider. push_contacting_bodies(rb_colliders, colliders, narrow_phase, &mut self.stack); - for inter in joints.joints_with(handle) { + for inter in impulse_joints.joints_with(handle) { let other = crate::utils::select_other((inter.0, inter.1), handle); self.stack.push(other); } + for other in multibody_joints.attached_bodies(handle) { + self.stack.push(other); + } + bodies.map_mut_internal(handle.0, |activation: &mut RigidBodyActivation| { activation.wake_up(false); }); diff --git a/src/dynamics/joint/ball_joint.rs b/src/dynamics/joint/ball_joint.rs deleted file mode 100644 index 0b626c0..0000000 --- a/src/dynamics/joint/ball_joint.rs +++ /dev/null @@ -1,148 +0,0 @@ -use crate::dynamics::SpringModel; -use crate::math::{Point, Real, Rotation, UnitVector, Vector}; - -#[derive(Copy, Clone, PartialEq)] -#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] -/// A joint that removes all relative linear motion between a pair of points on two bodies. -pub struct BallJoint { - /// Where the ball joint is attached on the first body, expressed in the first body local frame. - pub local_anchor1: Point, - /// Where the ball joint is attached on the second body, expressed in the second body local frame. - pub local_anchor2: Point, - /// The impulse applied by this joint on the first body. - /// - /// The impulse applied to the second body is given by `-impulse`. - pub impulse: Vector, - - /// The target relative angular velocity the motor will attempt to reach. - #[cfg(feature = "dim2")] - pub motor_target_vel: Real, - /// The target relative angular velocity the motor will attempt to reach. - #[cfg(feature = "dim3")] - pub motor_target_vel: Vector, - /// The target angular position of this joint, expressed as an axis-angle. - pub motor_target_pos: Rotation, - /// The motor's stiffness. - /// See the documentation of `SpringModel` for more information on this parameter. - pub motor_stiffness: Real, - /// The motor's damping. - /// See the documentation of `SpringModel` for more information on this parameter. - pub motor_damping: Real, - /// The maximal impulse the motor is able to deliver. - pub motor_max_impulse: Real, - /// The angular impulse applied by the motor. - #[cfg(feature = "dim2")] - pub motor_impulse: Real, - /// The angular impulse applied by the motor. - #[cfg(feature = "dim3")] - pub motor_impulse: Vector, - /// The spring-like model used by the motor to reach the target velocity and . - pub motor_model: SpringModel, - - /// Are the limits enabled for this joint? - pub limits_enabled: bool, - /// The axis of the limit cone for this joint, if the local-space of the first body. - pub limits_local_axis1: UnitVector, - /// The axis of the limit cone for this joint, if the local-space of the first body. - pub limits_local_axis2: UnitVector, - /// The maximum angle allowed between the two limit axes in world-space. - pub limits_angle: Real, - /// The impulse applied to enforce joint limits. - pub limits_impulse: Real, -} - -impl BallJoint { - /// Creates a new Ball joint from two anchors given on the local spaces of the respective bodies. - pub fn new(local_anchor1: Point, local_anchor2: Point) -> Self { - Self::with_impulse(local_anchor1, local_anchor2, Vector::zeros()) - } - - pub(crate) fn with_impulse( - local_anchor1: Point, - local_anchor2: Point, - impulse: Vector, - ) -> Self { - Self { - local_anchor1, - local_anchor2, - impulse, - motor_target_vel: na::zero(), - motor_target_pos: Rotation::identity(), - motor_stiffness: 0.0, - motor_damping: 0.0, - motor_impulse: na::zero(), - motor_max_impulse: Real::MAX, - motor_model: SpringModel::default(), - limits_enabled: false, - limits_local_axis1: Vector::x_axis(), - limits_local_axis2: Vector::x_axis(), - limits_angle: Real::MAX, - limits_impulse: 0.0, - } - } - - /// Can a SIMD constraint be used for resolving this joint? - pub fn supports_simd_constraints(&self) -> bool { - // SIMD ball constraints don't support motors and limits right now. - !self.limits_enabled - && (self.motor_max_impulse == 0.0 - || (self.motor_stiffness == 0.0 && self.motor_damping == 0.0)) - } - - /// Set the spring-like model used by the motor to reach the desired target velocity and position. - pub fn configure_motor_model(&mut self, model: SpringModel) { - self.motor_model = model; - } - - /// Sets the target velocity and velocity correction factor this motor. - #[cfg(feature = "dim2")] - pub fn configure_motor_velocity(&mut self, target_vel: Real, factor: Real) { - self.configure_motor(self.motor_target_pos, target_vel, 0.0, factor) - } - - /// Sets the target velocity and velocity correction factor this motor. - #[cfg(feature = "dim3")] - pub fn configure_motor_velocity(&mut self, target_vel: Vector, factor: Real) { - self.configure_motor(self.motor_target_pos, target_vel, 0.0, factor) - } - - /// Sets the target orientation this motor needs to reach. - pub fn configure_motor_position( - &mut self, - target_pos: Rotation, - stiffness: Real, - damping: Real, - ) { - self.configure_motor(target_pos, na::zero(), stiffness, damping) - } - - /// Sets the target orientation this motor needs to reach. - #[cfg(feature = "dim2")] - pub fn configure_motor( - &mut self, - target_pos: Rotation, - target_vel: Real, - stiffness: Real, - damping: Real, - ) { - self.motor_target_vel = target_vel; - self.motor_target_pos = target_pos; - self.motor_stiffness = stiffness; - self.motor_damping = damping; - } - - /// Configure both the target orientation and target velocity of the motor. - #[cfg(feature = "dim3")] - pub fn configure_motor( - &mut self, - target_pos: Rotation, - target_vel: Vector, - stiffness: Real, - damping: Real, - ) { - self.motor_target_vel = target_vel; - self.motor_target_pos = target_pos; - self.motor_stiffness = stiffness; - self.motor_damping = damping; - } -} diff --git a/src/dynamics/joint/fixed_joint.rs b/src/dynamics/joint/fixed_joint.rs index 6424750..10c4d7e 100644 --- a/src/dynamics/joint/fixed_joint.rs +++ b/src/dynamics/joint/fixed_joint.rs @@ -1,38 +1,55 @@ -use crate::math::{Isometry, Real, SpacialVector}; +use crate::dynamics::{JointAxesMask, JointData}; +use crate::math::{Isometry, Point, Real}; -#[derive(Copy, Clone, PartialEq)] #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] -/// A joint that prevents all relative movement between two bodies. -/// -/// Given two frames of references, this joint aims to ensure these frame always coincide in world-space. +#[derive(Copy, Clone, Debug, PartialEq)] pub struct FixedJoint { - /// The frame of reference for the first body affected by this joint, expressed in the local frame - /// of the first body. - pub local_frame1: Isometry, - /// The frame of reference for the second body affected by this joint, expressed in the local frame - /// of the first body. - pub local_frame2: Isometry, - /// The impulse applied to the first body affected by this joint. - /// - /// The impulse applied to the second body affected by this joint is given by `-impulse`. - /// This combines both linear and angular impulses: - /// - In 2D, `impulse.xy()` gives the linear impulse, and `impulse.z` the angular impulse. - /// - In 3D, `impulse.xyz()` gives the linear impulse, and `(impulse[3], impulse[4], impulse[5])` the angular impulse. - pub impulse: SpacialVector, + data: JointData, } impl FixedJoint { - /// Creates a new fixed joint from the frames of reference of both bodies. - pub fn new(local_frame1: Isometry, local_frame2: Isometry) -> Self { - Self { - local_frame1, - local_frame2, - impulse: SpacialVector::zeros(), - } + pub fn new() -> Self { + #[cfg(feature = "dim2")] + let mask = JointAxesMask::X | JointAxesMask::Y | JointAxesMask::ANG_X; + #[cfg(feature = "dim3")] + let mask = JointAxesMask::X + | JointAxesMask::Y + | JointAxesMask::Z + | JointAxesMask::ANG_X + | JointAxesMask::ANG_Y + | JointAxesMask::ANG_Z; + + let data = JointData::default().lock_axes(mask); + Self { data } } - /// Can a SIMD constraint be used for resolving this joint? - pub fn supports_simd_constraints(&self) -> bool { - true + #[must_use] + pub fn local_frame1(mut self, local_frame: Isometry) -> Self { + self.data = self.data.local_frame1(local_frame); + self + } + + #[must_use] + pub fn local_frame2(mut self, local_frame: Isometry) -> Self { + self.data = self.data.local_frame2(local_frame); + self + } + + #[must_use] + pub fn local_anchor1(mut self, anchor1: Point) -> Self { + self.data = self.data.local_anchor1(anchor1); + self + } + + #[must_use] + pub fn local_anchor2(mut self, anchor2: Point) -> Self { + self.data = self.data.local_anchor2(anchor2); + self + } +} + +impl Into for FixedJoint { + fn into(self) -> JointData { + self.data } } diff --git a/src/dynamics/joint/generic_joint.rs b/src/dynamics/joint/generic_joint.rs deleted file mode 100644 index 78f1e84..0000000 --- a/src/dynamics/joint/generic_joint.rs +++ /dev/null @@ -1,144 +0,0 @@ -use crate::dynamics::{BallJoint, FixedJoint, PrismaticJoint, RevoluteJoint}; -use crate::math::{Isometry, Real, SpacialVector}; -use crate::na::{Rotation3, UnitQuaternion}; - -#[derive(Copy, Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] -/// A joint that prevents all relative movement between two bodies. -/// -/// Given two frames of references, this joint aims to ensure these frame always coincide in world-space. -pub struct GenericJoint { - /// The frame of reference for the first body affected by this joint, expressed in the local frame - /// of the first body. - pub local_anchor1: Isometry, - /// The frame of reference for the second body affected by this joint, expressed in the local frame - /// of the first body. - pub local_anchor2: Isometry, - /// The impulse applied to the first body affected by this joint. - /// - /// The impulse applied to the second body affected by this joint is given by `-impulse`. - /// This combines both linear and angular impulses: - /// - In 2D, `impulse.xy()` gives the linear impulse, and `impulse.z` the angular impulse. - /// - In 3D, `impulse.xyz()` gives the linear impulse, and `(impulse[3], impulse[4], impulse[5])` the angular impulse. - pub impulse: SpacialVector, - - pub min_position: SpacialVector, - pub max_position: SpacialVector, - pub min_velocity: SpacialVector, - pub max_velocity: SpacialVector, - /// The minimum negative impulse the joint can apply on each DoF. Must be <= 0.0 - pub min_impulse: SpacialVector, - /// The maximum positive impulse the joint can apply on each DoF. Must be >= 0.0 - pub max_impulse: SpacialVector, - /// The minimum negative position impulse the joint can apply on each DoF. Must be <= 0.0 - pub min_pos_impulse: SpacialVector, - /// The maximum positive position impulse the joint can apply on each DoF. Must be >= 0.0 - pub max_pos_impulse: SpacialVector, -} - -impl GenericJoint { - /// Creates a new fixed joint from the frames of reference of both bodies. - pub fn new(local_anchor1: Isometry, local_anchor2: Isometry) -> Self { - Self { - local_anchor1, - local_anchor2, - impulse: SpacialVector::zeros(), - min_position: SpacialVector::zeros(), - max_position: SpacialVector::zeros(), - min_velocity: SpacialVector::zeros(), - max_velocity: SpacialVector::zeros(), - min_impulse: SpacialVector::repeat(-Real::MAX), - max_impulse: SpacialVector::repeat(Real::MAX), - min_pos_impulse: SpacialVector::repeat(-Real::MAX), - max_pos_impulse: SpacialVector::repeat(Real::MAX), - } - } - - pub fn set_dof_vel(&mut self, dof: u8, target_vel: Real, max_force: Real) { - self.min_velocity[dof as usize] = target_vel; - self.max_velocity[dof as usize] = target_vel; - self.min_impulse[dof as usize] = -max_force; - self.max_impulse[dof as usize] = max_force; - } - - pub fn free_dof(&mut self, dof: u8) { - self.min_position[dof as usize] = -Real::MAX; - self.max_position[dof as usize] = Real::MAX; - self.min_velocity[dof as usize] = -Real::MAX; - self.max_velocity[dof as usize] = Real::MAX; - self.min_impulse[dof as usize] = 0.0; - self.max_impulse[dof as usize] = 0.0; - self.min_pos_impulse[dof as usize] = 0.0; - self.max_pos_impulse[dof as usize] = 0.0; - } - - pub fn set_dof_limits(&mut self, dof: u8, min: Real, max: Real) { - self.min_position[dof as usize] = min; - self.max_position[dof as usize] = max; - } -} - -impl From for GenericJoint { - fn from(joint: RevoluteJoint) -> Self { - let basis1 = [*joint.local_axis1, joint.basis1[0], joint.basis1[1]]; - let basis2 = [*joint.local_axis2, joint.basis2[0], joint.basis2[1]]; - let quat1 = UnitQuaternion::from_basis_unchecked(&basis1); - let quat2 = UnitQuaternion::from_basis_unchecked(&basis2); - let local_anchor1 = Isometry::from_parts(joint.local_anchor1.coords.into(), quat1); - let local_anchor2 = Isometry::from_parts(joint.local_anchor2.coords.into(), quat2); - - let mut result = Self::new(local_anchor1, local_anchor2); - result.free_dof(3); - - if joint.motor_damping != 0.0 { - result.set_dof_vel(3, joint.motor_target_vel, joint.motor_max_impulse); - } - - result.impulse[0] = joint.impulse[0]; - result.impulse[1] = joint.impulse[1]; - result.impulse[2] = joint.impulse[2]; - result.impulse[3] = joint.motor_impulse; - result.impulse[4] = joint.impulse[3]; - result.impulse[5] = joint.impulse[4]; - - result - } -} - -impl From for GenericJoint { - fn from(joint: BallJoint) -> Self { - let local_anchor1 = Isometry::new(joint.local_anchor1.coords, na::zero()); - let local_anchor2 = Isometry::new(joint.local_anchor2.coords, na::zero()); - - let mut result = Self::new(local_anchor1, local_anchor2); - result.impulse[0] = joint.impulse[0]; - result.impulse[1] = joint.impulse[1]; - result.impulse[2] = joint.impulse[2]; - result.free_dof(3); - result.free_dof(4); - result.free_dof(5); - result - } -} - -impl From for GenericJoint { - fn from(joint: PrismaticJoint) -> Self { - let basis1 = [*joint.local_axis1, joint.basis1[0], joint.basis1[1]]; - let basis2 = [*joint.local_axis2, joint.basis2[0], joint.basis2[1]]; - let quat1 = UnitQuaternion::from_basis_unchecked(&basis1); - let quat2 = UnitQuaternion::from_basis_unchecked(&basis2); - let local_anchor1 = Isometry::from_parts(joint.local_anchor1.coords.into(), quat1); - let local_anchor2 = Isometry::from_parts(joint.local_anchor2.coords.into(), quat2); - - let mut result = Self::new(local_anchor1, local_anchor2); - result.free_dof(0); - result.set_dof_limits(0, joint.limits[0], joint.limits[1]); - result - } -} - -impl From for GenericJoint { - fn from(joint: FixedJoint) -> Self { - Self::new(joint.local_anchor1, joint.local_anchor2) - } -} diff --git a/src/dynamics/joint/impulse_joint/impulse_joint.rs b/src/dynamics/joint/impulse_joint/impulse_joint.rs new file mode 100644 index 0000000..a12c533 --- /dev/null +++ b/src/dynamics/joint/impulse_joint/impulse_joint.rs @@ -0,0 +1,20 @@ +use crate::dynamics::{JointData, JointHandle, RigidBodyHandle}; +use crate::math::{Real, SpacialVector}; + +#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, PartialEq)] +/// A joint attached to two bodies. +pub struct ImpulseJoint { + /// Handle to the first body attached to this joint. + pub body1: RigidBodyHandle, + /// Handle to the second body attached to this joint. + pub body2: RigidBodyHandle, + + pub data: JointData, + pub impulses: SpacialVector, + + // A joint needs to know its handle to simplify its removal. + pub(crate) handle: JointHandle, + #[cfg(feature = "parallel")] + pub(crate) constraint_index: usize, +} diff --git a/src/dynamics/joint/joint_set.rs b/src/dynamics/joint/impulse_joint/impulse_joint_set.rs similarity index 86% rename from src/dynamics/joint/joint_set.rs rename to src/dynamics/joint/impulse_joint/impulse_joint_set.rs index aba2aa8..183b668 100644 --- a/src/dynamics/joint/joint_set.rs +++ b/src/dynamics/joint/impulse_joint/impulse_joint_set.rs @@ -1,10 +1,10 @@ -use super::Joint; +use super::ImpulseJoint; use crate::geometry::{InteractionGraph, RigidBodyGraphIndex, TemporaryInteractionIndex}; use crate::data::arena::Arena; use crate::data::{BundleSet, Coarena, ComponentSet, ComponentSetMut}; use crate::dynamics::{IslandManager, RigidBodyActivation, RigidBodyIds, RigidBodyType}; -use crate::dynamics::{JointParams, RigidBodyHandle}; +use crate::dynamics::{JointData, RigidBodyHandle}; /// The unique identifier of a joint added to the joint set. /// The unique identifier of a collider added to a collider set. @@ -34,19 +34,19 @@ impl JointHandle { } pub(crate) type JointIndex = usize; -pub(crate) type JointGraphEdge = crate::data::graph::Edge; +pub(crate) type JointGraphEdge = crate::data::graph::Edge; #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] #[derive(Clone, Default)] -/// A set of joints that can be handled by a physics `World`. -pub struct JointSet { +/// A set of impulse_joints that can be handled by a physics `World`. +pub struct ImpulseJointSet { rb_graph_ids: Coarena, joint_ids: Arena, // Map joint handles to edge ids on the graph. - joint_graph: InteractionGraph, + joint_graph: InteractionGraph, } -impl JointSet { - /// Creates a new empty set of joints. +impl ImpulseJointSet { + /// Creates a new empty set of impulse_joints. pub fn new() -> Self { Self { rb_graph_ids: Coarena::new(), @@ -55,26 +55,26 @@ impl JointSet { } } - /// The number of joints on this set. + /// The number of impulse_joints on this set. pub fn len(&self) -> usize { self.joint_graph.graph.edges.len() } - /// `true` if there are no joints in this set. + /// `true` if there are no impulse_joints in this set. pub fn is_empty(&self) -> bool { self.joint_graph.graph.edges.is_empty() } - /// Retrieve the joint graph where edges are joints and nodes are rigid body handles. - pub fn joint_graph(&self) -> &InteractionGraph { + /// Retrieve the joint graph where edges are impulse_joints and nodes are rigid body handles. + pub fn joint_graph(&self) -> &InteractionGraph { &self.joint_graph } - /// Iterates through all the joitns attached to the given rigid-body. + /// Iterates through all the impulse_joints attached to the given rigid-body. pub fn joints_with<'a>( &'a self, body: RigidBodyHandle, - ) -> impl Iterator { + ) -> impl Iterator { self.rb_graph_ids .get(body.0) .into_iter() @@ -87,13 +87,13 @@ impl JointSet { } /// Gets the joint with the given handle. - pub fn get(&self, handle: JointHandle) -> Option<&Joint> { + pub fn get(&self, handle: JointHandle) -> Option<&ImpulseJoint> { let id = self.joint_ids.get(handle.0)?; self.joint_graph.graph.edge_weight(*id) } /// Gets a mutable reference to the joint with the given handle. - pub fn get_mut(&mut self, handle: JointHandle) -> Option<&mut Joint> { + pub fn get_mut(&mut self, handle: JointHandle) -> Option<&mut ImpulseJoint> { let id = self.joint_ids.get(handle.0)?; self.joint_graph.graph.edge_weight_mut(*id) } @@ -107,7 +107,7 @@ impl JointSet { /// /// Using this is discouraged in favor of `self.get(handle)` which does not /// suffer form the ABA problem. - pub fn get_unknown_gen(&self, i: u32) -> Option<(&Joint, JointHandle)> { + pub fn get_unknown_gen(&self, i: u32) -> Option<(&ImpulseJoint, JointHandle)> { let (id, handle) = self.joint_ids.get_unknown_gen(i)?; Some(( self.joint_graph.graph.edge_weight(*id)?, @@ -124,7 +124,7 @@ impl JointSet { /// /// Using this is discouraged in favor of `self.get_mut(handle)` which does not /// suffer form the ABA problem. - pub fn get_unknown_gen_mut(&mut self, i: u32) -> Option<(&mut Joint, JointHandle)> { + pub fn get_unknown_gen_mut(&mut self, i: u32) -> Option<(&mut ImpulseJoint, JointHandle)> { let (id, handle) = self.joint_ids.get_unknown_gen(i)?; Some(( self.joint_graph.graph.edge_weight_mut(*id)?, @@ -133,7 +133,7 @@ impl JointSet { } /// Iterates through all the joint on this set. - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { self.joint_graph .graph .edges @@ -142,7 +142,7 @@ impl JointSet { } /// Iterates mutably through all the joint on this set. - pub fn iter_mut(&mut self) -> impl Iterator { + pub fn iter_mut(&mut self) -> impl Iterator { self.joint_graph .graph .edges @@ -150,8 +150,8 @@ impl JointSet { .map(|e| (e.weight.handle, &mut e.weight)) } - // /// The set of joints as an array. - // pub(crate) fn joints(&self) -> &[JointGraphEdge] { + // /// The set of impulse_joints as an array. + // pub(crate) fn impulse_joints(&self) -> &[JointGraphEdge] { // // self.joint_graph // // .graph // // .edges @@ -170,25 +170,22 @@ impl JointSet { } /// Inserts a new joint into this set and retrieve its handle. - pub fn insert( + pub fn insert( &mut self, body1: RigidBodyHandle, body2: RigidBodyHandle, - joint_params: J, - ) -> JointHandle - where - J: Into, - { + data: impl Into, + ) -> JointHandle { + let data = data.into(); let handle = self.joint_ids.insert(0.into()); - let joint = Joint { + let joint = ImpulseJoint { body1, body2, + data, + impulses: na::zero(), handle: JointHandle(handle), #[cfg(feature = "parallel")] constraint_index: 0, - #[cfg(feature = "parallel")] - position_constraint_index: 0, - params: joint_params.into(), }; let default_id = InteractionGraph::<(), ()>::invalid_graph_index(); @@ -201,12 +198,12 @@ impl JointSet { // NOTE: the body won't have a graph index if it does not // have any joint attached. - if !InteractionGraph::::is_graph_index_valid(graph_index1) { + if !InteractionGraph::::is_graph_index_valid(graph_index1) { graph_index1 = self.joint_graph.graph.add_node(joint.body1); self.rb_graph_ids.insert(joint.body1.0, graph_index1); } - if !InteractionGraph::::is_graph_index_valid(graph_index2) { + if !InteractionGraph::::is_graph_index_valid(graph_index2) { graph_index2 = self.joint_graph.graph.add_node(joint.body2); self.rb_graph_ids.insert(joint.body2.0, graph_index2); } @@ -215,7 +212,7 @@ impl JointSet { JointHandle(handle) } - /// Retrieve all the joints happening between two active bodies. + /// Retrieve all the impulse_joints happening between two active bodies. // NOTE: this is very similar to the code from NarrowPhase::select_active_interactions. pub(crate) fn select_active_interactions( &self, @@ -271,7 +268,7 @@ impl JointSet { islands: &mut IslandManager, bodies: &mut Bodies, wake_up: bool, - ) -> Option + ) -> Option where Bodies: ComponentSetMut + ComponentSet @@ -299,11 +296,11 @@ impl JointSet { removed_joint } - /// Deletes all the joints attached to the given rigid-body. + /// Deletes all the impulse_joints attached to the given rigid-body. /// /// The provided rigid-body handle is not required to identify a rigid-body that /// is still contained by the `bodies` component set. - /// Returns the (now invalid) handles of the removed joints. + /// Returns the (now invalid) handles of the removed impulse_joints. pub fn remove_joints_attached_to_rigid_body( &mut self, handle: RigidBodyHandle, diff --git a/src/dynamics/joint/impulse_joint/mod.rs b/src/dynamics/joint/impulse_joint/mod.rs new file mode 100644 index 0000000..2afe078 --- /dev/null +++ b/src/dynamics/joint/impulse_joint/mod.rs @@ -0,0 +1,6 @@ +pub use self::impulse_joint::ImpulseJoint; +pub use self::impulse_joint_set::{ImpulseJointSet, JointHandle}; +pub(crate) use self::impulse_joint_set::{JointGraphEdge, JointIndex}; + +mod impulse_joint; +mod impulse_joint_set; diff --git a/src/dynamics/joint/joint.rs b/src/dynamics/joint/joint.rs deleted file mode 100644 index 0c2c864..0000000 --- a/src/dynamics/joint/joint.rs +++ /dev/null @@ -1,143 +0,0 @@ -#[cfg(feature = "dim3")] -use crate::dynamics::RevoluteJoint; -use crate::dynamics::{BallJoint, FixedJoint, JointHandle, PrismaticJoint, RigidBodyHandle}; - -#[derive(Copy, Clone, PartialEq)] -#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] -/// An enum grouping all possible types of joints. -pub enum JointParams { - /// A Ball joint that removes all relative linear degrees of freedom between the affected bodies. - BallJoint(BallJoint), - /// A fixed joint that removes all relative degrees of freedom between the affected bodies. - FixedJoint(FixedJoint), - /// A prismatic joint that removes all degrees of degrees of freedom between the affected - /// bodies except for the translation along one axis. - PrismaticJoint(PrismaticJoint), - #[cfg(feature = "dim3")] - /// A revolute joint that removes all degrees of degrees of freedom between the affected - /// bodies except for the translation along one axis. - RevoluteJoint(RevoluteJoint), - // GenericJoint(GenericJoint), -} - -impl JointParams { - /// An integer identifier for each type of joint. - pub fn type_id(&self) -> usize { - match self { - JointParams::BallJoint(_) => 0, - JointParams::FixedJoint(_) => 1, - JointParams::PrismaticJoint(_) => 2, - // JointParams::GenericJoint(_) => 3, - #[cfg(feature = "dim3")] - JointParams::RevoluteJoint(_) => 4, - } - } - - /// Gets a reference to the underlying ball joint, if `self` is one. - pub fn as_ball_joint(&self) -> Option<&BallJoint> { - if let JointParams::BallJoint(j) = self { - Some(j) - } else { - None - } - } - - /// Gets a reference to the underlying fixed joint, if `self` is one. - pub fn as_fixed_joint(&self) -> Option<&FixedJoint> { - if let JointParams::FixedJoint(j) = self { - Some(j) - } else { - None - } - } - - // /// Gets a reference to the underlying generic joint, if `self` is one. - // pub fn as_generic_joint(&self) -> Option<&GenericJoint> { - // if let JointParams::GenericJoint(j) = self { - // Some(j) - // } else { - // None - // } - // } - - /// Gets a reference to the underlying prismatic joint, if `self` is one. - pub fn as_prismatic_joint(&self) -> Option<&PrismaticJoint> { - if let JointParams::PrismaticJoint(j) = self { - Some(j) - } else { - None - } - } - - /// Gets a reference to the underlying revolute joint, if `self` is one. - #[cfg(feature = "dim3")] - pub fn as_revolute_joint(&self) -> Option<&RevoluteJoint> { - if let JointParams::RevoluteJoint(j) = self { - Some(j) - } else { - None - } - } -} - -impl From for JointParams { - fn from(j: BallJoint) -> Self { - JointParams::BallJoint(j) - } -} - -impl From for JointParams { - fn from(j: FixedJoint) -> Self { - JointParams::FixedJoint(j) - } -} - -// impl From for JointParams { -// fn from(j: GenericJoint) -> Self { -// JointParams::GenericJoint(j) -// } -// } - -#[cfg(feature = "dim3")] -impl From for JointParams { - fn from(j: RevoluteJoint) -> Self { - JointParams::RevoluteJoint(j) - } -} - -impl From for JointParams { - fn from(j: PrismaticJoint) -> Self { - JointParams::PrismaticJoint(j) - } -} - -#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] -#[derive(Clone)] -/// A joint attached to two bodies. -pub struct Joint { - /// Handle to the first body attached to this joint. - pub body1: RigidBodyHandle, - /// Handle to the second body attached to this joint. - pub body2: RigidBodyHandle, - // A joint needs to know its handle to simplify its removal. - pub(crate) handle: JointHandle, - #[cfg(feature = "parallel")] - pub(crate) constraint_index: usize, - #[cfg(feature = "parallel")] - pub(crate) position_constraint_index: usize, - /// The joint geometric parameters and impulse. - pub params: JointParams, -} - -impl Joint { - /// Can this joint use SIMD-accelerated constraint formulations? - pub fn supports_simd_constraints(&self) -> bool { - match &self.params { - JointParams::PrismaticJoint(joint) => joint.supports_simd_constraints(), - JointParams::FixedJoint(joint) => joint.supports_simd_constraints(), - JointParams::BallJoint(joint) => joint.supports_simd_constraints(), - #[cfg(feature = "dim3")] - JointParams::RevoluteJoint(joint) => joint.supports_simd_constraints(), - } - } -} diff --git a/src/dynamics/joint/joint_data.rs b/src/dynamics/joint/joint_data.rs new file mode 100644 index 0000000..35d832d --- /dev/null +++ b/src/dynamics/joint/joint_data.rs @@ -0,0 +1,270 @@ +use crate::dynamics::solver::MotorParameters; +use crate::dynamics::MotorModel; +use crate::math::{Isometry, Point, Real, Rotation, UnitVector, SPATIAL_DIM}; +use crate::utils::WBasis; + +#[cfg(feature = "dim3")] +bitflags::bitflags! { + #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] + pub struct JointAxesMask: u8 { + const FREE = 0; + const X = 1 << 0; + const Y = 1 << 1; + const Z = 1 << 2; + const ANG_X = 1 << 3; + const ANG_Y = 1 << 4; + const ANG_Z = 1 << 5; + } +} + +#[cfg(feature = "dim2")] +bitflags::bitflags! { + #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] + pub struct JointAxesMask: u8 { + const FREE = 0; + const X = 1 << 0; + const Y = 1 << 1; + const ANG_X = 1 << 2; + } +} + +#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum JointAxis { + X = 0, + Y, + #[cfg(feature = "dim3")] + Z, + AngX, + #[cfg(feature = "dim3")] + AngY, + #[cfg(feature = "dim3")] + AngZ, +} + +impl From for JointAxesMask { + fn from(axis: JointAxis) -> Self { + JointAxesMask::from_bits(1 << axis as usize).unwrap() + } +} + +#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct JointLimits { + pub min: Real, + pub max: Real, + pub impulse: Real, +} + +impl Default for JointLimits { + fn default() -> Self { + Self { + min: -Real::MAX, + max: Real::MAX, + impulse: 0.0, + } + } +} + +#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct JointMotor { + pub target_vel: Real, + pub target_pos: Real, + pub stiffness: Real, + pub damping: Real, + pub max_impulse: Real, + pub impulse: Real, + pub model: MotorModel, +} + +impl Default for JointMotor { + fn default() -> Self { + Self { + target_pos: 0.0, + target_vel: 0.0, + stiffness: 0.0, + damping: 0.0, + max_impulse: Real::MAX, + impulse: 0.0, + model: MotorModel::VelocityBased, + } + } +} + +impl JointMotor { + pub(crate) fn motor_params(&self, dt: Real) -> MotorParameters { + let (stiffness, damping, gamma, _keep_lhs) = + self.model + .combine_coefficients(dt, self.stiffness, self.damping); + MotorParameters { + stiffness, + damping, + gamma, + // keep_lhs, + target_pos: self.target_pos, + target_vel: self.target_vel, + max_impulse: self.max_impulse, + } + } +} + +#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct JointData { + pub local_frame1: Isometry, + pub local_frame2: Isometry, + pub locked_axes: JointAxesMask, + pub limit_axes: JointAxesMask, + pub motor_axes: JointAxesMask, + pub limits: [JointLimits; SPATIAL_DIM], + pub motors: [JointMotor; SPATIAL_DIM], +} + +impl Default for JointData { + fn default() -> Self { + Self { + local_frame1: Isometry::identity(), + local_frame2: Isometry::identity(), + locked_axes: JointAxesMask::FREE, + limit_axes: JointAxesMask::FREE, + motor_axes: JointAxesMask::FREE, + limits: [JointLimits::default(); SPATIAL_DIM], + motors: [JointMotor::default(); SPATIAL_DIM], + } + } +} + +impl JointData { + #[must_use] + pub fn new(locked_axes: JointAxesMask) -> Self { + Self::default().lock_axes(locked_axes) + } + + /// Can this joint use SIMD-accelerated constraint formulations? + pub fn supports_simd_constraints(&self) -> bool { + self.limit_axes.is_empty() && self.motor_axes.is_empty() + } + + #[must_use] + pub fn lock_axes(mut self, axes: JointAxesMask) -> Self { + self.locked_axes |= axes; + self + } + + fn complete_ang_frame(axis: UnitVector) -> Rotation { + let basis = axis.orthonormal_basis(); + + #[cfg(feature = "dim2")] + { + use na::{Matrix2, Rotation2, UnitComplex}; + let mat = Matrix2::from_columns(&[axis.into_inner(), basis[0]]); + let rotmat = Rotation2::from_matrix_unchecked(mat); + UnitComplex::from_rotation_matrix(&rotmat) + } + + #[cfg(feature = "dim3")] + { + use na::{Matrix3, Rotation3, UnitQuaternion}; + let mat = Matrix3::from_columns(&[axis.into_inner(), basis[0], basis[1]]); + let rotmat = Rotation3::from_matrix_unchecked(mat); + UnitQuaternion::from_rotation_matrix(&rotmat) + } + } + + #[must_use] + pub fn local_frame1(mut self, local_frame: Isometry) -> Self { + self.local_frame1 = local_frame; + self + } + + #[must_use] + pub fn local_frame2(mut self, local_frame: Isometry) -> Self { + self.local_frame2 = local_frame; + self + } + + #[must_use] + pub fn local_axis1(mut self, local_axis: UnitVector) -> Self { + self.local_frame1.rotation = Self::complete_ang_frame(local_axis); + self + } + + #[must_use] + pub fn local_axis2(mut self, local_axis: UnitVector) -> Self { + self.local_frame2.rotation = Self::complete_ang_frame(local_axis); + self + } + + #[must_use] + pub fn local_anchor1(mut self, anchor1: Point) -> Self { + self.local_frame1.translation.vector = anchor1.coords; + self + } + + #[must_use] + pub fn local_anchor2(mut self, anchor2: Point) -> Self { + self.local_frame2.translation.vector = anchor2.coords; + self + } + + #[must_use] + pub fn limit_axis(mut self, axis: JointAxis, limits: [Real; 2]) -> Self { + let i = axis as usize; + self.limit_axes |= axis.into(); + self.limits[i].min = limits[0]; + self.limits[i].max = limits[1]; + self + } + + /// Set the spring-like model used by the motor to reach the desired target velocity and position. + pub fn motor_model(mut self, axis: JointAxis, model: MotorModel) -> Self { + self.motors[axis as usize].model = model; + self + } + + /// Sets the target velocity this motor needs to reach. + pub fn motor_velocity(self, axis: JointAxis, target_vel: Real, factor: Real) -> Self { + self.motor_axis( + axis, + self.motors[axis as usize].target_pos, + target_vel, + 0.0, + factor, + ) + } + + /// Sets the target angle this motor needs to reach. + pub fn motor_position( + self, + axis: JointAxis, + target_pos: Real, + stiffness: Real, + damping: Real, + ) -> Self { + self.motor_axis(axis, target_pos, 0.0, stiffness, damping) + } + + /// Configure both the target angle and target velocity of the motor. + pub fn motor_axis( + mut self, + axis: JointAxis, + target_pos: Real, + target_vel: Real, + stiffness: Real, + damping: Real, + ) -> Self { + self.motor_axes |= axis.into(); + let i = axis as usize; + self.motors[i].target_vel = target_vel; + self.motors[i].target_pos = target_pos; + self.motors[i].stiffness = stiffness; + self.motors[i].damping = damping; + self + } + + pub fn motor_max_impulse(mut self, axis: JointAxis, max_impulse: Real) -> Self { + self.motors[axis as usize].max_impulse = max_impulse; + self + } +} diff --git a/src/dynamics/joint/mod.rs b/src/dynamics/joint/mod.rs index 72a7483..6594b83 100644 --- a/src/dynamics/joint/mod.rs +++ b/src/dynamics/joint/mod.rs @@ -1,20 +1,21 @@ -pub use self::ball_joint::BallJoint; pub use self::fixed_joint::FixedJoint; -// pub use self::generic_joint::GenericJoint; -pub use self::joint::{Joint, JointParams}; -pub(crate) use self::joint_set::{JointGraphEdge, JointIndex}; -pub use self::joint_set::{JointHandle, JointSet}; +pub use self::impulse_joint::*; +pub use self::joint_data::*; +pub use self::motor_model::MotorModel; +pub use self::multibody_joint::*; pub use self::prismatic_joint::PrismaticJoint; -#[cfg(feature = "dim3")] pub use self::revolute_joint::RevoluteJoint; -pub use self::spring_model::SpringModel; -mod ball_joint; -mod fixed_joint; -// mod generic_joint; -mod joint; -mod joint_set; -mod prismatic_joint; #[cfg(feature = "dim3")] +pub use self::spherical_joint::SphericalJoint; + +mod fixed_joint; +mod impulse_joint; +mod joint_data; +mod motor_model; +mod multibody_joint; +mod prismatic_joint; mod revolute_joint; -mod spring_model; + +#[cfg(feature = "dim3")] +mod spherical_joint; diff --git a/src/dynamics/joint/spring_model.rs b/src/dynamics/joint/motor_model.rs similarity index 66% rename from src/dynamics/joint/spring_model.rs rename to src/dynamics/joint/motor_model.rs index c2c9ebd..e5a6549 100644 --- a/src/dynamics/joint/spring_model.rs +++ b/src/dynamics/joint/motor_model.rs @@ -3,9 +3,7 @@ use crate::math::Real; /// The spring-like model used for constraints resolution. #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] -pub enum SpringModel { - /// No equation is solved. - Disabled, +pub enum MotorModel { /// The solved spring-like equation is: /// `delta_velocity(t + dt) = stiffness / dt * (target_pos - pos(t)) + damping * (target_vel - vel(t))` /// @@ -16,21 +14,21 @@ pub enum SpringModel { /// The solved spring-like equation is: /// `acceleration(t + dt) = stiffness * (target_pos - pos(t)) + damping * (target_vel - vel(t))` AccelerationBased, - /// The solved spring-like equation is: - /// `force(t + dt) = stiffness * (target_pos - pos(t + dt)) + damping * (target_vel - vel(t + dt))` - ForceBased, + // /// The solved spring-like equation is: + // /// `force(t + dt) = stiffness * (target_pos - pos(t + dt)) + damping * (target_vel - vel(t + dt))` + // ForceBased, } -impl Default for SpringModel { +impl Default for MotorModel { fn default() -> Self { - SpringModel::VelocityBased + MotorModel::VelocityBased } } -impl SpringModel { +impl MotorModel { /// Combines the coefficients used for solving the spring equation. /// - /// Returns the new coefficients (stiffness, damping, inv_lhs_scale, keep_inv_lhs) + /// Returns the new coefficients (stiffness, damping, gamma, keep_inv_lhs) /// coefficients for the equivalent impulse-based equation. These new /// coefficients must be used in the following way: /// - `rhs = (stiffness * pos_err + damping * vel_err) / gamma`. @@ -43,8 +41,8 @@ impl SpringModel { damping: Real, ) -> (Real, Real, Real, bool) { match self { - SpringModel::VelocityBased => (stiffness * crate::utils::inv(dt), damping, 1.0, true), - SpringModel::AccelerationBased => { + MotorModel::VelocityBased => (stiffness * crate::utils::inv(dt), damping, 1.0, true), + MotorModel::AccelerationBased => { let effective_stiffness = stiffness * dt; let effective_damping = damping * dt; // TODO: Using gamma behaves very badly for some reasons. @@ -52,14 +50,12 @@ impl SpringModel { // and get back to this later. // let gamma = effective_stiffness * dt + effective_damping; (effective_stiffness, effective_damping, 1.0, true) - } - SpringModel::ForceBased => { - let effective_stiffness = stiffness * dt; - let effective_damping = damping * dt; - let gamma = effective_stiffness * dt + effective_damping; - (effective_stiffness, effective_damping, gamma, false) - } - SpringModel::Disabled => return (0.0, 0.0, 0.0, false), + } // MotorModel::ForceBased => { + // let effective_stiffness = stiffness * dt; + // let effective_damping = damping * dt; + // let gamma = effective_stiffness * dt + effective_damping; + // (effective_stiffness, effective_damping, gamma, false) + // } } } } diff --git a/src/dynamics/joint/multibody_joint/mod.rs b/src/dynamics/joint/multibody_joint/mod.rs new file mode 100644 index 0000000..a701350 --- /dev/null +++ b/src/dynamics/joint/multibody_joint/mod.rs @@ -0,0 +1,15 @@ +//! MultibodyJoints using the reduced-coordinates formalism or using constraints. + +pub use self::multibody::Multibody; +pub use self::multibody_joint::MultibodyJoint; +pub use self::multibody_joint_set::{MultibodyIndex, MultibodyJointHandle, MultibodyJointSet}; +pub use self::multibody_link::MultibodyLink; +pub use self::unit_multibody_joint::{unit_joint_limit_constraint, unit_joint_motor_constraint}; + +mod multibody; +mod multibody_joint_set; +mod multibody_link; +mod multibody_workspace; + +mod multibody_joint; +mod unit_multibody_joint; diff --git a/src/dynamics/joint/multibody_joint/multibody.rs b/src/dynamics/joint/multibody_joint/multibody.rs new file mode 100644 index 0000000..70043d1 --- /dev/null +++ b/src/dynamics/joint/multibody_joint/multibody.rs @@ -0,0 +1,1021 @@ +use super::multibody_link::{MultibodyLink, MultibodyLinkVec}; +use super::multibody_workspace::MultibodyWorkspace; +use crate::data::{BundleSet, ComponentSet, ComponentSetMut}; +use crate::dynamics::solver::AnyJointVelocityConstraint; +use crate::dynamics::{ + IntegrationParameters, RigidBodyForces, RigidBodyHandle, RigidBodyMassProps, RigidBodyPosition, + RigidBodyType, RigidBodyVelocity, +}; +#[cfg(feature = "dim3")] +use crate::math::Matrix; +use crate::math::{ + AngDim, AngVector, Dim, Isometry, Jacobian, Real, Vector, ANG_DIM, DIM, SPATIAL_DIM, +}; +use crate::prelude::MultibodyJoint; +use crate::utils::{IndexMut2, WAngularInertia, WCross, WCrossMatrix}; +use na::{ + self, DMatrix, DVector, DVectorSlice, DVectorSliceMut, Dynamic, OMatrix, SMatrix, SVector, LU, +}; + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct Force { + linear: Vector, + angular: AngVector, +} + +impl Force { + fn new(linear: Vector, angular: AngVector) -> Self { + Self { linear, angular } + } + + fn as_vector(&self) -> &SVector { + unsafe { std::mem::transmute(self) } + } +} + +#[cfg(feature = "dim2")] +fn concat_rb_mass_matrix(mass: Real, inertia: Real) -> SMatrix { + let mut result = SMatrix::::zeros(); + result[(0, 0)] = mass; + result[(1, 1)] = mass; + result[(2, 2)] = inertia; + result +} + +#[cfg(feature = "dim3")] +fn concat_rb_mass_matrix( + mass: Real, + inertia: Matrix, +) -> SMatrix { + let mut result = SMatrix::::zeros(); + result[(0, 0)] = mass; + result[(1, 1)] = mass; + result[(2, 2)] = mass; + result + .fixed_slice_mut::(DIM, DIM) + .copy_from(&inertia); + result +} + +/// An articulated body simulated using the reduced-coordinates approach. +pub struct Multibody { + links: MultibodyLinkVec, + pub(crate) velocities: DVector, + pub(crate) damping: DVector, + pub(crate) accelerations: DVector, + + body_jacobians: Vec>, + // FIXME: use sparse matrices. + augmented_mass: DMatrix, + inv_augmented_mass: LU, + + acc_augmented_mass: DMatrix, + acc_inv_augmented_mass: LU, + + ndofs: usize, + pub(crate) root_is_dynamic: bool, + pub(crate) solver_id: usize, + + /* + * Workspaces. + */ + workspace: MultibodyWorkspace, + coriolis_v: Vec>, + coriolis_w: Vec>, + i_coriolis_dt: Jacobian, +} + +impl Multibody { + /// Creates a new multibody with no link. + pub fn new() -> Self { + Multibody { + links: MultibodyLinkVec(Vec::new()), + velocities: DVector::zeros(0), + damping: DVector::zeros(0), + accelerations: DVector::zeros(0), + body_jacobians: Vec::new(), + augmented_mass: DMatrix::zeros(0, 0), + inv_augmented_mass: LU::new(DMatrix::zeros(0, 0)), + acc_augmented_mass: DMatrix::zeros(0, 0), + acc_inv_augmented_mass: LU::new(DMatrix::zeros(0, 0)), + ndofs: 0, + solver_id: 0, + workspace: MultibodyWorkspace::new(), + coriolis_v: Vec::new(), + coriolis_w: Vec::new(), + i_coriolis_dt: Jacobian::zeros(0), + root_is_dynamic: false, + // solver_workspace: Some(SolverWorkspace::new()), + } + } + + pub fn with_root(handle: RigidBodyHandle) -> Self { + let mut mb = Multibody::new(); + mb.root_is_dynamic = true; + let joint = MultibodyJoint::free(Isometry::identity()); + mb.add_link(None, joint, handle); + mb + } + + pub fn remove_link(self, to_remove: usize, joint_only: bool) -> Vec { + let mut result = vec![]; + let mut link2mb = vec![usize::MAX; self.links.len()]; + let mut link_id2new_id = vec![usize::MAX; self.links.len()]; + + for (i, mut link) in self.links.0.into_iter().enumerate() { + let is_new_root = (!joint_only && (i == 0 || link.parent_internal_id == to_remove)) + || (joint_only && (i == 0 || i == to_remove)); + + if !joint_only && i == to_remove { + continue; + } else if is_new_root { + link2mb[i] = result.len(); + result.push(Multibody::new()); + } else { + link2mb[i] = link2mb[link.parent_internal_id] + } + + let curr_mb = &mut result[link2mb[i]]; + link_id2new_id[i] = curr_mb.links.len(); + + if is_new_root { + let joint = MultibodyJoint::fixed(*link.local_to_world()); + link.state.joint = joint; + } + + curr_mb.ndofs += link.joint().ndofs(); + curr_mb.links.push(link); + } + + // Adjust all the internal ids, and copy the data from the + // previous multibody to the new one. + for mb in &mut result { + mb.grow_buffers(mb.ndofs, mb.links.len()); + mb.workspace.resize(mb.links.len(), mb.ndofs); + + let mut assembly_id = 0; + for (i, link) in mb.links.iter_mut().enumerate() { + let link_ndofs = link.joint().ndofs(); + mb.velocities + .rows_mut(assembly_id, link_ndofs) + .copy_from(&self.velocities.rows(link.assembly_id, link_ndofs)); + mb.damping + .rows_mut(assembly_id, link_ndofs) + .copy_from(&self.damping.rows(link.assembly_id, link_ndofs)); + mb.accelerations + .rows_mut(assembly_id, link_ndofs) + .copy_from(&self.accelerations.rows(link.assembly_id, link_ndofs)); + + link.internal_id = i; + link.assembly_id = assembly_id; + link.parent_internal_id = link_id2new_id[link.parent_internal_id]; + assembly_id += link_ndofs; + } + } + + result + } + + pub fn append(&mut self, mut rhs: Multibody, parent: usize, joint: MultibodyJoint) { + let rhs_root_ndofs = rhs.links[0].state.joint.ndofs(); + let rhs_copy_shift = self.ndofs + rhs_root_ndofs; + let rhs_copy_ndofs = rhs.ndofs - rhs_root_ndofs; + + // Adjust the ids of all the rhs links except the first one. + let base_assembly_id = self.velocities.len() - rhs_root_ndofs + joint.ndofs(); + let base_internal_id = self.links.len() + 1; + let base_parent_id = self.links.len(); + + for link in &mut rhs.links.0[1..] { + link.assembly_id += base_assembly_id; + link.internal_id += base_internal_id; + link.parent_internal_id += base_parent_id; + } + + // Adjust the first link. + { + rhs.links[0].state.joint = joint; + rhs.links[0].assembly_id = self.velocities.len(); + rhs.links[0].internal_id = self.links.len(); + rhs.links[0].parent_internal_id = parent; + } + + // Grow buffers and append data from rhs. + self.grow_buffers( + rhs_copy_ndofs + rhs.links[0].state.joint.ndofs(), + rhs.links.len(), + ); + + if rhs_copy_ndofs > 0 { + self.velocities + .rows_mut(rhs_copy_shift, rhs_copy_ndofs) + .copy_from(&rhs.velocities.rows(rhs_root_ndofs, rhs_copy_ndofs)); + self.damping + .rows_mut(rhs_copy_shift, rhs_copy_ndofs) + .copy_from(&rhs.damping.rows(rhs_root_ndofs, rhs_copy_ndofs)); + self.accelerations + .rows_mut(rhs_copy_shift, rhs_copy_ndofs) + .copy_from(&rhs.accelerations.rows(rhs_root_ndofs, rhs_copy_ndofs)); + } + + rhs.links[0] + .state + .joint + .default_damping(&mut self.damping.rows_mut(base_assembly_id, rhs_root_ndofs)); + + self.links.append(&mut rhs.links); + self.ndofs = self.velocities.len(); + self.workspace.resize(self.links.len(), self.ndofs); + } + + pub fn inv_augmented_mass(&self) -> &LU { + &self.inv_augmented_mass + } + + /// The first link of this multibody. + #[inline] + pub fn root(&self) -> &MultibodyLink { + &self.links[0] + } + + /// Mutable reference to the first link of this multibody. + #[inline] + pub fn root_mut(&mut self) -> &mut MultibodyLink { + &mut self.links[0] + } + + /// Reference `i`-th multibody link of this multibody. + /// + /// Return `None` if there is less than `i + 1` multibody links. + #[inline] + pub fn link(&self, id: usize) -> Option<&MultibodyLink> { + self.links.get(id) + } + + /// Mutable reference to the multibody link with the given id. + /// + /// Return `None` if the given id does not identifies a multibody link part of `self`. + #[inline] + pub fn link_mut(&mut self, id: usize) -> Option<&mut MultibodyLink> { + self.links.get_mut(id) + } + + /// The links of this multibody with the given `name`. + pub fn links_with_name<'a>( + &'a self, + name: &'a str, + ) -> impl Iterator { + self.links + .iter() + .enumerate() + .filter(move |(_i, l)| l.name == name) + } + + /// The number of links on this multibody. + pub fn num_links(&self) -> usize { + self.links.len() + } + + /// Iterator through all the links of this multibody. + /// + /// All link are guaranteed to be yielded before its descendant. + pub fn links(&self) -> impl Iterator { + self.links.iter() + } + + /// Mutable iterator through all the links of this multibody. + /// + /// All link are guaranteed to be yielded before its descendant. + pub fn links_mut(&mut self) -> impl Iterator { + self.links.iter_mut() + } + + /// The vector of damping applied to this multibody. + #[inline] + pub fn damping(&self) -> &DVector { + &self.damping + } + + /// Mutable vector of damping applied to this multibody. + #[inline] + pub fn damping_mut(&mut self) -> &mut DVector { + &mut self.damping + } + + pub fn add_link( + &mut self, + parent: Option, // FIXME: should be a RigidBodyHandle? + mut dof: MultibodyJoint, + body: RigidBodyHandle, + ) -> &mut MultibodyLink { + assert!( + parent.is_none() || !self.links.is_empty(), + "Multibody::build_body: invalid parent id." + ); + + /* + * Compute the indices. + */ + let assembly_id = self.velocities.len(); + let internal_id = self.links.len(); + + /* + * Grow the buffers. + */ + let ndofs = dof.ndofs(); + self.grow_buffers(ndofs, 1); + self.ndofs += ndofs; + + /* + * Setup default damping. + */ + dof.default_damping(&mut self.damping.rows_mut(assembly_id, ndofs)); + + /* + * Create the multibody. + */ + dof.update_jacobians(&self.velocities.as_slice()[assembly_id..]); + let local_to_parent = dof.body_to_parent(); + let local_to_world; + let parent_to_world; + + let parent_internal_id; + if let Some(parent) = parent { + parent_internal_id = parent; + let parent_link = &mut self.links[parent_internal_id]; + parent_link.is_leaf = false; + parent_to_world = parent_link.state.local_to_world; + local_to_world = parent_link.state.local_to_world * local_to_parent; + } else { + parent_internal_id = 0; + parent_to_world = Isometry::identity(); + local_to_world = local_to_parent; + } + + let rb = MultibodyLink::new( + body, + internal_id, + assembly_id, + parent_internal_id, + dof, + parent_to_world, + local_to_world, + local_to_parent, + ); + + self.links.push(rb); + self.workspace.resize(self.links.len(), self.ndofs); + + &mut self.links[internal_id] + } + + fn grow_buffers(&mut self, ndofs: usize, num_jacobians: usize) { + let len = self.velocities.len(); + self.velocities.resize_vertically_mut(len + ndofs, 0.0); + self.damping.resize_vertically_mut(len + ndofs, 0.0); + self.accelerations.resize_vertically_mut(len + ndofs, 0.0); + self.body_jacobians + .extend((0..num_jacobians).map(|_| Jacobian::zeros(0))); + } + + pub fn update_acceleration(&mut self, bodies: &Bodies) + where + Bodies: ComponentSet + + ComponentSet + + ComponentSet, + { + if self.ndofs == 0 { + return; // Nothing to do. + } + + self.accelerations.fill(0.0); + + for i in 0..self.links.len() { + let link = &self.links[i]; + + let (rb_vels, rb_mprops, rb_forces): ( + &RigidBodyVelocity, + &RigidBodyMassProps, + &RigidBodyForces, + ) = bodies.index_bundle(link.rigid_body.0); + + let mut acc = link.velocity_dot_wrt_joint; + + if i != 0 { + let parent_id = link.parent_internal_id; + let parent_link = &self.links[parent_id]; + + let (parent_rb_vels, parent_rb_mprops): (&RigidBodyVelocity, &RigidBodyMassProps) = + bodies.index_bundle(parent_link.rigid_body.0); + + acc += self.workspace.accs[parent_id]; + acc.linvel += parent_rb_vels.angvel.gcross(link.velocity_wrt_joint.linvel); + #[cfg(feature = "dim3")] + { + acc.angvel += parent_rb_vels.angvel.cross(&link.velocity_wrt_joint.angvel); + } + + let shift = rb_mprops.world_com - parent_rb_mprops.world_com; + let dvel = rb_vels.linvel - parent_rb_vels.linvel; + + acc.linvel += parent_rb_vels.angvel.gcross(dvel); + acc.linvel += self.workspace.accs[parent_id].angvel.gcross(shift); + } + + self.workspace.accs[i] = acc; + + // TODO: should gyroscopic forces already be computed by the rigid-body itself + // (at the same time that we add the gravity force)? + let gyroscopic; + let rb_inertia = rb_mprops.effective_angular_inertia(); + let rb_mass = rb_mprops.effective_mass(); + + #[cfg(feature = "dim3")] + { + gyroscopic = rb_vels.angvel.cross(&(rb_inertia * rb_vels.angvel)); + } + #[cfg(feature = "dim2")] + { + gyroscopic = 0.0; + } + + let external_forces = Force::new( + rb_forces.force - rb_mass * acc.linvel, + rb_forces.torque - gyroscopic - rb_inertia * acc.angvel, + ); + self.accelerations.gemv_tr( + 1.0, + &self.body_jacobians[i], + external_forces.as_vector(), + 1.0, + ); + } + + self.accelerations + .cmpy(-1.0, &self.damping, &self.velocities, 1.0); + + self.acc_inv_augmented_mass + .solve_mut(&mut self.accelerations); + } + + /// Computes the constant terms of the dynamics. + pub fn update_dynamics(&mut self, dt: Real, bodies: &mut Bodies) + where + Bodies: ComponentSetMut + ComponentSet, + { + /* + * Compute velocities. + * NOTE: this is needed for kinematic bodies too. + */ + let link = &mut self.links[0]; + let velocity_wrt_joint = link + .state + .joint + .jacobian_mul_coordinates(&self.velocities.as_slice()[link.assembly_id..]); + let velocity_dot_wrt_joint = link + .state + .joint + .jacobian_dot_mul_coordinates(&self.velocities.as_slice()[link.assembly_id..]); + + link.velocity_dot_wrt_joint = velocity_dot_wrt_joint; + link.velocity_wrt_joint = velocity_wrt_joint; + bodies.set_internal(link.rigid_body.0, link.velocity_wrt_joint); + + for i in 1..self.links.len() { + let (link, parent_link) = self.links.get_mut_with_parent(i); + let rb_mprops: &RigidBodyMassProps = bodies.index(link.rigid_body.0); + let (parent_rb_vels, parent_rb_mprops): (&RigidBodyVelocity, &RigidBodyMassProps) = + bodies.index_bundle(parent_link.rigid_body.0); + + let velocity_wrt_joint = link + .state + .joint + .jacobian_mul_coordinates(&self.velocities.as_slice()[link.assembly_id..]); + let velocity_dot_wrt_joint = link + .state + .joint + .jacobian_dot_mul_coordinates(&self.velocities.as_slice()[link.assembly_id..]); + + link.velocity_dot_wrt_joint = + velocity_dot_wrt_joint.transformed(&parent_link.state.local_to_world); + link.velocity_wrt_joint = + velocity_wrt_joint.transformed(&parent_link.state.local_to_world); + let mut new_rb_vels = *parent_rb_vels + link.velocity_wrt_joint; + let shift = rb_mprops.world_com - parent_rb_mprops.world_com; + new_rb_vels.linvel += parent_rb_vels.angvel.gcross(shift); + + bodies.set_internal(link.rigid_body.0, new_rb_vels); + } + + /* + * Update augmented mass matrix. + */ + self.update_inertias(dt, bodies); + } + + fn update_body_next_jacobians(&mut self, bodies: &Bodies) + where + Bodies: ComponentSet, + { + for i in 0..self.links.len() { + let link = &self.links[i]; + let rb_mprops = bodies.index(link.rigid_body.0); + + if self.body_jacobians[i].ncols() != self.ndofs { + // FIXME: use a resize instead. + self.body_jacobians[i] = Jacobian::zeros(self.ndofs); + } + + if i != 0 { + let parent_id = link.parent_internal_id; + let parent_link = &self.links[parent_id]; + let parent_rb_mprops = bodies.index(parent_link.rigid_body.0); + + let (link_j, parent_j) = self.body_jacobians.index_mut_const(i, parent_id); + link_j.copy_from(&parent_j); + + { + let mut link_j_v = link_j.fixed_rows_mut::(0); + let parent_j_w = parent_j.fixed_rows::(DIM); + + let shift_tr = (link.state.local_to_world * rb_mprops.local_mprops.local_com + - parent_link.state.local_to_world + * parent_rb_mprops.local_mprops.local_com) + .gcross_matrix_tr(); + link_j_v.gemm(1.0, &shift_tr, &parent_j_w, 1.0); + } + } else { + self.body_jacobians[i].fill(0.0); + } + + let ndofs = link.state.joint.ndofs(); + let mut tmp = SMatrix::::zeros(); + let mut link_joint_j = tmp.columns_mut(0, ndofs); + let mut link_j_part = self.body_jacobians[i].columns_mut(link.assembly_id, ndofs); + link.state + .joint + .jacobian(&link.state.parent_to_world, &mut link_joint_j); + + link_j_part += link_joint_j; + } + } + + fn update_inertias(&mut self, dt: Real, bodies: &Bodies) + where + Bodies: ComponentSet + ComponentSet, + { + if self.ndofs == 0 { + return; // Nothing to do. + } + + if self.augmented_mass.ncols() != self.ndofs { + // TODO: do a resize instead of a full reallocation. + self.augmented_mass = DMatrix::zeros(self.ndofs, self.ndofs); + self.acc_augmented_mass = DMatrix::zeros(self.ndofs, self.ndofs); + } else { + self.augmented_mass.fill(0.0); + self.acc_augmented_mass.fill(0.0); + } + + if self.coriolis_v.len() != self.links.len() { + self.coriolis_v.resize( + self.links.len(), + OMatrix::::zeros(self.ndofs), + ); + self.coriolis_w.resize( + self.links.len(), + OMatrix::::zeros(self.ndofs), + ); + self.i_coriolis_dt = Jacobian::zeros(self.ndofs); + } + + for i in 0..self.links.len() { + let link = &self.links[i]; + let (rb_vels, rb_mprops): (&RigidBodyVelocity, &RigidBodyMassProps) = + bodies.index_bundle(link.rigid_body.0); + let rb_mass = rb_mprops.effective_mass(); + let rb_inertia = rb_mprops.effective_angular_inertia().into_matrix(); + + let body_jacobian = &self.body_jacobians[i]; + + #[allow(unused_mut)] // mut is needed for 3D but not for 2D. + let mut augmented_inertia = rb_inertia; + + #[cfg(feature = "dim3")] + { + // Derivative of gyroscopic forces. + let gyroscopic_matrix = rb_vels.angvel.gcross_matrix() * rb_inertia + - (rb_inertia * rb_vels.angvel).gcross_matrix(); + + augmented_inertia += gyroscopic_matrix * dt; + } + + // TODO: optimize that (knowing the structure of the augmented inertia matrix). + // TODO: this could be better optimized in 2D. + let rb_mass_matrix_wo_gyro = concat_rb_mass_matrix(rb_mass, rb_inertia); + let rb_mass_matrix = concat_rb_mass_matrix(rb_mass, augmented_inertia); + self.augmented_mass + .quadform(1.0, &rb_mass_matrix_wo_gyro, body_jacobian, 1.0); + self.acc_augmented_mass + .quadform(1.0, &rb_mass_matrix, body_jacobian, 1.0); + + /* + * + * Coriolis matrix. + * + */ + let rb_j = &self.body_jacobians[i]; + let rb_j_v = rb_j.fixed_rows::(0); + + let ndofs = link.state.joint.ndofs(); + + if i != 0 { + let parent_id = link.parent_internal_id; + let parent_link = &self.links[parent_id]; + let (parent_rb_vels, parent_rb_mprops): (&RigidBodyVelocity, &RigidBodyMassProps) = + bodies.index_bundle(parent_link.rigid_body.0); + let parent_j = &self.body_jacobians[parent_id]; + let parent_j_v = parent_j.fixed_rows::(0); + let parent_j_w = parent_j.fixed_rows::(DIM); + let parent_w = parent_rb_vels.angvel.gcross_matrix(); + + let (coriolis_v, parent_coriolis_v) = self.coriolis_v.index_mut2(i, parent_id); + let (coriolis_w, parent_coriolis_w) = self.coriolis_w.index_mut2(i, parent_id); + + // JDot + JDot/u * qdot + coriolis_v.copy_from(&parent_coriolis_v); + coriolis_w.copy_from(&parent_coriolis_w); + + let shift_cross = + (rb_mprops.world_com - parent_rb_mprops.world_com).gcross_matrix_tr(); + coriolis_v.gemm(1.0, &shift_cross, &parent_coriolis_w, 1.0); + + // JDot + let dvel_cross = (rb_vels.linvel - parent_rb_vels.linvel).gcross_matrix_tr(); + coriolis_v.gemm(1.0, &dvel_cross, &parent_j_w, 1.0); + + // JDot/u * qdot + coriolis_v.gemm( + 1.0, + &link.velocity_wrt_joint.linvel.gcross_matrix_tr(), + &parent_j_w, + 1.0, + ); + coriolis_v.gemm(1.0, &parent_w, &rb_j_v, 1.0); + coriolis_v.gemm(-1.0, &parent_w, &parent_j_v, 1.0); + + #[cfg(feature = "dim3")] + { + let vel_wrt_joint_w = link.velocity_wrt_joint.angvel.gcross_matrix(); + coriolis_w.gemm(-1.0, &vel_wrt_joint_w, &parent_j_w, 1.0); + } + + // JDot + { + let mut coriolis_v_part = coriolis_v.columns_mut(link.assembly_id, ndofs); + + let mut tmp1 = SMatrix::::zeros(); + let mut rb_joint_j = tmp1.columns_mut(0, ndofs); + link.state + .joint + .jacobian(&parent_link.state.local_to_world, &mut rb_joint_j); + + let rb_joint_j_v = rb_joint_j.fixed_rows::(0); + + coriolis_v_part.gemm(1.0, &parent_w, &rb_joint_j_v, 1.0); + + #[cfg(feature = "dim3")] + { + let rb_joint_j_w = rb_joint_j.fixed_rows::(DIM); + let mut coriolis_w_part = coriolis_w.columns_mut(link.assembly_id, ndofs); + coriolis_w_part.gemm(1.0, &parent_w, &rb_joint_j_w, 1.0); + } + } + } else { + self.coriolis_v[i].fill(0.0); + self.coriolis_w[i].fill(0.0); + } + + let coriolis_v = &mut self.coriolis_v[i]; + let coriolis_w = &mut self.coriolis_w[i]; + + { + let mut tmp1 = SMatrix::::zeros(); + let mut tmp2 = SMatrix::::zeros(); + let mut rb_joint_j_dot = tmp1.columns_mut(0, ndofs); + let mut rb_joint_j_dot_veldiff = tmp2.columns_mut(0, ndofs); + + link.state + .joint + .jacobian_dot(&link.state.parent_to_world, &mut rb_joint_j_dot); + link.state.joint.jacobian_dot_veldiff_mul_coordinates( + &link.state.parent_to_world, + &self.velocities.as_slice()[link.assembly_id..], + &mut rb_joint_j_dot_veldiff, + ); + + let rb_joint_j_v_dot = rb_joint_j_dot.fixed_rows::(0); + let rb_joint_j_w_dot = rb_joint_j_dot.fixed_rows::(DIM); + let rb_joint_j_v_dot_veldiff = rb_joint_j_dot_veldiff.fixed_rows::(0); + let rb_joint_j_w_dot_veldiff = rb_joint_j_dot_veldiff.fixed_rows::(DIM); + + let mut coriolis_v_part = coriolis_v.columns_mut(link.assembly_id, ndofs); + let mut coriolis_w_part = coriolis_w.columns_mut(link.assembly_id, ndofs); + + // JDot + coriolis_v_part += rb_joint_j_v_dot; + coriolis_w_part += rb_joint_j_w_dot; + + // JDot/u * qdot + coriolis_v_part += rb_joint_j_v_dot_veldiff; + coriolis_w_part += rb_joint_j_w_dot_veldiff; + } + + /* + * Meld with the mass matrix. + */ + { + let mut i_coriolis_dt_v = self.i_coriolis_dt.fixed_rows_mut::(0); + i_coriolis_dt_v.copy_from(coriolis_v); + i_coriolis_dt_v *= rb_mass * dt; + } + + #[cfg(feature = "dim2")] + { + let mut i_coriolis_dt_w = self.i_coriolis_dt.fixed_rows_mut::(DIM); + // NOTE: this is just an axpy, but on row columns. + i_coriolis_dt_w.zip_apply(&coriolis_w, |o, x| *o = x * dt * rb_inertia); + } + #[cfg(feature = "dim3")] + { + let mut i_coriolis_dt_w = self.i_coriolis_dt.fixed_rows_mut::(DIM); + i_coriolis_dt_w.gemm(dt, &rb_inertia, &coriolis_w, 0.0); + } + + self.acc_augmented_mass + .gemm_tr(1.0, &rb_j, &self.i_coriolis_dt, 1.0); + } + + /* + * Damping. + */ + for i in 0..self.ndofs { + self.acc_augmented_mass[(i, i)] += self.damping[i] * dt; + self.augmented_mass[(i, i)] += self.damping[i] * dt; + } + + // FIXME: avoid allocation inside LU at each timestep. + self.acc_inv_augmented_mass = LU::new(self.acc_augmented_mass.clone()); + self.inv_augmented_mass = LU::new(self.augmented_mass.clone()); + // self.inv_augmented_mass = self.acc_inv_augmented_mass.clone(); + } + + /// The generalized velocity at the multibody_joint of the given link. + #[inline] + pub(crate) fn joint_velocity(&self, link: &MultibodyLink) -> DVectorSlice { + let ndofs = link.joint().ndofs(); + DVectorSlice::from_slice( + &self.velocities.as_slice()[link.assembly_id..link.assembly_id + ndofs], + ndofs, + ) + } + + #[inline] + pub fn generalized_acceleration(&self) -> DVectorSlice { + self.accelerations.rows(0, self.ndofs) + } + + #[inline] + pub fn generalized_velocity(&self) -> DVectorSlice { + self.velocities.rows(0, self.ndofs) + } + + #[inline] + pub fn generalized_velocity_mut(&mut self) -> DVectorSliceMut { + self.velocities.rows_mut(0, self.ndofs) + } + + #[inline] + pub fn integrate_next(&mut self, dt: Real) { + for rb in self.links.iter_mut() { + rb.state + .joint + .integrate(dt, &self.velocities.as_slice()[rb.assembly_id..]) + } + } + + pub fn apply_next_displacements(&mut self, disp: &[Real]) { + for link in self.links.iter_mut() { + link.state + .joint + .apply_displacement(&disp[link.assembly_id..]) + } + } + + pub fn update_root_type(&mut self, bodies: &mut Bodies) + where + Bodies: ComponentSet + ComponentSet, + { + let rb_type: Option<&RigidBodyType> = bodies.get(self.links[0].rigid_body.0); + if let Some(rb_type) = rb_type { + let rb_pos: &RigidBodyPosition = bodies.index(self.links[0].rigid_body.0); + + if rb_type.is_dynamic() != self.root_is_dynamic { + if rb_type.is_dynamic() { + let free_joint = MultibodyJoint::free(rb_pos.position); + let prev_root_ndofs = self.links[0].joint().ndofs(); + self.links[0].state.joint = free_joint; + self.links[0].assembly_id = 0; + self.ndofs += SPATIAL_DIM; + + self.velocities = self.velocities.clone().insert_rows(0, SPATIAL_DIM, 0.0); + self.damping = self.damping.clone().insert_rows(0, SPATIAL_DIM, 0.0); + self.accelerations = + self.accelerations.clone().insert_rows(0, SPATIAL_DIM, 0.0); + + for link in &mut self.links[1..] { + link.assembly_id += SPATIAL_DIM - prev_root_ndofs; + } + } else { + assert!(self.velocities.len() >= SPATIAL_DIM); + assert!(self.damping.len() >= SPATIAL_DIM); + assert!(self.accelerations.len() >= SPATIAL_DIM); + + let fixed_joint = MultibodyJoint::fixed(rb_pos.position); + let prev_root_ndofs = self.links[0].joint().ndofs(); + self.links[0].state.joint = fixed_joint; + self.links[0].assembly_id = 0; + self.ndofs -= prev_root_ndofs; + + if self.ndofs == 0 { + self.velocities = DVector::zeros(0); + self.damping = DVector::zeros(0); + self.accelerations = DVector::zeros(0); + } else { + self.velocities = + self.velocities.index((prev_root_ndofs.., 0)).into_owned(); + self.damping = self.damping.index((prev_root_ndofs.., 0)).into_owned(); + self.accelerations = self + .accelerations + .index((prev_root_ndofs.., 0)) + .into_owned(); + } + + for link in &mut self.links[1..] { + link.assembly_id -= prev_root_ndofs; + } + } + + self.root_is_dynamic = rb_type.is_dynamic(); + } + + // Make sure the positions are properly set to match the rigid-body’s. + if self.links[0].state.joint.data.locked_axes.is_empty() { + self.links[0].state.joint.set_free_pos(rb_pos.position); + } else { + self.links[0].state.joint.data.local_frame1 = rb_pos.position; + } + } + } + + pub fn forward_kinematics_next(&mut self, bodies: &mut Bodies, update_mass_props: bool) + where + Bodies: ComponentSet + + ComponentSetMut + + ComponentSetMut, + { + // Special case for the root, which has no parent. + { + let link = &mut self.links[0]; + link.state + .joint + .update_jacobians(&self.velocities.as_slice()); + link.state.local_to_parent = link.state.joint.body_to_parent(); + link.state.local_to_world = link.state.local_to_parent; + + bodies.map_mut_internal(link.rigid_body.0, |poss: &mut RigidBodyPosition| { + poss.next_position = link.state.local_to_world; + }); + + if update_mass_props { + bodies.map_mut_internal(link.rigid_body.0, |mprops: &mut RigidBodyMassProps| { + mprops.update_world_mass_properties(&link.state.local_to_world) + }); + } + } + + // Handle the children. They all have a parent within this multibody. + for i in 1..self.links.len() { + let (link, parent_link) = self.links.get_mut_with_parent(i); + + link.state + .joint + .update_jacobians(&self.velocities.as_slice()[link.assembly_id..]); + link.state.local_to_parent = link.state.joint.body_to_parent(); + link.state.local_to_world = + parent_link.state.local_to_world * link.state.local_to_parent; + link.state.parent_to_world = parent_link.state.local_to_world; + + bodies.map_mut_internal(link.rigid_body.0, |poss: &mut RigidBodyPosition| { + poss.next_position = link.state.local_to_world; + }); + + let rb_type: &RigidBodyType = bodies.index(link.rigid_body.0); + assert_eq!( + *rb_type, + RigidBodyType::Dynamic, + "A rigid-body that is not at the root of a multibody must be dynamic." + ); + + if update_mass_props { + bodies.map_mut_internal(link.rigid_body.0, |mprops: &mut RigidBodyMassProps| { + mprops.update_world_mass_properties(&link.state.local_to_world) + }); + } + } + + /* + * Compute body jacobians. + */ + self.update_body_next_jacobians(bodies); + } + + #[inline] + pub fn ndofs(&self) -> usize { + self.ndofs + } + + pub fn fill_jacobians( + &self, + link_id: usize, + unit_force: Vector, + unit_torque: SVector, + j_id: &mut usize, + jacobians: &mut DVector, + ) -> (Real, Real) { + if self.ndofs == 0 { + return (0.0, 0.0); + } + + let wj_id = *j_id + self.ndofs; + let force = Force { + linear: unit_force, + #[cfg(feature = "dim2")] + angular: unit_torque[0], + #[cfg(feature = "dim3")] + angular: unit_torque, + }; + + let link = &self.links[link_id]; + let mut out_j = jacobians.rows_mut(*j_id, self.ndofs); + self.body_jacobians[link.internal_id].tr_mul_to(force.as_vector(), &mut out_j); + + // TODO: Optimize with a copy_nonoverlapping? + for i in 0..self.ndofs { + jacobians[wj_id + i] = jacobians[*j_id + i]; + } + + { + let mut out_invm_j = jacobians.rows_mut(wj_id, self.ndofs); + self.inv_augmented_mass.solve_mut(&mut out_invm_j); + } + + let j = jacobians.rows(*j_id, self.ndofs); + let invm_j = jacobians.rows(wj_id, self.ndofs); + *j_id += self.ndofs * 2; + + (j.dot(&invm_j), j.dot(&self.generalized_velocity())) + } + + #[inline] + pub fn has_active_internal_constraints(&self) -> bool { + self.links() + .any(|link| link.joint().num_velocity_constraints() != 0) + } + + #[inline] + pub fn generate_internal_constraints( + &self, + params: &IntegrationParameters, + j_id: &mut usize, + jacobians: &mut DVector, + out: &mut Vec, + ) { + let num_constraints: usize = self + .links + .iter() + .map(|l| l.joint().num_velocity_constraints()) + .sum(); + + let required_jacobian_len = *j_id + num_constraints * self.ndofs * 2; + if jacobians.nrows() < required_jacobian_len { + jacobians.resize_vertically_mut(required_jacobian_len, 0.0); + } + + for link in self.links.iter() { + link.joint() + .velocity_constraints(params, self, link, 0, j_id, jacobians, out); + } + } +} diff --git a/src/dynamics/joint/multibody_joint/multibody_joint.rs b/src/dynamics/joint/multibody_joint/multibody_joint.rs new file mode 100644 index 0000000..1e8522e --- /dev/null +++ b/src/dynamics/joint/multibody_joint/multibody_joint.rs @@ -0,0 +1,571 @@ +use crate::dynamics::solver::AnyJointVelocityConstraint; +use crate::dynamics::{ + joint, FixedJoint, IntegrationParameters, JointAxesMask, JointData, Multibody, MultibodyLink, + RigidBodyVelocity, +}; +use crate::math::{ + Isometry, JacobianSliceMut, Matrix, Real, Rotation, SpacialVector, Translation, Vector, + ANG_DIM, DIM, SPATIAL_DIM, +}; +use crate::utils::WCross; +use na::{DVector, DVectorSliceMut}; +#[cfg(feature = "dim3")] +use { + crate::utils::WCrossMatrix, + na::{UnitQuaternion, Vector3, VectorSlice3}, +}; + +#[derive(Copy, Clone, Debug)] +pub struct MultibodyJoint { + pub data: JointData, + pub(crate) coords: SpacialVector, + pub(crate) joint_rot: Rotation, + jacobian_v: Matrix, + jacobian_dot_v: Matrix, + jacobian_dot_veldiff_v: Matrix, +} + +#[cfg(feature = "dim2")] +fn revolute_locked_axes() -> JointAxesMask { + JointAxesMask::X | JointAxesMask::Y +} + +#[cfg(feature = "dim3")] +fn revolute_locked_axes() -> JointAxesMask { + JointAxesMask::X + | JointAxesMask::Y + | JointAxesMask::Z + | JointAxesMask::ANG_Y + | JointAxesMask::ANG_Z +} + +impl MultibodyJoint { + pub fn new(data: JointData) -> Self { + Self { + data, + coords: na::zero(), + joint_rot: Rotation::identity(), + jacobian_v: na::zero(), + jacobian_dot_v: na::zero(), + jacobian_dot_veldiff_v: na::zero(), + } + } + + pub(crate) fn free(pos: Isometry) -> Self { + let mut result = Self::new(JointData::default()); + result.set_free_pos(pos); + result + } + + pub(crate) fn fixed(pos: Isometry) -> Self { + Self::new(FixedJoint::new().local_frame1(pos).into()) + } + + pub(crate) fn set_free_pos(&mut self, pos: Isometry) { + self.coords + .fixed_rows_mut::(0) + .copy_from(&pos.translation.vector); + self.joint_rot = pos.rotation; + } + + pub fn local_joint_rot(&self) -> &Rotation { + &self.joint_rot + } + + fn num_free_lin_dofs(&self) -> usize { + let locked_bits = self.data.locked_axes.bits(); + DIM - (locked_bits & ((1 << DIM) - 1)).count_ones() as usize + } + + /// The number of degrees of freedom allowed by the multibody_joint. + pub fn ndofs(&self) -> usize { + SPATIAL_DIM - self.data.locked_axes.bits().count_ones() as usize + } + + /// The position of the multibody link containing this multibody_joint relative to its parent. + pub fn body_to_parent(&self) -> Isometry { + if self.data.locked_axes == revolute_locked_axes() { + // FIXME: this is a special case for the revolute joint. + // We have the mathematical formulation ready that works in the general case, but its + // implementation will take some time. So let’s make a special case for the alpha + // release and fix is soon after. + self.data.local_frame1.translation + * self.joint_rot + * self.data.local_frame2.translation.inverse() + } else { + let locked_bits = self.data.locked_axes.bits(); + let mut transform = self.joint_rot * self.data.local_frame2.inverse(); + + for i in 0..DIM { + if (locked_bits & (1 << i)) == 0 { + transform = Translation::from(Vector::ith(i, self.coords[i])) * transform; + } + } + + self.data.local_frame1 * transform + } + } + + /// Integrate the position of this multibody_joint. + pub fn integrate(&mut self, dt: Real, vels: &[Real]) { + if self.data.locked_axes == revolute_locked_axes() { + // FIXME: this is a special case for the revolute joint. + // We have the mathematical formulation ready that works in the general case, but its + // implementation will take some time. So let’s make a special case for the alpha + // release and fix is soon after. + #[cfg(feature = "dim3")] + let axis = self.data.local_frame1 * Vector::x_axis(); + self.coords[DIM] += vels[0] * dt; + + #[cfg(feature = "dim2")] + { + self.joint_rot = Rotation::from_angle(self.coords[DIM]); + } + #[cfg(feature = "dim3")] + { + self.joint_rot = Rotation::from_axis_angle(&axis, self.coords[DIM]); + } + } else { + let locked_bits = self.data.locked_axes.bits(); + let mut curr_free_dof = 0; + + for i in 0..DIM { + if (locked_bits & (1 << i)) == 0 { + self.coords[i] += vels[curr_free_dof] * dt; + curr_free_dof += 1; + } + } + + let locked_ang_bits = locked_bits >> DIM; + let num_free_ang_dofs = ANG_DIM - locked_ang_bits.count_ones() as usize; + match num_free_ang_dofs { + 0 => { /* No free dofs. */ } + 1 => { + todo!() + } + 2 => { + todo!() + } + #[cfg(feature = "dim3")] + 3 => { + let angvel = Vector3::from_row_slice(&vels[curr_free_dof..curr_free_dof + 3]); + let disp = UnitQuaternion::new_eps(angvel * dt, 0.0); + self.joint_rot = disp * self.joint_rot; + } + _ => unreachable!(), + } + } + } + + /// Apply a displacement to the multibody_joint. + pub fn apply_displacement(&mut self, disp: &[Real]) { + self.integrate(1.0, disp); + } + + /// Update the jacobians of this multibody_joint. + pub fn update_jacobians(&mut self, vels: &[Real]) { + if self.data.locked_axes == revolute_locked_axes() { + // FIXME: this is a special case for the revolute joint. + // We have the mathematical formulation ready that works in the general case, but its + // implementation will take some time. So let’s make a special case for the alpha + // release and fix is soon after. + #[cfg(feature = "dim2")] + let axis = 1.0; + #[cfg(feature = "dim3")] + let axis = self.data.local_frame1 * Vector::x_axis(); + let body_shift = self.data.local_frame2.translation.vector; + let shift = self.joint_rot * -body_shift; + let shift_dot_veldiff = axis.gcross(shift); + + #[cfg(feature = "dim2")] + { + self.jacobian_v.column_mut(0).copy_from(&axis.gcross(shift)); + } + #[cfg(feature = "dim3")] + { + self.jacobian_v.column_mut(0).copy_from(&axis.gcross(shift)); + } + self.jacobian_dot_veldiff_v + .column_mut(0) + .copy_from(&axis.gcross(shift_dot_veldiff)); + self.jacobian_dot_v + .column_mut(0) + .copy_from(&(axis.gcross(shift_dot_veldiff) * vels[0])); + } else { + let locked_bits = self.data.locked_axes.bits(); + let locked_ang_bits = locked_bits >> DIM; + let num_free_ang_dofs = ANG_DIM - locked_ang_bits.count_ones() as usize; + match num_free_ang_dofs { + 0 => { /* No free dofs. */ } + 1 => { + todo!() + } + 2 => { + todo!() + } + #[cfg(feature = "dim3")] + 3 => { + let num_free_lin_dofs = self.num_free_lin_dofs(); + let inv_frame2 = self.data.local_frame2.inverse(); + let shift = self.joint_rot * inv_frame2.translation.vector; + let angvel = + VectorSlice3::from_slice(&vels[num_free_lin_dofs..num_free_lin_dofs + 3]); + let inv_rotmat2 = inv_frame2.rotation.to_rotation_matrix().into_inner(); + + self.jacobian_v = inv_rotmat2 * shift.gcross_matrix().transpose(); + self.jacobian_dot_v = + inv_rotmat2 * angvel.cross(&shift).gcross_matrix().transpose(); + } + _ => unreachable!(), + } + } + } + + /// Sets in `out` the non-zero entries of the multibody_joint jacobian transformed by `transform`. + pub fn jacobian(&self, transform: &Isometry, out: &mut JacobianSliceMut) { + if self.data.locked_axes == revolute_locked_axes() { + // FIXME: this is a special case for the revolute joint. + // We have the mathematical formulation ready that works in the general case, but its + // implementation will take some time. So let’s make a special case for the alpha + // release and fix is soon after. + #[cfg(feature = "dim2")] + let axis = 1.0; + #[cfg(feature = "dim3")] + let axis = self.data.local_frame1 * Vector::x(); + let jacobian = RigidBodyVelocity::new(self.jacobian_v.column(0).into_owned(), axis); + out.copy_from(jacobian.transformed(transform).as_vector()) + } else { + let locked_bits = self.data.locked_axes.bits(); + let mut curr_free_dof = 0; + + for i in 0..DIM { + if (locked_bits & (1 << i)) == 0 { + let transformed_axis = transform * self.data.local_frame1 * Vector::ith(i, 1.0); + out.fixed_slice_mut::(0, curr_free_dof) + .copy_from(&transformed_axis); + curr_free_dof += 1; + } + } + + let locked_ang_bits = locked_bits >> DIM; + let num_free_ang_dofs = ANG_DIM - locked_ang_bits.count_ones() as usize; + match num_free_ang_dofs { + 0 => { /* No free dofs. */ } + 1 => { + todo!() + } + 2 => { + todo!() + } + #[cfg(feature = "dim3")] + 3 => { + let rotmat = transform.rotation.to_rotation_matrix(); + out.fixed_slice_mut::<3, 3>(0, curr_free_dof) + .copy_from(&(rotmat * self.jacobian_v)); + out.fixed_slice_mut::<3, 3>(3, curr_free_dof) + .copy_from(rotmat.matrix()); + } + _ => unreachable!(), + } + } + } + + /// Sets in `out` the non-zero entries of the time-derivative of the multibody_joint jacobian transformed by `transform`. + pub fn jacobian_dot(&self, transform: &Isometry, out: &mut JacobianSliceMut) { + if self.data.locked_axes == revolute_locked_axes() { + // FIXME: this is a special case for the revolute joint. + // We have the mathematical formulation ready that works in the general case, but its + // implementation will take some time. So let’s make a special case for the alpha + // release and fix is soon after. + let jacobian = RigidBodyVelocity::from_vectors( + self.jacobian_dot_v.column(0).into_owned(), + na::zero(), + ); + out.copy_from(jacobian.transformed(transform).as_vector()) + } else { + let locked_bits = self.data.locked_axes.bits(); + let locked_ang_bits = locked_bits >> DIM; + let num_free_ang_dofs = ANG_DIM - locked_ang_bits.count_ones() as usize; + match num_free_ang_dofs { + 0 => { /* No free dofs. */ } + 1 => { + todo!() + } + 2 => { + todo!() + } + #[cfg(feature = "dim3")] + 3 => { + let num_free_lin_dofs = self.num_free_lin_dofs(); + let rotmat = transform.rotation.to_rotation_matrix(); + out.fixed_slice_mut::<3, 3>(0, num_free_lin_dofs) + .copy_from(&(rotmat * self.jacobian_dot_v)); + } + _ => unreachable!(), + } + } + } + + /// Sets in `out` the non-zero entries of the velocity-derivative of the time-derivative of the multibody_joint jacobian transformed by `transform`. + pub fn jacobian_dot_veldiff_mul_coordinates( + &self, + transform: &Isometry, + acc: &[Real], + out: &mut JacobianSliceMut, + ) { + if self.data.locked_axes == revolute_locked_axes() { + // FIXME: this is a special case for the revolute joint. + // We have the mathematical formulation ready that works in the general case, but its + // implementation will take some time. So let’s make a special case for the alpha + // release and fix is soon after. + let jacobian = RigidBodyVelocity::from_vectors( + self.jacobian_dot_veldiff_v.column(0).into_owned(), + na::zero(), + ); + out.copy_from((jacobian.transformed(transform) * acc[0]).as_vector()) + } else { + let locked_bits = self.data.locked_axes.bits(); + let locked_ang_bits = locked_bits >> DIM; + let num_free_ang_dofs = ANG_DIM - locked_ang_bits.count_ones() as usize; + match num_free_ang_dofs { + 0 => { /* No free dofs. */ } + 1 => { + todo!() + } + 2 => { + todo!() + } + #[cfg(feature = "dim3")] + 3 => { + let num_free_lin_dofs = self.num_free_lin_dofs(); + let angvel = + Vector3::from_row_slice(&acc[num_free_lin_dofs..num_free_lin_dofs + 3]); + let rotmat = transform.rotation.to_rotation_matrix(); + let res = rotmat * angvel.gcross_matrix() * self.jacobian_v; + out.fixed_slice_mut::<3, 3>(0, num_free_lin_dofs) + .copy_from(&res); + } + _ => unreachable!(), + } + } + } + + /// Multiply the multibody_joint jacobian by generalized velocities to obtain the + /// relative velocity of the multibody link containing this multibody_joint. + pub fn jacobian_mul_coordinates(&self, acc: &[Real]) -> RigidBodyVelocity { + if self.data.locked_axes == revolute_locked_axes() { + // FIXME: this is a special case for the revolute joint. + // We have the mathematical formulation ready that works in the general case, but its + // implementation will take some time. So let’s make a special case for the alpha + // release and fix is soon after. + #[cfg(feature = "dim2")] + let axis = 1.0; + #[cfg(feature = "dim3")] + let axis = self.data.local_frame1 * Vector::x(); + RigidBodyVelocity::new(self.jacobian_v.column(0).into_owned(), axis) * acc[0] + } else { + let locked_bits = self.data.locked_axes.bits(); + let mut result = RigidBodyVelocity::zero(); + let mut curr_free_dof = 0; + + for i in 0..DIM { + if (locked_bits & (1 << i)) == 0 { + result.linvel += self.data.local_frame1 * Vector::ith(i, acc[curr_free_dof]); + curr_free_dof += 1; + } + } + + let locked_ang_bits = locked_bits >> DIM; + let num_free_ang_dofs = ANG_DIM - locked_ang_bits.count_ones() as usize; + match num_free_ang_dofs { + 0 => { /* No free dofs. */ } + 1 => { + todo!() + } + 2 => { + todo!() + } + #[cfg(feature = "dim3")] + 3 => { + let angvel = Vector3::from_row_slice(&acc[curr_free_dof..curr_free_dof + 3]); + let linvel = self.jacobian_v * angvel; + result += RigidBodyVelocity::new(linvel, angvel); + } + _ => unreachable!(), + } + result + } + } + + /// Multiply the multibody_joint jacobian by generalized accelerations to obtain the + /// relative acceleration of the multibody link containing this multibody_joint. + pub fn jacobian_dot_mul_coordinates(&self, acc: &[Real]) -> RigidBodyVelocity { + if self.data.locked_axes == revolute_locked_axes() { + // FIXME: this is a special case for the revolute joint. + // We have the mathematical formulation ready that works in the general case, but its + // implementation will take some time. So let’s make a special case for the alpha + // release and fix is soon after. + RigidBodyVelocity::from_vectors(self.jacobian_dot_v.column(0).into_owned(), na::zero()) + * acc[0] + } else { + let locked_bits = self.data.locked_axes.bits(); + + let locked_ang_bits = locked_bits >> DIM; + let num_free_ang_dofs = ANG_DIM - locked_ang_bits.count_ones() as usize; + match num_free_ang_dofs { + 0 => { + /* No free dofs. */ + RigidBodyVelocity::zero() + } + 1 => { + todo!() + } + 2 => { + todo!() + } + #[cfg(feature = "dim3")] + 3 => { + let num_free_lin_dofs = self.num_free_lin_dofs(); + let angvel = + Vector3::from_row_slice(&acc[num_free_lin_dofs..num_free_lin_dofs + 3]); + let linvel = self.jacobian_dot_v * angvel; + RigidBodyVelocity::new(linvel, na::zero()) + } + _ => unreachable!(), + } + } + } + + /// Fill `out` with the non-zero entries of a damping that can be applied by default to ensure a good stability of the multibody_joint. + pub fn default_damping(&self, out: &mut DVectorSliceMut) { + let locked_bits = self.data.locked_axes.bits(); + let mut curr_free_dof = self.num_free_lin_dofs(); + + // A default damping only for the angular dofs + for i in DIM..SPATIAL_DIM { + if locked_bits & (1 << i) == 0 { + // This is a free angular DOF. + out[curr_free_dof] = 0.2; + curr_free_dof += 1; + } + } + } + + /// Maximum number of velocity constrains that can be generated by this multibody_joint. + pub fn num_velocity_constraints(&self) -> usize { + let locked_bits = self.data.locked_axes.bits(); + let limit_bits = self.data.limit_axes.bits(); + let motor_bits = self.data.motor_axes.bits(); + let mut num_constraints = 0; + + for i in 0..SPATIAL_DIM { + if (locked_bits & (1 << i)) == 0 { + if (limit_bits & (1 << i)) != 0 { + num_constraints += 1; + } + if (motor_bits & (1 << i)) != 0 { + num_constraints += 1; + } + } + } + + num_constraints + } + + /// Initialize and generate velocity constraints to enforce, e.g., multibody_joint limits and motors. + pub fn velocity_constraints( + &self, + params: &IntegrationParameters, + multibody: &Multibody, + link: &MultibodyLink, + dof_id: usize, + j_id: &mut usize, + jacobians: &mut DVector, + constraints: &mut Vec, + ) { + let locked_bits = self.data.locked_axes.bits(); + let limit_bits = self.data.limit_axes.bits(); + let motor_bits = self.data.motor_axes.bits(); + let mut curr_free_dof = 0; + + for i in 0..DIM { + if (locked_bits & (1 << i)) == 0 { + if (limit_bits & (1 << i)) != 0 { + joint::unit_joint_limit_constraint( + params, + multibody, + link, + [self.data.limits[i].min, self.data.limits[i].max], + self.coords[i], + dof_id + curr_free_dof, + j_id, + jacobians, + constraints, + ); + } + + if (motor_bits & (1 << i)) != 0 { + joint::unit_joint_motor_constraint( + params, + multibody, + link, + &self.data.motors[i], + self.coords[i], + dof_id + curr_free_dof, + j_id, + jacobians, + constraints, + ); + } + curr_free_dof += 1; + } + } + + /* + let locked_ang_bits = locked_bits >> DIM; + let num_free_ang_dofs = ANG_DIM - locked_ang_bits.count_ones() as usize; + match num_free_ang_dofs { + 0 => { /* No free dofs. */ } + 1 => {} + 2 => { + todo!() + } + 3 => {} + _ => unreachable!(), + } + */ + // TODO: we should make special cases for multi-angular-dofs limits/motors + for i in DIM..SPATIAL_DIM { + if (locked_bits & (1 << i)) == 0 { + if (limit_bits & (1 << i)) != 0 { + joint::unit_joint_limit_constraint( + params, + multibody, + link, + [self.data.limits[i].min, self.data.limits[i].max], + self.coords[i], + dof_id + curr_free_dof, + j_id, + jacobians, + constraints, + ); + } + + if (motor_bits & (1 << i)) != 0 { + joint::unit_joint_motor_constraint( + params, + multibody, + link, + &self.data.motors[i], + self.coords[i], + dof_id + curr_free_dof, + j_id, + jacobians, + constraints, + ); + } + curr_free_dof += 1; + } + } + } +} diff --git a/src/dynamics/joint/multibody_joint/multibody_joint_set.rs b/src/dynamics/joint/multibody_joint/multibody_joint_set.rs new file mode 100644 index 0000000..8337158 --- /dev/null +++ b/src/dynamics/joint/multibody_joint/multibody_joint_set.rs @@ -0,0 +1,352 @@ +use crate::data::{Arena, Coarena, ComponentSet, ComponentSetMut}; +use crate::dynamics::joint::MultibodyLink; +use crate::dynamics::{ + IslandManager, JointData, Multibody, MultibodyJoint, RigidBodyActivation, RigidBodyHandle, + RigidBodyIds, RigidBodyType, +}; +use crate::geometry::{InteractionGraph, RigidBodyGraphIndex}; +use crate::parry::partitioning::IndexedData; +use std::ops::Index; + +/// The unique handle of an multibody_joint added to a `MultibodyJointSet`. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] +#[repr(transparent)] +pub struct MultibodyJointHandle(pub crate::data::arena::Index); + +/// The temporary index of a multibody added to a `MultibodyJointSet`. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] +#[repr(transparent)] +pub struct MultibodyIndex(pub crate::data::arena::Index); + +impl MultibodyJointHandle { + /// Converts this handle into its (index, generation) components. + pub fn into_raw_parts(self) -> (u32, u32) { + self.0.into_raw_parts() + } + + /// Reconstructs an handle from its (index, generation) components. + pub fn from_raw_parts(id: u32, generation: u32) -> Self { + Self(crate::data::arena::Index::from_raw_parts(id, generation)) + } + + /// An always-invalid rigid-body handle. + pub fn invalid() -> Self { + Self(crate::data::arena::Index::from_raw_parts( + crate::INVALID_U32, + crate::INVALID_U32, + )) + } +} + +impl Default for MultibodyJointHandle { + fn default() -> Self { + Self::invalid() + } +} + +impl IndexedData for MultibodyJointHandle { + fn default() -> Self { + Self(IndexedData::default()) + } + fn index(&self) -> usize { + self.0.index() + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct MultibodyJointLink { + pub graph_id: RigidBodyGraphIndex, + pub multibody: MultibodyIndex, + pub id: usize, +} + +impl Default for MultibodyJointLink { + fn default() -> Self { + Self { + graph_id: RigidBodyGraphIndex::new(crate::INVALID_U32), + multibody: MultibodyIndex(crate::data::arena::Index::from_raw_parts( + crate::INVALID_U32, + crate::INVALID_U32, + )), + id: 0, + } + } +} + +/// A set of rigid bodies that can be handled by a physics pipeline. +pub struct MultibodyJointSet { + pub(crate) multibodies: Arena, // NOTE: a Slab would be sufficient. + pub(crate) rb2mb: Coarena, + // NOTE: this is mostly for the island extraction. So perhaps we won’t need + // that any more in the future when we improve our island builder. + pub(crate) connectivity_graph: InteractionGraph, +} + +impl MultibodyJointSet { + /// Create a new empty set of multibodies. + pub fn new() -> Self { + Self { + multibodies: Arena::new(), + rb2mb: Coarena::new(), + connectivity_graph: InteractionGraph::new(), + } + } + + pub fn iter(&self) -> impl Iterator { + self.rb2mb + .iter() + .filter(|(_, link)| link.id > 0) // The first link of a rigid-body hasn’t been added by the user. + .map(|(h, link)| { + let mb = &self.multibodies[link.multibody.0]; + (MultibodyJointHandle(h), mb, mb.link(link.id).unwrap()) + }) + } + + /// Inserts a new multibody_joint into this set. + pub fn insert( + &mut self, + body1: RigidBodyHandle, + body2: RigidBodyHandle, + data: impl Into, + ) -> Option { + let data = data.into(); + let link1 = self.rb2mb.get(body1.0).copied().unwrap_or_else(|| { + let mb_handle = self.multibodies.insert(Multibody::with_root(body1)); + MultibodyJointLink { + graph_id: self.connectivity_graph.graph.add_node(body1), + multibody: MultibodyIndex(mb_handle), + id: 0, + } + }); + + let link2 = self.rb2mb.get(body2.0).copied().unwrap_or_else(|| { + let mb_handle = self.multibodies.insert(Multibody::with_root(body2)); + MultibodyJointLink { + graph_id: self.connectivity_graph.graph.add_node(body2), + multibody: MultibodyIndex(mb_handle), + id: 0, + } + }); + + if link1.multibody == link2.multibody || link2.id != 0 { + // This would introduce an invalid configuration. + return None; + } + + self.connectivity_graph + .graph + .add_edge(link1.graph_id, link2.graph_id, ()); + self.rb2mb.insert(body1.0, link1); + self.rb2mb.insert(body2.0, link2); + + let mb2 = self.multibodies.remove(link2.multibody.0).unwrap(); + let multibody1 = &mut self.multibodies[link1.multibody.0]; + + for mb_link2 in mb2.links() { + let link = self.rb2mb.get_mut(mb_link2.rigid_body.0).unwrap(); + link.multibody = link1.multibody; + link.id += multibody1.num_links(); + } + + multibody1.append(mb2, link1.id, MultibodyJoint::new(data)); + + // Because each rigid-body can only have one parent link, + // we can use the second rigid-body’s handle as the multibody_joint’s + // handle. + Some(MultibodyJointHandle(body2.0)) + } + + /// Removes an multibody_joint from this set. + pub fn remove( + &mut self, + handle: MultibodyJointHandle, + islands: &mut IslandManager, + bodies: &mut Bodies, + wake_up: bool, + ) where + Bodies: ComponentSetMut + + ComponentSet + + ComponentSetMut, + { + if let Some(removed) = self.rb2mb.get(handle.0).copied() { + let multibody = self.multibodies.remove(removed.multibody.0).unwrap(); + + // Remove the edge from the connectivity graph. + if let Some(parent_link) = multibody.link(removed.id).unwrap().parent_id() { + let parent_rb = multibody.link(parent_link).unwrap().rigid_body; + self.connectivity_graph.remove_edge( + self.rb2mb.get(parent_rb.0).unwrap().graph_id, + removed.graph_id, + ); + + if wake_up { + islands.wake_up(bodies, RigidBodyHandle(handle.0), true); + islands.wake_up(bodies, parent_rb, true); + } + + // TODO: remove the node if it no longer has any attached edges? + + // Extract the individual sub-trees generated by this removal. + let multibodies = multibody.remove_link(removed.id, true); + + // Update the rb2mb mapping. + for multibody in multibodies { + if multibody.num_links() == 1 { + // We don’t have any multibody_joint attached to this body, remove it. + if let Some(other) = self.connectivity_graph.remove_node(removed.graph_id) { + self.rb2mb.get_mut(other.0).unwrap().graph_id = removed.graph_id; + } + } else { + let mb_id = self.multibodies.insert(multibody); + for link in self.multibodies[mb_id].links() { + let ids = self.rb2mb.get_mut(link.rigid_body.0).unwrap(); + ids.multibody = MultibodyIndex(mb_id); + ids.id = link.internal_id; + } + } + } + } + } + } + + /// Removes all the multibody_joints from the multibody the given rigid-body is part of. + pub fn remove_multibody_articulations( + &mut self, + handle: RigidBodyHandle, + islands: &mut IslandManager, + bodies: &mut Bodies, + wake_up: bool, + ) where + Bodies: ComponentSetMut + + ComponentSet + + ComponentSetMut, + { + if let Some(removed) = self.rb2mb.get(handle.0).copied() { + // Remove the multibody. + let multibody = self.multibodies.remove(removed.multibody.0).unwrap(); + for link in multibody.links() { + let rb_handle = link.rigid_body; + + if wake_up { + islands.wake_up(bodies, rb_handle, true); + } + + // Remove the rigid-body <-> multibody mapping for this link. + let removed = self.rb2mb.remove(rb_handle.0, Default::default()).unwrap(); + // Remove the node (and all it’s edges) from the connectivity graph. + if let Some(other) = self.connectivity_graph.remove_node(removed.graph_id) { + self.rb2mb.get_mut(other.0).unwrap().graph_id = removed.graph_id; + } + } + } + } + + pub fn remove_articulations_attached_to_rigid_body( + &mut self, + rb_to_remove: RigidBodyHandle, + islands: &mut IslandManager, + bodies: &mut Bodies, + ) where + Bodies: ComponentSetMut + + ComponentSet + + ComponentSetMut, + { + // TODO: optimize this. + if let Some(link_to_remove) = self.rb2mb.get(rb_to_remove.0).copied() { + let mut articulations_to_remove = vec![]; + for (rb1, rb2, _) in self + .connectivity_graph + .interactions_with(link_to_remove.graph_id) + { + // There is an multibody_joint that we need to remove between these two bodies. + // If this is an outbound edge, then the multibody_joint’s handle is equal to the + // second body handle. + if rb1 == rb_to_remove { + articulations_to_remove.push(MultibodyJointHandle(rb2.0)); + } else { + articulations_to_remove.push(MultibodyJointHandle(rb1.0)); + } + + islands.wake_up(bodies, rb1, true); + islands.wake_up(bodies, rb2, true); + } + + for articulation_handle in articulations_to_remove { + self.remove(articulation_handle, islands, bodies, true); + } + } + } + + /// Returns the link of this multibody attached to the given rigid-body. + /// + /// Returns `None` if `rb` isn’t part of any rigid-body. + pub fn rigid_body_link(&self, rb: RigidBodyHandle) -> Option<&MultibodyJointLink> { + self.rb2mb.get(rb.0) + } + + /// Gets a reference to a multibody, based on its temporary index. + pub fn get_multibody(&self, index: MultibodyIndex) -> Option<&Multibody> { + self.multibodies.get(index.0) + } + + /// Gets a mutable reference to a multibody, based on its temporary index. + /// + /// This method will bypass any modification-detection automatically done by the + /// `MultibodyJointSet`. + pub fn get_multibody_mut_internal(&mut self, index: MultibodyIndex) -> Option<&mut Multibody> { + self.multibodies.get_mut(index.0) + } + + /// Gets a reference to the multibody identified by its `handle`. + pub fn get(&self, handle: MultibodyJointHandle) -> Option<(&Multibody, usize)> { + let link = self.rb2mb.get(handle.0)?; + let multibody = self.multibodies.get(link.multibody.0)?; + Some((multibody, link.id)) + } + + /// Gets a mutable reference to the multibody identified by its `handle`. + pub fn get_mut_internal( + &mut self, + handle: MultibodyJointHandle, + ) -> Option<(&mut Multibody, usize)> { + let link = self.rb2mb.get(handle.0)?; + let multibody = self.multibodies.get_mut(link.multibody.0)?; + Some((multibody, link.id)) + } + + /// Iterate through the handles of all the rigid-bodies attached to this rigid-body + /// by an multibody_joint. + pub fn attached_bodies<'a>( + &'a self, + body: RigidBodyHandle, + ) -> impl Iterator + 'a { + self.rb2mb + .get(body.0) + .into_iter() + .flat_map(move |id| self.connectivity_graph.interactions_with(id.graph_id)) + .map(move |inter| crate::utils::select_other((inter.0, inter.1), body)) + } + + /// Iterates through all the multibodies on this set. + pub fn multibodies(&self) -> impl Iterator { + self.multibodies.iter().map(|e| e.1) + } +} + +impl Index for MultibodyJointSet { + type Output = Multibody; + + fn index(&self, index: MultibodyIndex) -> &Multibody { + &self.multibodies[index.0] + } +} + +// impl Index for MultibodyJointSet { +// type Output = Multibody; +// +// fn index(&self, index: MultibodyJointHandle) -> &Multibody { +// &self.multibodies[index.0] +// } +// } diff --git a/src/dynamics/joint/multibody_joint/multibody_link.rs b/src/dynamics/joint/multibody_joint/multibody_link.rs new file mode 100644 index 0000000..94bd3de --- /dev/null +++ b/src/dynamics/joint/multibody_joint/multibody_link.rs @@ -0,0 +1,173 @@ +use std::ops::{Deref, DerefMut}; + +use crate::dynamics::{MultibodyJoint, RigidBodyHandle}; +use crate::math::{Isometry, Real}; +use crate::prelude::RigidBodyVelocity; + +pub(crate) struct KinematicState { + pub joint: MultibodyJoint, + pub parent_to_world: Isometry, + // TODO: should this be removed in favor of the rigid-body position? + pub local_to_world: Isometry, + pub local_to_parent: Isometry, +} + +impl Clone for KinematicState { + fn clone(&self) -> Self { + Self { + joint: self.joint.clone(), + parent_to_world: self.parent_to_world.clone(), + local_to_world: self.local_to_world.clone(), + local_to_parent: self.local_to_parent.clone(), + } + } +} + +/// One link of a multibody. +pub struct MultibodyLink { + pub(crate) name: String, + // FIXME: make all those private. + pub(crate) internal_id: usize, + pub(crate) assembly_id: usize, + pub(crate) is_leaf: bool, + + pub(crate) parent_internal_id: usize, + pub(crate) rigid_body: RigidBodyHandle, + + /* + * Change at each time step. + */ + pub(crate) state: KinematicState, + + // FIXME: put this on a workspace buffer instead ? + pub(crate) velocity_dot_wrt_joint: RigidBodyVelocity, + pub(crate) velocity_wrt_joint: RigidBodyVelocity, +} + +impl MultibodyLink { + /// Creates a new multibody link. + pub fn new( + rigid_body: RigidBodyHandle, + internal_id: usize, + assembly_id: usize, + parent_internal_id: usize, + joint: MultibodyJoint, + parent_to_world: Isometry, + local_to_world: Isometry, + local_to_parent: Isometry, + ) -> Self { + let is_leaf = true; + let velocity_dot_wrt_joint = RigidBodyVelocity::zero(); + let velocity_wrt_joint = RigidBodyVelocity::zero(); + let kinematic_state = KinematicState { + joint, + parent_to_world, + local_to_world, + local_to_parent, + }; + + MultibodyLink { + name: String::new(), + internal_id, + assembly_id, + is_leaf, + parent_internal_id, + state: kinematic_state, + velocity_dot_wrt_joint, + velocity_wrt_joint, + rigid_body, + } + } + + pub fn joint(&self) -> &MultibodyJoint { + &self.state.joint + } + + pub fn rigid_body_handle(&self) -> RigidBodyHandle { + self.rigid_body + } + + /// Checks if this link is the root of the multibody. + #[inline] + pub fn is_root(&self) -> bool { + self.internal_id == 0 + } + + /// This link's name. + #[inline] + pub fn name(&self) -> &str { + &self.name + } + + /// Sets this link's name. + #[inline] + pub fn set_name(&mut self, name: String) { + self.name = name + } + + /// The handle of this multibody link. + #[inline] + pub fn link_id(&self) -> usize { + self.internal_id + } + + /// The handle of the parent link. + #[inline] + pub fn parent_id(&self) -> Option { + if self.internal_id != 0 { + Some(self.parent_internal_id) + } else { + None + } + } + + #[inline] + pub fn local_to_world(&self) -> &Isometry { + &self.state.local_to_world + } + + #[inline] + pub fn local_to_parent(&self) -> &Isometry { + &self.state.local_to_parent + } +} + +// FIXME: keep this even if we already have the Index2 traits? +pub(crate) struct MultibodyLinkVec(pub Vec); + +impl MultibodyLinkVec { + #[inline] + pub fn get_mut_with_parent(&mut self, i: usize) -> (&mut MultibodyLink, &MultibodyLink) { + let parent_id = self[i].parent_internal_id; + + assert!( + parent_id != i, + "Internal error: circular rigid body dependency." + ); + assert!(parent_id < self.len(), "Invalid parent index."); + + unsafe { + let rb = &mut *(self.get_unchecked_mut(i) as *mut _); + let parent_rb = &*(self.get_unchecked(parent_id) as *const _); + (rb, parent_rb) + } + } +} + +impl Deref for MultibodyLinkVec { + type Target = Vec; + + #[inline] + fn deref(&self) -> &Vec { + let MultibodyLinkVec(ref me) = *self; + me + } +} + +impl DerefMut for MultibodyLinkVec { + #[inline] + fn deref_mut(&mut self) -> &mut Vec { + let MultibodyLinkVec(ref mut me) = *self; + me + } +} diff --git a/src/dynamics/joint/multibody_joint/multibody_workspace.rs b/src/dynamics/joint/multibody_joint/multibody_workspace.rs new file mode 100644 index 0000000..767d751 --- /dev/null +++ b/src/dynamics/joint/multibody_joint/multibody_workspace.rs @@ -0,0 +1,27 @@ +use crate::dynamics::RigidBodyVelocity; +use crate::math::Real; +use na::DVector; + +/// A temporary workspace for various updates of the multibody. +#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] +#[derive(Clone)] +pub(crate) struct MultibodyWorkspace { + pub accs: Vec, + pub ndofs_vec: DVector, +} + +impl MultibodyWorkspace { + /// Create an empty workspace. + pub fn new() -> Self { + MultibodyWorkspace { + accs: Vec::new(), + ndofs_vec: DVector::zeros(0), + } + } + + /// Resize the workspace so it is enough for `nlinks` links. + pub fn resize(&mut self, nlinks: usize, ndofs: usize) { + self.accs.resize(nlinks, RigidBodyVelocity::zero()); + self.ndofs_vec = DVector::zeros(ndofs) + } +} diff --git a/src/dynamics/joint/multibody_joint/unit_multibody_joint.rs b/src/dynamics/joint/multibody_joint/unit_multibody_joint.rs new file mode 100644 index 0000000..3367108 --- /dev/null +++ b/src/dynamics/joint/multibody_joint/unit_multibody_joint.rs @@ -0,0 +1,122 @@ +#![allow(missing_docs)] // For downcast. + +use crate::dynamics::joint::MultibodyLink; +use crate::dynamics::solver::{ + AnyJointVelocityConstraint, JointGenericVelocityGroundConstraint, WritebackId, +}; +use crate::dynamics::{IntegrationParameters, JointMotor, Multibody}; +use crate::math::Real; +use na::DVector; + +/// Initializes and generate the velocity constraints applicable to the multibody links attached +/// to this multibody_joint. +pub fn unit_joint_limit_constraint( + params: &IntegrationParameters, + multibody: &Multibody, + link: &MultibodyLink, + limits: [Real; 2], + curr_pos: Real, + dof_id: usize, + j_id: &mut usize, + jacobians: &mut DVector, + constraints: &mut Vec, +) { + let ndofs = multibody.ndofs(); + let joint_velocity = multibody.joint_velocity(link); + + let min_enabled = curr_pos < limits[0]; + let max_enabled = limits[1] < curr_pos; + let erp_inv_dt = params.erp_inv_dt(); + let rhs_bias = ((curr_pos - limits[1]).max(0.0) - (limits[0] - curr_pos).max(0.0)) * erp_inv_dt; + let rhs_wo_bias = joint_velocity[dof_id]; + + let dof_j_id = *j_id + dof_id + link.assembly_id; + jacobians.rows_mut(*j_id, ndofs * 2).fill(0.0); + jacobians[dof_j_id] = 1.0; + jacobians[dof_j_id + ndofs] = 1.0; + multibody + .inv_augmented_mass() + .solve_mut(&mut jacobians.rows_mut(*j_id + ndofs, ndofs)); + + let lhs = jacobians[dof_j_id + ndofs]; // = J^t * M^-1 J + let impulse_bounds = [ + min_enabled as u32 as Real * -Real::MAX, + max_enabled as u32 as Real * Real::MAX, + ]; + + let constraint = JointGenericVelocityGroundConstraint { + mj_lambda2: multibody.solver_id, + ndofs2: ndofs, + j_id2: *j_id, + joint_id: usize::MAX, + impulse: 0.0, + impulse_bounds, + inv_lhs: crate::utils::inv(lhs), + rhs: rhs_wo_bias + rhs_bias, + rhs_wo_bias, + writeback_id: WritebackId::Limit(dof_id), + }; + + constraints.push(AnyJointVelocityConstraint::JointGenericGroundConstraint( + constraint, + )); + *j_id += 2 * ndofs; +} + +/// Initializes and generate the velocity constraints applicable to the multibody links attached +/// to this multibody_joint. +pub fn unit_joint_motor_constraint( + params: &IntegrationParameters, + multibody: &Multibody, + link: &MultibodyLink, + motor: &JointMotor, + curr_pos: Real, + dof_id: usize, + j_id: &mut usize, + jacobians: &mut DVector, + constraints: &mut Vec, +) { + let ndofs = multibody.ndofs(); + let joint_velocity = multibody.joint_velocity(link); + + let motor_params = motor.motor_params(params.dt); + + let dof_j_id = *j_id + dof_id + link.assembly_id; + jacobians.rows_mut(*j_id, ndofs * 2).fill(0.0); + jacobians[dof_j_id] = 1.0; + jacobians[dof_j_id + ndofs] = 1.0; + multibody + .inv_augmented_mass() + .solve_mut(&mut jacobians.rows_mut(*j_id + ndofs, ndofs)); + + let lhs = jacobians[dof_j_id + ndofs]; // = J^t * M^-1 J + let impulse_bounds = [-motor_params.max_impulse, motor_params.max_impulse]; + + let mut rhs_wo_bias = 0.0; + if motor_params.stiffness != 0.0 { + rhs_wo_bias += (curr_pos - motor_params.target_pos) * motor_params.stiffness; + } + + if motor_params.damping != 0.0 { + let dvel = joint_velocity[dof_id]; + rhs_wo_bias += (dvel - motor_params.target_vel) * motor_params.damping; + } + + let constraint = JointGenericVelocityGroundConstraint { + mj_lambda2: multibody.solver_id, + ndofs2: ndofs, + j_id2: *j_id, + joint_id: usize::MAX, + impulse: 0.0, + impulse_bounds, + inv_lhs: crate::utils::inv(lhs), + rhs: rhs_wo_bias, + rhs_wo_bias, + writeback_id: WritebackId::Limit(dof_id), + }; + + constraints.push(AnyJointVelocityConstraint::JointGenericGroundConstraint( + constraint, + )); + *j_id += 2 * ndofs; +} diff --git a/src/dynamics/joint/prismatic_joint.rs b/src/dynamics/joint/prismatic_joint.rs index 69edcb7..92fabde 100644 --- a/src/dynamics/joint/prismatic_joint.rs +++ b/src/dynamics/joint/prismatic_joint.rs @@ -1,250 +1,91 @@ -use crate::dynamics::SpringModel; -use crate::math::{Isometry, Point, Real, Vector, DIM}; -use crate::utils::WBasis; -use na::Unit; -#[cfg(feature = "dim2")] -use na::Vector2; -#[cfg(feature = "dim3")] -use na::Vector5; +use crate::dynamics::joint::{JointAxesMask, JointData}; +use crate::dynamics::{JointAxis, MotorModel}; +use crate::math::{Point, Real, UnitVector}; -#[derive(Copy, Clone, PartialEq)] #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] -/// A joint that removes all relative motion between two bodies, except for the translations along one axis. +#[derive(Copy, Clone, Debug, PartialEq)] pub struct PrismaticJoint { - /// Where the prismatic joint is attached on the first body, expressed in the local space of the first attached body. - pub local_anchor1: Point, - /// Where the prismatic joint is attached on the second body, expressed in the local space of the second attached body. - pub local_anchor2: Point, - pub(crate) local_axis1: Unit>, - pub(crate) local_axis2: Unit>, - pub(crate) basis1: [Vector; DIM - 1], - pub(crate) basis2: [Vector; DIM - 1], - - /// The impulse applied by this joint on the first body. - /// - /// The impulse applied to the second body is given by `-impulse`. - #[cfg(feature = "dim3")] - pub impulse: Vector5, - /// The impulse applied by this joint on the first body. - /// - /// The impulse applied to the second body is given by `-impulse`. - #[cfg(feature = "dim2")] - pub impulse: Vector2, - - /// Whether or not this joint should enforce translational limits along its axis. - pub limits_enabled: bool, - /// The min an max relative position of the attached bodies along this joint's axis. - pub limits: [Real; 2], - /// The impulse applied by this joint on the first body to enforce the position limit along this joint's axis. - /// - /// The impulse applied to the second body is given by `-impulse`. - pub limits_impulse: Real, - - /// The target relative angular velocity the motor will attempt to reach. - pub motor_target_vel: Real, - /// The target relative angle along the joint axis the motor will attempt to reach. - pub motor_target_pos: Real, - /// The motor's stiffness. - /// See the documentation of `SpringModel` for more information on this parameter. - pub motor_stiffness: Real, - /// The motor's damping. - /// See the documentation of `SpringModel` for more information on this parameter. - pub motor_damping: Real, - /// The maximal impulse the motor is able to deliver. - pub motor_max_impulse: Real, - /// The angular impulse applied by the motor. - pub motor_impulse: Real, - /// The spring-like model used by the motor to reach the target velocity and . - pub motor_model: SpringModel, + data: JointData, } impl PrismaticJoint { - /// Creates a new prismatic joint with the given point of applications and axis, all expressed - /// in the local-space of the affected bodies. - #[cfg(feature = "dim2")] - pub fn new( - local_anchor1: Point, - local_axis1: Unit>, - local_anchor2: Point, - local_axis2: Unit>, - ) -> Self { - Self { - local_anchor1, - local_anchor2, - local_axis1, - local_axis2, - basis1: local_axis1.orthonormal_basis(), - basis2: local_axis2.orthonormal_basis(), - impulse: na::zero(), - limits_enabled: false, - limits: [-Real::MAX, Real::MAX], - limits_impulse: 0.0, - motor_target_vel: 0.0, - motor_target_pos: 0.0, - motor_stiffness: 0.0, - motor_damping: 0.0, - motor_max_impulse: Real::MAX, - motor_impulse: 0.0, - motor_model: SpringModel::VelocityBased, - } + pub fn new(axis: UnitVector) -> Self { + #[cfg(feature = "dim2")] + let mask = JointAxesMask::Y | JointAxesMask::ANG_X; + #[cfg(feature = "dim3")] + let mask = JointAxesMask::Y + | JointAxesMask::Z + | JointAxesMask::ANG_X + | JointAxesMask::ANG_Y + | JointAxesMask::ANG_Z; + + let data = JointData::default() + .lock_axes(mask) + .local_axis1(axis) + .local_axis2(axis); + Self { data } } - /// Creates a new prismatic joint with the given point of applications and axis, all expressed - /// in the local-space of the affected bodies. - /// - /// The local tangent are vector orthogonal to the local axis. It is used to compute a basis orthonormal - /// to the joint's axis. If this tangent is set to zero, te orthonormal basis will be automatically - /// computed arbitrarily. - #[cfg(feature = "dim3")] - pub fn new( - local_anchor1: Point, - local_axis1: Unit>, - local_tangent1: Vector, - local_anchor2: Point, - local_axis2: Unit>, - local_tangent2: Vector, - ) -> Self { - let basis1 = if let Some(local_bitangent1) = - Unit::try_new(local_axis1.cross(&local_tangent1), 1.0e-3) - { - [ - local_bitangent1.cross(&local_axis1), - local_bitangent1.into_inner(), - ] - } else { - local_axis1.orthonormal_basis() - }; - - let basis2 = if let Some(local_bitangent2) = - Unit::try_new(local_axis2.cross(&local_tangent2), 2.0e-3) - { - [ - local_bitangent2.cross(&local_axis2), - local_bitangent2.into_inner(), - ] - } else { - local_axis2.orthonormal_basis() - }; - - Self { - local_anchor1, - local_anchor2, - local_axis1, - local_axis2, - basis1, - basis2, - impulse: na::zero(), - limits_enabled: false, - limits: [-Real::MAX, Real::MAX], - limits_impulse: 0.0, - motor_target_vel: 0.0, - motor_target_pos: 0.0, - motor_stiffness: 0.0, - motor_damping: 0.0, - motor_max_impulse: Real::MAX, - motor_impulse: 0.0, - motor_model: SpringModel::VelocityBased, - } + #[must_use] + pub fn local_anchor1(mut self, anchor1: Point) -> Self { + self.data = self.data.local_anchor1(anchor1); + self } - /// The local axis of this joint, expressed in the local-space of the first attached body. - pub fn local_axis1(&self) -> Unit> { - self.local_axis1 - } - - /// The local axis of this joint, expressed in the local-space of the second attached body. - pub fn local_axis2(&self) -> Unit> { - self.local_axis2 - } - - /// Can a SIMD constraint be used for resolving this joint? - pub fn supports_simd_constraints(&self) -> bool { - // SIMD revolute constraints don't support motors right now. - self.motor_max_impulse == 0.0 || (self.motor_stiffness == 0.0 && self.motor_damping == 0.0) - } - - // FIXME: precompute this? - #[cfg(feature = "dim2")] - pub(crate) fn local_frame1(&self) -> Isometry { - use na::{Matrix2, Rotation2, UnitComplex}; - - let mat = Matrix2::from_columns(&[self.local_axis1.into_inner(), self.basis1[0]]); - let rotmat = Rotation2::from_matrix_unchecked(mat); - let rotation = UnitComplex::from_rotation_matrix(&rotmat); - let translation = self.local_anchor1.coords.into(); - Isometry::from_parts(translation, rotation) - } - - // FIXME: precompute this? - #[cfg(feature = "dim2")] - pub(crate) fn local_frame2(&self) -> Isometry { - use na::{Matrix2, Rotation2, UnitComplex}; - - let mat = Matrix2::from_columns(&[self.local_axis2.into_inner(), self.basis2[0]]); - let rotmat = Rotation2::from_matrix_unchecked(mat); - let rotation = UnitComplex::from_rotation_matrix(&rotmat); - let translation = self.local_anchor2.coords.into(); - Isometry::from_parts(translation, rotation) - } - - // FIXME: precompute this? - #[cfg(feature = "dim3")] - pub(crate) fn local_frame1(&self) -> Isometry { - use na::{Matrix3, Rotation3, UnitQuaternion}; - - let mat = Matrix3::from_columns(&[ - self.local_axis1.into_inner(), - self.basis1[0], - self.basis1[1], - ]); - let rotmat = Rotation3::from_matrix_unchecked(mat); - let rotation = UnitQuaternion::from_rotation_matrix(&rotmat); - let translation = self.local_anchor1.coords.into(); - Isometry::from_parts(translation, rotation) - } - - // FIXME: precompute this? - #[cfg(feature = "dim3")] - pub(crate) fn local_frame2(&self) -> Isometry { - use na::{Matrix3, Rotation3, UnitQuaternion}; - - let mat = Matrix3::from_columns(&[ - self.local_axis2.into_inner(), - self.basis2[0], - self.basis2[1], - ]); - let rotmat = Rotation3::from_matrix_unchecked(mat); - let rotation = UnitQuaternion::from_rotation_matrix(&rotmat); - let translation = self.local_anchor2.coords.into(); - Isometry::from_parts(translation, rotation) + #[must_use] + pub fn local_anchor2(mut self, anchor2: Point) -> Self { + self.data = self.data.local_anchor2(anchor2); + self } /// Set the spring-like model used by the motor to reach the desired target velocity and position. - pub fn configure_motor_model(&mut self, model: SpringModel) { - self.motor_model = model; + pub fn motor_model(mut self, model: MotorModel) -> Self { + self.data = self.data.motor_model(JointAxis::X, model); + self } /// Sets the target velocity this motor needs to reach. - pub fn configure_motor_velocity(&mut self, target_vel: Real, factor: Real) { - self.configure_motor(self.motor_target_pos, target_vel, 0.0, factor) + pub fn motor_velocity(mut self, target_vel: Real, factor: Real) -> Self { + self.data = self.data.motor_velocity(JointAxis::X, target_vel, factor); + self } - /// Sets the target position this motor needs to reach. - pub fn configure_motor_position(&mut self, target_pos: Real, stiffness: Real, damping: Real) { - self.configure_motor(target_pos, 0.0, stiffness, damping) + /// Sets the target angle this motor needs to reach. + pub fn motor_position(mut self, target_pos: Real, stiffness: Real, damping: Real) -> Self { + self.data = self + .data + .motor_position(JointAxis::X, target_pos, stiffness, damping); + self } - /// Configure both the target position and target velocity of the motor. - pub fn configure_motor( - &mut self, + /// Configure both the target angle and target velocity of the motor. + pub fn motor_axis( + mut self, target_pos: Real, target_vel: Real, stiffness: Real, damping: Real, - ) { - self.motor_target_vel = target_vel; - self.motor_target_pos = target_pos; - self.motor_stiffness = stiffness; - self.motor_damping = damping; + ) -> Self { + self.data = self + .data + .motor_axis(JointAxis::X, target_pos, target_vel, stiffness, damping); + self + } + + pub fn motor_max_impulse(mut self, max_impulse: Real) -> Self { + self.data = self.data.motor_max_impulse(JointAxis::X, max_impulse); + self + } + + #[must_use] + pub fn limit_axis(mut self, limits: [Real; 2]) -> Self { + self.data = self.data.limit_axis(JointAxis::X, limits); + self + } +} + +impl Into for PrismaticJoint { + fn into(self) -> JointData { + self.data } } diff --git a/src/dynamics/joint/revolute_joint.rs b/src/dynamics/joint/revolute_joint.rs index 6531a89..9084c4d 100644 --- a/src/dynamics/joint/revolute_joint.rs +++ b/src/dynamics/joint/revolute_joint.rs @@ -1,174 +1,106 @@ -use crate::dynamics::SpringModel; -use crate::math::{Isometry, Point, Real, Vector}; -use crate::utils::WBasis; -use na::{RealField, Unit, Vector5}; +use crate::dynamics::joint::{JointAxesMask, JointData}; +use crate::dynamics::{JointAxis, MotorModel}; +use crate::math::{Point, Real}; + +#[cfg(feature = "dim3")] +use crate::math::UnitVector; -#[derive(Copy, Clone, PartialEq)] #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] -/// A joint that removes all relative motion between two bodies, except for the rotations along one axis. +#[derive(Copy, Clone, Debug, PartialEq)] pub struct RevoluteJoint { - /// Where the revolute joint is attached on the first body, expressed in the local space of the first attached body. - pub local_anchor1: Point, - /// Where the revolute joint is attached on the second body, expressed in the local space of the second attached body. - pub local_anchor2: Point, - /// The rotation axis of this revolute joint expressed in the local space of the first attached body. - pub local_axis1: Unit>, - /// The rotation axis of this revolute joint expressed in the local space of the second attached body. - pub local_axis2: Unit>, - /// The basis orthonormal to `local_axis1`, expressed in the local space of the first attached body. - pub basis1: [Vector; 2], - /// The basis orthonormal to `local_axis2`, expressed in the local space of the second attached body. - pub basis2: [Vector; 2], - /// The impulse applied by this joint on the first body. - /// - /// The impulse applied to the second body is given by `-impulse`. - pub impulse: Vector5, - - /// Whether or not this joint should enforce translational limits along its axis. - pub limits_enabled: bool, - /// The min an max relative position of the attached bodies along this joint's axis. - pub limits: [Real; 2], - /// The impulse applied by this joint on the first body to enforce the position limit along this joint's axis. - /// - /// The impulse applied to the second body is given by `-impulse`. - pub limits_impulse: Real, - - /// The target relative angular velocity the motor will attempt to reach. - pub motor_target_vel: Real, - /// The target relative angle along the joint axis the motor will attempt to reach. - pub motor_target_pos: Real, - /// The motor's stiffness. - /// See the documentation of `SpringModel` for more information on this parameter. - pub motor_stiffness: Real, - /// The motor's damping. - /// See the documentation of `SpringModel` for more information on this parameter. - pub motor_damping: Real, - /// The maximal impulse the motor is able to deliver. - pub motor_max_impulse: Real, - /// The angular impulse applied by the motor. - pub motor_impulse: Real, - /// The spring-like model used by the motor to reach the target velocity and . - pub motor_model: SpringModel, - - // Used to handle cases where the position target ends up being more than pi radians away. - pub(crate) motor_last_angle: Real, - // The angular impulse expressed in world-space. - pub(crate) world_ang_impulse: Vector, - // The world-space orientation of the free axis of the first attached body. - pub(crate) prev_axis1: Vector, + data: JointData, } impl RevoluteJoint { - /// Creates a new revolute joint with the given point of applications and axis, all expressed - /// in the local-space of the affected bodies. - pub fn new( - local_anchor1: Point, - local_axis1: Unit>, - local_anchor2: Point, - local_axis2: Unit>, - ) -> Self { - Self { - local_anchor1, - local_anchor2, - local_axis1, - local_axis2, - basis1: local_axis1.orthonormal_basis(), - basis2: local_axis2.orthonormal_basis(), - impulse: na::zero(), - world_ang_impulse: na::zero(), - limits_enabled: false, - limits: [-Real::MAX, Real::MAX], - limits_impulse: 0.0, - motor_target_vel: 0.0, - motor_target_pos: 0.0, - motor_stiffness: 0.0, - motor_damping: 0.0, - motor_max_impulse: Real::MAX, - motor_impulse: 0.0, - prev_axis1: *local_axis1, - motor_model: SpringModel::default(), - motor_last_angle: 0.0, - } + #[cfg(feature = "dim2")] + pub fn new() -> Self { + let mask = JointAxesMask::X | JointAxesMask::Y; + + let data = JointData::default().lock_axes(mask); + Self { data } } - /// Can a SIMD constraint be used for resolving this joint? - pub fn supports_simd_constraints(&self) -> bool { - // SIMD revolute constraints don't support motors and limits right now. - !self.limits_enabled - && (self.motor_max_impulse == 0.0 - || (self.motor_stiffness == 0.0 && self.motor_damping == 0.0)) + #[cfg(feature = "dim3")] + pub fn new(axis: UnitVector) -> Self { + let mask = JointAxesMask::X + | JointAxesMask::Y + | JointAxesMask::Z + | JointAxesMask::ANG_Y + | JointAxesMask::ANG_Z; + + let data = JointData::default() + .lock_axes(mask) + .local_axis1(axis) + .local_axis2(axis); + Self { data } + } + + pub fn data(&self) -> &JointData { + &self.data + } + + #[must_use] + pub fn local_anchor1(mut self, anchor1: Point) -> Self { + self.data = self.data.local_anchor1(anchor1); + self + } + + #[must_use] + pub fn local_anchor2(mut self, anchor2: Point) -> Self { + self.data = self.data.local_anchor2(anchor2); + self } /// Set the spring-like model used by the motor to reach the desired target velocity and position. - pub fn configure_motor_model(&mut self, model: SpringModel) { - self.motor_model = model; + pub fn motor_model(mut self, model: MotorModel) -> Self { + self.data = self.data.motor_model(JointAxis::AngX, model); + self } /// Sets the target velocity this motor needs to reach. - pub fn configure_motor_velocity(&mut self, target_vel: Real, factor: Real) { - self.configure_motor(self.motor_target_pos, target_vel, 0.0, factor) + pub fn motor_velocity(mut self, target_vel: Real, factor: Real) -> Self { + self.data = self + .data + .motor_velocity(JointAxis::AngX, target_vel, factor); + self } /// Sets the target angle this motor needs to reach. - pub fn configure_motor_position(&mut self, target_pos: Real, stiffness: Real, damping: Real) { - self.configure_motor(target_pos, 0.0, stiffness, damping) + pub fn motor_position(mut self, target_pos: Real, stiffness: Real, damping: Real) -> Self { + self.data = self + .data + .motor_position(JointAxis::AngX, target_pos, stiffness, damping); + self } /// Configure both the target angle and target velocity of the motor. - pub fn configure_motor( - &mut self, + pub fn motor_axis( + mut self, target_pos: Real, target_vel: Real, stiffness: Real, damping: Real, - ) { - self.motor_target_vel = target_vel; - self.motor_target_pos = target_pos; - self.motor_stiffness = stiffness; - self.motor_damping = damping; + ) -> Self { + self.data = + self.data + .motor_axis(JointAxis::AngX, target_pos, target_vel, stiffness, damping); + self } - /// Estimates the current position of the motor angle. - pub fn estimate_motor_angle( - &self, - body_pos1: &Isometry, - body_pos2: &Isometry, - ) -> Real { - Self::estimate_motor_angle_from_params( - &(body_pos1 * self.local_axis1), - &(body_pos1 * self.basis1[0]), - &(body_pos2 * self.basis2[0]), - self.motor_last_angle, - ) + pub fn motor_max_impulse(mut self, max_impulse: Real) -> Self { + self.data = self.data.motor_max_impulse(JointAxis::AngX, max_impulse); + self } - /// Estimates the current position of the motor angle given the joint parameters. - pub fn estimate_motor_angle_from_params( - axis1: &Unit>, - tangent1: &Vector, - tangent2: &Vector, - motor_last_angle: Real, - ) -> Real { - let last_angle_cycles = (motor_last_angle / Real::two_pi()).trunc() * Real::two_pi(); - - // Measure the position between 0 and 2-pi - let new_angle = if tangent1.cross(&tangent2).dot(&axis1) < 0.0 { - Real::two_pi() - tangent1.angle(&tangent2) - } else { - tangent1.angle(&tangent2) - }; - - // The last angle between 0 and 2-pi - let last_angle_zero_two_pi = motor_last_angle - last_angle_cycles; - - // Figure out the smallest angle differance. - let mut angle_diff = new_angle - last_angle_zero_two_pi; - if angle_diff > Real::pi() { - angle_diff -= Real::two_pi() - } else if angle_diff < -Real::pi() { - angle_diff += Real::two_pi() - } - - motor_last_angle + angle_diff + #[must_use] + pub fn limit_axis(mut self, limits: [Real; 2]) -> Self { + self.data = self.data.limit_axis(JointAxis::AngX, limits); + self + } +} + +impl Into for RevoluteJoint { + fn into(self) -> JointData { + self.data } } diff --git a/src/dynamics/joint/spherical_joint.rs b/src/dynamics/joint/spherical_joint.rs new file mode 100644 index 0000000..581b959 --- /dev/null +++ b/src/dynamics/joint/spherical_joint.rs @@ -0,0 +1,91 @@ +use crate::dynamics::joint::{JointAxesMask, JointData}; +use crate::dynamics::{JointAxis, MotorModel}; +use crate::math::{Point, Real}; + +#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct SphericalJoint { + data: JointData, +} + +impl SphericalJoint { + pub fn new() -> Self { + let data = + JointData::default().lock_axes(JointAxesMask::X | JointAxesMask::Y | JointAxesMask::Z); + Self { data } + } + + pub fn data(&self) -> &JointData { + &self.data + } + + #[must_use] + pub fn local_anchor1(mut self, anchor1: Point) -> Self { + self.data = self.data.local_anchor1(anchor1); + self + } + + #[must_use] + pub fn local_anchor2(mut self, anchor2: Point) -> Self { + self.data = self.data.local_anchor2(anchor2); + self + } + + /// Set the spring-like model used by the motor to reach the desired target velocity and position. + pub fn motor_model(mut self, axis: JointAxis, model: MotorModel) -> Self { + self.data = self.data.motor_model(axis, model); + self + } + + /// Sets the target velocity this motor needs to reach. + pub fn motor_velocity(mut self, axis: JointAxis, target_vel: Real, factor: Real) -> Self { + self.data = self.data.motor_velocity(axis, target_vel, factor); + self + } + + /// Sets the target angle this motor needs to reach. + pub fn motor_position( + mut self, + axis: JointAxis, + target_pos: Real, + stiffness: Real, + damping: Real, + ) -> Self { + self.data = self + .data + .motor_position(axis, target_pos, stiffness, damping); + self + } + + /// Configure both the target angle and target velocity of the motor. + pub fn motor_axis( + mut self, + axis: JointAxis, + target_pos: Real, + target_vel: Real, + stiffness: Real, + damping: Real, + ) -> Self { + self.data = self + .data + .motor_axis(axis, target_pos, target_vel, stiffness, damping); + self + } + + pub fn motor_max_impulse(mut self, axis: JointAxis, max_impulse: Real) -> Self { + self.data = self.data.motor_max_impulse(axis, max_impulse); + self + } + + #[must_use] + pub fn limit_axis(mut self, axis: JointAxis, limits: [Real; 2]) -> Self { + self.data = self.data.limit_axis(axis, limits); + self + } +} + +impl Into for SphericalJoint { + fn into(self) -> JointData { + self.data + } +} diff --git a/src/dynamics/mod.rs b/src/dynamics/mod.rs index 825fa49..65c294c 100644 --- a/src/dynamics/mod.rs +++ b/src/dynamics/mod.rs @@ -1,4 +1,4 @@ -//! Structures related to dynamics: bodies, joints, etc. +//! Structures related to dynamics: bodies, impulse_joints, etc. pub use self::ccd::CCDSolver; pub use self::coefficient_combine_rule::CoefficientCombineRule; @@ -6,18 +6,7 @@ pub use self::integration_parameters::IntegrationParameters; pub use self::island_manager::IslandManager; pub(crate) use self::joint::JointGraphEdge; pub(crate) use self::joint::JointIndex; -#[cfg(feature = "dim3")] -pub use self::joint::RevoluteJoint; -pub use self::joint::{ - BallJoint, - FixedJoint, - Joint, - JointHandle, - JointParams, - JointSet, - PrismaticJoint, - SpringModel, // GenericJoint -}; +pub use self::joint::*; pub use self::rigid_body_components::*; #[cfg(not(feature = "parallel"))] pub(crate) use self::solver::IslandSolver; diff --git a/src/dynamics/rigid_body_components.rs b/src/dynamics/rigid_body_components.rs index a135a1c..e24cb57 100644 --- a/src/dynamics/rigid_body_components.rs +++ b/src/dynamics/rigid_body_components.rs @@ -4,9 +4,11 @@ use crate::geometry::{ ColliderChanges, ColliderHandle, ColliderMassProps, ColliderParent, ColliderPosition, ColliderShape, }; -use crate::math::{AngVector, AngularInertia, Isometry, Point, Real, Translation, Vector}; +use crate::math::{ + AngVector, AngularInertia, Isometry, Point, Real, Rotation, Translation, Vector, +}; use crate::parry::partitioning::IndexedData; -use crate::utils::{WCross, WDot}; +use crate::utils::{WAngularInertia, WCross, WDot}; use num::Zero; /// The unique handle of a rigid body added to a `RigidBodySet`. @@ -276,6 +278,13 @@ impl RigidBodyMassProps { crate::utils::inv(self.effective_inv_mass) } + /// The effective world-space angular inertia (that takes the potential rotation locking into account) of + /// this rigid-body. + #[must_use] + pub fn effective_angular_inertia(&self) -> AngularInertia { + self.effective_world_inv_inertia_sqrt.squared().inverse() + } + /// Update the world-space mass properties of `self`, taking into account the new position. pub fn update_world_mass_properties(&mut self, position: &Isometry) { self.world_com = self.local_mprops.world_com(&position); @@ -348,6 +357,51 @@ impl Default for RigidBodyVelocity { } impl RigidBodyVelocity { + /// Create a new rigid-body velocity component. + #[must_use] + pub fn new(linvel: Vector, angvel: AngVector) -> Self { + Self { linvel, angvel } + } + + /// Converts a slice to a rigid-body velocity. + /// + /// The slice must contain at least 3 elements: the `slice[0..2] contains + /// the linear velocity and the `slice[2]` contains the angular velocity. + #[must_use] + #[cfg(feature = "dim2")] + pub fn from_slice(slice: &[Real]) -> Self { + Self { + linvel: Vector::new(slice[0], slice[1]), + angvel: slice[2], + } + } + + /// Converts a slice to a rigid-body velocity. + /// + /// The slice must contain at least 6 elements: the `slice[0..3] contains + /// the linear velocity and the `slice[3..6]` contains the angular velocity. + #[must_use] + #[cfg(feature = "dim3")] + pub fn from_slice(slice: &[Real]) -> Self { + Self { + linvel: Vector::new(slice[0], slice[1], slice[2]), + angvel: AngVector::new(slice[3], slice[4], slice[5]), + } + } + + #[cfg(feature = "dim2")] + pub(crate) fn from_vectors(linvel: Vector, angvel: na::Vector1) -> Self { + Self { + linvel, + angvel: angvel.x, + } + } + + #[cfg(feature = "dim3")] + pub(crate) fn from_vectors(linvel: Vector, angvel: Vector) -> Self { + Self { linvel, angvel } + } + /// Velocities set to zero. #[must_use] pub fn zero() -> Self { @@ -357,6 +411,82 @@ impl RigidBodyVelocity { } } + /// This velocity seen as a slice. + /// + /// The linear part is stored first. + #[inline] + pub fn as_slice(&self) -> &[Real] { + self.as_vector().as_slice() + } + + /// This velocity seen as a mutable slice. + /// + /// The linear part is stored first. + #[inline] + pub fn as_mut_slice(&mut self) -> &mut [Real] { + self.as_vector_mut().as_mut_slice() + } + + /// This velocity seen as a vector. + /// + /// The linear part is stored first. + #[inline] + #[cfg(feature = "dim2")] + pub fn as_vector(&self) -> &na::Vector3 { + unsafe { std::mem::transmute(self) } + } + + /// This velocity seen as a mutable vector. + /// + /// The linear part is stored first. + #[inline] + #[cfg(feature = "dim2")] + pub fn as_vector_mut(&mut self) -> &mut na::Vector3 { + unsafe { std::mem::transmute(self) } + } + + /// This velocity seen as a vector. + /// + /// The linear part is stored first. + #[inline] + #[cfg(feature = "dim3")] + pub fn as_vector(&self) -> &na::Vector6 { + unsafe { std::mem::transmute(self) } + } + + /// This velocity seen as a mutable vector. + /// + /// The linear part is stored first. + #[inline] + #[cfg(feature = "dim3")] + pub fn as_vector_mut(&mut self) -> &mut na::Vector6 { + unsafe { std::mem::transmute(self) } + } + + /// Return `self` transformed by `transform`. + #[must_use] + pub fn transformed(self, transform: &Isometry) -> Self { + Self { + linvel: transform * self.linvel, + #[cfg(feature = "dim2")] + angvel: self.angvel, + #[cfg(feature = "dim3")] + angvel: transform * self.angvel, + } + } + + /// Return `self` rotated by `rotation`. + #[must_use] + pub fn rotated(self, rotation: &Rotation) -> Self { + Self { + linvel: rotation * self.linvel, + #[cfg(feature = "dim2")] + angvel: self.angvel, + #[cfg(feature = "dim3")] + angvel: rotation * self.angvel, + } + } + /// The approximate kinetic energy of this rigid-body. /// /// This approximation does not take the rigid-body's mass and angular inertia @@ -471,6 +601,38 @@ impl RigidBodyVelocity { } } +impl std::ops::Mul for RigidBodyVelocity { + type Output = Self; + + #[must_use] + fn mul(self, rhs: Real) -> Self { + RigidBodyVelocity { + linvel: self.linvel * rhs, + angvel: self.angvel * rhs, + } + } +} + +impl std::ops::Add for RigidBodyVelocity { + type Output = Self; + + #[must_use] + fn add(self, rhs: Self) -> Self { + RigidBodyVelocity { + linvel: self.linvel + rhs.linvel, + angvel: self.angvel + rhs.angvel, + } + } +} + +impl std::ops::AddAssign for RigidBodyVelocity { + #[must_use] + fn add_assign(&mut self, rhs: Self) { + self.linvel += rhs.linvel; + self.angvel += rhs.angvel; + } +} + #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] #[derive(Clone, Debug, Copy, PartialEq)] /// Damping factors to progressively slow down a rigid-body. @@ -784,7 +946,7 @@ impl Default for RigidBodyActivation { } impl RigidBodyActivation { - /// The default amount of energy bellow which a body can be put to sleep by nphysics. + /// The default amount of energy bellow which a body can be put to sleep by rapier. pub fn default_threshold() -> Real { 0.01 } diff --git a/src/dynamics/rigid_body_set.rs b/src/dynamics/rigid_body_set.rs index 607f7ff..34f7bdf 100644 --- a/src/dynamics/rigid_body_set.rs +++ b/src/dynamics/rigid_body_set.rs @@ -1,11 +1,11 @@ use crate::data::{Arena, ComponentSet, ComponentSetMut, ComponentSetOption}; use crate::dynamics::{ - IslandManager, RigidBodyActivation, RigidBodyColliders, RigidBodyDominance, RigidBodyHandle, - RigidBodyType, + ImpulseJointSet, RigidBody, RigidBodyCcd, RigidBodyChanges, RigidBodyDamping, RigidBodyForces, + RigidBodyIds, RigidBodyMassProps, RigidBodyPosition, RigidBodyVelocity, }; use crate::dynamics::{ - JointSet, RigidBody, RigidBodyCcd, RigidBodyChanges, RigidBodyDamping, RigidBodyForces, - RigidBodyIds, RigidBodyMassProps, RigidBodyPosition, RigidBodyVelocity, + IslandManager, MultibodyJointSet, RigidBodyActivation, RigidBodyColliders, RigidBodyDominance, + RigidBodyHandle, RigidBodyType, }; use crate::geometry::ColliderSet; use std::ops::{Index, IndexMut}; @@ -132,13 +132,14 @@ impl RigidBodySet { handle } - /// Removes a rigid-body, and all its attached colliders and joints, from these sets. + /// Removes a rigid-body, and all its attached colliders and impulse_joints, from these sets. pub fn remove( &mut self, handle: RigidBodyHandle, islands: &mut IslandManager, colliders: &mut ColliderSet, - joints: &mut JointSet, + impulse_joints: &mut ImpulseJointSet, + multibody_joints: &mut MultibodyJointSet, ) -> Option { let rb = self.bodies.remove(handle.0)?; /* @@ -154,9 +155,10 @@ impl RigidBodySet { } /* - * Remove joints attached to this rigid-body. + * Remove impulse_joints attached to this rigid-body. */ - joints.remove_joints_attached_to_rigid_body(handle, islands, self); + impulse_joints.remove_joints_attached_to_rigid_body(handle, islands, self); + multibody_joints.remove_articulations_attached_to_rigid_body(handle, islands, self); Some(rb) } diff --git a/src/dynamics/solver/categorization.rs b/src/dynamics/solver/categorization.rs index 366a5b3..c5b2601 100644 --- a/src/dynamics/solver/categorization.rs +++ b/src/dynamics/solver/categorization.rs @@ -1,18 +1,33 @@ use crate::data::ComponentSet; -use crate::dynamics::{JointGraphEdge, JointIndex, RigidBodyType}; +use crate::dynamics::{JointGraphEdge, JointIndex, MultibodyJointSet, RigidBodyType}; use crate::geometry::{ContactManifold, ContactManifoldIndex}; pub(crate) fn categorize_contacts( _bodies: &impl ComponentSet, // Unused but useful to simplify the parallel code. + multibody_joints: &MultibodyJointSet, manifolds: &[&mut ContactManifold], manifold_indices: &[ContactManifoldIndex], out_ground: &mut Vec, out_not_ground: &mut Vec, + out_generic: &mut Vec, + _unused: &mut Vec, // Unused but useful to simplify the parallel code. ) { for manifold_i in manifold_indices { let manifold = &manifolds[*manifold_i]; - if manifold.data.relative_dominance != 0 { + if manifold + .data + .rigid_body1 + .map(|rb| multibody_joints.rigid_body_link(rb)) + .is_some() + || manifold + .data + .rigid_body1 + .map(|rb| multibody_joints.rigid_body_link(rb)) + .is_some() + { + out_generic.push(*manifold_i); + } else if manifold.data.relative_dominance != 0 { out_ground.push(*manifold_i) } else { out_not_ground.push(*manifold_i) @@ -22,17 +37,28 @@ pub(crate) fn categorize_contacts( pub(crate) fn categorize_joints( bodies: &impl ComponentSet, - joints: &[JointGraphEdge], + multibody_joints: &MultibodyJointSet, + impulse_joints: &[JointGraphEdge], joint_indices: &[JointIndex], ground_joints: &mut Vec, nonground_joints: &mut Vec, + generic_ground_joints: &mut Vec, + generic_nonground_joints: &mut Vec, ) { for joint_i in joint_indices { - let joint = &joints[*joint_i].weight; + let joint = &impulse_joints[*joint_i].weight; let status1 = bodies.index(joint.body1.0); let status2 = bodies.index(joint.body2.0); - if !status1.is_dynamic() || !status2.is_dynamic() { + if multibody_joints.rigid_body_link(joint.body1).is_some() + || multibody_joints.rigid_body_link(joint.body2).is_some() + { + if !status1.is_dynamic() || !status2.is_dynamic() { + generic_ground_joints.push(*joint_i); + } else { + generic_nonground_joints.push(*joint_i); + } + } else if !status1.is_dynamic() || !status2.is_dynamic() { ground_joints.push(*joint_i); } else { nonground_joints.push(*joint_i); diff --git a/src/dynamics/solver/delta_vel.rs b/src/dynamics/solver/delta_vel.rs index b457020..9160f2e 100644 --- a/src/dynamics/solver/delta_vel.rs +++ b/src/dynamics/solver/delta_vel.rs @@ -1,14 +1,34 @@ -use crate::math::{AngVector, Vector}; +use crate::math::{AngVector, Vector, SPATIAL_DIM}; +use na::{DVectorSlice, DVectorSliceMut}; use na::{Scalar, SimdRealField}; use std::ops::AddAssign; #[derive(Copy, Clone, Debug)] +#[repr(C)] //#[repr(align(64))] -pub(crate) struct DeltaVel { +pub struct DeltaVel { pub linear: Vector, pub angular: AngVector, } +impl DeltaVel { + pub fn as_slice(&self) -> &[N; SPATIAL_DIM] { + unsafe { std::mem::transmute(self) } + } + + pub fn as_mut_slice(&mut self) -> &mut [N; SPATIAL_DIM] { + unsafe { std::mem::transmute(self) } + } + + pub fn as_vector_slice(&self) -> DVectorSlice { + DVectorSlice::from_slice(&self.as_slice()[..], SPATIAL_DIM) + } + + pub fn as_vector_slice_mut(&mut self) -> DVectorSliceMut { + DVectorSliceMut::from_slice(&mut self.as_mut_slice()[..], SPATIAL_DIM) + } +} + impl DeltaVel { pub fn zero() -> Self { Self { diff --git a/src/dynamics/solver/generic_velocity_constraint.rs b/src/dynamics/solver/generic_velocity_constraint.rs new file mode 100644 index 0000000..fb06335 --- /dev/null +++ b/src/dynamics/solver/generic_velocity_constraint.rs @@ -0,0 +1,377 @@ +use crate::data::{BundleSet, ComponentSet}; +use crate::dynamics::solver::{GenericRhs, VelocityConstraint}; +use crate::dynamics::{ + IntegrationParameters, MultibodyJointSet, RigidBodyIds, RigidBodyMassProps, RigidBodyType, + RigidBodyVelocity, +}; +use crate::geometry::{ContactManifold, ContactManifoldIndex}; +use crate::math::{Real, DIM, MAX_MANIFOLD_POINTS}; +use crate::utils::{WAngularInertia, WCross, WDot}; + +use super::{DeltaVel, VelocityConstraintElement, VelocityConstraintNormalPart}; +#[cfg(feature = "dim2")] +use crate::utils::WBasis; +use na::DVector; + +#[derive(Copy, Clone, Debug)] +pub(crate) struct GenericVelocityConstraint { + // We just build the generic constraint on top of the velocity constraint, + // adding some information we can use in the generic case. + pub velocity_constraint: VelocityConstraint, + pub j_id: usize, + pub ndofs1: usize, + pub ndofs2: usize, + pub generic_constraint_mask: u8, +} + +impl GenericVelocityConstraint { + pub fn generate( + params: &IntegrationParameters, + manifold_id: ContactManifoldIndex, + manifold: &ContactManifold, + bodies: &Bodies, + multibodies: &MultibodyJointSet, + out_constraints: &mut Vec, + jacobians: &mut DVector, + jacobian_id: &mut usize, + push: bool, + ) where + Bodies: ComponentSet + + ComponentSet + + ComponentSet + + ComponentSet, + { + let inv_dt = params.inv_dt(); + let erp_inv_dt = params.erp_inv_dt(); + + let handle1 = manifold.data.rigid_body1.unwrap(); + let handle2 = manifold.data.rigid_body2.unwrap(); + let (rb_ids1, rb_vels1, rb_mprops1, rb_type1): ( + &RigidBodyIds, + &RigidBodyVelocity, + &RigidBodyMassProps, + &RigidBodyType, + ) = bodies.index_bundle(handle1.0); + let (rb_ids2, rb_vels2, rb_mprops2, rb_type2): ( + &RigidBodyIds, + &RigidBodyVelocity, + &RigidBodyMassProps, + &RigidBodyType, + ) = bodies.index_bundle(handle2.0); + + let multibody1 = multibodies + .rigid_body_link(handle1) + .map(|m| (&multibodies[m.multibody], m.id)); + let multibody2 = multibodies + .rigid_body_link(handle2) + .map(|m| (&multibodies[m.multibody], m.id)); + let mj_lambda1 = multibody1 + .map(|mb| mb.0.solver_id) + .unwrap_or(if rb_type1.is_dynamic() { + rb_ids1.active_set_offset + } else { + 0 + }); + let mj_lambda2 = multibody2 + .map(|mb| mb.0.solver_id) + .unwrap_or(if rb_type2.is_dynamic() { + rb_ids2.active_set_offset + } else { + 0 + }); + let force_dir1 = -manifold.data.normal; + + #[cfg(feature = "dim2")] + let tangents1 = force_dir1.orthonormal_basis(); + #[cfg(feature = "dim3")] + let tangents1 = super::compute_tangent_contact_directions( + &force_dir1, + &rb_vels1.linvel, + &rb_vels2.linvel, + ); + + let multibodies_ndof = multibody1.map(|m| m.0.ndofs()).unwrap_or(0) + + multibody2.map(|m| m.0.ndofs()).unwrap_or(0); + // For each solver contact we generate DIM constraints, and each constraints appends + // the multibodies jacobian and weighted jacobians + let required_jacobian_len = + *jacobian_id + manifold.data.solver_contacts.len() * multibodies_ndof * 2 * DIM; + + if jacobians.nrows() < required_jacobian_len { + jacobians.resize_vertically_mut(required_jacobian_len, 0.0); + } + + for (_l, manifold_points) in manifold + .data + .solver_contacts + .chunks(MAX_MANIFOLD_POINTS) + .enumerate() + { + let chunk_j_id = *jacobian_id; + let mut constraint = VelocityConstraint { + dir1: force_dir1, + #[cfg(feature = "dim3")] + tangent1: tangents1[0], + elements: [VelocityConstraintElement::zero(); MAX_MANIFOLD_POINTS], + im1: if rb_type1.is_dynamic() { + rb_mprops1.effective_inv_mass + } else { + 0.0 + }, + im2: if rb_type2.is_dynamic() { + rb_mprops2.effective_inv_mass + } else { + 0.0 + }, + limit: 0.0, + mj_lambda1, + mj_lambda2, + manifold_id, + manifold_contact_id: [0; MAX_MANIFOLD_POINTS], + num_contacts: manifold_points.len() as u8, + }; + + for k in 0..manifold_points.len() { + let manifold_point = &manifold_points[k]; + let dp1 = manifold_point.point - rb_mprops1.world_com; + let dp2 = manifold_point.point - rb_mprops2.world_com; + + let vel1 = rb_vels1.linvel + rb_vels1.angvel.gcross(dp1); + let vel2 = rb_vels2.linvel + rb_vels2.angvel.gcross(dp2); + + constraint.limit = manifold_point.friction; + constraint.manifold_contact_id[k] = manifold_point.contact_id; + + // Normal part. + { + let torque_dir1 = dp1.gcross(force_dir1); + let torque_dir2 = dp2.gcross(-force_dir1); + + let gcross1 = if rb_type1.is_dynamic() { + rb_mprops1 + .effective_world_inv_inertia_sqrt + .transform_vector(torque_dir1) + } else { + na::zero() + }; + let gcross2 = if rb_type2.is_dynamic() { + rb_mprops2 + .effective_world_inv_inertia_sqrt + .transform_vector(torque_dir2) + } else { + na::zero() + }; + + let inv_r1 = if let Some((mb1, link_id1)) = multibody1.as_ref() { + mb1.fill_jacobians( + *link_id1, + force_dir1, + #[cfg(feature = "dim2")] + na::vector!(torque_dir1), + #[cfg(feature = "dim3")] + torque_dir1, + jacobian_id, + jacobians, + ) + .0 + } else if rb_type1.is_dynamic() { + rb_mprops1.effective_inv_mass + gcross1.gdot(gcross1) + } else { + 0.0 + }; + + let inv_r2 = if let Some((mb2, link_id2)) = multibody2.as_ref() { + mb2.fill_jacobians( + *link_id2, + -force_dir1, + #[cfg(feature = "dim2")] + na::vector!(torque_dir2), + #[cfg(feature = "dim3")] + torque_dir2, + jacobian_id, + jacobians, + ) + .0 + } else if rb_type2.is_dynamic() { + rb_mprops2.effective_inv_mass + gcross2.gdot(gcross2) + } else { + 0.0 + }; + + let r = crate::utils::inv(inv_r1 + inv_r2); + + let is_bouncy = manifold_point.is_bouncy() as u32 as Real; + let is_resting = 1.0 - is_bouncy; + + let mut rhs_wo_bias = (1.0 + is_bouncy * manifold_point.restitution) + * (vel1 - vel2).dot(&force_dir1); + rhs_wo_bias += manifold_point.dist.max(0.0) * inv_dt; + rhs_wo_bias *= is_bouncy + is_resting * params.velocity_solve_fraction; + let rhs_bias = + /* is_resting * */ erp_inv_dt * manifold_point.dist.min(0.0); + + constraint.elements[k].normal_part = VelocityConstraintNormalPart { + gcross1, + gcross2, + rhs: rhs_wo_bias + rhs_bias, + rhs_wo_bias, + impulse: na::zero(), + r, + }; + } + + // Tangent parts. + { + constraint.elements[k].tangent_part.impulse = na::zero(); + + for j in 0..DIM - 1 { + let torque_dir1 = dp1.gcross(tangents1[j]); + let gcross1 = if rb_type1.is_dynamic() { + rb_mprops1 + .effective_world_inv_inertia_sqrt + .transform_vector(torque_dir1) + } else { + na::zero() + }; + constraint.elements[k].tangent_part.gcross1[j] = gcross1; + + let torque_dir2 = dp2.gcross(-tangents1[j]); + let gcross2 = if rb_type2.is_dynamic() { + rb_mprops2 + .effective_world_inv_inertia_sqrt + .transform_vector(torque_dir2) + } else { + na::zero() + }; + constraint.elements[k].tangent_part.gcross2[j] = gcross2; + + let inv_r1 = if let Some((mb1, link_id1)) = multibody1.as_ref() { + mb1.fill_jacobians( + *link_id1, + tangents1[j], + #[cfg(feature = "dim2")] + na::vector![torque_dir1], + #[cfg(feature = "dim3")] + torque_dir1, + jacobian_id, + jacobians, + ) + .0 + } else if rb_type1.is_dynamic() { + rb_mprops1.effective_inv_mass + gcross1.gdot(gcross1) + } else { + 0.0 + }; + + let inv_r2 = if let Some((mb2, link_id2)) = multibody2.as_ref() { + mb2.fill_jacobians( + *link_id2, + -tangents1[j], + #[cfg(feature = "dim2")] + na::vector![torque_dir2], + #[cfg(feature = "dim3")] + torque_dir2, + jacobian_id, + jacobians, + ) + .0 + } else if rb_type2.is_dynamic() { + rb_mprops2.effective_inv_mass + gcross2.gdot(gcross2) + } else { + 0.0 + }; + + let r = crate::utils::inv(inv_r1 + inv_r2); + + let rhs = + (vel1 - vel2 + manifold_point.tangent_velocity).dot(&tangents1[j]); + + constraint.elements[k].tangent_part.rhs[j] = rhs; + constraint.elements[k].tangent_part.r[j] = r; + } + } + } + + let ndofs1 = multibody1.map(|mb| mb.0.ndofs()).unwrap_or(0); + let ndofs2 = multibody2.map(|mb| mb.0.ndofs()).unwrap_or(0); + // NOTE: we use the generic constraint for non-dynamic bodies because this will + // reduce all ops to nothing because its ndofs will be zero. + let generic_constraint_mask = (multibody1.is_some() as u8) + | ((multibody2.is_some() as u8) << 1) + | (!rb_type1.is_dynamic() as u8) + | ((!rb_type2.is_dynamic() as u8) << 1); + + let constraint = GenericVelocityConstraint { + velocity_constraint: constraint, + j_id: chunk_j_id, + ndofs1, + ndofs2, + generic_constraint_mask, + }; + + if push { + out_constraints.push(constraint); + } else { + out_constraints[manifold.data.constraint_index + _l] = constraint; + } + } + } + + pub fn solve( + &mut self, + jacobians: &DVector, + mj_lambdas: &mut [DeltaVel], + generic_mj_lambdas: &mut DVector, + solve_restitution: bool, + solve_friction: bool, + ) { + let mut mj_lambda1 = if self.generic_constraint_mask & 0b01 == 0 { + GenericRhs::DeltaVel(mj_lambdas[self.velocity_constraint.mj_lambda1 as usize]) + } else { + GenericRhs::GenericId(self.velocity_constraint.mj_lambda1 as usize) + }; + + let mut mj_lambda2 = if self.generic_constraint_mask & 0b10 == 0 { + GenericRhs::DeltaVel(mj_lambdas[self.velocity_constraint.mj_lambda2 as usize]) + } else { + GenericRhs::GenericId(self.velocity_constraint.mj_lambda2 as usize) + }; + + let elements = &mut self.velocity_constraint.elements + [..self.velocity_constraint.num_contacts as usize]; + VelocityConstraintElement::generic_solve_group( + elements, + jacobians, + &self.velocity_constraint.dir1, + #[cfg(feature = "dim3")] + &self.velocity_constraint.tangent1, + self.velocity_constraint.im1, + self.velocity_constraint.im2, + self.velocity_constraint.limit, + self.ndofs1, + self.ndofs2, + self.j_id, + &mut mj_lambda1, + &mut mj_lambda2, + generic_mj_lambdas, + solve_restitution, + solve_friction, + ); + + if let GenericRhs::DeltaVel(mj_lambda1) = mj_lambda1 { + mj_lambdas[self.velocity_constraint.mj_lambda1 as usize] = mj_lambda1; + } + + if let GenericRhs::DeltaVel(mj_lambda2) = mj_lambda2 { + mj_lambdas[self.velocity_constraint.mj_lambda2 as usize] = mj_lambda2; + } + } + + pub fn writeback_impulses(&self, manifolds_all: &mut [&mut ContactManifold]) { + self.velocity_constraint.writeback_impulses(manifolds_all); + } + + pub fn remove_bias_from_rhs(&mut self) { + self.velocity_constraint.remove_bias_from_rhs(); + } +} diff --git a/src/dynamics/solver/generic_velocity_constraint_element.rs b/src/dynamics/solver/generic_velocity_constraint_element.rs new file mode 100644 index 0000000..150be8b --- /dev/null +++ b/src/dynamics/solver/generic_velocity_constraint_element.rs @@ -0,0 +1,348 @@ +use super::DeltaVel; +use crate::dynamics::solver::{ + VelocityConstraintElement, VelocityConstraintNormalPart, VelocityConstraintTangentPart, +}; +use crate::math::{AngVector, Real, Vector, DIM}; +use crate::utils::WDot; +use na::DVector; +#[cfg(feature = "dim2")] +use {crate::utils::WBasis, na::SimdPartialOrd}; + +pub(crate) enum GenericRhs { + DeltaVel(DeltaVel), + GenericId(usize), +} + +// Offset between the jacobians of two consecutive constraints. +#[inline(always)] +fn j_step(ndofs1: usize, ndofs2: usize) -> usize { + (ndofs1 + ndofs2) * 2 +} + +#[inline(always)] +fn j_id1(j_id: usize, _ndofs1: usize, _ndofs2: usize) -> usize { + j_id +} + +#[inline(always)] +fn j_id2(j_id: usize, ndofs1: usize, _ndofs2: usize) -> usize { + j_id + ndofs1 * 2 +} + +#[inline(always)] +fn normal_j_id(j_id: usize, _ndofs1: usize, _ndofs2: usize) -> usize { + j_id +} + +#[inline(always)] +fn tangent_j_id(j_id: usize, ndofs1: usize, ndofs2: usize) -> usize { + j_id + (ndofs1 + ndofs2) * 2 +} + +impl GenericRhs { + #[inline(always)] + fn dimpulse( + &self, + j_id: usize, + ndofs: usize, + jacobians: &DVector, + dir: &Vector, + gcross: &AngVector, + mj_lambdas: &DVector, + ) -> Real { + match self { + GenericRhs::DeltaVel(rhs) => dir.dot(&rhs.linear) + gcross.gdot(rhs.angular), + GenericRhs::GenericId(mj_lambda) => { + let j = jacobians.rows(j_id, ndofs); + let rhs = mj_lambdas.rows(*mj_lambda, ndofs); + j.dot(&rhs) + } + } + } + + #[inline(always)] + fn apply_impulse( + &mut self, + j_id: usize, + ndofs: usize, + impulse: Real, + jacobians: &DVector, + dir: &Vector, + gcross: &AngVector, + mj_lambdas: &mut DVector, + inv_mass: Real, + ) { + match self { + GenericRhs::DeltaVel(rhs) => { + rhs.linear += dir * (inv_mass * impulse); + rhs.angular += gcross * impulse; + } + GenericRhs::GenericId(mj_lambda) => { + let wj_id = j_id + ndofs; + let wj = jacobians.rows(wj_id, ndofs); + let mut rhs = mj_lambdas.rows_mut(*mj_lambda, ndofs); + rhs.axpy(impulse, &wj, 1.0); + } + } + } +} + +impl VelocityConstraintTangentPart { + #[inline] + pub fn generic_solve( + &mut self, + j_id: usize, + jacobians: &DVector, + tangents1: [&Vector; DIM - 1], + im1: Real, + im2: Real, + ndofs1: usize, + ndofs2: usize, + limit: Real, + mj_lambda1: &mut GenericRhs, + mj_lambda2: &mut GenericRhs, + mj_lambdas: &mut DVector, + ) { + let j_id1 = j_id1(j_id, ndofs1, ndofs2); + let j_id2 = j_id2(j_id, ndofs1, ndofs2); + #[cfg(feature = "dim3")] + let j_step = j_step(ndofs1, ndofs2); + + #[cfg(feature = "dim2")] + { + let dimpulse_0 = mj_lambda1.dimpulse( + j_id1, + ndofs1, + jacobians, + &tangents1[0], + &self.gcross1[0], + mj_lambdas, + ) + mj_lambda2.dimpulse( + j_id2, + ndofs2, + jacobians, + &-tangents1[0], + &self.gcross2[0], + mj_lambdas, + ) + self.rhs[0]; + + let new_impulse = (self.impulse[0] - self.r[0] * dimpulse_0).simd_clamp(-limit, limit); + let dlambda = new_impulse - self.impulse[0]; + self.impulse[0] = new_impulse; + + mj_lambda1.apply_impulse( + j_id1, + ndofs1, + dlambda, + jacobians, + &tangents1[0], + &self.gcross1[0], + mj_lambdas, + im1, + ); + mj_lambda2.apply_impulse( + j_id2, + ndofs2, + dlambda, + jacobians, + &-tangents1[0], + &self.gcross2[0], + mj_lambdas, + im2, + ); + } + + #[cfg(feature = "dim3")] + { + let dimpulse_0 = mj_lambda1.dimpulse( + j_id1, + ndofs1, + jacobians, + &tangents1[0], + &self.gcross1[0], + mj_lambdas, + ) + mj_lambda2.dimpulse( + j_id2, + ndofs2, + jacobians, + &-tangents1[0], + &self.gcross2[0], + mj_lambdas, + ) + self.rhs[0]; + let dimpulse_1 = mj_lambda1.dimpulse( + j_id1 + j_step, + ndofs1, + jacobians, + &tangents1[1], + &self.gcross1[1], + mj_lambdas, + ) + mj_lambda2.dimpulse( + j_id2 + j_step, + ndofs2, + jacobians, + &-tangents1[1], + &self.gcross2[1], + mj_lambdas, + ) + self.rhs[1]; + + let new_impulse = na::Vector2::new( + self.impulse[0] - self.r[0] * dimpulse_0, + self.impulse[1] - self.r[1] * dimpulse_1, + ); + let new_impulse = new_impulse.cap_magnitude(limit); + + let dlambda = new_impulse - self.impulse; + self.impulse = new_impulse; + + mj_lambda1.apply_impulse( + j_id1, + ndofs1, + dlambda[0], + jacobians, + &tangents1[0], + &self.gcross1[0], + mj_lambdas, + im1, + ); + mj_lambda1.apply_impulse( + j_id1 + j_step, + ndofs1, + dlambda[1], + jacobians, + &tangents1[1], + &self.gcross1[1], + mj_lambdas, + im1, + ); + + mj_lambda2.apply_impulse( + j_id2, + ndofs2, + dlambda[0], + jacobians, + &-tangents1[0], + &self.gcross2[0], + mj_lambdas, + im2, + ); + mj_lambda2.apply_impulse( + j_id2 + j_step, + ndofs2, + dlambda[1], + jacobians, + &-tangents1[1], + &self.gcross2[1], + mj_lambdas, + im2, + ); + } + } +} + +impl VelocityConstraintNormalPart { + #[inline] + pub fn generic_solve( + &mut self, + j_id: usize, + jacobians: &DVector, + dir1: &Vector, + im1: Real, + im2: Real, + ndofs1: usize, + ndofs2: usize, + mj_lambda1: &mut GenericRhs, + mj_lambda2: &mut GenericRhs, + mj_lambdas: &mut DVector, + ) { + let j_id1 = j_id1(j_id, ndofs1, ndofs2); + let j_id2 = j_id2(j_id, ndofs1, ndofs2); + + let dimpulse = + mj_lambda1.dimpulse(j_id1, ndofs1, jacobians, &dir1, &self.gcross1, mj_lambdas) + + mj_lambda2.dimpulse(j_id2, ndofs2, jacobians, &-dir1, &self.gcross2, mj_lambdas) + + self.rhs; + + let new_impulse = (self.impulse - self.r * dimpulse).max(0.0); + let dlambda = new_impulse - self.impulse; + self.impulse = new_impulse; + + mj_lambda1.apply_impulse( + j_id1, + ndofs1, + dlambda, + jacobians, + &dir1, + &self.gcross1, + mj_lambdas, + im1, + ); + mj_lambda2.apply_impulse( + j_id2, + ndofs2, + dlambda, + jacobians, + &-dir1, + &self.gcross2, + mj_lambdas, + im2, + ); + } +} + +impl VelocityConstraintElement { + #[inline] + pub fn generic_solve_group( + elements: &mut [Self], + jacobians: &DVector, + dir1: &Vector, + #[cfg(feature = "dim3")] tangent1: &Vector, + im1: Real, + im2: Real, + limit: Real, + // ndofs is 0 for a non-multibody body, or a multibody with zero + // degrees of freedom. + ndofs1: usize, + ndofs2: usize, + // Jacobian index of the first constraint. + j_id: usize, + mj_lambda1: &mut GenericRhs, + mj_lambda2: &mut GenericRhs, + mj_lambdas: &mut DVector, + solve_restitution: bool, + solve_friction: bool, + ) { + let j_step = j_step(ndofs1, ndofs2) * DIM; + + // Solve penetration. + if solve_restitution { + let mut nrm_j_id = normal_j_id(j_id, ndofs1, ndofs2); + + for element in elements.iter_mut() { + element.normal_part.generic_solve( + nrm_j_id, jacobians, &dir1, im1, im2, ndofs1, ndofs2, mj_lambda1, mj_lambda2, + mj_lambdas, + ); + nrm_j_id += j_step; + } + } + + // Solve friction. + if solve_friction { + #[cfg(feature = "dim3")] + let tangents1 = [tangent1, &dir1.cross(&tangent1)]; + #[cfg(feature = "dim2")] + let tangents1 = [&dir1.orthonormal_vector()]; + let mut tng_j_id = tangent_j_id(j_id, ndofs1, ndofs2); + + for element in elements.iter_mut() { + let limit = limit * element.normal_part.impulse; + let part = &mut element.tangent_part; + part.generic_solve( + tng_j_id, jacobians, tangents1, im1, im2, ndofs1, ndofs2, limit, mj_lambda1, + mj_lambda2, mj_lambdas, + ); + tng_j_id += j_step; + } + } + } +} diff --git a/src/dynamics/solver/interaction_groups.rs b/src/dynamics/solver/interaction_groups.rs index c6baea1..67cc805 100644 --- a/src/dynamics/solver/interaction_groups.rs +++ b/src/dynamics/solver/interaction_groups.rs @@ -216,12 +216,13 @@ impl InteractionGroups { ) where Bodies: ComponentSet + ComponentSet, { - // NOTE: in 3D we have up to 10 different joint types. - // In 2D we only have 5 joint types. + // TODO: right now, we only sort based on the axes locked by the joint. + // We could also take motors and limits into account in the future (most of + // the SIMD constraints generation for motors and limits is already implemented). #[cfg(feature = "dim3")] - const NUM_JOINT_TYPES: usize = 10; + const NUM_JOINT_TYPES: usize = 64; #[cfg(feature = "dim2")] - const NUM_JOINT_TYPES: usize = 5; + const NUM_JOINT_TYPES: usize = 8; // The j-th bit of joint_type_conflicts[i] indicates that the // j-th bucket contains a joint with a type different than `i`. @@ -254,13 +255,13 @@ impl InteractionGroups { continue; } - if !interaction.supports_simd_constraints() { + if !interaction.data.supports_simd_constraints() { // This joint does not support simd constraints yet. self.nongrouped_interactions.push(*interaction_i); continue; } - let ijoint = interaction.params.type_id(); + let ijoint = interaction.data.locked_axes.bits() as usize; let i1 = ids1.active_set_offset; let i2 = ids2.active_set_offset; let conflicts = diff --git a/src/dynamics/solver/island_solver.rs b/src/dynamics/solver/island_solver.rs index 912fe1d..a862480 100644 --- a/src/dynamics/solver/island_solver.rs +++ b/src/dynamics/solver/island_solver.rs @@ -1,9 +1,8 @@ -use super::{PositionSolver, VelocitySolver}; +use super::VelocitySolver; use crate::counters::Counters; use crate::data::{BundleSet, ComponentSet, ComponentSetMut}; use crate::dynamics::solver::{ - AnyJointPositionConstraint, AnyJointVelocityConstraint, AnyPositionConstraint, - AnyVelocityConstraint, SolverConstraints, + AnyJointVelocityConstraint, AnyVelocityConstraint, GenericVelocityConstraint, SolverConstraints, }; use crate::dynamics::{ IntegrationParameters, JointGraphEdge, JointIndex, RigidBodyDamping, RigidBodyForces, @@ -11,12 +10,12 @@ use crate::dynamics::{ }; use crate::dynamics::{IslandManager, RigidBodyVelocity}; use crate::geometry::{ContactManifold, ContactManifoldIndex}; +use crate::prelude::{MultibodyJointSet, RigidBodyActivation}; pub struct IslandSolver { - contact_constraints: SolverConstraints, - joint_constraints: SolverConstraints, + contact_constraints: SolverConstraints, + joint_constraints: SolverConstraints, velocity_solver: VelocitySolver, - position_solver: PositionSolver, } impl Default for IslandSolver { @@ -31,33 +30,10 @@ impl IslandSolver { contact_constraints: SolverConstraints::new(), joint_constraints: SolverConstraints::new(), velocity_solver: VelocitySolver::new(), - position_solver: PositionSolver::new(), } } - pub fn solve_position_constraints( - &mut self, - island_id: usize, - islands: &IslandManager, - counters: &mut Counters, - params: &IntegrationParameters, - bodies: &mut Bodies, - ) where - Bodies: ComponentSet + ComponentSetMut, - { - counters.solver.position_resolution_time.resume(); - self.position_solver.solve( - island_id, - params, - islands, - bodies, - &self.contact_constraints.position_constraints, - &self.joint_constraints.position_constraints, - ); - counters.solver.position_resolution_time.pause(); - } - - pub fn init_constraints_and_solve_velocity_constraints( + pub fn init_and_solve( &mut self, island_id: usize, counters: &mut Counters, @@ -66,31 +42,64 @@ impl IslandSolver { bodies: &mut Bodies, manifolds: &mut [&mut ContactManifold], manifold_indices: &[ContactManifoldIndex], - joints: &mut [JointGraphEdge], + impulse_joints: &mut [JointGraphEdge], joint_indices: &[JointIndex], + multibody_joints: &mut MultibodyJointSet, ) where Bodies: ComponentSet + ComponentSetMut + ComponentSetMut - + ComponentSet + + ComponentSetMut + + ComponentSetMut + ComponentSet + ComponentSet + ComponentSet, { - let has_constraints = manifold_indices.len() != 0 || joint_indices.len() != 0; + let mut has_constraints = manifold_indices.len() != 0 || joint_indices.len() != 0; + if !has_constraints { + // Check if the multibody_joints have internal constraints. + for handle in islands.active_island(island_id) { + if let Some(link) = multibody_joints.rigid_body_link(*handle) { + let multibody = multibody_joints.get_multibody(link.multibody).unwrap(); + + if (link.id == 0 || link.id == 1 && !multibody.root_is_dynamic) + && multibody.has_active_internal_constraints() + { + has_constraints = true; + break; + } + } + } + } if has_constraints { + // Init the solver id for multibody_joints. + // We need that for building the constraints. + let mut solver_id = 0; + for (_, multibody) in multibody_joints.multibodies.iter_mut() { + multibody.solver_id = solver_id; + solver_id += multibody.ndofs(); + } + counters.solver.velocity_assembly_time.resume(); self.contact_constraints.init( island_id, params, islands, bodies, + multibody_joints, manifolds, manifold_indices, ); - self.joint_constraints - .init(island_id, params, islands, bodies, joints, joint_indices); + self.joint_constraints.init( + island_id, + params, + islands, + bodies, + multibody_joints, + impulse_joints, + joint_indices, + ); counters.solver.velocity_assembly_time.pause(); counters.solver.velocity_resolution_time.resume(); @@ -99,57 +108,53 @@ impl IslandSolver { params, islands, bodies, + multibody_joints, manifolds, - joints, + impulse_joints, &mut self.contact_constraints.velocity_constraints, + &mut self.contact_constraints.generic_velocity_constraints, + &self.contact_constraints.generic_jacobians, &mut self.joint_constraints.velocity_constraints, + &self.joint_constraints.generic_jacobians, ); counters.solver.velocity_resolution_time.pause(); - - counters.solver.velocity_update_time.resume(); - - for handle in islands.active_island(island_id) { - let (poss, vels, damping, mprops): ( - &RigidBodyPosition, - &RigidBodyVelocity, - &RigidBodyDamping, - &RigidBodyMassProps, - ) = bodies.index_bundle(handle.0); - - let mut new_poss = *poss; - let new_vels = vels.apply_damping(params.dt, damping); - new_poss.next_position = - vels.integrate(params.dt, &poss.position, &mprops.local_mprops.local_com); - - bodies.set_internal(handle.0, new_vels); - bodies.set_internal(handle.0, new_poss); - } - - counters.solver.velocity_update_time.pause(); } else { self.contact_constraints.clear(); self.joint_constraints.clear(); counters.solver.velocity_update_time.resume(); for handle in islands.active_island(island_id) { - // Since we didn't run the velocity solver we need to integrate the accelerations here - let (poss, vels, forces, damping, mprops): ( - &RigidBodyPosition, - &RigidBodyVelocity, - &RigidBodyForces, - &RigidBodyDamping, - &RigidBodyMassProps, - ) = bodies.index_bundle(handle.0); + if let Some(link) = multibody_joints.rigid_body_link(*handle).copied() { + let multibody = multibody_joints + .get_multibody_mut_internal(link.multibody) + .unwrap(); - let mut new_poss = *poss; - let new_vels = forces - .integrate(params.dt, vels, mprops) - .apply_damping(params.dt, &damping); - new_poss.next_position = - vels.integrate(params.dt, &poss.position, &mprops.local_mprops.local_com); + if link.id == 0 || link.id == 1 && !multibody.root_is_dynamic { + let accels = &multibody.accelerations; + multibody.velocities.axpy(params.dt, accels, 1.0); + multibody.integrate_next(params.dt); + multibody.forward_kinematics_next(bodies, false); + } + } else { + // Since we didn't run the velocity solver we need to integrate the accelerations here + let (poss, vels, forces, damping, mprops): ( + &RigidBodyPosition, + &RigidBodyVelocity, + &RigidBodyForces, + &RigidBodyDamping, + &RigidBodyMassProps, + ) = bodies.index_bundle(handle.0); - bodies.set_internal(handle.0, new_vels); - bodies.set_internal(handle.0, new_poss); + let mut new_poss = *poss; + let new_vels = forces + .integrate(params.dt, vels, mprops) + .apply_damping(params.dt, &damping); + new_poss.next_position = + vels.integrate(params.dt, &poss.position, &mprops.local_mprops.local_com); + + bodies.set_internal(handle.0, new_vels); + bodies.set_internal(handle.0, new_poss); + } } counters.solver.velocity_update_time.pause(); } diff --git a/src/dynamics/solver/joint_constraint/ball_position_constraint.rs b/src/dynamics/solver/joint_constraint/ball_position_constraint.rs deleted file mode 100644 index 0dfdb86..0000000 --- a/src/dynamics/solver/joint_constraint/ball_position_constraint.rs +++ /dev/null @@ -1,266 +0,0 @@ -use crate::dynamics::{ - BallJoint, IntegrationParameters, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition, -}; -#[cfg(feature = "dim2")] -use crate::math::SdpMatrix; -use crate::math::{AngularInertia, Isometry, Point, Real, Rotation, UnitVector}; -use crate::utils::{WAngularInertia, WCross, WCrossMatrix}; - -#[derive(Debug)] -pub(crate) struct BallPositionConstraint { - position1: usize, - position2: usize, - - local_com1: Point, - local_com2: Point, - - im1: Real, - im2: Real, - - ii1: AngularInertia, - ii2: AngularInertia, - inv_ii1_ii2: AngularInertia, - - local_anchor1: Point, - local_anchor2: Point, - - limits_enabled: bool, - limits_angle: Real, - limits_local_axis1: UnitVector, - limits_local_axis2: UnitVector, -} - -impl BallPositionConstraint { - pub fn from_params( - rb1: (&RigidBodyMassProps, &RigidBodyIds), - rb2: (&RigidBodyMassProps, &RigidBodyIds), - cparams: &BallJoint, - ) -> Self { - let (mprops1, ids1) = rb1; - let (mprops2, ids2) = rb2; - - let ii1 = mprops1.effective_world_inv_inertia_sqrt.squared(); - let ii2 = mprops2.effective_world_inv_inertia_sqrt.squared(); - let inv_ii1_ii2 = (ii1 + ii2).inverse(); - - Self { - local_com1: mprops1.local_mprops.local_com, - local_com2: mprops2.local_mprops.local_com, - im1: mprops1.effective_inv_mass, - im2: mprops2.effective_inv_mass, - ii1, - ii2, - inv_ii1_ii2, - local_anchor1: cparams.local_anchor1, - local_anchor2: cparams.local_anchor2, - position1: ids1.active_set_offset, - position2: ids2.active_set_offset, - limits_enabled: cparams.limits_enabled, - limits_angle: cparams.limits_angle, - limits_local_axis1: cparams.limits_local_axis1, - limits_local_axis2: cparams.limits_local_axis2, - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - let mut position1 = positions[self.position1 as usize]; - let mut position2 = positions[self.position2 as usize]; - - let anchor1 = position1 * self.local_anchor1; - let anchor2 = position2 * self.local_anchor2; - - let com1 = position1 * self.local_com1; - let com2 = position2 * self.local_com2; - - let err = anchor1 - anchor2; - - let centered_anchor1 = anchor1 - com1; - let centered_anchor2 = anchor2 - com2; - - let cmat1 = centered_anchor1.gcross_matrix(); - let cmat2 = centered_anchor2.gcross_matrix(); - - // NOTE: the -cmat1 is just a simpler way of doing cmat1.transpose() - // because it is anti-symmetric. - #[cfg(feature = "dim3")] - let lhs = self.ii1.quadform(&cmat1).add_diagonal(self.im1) - + self.ii2.quadform(&cmat2).add_diagonal(self.im2); - - // In 2D we just unroll the computation because - // it's just easier that way. It is also - // faster because in 2D lhs will be symmetric. - #[cfg(feature = "dim2")] - let lhs = { - let m11 = - self.im1 + self.im2 + cmat1.x * cmat1.x * self.ii1 + cmat2.x * cmat2.x * self.ii2; - let m12 = cmat1.x * cmat1.y * self.ii1 + cmat2.x * cmat2.y * self.ii2; - let m22 = - self.im1 + self.im2 + cmat1.y * cmat1.y * self.ii1 + cmat2.y * cmat2.y * self.ii2; - SdpMatrix::new(m11, m12, m22) - }; - - let inv_lhs = lhs.inverse_unchecked(); - let impulse = inv_lhs * -(err * params.joint_erp); - - position1.translation.vector += self.im1 * impulse; - position2.translation.vector -= self.im2 * impulse; - - let angle1 = self.ii1.transform_vector(centered_anchor1.gcross(impulse)); - let angle2 = self.ii2.transform_vector(centered_anchor2.gcross(-impulse)); - - position1.rotation = Rotation::new(angle1) * position1.rotation; - position2.rotation = Rotation::new(angle2) * position2.rotation; - - /* - * Limits part. - */ - if self.limits_enabled { - let axis1 = position1 * self.limits_local_axis1; - let axis2 = position2 * self.limits_local_axis2; - - #[cfg(feature = "dim2")] - let axis_angle = Rotation::rotation_between_axis(&axis2, &axis1).axis_angle(); - #[cfg(feature = "dim3")] - let axis_angle = - Rotation::rotation_between_axis(&axis2, &axis1).and_then(|r| r.axis_angle()); - - // TODO: handle the case where dot(axis1, axis2) = -1.0 - if let Some((axis, angle)) = axis_angle { - if angle >= self.limits_angle { - #[cfg(feature = "dim2")] - let axis = axis[0]; - #[cfg(feature = "dim3")] - let axis = axis.into_inner(); - let ang_error = angle - self.limits_angle; - let ang_impulse = self - .inv_ii1_ii2 - .transform_vector(axis * ang_error * params.joint_erp); - - position1.rotation = - Rotation::new(self.ii1.transform_vector(-ang_impulse)) * position1.rotation; - position2.rotation = - Rotation::new(self.ii2.transform_vector(ang_impulse)) * position2.rotation; - } - } - } - - positions[self.position1 as usize] = position1; - positions[self.position2 as usize] = position2; - } -} - -#[derive(Debug)] -pub(crate) struct BallPositionGroundConstraint { - position2: usize, - anchor1: Point, - im2: Real, - ii2: AngularInertia, - local_anchor2: Point, - local_com2: Point, - - limits_enabled: bool, - limits_angle: Real, - limits_axis1: UnitVector, - limits_local_axis2: UnitVector, -} - -impl BallPositionGroundConstraint { - pub fn from_params( - rb1: &RigidBodyPosition, - rb2: (&RigidBodyMassProps, &RigidBodyIds), - cparams: &BallJoint, - flipped: bool, - ) -> Self { - let poss1 = rb1; - let (mprops2, ids2) = rb2; - - if flipped { - // Note the only thing that is flipped here - // are the local_anchors. The rb1 and rb2 have - // already been flipped by the caller. - Self { - anchor1: poss1.next_position * cparams.local_anchor2, - im2: mprops2.effective_inv_mass, - ii2: mprops2.effective_world_inv_inertia_sqrt.squared(), - local_anchor2: cparams.local_anchor1, - position2: ids2.active_set_offset, - local_com2: mprops2.local_mprops.local_com, - limits_enabled: cparams.limits_enabled, - limits_angle: cparams.limits_angle, - limits_axis1: poss1.next_position * cparams.limits_local_axis2, - limits_local_axis2: cparams.limits_local_axis1, - } - } else { - Self { - anchor1: poss1.next_position * cparams.local_anchor1, - im2: mprops2.effective_inv_mass, - ii2: mprops2.effective_world_inv_inertia_sqrt.squared(), - local_anchor2: cparams.local_anchor2, - position2: ids2.active_set_offset, - local_com2: mprops2.local_mprops.local_com, - limits_enabled: cparams.limits_enabled, - limits_angle: cparams.limits_angle, - limits_axis1: poss1.next_position * cparams.limits_local_axis1, - limits_local_axis2: cparams.limits_local_axis2, - } - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - let mut position2 = positions[self.position2 as usize]; - - let anchor2 = position2 * self.local_anchor2; - let com2 = position2 * self.local_com2; - - let err = self.anchor1 - anchor2; - let centered_anchor2 = anchor2 - com2; - let cmat2 = centered_anchor2.gcross_matrix(); - - #[cfg(feature = "dim3")] - let lhs = self.ii2.quadform(&cmat2).add_diagonal(self.im2); - - #[cfg(feature = "dim2")] - let lhs = { - let m11 = self.im2 + cmat2.x * cmat2.x * self.ii2; - let m12 = cmat2.x * cmat2.y * self.ii2; - let m22 = self.im2 + cmat2.y * cmat2.y * self.ii2; - SdpMatrix::new(m11, m12, m22) - }; - - let inv_lhs = lhs.inverse_unchecked(); - let impulse = inv_lhs * -(err * params.joint_erp); - position2.translation.vector -= self.im2 * impulse; - - let angle2 = self.ii2.transform_vector(centered_anchor2.gcross(-impulse)); - position2.rotation = Rotation::new(angle2) * position2.rotation; - - /* - * Limits part. - */ - if self.limits_enabled { - let axis2 = position2 * self.limits_local_axis2; - - #[cfg(feature = "dim2")] - let axis_angle = - Rotation::rotation_between_axis(&axis2, &self.limits_axis1).axis_angle(); - #[cfg(feature = "dim3")] - let axis_angle = Rotation::rotation_between_axis(&axis2, &self.limits_axis1) - .and_then(|r| r.axis_angle()); - - // TODO: handle the case where dot(axis1, axis2) = -1.0 - if let Some((axis, angle)) = axis_angle { - if angle >= self.limits_angle { - #[cfg(feature = "dim2")] - let axis = axis[0]; - #[cfg(feature = "dim3")] - let axis = axis.into_inner(); - let ang_error = angle - self.limits_angle; - let ang_correction = axis * ang_error * params.joint_erp; - position2.rotation = Rotation::new(ang_correction) * position2.rotation; - } - } - } - - positions[self.position2 as usize] = position2; - } -} diff --git a/src/dynamics/solver/joint_constraint/ball_position_constraint_wide.rs b/src/dynamics/solver/joint_constraint/ball_position_constraint_wide.rs deleted file mode 100644 index ea462ed..0000000 --- a/src/dynamics/solver/joint_constraint/ball_position_constraint_wide.rs +++ /dev/null @@ -1,216 +0,0 @@ -use crate::dynamics::{ - BallJoint, IntegrationParameters, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition, -}; -#[cfg(feature = "dim2")] -use crate::math::SdpMatrix; -use crate::math::{AngularInertia, Isometry, Point, Real, Rotation, SimdReal, SIMD_WIDTH}; -use crate::utils::{WAngularInertia, WCross, WCrossMatrix}; -use simba::simd::SimdValue; - -#[derive(Debug)] -pub(crate) struct WBallPositionConstraint { - position1: [usize; SIMD_WIDTH], - position2: [usize; SIMD_WIDTH], - - local_com1: Point, - local_com2: Point, - - im1: SimdReal, - im2: SimdReal, - - ii1: AngularInertia, - ii2: AngularInertia, - - local_anchor1: Point, - local_anchor2: Point, -} - -impl WBallPositionConstraint { - pub fn from_params( - rbs1: ( - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - rbs2: ( - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - cparams: [&BallJoint; SIMD_WIDTH], - ) -> Self { - let (mprops1, ids1) = rbs1; - let (mprops2, ids2) = rbs2; - - let local_com1 = Point::from(gather![|ii| mprops1[ii].local_mprops.local_com]); - let local_com2 = Point::from(gather![|ii| mprops2[ii].local_mprops.local_com]); - let im1 = SimdReal::from(gather![|ii| mprops1[ii].effective_inv_mass]); - let im2 = SimdReal::from(gather![|ii| mprops2[ii].effective_inv_mass]); - let ii1 = AngularInertia::::from(gather![ - |ii| mprops1[ii].effective_world_inv_inertia_sqrt - ]) - .squared(); - let ii2 = AngularInertia::::from(gather![ - |ii| mprops2[ii].effective_world_inv_inertia_sqrt - ]) - .squared(); - let local_anchor1 = Point::from(gather![|ii| cparams[ii].local_anchor1]); - let local_anchor2 = Point::from(gather![|ii| cparams[ii].local_anchor2]); - let position1 = gather![|ii| ids1[ii].active_set_offset]; - let position2 = gather![|ii| ids2[ii].active_set_offset]; - - Self { - local_com1, - local_com2, - im1, - im2, - ii1, - ii2, - local_anchor1, - local_anchor2, - position1, - position2, - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - let mut position1 = Isometry::from(gather![|ii| positions[self.position1[ii]]]); - let mut position2 = Isometry::from(gather![|ii| positions[self.position2[ii]]]); - - let anchor1 = position1 * self.local_anchor1; - let anchor2 = position2 * self.local_anchor2; - - let com1 = position1 * self.local_com1; - let com2 = position2 * self.local_com2; - - let err = anchor1 - anchor2; - - let centered_anchor1 = anchor1 - com1; - let centered_anchor2 = anchor2 - com2; - - let cmat1 = centered_anchor1.gcross_matrix(); - let cmat2 = centered_anchor2.gcross_matrix(); - - // NOTE: the -cmat1 is just a simpler way of doing cmat1.transpose() - // because it is anti-symmetric. - #[cfg(feature = "dim3")] - let lhs = self.ii1.quadform(&cmat1).add_diagonal(self.im1) - + self.ii2.quadform(&cmat2).add_diagonal(self.im2); - - // In 2D we just unroll the computation because - // it's just easier that way. - #[cfg(feature = "dim2")] - let lhs = { - let m11 = - self.im1 + self.im2 + cmat1.x * cmat1.x * self.ii1 + cmat2.x * cmat2.x * self.ii2; - let m12 = cmat1.x * cmat1.y * self.ii1 + cmat2.x * cmat2.y * self.ii2; - let m22 = - self.im1 + self.im2 + cmat1.y * cmat1.y * self.ii1 + cmat2.y * cmat2.y * self.ii2; - SdpMatrix::new(m11, m12, m22) - }; - - let inv_lhs = lhs.inverse_unchecked(); - let impulse = inv_lhs * -(err * SimdReal::splat(params.joint_erp)); - - position1.translation.vector += impulse * self.im1; - position2.translation.vector -= impulse * self.im2; - - let angle1 = self.ii1.transform_vector(centered_anchor1.gcross(impulse)); - let angle2 = self.ii2.transform_vector(centered_anchor2.gcross(-impulse)); - - position1.rotation = Rotation::new(angle1) * position1.rotation; - position2.rotation = Rotation::new(angle2) * position2.rotation; - - for ii in 0..SIMD_WIDTH { - positions[self.position1[ii]] = position1.extract(ii); - } - for ii in 0..SIMD_WIDTH { - positions[self.position2[ii]] = position2.extract(ii); - } - } -} - -#[derive(Debug)] -pub(crate) struct WBallPositionGroundConstraint { - position2: [usize; SIMD_WIDTH], - anchor1: Point, - im2: SimdReal, - ii2: AngularInertia, - local_anchor2: Point, - local_com2: Point, -} - -impl WBallPositionGroundConstraint { - pub fn from_params( - rbs1: [&RigidBodyPosition; SIMD_WIDTH], - rbs2: ( - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - cparams: [&BallJoint; SIMD_WIDTH], - flipped: [bool; SIMD_WIDTH], - ) -> Self { - let poss1 = rbs1; - let (mprops2, ids2) = rbs2; - - let position1 = Isometry::from(gather![|ii| poss1[ii].next_position]); - let anchor1 = position1 - * Point::from(gather![|ii| if flipped[ii] { - cparams[ii].local_anchor2 - } else { - cparams[ii].local_anchor1 - }]); - let im2 = SimdReal::from(gather![|ii| mprops2[ii].effective_inv_mass]); - let ii2 = AngularInertia::::from(gather![ - |ii| mprops2[ii].effective_world_inv_inertia_sqrt - ]) - .squared(); - let local_anchor2 = Point::from(gather![|ii| if flipped[ii] { - cparams[ii].local_anchor1 - } else { - cparams[ii].local_anchor2 - }]); - let position2 = gather![|ii| ids2[ii].active_set_offset]; - let local_com2 = Point::from(gather![|ii| mprops2[ii].local_mprops.local_com]); - - Self { - anchor1, - im2, - ii2, - local_anchor2, - position2, - local_com2, - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - let mut position2 = Isometry::from(gather![|ii| positions[self.position2[ii]]]); - - let anchor2 = position2 * self.local_anchor2; - let com2 = position2 * self.local_com2; - - let err = self.anchor1 - anchor2; - let centered_anchor2 = anchor2 - com2; - let cmat2 = centered_anchor2.gcross_matrix(); - - #[cfg(feature = "dim3")] - let lhs = self.ii2.quadform(&cmat2).add_diagonal(self.im2); - - #[cfg(feature = "dim2")] - let lhs = { - let m11 = self.im2 + cmat2.x * cmat2.x * self.ii2; - let m12 = cmat2.x * cmat2.y * self.ii2; - let m22 = self.im2 + cmat2.y * cmat2.y * self.ii2; - SdpMatrix::new(m11, m12, m22) - }; - - let inv_lhs = lhs.inverse_unchecked(); - let impulse = inv_lhs * -(err * SimdReal::splat(params.joint_erp)); - position2.translation.vector -= impulse * self.im2; - - let angle2 = self.ii2.transform_vector(centered_anchor2.gcross(-impulse)); - position2.rotation = Rotation::new(angle2) * position2.rotation; - - for ii in 0..SIMD_WIDTH { - positions[self.position2[ii]] = position2.extract(ii); - } - } -} diff --git a/src/dynamics/solver/joint_constraint/ball_velocity_constraint.rs b/src/dynamics/solver/joint_constraint/ball_velocity_constraint.rs deleted file mode 100644 index 5abd726..0000000 --- a/src/dynamics/solver/joint_constraint/ball_velocity_constraint.rs +++ /dev/null @@ -1,660 +0,0 @@ -use crate::dynamics::solver::DeltaVel; -use crate::dynamics::{ - BallJoint, IntegrationParameters, JointGraphEdge, JointIndex, JointParams, RigidBodyIds, - RigidBodyMassProps, RigidBodyPosition, RigidBodyVelocity, -}; -use crate::math::{AngVector, AngularInertia, Real, Rotation, SdpMatrix, Vector}; -use crate::utils::{WAngularInertia, WCross, WCrossMatrix, WDot}; - -#[derive(Debug)] -pub(crate) struct BallVelocityConstraint { - mj_lambda1: usize, - mj_lambda2: usize, - - joint_id: JointIndex, - - rhs: Vector, - impulse: Vector, - - r1: Vector, - r2: Vector, - - inv_lhs: SdpMatrix, - - motor_rhs: AngVector, - motor_impulse: AngVector, - motor_inv_lhs: Option>, - motor_max_impulse: Real, - - limits_active: bool, - limits_rhs: Real, - limits_inv_lhs: Real, - limits_impulse: Real, - limits_axis: AngVector, - - im1: Real, - im2: Real, - - ii1_sqrt: AngularInertia, - ii2_sqrt: AngularInertia, -} - -impl BallVelocityConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: JointIndex, - rb1: ( - &RigidBodyPosition, - &RigidBodyVelocity, - &RigidBodyMassProps, - &RigidBodyIds, - ), - rb2: ( - &RigidBodyPosition, - &RigidBodyVelocity, - &RigidBodyMassProps, - &RigidBodyIds, - ), - joint: &BallJoint, - ) -> Self { - let (rb_pos1, rb_vels1, rb_mprops1, rb_ids1) = rb1; - let (rb_pos2, rb_vels2, rb_mprops2, rb_ids2) = rb2; - - let anchor_world1 = rb_pos1.position * joint.local_anchor1; - let anchor_world2 = rb_pos2.position * joint.local_anchor2; - let anchor1 = anchor_world1 - rb_mprops1.world_com; - let anchor2 = anchor_world2 - rb_mprops2.world_com; - - let vel1 = rb_vels1.linvel + rb_vels1.angvel.gcross(anchor1); - let vel2 = rb_vels2.linvel + rb_vels2.angvel.gcross(anchor2); - let im1 = rb_mprops1.effective_inv_mass; - let im2 = rb_mprops2.effective_inv_mass; - - let rhs = (vel2 - vel1) * params.velocity_solve_fraction - + (anchor_world2 - anchor_world1) * params.velocity_based_erp_inv_dt(); - - let lhs; - let cmat1 = anchor1.gcross_matrix(); - let cmat2 = anchor2.gcross_matrix(); - - #[cfg(feature = "dim3")] - { - lhs = rb_mprops2 - .effective_world_inv_inertia_sqrt - .squared() - .quadform(&cmat2) - .add_diagonal(im2) - + rb_mprops1 - .effective_world_inv_inertia_sqrt - .squared() - .quadform(&cmat1) - .add_diagonal(im1); - } - - // In 2D we just unroll the computation because - // it's just easier that way. - #[cfg(feature = "dim2")] - { - let ii1 = rb_mprops1.effective_world_inv_inertia_sqrt.squared(); - let ii2 = rb_mprops2.effective_world_inv_inertia_sqrt.squared(); - let m11 = im1 + im2 + cmat1.x * cmat1.x * ii1 + cmat2.x * cmat2.x * ii2; - let m12 = cmat1.x * cmat1.y * ii1 + cmat2.x * cmat2.y * ii2; - let m22 = im1 + im2 + cmat1.y * cmat1.y * ii1 + cmat2.y * cmat2.y * ii2; - lhs = SdpMatrix::new(m11, m12, m22) - } - - let inv_lhs = lhs.inverse_unchecked(); - - /* - * Motor part. - */ - let mut motor_rhs = na::zero(); - let mut motor_inv_lhs = None; - let motor_max_impulse = joint.motor_max_impulse; - - if motor_max_impulse > 0.0 { - let (stiffness, damping, gamma, keep_lhs) = joint.motor_model.combine_coefficients( - params.dt, - joint.motor_stiffness, - joint.motor_damping, - ); - - if stiffness != 0.0 { - let dpos = rb_pos2.position.rotation - * (rb_pos1.position.rotation * joint.motor_target_pos).inverse(); - #[cfg(feature = "dim2")] - { - motor_rhs += dpos.angle() * stiffness; - } - #[cfg(feature = "dim3")] - { - motor_rhs += dpos.scaled_axis() * stiffness; - } - } - - if damping != 0.0 { - let curr_vel = rb_vels2.angvel - rb_vels1.angvel; - motor_rhs += (curr_vel - joint.motor_target_vel) * damping; - } - - #[cfg(feature = "dim2")] - if stiffness != 0.0 || damping != 0.0 { - motor_inv_lhs = if keep_lhs { - let ii1 = rb_mprops1.effective_world_inv_inertia_sqrt.squared(); - let ii2 = rb_mprops2.effective_world_inv_inertia_sqrt.squared(); - Some(gamma / (ii1 + ii2)) - } else { - Some(gamma) - }; - motor_rhs /= gamma; - } - - #[cfg(feature = "dim3")] - if stiffness != 0.0 || damping != 0.0 { - motor_inv_lhs = if keep_lhs { - let ii1 = rb_mprops1.effective_world_inv_inertia_sqrt.squared(); - let ii2 = rb_mprops2.effective_world_inv_inertia_sqrt.squared(); - Some((ii1 + ii2).inverse_unchecked() * gamma) - } else { - Some(SdpMatrix::diagonal(gamma)) - }; - motor_rhs /= gamma; - } - } - - #[cfg(feature = "dim2")] - let motor_impulse = na::clamp(joint.motor_impulse, -motor_max_impulse, motor_max_impulse) - * params.warmstart_coeff; - #[cfg(feature = "dim3")] - let motor_impulse = - joint.motor_impulse.cap_magnitude(motor_max_impulse) * params.warmstart_coeff; - - /* - * Setup the limits constraint. - */ - let mut limits_active = false; - let mut limits_rhs = 0.0; - let mut limits_inv_lhs = 0.0; - let mut limits_impulse = 0.0; - let mut limits_axis = na::zero(); - - if joint.limits_enabled { - let axis1 = rb_pos1.position * joint.limits_local_axis1; - let axis2 = rb_pos2.position * joint.limits_local_axis2; - - #[cfg(feature = "dim2")] - let axis_angle = Rotation::rotation_between_axis(&axis2, &axis1).axis_angle(); - #[cfg(feature = "dim3")] - let axis_angle = - Rotation::rotation_between_axis(&axis2, &axis1).and_then(|r| r.axis_angle()); - - // TODO: handle the case where dot(axis1, axis2) = -1.0 - if let Some((axis, angle)) = axis_angle { - if angle >= joint.limits_angle { - #[cfg(feature = "dim2")] - let axis = axis[0]; - #[cfg(feature = "dim3")] - let axis = axis.into_inner(); - - limits_active = true; - limits_rhs = (rb_vels2.angvel.gdot(axis) - rb_vels1.angvel.gdot(axis)) - * params.velocity_solve_fraction; - - limits_rhs += (angle - joint.limits_angle) * params.velocity_based_erp_inv_dt(); - - let ii1 = rb_mprops1.effective_world_inv_inertia_sqrt.squared(); - let ii2 = rb_mprops2.effective_world_inv_inertia_sqrt.squared(); - limits_inv_lhs = crate::utils::inv( - axis.gdot(ii2.transform_vector(axis)) - + axis.gdot(ii1.transform_vector(axis)), - ); - - limits_impulse = joint.limits_impulse * params.warmstart_coeff; - limits_axis = axis; - } - } - } - - BallVelocityConstraint { - joint_id, - mj_lambda1: rb_ids1.active_set_offset, - mj_lambda2: rb_ids2.active_set_offset, - im1, - im2, - impulse: joint.impulse * params.warmstart_coeff, - r1: anchor1, - r2: anchor2, - rhs, - inv_lhs, - motor_rhs, - motor_impulse, - motor_inv_lhs, - motor_max_impulse: joint.motor_max_impulse, - ii1_sqrt: rb_mprops1.effective_world_inv_inertia_sqrt, - ii2_sqrt: rb_mprops2.effective_world_inv_inertia_sqrt, - limits_active, - limits_axis, - limits_rhs, - limits_inv_lhs, - limits_impulse, - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = mj_lambdas[self.mj_lambda1 as usize]; - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - mj_lambda1.linear += self.im1 * self.impulse; - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(self.r1.gcross(self.impulse) + self.motor_impulse); - mj_lambda2.linear -= self.im2 * self.impulse; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(self.r2.gcross(self.impulse) + self.motor_impulse); - - /* - * Warmstart limits. - */ - if self.limits_active { - let limit_impulse1 = -self.limits_axis * self.limits_impulse; - let limit_impulse2 = self.limits_axis * self.limits_impulse; - mj_lambda1.angular += self.ii1_sqrt.transform_vector(limit_impulse1); - mj_lambda2.angular += self.ii2_sqrt.transform_vector(limit_impulse2); - } - - mj_lambdas[self.mj_lambda1 as usize] = mj_lambda1; - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - fn solve_dofs(&mut self, mj_lambda1: &mut DeltaVel, mj_lambda2: &mut DeltaVel) { - let ang_vel1 = self.ii1_sqrt.transform_vector(mj_lambda1.angular); - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - let vel1 = mj_lambda1.linear + ang_vel1.gcross(self.r1); - let vel2 = mj_lambda2.linear + ang_vel2.gcross(self.r2); - let dvel = -vel1 + vel2 + self.rhs; - - let impulse = self.inv_lhs * dvel; - self.impulse += impulse; - - mj_lambda1.linear += self.im1 * impulse; - mj_lambda1.angular += self.ii1_sqrt.transform_vector(self.r1.gcross(impulse)); - - mj_lambda2.linear -= self.im2 * impulse; - mj_lambda2.angular -= self.ii2_sqrt.transform_vector(self.r2.gcross(impulse)); - } - - fn solve_limits(&mut self, mj_lambda1: &mut DeltaVel, mj_lambda2: &mut DeltaVel) { - if self.limits_active { - let limits_torquedir1 = -self.limits_axis; - let limits_torquedir2 = self.limits_axis; - - let ang_vel1 = self.ii1_sqrt.transform_vector(mj_lambda1.angular); - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let ang_dvel = limits_torquedir1.gdot(ang_vel1) - + limits_torquedir2.gdot(ang_vel2) - + self.limits_rhs; - let new_impulse = (self.limits_impulse - ang_dvel * self.limits_inv_lhs).max(0.0); - let dimpulse = new_impulse - self.limits_impulse; - self.limits_impulse = new_impulse; - - let ang_impulse1 = limits_torquedir1 * dimpulse; - let ang_impulse2 = limits_torquedir2 * dimpulse; - - mj_lambda1.angular += self.ii1_sqrt.transform_vector(ang_impulse1); - mj_lambda2.angular += self.ii2_sqrt.transform_vector(ang_impulse2); - } - } - - fn solve_motors(&mut self, mj_lambda1: &mut DeltaVel, mj_lambda2: &mut DeltaVel) { - if let Some(motor_inv_lhs) = &self.motor_inv_lhs { - let ang_vel1 = self.ii1_sqrt.transform_vector(mj_lambda1.angular); - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let dangvel = (ang_vel2 - ang_vel1) + self.motor_rhs; - - let new_impulse = self.motor_impulse + motor_inv_lhs.transform_vector(dangvel); - - #[cfg(feature = "dim2")] - let clamped_impulse = - na::clamp(new_impulse, -self.motor_max_impulse, self.motor_max_impulse); - #[cfg(feature = "dim3")] - let clamped_impulse = new_impulse.cap_magnitude(self.motor_max_impulse); - - let effective_impulse = clamped_impulse - self.motor_impulse; - self.motor_impulse = clamped_impulse; - - mj_lambda1.angular += self.ii1_sqrt.transform_vector(effective_impulse); - mj_lambda2.angular -= self.ii2_sqrt.transform_vector(effective_impulse); - } - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = mj_lambdas[self.mj_lambda1 as usize]; - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - self.solve_limits(&mut mj_lambda1, &mut mj_lambda2); - self.solve_dofs(&mut mj_lambda1, &mut mj_lambda2); - self.solve_motors(&mut mj_lambda1, &mut mj_lambda2); - - mj_lambdas[self.mj_lambda1 as usize] = mj_lambda1; - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - let joint = &mut joints_all[self.joint_id].weight; - if let JointParams::BallJoint(ball) = &mut joint.params { - ball.impulse = self.impulse; - ball.motor_impulse = self.motor_impulse; - ball.limits_impulse = self.limits_impulse; - } - } -} - -#[derive(Debug)] -pub(crate) struct BallVelocityGroundConstraint { - mj_lambda2: usize, - joint_id: JointIndex, - r2: Vector, - - rhs: Vector, - impulse: Vector, - inv_lhs: SdpMatrix, - - motor_rhs: AngVector, - motor_impulse: AngVector, - motor_inv_lhs: Option>, - motor_max_impulse: Real, - - limits_active: bool, - limits_rhs: Real, - limits_inv_lhs: Real, - limits_impulse: Real, - limits_axis: AngVector, - - im2: Real, - ii2_sqrt: AngularInertia, -} - -impl BallVelocityGroundConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: JointIndex, - rb1: (&RigidBodyPosition, &RigidBodyVelocity, &RigidBodyMassProps), - rb2: ( - &RigidBodyPosition, - &RigidBodyVelocity, - &RigidBodyMassProps, - &RigidBodyIds, - ), - joint: &BallJoint, - flipped: bool, - ) -> Self { - let (rb_pos1, rb_vels1, rb_mprops1) = rb1; - let (rb_pos2, rb_vels2, rb_mprops2, rb_ids2) = rb2; - - let (anchor_world1, anchor_world2) = if flipped { - ( - rb_pos1.position * joint.local_anchor2, - rb_pos2.position * joint.local_anchor1, - ) - } else { - ( - rb_pos1.position * joint.local_anchor1, - rb_pos2.position * joint.local_anchor2, - ) - }; - - let anchor1 = anchor_world1 - rb_mprops1.world_com; - let anchor2 = anchor_world2 - rb_mprops2.world_com; - - let im2 = rb_mprops2.effective_inv_mass; - let vel1 = rb_vels1.linvel + rb_vels1.angvel.gcross(anchor1); - let vel2 = rb_vels2.linvel + rb_vels2.angvel.gcross(anchor2); - - let rhs = (vel2 - vel1) * params.velocity_solve_fraction - + (anchor_world2 - anchor_world1) * params.velocity_based_erp_inv_dt(); - - let cmat2 = anchor2.gcross_matrix(); - - let lhs; - - #[cfg(feature = "dim3")] - { - lhs = rb_mprops2 - .effective_world_inv_inertia_sqrt - .squared() - .quadform(&cmat2) - .add_diagonal(im2); - } - - #[cfg(feature = "dim2")] - { - let ii2 = rb_mprops2.effective_world_inv_inertia_sqrt.squared(); - let m11 = im2 + cmat2.x * cmat2.x * ii2; - let m12 = cmat2.x * cmat2.y * ii2; - let m22 = im2 + cmat2.y * cmat2.y * ii2; - lhs = SdpMatrix::new(m11, m12, m22) - } - - let inv_lhs = lhs.inverse_unchecked(); - - /* - * Motor part. - */ - let mut motor_rhs = na::zero(); - let mut motor_inv_lhs = None; - let motor_max_impulse = joint.motor_max_impulse; - - if motor_max_impulse > 0.0 { - let (stiffness, damping, gamma, keep_lhs) = joint.motor_model.combine_coefficients( - params.dt, - joint.motor_stiffness, - joint.motor_damping, - ); - - if stiffness != 0.0 { - let dpos = rb_pos2.position.rotation - * (rb_pos1.position.rotation * joint.motor_target_pos).inverse(); - #[cfg(feature = "dim2")] - { - motor_rhs += dpos.angle() * stiffness; - } - #[cfg(feature = "dim3")] - { - motor_rhs += dpos.scaled_axis() * stiffness; - } - } - - if damping != 0.0 { - let curr_vel = rb_vels2.angvel - rb_vels1.angvel; - motor_rhs += (curr_vel - joint.motor_target_vel) * damping; - } - - #[cfg(feature = "dim2")] - if stiffness != 0.0 || damping != 0.0 { - motor_inv_lhs = if keep_lhs { - let ii2 = rb_mprops2.effective_world_inv_inertia_sqrt.squared(); - Some(gamma / ii2) - } else { - Some(gamma) - }; - motor_rhs /= gamma; - } - - #[cfg(feature = "dim3")] - if stiffness != 0.0 || damping != 0.0 { - motor_inv_lhs = if keep_lhs { - let ii2 = rb_mprops2.effective_world_inv_inertia_sqrt.squared(); - Some(ii2.inverse_unchecked() * gamma) - } else { - Some(SdpMatrix::diagonal(gamma)) - }; - motor_rhs /= gamma; - } - } - - #[cfg(feature = "dim2")] - let motor_impulse = na::clamp(joint.motor_impulse, -motor_max_impulse, motor_max_impulse) - * params.warmstart_coeff; - #[cfg(feature = "dim3")] - let motor_impulse = - joint.motor_impulse.cap_magnitude(motor_max_impulse) * params.warmstart_coeff; - - /* - * Setup the limits constraint. - */ - let mut limits_active = false; - let mut limits_rhs = 0.0; - let mut limits_inv_lhs = 0.0; - let mut limits_impulse = 0.0; - let mut limits_axis = na::zero(); - - if joint.limits_enabled { - let (axis1, axis2) = if flipped { - ( - rb_pos1.position * joint.limits_local_axis2, - rb_pos2.position * joint.limits_local_axis1, - ) - } else { - ( - rb_pos1.position * joint.limits_local_axis1, - rb_pos2.position * joint.limits_local_axis2, - ) - }; - - #[cfg(feature = "dim2")] - let axis_angle = Rotation::rotation_between_axis(&axis2, &axis1).axis_angle(); - #[cfg(feature = "dim3")] - let axis_angle = - Rotation::rotation_between_axis(&axis2, &axis1).and_then(|r| r.axis_angle()); - - // TODO: handle the case where dot(axis1, axis2) = -1.0 - if let Some((axis, angle)) = axis_angle { - if angle >= joint.limits_angle { - #[cfg(feature = "dim2")] - let axis = axis[0]; - #[cfg(feature = "dim3")] - let axis = axis.into_inner(); - - limits_active = true; - limits_rhs = (rb_vels2.angvel.gdot(axis) - rb_vels1.angvel.gdot(axis)) - * params.velocity_solve_fraction; - - limits_rhs += (angle - joint.limits_angle) * params.velocity_based_erp_inv_dt(); - - let ii2 = rb_mprops2.effective_world_inv_inertia_sqrt.squared(); - limits_inv_lhs = crate::utils::inv(axis.gdot(ii2.transform_vector(axis))); - limits_impulse = joint.limits_impulse * params.warmstart_coeff; - limits_axis = axis; - } - } - } - - BallVelocityGroundConstraint { - joint_id, - mj_lambda2: rb_ids2.active_set_offset, - im2, - impulse: joint.impulse * params.warmstart_coeff, - r2: anchor2, - rhs, - inv_lhs, - motor_rhs, - motor_impulse, - motor_inv_lhs, - motor_max_impulse: joint.motor_max_impulse, - ii2_sqrt: rb_mprops2.effective_world_inv_inertia_sqrt, - limits_active, - limits_axis, - limits_rhs, - limits_inv_lhs, - limits_impulse, - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - mj_lambda2.linear -= self.im2 * self.impulse; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(self.r2.gcross(self.impulse) + self.motor_impulse); - - /* - * Warmstart limits. - */ - if self.limits_active { - let limit_impulse2 = self.limits_axis * self.limits_impulse; - mj_lambda2.angular += self.ii2_sqrt.transform_vector(limit_impulse2); - } - - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - fn solve_dofs(&mut self, mj_lambda2: &mut DeltaVel) { - let angvel = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - let vel2 = mj_lambda2.linear + angvel.gcross(self.r2); - let dvel = vel2 + self.rhs; - - let impulse = self.inv_lhs * dvel; - self.impulse += impulse; - - mj_lambda2.linear -= self.im2 * impulse; - mj_lambda2.angular -= self.ii2_sqrt.transform_vector(self.r2.gcross(impulse)); - } - - fn solve_limits(&mut self, mj_lambda2: &mut DeltaVel) { - if self.limits_active { - let limits_torquedir2 = self.limits_axis; - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let ang_dvel = limits_torquedir2.gdot(ang_vel2) + self.limits_rhs; - let new_impulse = (self.limits_impulse - ang_dvel * self.limits_inv_lhs).max(0.0); - let dimpulse = new_impulse - self.limits_impulse; - self.limits_impulse = new_impulse; - - let ang_impulse2 = limits_torquedir2 * dimpulse; - mj_lambda2.angular += self.ii2_sqrt.transform_vector(ang_impulse2); - } - } - - fn solve_motors(&mut self, mj_lambda2: &mut DeltaVel) { - if let Some(motor_inv_lhs) = &self.motor_inv_lhs { - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let dangvel = ang_vel2 + self.motor_rhs; - let new_impulse = self.motor_impulse + motor_inv_lhs.transform_vector(dangvel); - - #[cfg(feature = "dim2")] - let clamped_impulse = - na::clamp(new_impulse, -self.motor_max_impulse, self.motor_max_impulse); - #[cfg(feature = "dim3")] - let clamped_impulse = new_impulse.cap_magnitude(self.motor_max_impulse); - - let effective_impulse = clamped_impulse - self.motor_impulse; - self.motor_impulse = clamped_impulse; - - mj_lambda2.angular -= self.ii2_sqrt.transform_vector(effective_impulse); - } - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - self.solve_limits(&mut mj_lambda2); - self.solve_dofs(&mut mj_lambda2); - self.solve_motors(&mut mj_lambda2); - - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - // FIXME: duplicated code with the non-ground constraint. - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - let joint = &mut joints_all[self.joint_id].weight; - if let JointParams::BallJoint(ball) = &mut joint.params { - ball.impulse = self.impulse; - ball.motor_impulse = self.motor_impulse; - ball.limits_impulse = self.limits_impulse; - } - } -} diff --git a/src/dynamics/solver/joint_constraint/ball_velocity_constraint_wide.rs b/src/dynamics/solver/joint_constraint/ball_velocity_constraint_wide.rs deleted file mode 100644 index ee465cd..0000000 --- a/src/dynamics/solver/joint_constraint/ball_velocity_constraint_wide.rs +++ /dev/null @@ -1,359 +0,0 @@ -use crate::dynamics::solver::DeltaVel; -use crate::dynamics::{ - BallJoint, IntegrationParameters, JointGraphEdge, JointIndex, JointParams, RigidBodyIds, - RigidBodyMassProps, RigidBodyPosition, RigidBodyVelocity, -}; -use crate::math::{ - AngVector, AngularInertia, Isometry, Point, Real, SdpMatrix, SimdReal, Vector, SIMD_WIDTH, -}; -use crate::utils::{WAngularInertia, WCross, WCrossMatrix}; -use simba::simd::SimdValue; - -#[derive(Debug)] -pub(crate) struct WBallVelocityConstraint { - mj_lambda1: [usize; SIMD_WIDTH], - mj_lambda2: [usize; SIMD_WIDTH], - - joint_id: [JointIndex; SIMD_WIDTH], - - rhs: Vector, - pub(crate) impulse: Vector, - - r1: Vector, - r2: Vector, - - inv_lhs: SdpMatrix, - - im1: SimdReal, - im2: SimdReal, - - ii1_sqrt: AngularInertia, - ii2_sqrt: AngularInertia, -} - -impl WBallVelocityConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: [JointIndex; SIMD_WIDTH], - rbs1: ( - [&RigidBodyPosition; SIMD_WIDTH], - [&RigidBodyVelocity; SIMD_WIDTH], - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - rbs2: ( - [&RigidBodyPosition; SIMD_WIDTH], - [&RigidBodyVelocity; SIMD_WIDTH], - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - cparams: [&BallJoint; SIMD_WIDTH], - ) -> Self { - let (poss1, vels1, mprops1, ids1) = rbs1; - let (poss2, vels2, mprops2, ids2) = rbs2; - - let position1 = Isometry::from(gather![|ii| poss1[ii].position]); - let linvel1 = Vector::from(gather![|ii| vels1[ii].linvel]); - let angvel1 = AngVector::::from(gather![|ii| vels1[ii].angvel]); - let world_com1 = Point::from(gather![|ii| mprops1[ii].world_com]); - let im1 = SimdReal::from(gather![|ii| mprops1[ii].effective_inv_mass]); - let ii1_sqrt = AngularInertia::::from(gather![ - |ii| mprops1[ii].effective_world_inv_inertia_sqrt - ]); - let mj_lambda1 = gather![|ii| ids1[ii].active_set_offset]; - - let position2 = Isometry::from(gather![|ii| poss2[ii].position]); - let linvel2 = Vector::from(gather![|ii| vels2[ii].linvel]); - let angvel2 = AngVector::::from(gather![|ii| vels2[ii].angvel]); - let world_com2 = Point::from(gather![|ii| mprops2[ii].world_com]); - let im2 = SimdReal::from(gather![|ii| mprops2[ii].effective_inv_mass]); - let ii2_sqrt = AngularInertia::::from(gather![ - |ii| mprops2[ii].effective_world_inv_inertia_sqrt - ]); - let mj_lambda2 = gather![|ii| ids2[ii].active_set_offset]; - - let local_anchor1 = Point::from(gather![|ii| cparams[ii].local_anchor1]); - let local_anchor2 = Point::from(gather![|ii| cparams[ii].local_anchor2]); - let impulse = Vector::from(gather![|ii| cparams[ii].impulse]); - - let anchor_world1 = position1 * local_anchor1; - let anchor_world2 = position2 * local_anchor2; - let anchor1 = anchor_world1 - world_com1; - let anchor2 = anchor_world2 - world_com2; - - let vel1: Vector = linvel1 + angvel1.gcross(anchor1); - let vel2: Vector = linvel2 + angvel2.gcross(anchor2); - let rhs = (vel2 - vel1) * SimdReal::splat(params.velocity_solve_fraction) - + (anchor_world2 - anchor_world1) * SimdReal::splat(params.velocity_based_erp_inv_dt()); - let lhs; - - let cmat1 = anchor1.gcross_matrix(); - let cmat2 = anchor2.gcross_matrix(); - - #[cfg(feature = "dim3")] - { - lhs = ii2_sqrt.squared().quadform(&cmat2).add_diagonal(im2) - + ii1_sqrt.squared().quadform(&cmat1).add_diagonal(im1); - } - - // In 2D we just unroll the computation because - // it's just easier that way. - #[cfg(feature = "dim2")] - { - let ii1 = ii1_sqrt.squared(); - let ii2 = ii2_sqrt.squared(); - let m11 = im1 + im2 + cmat1.x * cmat1.x * ii1 + cmat2.x * cmat2.x * ii2; - let m12 = cmat1.x * cmat1.y * ii1 + cmat2.x * cmat2.y * ii2; - let m22 = im1 + im2 + cmat1.y * cmat1.y * ii1 + cmat2.y * cmat2.y * ii2; - lhs = SdpMatrix::new(m11, m12, m22) - } - - let inv_lhs = lhs.inverse_unchecked(); - - WBallVelocityConstraint { - joint_id, - mj_lambda1, - mj_lambda2, - im1, - im2, - impulse: impulse * SimdReal::splat(params.warmstart_coeff), - r1: anchor1, - r2: anchor2, - rhs, - inv_lhs, - ii1_sqrt, - ii2_sqrt, - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda1[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda1[ii] as usize].angular - ]), - }; - let mut mj_lambda2 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - mj_lambda1.linear += self.impulse * self.im1; - mj_lambda1.angular += self.ii1_sqrt.transform_vector(self.r1.gcross(self.impulse)); - mj_lambda2.linear -= self.impulse * self.im2; - mj_lambda2.angular -= self.ii2_sqrt.transform_vector(self.r2.gcross(self.impulse)); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda1[ii] as usize].linear = mj_lambda1.linear.extract(ii); - mj_lambdas[self.mj_lambda1[ii] as usize].angular = mj_lambda1.angular.extract(ii); - } - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1: DeltaVel = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda1[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda1[ii] as usize].angular - ]), - }; - let mut mj_lambda2: DeltaVel = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - let ang_vel1 = self.ii1_sqrt.transform_vector(mj_lambda1.angular); - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - let vel1 = mj_lambda1.linear + ang_vel1.gcross(self.r1); - let vel2 = mj_lambda2.linear + ang_vel2.gcross(self.r2); - let dvel = -vel1 + vel2 + self.rhs; - - let impulse = self.inv_lhs * dvel; - self.impulse += impulse; - - mj_lambda1.linear += impulse * self.im1; - mj_lambda1.angular += self.ii1_sqrt.transform_vector(self.r1.gcross(impulse)); - - mj_lambda2.linear -= impulse * self.im2; - mj_lambda2.angular -= self.ii2_sqrt.transform_vector(self.r2.gcross(impulse)); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda1[ii] as usize].linear = mj_lambda1.linear.extract(ii); - mj_lambdas[self.mj_lambda1[ii] as usize].angular = mj_lambda1.angular.extract(ii); - } - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - for ii in 0..SIMD_WIDTH { - let joint = &mut joints_all[self.joint_id[ii]].weight; - if let JointParams::BallJoint(ball) = &mut joint.params { - ball.impulse = self.impulse.extract(ii) - } - } - } -} - -#[derive(Debug)] -pub(crate) struct WBallVelocityGroundConstraint { - mj_lambda2: [usize; SIMD_WIDTH], - joint_id: [JointIndex; SIMD_WIDTH], - rhs: Vector, - pub(crate) impulse: Vector, - r2: Vector, - inv_lhs: SdpMatrix, - im2: SimdReal, - ii2_sqrt: AngularInertia, -} - -impl WBallVelocityGroundConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: [JointIndex; SIMD_WIDTH], - rbs1: ( - [&RigidBodyPosition; SIMD_WIDTH], - [&RigidBodyVelocity; SIMD_WIDTH], - [&RigidBodyMassProps; SIMD_WIDTH], - ), - rbs2: ( - [&RigidBodyPosition; SIMD_WIDTH], - [&RigidBodyVelocity; SIMD_WIDTH], - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - cparams: [&BallJoint; SIMD_WIDTH], - flipped: [bool; SIMD_WIDTH], - ) -> Self { - let (poss1, vels1, mprops1) = rbs1; - let (poss2, vels2, mprops2, ids2) = rbs2; - - let position1 = Isometry::from(gather![|ii| poss1[ii].position]); - let linvel1 = Vector::from(gather![|ii| vels1[ii].linvel]); - let angvel1 = AngVector::::from(gather![|ii| vels1[ii].angvel]); - let world_com1 = Point::from(gather![|ii| mprops1[ii].world_com]); - let local_anchor1 = Point::from(gather![|ii| if flipped[ii] { - cparams[ii].local_anchor2 - } else { - cparams[ii].local_anchor1 - }]); - - let position2 = Isometry::from(gather![|ii| poss2[ii].position]); - let linvel2 = Vector::from(gather![|ii| vels2[ii].linvel]); - let angvel2 = AngVector::::from(gather![|ii| vels2[ii].angvel]); - let world_com2 = Point::from(gather![|ii| mprops2[ii].world_com]); - let im2 = SimdReal::from(gather![|ii| mprops2[ii].effective_inv_mass]); - let ii2_sqrt = AngularInertia::::from(gather![ - |ii| mprops2[ii].effective_world_inv_inertia_sqrt - ]); - let mj_lambda2 = gather![|ii| ids2[ii].active_set_offset]; - - let local_anchor2 = Point::from(gather![|ii| if flipped[ii] { - cparams[ii].local_anchor1 - } else { - cparams[ii].local_anchor2 - }]); - let impulse = Vector::from(gather![|ii| cparams[ii].impulse]); - - let anchor_world1 = position1 * local_anchor1; - let anchor_world2 = position2 * local_anchor2; - let anchor1 = anchor_world1 - world_com1; - let anchor2 = anchor_world2 - world_com2; - - let vel1: Vector = linvel1 + angvel1.gcross(anchor1); - let vel2: Vector = linvel2 + angvel2.gcross(anchor2); - let rhs = (vel2 - vel1) * SimdReal::splat(params.velocity_solve_fraction) - + (anchor_world2 - anchor_world1) * SimdReal::splat(params.velocity_based_erp_inv_dt()); - let lhs; - - let cmat2 = anchor2.gcross_matrix(); - - #[cfg(feature = "dim3")] - { - lhs = ii2_sqrt.squared().quadform(&cmat2).add_diagonal(im2); - } - - // In 2D we just unroll the computation because - // it's just easier that way. - #[cfg(feature = "dim2")] - { - let ii2 = ii2_sqrt.squared(); - let m11 = im2 + cmat2.x * cmat2.x * ii2; - let m12 = cmat2.x * cmat2.y * ii2; - let m22 = im2 + cmat2.y * cmat2.y * ii2; - lhs = SdpMatrix::new(m11, m12, m22) - } - - let inv_lhs = lhs.inverse_unchecked(); - - WBallVelocityGroundConstraint { - joint_id, - mj_lambda2, - im2, - impulse: impulse * SimdReal::splat(params.warmstart_coeff), - r2: anchor2, - rhs, - inv_lhs, - ii2_sqrt, - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - mj_lambda2.linear -= self.impulse * self.im2; - mj_lambda2.angular -= self.ii2_sqrt.transform_vector(self.r2.gcross(self.impulse)); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2: DeltaVel = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - let angvel = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - let vel2 = mj_lambda2.linear + angvel.gcross(self.r2); - let dvel = vel2 + self.rhs; - - let impulse = self.inv_lhs * dvel; - self.impulse += impulse; - - mj_lambda2.linear -= impulse * self.im2; - mj_lambda2.angular -= self.ii2_sqrt.transform_vector(self.r2.gcross(impulse)); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - for ii in 0..SIMD_WIDTH { - let joint = &mut joints_all[self.joint_id[ii]].weight; - if let JointParams::BallJoint(ball) = &mut joint.params { - ball.impulse = self.impulse.extract(ii) - } - } - } -} diff --git a/src/dynamics/solver/joint_constraint/fixed_position_constraint.rs b/src/dynamics/solver/joint_constraint/fixed_position_constraint.rs deleted file mode 100644 index 3ab13f7..0000000 --- a/src/dynamics/solver/joint_constraint/fixed_position_constraint.rs +++ /dev/null @@ -1,151 +0,0 @@ -use crate::dynamics::{ - FixedJoint, IntegrationParameters, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition, -}; -use crate::math::{AngularInertia, Isometry, Point, Real, Rotation}; -use crate::utils::WAngularInertia; - -#[derive(Debug)] -pub(crate) struct FixedPositionConstraint { - position1: usize, - position2: usize, - local_frame1: Isometry, - local_frame2: Isometry, - local_com1: Point, - local_com2: Point, - im1: Real, - im2: Real, - ii1: AngularInertia, - ii2: AngularInertia, - - lin_inv_lhs: Real, - ang_inv_lhs: AngularInertia, -} - -impl FixedPositionConstraint { - pub fn from_params( - rb1: (&RigidBodyMassProps, &RigidBodyIds), - rb2: (&RigidBodyMassProps, &RigidBodyIds), - cparams: &FixedJoint, - ) -> Self { - let (mprops1, ids1) = rb1; - let (mprops2, ids2) = rb2; - - let ii1 = mprops1.effective_world_inv_inertia_sqrt.squared(); - let ii2 = mprops2.effective_world_inv_inertia_sqrt.squared(); - let im1 = mprops1.effective_inv_mass; - let im2 = mprops2.effective_inv_mass; - let lin_inv_lhs = 1.0 / (im1 + im2); - let ang_inv_lhs = (ii1 + ii2).inverse(); - - Self { - local_frame1: cparams.local_frame1, - local_frame2: cparams.local_frame2, - position1: ids1.active_set_offset, - position2: ids2.active_set_offset, - im1, - im2, - ii1, - ii2, - local_com1: mprops1.local_mprops.local_com, - local_com2: mprops2.local_mprops.local_com, - lin_inv_lhs, - ang_inv_lhs, - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - let mut position1 = positions[self.position1 as usize]; - let mut position2 = positions[self.position2 as usize]; - - // Angular correction. - let anchor1 = position1 * self.local_frame1; - let anchor2 = position2 * self.local_frame2; - let ang_err = anchor2.rotation * anchor1.rotation.inverse(); - #[cfg(feature = "dim3")] - let ang_impulse = self - .ang_inv_lhs - .transform_vector(ang_err.scaled_axis() * params.joint_erp); - #[cfg(feature = "dim2")] - let ang_impulse = self - .ang_inv_lhs - .transform_vector(ang_err.angle() * params.joint_erp); - position1.rotation = - Rotation::new(self.ii1.transform_vector(ang_impulse)) * position1.rotation; - position2.rotation = - Rotation::new(self.ii2.transform_vector(-ang_impulse)) * position2.rotation; - - // Linear correction. - let anchor1 = position1 * Point::from(self.local_frame1.translation.vector); - let anchor2 = position2 * Point::from(self.local_frame2.translation.vector); - let err = anchor2 - anchor1; - let impulse = err * (self.lin_inv_lhs * params.joint_erp); - position1.translation.vector += self.im1 * impulse; - position2.translation.vector -= self.im2 * impulse; - - positions[self.position1 as usize] = position1; - positions[self.position2 as usize] = position2; - } -} - -#[derive(Debug)] -pub(crate) struct FixedPositionGroundConstraint { - position2: usize, - anchor1: Isometry, - local_frame2: Isometry, - local_com2: Point, - im2: Real, - ii2: AngularInertia, - impulse: Real, -} - -impl FixedPositionGroundConstraint { - pub fn from_params( - rb1: &RigidBodyPosition, - rb2: (&RigidBodyMassProps, &RigidBodyIds), - cparams: &FixedJoint, - flipped: bool, - ) -> Self { - let poss1 = rb1; - let (mprops2, ids2) = rb2; - - let anchor1; - let local_frame2; - - if flipped { - anchor1 = poss1.next_position * cparams.local_frame2; - local_frame2 = cparams.local_frame1; - } else { - anchor1 = poss1.next_position * cparams.local_frame1; - local_frame2 = cparams.local_frame2; - }; - - Self { - anchor1, - local_frame2, - position2: ids2.active_set_offset, - im2: mprops2.effective_inv_mass, - ii2: mprops2.effective_world_inv_inertia_sqrt.squared(), - local_com2: mprops2.local_mprops.local_com, - impulse: 0.0, - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - let mut position2 = positions[self.position2 as usize]; - - // Angular correction. - let anchor2 = position2 * self.local_frame2; - let ang_err = anchor2.rotation * self.anchor1.rotation.inverse(); - position2.rotation = ang_err.powf(-params.joint_erp) * position2.rotation; - - // Linear correction. - let anchor1 = Point::from(self.anchor1.translation.vector); - let anchor2 = position2 * Point::from(self.local_frame2.translation.vector); - let err = anchor2 - anchor1; - // NOTE: no need to divide by im2 just to multiply right after. - let impulse = err * params.joint_erp; - position2.translation.vector -= impulse; - - positions[self.position2 as usize] = position2; - } -} diff --git a/src/dynamics/solver/joint_constraint/fixed_position_constraint_wide.rs b/src/dynamics/solver/joint_constraint/fixed_position_constraint_wide.rs deleted file mode 100644 index 0c0a6fd..0000000 --- a/src/dynamics/solver/joint_constraint/fixed_position_constraint_wide.rs +++ /dev/null @@ -1,71 +0,0 @@ -use super::{FixedPositionConstraint, FixedPositionGroundConstraint}; -use crate::dynamics::{ - FixedJoint, IntegrationParameters, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition, -}; -use crate::math::{Isometry, Real, SIMD_WIDTH}; - -// TODO: this does not uses SIMD optimizations yet. -#[derive(Debug)] -pub(crate) struct WFixedPositionConstraint { - constraints: [FixedPositionConstraint; SIMD_WIDTH], -} - -impl WFixedPositionConstraint { - pub fn from_params( - rbs1: ( - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - rbs2: ( - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - cparams: [&FixedJoint; SIMD_WIDTH], - ) -> Self { - Self { - constraints: gather![|ii| FixedPositionConstraint::from_params( - (rbs1.0[ii], rbs1.1[ii]), - (rbs2.0[ii], rbs2.1[ii]), - cparams[ii] - )], - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - for constraint in &self.constraints { - constraint.solve(params, positions); - } - } -} - -#[derive(Debug)] -pub(crate) struct WFixedPositionGroundConstraint { - constraints: [FixedPositionGroundConstraint; SIMD_WIDTH], -} - -impl WFixedPositionGroundConstraint { - pub fn from_params( - rbs1: [&RigidBodyPosition; SIMD_WIDTH], - rbs2: ( - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - cparams: [&FixedJoint; SIMD_WIDTH], - flipped: [bool; SIMD_WIDTH], - ) -> Self { - Self { - constraints: gather![|ii| FixedPositionGroundConstraint::from_params( - rbs1[ii], - (rbs2.0[ii], rbs2.1[ii]), - cparams[ii], - flipped[ii] - )], - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - for constraint in &self.constraints { - constraint.solve(params, positions); - } - } -} diff --git a/src/dynamics/solver/joint_constraint/fixed_velocity_constraint.rs b/src/dynamics/solver/joint_constraint/fixed_velocity_constraint.rs deleted file mode 100644 index 8bfc1a6..0000000 --- a/src/dynamics/solver/joint_constraint/fixed_velocity_constraint.rs +++ /dev/null @@ -1,436 +0,0 @@ -use crate::dynamics::solver::DeltaVel; -use crate::dynamics::{ - FixedJoint, IntegrationParameters, JointGraphEdge, JointIndex, JointParams, RigidBodyIds, - RigidBodyMassProps, RigidBodyPosition, RigidBodyVelocity, -}; -use crate::math::{AngularInertia, Real, SpacialVector, Vector, DIM}; -use crate::utils::{WAngularInertia, WCross, WCrossMatrix}; -#[cfg(feature = "dim2")] -use na::{Matrix3, Vector3}; -#[cfg(feature = "dim3")] -use na::{Matrix6, Vector6}; - -#[derive(Debug)] -pub(crate) struct FixedVelocityConstraint { - mj_lambda1: usize, - mj_lambda2: usize, - - joint_id: JointIndex, - - impulse: SpacialVector, - - #[cfg(feature = "dim3")] - inv_lhs: Matrix6, // FIXME: replace by Cholesky. - #[cfg(feature = "dim3")] - rhs: Vector6, - - #[cfg(feature = "dim2")] - inv_lhs: Matrix3, // FIXME: replace by Cholesky. - #[cfg(feature = "dim2")] - rhs: Vector3, - - im1: Real, - im2: Real, - - ii1: AngularInertia, - ii2: AngularInertia, - - ii1_sqrt: AngularInertia, - ii2_sqrt: AngularInertia, - - r1: Vector, - r2: Vector, -} - -impl FixedVelocityConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: JointIndex, - rb1: ( - &RigidBodyPosition, - &RigidBodyVelocity, - &RigidBodyMassProps, - &RigidBodyIds, - ), - rb2: ( - &RigidBodyPosition, - &RigidBodyVelocity, - &RigidBodyMassProps, - &RigidBodyIds, - ), - cparams: &FixedJoint, - ) -> Self { - let (poss1, vels1, mprops1, ids1) = rb1; - let (poss2, vels2, mprops2, ids2) = rb2; - - let anchor1 = poss1.position * cparams.local_frame1; - let anchor2 = poss2.position * cparams.local_frame2; - let im1 = mprops1.effective_inv_mass; - let im2 = mprops2.effective_inv_mass; - let ii1 = mprops1.effective_world_inv_inertia_sqrt.squared(); - let ii2 = mprops2.effective_world_inv_inertia_sqrt.squared(); - let r1 = anchor1.translation.vector - mprops1.world_com.coords; - let r2 = anchor2.translation.vector - mprops2.world_com.coords; - let rmat1 = r1.gcross_matrix(); - let rmat2 = r2.gcross_matrix(); - - #[allow(unused_mut)] // For 2D - let mut lhs; - - #[cfg(feature = "dim3")] - { - let lhs00 = - ii1.quadform(&rmat1).add_diagonal(im1) + ii2.quadform(&rmat2).add_diagonal(im2); - let lhs10 = ii1.transform_matrix(&rmat1) + ii2.transform_matrix(&rmat2); - let lhs11 = (ii1 + ii2).into_matrix(); - - // Note that Cholesky only reads the lower-triangular part of the matrix - // so we don't need to fill lhs01. - lhs = Matrix6::zeros(); - lhs.fixed_slice_mut::<3, 3>(0, 0) - .copy_from(&lhs00.into_matrix()); - lhs.fixed_slice_mut::<3, 3>(3, 0).copy_from(&lhs10); - lhs.fixed_slice_mut::<3, 3>(3, 3).copy_from(&lhs11); - } - - // In 2D we just unroll the computation because - // it's just easier that way. - #[cfg(feature = "dim2")] - { - let m11 = im1 + im2 + rmat1.x * rmat1.x * ii1 + rmat2.x * rmat2.x * ii2; - let m12 = rmat1.x * rmat1.y * ii1 + rmat2.x * rmat2.y * ii2; - let m22 = im1 + im2 + rmat1.y * rmat1.y * ii1 + rmat2.y * rmat2.y * ii2; - let m13 = rmat1.x * ii1 + rmat2.x * ii2; - let m23 = rmat1.y * ii1 + rmat2.y * ii2; - let m33 = ii1 + ii2; - lhs = Matrix3::new(m11, m12, m13, m12, m22, m23, m13, m23, m33) - } - - // NOTE: we don't use cholesky in 2D because we only have a 3x3 matrix - // for which a textbook inverse is still efficient. - #[cfg(feature = "dim2")] - let inv_lhs = lhs.try_inverse().expect("Singular system."); - #[cfg(feature = "dim3")] - let inv_lhs = lhs.cholesky().expect("Singular system.").inverse(); - - let lin_dvel = - -vels1.linvel - vels1.angvel.gcross(r1) + vels2.linvel + vels2.angvel.gcross(r2); - let ang_dvel = -vels1.angvel + vels2.angvel; - - #[cfg(feature = "dim2")] - let mut rhs = - Vector3::new(lin_dvel.x, lin_dvel.y, ang_dvel) * params.velocity_solve_fraction; - - #[cfg(feature = "dim3")] - let mut rhs = Vector6::new( - lin_dvel.x, lin_dvel.y, lin_dvel.z, ang_dvel.x, ang_dvel.y, ang_dvel.z, - ) * params.velocity_solve_fraction; - - let velocity_based_erp_inv_dt = params.velocity_based_erp_inv_dt(); - if velocity_based_erp_inv_dt != 0.0 { - let lin_err = anchor2.translation.vector - anchor1.translation.vector; - let ang_err = anchor2.rotation * anchor1.rotation.inverse(); - - #[cfg(feature = "dim2")] - { - let ang_err = ang_err.angle(); - rhs += Vector3::new(lin_err.x, lin_err.y, ang_err) * velocity_based_erp_inv_dt; - } - - #[cfg(feature = "dim3")] - { - let ang_err = ang_err.scaled_axis(); - rhs += Vector6::new( - lin_err.x, lin_err.y, lin_err.z, ang_err.x, ang_err.y, ang_err.z, - ) * velocity_based_erp_inv_dt; - } - } - - FixedVelocityConstraint { - joint_id, - mj_lambda1: ids1.active_set_offset, - mj_lambda2: ids2.active_set_offset, - im1, - im2, - ii1, - ii2, - ii1_sqrt: mprops1.effective_world_inv_inertia_sqrt, - ii2_sqrt: mprops2.effective_world_inv_inertia_sqrt, - impulse: cparams.impulse * params.warmstart_coeff, - inv_lhs, - r1, - r2, - rhs, - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = mj_lambdas[self.mj_lambda1 as usize]; - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - let lin_impulse = self.impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = self.impulse[2]; - #[cfg(feature = "dim3")] - let ang_impulse = self.impulse.fixed_rows::<3>(3).into_owned(); - - mj_lambda1.linear += self.im1 * lin_impulse; - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(ang_impulse + self.r1.gcross(lin_impulse)); - - mj_lambda2.linear -= self.im2 * lin_impulse; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - mj_lambdas[self.mj_lambda1 as usize] = mj_lambda1; - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = mj_lambdas[self.mj_lambda1 as usize]; - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - let ang_vel1 = self.ii1_sqrt.transform_vector(mj_lambda1.angular); - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let dlinvel = -mj_lambda1.linear - ang_vel1.gcross(self.r1) - + mj_lambda2.linear - + ang_vel2.gcross(self.r2); - let dangvel = -ang_vel1 + ang_vel2; - - #[cfg(feature = "dim2")] - let rhs = Vector3::new(dlinvel.x, dlinvel.y, dangvel) + self.rhs; - #[cfg(feature = "dim3")] - let rhs = Vector6::new( - dlinvel.x, dlinvel.y, dlinvel.z, dangvel.x, dangvel.y, dangvel.z, - ) + self.rhs; - - let impulse = self.inv_lhs * rhs; - self.impulse += impulse; - let lin_impulse = impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = impulse[2]; - #[cfg(feature = "dim3")] - let ang_impulse = impulse.fixed_rows::<3>(3).into_owned(); - - mj_lambda1.linear += self.im1 * lin_impulse; - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(ang_impulse + self.r1.gcross(lin_impulse)); - - mj_lambda2.linear -= self.im2 * lin_impulse; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - mj_lambdas[self.mj_lambda1 as usize] = mj_lambda1; - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - let joint = &mut joints_all[self.joint_id].weight; - if let JointParams::FixedJoint(fixed) = &mut joint.params { - fixed.impulse = self.impulse; - } - } -} - -#[derive(Debug)] -pub(crate) struct FixedVelocityGroundConstraint { - mj_lambda2: usize, - - joint_id: JointIndex, - - impulse: SpacialVector, - - #[cfg(feature = "dim3")] - inv_lhs: Matrix6, // FIXME: replace by Cholesky. - #[cfg(feature = "dim3")] - rhs: Vector6, - - #[cfg(feature = "dim2")] - inv_lhs: Matrix3, // FIXME: replace by Cholesky. - #[cfg(feature = "dim2")] - rhs: Vector3, - - im2: Real, - ii2: AngularInertia, - ii2_sqrt: AngularInertia, - r2: Vector, -} - -impl FixedVelocityGroundConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: JointIndex, - rb1: (&RigidBodyPosition, &RigidBodyVelocity, &RigidBodyMassProps), - rb2: ( - &RigidBodyPosition, - &RigidBodyVelocity, - &RigidBodyMassProps, - &RigidBodyIds, - ), - cparams: &FixedJoint, - flipped: bool, - ) -> Self { - let (poss1, vels1, mprops1) = rb1; - let (poss2, vels2, mprops2, ids2) = rb2; - - let (anchor1, anchor2) = if flipped { - ( - poss1.position * cparams.local_frame2, - poss2.position * cparams.local_frame1, - ) - } else { - ( - poss1.position * cparams.local_frame1, - poss2.position * cparams.local_frame2, - ) - }; - - let r1 = anchor1.translation.vector - mprops1.world_com.coords; - - let im2 = mprops2.effective_inv_mass; - let ii2 = mprops2.effective_world_inv_inertia_sqrt.squared(); - let r2 = anchor2.translation.vector - mprops2.world_com.coords; - let rmat2 = r2.gcross_matrix(); - - #[allow(unused_mut)] // For 2D. - let mut lhs; - - #[cfg(feature = "dim3")] - { - let lhs00 = ii2.quadform(&rmat2).add_diagonal(im2); - let lhs10 = ii2.transform_matrix(&rmat2); - let lhs11 = ii2.into_matrix(); - - // Note that Cholesky only reads the lower-triangular part of the matrix - // so we don't need to fill lhs01. - lhs = Matrix6::zeros(); - lhs.fixed_slice_mut::<3, 3>(0, 0) - .copy_from(&lhs00.into_matrix()); - lhs.fixed_slice_mut::<3, 3>(3, 0).copy_from(&lhs10); - lhs.fixed_slice_mut::<3, 3>(3, 3).copy_from(&lhs11); - } - - // In 2D we just unroll the computation because - // it's just easier that way. - #[cfg(feature = "dim2")] - { - let m11 = im2 + rmat2.x * rmat2.x * ii2; - let m12 = rmat2.x * rmat2.y * ii2; - let m22 = im2 + rmat2.y * rmat2.y * ii2; - let m13 = rmat2.x * ii2; - let m23 = rmat2.y * ii2; - let m33 = ii2; - lhs = Matrix3::new(m11, m12, m13, m12, m22, m23, m13, m23, m33) - } - - #[cfg(feature = "dim2")] - let inv_lhs = lhs.try_inverse().expect("Singular system."); - #[cfg(feature = "dim3")] - let inv_lhs = lhs.cholesky().expect("Singular system.").inverse(); - - let lin_dvel = - vels2.linvel + vels2.angvel.gcross(r2) - vels1.linvel - vels1.angvel.gcross(r1); - let ang_dvel = vels2.angvel - vels1.angvel; - - #[cfg(feature = "dim2")] - let mut rhs = - Vector3::new(lin_dvel.x, lin_dvel.y, ang_dvel) * params.velocity_solve_fraction; - #[cfg(feature = "dim3")] - let mut rhs = Vector6::new( - lin_dvel.x, lin_dvel.y, lin_dvel.z, ang_dvel.x, ang_dvel.y, ang_dvel.z, - ) * params.velocity_solve_fraction; - - let velocity_based_erp_inv_dt = params.velocity_based_erp_inv_dt(); - if velocity_based_erp_inv_dt != 0.0 { - let lin_err = anchor2.translation.vector - anchor1.translation.vector; - let ang_err = anchor2.rotation * anchor1.rotation.inverse(); - - #[cfg(feature = "dim2")] - { - let ang_err = ang_err.angle(); - rhs += Vector3::new(lin_err.x, lin_err.y, ang_err) * velocity_based_erp_inv_dt; - } - - #[cfg(feature = "dim3")] - { - let ang_err = ang_err.scaled_axis(); - rhs += Vector6::new( - lin_err.x, lin_err.y, lin_err.z, ang_err.x, ang_err.y, ang_err.z, - ) * velocity_based_erp_inv_dt; - } - } - - FixedVelocityGroundConstraint { - joint_id, - mj_lambda2: ids2.active_set_offset, - im2, - ii2, - ii2_sqrt: mprops2.effective_world_inv_inertia_sqrt, - impulse: cparams.impulse * params.warmstart_coeff, - inv_lhs, - r2, - rhs, - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - let lin_impulse = self.impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = self.impulse[2]; - #[cfg(feature = "dim3")] - let ang_impulse = self.impulse.fixed_rows::<3>(3).into_owned(); - - mj_lambda2.linear -= self.im2 * lin_impulse; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let dlinvel = mj_lambda2.linear + ang_vel2.gcross(self.r2); - let dangvel = ang_vel2; - #[cfg(feature = "dim2")] - let rhs = Vector3::new(dlinvel.x, dlinvel.y, dangvel) + self.rhs; - #[cfg(feature = "dim3")] - let rhs = Vector6::new( - dlinvel.x, dlinvel.y, dlinvel.z, dangvel.x, dangvel.y, dangvel.z, - ) + self.rhs; - - let impulse = self.inv_lhs * rhs; - - self.impulse += impulse; - let lin_impulse = impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = impulse[2]; - #[cfg(feature = "dim3")] - let ang_impulse = impulse.fixed_rows::<3>(3).into_owned(); - - mj_lambda2.linear -= self.im2 * lin_impulse; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - // FIXME: duplicated code with the non-ground constraint. - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - let joint = &mut joints_all[self.joint_id].weight; - if let JointParams::FixedJoint(fixed) = &mut joint.params { - fixed.impulse = self.impulse; - } - } -} diff --git a/src/dynamics/solver/joint_constraint/fixed_velocity_constraint_wide.rs b/src/dynamics/solver/joint_constraint/fixed_velocity_constraint_wide.rs deleted file mode 100644 index 0421d49..0000000 --- a/src/dynamics/solver/joint_constraint/fixed_velocity_constraint_wide.rs +++ /dev/null @@ -1,539 +0,0 @@ -use simba::simd::SimdValue; - -use crate::dynamics::solver::DeltaVel; -use crate::dynamics::{ - FixedJoint, IntegrationParameters, JointGraphEdge, JointIndex, JointParams, RigidBodyIds, - RigidBodyMassProps, RigidBodyPosition, RigidBodyVelocity, -}; -use crate::math::{ - AngVector, AngularInertia, CrossMatrix, Isometry, Point, Real, SimdReal, SpacialVector, Vector, - DIM, SIMD_WIDTH, -}; -use crate::utils::{WAngularInertia, WCross, WCrossMatrix}; -#[cfg(feature = "dim3")] -use na::{Cholesky, Matrix6, Vector3, Vector6}; -#[cfg(feature = "dim2")] -use { - na::{Matrix3, Vector3}, - parry::utils::SdpMatrix3, -}; - -#[derive(Debug)] -pub(crate) struct WFixedVelocityConstraint { - mj_lambda1: [usize; SIMD_WIDTH], - mj_lambda2: [usize; SIMD_WIDTH], - - joint_id: [JointIndex; SIMD_WIDTH], - - impulse: SpacialVector, - - #[cfg(feature = "dim3")] - inv_lhs: Matrix6, // FIXME: replace by Cholesky. - #[cfg(feature = "dim3")] - rhs: Vector6, - - #[cfg(feature = "dim2")] - inv_lhs: Matrix3, - #[cfg(feature = "dim2")] - rhs: Vector3, - - im1: SimdReal, - im2: SimdReal, - - ii1: AngularInertia, - ii2: AngularInertia, - - ii1_sqrt: AngularInertia, - ii2_sqrt: AngularInertia, - - r1: Vector, - r2: Vector, -} - -impl WFixedVelocityConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: [JointIndex; SIMD_WIDTH], - rbs1: ( - [&RigidBodyPosition; SIMD_WIDTH], - [&RigidBodyVelocity; SIMD_WIDTH], - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - rbs2: ( - [&RigidBodyPosition; SIMD_WIDTH], - [&RigidBodyVelocity; SIMD_WIDTH], - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - cparams: [&FixedJoint; SIMD_WIDTH], - ) -> Self { - let (poss1, vels1, mprops1, ids1) = rbs1; - let (poss2, vels2, mprops2, ids2) = rbs2; - - let position1 = Isometry::from(gather![|ii| poss1[ii].position]); - let linvel1 = Vector::from(gather![|ii| vels1[ii].linvel]); - let angvel1 = AngVector::::from(gather![|ii| vels1[ii].angvel]); - let world_com1 = Point::from(gather![|ii| mprops1[ii].world_com]); - let im1 = SimdReal::from(gather![|ii| mprops1[ii].effective_inv_mass]); - let ii1_sqrt = AngularInertia::::from(gather![ - |ii| mprops1[ii].effective_world_inv_inertia_sqrt - ]); - let mj_lambda1 = gather![|ii| ids1[ii].active_set_offset]; - - let position2 = Isometry::from(gather![|ii| poss2[ii].position]); - let linvel2 = Vector::from(gather![|ii| vels2[ii].linvel]); - let angvel2 = AngVector::::from(gather![|ii| vels2[ii].angvel]); - let world_com2 = Point::from(gather![|ii| mprops2[ii].world_com]); - let im2 = SimdReal::from(gather![|ii| mprops2[ii].effective_inv_mass]); - let ii2_sqrt = AngularInertia::::from(gather![ - |ii| mprops2[ii].effective_world_inv_inertia_sqrt - ]); - let mj_lambda2 = gather![|ii| ids2[ii].active_set_offset]; - - let local_frame1 = Isometry::from(gather![|ii| cparams[ii].local_frame1]); - let local_frame2 = Isometry::from(gather![|ii| cparams[ii].local_frame2]); - let impulse = SpacialVector::from(gather![|ii| cparams[ii].impulse]); - - let anchor1 = position1 * local_frame1; - let anchor2 = position2 * local_frame2; - let ii1 = ii1_sqrt.squared(); - let ii2 = ii2_sqrt.squared(); - let r1 = anchor1.translation.vector - world_com1.coords; - let r2 = anchor2.translation.vector - world_com2.coords; - let rmat1: CrossMatrix<_> = r1.gcross_matrix(); - let rmat2: CrossMatrix<_> = r2.gcross_matrix(); - - #[allow(unused_mut)] // For 2D. - let mut lhs; - - #[cfg(feature = "dim3")] - { - let lhs00 = - ii1.quadform(&rmat1).add_diagonal(im1) + ii2.quadform(&rmat2).add_diagonal(im2); - let lhs10 = ii1.transform_matrix(&rmat1) + ii2.transform_matrix(&rmat2); - let lhs11 = (ii1 + ii2).into_matrix(); - - // Note that Cholesky only reads the lower-triangular part of the matrix - // so we don't need to fill lhs01. - lhs = Matrix6::zeros(); - lhs.fixed_slice_mut::<3, 3>(0, 0) - .copy_from(&lhs00.into_matrix()); - lhs.fixed_slice_mut::<3, 3>(3, 0).copy_from(&lhs10); - lhs.fixed_slice_mut::<3, 3>(3, 3).copy_from(&lhs11); - } - - // In 2D we just unroll the computation because - // it's just easier that way. - #[cfg(feature = "dim2")] - { - let m11 = im1 + im2 + rmat1.x * rmat1.x * ii1 + rmat2.x * rmat2.x * ii2; - let m12 = rmat1.x * rmat1.y * ii1 + rmat2.x * rmat2.y * ii2; - let m22 = im1 + im2 + rmat1.y * rmat1.y * ii1 + rmat2.y * rmat2.y * ii2; - let m13 = rmat1.x * ii1 + rmat2.x * ii2; - let m23 = rmat1.y * ii1 + rmat2.y * ii2; - let m33 = ii1 + ii2; - lhs = SdpMatrix3::new(m11, m12, m13, m22, m23, m33) - } - - // NOTE: we don't use cholesky in 2D because we only have a 3x3 matrix - // for which a textbook inverse is still efficient. - #[cfg(feature = "dim2")] - let inv_lhs = lhs.inverse_unchecked().into_matrix(); // FIXME: don't extract the matrix? - #[cfg(feature = "dim3")] - let inv_lhs = Cholesky::new_unchecked(lhs).inverse(); - - let lin_dvel = -linvel1 - angvel1.gcross(r1) + linvel2 + angvel2.gcross(r2); - let ang_dvel = -angvel1 + angvel2; - - let velocity_solve_fraction = SimdReal::splat(params.velocity_solve_fraction); - - #[cfg(feature = "dim2")] - let mut rhs = Vector3::new(lin_dvel.x, lin_dvel.y, ang_dvel) * velocity_solve_fraction; - - #[cfg(feature = "dim3")] - let mut rhs = Vector6::new( - lin_dvel.x, lin_dvel.y, lin_dvel.z, ang_dvel.x, ang_dvel.y, ang_dvel.z, - ) * velocity_solve_fraction; - - let velocity_based_erp_inv_dt = params.velocity_based_erp_inv_dt(); - if velocity_based_erp_inv_dt != 0.0 { - let velocity_based_erp_inv_dt = SimdReal::splat(velocity_based_erp_inv_dt); - - let lin_err = anchor2.translation.vector - anchor1.translation.vector; - let ang_err = anchor2.rotation * anchor1.rotation.inverse(); - - #[cfg(feature = "dim2")] - { - let ang_err = ang_err.angle(); - rhs += Vector3::new(lin_err.x, lin_err.y, ang_err) * velocity_based_erp_inv_dt; - } - - #[cfg(feature = "dim3")] - { - let ang_err = Vector3::from(gather![|ii| ang_err.extract(ii).scaled_axis()]); - rhs += Vector6::new( - lin_err.x, lin_err.y, lin_err.z, ang_err.x, ang_err.y, ang_err.z, - ) * velocity_based_erp_inv_dt; - } - } - - WFixedVelocityConstraint { - joint_id, - mj_lambda1, - mj_lambda2, - im1, - im2, - ii1, - ii2, - ii1_sqrt, - ii2_sqrt, - impulse: impulse * SimdReal::splat(params.warmstart_coeff), - inv_lhs, - r1, - r2, - rhs, - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda1[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda1[ii] as usize].angular - ]), - }; - let mut mj_lambda2 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - let lin_impulse = self.impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = self.impulse[2]; - #[cfg(feature = "dim3")] - let ang_impulse = self.impulse.fixed_rows::<3>(3).into_owned(); - - mj_lambda1.linear += lin_impulse * self.im1; - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(ang_impulse + self.r1.gcross(lin_impulse)); - - mj_lambda2.linear -= lin_impulse * self.im2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda1[ii] as usize].linear = mj_lambda1.linear.extract(ii); - mj_lambdas[self.mj_lambda1[ii] as usize].angular = mj_lambda1.angular.extract(ii); - } - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1: DeltaVel = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda1[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda1[ii] as usize].angular - ]), - }; - let mut mj_lambda2: DeltaVel = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - let ang_vel1 = self.ii1_sqrt.transform_vector(mj_lambda1.angular); - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let dlinvel = -mj_lambda1.linear - ang_vel1.gcross(self.r1) - + mj_lambda2.linear - + ang_vel2.gcross(self.r2); - let dangvel = -ang_vel1 + ang_vel2; - - #[cfg(feature = "dim2")] - let rhs = Vector3::new(dlinvel.x, dlinvel.y, dangvel) + self.rhs; - #[cfg(feature = "dim3")] - let rhs = Vector6::new( - dlinvel.x, dlinvel.y, dlinvel.z, dangvel.x, dangvel.y, dangvel.z, - ) + self.rhs; - - let impulse = self.inv_lhs * rhs; - self.impulse += impulse; - let lin_impulse = impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = impulse[2]; - #[cfg(feature = "dim3")] - let ang_impulse = impulse.fixed_rows::<3>(3).into_owned(); - - mj_lambda1.linear += lin_impulse * self.im1; - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(ang_impulse + self.r1.gcross(lin_impulse)); - - mj_lambda2.linear -= lin_impulse * self.im2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda1[ii] as usize].linear = mj_lambda1.linear.extract(ii); - mj_lambdas[self.mj_lambda1[ii] as usize].angular = mj_lambda1.angular.extract(ii); - } - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - for ii in 0..SIMD_WIDTH { - let joint = &mut joints_all[self.joint_id[ii]].weight; - if let JointParams::FixedJoint(fixed) = &mut joint.params { - fixed.impulse = self.impulse.extract(ii) - } - } - } -} - -#[derive(Debug)] -pub(crate) struct WFixedVelocityGroundConstraint { - mj_lambda2: [usize; SIMD_WIDTH], - - joint_id: [JointIndex; SIMD_WIDTH], - - impulse: SpacialVector, - - #[cfg(feature = "dim3")] - inv_lhs: Matrix6, // FIXME: replace by Cholesky. - #[cfg(feature = "dim3")] - rhs: Vector6, - - #[cfg(feature = "dim2")] - inv_lhs: Matrix3, - #[cfg(feature = "dim2")] - rhs: Vector3, - - im2: SimdReal, - ii2: AngularInertia, - ii2_sqrt: AngularInertia, - r2: Vector, -} - -impl WFixedVelocityGroundConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: [JointIndex; SIMD_WIDTH], - rbs1: ( - [&RigidBodyPosition; SIMD_WIDTH], - [&RigidBodyVelocity; SIMD_WIDTH], - [&RigidBodyMassProps; SIMD_WIDTH], - ), - rbs2: ( - [&RigidBodyPosition; SIMD_WIDTH], - [&RigidBodyVelocity; SIMD_WIDTH], - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - cparams: [&FixedJoint; SIMD_WIDTH], - flipped: [bool; SIMD_WIDTH], - ) -> Self { - let (poss1, vels1, mprops1) = rbs1; - let (poss2, vels2, mprops2, ids2) = rbs2; - - let position1 = Isometry::from(gather![|ii| poss1[ii].position]); - let linvel1 = Vector::from(gather![|ii| vels1[ii].linvel]); - let angvel1 = AngVector::::from(gather![|ii| vels1[ii].angvel]); - let world_com1 = Point::from(gather![|ii| mprops1[ii].world_com]); - - let position2 = Isometry::from(gather![|ii| poss2[ii].position]); - let linvel2 = Vector::from(gather![|ii| vels2[ii].linvel]); - let angvel2 = AngVector::::from(gather![|ii| vels2[ii].angvel]); - let world_com2 = Point::from(gather![|ii| mprops2[ii].world_com]); - let im2 = SimdReal::from(gather![|ii| mprops2[ii].effective_inv_mass]); - let ii2_sqrt = AngularInertia::::from(gather![ - |ii| mprops2[ii].effective_world_inv_inertia_sqrt - ]); - let mj_lambda2 = gather![|ii| ids2[ii].active_set_offset]; - - let local_frame1 = Isometry::from(gather![|ii| if flipped[ii] { - cparams[ii].local_frame2 - } else { - cparams[ii].local_frame1 - }]); - let local_frame2 = Isometry::from(gather![|ii| if flipped[ii] { - cparams[ii].local_frame1 - } else { - cparams[ii].local_frame2 - }]); - let impulse = SpacialVector::from(gather![|ii| cparams[ii].impulse]); - - let anchor1 = position1 * local_frame1; - let anchor2 = position2 * local_frame2; - let ii2 = ii2_sqrt.squared(); - let r1 = anchor1.translation.vector - world_com1.coords; - let r2 = anchor2.translation.vector - world_com2.coords; - let rmat2: CrossMatrix<_> = r2.gcross_matrix(); - - #[allow(unused_mut)] // For 2D. - let mut lhs; - - #[cfg(feature = "dim3")] - { - let lhs00 = ii2.quadform(&rmat2).add_diagonal(im2); - let lhs10 = ii2.transform_matrix(&rmat2); - let lhs11 = ii2.into_matrix(); - - lhs = Matrix6::zeros(); - lhs.fixed_slice_mut::<3, 3>(0, 0) - .copy_from(&lhs00.into_matrix()); - lhs.fixed_slice_mut::<3, 3>(3, 0).copy_from(&lhs10); - lhs.fixed_slice_mut::<3, 3>(3, 3).copy_from(&lhs11); - } - - // In 2D we just unroll the computation because - // it's just easier that way. - #[cfg(feature = "dim2")] - { - let m11 = im2 + rmat2.x * rmat2.x * ii2; - let m12 = rmat2.x * rmat2.y * ii2; - let m22 = im2 + rmat2.y * rmat2.y * ii2; - let m13 = rmat2.x * ii2; - let m23 = rmat2.y * ii2; - let m33 = ii2; - lhs = SdpMatrix3::new(m11, m12, m13, m22, m23, m33) - } - - #[cfg(feature = "dim2")] - let inv_lhs = lhs.inverse_unchecked().into_matrix(); // FIXME: don't do into_matrix? - #[cfg(feature = "dim3")] - let inv_lhs = Cholesky::new_unchecked(lhs).inverse(); - - let lin_dvel = linvel2 + angvel2.gcross(r2) - linvel1 - angvel1.gcross(r1); - let ang_dvel = angvel2 - angvel1; - - let velocity_solve_fraction = SimdReal::splat(params.velocity_solve_fraction); - - #[cfg(feature = "dim2")] - let mut rhs = Vector3::new(lin_dvel.x, lin_dvel.y, ang_dvel) * velocity_solve_fraction; - - #[cfg(feature = "dim3")] - let mut rhs = Vector6::new( - lin_dvel.x, lin_dvel.y, lin_dvel.z, ang_dvel.x, ang_dvel.y, ang_dvel.z, - ) * velocity_solve_fraction; - - let velocity_based_erp_inv_dt = params.velocity_based_erp_inv_dt(); - if velocity_based_erp_inv_dt != 0.0 { - let velocity_based_erp_inv_dt = SimdReal::splat(velocity_based_erp_inv_dt); - - let lin_err = anchor2.translation.vector - anchor1.translation.vector; - let ang_err = anchor2.rotation * anchor1.rotation.inverse(); - - #[cfg(feature = "dim2")] - { - let ang_err = ang_err.angle(); - rhs += Vector3::new(lin_err.x, lin_err.y, ang_err) * velocity_based_erp_inv_dt; - } - - #[cfg(feature = "dim3")] - { - let ang_err = Vector3::from(gather![|ii| ang_err.extract(ii).scaled_axis()]); - rhs += Vector6::new( - lin_err.x, lin_err.y, lin_err.z, ang_err.x, ang_err.y, ang_err.z, - ) * velocity_based_erp_inv_dt; - } - } - - WFixedVelocityGroundConstraint { - joint_id, - mj_lambda2, - im2, - ii2, - ii2_sqrt, - impulse: impulse * SimdReal::splat(params.warmstart_coeff), - inv_lhs, - r2, - rhs, - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - let lin_impulse = self.impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = self.impulse[2]; - #[cfg(feature = "dim3")] - let ang_impulse = self.impulse.fixed_rows::<3>(3).into_owned(); - - mj_lambda2.linear -= lin_impulse * self.im2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2: DeltaVel = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - let dlinvel = mj_lambda2.linear + ang_vel2.gcross(self.r2); - let dangvel = ang_vel2; - #[cfg(feature = "dim2")] - let rhs = Vector3::new(dlinvel.x, dlinvel.y, dangvel) + self.rhs; - #[cfg(feature = "dim3")] - let rhs = Vector6::new( - dlinvel.x, dlinvel.y, dlinvel.z, dangvel.x, dangvel.y, dangvel.z, - ) + self.rhs; - - let impulse = self.inv_lhs * rhs; - - self.impulse += impulse; - let lin_impulse = impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = impulse[2]; - #[cfg(feature = "dim3")] - let ang_impulse = impulse.fixed_rows::<3>(3).into_owned(); - - mj_lambda2.linear -= lin_impulse * self.im2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - // FIXME: duplicated code with the non-ground constraint. - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - for ii in 0..SIMD_WIDTH { - let joint = &mut joints_all[self.joint_id[ii]].weight; - if let JointParams::FixedJoint(fixed) = &mut joint.params { - fixed.impulse = self.impulse.extract(ii) - } - } - } -} diff --git a/src/dynamics/solver/joint_constraint/generic_position_constraint.rs b/src/dynamics/solver/joint_constraint/generic_position_constraint.rs deleted file mode 100644 index a7b5ea0..0000000 --- a/src/dynamics/solver/joint_constraint/generic_position_constraint.rs +++ /dev/null @@ -1,346 +0,0 @@ -use super::{GenericVelocityConstraint, GenericVelocityGroundConstraint}; -use crate::dynamics::solver::DeltaVel; -use crate::dynamics::{GenericJoint, IntegrationParameters}; -use crate::math::{ - AngDim, AngVector, AngularInertia, Dim, Isometry, Point, Real, Rotation, SpatialVector, Vector, - DIM, -}; -use crate::utils::{WAngularInertia, WCross, WCrossMatrix}; -use na::{Vector3, Vector6}; - -// FIXME: review this code for the case where the center of masses are not the origin. -#[derive(Debug)] -pub(crate) struct GenericPositionConstraint { - position1: usize, - position2: usize, - local_anchor1: Isometry, - local_anchor2: Isometry, - local_com1: Point, - local_com2: Point, - im1: Real, - im2: Real, - ii1: AngularInertia, - ii2: AngularInertia, - - joint: GenericJoint, -} - -impl GenericPositionConstraint { - pub fn from_params(rb1: &RigidBody, rb2: &RigidBody, joint: &GenericJoint) -> Self { - let ii1 = rb1.effective_world_inv_inertia_sqrt.squared(); - let ii2 = rb2.effective_world_inv_inertia_sqrt.squared(); - let im1 = rb1.effective_inv_mass; - let im2 = rb2.effective_inv_mass; - - Self { - local_anchor1: joint.local_anchor1, - local_anchor2: joint.local_anchor2, - position1: rb1.active_set_offset, - position2: rb2.active_set_offset, - im1, - im2, - ii1, - ii2, - local_com1: rb1.local_mprops.local_com, - local_com2: rb2.local_mprops.local_com, - joint: *joint, - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - let mut position1 = positions[self.position1]; - let mut position2 = positions[self.position2]; - let mut params = *params; - params.joint_erp = 0.8; - - /* - * - * Translation part. - * - */ - { - let anchor1 = position1 * self.joint.local_anchor1; - let anchor2 = position2 * self.joint.local_anchor2; - let basis = anchor1.rotation; - let r1 = Point::from(anchor1.translation.vector) - position1 * self.local_com1; - let r2 = Point::from(anchor2.translation.vector) - position2 * self.local_com2; - let mut min_pos_impulse = self.joint.min_pos_impulse.xyz(); - let mut max_pos_impulse = self.joint.max_pos_impulse.xyz(); - - let pos_rhs = GenericVelocityConstraint::compute_lin_position_error( - &anchor1, - &anchor2, - &basis, - &self.joint.min_position.xyz(), - &self.joint.max_position.xyz(), - ) * params.joint_erp; - - for i in 0..3 { - if pos_rhs[i] < 0.0 { - min_pos_impulse[i] = -Real::MAX; - } - if pos_rhs[i] > 0.0 { - max_pos_impulse[i] = Real::MAX; - } - } - - let rotmat = basis.to_rotation_matrix().into_inner(); - let rmat1 = r1.gcross_matrix() * rotmat; - let rmat2 = r2.gcross_matrix() * rotmat; - - // Will be actually inverted right after. - // TODO: we should keep the SdpMatrix3 type. - let delassus = (self.ii1.quadform(&rmat1).add_diagonal(self.im1) - + self.ii2.quadform(&rmat2).add_diagonal(self.im2)) - .into_matrix(); - - let inv_delassus = GenericVelocityConstraint::invert_partial_delassus_matrix( - &min_pos_impulse, - &max_pos_impulse, - &mut Vector3::zeros(), - delassus, - ); - - let local_impulse = (inv_delassus * pos_rhs) - .inf(&max_pos_impulse) - .sup(&min_pos_impulse); - let impulse = basis * local_impulse; - - let rot1 = self.ii1.transform_vector(r1.gcross(impulse)); - let rot2 = self.ii2.transform_vector(r2.gcross(impulse)); - - position1.translation.vector += self.im1 * impulse; - position1.rotation = position1.rotation.append_axisangle_linearized(&rot1); - position2.translation.vector -= self.im2 * impulse; - position2.rotation = position2.rotation.append_axisangle_linearized(&-rot2); - } - - /* - * - * Rotation part - * - */ - { - let anchor1 = position1 * self.joint.local_anchor1; - let anchor2 = position2 * self.joint.local_anchor2; - let basis = anchor1.rotation; - let mut min_pos_impulse = self - .joint - .min_pos_impulse - .fixed_rows::(DIM) - .into_owned(); - let mut max_pos_impulse = self - .joint - .max_pos_impulse - .fixed_rows::(DIM) - .into_owned(); - - let pos_rhs = GenericVelocityConstraint::compute_ang_position_error( - &anchor1, - &anchor2, - &basis, - &self.joint.min_position.fixed_rows::(DIM).into_owned(), - &self.joint.max_position.fixed_rows::(DIM).into_owned(), - ) * params.joint_erp; - - for i in 0..3 { - if pos_rhs[i] < 0.0 { - min_pos_impulse[i] = -Real::MAX; - } - if pos_rhs[i] > 0.0 { - max_pos_impulse[i] = Real::MAX; - } - } - - // TODO: we should keep the SdpMatrix3 type. - let rotmat = basis.to_rotation_matrix().into_inner(); - let delassus = (self.ii1.quadform(&rotmat) + self.ii2.quadform(&rotmat)).into_matrix(); - - let inv_delassus = GenericVelocityConstraint::invert_partial_delassus_matrix( - &min_pos_impulse, - &max_pos_impulse, - &mut Vector3::zeros(), - delassus, - ); - - let local_impulse = (inv_delassus * pos_rhs) - .inf(&max_pos_impulse) - .sup(&min_pos_impulse); - let impulse = basis * local_impulse; - - let rot1 = self.ii1.transform_vector(impulse); - let rot2 = self.ii2.transform_vector(impulse); - - position1.rotation = position1.rotation.append_axisangle_linearized(&rot1); - position2.rotation = position2.rotation.append_axisangle_linearized(&-rot2); - } - - positions[self.position1] = position1; - positions[self.position2] = position2; - } -} - -#[derive(Debug)] -pub(crate) struct GenericPositionGroundConstraint { - position2: usize, - anchor1: Isometry, - local_anchor2: Isometry, - local_com2: Point, - im2: Real, - ii2: AngularInertia, - joint: GenericJoint, -} - -impl GenericPositionGroundConstraint { - pub fn from_params( - rb1: &RigidBody, - rb2: &RigidBody, - joint: &GenericJoint, - flipped: bool, - ) -> Self { - let anchor1; - let local_anchor2; - - if flipped { - anchor1 = rb1.predicted_position * joint.local_anchor2; - local_anchor2 = joint.local_anchor1; - } else { - anchor1 = rb1.predicted_position * joint.local_anchor1; - local_anchor2 = joint.local_anchor2; - }; - - Self { - anchor1, - local_anchor2, - position2: rb2.active_set_offset, - im2: rb2.effective_inv_mass, - ii2: rb2.effective_world_inv_inertia_sqrt.squared(), - local_com2: rb2.local_mprops.local_com, - joint: *joint, - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - let mut position2 = positions[self.position2]; - let mut params = *params; - params.joint_erp = 0.8; - - /* - * - * Translation part. - * - */ - { - let anchor1 = self.anchor1; - let anchor2 = position2 * self.local_anchor2; - let basis = anchor1.rotation; - let r2 = Point::from(anchor2.translation.vector) - position2 * self.local_com2; - let mut min_pos_impulse = self.joint.min_pos_impulse.xyz(); - let mut max_pos_impulse = self.joint.max_pos_impulse.xyz(); - - let pos_rhs = GenericVelocityConstraint::compute_lin_position_error( - &anchor1, - &anchor2, - &basis, - &self.joint.min_position.xyz(), - &self.joint.max_position.xyz(), - ) * params.joint_erp; - - for i in 0..3 { - if pos_rhs[i] < 0.0 { - min_pos_impulse[i] = -Real::MAX; - } - if pos_rhs[i] > 0.0 { - max_pos_impulse[i] = Real::MAX; - } - } - - let rotmat = basis.to_rotation_matrix().into_inner(); - let rmat2 = r2.gcross_matrix() * rotmat; - - // TODO: we should keep the SdpMatrix3 type. - let delassus = self - .ii2 - .quadform(&rmat2) - .add_diagonal(self.im2) - .into_matrix(); - - let inv_delassus = GenericVelocityConstraint::invert_partial_delassus_matrix( - &min_pos_impulse, - &max_pos_impulse, - &mut Vector3::zeros(), - delassus, - ); - - let local_impulse = (inv_delassus * pos_rhs) - .inf(&max_pos_impulse) - .sup(&min_pos_impulse); - let impulse = basis * local_impulse; - - let rot2 = self.ii2.transform_vector(r2.gcross(impulse)); - - position2.translation.vector -= self.im2 * impulse; - position2.rotation = position2.rotation.append_axisangle_linearized(&-rot2); - } - - /* - * - * Rotation part - * - */ - { - let anchor1 = self.anchor1; - let anchor2 = position2 * self.local_anchor2; - let basis = anchor1.rotation; - let mut min_pos_impulse = self - .joint - .min_pos_impulse - .fixed_rows::(DIM) - .into_owned(); - let mut max_pos_impulse = self - .joint - .max_pos_impulse - .fixed_rows::(DIM) - .into_owned(); - - let pos_rhs = GenericVelocityConstraint::compute_ang_position_error( - &anchor1, - &anchor2, - &basis, - &self.joint.min_position.fixed_rows::(DIM).into_owned(), - &self.joint.max_position.fixed_rows::(DIM).into_owned(), - ) * params.joint_erp; - - for i in 0..3 { - if pos_rhs[i] < 0.0 { - min_pos_impulse[i] = -Real::MAX; - } - if pos_rhs[i] > 0.0 { - max_pos_impulse[i] = Real::MAX; - } - } - - // Will be actually inverted right after. - // TODO: we should keep the SdpMatrix3 type. - let rotmat = basis.to_rotation_matrix().into_inner(); - let delassus = self.ii2.quadform(&rotmat).into_matrix(); - - let inv_delassus = GenericVelocityConstraint::invert_partial_delassus_matrix( - &min_pos_impulse, - &max_pos_impulse, - &mut Vector3::zeros(), - delassus, - ); - - let local_impulse = (inv_delassus * pos_rhs) - .inf(&max_pos_impulse) - .sup(&min_pos_impulse); - let impulse = basis * local_impulse; - let rot2 = self.ii2.transform_vector(impulse); - - position2.rotation = position2.rotation.append_axisangle_linearized(&-rot2); - } - - positions[self.position2] = position2; - } -} diff --git a/src/dynamics/solver/joint_constraint/generic_position_constraint_wide.rs b/src/dynamics/solver/joint_constraint/generic_position_constraint_wide.rs deleted file mode 100644 index d44c761..0000000 --- a/src/dynamics/solver/joint_constraint/generic_position_constraint_wide.rs +++ /dev/null @@ -1,60 +0,0 @@ -use super::{GenericPositionConstraint, GenericPositionGroundConstraint}; -use crate::dynamics::{GenericJoint, IntegrationParameters, RigidBody}; -use crate::math::{Isometry, Real, SIMD_WIDTH}; - -// TODO: this does not uses SIMD optimizations yet. -#[derive(Debug)] -pub(crate) struct WGenericPositionConstraint { - constraints: [GenericPositionConstraint; SIMD_WIDTH], -} - -impl WGenericPositionConstraint { - pub fn from_params( - rbs1: [&RigidBody; SIMD_WIDTH], - rbs2: [&RigidBody; SIMD_WIDTH], - cparams: [&GenericJoint; SIMD_WIDTH], - ) -> Self { - Self { - constraints: gather![|ii| GenericPositionConstraint::from_params( - rbs1[ii], - rbs2[ii], - cparams[ii] - )], - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - for constraint in &self.constraints { - constraint.solve(params, positions); - } - } -} - -#[derive(Debug)] -pub(crate) struct WGenericPositionGroundConstraint { - constraints: [GenericPositionGroundConstraint; SIMD_WIDTH], -} - -impl WGenericPositionGroundConstraint { - pub fn from_params( - rbs1: [&RigidBody; SIMD_WIDTH], - rbs2: [&RigidBody; SIMD_WIDTH], - cparams: [&GenericJoint; SIMD_WIDTH], - flipped: [bool; SIMD_WIDTH], - ) -> Self { - Self { - constraints: gather![|ii| GenericPositionGroundConstraint::from_params( - rbs1[ii], - rbs2[ii], - cparams[ii], - flipped[ii] - )], - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - for constraint in &self.constraints { - constraint.solve(params, positions); - } - } -} diff --git a/src/dynamics/solver/joint_constraint/generic_velocity_constraint.rs b/src/dynamics/solver/joint_constraint/generic_velocity_constraint.rs deleted file mode 100644 index 1eb04a9..0000000 --- a/src/dynamics/solver/joint_constraint/generic_velocity_constraint.rs +++ /dev/null @@ -1,706 +0,0 @@ -use crate::dynamics::solver::DeltaVel; -use crate::dynamics::{ - GenericJoint, IntegrationParameters, JointGraphEdge, JointIndex, JointParams, RigidBody, -}; -use crate::math::{AngularInertia, Dim, Isometry, Real, Rotation, SpacialVector, Vector, DIM}; -use crate::na::UnitQuaternion; -use crate::parry::math::{AngDim, SpatialVector}; -use crate::utils::{WAngularInertia, WCross, WCrossMatrix}; -#[cfg(feature = "dim3")] -use na::{Matrix3, Matrix6, Vector3, Vector6, U3}; -#[cfg(feature = "dim2")] -use na::{Matrix3, Vector3}; - -#[derive(Debug)] -pub(crate) struct GenericVelocityConstraint { - mj_lambda1: usize, - mj_lambda2: usize, - - joint_id: JointIndex, - - inv_lhs_lin: Matrix3, - inv_lhs_ang: Matrix3, - - im1: Real, - im2: Real, - - ii1: AngularInertia, - ii2: AngularInertia, - - ii1_sqrt: AngularInertia, - ii2_sqrt: AngularInertia, - - r1: Vector, - r2: Vector, - basis1: Rotation, - basis2: Rotation, - dependant_set_mask: SpacialVector, - - vel: GenericConstraintPart, -} - -impl GenericVelocityConstraint { - pub fn compute_velocity_error( - min_velocity: &SpatialVector, - max_velocity: &SpatialVector, - r1: &Vector, - r2: &Vector, - basis1: &Rotation, - basis2: &Rotation, - rb1: &RigidBody, - rb2: &RigidBody, - ) -> SpatialVector { - let lin_dvel = basis1.inverse_transform_vector(&(-rb1.linvel() - rb1.angvel().gcross(*r1))) - + basis2.inverse_transform_vector(&(rb2.linvel() + rb2.angvel().gcross(*r2))); - let ang_dvel = basis1.inverse_transform_vector(&-rb1.angvel) - + basis2.inverse_transform_vector(&rb2.angvel); - - let min_linvel = min_velocity.xyz(); - let min_angvel = min_velocity.fixed_rows::(DIM).into_owned(); - let max_linvel = max_velocity.xyz(); - let max_angvel = max_velocity.fixed_rows::(DIM).into_owned(); - let lin_rhs = lin_dvel - lin_dvel.sup(&min_linvel).inf(&max_linvel); - let ang_rhs = ang_dvel - ang_dvel.sup(&min_angvel).inf(&max_angvel); - - #[cfg(feature = "dim2")] - return Vector3::new(lin_rhs.x, lin_rhs.y, ang_rhs); - - #[cfg(feature = "dim3")] - return Vector6::new( - lin_rhs.x, lin_rhs.y, lin_rhs.z, ang_rhs.x, ang_rhs.y, ang_rhs.z, - ); - } - - pub fn compute_lin_position_error( - anchor1: &Isometry, - anchor2: &Isometry, - basis: &Rotation, - min_position: &Vector, - max_position: &Vector, - ) -> Vector { - let dpos = anchor2.translation.vector - anchor1.translation.vector; - let local_dpos = basis.inverse_transform_vector(&dpos); - local_dpos - local_dpos.sup(min_position).inf(max_position) - } - - pub fn compute_ang_position_error( - anchor1: &Isometry, - anchor2: &Isometry, - basis: &Rotation, - min_position: &Vector, - max_position: &Vector, - ) -> Vector { - let drot = anchor2.rotation * anchor1.rotation.inverse(); - let local_drot_diff = basis.inverse_transform_vector(&drot.scaled_axis()); - - let error = local_drot_diff - local_drot_diff.sup(min_position).inf(max_position); - let error_code = - (error[0] == 0.0) as usize + (error[1] == 0.0) as usize + (error[2] == 0.0) as usize; - - if error_code == 1 { - // Align two axes. - let constrained_axis = error.iamin(); - let axis1 = anchor1 - .rotation - .to_rotation_matrix() - .into_inner() - .column(constrained_axis) - .into_owned(); - let axis2 = anchor2 - .rotation - .to_rotation_matrix() - .into_inner() - .column(constrained_axis) - .into_owned(); - let rot_cross = UnitQuaternion::rotation_between(&axis1, &axis2) - .unwrap_or(UnitQuaternion::identity()); - let local_drot_diff = basis.inverse_transform_vector(&rot_cross.scaled_axis()); - local_drot_diff - local_drot_diff.sup(min_position).inf(max_position) - } else { - error - } - } - - pub fn invert_partial_delassus_matrix( - min_impulse: &Vector, - max_impulse: &Vector, - dependant_set_mask: &mut Vector, - mut delassus: na::Matrix3, - ) -> na::Matrix3 { - // Adjust the Delassus matrix to take force limits into account. - // If a DoF has a force limit, then we need to make its - // constraint independent from the others because otherwise - // the force clamping will cause errors to propagate in the - // other constraints. - for i in 0..3 { - if min_impulse[i] > -Real::MAX || max_impulse[i] < Real::MAX { - let diag = delassus[(i, i)]; - delassus.column_mut(i).fill(0.0); - delassus.row_mut(i).fill(0.0); - delassus[(i, i)] = diag; - dependant_set_mask[i] = 0.0; - } else { - dependant_set_mask[i] = 1.0; - } - } - - delassus.try_inverse().unwrap() - } - - pub fn compute_position_error( - joint: &GenericJoint, - anchor1: &Isometry, - anchor2: &Isometry, - basis: &Rotation, - ) -> SpatialVector { - let delta_pos = Isometry::from_parts( - anchor2.translation * anchor1.translation.inverse(), - anchor2.rotation * anchor1.rotation.inverse(), - ); - let lin_dpos = basis.inverse_transform_vector(&delta_pos.translation.vector); - let ang_dpos = basis.inverse_transform_vector(&delta_pos.rotation.scaled_axis()); - - let dpos = Vector6::new( - lin_dpos.x, lin_dpos.y, lin_dpos.z, ang_dpos.x, ang_dpos.y, ang_dpos.z, - ); - - let error = dpos - dpos.sup(&joint.min_position).inf(&joint.max_position); - let error_code = - (error[3] == 0.0) as usize + (error[4] == 0.0) as usize + (error[5] == 0.0) as usize; - - match error_code { - 1 => { - let constrained_axis = error.rows(3, 3).iamin(); - let axis1 = anchor1 - .rotation - .to_rotation_matrix() - .into_inner() - .column(constrained_axis) - .into_owned(); - let axis2 = anchor2 - .rotation - .to_rotation_matrix() - .into_inner() - .column(constrained_axis) - .into_owned(); - let rot_cross = UnitQuaternion::rotation_between(&axis1, &axis2) - .unwrap_or(UnitQuaternion::identity()); - let ang_dpos = basis.inverse_transform_vector(&rot_cross.scaled_axis()); - let dpos = Vector6::new( - lin_dpos.x, lin_dpos.y, lin_dpos.z, ang_dpos.x, ang_dpos.y, ang_dpos.z, - ); - - dpos - dpos.sup(&joint.min_position).inf(&joint.max_position) - } - _ => error, - } - } - - pub fn from_params( - params: &IntegrationParameters, - joint_id: JointIndex, - rb1: &RigidBody, - rb2: &RigidBody, - joint: &GenericJoint, - ) -> Self { - let anchor1 = rb1.position() * joint.local_anchor1; - let anchor2 = rb2.position() * joint.local_anchor2; - let basis1 = anchor1.rotation; - let basis2 = anchor2.rotation; - let im1 = rb1.effective_inv_mass; - let im2 = rb2.effective_inv_mass; - let ii1 = rb1.effective_world_inv_inertia_sqrt.squared(); - let ii2 = rb2.effective_world_inv_inertia_sqrt.squared(); - let r1 = anchor1.translation.vector - rb1.world_com.coords; - let r2 = anchor2.translation.vector - rb2.world_com.coords; - let mut min_impulse = joint.min_impulse; - let mut max_impulse = joint.max_impulse; - let mut min_pos_impulse = joint.min_pos_impulse; - let mut max_pos_impulse = joint.max_pos_impulse; - let mut min_velocity = joint.min_velocity; - let mut max_velocity = joint.max_velocity; - let mut dependant_set_mask = SpacialVector::repeat(1.0); - - let pos_rhs = Self::compute_position_error(joint, &anchor1, &anchor2, &basis1) - * params.inv_dt() - * params.joint_erp; - - for i in 0..6 { - if pos_rhs[i] < 0.0 { - min_impulse[i] = -Real::MAX; - min_pos_impulse[i] = -Real::MAX; - min_velocity[i] = 0.0; - } - if pos_rhs[i] > 0.0 { - max_impulse[i] = Real::MAX; - max_pos_impulse[i] = Real::MAX; - max_velocity[i] = 0.0; - } - } - - let rhs = Self::compute_velocity_error( - &min_velocity, - &max_velocity, - &r1, - &r2, - &basis1, - &basis2, - rb1, - rb2, - ); - let rhs_lin = rhs.xyz(); - let rhs_ang = rhs.fixed_rows::(DIM).into(); - - // TODO: we should keep the SdpMatrix3 type. - let rotmat1 = basis1.to_rotation_matrix().into_inner(); - let rotmat2 = basis2.to_rotation_matrix().into_inner(); - let rmat1 = r1.gcross_matrix() * rotmat1; - let rmat2 = r2.gcross_matrix() * rotmat2; - let delassus00 = (ii1.quadform(&rmat1).add_diagonal(im1) - + ii2.quadform(&rmat2).add_diagonal(im2)) - .into_matrix(); - let delassus11 = (ii1.quadform(&rotmat1) + ii2.quadform(&rotmat2)).into_matrix(); - - let inv_lhs_lin = GenericVelocityConstraint::invert_partial_delassus_matrix( - &min_pos_impulse.xyz(), - &max_pos_impulse.xyz(), - &mut Vector3::zeros(), - delassus00, - ); - let inv_lhs_ang = GenericVelocityConstraint::invert_partial_delassus_matrix( - &min_pos_impulse.fixed_rows::(DIM).into_owned(), - &max_pos_impulse.fixed_rows::(DIM).into_owned(), - &mut Vector3::zeros(), - delassus11, - ); - - let impulse = (joint.impulse * params.warmstart_coeff) - .inf(&max_impulse) - .sup(&min_impulse); - - let lin_impulse = impulse.xyz(); - let ang_impulse = impulse.fixed_rows::(DIM).into_owned(); - let min_lin_impulse = min_impulse.xyz(); - let min_ang_impulse = min_impulse.fixed_rows::(DIM).into_owned(); - let max_lin_impulse = max_impulse.xyz(); - let max_ang_impulse = max_impulse.fixed_rows::(DIM).into_owned(); - - GenericVelocityConstraint { - joint_id, - mj_lambda1: rb1.active_set_offset, - mj_lambda2: rb2.active_set_offset, - im1, - im2, - ii1, - ii2, - ii1_sqrt: rb1.effective_world_inv_inertia_sqrt, - ii2_sqrt: rb2.effective_world_inv_inertia_sqrt, - inv_lhs_lin, - inv_lhs_ang, - r1, - r2, - basis1, - basis2, - dependant_set_mask, - vel: GenericConstraintPart { - lin_impulse, - ang_impulse, - min_lin_impulse, - min_ang_impulse, - max_lin_impulse, - max_ang_impulse, - rhs_lin, - rhs_ang, - }, - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = mj_lambdas[self.mj_lambda1 as usize]; - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - let lin_impulse1 = self.basis1 * self.vel.lin_impulse; - let ang_impulse1 = self.basis1 * self.vel.ang_impulse; - let lin_impulse2 = self.basis2 * self.vel.lin_impulse; - let ang_impulse2 = self.basis2 * self.vel.ang_impulse; - - mj_lambda1.linear += self.im1 * lin_impulse1; - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(ang_impulse1 + self.r1.gcross(lin_impulse1)); - - mj_lambda2.linear -= self.im2 * lin_impulse2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse2 + self.r2.gcross(lin_impulse2)); - - mj_lambdas[self.mj_lambda1 as usize] = mj_lambda1; - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = mj_lambdas[self.mj_lambda1 as usize]; - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - let (lin_imp, ang_imp) = self.vel.solve(self, &mut mj_lambda1, &mut mj_lambda2); - self.vel.lin_impulse = lin_imp; - self.vel.ang_impulse = ang_imp; - - mj_lambdas[self.mj_lambda1 as usize] = mj_lambda1; - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - let joint = &mut joints_all[self.joint_id].weight; - match &mut joint.params { - JointParams::GenericJoint(out) => { - out.impulse[0] = self.vel.lin_impulse.x; - out.impulse[1] = self.vel.lin_impulse.y; - out.impulse[2] = self.vel.lin_impulse.z; - out.impulse[3] = self.vel.ang_impulse.x; - out.impulse[4] = self.vel.ang_impulse.y; - out.impulse[5] = self.vel.ang_impulse.z; - } - JointParams::RevoluteJoint(out) => { - out.impulse[0] = self.vel.lin_impulse.x; - out.impulse[1] = self.vel.lin_impulse.y; - out.impulse[2] = self.vel.lin_impulse.z; - out.motor_impulse = self.vel.ang_impulse.x; - out.impulse[3] = self.vel.ang_impulse.y; - out.impulse[4] = self.vel.ang_impulse.z; - } - _ => unimplemented!(), - } - } -} - -#[derive(Debug)] -pub(crate) struct GenericVelocityGroundConstraint { - mj_lambda2: usize, - - joint_id: JointIndex, - - inv_lhs_lin: Matrix3, - inv_lhs_ang: Matrix3, - - im2: Real, - ii2: AngularInertia, - ii2_sqrt: AngularInertia, - r2: Vector, - basis: Rotation, - - dependant_set_mask: SpacialVector, - - vel: GenericConstraintPart, -} - -impl GenericVelocityGroundConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: JointIndex, - rb1: &RigidBody, - rb2: &RigidBody, - joint: &GenericJoint, - flipped: bool, - ) -> Self { - let (anchor1, anchor2) = if flipped { - ( - rb1.position() * joint.local_anchor2, - rb2.position() * joint.local_anchor1, - ) - } else { - ( - rb1.position() * joint.local_anchor1, - rb2.position() * joint.local_anchor2, - ) - }; - - let basis = anchor2.rotation; - let im2 = rb2.effective_inv_mass; - let ii2 = rb2.effective_world_inv_inertia_sqrt.squared(); - let r1 = anchor1.translation.vector - rb1.world_com.coords; - let r2 = anchor2.translation.vector - rb2.world_com.coords; - let mut min_impulse = joint.min_impulse; - let mut max_impulse = joint.max_impulse; - let mut min_pos_impulse = joint.min_pos_impulse; - let mut max_pos_impulse = joint.max_pos_impulse; - let mut min_velocity = joint.min_velocity; - let mut max_velocity = joint.max_velocity; - let mut dependant_set_mask = SpacialVector::repeat(1.0); - - let pos_rhs = - GenericVelocityConstraint::compute_position_error(joint, &anchor1, &anchor2, &basis) - * params.inv_dt() - * params.joint_erp; - - for i in 0..6 { - if pos_rhs[i] < 0.0 { - min_impulse[i] = -Real::MAX; - min_pos_impulse[i] = -Real::MAX; - min_velocity[i] = 0.0; - } - if pos_rhs[i] > 0.0 { - max_impulse[i] = Real::MAX; - max_pos_impulse[i] = Real::MAX; - max_velocity[i] = 0.0; - } - } - - let rhs = GenericVelocityConstraint::compute_velocity_error( - &min_velocity, - &max_velocity, - &r1, - &r2, - &basis, - &basis, - rb1, - rb2, - ); - let rhs_lin = rhs.xyz(); - let rhs_ang = rhs.fixed_rows::(DIM).into_owned(); - - // TODO: we should keep the SdpMatrix3 type. - let rotmat = basis.to_rotation_matrix().into_inner(); - let rmat2 = r2.gcross_matrix() * rotmat; - let delassus00 = ii2.quadform(&rmat2).add_diagonal(im2).into_matrix(); - let delassus11 = ii2.quadform(&rotmat).into_matrix(); - - let inv_lhs_lin = GenericVelocityConstraint::invert_partial_delassus_matrix( - &min_pos_impulse.xyz(), - &max_pos_impulse.xyz(), - &mut Vector3::zeros(), - delassus00, - ); - let inv_lhs_ang = GenericVelocityConstraint::invert_partial_delassus_matrix( - &min_pos_impulse.fixed_rows::(DIM).into_owned(), - &max_pos_impulse.fixed_rows::(DIM).into_owned(), - &mut Vector3::zeros(), - delassus11, - ); - - let impulse = (joint.impulse * params.warmstart_coeff) - .inf(&max_impulse) - .sup(&min_impulse); - - let lin_impulse = impulse.xyz(); - let ang_impulse = impulse.fixed_rows::(DIM).into_owned(); - let min_lin_impulse = min_impulse.xyz(); - let min_ang_impulse = min_impulse.fixed_rows::(DIM).into_owned(); - let max_lin_impulse = max_impulse.xyz(); - let max_ang_impulse = max_impulse.fixed_rows::(DIM).into_owned(); - - GenericVelocityGroundConstraint { - joint_id, - mj_lambda2: rb2.active_set_offset, - im2, - ii2, - ii2_sqrt: rb2.effective_world_inv_inertia_sqrt, - inv_lhs_lin, - inv_lhs_ang, - r2, - basis, - vel: GenericConstraintPart { - lin_impulse, - ang_impulse, - min_lin_impulse, - min_ang_impulse, - max_lin_impulse, - max_ang_impulse, - rhs_lin, - rhs_ang, - }, - dependant_set_mask, - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - let lin_impulse = self.basis * self.vel.lin_impulse; - #[cfg(feature = "dim2")] - let ang_impulse = self.basis * self.vel.impulse[2]; - #[cfg(feature = "dim3")] - let ang_impulse = self.basis * self.vel.ang_impulse; - - mj_lambda2.linear -= self.im2 * lin_impulse; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - let (lin_imp, ang_imp) = self.vel.solve_ground(self, &mut mj_lambda2); - self.vel.lin_impulse = lin_imp; - self.vel.ang_impulse = ang_imp; - - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - // TODO: duplicated code with the non-ground constraint. - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - let joint = &mut joints_all[self.joint_id].weight; - match &mut joint.params { - JointParams::GenericJoint(out) => { - out.impulse[0] = self.vel.lin_impulse.x; - out.impulse[1] = self.vel.lin_impulse.y; - out.impulse[2] = self.vel.lin_impulse.z; - out.impulse[3] = self.vel.ang_impulse.x; - out.impulse[4] = self.vel.ang_impulse.y; - out.impulse[5] = self.vel.ang_impulse.z; - } - JointParams::RevoluteJoint(out) => { - out.impulse[0] = self.vel.lin_impulse.x; - out.impulse[1] = self.vel.lin_impulse.y; - out.impulse[2] = self.vel.lin_impulse.z; - out.motor_impulse = self.vel.ang_impulse.x; - out.impulse[3] = self.vel.ang_impulse.y; - out.impulse[4] = self.vel.ang_impulse.z; - } - _ => unimplemented!(), - } - } -} - -#[derive(Debug)] -struct GenericConstraintPart { - lin_impulse: Vector3, - max_lin_impulse: Vector3, - min_lin_impulse: Vector3, - rhs_lin: Vector3, - - ang_impulse: Vector3, - max_ang_impulse: Vector3, - min_ang_impulse: Vector3, - rhs_ang: Vector3, -} - -impl GenericConstraintPart { - fn solve( - &self, - parent: &GenericVelocityConstraint, - mj_lambda1: &mut DeltaVel, - mj_lambda2: &mut DeltaVel, - ) -> (Vector3, Vector3) { - let new_lin_impulse; - let new_ang_impulse; - - /* - * - * Solve translations. - * - */ - { - let ang_vel1 = parent.ii1_sqrt.transform_vector(mj_lambda1.angular); - let ang_vel2 = parent.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let dvel = parent - .basis1 - .inverse_transform_vector(&(-mj_lambda1.linear - ang_vel1.gcross(parent.r1))) - + parent - .basis2 - .inverse_transform_vector(&(mj_lambda2.linear + ang_vel2.gcross(parent.r2))); - - let err = dvel + self.rhs_lin; - - new_lin_impulse = (self.lin_impulse + parent.inv_lhs_lin * err) - .sup(&self.min_lin_impulse) - .inf(&self.max_lin_impulse); - let effective_impulse1 = parent.basis1 * (new_lin_impulse - self.lin_impulse); - let effective_impulse2 = parent.basis2 * (new_lin_impulse - self.lin_impulse); - - mj_lambda1.linear += parent.im1 * effective_impulse1; - mj_lambda1.angular += parent - .ii1_sqrt - .transform_vector(parent.r1.gcross(effective_impulse1)); - - mj_lambda2.linear -= parent.im2 * effective_impulse2; - mj_lambda2.angular -= parent - .ii2_sqrt - .transform_vector(parent.r2.gcross(effective_impulse2)); - } - - /* - * - * Solve rotations. - * - */ - { - let ang_vel1 = parent.ii1_sqrt.transform_vector(mj_lambda1.angular); - let ang_vel2 = parent.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let dvel = parent.basis2.inverse_transform_vector(&ang_vel2) - - parent.basis1.inverse_transform_vector(&ang_vel1); - let err = dvel + self.rhs_ang; - - new_ang_impulse = (self.ang_impulse + parent.inv_lhs_ang * err) - .sup(&self.min_ang_impulse) - .inf(&self.max_ang_impulse); - let effective_impulse1 = parent.basis1 * (new_ang_impulse - self.ang_impulse); - let effective_impulse2 = parent.basis2 * (new_ang_impulse - self.ang_impulse); - - mj_lambda1.angular += parent.ii1_sqrt.transform_vector(effective_impulse1); - mj_lambda2.angular -= parent.ii2_sqrt.transform_vector(effective_impulse2); - } - - (new_lin_impulse, new_ang_impulse) - } - - fn solve_ground( - &self, - parent: &GenericVelocityGroundConstraint, - mj_lambda2: &mut DeltaVel, - ) -> (Vector3, Vector3) { - let new_lin_impulse; - let new_ang_impulse; - - /* - * - * Solve translations. - * - */ - { - let ang_vel2 = parent.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let dvel = parent - .basis - .inverse_transform_vector(&(mj_lambda2.linear + ang_vel2.gcross(parent.r2))); - - let err = dvel + self.rhs_lin; - - new_lin_impulse = (self.lin_impulse + parent.inv_lhs_lin * err) - .sup(&self.min_lin_impulse) - .inf(&self.max_lin_impulse); - let effective_impulse = parent.basis * (new_lin_impulse - self.lin_impulse); - - mj_lambda2.linear -= parent.im2 * effective_impulse; - mj_lambda2.angular -= parent - .ii2_sqrt - .transform_vector(parent.r2.gcross(effective_impulse)); - } - - /* - * - * Solve rotations. - * - */ - { - let ang_vel2 = parent.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let dvel = parent.basis.inverse_transform_vector(&ang_vel2); - let err = dvel + self.rhs_ang; - - new_ang_impulse = (self.ang_impulse + parent.inv_lhs_ang * err) - .sup(&self.min_ang_impulse) - .inf(&self.max_ang_impulse); - let effective_impulse = parent.basis * (new_ang_impulse - self.ang_impulse); - - mj_lambda2.angular -= parent.ii2_sqrt.transform_vector(effective_impulse); - } - - (new_lin_impulse, new_ang_impulse) - } -} diff --git a/src/dynamics/solver/joint_constraint/generic_velocity_constraint_wide.rs b/src/dynamics/solver/joint_constraint/generic_velocity_constraint_wide.rs deleted file mode 100644 index 2751373..0000000 --- a/src/dynamics/solver/joint_constraint/generic_velocity_constraint_wide.rs +++ /dev/null @@ -1,464 +0,0 @@ -use simba::simd::SimdValue; - -use crate::dynamics::solver::DeltaVel; -use crate::dynamics::{ - GenericJoint, IntegrationParameters, JointGraphEdge, JointIndex, JointParams, RigidBody, -}; -use crate::math::{ - AngVector, AngularInertia, CrossMatrix, Dim, Isometry, Point, Real, SimdReal, SpacialVector, - Vector, SIMD_WIDTH, -}; -use crate::utils::{WAngularInertia, WCross, WCrossMatrix}; -#[cfg(feature = "dim3")] -use na::{Cholesky, Matrix6, Vector6, U3}; -#[cfg(feature = "dim2")] -use { - na::{Matrix3, Vector3}, - parry::utils::SdpMatrix3, -}; - -#[derive(Debug)] -pub(crate) struct WGenericVelocityConstraint { - mj_lambda1: [usize; SIMD_WIDTH], - mj_lambda2: [usize; SIMD_WIDTH], - - joint_id: [JointIndex; SIMD_WIDTH], - - impulse: SpacialVector, - - #[cfg(feature = "dim3")] - inv_lhs: Matrix6, // FIXME: replace by Cholesky. - #[cfg(feature = "dim3")] - rhs: Vector6, - - #[cfg(feature = "dim2")] - inv_lhs: Matrix3, - #[cfg(feature = "dim2")] - rhs: Vector3, - - im1: SimdReal, - im2: SimdReal, - - ii1: AngularInertia, - ii2: AngularInertia, - - ii1_sqrt: AngularInertia, - ii2_sqrt: AngularInertia, - - r1: Vector, - r2: Vector, -} - -impl WGenericVelocityConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: [JointIndex; SIMD_WIDTH], - rbs1: [&RigidBody; SIMD_WIDTH], - rbs2: [&RigidBody; SIMD_WIDTH], - cparams: [&GenericJoint; SIMD_WIDTH], - ) -> Self { - let position1 = Isometry::from(gather![|ii| rbs1[ii].position]); - let linvel1 = Vector::from(gather![|ii| *rbs1[ii].linvel()]); - let angvel1 = AngVector::::from(gather![|ii| *rbs1[ii].angvel()]); - let world_com1 = Point::from(gather![|ii| rbs1[ii].world_com]); - let im1 = SimdReal::from(gather![|ii| rbs1[ii].effective_inv_mass]); - let ii1_sqrt = AngularInertia::::from(gather![ - |ii| rbs1[ii].effective_world_inv_inertia_sqrt - ]); - let mj_lambda1 = gather![|ii| rbs1[ii].active_set_offset]; - - let position2 = Isometry::from(gather![|ii| rbs2[ii].position]); - let linvel2 = Vector::from(gather![|ii| *rbs2[ii].linvel()]); - let angvel2 = AngVector::::from(gather![|ii| *rbs2[ii].angvel()]); - let world_com2 = Point::from(gather![|ii| rbs2[ii].world_com]); - let im2 = SimdReal::from(gather![|ii| rbs2[ii].effective_inv_mass]); - let ii2_sqrt = AngularInertia::::from(gather![ - |ii| rbs2[ii].effective_world_inv_inertia_sqrt - ]); - let mj_lambda2 = gather![|ii| rbs2[ii].active_set_offset]; - - let local_anchor1 = Isometry::from(gather![|ii| cparams[ii].local_anchor1]); - let local_anchor2 = Isometry::from(gather![|ii| cparams[ii].local_anchor2]); - let impulse = SpacialVector::from(gather![|ii| cparams[ii].impulse]); - - let anchor1 = position1 * local_anchor1; - let anchor2 = position2 * local_anchor2; - let ii1 = ii1_sqrt.squared(); - let ii2 = ii2_sqrt.squared(); - let r1 = anchor1.translation.vector - world_com1.coords; - let r2 = anchor2.translation.vector - world_com2.coords; - let rmat1: CrossMatrix<_> = r1.gcross_matrix(); - let rmat2: CrossMatrix<_> = r2.gcross_matrix(); - - #[allow(unused_mut)] // For 2D. - let mut lhs; - - #[cfg(feature = "dim3")] - { - let lhs00 = - ii1.quadform(&rmat1).add_diagonal(im1) + ii2.quadform(&rmat2).add_diagonal(im2); - let lhs10 = ii1.transform_matrix(&rmat1) + ii2.transform_matrix(&rmat2); - let lhs11 = (ii1 + ii2).into_matrix(); - - // Note that Cholesky only reads the lower-triangular part of the matrix - // so we don't need to fill lhs01. - lhs = Matrix6::zeros(); - lhs.fixed_slice_mut::(0, 0) - .copy_from(&lhs00.into_matrix()); - lhs.fixed_slice_mut::(3, 0).copy_from(&lhs10); - lhs.fixed_slice_mut::(3, 3).copy_from(&lhs11); - } - - // In 2D we just unroll the computation because - // it's just easier that way. - #[cfg(feature = "dim2")] - { - let m11 = im1 + im2 + rmat1.x * rmat1.x * ii1 + rmat2.x * rmat2.x * ii2; - let m12 = rmat1.x * rmat1.y * ii1 + rmat2.x * rmat2.y * ii2; - let m22 = im1 + im2 + rmat1.y * rmat1.y * ii1 + rmat2.y * rmat2.y * ii2; - let m13 = rmat1.x * ii1 + rmat2.x * ii2; - let m23 = rmat1.y * ii1 + rmat2.y * ii2; - let m33 = ii1 + ii2; - lhs = SdpMatrix3::new(m11, m12, m13, m22, m23, m33) - } - - // NOTE: we don't use cholesky in 2D because we only have a 3x3 matrix - // for which a textbook inverse is still efficient. - #[cfg(feature = "dim2")] - let inv_lhs = lhs.inverse_unchecked().into_matrix(); // FIXME: don't extract the matrix? - #[cfg(feature = "dim3")] - let inv_lhs = Cholesky::new_unchecked(lhs).inverse(); - - let lin_dvel = -linvel1 - angvel1.gcross(r1) + linvel2 + angvel2.gcross(r2); - let ang_dvel = -angvel1 + angvel2; - - #[cfg(feature = "dim2")] - let rhs = Vector3::new(lin_dvel.x, lin_dvel.y, ang_dvel); - - #[cfg(feature = "dim3")] - let rhs = Vector6::new( - lin_dvel.x, lin_dvel.y, lin_dvel.z, ang_dvel.x, ang_dvel.y, ang_dvel.z, - ); - - WGenericVelocityConstraint { - joint_id, - mj_lambda1, - mj_lambda2, - im1, - im2, - ii1, - ii2, - ii1_sqrt, - ii2_sqrt, - impulse: impulse * SimdReal::splat(params.warmstart_coeff), - inv_lhs, - r1, - r2, - rhs, - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda1[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda1[ii] as usize].angular - ]), - }; - let mut mj_lambda2 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - let lin_impulse = self.impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = self.impulse[2]; - #[cfg(feature = "dim3")] - let ang_impulse = self.impulse.fixed_rows::(3).into_owned(); - - mj_lambda1.linear += lin_impulse * self.im1; - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(ang_impulse + self.r1.gcross(lin_impulse)); - - mj_lambda2.linear -= lin_impulse * self.im2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda1[ii] as usize].linear = mj_lambda1.linear.extract(ii); - mj_lambdas[self.mj_lambda1[ii] as usize].angular = mj_lambda1.angular.extract(ii); - } - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1: DeltaVel = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda1[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda1[ii] as usize].angular - ]), - }; - let mut mj_lambda2: DeltaVel = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - let ang_vel1 = self.ii1_sqrt.transform_vector(mj_lambda1.angular); - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let dlinvel = -mj_lambda1.linear - ang_vel1.gcross(self.r1) - + mj_lambda2.linear - + ang_vel2.gcross(self.r2); - let dangvel = -ang_vel1 + ang_vel2; - - #[cfg(feature = "dim2")] - let rhs = Vector3::new(dlinvel.x, dlinvel.y, dangvel) + self.rhs; - #[cfg(feature = "dim3")] - let rhs = Vector6::new( - dlinvel.x, dlinvel.y, dlinvel.z, dangvel.x, dangvel.y, dangvel.z, - ) + self.rhs; - - let impulse = self.inv_lhs * rhs; - self.impulse += impulse; - let lin_impulse = impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = impulse[2]; - #[cfg(feature = "dim3")] - let ang_impulse = impulse.fixed_rows::(3).into_owned(); - - mj_lambda1.linear += lin_impulse * self.im1; - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(ang_impulse + self.r1.gcross(lin_impulse)); - - mj_lambda2.linear -= lin_impulse * self.im2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda1[ii] as usize].linear = mj_lambda1.linear.extract(ii); - mj_lambdas[self.mj_lambda1[ii] as usize].angular = mj_lambda1.angular.extract(ii); - } - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - for ii in 0..SIMD_WIDTH { - let joint = &mut joints_all[self.joint_id[ii]].weight; - if let JointParams::GenericJoint(fixed) = &mut joint.params { - fixed.impulse = self.impulse.extract(ii) - } - } - } -} - -#[derive(Debug)] -pub(crate) struct WGenericVelocityGroundConstraint { - mj_lambda2: [usize; SIMD_WIDTH], - - joint_id: [JointIndex; SIMD_WIDTH], - - impulse: SpacialVector, - - #[cfg(feature = "dim3")] - inv_lhs: Matrix6, // FIXME: replace by Cholesky. - #[cfg(feature = "dim3")] - rhs: Vector6, - - #[cfg(feature = "dim2")] - inv_lhs: Matrix3, - #[cfg(feature = "dim2")] - rhs: Vector3, - - im2: SimdReal, - ii2: AngularInertia, - ii2_sqrt: AngularInertia, - r2: Vector, -} - -impl WGenericVelocityGroundConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: [JointIndex; SIMD_WIDTH], - rbs1: [&RigidBody; SIMD_WIDTH], - rbs2: [&RigidBody; SIMD_WIDTH], - cparams: [&GenericJoint; SIMD_WIDTH], - flipped: [bool; SIMD_WIDTH], - ) -> Self { - let position1 = Isometry::from(gather![|ii| rbs1[ii].position]); - let linvel1 = Vector::from(gather![|ii| *rbs1[ii].linvel()]); - let angvel1 = AngVector::::from(gather![|ii| *rbs1[ii].angvel()]); - let world_com1 = Point::from(gather![|ii| rbs1[ii].world_com]); - - let position2 = Isometry::from(gather![|ii| rbs2[ii].position]); - let linvel2 = Vector::from(gather![|ii| *rbs2[ii].linvel()]); - let angvel2 = AngVector::::from(gather![|ii| *rbs2[ii].angvel()]); - let world_com2 = Point::from(gather![|ii| rbs2[ii].world_com]); - let im2 = SimdReal::from(gather![|ii| rbs2[ii].effective_inv_mass]); - let ii2_sqrt = AngularInertia::::from(gather![ - |ii| rbs2[ii].effective_world_inv_inertia_sqrt - ]); - let mj_lambda2 = gather![|ii| rbs2[ii].active_set_offset]; - - let local_anchor1 = Isometry::from(gather![|ii| if flipped[ii] { - cparams[ii].local_anchor2 - } else { - cparams[ii].local_anchor1 - }]); - let local_anchor2 = Isometry::from(gather![|ii| if flipped[ii] { - cparams[ii].local_anchor1 - } else { - cparams[ii].local_anchor2 - }]); - let impulse = SpacialVector::from(gather![|ii| cparams[ii].impulse]); - - let anchor1 = position1 * local_anchor1; - let anchor2 = position2 * local_anchor2; - let ii2 = ii2_sqrt.squared(); - let r1 = anchor1.translation.vector - world_com1.coords; - let r2 = anchor2.translation.vector - world_com2.coords; - let rmat2: CrossMatrix<_> = r2.gcross_matrix(); - - #[allow(unused_mut)] // For 2D. - let mut lhs; - - #[cfg(feature = "dim3")] - { - let lhs00 = ii2.quadform(&rmat2).add_diagonal(im2); - let lhs10 = ii2.transform_matrix(&rmat2); - let lhs11 = ii2.into_matrix(); - - lhs = Matrix6::zeros(); - lhs.fixed_slice_mut::(0, 0) - .copy_from(&lhs00.into_matrix()); - lhs.fixed_slice_mut::(3, 0).copy_from(&lhs10); - lhs.fixed_slice_mut::(3, 3).copy_from(&lhs11); - } - - // In 2D we just unroll the computation because - // it's just easier that way. - #[cfg(feature = "dim2")] - { - let m11 = im2 + rmat2.x * rmat2.x * ii2; - let m12 = rmat2.x * rmat2.y * ii2; - let m22 = im2 + rmat2.y * rmat2.y * ii2; - let m13 = rmat2.x * ii2; - let m23 = rmat2.y * ii2; - let m33 = ii2; - lhs = SdpMatrix3::new(m11, m12, m13, m22, m23, m33) - } - - #[cfg(feature = "dim2")] - let inv_lhs = lhs.inverse_unchecked().into_matrix(); // FIXME: don't do into_matrix? - #[cfg(feature = "dim3")] - let inv_lhs = Cholesky::new_unchecked(lhs).inverse(); - - let lin_dvel = linvel2 + angvel2.gcross(r2) - linvel1 - angvel1.gcross(r1); - let ang_dvel = angvel2 - angvel1; - - #[cfg(feature = "dim2")] - let rhs = Vector3::new(lin_dvel.x, lin_dvel.y, ang_dvel); - #[cfg(feature = "dim3")] - let rhs = Vector6::new( - lin_dvel.x, lin_dvel.y, lin_dvel.z, ang_dvel.x, ang_dvel.y, ang_dvel.z, - ); - - WGenericVelocityGroundConstraint { - joint_id, - mj_lambda2, - im2, - ii2, - ii2_sqrt, - impulse: impulse * SimdReal::splat(params.warmstart_coeff), - inv_lhs, - r2, - rhs, - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - let lin_impulse = self.impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = self.impulse[2]; - #[cfg(feature = "dim3")] - let ang_impulse = self.impulse.fixed_rows::(3).into_owned(); - - mj_lambda2.linear -= lin_impulse * self.im2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2: DeltaVel = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - let dlinvel = mj_lambda2.linear + ang_vel2.gcross(self.r2); - let dangvel = ang_vel2; - #[cfg(feature = "dim2")] - let rhs = Vector3::new(dlinvel.x, dlinvel.y, dangvel) + self.rhs; - #[cfg(feature = "dim3")] - let rhs = Vector6::new( - dlinvel.x, dlinvel.y, dlinvel.z, dangvel.x, dangvel.y, dangvel.z, - ) + self.rhs; - - let impulse = self.inv_lhs * rhs; - - self.impulse += impulse; - let lin_impulse = impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = impulse[2]; - #[cfg(feature = "dim3")] - let ang_impulse = impulse.fixed_rows::(3).into_owned(); - - mj_lambda2.linear -= lin_impulse * self.im2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - // FIXME: duplicated code with the non-ground constraint. - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - for ii in 0..SIMD_WIDTH { - let joint = &mut joints_all[self.joint_id[ii]].weight; - if let JointParams::GenericJoint(fixed) = &mut joint.params { - fixed.impulse = self.impulse.extract(ii) - } - } - } -} diff --git a/src/dynamics/solver/joint_constraint/joint_constraint.rs b/src/dynamics/solver/joint_constraint/joint_constraint.rs index d723387..f42038c 100644 --- a/src/dynamics/solver/joint_constraint/joint_constraint.rs +++ b/src/dynamics/solver/joint_constraint/joint_constraint.rs @@ -1,118 +1,155 @@ -use super::{ - BallVelocityConstraint, BallVelocityGroundConstraint, FixedVelocityConstraint, - FixedVelocityGroundConstraint, PrismaticVelocityConstraint, PrismaticVelocityGroundConstraint, -}; -#[cfg(feature = "dim3")] -use super::{RevoluteVelocityConstraint, RevoluteVelocityGroundConstraint}; -#[cfg(feature = "simd-is-enabled")] -use super::{ - WBallVelocityConstraint, WBallVelocityGroundConstraint, WFixedVelocityConstraint, - WFixedVelocityGroundConstraint, WPrismaticVelocityConstraint, - WPrismaticVelocityGroundConstraint, -}; -#[cfg(feature = "dim3")] -#[cfg(feature = "simd-is-enabled")] -use super::{WRevoluteVelocityConstraint, WRevoluteVelocityGroundConstraint}; -// use crate::dynamics::solver::joint_constraint::generic_velocity_constraint::{ -// GenericVelocityConstraint, GenericVelocityGroundConstraint, -// }; use crate::data::{BundleSet, ComponentSet}; +use crate::dynamics::solver::joint_constraint::joint_generic_velocity_constraint::{ + JointGenericVelocityConstraint, JointGenericVelocityGroundConstraint, +}; +use crate::dynamics::solver::joint_constraint::joint_velocity_constraint::{ + JointVelocityConstraint, JointVelocityGroundConstraint, SolverBody, +}; use crate::dynamics::solver::DeltaVel; use crate::dynamics::{ - IntegrationParameters, Joint, JointGraphEdge, JointIndex, JointParams, RigidBodyIds, + ImpulseJoint, IntegrationParameters, JointGraphEdge, JointIndex, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition, RigidBodyType, RigidBodyVelocity, }; -use crate::math::Real; +use crate::math::{Isometry, Real, SPATIAL_DIM}; #[cfg(feature = "simd-is-enabled")] -use crate::math::SIMD_WIDTH; +use crate::math::{SimdReal, SIMD_WIDTH}; +use crate::prelude::MultibodyJointSet; +use na::DVector; -pub(crate) enum AnyJointVelocityConstraint { - BallConstraint(BallVelocityConstraint), - BallGroundConstraint(BallVelocityGroundConstraint), +pub enum AnyJointVelocityConstraint { + JointConstraint(JointVelocityConstraint), + JointGroundConstraint(JointVelocityGroundConstraint), + JointGenericConstraint(JointGenericVelocityConstraint), + JointGenericGroundConstraint(JointGenericVelocityGroundConstraint), #[cfg(feature = "simd-is-enabled")] - WBallConstraint(WBallVelocityConstraint), + JointConstraintSimd(JointVelocityConstraint), #[cfg(feature = "simd-is-enabled")] - WBallGroundConstraint(WBallVelocityGroundConstraint), - FixedConstraint(FixedVelocityConstraint), - FixedGroundConstraint(FixedVelocityGroundConstraint), - #[cfg(feature = "simd-is-enabled")] - WFixedConstraint(WFixedVelocityConstraint), - #[cfg(feature = "simd-is-enabled")] - WFixedGroundConstraint(WFixedVelocityGroundConstraint), - // GenericConstraint(GenericVelocityConstraint), - // GenericGroundConstraint(GenericVelocityGroundConstraint), - // #[cfg(feature = "simd-is-enabled")] - // WGenericConstraint(WGenericVelocityConstraint), - // #[cfg(feature = "simd-is-enabled")] - // WGenericGroundConstraint(WGenericVelocityGroundConstraint), - PrismaticConstraint(PrismaticVelocityConstraint), - PrismaticGroundConstraint(PrismaticVelocityGroundConstraint), - #[cfg(feature = "simd-is-enabled")] - WPrismaticConstraint(WPrismaticVelocityConstraint), - #[cfg(feature = "simd-is-enabled")] - WPrismaticGroundConstraint(WPrismaticVelocityGroundConstraint), - #[cfg(feature = "dim3")] - RevoluteConstraint(RevoluteVelocityConstraint), - #[cfg(feature = "dim3")] - RevoluteGroundConstraint(RevoluteVelocityGroundConstraint), - #[cfg(feature = "dim3")] - #[cfg(feature = "simd-is-enabled")] - WRevoluteConstraint(WRevoluteVelocityConstraint), - #[cfg(feature = "dim3")] - #[cfg(feature = "simd-is-enabled")] - WRevoluteGroundConstraint(WRevoluteVelocityGroundConstraint), - #[allow(dead_code)] // The Empty variant is only used with parallel code. + JointGroundConstraintSimd(JointVelocityGroundConstraint), Empty, } impl AnyJointVelocityConstraint { #[cfg(feature = "parallel")] - pub fn num_active_constraints(_: &Joint) -> usize { + pub fn num_active_constraints(_: &ImpulseJoint) -> usize { 1 } pub fn from_joint( params: &IntegrationParameters, joint_id: JointIndex, - joint: &Joint, + joint: &ImpulseJoint, bodies: &Bodies, - ) -> Self - where + multibodies: &MultibodyJointSet, + j_id: &mut usize, + jacobians: &mut DVector, + out: &mut Vec, + ) where Bodies: ComponentSet + ComponentSet + ComponentSet + ComponentSet, { - let rb1 = ( - bodies.index(joint.body1.0), - bodies.index(joint.body1.0), - bodies.index(joint.body1.0), - bodies.index(joint.body1.0), - ); - let rb2 = ( - bodies.index(joint.body2.0), - bodies.index(joint.body2.0), - bodies.index(joint.body2.0), - bodies.index(joint.body2.0), - ); + let local_frame1 = joint.data.local_frame1; + let local_frame2 = joint.data.local_frame2; + let rb1: ( + &RigidBodyPosition, + &RigidBodyVelocity, + &RigidBodyMassProps, + &RigidBodyIds, + ) = bodies.index_bundle(joint.body1.0); + let rb2: ( + &RigidBodyPosition, + &RigidBodyVelocity, + &RigidBodyMassProps, + &RigidBodyIds, + ) = bodies.index_bundle(joint.body2.0); - match &joint.params { - JointParams::BallJoint(p) => AnyJointVelocityConstraint::BallConstraint( - BallVelocityConstraint::from_params(params, joint_id, rb1, rb2, p), - ), - JointParams::FixedJoint(p) => AnyJointVelocityConstraint::FixedConstraint( - FixedVelocityConstraint::from_params(params, joint_id, rb1, rb2, p), - ), - JointParams::PrismaticJoint(p) => AnyJointVelocityConstraint::PrismaticConstraint( - PrismaticVelocityConstraint::from_params(params, joint_id, rb1, rb2, p), - ), - // JointParams::GenericJoint(p) => AnyJointVelocityConstraint::GenericConstraint( - // GenericVelocityConstraint::from_params(params, joint_id, rb1, rb2, p), - // ), - #[cfg(feature = "dim3")] - JointParams::RevoluteJoint(p) => AnyJointVelocityConstraint::RevoluteConstraint( - RevoluteVelocityConstraint::from_params(params, joint_id, rb1, rb2, p), - ), + let (rb_pos1, rb_vel1, rb_mprops1, rb_ids1) = rb1; + let (rb_pos2, rb_vel2, rb_mprops2, rb_ids2) = rb2; + let frame1 = rb_pos1.position * local_frame1; + let frame2 = rb_pos2.position * local_frame2; + + let body1 = SolverBody { + linvel: rb_vel1.linvel, + angvel: rb_vel1.angvel, + im: rb_mprops1.effective_inv_mass, + sqrt_ii: rb_mprops1.effective_world_inv_inertia_sqrt, + world_com: rb_mprops1.world_com, + mj_lambda: [rb_ids1.active_set_offset], + }; + let body2 = SolverBody { + linvel: rb_vel2.linvel, + angvel: rb_vel2.angvel, + im: rb_mprops2.effective_inv_mass, + sqrt_ii: rb_mprops2.effective_world_inv_inertia_sqrt, + world_com: rb_mprops2.world_com, + mj_lambda: [rb_ids2.active_set_offset], + }; + + let mb1 = multibodies + .rigid_body_link(joint.body1) + .map(|link| (&multibodies[link.multibody], link.id)); + let mb2 = multibodies + .rigid_body_link(joint.body2) + .map(|link| (&multibodies[link.multibody], link.id)); + + if mb1.is_some() || mb2.is_some() { + let multibodies_ndof = mb1.map(|m| m.0.ndofs()).unwrap_or(SPATIAL_DIM) + + mb2.map(|m| m.0.ndofs()).unwrap_or(SPATIAL_DIM); + + if multibodies_ndof == 0 { + // Both multibodies are fixed, don’t generate any constraint. + return; + } + + // For each solver contact we generate up to SPATIAL_DIM constraints, and each + // constraints appends the multibodies jacobian and weighted jacobians. + // Also note that for impulse_joints, the rigid-bodies will also add their jacobians + // to the generic DVector. + // TODO: is this count correct when we take both motors and limits into account? + let required_jacobian_len = *j_id + multibodies_ndof * 2 * SPATIAL_DIM; + + if jacobians.nrows() < required_jacobian_len { + jacobians.resize_vertically_mut(required_jacobian_len, 0.0); + } + + // TODO: find a way to avoid the temporary buffer. + let mut out_tmp = [JointGenericVelocityConstraint::invalid(); 12]; + let out_tmp_len = JointGenericVelocityConstraint::lock_axes( + params, + joint_id, + &body1, + &body2, + mb1, + mb2, + &frame1, + &frame2, + &joint.data, + jacobians, + j_id, + &mut out_tmp, + ); + + for c in out_tmp.into_iter().take(out_tmp_len) { + out.push(AnyJointVelocityConstraint::JointGenericConstraint(c)); + } + } else { + // TODO: find a way to avoid the temporary buffer. + let mut out_tmp = [JointVelocityConstraint::invalid(); 12]; + let out_tmp_len = JointVelocityConstraint::::lock_axes( + params, + joint_id, + &body1, + &body2, + &frame1, + &frame2, + &joint.data, + &mut out_tmp, + ); + + for c in out_tmp.into_iter().take(out_tmp_len) { + out.push(AnyJointVelocityConstraint::JointConstraint(c)); + } } } @@ -120,70 +157,96 @@ impl AnyJointVelocityConstraint { pub fn from_wide_joint( params: &IntegrationParameters, joint_id: [JointIndex; SIMD_WIDTH], - joints: [&Joint; SIMD_WIDTH], + impulse_joints: [&ImpulseJoint; SIMD_WIDTH], bodies: &Bodies, - ) -> Self - where + out: &mut Vec, + ) where Bodies: ComponentSet + ComponentSet + ComponentSet + ComponentSet, { - let rbs1 = ( - gather![|ii| bodies.index(joints[ii].body1.0)], - gather![|ii| bodies.index(joints[ii].body1.0)], - gather![|ii| bodies.index(joints[ii].body1.0)], - gather![|ii| bodies.index(joints[ii].body1.0)], + let rbs1: ( + [&RigidBodyPosition; SIMD_WIDTH], + [&RigidBodyVelocity; SIMD_WIDTH], + [&RigidBodyMassProps; SIMD_WIDTH], + [&RigidBodyIds; SIMD_WIDTH], + ) = ( + gather![|ii| bodies.index(impulse_joints[ii].body1.0)], + gather![|ii| bodies.index(impulse_joints[ii].body1.0)], + gather![|ii| bodies.index(impulse_joints[ii].body1.0)], + gather![|ii| bodies.index(impulse_joints[ii].body1.0)], ); - let rbs2 = ( - gather![|ii| bodies.index(joints[ii].body2.0)], - gather![|ii| bodies.index(joints[ii].body2.0)], - gather![|ii| bodies.index(joints[ii].body2.0)], - gather![|ii| bodies.index(joints[ii].body2.0)], + let rbs2: ( + [&RigidBodyPosition; SIMD_WIDTH], + [&RigidBodyVelocity; SIMD_WIDTH], + [&RigidBodyMassProps; SIMD_WIDTH], + [&RigidBodyIds; SIMD_WIDTH], + ) = ( + gather![|ii| bodies.index(impulse_joints[ii].body2.0)], + gather![|ii| bodies.index(impulse_joints[ii].body2.0)], + gather![|ii| bodies.index(impulse_joints[ii].body2.0)], + gather![|ii| bodies.index(impulse_joints[ii].body2.0)], ); - match &joints[0].params { - JointParams::BallJoint(_) => { - let joints = gather![|ii| joints[ii].params.as_ball_joint().unwrap()]; - AnyJointVelocityConstraint::WBallConstraint(WBallVelocityConstraint::from_params( - params, joint_id, rbs1, rbs2, joints, - )) - } - JointParams::FixedJoint(_) => { - let joints = gather![|ii| joints[ii].params.as_fixed_joint().unwrap()]; - AnyJointVelocityConstraint::WFixedConstraint(WFixedVelocityConstraint::from_params( - params, joint_id, rbs1, rbs2, joints, - )) - } - // JointParams::GenericJoint(_) => { - // let joints = gather![|ii| joints[ii].params.as_generic_joint().unwrap()]; - // AnyJointVelocityConstraint::WGenericConstraint( - // WGenericVelocityConstraint::from_params(params, joint_id, rbs1, rbs2, joints), - // ) - // } - JointParams::PrismaticJoint(_) => { - let joints = gather![|ii| joints[ii].params.as_prismatic_joint().unwrap()]; - AnyJointVelocityConstraint::WPrismaticConstraint( - WPrismaticVelocityConstraint::from_params(params, joint_id, rbs1, rbs2, joints), - ) - } - #[cfg(feature = "dim3")] - JointParams::RevoluteJoint(_) => { - let joints = gather![|ii| joints[ii].params.as_revolute_joint().unwrap()]; - AnyJointVelocityConstraint::WRevoluteConstraint( - WRevoluteVelocityConstraint::from_params(params, joint_id, rbs1, rbs2, joints), - ) - } + let (rb_pos1, rb_vel1, rb_mprops1, rb_ids1) = rbs1; + let (rb_pos2, rb_vel2, rb_mprops2, rb_ids2) = rbs2; + let pos1: Isometry = gather![|ii| rb_pos1[ii].position].into(); + let pos2: Isometry = gather![|ii| rb_pos2[ii].position].into(); + + let local_frame1: Isometry = + gather![|ii| impulse_joints[ii].data.local_frame1].into(); + let local_frame2: Isometry = + gather![|ii| impulse_joints[ii].data.local_frame2].into(); + + let frame1 = pos1 * local_frame1; + let frame2 = pos2 * local_frame2; + + let body1: SolverBody = SolverBody { + linvel: gather![|ii| rb_vel1[ii].linvel].into(), + angvel: gather![|ii| rb_vel1[ii].angvel].into(), + im: gather![|ii| rb_mprops1[ii].effective_inv_mass].into(), + sqrt_ii: gather![|ii| rb_mprops1[ii].effective_world_inv_inertia_sqrt].into(), + world_com: gather![|ii| rb_mprops1[ii].world_com].into(), + mj_lambda: gather![|ii| rb_ids1[ii].active_set_offset], + }; + let body2: SolverBody = SolverBody { + linvel: gather![|ii| rb_vel2[ii].linvel].into(), + angvel: gather![|ii| rb_vel2[ii].angvel].into(), + im: gather![|ii| rb_mprops2[ii].effective_inv_mass].into(), + sqrt_ii: gather![|ii| rb_mprops2[ii].effective_world_inv_inertia_sqrt].into(), + world_com: gather![|ii| rb_mprops2[ii].world_com].into(), + mj_lambda: gather![|ii| rb_ids2[ii].active_set_offset], + }; + + // TODO: find a way to avoid the temporary buffer. + let mut out_tmp = [JointVelocityConstraint::invalid(); 12]; + let out_tmp_len = JointVelocityConstraint::::lock_axes( + params, + joint_id, + &body1, + &body2, + &frame1, + &frame2, + impulse_joints[0].data.locked_axes.bits(), + &mut out_tmp, + ); + + for c in out_tmp.into_iter().take(out_tmp_len) { + out.push(AnyJointVelocityConstraint::JointConstraintSimd(c)); } } pub fn from_joint_ground( params: &IntegrationParameters, joint_id: JointIndex, - joint: &Joint, + joint: &ImpulseJoint, bodies: &Bodies, - ) -> Self - where + multibodies: &MultibodyJointSet, + j_id: &mut usize, + jacobians: &mut DVector, + out: &mut Vec, + ) where Bodies: ComponentSet + ComponentSet + ComponentSet @@ -195,36 +258,102 @@ impl AnyJointVelocityConstraint { let status2: &RigidBodyType = bodies.index(handle2.0); let flipped = !status2.is_dynamic(); - if flipped { + let (local_frame1, local_frame2) = if flipped { std::mem::swap(&mut handle1, &mut handle2); - } + (joint.data.local_frame2, joint.data.local_frame1) + } else { + (joint.data.local_frame1, joint.data.local_frame2) + }; - let rb1 = bodies.index_bundle(handle1.0); - let rb2 = bodies.index_bundle(handle2.0); + let rb1: (&RigidBodyPosition, &RigidBodyVelocity, &RigidBodyMassProps) = + bodies.index_bundle(handle1.0); + let rb2: ( + &RigidBodyPosition, + &RigidBodyVelocity, + &RigidBodyMassProps, + &RigidBodyIds, + ) = bodies.index_bundle(handle2.0); - match &joint.params { - JointParams::BallJoint(p) => AnyJointVelocityConstraint::BallGroundConstraint( - BallVelocityGroundConstraint::from_params(params, joint_id, rb1, rb2, p, flipped), - ), - JointParams::FixedJoint(p) => AnyJointVelocityConstraint::FixedGroundConstraint( - FixedVelocityGroundConstraint::from_params(params, joint_id, rb1, rb2, p, flipped), - ), - // JointParams::GenericJoint(p) => AnyJointVelocityConstraint::GenericGroundConstraint( - // GenericVelocityGroundConstraint::from_params( - // params, joint_id, rb1, rb2, p, flipped, - // ), - // ), - JointParams::PrismaticJoint(p) => { - AnyJointVelocityConstraint::PrismaticGroundConstraint( - PrismaticVelocityGroundConstraint::from_params( - params, joint_id, rb1, rb2, p, flipped, - ), - ) + let (rb_pos1, rb_vel1, rb_mprops1) = rb1; + let (rb_pos2, rb_vel2, rb_mprops2, rb_ids2) = rb2; + let frame1 = rb_pos1.position * local_frame1; + let frame2 = rb_pos2.position * local_frame2; + + let body1 = SolverBody { + linvel: rb_vel1.linvel, + angvel: rb_vel1.angvel, + im: rb_mprops1.effective_inv_mass, + sqrt_ii: rb_mprops1.effective_world_inv_inertia_sqrt, + world_com: rb_mprops1.world_com, + mj_lambda: [crate::INVALID_USIZE], + }; + let body2 = SolverBody { + linvel: rb_vel2.linvel, + angvel: rb_vel2.angvel, + im: rb_mprops2.effective_inv_mass, + sqrt_ii: rb_mprops2.effective_world_inv_inertia_sqrt, + world_com: rb_mprops2.world_com, + mj_lambda: [rb_ids2.active_set_offset], + }; + + if let Some(mb2) = multibodies + .rigid_body_link(handle2) + .map(|link| (&multibodies[link.multibody], link.id)) + { + let multibodies_ndof = mb2.0.ndofs(); + + if multibodies_ndof == 0 { + // The multibody is fixed, don’t generate any constraint. + return; + } + + // For each solver contact we generate up to SPATIAL_DIM constraints, and each + // constraints appends the multibodies jacobian and weighted jacobians. + // Also note that for impulse_joints, the rigid-bodies will also add their jacobians + // to the generic DVector. + // TODO: is this count correct when we take both motors and limits into account? + let required_jacobian_len = *j_id + multibodies_ndof * 2 * SPATIAL_DIM; + + if jacobians.nrows() < required_jacobian_len { + jacobians.resize_vertically_mut(required_jacobian_len, 0.0); + } + + // TODO: find a way to avoid the temporary buffer. + let mut out_tmp = [JointGenericVelocityGroundConstraint::invalid(); 12]; + let out_tmp_len = JointGenericVelocityGroundConstraint::lock_axes( + params, + joint_id, + &body1, + &body2, + mb2, + &frame1, + &frame2, + &joint.data, + jacobians, + j_id, + &mut out_tmp, + ); + + for c in out_tmp.into_iter().take(out_tmp_len) { + out.push(AnyJointVelocityConstraint::JointGenericGroundConstraint(c)); + } + } else { + // TODO: find a way to avoid the temporary buffer. + let mut out_tmp = [JointVelocityGroundConstraint::invalid(); 12]; + let out_tmp_len = JointVelocityGroundConstraint::::lock_axes( + params, + joint_id, + &body1, + &body2, + &frame1, + &frame2, + &joint.data, + &mut out_tmp, + ); + + for c in out_tmp.into_iter().take(out_tmp_len) { + out.push(AnyJointVelocityConstraint::JointGroundConstraint(c)); } - #[cfg(feature = "dim3")] - JointParams::RevoluteJoint(p) => RevoluteVelocityGroundConstraint::from_params( - params, joint_id, rb1, rb2, p, flipped, - ), } } @@ -232,18 +361,18 @@ impl AnyJointVelocityConstraint { pub fn from_wide_joint_ground( params: &IntegrationParameters, joint_id: [JointIndex; SIMD_WIDTH], - joints: [&Joint; SIMD_WIDTH], + impulse_joints: [&ImpulseJoint; SIMD_WIDTH], bodies: &Bodies, - ) -> Self - where + out: &mut Vec, + ) where Bodies: ComponentSet + ComponentSet + ComponentSet + ComponentSet + ComponentSet, { - let mut handles1 = gather![|ii| joints[ii].body1]; - let mut handles2 = gather![|ii| joints[ii].body2]; + let mut handles1 = gather![|ii| impulse_joints[ii].body1]; + let mut handles2 = gather![|ii| impulse_joints[ii].body2]; let status2: [&RigidBodyType; SIMD_WIDTH] = gather![|ii| bodies.index(handles2[ii].0)]; let mut flipped = [false; SIMD_WIDTH]; @@ -254,197 +383,136 @@ impl AnyJointVelocityConstraint { } } - let rbs1 = ( + let local_frame1: Isometry = gather![|ii| if flipped[ii] { + impulse_joints[ii].data.local_frame2 + } else { + impulse_joints[ii].data.local_frame1 + }] + .into(); + let local_frame2: Isometry = gather![|ii| if flipped[ii] { + impulse_joints[ii].data.local_frame1 + } else { + impulse_joints[ii].data.local_frame2 + }] + .into(); + + let rbs1: ( + [&RigidBodyPosition; SIMD_WIDTH], + [&RigidBodyVelocity; SIMD_WIDTH], + [&RigidBodyMassProps; SIMD_WIDTH], + ) = ( gather![|ii| bodies.index(handles1[ii].0)], gather![|ii| bodies.index(handles1[ii].0)], gather![|ii| bodies.index(handles1[ii].0)], ); - let rbs2 = ( + let rbs2: ( + [&RigidBodyPosition; SIMD_WIDTH], + [&RigidBodyVelocity; SIMD_WIDTH], + [&RigidBodyMassProps; SIMD_WIDTH], + [&RigidBodyIds; SIMD_WIDTH], + ) = ( gather![|ii| bodies.index(handles2[ii].0)], gather![|ii| bodies.index(handles2[ii].0)], gather![|ii| bodies.index(handles2[ii].0)], gather![|ii| bodies.index(handles2[ii].0)], ); - match &joints[0].params { - JointParams::BallJoint(_) => { - let joints = gather![|ii| joints[ii].params.as_ball_joint().unwrap()]; - AnyJointVelocityConstraint::WBallGroundConstraint( - WBallVelocityGroundConstraint::from_params( - params, joint_id, rbs1, rbs2, joints, flipped, - ), - ) - } - JointParams::FixedJoint(_) => { - let joints = gather![|ii| joints[ii].params.as_fixed_joint().unwrap()]; - AnyJointVelocityConstraint::WFixedGroundConstraint( - WFixedVelocityGroundConstraint::from_params( - params, joint_id, rbs1, rbs2, joints, flipped, - ), - ) - } - // JointParams::GenericJoint(_) => { - // let joints = gather![|ii| joints[ii].params.as_generic_joint().unwrap()]; - // AnyJointVelocityConstraint::WGenericGroundConstraint( - // WGenericVelocityGroundConstraint::from_params( - // params, joint_id, rbs1, rbs2, joints, flipped, - // ), - // ) - // } - JointParams::PrismaticJoint(_) => { - let joints = gather![|ii| joints[ii].params.as_prismatic_joint().unwrap()]; - AnyJointVelocityConstraint::WPrismaticGroundConstraint( - WPrismaticVelocityGroundConstraint::from_params( - params, joint_id, rbs1, rbs2, joints, flipped, - ), - ) - } - #[cfg(feature = "dim3")] - JointParams::RevoluteJoint(_) => { - let joints = gather![|ii| joints[ii].params.as_revolute_joint().unwrap()]; - AnyJointVelocityConstraint::WRevoluteGroundConstraint( - WRevoluteVelocityGroundConstraint::from_params( - params, joint_id, rbs1, rbs2, joints, flipped, - ), - ) - } + let (rb_pos1, rb_vel1, rb_mprops1) = rbs1; + let (rb_pos2, rb_vel2, rb_mprops2, rb_ids2) = rbs2; + let pos1: Isometry = gather![|ii| rb_pos1[ii].position].into(); + let pos2: Isometry = gather![|ii| rb_pos2[ii].position].into(); + + let frame1 = pos1 * local_frame1; + let frame2 = pos2 * local_frame2; + + let body1: SolverBody = SolverBody { + linvel: gather![|ii| rb_vel1[ii].linvel].into(), + angvel: gather![|ii| rb_vel1[ii].angvel].into(), + im: gather![|ii| rb_mprops1[ii].effective_inv_mass].into(), + sqrt_ii: gather![|ii| rb_mprops1[ii].effective_world_inv_inertia_sqrt].into(), + world_com: gather![|ii| rb_mprops1[ii].world_com].into(), + mj_lambda: [crate::INVALID_USIZE; SIMD_WIDTH], + }; + let body2: SolverBody = SolverBody { + linvel: gather![|ii| rb_vel2[ii].linvel].into(), + angvel: gather![|ii| rb_vel2[ii].angvel].into(), + im: gather![|ii| rb_mprops2[ii].effective_inv_mass].into(), + sqrt_ii: gather![|ii| rb_mprops2[ii].effective_world_inv_inertia_sqrt].into(), + world_com: gather![|ii| rb_mprops2[ii].world_com].into(), + mj_lambda: gather![|ii| rb_ids2[ii].active_set_offset], + }; + + // TODO: find a way to avoid the temporary buffer. + let mut out_tmp = [JointVelocityGroundConstraint::invalid(); 12]; + let out_tmp_len = JointVelocityGroundConstraint::::lock_axes( + params, + joint_id, + &body1, + &body2, + &frame1, + &frame2, + impulse_joints[0].data.locked_axes.bits(), + &mut out_tmp, + ); + + for c in out_tmp.into_iter().take(out_tmp_len) { + out.push(AnyJointVelocityConstraint::JointGroundConstraintSimd(c)); } } - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { + pub fn remove_bias_from_rhs(&mut self) { match self { - AnyJointVelocityConstraint::BallConstraint(c) => c.warmstart(mj_lambdas), - AnyJointVelocityConstraint::BallGroundConstraint(c) => c.warmstart(mj_lambdas), + AnyJointVelocityConstraint::JointConstraint(c) => c.remove_bias_from_rhs(), + AnyJointVelocityConstraint::JointGroundConstraint(c) => c.remove_bias_from_rhs(), #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WBallConstraint(c) => c.warmstart(mj_lambdas), + AnyJointVelocityConstraint::JointConstraintSimd(c) => c.remove_bias_from_rhs(), #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WBallGroundConstraint(c) => c.warmstart(mj_lambdas), - AnyJointVelocityConstraint::FixedConstraint(c) => c.warmstart(mj_lambdas), - AnyJointVelocityConstraint::FixedGroundConstraint(c) => c.warmstart(mj_lambdas), - #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WFixedConstraint(c) => c.warmstart(mj_lambdas), - #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WFixedGroundConstraint(c) => c.warmstart(mj_lambdas), - // AnyJointVelocityConstraint::GenericConstraint(c) => c.warmstart(mj_lambdas), - // AnyJointVelocityConstraint::GenericGroundConstraint(c) => c.warmstart(mj_lambdas), - // #[cfg(feature = "simd-is-enabled")] - // AnyJointVelocityConstraint::WGenericConstraint(c) => c.warmstart(mj_lambdas), - // #[cfg(feature = "simd-is-enabled")] - // AnyJointVelocityConstraint::WGenericGroundConstraint(c) => c.warmstart(mj_lambdas), - AnyJointVelocityConstraint::PrismaticConstraint(c) => c.warmstart(mj_lambdas), - AnyJointVelocityConstraint::PrismaticGroundConstraint(c) => c.warmstart(mj_lambdas), - #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WPrismaticConstraint(c) => c.warmstart(mj_lambdas), - #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WPrismaticGroundConstraint(c) => c.warmstart(mj_lambdas), - #[cfg(feature = "dim3")] - AnyJointVelocityConstraint::RevoluteConstraint(c) => c.warmstart(mj_lambdas), - #[cfg(feature = "dim3")] - AnyJointVelocityConstraint::RevoluteGroundConstraint(c) => c.warmstart(mj_lambdas), - #[cfg(feature = "dim3")] - #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WRevoluteConstraint(c) => c.warmstart(mj_lambdas), - #[cfg(feature = "dim3")] - #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WRevoluteGroundConstraint(c) => c.warmstart(mj_lambdas), + AnyJointVelocityConstraint::JointGroundConstraintSimd(c) => c.remove_bias_from_rhs(), + AnyJointVelocityConstraint::JointGenericConstraint(c) => c.remove_bias_from_rhs(), + AnyJointVelocityConstraint::JointGenericGroundConstraint(c) => c.remove_bias_from_rhs(), AnyJointVelocityConstraint::Empty => unreachable!(), } } - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { + pub fn solve( + &mut self, + jacobians: &DVector, + mj_lambdas: &mut [DeltaVel], + generic_mj_lambdas: &mut DVector, + ) { match self { - AnyJointVelocityConstraint::BallConstraint(c) => c.solve(mj_lambdas), - AnyJointVelocityConstraint::BallGroundConstraint(c) => c.solve(mj_lambdas), + AnyJointVelocityConstraint::JointConstraint(c) => c.solve(mj_lambdas), + AnyJointVelocityConstraint::JointGroundConstraint(c) => c.solve(mj_lambdas), #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WBallConstraint(c) => c.solve(mj_lambdas), + AnyJointVelocityConstraint::JointConstraintSimd(c) => c.solve(mj_lambdas), #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WBallGroundConstraint(c) => c.solve(mj_lambdas), - AnyJointVelocityConstraint::FixedConstraint(c) => c.solve(mj_lambdas), - AnyJointVelocityConstraint::FixedGroundConstraint(c) => c.solve(mj_lambdas), - #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WFixedConstraint(c) => c.solve(mj_lambdas), - #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WFixedGroundConstraint(c) => c.solve(mj_lambdas), - // AnyJointVelocityConstraint::GenericConstraint(c) => c.solve(mj_lambdas), - // AnyJointVelocityConstraint::GenericGroundConstraint(c) => c.solve(mj_lambdas), - // #[cfg(feature = "simd-is-enabled")] - // AnyJointVelocityConstraint::WGenericConstraint(c) => c.solve(mj_lambdas), - // #[cfg(feature = "simd-is-enabled")] - // AnyJointVelocityConstraint::WGenericGroundConstraint(c) => c.solve(mj_lambdas), - AnyJointVelocityConstraint::PrismaticConstraint(c) => c.solve(mj_lambdas), - AnyJointVelocityConstraint::PrismaticGroundConstraint(c) => c.solve(mj_lambdas), - #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WPrismaticConstraint(c) => c.solve(mj_lambdas), - #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WPrismaticGroundConstraint(c) => c.solve(mj_lambdas), - #[cfg(feature = "dim3")] - AnyJointVelocityConstraint::RevoluteConstraint(c) => c.solve(mj_lambdas), - #[cfg(feature = "dim3")] - AnyJointVelocityConstraint::RevoluteGroundConstraint(c) => c.solve(mj_lambdas), - #[cfg(feature = "dim3")] - #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WRevoluteConstraint(c) => c.solve(mj_lambdas), - #[cfg(feature = "dim3")] - #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WRevoluteGroundConstraint(c) => c.solve(mj_lambdas), + AnyJointVelocityConstraint::JointGroundConstraintSimd(c) => c.solve(mj_lambdas), + AnyJointVelocityConstraint::JointGenericConstraint(c) => { + c.solve(jacobians, mj_lambdas, generic_mj_lambdas) + } + AnyJointVelocityConstraint::JointGenericGroundConstraint(c) => { + c.solve(jacobians, mj_lambdas, generic_mj_lambdas) + } AnyJointVelocityConstraint::Empty => unreachable!(), } } pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { match self { - AnyJointVelocityConstraint::BallConstraint(c) => c.writeback_impulses(joints_all), - - AnyJointVelocityConstraint::BallGroundConstraint(c) => c.writeback_impulses(joints_all), - #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WBallConstraint(c) => c.writeback_impulses(joints_all), - - #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WBallGroundConstraint(c) => { - c.writeback_impulses(joints_all) - } - AnyJointVelocityConstraint::FixedConstraint(c) => c.writeback_impulses(joints_all), - AnyJointVelocityConstraint::FixedGroundConstraint(c) => { + AnyJointVelocityConstraint::JointConstraint(c) => c.writeback_impulses(joints_all), + AnyJointVelocityConstraint::JointGroundConstraint(c) => { c.writeback_impulses(joints_all) } #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WFixedConstraint(c) => c.writeback_impulses(joints_all), + AnyJointVelocityConstraint::JointConstraintSimd(c) => c.writeback_impulses(joints_all), #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WFixedGroundConstraint(c) => { + AnyJointVelocityConstraint::JointGroundConstraintSimd(c) => { c.writeback_impulses(joints_all) } - // AnyJointVelocityConstraint::GenericConstraint(c) => c.writeback_impulses(joints_all), - // AnyJointVelocityConstraint::GenericGroundConstraint(c) => { - // c.writeback_impulses(joints_all) - // } - // #[cfg(feature = "simd-is-enabled")] - // AnyJointVelocityConstraint::WGenericConstraint(c) => c.writeback_impulses(joints_all), - // #[cfg(feature = "simd-is-enabled")] - // AnyJointVelocityConstraint::WGenericGroundConstraint(c) => { - // c.writeback_impulses(joints_all) - // } - AnyJointVelocityConstraint::PrismaticConstraint(c) => c.writeback_impulses(joints_all), - AnyJointVelocityConstraint::PrismaticGroundConstraint(c) => { + AnyJointVelocityConstraint::JointGenericConstraint(c) => { c.writeback_impulses(joints_all) } - #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WPrismaticConstraint(c) => c.writeback_impulses(joints_all), - #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WPrismaticGroundConstraint(c) => { - c.writeback_impulses(joints_all) - } - #[cfg(feature = "dim3")] - AnyJointVelocityConstraint::RevoluteConstraint(c) => c.writeback_impulses(joints_all), - #[cfg(feature = "dim3")] - AnyJointVelocityConstraint::RevoluteGroundConstraint(c) => { - c.writeback_impulses(joints_all) - } - #[cfg(feature = "dim3")] - #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WRevoluteConstraint(c) => c.writeback_impulses(joints_all), - #[cfg(feature = "dim3")] - #[cfg(feature = "simd-is-enabled")] - AnyJointVelocityConstraint::WRevoluteGroundConstraint(c) => { + AnyJointVelocityConstraint::JointGenericGroundConstraint(c) => { c.writeback_impulses(joints_all) } AnyJointVelocityConstraint::Empty => unreachable!(), diff --git a/src/dynamics/solver/joint_constraint/joint_generic_velocity_constraint.rs b/src/dynamics/solver/joint_constraint/joint_generic_velocity_constraint.rs new file mode 100644 index 0000000..2646fe8 --- /dev/null +++ b/src/dynamics/solver/joint_constraint/joint_generic_velocity_constraint.rs @@ -0,0 +1,529 @@ +use crate::dynamics::solver::joint_constraint::joint_velocity_constraint::WritebackId; +use crate::dynamics::solver::joint_constraint::{JointVelocityConstraintBuilder, SolverBody}; +use crate::dynamics::solver::DeltaVel; +use crate::dynamics::{IntegrationParameters, JointData, JointGraphEdge, JointIndex, Multibody}; +use crate::math::{Isometry, Real, DIM}; +use crate::prelude::SPATIAL_DIM; +use na::{DVector, DVectorSlice, DVectorSliceMut}; + +#[derive(Debug, Copy, Clone)] +pub struct JointGenericVelocityConstraint { + pub is_rigid_body1: bool, + pub is_rigid_body2: bool, + pub mj_lambda1: usize, + pub mj_lambda2: usize, + + pub ndofs1: usize, + pub j_id1: usize, + pub ndofs2: usize, + pub j_id2: usize, + + pub joint_id: JointIndex, + + pub impulse: Real, + pub impulse_bounds: [Real; 2], + pub inv_lhs: Real, + pub rhs: Real, + pub rhs_wo_bias: Real, + + pub writeback_id: WritebackId, +} + +impl JointGenericVelocityConstraint { + pub fn invalid() -> Self { + Self { + is_rigid_body1: false, + is_rigid_body2: false, + mj_lambda1: 0, + mj_lambda2: 0, + ndofs1: 0, + j_id1: 0, + ndofs2: 0, + j_id2: 0, + joint_id: 0, + impulse: 0.0, + impulse_bounds: [-Real::MAX, Real::MAX], + inv_lhs: 0.0, + rhs: 0.0, + rhs_wo_bias: 0.0, + writeback_id: WritebackId::Dof(0), + } + } + + pub fn lock_axes( + params: &IntegrationParameters, + joint_id: JointIndex, + body1: &SolverBody, + body2: &SolverBody, + mb1: Option<(&Multibody, usize)>, + mb2: Option<(&Multibody, usize)>, + frame1: &Isometry, + frame2: &Isometry, + joint: &JointData, + jacobians: &mut DVector, + j_id: &mut usize, + out: &mut [Self], + ) -> usize { + let mut len = 0; + let locked_axes = joint.locked_axes.bits(); + let motor_axes = joint.motor_axes.bits(); + let limit_axes = joint.limit_axes.bits(); + + let builder = JointVelocityConstraintBuilder::new( + frame1, + frame2, + &body1.world_com, + &body2.world_com, + locked_axes, + ); + + for i in 0..DIM { + if locked_axes & (1 << i) != 0 { + out[len] = builder.lock_linear_generic( + params, + jacobians, + j_id, + joint_id, + body1, + body2, + mb1, + mb2, + i, + WritebackId::Dof(i), + ); + len += 1; + } + } + for i in DIM..SPATIAL_DIM { + if locked_axes & (1 << i) != 0 { + out[len] = builder.lock_angular_generic( + params, + jacobians, + j_id, + joint_id, + body1, + body2, + mb1, + mb2, + i - DIM, + WritebackId::Dof(i), + ); + len += 1; + } + } + + for i in 0..DIM { + if motor_axes & (1 << i) != 0 { + out[len] = builder.motor_linear_generic( + params, + jacobians, + j_id, + joint_id, + body1, + body2, + mb1, + mb2, + // locked_ang_axes, + i, + &joint.motors[i].motor_params(params.dt), + WritebackId::Motor(i), + ); + len += 1; + } + } + + for i in DIM..SPATIAL_DIM { + if (motor_axes >> DIM) & (1 << i) != 0 { + out[len] = builder.motor_angular_generic( + params, + jacobians, + j_id, + joint_id, + body1, + body2, + mb1, + mb2, + i - DIM, + &joint.motors[i].motor_params(params.dt), + WritebackId::Motor(i), + ); + len += 1; + } + } + + for i in 0..DIM { + if limit_axes & (1 << i) != 0 { + out[len] = builder.limit_linear_generic( + params, + jacobians, + j_id, + joint_id, + body1, + body2, + mb1, + mb2, + i, + [joint.limits[i].min, joint.limits[i].max], + WritebackId::Limit(i), + ); + len += 1; + } + } + + for i in DIM..SPATIAL_DIM { + if limit_axes & (1 << i) != 0 { + out[len] = builder.limit_angular_generic( + params, + jacobians, + j_id, + joint_id, + body1, + body2, + mb1, + mb2, + i - DIM, + [joint.limits[i].min, joint.limits[i].max], + WritebackId::Limit(i), + ); + len += 1; + } + } + + JointVelocityConstraintBuilder::finalize_generic_constraints(jacobians, &mut out[..len]); + len + } + + fn wj_id1(&self) -> usize { + self.j_id1 + self.ndofs1 + } + + fn wj_id2(&self) -> usize { + self.j_id2 + self.ndofs2 + } + + fn mj_lambda1<'a>( + &self, + mj_lambdas: &'a [DeltaVel], + generic_mj_lambdas: &'a DVector, + ) -> DVectorSlice<'a, Real> { + if self.is_rigid_body1 { + mj_lambdas[self.mj_lambda1].as_vector_slice() + } else { + generic_mj_lambdas.rows(self.mj_lambda1, self.ndofs1) + } + } + + fn mj_lambda1_mut<'a>( + &self, + mj_lambdas: &'a mut [DeltaVel], + generic_mj_lambdas: &'a mut DVector, + ) -> DVectorSliceMut<'a, Real> { + if self.is_rigid_body1 { + mj_lambdas[self.mj_lambda1].as_vector_slice_mut() + } else { + generic_mj_lambdas.rows_mut(self.mj_lambda1, self.ndofs1) + } + } + + fn mj_lambda2<'a>( + &self, + mj_lambdas: &'a [DeltaVel], + generic_mj_lambdas: &'a DVector, + ) -> DVectorSlice<'a, Real> { + if self.is_rigid_body2 { + mj_lambdas[self.mj_lambda2].as_vector_slice() + } else { + generic_mj_lambdas.rows(self.mj_lambda2, self.ndofs2) + } + } + + fn mj_lambda2_mut<'a>( + &self, + mj_lambdas: &'a mut [DeltaVel], + generic_mj_lambdas: &'a mut DVector, + ) -> DVectorSliceMut<'a, Real> { + if self.is_rigid_body2 { + mj_lambdas[self.mj_lambda2].as_vector_slice_mut() + } else { + generic_mj_lambdas.rows_mut(self.mj_lambda2, self.ndofs2) + } + } + + pub fn solve( + &mut self, + jacobians: &DVector, + mj_lambdas: &mut [DeltaVel], + generic_mj_lambdas: &mut DVector, + ) { + let jacobians = jacobians.as_slice(); + + let mj_lambda1 = self.mj_lambda1(mj_lambdas, generic_mj_lambdas); + let j1 = DVectorSlice::from_slice(&jacobians[self.j_id1..], self.ndofs1); + let vel1 = j1.dot(&mj_lambda1); + + let mj_lambda2 = self.mj_lambda2(mj_lambdas, generic_mj_lambdas); + let j2 = DVectorSlice::from_slice(&jacobians[self.j_id2..], self.ndofs2); + let vel2 = j2.dot(&mj_lambda2); + + let dvel = self.rhs + (vel2 - vel1); + let total_impulse = na::clamp( + self.impulse + self.inv_lhs * dvel, + self.impulse_bounds[0], + self.impulse_bounds[1], + ); + let delta_impulse = total_impulse - self.impulse; + self.impulse = total_impulse; + + let mut mj_lambda1 = self.mj_lambda1_mut(mj_lambdas, generic_mj_lambdas); + let wj1 = DVectorSlice::from_slice(&jacobians[self.wj_id1()..], self.ndofs1); + mj_lambda1.axpy(delta_impulse, &wj1, 1.0); + + let mut mj_lambda2 = self.mj_lambda2_mut(mj_lambdas, generic_mj_lambdas); + let wj2 = DVectorSlice::from_slice(&jacobians[self.wj_id2()..], self.ndofs2); + mj_lambda2.axpy(-delta_impulse, &wj2, 1.0); + } + + pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { + let joint = &mut joints_all[self.joint_id].weight; + match self.writeback_id { + WritebackId::Dof(i) => joint.impulses[i] = self.impulse, + WritebackId::Limit(i) => joint.data.limits[i].impulse = self.impulse, + WritebackId::Motor(i) => joint.data.motors[i].impulse = self.impulse, + } + } + + pub fn remove_bias_from_rhs(&mut self) { + self.rhs = self.rhs_wo_bias; + } +} + +#[derive(Debug, Copy, Clone)] +pub struct JointGenericVelocityGroundConstraint { + pub mj_lambda2: usize, + pub ndofs2: usize, + pub j_id2: usize, + + pub joint_id: JointIndex, + + pub impulse: Real, + pub impulse_bounds: [Real; 2], + pub inv_lhs: Real, + pub rhs: Real, + pub rhs_wo_bias: Real, + + pub writeback_id: WritebackId, +} + +impl JointGenericVelocityGroundConstraint { + pub fn invalid() -> Self { + Self { + mj_lambda2: crate::INVALID_USIZE, + ndofs2: 0, + j_id2: crate::INVALID_USIZE, + joint_id: 0, + impulse: 0.0, + impulse_bounds: [-Real::MAX, Real::MAX], + inv_lhs: 0.0, + rhs: 0.0, + rhs_wo_bias: 0.0, + writeback_id: WritebackId::Dof(0), + } + } + + pub fn lock_axes( + params: &IntegrationParameters, + joint_id: JointIndex, + body1: &SolverBody, + body2: &SolverBody, + mb2: (&Multibody, usize), + frame1: &Isometry, + frame2: &Isometry, + joint: &JointData, + jacobians: &mut DVector, + j_id: &mut usize, + out: &mut [Self], + ) -> usize { + let mut len = 0; + let locked_axes = joint.locked_axes.bits(); + let motor_axes = joint.motor_axes.bits(); + let limit_axes = joint.limit_axes.bits(); + + let builder = JointVelocityConstraintBuilder::new( + frame1, + frame2, + &body1.world_com, + &body2.world_com, + locked_axes, + ); + + for i in 0..DIM { + if locked_axes & (1 << i) != 0 { + out[len] = builder.lock_linear_generic_ground( + params, + jacobians, + j_id, + joint_id, + body1, + mb2, + i, + WritebackId::Dof(i), + ); + len += 1; + } + } + for i in DIM..SPATIAL_DIM { + if locked_axes & (1 << i) != 0 { + out[len] = builder.lock_angular_generic_ground( + params, + jacobians, + j_id, + joint_id, + body1, + mb2, + i - DIM, + WritebackId::Dof(i), + ); + len += 1; + } + } + + for i in 0..DIM { + if motor_axes & (1 << i) != 0 { + out[len] = builder.motor_linear_generic_ground( + params, + jacobians, + j_id, + joint_id, + body1, + body2, + mb2, + // locked_ang_axes, + i, + &joint.motors[i].motor_params(params.dt), + WritebackId::Motor(i), + ); + len += 1; + } + } + + for i in DIM..SPATIAL_DIM { + if (motor_axes >> DIM) & (1 << i) != 0 { + out[len] = builder.motor_angular_generic_ground( + params, + jacobians, + j_id, + joint_id, + body1, + body2, + mb2, + i - DIM, + &joint.motors[i].motor_params(params.dt), + WritebackId::Motor(i), + ); + len += 1; + } + } + + for i in 0..DIM { + if limit_axes & (1 << i) != 0 { + out[len] = builder.limit_linear_generic_ground( + params, + jacobians, + j_id, + joint_id, + body1, + mb2, + i, + [joint.limits[i].min, joint.limits[i].max], + WritebackId::Limit(i), + ); + len += 1; + } + } + + for i in DIM..SPATIAL_DIM { + if limit_axes & (1 << i) != 0 { + out[len] = builder.limit_angular_generic_ground( + params, + jacobians, + j_id, + joint_id, + body1, + mb2, + i - DIM, + [joint.limits[i].min, joint.limits[i].max], + WritebackId::Limit(i), + ); + len += 1; + } + } + + JointVelocityConstraintBuilder::finalize_generic_constraints_ground( + jacobians, + &mut out[..len], + ); + len + } + + fn wj_id2(&self) -> usize { + self.j_id2 + self.ndofs2 + } + + fn mj_lambda2<'a>( + &self, + _mj_lambdas: &'a [DeltaVel], + generic_mj_lambdas: &'a DVector, + ) -> DVectorSlice<'a, Real> { + generic_mj_lambdas.rows(self.mj_lambda2, self.ndofs2) + } + + fn mj_lambda2_mut<'a>( + &self, + _mj_lambdas: &'a mut [DeltaVel], + generic_mj_lambdas: &'a mut DVector, + ) -> DVectorSliceMut<'a, Real> { + generic_mj_lambdas.rows_mut(self.mj_lambda2, self.ndofs2) + } + + pub fn solve( + &mut self, + jacobians: &DVector, + mj_lambdas: &mut [DeltaVel], + generic_mj_lambdas: &mut DVector, + ) { + let jacobians = jacobians.as_slice(); + + let mj_lambda2 = self.mj_lambda2(mj_lambdas, generic_mj_lambdas); + let j2 = DVectorSlice::from_slice(&jacobians[self.j_id2..], self.ndofs2); + let vel2 = j2.dot(&mj_lambda2); + + let dvel = self.rhs + vel2; + let total_impulse = na::clamp( + self.impulse + self.inv_lhs * dvel, + self.impulse_bounds[0], + self.impulse_bounds[1], + ); + let delta_impulse = total_impulse - self.impulse; + self.impulse = total_impulse; + + let mut mj_lambda2 = self.mj_lambda2_mut(mj_lambdas, generic_mj_lambdas); + let wj2 = DVectorSlice::from_slice(&jacobians[self.wj_id2()..], self.ndofs2); + mj_lambda2.axpy(-delta_impulse, &wj2, 1.0); + } + + pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { + // FIXME: impulse writeback isn’t supported yet for internal multibody_joint constraints. + if self.joint_id != usize::MAX { + let joint = &mut joints_all[self.joint_id].weight; + match self.writeback_id { + WritebackId::Dof(i) => joint.impulses[i] = self.impulse, + WritebackId::Limit(i) => joint.data.limits[i].impulse = self.impulse, + WritebackId::Motor(i) => joint.data.motors[i].impulse = self.impulse, + } + } + } + + pub fn remove_bias_from_rhs(&mut self) { + self.rhs = self.rhs_wo_bias; + } +} diff --git a/src/dynamics/solver/joint_constraint/joint_generic_velocity_constraint_builder.rs b/src/dynamics/solver/joint_constraint/joint_generic_velocity_constraint_builder.rs new file mode 100644 index 0000000..92bcc9f --- /dev/null +++ b/src/dynamics/solver/joint_constraint/joint_generic_velocity_constraint_builder.rs @@ -0,0 +1,853 @@ +use crate::dynamics::solver::joint_constraint::joint_generic_velocity_constraint::{ + JointGenericVelocityConstraint, JointGenericVelocityGroundConstraint, +}; +use crate::dynamics::solver::joint_constraint::joint_velocity_constraint::WritebackId; +use crate::dynamics::solver::joint_constraint::{JointVelocityConstraintBuilder, SolverBody}; +use crate::dynamics::solver::MotorParameters; +use crate::dynamics::{IntegrationParameters, JointIndex, Multibody}; +use crate::math::{Real, Vector, ANG_DIM, DIM, SPATIAL_DIM}; +use crate::utils::IndexMut2; +use crate::utils::WDot; +use na::{DVector, SVector}; + +#[cfg(feature = "dim3")] +use crate::utils::WAngularInertia; + +impl SolverBody { + pub fn fill_jacobians( + &self, + unit_force: Vector, + unit_torque: SVector, + j_id: &mut usize, + jacobians: &mut DVector, + ) -> Real { + let wj_id = *j_id + SPATIAL_DIM; + jacobians + .fixed_rows_mut::(*j_id) + .copy_from(&unit_force); + jacobians + .fixed_rows_mut::(*j_id + DIM) + .copy_from(&unit_torque); + + { + let mut out_invm_j = jacobians.fixed_rows_mut::(wj_id); + out_invm_j + .fixed_rows_mut::(0) + .axpy(self.im, &unit_force, 0.0); + + #[cfg(feature = "dim2")] + { + out_invm_j[DIM] *= self.sqrt_ii; + } + #[cfg(feature = "dim3")] + { + out_invm_j.fixed_rows_mut::(DIM).gemv( + 1.0, + &self.sqrt_ii.into_matrix(), + &unit_torque, + 0.0, + ); + } + } + + *j_id += SPATIAL_DIM * 2; + unit_force.dot(&self.linvel) + unit_torque.gdot(self.angvel) + } +} + +impl JointVelocityConstraintBuilder { + pub fn lock_jacobians_generic( + &self, + params: &IntegrationParameters, + jacobians: &mut DVector, + j_id: &mut usize, + joint_id: JointIndex, + body1: &SolverBody, + body2: &SolverBody, + mb1: Option<(&Multibody, usize)>, + mb2: Option<(&Multibody, usize)>, + writeback_id: WritebackId, + lin_jac: Vector, + ang_jac1: SVector, + ang_jac2: SVector, + ) -> JointGenericVelocityConstraint { + let is_rigid_body1 = mb1.is_none(); + let is_rigid_body2 = mb2.is_none(); + + let ndofs1 = mb1.map(|(m, _)| m.ndofs()).unwrap_or(SPATIAL_DIM); + let ndofs2 = mb2.map(|(m, _)| m.ndofs()).unwrap_or(SPATIAL_DIM); + + let j_id1 = *j_id; + let vel1 = if let Some((mb1, link_id1)) = mb1 { + mb1.fill_jacobians(link_id1, lin_jac, ang_jac1, j_id, jacobians) + .1 + } else { + body1.fill_jacobians(lin_jac, ang_jac1, j_id, jacobians) + }; + + let j_id2 = *j_id; + let vel2 = if let Some((mb2, link_id2)) = mb2 { + mb2.fill_jacobians(link_id2, lin_jac, ang_jac2, j_id, jacobians) + .1 + } else { + body2.fill_jacobians(lin_jac, ang_jac2, j_id, jacobians) + }; + + if is_rigid_body1 { + let ang_j_id1 = j_id1 + DIM; + let ang_wj_id1 = j_id1 + DIM + ndofs1; + let (mut j, wj) = jacobians.rows_range_pair_mut( + ang_j_id1..ang_j_id1 + ANG_DIM, + ang_wj_id1..ang_wj_id1 + ANG_DIM, + ); + j.copy_from(&wj); + } + + if is_rigid_body2 { + let ang_j_id2 = j_id2 + DIM; + let ang_wj_id2 = j_id2 + DIM + ndofs2; + let (mut j, wj) = jacobians.rows_range_pair_mut( + ang_j_id2..ang_j_id2 + ANG_DIM, + ang_wj_id2..ang_wj_id2 + ANG_DIM, + ); + j.copy_from(&wj); + } + + let rhs_wo_bias = (vel2 - vel1) * params.velocity_solve_fraction; + + let mj_lambda1 = mb1.map(|m| m.0.solver_id).unwrap_or(body1.mj_lambda[0]); + let mj_lambda2 = mb2.map(|m| m.0.solver_id).unwrap_or(body2.mj_lambda[0]); + + JointGenericVelocityConstraint { + is_rigid_body1, + is_rigid_body2, + mj_lambda1, + mj_lambda2, + ndofs1, + j_id1, + ndofs2, + j_id2, + joint_id, + impulse: 0.0, + impulse_bounds: [-Real::MAX, Real::MAX], + inv_lhs: 0.0, + rhs: rhs_wo_bias, + rhs_wo_bias, + writeback_id, + } + } + + pub fn lock_linear_generic( + &self, + params: &IntegrationParameters, + jacobians: &mut DVector, + j_id: &mut usize, + joint_id: JointIndex, + body1: &SolverBody, + body2: &SolverBody, + mb1: Option<(&Multibody, usize)>, + mb2: Option<(&Multibody, usize)>, + locked_axis: usize, + writeback_id: WritebackId, + ) -> JointGenericVelocityConstraint { + let lin_jac = self.basis.column(locked_axis).into_owned(); + let ang_jac1 = self.cmat1_basis.column(locked_axis).into_owned(); + let ang_jac2 = self.cmat2_basis.column(locked_axis).into_owned(); + + let mut c = self.lock_jacobians_generic( + params, + jacobians, + j_id, + joint_id, + body1, + body2, + mb1, + mb2, + writeback_id, + lin_jac, + ang_jac1, + ang_jac2, + ); + + let erp_inv_dt = params.erp_inv_dt(); + let rhs_bias = lin_jac.dot(&self.lin_err) * erp_inv_dt; + c.rhs += rhs_bias; + c + } + + pub fn limit_linear_generic( + &self, + params: &IntegrationParameters, + jacobians: &mut DVector, + j_id: &mut usize, + joint_id: JointIndex, + body1: &SolverBody, + body2: &SolverBody, + mb1: Option<(&Multibody, usize)>, + mb2: Option<(&Multibody, usize)>, + limited_axis: usize, + limits: [Real; 2], + writeback_id: WritebackId, + ) -> JointGenericVelocityConstraint { + let lin_jac = self.basis.column(limited_axis).into_owned(); + let ang_jac1 = self.cmat1_basis.column(limited_axis).into_owned(); + let ang_jac2 = self.cmat2_basis.column(limited_axis).into_owned(); + + let mut constraint = self.lock_jacobians_generic( + params, + jacobians, + j_id, + joint_id, + body1, + body2, + mb1, + mb2, + writeback_id, + lin_jac, + ang_jac1, + ang_jac2, + ); + + let dist = self.lin_err.dot(&lin_jac); + let min_enabled = dist < limits[0]; + let max_enabled = limits[1] < dist; + + let erp_inv_dt = params.erp_inv_dt(); + let rhs_bias = ((dist - limits[1]).max(0.0) - (limits[0] - dist).max(0.0)) * erp_inv_dt; + constraint.rhs += rhs_bias; + constraint.impulse_bounds = [ + min_enabled as u32 as Real * -Real::MAX, + max_enabled as u32 as Real * Real::MAX, + ]; + + constraint + } + + pub fn motor_linear_generic( + &self, + params: &IntegrationParameters, + jacobians: &mut DVector, + j_id: &mut usize, + joint_id: JointIndex, + body1: &SolverBody, + body2: &SolverBody, + mb1: Option<(&Multibody, usize)>, + mb2: Option<(&Multibody, usize)>, + motor_axis: usize, + motor_params: &MotorParameters, + writeback_id: WritebackId, + ) -> JointGenericVelocityConstraint { + let lin_jac = self.basis.column(motor_axis).into_owned(); + let ang_jac1 = self.cmat1_basis.column(motor_axis).into_owned(); + let ang_jac2 = self.cmat2_basis.column(motor_axis).into_owned(); + + // TODO: do we need the same trick as for the non-generic constraint? + // if locked_ang_axes & (1 << motor_axis) != 0 { + // // FIXME: check that this also works for cases + // // whene not all the angular axes are locked. + // constraint.ang_jac1.fill(0.0); + // constraint.ang_jac2.fill(0.0); + // } + + let mut constraint = self.lock_jacobians_generic( + params, + jacobians, + j_id, + joint_id, + body1, + body2, + mb1, + mb2, + writeback_id, + lin_jac, + ang_jac1, + ang_jac2, + ); + + let mut rhs_wo_bias = 0.0; + if motor_params.stiffness != 0.0 { + let dist = self.lin_err.dot(&lin_jac); + rhs_wo_bias += (dist - motor_params.target_pos) * motor_params.stiffness; + } + + if motor_params.damping != 0.0 { + let dvel = lin_jac.dot(&(body2.linvel - body1.linvel)) + + (ang_jac2.gdot(body2.angvel) - ang_jac1.gdot(body1.angvel)); + rhs_wo_bias += (dvel - motor_params.target_vel) * motor_params.damping; + } + + constraint.impulse_bounds = [-motor_params.max_impulse, motor_params.max_impulse]; + constraint.rhs = rhs_wo_bias; + constraint.rhs_wo_bias = rhs_wo_bias; + constraint + } + + pub fn lock_angular_generic( + &self, + params: &IntegrationParameters, + jacobians: &mut DVector, + j_id: &mut usize, + joint_id: JointIndex, + body1: &SolverBody, + body2: &SolverBody, + mb1: Option<(&Multibody, usize)>, + mb2: Option<(&Multibody, usize)>, + locked_axis: usize, + writeback_id: WritebackId, + ) -> JointGenericVelocityConstraint { + let ang_jac = self.ang_basis.column(locked_axis).into_owned(); + + let mut constraint = self.lock_jacobians_generic( + params, + jacobians, + j_id, + joint_id, + body1, + body2, + mb1, + mb2, + writeback_id, + na::zero(), + ang_jac, + ang_jac, + ); + + let erp_inv_dt = params.erp_inv_dt(); + #[cfg(feature = "dim2")] + let rhs_bias = self.ang_err.im * erp_inv_dt; + #[cfg(feature = "dim3")] + let rhs_bias = self.ang_err.imag()[locked_axis] * erp_inv_dt; + constraint.rhs += rhs_bias; + constraint + } + + pub fn limit_angular_generic( + &self, + params: &IntegrationParameters, + jacobians: &mut DVector, + j_id: &mut usize, + joint_id: JointIndex, + body1: &SolverBody, + body2: &SolverBody, + mb1: Option<(&Multibody, usize)>, + mb2: Option<(&Multibody, usize)>, + limited_axis: usize, + limits: [Real; 2], + writeback_id: WritebackId, + ) -> JointGenericVelocityConstraint { + let ang_jac = self.ang_basis.column(limited_axis).into_owned(); + + let mut constraint = self.lock_jacobians_generic( + params, + jacobians, + j_id, + joint_id, + body1, + body2, + mb1, + mb2, + writeback_id, + na::zero(), + ang_jac, + ang_jac, + ); + + let s_limits = [(limits[0] / 2.0).sin(), (limits[1] / 2.0).sin()]; + #[cfg(feature = "dim2")] + let s_ang = self.ang_err.im; + #[cfg(feature = "dim3")] + let s_ang = self.ang_err.imag()[limited_axis]; + let min_enabled = s_ang < s_limits[0]; + let max_enabled = s_limits[1] < s_ang; + let impulse_bounds = [ + min_enabled as u32 as Real * -Real::MAX, + max_enabled as u32 as Real * Real::MAX, + ]; + + let erp_inv_dt = params.erp_inv_dt(); + let rhs_bias = + ((s_ang - s_limits[1]).max(0.0) - (s_limits[0] - s_ang).max(0.0)) * erp_inv_dt; + + constraint.rhs += rhs_bias; + constraint.impulse_bounds = impulse_bounds; + constraint + } + + pub fn motor_angular_generic( + &self, + params: &IntegrationParameters, + jacobians: &mut DVector, + j_id: &mut usize, + joint_id: JointIndex, + body1: &SolverBody, + body2: &SolverBody, + mb1: Option<(&Multibody, usize)>, + mb2: Option<(&Multibody, usize)>, + _motor_axis: usize, + motor_params: &MotorParameters, + writeback_id: WritebackId, + ) -> JointGenericVelocityConstraint { + // let mut ang_jac = self.ang_basis.column(motor_axis).into_owned(); + #[cfg(feature = "dim2")] + let ang_jac = na::Vector1::new(1.0); + #[cfg(feature = "dim3")] + let ang_jac = self.basis.column(_motor_axis).into_owned(); + + let mut constraint = self.lock_jacobians_generic( + params, + jacobians, + j_id, + joint_id, + body1, + body2, + mb1, + mb2, + writeback_id, + na::zero(), + ang_jac, + ang_jac, + ); + + let mut rhs_wo_bias = 0.0; + if motor_params.stiffness != 0.0 { + #[cfg(feature = "dim2")] + let s_ang_dist = self.ang_err.im; + #[cfg(feature = "dim3")] + let s_ang_dist = self.ang_err.imag()[_motor_axis]; + let s_target_ang = motor_params.target_pos.sin(); + rhs_wo_bias += (s_ang_dist - s_target_ang) * motor_params.stiffness; + } + + if motor_params.damping != 0.0 { + let dvel = ang_jac.gdot(body2.angvel) - ang_jac.gdot(body1.angvel); + rhs_wo_bias += (dvel - motor_params.target_vel * ang_jac.norm()) * motor_params.damping; + } + + constraint.rhs_wo_bias = rhs_wo_bias; + constraint.rhs = rhs_wo_bias; + constraint.impulse_bounds = [-motor_params.max_impulse, motor_params.max_impulse]; + constraint + } + + pub fn finalize_generic_constraints( + jacobians: &mut DVector, + constraints: &mut [JointGenericVelocityConstraint], + ) { + // TODO: orthogonalization doesn’t seem to give good results for multibodies? + const ORTHOGONALIZE: bool = false; + let len = constraints.len(); + let ndofs1 = constraints[0].ndofs1; + let ndofs2 = constraints[0].ndofs2; + + // Use the modified Gramm-Schmidt orthogonalization. + for j in 0..len { + let c_j = &mut constraints[j]; + + let jac_j1 = jacobians.rows(c_j.j_id1, ndofs1); + let jac_j2 = jacobians.rows(c_j.j_id2, ndofs2); + let w_jac_j1 = jacobians.rows(c_j.j_id1 + ndofs1, ndofs1); + let w_jac_j2 = jacobians.rows(c_j.j_id2 + ndofs2, ndofs2); + + let dot_jj = jac_j1.dot(&w_jac_j1) + jac_j2.dot(&w_jac_j2); + let inv_dot_jj = crate::utils::inv(dot_jj); + c_j.inv_lhs = inv_dot_jj; // Don’t forget to update the inv_lhs. + + if c_j.impulse_bounds != [-Real::MAX, Real::MAX] { + // Don't remove constraints with limited forces from the others + // because they may not deliver the necessary forces to fulfill + // the removed parts of other constraints. + continue; + } + + if !ORTHOGONALIZE { + continue; + } + + for i in (j + 1)..len { + let (c_i, c_j) = constraints.index_mut_const(i, j); + + let jac_i1 = jacobians.rows(c_i.j_id1, ndofs1); + let jac_i2 = jacobians.rows(c_i.j_id2, ndofs2); + let w_jac_j1 = jacobians.rows(c_j.j_id1 + ndofs1, ndofs1); + let w_jac_j2 = jacobians.rows(c_j.j_id2 + ndofs2, ndofs2); + + let dot_ij = jac_i1.dot(&w_jac_j1) + jac_i2.dot(&w_jac_j2); + let coeff = dot_ij * inv_dot_jj; + + let (mut jac_i, jac_j) = jacobians.rows_range_pair_mut( + c_i.j_id1..c_i.j_id1 + 2 * (ndofs1 + ndofs2), + c_j.j_id1..c_j.j_id1 + 2 * (ndofs1 + ndofs2), + ); + + jac_i.axpy(-coeff, &jac_j, 1.0); + + c_i.rhs_wo_bias -= c_j.rhs_wo_bias * coeff; + c_i.rhs -= c_j.rhs * coeff; + } + } + } +} + +impl JointVelocityConstraintBuilder { + pub fn lock_jacobians_generic_ground( + &self, + params: &IntegrationParameters, + jacobians: &mut DVector, + j_id: &mut usize, + joint_id: JointIndex, + body1: &SolverBody, + (mb2, link_id2): (&Multibody, usize), + writeback_id: WritebackId, + lin_jac: Vector, + ang_jac1: SVector, + ang_jac2: SVector, + ) -> JointGenericVelocityGroundConstraint { + let ndofs2 = mb2.ndofs(); + + let vel1 = lin_jac.dot(&body1.linvel) + ang_jac1.gdot(body1.angvel); + + let j_id2 = *j_id; + let vel2 = mb2 + .fill_jacobians(link_id2, lin_jac, ang_jac2, j_id, jacobians) + .1; + let rhs_wo_bias = (vel2 - vel1) * params.velocity_solve_fraction; + + let mj_lambda2 = mb2.solver_id; + + JointGenericVelocityGroundConstraint { + mj_lambda2, + ndofs2, + j_id2, + joint_id, + impulse: 0.0, + impulse_bounds: [-Real::MAX, Real::MAX], + inv_lhs: 0.0, + rhs: rhs_wo_bias, + rhs_wo_bias, + writeback_id, + } + } + + pub fn lock_linear_generic_ground( + &self, + params: &IntegrationParameters, + jacobians: &mut DVector, + j_id: &mut usize, + joint_id: JointIndex, + body1: &SolverBody, + mb2: (&Multibody, usize), + locked_axis: usize, + writeback_id: WritebackId, + ) -> JointGenericVelocityGroundConstraint { + let lin_jac = self.basis.column(locked_axis).into_owned(); + let ang_jac1 = self.cmat1_basis.column(locked_axis).into_owned(); + let ang_jac2 = self.cmat2_basis.column(locked_axis).into_owned(); + + let mut c = self.lock_jacobians_generic_ground( + params, + jacobians, + j_id, + joint_id, + body1, + mb2, + writeback_id, + lin_jac, + ang_jac1, + ang_jac2, + ); + + let erp_inv_dt = params.erp_inv_dt(); + let rhs_bias = lin_jac.dot(&self.lin_err) * erp_inv_dt; + c.rhs += rhs_bias; + c + } + + pub fn limit_linear_generic_ground( + &self, + params: &IntegrationParameters, + jacobians: &mut DVector, + j_id: &mut usize, + joint_id: JointIndex, + body1: &SolverBody, + mb2: (&Multibody, usize), + limited_axis: usize, + limits: [Real; 2], + writeback_id: WritebackId, + ) -> JointGenericVelocityGroundConstraint { + let lin_jac = self.basis.column(limited_axis).into_owned(); + let ang_jac1 = self.cmat1_basis.column(limited_axis).into_owned(); + let ang_jac2 = self.cmat2_basis.column(limited_axis).into_owned(); + + let mut constraint = self.lock_jacobians_generic_ground( + params, + jacobians, + j_id, + joint_id, + body1, + mb2, + writeback_id, + lin_jac, + ang_jac1, + ang_jac2, + ); + + let dist = self.lin_err.dot(&lin_jac); + let min_enabled = dist < limits[0]; + let max_enabled = limits[1] < dist; + + let erp_inv_dt = params.erp_inv_dt(); + let rhs_bias = ((dist - limits[1]).max(0.0) - (limits[0] - dist).max(0.0)) * erp_inv_dt; + constraint.rhs += rhs_bias; + constraint.impulse_bounds = [ + min_enabled as u32 as Real * -Real::MAX, + max_enabled as u32 as Real * Real::MAX, + ]; + + constraint + } + + pub fn motor_linear_generic_ground( + &self, + params: &IntegrationParameters, + jacobians: &mut DVector, + j_id: &mut usize, + joint_id: JointIndex, + body1: &SolverBody, + body2: &SolverBody, // TODO: we shouldn’t need this. + mb2: (&Multibody, usize), + motor_axis: usize, + motor_params: &MotorParameters, + writeback_id: WritebackId, + ) -> JointGenericVelocityGroundConstraint { + let lin_jac = self.basis.column(motor_axis).into_owned(); + let ang_jac1 = self.cmat1_basis.column(motor_axis).into_owned(); + let ang_jac2 = self.cmat2_basis.column(motor_axis).into_owned(); + + // TODO: do we need the same trick as for the non-generic constraint? + // if locked_ang_axes & (1 << motor_axis) != 0 { + // // FIXME: check that this also works for cases + // // whene not all the angular axes are locked. + // constraint.ang_jac1.fill(0.0); + // constraint.ang_jac2.fill(0.0); + // } + + let mut constraint = self.lock_jacobians_generic_ground( + params, + jacobians, + j_id, + joint_id, + body1, + mb2, + writeback_id, + lin_jac, + ang_jac1, + ang_jac2, + ); + + let mut rhs_wo_bias = 0.0; + if motor_params.stiffness != 0.0 { + let dist = self.lin_err.dot(&lin_jac); + rhs_wo_bias += (dist - motor_params.target_pos) * motor_params.stiffness; + } + + if motor_params.damping != 0.0 { + let dvel = lin_jac.dot(&(body2.linvel - body1.linvel)) + + (ang_jac2.gdot(body2.angvel) - ang_jac1.gdot(body1.angvel)); + rhs_wo_bias += (dvel - motor_params.target_vel) * motor_params.damping; + } + + constraint.impulse_bounds = [-motor_params.max_impulse, motor_params.max_impulse]; + constraint.rhs = rhs_wo_bias; + constraint.rhs_wo_bias = rhs_wo_bias; + constraint + } + + pub fn lock_angular_generic_ground( + &self, + params: &IntegrationParameters, + jacobians: &mut DVector, + j_id: &mut usize, + joint_id: JointIndex, + body1: &SolverBody, + mb2: (&Multibody, usize), + locked_axis: usize, + writeback_id: WritebackId, + ) -> JointGenericVelocityGroundConstraint { + let ang_jac = self.ang_basis.column(locked_axis).into_owned(); + + let mut constraint = self.lock_jacobians_generic_ground( + params, + jacobians, + j_id, + joint_id, + body1, + mb2, + writeback_id, + na::zero(), + ang_jac, + ang_jac, + ); + + let erp_inv_dt = params.erp_inv_dt(); + #[cfg(feature = "dim2")] + let rhs_bias = self.ang_err.im * erp_inv_dt; + #[cfg(feature = "dim3")] + let rhs_bias = self.ang_err.imag()[locked_axis] * erp_inv_dt; + constraint.rhs += rhs_bias; + constraint + } + + pub fn limit_angular_generic_ground( + &self, + params: &IntegrationParameters, + jacobians: &mut DVector, + j_id: &mut usize, + joint_id: JointIndex, + body1: &SolverBody, + mb2: (&Multibody, usize), + limited_axis: usize, + limits: [Real; 2], + writeback_id: WritebackId, + ) -> JointGenericVelocityGroundConstraint { + let ang_jac = self.ang_basis.column(limited_axis).into_owned(); + + let mut constraint = self.lock_jacobians_generic_ground( + params, + jacobians, + j_id, + joint_id, + body1, + mb2, + writeback_id, + na::zero(), + ang_jac, + ang_jac, + ); + + let s_limits = [(limits[0] / 2.0).sin(), (limits[1] / 2.0).sin()]; + #[cfg(feature = "dim2")] + let s_ang = self.ang_err.im; + #[cfg(feature = "dim3")] + let s_ang = self.ang_err.imag()[limited_axis]; + let min_enabled = s_ang < s_limits[0]; + let max_enabled = s_limits[1] < s_ang; + let impulse_bounds = [ + min_enabled as u32 as Real * -Real::MAX, + max_enabled as u32 as Real * Real::MAX, + ]; + + let erp_inv_dt = params.erp_inv_dt(); + let rhs_bias = + ((s_ang - s_limits[1]).max(0.0) - (s_limits[0] - s_ang).max(0.0)) * erp_inv_dt; + + constraint.rhs += rhs_bias; + constraint.impulse_bounds = impulse_bounds; + constraint + } + + pub fn motor_angular_generic_ground( + &self, + params: &IntegrationParameters, + jacobians: &mut DVector, + j_id: &mut usize, + joint_id: JointIndex, + body1: &SolverBody, + body2: &SolverBody, // TODO: we shouldn’t need this. + mb2: (&Multibody, usize), + _motor_axis: usize, + motor_params: &MotorParameters, + writeback_id: WritebackId, + ) -> JointGenericVelocityGroundConstraint { + // let mut ang_jac = self.ang_basis.column(_motor_axis).into_owned(); + #[cfg(feature = "dim2")] + let ang_jac = na::Vector1::new(1.0); + #[cfg(feature = "dim3")] + let ang_jac = self.basis.column(_motor_axis).into_owned(); + + let mut constraint = self.lock_jacobians_generic_ground( + params, + jacobians, + j_id, + joint_id, + body1, + mb2, + writeback_id, + na::zero(), + ang_jac, + ang_jac, + ); + + let mut rhs = 0.0; + if motor_params.stiffness != 0.0 { + #[cfg(feature = "dim2")] + let s_ang_dist = self.ang_err.im; + #[cfg(feature = "dim3")] + let s_ang_dist = self.ang_err.imag()[_motor_axis]; + let s_target_ang = motor_params.target_pos.sin(); + rhs += (s_ang_dist - s_target_ang) * motor_params.stiffness; + } + + if motor_params.damping != 0.0 { + let dvel = ang_jac.gdot(body2.angvel) - ang_jac.gdot(body1.angvel); + rhs += (dvel - motor_params.target_vel * ang_jac.norm()) * motor_params.damping; + } + + constraint.rhs_wo_bias = rhs; + constraint.rhs = rhs; + constraint.impulse_bounds = [-motor_params.max_impulse, motor_params.max_impulse]; + constraint + } + + pub fn finalize_generic_constraints_ground( + jacobians: &mut DVector, + constraints: &mut [JointGenericVelocityGroundConstraint], + ) { + // TODO: orthogonalization doesn’t seem to give good results for multibodies? + const ORTHOGONALIZE: bool = false; + let len = constraints.len(); + let ndofs2 = constraints[0].ndofs2; + + // Use the modified Gramm-Schmidt orthogonalization. + for j in 0..len { + let c_j = &mut constraints[j]; + + let jac_j2 = jacobians.rows(c_j.j_id2, ndofs2); + let w_jac_j2 = jacobians.rows(c_j.j_id2 + ndofs2, ndofs2); + + let dot_jj = jac_j2.dot(&w_jac_j2); + let inv_dot_jj = crate::utils::inv(dot_jj); + c_j.inv_lhs = inv_dot_jj; // Don’t forget to update the inv_lhs. + + if c_j.impulse_bounds != [-Real::MAX, Real::MAX] { + // Don't remove constraints with limited forces from the others + // because they may not deliver the necessary forces to fulfill + // the removed parts of other constraints. + continue; + } + + if !ORTHOGONALIZE { + continue; + } + + for i in (j + 1)..len { + let (c_i, c_j) = constraints.index_mut_const(i, j); + + let jac_i2 = jacobians.rows(c_i.j_id2, ndofs2); + let w_jac_j2 = jacobians.rows(c_j.j_id2 + ndofs2, ndofs2); + + let dot_ij = jac_i2.dot(&w_jac_j2); + let coeff = dot_ij * inv_dot_jj; + + let (mut jac_i, jac_j) = jacobians.rows_range_pair_mut( + c_i.j_id2..c_i.j_id2 + 2 * ndofs2, + c_j.j_id2..c_j.j_id2 + 2 * ndofs2, + ); + + jac_i.axpy(-coeff, &jac_j, 1.0); + + c_i.rhs_wo_bias -= c_j.rhs_wo_bias * coeff; + c_i.rhs -= c_j.rhs * coeff; + } + } + } +} diff --git a/src/dynamics/solver/joint_constraint/joint_position_constraint.rs b/src/dynamics/solver/joint_constraint/joint_position_constraint.rs deleted file mode 100644 index 56e19fc..0000000 --- a/src/dynamics/solver/joint_constraint/joint_position_constraint.rs +++ /dev/null @@ -1,280 +0,0 @@ -use super::{ - BallPositionConstraint, BallPositionGroundConstraint, FixedPositionConstraint, - FixedPositionGroundConstraint, PrismaticPositionConstraint, PrismaticPositionGroundConstraint, -}; -#[cfg(feature = "dim3")] -use super::{RevolutePositionConstraint, RevolutePositionGroundConstraint}; -#[cfg(all(feature = "dim3", feature = "simd-is-enabled"))] -use super::{WRevolutePositionConstraint, WRevolutePositionGroundConstraint}; - -#[cfg(feature = "simd-is-enabled")] -use super::{ - WBallPositionConstraint, WBallPositionGroundConstraint, WFixedPositionConstraint, - WFixedPositionGroundConstraint, WPrismaticPositionConstraint, - WPrismaticPositionGroundConstraint, -}; -use crate::data::{BundleSet, ComponentSet}; -use crate::dynamics::{ - IntegrationParameters, Joint, JointParams, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition, - RigidBodyType, -}; -#[cfg(feature = "simd-is-enabled")] -use crate::math::SIMD_WIDTH; -use crate::math::{Isometry, Real}; - -pub(crate) enum AnyJointPositionConstraint { - BallJoint(BallPositionConstraint), - BallGroundConstraint(BallPositionGroundConstraint), - #[cfg(feature = "simd-is-enabled")] - WBallJoint(WBallPositionConstraint), - #[cfg(feature = "simd-is-enabled")] - WBallGroundConstraint(WBallPositionGroundConstraint), - FixedJoint(FixedPositionConstraint), - FixedGroundConstraint(FixedPositionGroundConstraint), - #[cfg(feature = "simd-is-enabled")] - WFixedJoint(WFixedPositionConstraint), - #[cfg(feature = "simd-is-enabled")] - WFixedGroundConstraint(WFixedPositionGroundConstraint), - // GenericJoint(GenericPositionConstraint), - // GenericGroundConstraint(GenericPositionGroundConstraint), - // #[cfg(feature = "simd-is-enabled")] - // WGenericJoint(WGenericPositionConstraint), - // #[cfg(feature = "simd-is-enabled")] - // WGenericGroundConstraint(WGenericPositionGroundConstraint), - PrismaticJoint(PrismaticPositionConstraint), - PrismaticGroundConstraint(PrismaticPositionGroundConstraint), - #[cfg(feature = "simd-is-enabled")] - WPrismaticJoint(WPrismaticPositionConstraint), - #[cfg(feature = "simd-is-enabled")] - WPrismaticGroundConstraint(WPrismaticPositionGroundConstraint), - #[cfg(feature = "dim3")] - RevoluteJoint(RevolutePositionConstraint), - #[cfg(feature = "dim3")] - RevoluteGroundConstraint(RevolutePositionGroundConstraint), - #[cfg(all(feature = "dim3", feature = "simd-is-enabled"))] - WRevoluteJoint(WRevolutePositionConstraint), - #[cfg(all(feature = "dim3", feature = "simd-is-enabled"))] - WRevoluteGroundConstraint(WRevolutePositionGroundConstraint), - #[allow(dead_code)] // The Empty variant is only used with parallel code. - Empty, -} - -impl AnyJointPositionConstraint { - pub fn from_joint(joint: &Joint, bodies: &Bodies) -> Self - where - Bodies: ComponentSet + ComponentSet, - { - let rb1 = bodies.index_bundle(joint.body1.0); - let rb2 = bodies.index_bundle(joint.body2.0); - - match &joint.params { - JointParams::BallJoint(p) => AnyJointPositionConstraint::BallJoint( - BallPositionConstraint::from_params(rb1, rb2, p), - ), - JointParams::FixedJoint(p) => AnyJointPositionConstraint::FixedJoint( - FixedPositionConstraint::from_params(rb1, rb2, p), - ), - // JointParams::GenericJoint(p) => AnyJointPositionConstraint::GenericJoint( - // GenericPositionConstraint::from_params(rb1, rb2, p), - // ), - JointParams::PrismaticJoint(p) => AnyJointPositionConstraint::PrismaticJoint( - PrismaticPositionConstraint::from_params(rb1, rb2, p), - ), - #[cfg(feature = "dim3")] - JointParams::RevoluteJoint(p) => AnyJointPositionConstraint::RevoluteJoint( - RevolutePositionConstraint::from_params(rb1, rb2, p), - ), - } - } - - #[cfg(feature = "simd-is-enabled")] - pub fn from_wide_joint(joints: [&Joint; SIMD_WIDTH], bodies: &Bodies) -> Self - where - Bodies: ComponentSet + ComponentSet, - { - let rbs1 = ( - gather![|ii| bodies.index(joints[ii].body1.0)], - gather![|ii| bodies.index(joints[ii].body1.0)], - ); - let rbs2 = ( - gather![|ii| bodies.index(joints[ii].body2.0)], - gather![|ii| bodies.index(joints[ii].body2.0)], - ); - - match &joints[0].params { - JointParams::BallJoint(_) => { - let joints = gather![|ii| joints[ii].params.as_ball_joint().unwrap()]; - AnyJointPositionConstraint::WBallJoint(WBallPositionConstraint::from_params( - rbs1, rbs2, joints, - )) - } - JointParams::FixedJoint(_) => { - let joints = gather![|ii| joints[ii].params.as_fixed_joint().unwrap()]; - AnyJointPositionConstraint::WFixedJoint(WFixedPositionConstraint::from_params( - rbs1, rbs2, joints, - )) - } - // JointParams::GenericJoint(_) => { - // let joints = gather![|ii| joints[ii].params.as_generic_joint().unwrap()]; - // AnyJointPositionConstraint::WGenericJoint(WGenericPositionConstraint::from_params( - // rbs1, rbs2, joints, - // )) - // } - JointParams::PrismaticJoint(_) => { - let joints = gather![|ii| joints[ii].params.as_prismatic_joint().unwrap()]; - AnyJointPositionConstraint::WPrismaticJoint( - WPrismaticPositionConstraint::from_params(rbs1, rbs2, joints), - ) - } - #[cfg(feature = "dim3")] - JointParams::RevoluteJoint(_) => { - let joints = gather![|ii| joints[ii].params.as_revolute_joint().unwrap()]; - AnyJointPositionConstraint::WRevoluteJoint( - WRevolutePositionConstraint::from_params(rbs1, rbs2, joints), - ) - } - } - } - - pub fn from_joint_ground(joint: &Joint, bodies: &Bodies) -> Self - where - Bodies: ComponentSet - + ComponentSet - + ComponentSet - + ComponentSet, - { - let mut handle1 = joint.body1; - let mut handle2 = joint.body2; - - let status2: &RigidBodyType = bodies.index(handle2.0); - let flipped = !status2.is_dynamic(); - - if flipped { - std::mem::swap(&mut handle1, &mut handle2); - } - - let rb1 = bodies.index(handle1.0); - let rb2 = (bodies.index(handle2.0), bodies.index(handle2.0)); - - match &joint.params { - JointParams::BallJoint(p) => AnyJointPositionConstraint::BallGroundConstraint( - BallPositionGroundConstraint::from_params(rb1, rb2, p, flipped), - ), - JointParams::FixedJoint(p) => AnyJointPositionConstraint::FixedGroundConstraint( - FixedPositionGroundConstraint::from_params(rb1, rb2, p, flipped), - ), - // JointParams::GenericJoint(p) => AnyJointPositionConstraint::GenericGroundConstraint( - // GenericPositionGroundConstraint::from_params(rb1, rb2, p, flipped), - // ), - JointParams::PrismaticJoint(p) => { - AnyJointPositionConstraint::PrismaticGroundConstraint( - PrismaticPositionGroundConstraint::from_params(rb1, rb2, p, flipped), - ) - } - #[cfg(feature = "dim3")] - JointParams::RevoluteJoint(p) => AnyJointPositionConstraint::RevoluteGroundConstraint( - RevolutePositionGroundConstraint::from_params(rb1, rb2, p, flipped), - ), - } - } - - #[cfg(feature = "simd-is-enabled")] - pub fn from_wide_joint_ground(joints: [&Joint; SIMD_WIDTH], bodies: &Bodies) -> Self - where - Bodies: ComponentSet - + ComponentSet - + ComponentSet - + ComponentSet, - { - let mut handles1 = gather![|ii| joints[ii].body1]; - let mut handles2 = gather![|ii| joints[ii].body2]; - let status2: [&RigidBodyType; SIMD_WIDTH] = gather![|ii| bodies.index(handles2[ii].0)]; - - let mut flipped = [false; SIMD_WIDTH]; - - for ii in 0..SIMD_WIDTH { - if !status2[ii].is_dynamic() { - std::mem::swap(&mut handles1[ii], &mut handles2[ii]); - flipped[ii] = true; - } - } - - let rbs1 = gather![|ii| bodies.index(handles1[ii].0)]; - let rbs2 = ( - gather![|ii| bodies.index(handles2[ii].0)], - gather![|ii| bodies.index(handles2[ii].0)], - ); - - match &joints[0].params { - JointParams::BallJoint(_) => { - let joints = gather![|ii| joints[ii].params.as_ball_joint().unwrap()]; - AnyJointPositionConstraint::WBallGroundConstraint( - WBallPositionGroundConstraint::from_params(rbs1, rbs2, joints, flipped), - ) - } - JointParams::FixedJoint(_) => { - let joints = gather![|ii| joints[ii].params.as_fixed_joint().unwrap()]; - AnyJointPositionConstraint::WFixedGroundConstraint( - WFixedPositionGroundConstraint::from_params(rbs1, rbs2, joints, flipped), - ) - } - // JointParams::GenericJoint(_) => { - // let joints = gather![|ii| joints[ii].params.as_generic_joint().unwrap()]; - // AnyJointPositionConstraint::WGenericGroundConstraint( - // WGenericPositionGroundConstraint::from_params(rbs1, rbs2, joints, flipped), - // ) - // } - JointParams::PrismaticJoint(_) => { - let joints = gather![|ii| joints[ii].params.as_prismatic_joint().unwrap()]; - AnyJointPositionConstraint::WPrismaticGroundConstraint( - WPrismaticPositionGroundConstraint::from_params(rbs1, rbs2, joints, flipped), - ) - } - #[cfg(feature = "dim3")] - JointParams::RevoluteJoint(_) => { - let joints = gather![|ii| joints[ii].params.as_revolute_joint().unwrap()]; - AnyJointPositionConstraint::WRevoluteGroundConstraint( - WRevolutePositionGroundConstraint::from_params(rbs1, rbs2, joints, flipped), - ) - } - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - match self { - AnyJointPositionConstraint::BallJoint(c) => c.solve(params, positions), - AnyJointPositionConstraint::BallGroundConstraint(c) => c.solve(params, positions), - #[cfg(feature = "simd-is-enabled")] - AnyJointPositionConstraint::WBallJoint(c) => c.solve(params, positions), - #[cfg(feature = "simd-is-enabled")] - AnyJointPositionConstraint::WBallGroundConstraint(c) => c.solve(params, positions), - AnyJointPositionConstraint::FixedJoint(c) => c.solve(params, positions), - AnyJointPositionConstraint::FixedGroundConstraint(c) => c.solve(params, positions), - #[cfg(feature = "simd-is-enabled")] - AnyJointPositionConstraint::WFixedJoint(c) => c.solve(params, positions), - #[cfg(feature = "simd-is-enabled")] - AnyJointPositionConstraint::WFixedGroundConstraint(c) => c.solve(params, positions), - // AnyJointPositionConstraint::GenericJoint(c) => c.solve(params, positions), - // AnyJointPositionConstraint::GenericGroundConstraint(c) => c.solve(params, positions), - // #[cfg(feature = "simd-is-enabled")] - // AnyJointPositionConstraint::WGenericJoint(c) => c.solve(params, positions), - // #[cfg(feature = "simd-is-enabled")] - // AnyJointPositionConstraint::WGenericGroundConstraint(c) => c.solve(params, positions), - AnyJointPositionConstraint::PrismaticJoint(c) => c.solve(params, positions), - AnyJointPositionConstraint::PrismaticGroundConstraint(c) => c.solve(params, positions), - #[cfg(feature = "simd-is-enabled")] - AnyJointPositionConstraint::WPrismaticJoint(c) => c.solve(params, positions), - #[cfg(feature = "simd-is-enabled")] - AnyJointPositionConstraint::WPrismaticGroundConstraint(c) => c.solve(params, positions), - #[cfg(feature = "dim3")] - AnyJointPositionConstraint::RevoluteJoint(c) => c.solve(params, positions), - #[cfg(feature = "dim3")] - AnyJointPositionConstraint::RevoluteGroundConstraint(c) => c.solve(params, positions), - #[cfg(all(feature = "dim3", feature = "simd-is-enabled"))] - AnyJointPositionConstraint::WRevoluteJoint(c) => c.solve(params, positions), - #[cfg(all(feature = "dim3", feature = "simd-is-enabled"))] - AnyJointPositionConstraint::WRevoluteGroundConstraint(c) => c.solve(params, positions), - AnyJointPositionConstraint::Empty => unreachable!(), - } - } -} diff --git a/src/dynamics/solver/joint_constraint/joint_velocity_constraint.rs b/src/dynamics/solver/joint_constraint/joint_velocity_constraint.rs new file mode 100644 index 0000000..3e31256 --- /dev/null +++ b/src/dynamics/solver/joint_constraint/joint_velocity_constraint.rs @@ -0,0 +1,608 @@ +use crate::dynamics::solver::joint_constraint::JointVelocityConstraintBuilder; +use crate::dynamics::solver::DeltaVel; +use crate::dynamics::{IntegrationParameters, JointData, JointGraphEdge, JointIndex}; +use crate::math::{AngVector, AngularInertia, Isometry, Point, Real, Vector, DIM, SPATIAL_DIM}; +use crate::utils::{WDot, WReal}; +use simba::simd::SimdRealField; + +#[cfg(feature = "simd-is-enabled")] +use { + crate::math::{SimdReal, SIMD_WIDTH}, + na::SimdValue, +}; + +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct MotorParameters { + pub stiffness: N, + pub damping: N, + pub gamma: N, + // pub keep_lhs: bool, + pub target_pos: N, + pub target_vel: N, + pub max_impulse: N, +} + +impl Default for MotorParameters { + fn default() -> Self { + Self { + stiffness: N::zero(), + damping: N::zero(), + gamma: N::zero(), + // keep_lhs: true, + target_pos: N::zero(), + target_vel: N::zero(), + max_impulse: N::zero(), + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum WritebackId { + Dof(usize), + Limit(usize), + Motor(usize), +} + +// TODO: right now we only use this for impulse_joints. +// However, it may actually be a good idea to use this everywhere in +// the solver, to avoid fetching data from the rigid-body set +// every time. +#[derive(Copy, Clone)] +pub struct SolverBody { + pub linvel: Vector, + pub angvel: AngVector, + pub im: N, + pub sqrt_ii: AngularInertia, + pub world_com: Point, + pub mj_lambda: [usize; LANES], +} + +#[derive(Debug, Copy, Clone)] +pub struct JointVelocityConstraint { + pub mj_lambda1: [usize; LANES], + pub mj_lambda2: [usize; LANES], + + pub joint_id: [JointIndex; LANES], + + pub impulse: N, + pub impulse_bounds: [N; 2], + pub lin_jac: Vector, + pub ang_jac1: AngVector, + pub ang_jac2: AngVector, + + pub inv_lhs: N, + pub rhs: N, + pub rhs_wo_bias: N, + + pub im1: N, + pub im2: N, + + pub writeback_id: WritebackId, +} + +impl JointVelocityConstraint { + pub fn invalid() -> Self { + Self { + mj_lambda1: [crate::INVALID_USIZE; LANES], + mj_lambda2: [crate::INVALID_USIZE; LANES], + joint_id: [crate::INVALID_USIZE; LANES], + impulse: N::zero(), + impulse_bounds: [N::zero(), N::zero()], + lin_jac: Vector::zeros(), + ang_jac1: na::zero(), + ang_jac2: na::zero(), + inv_lhs: N::zero(), + rhs: N::zero(), + rhs_wo_bias: N::zero(), + im1: N::zero(), + im2: N::zero(), + writeback_id: WritebackId::Dof(0), + } + } + + pub fn solve_generic(&mut self, mj_lambda1: &mut DeltaVel, mj_lambda2: &mut DeltaVel) { + let dlinvel = self.lin_jac.dot(&(mj_lambda2.linear - mj_lambda1.linear)); + let dangvel = + self.ang_jac2.gdot(mj_lambda2.angular) - self.ang_jac1.gdot(mj_lambda1.angular); + + let rhs = dlinvel + dangvel + self.rhs; + let total_impulse = (self.impulse + self.inv_lhs * rhs) + .simd_clamp(self.impulse_bounds[0], self.impulse_bounds[1]); + let delta_impulse = total_impulse - self.impulse; + self.impulse = total_impulse; + + let lin_impulse = self.lin_jac * delta_impulse; + let ang_impulse1 = self.ang_jac1 * delta_impulse; + let ang_impulse2 = self.ang_jac2 * delta_impulse; + + mj_lambda1.linear += lin_impulse * self.im1; + mj_lambda1.angular += ang_impulse1; + mj_lambda2.linear -= lin_impulse * self.im2; + mj_lambda2.angular -= ang_impulse2; + } + + pub fn remove_bias_from_rhs(&mut self) { + self.rhs = self.rhs_wo_bias; + } +} + +impl JointVelocityConstraint { + pub fn lock_axes( + params: &IntegrationParameters, + joint_id: JointIndex, + body1: &SolverBody, + body2: &SolverBody, + frame1: &Isometry, + frame2: &Isometry, + joint: &JointData, + out: &mut [Self], + ) -> usize { + let mut len = 0; + let locked_axes = joint.locked_axes.bits(); + let motor_axes = joint.motor_axes.bits(); + let limit_axes = joint.limit_axes.bits(); + + let builder = JointVelocityConstraintBuilder::new( + frame1, + frame2, + &body1.world_com, + &body2.world_com, + locked_axes, + ); + + for i in 0..DIM { + if locked_axes & (1 << i) != 0 { + out[len] = + builder.lock_linear(params, [joint_id], body1, body2, i, WritebackId::Dof(i)); + len += 1; + } + } + for i in DIM..SPATIAL_DIM { + if locked_axes & (1 << i) != 0 { + out[len] = builder.lock_angular( + params, + [joint_id], + body1, + body2, + i - DIM, + WritebackId::Dof(i), + ); + len += 1; + } + } + + for i in 0..DIM { + if motor_axes & (1 << i) != 0 { + out[len] = builder.motor_linear( + params, + [joint_id], + body1, + body2, + locked_axes >> DIM, + i, + &joint.motors[i].motor_params(params.dt), + WritebackId::Motor(i), + ); + len += 1; + } + } + for i in DIM..SPATIAL_DIM { + if motor_axes & (1 << i) != 0 { + out[len] = builder.motor_angular( + [joint_id], + body1, + body2, + i - DIM, + &joint.motors[i].motor_params(params.dt), + WritebackId::Motor(i), + ); + len += 1; + } + } + + for i in 0..DIM { + if limit_axes & (1 << i) != 0 { + out[len] = builder.limit_linear( + params, + [joint_id], + body1, + body2, + i, + [joint.limits[i].min, joint.limits[i].max], + WritebackId::Limit(i), + ); + len += 1; + } + } + for i in DIM..SPATIAL_DIM { + if limit_axes & (1 << i) != 0 { + out[len] = builder.limit_angular( + params, + [joint_id], + body1, + body2, + i - DIM, + [joint.limits[i].min, joint.limits[i].max], + WritebackId::Limit(i), + ); + len += 1; + } + } + + JointVelocityConstraintBuilder::finalize_constraints(&mut out[..len]); + len + } + + pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { + let mut mj_lambda1 = mj_lambdas[self.mj_lambda1[0] as usize]; + let mut mj_lambda2 = mj_lambdas[self.mj_lambda2[0] as usize]; + + self.solve_generic(&mut mj_lambda1, &mut mj_lambda2); + + mj_lambdas[self.mj_lambda1[0] as usize] = mj_lambda1; + mj_lambdas[self.mj_lambda2[0] as usize] = mj_lambda2; + } + + pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { + let joint = &mut joints_all[self.joint_id[0]].weight; + match self.writeback_id { + WritebackId::Dof(i) => joint.impulses[i] = self.impulse, + WritebackId::Limit(i) => joint.data.limits[i].impulse = self.impulse, + WritebackId::Motor(i) => joint.data.motors[i].impulse = self.impulse, + } + } +} +#[cfg(feature = "simd-is-enabled")] +impl JointVelocityConstraint { + pub fn lock_axes( + params: &IntegrationParameters, + joint_id: [JointIndex; SIMD_WIDTH], + body1: &SolverBody, + body2: &SolverBody, + frame1: &Isometry, + frame2: &Isometry, + locked_axes: u8, + out: &mut [Self], + ) -> usize { + let builder = JointVelocityConstraintBuilder::new( + frame1, + frame2, + &body1.world_com, + &body2.world_com, + locked_axes, + ); + + let mut len = 0; + for i in 0..DIM { + if locked_axes & (1 << i) != 0 { + out[len] = + builder.lock_linear(params, joint_id, body1, body2, i, WritebackId::Dof(i)); + len += 1; + } + } + + for i in DIM..SPATIAL_DIM { + if locked_axes & (1 << i) != 0 { + out[len] = builder.lock_angular( + params, + joint_id, + body1, + body2, + i - DIM, + WritebackId::Dof(i), + ); + len += 1; + } + } + + JointVelocityConstraintBuilder::finalize_constraints(&mut out[..len]); + len + } + + pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { + let mut mj_lambda1 = DeltaVel { + linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda1[ii] as usize].linear]), + angular: AngVector::from(gather![ + |ii| mj_lambdas[self.mj_lambda1[ii] as usize].angular + ]), + }; + let mut mj_lambda2 = DeltaVel { + linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), + angular: AngVector::from(gather![ + |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular + ]), + }; + + self.solve_generic(&mut mj_lambda1, &mut mj_lambda2); + + for ii in 0..SIMD_WIDTH { + mj_lambdas[self.mj_lambda1[ii] as usize].linear = mj_lambda1.linear.extract(ii); + mj_lambdas[self.mj_lambda1[ii] as usize].angular = mj_lambda1.angular.extract(ii); + mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); + mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); + } + } + + pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { + let impulses: [_; SIMD_WIDTH] = self.impulse.into(); + + // TODO: should we move the iteration on ii deeper in the mested match? + for ii in 0..SIMD_WIDTH { + let joint = &mut joints_all[self.joint_id[ii]].weight; + match self.writeback_id { + WritebackId::Dof(i) => joint.impulses[i] = impulses[ii], + WritebackId::Limit(i) => joint.data.limits[i].impulse = impulses[ii], + WritebackId::Motor(i) => joint.data.motors[i].impulse = impulses[ii], + } + } + } +} + +#[derive(Debug, Copy, Clone)] +pub struct JointVelocityGroundConstraint { + pub mj_lambda2: [usize; LANES], + + pub joint_id: [JointIndex; LANES], + + pub impulse: N, + pub impulse_bounds: [N; 2], + pub lin_jac: Vector, + pub ang_jac2: AngVector, + + pub inv_lhs: N, + pub rhs: N, + pub rhs_wo_bias: N, + + pub im2: N, + + pub writeback_id: WritebackId, +} + +impl JointVelocityGroundConstraint { + pub fn invalid() -> Self { + Self { + mj_lambda2: [crate::INVALID_USIZE; LANES], + joint_id: [crate::INVALID_USIZE; LANES], + impulse: N::zero(), + impulse_bounds: [N::zero(), N::zero()], + lin_jac: Vector::zeros(), + ang_jac2: na::zero(), + inv_lhs: N::zero(), + rhs: N::zero(), + rhs_wo_bias: N::zero(), + im2: N::zero(), + writeback_id: WritebackId::Dof(0), + } + } + + pub fn solve_generic(&mut self, mj_lambda2: &mut DeltaVel) { + let dlinvel = mj_lambda2.linear; + let dangvel = mj_lambda2.angular; + + let dvel = self.lin_jac.dot(&dlinvel) + self.ang_jac2.gdot(dangvel) + self.rhs; + let total_impulse = (self.impulse + self.inv_lhs * dvel) + .simd_clamp(self.impulse_bounds[0], self.impulse_bounds[1]); + let delta_impulse = total_impulse - self.impulse; + self.impulse = total_impulse; + + let lin_impulse = self.lin_jac * delta_impulse; + let ang_impulse = self.ang_jac2 * delta_impulse; + + mj_lambda2.linear -= lin_impulse * self.im2; + mj_lambda2.angular -= ang_impulse; + } + + pub fn remove_bias_from_rhs(&mut self) { + self.rhs = self.rhs_wo_bias; + } +} + +impl JointVelocityGroundConstraint { + pub fn lock_axes( + params: &IntegrationParameters, + joint_id: JointIndex, + body1: &SolverBody, + body2: &SolverBody, + frame1: &Isometry, + frame2: &Isometry, + joint: &JointData, + out: &mut [Self], + ) -> usize { + let mut len = 0; + let locked_axes = joint.locked_axes.bits() as u8; + let motor_axes = joint.motor_axes.bits() as u8; + let limit_axes = joint.limit_axes.bits() as u8; + + let builder = JointVelocityConstraintBuilder::new( + frame1, + frame2, + &body1.world_com, + &body2.world_com, + locked_axes, + ); + + for i in 0..DIM { + if locked_axes & (1 << i) != 0 { + out[len] = builder.lock_linear_ground( + params, + [joint_id], + body1, + body2, + i, + WritebackId::Dof(i), + ); + len += 1; + } + } + for i in DIM..SPATIAL_DIM { + if locked_axes & (1 << i) != 0 { + out[len] = builder.lock_angular_ground( + params, + [joint_id], + body1, + body2, + i - DIM, + WritebackId::Dof(i), + ); + len += 1; + } + } + + for i in 0..DIM { + if motor_axes & (1 << i) != 0 { + out[len] = builder.motor_linear_ground( + [joint_id], + body1, + body2, + i, + &joint.motors[i].motor_params(params.dt), + WritebackId::Motor(i), + ); + len += 1; + } + } + for i in DIM..SPATIAL_DIM { + if motor_axes & (1 << i) != 0 { + out[len] = builder.motor_angular_ground( + [joint_id], + body1, + body2, + i - DIM, + &joint.motors[i].motor_params(params.dt), + WritebackId::Motor(i), + ); + len += 1; + } + } + + for i in 0..DIM { + if limit_axes & (1 << i) != 0 { + out[len] = builder.limit_linear_ground( + params, + [joint_id], + body1, + body2, + i, + [joint.limits[i].min, joint.limits[i].max], + WritebackId::Limit(i), + ); + len += 1; + } + } + for i in DIM..SPATIAL_DIM { + if limit_axes & (1 << i) != 0 { + out[len] = builder.limit_angular_ground( + params, + [joint_id], + body1, + body2, + i - DIM, + [joint.limits[i].min, joint.limits[i].max], + WritebackId::Limit(i), + ); + len += 1; + } + } + + JointVelocityConstraintBuilder::finalize_ground_constraints(&mut out[..len]); + len + } + + pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { + let mut mj_lambda2 = mj_lambdas[self.mj_lambda2[0] as usize]; + self.solve_generic(&mut mj_lambda2); + mj_lambdas[self.mj_lambda2[0] as usize] = mj_lambda2; + } + + pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { + let joint = &mut joints_all[self.joint_id[0]].weight; + match self.writeback_id { + WritebackId::Dof(i) => joint.impulses[i] = self.impulse, + WritebackId::Limit(i) => joint.data.limits[i].impulse = self.impulse, + WritebackId::Motor(i) => joint.data.motors[i].impulse = self.impulse, + } + } +} + +#[cfg(feature = "simd-is-enabled")] +impl JointVelocityGroundConstraint { + pub fn lock_axes( + params: &IntegrationParameters, + joint_id: [JointIndex; SIMD_WIDTH], + body1: &SolverBody, + body2: &SolverBody, + frame1: &Isometry, + frame2: &Isometry, + locked_axes: u8, + out: &mut [Self], + ) -> usize { + let mut len = 0; + let builder = JointVelocityConstraintBuilder::new( + frame1, + frame2, + &body1.world_com, + &body2.world_com, + locked_axes, + ); + + for i in 0..DIM { + if locked_axes & (1 << i) != 0 { + out[len] = builder.lock_linear_ground( + params, + joint_id, + body1, + body2, + i, + WritebackId::Dof(i), + ); + len += 1; + } + } + for i in DIM..SPATIAL_DIM { + if locked_axes & (1 << i) != 0 { + out[len] = builder.lock_angular_ground( + params, + joint_id, + body1, + body2, + i - DIM, + WritebackId::Dof(i), + ); + len += 1; + } + } + + JointVelocityConstraintBuilder::finalize_ground_constraints(&mut out[..len]); + len + } + + pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { + let mut mj_lambda2 = DeltaVel { + linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), + angular: AngVector::from(gather![ + |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular + ]), + }; + + self.solve_generic(&mut mj_lambda2); + + for ii in 0..SIMD_WIDTH { + mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); + mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); + } + } + + pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { + let impulses: [_; SIMD_WIDTH] = self.impulse.into(); + + // TODO: should we move the iteration on ii deeper in the mested match? + for ii in 0..SIMD_WIDTH { + let joint = &mut joints_all[self.joint_id[ii]].weight; + match self.writeback_id { + WritebackId::Dof(i) => joint.impulses[i] = impulses[ii], + WritebackId::Limit(i) => joint.data.limits[i].impulse = impulses[ii], + WritebackId::Motor(i) => joint.data.motors[i].impulse = impulses[ii], + } + } + } +} diff --git a/src/dynamics/solver/joint_constraint/joint_velocity_constraint_builder.rs b/src/dynamics/solver/joint_constraint/joint_velocity_constraint_builder.rs new file mode 100644 index 0000000..dd17ecf --- /dev/null +++ b/src/dynamics/solver/joint_constraint/joint_velocity_constraint_builder.rs @@ -0,0 +1,699 @@ +use crate::dynamics::solver::joint_constraint::joint_velocity_constraint::{ + JointVelocityConstraint, JointVelocityGroundConstraint, WritebackId, +}; +use crate::dynamics::solver::joint_constraint::SolverBody; +use crate::dynamics::solver::MotorParameters; +use crate::dynamics::{IntegrationParameters, JointIndex}; +use crate::math::{Isometry, Matrix, Point, Real, Rotation, Vector, ANG_DIM, DIM}; +use crate::utils::{IndexMut2, WCrossMatrix, WDot, WQuat, WReal}; +use na::SMatrix; +use simba::simd::SimdRealField; + +#[derive(Debug, Copy, Clone)] +pub struct JointVelocityConstraintBuilder { + pub basis: Matrix, + pub cmat1_basis: SMatrix, + pub cmat2_basis: SMatrix, + pub ang_basis: SMatrix, + pub lin_err: Vector, + pub ang_err: Rotation, +} + +impl JointVelocityConstraintBuilder { + pub fn new( + frame1: &Isometry, + frame2: &Isometry, + world_com1: &Point, + world_com2: &Point, + locked_lin_axes: u8, + ) -> Self { + let mut frame1 = *frame1; + let basis = frame1.rotation.to_rotation_matrix().into_inner(); + let lin_err = frame2.translation.vector - frame1.translation.vector; + + // Adjust the point of application of the force for the first body, + // by snapping free axes to the second frame’s center (to account for + // the allowed relative movement). + { + let mut new_center1 = frame2.translation.vector; // First, assume all dofs are free. + + // Then snap the locked ones. + for i in 0..DIM { + if locked_lin_axes & (1 << i) != 0 { + let axis = basis.column(i); + new_center1 -= axis * lin_err.dot(&axis); + } + } + frame1.translation.vector = new_center1; + } + + let r1 = frame1.translation.vector - world_com1.coords; + let r2 = frame2.translation.vector - world_com2.coords; + + let cmat1 = r1.gcross_matrix(); + let cmat2 = r2.gcross_matrix(); + + #[allow(unused_mut)] // The mut is needed for 3D + let mut ang_basis = frame1.rotation.diff_conj1_2(&frame2.rotation).transpose(); + #[allow(unused_mut)] // The mut is needed for 3D + let mut ang_err = frame1.rotation.inverse() * frame2.rotation; + + #[cfg(feature = "dim3")] + { + let sgn = N::one().simd_copysign(frame1.rotation.dot(&frame2.rotation)); + ang_basis *= sgn; + *ang_err.as_mut_unchecked() *= sgn; + } + + Self { + basis, + cmat1_basis: cmat1 * basis, + cmat2_basis: cmat2 * basis, + ang_basis, + lin_err, + ang_err, + } + } + + pub fn limit_linear( + &self, + params: &IntegrationParameters, + joint_id: [JointIndex; LANES], + body1: &SolverBody, + body2: &SolverBody, + limited_axis: usize, + limits: [N; 2], + writeback_id: WritebackId, + ) -> JointVelocityConstraint { + let zero = N::zero(); + let mut constraint = + self.lock_linear(params, joint_id, body1, body2, limited_axis, writeback_id); + + let dist = self.lin_err.dot(&constraint.lin_jac); + let min_enabled = dist.simd_lt(limits[0]); + let max_enabled = limits[1].simd_lt(dist); + + let erp_inv_dt = N::splat(params.erp_inv_dt()); + let rhs_bias = + ((dist - limits[1]).simd_max(zero) - (limits[0] - dist).simd_max(zero)) * erp_inv_dt; + constraint.rhs = constraint.rhs_wo_bias + rhs_bias; + constraint.impulse_bounds = [ + N::splat(-Real::INFINITY).select(min_enabled, zero), + N::splat(Real::INFINITY).select(max_enabled, zero), + ]; + + constraint + } + + pub fn motor_linear( + &self, + params: &IntegrationParameters, + joint_id: [JointIndex; LANES], + body1: &SolverBody, + body2: &SolverBody, + _locked_ang_axes: u8, + motor_axis: usize, + motor_params: &MotorParameters, + writeback_id: WritebackId, + ) -> JointVelocityConstraint { + let mut constraint = + self.lock_linear(params, joint_id, body1, body2, motor_axis, writeback_id); + + // if locked_ang_axes & (1 << motor_axis) != 0 { + // // FIXME: check that this also works for cases + // // when not all the angular axes are locked. + // constraint.ang_jac1 = na::zero(); + // constraint.ang_jac2 = na::zero(); + // } + + let mut rhs_wo_bias = N::zero(); + if motor_params.stiffness != N::zero() { + let dist = self.lin_err.dot(&constraint.lin_jac); + rhs_wo_bias += (dist - motor_params.target_pos) * motor_params.stiffness; + } + + if motor_params.damping != N::zero() { + let dvel = constraint.lin_jac.dot(&(body2.linvel - body1.linvel)) + + (constraint.ang_jac2.gdot(body2.angvel) - constraint.ang_jac1.gdot(body1.angvel)); + rhs_wo_bias += (dvel - motor_params.target_vel) * motor_params.damping; + } + + constraint.impulse_bounds = [-motor_params.max_impulse, motor_params.max_impulse]; + constraint.rhs = rhs_wo_bias; + constraint.rhs_wo_bias = rhs_wo_bias; + constraint + } + + pub fn lock_linear( + &self, + params: &IntegrationParameters, + joint_id: [JointIndex; LANES], + body1: &SolverBody, + body2: &SolverBody, + locked_axis: usize, + writeback_id: WritebackId, + ) -> JointVelocityConstraint { + let lin_jac = self.basis.column(locked_axis).into_owned(); + #[cfg(feature = "dim2")] + let mut ang_jac1 = self.cmat1_basis[locked_axis]; + #[cfg(feature = "dim2")] + let mut ang_jac2 = self.cmat2_basis[locked_axis]; + #[cfg(feature = "dim3")] + let mut ang_jac1 = self.cmat1_basis.column(locked_axis).into_owned(); + #[cfg(feature = "dim3")] + let mut ang_jac2 = self.cmat2_basis.column(locked_axis).into_owned(); + + let dvel = lin_jac.dot(&(body2.linvel - body1.linvel)) + + (ang_jac2.gdot(body2.angvel) - ang_jac1.gdot(body1.angvel)); + let rhs_wo_bias = dvel * N::splat(params.velocity_solve_fraction); + + let erp_inv_dt = params.erp_inv_dt(); + let rhs_bias = lin_jac.dot(&self.lin_err) * N::splat(erp_inv_dt); + + ang_jac1 = body1.sqrt_ii * ang_jac1; + ang_jac2 = body2.sqrt_ii * ang_jac2; + + JointVelocityConstraint { + joint_id, + mj_lambda1: body1.mj_lambda, + mj_lambda2: body2.mj_lambda, + im1: body1.im, + im2: body2.im, + impulse: N::zero(), + impulse_bounds: [-N::splat(Real::MAX), N::splat(Real::MAX)], + lin_jac, + ang_jac1, + ang_jac2, + inv_lhs: N::zero(), // Will be set during ortogonalization. + rhs: rhs_wo_bias + rhs_bias, + rhs_wo_bias, + writeback_id, + } + } + + pub fn limit_angular( + &self, + params: &IntegrationParameters, + joint_id: [JointIndex; LANES], + body1: &SolverBody, + body2: &SolverBody, + limited_axis: usize, + limits: [N; 2], + writeback_id: WritebackId, + ) -> JointVelocityConstraint { + let zero = N::zero(); + let half = N::splat(0.5); + let s_limits = [(limits[0] * half).simd_sin(), (limits[1] * half).simd_sin()]; + #[cfg(feature = "dim2")] + let s_ang = self.ang_err.im; + #[cfg(feature = "dim3")] + let s_ang = self.ang_err.imag()[limited_axis]; + let min_enabled = s_ang.simd_lt(s_limits[0]); + let max_enabled = s_limits[1].simd_lt(s_ang); + + let impulse_bounds = [ + N::splat(-Real::INFINITY).select(min_enabled, zero), + N::splat(Real::INFINITY).select(max_enabled, zero), + ]; + + #[cfg(feature = "dim2")] + let ang_jac = self.ang_basis[limited_axis]; + #[cfg(feature = "dim3")] + let ang_jac = self.ang_basis.column(limited_axis).into_owned(); + let dvel = ang_jac.gdot(body2.angvel) - ang_jac.gdot(body1.angvel); + let rhs_wo_bias = dvel * N::splat(params.velocity_solve_fraction); + + let erp_inv_dt = params.erp_inv_dt(); + let rhs_bias = ((s_ang - s_limits[1]).simd_max(zero) + - (s_limits[0] - s_ang).simd_max(zero)) + * N::splat(erp_inv_dt); + + let ang_jac1 = body1.sqrt_ii * ang_jac; + let ang_jac2 = body2.sqrt_ii * ang_jac; + + JointVelocityConstraint { + joint_id, + mj_lambda1: body1.mj_lambda, + mj_lambda2: body2.mj_lambda, + im1: body1.im, + im2: body2.im, + impulse: N::zero(), + impulse_bounds, + lin_jac: na::zero(), + ang_jac1, + ang_jac2, + inv_lhs: N::zero(), // Will be set during ortogonalization. + rhs: rhs_wo_bias + rhs_bias, + rhs_wo_bias, + writeback_id, + } + } + + pub fn motor_angular( + &self, + joint_id: [JointIndex; LANES], + body1: &SolverBody, + body2: &SolverBody, + _motor_axis: usize, + motor_params: &MotorParameters, + writeback_id: WritebackId, + ) -> JointVelocityConstraint { + // let mut ang_jac = self.ang_basis.column(_motor_axis).into_owned(); + #[cfg(feature = "dim2")] + let ang_jac = N::one(); + #[cfg(feature = "dim3")] + let ang_jac = self.basis.column(_motor_axis).into_owned(); + + let mut rhs_wo_bias = N::zero(); + if motor_params.stiffness != N::zero() { + #[cfg(feature = "dim2")] + let s_ang_dist = self.ang_err.im; + #[cfg(feature = "dim3")] + let s_ang_dist = self.ang_err.imag()[_motor_axis]; + let s_target_ang = motor_params.target_pos.simd_sin(); + rhs_wo_bias += (s_ang_dist - s_target_ang) * motor_params.stiffness; + } + + if motor_params.damping != N::zero() { + let dvel = ang_jac.gdot(body2.angvel) - ang_jac.gdot(body1.angvel); + rhs_wo_bias += + (dvel - motor_params.target_vel/* * ang_jac.norm() */) * motor_params.damping; + } + + let ang_jac1 = body1.sqrt_ii * ang_jac; + let ang_jac2 = body2.sqrt_ii * ang_jac; + + JointVelocityConstraint { + joint_id, + mj_lambda1: body1.mj_lambda, + mj_lambda2: body2.mj_lambda, + im1: body1.im, + im2: body2.im, + impulse: N::zero(), + impulse_bounds: [-motor_params.max_impulse, motor_params.max_impulse], + lin_jac: na::zero(), + ang_jac1, + ang_jac2, + inv_lhs: N::zero(), // Will be set during ortogonalization. + rhs: rhs_wo_bias, + rhs_wo_bias, + writeback_id, + } + } + + pub fn lock_angular( + &self, + params: &IntegrationParameters, + joint_id: [JointIndex; LANES], + body1: &SolverBody, + body2: &SolverBody, + locked_axis: usize, + writeback_id: WritebackId, + ) -> JointVelocityConstraint { + #[cfg(feature = "dim2")] + let ang_jac = self.ang_basis[locked_axis]; + #[cfg(feature = "dim3")] + let ang_jac = self.ang_basis.column(locked_axis).into_owned(); + + let dvel = ang_jac.gdot(body2.angvel) - ang_jac.gdot(body1.angvel); + let rhs_wo_bias = dvel * N::splat(params.velocity_solve_fraction); + + let erp_inv_dt = params.erp_inv_dt(); + #[cfg(feature = "dim2")] + let rhs_bias = self.ang_err.im * N::splat(erp_inv_dt); + #[cfg(feature = "dim3")] + let rhs_bias = self.ang_err.imag()[locked_axis] * N::splat(erp_inv_dt); + + let ang_jac1 = body1.sqrt_ii * ang_jac; + let ang_jac2 = body2.sqrt_ii * ang_jac; + + JointVelocityConstraint { + joint_id, + mj_lambda1: body1.mj_lambda, + mj_lambda2: body2.mj_lambda, + im1: body1.im, + im2: body2.im, + impulse: N::zero(), + impulse_bounds: [-N::splat(Real::MAX), N::splat(Real::MAX)], + lin_jac: na::zero(), + ang_jac1, + ang_jac2, + inv_lhs: N::zero(), // Will be set during ortogonalization. + rhs: rhs_wo_bias + rhs_bias, + rhs_wo_bias, + writeback_id, + } + } + + /// Orthogonalize the constraints and set their inv_lhs field. + pub fn finalize_constraints( + constraints: &mut [JointVelocityConstraint], + ) { + let len = constraints.len(); + let imsum = constraints[0].im1 + constraints[0].im2; + + // Use the modified Gram-Schmidt orthogonalization. + for j in 0..len { + let c_j = &mut constraints[j]; + let dot_jj = c_j.lin_jac.norm_squared() * imsum + + c_j.ang_jac1.gdot(c_j.ang_jac1) + + c_j.ang_jac2.gdot(c_j.ang_jac2); + let inv_dot_jj = crate::utils::simd_inv(dot_jj); + c_j.inv_lhs = inv_dot_jj; // Don’t forget to update the inv_lhs. + + if c_j.impulse_bounds != [-N::splat(Real::MAX), N::splat(Real::MAX)] { + // Don't remove constraints with limited forces from the others + // because they may not deliver the necessary forces to fulfill + // the removed parts of other constraints. + continue; + } + + for i in (j + 1)..len { + let (c_i, c_j) = constraints.index_mut_const(i, j); + let dot_ij = c_i.lin_jac.dot(&c_j.lin_jac) * imsum + + c_i.ang_jac1.gdot(c_j.ang_jac1) + + c_i.ang_jac2.gdot(c_j.ang_jac2); + let coeff = dot_ij * inv_dot_jj; + + c_i.lin_jac -= c_j.lin_jac * coeff; + c_i.ang_jac1 -= c_j.ang_jac1 * coeff; + c_i.ang_jac2 -= c_j.ang_jac2 * coeff; + c_i.rhs_wo_bias -= c_j.rhs_wo_bias * coeff; + c_i.rhs -= c_j.rhs * coeff; + } + } + } + + pub fn limit_linear_ground( + &self, + params: &IntegrationParameters, + joint_id: [JointIndex; LANES], + body1: &SolverBody, + body2: &SolverBody, + limited_axis: usize, + limits: [N; 2], + writeback_id: WritebackId, + ) -> JointVelocityGroundConstraint { + let zero = N::zero(); + let lin_jac = self.basis.column(limited_axis).into_owned(); + let dist = self.lin_err.dot(&lin_jac); + let min_enabled = dist.simd_lt(limits[0]); + let max_enabled = limits[1].simd_lt(dist); + + let impulse_bounds = [ + N::splat(-Real::INFINITY).select(min_enabled, zero), + N::splat(Real::INFINITY).select(max_enabled, zero), + ]; + + let ang_jac1 = self.cmat1_basis.column(limited_axis).into_owned(); + #[cfg(feature = "dim2")] + let mut ang_jac2 = self.cmat2_basis[limited_axis]; + #[cfg(feature = "dim3")] + let mut ang_jac2 = self.cmat2_basis.column(limited_axis).into_owned(); + + let dvel = lin_jac.dot(&(body2.linvel - body1.linvel)) + + (ang_jac2.gdot(body2.angvel) - ang_jac1.gdot(body1.angvel)); + let rhs_wo_bias = dvel * N::splat(params.velocity_solve_fraction); + + let erp_inv_dt = params.erp_inv_dt(); + let rhs_bias = ((dist - limits[1]).simd_max(zero) - (limits[0] - dist).simd_max(zero)) + * N::splat(erp_inv_dt); + + ang_jac2 = body2.sqrt_ii * ang_jac2; + + JointVelocityGroundConstraint { + joint_id, + mj_lambda2: body2.mj_lambda, + im2: body2.im, + impulse: zero, + impulse_bounds, + lin_jac, + ang_jac2, + inv_lhs: zero, // Will be set during ortogonalization. + rhs: rhs_wo_bias + rhs_bias, + rhs_wo_bias, + writeback_id, + } + } + + pub fn motor_linear_ground( + &self, + joint_id: [JointIndex; LANES], + body1: &SolverBody, + body2: &SolverBody, + motor_axis: usize, + motor_params: &MotorParameters, + writeback_id: WritebackId, + ) -> JointVelocityGroundConstraint { + let lin_jac = self.basis.column(motor_axis).into_owned(); + let ang_jac1 = self.cmat1_basis.column(motor_axis).into_owned(); + #[cfg(feature = "dim2")] + let mut ang_jac2 = self.cmat2_basis[motor_axis]; + #[cfg(feature = "dim3")] + let mut ang_jac2 = self.cmat2_basis.column(motor_axis).into_owned(); + + let mut rhs_wo_bias = N::zero(); + if motor_params.stiffness != N::zero() { + let dist = self.lin_err.dot(&lin_jac); + rhs_wo_bias += (dist - motor_params.target_pos) * motor_params.stiffness; + } + + if motor_params.damping != N::zero() { + let dvel = lin_jac.dot(&(body2.linvel - body1.linvel)) + + (ang_jac2.gdot(body2.angvel) - ang_jac1.gdot(body1.angvel)); + rhs_wo_bias += (dvel - motor_params.target_vel) * motor_params.damping; + } + + ang_jac2 = body2.sqrt_ii * ang_jac2; + + JointVelocityGroundConstraint { + joint_id, + mj_lambda2: body2.mj_lambda, + im2: body2.im, + impulse: N::zero(), + impulse_bounds: [-motor_params.max_impulse, motor_params.max_impulse], + lin_jac, + ang_jac2, + inv_lhs: N::zero(), // Will be set during ortogonalization. + rhs: rhs_wo_bias, + rhs_wo_bias, + writeback_id, + } + } + + pub fn lock_linear_ground( + &self, + params: &IntegrationParameters, + joint_id: [JointIndex; LANES], + body1: &SolverBody, + body2: &SolverBody, + locked_axis: usize, + writeback_id: WritebackId, + ) -> JointVelocityGroundConstraint { + let lin_jac = self.basis.column(locked_axis).into_owned(); + let ang_jac1 = self.cmat1_basis.column(locked_axis).into_owned(); + #[cfg(feature = "dim2")] + let mut ang_jac2 = self.cmat2_basis[locked_axis]; + #[cfg(feature = "dim3")] + let mut ang_jac2 = self.cmat2_basis.column(locked_axis).into_owned(); + + let dvel = lin_jac.dot(&(body2.linvel - body1.linvel)) + + (ang_jac2.gdot(body2.angvel) - ang_jac1.gdot(body1.angvel)); + let rhs_wo_bias = dvel * N::splat(params.velocity_solve_fraction); + + let erp_inv_dt = params.erp_inv_dt(); + let rhs_bias = lin_jac.dot(&self.lin_err) * N::splat(erp_inv_dt); + + ang_jac2 = body2.sqrt_ii * ang_jac2; + + JointVelocityGroundConstraint { + joint_id, + mj_lambda2: body2.mj_lambda, + im2: body2.im, + impulse: N::zero(), + impulse_bounds: [-N::splat(Real::MAX), N::splat(Real::MAX)], + lin_jac, + ang_jac2, + inv_lhs: N::zero(), // Will be set during ortogonalization. + rhs: rhs_wo_bias + rhs_bias, + rhs_wo_bias, + writeback_id, + } + } + + pub fn motor_angular_ground( + &self, + joint_id: [JointIndex; LANES], + body1: &SolverBody, + body2: &SolverBody, + _motor_axis: usize, + motor_params: &MotorParameters, + writeback_id: WritebackId, + ) -> JointVelocityGroundConstraint { + // let mut ang_jac = self.ang_basis.column(_motor_axis).into_owned(); + #[cfg(feature = "dim2")] + let ang_jac = N::one(); + #[cfg(feature = "dim3")] + let ang_jac = self.basis.column(_motor_axis).into_owned(); + + let mut rhs_wo_bias = N::zero(); + if motor_params.stiffness != N::zero() { + #[cfg(feature = "dim2")] + let s_ang_dist = self.ang_err.im; + #[cfg(feature = "dim3")] + let s_ang_dist = self.ang_err.imag()[_motor_axis]; + let s_target_ang = motor_params.target_pos.simd_sin(); + rhs_wo_bias += (s_ang_dist - s_target_ang) * motor_params.stiffness; + } + + if motor_params.damping != N::zero() { + let dvel = ang_jac.gdot(body2.angvel) - ang_jac.gdot(body1.angvel); + rhs_wo_bias += + (dvel - motor_params.target_vel/* * ang_jac.norm() */) * motor_params.damping; + } + + let ang_jac2 = body2.sqrt_ii * ang_jac; + + JointVelocityGroundConstraint { + joint_id, + mj_lambda2: body2.mj_lambda, + im2: body2.im, + impulse: N::zero(), + impulse_bounds: [-motor_params.max_impulse, motor_params.max_impulse], + lin_jac: na::zero(), + ang_jac2, + inv_lhs: N::zero(), // Will be set during ortogonalization. + rhs: rhs_wo_bias, + rhs_wo_bias, + writeback_id, + } + } + + pub fn limit_angular_ground( + &self, + params: &IntegrationParameters, + joint_id: [JointIndex; LANES], + body1: &SolverBody, + body2: &SolverBody, + limited_axis: usize, + limits: [N; 2], + writeback_id: WritebackId, + ) -> JointVelocityGroundConstraint { + let zero = N::zero(); + let half = N::splat(0.5); + let s_limits = [(limits[0] * half).simd_sin(), (limits[1] * half).simd_sin()]; + #[cfg(feature = "dim2")] + let s_ang = self.ang_err.im; + #[cfg(feature = "dim3")] + let s_ang = self.ang_err.imag()[limited_axis]; + let min_enabled = s_ang.simd_lt(s_limits[0]); + let max_enabled = s_limits[1].simd_lt(s_ang); + + let impulse_bounds = [ + N::splat(-Real::INFINITY).select(min_enabled, zero), + N::splat(Real::INFINITY).select(max_enabled, zero), + ]; + + #[cfg(feature = "dim2")] + let ang_jac = self.ang_basis[limited_axis]; + #[cfg(feature = "dim3")] + let ang_jac = self.ang_basis.column(limited_axis).into_owned(); + let dvel = ang_jac.gdot(body2.angvel) - ang_jac.gdot(body1.angvel); + let rhs_wo_bias = dvel * N::splat(params.velocity_solve_fraction); + + let erp_inv_dt = params.erp_inv_dt(); + let rhs_bias = ((s_ang - s_limits[1]).simd_max(zero) + - (s_limits[0] - s_ang).simd_max(zero)) + * N::splat(erp_inv_dt); + + let ang_jac2 = body2.sqrt_ii * ang_jac; + + JointVelocityGroundConstraint { + joint_id, + mj_lambda2: body2.mj_lambda, + im2: body2.im, + impulse: zero, + impulse_bounds, + lin_jac: na::zero(), + ang_jac2, + inv_lhs: zero, // Will be set during ortogonalization. + rhs: rhs_wo_bias + rhs_bias, + rhs_wo_bias, + writeback_id, + } + } + + pub fn lock_angular_ground( + &self, + params: &IntegrationParameters, + joint_id: [JointIndex; LANES], + body1: &SolverBody, + body2: &SolverBody, + locked_axis: usize, + writeback_id: WritebackId, + ) -> JointVelocityGroundConstraint { + #[cfg(feature = "dim2")] + let ang_jac = self.ang_basis[locked_axis]; + #[cfg(feature = "dim3")] + let ang_jac = self.ang_basis.column(locked_axis).into_owned(); + let dvel = ang_jac.gdot(body2.angvel) - ang_jac.gdot(body1.angvel); + let rhs_wo_bias = dvel * N::splat(params.velocity_solve_fraction); + + let erp_inv_dt = params.erp_inv_dt(); + #[cfg(feature = "dim2")] + let rhs_bias = self.ang_err.im * N::splat(erp_inv_dt); + #[cfg(feature = "dim3")] + let rhs_bias = self.ang_err.imag()[locked_axis] * N::splat(erp_inv_dt); + + let ang_jac2 = body2.sqrt_ii * ang_jac; + + JointVelocityGroundConstraint { + joint_id, + mj_lambda2: body2.mj_lambda, + im2: body2.im, + impulse: N::zero(), + impulse_bounds: [-N::splat(Real::MAX), N::splat(Real::MAX)], + lin_jac: na::zero(), + ang_jac2, + inv_lhs: N::zero(), // Will be set during ortogonalization. + rhs: rhs_wo_bias + rhs_bias, + rhs_wo_bias, + writeback_id, + } + } + + /// Orthogonalize the constraints and set their inv_lhs field. + pub fn finalize_ground_constraints( + constraints: &mut [JointVelocityGroundConstraint], + ) { + let len = constraints.len(); + let imsum = constraints[0].im2; + + // Use the modified Gram-Schmidt orthogonalization. + for j in 0..len { + let c_j = &mut constraints[j]; + let dot_jj = c_j.lin_jac.norm_squared() * imsum + c_j.ang_jac2.gdot(c_j.ang_jac2); + let inv_dot_jj = crate::utils::simd_inv(dot_jj); + c_j.inv_lhs = inv_dot_jj; // Don’t forget to update the inv_lhs. + + if c_j.impulse_bounds != [-N::splat(Real::MAX), N::splat(Real::MAX)] { + // Don't remove constraints with limited forces from the others + // because they may not deliver the necessary forces to fulfill + // the removed parts of other constraints. + continue; + } + + for i in (j + 1)..len { + let (c_i, c_j) = constraints.index_mut_const(i, j); + let dot_ij = + c_i.lin_jac.dot(&c_j.lin_jac) * imsum + c_i.ang_jac2.gdot(c_j.ang_jac2); + let coeff = dot_ij * inv_dot_jj; + + c_i.lin_jac -= c_j.lin_jac * coeff; + c_i.ang_jac2 -= c_j.ang_jac2 * coeff; + c_i.rhs_wo_bias -= c_j.rhs_wo_bias * coeff; + c_i.rhs -= c_j.rhs * coeff; + } + } + } +} diff --git a/src/dynamics/solver/joint_constraint/mod.rs b/src/dynamics/solver/joint_constraint/mod.rs index 9196e69..503bd81 100644 --- a/src/dynamics/solver/joint_constraint/mod.rs +++ b/src/dynamics/solver/joint_constraint/mod.rs @@ -1,102 +1,13 @@ -pub(self) use ball_position_constraint::{BallPositionConstraint, BallPositionGroundConstraint}; -#[cfg(feature = "simd-is-enabled")] -pub(self) use ball_position_constraint_wide::{ - WBallPositionConstraint, WBallPositionGroundConstraint, -}; -pub(self) use ball_velocity_constraint::{BallVelocityConstraint, BallVelocityGroundConstraint}; -#[cfg(feature = "simd-is-enabled")] -pub(self) use ball_velocity_constraint_wide::{ - WBallVelocityConstraint, WBallVelocityGroundConstraint, -}; -pub(self) use fixed_position_constraint::{FixedPositionConstraint, FixedPositionGroundConstraint}; -#[cfg(feature = "simd-is-enabled")] -pub(self) use fixed_position_constraint_wide::{ - WFixedPositionConstraint, WFixedPositionGroundConstraint, -}; -pub(self) use fixed_velocity_constraint::{FixedVelocityConstraint, FixedVelocityGroundConstraint}; -#[cfg(feature = "simd-is-enabled")] -pub(self) use fixed_velocity_constraint_wide::{ - WFixedVelocityConstraint, WFixedVelocityGroundConstraint, -}; -// pub(self) use generic_position_constraint::{ -// GenericPositionConstraint, GenericPositionGroundConstraint, -// }; -// #[cfg(feature = "simd-is-enabled")] -// pub(self) use generic_position_constraint_wide::{ -// WGenericPositionConstraint, WGenericPositionGroundConstraint, -// }; -// pub(self) use generic_velocity_constraint::{ -// GenericVelocityConstraint, GenericVelocityGroundConstraint, -// }; -// #[cfg(feature = "simd-is-enabled")] -// pub(self) use generic_velocity_constraint_wide::{ -// WGenericVelocityConstraint, WGenericVelocityGroundConstraint, -// }; +pub use joint_velocity_constraint::{MotorParameters, SolverBody, WritebackId}; -pub(crate) use joint_constraint::AnyJointVelocityConstraint; -pub(crate) use joint_position_constraint::AnyJointPositionConstraint; -pub(self) use prismatic_position_constraint::{ - PrismaticPositionConstraint, PrismaticPositionGroundConstraint, -}; -#[cfg(feature = "simd-is-enabled")] -pub(self) use prismatic_position_constraint_wide::{ - WPrismaticPositionConstraint, WPrismaticPositionGroundConstraint, -}; -pub(self) use prismatic_velocity_constraint::{ - PrismaticVelocityConstraint, PrismaticVelocityGroundConstraint, -}; -#[cfg(feature = "simd-is-enabled")] -pub(self) use prismatic_velocity_constraint_wide::{ - WPrismaticVelocityConstraint, WPrismaticVelocityGroundConstraint, -}; -#[cfg(feature = "dim3")] -pub(self) use revolute_position_constraint::{ - RevolutePositionConstraint, RevolutePositionGroundConstraint, -}; -#[cfg(all(feature = "dim3", feature = "simd-is-enabled"))] -pub(self) use revolute_position_constraint_wide::{ - WRevolutePositionConstraint, WRevolutePositionGroundConstraint, -}; -#[cfg(feature = "dim3")] -pub(self) use revolute_velocity_constraint::{ - RevoluteVelocityConstraint, RevoluteVelocityGroundConstraint, -}; -#[cfg(all(feature = "dim3", feature = "simd-is-enabled"))] -pub(self) use revolute_velocity_constraint_wide::{ - WRevoluteVelocityConstraint, WRevoluteVelocityGroundConstraint, +pub use joint_constraint::AnyJointVelocityConstraint; +pub use joint_generic_velocity_constraint::{ + JointGenericVelocityConstraint, JointGenericVelocityGroundConstraint, }; +pub use joint_velocity_constraint_builder::JointVelocityConstraintBuilder; -mod ball_position_constraint; -#[cfg(feature = "simd-is-enabled")] -mod ball_position_constraint_wide; -mod ball_velocity_constraint; -#[cfg(feature = "simd-is-enabled")] -mod ball_velocity_constraint_wide; -mod fixed_position_constraint; -#[cfg(feature = "simd-is-enabled")] -mod fixed_position_constraint_wide; -mod fixed_velocity_constraint; -#[cfg(feature = "simd-is-enabled")] -mod fixed_velocity_constraint_wide; -// mod generic_position_constraint; -// #[cfg(feature = "simd-is-enabled")] -// mod generic_position_constraint_wide; -// mod generic_velocity_constraint; -// #[cfg(feature = "simd-is-enabled")] -// mod generic_velocity_constraint_wide; mod joint_constraint; -mod joint_position_constraint; -mod prismatic_position_constraint; -#[cfg(feature = "simd-is-enabled")] -mod prismatic_position_constraint_wide; -mod prismatic_velocity_constraint; -#[cfg(feature = "simd-is-enabled")] -mod prismatic_velocity_constraint_wide; -#[cfg(feature = "dim3")] -mod revolute_position_constraint; -#[cfg(all(feature = "dim3", feature = "simd-is-enabled"))] -mod revolute_position_constraint_wide; -#[cfg(feature = "dim3")] -mod revolute_velocity_constraint; -#[cfg(all(feature = "dim3", feature = "simd-is-enabled"))] -mod revolute_velocity_constraint_wide; +mod joint_generic_velocity_constraint; +mod joint_generic_velocity_constraint_builder; +mod joint_velocity_constraint; +mod joint_velocity_constraint_builder; diff --git a/src/dynamics/solver/joint_constraint/prismatic_position_constraint.rs b/src/dynamics/solver/joint_constraint/prismatic_position_constraint.rs deleted file mode 100644 index d68f7ef..0000000 --- a/src/dynamics/solver/joint_constraint/prismatic_position_constraint.rs +++ /dev/null @@ -1,182 +0,0 @@ -use crate::dynamics::{ - IntegrationParameters, PrismaticJoint, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition, -}; -use crate::math::{AngularInertia, Isometry, Point, Real, Rotation, Vector}; -use crate::utils::WAngularInertia; -use na::Unit; - -#[derive(Debug)] -pub(crate) struct PrismaticPositionConstraint { - position1: usize, - position2: usize, - - im1: Real, - im2: Real, - - ii1: AngularInertia, - ii2: AngularInertia, - - lin_inv_lhs: Real, - ang_inv_lhs: AngularInertia, - - limits: [Real; 2], - - local_frame1: Isometry, - local_frame2: Isometry, - - local_axis1: Unit>, - local_axis2: Unit>, -} - -impl PrismaticPositionConstraint { - pub fn from_params( - rb1: (&RigidBodyMassProps, &RigidBodyIds), - rb2: (&RigidBodyMassProps, &RigidBodyIds), - cparams: &PrismaticJoint, - ) -> Self { - let (mprops1, ids1) = rb1; - let (mprops2, ids2) = rb2; - - let ii1 = mprops1.effective_world_inv_inertia_sqrt.squared(); - let ii2 = mprops2.effective_world_inv_inertia_sqrt.squared(); - let im1 = mprops1.effective_inv_mass; - let im2 = mprops2.effective_inv_mass; - let lin_inv_lhs = 1.0 / (im1 + im2); - let ang_inv_lhs = (ii1 + ii2).inverse(); - - Self { - im1, - im2, - ii1, - ii2, - lin_inv_lhs, - ang_inv_lhs, - local_frame1: cparams.local_frame1(), - local_frame2: cparams.local_frame2(), - local_axis1: cparams.local_axis1, - local_axis2: cparams.local_axis2, - position1: ids1.active_set_offset, - position2: ids2.active_set_offset, - limits: cparams.limits, - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - let mut position1 = positions[self.position1 as usize]; - let mut position2 = positions[self.position2 as usize]; - - // Angular correction. - let frame1 = position1 * self.local_frame1; - let frame2 = position2 * self.local_frame2; - let ang_err = frame2.rotation * frame1.rotation.inverse(); - #[cfg(feature = "dim2")] - let ang_impulse = self - .ang_inv_lhs - .transform_vector(ang_err.angle() * params.joint_erp); - #[cfg(feature = "dim3")] - let ang_impulse = self - .ang_inv_lhs - .transform_vector(ang_err.scaled_axis() * params.joint_erp); - position1.rotation = - Rotation::new(self.ii1.transform_vector(ang_impulse)) * position1.rotation; - position2.rotation = - Rotation::new(self.ii2.transform_vector(-ang_impulse)) * position2.rotation; - - // Linear correction. - let anchor1 = position1 * Point::from(self.local_frame1.translation.vector); - let anchor2 = position2 * Point::from(self.local_frame2.translation.vector); - let axis1 = position1 * self.local_axis1; - let dpos = anchor2 - anchor1; - let limit_err = dpos.dot(&axis1); - let mut err = dpos - *axis1 * limit_err; - - if limit_err < self.limits[0] { - err += *axis1 * (limit_err - self.limits[0]); - } else if limit_err > self.limits[1] { - err += *axis1 * (limit_err - self.limits[1]); - } - - let impulse = err * (self.lin_inv_lhs * params.joint_erp); - position1.translation.vector += self.im1 * impulse; - position2.translation.vector -= self.im2 * impulse; - - positions[self.position1 as usize] = position1; - positions[self.position2 as usize] = position2; - } -} - -#[derive(Debug)] -pub(crate) struct PrismaticPositionGroundConstraint { - position2: usize, - frame1: Isometry, - local_frame2: Isometry, - axis1: Unit>, - local_axis2: Unit>, - limits: [Real; 2], -} - -impl PrismaticPositionGroundConstraint { - pub fn from_params( - rb1: &RigidBodyPosition, - rb2: (&RigidBodyMassProps, &RigidBodyIds), - cparams: &PrismaticJoint, - flipped: bool, - ) -> Self { - let poss1 = rb1; - let (_, ids2) = rb2; - - let frame1; - let local_frame2; - let axis1; - let local_axis2; - - if flipped { - frame1 = poss1.next_position * cparams.local_frame2(); - local_frame2 = cparams.local_frame1(); - axis1 = poss1.next_position * cparams.local_axis2; - local_axis2 = cparams.local_axis1; - } else { - frame1 = poss1.next_position * cparams.local_frame1(); - local_frame2 = cparams.local_frame2(); - axis1 = poss1.next_position * cparams.local_axis1; - local_axis2 = cparams.local_axis2; - }; - - Self { - frame1, - local_frame2, - axis1, - local_axis2, - position2: ids2.active_set_offset, - limits: cparams.limits, - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - let mut position2 = positions[self.position2 as usize]; - - // Angular correction. - let frame2 = position2 * self.local_frame2; - let ang_err = frame2.rotation * self.frame1.rotation.inverse(); - position2.rotation = ang_err.powf(-params.joint_erp) * position2.rotation; - - // Linear correction. - let anchor1 = Point::from(self.frame1.translation.vector); - let anchor2 = position2 * Point::from(self.local_frame2.translation.vector); - let dpos = anchor2 - anchor1; - let limit_err = dpos.dot(&self.axis1); - let mut err = dpos - *self.axis1 * limit_err; - - if limit_err < self.limits[0] { - err += *self.axis1 * (limit_err - self.limits[0]); - } else if limit_err > self.limits[1] { - err += *self.axis1 * (limit_err - self.limits[1]); - } - - // NOTE: no need to divide by im2 just to multiply right after. - let impulse = err * params.joint_erp; - position2.translation.vector -= impulse; - - positions[self.position2 as usize] = position2; - } -} diff --git a/src/dynamics/solver/joint_constraint/prismatic_position_constraint_wide.rs b/src/dynamics/solver/joint_constraint/prismatic_position_constraint_wide.rs deleted file mode 100644 index 95c1bcb..0000000 --- a/src/dynamics/solver/joint_constraint/prismatic_position_constraint_wide.rs +++ /dev/null @@ -1,71 +0,0 @@ -use super::{PrismaticPositionConstraint, PrismaticPositionGroundConstraint}; -use crate::dynamics::{ - IntegrationParameters, PrismaticJoint, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition, -}; -use crate::math::{Isometry, Real, SIMD_WIDTH}; - -// TODO: this does not uses SIMD optimizations yet. -#[derive(Debug)] -pub(crate) struct WPrismaticPositionConstraint { - constraints: [PrismaticPositionConstraint; SIMD_WIDTH], -} - -impl WPrismaticPositionConstraint { - pub fn from_params( - rbs1: ( - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - rbs2: ( - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - cparams: [&PrismaticJoint; SIMD_WIDTH], - ) -> Self { - Self { - constraints: gather![|ii| PrismaticPositionConstraint::from_params( - (rbs1.0[ii], rbs1.1[ii]), - (rbs2.0[ii], rbs2.1[ii]), - cparams[ii] - )], - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - for constraint in &self.constraints { - constraint.solve(params, positions); - } - } -} - -#[derive(Debug)] -pub(crate) struct WPrismaticPositionGroundConstraint { - constraints: [PrismaticPositionGroundConstraint; SIMD_WIDTH], -} - -impl WPrismaticPositionGroundConstraint { - pub fn from_params( - rbs1: [&RigidBodyPosition; SIMD_WIDTH], - rbs2: ( - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - cparams: [&PrismaticJoint; SIMD_WIDTH], - flipped: [bool; SIMD_WIDTH], - ) -> Self { - Self { - constraints: gather![|ii| PrismaticPositionGroundConstraint::from_params( - rbs1[ii], - (rbs2.0[ii], rbs2.1[ii]), - cparams[ii], - flipped[ii] - )], - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - for constraint in &self.constraints { - constraint.solve(params, positions); - } - } -} diff --git a/src/dynamics/solver/joint_constraint/prismatic_velocity_constraint.rs b/src/dynamics/solver/joint_constraint/prismatic_velocity_constraint.rs deleted file mode 100644 index 4e94c79..0000000 --- a/src/dynamics/solver/joint_constraint/prismatic_velocity_constraint.rs +++ /dev/null @@ -1,859 +0,0 @@ -use crate::dynamics::solver::DeltaVel; -use crate::dynamics::{ - IntegrationParameters, JointGraphEdge, JointIndex, JointParams, PrismaticJoint, RigidBodyIds, - RigidBodyMassProps, RigidBodyPosition, RigidBodyVelocity, -}; -use crate::math::{AngularInertia, Real, Vector}; -use crate::utils::{WAngularInertia, WCross, WCrossMatrix, WDot}; -#[cfg(feature = "dim3")] -use na::{Cholesky, Matrix3x2, Matrix5, Vector5}; -#[cfg(feature = "dim2")] -use { - na::{Matrix2, Vector2}, - parry::utils::SdpMatrix2, -}; - -#[cfg(feature = "dim2")] -const LIN_IMPULSE_DIM: usize = 1; -#[cfg(feature = "dim3")] -const LIN_IMPULSE_DIM: usize = 2; - -#[derive(Debug)] -pub(crate) struct PrismaticVelocityConstraint { - mj_lambda1: usize, - mj_lambda2: usize, - - joint_id: JointIndex, - - r1: Vector, - r2: Vector, - - #[cfg(feature = "dim3")] - inv_lhs: Matrix5, - #[cfg(feature = "dim3")] - rhs: Vector5, - #[cfg(feature = "dim3")] - impulse: Vector5, - - #[cfg(feature = "dim2")] - inv_lhs: Matrix2, - #[cfg(feature = "dim2")] - rhs: Vector2, - #[cfg(feature = "dim2")] - impulse: Vector2, - - motor_axis1: Vector, - motor_axis2: Vector, - motor_impulse: Real, - motor_rhs: Real, - motor_inv_lhs: Real, - motor_max_impulse: Real, - - limits_active: bool, - limits_impulse: Real, - /// World-coordinate direction of the limit force on rb2. - /// The force direction on rb1 is opposite (Newton's third law).. - limits_forcedir2: Vector, - limits_rhs: Real, - limits_inv_lhs: Real, - /// min/max applied impulse due to limits - limits_impulse_limits: (Real, Real), - - #[cfg(feature = "dim2")] - basis1: Vector2, - #[cfg(feature = "dim3")] - basis1: Matrix3x2, - - im1: Real, - im2: Real, - - ii1_sqrt: AngularInertia, - ii2_sqrt: AngularInertia, -} - -impl PrismaticVelocityConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: JointIndex, - rb1: ( - &RigidBodyPosition, - &RigidBodyVelocity, - &RigidBodyMassProps, - &RigidBodyIds, - ), - rb2: ( - &RigidBodyPosition, - &RigidBodyVelocity, - &RigidBodyMassProps, - &RigidBodyIds, - ), - joint: &PrismaticJoint, - ) -> Self { - let (poss1, vels1, mprops1, ids1) = rb1; - let (poss2, vels2, mprops2, ids2) = rb2; - - // Linear part. - let anchor1 = poss1.position * joint.local_anchor1; - let anchor2 = poss2.position * joint.local_anchor2; - let axis1 = poss1.position * joint.local_axis1; - let axis2 = poss2.position * joint.local_axis2; - - #[cfg(feature = "dim2")] - let basis1 = poss1.position * joint.basis1[0]; - #[cfg(feature = "dim3")] - let basis1 = Matrix3x2::from_columns(&[ - poss1.position * joint.basis1[0], - poss1.position * joint.basis1[1], - ]); - - let im1 = mprops1.effective_inv_mass; - let ii1 = mprops1.effective_world_inv_inertia_sqrt.squared(); - let r1 = anchor1 - mprops1.world_com; - let r1_mat = r1.gcross_matrix(); - - let im2 = mprops2.effective_inv_mass; - let ii2 = mprops2.effective_world_inv_inertia_sqrt.squared(); - let r2 = anchor2 - mprops2.world_com; - let r2_mat = r2.gcross_matrix(); - - #[allow(unused_mut)] // For 2D. - let mut lhs; - - #[cfg(feature = "dim3")] - { - let r1_mat_b1 = r1_mat * basis1; - let r2_mat_b1 = r2_mat * basis1; - - lhs = Matrix5::zeros(); - let lhs00 = ii1.quadform3x2(&r1_mat_b1).add_diagonal(im1) - + ii2.quadform3x2(&r2_mat_b1).add_diagonal(im2); - let lhs10 = ii1 * r1_mat_b1 + ii2 * r2_mat_b1; - let lhs11 = (ii1 + ii2).into_matrix(); - lhs.fixed_slice_mut::<2, 2>(0, 0) - .copy_from(&lhs00.into_matrix()); - lhs.fixed_slice_mut::<3, 2>(2, 0).copy_from(&lhs10); - lhs.fixed_slice_mut::<3, 3>(2, 2).copy_from(&lhs11); - } - - #[cfg(feature = "dim2")] - { - let b1r1 = basis1.dot(&r1_mat); - let b2r2 = basis1.dot(&r2_mat); - let m11 = im1 + im2 + b1r1 * ii1 * b1r1 + b2r2 * ii2 * b2r2; - let m12 = basis1.dot(&r1_mat) * ii1 + basis1.dot(&r2_mat) * ii2; - let m22 = ii1 + ii2; - lhs = SdpMatrix2::new(m11, m12, m22); - } - - let anchor_linvel1 = vels1.linvel + vels1.angvel.gcross(r1); - let anchor_linvel2 = vels2.linvel + vels2.angvel.gcross(r2); - - // NOTE: we don't use Cholesky in 2D because we only have a 2x2 matrix - // for which a textbook inverse is still efficient. - #[cfg(feature = "dim2")] - let inv_lhs = lhs.inverse_unchecked().into_matrix(); - #[cfg(feature = "dim3")] - let inv_lhs = Cholesky::new_unchecked(lhs).inverse(); - - let linvel_err = basis1.tr_mul(&(anchor_linvel2 - anchor_linvel1)); - let angvel_err = vels2.angvel - vels1.angvel; - - #[cfg(feature = "dim2")] - let mut rhs = Vector2::new(linvel_err.x, angvel_err) * params.velocity_solve_fraction; - #[cfg(feature = "dim3")] - let mut rhs = Vector5::new( - linvel_err.x, - linvel_err.y, - angvel_err.x, - angvel_err.y, - angvel_err.z, - ) * params.velocity_solve_fraction; - - let velocity_based_erp_inv_dt = params.velocity_based_erp_inv_dt(); - if velocity_based_erp_inv_dt != 0.0 { - let linear_err = basis1.tr_mul(&(anchor2 - anchor1)); - - let frame1 = poss1.position * joint.local_frame1(); - let frame2 = poss2.position * joint.local_frame2(); - let ang_err = frame2.rotation * frame1.rotation.inverse(); - - #[cfg(feature = "dim2")] - { - rhs += Vector2::new(linear_err.x, ang_err.angle()) * velocity_based_erp_inv_dt; - } - #[cfg(feature = "dim3")] - { - let ang_err = ang_err.scaled_axis(); - rhs += Vector5::new(linear_err.x, linear_err.y, ang_err.x, ang_err.y, ang_err.z) - * velocity_based_erp_inv_dt; - } - } - - /* - * Setup motor. - */ - let mut motor_rhs = 0.0; - let mut motor_inv_lhs = 0.0; - let gcross1 = r1.gcross(*axis1); - let gcross2 = r2.gcross(*axis2); - - let (stiffness, damping, gamma, keep_lhs) = joint.motor_model.combine_coefficients( - params.dt, - joint.motor_stiffness, - joint.motor_damping, - ); - - if stiffness != 0.0 { - let dist = anchor2.coords.dot(&axis2) - anchor1.coords.dot(&axis1); - motor_rhs += (dist - joint.motor_target_pos) * stiffness; - } - - if damping != 0.0 { - let curr_vel = vels2.linvel.dot(&axis2) + vels2.angvel.gdot(gcross2) - - vels1.linvel.dot(&axis1) - - vels1.angvel.gdot(gcross1); - motor_rhs += (curr_vel - joint.motor_target_vel) * damping; - } - - if stiffness != 0.0 || damping != 0.0 { - motor_inv_lhs = if keep_lhs { - let inv_projected_mass = crate::utils::inv( - im1 + im2 - + gcross1.gdot(ii1.transform_vector(gcross1)) - + gcross2.gdot(ii2.transform_vector(gcross2)), - ); - - gamma * inv_projected_mass - } else { - gamma - }; - motor_rhs /= gamma; - } - - let motor_impulse = na::clamp( - joint.motor_impulse, - -joint.motor_max_impulse, - joint.motor_max_impulse, - ); - - // Setup limit constraint. - let mut limits_active = false; - let limits_forcedir2 = axis2.into_inner(); // hopefully axis1 is colinear with axis2 - let mut limits_rhs = 0.0; - let mut limits_impulse = 0.0; - let mut limits_inv_lhs = 0.0; - let mut limits_impulse_limits = (0.0, 0.0); - - if joint.limits_enabled { - let danchor = anchor2 - anchor1; - let dist = danchor.dot(&axis1); - - // TODO: we should allow predictive constraint activation. - - let (min_limit, max_limit) = (joint.limits[0], joint.limits[1]); - let min_enabled = dist < min_limit; - let max_enabled = max_limit < dist; - - limits_impulse_limits.0 = if max_enabled { -Real::INFINITY } else { 0.0 }; - limits_impulse_limits.1 = if min_enabled { Real::INFINITY } else { 0.0 }; - limits_active = min_enabled || max_enabled; - - if limits_active { - limits_rhs = (anchor_linvel2.dot(&axis2) - anchor_linvel1.dot(&axis1)) - * params.velocity_solve_fraction; - - limits_rhs += ((dist - max_limit).max(0.0) - (min_limit - dist).max(0.0)) - * velocity_based_erp_inv_dt; - - limits_inv_lhs = crate::utils::inv( - im1 + im2 - + gcross1.gdot(ii1.transform_vector(gcross1)) - + gcross2.gdot(ii2.transform_vector(gcross2)), - ); - - limits_impulse = joint - .limits_impulse - .max(limits_impulse_limits.0) - .min(limits_impulse_limits.1); - } - } - - PrismaticVelocityConstraint { - joint_id, - mj_lambda1: ids1.active_set_offset, - mj_lambda2: ids2.active_set_offset, - im1, - ii1_sqrt: mprops1.effective_world_inv_inertia_sqrt, - im2, - ii2_sqrt: mprops2.effective_world_inv_inertia_sqrt, - impulse: joint.impulse * params.warmstart_coeff, - limits_active, - limits_impulse: limits_impulse * params.warmstart_coeff, - limits_forcedir2, - limits_rhs, - limits_inv_lhs, - limits_impulse_limits, - motor_rhs, - motor_inv_lhs, - motor_impulse, - motor_axis1: *axis1, - motor_axis2: *axis2, - motor_max_impulse: joint.motor_max_impulse, - basis1, - inv_lhs, - rhs, - r1, - r2, - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = mj_lambdas[self.mj_lambda1 as usize]; - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - let lin_impulse = self.basis1 * self.impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = self.impulse.y; - #[cfg(feature = "dim3")] - let ang_impulse = self.impulse.fixed_rows::<3>(2).into_owned(); - - mj_lambda1.linear += self.im1 * lin_impulse; - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(ang_impulse + self.r1.gcross(lin_impulse)); - - mj_lambda2.linear -= self.im2 * lin_impulse; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - // Warmstart motors. - if self.motor_impulse != 0.0 { - let lin_impulse1 = self.motor_axis1 * self.motor_impulse; - let lin_impulse2 = self.motor_axis2 * self.motor_impulse; - - mj_lambda1.linear += lin_impulse1 * self.im1; - mj_lambda1.angular += self.ii1_sqrt.transform_vector(self.r1.gcross(lin_impulse1)); - - mj_lambda2.linear -= lin_impulse2 * self.im2; - mj_lambda2.angular -= self.ii2_sqrt.transform_vector(self.r2.gcross(lin_impulse2)); - } - - // Warmstart limits. - if self.limits_active { - let limit_impulse1 = -self.limits_forcedir2 * self.limits_impulse; - let limit_impulse2 = self.limits_forcedir2 * self.limits_impulse; - mj_lambda1.linear += self.im1 * limit_impulse1; - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(self.r1.gcross(limit_impulse1)); - mj_lambda2.linear += self.im2 * limit_impulse2; - mj_lambda2.angular += self - .ii2_sqrt - .transform_vector(self.r2.gcross(limit_impulse2)); - } - - mj_lambdas[self.mj_lambda1 as usize] = mj_lambda1; - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - fn solve_dofs(&mut self, mj_lambda1: &mut DeltaVel, mj_lambda2: &mut DeltaVel) { - let ang_vel1 = self.ii1_sqrt.transform_vector(mj_lambda1.angular); - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - let lin_vel1 = mj_lambda1.linear + ang_vel1.gcross(self.r1); - let lin_vel2 = mj_lambda2.linear + ang_vel2.gcross(self.r2); - let lin_dvel = self.basis1.tr_mul(&(lin_vel2 - lin_vel1)); - let ang_dvel = ang_vel2 - ang_vel1; - #[cfg(feature = "dim2")] - let rhs = Vector2::new(lin_dvel.x, ang_dvel) + self.rhs; - #[cfg(feature = "dim3")] - let rhs = - Vector5::new(lin_dvel.x, lin_dvel.y, ang_dvel.x, ang_dvel.y, ang_dvel.z) + self.rhs; - let impulse = self.inv_lhs * rhs; - self.impulse += impulse; - let lin_impulse = self.basis1 * impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = impulse.y; - #[cfg(feature = "dim3")] - let ang_impulse = impulse.fixed_rows::<3>(2).into_owned(); - - mj_lambda1.linear += self.im1 * lin_impulse; - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(ang_impulse + self.r1.gcross(lin_impulse)); - - mj_lambda2.linear -= self.im2 * lin_impulse; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - } - - fn solve_limits(&mut self, mj_lambda1: &mut DeltaVel, mj_lambda2: &mut DeltaVel) { - if self.limits_active { - let limits_forcedir1 = -self.limits_forcedir2; - let limits_forcedir2 = self.limits_forcedir2; - - let ang_vel1 = self.ii1_sqrt.transform_vector(mj_lambda1.angular); - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let lin_dvel = limits_forcedir2.dot(&(mj_lambda2.linear + ang_vel2.gcross(self.r2))) - + limits_forcedir1.dot(&(mj_lambda1.linear + ang_vel1.gcross(self.r1))) - + self.limits_rhs; - let new_impulse = (self.limits_impulse - lin_dvel * self.limits_inv_lhs) - .max(self.limits_impulse_limits.0) - .min(self.limits_impulse_limits.1); - let dimpulse = new_impulse - self.limits_impulse; - self.limits_impulse = new_impulse; - - let lin_impulse1 = limits_forcedir1 * dimpulse; - let lin_impulse2 = limits_forcedir2 * dimpulse; - - mj_lambda1.linear += self.im1 * lin_impulse1; - mj_lambda1.angular += self.ii1_sqrt.transform_vector(self.r1.gcross(lin_impulse1)); - mj_lambda2.linear += self.im2 * lin_impulse2; - mj_lambda2.angular += self.ii2_sqrt.transform_vector(self.r2.gcross(lin_impulse2)); - } - } - - fn solve_motors(&mut self, mj_lambda1: &mut DeltaVel, mj_lambda2: &mut DeltaVel) { - if self.motor_inv_lhs != 0.0 { - let ang_vel1 = self.ii1_sqrt.transform_vector(mj_lambda1.angular); - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let dvel = self - .motor_axis2 - .dot(&(mj_lambda2.linear + ang_vel2.gcross(self.r2))) - - self - .motor_axis1 - .dot(&(mj_lambda1.linear + ang_vel1.gcross(self.r1))) - + self.motor_rhs; - let new_impulse = na::clamp( - self.motor_impulse + dvel * self.motor_inv_lhs, - -self.motor_max_impulse, - self.motor_max_impulse, - ); - let dimpulse = new_impulse - self.motor_impulse; - self.motor_impulse = new_impulse; - - let lin_impulse1 = self.motor_axis1 * dimpulse; - let lin_impulse2 = self.motor_axis2 * dimpulse; - - mj_lambda1.linear += lin_impulse1 * self.im1; - mj_lambda1.angular += self.ii1_sqrt.transform_vector(self.r1.gcross(lin_impulse1)); - mj_lambda2.linear -= lin_impulse2 * self.im2; - mj_lambda2.angular -= self.ii2_sqrt.transform_vector(self.r2.gcross(lin_impulse2)); - } - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = mj_lambdas[self.mj_lambda1 as usize]; - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - self.solve_limits(&mut mj_lambda1, &mut mj_lambda2); - self.solve_motors(&mut mj_lambda1, &mut mj_lambda2); - self.solve_dofs(&mut mj_lambda1, &mut mj_lambda2); - - mj_lambdas[self.mj_lambda1 as usize] = mj_lambda1; - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - let joint = &mut joints_all[self.joint_id].weight; - if let JointParams::PrismaticJoint(revolute) = &mut joint.params { - revolute.impulse = self.impulse; - revolute.motor_impulse = self.motor_impulse; - revolute.limits_impulse = self.limits_impulse; - } - } -} - -#[derive(Debug)] -pub(crate) struct PrismaticVelocityGroundConstraint { - mj_lambda2: usize, - - joint_id: JointIndex, - - r2: Vector, - - #[cfg(feature = "dim2")] - inv_lhs: Matrix2, - #[cfg(feature = "dim2")] - rhs: Vector2, - #[cfg(feature = "dim2")] - impulse: Vector2, - - #[cfg(feature = "dim3")] - inv_lhs: Matrix5, - #[cfg(feature = "dim3")] - rhs: Vector5, - #[cfg(feature = "dim3")] - impulse: Vector5, - - limits_active: bool, - limits_forcedir2: Vector, - limits_impulse: Real, - limits_rhs: Real, - /// min/max applied impulse due to limits - limits_impulse_limits: (Real, Real), - - axis2: Vector, - motor_impulse: Real, - motor_rhs: Real, - motor_inv_lhs: Real, - motor_max_impulse: Real, - - #[cfg(feature = "dim2")] - basis1: Vector2, - #[cfg(feature = "dim3")] - basis1: Matrix3x2, - - im2: Real, - ii2_sqrt: AngularInertia, -} - -impl PrismaticVelocityGroundConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: JointIndex, - rb1: (&RigidBodyPosition, &RigidBodyVelocity, &RigidBodyMassProps), - rb2: ( - &RigidBodyPosition, - &RigidBodyVelocity, - &RigidBodyMassProps, - &RigidBodyIds, - ), - joint: &PrismaticJoint, - flipped: bool, - ) -> Self { - let (poss1, vels1, mprops1) = rb1; - let (poss2, vels2, mprops2, ids2) = rb2; - - let anchor2; - let anchor1; - let axis2; - let axis1; - let basis1; - - if flipped { - anchor2 = poss2.position * joint.local_anchor1; - anchor1 = poss1.position * joint.local_anchor2; - axis2 = poss2.position * joint.local_axis1; - axis1 = poss1.position * joint.local_axis2; - #[cfg(feature = "dim2")] - { - basis1 = poss1.position * joint.basis2[0]; - } - #[cfg(feature = "dim3")] - { - basis1 = Matrix3x2::from_columns(&[ - poss1.position * joint.basis2[0], - poss1.position * joint.basis2[1], - ]); - } - } else { - anchor2 = poss2.position * joint.local_anchor2; - anchor1 = poss1.position * joint.local_anchor1; - axis2 = poss2.position * joint.local_axis2; - axis1 = poss1.position * joint.local_axis1; - #[cfg(feature = "dim2")] - { - basis1 = poss1.position * joint.basis1[0]; - } - #[cfg(feature = "dim3")] - { - basis1 = Matrix3x2::from_columns(&[ - poss1.position * joint.basis1[0], - poss1.position * joint.basis1[1], - ]); - } - }; - - // #[cfg(feature = "dim2")] - // let r21 = Rotation::rotation_between_axis(&axis1, &axis2) - // .to_rotation_matrix() - // .into_inner(); - // #[cfg(feature = "dim3")] - // let r21 = Rotation::rotation_between_axis(&axis1, &axis2) - // .unwrap_or_else(Rotation::identity) - // .to_rotation_matrix() - // .into_inner(); - // let basis2 = r21 * basis1; - // NOTE: we use basis2 := basis1 for now is that allows - // simplifications of the computation without introducing - // much instabilities. - - let im2 = mprops2.effective_inv_mass; - let ii2 = mprops2.effective_world_inv_inertia_sqrt.squared(); - let r1 = anchor1 - mprops1.world_com; - let r2 = anchor2 - mprops2.world_com; - let r2_mat = r2.gcross_matrix(); - - #[allow(unused_mut)] // For 2D. - let mut lhs; - - #[cfg(feature = "dim3")] - { - let r2_mat_b1 = r2_mat * basis1; - - lhs = Matrix5::zeros(); - let lhs00 = ii2.quadform3x2(&r2_mat_b1).add_diagonal(im2); - let lhs10 = ii2 * r2_mat_b1; - let lhs11 = ii2.into_matrix(); - lhs.fixed_slice_mut::<2, 2>(0, 0) - .copy_from(&lhs00.into_matrix()); - lhs.fixed_slice_mut::<3, 2>(2, 0).copy_from(&lhs10); - lhs.fixed_slice_mut::<3, 3>(2, 2).copy_from(&lhs11); - } - - #[cfg(feature = "dim2")] - { - let b2r2 = basis1.dot(&r2_mat); - let m11 = im2 + b2r2 * ii2 * b2r2; - let m12 = basis1.dot(&r2_mat) * ii2; - let m22 = ii2; - lhs = SdpMatrix2::new(m11, m12, m22); - } - - let anchor_linvel1 = vels1.linvel + vels1.angvel.gcross(r1); - let anchor_linvel2 = vels2.linvel + vels2.angvel.gcross(r2); - - // NOTE: we don't use Cholesky in 2D because we only have a 2x2 matrix - // for which a textbook inverse is still efficient. - #[cfg(feature = "dim2")] - let inv_lhs = lhs.inverse_unchecked().into_matrix(); - #[cfg(feature = "dim3")] - let inv_lhs = Cholesky::new_unchecked(lhs).inverse(); - - let linvel_err = basis1.tr_mul(&(anchor_linvel2 - anchor_linvel1)); - let angvel_err = vels2.angvel - vels1.angvel; - - #[cfg(feature = "dim2")] - let mut rhs = Vector2::new(linvel_err.x, angvel_err) * params.velocity_solve_fraction; - #[cfg(feature = "dim3")] - let mut rhs = Vector5::new( - linvel_err.x, - linvel_err.y, - angvel_err.x, - angvel_err.y, - angvel_err.z, - ) * params.velocity_solve_fraction; - - let velocity_based_erp_inv_dt = params.velocity_based_erp_inv_dt(); - if velocity_based_erp_inv_dt != 0.0 { - let linear_err = basis1.tr_mul(&(anchor2 - anchor1)); - - let (frame1, frame2); - if flipped { - frame1 = poss1.position * joint.local_frame2(); - frame2 = poss2.position * joint.local_frame1(); - } else { - frame1 = poss1.position * joint.local_frame1(); - frame2 = poss2.position * joint.local_frame2(); - } - - let ang_err = frame2.rotation * frame1.rotation.inverse(); - #[cfg(feature = "dim2")] - { - rhs += Vector2::new(linear_err.x, ang_err.angle()) * velocity_based_erp_inv_dt; - } - #[cfg(feature = "dim3")] - { - let ang_err = ang_err.scaled_axis(); - rhs += Vector5::new(linear_err.x, linear_err.y, ang_err.x, ang_err.y, ang_err.z) - * velocity_based_erp_inv_dt; - } - } - - /* - * Setup motor. - */ - let mut motor_rhs = 0.0; - let mut motor_inv_lhs = 0.0; - - let (stiffness, damping, gamma, keep_lhs) = joint.motor_model.combine_coefficients( - params.dt, - joint.motor_stiffness, - joint.motor_damping, - ); - - if stiffness != 0.0 { - let dist = anchor2.coords.dot(&axis2) - anchor1.coords.dot(&axis1); - motor_rhs += (dist - joint.motor_target_pos) * stiffness; - } - - if damping != 0.0 { - let curr_vel = vels2.linvel.dot(&axis2) - vels1.linvel.dot(&axis1); - motor_rhs += (curr_vel - joint.motor_target_vel) * damping; - } - - if stiffness != 0.0 || damping != 0.0 { - motor_inv_lhs = if keep_lhs { gamma / im2 } else { gamma }; - motor_rhs /= gamma; - } - - let motor_impulse = na::clamp( - joint.motor_impulse, - -joint.motor_max_impulse, - joint.motor_max_impulse, - ); - - /* - * Setup limit constraint. - */ - let mut limits_active = false; - let limits_forcedir2 = axis2.into_inner(); - let mut limits_rhs = 0.0; - let mut limits_impulse = 0.0; - let mut limits_impulse_limits = (0.0, 0.0); - - if joint.limits_enabled { - let danchor = anchor2 - anchor1; - let dist = danchor.dot(&axis1); - - // TODO: we should allow predictive constraint activation. - - let (min_limit, max_limit) = (joint.limits[0], joint.limits[1]); - let min_enabled = dist < min_limit; - let max_enabled = max_limit < dist; - - limits_impulse_limits.0 = if max_enabled { -Real::INFINITY } else { 0.0 }; - limits_impulse_limits.1 = if min_enabled { Real::INFINITY } else { 0.0 }; - - limits_active = min_enabled || max_enabled; - if limits_active { - limits_rhs = (anchor_linvel2.dot(&axis2) - anchor_linvel1.dot(&axis1)) - * params.velocity_solve_fraction; - - limits_rhs += ((dist - max_limit).max(0.0) - (min_limit - dist).max(0.0)) - * velocity_based_erp_inv_dt; - - limits_impulse = joint - .limits_impulse - .max(limits_impulse_limits.0) - .min(limits_impulse_limits.1); - } - } - - PrismaticVelocityGroundConstraint { - joint_id, - mj_lambda2: ids2.active_set_offset, - im2, - ii2_sqrt: mprops2.effective_world_inv_inertia_sqrt, - impulse: joint.impulse * params.warmstart_coeff, - limits_active, - limits_forcedir2, - limits_impulse: limits_impulse * params.warmstart_coeff, - limits_rhs, - limits_impulse_limits, - motor_rhs, - motor_inv_lhs, - motor_impulse, - motor_max_impulse: joint.motor_max_impulse, - basis1, - inv_lhs, - rhs, - r2, - axis2: axis2.into_inner(), - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - let lin_impulse = self.basis1 * self.impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = self.impulse.y; - #[cfg(feature = "dim3")] - let ang_impulse = self.impulse.fixed_rows::<3>(2).into_owned(); - - mj_lambda2.linear -= self.im2 * lin_impulse; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - // Warmstart motors. - mj_lambda2.linear -= self.axis2 * (self.im2 * self.motor_impulse); - - // Warmstart limits. - mj_lambda2.linear += self.limits_forcedir2 * (self.im2 * self.limits_impulse); - - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - fn solve_dofs(&mut self, mj_lambda2: &mut DeltaVel) { - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - let lin_vel2 = mj_lambda2.linear + ang_vel2.gcross(self.r2); - let lin_dvel = self.basis1.tr_mul(&lin_vel2); - let ang_dvel = ang_vel2; - #[cfg(feature = "dim2")] - let rhs = Vector2::new(lin_dvel.x, ang_dvel) + self.rhs; - #[cfg(feature = "dim3")] - let rhs = - Vector5::new(lin_dvel.x, lin_dvel.y, ang_dvel.x, ang_dvel.y, ang_dvel.z) + self.rhs; - let impulse = self.inv_lhs * rhs; - self.impulse += impulse; - let lin_impulse = self.basis1 * impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = impulse.y; - #[cfg(feature = "dim3")] - let ang_impulse = impulse.fixed_rows::<3>(2).into_owned(); - - mj_lambda2.linear -= self.im2 * lin_impulse; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - } - - fn solve_limits(&mut self, mj_lambda2: &mut DeltaVel) { - if self.limits_active { - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let lin_dvel = self - .limits_forcedir2 - .dot(&(mj_lambda2.linear + ang_vel2.gcross(self.r2))) - + self.limits_rhs; - let new_impulse = (self.limits_impulse - lin_dvel / self.im2) - .max(self.limits_impulse_limits.0) - .min(self.limits_impulse_limits.1); - let dimpulse = new_impulse - self.limits_impulse; - self.limits_impulse = new_impulse; - - mj_lambda2.linear += self.limits_forcedir2 * (self.im2 * dimpulse); - } - } - - fn solve_motors(&mut self, mj_lambda2: &mut DeltaVel) { - if self.motor_inv_lhs != 0.0 { - let lin_dvel = self.axis2.dot(&mj_lambda2.linear) + self.motor_rhs; - let new_impulse = na::clamp( - self.motor_impulse + lin_dvel * self.motor_inv_lhs, - -self.motor_max_impulse, - self.motor_max_impulse, - ); - let dimpulse = new_impulse - self.motor_impulse; - self.motor_impulse = new_impulse; - - mj_lambda2.linear -= self.axis2 * (self.im2 * dimpulse); - } - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - self.solve_limits(&mut mj_lambda2); - self.solve_motors(&mut mj_lambda2); - self.solve_dofs(&mut mj_lambda2); - - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - // TODO: duplicated code with the non-ground constraint. - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - let joint = &mut joints_all[self.joint_id].weight; - if let JointParams::PrismaticJoint(revolute) = &mut joint.params { - revolute.impulse = self.impulse; - revolute.motor_impulse = self.motor_impulse; - revolute.limits_impulse = self.limits_impulse; - } - } -} diff --git a/src/dynamics/solver/joint_constraint/prismatic_velocity_constraint_wide.rs b/src/dynamics/solver/joint_constraint/prismatic_velocity_constraint_wide.rs deleted file mode 100644 index 984adcb..0000000 --- a/src/dynamics/solver/joint_constraint/prismatic_velocity_constraint_wide.rs +++ /dev/null @@ -1,848 +0,0 @@ -use simba::simd::{SimdBool as _, SimdPartialOrd, SimdValue}; - -use crate::dynamics::solver::DeltaVel; -use crate::dynamics::{ - IntegrationParameters, JointGraphEdge, JointIndex, JointParams, PrismaticJoint, RigidBodyIds, - RigidBodyMassProps, RigidBodyPosition, RigidBodyVelocity, -}; -use crate::math::{ - AngVector, AngularInertia, Isometry, Point, Real, SimdBool, SimdReal, Vector, SIMD_WIDTH, -}; -use crate::utils::{WAngularInertia, WCross, WCrossMatrix, WDot}; - -#[cfg(feature = "dim3")] -use na::{Cholesky, Matrix3x2, Matrix5, Vector3, Vector5}; - -#[cfg(feature = "dim2")] -use { - na::{Matrix2, Vector2}, - parry::utils::SdpMatrix2, -}; - -#[cfg(feature = "dim2")] -const LIN_IMPULSE_DIM: usize = 1; - -#[cfg(feature = "dim3")] -const LIN_IMPULSE_DIM: usize = 2; - -#[derive(Debug)] -pub(crate) struct WPrismaticVelocityConstraint { - mj_lambda1: [usize; SIMD_WIDTH], - mj_lambda2: [usize; SIMD_WIDTH], - - joint_id: [JointIndex; SIMD_WIDTH], - - r1: Vector, - r2: Vector, - - #[cfg(feature = "dim3")] - inv_lhs: Matrix5, - #[cfg(feature = "dim3")] - rhs: Vector5, - #[cfg(feature = "dim3")] - impulse: Vector5, - - #[cfg(feature = "dim2")] - inv_lhs: Matrix2, - #[cfg(feature = "dim2")] - rhs: Vector2, - #[cfg(feature = "dim2")] - impulse: Vector2, - - limits_active: bool, - limits_impulse: SimdReal, - limits_forcedir2: Vector, - limits_rhs: SimdReal, - limits_inv_lhs: SimdReal, - limits_impulse_limits: (SimdReal, SimdReal), - - #[cfg(feature = "dim2")] - basis1: Vector2, - #[cfg(feature = "dim3")] - basis1: Matrix3x2, - - im1: SimdReal, - im2: SimdReal, - - ii1_sqrt: AngularInertia, - ii2_sqrt: AngularInertia, -} - -impl WPrismaticVelocityConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: [JointIndex; SIMD_WIDTH], - rbs1: ( - [&RigidBodyPosition; SIMD_WIDTH], - [&RigidBodyVelocity; SIMD_WIDTH], - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - rbs2: ( - [&RigidBodyPosition; SIMD_WIDTH], - [&RigidBodyVelocity; SIMD_WIDTH], - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - cparams: [&PrismaticJoint; SIMD_WIDTH], - ) -> Self { - let (poss1, vels1, mprops1, ids1) = rbs1; - let (poss2, vels2, mprops2, ids2) = rbs2; - - let position1 = Isometry::from(gather![|ii| poss1[ii].position]); - let linvel1 = Vector::from(gather![|ii| vels1[ii].linvel]); - let angvel1 = AngVector::::from(gather![|ii| vels1[ii].angvel]); - let world_com1 = Point::from(gather![|ii| mprops1[ii].world_com]); - let im1 = SimdReal::from(gather![|ii| mprops1[ii].effective_inv_mass]); - let ii1_sqrt = AngularInertia::::from(gather![ - |ii| mprops1[ii].effective_world_inv_inertia_sqrt - ]); - let mj_lambda1 = gather![|ii| ids1[ii].active_set_offset]; - - let position2 = Isometry::from(gather![|ii| poss2[ii].position]); - let linvel2 = Vector::from(gather![|ii| vels2[ii].linvel]); - let angvel2 = AngVector::::from(gather![|ii| vels2[ii].angvel]); - let world_com2 = Point::from(gather![|ii| mprops2[ii].world_com]); - let im2 = SimdReal::from(gather![|ii| mprops2[ii].effective_inv_mass]); - let ii2_sqrt = AngularInertia::::from(gather![ - |ii| mprops2[ii].effective_world_inv_inertia_sqrt - ]); - let mj_lambda2 = gather![|ii| ids2[ii].active_set_offset]; - - let local_anchor1 = Point::from(gather![|ii| cparams[ii].local_anchor1]); - let local_anchor2 = Point::from(gather![|ii| cparams[ii].local_anchor2]); - let local_axis1 = Vector::from(gather![|ii| *cparams[ii].local_axis1]); - let local_axis2 = Vector::from(gather![|ii| *cparams[ii].local_axis2]); - - #[cfg(feature = "dim2")] - let local_basis1 = [Vector::from(gather![|ii| cparams[ii].basis1[0]])]; - #[cfg(feature = "dim3")] - let local_basis1 = [ - Vector::from(gather![|ii| cparams[ii].basis1[0]]), - Vector::from(gather![|ii| cparams[ii].basis1[1]]), - ]; - - #[cfg(feature = "dim2")] - let impulse = Vector2::from(gather![|ii| cparams[ii].impulse]); - #[cfg(feature = "dim3")] - let impulse = Vector5::from(gather![|ii| cparams[ii].impulse]); - - let anchor1 = position1 * local_anchor1; - let anchor2 = position2 * local_anchor2; - let axis1 = position1 * local_axis1; - let axis2 = position2 * local_axis2; - - #[cfg(feature = "dim2")] - let basis1 = position1 * local_basis1[0]; - #[cfg(feature = "dim3")] - let basis1 = - Matrix3x2::from_columns(&[position1 * local_basis1[0], position1 * local_basis1[1]]); - - // #[cfg(feature = "dim2")] - // let r21 = Rotation::rotation_between_axis(&axis1, &axis2) - // .to_rotation_matrix() - // .into_inner(); - // #[cfg(feature = "dim3")] - // let r21 = Rotation::rotation_between_axis(&axis1, &axis2) - // .unwrap_or_else(Rotation::identity) - // .to_rotation_matrix() - // .into_inner(); - // let basis2 = r21 * basis1; - // NOTE: we use basis2 := basis1 for now is that allows - // simplifications of the computation without introducing - // much instabilities. - - let ii1 = ii1_sqrt.squared(); - let r1 = anchor1 - world_com1; - let r1_mat = r1.gcross_matrix(); - - let ii2 = ii2_sqrt.squared(); - let r2 = anchor2 - world_com2; - let r2_mat = r2.gcross_matrix(); - - #[allow(unused_mut)] // For 2D. - let mut lhs; - - #[cfg(feature = "dim3")] - { - let r1_mat_b1 = r1_mat * basis1; - let r2_mat_b1 = r2_mat * basis1; - - lhs = Matrix5::zeros(); - let lhs00 = ii1.quadform3x2(&r1_mat_b1).add_diagonal(im1) - + ii2.quadform3x2(&r2_mat_b1).add_diagonal(im2); - let lhs10 = ii1 * r1_mat_b1 + ii2 * r2_mat_b1; - let lhs11 = (ii1 + ii2).into_matrix(); - lhs.fixed_slice_mut::<2, 2>(0, 0) - .copy_from(&lhs00.into_matrix()); - lhs.fixed_slice_mut::<3, 2>(2, 0).copy_from(&lhs10); - lhs.fixed_slice_mut::<3, 3>(2, 2).copy_from(&lhs11); - } - - #[cfg(feature = "dim2")] - { - let b1r1 = basis1.dot(&r1_mat); - let b2r2 = basis1.dot(&r2_mat); - let m11 = im1 + im2 + b1r1 * ii1 * b1r1 + b2r2 * ii2 * b2r2; - let m12 = basis1.dot(&r1_mat) * ii1 + basis1.dot(&r2_mat) * ii2; - let m22 = ii1 + ii2; - lhs = SdpMatrix2::new(m11, m12, m22); - } - - let anchor_linvel1 = linvel1 + angvel1.gcross(r1); - let anchor_linvel2 = linvel2 + angvel2.gcross(r2); - - // NOTE: we don't use Cholesky in 2D because we only have a 2x2 matrix - // for which a textbook inverse is still efficient. - #[cfg(feature = "dim2")] - let inv_lhs = lhs.inverse_unchecked().into_matrix(); - #[cfg(feature = "dim3")] - let inv_lhs = Cholesky::new_unchecked(lhs).inverse(); - - let linvel_err = basis1.tr_mul(&(anchor_linvel2 - anchor_linvel1)); - let angvel_err = angvel2 - angvel1; - - let velocity_solve_fraction = SimdReal::splat(params.velocity_solve_fraction); - - #[cfg(feature = "dim2")] - let mut rhs = Vector2::new(linvel_err.x, angvel_err) * velocity_solve_fraction; - #[cfg(feature = "dim3")] - let mut rhs = Vector5::new( - linvel_err.x, - linvel_err.y, - angvel_err.x, - angvel_err.y, - angvel_err.z, - ) * velocity_solve_fraction; - - let velocity_based_erp_inv_dt = params.velocity_based_erp_inv_dt(); - if velocity_based_erp_inv_dt != 0.0 { - let velocity_based_erp_inv_dt = SimdReal::splat(velocity_based_erp_inv_dt); - - let linear_err = basis1.tr_mul(&(anchor2 - anchor1)); - - let local_frame1 = Isometry::from(gather![|ii| cparams[ii].local_frame1()]); - let local_frame2 = Isometry::from(gather![|ii| cparams[ii].local_frame2()]); - - let frame1 = position1 * local_frame1; - let frame2 = position2 * local_frame2; - let ang_err = frame2.rotation * frame1.rotation.inverse(); - - #[cfg(feature = "dim2")] - { - rhs += Vector2::new(linear_err.x, ang_err.angle()) * velocity_based_erp_inv_dt; - } - - #[cfg(feature = "dim3")] - { - let ang_err = Vector3::from(gather![|ii| ang_err.extract(ii).scaled_axis()]); - rhs += Vector5::new(linear_err.x, linear_err.y, ang_err.x, ang_err.y, ang_err.z) - * velocity_based_erp_inv_dt; - } - } - - // Setup limit constraint. - let zero: SimdReal = na::zero(); - let limits_forcedir2 = axis2; // hopefully axis1 is colinear with axis2 - let mut limits_active = false; - let mut limits_rhs = zero; - let mut limits_impulse = zero; - let mut limits_inv_lhs = zero; - let mut limits_impulse_limits = (zero, zero); - - let limits_enabled = SimdBool::from(gather![|ii| cparams[ii].limits_enabled]); - if limits_enabled.any() { - let danchor = anchor2 - anchor1; - let dist = danchor.dot(&axis1); - - // TODO: we should allow predictive constraint activation. - - let min_limit = SimdReal::from(gather![|ii| cparams[ii].limits[0]]); - let max_limit = SimdReal::from(gather![|ii| cparams[ii].limits[1]]); - - let min_enabled = dist.simd_lt(min_limit); - let max_enabled = dist.simd_gt(max_limit); - - limits_impulse_limits.0 = SimdReal::splat(-Real::INFINITY).select(max_enabled, zero); - limits_impulse_limits.1 = SimdReal::splat(Real::INFINITY).select(min_enabled, zero); - - limits_active = (min_enabled | max_enabled).any(); - if limits_active { - let gcross1 = r1.gcross(axis1); - let gcross2 = r2.gcross(axis2); - - limits_rhs = (anchor_linvel2.dot(&axis2) - anchor_linvel1.dot(&axis1)) - * velocity_solve_fraction; - - limits_rhs += ((dist - max_limit).simd_max(zero) - - (min_limit - dist).simd_max(zero)) - * SimdReal::splat(velocity_based_erp_inv_dt); - - limits_impulse = SimdReal::from(gather![|ii| cparams[ii].limits_impulse]) - .simd_max(limits_impulse_limits.0) - .simd_min(limits_impulse_limits.1); - - limits_inv_lhs = SimdReal::splat(1.0) - / (im1 - + im2 - + gcross1.gdot(ii1.transform_vector(gcross1)) - + gcross2.gdot(ii2.transform_vector(gcross2))); - } - } - - WPrismaticVelocityConstraint { - joint_id, - mj_lambda1, - mj_lambda2, - im1, - ii1_sqrt, - im2, - ii2_sqrt, - limits_active, - impulse: impulse * SimdReal::splat(params.warmstart_coeff), - limits_impulse: limits_impulse * SimdReal::splat(params.warmstart_coeff), - limits_forcedir2, - limits_rhs, - limits_inv_lhs, - limits_impulse_limits, - basis1, - inv_lhs, - rhs, - r1, - r2, - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda1[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda1[ii] as usize].angular - ]), - }; - let mut mj_lambda2 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - let lin_impulse = self.basis1 * self.impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = self.impulse.y; - #[cfg(feature = "dim3")] - let ang_impulse = self.impulse.fixed_rows::<3>(2).into_owned(); - - mj_lambda1.linear += lin_impulse * self.im1; - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(ang_impulse + self.r1.gcross(lin_impulse)); - - mj_lambda2.linear -= lin_impulse * self.im2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - // Warmstart limits. - if self.limits_active { - let limit_impulse1 = -self.limits_forcedir2 * self.limits_impulse; - let limit_impulse2 = self.limits_forcedir2 * self.limits_impulse; - - mj_lambda1.linear += limit_impulse1 * self.im1; - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(self.r1.gcross(limit_impulse1)); - mj_lambda2.linear += limit_impulse2 * self.im2; - mj_lambda2.angular += self - .ii2_sqrt - .transform_vector(self.r2.gcross(limit_impulse2)); - } - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda1[ii] as usize].linear = mj_lambda1.linear.extract(ii); - mj_lambdas[self.mj_lambda1[ii] as usize].angular = mj_lambda1.angular.extract(ii); - } - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - fn solve_dofs( - &mut self, - mj_lambda1: &mut DeltaVel, - mj_lambda2: &mut DeltaVel, - ) { - let ang_vel1 = self.ii1_sqrt.transform_vector(mj_lambda1.angular); - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - let lin_vel1 = mj_lambda1.linear + ang_vel1.gcross(self.r1); - let lin_vel2 = mj_lambda2.linear + ang_vel2.gcross(self.r2); - let lin_dvel = self.basis1.tr_mul(&(lin_vel2 - lin_vel1)); - let ang_dvel = ang_vel2 - ang_vel1; - #[cfg(feature = "dim2")] - let rhs = Vector2::new(lin_dvel.x, ang_dvel) + self.rhs; - #[cfg(feature = "dim3")] - let rhs = - Vector5::new(lin_dvel.x, lin_dvel.y, ang_dvel.x, ang_dvel.y, ang_dvel.z) + self.rhs; - let impulse = self.inv_lhs * rhs; - self.impulse += impulse; - let lin_impulse = self.basis1 * impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = impulse.y; - #[cfg(feature = "dim3")] - let ang_impulse = impulse.fixed_rows::<3>(2).into_owned(); - - mj_lambda1.linear += lin_impulse * self.im1; - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(ang_impulse + self.r1.gcross(lin_impulse)); - - mj_lambda2.linear -= lin_impulse * self.im2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - } - - fn solve_limits( - &mut self, - mj_lambda1: &mut DeltaVel, - mj_lambda2: &mut DeltaVel, - ) { - if self.limits_active { - let limits_forcedir1 = -self.limits_forcedir2; - let limits_forcedir2 = self.limits_forcedir2; - - let ang_vel1 = self.ii1_sqrt.transform_vector(mj_lambda1.angular); - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let lin_dvel = limits_forcedir2.dot(&(mj_lambda2.linear + ang_vel2.gcross(self.r2))) - + limits_forcedir1.dot(&(mj_lambda1.linear + ang_vel1.gcross(self.r1))) - + self.limits_rhs; - let new_impulse = (self.limits_impulse - lin_dvel * self.limits_inv_lhs) - .simd_max(self.limits_impulse_limits.0) - .simd_min(self.limits_impulse_limits.1); - let dimpulse = new_impulse - self.limits_impulse; - self.limits_impulse = new_impulse; - - let lin_impulse1 = limits_forcedir1 * dimpulse; - let lin_impulse2 = limits_forcedir2 * dimpulse; - - mj_lambda1.linear += lin_impulse1 * self.im1; - mj_lambda1.angular += self.ii1_sqrt.transform_vector(self.r1.gcross(lin_impulse1)); - mj_lambda2.linear += lin_impulse2 * self.im2; - mj_lambda2.angular += self.ii2_sqrt.transform_vector(self.r2.gcross(lin_impulse2)); - } - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda1[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda1[ii] as usize].angular - ]), - }; - let mut mj_lambda2 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - self.solve_dofs(&mut mj_lambda1, &mut mj_lambda2); - self.solve_limits(&mut mj_lambda1, &mut mj_lambda2); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda1[ii] as usize].linear = mj_lambda1.linear.extract(ii); - mj_lambdas[self.mj_lambda1[ii] as usize].angular = mj_lambda1.angular.extract(ii); - } - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - for ii in 0..SIMD_WIDTH { - let joint = &mut joints_all[self.joint_id[ii]].weight; - if let JointParams::PrismaticJoint(rev) = &mut joint.params { - rev.impulse = self.impulse.extract(ii); - rev.limits_impulse = self.limits_impulse.extract(ii); - } - } - } -} - -#[derive(Debug)] -pub(crate) struct WPrismaticVelocityGroundConstraint { - mj_lambda2: [usize; SIMD_WIDTH], - - joint_id: [JointIndex; SIMD_WIDTH], - - r2: Vector, - - #[cfg(feature = "dim2")] - inv_lhs: Matrix2, - #[cfg(feature = "dim2")] - rhs: Vector2, - #[cfg(feature = "dim2")] - impulse: Vector2, - - #[cfg(feature = "dim3")] - inv_lhs: Matrix5, - #[cfg(feature = "dim3")] - rhs: Vector5, - #[cfg(feature = "dim3")] - impulse: Vector5, - - limits_active: bool, - limits_forcedir2: Vector, - limits_impulse: SimdReal, - limits_rhs: SimdReal, - limits_impulse_limits: (SimdReal, SimdReal), - - axis2: Vector, - #[cfg(feature = "dim2")] - basis1: Vector2, - #[cfg(feature = "dim3")] - basis1: Matrix3x2, - - im2: SimdReal, - ii2_sqrt: AngularInertia, -} - -impl WPrismaticVelocityGroundConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: [JointIndex; SIMD_WIDTH], - rbs1: ( - [&RigidBodyPosition; SIMD_WIDTH], - [&RigidBodyVelocity; SIMD_WIDTH], - [&RigidBodyMassProps; SIMD_WIDTH], - ), - rbs2: ( - [&RigidBodyPosition; SIMD_WIDTH], - [&RigidBodyVelocity; SIMD_WIDTH], - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - cparams: [&PrismaticJoint; SIMD_WIDTH], - flipped: [bool; SIMD_WIDTH], - ) -> Self { - let (poss1, vels1, mprops1) = rbs1; - let (poss2, vels2, mprops2, ids2) = rbs2; - - let position1 = Isometry::from(gather![|ii| poss1[ii].position]); - let linvel1 = Vector::from(gather![|ii| vels1[ii].linvel]); - let angvel1 = AngVector::::from(gather![|ii| vels1[ii].angvel]); - let world_com1 = Point::from(gather![|ii| mprops1[ii].world_com]); - - let position2 = Isometry::from(gather![|ii| poss2[ii].position]); - let linvel2 = Vector::from(gather![|ii| vels2[ii].linvel]); - let angvel2 = AngVector::::from(gather![|ii| vels2[ii].angvel]); - let world_com2 = Point::from(gather![|ii| mprops2[ii].world_com]); - let im2 = SimdReal::from(gather![|ii| mprops2[ii].effective_inv_mass]); - let ii2_sqrt = AngularInertia::::from(gather![ - |ii| mprops2[ii].effective_world_inv_inertia_sqrt - ]); - let mj_lambda2 = gather![|ii| ids2[ii].active_set_offset]; - - #[cfg(feature = "dim2")] - let impulse = Vector2::from(gather![|ii| cparams[ii].impulse]); - #[cfg(feature = "dim3")] - let impulse = Vector5::from(gather![|ii| cparams[ii].impulse]); - - let local_anchor1 = Point::from(gather![|ii| if flipped[ii] { - cparams[ii].local_anchor2 - } else { - cparams[ii].local_anchor1 - }]); - let local_anchor2 = Point::from(gather![|ii| if flipped[ii] { - cparams[ii].local_anchor1 - } else { - cparams[ii].local_anchor2 - }]); - let local_axis1 = Vector::from(gather![|ii| if flipped[ii] { - *cparams[ii].local_axis2 - } else { - *cparams[ii].local_axis1 - }]); - let local_axis2 = Vector::from(gather![|ii| if flipped[ii] { - *cparams[ii].local_axis1 - } else { - *cparams[ii].local_axis2 - }]); - - #[cfg(feature = "dim2")] - let basis1 = position1 - * Vector::from(gather![|ii| if flipped[ii] { - cparams[ii].basis2[0] - } else { - cparams[ii].basis1[0] - }]); - #[cfg(feature = "dim3")] - let basis1 = Matrix3x2::from_columns(&[ - position1 - * Vector::from(gather![|ii| if flipped[ii] { - cparams[ii].basis2[0] - } else { - cparams[ii].basis1[0] - }]), - position1 - * Vector::from(gather![|ii| if flipped[ii] { - cparams[ii].basis2[1] - } else { - cparams[ii].basis1[1] - }]), - ]); - - let anchor1 = position1 * local_anchor1; - let anchor2 = position2 * local_anchor2; - let axis1 = position1 * local_axis1; - let axis2 = position2 * local_axis2; - - let ii2 = ii2_sqrt.squared(); - let r1 = anchor1 - world_com1; - let r2 = anchor2 - world_com2; - let r2_mat = r2.gcross_matrix(); - - #[allow(unused_mut)] // For 2D. - let mut lhs; - - #[cfg(feature = "dim3")] - { - let r2_mat_b1 = r2_mat * basis1; - - lhs = Matrix5::zeros(); - let lhs00 = ii2.quadform3x2(&r2_mat_b1).add_diagonal(im2); - let lhs10 = ii2 * r2_mat_b1; - let lhs11 = ii2.into_matrix(); - lhs.fixed_slice_mut::<2, 2>(0, 0) - .copy_from(&lhs00.into_matrix()); - lhs.fixed_slice_mut::<3, 2>(2, 0).copy_from(&lhs10); - lhs.fixed_slice_mut::<3, 3>(2, 2).copy_from(&lhs11); - } - - #[cfg(feature = "dim2")] - { - let b2r2 = basis1.dot(&r2_mat); - let m11 = im2 + b2r2 * ii2 * b2r2; - let m12 = basis1.dot(&r2_mat) * ii2; - let m22 = ii2; - lhs = SdpMatrix2::new(m11, m12, m22); - } - - let anchor_linvel1 = linvel1 + angvel1.gcross(r1); - let anchor_linvel2 = linvel2 + angvel2.gcross(r2); - - // NOTE: we don't use Cholesky in 2D because we only have a 2x2 matrix - // for which a textbook inverse is still efficient. - #[cfg(feature = "dim2")] - let inv_lhs = lhs.inverse_unchecked().into_matrix(); - #[cfg(feature = "dim3")] - let inv_lhs = Cholesky::new_unchecked(lhs).inverse(); - - let linvel_err = basis1.tr_mul(&(anchor_linvel2 - anchor_linvel1)); - let angvel_err = angvel2 - angvel1; - - let velocity_solve_fraction = SimdReal::splat(params.velocity_solve_fraction); - - #[cfg(feature = "dim2")] - let mut rhs = Vector2::new(linvel_err.x, angvel_err) * velocity_solve_fraction; - #[cfg(feature = "dim3")] - let mut rhs = Vector5::new( - linvel_err.x, - linvel_err.y, - angvel_err.x, - angvel_err.y, - angvel_err.z, - ) * velocity_solve_fraction; - - let velocity_based_erp_inv_dt = params.velocity_based_erp_inv_dt(); - if velocity_based_erp_inv_dt != 0.0 { - let velocity_based_erp_inv_dt = SimdReal::splat(velocity_based_erp_inv_dt); - - let linear_err = basis1.tr_mul(&(anchor2 - anchor1)); - - let frame1 = position1 - * Isometry::from(gather![|ii| if flipped[ii] { - cparams[ii].local_frame2() - } else { - cparams[ii].local_frame1() - }]); - let frame2 = position2 - * Isometry::from(gather![|ii| if flipped[ii] { - cparams[ii].local_frame1() - } else { - cparams[ii].local_frame2() - }]); - - let ang_err = frame2.rotation * frame1.rotation.inverse(); - - #[cfg(feature = "dim2")] - { - rhs += Vector2::new(linear_err.x, ang_err.angle()) * velocity_based_erp_inv_dt; - } - - #[cfg(feature = "dim3")] - { - let ang_err = Vector3::from(gather![|ii| ang_err.extract(ii).scaled_axis()]); - rhs += Vector5::new(linear_err.x, linear_err.y, ang_err.x, ang_err.y, ang_err.z) - * velocity_based_erp_inv_dt; - } - } - - // Setup limit constraint. - let zero: SimdReal = na::zero(); - let limits_forcedir2 = axis2; // hopefully axis1 is colinear with axis2 - let mut limits_active = false; - let mut limits_rhs = zero; - let mut limits_impulse = zero; - let mut limits_impulse_limits = (zero, zero); - - let limits_enabled = SimdBool::from(gather![|ii| cparams[ii].limits_enabled]); - if limits_enabled.any() { - let danchor = anchor2 - anchor1; - let dist = danchor.dot(&axis1); - - // TODO: we should allow predictive constraint activation. - let min_limit = SimdReal::from(gather![|ii| cparams[ii].limits[0]]); - let max_limit = SimdReal::from(gather![|ii| cparams[ii].limits[1]]); - - let min_enabled = dist.simd_lt(min_limit); - let max_enabled = dist.simd_gt(max_limit); - - limits_impulse_limits.0 = SimdReal::splat(-Real::INFINITY).select(max_enabled, zero); - limits_impulse_limits.1 = SimdReal::splat(Real::INFINITY).select(min_enabled, zero); - - limits_active = (min_enabled | max_enabled).any(); - if limits_active { - limits_rhs = (anchor_linvel2.dot(&axis2) - anchor_linvel1.dot(&axis1)) - * velocity_solve_fraction; - - limits_rhs += ((dist - max_limit).simd_max(zero) - - (min_limit - dist).simd_max(zero)) - * SimdReal::splat(velocity_based_erp_inv_dt); - - limits_impulse = SimdReal::from(gather![|ii| cparams[ii].limits_impulse]) - .simd_max(limits_impulse_limits.0) - .simd_min(limits_impulse_limits.1); - } - } - - WPrismaticVelocityGroundConstraint { - joint_id, - mj_lambda2, - im2, - ii2_sqrt, - impulse: impulse * SimdReal::splat(params.warmstart_coeff), - limits_active, - limits_forcedir2, - limits_rhs, - limits_impulse: limits_impulse * SimdReal::splat(params.warmstart_coeff), - limits_impulse_limits, - basis1, - inv_lhs, - rhs, - r2, - axis2, - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - let lin_impulse = self.basis1 * self.impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = self.impulse.y; - #[cfg(feature = "dim3")] - let ang_impulse = self.impulse.fixed_rows::<3>(2).into_owned(); - - mj_lambda2.linear -= lin_impulse * self.im2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - mj_lambda2.linear += self.limits_forcedir2 * (self.im2 * self.limits_impulse); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - fn solve_dofs(&mut self, mj_lambda2: &mut DeltaVel) { - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - let lin_vel2 = mj_lambda2.linear + ang_vel2.gcross(self.r2); - let lin_dvel = self.basis1.tr_mul(&lin_vel2); - let ang_dvel = ang_vel2; - #[cfg(feature = "dim2")] - let rhs = Vector2::new(lin_dvel.x, ang_dvel) + self.rhs; - #[cfg(feature = "dim3")] - let rhs = - Vector5::new(lin_dvel.x, lin_dvel.y, ang_dvel.x, ang_dvel.y, ang_dvel.z) + self.rhs; - let impulse = self.inv_lhs * rhs; - self.impulse += impulse; - let lin_impulse = self.basis1 * impulse.fixed_rows::(0).into_owned(); - #[cfg(feature = "dim2")] - let ang_impulse = impulse.y; - #[cfg(feature = "dim3")] - let ang_impulse = impulse.fixed_rows::<3>(2).into_owned(); - - mj_lambda2.linear -= lin_impulse * self.im2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - } - - fn solve_limits(&mut self, mj_lambda2: &mut DeltaVel) { - if self.limits_active { - // FIXME: the transformation by ii2_sqrt could be avoided by - // reusing some computations above. - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let lin_dvel = self - .limits_forcedir2 - .dot(&(mj_lambda2.linear + ang_vel2.gcross(self.r2))) - + self.limits_rhs; - let new_impulse = (self.limits_impulse - lin_dvel / self.im2) - .simd_max(self.limits_impulse_limits.0) - .simd_min(self.limits_impulse_limits.1); - let dimpulse = new_impulse - self.limits_impulse; - self.limits_impulse = new_impulse; - - mj_lambda2.linear += self.limits_forcedir2 * (self.im2 * dimpulse); - } - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - self.solve_dofs(&mut mj_lambda2); - self.solve_limits(&mut mj_lambda2); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - for ii in 0..SIMD_WIDTH { - let joint = &mut joints_all[self.joint_id[ii]].weight; - if let JointParams::PrismaticJoint(rev) = &mut joint.params { - rev.impulse = self.impulse.extract(ii); - rev.limits_impulse = self.limits_impulse.extract(ii); - } - } - } -} diff --git a/src/dynamics/solver/joint_constraint/revolute_position_constraint.rs b/src/dynamics/solver/joint_constraint/revolute_position_constraint.rs deleted file mode 100644 index ad470a4..0000000 --- a/src/dynamics/solver/joint_constraint/revolute_position_constraint.rs +++ /dev/null @@ -1,295 +0,0 @@ -use crate::dynamics::{ - IntegrationParameters, RevoluteJoint, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition, -}; -use crate::math::{AngularInertia, Isometry, Point, Real, Rotation, Vector}; -use crate::utils::{WAngularInertia, WCross, WCrossMatrix}; -use na::Unit; - -#[derive(Debug)] -pub(crate) struct RevolutePositionConstraint { - position1: usize, - position2: usize, - - local_com1: Point, - local_com2: Point, - - im1: Real, - im2: Real, - - ii1: AngularInertia, - ii2: AngularInertia, - - ang_inv_lhs: AngularInertia, - - limits_enabled: bool, - limits: [Real; 2], - - motor_last_angle: Real, - - local_anchor1: Point, - local_anchor2: Point, - - local_axis1: Unit>, - local_axis2: Unit>, - local_basis1: [Vector; 2], - local_basis2: [Vector; 2], -} - -impl RevolutePositionConstraint { - pub fn from_params( - rb1: (&RigidBodyMassProps, &RigidBodyIds), - rb2: (&RigidBodyMassProps, &RigidBodyIds), - cparams: &RevoluteJoint, - ) -> Self { - let (mprops1, ids1) = rb1; - let (mprops2, ids2) = rb2; - - let ii1 = mprops1.effective_world_inv_inertia_sqrt.squared(); - let ii2 = mprops2.effective_world_inv_inertia_sqrt.squared(); - let im1 = mprops1.effective_inv_mass; - let im2 = mprops2.effective_inv_mass; - let ang_inv_lhs = (ii1 + ii2).inverse(); - - Self { - im1, - im2, - ii1, - ii2, - ang_inv_lhs, - local_com1: mprops1.local_mprops.local_com, - local_com2: mprops2.local_mprops.local_com, - local_anchor1: cparams.local_anchor1, - local_anchor2: cparams.local_anchor2, - local_axis1: cparams.local_axis1, - local_axis2: cparams.local_axis2, - position1: ids1.active_set_offset, - position2: ids2.active_set_offset, - local_basis1: cparams.basis1, - local_basis2: cparams.basis2, - limits_enabled: cparams.limits_enabled, - limits: cparams.limits, - motor_last_angle: cparams.motor_last_angle, - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - let mut position1 = positions[self.position1 as usize]; - let mut position2 = positions[self.position2 as usize]; - - /* - * Linear part. - */ - { - let anchor1 = position1 * self.local_anchor1; - let anchor2 = position2 * self.local_anchor2; - - let r1 = anchor1 - position1 * self.local_com1; - let r2 = anchor2 - position2 * self.local_com2; - - // TODO: don't do the "to_matrix". - let lhs = (self - .ii2 - .quadform(&r2.gcross_matrix()) - .add_diagonal(self.im2) - + self - .ii1 - .quadform(&r1.gcross_matrix()) - .add_diagonal(self.im1)) - .into_matrix(); - let inv_lhs = lhs.try_inverse().unwrap(); - - let delta_tra = anchor2 - anchor1; - let lin_error = delta_tra * params.joint_erp; - let lin_impulse = inv_lhs * lin_error; - - let rot1 = self.ii1 * r1.gcross(lin_impulse); - let rot2 = self.ii2 * r2.gcross(lin_impulse); - position1.rotation = Rotation::new(rot1) * position1.rotation; - position2.rotation = Rotation::new(-rot2) * position2.rotation; - position1.translation.vector += self.im1 * lin_impulse; - position2.translation.vector -= self.im2 * lin_impulse; - } - - /* - * Angular part. - */ - { - let axis1 = position1 * self.local_axis1; - let axis2 = position2 * self.local_axis2; - let delta_rot = - Rotation::rotation_between_axis(&axis1, &axis2).unwrap_or_else(Rotation::identity); - let ang_error = delta_rot.scaled_axis() * params.joint_erp; - let ang_impulse = self.ang_inv_lhs.transform_vector(ang_error); - - position1.rotation = - Rotation::new(self.ii1.transform_vector(ang_impulse)) * position1.rotation; - position2.rotation = - Rotation::new(self.ii2.transform_vector(-ang_impulse)) * position2.rotation; - } - - /* - * Limits part. - */ - if self.limits_enabled { - let angle = RevoluteJoint::estimate_motor_angle_from_params( - &(position1 * self.local_axis1), - &(position1 * self.local_basis1[0]), - &(position2 * self.local_basis2[0]), - self.motor_last_angle, - ); - let ang_error = (angle - self.limits[1]).max(0.0) - (self.limits[0] - angle).max(0.0); - - if ang_error != 0.0 { - let axis2 = position2 * self.local_axis2; - let ang_impulse = self - .ang_inv_lhs - .transform_vector(*axis2 * ang_error * params.joint_erp); - - position1.rotation = - Rotation::new(self.ii1.transform_vector(ang_impulse)) * position1.rotation; - position2.rotation = - Rotation::new(self.ii2.transform_vector(-ang_impulse)) * position2.rotation; - } - } - - positions[self.position1 as usize] = position1; - positions[self.position2 as usize] = position2; - } -} - -#[derive(Debug)] -pub(crate) struct RevolutePositionGroundConstraint { - position2: usize, - local_com2: Point, - im2: Real, - ii2: AngularInertia, - anchor1: Point, - local_anchor2: Point, - axis1: Unit>, - basis1: [Vector; 2], - - limits_enabled: bool, - limits: [Real; 2], - - motor_last_angle: Real, - - local_axis2: Unit>, - local_basis2: [Vector; 2], -} - -impl RevolutePositionGroundConstraint { - pub fn from_params( - rb1: &RigidBodyPosition, - rb2: (&RigidBodyMassProps, &RigidBodyIds), - cparams: &RevoluteJoint, - flipped: bool, - ) -> Self { - let poss1 = rb1; - let (mprops2, ids2) = rb2; - - let anchor1; - let local_anchor2; - let axis1; - let local_axis2; - let basis1; - let local_basis2; - - if flipped { - anchor1 = poss1.next_position * cparams.local_anchor2; - local_anchor2 = cparams.local_anchor1; - axis1 = poss1.next_position * cparams.local_axis2; - local_axis2 = cparams.local_axis1; - basis1 = [ - poss1.next_position * cparams.basis2[0], - poss1.next_position * cparams.basis2[1], - ]; - local_basis2 = cparams.basis1; - } else { - anchor1 = poss1.next_position * cparams.local_anchor1; - local_anchor2 = cparams.local_anchor2; - axis1 = poss1.next_position * cparams.local_axis1; - local_axis2 = cparams.local_axis2; - basis1 = [ - poss1.next_position * cparams.basis1[0], - poss1.next_position * cparams.basis1[1], - ]; - local_basis2 = cparams.basis2; - }; - - Self { - anchor1, - local_anchor2, - im2: mprops2.effective_inv_mass, - ii2: mprops2.effective_world_inv_inertia_sqrt.squared(), - local_com2: mprops2.local_mprops.local_com, - axis1, - local_axis2, - position2: ids2.active_set_offset, - basis1, - local_basis2, - limits_enabled: cparams.limits_enabled, - limits: cparams.limits, - motor_last_angle: cparams.motor_last_angle, - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - let mut position2 = positions[self.position2 as usize]; - - /* - * Linear part. - */ - { - let anchor2 = position2 * self.local_anchor2; - - let r2 = anchor2 - position2 * self.local_com2; - // TODO: don't the the "to_matrix". - let lhs = self - .ii2 - .quadform(&r2.gcross_matrix()) - .add_diagonal(self.im2) - .into_matrix(); - let inv_lhs = lhs.try_inverse().unwrap(); - - let delta_tra = anchor2 - self.anchor1; - let lin_error = delta_tra * params.joint_erp; - let lin_impulse = inv_lhs * lin_error; - - let rot2 = self.ii2 * r2.gcross(lin_impulse); - position2.rotation = Rotation::new(-rot2) * position2.rotation; - position2.translation.vector -= self.im2 * lin_impulse; - } - - /* - * Angular part. - */ - { - let axis2 = position2 * self.local_axis2; - let delta_rot = Rotation::rotation_between_axis(&self.axis1, &axis2) - .unwrap_or_else(Rotation::identity); - let ang_error = delta_rot.scaled_axis() * params.joint_erp; - position2.rotation = Rotation::new(-ang_error) * position2.rotation; - } - - /* - * Limits part. - */ - if self.limits_enabled { - let angle = RevoluteJoint::estimate_motor_angle_from_params( - &self.axis1, - &self.basis1[0], - &(position2 * self.local_basis2[0]), - self.motor_last_angle, - ); - let ang_error = (angle - self.limits[1]).max(0.0) - (self.limits[0] - angle).max(0.0); - - if ang_error != 0.0 { - let axis2 = position2 * self.local_axis2; - position2.rotation = - Rotation::new(*axis2 * -ang_error * params.joint_erp) * position2.rotation; - } - } - - positions[self.position2 as usize] = position2; - } -} diff --git a/src/dynamics/solver/joint_constraint/revolute_position_constraint_wide.rs b/src/dynamics/solver/joint_constraint/revolute_position_constraint_wide.rs deleted file mode 100644 index 5881cb3..0000000 --- a/src/dynamics/solver/joint_constraint/revolute_position_constraint_wide.rs +++ /dev/null @@ -1,71 +0,0 @@ -use super::{RevolutePositionConstraint, RevolutePositionGroundConstraint}; -use crate::dynamics::{ - IntegrationParameters, RevoluteJoint, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition, -}; -use crate::math::{Isometry, Real, SIMD_WIDTH}; - -// TODO: this does not uses SIMD optimizations yet. -#[derive(Debug)] -pub(crate) struct WRevolutePositionConstraint { - constraints: [RevolutePositionConstraint; SIMD_WIDTH], -} - -impl WRevolutePositionConstraint { - pub fn from_params( - rbs1: ( - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - rbs2: ( - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - cparams: [&RevoluteJoint; SIMD_WIDTH], - ) -> Self { - Self { - constraints: gather![|ii| RevolutePositionConstraint::from_params( - (rbs1.0[ii], rbs1.1[ii]), - (rbs2.0[ii], rbs2.1[ii]), - cparams[ii] - )], - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - for constraint in &self.constraints { - constraint.solve(params, positions); - } - } -} - -#[derive(Debug)] -pub(crate) struct WRevolutePositionGroundConstraint { - constraints: [RevolutePositionGroundConstraint; SIMD_WIDTH], -} - -impl WRevolutePositionGroundConstraint { - pub fn from_params( - rbs1: [&RigidBodyPosition; SIMD_WIDTH], - rbs2: ( - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - cparams: [&RevoluteJoint; SIMD_WIDTH], - flipped: [bool; SIMD_WIDTH], - ) -> Self { - Self { - constraints: gather![|ii| RevolutePositionGroundConstraint::from_params( - rbs1[ii], - (rbs2.0[ii], rbs2.1[ii]), - cparams[ii], - flipped[ii] - )], - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - for constraint in &self.constraints { - constraint.solve(params, positions); - } - } -} diff --git a/src/dynamics/solver/joint_constraint/revolute_velocity_constraint.rs b/src/dynamics/solver/joint_constraint/revolute_velocity_constraint.rs deleted file mode 100644 index ca15cd9..0000000 --- a/src/dynamics/solver/joint_constraint/revolute_velocity_constraint.rs +++ /dev/null @@ -1,750 +0,0 @@ -use crate::dynamics::solver::{AnyJointVelocityConstraint, DeltaVel}; -use crate::dynamics::{ - IntegrationParameters, JointGraphEdge, JointIndex, JointParams, RevoluteJoint, RigidBodyIds, - RigidBodyMassProps, RigidBodyPosition, RigidBodyVelocity, -}; -use crate::math::{AngularInertia, Real, Rotation, Vector}; -use crate::utils::{WAngularInertia, WCross, WCrossMatrix}; -use na::{Cholesky, Matrix3x2, Matrix5, UnitQuaternion, Vector5}; - -#[derive(Debug)] -pub(crate) struct RevoluteVelocityConstraint { - mj_lambda1: usize, - mj_lambda2: usize, - - joint_id: JointIndex, - - r1: Vector, - r2: Vector, - - inv_lhs: Matrix5, - rhs: Vector5, - impulse: Vector5, - - motor_inv_lhs: Real, - motor_rhs: Real, - motor_impulse: Real, - motor_max_impulse: Real, - motor_angle: Real, // Exists only to write it back into the joint. - - motor_axis1: Vector, - motor_axis2: Vector, - - limits_active: bool, - limits_impulse: Real, - limits_rhs: Real, - limits_inv_lhs: Real, - limits_impulse_limits: (Real, Real), - - basis1: Matrix3x2, - basis2: Matrix3x2, - - im1: Real, - im2: Real, - - ii1_sqrt: AngularInertia, - ii2_sqrt: AngularInertia, -} - -impl RevoluteVelocityConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: JointIndex, - rb1: ( - &RigidBodyPosition, - &RigidBodyVelocity, - &RigidBodyMassProps, - &RigidBodyIds, - ), - rb2: ( - &RigidBodyPosition, - &RigidBodyVelocity, - &RigidBodyMassProps, - &RigidBodyIds, - ), - joint: &RevoluteJoint, - ) -> Self { - let (poss1, vels1, mprops1, ids1) = rb1; - let (poss2, vels2, mprops2, ids2) = rb2; - - // Linear part. - let anchor1 = poss1.position * joint.local_anchor1; - let anchor2 = poss2.position * joint.local_anchor2; - let basis1 = Matrix3x2::from_columns(&[ - poss1.position * joint.basis1[0], - poss1.position * joint.basis1[1], - ]); - - let basis2 = Matrix3x2::from_columns(&[ - poss2.position * joint.basis2[0], - poss2.position * joint.basis2[1], - ]); - let basis_projection2 = basis2 * basis2.transpose(); - let basis2 = basis_projection2 * basis1; - - let im1 = mprops1.effective_inv_mass; - let im2 = mprops2.effective_inv_mass; - - let ii1 = mprops1.effective_world_inv_inertia_sqrt.squared(); - let r1 = anchor1 - mprops1.world_com; - let r1_mat = r1.gcross_matrix(); - - let ii2 = mprops2.effective_world_inv_inertia_sqrt.squared(); - let r2 = anchor2 - mprops2.world_com; - let r2_mat = r2.gcross_matrix(); - - let mut lhs = Matrix5::zeros(); - - let lhs00 = - ii2.quadform(&r2_mat).add_diagonal(im2) + ii1.quadform(&r1_mat).add_diagonal(im1); - let lhs10 = basis2.tr_mul(&(ii2 * r2_mat)) + basis1.tr_mul(&(ii1 * r1_mat)); - let lhs11 = (ii1.quadform3x2(&basis1) + ii2.quadform3x2(&basis2)).into_matrix(); - - // Note that Cholesky won't read the upper-right part - // of lhs so we don't have to fill it. - lhs.fixed_slice_mut::<3, 3>(0, 0) - .copy_from(&lhs00.into_matrix()); - lhs.fixed_slice_mut::<2, 3>(3, 0).copy_from(&lhs10); - lhs.fixed_slice_mut::<2, 2>(3, 3).copy_from(&lhs11); - - let inv_lhs = Cholesky::new_unchecked(lhs).inverse(); - - let linvel_err = - (vels2.linvel + vels2.angvel.gcross(r2)) - (vels1.linvel + vels1.angvel.gcross(r1)); - let angvel_err = basis2.tr_mul(&vels2.angvel) - basis1.tr_mul(&vels1.angvel); - - let mut rhs = Vector5::new( - linvel_err.x, - linvel_err.y, - linvel_err.z, - angvel_err.x, - angvel_err.y, - ) * params.velocity_solve_fraction; - - let velocity_based_erp_inv_dt = params.velocity_based_erp_inv_dt(); - if velocity_based_erp_inv_dt != 0.0 { - let lin_err = anchor2 - anchor1; - - let axis1 = poss1.position * joint.local_axis1; - let axis2 = poss2.position * joint.local_axis2; - - let axis_error = axis1.cross(&axis2); - let ang_err = (basis2.tr_mul(&axis_error) + basis1.tr_mul(&axis_error)) * 0.5; - - rhs += Vector5::new(lin_err.x, lin_err.y, lin_err.z, ang_err.x, ang_err.y) - * velocity_based_erp_inv_dt; - } - - /* - * Motor. - */ - let motor_axis1 = poss1.position * *joint.local_axis1; - let motor_axis2 = poss2.position * *joint.local_axis2; - let mut motor_rhs = 0.0; - let mut motor_inv_lhs = 0.0; - let mut motor_angle = 0.0; - let motor_max_impulse = joint.motor_max_impulse; - - let (stiffness, damping, gamma, keep_lhs) = joint.motor_model.combine_coefficients( - params.dt, - joint.motor_stiffness, - joint.motor_damping, - ); - - if stiffness != 0.0 || joint.limits_enabled { - motor_angle = joint.estimate_motor_angle(&poss1.position, &poss2.position); - } - - if stiffness != 0.0 { - motor_rhs += (motor_angle - joint.motor_target_pos) * stiffness; - } - - if damping != 0.0 { - let curr_vel = vels2.angvel.dot(&motor_axis2) - vels1.angvel.dot(&motor_axis1); - motor_rhs += (curr_vel - joint.motor_target_vel) * damping; - } - - if stiffness != 0.0 || damping != 0.0 { - motor_inv_lhs = if keep_lhs { - crate::utils::inv( - motor_axis2.dot(&ii2.transform_vector(motor_axis2)) - + motor_axis1.dot(&ii1.transform_vector(motor_axis1)), - ) * gamma - } else { - gamma - }; - motor_rhs /= gamma; - } - - /* - * Setup limit constraint. - */ - let mut limits_active = false; - let mut limits_rhs = 0.0; - let mut limits_inv_lhs = 0.0; - let mut limits_impulse_limits = (0.0, 0.0); - let mut limits_impulse = 0.0; - - if joint.limits_enabled { - // TODO: we should allow predictive constraint activation. - - let (min_limit, max_limit) = (joint.limits[0], joint.limits[1]); - let min_enabled = motor_angle < min_limit; - let max_enabled = max_limit < motor_angle; - - limits_impulse_limits.0 = if max_enabled { -Real::INFINITY } else { 0.0 }; - limits_impulse_limits.1 = if min_enabled { Real::INFINITY } else { 0.0 }; - limits_active = min_enabled || max_enabled; - - if limits_active { - limits_rhs = (vels2.angvel.dot(&motor_axis2) - vels1.angvel.dot(&motor_axis1)) - * params.velocity_solve_fraction; - - limits_rhs += ((motor_angle - max_limit).max(0.0) - - (min_limit - motor_angle).max(0.0)) - * velocity_based_erp_inv_dt; - - limits_inv_lhs = crate::utils::inv( - motor_axis2.dot(&ii2.transform_vector(motor_axis2)) - + motor_axis1.dot(&ii1.transform_vector(motor_axis1)), - ); - - limits_impulse = joint - .limits_impulse - .max(limits_impulse_limits.0) - .min(limits_impulse_limits.1) - * params.warmstart_coeff; - } - } - - /* - * Adjust the warmstart impulse. - * If the velocity along the free axis is somewhat high, - * we need to adjust the angular warmstart impulse because it - * may have a direction that is too different than last frame, - * making it counter-productive. - */ - let mut impulse = joint.impulse * params.warmstart_coeff; - let axis_rot = Rotation::rotation_between(&joint.prev_axis1, &motor_axis1) - .unwrap_or_else(UnitQuaternion::identity); - let rotated_impulse = basis1.tr_mul(&(axis_rot * joint.world_ang_impulse)); - impulse[3] = rotated_impulse.x * params.warmstart_coeff; - impulse[4] = rotated_impulse.y * params.warmstart_coeff; - let motor_impulse = na::clamp(joint.motor_impulse, -motor_max_impulse, motor_max_impulse) - * params.warmstart_coeff; - - RevoluteVelocityConstraint { - joint_id, - mj_lambda1: ids1.active_set_offset, - mj_lambda2: ids2.active_set_offset, - im1, - ii1_sqrt: mprops1.effective_world_inv_inertia_sqrt, - basis1, - basis2, - im2, - ii2_sqrt: mprops2.effective_world_inv_inertia_sqrt, - impulse, - inv_lhs, - rhs, - r1, - r2, - motor_rhs, - motor_inv_lhs, - motor_max_impulse, - motor_axis1, - motor_axis2, - motor_impulse, - motor_angle, - limits_impulse, - limits_impulse_limits, - limits_active, - limits_inv_lhs, - limits_rhs, - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = mj_lambdas[self.mj_lambda1 as usize]; - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - let lin_impulse1 = self.impulse.fixed_rows::<3>(0).into_owned(); - let lin_impulse2 = self.impulse.fixed_rows::<3>(0).into_owned(); - let ang_impulse1 = self.basis1 * self.impulse.fixed_rows::<2>(3).into_owned(); - let ang_impulse2 = self.basis2 * self.impulse.fixed_rows::<2>(3).into_owned(); - - mj_lambda1.linear += self.im1 * lin_impulse1; - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(ang_impulse1 + self.r1.gcross(lin_impulse1)); - - mj_lambda2.linear -= self.im2 * lin_impulse2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse2 + self.r2.gcross(lin_impulse2)); - - /* - * Warmstart motor. - */ - if self.motor_inv_lhs != 0.0 { - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(self.motor_axis1 * self.motor_impulse); - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(self.motor_axis2 * self.motor_impulse); - } - - /* - * Warmstart limits. - */ - if self.limits_active { - let limit_impulse1 = -self.motor_axis2 * self.limits_impulse; - let limit_impulse2 = self.motor_axis2 * self.limits_impulse; - mj_lambda1.angular += self.ii1_sqrt.transform_vector(limit_impulse1); - mj_lambda2.angular += self.ii2_sqrt.transform_vector(limit_impulse2); - } - - mj_lambdas[self.mj_lambda1 as usize] = mj_lambda1; - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - fn solve_dofs(&mut self, mj_lambda1: &mut DeltaVel, mj_lambda2: &mut DeltaVel) { - let ang_vel1 = self.ii1_sqrt.transform_vector(mj_lambda1.angular); - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let lin_dvel = (mj_lambda2.linear + ang_vel2.gcross(self.r2)) - - (mj_lambda1.linear + ang_vel1.gcross(self.r1)); - let ang_dvel = self.basis2.tr_mul(&ang_vel2) - self.basis1.tr_mul(&ang_vel1); - let rhs = - Vector5::new(lin_dvel.x, lin_dvel.y, lin_dvel.z, ang_dvel.x, ang_dvel.y) + self.rhs; - let impulse = self.inv_lhs * rhs; - self.impulse += impulse; - let lin_impulse1 = impulse.fixed_rows::<3>(0).into_owned(); - let lin_impulse2 = impulse.fixed_rows::<3>(0).into_owned(); - let ang_impulse1 = self.basis1 * impulse.fixed_rows::<2>(3).into_owned(); - let ang_impulse2 = self.basis2 * impulse.fixed_rows::<2>(3).into_owned(); - - mj_lambda1.linear += self.im1 * lin_impulse1; - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(ang_impulse1 + self.r1.gcross(lin_impulse1)); - - mj_lambda2.linear -= self.im2 * lin_impulse2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse2 + self.r2.gcross(lin_impulse2)); - } - - fn solve_limits(&mut self, mj_lambda1: &mut DeltaVel, mj_lambda2: &mut DeltaVel) { - if self.limits_active { - let limits_torquedir1 = -self.motor_axis2; - let limits_torquedir2 = self.motor_axis2; - - let ang_vel1 = self.ii1_sqrt.transform_vector(mj_lambda1.angular); - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let ang_dvel = limits_torquedir1.dot(&ang_vel1) - + limits_torquedir2.dot(&ang_vel2) - + self.limits_rhs; - let new_impulse = (self.limits_impulse - ang_dvel * self.limits_inv_lhs) - .max(self.limits_impulse_limits.0) - .min(self.limits_impulse_limits.1); - let dimpulse = new_impulse - self.limits_impulse; - self.limits_impulse = new_impulse; - - let ang_impulse1 = limits_torquedir1 * dimpulse; - let ang_impulse2 = limits_torquedir2 * dimpulse; - - mj_lambda1.angular += self.ii1_sqrt.transform_vector(ang_impulse1); - mj_lambda2.angular += self.ii2_sqrt.transform_vector(ang_impulse2); - } - } - - fn solve_motors(&mut self, mj_lambda1: &mut DeltaVel, mj_lambda2: &mut DeltaVel) { - if self.motor_inv_lhs != 0.0 { - let ang_vel1 = self.ii1_sqrt.transform_vector(mj_lambda1.angular); - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - let ang_dvel = ang_vel2.dot(&self.motor_axis2) - ang_vel1.dot(&self.motor_axis1); - let rhs = ang_dvel + self.motor_rhs; - - let new_motor_impulse = na::clamp( - self.motor_impulse + self.motor_inv_lhs * rhs, - -self.motor_max_impulse, - self.motor_max_impulse, - ); - let impulse = new_motor_impulse - self.motor_impulse; - self.motor_impulse = new_motor_impulse; - - mj_lambda1.angular += self.ii1_sqrt.transform_vector(self.motor_axis1 * impulse); - mj_lambda2.angular -= self.ii2_sqrt.transform_vector(self.motor_axis2 * impulse); - } - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = mj_lambdas[self.mj_lambda1 as usize]; - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - self.solve_limits(&mut mj_lambda1, &mut mj_lambda2); - self.solve_dofs(&mut mj_lambda1, &mut mj_lambda2); - self.solve_motors(&mut mj_lambda1, &mut mj_lambda2); - - mj_lambdas[self.mj_lambda1 as usize] = mj_lambda1; - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - let joint = &mut joints_all[self.joint_id].weight; - if let JointParams::RevoluteJoint(revolute) = &mut joint.params { - revolute.impulse = self.impulse; - let rot_part = self.impulse.fixed_rows::<2>(3).into_owned(); - revolute.world_ang_impulse = self.basis1 * rot_part; - revolute.prev_axis1 = self.motor_axis1; - revolute.motor_last_angle = self.motor_angle; - revolute.motor_impulse = self.motor_impulse; - revolute.limits_impulse = self.limits_impulse; - } - } -} - -#[derive(Debug)] -pub(crate) struct RevoluteVelocityGroundConstraint { - mj_lambda2: usize, - - joint_id: JointIndex, - - r2: Vector, - - inv_lhs: Matrix5, - rhs: Vector5, - impulse: Vector5, - - motor_axis2: Vector, - motor_inv_lhs: Real, - motor_rhs: Real, - motor_impulse: Real, - motor_max_impulse: Real, - motor_angle: Real, // Exists just for writing it into the joint. - - limits_active: bool, - limits_impulse: Real, - limits_rhs: Real, - limits_inv_lhs: Real, - limits_impulse_limits: (Real, Real), - - basis2: Matrix3x2, - - im2: Real, - - ii2_sqrt: AngularInertia, -} - -impl RevoluteVelocityGroundConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: JointIndex, - rb1: (&RigidBodyPosition, &RigidBodyVelocity, &RigidBodyMassProps), - rb2: ( - &RigidBodyPosition, - &RigidBodyVelocity, - &RigidBodyMassProps, - &RigidBodyIds, - ), - joint: &RevoluteJoint, - flipped: bool, - ) -> AnyJointVelocityConstraint { - let (poss1, vels1, mprops1) = rb1; - let (poss2, vels2, mprops2, ids2) = rb2; - - let anchor2; - let anchor1; - let axis1; - let axis2; - let basis1; - let basis2; - - if flipped { - axis1 = poss1.position * *joint.local_axis2; - axis2 = poss2.position * *joint.local_axis1; - anchor1 = poss1.position * joint.local_anchor2; - anchor2 = poss2.position * joint.local_anchor1; - basis1 = Matrix3x2::from_columns(&[ - poss1.position * joint.basis2[0], - poss1.position * joint.basis2[1], - ]); - basis2 = Matrix3x2::from_columns(&[ - poss2.position * joint.basis1[0], - poss2.position * joint.basis1[1], - ]); - } else { - axis1 = poss1.position * *joint.local_axis1; - axis2 = poss2.position * *joint.local_axis2; - anchor1 = poss1.position * joint.local_anchor1; - anchor2 = poss2.position * joint.local_anchor2; - basis1 = Matrix3x2::from_columns(&[ - poss1.position * joint.basis1[0], - poss1.position * joint.basis1[1], - ]); - basis2 = Matrix3x2::from_columns(&[ - poss2.position * joint.basis2[0], - poss2.position * joint.basis2[1], - ]); - }; - - let basis_projection2 = basis2 * basis2.transpose(); - let basis2 = basis_projection2 * basis1; - let im2 = mprops2.effective_inv_mass; - let ii2 = mprops2.effective_world_inv_inertia_sqrt.squared(); - let r1 = anchor1 - mprops1.world_com; - let r2 = anchor2 - mprops2.world_com; - let r2_mat = r2.gcross_matrix(); - - let mut lhs = Matrix5::zeros(); - let lhs00 = ii2.quadform(&r2_mat).add_diagonal(im2); - let lhs10 = basis2.tr_mul(&(ii2 * r2_mat)); - let lhs11 = ii2.quadform3x2(&basis2).into_matrix(); - - // Note that cholesky won't read the upper-right part - // of lhs so we don't have to fill it. - lhs.fixed_slice_mut::<3, 3>(0, 0) - .copy_from(&lhs00.into_matrix()); - lhs.fixed_slice_mut::<2, 3>(3, 0).copy_from(&lhs10); - lhs.fixed_slice_mut::<2, 2>(3, 3).copy_from(&lhs11); - - let inv_lhs = Cholesky::new_unchecked(lhs).inverse(); - - let linvel_err = - (vels2.linvel + vels2.angvel.gcross(r2)) - (vels1.linvel + vels1.angvel.gcross(r1)); - let angvel_err = basis2.tr_mul(&vels2.angvel) - basis1.tr_mul(&vels1.angvel); - let mut rhs = Vector5::new( - linvel_err.x, - linvel_err.y, - linvel_err.z, - angvel_err.x, - angvel_err.y, - ) * params.velocity_solve_fraction; - - let velocity_based_erp_inv_dt = params.velocity_based_erp_inv_dt(); - if velocity_based_erp_inv_dt != 0.0 { - let lin_err = anchor2 - anchor1; - - let (axis1, axis2); - if flipped { - axis1 = poss1.position * joint.local_axis2; - axis2 = poss2.position * joint.local_axis1; - } else { - axis1 = poss1.position * joint.local_axis1; - axis2 = poss2.position * joint.local_axis2; - } - let axis_error = axis1.cross(&axis2); - let ang_err = basis2.tr_mul(&axis_error); - - rhs += Vector5::new(lin_err.x, lin_err.y, lin_err.z, ang_err.x, ang_err.y) - * velocity_based_erp_inv_dt; - } - - /* - * Motor part. - */ - let mut motor_rhs = 0.0; - let mut motor_inv_lhs = 0.0; - let mut motor_angle = 0.0; - let motor_max_impulse = joint.motor_max_impulse; - - let (stiffness, damping, gamma, keep_lhs) = joint.motor_model.combine_coefficients( - params.dt, - joint.motor_stiffness, - joint.motor_damping, - ); - - if stiffness != 0.0 || joint.limits_enabled { - motor_angle = joint.estimate_motor_angle(&poss1.position, &poss2.position); - } - - if stiffness != 0.0 { - motor_rhs += (motor_angle - joint.motor_target_pos) * stiffness; - } - - if damping != 0.0 { - let curr_vel = vels2.angvel.dot(&axis2) - vels1.angvel.dot(&axis1); - motor_rhs += (curr_vel - joint.motor_target_vel) * damping; - } - - if stiffness != 0.0 || damping != 0.0 { - motor_inv_lhs = if keep_lhs { - crate::utils::inv(axis2.dot(&ii2.transform_vector(axis2))) * gamma - } else { - gamma - }; - motor_rhs /= gamma; - } - - let motor_impulse = na::clamp(joint.motor_impulse, -motor_max_impulse, motor_max_impulse) - * params.warmstart_coeff; - - /* - * Setup limit constraint. - */ - let mut limits_active = false; - let mut limits_rhs = 0.0; - let mut limits_inv_lhs = 0.0; - let mut limits_impulse_limits = (0.0, 0.0); - let mut limits_impulse = 0.0; - - if joint.limits_enabled { - // TODO: we should allow predictive constraint activation. - let (min_limit, max_limit) = (joint.limits[0], joint.limits[1]); - let min_enabled = motor_angle < min_limit; - let max_enabled = max_limit < motor_angle; - - limits_impulse_limits.0 = if max_enabled { -Real::INFINITY } else { 0.0 }; - limits_impulse_limits.1 = if min_enabled { Real::INFINITY } else { 0.0 }; - limits_active = min_enabled || max_enabled; - - if limits_active { - limits_rhs = (vels2.angvel.dot(&axis2) - vels1.angvel.dot(&axis1)) - * params.velocity_solve_fraction; - - limits_rhs += ((motor_angle - max_limit).max(0.0) - - (min_limit - motor_angle).max(0.0)) - * velocity_based_erp_inv_dt; - - limits_inv_lhs = crate::utils::inv(axis2.dot(&ii2.transform_vector(axis2))); - - limits_impulse = joint - .limits_impulse - .max(limits_impulse_limits.0) - .min(limits_impulse_limits.1) - * params.warmstart_coeff; - } - } - - let result = RevoluteVelocityGroundConstraint { - joint_id, - mj_lambda2: ids2.active_set_offset, - im2, - ii2_sqrt: mprops2.effective_world_inv_inertia_sqrt, - impulse: joint.impulse * params.warmstart_coeff, - basis2, - inv_lhs, - rhs, - r2, - motor_inv_lhs, - motor_impulse, - motor_axis2: axis2, - motor_max_impulse, - motor_rhs, - motor_angle, - limits_impulse, - limits_impulse_limits, - limits_active, - limits_inv_lhs, - limits_rhs, - }; - - AnyJointVelocityConstraint::RevoluteGroundConstraint(result) - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - let lin_impulse = self.impulse.fixed_rows::<3>(0).into_owned(); - let ang_impulse = self.basis2 * self.impulse.fixed_rows::<2>(3).into_owned(); - - mj_lambda2.linear -= self.im2 * lin_impulse; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - /* - * Motor - */ - if self.motor_inv_lhs != 0.0 { - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(self.motor_axis2 * self.motor_impulse); - } - - // Warmstart limits. - if self.limits_active { - let limit_impulse2 = self.motor_axis2 * self.limits_impulse; - mj_lambda2.angular += self.ii2_sqrt.transform_vector(limit_impulse2); - } - - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - fn solve_dofs(&mut self, mj_lambda2: &mut DeltaVel) { - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let lin_dvel = mj_lambda2.linear + ang_vel2.gcross(self.r2); - let ang_dvel = self.basis2.tr_mul(&ang_vel2); - let rhs = - Vector5::new(lin_dvel.x, lin_dvel.y, lin_dvel.z, ang_dvel.x, ang_dvel.y) + self.rhs; - let impulse = self.inv_lhs * rhs; - self.impulse += impulse; - let lin_impulse = impulse.fixed_rows::<3>(0).into_owned(); - let ang_impulse = self.basis2 * impulse.fixed_rows::<2>(3).into_owned(); - - mj_lambda2.linear -= self.im2 * lin_impulse; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - } - - fn solve_limits(&mut self, mj_lambda2: &mut DeltaVel) { - if self.limits_active { - let limits_torquedir2 = self.motor_axis2; - - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let ang_dvel = limits_torquedir2.dot(&ang_vel2) + self.limits_rhs; - let new_impulse = (self.limits_impulse - ang_dvel * self.limits_inv_lhs) - .max(self.limits_impulse_limits.0) - .min(self.limits_impulse_limits.1); - let dimpulse = new_impulse - self.limits_impulse; - self.limits_impulse = new_impulse; - - let ang_impulse2 = limits_torquedir2 * dimpulse; - mj_lambda2.angular += self.ii2_sqrt.transform_vector(ang_impulse2); - } - } - - fn solve_motors(&mut self, mj_lambda2: &mut DeltaVel) { - if self.motor_inv_lhs != 0.0 { - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - let ang_dvel = ang_vel2.dot(&self.motor_axis2); - let rhs = ang_dvel + self.motor_rhs; - - let new_motor_impulse = na::clamp( - self.motor_impulse + self.motor_inv_lhs * rhs, - -self.motor_max_impulse, - self.motor_max_impulse, - ); - let impulse = new_motor_impulse - self.motor_impulse; - self.motor_impulse = new_motor_impulse; - - mj_lambda2.angular -= self.ii2_sqrt.transform_vector(self.motor_axis2 * impulse); - } - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; - - self.solve_limits(&mut mj_lambda2); - self.solve_dofs(&mut mj_lambda2); - self.solve_motors(&mut mj_lambda2); - - mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; - } - - // FIXME: duplicated code with the non-ground constraint. - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - let joint = &mut joints_all[self.joint_id].weight; - if let JointParams::RevoluteJoint(revolute) = &mut joint.params { - revolute.impulse = self.impulse; - revolute.motor_impulse = self.motor_impulse; - revolute.motor_last_angle = self.motor_angle; - revolute.limits_impulse = self.limits_impulse; - } - } -} diff --git a/src/dynamics/solver/joint_constraint/revolute_velocity_constraint_wide.rs b/src/dynamics/solver/joint_constraint/revolute_velocity_constraint_wide.rs deleted file mode 100644 index 6e65b76..0000000 --- a/src/dynamics/solver/joint_constraint/revolute_velocity_constraint_wide.rs +++ /dev/null @@ -1,527 +0,0 @@ -use simba::simd::SimdValue; - -use crate::dynamics::solver::DeltaVel; -use crate::dynamics::{ - IntegrationParameters, JointGraphEdge, JointIndex, JointParams, RevoluteJoint, RigidBodyIds, - RigidBodyMassProps, RigidBodyPosition, RigidBodyVelocity, -}; -use crate::math::{ - AngVector, AngularInertia, Isometry, Point, Real, Rotation, SimdReal, Vector, SIMD_WIDTH, -}; -use crate::utils::{WAngularInertia, WCross, WCrossMatrix}; -use na::{Cholesky, Matrix3x2, Matrix5, Unit, Vector5}; - -#[derive(Debug)] -pub(crate) struct WRevoluteVelocityConstraint { - mj_lambda1: [usize; SIMD_WIDTH], - mj_lambda2: [usize; SIMD_WIDTH], - - joint_id: [JointIndex; SIMD_WIDTH], - - r1: Vector, - r2: Vector, - - inv_lhs: Matrix5, - rhs: Vector5, - impulse: Vector5, - - axis1: [Vector; SIMD_WIDTH], - basis1: Matrix3x2, - basis2: Matrix3x2, - - im1: SimdReal, - im2: SimdReal, - - ii1_sqrt: AngularInertia, - ii2_sqrt: AngularInertia, -} - -impl WRevoluteVelocityConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: [JointIndex; SIMD_WIDTH], - rbs1: ( - [&RigidBodyPosition; SIMD_WIDTH], - [&RigidBodyVelocity; SIMD_WIDTH], - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - rbs2: ( - [&RigidBodyPosition; SIMD_WIDTH], - [&RigidBodyVelocity; SIMD_WIDTH], - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - joints: [&RevoluteJoint; SIMD_WIDTH], - ) -> Self { - let (poss1, vels1, mprops1, ids1) = rbs1; - let (poss2, vels2, mprops2, ids2) = rbs2; - - let position1 = Isometry::from(gather![|ii| poss1[ii].position]); - let linvel1 = Vector::from(gather![|ii| vels1[ii].linvel]); - let angvel1 = AngVector::::from(gather![|ii| vels1[ii].angvel]); - let world_com1 = Point::from(gather![|ii| mprops1[ii].world_com]); - let im1 = SimdReal::from(gather![|ii| mprops1[ii].effective_inv_mass]); - let ii1_sqrt = AngularInertia::::from(gather![ - |ii| mprops1[ii].effective_world_inv_inertia_sqrt - ]); - let mj_lambda1 = gather![|ii| ids1[ii].active_set_offset]; - - let position2 = Isometry::from(gather![|ii| poss2[ii].position]); - let linvel2 = Vector::from(gather![|ii| vels2[ii].linvel]); - let angvel2 = AngVector::::from(gather![|ii| vels2[ii].angvel]); - let world_com2 = Point::from(gather![|ii| mprops2[ii].world_com]); - let im2 = SimdReal::from(gather![|ii| mprops2[ii].effective_inv_mass]); - let ii2_sqrt = AngularInertia::::from(gather![ - |ii| mprops2[ii].effective_world_inv_inertia_sqrt - ]); - let mj_lambda2 = gather![|ii| ids2[ii].active_set_offset]; - - let local_anchor1 = Point::from(gather![|ii| joints[ii].local_anchor1]); - let local_anchor2 = Point::from(gather![|ii| joints[ii].local_anchor2]); - let local_basis1 = [ - Vector::from(gather![|ii| joints[ii].basis1[0]]), - Vector::from(gather![|ii| joints[ii].basis1[1]]), - ]; - let local_basis2 = [ - Vector::from(gather![|ii| joints[ii].basis2[0]]), - Vector::from(gather![|ii| joints[ii].basis2[1]]), - ]; - let impulse = Vector5::from(gather![|ii| joints[ii].impulse]); - - let anchor1 = position1 * local_anchor1; - let anchor2 = position2 * local_anchor2; - let basis1 = - Matrix3x2::from_columns(&[position1 * local_basis1[0], position1 * local_basis1[1]]); - let basis2 = - Matrix3x2::from_columns(&[position2 * local_basis2[0], position2 * local_basis2[1]]); - let basis_projection2 = basis2 * basis2.transpose(); - let basis2 = basis_projection2 * basis1; - - let ii1 = ii1_sqrt.squared(); - let r1 = anchor1 - world_com1; - let r1_mat = r1.gcross_matrix(); - - let ii2 = ii2_sqrt.squared(); - let r2 = anchor2 - world_com2; - let r2_mat = r2.gcross_matrix(); - - let mut lhs = Matrix5::zeros(); - let lhs00 = - ii2.quadform(&r2_mat).add_diagonal(im2) + ii1.quadform(&r1_mat).add_diagonal(im1); - let lhs10 = basis1.tr_mul(&(ii2 * r2_mat)) + basis2.tr_mul(&(ii1 * r1_mat)); - let lhs11 = (ii1.quadform3x2(&basis1) + ii2.quadform3x2(&basis2)).into_matrix(); - - // Note that Cholesky won't read the upper-right part - // of lhs so we don't have to fill it. - lhs.fixed_slice_mut::<3, 3>(0, 0) - .copy_from(&lhs00.into_matrix()); - lhs.fixed_slice_mut::<2, 3>(3, 0).copy_from(&lhs10); - lhs.fixed_slice_mut::<2, 2>(3, 3).copy_from(&lhs11); - - let inv_lhs = Cholesky::new_unchecked(lhs).inverse(); - - let linvel_err = linvel2 + angvel2.gcross(r2) - linvel1 - angvel1.gcross(r1); - let angvel_err = basis2.tr_mul(&angvel2) - basis1.tr_mul(&angvel1); - - let mut rhs = Vector5::new( - linvel_err.x, - linvel_err.y, - linvel_err.z, - angvel_err.x, - angvel_err.y, - ) * SimdReal::splat(params.velocity_solve_fraction); - - let velocity_based_erp_inv_dt = params.velocity_based_erp_inv_dt(); - if velocity_based_erp_inv_dt != 0.0 { - let velocity_based_erp_inv_dt = SimdReal::splat(velocity_based_erp_inv_dt); - - let lin_err = anchor2 - anchor1; - - let local_axis1 = Unit::>::from(gather![|ii| joints[ii].local_axis1]); - let local_axis2 = Unit::>::from(gather![|ii| joints[ii].local_axis2]); - - let axis1 = position1 * local_axis1; - let axis2 = position2 * local_axis2; - - let axis_error = axis1.cross(&axis2); - let ang_err = - (basis2.tr_mul(&axis_error) + basis1.tr_mul(&axis_error)) * SimdReal::splat(0.5); - - rhs += Vector5::new(lin_err.x, lin_err.y, lin_err.z, ang_err.x, ang_err.y) - * velocity_based_erp_inv_dt; - } - - /* - * Adjust the warmstart impulse. - * If the velocity along the free axis is somewhat high, - * we need to adjust the angular warmstart impulse because it - * may have a direction that is too different than last frame, - * making it counter-productive. - */ - let warmstart_coeff = SimdReal::splat(params.warmstart_coeff); - let mut impulse = impulse * warmstart_coeff; - - let axis1 = gather![|ii| poss1[ii].position * *joints[ii].local_axis1]; - let rotated_impulse = Vector::from(gather![|ii| { - let axis_rot = Rotation::rotation_between(&joints[ii].prev_axis1, &axis1[ii]) - .unwrap_or_else(Rotation::identity); - axis_rot * joints[ii].world_ang_impulse - }]); - - let rotated_basis_impulse = basis1.tr_mul(&rotated_impulse); - impulse[3] = rotated_basis_impulse.x * warmstart_coeff; - impulse[4] = rotated_basis_impulse.y * warmstart_coeff; - - WRevoluteVelocityConstraint { - joint_id, - mj_lambda1, - mj_lambda2, - im1, - ii1_sqrt, - axis1, - basis1, - basis2, - im2, - ii2_sqrt, - impulse, - inv_lhs, - rhs, - r1, - r2, - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda1[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda1[ii] as usize].angular - ]), - }; - let mut mj_lambda2 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - let lin_impulse1 = self.impulse.fixed_rows::<3>(0).into_owned(); - let lin_impulse2 = self.impulse.fixed_rows::<3>(0).into_owned(); - let ang_impulse1 = self.basis1 * self.impulse.fixed_rows::<2>(3).into_owned(); - let ang_impulse2 = self.basis2 * self.impulse.fixed_rows::<2>(3).into_owned(); - - mj_lambda1.linear += lin_impulse1 * self.im1; - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(ang_impulse1 + self.r1.gcross(lin_impulse1)); - - mj_lambda2.linear -= lin_impulse2 * self.im2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse2 + self.r2.gcross(lin_impulse2)); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda1[ii] as usize].linear = mj_lambda1.linear.extract(ii); - mj_lambdas[self.mj_lambda1[ii] as usize].angular = mj_lambda1.angular.extract(ii); - } - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda1[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda1[ii] as usize].angular - ]), - }; - let mut mj_lambda2 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - let ang_vel1 = self.ii1_sqrt.transform_vector(mj_lambda1.angular); - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - - let lin_dvel = (mj_lambda2.linear + ang_vel2.gcross(self.r2)) - - (mj_lambda1.linear + ang_vel1.gcross(self.r1)); - let ang_dvel = self.basis2.tr_mul(&ang_vel2) - self.basis1.tr_mul(&ang_vel1); - let rhs = - Vector5::new(lin_dvel.x, lin_dvel.y, lin_dvel.z, ang_dvel.x, ang_dvel.y) + self.rhs; - let impulse = self.inv_lhs * rhs; - self.impulse += impulse; - let lin_impulse1 = impulse.fixed_rows::<3>(0).into_owned(); - let lin_impulse2 = impulse.fixed_rows::<3>(0).into_owned(); - let ang_impulse1 = self.basis1 * impulse.fixed_rows::<2>(3).into_owned(); - let ang_impulse2 = self.basis2 * impulse.fixed_rows::<2>(3).into_owned(); - - mj_lambda1.linear += lin_impulse1 * self.im1; - mj_lambda1.angular += self - .ii1_sqrt - .transform_vector(ang_impulse1 + self.r1.gcross(lin_impulse1)); - - mj_lambda2.linear -= lin_impulse2 * self.im2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse2 + self.r2.gcross(lin_impulse2)); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda1[ii] as usize].linear = mj_lambda1.linear.extract(ii); - mj_lambdas[self.mj_lambda1[ii] as usize].angular = mj_lambda1.angular.extract(ii); - } - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - let rot_part = self.impulse.fixed_rows::<2>(3).into_owned(); - let world_ang_impulse = self.basis1 * rot_part; - - for ii in 0..SIMD_WIDTH { - let joint = &mut joints_all[self.joint_id[ii]].weight; - if let JointParams::RevoluteJoint(rev) = &mut joint.params { - rev.impulse = self.impulse.extract(ii); - rev.world_ang_impulse = world_ang_impulse.extract(ii); - rev.prev_axis1 = self.axis1[ii]; - } - } - } -} - -#[derive(Debug)] -pub(crate) struct WRevoluteVelocityGroundConstraint { - mj_lambda2: [usize; SIMD_WIDTH], - - joint_id: [JointIndex; SIMD_WIDTH], - - r2: Vector, - - inv_lhs: Matrix5, - rhs: Vector5, - impulse: Vector5, - - basis2: Matrix3x2, - - im2: SimdReal, - - ii2_sqrt: AngularInertia, -} - -impl WRevoluteVelocityGroundConstraint { - pub fn from_params( - params: &IntegrationParameters, - joint_id: [JointIndex; SIMD_WIDTH], - rbs1: ( - [&RigidBodyPosition; SIMD_WIDTH], - [&RigidBodyVelocity; SIMD_WIDTH], - [&RigidBodyMassProps; SIMD_WIDTH], - ), - rbs2: ( - [&RigidBodyPosition; SIMD_WIDTH], - [&RigidBodyVelocity; SIMD_WIDTH], - [&RigidBodyMassProps; SIMD_WIDTH], - [&RigidBodyIds; SIMD_WIDTH], - ), - joints: [&RevoluteJoint; SIMD_WIDTH], - flipped: [bool; SIMD_WIDTH], - ) -> Self { - let (poss1, vels1, mprops1) = rbs1; - let (poss2, vels2, mprops2, ids2) = rbs2; - - let position1 = Isometry::from(gather![|ii| poss1[ii].position]); - let linvel1 = Vector::from(gather![|ii| vels1[ii].linvel]); - let angvel1 = AngVector::::from(gather![|ii| vels1[ii].angvel]); - let world_com1 = Point::from(gather![|ii| mprops1[ii].world_com]); - - let position2 = Isometry::from(gather![|ii| poss2[ii].position]); - let linvel2 = Vector::from(gather![|ii| vels2[ii].linvel]); - let angvel2 = AngVector::::from(gather![|ii| vels2[ii].angvel]); - let world_com2 = Point::from(gather![|ii| mprops2[ii].world_com]); - let im2 = SimdReal::from(gather![|ii| mprops2[ii].effective_inv_mass]); - let ii2_sqrt = AngularInertia::::from(gather![ - |ii| mprops2[ii].effective_world_inv_inertia_sqrt - ]); - let mj_lambda2 = gather![|ii| ids2[ii].active_set_offset]; - let impulse = Vector5::from(gather![|ii| joints[ii].impulse]); - - let local_anchor1 = Point::from(gather![|ii| if flipped[ii] { - joints[ii].local_anchor2 - } else { - joints[ii].local_anchor1 - }]); - let local_anchor2 = Point::from(gather![|ii| if flipped[ii] { - joints[ii].local_anchor1 - } else { - joints[ii].local_anchor2 - }]); - let basis1 = Matrix3x2::from_columns(&[ - position1 - * Vector::from(gather![|ii| if flipped[ii] { - joints[ii].basis2[0] - } else { - joints[ii].basis1[0] - }]), - position1 - * Vector::from(gather![|ii| if flipped[ii] { - joints[ii].basis2[1] - } else { - joints[ii].basis1[1] - }]), - ]); - let basis2 = Matrix3x2::from_columns(&[ - position2 - * Vector::from(gather![|ii| if flipped[ii] { - joints[ii].basis1[0] - } else { - joints[ii].basis2[0] - }]), - position2 - * Vector::from(gather![|ii| if flipped[ii] { - joints[ii].basis1[1] - } else { - joints[ii].basis2[1] - }]), - ]); - let basis_projection2 = basis2 * basis2.transpose(); - let basis2 = basis_projection2 * basis1; - - let anchor1 = position1 * local_anchor1; - let anchor2 = position2 * local_anchor2; - - let ii2 = ii2_sqrt.squared(); - let r1 = anchor1 - world_com1; - let r2 = anchor2 - world_com2; - let r2_mat = r2.gcross_matrix(); - - let mut lhs = Matrix5::zeros(); - let lhs00 = ii2.quadform(&r2_mat).add_diagonal(im2); - let lhs10 = basis2.tr_mul(&(ii2 * r2_mat)); - let lhs11 = ii2.quadform3x2(&basis2).into_matrix(); - - // Note that cholesky won't read the upper-right part - // of lhs so we don't have to fill it. - lhs.fixed_slice_mut::<3, 3>(0, 0) - .copy_from(&lhs00.into_matrix()); - lhs.fixed_slice_mut::<2, 3>(3, 0).copy_from(&lhs10); - lhs.fixed_slice_mut::<2, 2>(3, 3).copy_from(&lhs11); - - let inv_lhs = Cholesky::new_unchecked(lhs).inverse(); - - let linvel_err = (linvel2 + angvel2.gcross(r2)) - (linvel1 + angvel1.gcross(r1)); - let angvel_err = basis2.tr_mul(&angvel2) - basis1.tr_mul(&angvel1); - - let mut rhs = Vector5::new( - linvel_err.x, - linvel_err.y, - linvel_err.z, - angvel_err.x, - angvel_err.y, - ) * SimdReal::splat(params.velocity_solve_fraction); - - let velocity_based_erp_inv_dt = params.velocity_based_erp_inv_dt(); - if velocity_based_erp_inv_dt != 0.0 { - let velocity_based_erp_inv_dt = SimdReal::splat(velocity_based_erp_inv_dt); - - let lin_err = anchor2 - anchor1; - - let local_axis1 = Unit::>::from(gather![|ii| if flipped[ii] { - joints[ii].local_axis2 - } else { - joints[ii].local_axis1 - }]); - let local_axis2 = Unit::>::from(gather![|ii| if flipped[ii] { - joints[ii].local_axis1 - } else { - joints[ii].local_axis2 - }]); - let axis1 = position1 * local_axis1; - let axis2 = position2 * local_axis2; - - let axis_error = axis1.cross(&axis2); - let ang_err = basis2.tr_mul(&axis_error) - basis1.tr_mul(&axis_error); - - rhs += Vector5::new(lin_err.x, lin_err.y, lin_err.z, ang_err.x, ang_err.y) - * velocity_based_erp_inv_dt; - } - - WRevoluteVelocityGroundConstraint { - joint_id, - mj_lambda2, - im2, - ii2_sqrt, - impulse: impulse * SimdReal::splat(params.warmstart_coeff), - basis2, - inv_lhs, - rhs, - r2, - } - } - - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - let lin_impulse = self.impulse.fixed_rows::<3>(0).into_owned(); - let ang_impulse = self.basis2 * self.impulse.fixed_rows::<2>(3).into_owned(); - - mj_lambda2.linear -= lin_impulse * self.im2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - let ang_vel2 = self.ii2_sqrt.transform_vector(mj_lambda2.angular); - let lin_dvel = mj_lambda2.linear + ang_vel2.gcross(self.r2); - let ang_dvel = self.basis2.tr_mul(&ang_vel2); - let rhs = - Vector5::new(lin_dvel.x, lin_dvel.y, lin_dvel.z, ang_dvel.x, ang_dvel.y) + self.rhs; - let impulse = self.inv_lhs * rhs; - self.impulse += impulse; - let lin_impulse = impulse.fixed_rows::<3>(0).into_owned(); - let ang_impulse = self.basis2 * impulse.fixed_rows::<2>(3).into_owned(); - - mj_lambda2.linear -= lin_impulse * self.im2; - mj_lambda2.angular -= self - .ii2_sqrt - .transform_vector(ang_impulse + self.r2.gcross(lin_impulse)); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - // FIXME: duplicated code with the non-ground constraint. - pub fn writeback_impulses(&self, joints_all: &mut [JointGraphEdge]) { - for ii in 0..SIMD_WIDTH { - let joint = &mut joints_all[self.joint_id[ii]].weight; - if let JointParams::RevoluteJoint(rev) = &mut joint.params { - rev.impulse = self.impulse.extract(ii) - } - } - } -} diff --git a/src/dynamics/solver/mod.rs b/src/dynamics/solver/mod.rs index 33b6f9a..7607c28 100644 --- a/src/dynamics/solver/mod.rs +++ b/src/dynamics/solver/mod.rs @@ -3,26 +3,19 @@ pub(crate) use self::island_solver::IslandSolver; #[cfg(feature = "parallel")] pub(crate) use self::parallel_island_solver::{ParallelIslandSolver, ThreadContext}; #[cfg(feature = "parallel")] -pub(self) use self::parallel_position_solver::ParallelPositionSolver; -#[cfg(feature = "parallel")] pub(self) use self::parallel_solver_constraints::ParallelSolverConstraints; #[cfg(feature = "parallel")] pub(self) use self::parallel_velocity_solver::ParallelVelocitySolver; #[cfg(not(feature = "parallel"))] -pub(self) use self::position_solver::PositionSolver; -#[cfg(not(feature = "parallel"))] pub(self) use self::solver_constraints::SolverConstraints; #[cfg(not(feature = "parallel"))] pub(self) use self::velocity_solver::VelocitySolver; pub(self) use delta_vel::DeltaVel; +pub(self) use generic_velocity_constraint::*; +pub(self) use generic_velocity_constraint_element::*; pub(self) use interaction_groups::*; -pub(self) use joint_constraint::*; -pub(self) use position_constraint::*; -#[cfg(feature = "simd-is-enabled")] -pub(self) use position_constraint_wide::*; -pub(self) use position_ground_constraint::*; -#[cfg(feature = "simd-is-enabled")] -pub(self) use position_ground_constraint_wide::*; +pub(crate) use joint_constraint::MotorParameters; +pub use joint_constraint::*; pub(self) use velocity_constraint::*; pub(self) use velocity_constraint_element::*; #[cfg(feature = "simd-is-enabled")] @@ -34,6 +27,8 @@ pub(self) use velocity_ground_constraint_wide::*; mod categorization; mod delta_vel; +mod generic_velocity_constraint; +mod generic_velocity_constraint_element; mod interaction_groups; #[cfg(not(feature = "parallel"))] mod island_solver; @@ -41,19 +36,9 @@ mod joint_constraint; #[cfg(feature = "parallel")] mod parallel_island_solver; #[cfg(feature = "parallel")] -mod parallel_position_solver; -#[cfg(feature = "parallel")] mod parallel_solver_constraints; #[cfg(feature = "parallel")] mod parallel_velocity_solver; -mod position_constraint; -#[cfg(feature = "simd-is-enabled")] -mod position_constraint_wide; -mod position_ground_constraint; -#[cfg(feature = "simd-is-enabled")] -mod position_ground_constraint_wide; -#[cfg(not(feature = "parallel"))] -mod position_solver; #[cfg(not(feature = "parallel"))] mod solver_constraints; mod velocity_constraint; diff --git a/src/dynamics/solver/parallel_island_solver.rs b/src/dynamics/solver/parallel_island_solver.rs index bd6fc5d..e695b3d 100644 --- a/src/dynamics/solver/parallel_island_solver.rs +++ b/src/dynamics/solver/parallel_island_solver.rs @@ -3,14 +3,14 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use rayon::Scope; use crate::data::{BundleSet, ComponentSet, ComponentSetMut}; +use crate::dynamics::solver::generic_velocity_constraint::GenericVelocityConstraint; use crate::dynamics::solver::{ - AnyJointPositionConstraint, AnyJointVelocityConstraint, AnyPositionConstraint, - AnyVelocityConstraint, ParallelPositionSolver, ParallelSolverConstraints, + AnyJointVelocityConstraint, AnyVelocityConstraint, ParallelSolverConstraints, }; use crate::dynamics::{ - IntegrationParameters, IslandManager, JointGraphEdge, JointIndex, RigidBodyDamping, - RigidBodyForces, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition, RigidBodyType, - RigidBodyVelocity, + IntegrationParameters, IslandManager, JointGraphEdge, JointIndex, MultibodyJointSet, + RigidBodyDamping, RigidBodyForces, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition, + RigidBodyType, RigidBodyVelocity, }; use crate::geometry::{ContactManifold, ContactManifoldIndex}; use crate::math::{Isometry, Real}; @@ -69,8 +69,6 @@ pub(crate) struct ThreadContext { pub num_initialized_constraints: AtomicUsize, pub joint_constraint_initialization_index: AtomicUsize, pub num_initialized_joint_constraints: AtomicUsize, - pub warmstart_constraint_index: AtomicUsize, - pub num_warmstarted_constraints: AtomicUsize, pub solve_interaction_index: AtomicUsize, pub num_solved_interactions: AtomicUsize, pub impulse_writeback_index: AtomicUsize, @@ -79,10 +77,6 @@ pub(crate) struct ThreadContext { pub body_force_integration_index: AtomicUsize, pub num_force_integrated_bodies: AtomicUsize, pub num_integrated_bodies: AtomicUsize, - // Position solver. - pub solve_position_interaction_index: AtomicUsize, - pub num_solved_position_interactions: AtomicUsize, - pub position_writeback_index: AtomicUsize, } impl ThreadContext { @@ -93,8 +87,6 @@ impl ThreadContext { num_initialized_constraints: AtomicUsize::new(0), joint_constraint_initialization_index: AtomicUsize::new(0), num_initialized_joint_constraints: AtomicUsize::new(0), - num_warmstarted_constraints: AtomicUsize::new(0), - warmstart_constraint_index: AtomicUsize::new(0), solve_interaction_index: AtomicUsize::new(0), num_solved_interactions: AtomicUsize::new(0), impulse_writeback_index: AtomicUsize::new(0), @@ -103,9 +95,6 @@ impl ThreadContext { num_force_integrated_bodies: AtomicUsize::new(0), body_integration_index: AtomicUsize::new(0), num_integrated_bodies: AtomicUsize::new(0), - solve_position_interaction_index: AtomicUsize::new(0), - num_solved_position_interactions: AtomicUsize::new(0), - position_writeback_index: AtomicUsize::new(0), } } @@ -122,14 +111,13 @@ impl ThreadContext { } pub struct ParallelIslandSolver { - mj_lambdas: Vec>, + velocity_solver: ParallelVelocitySolver, positions: Vec>, parallel_groups: ParallelInteractionGroups, parallel_joint_groups: ParallelInteractionGroups, parallel_contact_constraints: - ParallelSolverConstraints, - parallel_joint_constraints: - ParallelSolverConstraints, + ParallelSolverConstraints, + parallel_joint_constraints: ParallelSolverConstraints, thread: ThreadContext, } @@ -142,7 +130,7 @@ impl Default for ParallelIslandSolver { impl ParallelIslandSolver { pub fn new() -> Self { Self { - mj_lambdas: Vec::new(), + velocity_solver: ParallelVelocitySolver::new(), positions: Vec::new(), parallel_groups: ParallelInteractionGroups::new(), parallel_joint_groups: ParallelInteractionGroups::new(), @@ -152,84 +140,7 @@ impl ParallelIslandSolver { } } - pub fn solve_position_constraints<'s, Bodies>( - &'s mut self, - scope: &Scope<'s>, - island_id: usize, - islands: &'s IslandManager, - params: &'s IntegrationParameters, - bodies: &'s mut Bodies, - ) where - Bodies: ComponentSetMut + ComponentSet, - { - let num_threads = rayon::current_num_threads(); - let num_task_per_island = num_threads; // (num_threads / num_islands).max(1); // TODO: not sure this is the best value. Also, perhaps it is better to interleave tasks of each island? - self.thread = ThreadContext::new(8); // TODO: could we compute some kind of optimal value here? - self.positions.clear(); - self.positions - .resize(islands.active_island(island_id).len(), Isometry::identity()); - - for _ in 0..num_task_per_island { - // We use AtomicPtr because it is Send+Sync while *mut is not. - // See https://internals.rust-lang.org/t/shouldnt-pointers-be-send-sync-or/8818 - let thread = &self.thread; - let positions = std::sync::atomic::AtomicPtr::new(&mut self.positions as *mut _); - let bodies = std::sync::atomic::AtomicPtr::new(bodies as *mut _); - let parallel_contact_constraints = - std::sync::atomic::AtomicPtr::new(&mut self.parallel_contact_constraints as *mut _); - let parallel_joint_constraints = - std::sync::atomic::AtomicPtr::new(&mut self.parallel_joint_constraints as *mut _); - - scope.spawn(move |_| { - // Transmute *mut -> &mut - let positions: &mut Vec> = - unsafe { std::mem::transmute(positions.load(Ordering::Relaxed)) }; - let bodies: &mut Bodies = - unsafe { std::mem::transmute(bodies.load(Ordering::Relaxed)) }; - let parallel_contact_constraints: &mut ParallelSolverConstraints = unsafe { - std::mem::transmute(parallel_contact_constraints.load(Ordering::Relaxed)) - }; - let parallel_joint_constraints: &mut ParallelSolverConstraints = unsafe { - std::mem::transmute(parallel_joint_constraints.load(Ordering::Relaxed)) - }; - - enable_flush_to_zero!(); // Ensure this is enabled on each thread. - - // Write results back to rigid bodies and integrate velocities. - let island_range = islands.active_island_range(island_id); - let active_bodies = &islands.active_dynamic_set[island_range]; - - concurrent_loop! { - let batch_size = thread.batch_size; - for handle in active_bodies[thread.body_integration_index, thread.num_integrated_bodies] { - let (rb_ids, rb_pos): (&RigidBodyIds, &RigidBodyPosition) = bodies.index_bundle(handle.0); - positions[rb_ids.active_set_offset] = rb_pos.next_position; - } - } - - ThreadContext::lock_until_ge(&thread.num_integrated_bodies, active_bodies.len()); - - ParallelPositionSolver::solve( - &thread, - params, - positions, - parallel_contact_constraints, - parallel_joint_constraints - ); - - // Write results back to rigid bodies. - concurrent_loop! { - let batch_size = thread.batch_size; - for handle in active_bodies[thread.position_writeback_index] { - let rb_ids: RigidBodyIds = *bodies.index(handle.0); - bodies.map_mut_internal(handle.0, |rb_pos: &mut RigidBodyPosition| rb_pos.next_position = positions[rb_ids.active_set_offset]); - } - } - }) - } - } - - pub fn init_constraints_and_solve_velocity_constraints<'s, Bodies>( + pub fn init_and_solve<'s, Bodies>( &'s mut self, scope: &Scope<'s>, island_id: usize, @@ -238,8 +149,9 @@ impl ParallelIslandSolver { bodies: &'s mut Bodies, manifolds: &'s mut Vec<&'s mut ContactManifold>, manifold_indices: &'s [ContactManifoldIndex], - joints: &'s mut Vec, + impulse_joints: &'s mut Vec, joint_indices: &[JointIndex], + multibody_joints: &mut MultibodyJointSet, ) where Bodies: ComponentSet + ComponentSetMut @@ -263,13 +175,14 @@ impl ParallelIslandSolver { island_id, islands, bodies, - joints, + impulse_joints, joint_indices, ); self.parallel_contact_constraints.init_constraint_groups( island_id, islands, bodies, + multibody_joints, manifolds, &self.parallel_groups, ); @@ -277,25 +190,25 @@ impl ParallelIslandSolver { island_id, islands, bodies, - joints, + multibody_joints, + impulse_joints, &self.parallel_joint_groups, ); - self.mj_lambdas.clear(); - self.mj_lambdas + self.velocity_solver.mj_lambdas.clear(); + self.velocity_solver + .mj_lambdas .resize(islands.active_island(island_id).len(), DeltaVel::zero()); - self.positions.clear(); - self.positions - .resize(islands.active_island(island_id).len(), Isometry::identity()); for _ in 0..num_task_per_island { // We use AtomicPtr because it is Send+Sync while *mut is not. // See https://internals.rust-lang.org/t/shouldnt-pointers-be-send-sync-or/8818 let thread = &self.thread; - let mj_lambdas = std::sync::atomic::AtomicPtr::new(&mut self.mj_lambdas as *mut _); + let velocity_solver = + std::sync::atomic::AtomicPtr::new(&mut self.velocity_solver as *mut _); let bodies = std::sync::atomic::AtomicPtr::new(bodies as *mut _); let manifolds = std::sync::atomic::AtomicPtr::new(manifolds as *mut _); - let joints = std::sync::atomic::AtomicPtr::new(joints as *mut _); + let impulse_joints = std::sync::atomic::AtomicPtr::new(impulse_joints as *mut _); let parallel_contact_constraints = std::sync::atomic::AtomicPtr::new(&mut self.parallel_contact_constraints as *mut _); let parallel_joint_constraints = @@ -303,18 +216,18 @@ impl ParallelIslandSolver { scope.spawn(move |_| { // Transmute *mut -> &mut - let mj_lambdas: &mut Vec> = - unsafe { std::mem::transmute(mj_lambdas.load(Ordering::Relaxed)) }; + let velocity_solver: &mut ParallelVelocitySolver = + unsafe { std::mem::transmute(velocity_solver.load(Ordering::Relaxed)) }; let bodies: &mut Bodies = unsafe { std::mem::transmute(bodies.load(Ordering::Relaxed)) }; let manifolds: &mut Vec<&mut ContactManifold> = unsafe { std::mem::transmute(manifolds.load(Ordering::Relaxed)) }; - let joints: &mut Vec = - unsafe { std::mem::transmute(joints.load(Ordering::Relaxed)) }; - let parallel_contact_constraints: &mut ParallelSolverConstraints = unsafe { + let impulse_joints: &mut Vec = + unsafe { std::mem::transmute(impulse_joints.load(Ordering::Relaxed)) }; + let parallel_contact_constraints: &mut ParallelSolverConstraints = unsafe { std::mem::transmute(parallel_contact_constraints.load(Ordering::Relaxed)) }; - let parallel_joint_constraints: &mut ParallelSolverConstraints = unsafe { + let parallel_joint_constraints: &mut ParallelSolverConstraints = unsafe { std::mem::transmute(parallel_joint_constraints.load(Ordering::Relaxed)) }; @@ -329,7 +242,7 @@ impl ParallelIslandSolver { let batch_size = thread.batch_size; for handle in active_bodies[thread.body_force_integration_index, thread.num_force_integrated_bodies] { let (rb_ids, rb_forces, rb_mass_props): (&RigidBodyIds, &RigidBodyForces, &RigidBodyMassProps) = bodies.index_bundle(handle.0); - let dvel = &mut mj_lambdas[rb_ids.active_set_offset]; + let dvel = &mut velocity_solver.mj_lambdas[rb_ids.active_set_offset]; // NOTE: `dvel.angular` is actually storing angular velocity delta multiplied // by the square root of the inertia tensor: @@ -345,7 +258,7 @@ impl ParallelIslandSolver { parallel_contact_constraints.fill_constraints(&thread, params, bodies, manifolds); - parallel_joint_constraints.fill_constraints(&thread, params, bodies, joints); + parallel_joint_constraints.fill_constraints(&thread, params, bodies, impulse_joints); ThreadContext::lock_until_ge( &thread.num_initialized_constraints, parallel_contact_constraints.constraint_descs.len(), @@ -355,14 +268,13 @@ impl ParallelIslandSolver { parallel_joint_constraints.constraint_descs.len(), ); - ParallelVelocitySolver::solve( + velocity_solver.solve( &thread, params, manifolds, - joints, - mj_lambdas, + impulse_joints, parallel_contact_constraints, - parallel_joint_constraints + parallel_joint_constraints, ); // Write results back to rigid bodies and integrate velocities. @@ -383,7 +295,7 @@ impl ParallelIslandSolver { let mut new_rb_pos = *rb_pos; let mut new_rb_vels = *rb_vels; - let dvels = mj_lambdas[rb_ids.active_set_offset]; + let dvels = velocity_solver.mj_lambdas[rb_ids.active_set_offset]; new_rb_vels.linvel += dvels.linear; new_rb_vels.angvel += rb_mprops.effective_world_inv_inertia_sqrt.transform_vector(dvels.angular); diff --git a/src/dynamics/solver/parallel_position_solver.rs b/src/dynamics/solver/parallel_position_solver.rs deleted file mode 100644 index ec480f5..0000000 --- a/src/dynamics/solver/parallel_position_solver.rs +++ /dev/null @@ -1,107 +0,0 @@ -use super::{AnyJointPositionConstraint, AnyPositionConstraint, ThreadContext}; -use crate::dynamics::solver::{ - AnyJointVelocityConstraint, AnyVelocityConstraint, ParallelSolverConstraints, -}; -use crate::dynamics::IntegrationParameters; -use crate::math::{Isometry, Real}; -use std::sync::atomic::Ordering; - -pub(crate) struct ParallelPositionSolver; - -impl ParallelPositionSolver { - pub fn solve( - thread: &ThreadContext, - params: &IntegrationParameters, - positions: &mut [Isometry], - contact_constraints: &mut ParallelSolverConstraints< - AnyVelocityConstraint, - AnyPositionConstraint, - >, - joint_constraints: &mut ParallelSolverConstraints< - AnyJointVelocityConstraint, - AnyJointPositionConstraint, - >, - ) { - if contact_constraints.constraint_descs.is_empty() - && joint_constraints.constraint_descs.is_empty() - { - return; - } - - /* - * Solve constraints. - */ - { - // Each thread will concurrently grab thread.batch_size constraint desc to - // solve. If the batch size is large enough for to cross the boundary of - // a palallel_desc_group, we have to wait util the current group is finished - // before starting the next one. - let mut start_index = thread - .solve_position_interaction_index - .fetch_add(thread.batch_size, Ordering::SeqCst); - let mut batch_size = thread.batch_size; - let contact_descs = &contact_constraints.constraint_descs[..]; - let joint_descs = &joint_constraints.constraint_descs[..]; - let mut target_num_desc = 0; - let mut shift = 0; - - for _ in 0..params.max_position_iterations { - macro_rules! solve { - ($part: expr) => { - // Joint groups. - for group in $part.parallel_desc_groups.windows(2) { - let num_descs_in_group = group[1] - group[0]; - target_num_desc += num_descs_in_group; - - while start_index < group[1] { - let end_index = (start_index + batch_size).min(group[1]); - - let constraints = if end_index == $part.constraint_descs.len() { - &mut $part.position_constraints - [$part.constraint_descs[start_index].0..] - } else { - &mut $part.position_constraints[$part.constraint_descs - [start_index] - .0 - ..$part.constraint_descs[end_index].0] - }; - - for constraint in constraints { - constraint.solve(params, positions); - } - - let num_solved = end_index - start_index; - batch_size -= num_solved; - - thread - .num_solved_position_interactions - .fetch_add(num_solved, Ordering::SeqCst); - - if batch_size == 0 { - start_index = thread - .solve_position_interaction_index - .fetch_add(thread.batch_size, Ordering::SeqCst); - start_index -= shift; - batch_size = thread.batch_size; - } else { - start_index += num_solved; - } - } - ThreadContext::lock_until_ge( - &thread.num_solved_position_interactions, - target_num_desc, - ); - } - }; - } - - solve!(joint_constraints); - shift += joint_descs.len(); - start_index -= joint_descs.len(); - solve!(contact_constraints); - shift += contact_descs.len(); - start_index -= contact_descs.len(); - } - } - } -} diff --git a/src/dynamics/solver/parallel_solver_constraints.rs b/src/dynamics/solver/parallel_solver_constraints.rs index 6b00b73..3871731 100644 --- a/src/dynamics/solver/parallel_solver_constraints.rs +++ b/src/dynamics/solver/parallel_solver_constraints.rs @@ -2,23 +2,20 @@ use super::ParallelInteractionGroups; use super::{AnyJointVelocityConstraint, AnyVelocityConstraint, ThreadContext}; use crate::data::ComponentSet; use crate::dynamics::solver::categorization::{categorize_contacts, categorize_joints}; -use crate::dynamics::solver::{ - AnyJointPositionConstraint, AnyPositionConstraint, InteractionGroups, PositionConstraint, - PositionGroundConstraint, VelocityConstraint, VelocityGroundConstraint, -}; +use crate::dynamics::solver::generic_velocity_constraint::GenericVelocityConstraint; +use crate::dynamics::solver::{InteractionGroups, VelocityConstraint, VelocityGroundConstraint}; use crate::dynamics::{ - IntegrationParameters, IslandManager, JointGraphEdge, RigidBodyIds, RigidBodyMassProps, - RigidBodyPosition, RigidBodyType, RigidBodyVelocity, + IntegrationParameters, IslandManager, JointGraphEdge, MultibodyJointSet, RigidBodyIds, + RigidBodyMassProps, RigidBodyPosition, RigidBodyType, RigidBodyVelocity, }; use crate::geometry::ContactManifold; +use crate::math::Real; #[cfg(feature = "simd-is-enabled")] use crate::{ - dynamics::solver::{ - WPositionConstraint, WPositionGroundConstraint, WVelocityConstraint, - WVelocityGroundConstraint, - }, + dynamics::solver::{WVelocityConstraint, WVelocityGroundConstraint}, math::SIMD_WIDTH, }; +use na::DVector; use std::sync::atomic::Ordering; // pub fn init_constraint_groups( @@ -27,13 +24,13 @@ use std::sync::atomic::Ordering; // bodies: &impl ComponentSet, // manifolds: &mut [&mut ContactManifold], // manifold_groups: &ParallelInteractionGroups, -// joints: &mut [JointGraphEdge], +// impulse_joints: &mut [JointGraphEdge], // joint_groups: &ParallelInteractionGroups, // ) { // self.part // .init_constraints_groups(island_id, bodies, manifolds, manifold_groups); // self.joint_part -// .init_constraints_groups(island_id, bodies, joints, joint_groups); +// .init_constraints_groups(island_id, bodies, impulse_joints, joint_groups); // } pub(crate) enum ConstraintDesc { @@ -45,45 +42,52 @@ pub(crate) enum ConstraintDesc { GroundGrouped([usize; SIMD_WIDTH]), } -pub(crate) struct ParallelSolverConstraints { +pub(crate) struct ParallelSolverConstraints { + pub generic_jacobians: DVector, pub not_ground_interactions: Vec, pub ground_interactions: Vec, + pub generic_not_ground_interactions: Vec, + pub generic_ground_interactions: Vec, pub interaction_groups: InteractionGroups, pub ground_interaction_groups: InteractionGroups, pub velocity_constraints: Vec, - pub position_constraints: Vec, + pub generic_velocity_constraints: Vec, pub constraint_descs: Vec<(usize, ConstraintDesc)>, pub parallel_desc_groups: Vec, } -impl - ParallelSolverConstraints +impl + ParallelSolverConstraints { pub fn new() -> Self { Self { - not_ground_interactions: Vec::new(), - ground_interactions: Vec::new(), + generic_jacobians: DVector::zeros(0), + not_ground_interactions: vec![], + ground_interactions: vec![], + generic_not_ground_interactions: vec![], + generic_ground_interactions: vec![], interaction_groups: InteractionGroups::new(), ground_interaction_groups: InteractionGroups::new(), - velocity_constraints: Vec::new(), - position_constraints: Vec::new(), - constraint_descs: Vec::new(), - parallel_desc_groups: Vec::new(), + velocity_constraints: vec![], + generic_velocity_constraints: vec![], + constraint_descs: vec![], + parallel_desc_groups: vec![], } } } macro_rules! impl_init_constraints_group { - ($VelocityConstraint: ty, $PositionConstraint: ty, $Interaction: ty, + ($VelocityConstraint: ty, $GenVelocityConstraint: ty, $Interaction: ty, $categorize: ident, $group: ident, $data: ident$(.$constraint_index: ident)*, - $num_active_constraints: path, $empty_velocity_constraint: expr, $empty_position_constraint: expr $(, $weight: ident)*) => { - impl ParallelSolverConstraints<$VelocityConstraint, $PositionConstraint> { + $num_active_constraints: path, $empty_velocity_constraint: expr $(, $weight: ident)*) => { + impl ParallelSolverConstraints<$VelocityConstraint, $GenVelocityConstraint> { pub fn init_constraint_groups( &mut self, island_id: usize, islands: &IslandManager, bodies: &Bodies, + multibody_joints: &MultibodyJointSet, interactions: &mut [$Interaction], interaction_groups: &ParallelInteractionGroups, ) where Bodies: ComponentSet + ComponentSet { @@ -103,10 +107,13 @@ macro_rules! impl_init_constraints_group { self.ground_interactions.clear(); $categorize( bodies, + multibody_joints, interactions, group, &mut self.ground_interactions, &mut self.not_ground_interactions, + &mut self.generic_ground_interactions, + &mut self.generic_not_ground_interactions, ); #[cfg(feature = "simd-is-enabled")] @@ -192,9 +199,6 @@ macro_rules! impl_init_constraints_group { self.velocity_constraints.clear(); self.velocity_constraints .resize_with(total_num_constraints, || $empty_velocity_constraint); - self.position_constraints.clear(); - self.position_constraints - .resize_with(total_num_constraints, || $empty_position_constraint); } } } @@ -202,30 +206,28 @@ macro_rules! impl_init_constraints_group { impl_init_constraints_group!( AnyVelocityConstraint, - AnyPositionConstraint, + GenericVelocityConstraint, &mut ContactManifold, categorize_contacts, group_manifolds, data.constraint_index, VelocityConstraint::num_active_constraints, - AnyVelocityConstraint::Empty, - AnyPositionConstraint::Empty + AnyVelocityConstraint::Empty ); impl_init_constraints_group!( AnyJointVelocityConstraint, - AnyJointPositionConstraint, + (), JointGraphEdge, categorize_joints, group_joints, constraint_index, AnyJointVelocityConstraint::num_active_constraints, AnyJointVelocityConstraint::Empty, - AnyJointPositionConstraint::Empty, weight ); -impl ParallelSolverConstraints { +impl ParallelSolverConstraints { pub fn fill_constraints( &mut self, thread: &ThreadContext, @@ -247,24 +249,20 @@ impl ParallelSolverConstraints { ConstraintDesc::NongroundNongrouped(manifold_id) => { let manifold = &*manifolds_all[*manifold_id]; VelocityConstraint::generate(params, *manifold_id, manifold, bodies, &mut self.velocity_constraints, false); - PositionConstraint::generate(params, manifold, bodies, &mut self.position_constraints, false); } ConstraintDesc::GroundNongrouped(manifold_id) => { let manifold = &*manifolds_all[*manifold_id]; VelocityGroundConstraint::generate(params, *manifold_id, manifold, bodies, &mut self.velocity_constraints, false); - PositionGroundConstraint::generate(params, manifold, bodies, &mut self.position_constraints, false); } #[cfg(feature = "simd-is-enabled")] ConstraintDesc::NongroundGrouped(manifold_id) => { let manifolds = gather![|ii| &*manifolds_all[manifold_id[ii]]]; WVelocityConstraint::generate(params, *manifold_id, manifolds, bodies, &mut self.velocity_constraints, false); - WPositionConstraint::generate(params, manifolds, bodies, &mut self.position_constraints, false); } #[cfg(feature = "simd-is-enabled")] ConstraintDesc::GroundGrouped(manifold_id) => { let manifolds = gather![|ii| &*manifolds_all[manifold_id[ii]]]; WVelocityGroundConstraint::generate(params, *manifold_id, manifolds, bodies, &mut self.velocity_constraints, false); - WPositionGroundConstraint::generate(params, manifolds, bodies, &mut self.position_constraints, false); } } } @@ -272,7 +270,7 @@ impl ParallelSolverConstraints { } } -impl ParallelSolverConstraints { +impl ParallelSolverConstraints { pub fn fill_constraints( &mut self, thread: &ThreadContext, @@ -286,6 +284,9 @@ impl ParallelSolverConstraints + ComponentSet, { + return; + + /* let descs = &self.constraint_descs; crate::concurrent_loop! { @@ -295,35 +296,29 @@ impl ParallelSolverConstraints { let joint = &joints_all[*joint_id].weight; let velocity_constraint = AnyJointVelocityConstraint::from_joint(params, *joint_id, joint, bodies); - let position_constraint = AnyJointPositionConstraint::from_joint(joint, bodies); self.velocity_constraints[joint.constraint_index] = velocity_constraint; - self.position_constraints[joint.constraint_index] = position_constraint; } ConstraintDesc::GroundNongrouped(joint_id) => { let joint = &joints_all[*joint_id].weight; let velocity_constraint = AnyJointVelocityConstraint::from_joint_ground(params, *joint_id, joint, bodies); - let position_constraint = AnyJointPositionConstraint::from_joint_ground(joint, bodies); self.velocity_constraints[joint.constraint_index] = velocity_constraint; - self.position_constraints[joint.constraint_index] = position_constraint; } #[cfg(feature = "simd-is-enabled")] ConstraintDesc::NongroundGrouped(joint_id) => { - let joints = gather![|ii| &joints_all[joint_id[ii]].weight]; - let velocity_constraint = AnyJointVelocityConstraint::from_wide_joint(params, *joint_id, joints, bodies); - let position_constraint = AnyJointPositionConstraint::from_wide_joint(joints, bodies); - self.velocity_constraints[joints[0].constraint_index] = velocity_constraint; - self.position_constraints[joints[0].constraint_index] = position_constraint; + let impulse_joints = gather![|ii| &joints_all[joint_id[ii]].weight]; + let velocity_constraint = AnyJointVelocityConstraint::from_wide_joint(params, *joint_id, impulse_joints, bodies); + self.velocity_constraints[impulse_joints[0].constraint_index] = velocity_constraint; } #[cfg(feature = "simd-is-enabled")] ConstraintDesc::GroundGrouped(joint_id) => { - let joints = gather![|ii| &joints_all[joint_id[ii]].weight]; - let velocity_constraint = AnyJointVelocityConstraint::from_wide_joint_ground(params, *joint_id, joints, bodies); - let position_constraint = AnyJointPositionConstraint::from_wide_joint_ground(joints, bodies); - self.velocity_constraints[joints[0].constraint_index] = velocity_constraint; - self.position_constraints[joints[0].constraint_index] = position_constraint; + let impulse_joints = gather![|ii| &joints_all[joint_id[ii]].weight]; + let velocity_constraint = AnyJointVelocityConstraint::from_wide_joint_ground(params, *joint_id, impulse_joints, bodies); + self.velocity_constraints[impulse_joints[0].constraint_index] = velocity_constraint; } } } } + + */ } } diff --git a/src/dynamics/solver/parallel_velocity_solver.rs b/src/dynamics/solver/parallel_velocity_solver.rs index 54792f8..69ceb03 100644 --- a/src/dynamics/solver/parallel_velocity_solver.rs +++ b/src/dynamics/solver/parallel_velocity_solver.rs @@ -1,29 +1,36 @@ use super::{AnyJointVelocityConstraint, AnyVelocityConstraint, DeltaVel, ThreadContext}; -use crate::dynamics::solver::{ - AnyJointPositionConstraint, AnyPositionConstraint, ParallelSolverConstraints, -}; +use crate::dynamics::solver::generic_velocity_constraint::GenericVelocityConstraint; +use crate::dynamics::solver::ParallelSolverConstraints; use crate::dynamics::{IntegrationParameters, JointGraphEdge}; use crate::geometry::ContactManifold; use crate::math::Real; +use na::DVector; use std::sync::atomic::Ordering; -pub(crate) struct ParallelVelocitySolver {} +pub(crate) struct ParallelVelocitySolver { + pub mj_lambdas: Vec>, + pub generic_mj_lambdas: DVector, +} impl ParallelVelocitySolver { + pub fn new() -> Self { + Self { + mj_lambdas: Vec::new(), + generic_mj_lambdas: DVector::zeros(0), + } + } + pub fn solve( + &mut self, thread: &ThreadContext, params: &IntegrationParameters, manifolds_all: &mut [&mut ContactManifold], joints_all: &mut [JointGraphEdge], - mj_lambdas: &mut [DeltaVel], contact_constraints: &mut ParallelSolverConstraints< AnyVelocityConstraint, - AnyPositionConstraint, - >, - joint_constraints: &mut ParallelSolverConstraints< - AnyJointVelocityConstraint, - AnyJointPositionConstraint, + GenericVelocityConstraint, >, + joint_constraints: &mut ParallelSolverConstraints, ) { if contact_constraints.constraint_descs.is_empty() && joint_constraints.constraint_descs.is_empty() @@ -31,69 +38,6 @@ impl ParallelVelocitySolver { return; } - /* - * Warmstart constraints. - */ - { - // Each thread will concurrently grab thread.batch_size constraint desc to - // solve. If the batch size is large enough for to cross the boundary of - // a parallel_desc_group, we have to wait util the current group is finished - // before starting the next one. - let mut target_num_desc = 0; - let mut start_index = thread - .warmstart_constraint_index - .fetch_add(thread.batch_size, Ordering::SeqCst); - let mut batch_size = thread.batch_size; - let mut shift = 0; - - macro_rules! warmstart( - ($part: expr) => { - for group in $part.parallel_desc_groups.windows(2) { - let num_descs_in_group = group[1] - group[0]; - target_num_desc += num_descs_in_group; - - while start_index < group[1] { - let end_index = (start_index + batch_size).min(group[1]); - - let constraints = if end_index == $part.constraint_descs.len() { - &mut $part.velocity_constraints[$part.constraint_descs[start_index].0..] - } else { - &mut $part.velocity_constraints[$part.constraint_descs[start_index].0..$part.constraint_descs[end_index].0] - }; - - for constraint in constraints { - constraint.warmstart(mj_lambdas); - } - - let num_solved = end_index - start_index; - batch_size -= num_solved; - - thread - .num_warmstarted_constraints - .fetch_add(num_solved, Ordering::SeqCst); - - if batch_size == 0 { - start_index = thread - .warmstart_constraint_index - .fetch_add(thread.batch_size, Ordering::SeqCst); - start_index -= shift; - batch_size = thread.batch_size; - } else { - start_index += num_solved; - } - } - - ThreadContext::lock_until_ge(&thread.num_warmstarted_constraints, target_num_desc); - } - } - ); - - warmstart!(joint_constraints); - shift = joint_constraints.constraint_descs.len(); - start_index -= shift; - warmstart!(contact_constraints); - } - /* * Solve constraints. */ @@ -113,8 +57,8 @@ impl ParallelVelocitySolver { for _ in 0..params.max_velocity_iterations { macro_rules! solve { - ($part: expr) => { - // Joint groups. + ($part: expr, $($solve_args: expr),*) => { + // ImpulseJoint groups. for group in $part.parallel_desc_groups.windows(2) { let num_descs_in_group = group[1] - group[0]; @@ -133,12 +77,10 @@ impl ParallelVelocitySolver { ..$part.constraint_descs[end_index].0] }; - // println!( - // "Solving a constraint {:?}.", - // rayon::current_thread_index() - // ); for constraint in constraints { - constraint.solve(mj_lambdas); + constraint.solve( + $($solve_args),* + ); } let num_solved = end_index - start_index; @@ -166,10 +108,15 @@ impl ParallelVelocitySolver { }; } - solve!(joint_constraints); + solve!( + joint_constraints, + &joint_constraints.generic_jacobians, + &mut self.mj_lambdas, + &mut self.generic_mj_lambdas + ); shift += joint_descs.len(); start_index -= joint_descs.len(); - solve!(contact_constraints); + solve!(contact_constraints, &mut self.mj_lambdas, true, true); shift += contact_descs.len(); start_index -= contact_descs.len(); } diff --git a/src/dynamics/solver/position_constraint.rs b/src/dynamics/solver/position_constraint.rs deleted file mode 100644 index dca0655..0000000 --- a/src/dynamics/solver/position_constraint.rs +++ /dev/null @@ -1,168 +0,0 @@ -use crate::data::ComponentSet; -use crate::dynamics::solver::PositionGroundConstraint; -#[cfg(feature = "simd-is-enabled")] -use crate::dynamics::solver::{WPositionConstraint, WPositionGroundConstraint}; -use crate::dynamics::{IntegrationParameters, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition}; -use crate::geometry::ContactManifold; -use crate::math::{ - AngularInertia, Isometry, Point, Real, Rotation, Translation, Vector, MAX_MANIFOLD_POINTS, -}; -use crate::utils::{WAngularInertia, WCross, WDot}; - -pub(crate) enum AnyPositionConstraint { - #[cfg(feature = "simd-is-enabled")] - GroupedGround(WPositionGroundConstraint), - NonGroupedGround(PositionGroundConstraint), - #[cfg(feature = "simd-is-enabled")] - GroupedNonGround(WPositionConstraint), - NonGroupedNonGround(PositionConstraint), - #[allow(dead_code)] // The Empty variant is only used with parallel code. - Empty, -} - -impl AnyPositionConstraint { - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - match self { - #[cfg(feature = "simd-is-enabled")] - AnyPositionConstraint::GroupedGround(c) => c.solve(params, positions), - AnyPositionConstraint::NonGroupedGround(c) => c.solve(params, positions), - #[cfg(feature = "simd-is-enabled")] - AnyPositionConstraint::GroupedNonGround(c) => c.solve(params, positions), - AnyPositionConstraint::NonGroupedNonGround(c) => c.solve(params, positions), - AnyPositionConstraint::Empty => unreachable!(), - } - } -} - -pub(crate) struct PositionConstraint { - pub rb1: usize, - pub rb2: usize, - // NOTE: the points are relative to the center of masses. - pub local_p1: [Point; MAX_MANIFOLD_POINTS], - pub local_p2: [Point; MAX_MANIFOLD_POINTS], - pub dists: [Real; MAX_MANIFOLD_POINTS], - pub local_n1: Vector, - pub num_contacts: u8, - pub im1: Real, - pub im2: Real, - pub ii1: AngularInertia, - pub ii2: AngularInertia, - pub erp: Real, - pub max_linear_correction: Real, -} - -impl PositionConstraint { - pub fn generate( - params: &IntegrationParameters, - manifold: &ContactManifold, - bodies: &Bodies, - out_constraints: &mut Vec, - push: bool, - ) where - Bodies: ComponentSet - + ComponentSet - + ComponentSet, - { - let handle1 = manifold.data.rigid_body1.unwrap(); - let handle2 = manifold.data.rigid_body2.unwrap(); - - let ids1: &RigidBodyIds = bodies.index(handle1.0); - let ids2: &RigidBodyIds = bodies.index(handle2.0); - let poss1: &RigidBodyPosition = bodies.index(handle1.0); - let poss2: &RigidBodyPosition = bodies.index(handle2.0); - let mprops1: &RigidBodyMassProps = bodies.index(handle1.0); - let mprops2: &RigidBodyMassProps = bodies.index(handle2.0); - - for (l, manifold_points) in manifold - .data - .solver_contacts - .chunks(MAX_MANIFOLD_POINTS) - .enumerate() - { - let mut local_p1 = [Point::origin(); MAX_MANIFOLD_POINTS]; - let mut local_p2 = [Point::origin(); MAX_MANIFOLD_POINTS]; - let mut dists = [0.0; MAX_MANIFOLD_POINTS]; - - for l in 0..manifold_points.len() { - local_p1[l] = poss1 - .position - .inverse_transform_point(&manifold_points[l].point); - local_p2[l] = poss2 - .position - .inverse_transform_point(&manifold_points[l].point); - dists[l] = manifold_points[l].dist; - } - - let constraint = PositionConstraint { - rb1: ids1.active_set_offset, - rb2: ids2.active_set_offset, - local_p1, - local_p2, - local_n1: poss1 - .position - .inverse_transform_vector(&manifold.data.normal), - dists, - im1: mprops1.effective_inv_mass, - im2: mprops2.effective_inv_mass, - ii1: mprops1.effective_world_inv_inertia_sqrt.squared(), - ii2: mprops2.effective_world_inv_inertia_sqrt.squared(), - num_contacts: manifold_points.len() as u8, - erp: params.erp, - max_linear_correction: params.max_linear_correction, - }; - - if push { - out_constraints.push(AnyPositionConstraint::NonGroupedNonGround(constraint)); - } else { - out_constraints[manifold.data.constraint_index + l] = - AnyPositionConstraint::NonGroupedNonGround(constraint); - } - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - // FIXME: can we avoid most of the multiplications by pos1/pos2? - // Compute jacobians. - let mut pos1 = positions[self.rb1]; - let mut pos2 = positions[self.rb2]; - let allowed_err = params.allowed_linear_error; - - for k in 0..self.num_contacts as usize { - let target_dist = -self.dists[k] - allowed_err; - let n1 = pos1 * self.local_n1; - let p1 = pos1 * self.local_p1[k]; - let p2 = pos2 * self.local_p2[k]; - let dpos = p2 - p1; - let dist = dpos.dot(&n1); - - if dist < target_dist { - let p1 = p2 - n1 * dist; - let err = ((dist - target_dist) * self.erp).max(-self.max_linear_correction); - let dp1 = p1.coords - pos1.translation.vector; - let dp2 = p2.coords - pos2.translation.vector; - - let gcross1 = dp1.gcross(n1); - let gcross2 = -dp2.gcross(n1); - let ii_gcross1 = self.ii1.transform_vector(gcross1); - let ii_gcross2 = self.ii2.transform_vector(gcross2); - - // Compute impulse. - let inv_r = - self.im1 + self.im2 + gcross1.gdot(ii_gcross1) + gcross2.gdot(ii_gcross2); - let impulse = err / inv_r; - - // Apply impulse. - let tra1 = Translation::from(n1 * (impulse * self.im1)); - let tra2 = Translation::from(n1 * (-impulse * self.im2)); - let rot1 = Rotation::new(ii_gcross1 * impulse); - let rot2 = Rotation::new(ii_gcross2 * impulse); - - pos1 = Isometry::from_parts(tra1 * pos1.translation, rot1 * pos1.rotation); - pos2 = Isometry::from_parts(tra2 * pos2.translation, rot2 * pos2.rotation); - } - } - - positions[self.rb1] = pos1; - positions[self.rb2] = pos2; - } -} diff --git a/src/dynamics/solver/position_constraint_wide.rs b/src/dynamics/solver/position_constraint_wide.rs deleted file mode 100644 index 0b8e762..0000000 --- a/src/dynamics/solver/position_constraint_wide.rs +++ /dev/null @@ -1,157 +0,0 @@ -use super::AnyPositionConstraint; -use crate::dynamics::{IntegrationParameters, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition}; -use crate::geometry::ContactManifold; -use crate::math::{ - AngularInertia, Isometry, Point, Real, Rotation, SimdReal, Translation, Vector, - MAX_MANIFOLD_POINTS, SIMD_WIDTH, -}; -use crate::utils::{WAngularInertia, WCross, WDot}; - -use crate::data::ComponentSet; -use num::Zero; -use simba::simd::{SimdBool as _, SimdPartialOrd, SimdValue}; - -pub(crate) struct WPositionConstraint { - pub rb1: [usize; SIMD_WIDTH], - pub rb2: [usize; SIMD_WIDTH], - // NOTE: the points are relative to the center of masses. - pub local_p1: [Point; MAX_MANIFOLD_POINTS], - pub local_p2: [Point; MAX_MANIFOLD_POINTS], - pub dists: [SimdReal; MAX_MANIFOLD_POINTS], - pub local_n1: Vector, - pub im1: SimdReal, - pub im2: SimdReal, - pub ii1: AngularInertia, - pub ii2: AngularInertia, - pub erp: SimdReal, - pub max_linear_correction: SimdReal, - pub num_contacts: u8, -} - -impl WPositionConstraint { - pub fn generate( - params: &IntegrationParameters, - manifolds: [&ContactManifold; SIMD_WIDTH], - bodies: &Bodies, - out_constraints: &mut Vec, - push: bool, - ) where - Bodies: ComponentSet - + ComponentSet - + ComponentSet, - { - let handles1 = gather![|ii| manifolds[ii].data.rigid_body1.unwrap()]; - let handles2 = gather![|ii| manifolds[ii].data.rigid_body2.unwrap()]; - - let poss1: [&RigidBodyPosition; SIMD_WIDTH] = gather![|ii| bodies.index(handles1[ii].0)]; - let poss2: [&RigidBodyPosition; SIMD_WIDTH] = gather![|ii| bodies.index(handles2[ii].0)]; - let ids1: [&RigidBodyIds; SIMD_WIDTH] = gather![|ii| bodies.index(handles1[ii].0)]; - let ids2: [&RigidBodyIds; SIMD_WIDTH] = gather![|ii| bodies.index(handles2[ii].0)]; - let mprops1: [&RigidBodyMassProps; SIMD_WIDTH] = gather![|ii| bodies.index(handles1[ii].0)]; - let mprops2: [&RigidBodyMassProps; SIMD_WIDTH] = gather![|ii| bodies.index(handles2[ii].0)]; - - let im1 = SimdReal::from(gather![|ii| mprops1[ii].effective_inv_mass]); - let sqrt_ii1: AngularInertia = - AngularInertia::from(gather![|ii| mprops1[ii].effective_world_inv_inertia_sqrt]); - let im2 = SimdReal::from(gather![|ii| mprops2[ii].effective_inv_mass]); - let sqrt_ii2: AngularInertia = - AngularInertia::from(gather![|ii| mprops2[ii].effective_world_inv_inertia_sqrt]); - - let pos1 = Isometry::from(gather![|ii| poss1[ii].position]); - let pos2 = Isometry::from(gather![|ii| poss2[ii].position]); - - let local_n1 = - pos1.inverse_transform_vector(&Vector::from(gather![|ii| manifolds[ii].data.normal])); - - let rb1 = gather![|ii| ids1[ii].active_set_offset]; - let rb2 = gather![|ii| ids2[ii].active_set_offset]; - - let num_active_contacts = manifolds[0].data.num_active_contacts(); - - for l in (0..num_active_contacts).step_by(MAX_MANIFOLD_POINTS) { - let manifold_points = gather![|ii| &manifolds[ii].data.solver_contacts[l..]]; - let num_points = manifold_points[0].len().min(MAX_MANIFOLD_POINTS); - - let mut constraint = WPositionConstraint { - rb1, - rb2, - local_p1: [Point::origin(); MAX_MANIFOLD_POINTS], - local_p2: [Point::origin(); MAX_MANIFOLD_POINTS], - local_n1, - dists: [SimdReal::zero(); MAX_MANIFOLD_POINTS], - im1, - im2, - ii1: sqrt_ii1.squared(), - ii2: sqrt_ii2.squared(), - erp: SimdReal::splat(params.erp), - max_linear_correction: SimdReal::splat(params.max_linear_correction), - num_contacts: num_points as u8, - }; - - for i in 0..num_points { - let point = Point::from(gather![|ii| manifold_points[ii][i].point]); - let dist = SimdReal::from(gather![|ii| manifold_points[ii][i].dist]); - constraint.local_p1[i] = pos1.inverse_transform_point(&point); - constraint.local_p2[i] = pos2.inverse_transform_point(&point); - constraint.dists[i] = dist; - } - - if push { - out_constraints.push(AnyPositionConstraint::GroupedNonGround(constraint)); - } else { - out_constraints[manifolds[0].data.constraint_index + l / MAX_MANIFOLD_POINTS] = - AnyPositionConstraint::GroupedNonGround(constraint); - } - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - // FIXME: can we avoid most of the multiplications by pos1/pos2? - // Compute jacobians. - let mut pos1 = Isometry::from(gather![|ii| positions[self.rb1[ii]]]); - let mut pos2 = Isometry::from(gather![|ii| positions[self.rb2[ii]]]); - let allowed_err = SimdReal::splat(params.allowed_linear_error); - - for k in 0..self.num_contacts as usize { - let target_dist = -self.dists[k] - allowed_err; - let n1 = pos1 * self.local_n1; - let p1 = pos1 * self.local_p1[k]; - let p2 = pos2 * self.local_p2[k]; - let dpos = p2 - p1; - let dist = dpos.dot(&n1); - - // NOTE: this condition does not seem to be useful perfomancewise? - if dist.simd_lt(target_dist).any() { - // NOTE: only works for the point-point case. - let p1 = p2 - n1 * dist; - let err = ((dist - target_dist) * self.erp) - .simd_clamp(-self.max_linear_correction, SimdReal::zero()); - let dp1 = p1.coords - pos1.translation.vector; - let dp2 = p2.coords - pos2.translation.vector; - - let gcross1 = dp1.gcross(n1); - let gcross2 = -dp2.gcross(n1); - let ii_gcross1 = self.ii1.transform_vector(gcross1); - let ii_gcross2 = self.ii2.transform_vector(gcross2); - - // Compute impulse. - let inv_r = - self.im1 + self.im2 + gcross1.gdot(ii_gcross1) + gcross2.gdot(ii_gcross2); - let impulse = err / inv_r; - - // Apply impulse. - pos1.translation = Translation::from(n1 * (impulse * self.im1)) * pos1.translation; - pos1.rotation = Rotation::new(ii_gcross1 * impulse) * pos1.rotation; - pos2.translation = Translation::from(n1 * (-impulse * self.im2)) * pos2.translation; - pos2.rotation = Rotation::new(ii_gcross2 * impulse) * pos2.rotation; - } - } - - for ii in 0..SIMD_WIDTH { - positions[self.rb1[ii]] = pos1.extract(ii); - } - for ii in 0..SIMD_WIDTH { - positions[self.rb2[ii]] = pos2.extract(ii); - } - } -} diff --git a/src/dynamics/solver/position_ground_constraint.rs b/src/dynamics/solver/position_ground_constraint.rs deleted file mode 100644 index 53df7f8..0000000 --- a/src/dynamics/solver/position_ground_constraint.rs +++ /dev/null @@ -1,121 +0,0 @@ -use super::AnyPositionConstraint; -use crate::data::{BundleSet, ComponentSet}; -use crate::dynamics::{IntegrationParameters, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition}; -use crate::geometry::ContactManifold; -use crate::math::{ - AngularInertia, Isometry, Point, Real, Rotation, Translation, Vector, MAX_MANIFOLD_POINTS, -}; -use crate::utils::{WAngularInertia, WCross, WDot}; - -pub(crate) struct PositionGroundConstraint { - pub rb2: usize, - // NOTE: the points are relative to the center of masses. - pub p1: [Point; MAX_MANIFOLD_POINTS], - pub local_p2: [Point; MAX_MANIFOLD_POINTS], - pub dists: [Real; MAX_MANIFOLD_POINTS], - pub n1: Vector, - pub num_contacts: u8, - pub im2: Real, - pub ii2: AngularInertia, - pub erp: Real, - pub max_linear_correction: Real, -} - -impl PositionGroundConstraint { - pub fn generate( - params: &IntegrationParameters, - manifold: &ContactManifold, - bodies: &Bodies, - out_constraints: &mut Vec, - push: bool, - ) where - Bodies: ComponentSet - + ComponentSet - + ComponentSet, - { - let flip = manifold.data.relative_dominance < 0; - - let (handle2, n1) = if flip { - (manifold.data.rigid_body1.unwrap(), -manifold.data.normal) - } else { - (manifold.data.rigid_body2.unwrap(), manifold.data.normal) - }; - - let (ids2, poss2, mprops2): (&RigidBodyIds, &RigidBodyPosition, &RigidBodyMassProps) = - bodies.index_bundle(handle2.0); - - for (l, manifold_contacts) in manifold - .data - .solver_contacts - .chunks(MAX_MANIFOLD_POINTS) - .enumerate() - { - let mut p1 = [Point::origin(); MAX_MANIFOLD_POINTS]; - let mut local_p2 = [Point::origin(); MAX_MANIFOLD_POINTS]; - let mut dists = [0.0; MAX_MANIFOLD_POINTS]; - - for k in 0..manifold_contacts.len() { - p1[k] = manifold_contacts[k].point; - local_p2[k] = poss2 - .position - .inverse_transform_point(&manifold_contacts[k].point); - dists[k] = manifold_contacts[k].dist; - } - - let constraint = PositionGroundConstraint { - rb2: ids2.active_set_offset, - p1, - local_p2, - n1, - dists, - im2: mprops2.effective_inv_mass, - ii2: mprops2.effective_world_inv_inertia_sqrt.squared(), - num_contacts: manifold_contacts.len() as u8, - erp: params.erp, - max_linear_correction: params.max_linear_correction, - }; - - if push { - out_constraints.push(AnyPositionConstraint::NonGroupedGround(constraint)); - } else { - out_constraints[manifold.data.constraint_index + l] = - AnyPositionConstraint::NonGroupedGround(constraint); - } - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - // FIXME: can we avoid most of the multiplications by pos1/pos2? - // Compute jacobians. - let mut pos2 = positions[self.rb2]; - let allowed_err = params.allowed_linear_error; - - for k in 0..self.num_contacts as usize { - let target_dist = -self.dists[k] - allowed_err; - let n1 = self.n1; - let p1 = self.p1[k]; - let p2 = pos2 * self.local_p2[k]; - let dpos = p2 - p1; - let dist = dpos.dot(&n1); - - if dist < target_dist { - let err = ((dist - target_dist) * self.erp).max(-self.max_linear_correction); - let dp2 = p2.coords - pos2.translation.vector; - - let gcross2 = -dp2.gcross(n1); - let ii_gcross2 = self.ii2.transform_vector(gcross2); - - // Compute impulse. - let inv_r = self.im2 + gcross2.gdot(ii_gcross2); - let impulse = err / inv_r; - - // Apply impulse. - let tra2 = Translation::from(n1 * (-impulse * self.im2)); - let rot2 = Rotation::new(ii_gcross2 * impulse); - pos2 = Isometry::from_parts(tra2 * pos2.translation, rot2 * pos2.rotation); - } - } - - positions[self.rb2] = pos2; - } -} diff --git a/src/dynamics/solver/position_ground_constraint_wide.rs b/src/dynamics/solver/position_ground_constraint_wide.rs deleted file mode 100644 index 7d4ff96..0000000 --- a/src/dynamics/solver/position_ground_constraint_wide.rs +++ /dev/null @@ -1,143 +0,0 @@ -use super::AnyPositionConstraint; -use crate::dynamics::{IntegrationParameters, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition}; -use crate::geometry::ContactManifold; -use crate::math::{ - AngularInertia, Isometry, Point, Real, Rotation, SimdReal, Translation, Vector, - MAX_MANIFOLD_POINTS, SIMD_WIDTH, -}; -use crate::utils::{WAngularInertia, WCross, WDot}; - -use crate::data::ComponentSet; -use num::Zero; -use simba::simd::{SimdBool as _, SimdPartialOrd, SimdValue}; - -pub(crate) struct WPositionGroundConstraint { - pub rb2: [usize; SIMD_WIDTH], - // NOTE: the points are relative to the center of masses. - pub p1: [Point; MAX_MANIFOLD_POINTS], - pub local_p2: [Point; MAX_MANIFOLD_POINTS], - pub dists: [SimdReal; MAX_MANIFOLD_POINTS], - pub n1: Vector, - pub im2: SimdReal, - pub ii2: AngularInertia, - pub erp: SimdReal, - pub max_linear_correction: SimdReal, - pub num_contacts: u8, -} - -impl WPositionGroundConstraint { - pub fn generate( - params: &IntegrationParameters, - manifolds: [&ContactManifold; SIMD_WIDTH], - bodies: &Bodies, - out_constraints: &mut Vec, - push: bool, - ) where - Bodies: ComponentSet - + ComponentSet - + ComponentSet, - { - let mut handles1 = gather![|ii| manifolds[ii].data.rigid_body1]; - let mut handles2 = gather![|ii| manifolds[ii].data.rigid_body2]; - let mut flipped = [false; SIMD_WIDTH]; - - for ii in 0..SIMD_WIDTH { - if manifolds[ii].data.relative_dominance < 0 { - flipped[ii] = true; - std::mem::swap(&mut handles1[ii], &mut handles2[ii]); - } - } - - let poss2: [&RigidBodyPosition; SIMD_WIDTH] = - gather![|ii| bodies.index(handles2[ii].unwrap().0)]; - let ids2: [&RigidBodyIds; SIMD_WIDTH] = gather![|ii| bodies.index(handles2[ii].unwrap().0)]; - let mprops2: [&RigidBodyMassProps; SIMD_WIDTH] = - gather![|ii| bodies.index(handles2[ii].unwrap().0)]; - - let im2 = SimdReal::from(gather![|ii| mprops2[ii].effective_inv_mass]); - let sqrt_ii2: AngularInertia = - AngularInertia::from(gather![|ii| mprops2[ii].effective_world_inv_inertia_sqrt]); - - let n1 = Vector::from(gather![|ii| if flipped[ii] { - -manifolds[ii].data.normal - } else { - manifolds[ii].data.normal - }]); - - let pos2 = Isometry::from(gather![|ii| poss2[ii].position]); - let rb2 = gather![|ii| ids2[ii].active_set_offset]; - - let num_active_contacts = manifolds[0].data.num_active_contacts(); - - for l in (0..num_active_contacts).step_by(MAX_MANIFOLD_POINTS) { - let manifold_points = gather![|ii| &manifolds[ii].data.solver_contacts[l..]]; - let num_points = manifold_points[0].len().min(MAX_MANIFOLD_POINTS); - - let mut constraint = WPositionGroundConstraint { - rb2, - p1: [Point::origin(); MAX_MANIFOLD_POINTS], - local_p2: [Point::origin(); MAX_MANIFOLD_POINTS], - n1, - dists: [SimdReal::zero(); MAX_MANIFOLD_POINTS], - im2, - ii2: sqrt_ii2.squared(), - erp: SimdReal::splat(params.erp), - max_linear_correction: SimdReal::splat(params.max_linear_correction), - num_contacts: num_points as u8, - }; - - for i in 0..num_points { - let point = Point::from(gather![|ii| manifold_points[ii][i].point]); - let dist = SimdReal::from(gather![|ii| manifold_points[ii][i].dist]); - constraint.p1[i] = point; - constraint.local_p2[i] = pos2.inverse_transform_point(&point); - constraint.dists[i] = dist; - } - - if push { - out_constraints.push(AnyPositionConstraint::GroupedGround(constraint)); - } else { - out_constraints[manifolds[0].data.constraint_index + l / MAX_MANIFOLD_POINTS] = - AnyPositionConstraint::GroupedGround(constraint); - } - } - } - - pub fn solve(&self, params: &IntegrationParameters, positions: &mut [Isometry]) { - // FIXME: can we avoid most of the multiplications by pos1/pos2? - // Compute jacobians. - let mut pos2 = Isometry::from(gather![|ii| positions[self.rb2[ii]]]); - let allowed_err = SimdReal::splat(params.allowed_linear_error); - - for k in 0..self.num_contacts as usize { - let target_dist = -self.dists[k] - allowed_err; - let n1 = self.n1; - let p1 = self.p1[k]; - let p2 = pos2 * self.local_p2[k]; - let dpos = p2 - p1; - let dist = dpos.dot(&n1); - - // NOTE: this condition does not seem to be useful perfomancewise? - if dist.simd_lt(target_dist).any() { - let err = ((dist - target_dist) * self.erp) - .simd_clamp(-self.max_linear_correction, SimdReal::zero()); - let dp2 = p2.coords - pos2.translation.vector; - - let gcross2 = -dp2.gcross(n1); - let ii_gcross2 = self.ii2.transform_vector(gcross2); - - // Compute impulse. - let inv_r = self.im2 + gcross2.gdot(ii_gcross2); - let impulse = err / inv_r; - - // Apply impulse. - pos2.translation = Translation::from(n1 * (-impulse * self.im2)) * pos2.translation; - pos2.rotation = Rotation::new(ii_gcross2 * impulse) * pos2.rotation; - } - } - - for ii in 0..SIMD_WIDTH { - positions[self.rb2[ii]] = pos2.extract(ii); - } - } -} diff --git a/src/dynamics/solver/position_solver.rs b/src/dynamics/solver/position_solver.rs deleted file mode 100644 index 48b71aa..0000000 --- a/src/dynamics/solver/position_solver.rs +++ /dev/null @@ -1,57 +0,0 @@ -use super::AnyJointPositionConstraint; -use crate::data::{ComponentSet, ComponentSetMut}; -use crate::dynamics::{solver::AnyPositionConstraint, IntegrationParameters}; -use crate::dynamics::{IslandManager, RigidBodyIds, RigidBodyPosition}; -use crate::math::{Isometry, Real}; - -pub(crate) struct PositionSolver { - positions: Vec>, -} - -impl PositionSolver { - pub fn new() -> Self { - Self { - positions: Vec::new(), - } - } - - pub fn solve( - &mut self, - island_id: usize, - params: &IntegrationParameters, - islands: &IslandManager, - bodies: &mut Bodies, - contact_constraints: &[AnyPositionConstraint], - joint_constraints: &[AnyJointPositionConstraint], - ) where - Bodies: ComponentSet + ComponentSetMut, - { - if contact_constraints.is_empty() && joint_constraints.is_empty() { - // Nothing to do. - return; - } - - self.positions.clear(); - self.positions - .extend(islands.active_island(island_id).iter().map(|h| { - let poss: &RigidBodyPosition = bodies.index(h.0); - poss.next_position - })); - - for _ in 0..params.max_position_iterations { - for constraint in joint_constraints { - constraint.solve(params, &mut self.positions) - } - - for constraint in contact_constraints { - constraint.solve(params, &mut self.positions) - } - } - - for handle in islands.active_island(island_id) { - let ids: &RigidBodyIds = bodies.index(handle.0); - let next_pos = &self.positions[ids.active_set_offset]; - bodies.map_mut_internal(handle.0, |poss| poss.next_position = *next_pos); - } - } -} diff --git a/src/dynamics/solver/solver_constraints.rs b/src/dynamics/solver/solver_constraints.rs index a9aa780..cfd7575 100644 --- a/src/dynamics/solver/solver_constraints.rs +++ b/src/dynamics/solver/solver_constraints.rs @@ -2,62 +2,69 @@ use super::{ AnyJointVelocityConstraint, InteractionGroups, VelocityConstraint, VelocityGroundConstraint, }; #[cfg(feature = "simd-is-enabled")] -use super::{ - WPositionConstraint, WPositionGroundConstraint, WVelocityConstraint, WVelocityGroundConstraint, -}; +use super::{WVelocityConstraint, WVelocityGroundConstraint}; use crate::data::ComponentSet; use crate::dynamics::solver::categorization::{categorize_contacts, categorize_joints}; -use crate::dynamics::solver::{ - AnyJointPositionConstraint, AnyPositionConstraint, PositionConstraint, PositionGroundConstraint, -}; +use crate::dynamics::solver::GenericVelocityConstraint; use crate::dynamics::{ - solver::AnyVelocityConstraint, IntegrationParameters, JointGraphEdge, JointIndex, RigidBodyIds, - RigidBodyMassProps, RigidBodyPosition, RigidBodyType, + solver::AnyVelocityConstraint, IntegrationParameters, JointGraphEdge, JointIndex, + MultibodyJointSet, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition, RigidBodyType, }; use crate::dynamics::{IslandManager, RigidBodyVelocity}; use crate::geometry::{ContactManifold, ContactManifoldIndex}; +use crate::math::Real; #[cfg(feature = "simd-is-enabled")] use crate::math::SIMD_WIDTH; +use na::DVector; -pub(crate) struct SolverConstraints { +pub(crate) struct SolverConstraints { + pub generic_jacobians: DVector, pub not_ground_interactions: Vec, pub ground_interactions: Vec, + pub generic_not_ground_interactions: Vec, + pub generic_ground_interactions: Vec, pub interaction_groups: InteractionGroups, pub ground_interaction_groups: InteractionGroups, pub velocity_constraints: Vec, - pub position_constraints: Vec, + pub generic_velocity_constraints: Vec, } -impl - SolverConstraints +impl + SolverConstraints { pub fn new() -> Self { Self { - not_ground_interactions: Vec::new(), - ground_interactions: Vec::new(), + generic_jacobians: DVector::zeros(0), + not_ground_interactions: vec![], + ground_interactions: vec![], + generic_not_ground_interactions: vec![], + generic_ground_interactions: vec![], interaction_groups: InteractionGroups::new(), ground_interaction_groups: InteractionGroups::new(), - velocity_constraints: Vec::new(), - position_constraints: Vec::new(), + velocity_constraints: vec![], + generic_velocity_constraints: vec![], } } pub fn clear(&mut self) { self.not_ground_interactions.clear(); self.ground_interactions.clear(); + self.generic_not_ground_interactions.clear(); + self.generic_ground_interactions.clear(); self.interaction_groups.clear(); self.ground_interaction_groups.clear(); self.velocity_constraints.clear(); - self.position_constraints.clear(); + self.generic_velocity_constraints.clear(); } } -impl SolverConstraints { +impl SolverConstraints { pub fn init_constraint_groups( &mut self, island_id: usize, islands: &IslandManager, bodies: &Bodies, + multibody_joints: &MultibodyJointSet, manifolds: &[&mut ContactManifold], manifold_indices: &[ContactManifoldIndex], ) where @@ -65,12 +72,18 @@ impl SolverConstraints { { self.not_ground_interactions.clear(); self.ground_interactions.clear(); + self.generic_not_ground_interactions.clear(); + self.generic_ground_interactions.clear(); + categorize_contacts( bodies, + multibody_joints, manifolds, manifold_indices, &mut self.ground_interactions, &mut self.not_ground_interactions, + &mut self.generic_not_ground_interactions, + &mut self.generic_ground_interactions, ); self.interaction_groups.clear_groups(); @@ -106,6 +119,7 @@ impl SolverConstraints { params: &IntegrationParameters, islands: &IslandManager, bodies: &Bodies, + multibody_joints: &MultibodyJointSet, manifolds: &[&mut ContactManifold], manifold_indices: &[ContactManifoldIndex], ) where @@ -116,15 +130,24 @@ impl SolverConstraints { + ComponentSet, { self.velocity_constraints.clear(); - self.position_constraints.clear(); + self.generic_velocity_constraints.clear(); - self.init_constraint_groups(island_id, islands, bodies, manifolds, manifold_indices); + self.init_constraint_groups( + island_id, + islands, + bodies, + multibody_joints, + manifolds, + manifold_indices, + ); #[cfg(feature = "simd-is-enabled")] { self.compute_grouped_constraints(params, bodies, manifolds); } self.compute_nongrouped_constraints(params, bodies, manifolds); + self.compute_generic_constraints(params, bodies, multibody_joints, manifolds); + #[cfg(feature = "simd-is-enabled")] { self.compute_grouped_ground_constraints(params, bodies, manifolds); @@ -159,13 +182,6 @@ impl SolverConstraints { &mut self.velocity_constraints, true, ); - WPositionConstraint::generate( - params, - manifolds, - bodies, - &mut self.position_constraints, - true, - ); } } @@ -190,11 +206,34 @@ impl SolverConstraints { &mut self.velocity_constraints, true, ); - PositionConstraint::generate( + } + } + + fn compute_generic_constraints( + &mut self, + params: &IntegrationParameters, + bodies: &Bodies, + multibody_joints: &MultibodyJointSet, + manifolds_all: &[&mut ContactManifold], + ) where + Bodies: ComponentSet + + ComponentSet + + ComponentSet + + ComponentSet + + ComponentSet, + { + let mut jacobian_id = 0; + for manifold_i in &self.generic_not_ground_interactions { + let manifold = &manifolds_all[*manifold_i]; + GenericVelocityConstraint::generate( params, + *manifold_i, manifold, bodies, - &mut self.position_constraints, + multibody_joints, + &mut self.generic_velocity_constraints, + &mut self.generic_jacobians, + &mut jacobian_id, true, ); } @@ -227,13 +266,6 @@ impl SolverConstraints { &mut self.velocity_constraints, true, ); - WPositionGroundConstraint::generate( - params, - manifolds, - bodies, - &mut self.position_constraints, - true, - ); } } @@ -258,25 +290,19 @@ impl SolverConstraints { &mut self.velocity_constraints, true, ); - PositionGroundConstraint::generate( - params, - manifold, - bodies, - &mut self.position_constraints, - true, - ) } } } -impl SolverConstraints { +impl SolverConstraints { pub fn init( &mut self, island_id: usize, params: &IntegrationParameters, islands: &IslandManager, bodies: &Bodies, - joints: &[JointGraphEdge], + multibody_joints: &MultibodyJointSet, + impulse_joints: &[JointGraphEdge], joint_constraint_indices: &[JointIndex], ) where Bodies: ComponentSet @@ -285,26 +311,32 @@ impl SolverConstraints { + ComponentSet + ComponentSet, { - // Generate constraints for joints. + // Generate constraints for impulse_joints. self.not_ground_interactions.clear(); self.ground_interactions.clear(); + self.generic_not_ground_interactions.clear(); + self.generic_ground_interactions.clear(); + categorize_joints( bodies, - joints, + multibody_joints, + impulse_joints, joint_constraint_indices, &mut self.ground_interactions, &mut self.not_ground_interactions, + &mut self.generic_ground_interactions, + &mut self.generic_not_ground_interactions, ); self.velocity_constraints.clear(); - self.position_constraints.clear(); + self.generic_velocity_constraints.clear(); self.interaction_groups.clear_groups(); self.interaction_groups.group_joints( island_id, islands, bodies, - joints, + impulse_joints, &self.not_ground_interactions, ); @@ -313,7 +345,7 @@ impl SolverConstraints { island_id, islands, bodies, - joints, + impulse_joints, &self.ground_interactions, ); // NOTE: uncomment this do disable SIMD joint resolution. @@ -324,15 +356,72 @@ impl SolverConstraints { // .nongrouped_interactions // .append(&mut self.ground_interaction_groups.grouped_interactions); - self.compute_nongrouped_joint_ground_constraints(params, bodies, joints); + let mut j_id = 0; + self.compute_nongrouped_joint_constraints( + params, + bodies, + multibody_joints, + impulse_joints, + &mut j_id, + ); #[cfg(feature = "simd-is-enabled")] { - self.compute_grouped_joint_ground_constraints(params, bodies, joints); + self.compute_grouped_joint_constraints(params, bodies, impulse_joints); } - self.compute_nongrouped_joint_constraints(params, bodies, joints); + self.compute_generic_joint_constraints( + params, + bodies, + multibody_joints, + impulse_joints, + &mut j_id, + ); + + self.compute_nongrouped_joint_ground_constraints( + params, + bodies, + multibody_joints, + impulse_joints, + ); #[cfg(feature = "simd-is-enabled")] { - self.compute_grouped_joint_constraints(params, bodies, joints); + self.compute_grouped_joint_ground_constraints(params, bodies, impulse_joints); + } + self.compute_generic_ground_joint_constraints( + params, + bodies, + multibody_joints, + impulse_joints, + &mut j_id, + ); + self.compute_articulation_constraints( + params, + island_id, + islands, + multibody_joints, + &mut j_id, + ); + } + + fn compute_articulation_constraints( + &mut self, + params: &IntegrationParameters, + island_id: usize, + islands: &IslandManager, + multibody_joints: &MultibodyJointSet, + j_id: &mut usize, + ) { + for handle in islands.active_island(island_id) { + if let Some(link) = multibody_joints.rigid_body_link(*handle) { + let multibody = multibody_joints.get_multibody(link.multibody).unwrap(); + if link.id == 0 || link.id == 1 && !multibody.root_is_dynamic { + multibody.generate_internal_constraints( + params, + j_id, + &mut self.generic_jacobians, + &mut self.velocity_constraints, + ) + } + } } } @@ -340,6 +429,7 @@ impl SolverConstraints { &mut self, params: &IntegrationParameters, bodies: &Bodies, + multibody_joints: &MultibodyJointSet, joints_all: &[JointGraphEdge], ) where Bodies: ComponentSet @@ -348,13 +438,19 @@ impl SolverConstraints { + ComponentSet + ComponentSet, { + let mut j_id = 0; for joint_i in &self.ground_interaction_groups.nongrouped_interactions { let joint = &joints_all[*joint_i].weight; - let vel_constraint = - AnyJointVelocityConstraint::from_joint_ground(params, *joint_i, joint, bodies); - self.velocity_constraints.push(vel_constraint); - let pos_constraint = AnyJointPositionConstraint::from_joint_ground(joint, bodies); - self.position_constraints.push(pos_constraint); + AnyJointVelocityConstraint::from_joint_ground( + params, + *joint_i, + joint, + bodies, + multibody_joints, + &mut j_id, + &mut self.generic_jacobians, + &mut self.velocity_constraints, + ); } } @@ -377,14 +473,14 @@ impl SolverConstraints { .chunks_exact(SIMD_WIDTH) { let joints_id = gather![|ii| joints_i[ii]]; - let joints = gather![|ii| &joints_all[joints_i[ii]].weight]; - let vel_constraint = AnyJointVelocityConstraint::from_wide_joint_ground( - params, joints_id, joints, bodies, + let impulse_joints = gather![|ii| &joints_all[joints_i[ii]].weight]; + AnyJointVelocityConstraint::from_wide_joint_ground( + params, + joints_id, + impulse_joints, + bodies, + &mut self.velocity_constraints, ); - self.velocity_constraints.push(vel_constraint); - - let pos_constraint = AnyJointPositionConstraint::from_wide_joint_ground(joints, bodies); - self.position_constraints.push(pos_constraint); } } @@ -392,7 +488,9 @@ impl SolverConstraints { &mut self, params: &IntegrationParameters, bodies: &Bodies, + multibody_joints: &MultibodyJointSet, joints_all: &[JointGraphEdge], + j_id: &mut usize, ) where Bodies: ComponentSet + ComponentSet @@ -401,11 +499,73 @@ impl SolverConstraints { { for joint_i in &self.interaction_groups.nongrouped_interactions { let joint = &joints_all[*joint_i].weight; - let vel_constraint = - AnyJointVelocityConstraint::from_joint(params, *joint_i, joint, bodies); - self.velocity_constraints.push(vel_constraint); - let pos_constraint = AnyJointPositionConstraint::from_joint(joint, bodies); - self.position_constraints.push(pos_constraint); + AnyJointVelocityConstraint::from_joint( + params, + *joint_i, + joint, + bodies, + multibody_joints, + j_id, + &mut self.generic_jacobians, + &mut self.velocity_constraints, + ); + } + } + + fn compute_generic_joint_constraints( + &mut self, + params: &IntegrationParameters, + bodies: &Bodies, + multibody_joints: &MultibodyJointSet, + joints_all: &[JointGraphEdge], + j_id: &mut usize, + ) where + Bodies: ComponentSet + + ComponentSet + + ComponentSet + + ComponentSet, + { + for joint_i in &self.generic_not_ground_interactions { + let joint = &joints_all[*joint_i].weight; + AnyJointVelocityConstraint::from_joint( + params, + *joint_i, + joint, + bodies, + multibody_joints, + j_id, + &mut self.generic_jacobians, + &mut self.velocity_constraints, + ) + } + } + + fn compute_generic_ground_joint_constraints( + &mut self, + params: &IntegrationParameters, + bodies: &Bodies, + multibody_joints: &MultibodyJointSet, + joints_all: &[JointGraphEdge], + j_id: &mut usize, + ) where + Bodies: ComponentSet + + ComponentSet + + ComponentSet + + ComponentSet + + ComponentSet, + { + for joint_i in &self.generic_ground_interactions { + let joint = &joints_all[*joint_i].weight; + AnyJointVelocityConstraint::from_joint_ground( + params, + *joint_i, + joint, + bodies, + multibody_joints, + j_id, + &mut self.generic_jacobians, + &mut self.velocity_constraints, + ) } } @@ -427,13 +587,14 @@ impl SolverConstraints { .chunks_exact(SIMD_WIDTH) { let joints_id = gather![|ii| joints_i[ii]]; - let joints = gather![|ii| &joints_all[joints_i[ii]].weight]; - let vel_constraint = - AnyJointVelocityConstraint::from_wide_joint(params, joints_id, joints, bodies); - self.velocity_constraints.push(vel_constraint); - - let pos_constraint = AnyJointPositionConstraint::from_wide_joint(joints, bodies); - self.position_constraints.push(pos_constraint); + let impulse_joints = gather![|ii| &joints_all[joints_i[ii]].weight]; + AnyJointVelocityConstraint::from_wide_joint( + params, + joints_id, + impulse_joints, + bodies, + &mut self.velocity_constraints, + ); } } } diff --git a/src/dynamics/solver/velocity_constraint.rs b/src/dynamics/solver/velocity_constraint.rs index 0d82f81..719468d 100644 --- a/src/dynamics/solver/velocity_constraint.rs +++ b/src/dynamics/solver/velocity_constraint.rs @@ -41,26 +41,37 @@ impl AnyVelocityConstraint { } } - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { + pub fn remove_bias_from_rhs(&mut self) { match self { - AnyVelocityConstraint::NongroupedGround(c) => c.warmstart(mj_lambdas), - AnyVelocityConstraint::Nongrouped(c) => c.warmstart(mj_lambdas), + AnyVelocityConstraint::Nongrouped(c) => c.remove_bias_from_rhs(), + AnyVelocityConstraint::NongroupedGround(c) => c.remove_bias_from_rhs(), #[cfg(feature = "simd-is-enabled")] - AnyVelocityConstraint::GroupedGround(c) => c.warmstart(mj_lambdas), + AnyVelocityConstraint::Grouped(c) => c.remove_bias_from_rhs(), #[cfg(feature = "simd-is-enabled")] - AnyVelocityConstraint::Grouped(c) => c.warmstart(mj_lambdas), - AnyVelocityConstraint::Empty => unreachable!(), + AnyVelocityConstraint::GroupedGround(c) => c.remove_bias_from_rhs(), + AnyVelocityConstraint::Empty => {} } } - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { + pub fn solve( + &mut self, + mj_lambdas: &mut [DeltaVel], + solve_normal: bool, + solve_friction: bool, + ) { match self { - AnyVelocityConstraint::NongroupedGround(c) => c.solve(mj_lambdas), - AnyVelocityConstraint::Nongrouped(c) => c.solve(mj_lambdas), + AnyVelocityConstraint::NongroupedGround(c) => { + c.solve(mj_lambdas, solve_normal, solve_friction) + } + AnyVelocityConstraint::Nongrouped(c) => { + c.solve(mj_lambdas, solve_normal, solve_friction) + } #[cfg(feature = "simd-is-enabled")] - AnyVelocityConstraint::GroupedGround(c) => c.solve(mj_lambdas), + AnyVelocityConstraint::GroupedGround(c) => { + c.solve(mj_lambdas, solve_normal, solve_friction) + } #[cfg(feature = "simd-is-enabled")] - AnyVelocityConstraint::Grouped(c) => c.solve(mj_lambdas), + AnyVelocityConstraint::Grouped(c) => c.solve(mj_lambdas, solve_normal, solve_friction), AnyVelocityConstraint::Empty => unreachable!(), } } @@ -83,8 +94,6 @@ pub(crate) struct VelocityConstraint { pub dir1: Vector, // Non-penetration force direction for the first body. #[cfg(feature = "dim3")] pub tangent1: Vector, // One of the friction force directions. - #[cfg(feature = "dim3")] - pub tangent_rot1: na::UnitComplex, // Orientation of the tangent basis wrt. the reference basis. pub im1: Real, pub im2: Real, pub limit: Real, @@ -118,7 +127,7 @@ impl VelocityConstraint { assert_eq!(manifold.data.relative_dominance, 0); let inv_dt = params.inv_dt(); - let velocity_based_erp_inv_dt = params.velocity_based_erp_inv_dt(); + let erp_inv_dt = params.erp_inv_dt(); let handle1 = manifold.data.rigid_body1.unwrap(); let handle2 = manifold.data.rigid_body2.unwrap(); @@ -130,12 +139,11 @@ impl VelocityConstraint { let mj_lambda1 = ids1.active_set_offset; let mj_lambda2 = ids2.active_set_offset; let force_dir1 = -manifold.data.normal; - let warmstart_coeff = manifold.data.warmstart_multiplier * params.warmstart_coeff; #[cfg(feature = "dim2")] let tangents1 = force_dir1.orthonormal_basis(); #[cfg(feature = "dim3")] - let (tangents1, tangent_rot1) = + let tangents1 = super::compute_tangent_contact_directions(&force_dir1, &vels1.linvel, &vels2.linvel); for (_l, manifold_points) in manifold @@ -149,8 +157,6 @@ impl VelocityConstraint { dir1: force_dir1, #[cfg(feature = "dim3")] tangent1: tangents1[0], - #[cfg(feature = "dim3")] - tangent_rot1, elements: [VelocityConstraintElement::zero(); MAX_MANIFOLD_POINTS], im1: mprops1.effective_inv_mass, im2: mprops2.effective_inv_mass, @@ -171,7 +177,7 @@ impl VelocityConstraint { // avoid spurious copying. // Is this optimization beneficial when targeting non-WASM platforms? // - // NOTE: joints have the same problem, but it is not easy to refactor the code that way + // NOTE: impulse_joints have the same problem, but it is not easy to refactor the code that way // for the moment. #[cfg(target_arch = "wasm32")] let constraint = if push { @@ -198,7 +204,6 @@ impl VelocityConstraint { #[cfg(feature = "dim3")] { constraint.tangent1 = tangents1[0]; - constraint.tangent_rot1 = tangent_rot1; } constraint.im1 = mprops1.effective_inv_mass; constraint.im2 = mprops2.effective_inv_mass; @@ -218,8 +223,6 @@ impl VelocityConstraint { let vel1 = vels1.linvel + vels1.angvel.gcross(dp1); let vel2 = vels2.linvel + vels2.angvel.gcross(dp2); - let warmstart_correction; - constraint.limit = manifold_point.friction; constraint.manifold_contact_id[k] = manifold_point.contact_id; @@ -241,34 +244,28 @@ impl VelocityConstraint { let is_bouncy = manifold_point.is_bouncy() as u32 as Real; let is_resting = 1.0 - is_bouncy; - let mut rhs = (1.0 + is_bouncy * manifold_point.restitution) + let mut rhs_wo_bias = (1.0 + is_bouncy * manifold_point.restitution) * (vel1 - vel2).dot(&force_dir1); - rhs += manifold_point.dist.max(0.0) * inv_dt; - rhs *= is_bouncy + is_resting * params.velocity_solve_fraction; - rhs += is_resting * velocity_based_erp_inv_dt * manifold_point.dist.min(0.0); - warmstart_correction = (params.warmstart_correction_slope - / (rhs - manifold_point.prev_rhs).abs()) - .min(warmstart_coeff); + rhs_wo_bias += + (manifold_point.dist + params.allowed_linear_error).max(0.0) * inv_dt; + rhs_wo_bias *= is_bouncy + is_resting * params.velocity_solve_fraction; + let rhs_bias = /* is_resting + * */ erp_inv_dt + * (manifold_point.dist + params.allowed_linear_error).min(0.0); constraint.elements[k].normal_part = VelocityConstraintNormalPart { gcross1, gcross2, - rhs, - impulse: manifold_point.warmstart_impulse * warmstart_correction, + rhs: rhs_wo_bias + rhs_bias, + rhs_wo_bias, + impulse: 0.0, r, }; } // Tangent parts. { - #[cfg(feature = "dim3")] - let impulse = tangent_rot1 - * manifold_points[k].warmstart_tangent_impulse - * warmstart_correction; - #[cfg(feature = "dim2")] - let impulse = - [manifold_points[k].warmstart_tangent_impulse * warmstart_correction]; - constraint.elements[k].tangent_part.impulse = impulse; + constraint.elements[k].tangent_part.impulse = na::zero(); for j in 0..DIM - 1 { let gcross1 = mprops1 @@ -303,26 +300,12 @@ impl VelocityConstraint { } } - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = DeltaVel::zero(); - let mut mj_lambda2 = DeltaVel::zero(); - - VelocityConstraintElement::warmstart_group( - &self.elements[..self.num_contacts as usize], - &self.dir1, - #[cfg(feature = "dim3")] - &self.tangent1, - self.im1, - self.im2, - &mut mj_lambda1, - &mut mj_lambda2, - ); - - mj_lambdas[self.mj_lambda1 as usize] += mj_lambda1; - mj_lambdas[self.mj_lambda2 as usize] += mj_lambda2; - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { + pub fn solve( + &mut self, + mj_lambdas: &mut [DeltaVel], + solve_normal: bool, + solve_friction: bool, + ) { let mut mj_lambda1 = mj_lambdas[self.mj_lambda1 as usize]; let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; @@ -336,6 +319,8 @@ impl VelocityConstraint { self.limit, &mut mj_lambda1, &mut mj_lambda2, + solve_normal, + solve_friction, ); mj_lambdas[self.mj_lambda1 as usize] = mj_lambda1; @@ -349,7 +334,6 @@ impl VelocityConstraint { let contact_id = self.manifold_contact_id[k]; let active_contact = &mut manifold.points[contact_id as usize]; active_contact.data.impulse = self.elements[k].normal_part.impulse; - active_contact.data.rhs = self.elements[k].normal_part.rhs; #[cfg(feature = "dim2")] { @@ -357,12 +341,16 @@ impl VelocityConstraint { } #[cfg(feature = "dim3")] { - active_contact.data.tangent_impulse = self - .tangent_rot1 - .inverse_transform_vector(&self.elements[k].tangent_part.impulse); + active_contact.data.tangent_impulse = self.elements[k].tangent_part.impulse; } } } + + pub fn remove_bias_from_rhs(&mut self) { + for elt in &mut self.elements { + elt.normal_part.rhs = elt.normal_part.rhs_wo_bias; + } + } } #[inline(always)] @@ -371,7 +359,7 @@ pub(crate) fn compute_tangent_contact_directions( force_dir1: &Vector, linvel1: &Vector, linvel2: &Vector, -) -> ([Vector; DIM - 1], na::UnitComplex) +) -> [Vector; DIM - 1] where N: na::SimdRealField + Copy, N::Element: na::RealField + Copy, @@ -399,18 +387,5 @@ where let tangent1 = tangent_fallback.select(use_fallback, tangent_relative_linvel); let bitangent1 = force_dir1.cross(&tangent1); - // Rotation such that: rot * tangent_fallback = tangent1 - // (when projected in the tangent plane.) This is needed to ensure the - // warmstart impulse has the correct orientation. Indeed, at frame n + 1, - // we need to reapply the same impulse as we did in frame n. However the - // basis on which the tangent impulse is expresses may change at each frame - // (because the the relative linvel may change direction at each frame). - // So we need this rotation to: - // - Project the impulse back to the "reference" basis at after friction is resolved. - // - Project the old impulse on the new basis before the friction is resolved. - let rot = na::UnitComplex::new_unchecked(na::Complex::new( - tangent1.dot(&tangent_fallback), - bitangent1.dot(&tangent_fallback), - )); - ([tangent1, bitangent1], rot) + [tangent1, bitangent1] } diff --git a/src/dynamics/solver/velocity_constraint_element.rs b/src/dynamics/solver/velocity_constraint_element.rs index cb6d476..406f68e 100644 --- a/src/dynamics/solver/velocity_constraint_element.rs +++ b/src/dynamics/solver/velocity_constraint_element.rs @@ -9,48 +9,23 @@ pub(crate) struct VelocityConstraintTangentPart { pub gcross2: [AngVector; DIM - 1], pub rhs: [N; DIM - 1], #[cfg(feature = "dim2")] - pub impulse: [N; DIM - 1], + pub impulse: na::Vector1, #[cfg(feature = "dim3")] pub impulse: na::Vector2, pub r: [N; DIM - 1], } impl VelocityConstraintTangentPart { - #[cfg(any(not(target_arch = "wasm32"), feature = "simd-is-enabled"))] fn zero() -> Self { Self { gcross1: [na::zero(); DIM - 1], gcross2: [na::zero(); DIM - 1], rhs: [na::zero(); DIM - 1], - #[cfg(feature = "dim2")] - impulse: [na::zero(); DIM - 1], - #[cfg(feature = "dim3")] impulse: na::zero(), r: [na::zero(); DIM - 1], } } - #[inline] - pub fn warmstart( - &self, - tangents1: [&Vector; DIM - 1], - im1: N, - im2: N, - mj_lambda1: &mut DeltaVel, - mj_lambda2: &mut DeltaVel, - ) where - AngVector: WDot, Result = N>, - N::Element: SimdRealField + Copy, - { - for j in 0..DIM - 1 { - mj_lambda1.linear += tangents1[j] * (im1 * self.impulse[j]); - mj_lambda1.angular += self.gcross1[j] * self.impulse[j]; - - mj_lambda2.linear += tangents1[j] * (-im2 * self.impulse[j]); - mj_lambda2.angular += self.gcross2[j] * self.impulse[j]; - } - } - #[inline] pub fn solve( &mut self, @@ -125,40 +100,23 @@ pub(crate) struct VelocityConstraintNormalPart { pub gcross1: AngVector, pub gcross2: AngVector, pub rhs: N, + pub rhs_wo_bias: N, pub impulse: N, pub r: N, } impl VelocityConstraintNormalPart { - #[cfg(any(not(target_arch = "wasm32"), feature = "simd-is-enabled"))] fn zero() -> Self { Self { gcross1: na::zero(), gcross2: na::zero(), rhs: na::zero(), + rhs_wo_bias: na::zero(), impulse: na::zero(), r: na::zero(), } } - #[inline] - pub fn warmstart( - &self, - dir1: &Vector, - im1: N, - im2: N, - mj_lambda1: &mut DeltaVel, - mj_lambda2: &mut DeltaVel, - ) where - AngVector: WDot, Result = N>, - { - mj_lambda1.linear += dir1 * (im1 * self.impulse); - mj_lambda1.angular += self.gcross1 * self.impulse; - - mj_lambda2.linear += dir1 * (-im2 * self.impulse); - mj_lambda2.angular += self.gcross2 * self.impulse; - } - #[inline] pub fn solve( &mut self, @@ -193,7 +151,6 @@ pub(crate) struct VelocityConstraintElement { } impl VelocityConstraintElement { - #[cfg(any(not(target_arch = "wasm32"), feature = "simd-is-enabled"))] pub fn zero() -> Self { Self { normal_part: VelocityConstraintNormalPart::zero(), @@ -201,35 +158,6 @@ impl VelocityConstraintElement { } } - #[inline] - pub fn warmstart_group( - elements: &[Self], - dir1: &Vector, - #[cfg(feature = "dim3")] tangent1: &Vector, - im1: N, - im2: N, - mj_lambda1: &mut DeltaVel, - mj_lambda2: &mut DeltaVel, - ) where - Vector: WBasis, - AngVector: WDot, Result = N>, - N::Element: SimdRealField + Copy, - { - #[cfg(feature = "dim3")] - let tangents1 = [tangent1, &dir1.cross(&tangent1)]; - #[cfg(feature = "dim2")] - let tangents1 = [&dir1.orthonormal_vector()]; - - for element in elements { - element - .tangent_part - .warmstart(tangents1, im1, im2, mj_lambda1, mj_lambda2); - element - .normal_part - .warmstart(dir1, im1, im2, mj_lambda1, mj_lambda2); - } - } - #[inline] pub fn solve_group( elements: &mut [Self], @@ -240,28 +168,34 @@ impl VelocityConstraintElement { limit: N, mj_lambda1: &mut DeltaVel, mj_lambda2: &mut DeltaVel, + solve_normal: bool, + solve_friction: bool, ) where Vector: WBasis, AngVector: WDot, Result = N>, N::Element: SimdRealField + Copy, { - // Solve friction. - #[cfg(feature = "dim3")] - let tangents1 = [tangent1, &dir1.cross(&tangent1)]; - #[cfg(feature = "dim2")] - let tangents1 = [&dir1.orthonormal_vector()]; - - for element in elements.iter_mut() { - let limit = limit * element.normal_part.impulse; - let part = &mut element.tangent_part; - part.solve(tangents1, im1, im2, limit, mj_lambda1, mj_lambda2); + // Solve penetration. + if solve_normal { + for element in elements.iter_mut() { + element + .normal_part + .solve(&dir1, im1, im2, mj_lambda1, mj_lambda2); + } } - // Solve penetration. - for element in elements.iter_mut() { - element - .normal_part - .solve(&dir1, im1, im2, mj_lambda1, mj_lambda2); + // Solve friction. + if solve_friction { + #[cfg(feature = "dim3")] + let tangents1 = [tangent1, &dir1.cross(&tangent1)]; + #[cfg(feature = "dim2")] + let tangents1 = [&dir1.orthonormal_vector()]; + + for element in elements.iter_mut() { + let limit = limit * element.normal_part.impulse; + let part = &mut element.tangent_part; + part.solve(tangents1, im1, im2, limit, mj_lambda1, mj_lambda2); + } } } } diff --git a/src/dynamics/solver/velocity_constraint_wide.rs b/src/dynamics/solver/velocity_constraint_wide.rs index baaf643..0e2e36a 100644 --- a/src/dynamics/solver/velocity_constraint_wide.rs +++ b/src/dynamics/solver/velocity_constraint_wide.rs @@ -10,7 +10,6 @@ use crate::math::{ #[cfg(feature = "dim2")] use crate::utils::WBasis; use crate::utils::{WAngularInertia, WCross, WDot}; -use na::SimdComplexField; use num::Zero; use simba::simd::{SimdPartialOrd, SimdValue}; @@ -19,8 +18,6 @@ pub(crate) struct WVelocityConstraint { pub dir1: Vector, // Non-penetration force direction for the first body. #[cfg(feature = "dim3")] pub tangent1: Vector, // One of the friction force directions. - #[cfg(feature = "dim3")] - pub tangent_rot1: na::UnitComplex, // Orientation of the tangent basis wrt. the reference basis. pub elements: [VelocityConstraintElement; MAX_MANIFOLD_POINTS], pub num_contacts: u8, pub im1: SimdReal, @@ -50,9 +47,9 @@ impl WVelocityConstraint { } let inv_dt = SimdReal::splat(params.inv_dt()); - let warmstart_correction_slope = SimdReal::splat(params.warmstart_correction_slope); let velocity_solve_fraction = SimdReal::splat(params.velocity_solve_fraction); - let velocity_based_erp_inv_dt = SimdReal::splat(params.velocity_based_erp_inv_dt()); + let erp_inv_dt = SimdReal::splat(params.erp_inv_dt()); + let allowed_lin_err = SimdReal::splat(params.allowed_linear_error); let handles1 = gather![|ii| manifolds[ii].data.rigid_body1.unwrap()]; let handles2 = gather![|ii| manifolds[ii].data.rigid_body2.unwrap()]; @@ -85,16 +82,12 @@ impl WVelocityConstraint { let mj_lambda1 = gather![|ii| ids1[ii].active_set_offset]; let mj_lambda2 = gather![|ii| ids2[ii].active_set_offset]; - let warmstart_multiplier = - SimdReal::from(gather![|ii| manifolds[ii].data.warmstart_multiplier]); - let warmstart_coeff = warmstart_multiplier * SimdReal::splat(params.warmstart_coeff); let num_active_contacts = manifolds[0].data.num_active_contacts(); #[cfg(feature = "dim2")] let tangents1 = force_dir1.orthonormal_basis(); #[cfg(feature = "dim3")] - let (tangents1, tangent_rot1) = - super::compute_tangent_contact_directions(&force_dir1, &linvel1, &linvel2); + let tangents1 = super::compute_tangent_contact_directions(&force_dir1, &linvel1, &linvel2); for l in (0..num_active_contacts).step_by(MAX_MANIFOLD_POINTS) { let manifold_points = @@ -105,8 +98,6 @@ impl WVelocityConstraint { dir1: force_dir1, #[cfg(feature = "dim3")] tangent1: tangents1[0], - #[cfg(feature = "dim3")] - tangent_rot1, elements: [VelocityConstraintElement::zero(); MAX_MANIFOLD_POINTS], im1, im2, @@ -130,18 +121,12 @@ impl WVelocityConstraint { let tangent_velocity = Vector::from(gather![|ii| manifold_points[ii][k].tangent_velocity]); - let impulse = - SimdReal::from(gather![|ii| manifold_points[ii][k].warmstart_impulse]); - let prev_rhs = SimdReal::from(gather![|ii| manifold_points[ii][k].prev_rhs]); - let dp1 = point - world_com1; let dp2 = point - world_com2; let vel1 = linvel1 + angvel1.gcross(dp1); let vel2 = linvel2 + angvel2.gcross(dp2); - let warmstart_correction; - constraint.limit = friction; constraint.manifold_contact_id[k] = gather![|ii| manifold_points[ii][k].contact_id]; @@ -153,39 +138,25 @@ impl WVelocityConstraint { let r = SimdReal::splat(1.0) / (im1 + im2 + gcross1.gdot(gcross1) + gcross2.gdot(gcross2)); let projected_velocity = (vel1 - vel2).dot(&force_dir1); - let mut rhs = + let mut rhs_wo_bias = (SimdReal::splat(1.0) + is_bouncy * restitution) * projected_velocity; - rhs += dist.simd_max(SimdReal::zero()) * inv_dt; - rhs *= is_bouncy + is_resting * velocity_solve_fraction; - rhs += - dist.simd_min(SimdReal::zero()) * (velocity_based_erp_inv_dt * is_resting); - warmstart_correction = (warmstart_correction_slope - / (rhs - prev_rhs).simd_abs()) - .simd_min(warmstart_coeff); + rhs_wo_bias += (dist + allowed_lin_err).simd_max(SimdReal::zero()) * inv_dt; + rhs_wo_bias *= is_bouncy + is_resting * velocity_solve_fraction; + let rhs_bias = (dist + allowed_lin_err).simd_min(SimdReal::zero()) + * (erp_inv_dt/* * is_resting */); constraint.elements[k].normal_part = VelocityConstraintNormalPart { gcross1, gcross2, - rhs, - impulse: impulse * warmstart_correction, + rhs: rhs_wo_bias + rhs_bias, + rhs_wo_bias, + impulse: na::zero(), r, }; } // tangent parts. - #[cfg(feature = "dim2")] - let impulse = [SimdReal::from(gather![ - |ii| manifold_points[ii][k].warmstart_tangent_impulse - ]) * warmstart_correction]; - - #[cfg(feature = "dim3")] - let impulse = tangent_rot1 - * na::Vector2::from(gather![ - |ii| manifold_points[ii][k].warmstart_tangent_impulse - ]) - * warmstart_correction; - - constraint.elements[k].tangent_part.impulse = impulse; + constraint.elements[k].tangent_part.impulse = na::zero(); for j in 0..DIM - 1 { let gcross1 = ii1.transform_vector(dp1.gcross(tangents1[j])); @@ -210,43 +181,12 @@ impl WVelocityConstraint { } } - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda1 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda1[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda1[ii] as usize].angular - ]), - }; - - let mut mj_lambda2 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - VelocityConstraintElement::warmstart_group( - &self.elements[..self.num_contacts as usize], - &self.dir1, - #[cfg(feature = "dim3")] - &self.tangent1, - self.im1, - self.im2, - &mut mj_lambda1, - &mut mj_lambda2, - ); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda1[ii] as usize].linear = mj_lambda1.linear.extract(ii); - mj_lambdas[self.mj_lambda1[ii] as usize].angular = mj_lambda1.angular.extract(ii); - } - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { + pub fn solve( + &mut self, + mj_lambdas: &mut [DeltaVel], + solve_normal: bool, + solve_friction: bool, + ) { let mut mj_lambda1 = DeltaVel { linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda1[ii] as usize].linear]), angular: AngVector::from(gather![ @@ -271,6 +211,8 @@ impl WVelocityConstraint { self.limit, &mut mj_lambda1, &mut mj_lambda2, + solve_normal, + solve_friction, ); for ii in 0..SIMD_WIDTH { @@ -286,19 +228,15 @@ impl WVelocityConstraint { pub fn writeback_impulses(&self, manifolds_all: &mut [&mut ContactManifold]) { for k in 0..self.num_contacts as usize { let impulses: [_; SIMD_WIDTH] = self.elements[k].normal_part.impulse.into(); - let rhs: [_; SIMD_WIDTH] = self.elements[k].normal_part.rhs.into(); #[cfg(feature = "dim2")] let tangent_impulses: [_; SIMD_WIDTH] = self.elements[k].tangent_part.impulse[0].into(); #[cfg(feature = "dim3")] - let tangent_impulses = self - .tangent_rot1 - .inverse_transform_vector(&self.elements[k].tangent_part.impulse); + let tangent_impulses = self.elements[k].tangent_part.impulse; for ii in 0..SIMD_WIDTH { let manifold = &mut manifolds_all[self.manifold_id[ii]]; let contact_id = self.manifold_contact_id[k][ii]; let active_contact = &mut manifold.points[contact_id as usize]; - active_contact.data.rhs = rhs[ii]; active_contact.data.impulse = impulses[ii]; #[cfg(feature = "dim2")] @@ -312,4 +250,10 @@ impl WVelocityConstraint { } } } + + pub fn remove_bias_from_rhs(&mut self) { + for elt in &mut self.elements { + elt.normal_part.rhs = elt.normal_part.rhs_wo_bias; + } + } } diff --git a/src/dynamics/solver/velocity_ground_constraint.rs b/src/dynamics/solver/velocity_ground_constraint.rs index d1d5e8c..87865b3 100644 --- a/src/dynamics/solver/velocity_ground_constraint.rs +++ b/src/dynamics/solver/velocity_ground_constraint.rs @@ -21,8 +21,6 @@ pub(crate) struct VelocityGroundConstraint { pub limit: Real, pub elements: [VelocityGroundConstraintElement; MAX_MANIFOLD_POINTS], - #[cfg(feature = "dim3")] - pub tangent_rot1: na::UnitComplex, // Orientation of the tangent basis wrt. the reference basis. pub manifold_id: ContactManifoldIndex, pub manifold_contact_id: [u8; MAX_MANIFOLD_POINTS], pub num_contacts: u8, @@ -42,7 +40,7 @@ impl VelocityGroundConstraint { + ComponentSet, { let inv_dt = params.inv_dt(); - let velocity_based_erp_inv_dt = params.velocity_based_erp_inv_dt(); + let erp_inv_dt = params.erp_inv_dt(); let mut handle1 = manifold.data.rigid_body1; let mut handle2 = manifold.data.rigid_body2; @@ -69,11 +67,10 @@ impl VelocityGroundConstraint { #[cfg(feature = "dim2")] let tangents1 = force_dir1.orthonormal_basis(); #[cfg(feature = "dim3")] - let (tangents1, tangent_rot1) = + let tangents1 = super::compute_tangent_contact_directions(&force_dir1, &vels1.linvel, &vels2.linvel); let mj_lambda2 = ids2.active_set_offset; - let warmstart_coeff = manifold.data.warmstart_multiplier * params.warmstart_coeff; for (_l, manifold_points) in manifold .data @@ -86,8 +83,6 @@ impl VelocityGroundConstraint { dir1: force_dir1, #[cfg(feature = "dim3")] tangent1: tangents1[0], - #[cfg(feature = "dim3")] - tangent_rot1, elements: [VelocityGroundConstraintElement::zero(); MAX_MANIFOLD_POINTS], im2: mprops2.effective_inv_mass, limit: 0.0, @@ -106,7 +101,7 @@ impl VelocityGroundConstraint { // avoid spurious copying. // Is this optimization beneficial when targeting non-WASM platforms? // - // NOTE: joints have the same problem, but it is not easy to refactor the code that way + // NOTE: impulse_joints have the same problem, but it is not easy to refactor the code that way // for the moment. #[cfg(target_arch = "wasm32")] let constraint = if push { @@ -133,7 +128,6 @@ impl VelocityGroundConstraint { #[cfg(feature = "dim3")] { constraint.tangent1 = tangents1[0]; - constraint.tangent_rot1 = tangent_rot1; } constraint.im2 = mprops2.effective_inv_mass; constraint.limit = 0.0; @@ -149,7 +143,6 @@ impl VelocityGroundConstraint { let dp1 = manifold_point.point - world_com1; let vel1 = vels1.linvel + vels1.angvel.gcross(dp1); let vel2 = vels2.linvel + vels2.angvel.gcross(dp2); - let warmstart_correction; constraint.limit = manifold_point.friction; constraint.manifold_contact_id[k] = manifold_point.contact_id; @@ -165,33 +158,27 @@ impl VelocityGroundConstraint { let is_bouncy = manifold_point.is_bouncy() as u32 as Real; let is_resting = 1.0 - is_bouncy; - let mut rhs = (1.0 + is_bouncy * manifold_point.restitution) + let mut rhs_wo_bias = (1.0 + is_bouncy * manifold_point.restitution) * (vel1 - vel2).dot(&force_dir1); - rhs += manifold_point.dist.max(0.0) * inv_dt; - rhs *= is_bouncy + is_resting * params.velocity_solve_fraction; - rhs += is_resting * velocity_based_erp_inv_dt * manifold_point.dist.min(0.0); - warmstart_correction = (params.warmstart_correction_slope - / (rhs - manifold_point.prev_rhs).abs()) - .min(warmstart_coeff); + rhs_wo_bias += + (manifold_point.dist + params.allowed_linear_error).max(0.0) * inv_dt; + rhs_wo_bias *= is_bouncy + is_resting * params.velocity_solve_fraction; + let rhs_bias = /* is_resting + * */ erp_inv_dt + * (manifold_point.dist + params.allowed_linear_error).min(0.0); constraint.elements[k].normal_part = VelocityGroundConstraintNormalPart { gcross2, - rhs, - impulse: manifold_point.warmstart_impulse * warmstart_correction, + rhs: rhs_wo_bias + rhs_bias, + rhs_wo_bias, + impulse: 0.0, r, }; } // Tangent parts. { - #[cfg(feature = "dim3")] - let impulse = tangent_rot1 - * manifold_points[k].warmstart_tangent_impulse - * warmstart_correction; - #[cfg(feature = "dim2")] - let impulse = - [manifold_points[k].warmstart_tangent_impulse * warmstart_correction]; - constraint.elements[k].tangent_part.impulse = impulse; + constraint.elements[k].tangent_part.impulse = na::zero(); for j in 0..DIM - 1 { let gcross2 = mprops2 @@ -219,23 +206,12 @@ impl VelocityGroundConstraint { } } - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = DeltaVel::zero(); - - VelocityGroundConstraintElement::warmstart_group( - &self.elements[..self.num_contacts as usize], - &self.dir1, - #[cfg(feature = "dim3")] - &self.tangent1, - self.im2, - &mut mj_lambda2, - ); - - mj_lambdas[self.mj_lambda2 as usize].linear += mj_lambda2.linear; - mj_lambdas[self.mj_lambda2 as usize].angular += mj_lambda2.angular; - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { + pub fn solve( + &mut self, + mj_lambdas: &mut [DeltaVel], + solve_normal: bool, + solve_friction: bool, + ) { let mut mj_lambda2 = mj_lambdas[self.mj_lambda2 as usize]; VelocityGroundConstraintElement::solve_group( @@ -246,6 +222,8 @@ impl VelocityGroundConstraint { self.im2, self.limit, &mut mj_lambda2, + solve_normal, + solve_friction, ); mj_lambdas[self.mj_lambda2 as usize] = mj_lambda2; @@ -259,7 +237,6 @@ impl VelocityGroundConstraint { let contact_id = self.manifold_contact_id[k]; let active_contact = &mut manifold.points[contact_id as usize]; active_contact.data.impulse = self.elements[k].normal_part.impulse; - active_contact.data.rhs = self.elements[k].normal_part.rhs; #[cfg(feature = "dim2")] { @@ -267,10 +244,14 @@ impl VelocityGroundConstraint { } #[cfg(feature = "dim3")] { - active_contact.data.tangent_impulse = self - .tangent_rot1 - .inverse_transform_vector(&self.elements[k].tangent_part.impulse); + active_contact.data.tangent_impulse = self.elements[k].tangent_part.impulse; } } } + + pub fn remove_bias_from_rhs(&mut self) { + for elt in &mut self.elements { + elt.normal_part.rhs = elt.normal_part.rhs_wo_bias; + } + } } diff --git a/src/dynamics/solver/velocity_ground_constraint_element.rs b/src/dynamics/solver/velocity_ground_constraint_element.rs index 2d4d633..adfcfda 100644 --- a/src/dynamics/solver/velocity_ground_constraint_element.rs +++ b/src/dynamics/solver/velocity_ground_constraint_element.rs @@ -8,7 +8,7 @@ pub(crate) struct VelocityGroundConstraintTangentPart { pub gcross2: [AngVector; DIM - 1], pub rhs: [N; DIM - 1], #[cfg(feature = "dim2")] - pub impulse: [N; DIM - 1], + pub impulse: na::Vector1, #[cfg(feature = "dim3")] pub impulse: na::Vector2, pub r: [N; DIM - 1], @@ -20,27 +20,11 @@ impl VelocityGroundConstraintTangentPart { Self { gcross2: [na::zero(); DIM - 1], rhs: [na::zero(); DIM - 1], - #[cfg(feature = "dim2")] - impulse: [na::zero(); DIM - 1], - #[cfg(feature = "dim3")] impulse: na::zero(), r: [na::zero(); DIM - 1], } } - #[inline] - pub fn warmstart( - &self, - tangents1: [&Vector; DIM - 1], - im2: N, - mj_lambda2: &mut DeltaVel, - ) { - for j in 0..DIM - 1 { - mj_lambda2.linear += tangents1[j] * (-im2 * self.impulse[j]); - mj_lambda2.angular += self.gcross2[j] * self.impulse[j]; - } - } - #[inline] pub fn solve( &mut self, @@ -99,6 +83,7 @@ impl VelocityGroundConstraintTangentPart { pub(crate) struct VelocityGroundConstraintNormalPart { pub gcross2: AngVector, pub rhs: N, + pub rhs_wo_bias: N, pub impulse: N, pub r: N, } @@ -109,17 +94,12 @@ impl VelocityGroundConstraintNormalPart { Self { gcross2: na::zero(), rhs: na::zero(), + rhs_wo_bias: na::zero(), impulse: na::zero(), r: na::zero(), } } - #[inline] - pub fn warmstart(&self, dir1: &Vector, im2: N, mj_lambda2: &mut DeltaVel) { - mj_lambda2.linear += dir1 * (-im2 * self.impulse); - mj_lambda2.angular += self.gcross2 * self.impulse; - } - #[inline] pub fn solve(&mut self, dir1: &Vector, im2: N, mj_lambda2: &mut DeltaVel) where @@ -151,29 +131,6 @@ impl VelocityGroundConstraintElement { } } - #[inline] - pub fn warmstart_group( - elements: &[Self], - dir1: &Vector, - #[cfg(feature = "dim3")] tangent1: &Vector, - im2: N, - mj_lambda2: &mut DeltaVel, - ) where - Vector: WBasis, - AngVector: WDot, Result = N>, - N::Element: SimdRealField + Copy, - { - #[cfg(feature = "dim3")] - let tangents1 = [tangent1, &dir1.cross(&tangent1)]; - #[cfg(feature = "dim2")] - let tangents1 = [&dir1.orthonormal_vector()]; - - for element in elements { - element.normal_part.warmstart(dir1, im2, mj_lambda2); - element.tangent_part.warmstart(tangents1, im2, mj_lambda2); - } - } - #[inline] pub fn solve_group( elements: &mut [Self], @@ -182,26 +139,32 @@ impl VelocityGroundConstraintElement { im2: N, limit: N, mj_lambda2: &mut DeltaVel, + solve_normal: bool, + solve_friction: bool, ) where Vector: WBasis, AngVector: WDot, Result = N>, N::Element: SimdRealField + Copy, { - // Solve friction. - #[cfg(feature = "dim3")] - let tangents1 = [tangent1, &dir1.cross(&tangent1)]; - #[cfg(feature = "dim2")] - let tangents1 = [&dir1.orthonormal_vector()]; - - for element in elements.iter_mut() { - let limit = limit * element.normal_part.impulse; - let part = &mut element.tangent_part; - part.solve(tangents1, im2, limit, mj_lambda2); + // Solve penetration. + if solve_normal { + for element in elements.iter_mut() { + element.normal_part.solve(&dir1, im2, mj_lambda2); + } } - // Solve penetration. - for element in elements.iter_mut() { - element.normal_part.solve(&dir1, im2, mj_lambda2); + // Solve friction. + if solve_friction { + #[cfg(feature = "dim3")] + let tangents1 = [tangent1, &dir1.cross(&tangent1)]; + #[cfg(feature = "dim2")] + let tangents1 = [&dir1.orthonormal_vector()]; + + for element in elements.iter_mut() { + let limit = limit * element.normal_part.impulse; + let part = &mut element.tangent_part; + part.solve(tangents1, im2, limit, mj_lambda2); + } } } } diff --git a/src/dynamics/solver/velocity_ground_constraint_wide.rs b/src/dynamics/solver/velocity_ground_constraint_wide.rs index 3aede0b..e1ea8f6 100644 --- a/src/dynamics/solver/velocity_ground_constraint_wide.rs +++ b/src/dynamics/solver/velocity_ground_constraint_wide.rs @@ -11,7 +11,6 @@ use crate::math::{ #[cfg(feature = "dim2")] use crate::utils::WBasis; use crate::utils::{WAngularInertia, WCross, WDot}; -use na::SimdComplexField; use num::Zero; use simba::simd::{SimdPartialOrd, SimdValue}; @@ -20,8 +19,6 @@ pub(crate) struct WVelocityGroundConstraint { pub dir1: Vector, // Non-penetration force direction for the first body. #[cfg(feature = "dim3")] pub tangent1: Vector, // One of the friction force directions. - #[cfg(feature = "dim3")] - pub tangent_rot1: na::UnitComplex, // Orientation of the tangent basis wrt. the reference basis. pub elements: [VelocityGroundConstraintElement; MAX_MANIFOLD_POINTS], pub num_contacts: u8, pub im2: SimdReal, @@ -46,7 +43,8 @@ impl WVelocityGroundConstraint { { let inv_dt = SimdReal::splat(params.inv_dt()); let velocity_solve_fraction = SimdReal::splat(params.velocity_solve_fraction); - let velocity_based_erp_inv_dt = SimdReal::splat(params.velocity_based_erp_inv_dt()); + let erp_inv_dt = SimdReal::splat(params.erp_inv_dt()); + let allowed_lin_err = SimdReal::splat(params.allowed_linear_error); let mut handles1 = gather![|ii| manifolds[ii].data.rigid_body1]; let mut handles2 = gather![|ii| manifolds[ii].data.rigid_body2]; @@ -95,17 +93,12 @@ impl WVelocityGroundConstraint { let mj_lambda2 = gather![|ii| ids2[ii].active_set_offset]; - let warmstart_multiplier = - SimdReal::from(gather![|ii| manifolds[ii].data.warmstart_multiplier]); - let warmstart_coeff = warmstart_multiplier * SimdReal::splat(params.warmstart_coeff); - let warmstart_correction_slope = SimdReal::splat(params.warmstart_correction_slope); let num_active_contacts = manifolds[0].data.num_active_contacts(); #[cfg(feature = "dim2")] let tangents1 = force_dir1.orthonormal_basis(); #[cfg(feature = "dim3")] - let (tangents1, tangent_rot1) = - super::compute_tangent_contact_directions(&force_dir1, &linvel1, &linvel2); + let tangents1 = super::compute_tangent_contact_directions(&force_dir1, &linvel1, &linvel2); for l in (0..num_active_contacts).step_by(MAX_MANIFOLD_POINTS) { let manifold_points = gather![|ii| &manifolds[ii].data.solver_contacts[l..]]; @@ -115,8 +108,6 @@ impl WVelocityGroundConstraint { dir1: force_dir1, #[cfg(feature = "dim3")] tangent1: tangents1[0], - #[cfg(feature = "dim3")] - tangent_rot1, elements: [VelocityGroundConstraintElement::zero(); MAX_MANIFOLD_POINTS], im2, limit: SimdReal::splat(0.0), @@ -138,15 +129,11 @@ impl WVelocityGroundConstraint { let tangent_velocity = Vector::from(gather![|ii| manifold_points[ii][k].tangent_velocity]); - let impulse = - SimdReal::from(gather![|ii| manifold_points[ii][k].warmstart_impulse]); - let prev_rhs = SimdReal::from(gather![|ii| manifold_points[ii][k].prev_rhs]); let dp1 = point - world_com1; let dp2 = point - world_com2; let vel1 = linvel1 + angvel1.gcross(dp1); let vel2 = linvel2 + angvel2.gcross(dp2); - let warmstart_correction; constraint.limit = friction; constraint.manifold_contact_id[k] = gather![|ii| manifold_points[ii][k].contact_id]; @@ -157,36 +144,24 @@ impl WVelocityGroundConstraint { let r = SimdReal::splat(1.0) / (im2 + gcross2.gdot(gcross2)); let projected_velocity = (vel1 - vel2).dot(&force_dir1); - let mut rhs = + let mut rhs_wo_bias = (SimdReal::splat(1.0) + is_bouncy * restitution) * projected_velocity; - rhs += dist.simd_max(SimdReal::zero()) * inv_dt; - rhs *= is_bouncy + is_resting * velocity_solve_fraction; - rhs += - dist.simd_min(SimdReal::zero()) * (velocity_based_erp_inv_dt * is_resting); - warmstart_correction = (warmstart_correction_slope - / (rhs - prev_rhs).simd_abs()) - .simd_min(warmstart_coeff); + rhs_wo_bias += (dist + allowed_lin_err).simd_max(SimdReal::zero()) * inv_dt; + rhs_wo_bias *= is_bouncy + is_resting * velocity_solve_fraction; + let rhs_bias = (dist + allowed_lin_err).simd_min(SimdReal::zero()) + * (erp_inv_dt/* * is_resting */); constraint.elements[k].normal_part = VelocityGroundConstraintNormalPart { gcross2, - rhs, - impulse: impulse * warmstart_correction, + rhs: rhs_wo_bias + rhs_bias, + rhs_wo_bias, + impulse: na::zero(), r, }; } // tangent parts. - #[cfg(feature = "dim2")] - let impulse = [SimdReal::from(gather![ - |ii| manifold_points[ii][k].warmstart_tangent_impulse - ]) * warmstart_correction]; - #[cfg(feature = "dim3")] - let impulse = tangent_rot1 - * na::Vector2::from(gather![ - |ii| manifold_points[ii][k].warmstart_tangent_impulse - ]) - * warmstart_correction; - constraint.elements[k].tangent_part.impulse = impulse; + constraint.elements[k].tangent_part.impulse = na::zero(); for j in 0..DIM - 1 { let gcross2 = ii2.transform_vector(dp2.gcross(-tangents1[j])); @@ -208,30 +183,12 @@ impl WVelocityGroundConstraint { } } - pub fn warmstart(&self, mj_lambdas: &mut [DeltaVel]) { - let mut mj_lambda2 = DeltaVel { - linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), - angular: AngVector::from(gather![ - |ii| mj_lambdas[self.mj_lambda2[ii] as usize].angular - ]), - }; - - VelocityGroundConstraintElement::warmstart_group( - &self.elements[..self.num_contacts as usize], - &self.dir1, - #[cfg(feature = "dim3")] - &self.tangent1, - self.im2, - &mut mj_lambda2, - ); - - for ii in 0..SIMD_WIDTH { - mj_lambdas[self.mj_lambda2[ii] as usize].linear = mj_lambda2.linear.extract(ii); - mj_lambdas[self.mj_lambda2[ii] as usize].angular = mj_lambda2.angular.extract(ii); - } - } - - pub fn solve(&mut self, mj_lambdas: &mut [DeltaVel]) { + pub fn solve( + &mut self, + mj_lambdas: &mut [DeltaVel], + solve_normal: bool, + solve_friction: bool, + ) { let mut mj_lambda2 = DeltaVel { linear: Vector::from(gather![|ii| mj_lambdas[self.mj_lambda2[ii] as usize].linear]), angular: AngVector::from(gather![ @@ -247,6 +204,8 @@ impl WVelocityGroundConstraint { self.im2, self.limit, &mut mj_lambda2, + solve_normal, + solve_friction, ); for ii in 0..SIMD_WIDTH { @@ -258,20 +217,16 @@ impl WVelocityGroundConstraint { // FIXME: duplicated code. This is exactly the same as in the non-ground velocity constraint. pub fn writeback_impulses(&self, manifolds_all: &mut [&mut ContactManifold]) { for k in 0..self.num_contacts as usize { - let rhs: [_; SIMD_WIDTH] = self.elements[k].normal_part.rhs.into(); let impulses: [_; SIMD_WIDTH] = self.elements[k].normal_part.impulse.into(); #[cfg(feature = "dim2")] let tangent_impulses: [_; SIMD_WIDTH] = self.elements[k].tangent_part.impulse[0].into(); #[cfg(feature = "dim3")] - let tangent_impulses = self - .tangent_rot1 - .inverse_transform_vector(&self.elements[k].tangent_part.impulse); + let tangent_impulses = self.elements[k].tangent_part.impulse; for ii in 0..SIMD_WIDTH { let manifold = &mut manifolds_all[self.manifold_id[ii]]; let contact_id = self.manifold_contact_id[k][ii]; let active_contact = &mut manifold.points[contact_id as usize]; - active_contact.data.rhs = rhs[ii]; active_contact.data.impulse = impulses[ii]; #[cfg(feature = "dim2")] @@ -285,4 +240,10 @@ impl WVelocityGroundConstraint { } } } + + pub fn remove_bias_from_rhs(&mut self) { + for elt in &mut self.elements { + elt.normal_part.rhs = elt.normal_part.rhs_wo_bias; + } + } } diff --git a/src/dynamics/solver/velocity_solver.rs b/src/dynamics/solver/velocity_solver.rs index 9ceb9d9..9ecdbfb 100644 --- a/src/dynamics/solver/velocity_solver.rs +++ b/src/dynamics/solver/velocity_solver.rs @@ -1,22 +1,28 @@ use super::AnyJointVelocityConstraint; use crate::data::{BundleSet, ComponentSet, ComponentSetMut}; +use crate::dynamics::solver::GenericVelocityConstraint; use crate::dynamics::{ solver::{AnyVelocityConstraint, DeltaVel}, - IntegrationParameters, JointGraphEdge, RigidBodyForces, RigidBodyVelocity, + IntegrationParameters, JointGraphEdge, MultibodyJointSet, RigidBodyForces, RigidBodyType, + RigidBodyVelocity, }; use crate::dynamics::{IslandManager, RigidBodyIds, RigidBodyMassProps}; use crate::geometry::ContactManifold; use crate::math::Real; +use crate::prelude::{RigidBodyActivation, RigidBodyDamping, RigidBodyPosition}; use crate::utils::WAngularInertia; +use na::DVector; pub(crate) struct VelocitySolver { pub mj_lambdas: Vec>, + pub generic_mj_lambdas: DVector, } impl VelocitySolver { pub fn new() -> Self { Self { mj_lambdas: Vec::new(), + generic_mj_lambdas: DVector::zeros(0), } } @@ -26,20 +32,31 @@ impl VelocitySolver { params: &IntegrationParameters, islands: &IslandManager, bodies: &mut Bodies, + multibodies: &mut MultibodyJointSet, manifolds_all: &mut [&mut ContactManifold], joints_all: &mut [JointGraphEdge], contact_constraints: &mut [AnyVelocityConstraint], + generic_contact_constraints: &mut [GenericVelocityConstraint], + generic_contact_jacobians: &DVector, joint_constraints: &mut [AnyJointVelocityConstraint], + generic_joint_jacobians: &DVector, ) where Bodies: ComponentSet + ComponentSet + + ComponentSet + ComponentSetMut - + ComponentSet, + + ComponentSetMut + + ComponentSetMut + + ComponentSetMut + + ComponentSet, { self.mj_lambdas.clear(); self.mj_lambdas .resize(islands.active_island(island_id).len(), DeltaVel::zero()); + let total_multibodies_ndofs = multibodies.multibodies.iter().map(|m| m.1.ndofs()).sum(); + self.generic_mj_lambdas = DVector::zeros(total_multibodies_ndofs); + // Initialize delta-velocities (`mj_lambdas`) with external forces (gravity etc): for handle in islands.active_island(island_id) { let (ids, mprops, forces): (&RigidBodyIds, &RigidBodyMassProps, &RigidBodyForces) = @@ -53,43 +70,196 @@ impl VelocitySolver { dvel.linear += forces.force * (mprops.effective_inv_mass * params.dt); } - /* - * Warmstart constraints. - */ - for constraint in &*joint_constraints { - constraint.warmstart(&mut self.mj_lambdas[..]); - } - - for constraint in &*contact_constraints { - constraint.warmstart(&mut self.mj_lambdas[..]); + for (_, multibody) in multibodies.multibodies.iter_mut() { + let mut mj_lambdas = self + .generic_mj_lambdas + .rows_mut(multibody.solver_id, multibody.ndofs()); + mj_lambdas.axpy(params.dt, &multibody.accelerations, 0.0); } /* * Solve constraints. */ - for _ in 0..params.max_velocity_iterations { + for i in 0..params.max_velocity_iterations { + let solve_friction = params.interleave_restitution_and_friction_resolution + && params.max_velocity_friction_iterations + i >= params.max_velocity_iterations; + for constraint in &mut *joint_constraints { - constraint.solve(&mut self.mj_lambdas[..]); + constraint.solve( + generic_joint_jacobians, + &mut self.mj_lambdas[..], + &mut self.generic_mj_lambdas, + ); } for constraint in &mut *contact_constraints { - constraint.solve(&mut self.mj_lambdas[..]); + constraint.solve(&mut self.mj_lambdas[..], true, solve_friction); + } + + for constraint in &mut *generic_contact_constraints { + constraint.solve( + generic_contact_jacobians, + &mut self.mj_lambdas[..], + &mut self.generic_mj_lambdas, + true, + solve_friction, + ); } } - // Update velocities. + let remaining_friction_iterations = + if !params.interleave_restitution_and_friction_resolution { + params.max_velocity_friction_iterations + } else if params.max_velocity_friction_iterations > params.max_velocity_iterations { + params.max_velocity_friction_iterations - params.max_velocity_iterations + } else { + 0 + }; + + for _ in 0..remaining_friction_iterations { + for constraint in &mut *contact_constraints { + constraint.solve(&mut self.mj_lambdas[..], false, true); + } + + for constraint in &mut *generic_contact_constraints { + constraint.solve( + generic_contact_jacobians, + &mut self.mj_lambdas[..], + &mut self.generic_mj_lambdas, + false, + true, + ); + } + } + + // Update positions. for handle in islands.active_island(island_id) { - let (ids, mprops): (&RigidBodyIds, &RigidBodyMassProps) = bodies.index_bundle(handle.0); + if let Some(link) = multibodies.rigid_body_link(*handle).copied() { + let multibody = multibodies + .get_multibody_mut_internal(link.multibody) + .unwrap(); - let dvel = self.mj_lambdas[ids.active_set_offset]; - let dangvel = mprops - .effective_world_inv_inertia_sqrt - .transform_vector(dvel.angular); + if link.id == 0 || link.id == 1 && !multibody.root_is_dynamic { + let mj_lambdas = self + .generic_mj_lambdas + .rows(multibody.solver_id, multibody.ndofs()); + let prev_vels = multibody.velocities.clone(); // FIXME: avoid allocations. + multibody.velocities += mj_lambdas; + multibody.integrate_next(params.dt); + multibody.forward_kinematics_next(bodies, false); - bodies.map_mut_internal(handle.0, |vels| { - vels.linvel += dvel.linear; - vels.angvel += dangvel; - }); + if params.max_stabilization_iterations > 0 { + multibody.velocities = prev_vels; + } + } + } else { + let (ids, mprops): (&RigidBodyIds, &RigidBodyMassProps) = + bodies.index_bundle(handle.0); + + let dvel = self.mj_lambdas[ids.active_set_offset]; + let dangvel = mprops + .effective_world_inv_inertia_sqrt + .transform_vector(dvel.angular); + + // Update positions. + let (poss, vels, damping, mprops): ( + &RigidBodyPosition, + &RigidBodyVelocity, + &RigidBodyDamping, + &RigidBodyMassProps, + ) = bodies.index_bundle(handle.0); + + let mut new_poss = *poss; + let mut new_vels = *vels; + new_vels.linvel += dvel.linear; + new_vels.angvel += dangvel; + new_vels = new_vels.apply_damping(params.dt, damping); + new_poss.next_position = + new_vels.integrate(params.dt, &poss.position, &mprops.local_mprops.local_com); + bodies.set_internal(handle.0, new_poss); + + if params.max_stabilization_iterations == 0 { + bodies.set_internal(handle.0, new_vels); + } + } + } + + if params.max_stabilization_iterations > 0 { + for joint in &mut *joint_constraints { + joint.remove_bias_from_rhs(); + } + for constraint in &mut *contact_constraints { + constraint.remove_bias_from_rhs(); + } + for constraint in &mut *generic_contact_constraints { + constraint.remove_bias_from_rhs(); + } + + for _ in 0..params.max_stabilization_iterations { + for constraint in &mut *joint_constraints { + constraint.solve( + generic_joint_jacobians, + &mut self.mj_lambdas[..], + &mut self.generic_mj_lambdas, + ); + } + + for constraint in &mut *contact_constraints { + constraint.solve(&mut self.mj_lambdas[..], true, true); + } + + for constraint in &mut *generic_contact_constraints { + constraint.solve( + generic_contact_jacobians, + &mut self.mj_lambdas[..], + &mut self.generic_mj_lambdas, + true, + true, + ); + } + } + + // Update velocities. + for handle in islands.active_island(island_id) { + if let Some(link) = multibodies.rigid_body_link(*handle).copied() { + let multibody = multibodies + .get_multibody_mut_internal(link.multibody) + .unwrap(); + + if link.id == 0 || link.id == 1 && !multibody.root_is_dynamic { + let mj_lambdas = self + .generic_mj_lambdas + .rows(multibody.solver_id, multibody.ndofs()); + multibody.velocities += mj_lambdas; + } + } else { + let (ids, mprops): (&RigidBodyIds, &RigidBodyMassProps) = + bodies.index_bundle(handle.0); + + let dvel = self.mj_lambdas[ids.active_set_offset]; + let dangvel = mprops + .effective_world_inv_inertia_sqrt + .transform_vector(dvel.angular); + + // let mut curr_vel_pseudo_energy = 0.0; + bodies.map_mut_internal(handle.0, |vels: &mut RigidBodyVelocity| { + // curr_vel_pseudo_energy = vels.pseudo_kinetic_energy(); + vels.linvel += dvel.linear; + vels.angvel += dangvel; + }); + + // let impulse_vel_pseudo_energy = RigidBodyVelocity { + // linvel: dvel.linear, + // angvel: dangvel, + // } + // .pseudo_kinetic_energy(); + // + // bodies.map_mut_internal(handle.0, |activation: &mut RigidBodyActivation| { + // activation.energy = + // impulse_vel_pseudo_energy.max(curr_vel_pseudo_energy / 5.0); + // }); + } + } } // Write impulses back into the manifold structures. @@ -100,5 +270,9 @@ impl VelocitySolver { for constraint in &*contact_constraints { constraint.writeback_impulses(manifolds_all); } + + for constraint in &*generic_contact_constraints { + constraint.writeback_impulses(manifolds_all); + } } } diff --git a/src/geometry/broad_phase_multi_sap/broad_phase.rs b/src/geometry/broad_phase_multi_sap/broad_phase.rs index 0ed12b6..f4b9fa1 100644 --- a/src/geometry/broad_phase_multi_sap/broad_phase.rs +++ b/src/geometry/broad_phase_multi_sap/broad_phase.rs @@ -622,7 +622,7 @@ impl BroadPhase { #[cfg(test)] mod test { - use crate::dynamics::{IslandManager, JointSet, RigidBodyBuilder, RigidBodySet}; + use crate::dynamics::{IslandManager, ImpulseJointSet, RigidBodyBuilder, RigidBodySet}; use crate::geometry::{BroadPhase, ColliderBuilder, ColliderSet}; #[test] @@ -630,7 +630,7 @@ mod test { let mut broad_phase = BroadPhase::new(); let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); - let mut joints = JointSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); let mut islands = IslandManager::new(); let rb = RigidBodyBuilder::new_dynamic().build(); @@ -641,7 +641,7 @@ mod test { let mut events = Vec::new(); broad_phase.update(0.0, &mut colliders, &[coh], &[], &mut events); - bodies.remove(hrb, &mut islands, &mut colliders, &mut joints); + bodies.remove(hrb, &mut islands, &mut colliders, &mut impulse_joints); broad_phase.update(0.0, &mut colliders, &[], &[coh], &mut events); // Create another body. diff --git a/src/geometry/contact_pair.rs b/src/geometry/contact_pair.rs index f4e7834..92b7800 100644 --- a/src/geometry/contact_pair.rs +++ b/src/geometry/contact_pair.rs @@ -35,8 +35,6 @@ pub struct ContactData { /// collider's rigid-body. #[cfg(feature = "dim3")] pub tangent_impulse: na::Vector2, - /// The target velocity correction at the contact point. - pub rhs: Real, } impl Default for ContactData { @@ -44,7 +42,6 @@ impl Default for ContactData { Self { impulse: 0.0, tangent_impulse: na::zero(), - rhs: 0.0, } } } @@ -119,12 +116,9 @@ pub struct ContactManifoldData { pub rigid_body1: Option, /// The second rigid-body involved in this contact manifold. pub rigid_body2: Option, - pub(crate) warmstart_multiplier: Real, // The two following are set by the constraints solver. #[cfg_attr(feature = "serde-serialize", serde(skip))] pub(crate) constraint_index: usize, - #[cfg_attr(feature = "serde-serialize", serde(skip))] - pub(crate) position_constraint_index: usize, // We put the following fields here to avoids reading the colliders inside of the // contact preparation method. /// Flags used to control some aspects of the constraints solver for this contact manifold. @@ -177,26 +171,15 @@ pub struct SolverContact { /// This is set to zero by default. Set to a non-zero value to /// simulate, e.g., conveyor belts. pub tangent_velocity: Vector, - /// The warmstart impulse, along the contact normal, applied by this contact to the first collider's rigid-body. - pub warmstart_impulse: Real, - /// The warmstart friction impulse along the vector orthonormal to the contact normal, applied to the first - /// collider's rigid-body. - #[cfg(feature = "dim2")] - pub warmstart_tangent_impulse: Real, - /// The warmstart friction impulses along the basis orthonormal to the contact normal, applied to the first - /// collider's rigid-body. - #[cfg(feature = "dim3")] - pub warmstart_tangent_impulse: na::Vector2, - /// The last velocity correction targeted by this contact. - pub prev_rhs: Real, + /// Whether or not this contact existed during the last timestep. + pub is_new: bool, } impl SolverContact { /// Should we treat this contact as a bouncy contact? /// If `true`, use [`Self::restitution`]. pub fn is_bouncy(&self) -> bool { - let is_new = self.warmstart_impulse == 0.0; - if is_new { + if self.is_new { // Treat new collisions as bouncing at first, unless we have zero restitution. self.restitution > 0.0 } else { @@ -222,9 +205,7 @@ impl ContactManifoldData { Self { rigid_body1, rigid_body2, - warmstart_multiplier: Self::min_warmstart_multiplier(), constraint_index: 0, - position_constraint_index: 0, solver_flags, normal: Vector::zeros(), solver_contacts: Vec::new(), @@ -239,34 +220,4 @@ impl ContactManifoldData { pub fn num_active_contacts(&self) -> usize { self.solver_contacts.len() } - - pub(crate) fn min_warmstart_multiplier() -> Real { - // Multiplier used to reduce the amount of warm-starting. - // This coefficient increases exponentially over time, until it reaches 1.0. - // This will reduce significant overshoot at the timesteps that - // follow a timestep involving high-velocity impacts. - 1.0 // 0.01 - } - - // pub(crate) fn update_warmstart_multiplier(manifold: &mut ContactManifold) { - // // In 2D, tall stacks will actually suffer from this - // // because oscillation due to inaccuracies in 2D often - // // cause contacts to break, which would result in - // // a reset of the warmstart multiplier. - // if cfg!(feature = "dim2") { - // manifold.data.warmstart_multiplier = 1.0; - // return; - // } - // - // for pt in &manifold.points { - // if pt.data.impulse != 0.0 { - // manifold.data.warmstart_multiplier = - // (manifold.data.warmstart_multiplier * 2.0).min(1.0); - // return; - // } - // } - // - // // Reset the multiplier. - // manifold.data.warmstart_multiplier = Self::min_warmstart_multiplier() - // } } diff --git a/src/geometry/narrow_phase.rs b/src/geometry/narrow_phase.rs index 82ee99d..643b584 100644 --- a/src/geometry/narrow_phase.rs +++ b/src/geometry/narrow_phase.rs @@ -968,9 +968,7 @@ impl NarrowPhase { friction, restitution, tangent_velocity: Vector::zeros(), - warmstart_impulse: contact.data.impulse, - warmstart_tangent_impulse: contact.data.tangent_impulse, - prev_rhs: contact.data.rhs, + is_new: contact.data.impulse == 0.0, }; manifold.data.solver_contacts.push(solver_contact); @@ -1027,7 +1025,7 @@ impl NarrowPhase { } /// Retrieve all the interactions with at least one contact point, happening between two active bodies. - // NOTE: this is very similar to the code from JointSet::select_active_interactions. + // NOTE: this is very similar to the code from ImpulseJointSet::select_active_interactions. pub(crate) fn select_active_contacts<'a, Bodies>( &'a mut self, islands: &IslandManager, diff --git a/src/lib.rs b/src/lib.rs index 57e1f0d..515384d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ //! User documentation for Rapier is on [the official Rapier site](https://rapier.rs/docs/). #![deny(bare_trait_objects)] -#![warn(missing_docs)] +#![allow(missing_docs)] // FIXME: deny that #[cfg(all(feature = "dim2", feature = "f32"))] pub extern crate parry2d as parry; @@ -140,16 +140,64 @@ pub mod utils; /// Elementary mathematical entities (vectors, matrices, isometries, etc). pub mod math { pub use parry::math::*; + + /* + * 2D + */ /// Max number of pairs of contact points from the same /// contact manifold that can be solved as part of a /// single contact constraint. #[cfg(feature = "dim2")] pub const MAX_MANIFOLD_POINTS: usize = 2; + + /// The type of a constraint Jacobian in twist coordinates. + #[cfg(feature = "dim2")] + pub type Jacobian = na::Matrix3xX; + + /// The type of a slice of the constraint Jacobian in twist coordinates. + #[cfg(feature = "dim2")] + pub type JacobianSlice<'a, N> = na::MatrixSlice3xX<'a, N>; + + /// The type of a mutable slice of the constraint Jacobian in twist coordinates. + #[cfg(feature = "dim2")] + pub type JacobianSliceMut<'a, N> = na::MatrixSliceMut3xX<'a, N>; + + /// The maximum number of possible rotations and translations of a rigid body. + #[cfg(feature = "dim2")] + pub const SPATIAL_DIM: usize = 3; + + /// The maximum number of rotational degrees of freedom of a rigid-body. + #[cfg(feature = "dim2")] + pub const ANG_DIM: usize = 1; + + /* + * 3D + */ /// Max number of pairs of contact points from the same /// contact manifold that can be solved as part of a /// single contact constraint. #[cfg(feature = "dim3")] pub const MAX_MANIFOLD_POINTS: usize = 4; + + /// The type of a constraint Jacobian in twist coordinates. + #[cfg(feature = "dim3")] + pub type Jacobian = na::Matrix6xX; + + /// The type of a slice of the constraint Jacobian in twist coordinates. + #[cfg(feature = "dim3")] + pub type JacobianSlice<'a, N> = na::MatrixSlice6xX<'a, N>; + + /// The type of a mutable slice of the constraint Jacobian in twist coordinates. + #[cfg(feature = "dim3")] + pub type JacobianSliceMut<'a, N> = na::MatrixSliceMut6xX<'a, N>; + + /// The maximum number of possible rotations and translations of a rigid body. + #[cfg(feature = "dim3")] + pub const SPATIAL_DIM: usize = 6; + + /// The maximum number of rotational degrees of freedom of a rigid-body. + #[cfg(feature = "dim3")] + pub const ANG_DIM: usize = 3; } /// Prelude containing the common types defined by Rapier. diff --git a/src/pipeline/physics_pipeline.rs b/src/pipeline/physics_pipeline.rs index 4ba8bfa..b244860 100644 --- a/src/pipeline/physics_pipeline.rs +++ b/src/pipeline/physics_pipeline.rs @@ -5,10 +5,10 @@ use crate::data::{BundleSet, ComponentSet, ComponentSetMut, ComponentSetOption}; #[cfg(not(feature = "parallel"))] use crate::dynamics::IslandSolver; use crate::dynamics::{ - CCDSolver, IntegrationParameters, IslandManager, JointSet, RigidBodyActivation, RigidBodyCcd, - RigidBodyChanges, RigidBodyColliders, RigidBodyDamping, RigidBodyDominance, RigidBodyForces, - RigidBodyHandle, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition, RigidBodyType, - RigidBodyVelocity, + CCDSolver, ImpulseJointSet, IntegrationParameters, IslandManager, MultibodyJointSet, + RigidBodyActivation, RigidBodyCcd, RigidBodyChanges, RigidBodyColliders, RigidBodyDamping, + RigidBodyDominance, RigidBodyForces, RigidBodyHandle, RigidBodyIds, RigidBodyMassProps, + RigidBodyPosition, RigidBodyType, RigidBodyVelocity, }; #[cfg(feature = "parallel")] use crate::dynamics::{JointGraphEdge, ParallelIslandSolver as IslandSolver}; @@ -155,60 +155,6 @@ impl PhysicsPipeline { self.counters.stages.collision_detection_time.pause(); } - fn solve_position_constraints( - &mut self, - integration_parameters: &IntegrationParameters, - islands: &IslandManager, - bodies: &mut Bodies, - ) where - Bodies: ComponentSet + ComponentSetMut, - { - #[cfg(not(feature = "parallel"))] - { - enable_flush_to_zero!(); - - for island_id in 0..islands.num_islands() { - self.solvers[island_id].solve_position_constraints( - island_id, - islands, - &mut self.counters, - integration_parameters, - bodies, - ) - } - } - - #[cfg(feature = "parallel")] - { - use rayon::prelude::*; - use std::sync::atomic::Ordering; - - let num_islands = islands.num_islands(); - let solvers = &mut self.solvers[..num_islands]; - let bodies = &std::sync::atomic::AtomicPtr::new(bodies as *mut _); - - rayon::scope(|scope| { - enable_flush_to_zero!(); - - solvers - .par_iter_mut() - .enumerate() - .for_each(|(island_id, solver)| { - let bodies: &mut Bodies = - unsafe { std::mem::transmute(bodies.load(Ordering::Relaxed)) }; - - solver.solve_position_constraints( - scope, - island_id, - islands, - integration_parameters, - bodies, - ) - }); - }); - } - } - fn build_islands_and_solve_velocity_constraints( &mut self, gravity: &Vector, @@ -217,7 +163,8 @@ impl PhysicsPipeline { narrow_phase: &mut NarrowPhase, bodies: &mut Bodies, colliders: &mut Colliders, - joints: &mut JointSet, + impulse_joints: &mut ImpulseJointSet, + multibody_joints: &mut MultibodyJointSet, ) where Bodies: ComponentSetMut + ComponentSetMut @@ -235,7 +182,8 @@ impl PhysicsPipeline { bodies, colliders, narrow_phase, - joints, + impulse_joints, + multibody_joints, integration_parameters.min_island_size, ); self.counters.stages.island_construction_time.pause(); @@ -257,7 +205,11 @@ impl PhysicsPipeline { &mut manifolds, &mut self.manifold_indices, ); - joints.select_active_interactions(islands, bodies, &mut self.joint_constraint_indices); + impulse_joints.select_active_interactions( + islands, + bodies, + &mut self.joint_constraint_indices, + ); self.counters.stages.update_time.resume(); for handle in islands.active_dynamic_bodies() { @@ -274,6 +226,13 @@ impl PhysicsPipeline { forces.add_gravity_acceleration(&gravity, effective_inv_mass) }); } + + for multibody in &mut multibody_joints.multibodies { + multibody + .1 + .update_dynamics(integration_parameters.dt, bodies); + multibody.1.update_acceleration(bodies); + } self.counters.stages.update_time.pause(); self.counters.stages.solver_time.resume(); @@ -287,7 +246,7 @@ impl PhysicsPipeline { enable_flush_to_zero!(); for island_id in 0..islands.num_islands() { - self.solvers[island_id].init_constraints_and_solve_velocity_constraints( + self.solvers[island_id].init_and_solve( island_id, &mut self.counters, integration_parameters, @@ -295,8 +254,9 @@ impl PhysicsPipeline { bodies, &mut manifolds[..], &self.manifold_indices[island_id], - joints.joints_mut(), + impulse_joints.joints_mut(), &self.joint_constraint_indices[island_id], + multibody_joints, ) } } @@ -311,7 +271,9 @@ impl PhysicsPipeline { let solvers = &mut self.solvers[..num_islands]; let bodies = &std::sync::atomic::AtomicPtr::new(bodies as *mut _); let manifolds = &std::sync::atomic::AtomicPtr::new(&mut manifolds as *mut _); - let joints = &std::sync::atomic::AtomicPtr::new(joints.joints_vec_mut() as *mut _); + let impulse_joints = + &std::sync::atomic::AtomicPtr::new(impulse_joints.joints_vec_mut() as *mut _); + let multibody_joints = &std::sync::atomic::AtomicPtr::new(multibody_joints as *mut _); let manifold_indices = &self.manifold_indices[..]; let joint_constraint_indices = &self.joint_constraint_indices[..]; @@ -326,10 +288,13 @@ impl PhysicsPipeline { unsafe { std::mem::transmute(bodies.load(Ordering::Relaxed)) }; let manifolds: &mut Vec<&mut ContactManifold> = unsafe { std::mem::transmute(manifolds.load(Ordering::Relaxed)) }; - let joints: &mut Vec = - unsafe { std::mem::transmute(joints.load(Ordering::Relaxed)) }; + let impulse_joints: &mut Vec = + unsafe { std::mem::transmute(impulse_joints.load(Ordering::Relaxed)) }; + let multibody_joints: &mut MultibodyJointSet = unsafe { + std::mem::transmute(multibody_joints.load(Ordering::Relaxed)) + }; - solver.init_constraints_and_solve_velocity_constraints( + solver.init_and_solve( scope, island_id, islands, @@ -337,8 +302,9 @@ impl PhysicsPipeline { bodies, manifolds, &manifold_indices[island_id], - joints, + impulse_joints, &joint_constraint_indices[island_id], + multibody_joints, ) }); }); @@ -485,7 +451,8 @@ impl PhysicsPipeline { narrow_phase: &mut NarrowPhase, bodies: &mut RigidBodySet, colliders: &mut ColliderSet, - joints: &mut JointSet, + impulse_joints: &mut ImpulseJointSet, + multibody_joints: &mut MultibodyJointSet, ccd_solver: &mut CCDSolver, hooks: &dyn PhysicsHooks, events: &dyn EventHandler, @@ -505,7 +472,8 @@ impl PhysicsPipeline { &mut modified_bodies, &mut modified_colliders, &mut removed_colliders, - joints, + impulse_joints, + multibody_joints, ccd_solver, hooks, events, @@ -525,7 +493,8 @@ impl PhysicsPipeline { modified_bodies: &mut Vec, modified_colliders: &mut Vec, removed_colliders: &mut Vec, - joints: &mut JointSet, + impulse_joints: &mut ImpulseJointSet, + multibody_joints: &mut MultibodyJointSet, ccd_solver: &mut CCDSolver, hooks: &dyn PhysicsHooks, events: &dyn EventHandler, @@ -567,6 +536,15 @@ impl PhysicsPipeline { modified_colliders, ); + // TODO: do this only on user-change. + // TODO: do we want some kind of automatic inverse kinematics? + for multibody in &mut multibody_joints.multibodies { + multibody.1.update_root_type(bodies); + // FIXME: what should we do here? We should not + // rely on the next state here. + multibody.1.forward_kinematics_next(bodies, true); + } + self.detect_collisions( integration_parameters, islands, @@ -662,7 +640,8 @@ impl PhysicsPipeline { narrow_phase, bodies, colliders, - joints, + impulse_joints, + multibody_joints, ); // If CCD is enabled, execute the CCD motion clamping. @@ -688,15 +667,6 @@ impl PhysicsPipeline { } } - // NOTE: we need to run the position solver **after** the - // CCD motion clamping because otherwise the clamping - // would undo the depenetration done by the position - // solver. - // This happens because our CCD use the real rigid-body - // velocities instead of just interpolating between - // isometries. - self.solve_position_constraints(&integration_parameters, islands, bodies); - let clear_forces = remaining_substeps == 0; self.advance_to_final_positions( islands, @@ -705,6 +675,7 @@ impl PhysicsPipeline { modified_colliders, clear_forces, ); + self.detect_collisions( &integration_parameters, islands, @@ -729,7 +700,8 @@ impl PhysicsPipeline { #[cfg(test)] mod test { use crate::dynamics::{ - CCDSolver, IntegrationParameters, IslandManager, JointSet, RigidBodyBuilder, RigidBodySet, + CCDSolver, ImpulseJointSet, IntegrationParameters, IslandManager, RigidBodyBuilder, + RigidBodySet, }; use crate::geometry::{BroadPhase, ColliderBuilder, ColliderSet, NarrowPhase}; use crate::math::Vector; @@ -738,7 +710,7 @@ mod test { #[test] fn kinematic_and_static_contact_crash() { let mut colliders = ColliderSet::new(); - let mut joints = JointSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); let mut pipeline = PhysicsPipeline::new(); let mut bf = BroadPhase::new(); let mut nf = NarrowPhase::new(); @@ -763,7 +735,7 @@ mod test { &mut nf, &mut bodies, &mut colliders, - &mut joints, + &mut impulse_joints, &mut CCDSolver::new(), &(), &(), @@ -773,7 +745,7 @@ mod test { #[test] fn rigid_body_removal_before_step() { let mut colliders = ColliderSet::new(); - let mut joints = JointSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); let mut pipeline = PhysicsPipeline::new(); let mut bf = BroadPhase::new(); let mut nf = NarrowPhase::new(); @@ -798,7 +770,7 @@ mod test { let to_delete = [h1, h2, h3, h4]; for h in &to_delete { - bodies.remove(*h, &mut islands, &mut colliders, &mut joints); + bodies.remove(*h, &mut islands, &mut colliders, &mut impulse_joints); } pipeline.step( @@ -809,7 +781,7 @@ mod test { &mut nf, &mut bodies, &mut colliders, - &mut joints, + &mut impulse_joints, &mut CCDSolver::new(), &(), &(), @@ -820,7 +792,7 @@ mod test { #[test] fn rigid_body_removal_snapshot_handle_determinism() { let mut colliders = ColliderSet::new(); - let mut joints = JointSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); let mut islands = IslandManager::new(); let mut bodies = RigidBodySet::new(); @@ -829,9 +801,9 @@ mod test { let h2 = bodies.insert(rb.clone()); let h3 = bodies.insert(rb.clone()); - bodies.remove(h1, &mut islands, &mut colliders, &mut joints); - bodies.remove(h3, &mut islands, &mut colliders, &mut joints); - bodies.remove(h2, &mut islands, &mut colliders, &mut joints); + bodies.remove(h1, &mut islands, &mut colliders, &mut impulse_joints); + bodies.remove(h3, &mut islands, &mut colliders, &mut impulse_joints); + bodies.remove(h2, &mut islands, &mut colliders, &mut impulse_joints); let ser_bodies = bincode::serialize(&bodies).unwrap(); let mut bodies2: RigidBodySet = bincode::deserialize(&ser_bodies).unwrap(); @@ -859,7 +831,7 @@ mod test { let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); let mut ccd = CCDSolver::new(); - let mut joints = JointSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); let mut islands = IslandManager::new(); let physics_hooks = (); let event_handler = (); @@ -869,7 +841,7 @@ mod test { let collider = ColliderBuilder::ball(1.0).build(); let c_handle = colliders.insert_with_parent(collider, b_handle, &mut bodies); colliders.remove(c_handle, &mut islands, &mut bodies, true); - bodies.remove(b_handle, &mut islands, &mut colliders, &mut joints); + bodies.remove(b_handle, &mut islands, &mut colliders, &mut impulse_joints); for _ in 0..10 { pipeline.step( @@ -880,7 +852,7 @@ mod test { &mut narrow_phase, &mut bodies, &mut colliders, - &mut joints, + &mut impulse_joints, &mut ccd, &physics_hooks, &event_handler, diff --git a/src/utils.rs b/src/utils.rs index 7f0cf46..9aa4bf2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,9 @@ //! Miscellaneous utilities. -use na::{Matrix3, Point2, Point3, Scalar, SimdRealField, Vector2, Vector3}; +use na::{ + Matrix1, Matrix2, Matrix3, Point2, Point3, RowVector2, Scalar, SimdRealField, UnitComplex, + UnitQuaternion, Vector1, Vector2, Vector3, +}; use num::Zero; use simba::simd::SimdValue; use std::ops::IndexMut; @@ -12,6 +15,10 @@ use { num::One, }; +pub trait WReal: SimdRealField + Copy {} +impl WReal for Real {} +impl WReal for SimdReal {} + pub(crate) fn inv(val: Real) -> Real { if val == 0.0 { 0.0 @@ -20,6 +27,10 @@ pub(crate) fn inv(val: Real) -> Real { } } +pub(crate) fn simd_inv(val: N) -> N { + N::zero().select(val.simd_eq(N::zero()), N::one() / val) +} + /// Trait to copy the sign of each component of one scalar/vector/matrix to another. pub trait WSign: Sized { // See SIMD implementations of copy_sign there: https://stackoverflow.com/a/57872652 @@ -234,32 +245,79 @@ where pub(crate) trait WCrossMatrix: Sized { type CrossMat; + type CrossMatTr; fn gcross_matrix(self) -> Self::CrossMat; + fn gcross_matrix_tr(self) -> Self::CrossMatTr; } -impl WCrossMatrix for Vector3 { - type CrossMat = Matrix3; +impl WCrossMatrix for Vector3 { + type CrossMat = Matrix3; + type CrossMatTr = Matrix3; #[inline] #[rustfmt::skip] fn gcross_matrix(self) -> Self::CrossMat { Matrix3::new( - 0.0, -self.z, self.y, - self.z, 0.0, -self.x, - -self.y, self.x, 0.0, + N::zero(), -self.z, self.y, + self.z, N::zero(), -self.x, + -self.y, self.x, N::zero(), + ) + } + + #[inline] + #[rustfmt::skip] + fn gcross_matrix_tr(self) -> Self::CrossMatTr { + Matrix3::new( + N::zero(), self.z, -self.y, + -self.z, N::zero(), self.x, + self.y, -self.x, N::zero(), ) } } -impl WCrossMatrix for Vector2 { - type CrossMat = Vector2; +impl WCrossMatrix for Vector2 { + type CrossMat = RowVector2; + type CrossMatTr = Vector2; #[inline] fn gcross_matrix(self) -> Self::CrossMat { + RowVector2::new(-self.y, self.x) + } + #[inline] + fn gcross_matrix_tr(self) -> Self::CrossMatTr { Vector2::new(-self.y, self.x) } } +impl WCrossMatrix for Real { + type CrossMat = Matrix2; + type CrossMatTr = Matrix2; + + #[inline] + fn gcross_matrix(self) -> Matrix2 { + Matrix2::new(0.0, -self, self, 0.0) + } + + #[inline] + fn gcross_matrix_tr(self) -> Matrix2 { + Matrix2::new(0.0, self, -self, 0.0) + } +} + +impl WCrossMatrix for SimdReal { + type CrossMat = Matrix2; + type CrossMatTr = Matrix2; + + #[inline] + fn gcross_matrix(self) -> Matrix2 { + Matrix2::new(SimdReal::zero(), -self, self, SimdReal::zero()) + } + + #[inline] + fn gcross_matrix_tr(self) -> Matrix2 { + Matrix2::new(SimdReal::zero(), self, -self, SimdReal::zero()) + } +} pub(crate) trait WCross: Sized { type Result; @@ -295,50 +353,43 @@ pub(crate) trait WDot: Sized { fn gdot(&self, rhs: Rhs) -> Self::Result; } -impl WDot> for Vector3 { - type Result = Real; +impl WDot> for Vector3 { + type Result = N; - fn gdot(&self, rhs: Vector3) -> Self::Result { + fn gdot(&self, rhs: Vector3) -> Self::Result { self.x * rhs.x + self.y * rhs.y + self.z * rhs.z } } -impl WDot> for Vector2 { - type Result = Real; +impl WDot> for Vector2 { + type Result = N; - fn gdot(&self, rhs: Vector2) -> Self::Result { + fn gdot(&self, rhs: Vector2) -> Self::Result { self.x * rhs.x + self.y * rhs.y } } -impl WDot for Real { - type Result = Real; +impl WDot> for N { + type Result = N; - fn gdot(&self, rhs: Real) -> Self::Result { + fn gdot(&self, rhs: Vector1) -> Self::Result { + *self * rhs.x + } +} + +impl WDot for N { + type Result = N; + + fn gdot(&self, rhs: N) -> Self::Result { *self * rhs } } -impl WCrossMatrix for Vector3 { - type CrossMat = Matrix3; +impl WDot for Vector1 { + type Result = N; - #[inline] - #[rustfmt::skip] - fn gcross_matrix(self) -> Self::CrossMat { - Matrix3::new( - SimdReal::zero(), -self.z, self.y, - self.z, SimdReal::zero(), -self.x, - -self.y, self.x, SimdReal::zero(), - ) - } -} - -impl WCrossMatrix for Vector2 { - type CrossMat = Vector2; - - #[inline] - fn gcross_matrix(self) -> Self::CrossMat { - Vector2::new(-self.y, self.x) + fn gdot(&self, rhs: N) -> Self::Result { + self.x * rhs } } @@ -368,27 +419,42 @@ impl WCross> for Vector2 { } } -impl WDot> for Vector3 { - type Result = SimdReal; +pub trait WQuat { + type Result; - fn gdot(&self, rhs: Vector3) -> Self::Result { - self.x * rhs.x + self.y * rhs.y + self.z * rhs.z + fn diff_conj1_2(&self, rhs: &Self) -> Self::Result; +} + +impl WQuat for UnitComplex +where + ::Element: SimdRealField, +{ + type Result = Matrix1; + + fn diff_conj1_2(&self, rhs: &Self) -> Self::Result { + let two: N = na::convert(2.0); + Matrix1::new((self.im * rhs.im + self.re * rhs.re) * two) } } -impl WDot> for Vector2 { - type Result = SimdReal; +impl WQuat for UnitQuaternion +where + ::Element: SimdRealField, +{ + type Result = Matrix3; - fn gdot(&self, rhs: Vector2) -> Self::Result { - self.x * rhs.x + self.y * rhs.y - } -} + fn diff_conj1_2(&self, rhs: &Self) -> Self::Result { + let half: N = na::convert(0.5); + let v1 = self.imag(); + let v2 = rhs.imag(); + let w1 = self.w; + let w2 = rhs.w; -impl WDot for SimdReal { - type Result = SimdReal; - - fn gdot(&self, rhs: SimdReal) -> Self::Result { - *self * rhs + // TODO: this can probably be optimized a lot by unrolling the ops. + (v1 * v2.transpose() + Matrix3::from_diagonal_element(w1 * w2) + - (v1 * w2 + v2 * w1).cross_matrix() + + v1.cross_matrix() * v2.cross_matrix()) + * half } } @@ -404,59 +470,23 @@ pub(crate) trait WAngularInertia { fn into_matrix(self) -> Self::AngMatrix; } -impl WAngularInertia for Real { - type AngVector = Real; - type LinVector = Vector2; - type AngMatrix = Real; +impl WAngularInertia for N { + type AngVector = N; + type LinVector = Vector2; + type AngMatrix = N; fn inverse(&self) -> Self { - if *self != 0.0 { - 1.0 / *self - } else { - 0.0 - } + simd_inv(*self) } - fn transform_lin_vector(&self, pt: Vector2) -> Vector2 { - *self * pt + fn transform_lin_vector(&self, pt: Vector2) -> Vector2 { + pt * *self } - fn transform_vector(&self, pt: Real) -> Real { - *self * pt - } - - fn squared(&self) -> Real { - *self * *self - } - - fn transform_matrix(&self, mat: &Self::AngMatrix) -> Self::AngMatrix { - mat * *self - } - - fn into_matrix(self) -> Self::AngMatrix { - self - } -} - -impl WAngularInertia for SimdReal { - type AngVector = SimdReal; - type LinVector = Vector2; - type AngMatrix = SimdReal; - - fn inverse(&self) -> Self { - let zero = ::zero(); - let is_zero = self.simd_eq(zero); - (::one() / *self).select(is_zero, zero) - } - - fn transform_lin_vector(&self, pt: Vector2) -> Vector2 { + fn transform_vector(&self, pt: N) -> N { pt * *self } - fn transform_vector(&self, pt: SimdReal) -> SimdReal { - *self * pt - } - - fn squared(&self) -> SimdReal { + fn squared(&self) -> N { *self * *self } @@ -757,3 +787,17 @@ impl IndexMut2 for Vec { } } } + +impl IndexMut2 for [T] { + #[inline] + fn index_mut2(&mut self, i: usize, j: usize) -> (&mut T, &mut T) { + assert!(i != j, "Unable to index the same element twice."); + assert!(i < self.len() && j < self.len(), "Index out of bounds."); + + unsafe { + let a = &mut *(self.get_unchecked_mut(i) as *mut _); + let b = &mut *(self.get_unchecked_mut(j) as *mut _); + (a, b) + } + } +} diff --git a/src_testbed/box2d_backend.rs b/src_testbed/box2d_backend.rs index f0bffa3..2a143fb 100644 --- a/src_testbed/box2d_backend.rs +++ b/src_testbed/box2d_backend.rs @@ -2,14 +2,12 @@ use std::collections::HashMap; use na::{Isometry2, Vector2}; use rapier::counters::Counters; -use rapier::dynamics::{ - IntegrationParameters, JointParams, JointSet, RigidBodyHandle, RigidBodySet, -}; +use rapier::dynamics::{ImpulseJointSet, IntegrationParameters, RigidBodyHandle, RigidBodySet}; use rapier::geometry::{Collider, ColliderSet}; use std::f32; use wrapped2d::b2; -use wrapped2d::dynamics::joints::{PrismaticJointDef, RevoluteJointDef, WeldJointDef}; +// use wrapped2d::dynamics::joints::{PrismaticJointDef, RevoluteJointDef, WeldJointDef}; use wrapped2d::user_data::NoUserData; fn na_vec_to_b2_vec(v: Vector2) -> b2::Vec2 { @@ -34,7 +32,7 @@ impl Box2dWorld { gravity: Vector2, bodies: &RigidBodySet, colliders: &ColliderSet, - joints: &JointSet, + impulse_joints: &ImpulseJointSet, ) -> Self { let mut world = b2::World::new(&na_vec_to_b2_vec(gravity)); world.set_continuous_physics(bodies.iter().any(|b| b.1.is_ccd_enabled())); @@ -46,7 +44,7 @@ impl Box2dWorld { res.insert_bodies(bodies); res.insert_colliders(colliders); - res.insert_joints(joints); + res.insert_joints(impulse_joints); res } @@ -95,8 +93,9 @@ impl Box2dWorld { } } - fn insert_joints(&mut self, joints: &JointSet) { - for joint in joints.iter() { + fn insert_joints(&mut self, _impulse_joints: &ImpulseJointSet) { + /* + for joint in impulse_joints.iter() { let body_a = self.rapier2box2d[&joint.1.body1]; let body_b = self.rapier2box2d[&joint.1.body2]; @@ -154,6 +153,8 @@ impl Box2dWorld { } } } + + */ } fn create_fixture(collider: &Collider, body: &mut b2::MetaBody) { @@ -225,7 +226,7 @@ impl Box2dWorld { self.world.step( params.dt, params.max_velocity_iterations as i32, - params.max_position_iterations as i32, + params.max_stabilization_iterations as i32, ); counters.step_completed(); } diff --git a/src_testbed/harness/mod.rs b/src_testbed/harness/mod.rs index 0bc08d0..3358a70 100644 --- a/src_testbed/harness/mod.rs +++ b/src_testbed/harness/mod.rs @@ -3,7 +3,10 @@ use crate::{ TestbedGraphics, }; use plugin::HarnessPlugin; -use rapier::dynamics::{CCDSolver, IntegrationParameters, IslandManager, JointSet, RigidBodySet}; +use rapier::dynamics::{ + CCDSolver, ImpulseJointSet, IntegrationParameters, IslandManager, MultibodyJointSet, + RigidBodySet, +}; use rapier::geometry::{BroadPhase, ColliderSet, NarrowPhase}; use rapier::math::Vector; use rapier::pipeline::{ChannelEventCollector, PhysicsHooks, PhysicsPipeline, QueryPipeline}; @@ -78,9 +81,14 @@ impl Harness { } } - pub fn new(bodies: RigidBodySet, colliders: ColliderSet, joints: JointSet) -> Self { + pub fn new( + bodies: RigidBodySet, + colliders: ColliderSet, + impulse_joints: ImpulseJointSet, + multibody_joints: MultibodyJointSet, + ) -> Self { let mut res = Self::new_empty(); - res.set_world(bodies, colliders, joints); + res.set_world(bodies, colliders, impulse_joints, multibody_joints); res } @@ -100,24 +108,39 @@ impl Harness { &mut self.physics } - pub fn set_world(&mut self, bodies: RigidBodySet, colliders: ColliderSet, joints: JointSet) { - self.set_world_with_params(bodies, colliders, joints, Vector::y() * -9.81, ()) + 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, + Vector::y() * -9.81, + (), + ) } pub fn set_world_with_params( &mut self, bodies: RigidBodySet, colliders: ColliderSet, - joints: JointSet, + impulse_joints: ImpulseJointSet, + multibody_joints: MultibodyJointSet, gravity: Vector, hooks: impl PhysicsHooks + 'static, ) { // println!("Num bodies: {}", bodies.len()); - // println!("Num joints: {}", joints.len()); + // println!("Num impulse_joints: {}", impulse_joints.len()); self.physics.gravity = gravity; self.physics.bodies = bodies; self.physics.colliders = colliders; - self.physics.joints = joints; + self.physics.impulse_joints = impulse_joints; + self.physics.multibody_joints = multibody_joints; self.physics.hooks = Box::new(hooks); self.physics.islands = IslandManager::new(); @@ -162,7 +185,8 @@ impl Harness { &mut physics.narrow_phase, &mut physics.bodies, &mut physics.colliders, - &mut physics.joints, + &mut physics.impulse_joints, + &mut physics.multibody_joints, &mut physics.ccd_solver, &*physics.hooks, event_handler, @@ -179,7 +203,8 @@ impl Harness { &mut self.physics.narrow_phase, &mut self.physics.bodies, &mut self.physics.colliders, - &mut self.physics.joints, + &mut self.physics.impulse_joints, + &mut self.physics.multibody_joints, &mut self.physics.ccd_solver, &*self.physics.hooks, &self.event_handler, diff --git a/src_testbed/lib.rs b/src_testbed/lib.rs index a3e3a00..b9a8822 100644 --- a/src_testbed/lib.rs +++ b/src_testbed/lib.rs @@ -1,12 +1,4 @@ extern crate nalgebra as na; -#[cfg(all(feature = "dim2", feature = "other-backends"))] -extern crate ncollide2d as ncollide; -#[cfg(all(feature = "dim3", feature = "other-backends"))] -extern crate ncollide3d as ncollide; -#[cfg(all(feature = "dim2", feature = "other-backends"))] -extern crate nphysics2d as nphysics; -#[cfg(all(feature = "dim3", feature = "other-backends"))] -extern crate nphysics3d as nphysics; #[cfg(feature = "dim2")] extern crate parry2d as parry; #[cfg(feature = "dim3")] @@ -37,8 +29,6 @@ mod camera2d; mod camera3d; mod graphics; pub mod harness; -#[cfg(feature = "other-backends")] -mod nphysics_backend; pub mod objects; pub mod physics; #[cfg(all(feature = "dim3", feature = "other-backends"))] diff --git a/src_testbed/nphysics_backend.rs b/src_testbed/nphysics_backend.rs deleted file mode 100644 index c7d6c0e..0000000 --- a/src_testbed/nphysics_backend.rs +++ /dev/null @@ -1,248 +0,0 @@ -#[cfg(feature = "dim2")] -use ncollide::shape::ConvexPolygon; -use ncollide::shape::{Ball, Capsule, Cuboid, HeightField, ShapeHandle}; -use nphysics::force_generator::DefaultForceGeneratorSet; -use nphysics::joint::{ - DefaultJointConstraintSet, FixedConstraint, PrismaticConstraint, RevoluteConstraint, -}; -use nphysics::object::{ - BodyPartHandle, ColliderDesc, DefaultBodyHandle, DefaultBodySet, DefaultColliderSet, - RigidBodyDesc, -}; -use nphysics::world::{DefaultGeometricalWorld, DefaultMechanicalWorld}; -use rapier::counters::Counters; -use rapier::dynamics::{ - IntegrationParameters, JointParams, JointSet, RigidBodyHandle, RigidBodySet, -}; -use rapier::geometry::{Collider, ColliderSet}; -use rapier::math::Vector; -use std::collections::HashMap; -#[cfg(feature = "dim3")] -use {ncollide::shape::TriMesh, nphysics::joint::BallConstraint}; - -pub struct NPhysicsWorld { - rapier2nphysics: HashMap, - mechanical_world: DefaultMechanicalWorld, - geometrical_world: DefaultGeometricalWorld, - bodies: DefaultBodySet, - colliders: DefaultColliderSet, - joints: DefaultJointConstraintSet, - force_generators: DefaultForceGeneratorSet, -} - -impl NPhysicsWorld { - pub fn from_rapier( - gravity: Vector, - bodies: &RigidBodySet, - colliders: &ColliderSet, - joints: &JointSet, - ) -> Self { - let mut rapier2nphysics = HashMap::new(); - - let mechanical_world = DefaultMechanicalWorld::new(gravity); - let geometrical_world = DefaultGeometricalWorld::new(); - let mut nphysics_bodies = DefaultBodySet::new(); - let mut nphysics_colliders = DefaultColliderSet::new(); - let mut nphysics_joints = DefaultJointConstraintSet::new(); - let force_generators = DefaultForceGeneratorSet::new(); - - for (rapier_handle, rb) in bodies.iter() { - // let material = physics.create_material(rb.collider.friction, rb.collider.friction, 0.0); - let nphysics_rb = RigidBodyDesc::new().position(*rb.position()).build(); - let nphysics_rb_handle = nphysics_bodies.insert(nphysics_rb); - - rapier2nphysics.insert(rapier_handle, nphysics_rb_handle); - } - - for (_, collider) in colliders.iter() { - if let Some(parent_handle) = collider.parent() { - let parent = &bodies[parent_handle]; - let nphysics_rb_handle = rapier2nphysics[&parent_handle]; - if let Some(collider) = - nphysics_collider_from_rapier_collider(&collider, parent.is_dynamic()) - { - let nphysics_collider = collider.build(BodyPartHandle(nphysics_rb_handle, 0)); - nphysics_colliders.insert(nphysics_collider); - } else { - eprintln!("Creating shape unknown to the nphysics backend.") - } - } - } - - for joint in joints.iter() { - let b1 = BodyPartHandle(rapier2nphysics[&joint.1.body1], 0); - let b2 = BodyPartHandle(rapier2nphysics[&joint.1.body2], 0); - - match &joint.1.params { - JointParams::FixedJoint(params) => { - let c = FixedConstraint::new( - b1, - b2, - params.local_frame1.translation.vector.into(), - params.local_frame1.rotation, - params.local_frame2.translation.vector.into(), - params.local_frame2.rotation, - ); - nphysics_joints.insert(c); - } - #[cfg(feature = "dim3")] - JointParams::BallJoint(params) => { - let c = BallConstraint::new(b1, b2, params.local_anchor1, params.local_anchor2); - nphysics_joints.insert(c); - } - #[cfg(feature = "dim2")] - JointParams::BallJoint(params) => { - let c = - RevoluteConstraint::new(b1, b2, params.local_anchor1, params.local_anchor2); - nphysics_joints.insert(c); - } - #[cfg(feature = "dim3")] - JointParams::RevoluteJoint(params) => { - let c = RevoluteConstraint::new( - b1, - b2, - params.local_anchor1, - params.local_axis1, - params.local_anchor2, - params.local_axis2, - ); - nphysics_joints.insert(c); - } - JointParams::PrismaticJoint(params) => { - let mut c = PrismaticConstraint::new( - b1, - b2, - params.local_anchor1, - params.local_axis1(), - params.local_anchor2, - ); - - if params.limits_enabled { - c.enable_min_offset(params.limits[0]); - c.enable_max_offset(params.limits[1]); - } - - nphysics_joints.insert(c); - } // JointParams::GenericJoint(_) => { - // eprintln!( - // "Joint type currently unsupported by the nphysics backend: GenericJoint." - // ) - // } - } - } - - Self { - rapier2nphysics, - mechanical_world, - geometrical_world, - bodies: nphysics_bodies, - colliders: nphysics_colliders, - joints: nphysics_joints, - force_generators, - } - } - - pub fn step(&mut self, counters: &mut Counters, params: &IntegrationParameters) { - self.mechanical_world - .integration_parameters - .max_position_iterations = params.max_position_iterations; - self.mechanical_world - .integration_parameters - .max_velocity_iterations = params.max_velocity_iterations; - self.mechanical_world - .integration_parameters - .set_dt(params.dt); - self.mechanical_world.integration_parameters.warmstart_coeff = params.warmstart_coeff; - - counters.step_started(); - self.mechanical_world.step( - &mut self.geometrical_world, - &mut self.bodies, - &mut self.colliders, - &mut self.joints, - &mut self.force_generators, - ); - counters.step_completed(); - } - - pub fn sync(&self, bodies: &mut RigidBodySet, colliders: &mut ColliderSet) { - for (rapier_handle, nphysics_handle) in self.rapier2nphysics.iter() { - let rb = bodies.get_mut(*rapier_handle).unwrap(); - let ra = self.bodies.rigid_body(*nphysics_handle).unwrap(); - let pos = *ra.position(); - 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(na::one()), - ); - } - } - } -} - -fn nphysics_collider_from_rapier_collider( - collider: &Collider, - is_dynamic: bool, -) -> Option> { - let mut margin = ColliderDesc::::default_margin(); - let mut pos = collider.position_wrt_parent().copied().unwrap_or(na::one()); - let shape = collider.shape(); - - let shape = if let Some(cuboid) = shape.as_cuboid() { - ShapeHandle::new(Cuboid::new(cuboid.half_extents.map(|e| e - margin))) - } else if let Some(cuboid) = shape.as_round_cuboid() { - margin = cuboid.border_radius; - ShapeHandle::new(Cuboid::new(cuboid.base_shape.half_extents)) - } else if let Some(ball) = shape.as_ball() { - ShapeHandle::new(Ball::new(ball.radius - margin)) - } else if let Some(capsule) = shape.as_capsule() { - pos *= capsule.transform_wrt_y(); - ShapeHandle::new(Capsule::new(capsule.half_height(), capsule.radius)) - } else if let Some(heightfield) = shape.as_heightfield() { - let heights = heightfield.heights(); - let scale = heightfield.scale(); - let heightfield = HeightField::new(heights.clone(), *scale); - ShapeHandle::new(heightfield) - } else { - #[cfg(feature = "dim3")] - if let Some(trimesh) = shape.as_trimesh() { - ShapeHandle::new(TriMesh::new( - trimesh.vertices().to_vec(), - trimesh - .indices() - .iter() - .map(|idx| na::point![idx[0] as usize, idx[1] as usize, idx[2] as usize]) - .collect(), - None, - )) - } else { - return None; - } - - #[cfg(feature = "dim2")] - if let Some(polygon) = shape.as_round_convex_polygon() { - margin = polygon.border_radius; - ShapeHandle::new(ConvexPolygon::try_from_points(polygon.base_shape.points()).unwrap()) - } else if let Some(polygon) = shape.as_convex_polygon() { - ShapeHandle::new(ConvexPolygon::try_from_points(polygon.points()).unwrap()) - } else { - return None; - } - }; - - let density = if is_dynamic { - collider.density().unwrap_or(0.0) - } else { - 0.0 - }; - - Some( - ColliderDesc::new(shape) - .position(pos) - .density(density) - .sensor(collider.is_sensor()) - .margin(margin), - ) -} diff --git a/src_testbed/physics/mod.rs b/src_testbed/physics/mod.rs index 29e9a46..13bf915 100644 --- a/src_testbed/physics/mod.rs +++ b/src_testbed/physics/mod.rs @@ -1,5 +1,8 @@ use crossbeam::channel::Receiver; -use rapier::dynamics::{CCDSolver, IntegrationParameters, IslandManager, JointSet, RigidBodySet}; +use rapier::dynamics::{ + CCDSolver, ImpulseJointSet, IntegrationParameters, IslandManager, MultibodyJointSet, + RigidBodySet, +}; use rapier::geometry::{BroadPhase, ColliderSet, ContactEvent, IntersectionEvent, NarrowPhase}; use rapier::math::Vector; use rapier::pipeline::{PhysicsHooks, PhysicsPipeline, QueryPipeline}; @@ -10,7 +13,7 @@ pub struct PhysicsSnapshot { narrow_phase: Vec, bodies: Vec, colliders: Vec, - joints: Vec, + impulse_joints: Vec, } impl PhysicsSnapshot { @@ -20,7 +23,7 @@ impl PhysicsSnapshot { narrow_phase: &NarrowPhase, bodies: &RigidBodySet, colliders: &ColliderSet, - joints: &JointSet, + impulse_joints: &ImpulseJointSet, ) -> bincode::Result { Ok(Self { timestep_id, @@ -28,7 +31,7 @@ impl PhysicsSnapshot { narrow_phase: bincode::serialize(narrow_phase)?, bodies: bincode::serialize(bodies)?, colliders: bincode::serialize(colliders)?, - joints: bincode::serialize(joints)?, + impulse_joints: bincode::serialize(impulse_joints)?, }) } @@ -40,7 +43,7 @@ impl PhysicsSnapshot { NarrowPhase, RigidBodySet, ColliderSet, - JointSet, + ImpulseJointSet, )> { Ok(( self.timestep_id, @@ -48,7 +51,7 @@ impl PhysicsSnapshot { bincode::deserialize(&self.narrow_phase)?, bincode::deserialize(&self.bodies)?, bincode::deserialize(&self.colliders)?, - bincode::deserialize(&self.joints)?, + bincode::deserialize(&self.impulse_joints)?, )) } @@ -57,13 +60,13 @@ impl PhysicsSnapshot { + self.narrow_phase.len() + self.bodies.len() + self.colliders.len() - + self.joints.len(); + + self.impulse_joints.len(); println!("Snapshot length: {}B", total); println!("|_ broad_phase: {}B", self.broad_phase.len()); println!("|_ narrow_phase: {}B", self.narrow_phase.len()); println!("|_ bodies: {}B", self.bodies.len()); println!("|_ colliders: {}B", self.colliders.len()); - println!("|_ joints: {}B", self.joints.len()); + println!("|_ impulse_joints: {}B", self.impulse_joints.len()); } } @@ -73,7 +76,8 @@ pub struct PhysicsState { pub narrow_phase: NarrowPhase, pub bodies: RigidBodySet, pub colliders: ColliderSet, - pub joints: JointSet, + pub impulse_joints: ImpulseJointSet, + pub multibody_joints: MultibodyJointSet, pub ccd_solver: CCDSolver, pub pipeline: PhysicsPipeline, pub query_pipeline: QueryPipeline, @@ -90,7 +94,8 @@ impl PhysicsState { narrow_phase: NarrowPhase::new(), bodies: RigidBodySet::new(), colliders: ColliderSet::new(), - joints: JointSet::new(), + impulse_joints: ImpulseJointSet::new(), + multibody_joints: MultibodyJointSet::new(), ccd_solver: CCDSolver::new(), pipeline: PhysicsPipeline::new(), query_pipeline: QueryPipeline::new(), diff --git a/src_testbed/physx_backend.rs b/src_testbed/physx_backend.rs index 6c3155f..9eec3ff 100644 --- a/src_testbed/physx_backend.rs +++ b/src_testbed/physx_backend.rs @@ -1,9 +1,7 @@ #![allow(dead_code)] -use na::{ - Isometry3, Matrix3, Matrix4, Point3, Quaternion, Rotation3, Translation3, Unit, UnitQuaternion, - Vector3, -}; +use na::{Isometry3, Matrix4, Point3, Quaternion, Translation3, Unit, UnitQuaternion, Vector3}; +use physx::articulation_joint_base::JointMap; use physx::cooking::{ ConvexMeshCookingResult, PxConvexMeshDesc, PxCooking, PxCookingParams, PxHeightFieldDesc, PxTriangleMeshDesc, TriangleMeshCookingResult, @@ -13,15 +11,16 @@ use physx::prelude::*; use physx::scene::FrictionType; use physx::traits::Class; use physx_sys::{ - FilterShaderCallbackInfo, PxBitAndByte, PxConvexFlags, PxConvexMeshGeometryFlags, - PxHeightFieldSample, PxMeshGeometryFlags, PxMeshScale_new, PxRigidActor, + FilterShaderCallbackInfo, PxArticulationLink_getInboundJoint, PxBitAndByte, PxConvexFlags, + PxConvexMeshGeometryFlags, PxHeightFieldSample, PxMeshGeometryFlags, PxMeshScale_new, + PxRigidActor, }; use rapier::counters::Counters; use rapier::dynamics::{ - IntegrationParameters, JointParams, JointSet, RigidBodyHandle, RigidBodySet, + ImpulseJointSet, IntegrationParameters, MultibodyJointSet, RigidBodyHandle, RigidBodySet, }; use rapier::geometry::{Collider, ColliderSet}; -use rapier::utils::WBasis; +use rapier::prelude::JointAxesMask; use std::collections::HashMap; trait IntoNa { @@ -145,7 +144,8 @@ impl PhysxWorld { integration_parameters: &IntegrationParameters, bodies: &RigidBodySet, colliders: &ColliderSet, - joints: &JointSet, + impulse_joints: &ImpulseJointSet, + multibody_joints: &MultibodyJointSet, use_two_friction_directions: bool, num_threads: usize, ) -> Self { @@ -181,6 +181,7 @@ impl PhysxWorld { let mut scene: Owner = physics.create(scene_desc).unwrap(); let mut rapier2dynamic = HashMap::new(); let mut rapier2static = HashMap::new(); + let mut rapier2link = HashMap::new(); let cooking_params = PxCookingParams::new(&*physics).expect("Failed to init PhysX cooking."); let mut cooking = PxCooking::new(physics.foundation_mut(), &cooking_params) @@ -192,6 +193,10 @@ impl PhysxWorld { * */ 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(); @@ -200,8 +205,10 @@ impl PhysxWorld { actor.set_linear_velocity(&linvel, true); actor.set_angular_velocity(&angvel, true); actor.set_solver_iteration_counts( - integration_parameters.max_position_iterations as u32, - integration_parameters.max_velocity_iterations as u32, + // Use our number of velocity iterations as their number of position iterations. + integration_parameters.max_velocity_iterations.max(1) as u32, + // Use our number of velocity stabilization iterations as their number of velocity iterations. + integration_parameters.max_stabilization_iterations.max(1) as u32, ); rapier2dynamic.insert(rapier_handle, actor); @@ -211,6 +218,79 @@ impl PhysxWorld { } } + /* + * 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_static() { + 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 physx_sys::PxArticulationJointBase + as *mut JointMap) + .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 @@ -223,7 +303,15 @@ impl PhysxWorld { if let Some(parent_handle) = collider.parent() { let parent_body = &bodies[parent_handle]; - if !parent_body.is_dynamic() { + 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() { + println!("Ground collider"); let actor = rapier2static.get_mut(&parent_handle).unwrap(); actor.attach_shape(&mut px_shape); } else { @@ -246,8 +334,8 @@ impl PhysxWorld { } // Update mass properties and CCD flags. - for (rapier_handle, actor) in rapier2dynamic.iter_mut() { - let rb = &bodies[*rapier_handle]; + for (rapier_handle, _rb) in bodies.iter() { + let rb = &bodies[rapier_handle]; let densities: Vec<_> = rb .colliders() .iter() @@ -255,8 +343,16 @@ impl PhysxWorld { .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_mut( - std::mem::transmute(actor.as_mut()), + actor, densities.as_ptr(), densities.len() as u32, std::ptr::null(), @@ -265,19 +361,10 @@ impl PhysxWorld { if rb.is_ccd_enabled() { physx_sys::PxRigidBody_setRigidBodyFlag_mut( - std::mem::transmute(actor.as_mut()), + actor, RigidBodyFlag::EnableCCD as u32, true, ); - // physx_sys::PxRigidBody_setMinCCDAdvanceCoefficient_mut( - // std::mem::transmute(actor.as_mut()), - // 0.0, - // ); - // physx_sys::PxRigidBody_setRigidBodyFlag_mut( - // std::mem::transmute(actor.as_mut()), - // RigidBodyFlag::EnableCCDFriction as u32, - // true, - // ); } } } @@ -289,9 +376,10 @@ impl PhysxWorld { */ Self::setup_joints( &mut physics, - joints, + impulse_joints, &mut rapier2static, &mut rapier2dynamic, + &mut rapier2link, ); for (_, actor) in rapier2static { @@ -312,18 +400,22 @@ impl PhysxWorld { fn setup_joints( physics: &mut PxPhysicsFoundation, - joints: &JointSet, + impulse_joints: &ImpulseJointSet, rapier2static: &mut HashMap>, rapier2dynamic: &mut HashMap>, + rapier2link: &mut HashMap, ) { unsafe { - for joint in joints.iter() { + 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) @@ -331,150 +423,83 @@ impl PhysxWorld { .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(); - match &joint.1.params { - JointParams::BallJoint(params) => { - let frame1 = Isometry3::new(params.local_anchor1.coords, na::zero()) - .into_physx() - .into(); - let frame2 = Isometry3::new(params.local_anchor2.coords, na::zero()) - .into_physx() - .into(); + let px_frame1 = joint.1.data.local_frame1.into_physx(); + let px_frame2 = joint.1.data.local_frame2.into_physx(); - physx_sys::phys_PxSphericalJointCreate( - physics.as_mut_ptr(), - actor1, - &frame1 as *const _, - actor2, - &frame2 as *const _, - ); - } - JointParams::RevoluteJoint(params) => { - // NOTE: orthonormal_basis() returns the two basis vectors. - // However we only use one and recompute the other just to - // make sure our basis is right-handed. - let basis1a = params.local_axis1.orthonormal_basis()[0]; - let basis2a = params.local_axis2.orthonormal_basis()[0]; - let basis1b = params.local_axis1.cross(&basis1a); - let basis2b = params.local_axis2.cross(&basis2a); + let px_joint = physx_sys::phys_PxD6JointCreate( + physics.as_mut_ptr(), + actor1, + px_frame1.as_ptr(), + actor2, + px_frame2.as_ptr(), + ); - let rotmat1 = Rotation3::from_matrix_unchecked(Matrix3::from_columns(&[ - params.local_axis1.into_inner(), - basis1a, - basis1b, - ])); - let rotmat2 = Rotation3::from_matrix_unchecked(Matrix3::from_columns(&[ - params.local_axis2.into_inner(), - basis2a, - basis2b, - ])); - let axisangle1 = rotmat1.scaled_axis(); - let axisangle2 = rotmat2.scaled_axis(); + let motion_x = if joint.1.data.limit_axes.contains(JointAxesMask::X) { + physx_sys::PxD6Motion::eLIMITED + } else if !joint.1.data.locked_axes.contains(JointAxesMask::X) { + physx_sys::PxD6Motion::eFREE + } else { + physx_sys::PxD6Motion::eLOCKED + }; + let motion_y = if joint.1.data.limit_axes.contains(JointAxesMask::Y) { + physx_sys::PxD6Motion::eLIMITED + } else if !joint.1.data.locked_axes.contains(JointAxesMask::Y) { + physx_sys::PxD6Motion::eFREE + } else { + physx_sys::PxD6Motion::eLOCKED + }; + let motion_z = if joint.1.data.limit_axes.contains(JointAxesMask::Z) { + physx_sys::PxD6Motion::eLIMITED + } else if !joint.1.data.locked_axes.contains(JointAxesMask::Z) { + physx_sys::PxD6Motion::eFREE + } else { + physx_sys::PxD6Motion::eLOCKED + }; + let motion_ax = if joint.1.data.limit_axes.contains(JointAxesMask::ANG_X) { + physx_sys::PxD6Motion::eLIMITED + } else if !joint.1.data.locked_axes.contains(JointAxesMask::ANG_X) { + physx_sys::PxD6Motion::eFREE + } else { + physx_sys::PxD6Motion::eLOCKED + }; + let motion_ay = if joint.1.data.limit_axes.contains(JointAxesMask::ANG_Y) { + physx_sys::PxD6Motion::eLIMITED + } else if !joint.1.data.locked_axes.contains(JointAxesMask::ANG_Y) { + physx_sys::PxD6Motion::eFREE + } else { + physx_sys::PxD6Motion::eLOCKED + }; + let motion_az = if joint.1.data.limit_axes.contains(JointAxesMask::ANG_Z) { + physx_sys::PxD6Motion::eLIMITED + } else if !joint.1.data.locked_axes.contains(JointAxesMask::ANG_Z) { + physx_sys::PxD6Motion::eFREE + } else { + physx_sys::PxD6Motion::eLOCKED + }; - let frame1 = Isometry3::new(params.local_anchor1.coords, axisangle1) - .into_physx() - .into(); - let frame2 = Isometry3::new(params.local_anchor2.coords, axisangle2) - .into_physx() - .into(); - - let revolute_joint = physx_sys::phys_PxRevoluteJointCreate( - physics.as_mut_ptr(), - actor1, - &frame1 as *const _, - actor2, - &frame2 as *const _, - ); - - physx_sys::PxRevoluteJoint_setDriveVelocity_mut( - revolute_joint, - params.motor_target_vel, - true, - ); - - if params.motor_damping != 0.0 { - physx_sys::PxRevoluteJoint_setRevoluteJointFlag_mut( - revolute_joint, - physx_sys::PxRevoluteJointFlag::eDRIVE_ENABLED, - true, - ); - } - } - - JointParams::PrismaticJoint(params) => { - // NOTE: orthonormal_basis() returns the two basis vectors. - // However we only use one and recompute the other just to - // make sure our basis is right-handed. - let basis1a = params.local_axis1().orthonormal_basis()[0]; - let basis2a = params.local_axis2().orthonormal_basis()[0]; - let basis1b = params.local_axis1().cross(&basis1a); - let basis2b = params.local_axis2().cross(&basis2a); - - let rotmat1 = Rotation3::from_matrix_unchecked(Matrix3::from_columns(&[ - params.local_axis1().into_inner(), - basis1a, - basis1b, - ])); - let rotmat2 = Rotation3::from_matrix_unchecked(Matrix3::from_columns(&[ - params.local_axis2().into_inner(), - basis2a, - basis2b, - ])); - - let axisangle1 = rotmat1.scaled_axis(); - let axisangle2 = rotmat2.scaled_axis(); - - let frame1 = Isometry3::new(params.local_anchor1.coords, axisangle1) - .into_physx() - .into(); - let frame2 = Isometry3::new(params.local_anchor2.coords, axisangle2) - .into_physx() - .into(); - - let joint = physx_sys::phys_PxPrismaticJointCreate( - physics.as_mut_ptr(), - actor1, - &frame1 as *const _, - actor2, - &frame2 as *const _, - ); - - if params.limits_enabled { - let limits = physx_sys::PxJointLinearLimitPair { - restitution: 0.0, - bounceThreshold: 0.0, - stiffness: 0.0, - damping: 0.0, - contactDistance: 0.01, - lower: params.limits[0], - upper: params.limits[1], - }; - physx_sys::PxPrismaticJoint_setLimit_mut(joint, &limits); - physx_sys::PxPrismaticJoint_setPrismaticJointFlag_mut( - joint, - physx_sys::PxPrismaticJointFlag::eLIMIT_ENABLED, - true, - ); - } - } - JointParams::FixedJoint(params) => { - let frame1 = params.local_frame1.into_physx().into(); - let frame2 = params.local_frame2.into_physx().into(); - - physx_sys::phys_PxFixedJointCreate( - physics.as_mut_ptr(), - actor1, - &frame1 as *const _, - actor2, - &frame2 as *const _, - ); - } // JointParams::GenericJoint(_) => { - // eprintln!( - // "Joint type currently unsupported by the PhysX backend: GenericJoint." - // ) - // } - } + physx_sys::PxD6Joint_setMotion_mut(px_joint, physx_sys::PxD6Axis::eX, motion_x); + physx_sys::PxD6Joint_setMotion_mut(px_joint, physx_sys::PxD6Axis::eY, motion_y); + physx_sys::PxD6Joint_setMotion_mut(px_joint, physx_sys::PxD6Axis::eZ, motion_z); + physx_sys::PxD6Joint_setMotion_mut( + px_joint, + physx_sys::PxD6Axis::eTWIST, + motion_ax, + ); + physx_sys::PxD6Joint_setMotion_mut( + px_joint, + physx_sys::PxD6Axis::eSWING1, + motion_ay, + ); + physx_sys::PxD6Joint_setMotion_mut( + px_joint, + physx_sys::PxD6Axis::eSWING2, + motion_az, + ); } } } @@ -497,9 +522,7 @@ impl PhysxWorld { } pub fn sync(&mut self, bodies: &mut RigidBodySet, colliders: &mut ColliderSet) { - 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(); + let mut sync_pos = |handle: &RigidBodyHandle, pos: Isometry3| { let rb = &mut bodies[*handle]; rb.set_position(pos, false); @@ -509,6 +532,22 @@ impl PhysxWorld { pos * collider.position_wrt_parent().copied().unwrap_or(na::one()), ); } + }; + + 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); + } + + 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); + } + } } } } @@ -673,7 +712,7 @@ fn physx_collider_from_rapier_collider( type PxPhysicsFoundation = PhysicsFoundation; type PxMaterial = physx::material::PxMaterial<()>; type PxShape = physx::shape::PxShape<(), PxMaterial>; -type PxArticulationLink = physx::articulation_link::PxArticulationLink<(), PxShape>; +type PxArticulationLink = physx::articulation_link::PxArticulationLink; type PxRigidStatic = physx::rigid_static::PxRigidStatic<(), PxShape>; type PxRigidDynamic = physx::rigid_dynamic::PxRigidDynamic; type PxArticulation = physx::articulation::PxArticulation<(), PxArticulationLink>; diff --git a/src_testbed/testbed.rs b/src_testbed/testbed.rs index 706a40a..ac53733 100644 --- a/src_testbed/testbed.rs +++ b/src_testbed/testbed.rs @@ -11,7 +11,8 @@ use crate::{graphics::GraphicsManager, harness::RunState}; use na::{self, Point2, Point3, Vector3}; use rapier::dynamics::{ - IntegrationParameters, JointSet, RigidBodyActivation, RigidBodyHandle, RigidBodySet, + ImpulseJointSet, IntegrationParameters, MultibodyJointSet, RigidBodyActivation, + RigidBodyHandle, RigidBodySet, }; use rapier::geometry::{ColliderHandle, ColliderSet, NarrowPhase}; #[cfg(feature = "dim3")] @@ -22,8 +23,6 @@ use rapier::pipeline::PhysicsHooks; #[cfg(all(feature = "dim2", feature = "other-backends"))] use crate::box2d_backend::Box2dWorld; use crate::harness::Harness; -#[cfg(feature = "other-backends")] -use crate::nphysics_backend::NPhysicsWorld; #[cfg(all(feature = "dim3", feature = "other-backends"))] use crate::physx_backend::PhysxWorld; use bevy::render::camera::Camera; @@ -38,12 +37,10 @@ use crate::camera3d::{OrbitCamera, OrbitCameraPlugin}; use bevy::render::pipeline::PipelineDescriptor; const RAPIER_BACKEND: usize = 0; -#[cfg(feature = "other-backends")] -const NPHYSICS_BACKEND: usize = 1; #[cfg(all(feature = "dim2", feature = "other-backends"))] -const BOX2D_BACKEND: usize = 2; -pub(crate) const PHYSX_BACKEND_PATCH_FRICTION: usize = 2; -pub(crate) const PHYSX_BACKEND_TWO_FRICTION_DIR: usize = 3; +const BOX2D_BACKEND: usize = 1; +pub(crate) const PHYSX_BACKEND_PATCH_FRICTION: usize = 1; +pub(crate) const PHYSX_BACKEND_TWO_FRICTION_DIR: usize = 2; #[derive(PartialEq)] pub enum RunMode { @@ -128,7 +125,6 @@ struct OtherBackends { box2d: Option, #[cfg(feature = "dim3")] physx: Option, - nphysics: Option, } struct Plugins(Vec>); @@ -169,8 +165,6 @@ impl TestbedApp { #[allow(unused_mut)] let mut backend_names = vec!["rapier"]; - #[cfg(feature = "other-backends")] - backend_names.push("nphysics"); #[cfg(all(feature = "dim2", feature = "other-backends"))] backend_names.push("box2d"); #[cfg(all(feature = "dim3", feature = "other-backends"))] @@ -207,7 +201,6 @@ impl TestbedApp { box2d: None, #[cfg(feature = "dim3")] physx: None, - nphysics: None, }; TestbedApp { @@ -271,28 +264,15 @@ impl TestbedApp { for (backend_id, backend) in backend_names.iter().enumerate() { println!("|_ using backend {}", backend); self.state.selected_backend = backend_id; - if cfg!(feature = "dim3") - && (backend_id == PHYSX_BACKEND_PATCH_FRICTION - || backend_id == PHYSX_BACKEND_TWO_FRICTION_DIR) - { - self.harness - .physics - .integration_parameters - .max_velocity_iterations = 1; - self.harness - .physics - .integration_parameters - .max_position_iterations = 4; - } else { - self.harness - .physics - .integration_parameters - .max_velocity_iterations = 4; - self.harness - .physics - .integration_parameters - .max_position_iterations = 1; - } + self.harness + .physics + .integration_parameters + .max_velocity_iterations = 4; + self.harness + .physics + .integration_parameters + .max_stabilization_iterations = 1; + // Init world. let mut testbed = Testbed { graphics: None, @@ -341,20 +321,6 @@ impl TestbedApp { ); } } - - #[cfg(feature = "other-backends")] - { - if self.state.selected_backend == NPHYSICS_BACKEND { - self.other_backends.nphysics.as_mut().unwrap().step( - &mut self.harness.physics.pipeline.counters, - &self.harness.physics.integration_parameters, - ); - self.other_backends.nphysics.as_mut().unwrap().sync( - &mut self.harness.physics.bodies, - &mut self.harness.physics.colliders, - ); - } - } } // Skip the first update. @@ -498,20 +464,40 @@ impl<'a, 'b, 'c, 'd> Testbed<'a, 'b, 'c, 'd> { &mut self.harness } - pub fn set_world(&mut self, bodies: RigidBodySet, colliders: ColliderSet, joints: JointSet) { - self.set_world_with_params(bodies, colliders, joints, Vector::y() * -9.81, ()) + 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, + Vector::y() * -9.81, + (), + ) } pub fn set_world_with_params( &mut self, bodies: RigidBodySet, colliders: ColliderSet, - joints: JointSet, + impulse_joints: ImpulseJointSet, + multibody_joints: MultibodyJointSet, gravity: Vector, hooks: impl PhysicsHooks + 'static, ) { - self.harness - .set_world_with_params(bodies, colliders, joints, gravity, hooks); + self.harness.set_world_with_params( + bodies, + colliders, + impulse_joints, + multibody_joints, + gravity, + hooks, + ); self.state .action_flags @@ -526,7 +512,7 @@ impl<'a, 'b, 'c, 'd> Testbed<'a, 'b, 'c, 'd> { self.harness.physics.gravity, &self.harness.physics.bodies, &self.harness.physics.colliders, - &self.harness.physics.joints, + &self.harness.physics.impulse_joints, )); } } @@ -541,24 +527,13 @@ impl<'a, 'b, 'c, 'd> Testbed<'a, 'b, 'c, 'd> { &self.harness.physics.integration_parameters, &self.harness.physics.bodies, &self.harness.physics.colliders, - &self.harness.physics.joints, + &self.harness.physics.impulse_joints, + &self.harness.physics.multibody_joints, self.state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR, self.harness.state.num_threads, )); } } - - #[cfg(feature = "other-backends")] - { - if self.state.selected_backend == NPHYSICS_BACKEND { - self.other_backends.nphysics = Some(NPhysicsWorld::from_rapier( - self.harness.physics.gravity, - &self.harness.physics.bodies, - &self.harness.physics.colliders, - &self.harness.physics.joints, - )); - } - } } #[cfg(feature = "dim2")] @@ -634,85 +609,134 @@ impl<'a, 'b, 'c, 'd> Testbed<'a, 'b, 'c, 'd> { self.plugins.0.push(Box::new(plugin)); } - // fn handle_common_event<'b>(&mut self, event: Event<'b>) -> Event<'b> { - // match event.value { - // WindowEvent::Key(Key::T, Action::Release, _) => { - // if self.state.running == RunMode::Stop { - // self.state.running = RunMode::Running; - // } else { - // self.state.running = RunMode::Stop; - // } - // } - // WindowEvent::Key(Key::S, Action::Release, _) => self.state.running = RunMode::Step, - // WindowEvent::Key(Key::R, Action::Release, _) => self - // .state - // .action_flags - // .set(TestbedActionFlags::EXAMPLE_CHANGED, true), - // WindowEvent::Key(Key::C, Action::Release, _) => { - // // Delete 1 collider of 10% of the remaining dynamic bodies. - // let mut colliders: Vec<_> = self - // .harness - // .physics - // .bodies - // .iter() - // .filter(|e| e.1.is_dynamic()) - // .filter(|e| !e.1.colliders().is_empty()) - // .map(|e| e.1.colliders().to_vec()) - // .collect(); - // colliders.sort_by_key(|co| -(co.len() as isize)); - // - // let num_to_delete = (colliders.len() / 10).max(1); - // for to_delete in &colliders[..num_to_delete] { - // self.harness.physics.colliders.remove( - // to_delete[0], - // &mut self.harness.physics.islands, - // &mut self.harness.physics.bodies, - // true, - // ); - // } - // } - // WindowEvent::Key(Key::D, Action::Release, _) => { - // // Delete 10% of the remaining dynamic bodies. - // let dynamic_bodies: Vec<_> = self - // .harness - // .physics - // .bodies - // .iter() - // .filter(|e| !e.1.is_static()) - // .map(|e| e.0) - // .collect(); - // let num_to_delete = (dynamic_bodies.len() / 10).max(1); - // for to_delete in &dynamic_bodies[..num_to_delete] { - // self.harness.physics.bodies.remove( - // *to_delete, - // &mut self.harness.physics.islands, - // &mut self.harness.physics.colliders, - // &mut self.harness.physics.joints, - // ); - // } - // } - // WindowEvent::Key(Key::J, Action::Release, _) => { - // // Delete 10% of the remaining joints. - // let joints: Vec<_> = self.harness.physics.joints.iter().map(|e| e.0).collect(); - // let num_to_delete = (joints.len() / 10).max(1); - // for to_delete in &joints[..num_to_delete] { - // self.harness.physics.joints.remove( - // *to_delete, - // &mut self.harness.physics.islands, - // &mut self.harness.physics.bodies, - // true, - // ); - // } - // } - // WindowEvent::CursorPos(x, y, _) => { - // self.state.cursor_pos.x = x as f32; - // self.state.cursor_pos.y = y as f32; - // } - // _ => {} - // } - // - // event - // } + fn handle_common_events(&mut self, events: &Input) { + for key in events.get_just_released() { + match *key { + KeyCode::T => { + if self.state.running == RunMode::Stop { + self.state.running = RunMode::Running; + } else { + self.state.running = RunMode::Stop; + } + } + KeyCode::S => self.state.running = RunMode::Step, + KeyCode::R => self + .state + .action_flags + .set(TestbedActionFlags::EXAMPLE_CHANGED, true), + KeyCode::C => { + // Delete 1 collider of 10% of the remaining dynamic bodies. + let mut colliders: Vec<_> = self + .harness + .physics + .bodies + .iter() + .filter(|e| e.1.is_dynamic()) + .filter(|e| !e.1.colliders().is_empty()) + .map(|e| e.1.colliders().to_vec()) + .collect(); + colliders.sort_by_key(|co| -(co.len() as isize)); + + let num_to_delete = (colliders.len() / 10).max(1); + for to_delete in &colliders[..num_to_delete] { + if let Some(graphics) = self.graphics.as_mut() { + graphics.remove_collider(to_delete[0], &self.harness.physics.colliders); + } + self.harness.physics.colliders.remove( + to_delete[0], + &mut self.harness.physics.islands, + &mut self.harness.physics.bodies, + true, + ); + } + } + KeyCode::D => { + // Delete 10% of the remaining dynamic bodies. + let dynamic_bodies: Vec<_> = self + .harness + .physics + .bodies + .iter() + .filter(|e| !e.1.is_static()) + .map(|e| e.0) + .collect(); + let num_to_delete = (dynamic_bodies.len() / 10).max(1); + for to_delete in &dynamic_bodies[..num_to_delete] { + if let Some(graphics) = self.graphics.as_mut() { + graphics.remove_body(*to_delete); + } + self.harness.physics.bodies.remove( + *to_delete, + &mut self.harness.physics.islands, + &mut self.harness.physics.colliders, + &mut self.harness.physics.impulse_joints, + &mut self.harness.physics.multibody_joints, + ); + } + } + KeyCode::J => { + // Delete 10% of the remaining impulse_joints. + let impulse_joints: Vec<_> = self + .harness + .physics + .impulse_joints + .iter() + .map(|e| e.0) + .collect(); + let num_to_delete = (impulse_joints.len() / 10).max(1); + for to_delete in &impulse_joints[..num_to_delete] { + self.harness.physics.impulse_joints.remove( + *to_delete, + &mut self.harness.physics.islands, + &mut self.harness.physics.bodies, + true, + ); + } + } + KeyCode::A => { + // Delete 10% of the remaining multibody_joints. + let multibody_joints: Vec<_> = self + .harness + .physics + .multibody_joints + .iter() + .map(|e| e.0) + .collect(); + let num_to_delete = (multibody_joints.len() / 10).max(1); + for to_delete in &multibody_joints[..num_to_delete] { + self.harness.physics.multibody_joints.remove( + *to_delete, + &mut self.harness.physics.islands, + &mut self.harness.physics.bodies, + true, + ); + } + } + KeyCode::M => { + // Delete one remaining multibody. + let to_delete = self + .harness + .physics + .multibody_joints + .iter() + .next() + .map(|a| a.2.rigid_body_handle()); + if let Some(to_delete) = to_delete { + self.harness + .physics + .multibody_joints + .remove_multibody_articulations( + to_delete, + &mut self.harness.physics.islands, + &mut self.harness.physics.bodies, + true, + ); + } + } + _ => {} + } + } + } // #[cfg(feature = "dim2")] // fn handle_special_event(&mut self) {} @@ -878,11 +902,37 @@ fn update_testbed( ui_context: Res, mut gfx_components: Query<(&mut Transform,)>, mut cameras: Query<(&Camera, &GlobalTransform, &mut OrbitCamera)>, + keys: Res>, ) { let meshes = &mut *meshes; let materials = &mut *materials; let prev_example = state.selected_example; + // Handle inputs + { + let graphics_context = TestbedGraphics { + pipelines: &mut *pipelines, + shaders: &mut *shaders, + graphics: &mut *graphics, + commands: &mut commands, + meshes: &mut *meshes, + materials: &mut *materials, + components: &mut gfx_components, + camera: &mut cameras.iter_mut().next().unwrap().2, + }; + + let mut testbed = Testbed { + graphics: Some(graphics_context), + state: &mut *state, + harness: &mut *harness, + #[cfg(feature = "other-backends")] + other_backends: &mut *other_backends, + plugins: &mut *plugins, + }; + + testbed.handle_common_events(&*keys); + } + // Update UI { let harness = &mut *harness; @@ -943,28 +993,6 @@ fn update_testbed( if state.selected_example != prev_example { harness.physics.integration_parameters = IntegrationParameters::default(); - - if cfg!(feature = "dim3") - && (state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION - || state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR) - { - let max_position_iterations = harness - .physics - .integration_parameters - .max_position_iterations; - let max_velocity_iterations = harness - .physics - .integration_parameters - .max_velocity_iterations; - harness - .physics - .integration_parameters - .max_velocity_iterations = max_position_iterations; - harness - .physics - .integration_parameters - .max_position_iterations = max_velocity_iterations; - } } let selected_example = state.selected_example; @@ -1009,7 +1037,7 @@ fn update_testbed( &harness.physics.narrow_phase, &harness.physics.bodies, &harness.physics.colliders, - &harness.physics.joints, + &harness.physics.impulse_joints, ) .ok(); @@ -1172,22 +1200,6 @@ fn update_testbed( } } - #[cfg(feature = "other-backends")] - { - if state.selected_backend == NPHYSICS_BACKEND { - let harness = &mut *harness; - other_backends.nphysics.as_mut().unwrap().step( - &mut harness.physics.pipeline.counters, - &harness.physics.integration_parameters, - ); - other_backends - .nphysics - .as_mut() - .unwrap() - .sync(&mut harness.physics.bodies, &mut harness.physics.colliders); - } - } - for plugin in &mut plugins.0 { plugin.run_callbacks(&mut harness); } diff --git a/src_testbed/ui.rs b/src_testbed/ui.rs index 4a21072..894b6a4 100644 --- a/src_testbed/ui.rs +++ b/src_testbed/ui.rs @@ -1,7 +1,10 @@ use rapier::counters::Counters; use crate::harness::Harness; -use crate::testbed::{RunMode, TestbedActionFlags, TestbedState, TestbedStateFlags}; +use crate::testbed::{ + RunMode, TestbedActionFlags, TestbedState, TestbedStateFlags, PHYSX_BACKEND_PATCH_FRICTION, + PHYSX_BACKEND_TWO_FRICTION_DIR, +}; use crate::PhysicsState; use bevy_egui::egui::Slider; @@ -10,8 +13,6 @@ use bevy_egui::{egui, EguiContext}; pub fn update_ui(ui_context: &EguiContext, state: &mut TestbedState, harness: &mut Harness) { egui::Window::new("Parameters").show(ui_context.ctx(), |ui| { if state.backend_names.len() > 1 && !state.example_names.is_empty() { - #[cfg(all(feature = "dim3", feature = "other-backends"))] - let prev_selected_backend = state.selected_backend; let mut changed = false; egui::ComboBox::from_label("backend") .width(150.0) @@ -29,30 +30,6 @@ pub fn update_ui(ui_context: &EguiContext, state: &mut TestbedState, harness: &m state .action_flags .set(TestbedActionFlags::BACKEND_CHANGED, true); - - #[cfg(all(feature = "dim3", feature = "other-backends"))] - fn is_physx(id: usize) -> bool { - id == crate::testbed::PHYSX_BACKEND_PATCH_FRICTION - || id == crate::testbed::PHYSX_BACKEND_TWO_FRICTION_DIR - } - - #[cfg(all(feature = "dim3", feature = "other-backends"))] - if (is_physx(state.selected_backend) && !is_physx(prev_selected_backend)) - || (!is_physx(state.selected_backend) && is_physx(prev_selected_backend)) - { - // PhysX defaults (4 position iterations, 1 velocity) are the - // opposite of rapier's (4 velocity iterations, 1 position). - std::mem::swap( - &mut harness - .physics - .integration_parameters - .max_position_iterations, - &mut harness - .physics - .integration_parameters - .max_velocity_iterations, - ); - } } ui.separator(); @@ -113,14 +90,47 @@ pub fn update_ui(ui_context: &EguiContext, state: &mut TestbedState, harness: &m }); let integration_parameters = &mut harness.physics.integration_parameters; - ui.add( - Slider::new(&mut integration_parameters.max_velocity_iterations, 0..=200) - .text("vels. iters."), - ); - ui.add( - Slider::new(&mut integration_parameters.max_position_iterations, 0..=200) - .text("pos. iters."), + + ui.checkbox( + &mut integration_parameters.interleave_restitution_and_friction_resolution, + "interleave friction resolution", ); + + if state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION + || state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR + { + ui.add( + Slider::new(&mut integration_parameters.max_velocity_iterations, 0..=200) + .text("pos. iters."), + ); + ui.add( + Slider::new( + &mut integration_parameters.max_stabilization_iterations, + 0..=200, + ) + .text("vel. iters."), + ); + } else { + ui.add( + Slider::new(&mut integration_parameters.max_velocity_iterations, 0..=200) + .text("vel. rest. iters."), + ); + ui.add( + Slider::new( + &mut integration_parameters.max_velocity_friction_iterations, + 0..=200, + ) + .text("vel. frict. iters."), + ); + ui.add( + Slider::new( + &mut integration_parameters.max_stabilization_iterations, + 0..=200, + ) + .text("vel. stab. iters."), + ); + } + #[cfg(feature = "parallel")] { ui.add( @@ -135,10 +145,6 @@ pub fn update_ui(ui_context: &EguiContext, state: &mut TestbedState, harness: &m Slider::new(&mut integration_parameters.min_island_size, 1..=10_000) .text("min island size"), ); - ui.add( - Slider::new(&mut integration_parameters.warmstart_coeff, 0.0..=1.0) - .text("warmstart coeff"), - ); let mut frequency = integration_parameters.inv_dt().round() as u32; ui.add(Slider::new(&mut frequency, 0..=240).text("frequency (Hz)")); integration_parameters.set_inv_dt(frequency as f32); @@ -247,7 +253,7 @@ fn serialization_string(timestep_id: usize, physics: &PhysicsState) -> String { let cs = bincode::serialize(&physics.colliders).unwrap(); // println!("cs: {}", instant::now() - t); // let t = instant::now(); - let js = bincode::serialize(&physics.joints).unwrap(); + let js = bincode::serialize(&physics.impulse_joints).unwrap(); // println!("js: {}", instant::now() - t); let serialization_time = instant::now() - t; let hash_bf = md5::compute(&bf); From ae27e1c331a09319a7a077ea05254c34793e1a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Sun, 2 Jan 2022 17:22:37 +0100 Subject: [PATCH 2/5] Run cargo fmt --- src/geometry/broad_phase_multi_sap/broad_phase.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/geometry/broad_phase_multi_sap/broad_phase.rs b/src/geometry/broad_phase_multi_sap/broad_phase.rs index f4b9fa1..52b56d0 100644 --- a/src/geometry/broad_phase_multi_sap/broad_phase.rs +++ b/src/geometry/broad_phase_multi_sap/broad_phase.rs @@ -622,7 +622,7 @@ impl BroadPhase { #[cfg(test)] mod test { - use crate::dynamics::{IslandManager, ImpulseJointSet, RigidBodyBuilder, RigidBodySet}; + use crate::dynamics::{ImpulseJointSet, IslandManager, RigidBodyBuilder, RigidBodySet}; use crate::geometry::{BroadPhase, ColliderBuilder, ColliderSet}; #[test] From fcf9e61e28e45d391de7dd172ab6e2d2306b3c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Sun, 2 Jan 2022 17:25:15 +0100 Subject: [PATCH 3/5] Fix warnings --- src/dynamics/solver/joint_constraint/joint_constraint.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dynamics/solver/joint_constraint/joint_constraint.rs b/src/dynamics/solver/joint_constraint/joint_constraint.rs index f42038c..162cb7f 100644 --- a/src/dynamics/solver/joint_constraint/joint_constraint.rs +++ b/src/dynamics/solver/joint_constraint/joint_constraint.rs @@ -10,9 +10,9 @@ use crate::dynamics::{ ImpulseJoint, IntegrationParameters, JointGraphEdge, JointIndex, RigidBodyIds, RigidBodyMassProps, RigidBodyPosition, RigidBodyType, RigidBodyVelocity, }; -use crate::math::{Isometry, Real, SPATIAL_DIM}; #[cfg(feature = "simd-is-enabled")] -use crate::math::{SimdReal, SIMD_WIDTH}; +use crate::math::{Isometry, SimdReal, SIMD_WIDTH}; +use crate::math::{Real, SPATIAL_DIM}; use crate::prelude::MultibodyJointSet; use na::DVector; From 90edb4b53242fc24ecb666bebc593638ee0ff7fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Sun, 2 Jan 2022 17:29:19 +0100 Subject: [PATCH 4/5] More warning fixes + temporarily disable -D warning in the CI --- .github/workflows/rapier-ci-build.yml | 12 ++++++------ src/geometry/broad_phase_multi_sap/broad_phase.rs | 2 -- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/rapier-ci-build.yml b/.github/workflows/rapier-ci-build.yml index b4bc538..640dec8 100644 --- a/.github/workflows/rapier-ci-build.yml +++ b/.github/workflows/rapier-ci-build.yml @@ -18,8 +18,8 @@ jobs: run: cargo fmt -- --check build-native: runs-on: ubuntu-latest - env: - RUSTFLAGS: -D warnings +# env: +# RUSTFLAGS: -D warnings steps: - uses: actions/checkout@v2 - run: sudo apt-get install -y cmake libxcb-composite0-dev @@ -51,8 +51,8 @@ jobs: run: cargo check -j 1 --verbose -p rapier-examples-3d; build-wasm: runs-on: ubuntu-latest - env: - RUSTFLAGS: -D warnings +# env: +# RUSTFLAGS: -D warnings steps: - uses: actions/checkout@v2 - run: rustup target add wasm32-unknown-unknown @@ -62,8 +62,8 @@ jobs: run: cd crates/rapier3d && cargo build --verbose --features wasm-bindgen --target wasm32-unknown-unknown; build-wasm-emscripten: runs-on: ubuntu-latest - env: - RUSTFLAGS: -D warnings +# env: +# RUSTFLAGS: -D warnings steps: - uses: actions/checkout@v2 - run: rustup target add wasm32-unknown-emscripten diff --git a/src/geometry/broad_phase_multi_sap/broad_phase.rs b/src/geometry/broad_phase_multi_sap/broad_phase.rs index 52b56d0..8f1d310 100644 --- a/src/geometry/broad_phase_multi_sap/broad_phase.rs +++ b/src/geometry/broad_phase_multi_sap/broad_phase.rs @@ -80,7 +80,6 @@ pub struct BroadPhase { layers: Vec, smallest_layer: u8, largest_layer: u8, - deleted_any: bool, // NOTE: we maintain this hashmap to simplify collider removal. // This information is also present in the ColliderProxyId // component. However if that component is removed, we need @@ -133,7 +132,6 @@ impl BroadPhase { region_pool: Vec::new(), reporting: HashMap::default(), colliders_proxy_ids: HashMap::default(), - deleted_any: false, } } From 9f9d3293605fa84555c08bec5efe68a71cd18432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Crozet?= Date: Sun, 2 Jan 2022 17:43:38 +0100 Subject: [PATCH 5/5] Fix tests --- benchmarks2d/joint_prismatic2.rs | 2 +- .../broad_phase_multi_sap/broad_phase.rs | 13 ++++- src/pipeline/physics_pipeline.rs | 48 +++++++++++++++++-- 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/benchmarks2d/joint_prismatic2.rs b/benchmarks2d/joint_prismatic2.rs index 4733088..20d423e 100644 --- a/benchmarks2d/joint_prismatic2.rs +++ b/benchmarks2d/joint_prismatic2.rs @@ -47,7 +47,7 @@ pub fn init_world(testbed: &mut Testbed) { UnitVector::new_normalize(vector![-1.0, 1.0]) }; - let mut prism = PrismaticJoint::new(axis) + let prism = PrismaticJoint::new(axis) .local_anchor2(point![0.0, shift]) .limit_axis([-1.5, 1.5]); impulse_joints.insert(curr_parent, curr_child, prism); diff --git a/src/geometry/broad_phase_multi_sap/broad_phase.rs b/src/geometry/broad_phase_multi_sap/broad_phase.rs index 8f1d310..d2b0076 100644 --- a/src/geometry/broad_phase_multi_sap/broad_phase.rs +++ b/src/geometry/broad_phase_multi_sap/broad_phase.rs @@ -620,7 +620,9 @@ impl BroadPhase { #[cfg(test)] mod test { - use crate::dynamics::{ImpulseJointSet, IslandManager, RigidBodyBuilder, RigidBodySet}; + use crate::dynamics::{ + ImpulseJointSet, IslandManager, MultibodyJointSet, RigidBodyBuilder, RigidBodySet, + }; use crate::geometry::{BroadPhase, ColliderBuilder, ColliderSet}; #[test] @@ -629,6 +631,7 @@ mod test { let mut bodies = RigidBodySet::new(); let mut colliders = ColliderSet::new(); let mut impulse_joints = ImpulseJointSet::new(); + let mut multibody_joints = MultibodyJointSet::new(); let mut islands = IslandManager::new(); let rb = RigidBodyBuilder::new_dynamic().build(); @@ -639,7 +642,13 @@ mod test { let mut events = Vec::new(); broad_phase.update(0.0, &mut colliders, &[coh], &[], &mut events); - bodies.remove(hrb, &mut islands, &mut colliders, &mut impulse_joints); + bodies.remove( + hrb, + &mut islands, + &mut colliders, + &mut impulse_joints, + &mut multibody_joints, + ); broad_phase.update(0.0, &mut colliders, &[], &[coh], &mut events); // Create another body. diff --git a/src/pipeline/physics_pipeline.rs b/src/pipeline/physics_pipeline.rs index b244860..a289a09 100644 --- a/src/pipeline/physics_pipeline.rs +++ b/src/pipeline/physics_pipeline.rs @@ -706,11 +706,13 @@ mod test { use crate::geometry::{BroadPhase, ColliderBuilder, ColliderSet, NarrowPhase}; use crate::math::Vector; use crate::pipeline::PhysicsPipeline; + use crate::prelude::MultibodyJointSet; #[test] fn kinematic_and_static_contact_crash() { let mut colliders = ColliderSet::new(); let mut impulse_joints = ImpulseJointSet::new(); + let mut multibody_joints = MultibodyJointSet::new(); let mut pipeline = PhysicsPipeline::new(); let mut bf = BroadPhase::new(); let mut nf = NarrowPhase::new(); @@ -736,6 +738,7 @@ mod test { &mut bodies, &mut colliders, &mut impulse_joints, + &mut multibody_joints, &mut CCDSolver::new(), &(), &(), @@ -746,6 +749,7 @@ mod test { fn rigid_body_removal_before_step() { let mut colliders = ColliderSet::new(); let mut impulse_joints = ImpulseJointSet::new(); + let mut multibody_joints = MultibodyJointSet::new(); let mut pipeline = PhysicsPipeline::new(); let mut bf = BroadPhase::new(); let mut nf = NarrowPhase::new(); @@ -770,7 +774,13 @@ mod test { let to_delete = [h1, h2, h3, h4]; for h in &to_delete { - bodies.remove(*h, &mut islands, &mut colliders, &mut impulse_joints); + bodies.remove( + *h, + &mut islands, + &mut colliders, + &mut impulse_joints, + &mut multibody_joints, + ); } pipeline.step( @@ -782,6 +792,7 @@ mod test { &mut bodies, &mut colliders, &mut impulse_joints, + &mut multibody_joints, &mut CCDSolver::new(), &(), &(), @@ -793,6 +804,7 @@ mod test { fn rigid_body_removal_snapshot_handle_determinism() { let mut colliders = ColliderSet::new(); let mut impulse_joints = ImpulseJointSet::new(); + let mut multibody_joints = MultibodyJointSet::new(); let mut islands = IslandManager::new(); let mut bodies = RigidBodySet::new(); @@ -801,9 +813,27 @@ mod test { let h2 = bodies.insert(rb.clone()); let h3 = bodies.insert(rb.clone()); - bodies.remove(h1, &mut islands, &mut colliders, &mut impulse_joints); - bodies.remove(h3, &mut islands, &mut colliders, &mut impulse_joints); - bodies.remove(h2, &mut islands, &mut colliders, &mut impulse_joints); + bodies.remove( + h1, + &mut islands, + &mut colliders, + &mut impulse_joints, + &mut multibody_joints, + ); + bodies.remove( + h3, + &mut islands, + &mut colliders, + &mut impulse_joints, + &mut multibody_joints, + ); + bodies.remove( + h2, + &mut islands, + &mut colliders, + &mut impulse_joints, + &mut multibody_joints, + ); let ser_bodies = bincode::serialize(&bodies).unwrap(); let mut bodies2: RigidBodySet = bincode::deserialize(&ser_bodies).unwrap(); @@ -832,6 +862,7 @@ mod test { let mut colliders = ColliderSet::new(); let mut ccd = CCDSolver::new(); let mut impulse_joints = ImpulseJointSet::new(); + let mut multibody_joints = MultibodyJointSet::new(); let mut islands = IslandManager::new(); let physics_hooks = (); let event_handler = (); @@ -841,7 +872,13 @@ mod test { let collider = ColliderBuilder::ball(1.0).build(); let c_handle = colliders.insert_with_parent(collider, b_handle, &mut bodies); colliders.remove(c_handle, &mut islands, &mut bodies, true); - bodies.remove(b_handle, &mut islands, &mut colliders, &mut impulse_joints); + bodies.remove( + b_handle, + &mut islands, + &mut colliders, + &mut impulse_joints, + &mut multibody_joints, + ); for _ in 0..10 { pipeline.step( @@ -853,6 +890,7 @@ mod test { &mut bodies, &mut colliders, &mut impulse_joints, + &mut multibody_joints, &mut ccd, &physics_hooks, &event_handler,