Fix CharacterController max/min slope handling (#701)

This commit is contained in:
Thierry Berger
2024-09-23 11:10:29 +02:00
committed by GitHub
parent e7e196d9f9
commit 76357e3588
10 changed files with 210 additions and 22 deletions

View File

@@ -4,6 +4,7 @@
- The region key has been replaced by an i64 in the f64 version of rapier, increasing the range before panics occur. - The region key has been replaced by an i64 in the f64 version of rapier, increasing the range before panics occur.
- Fix `BroadphaseMultiSap` not being able to serialize correctly with serde_json. - Fix `BroadphaseMultiSap` not being able to serialize correctly with serde_json.
- Fix `KinematicCharacterController::move_shape` not respecting parameters `max_slope_climb_angle` and `min_slope_slide_angle`.
### Added ### Added

View File

@@ -50,7 +50,6 @@ crossbeam = "0.8"
bincode = "1" bincode = "1"
Inflector = "0.11" Inflector = "0.11"
md5 = "0.7" md5 = "0.7"
bevy_egui = "0.29" bevy_egui = "0.29"
bevy_ecs = "0.14" bevy_ecs = "0.14"
bevy_core_pipeline = "0.14" bevy_core_pipeline = "0.14"

View File

@@ -50,7 +50,6 @@ crossbeam = "0.8"
bincode = "1" bincode = "1"
Inflector = "0.11" Inflector = "0.11"
md5 = "0.7" md5 = "0.7"
bevy_egui = "0.29" bevy_egui = "0.29"
bevy_ecs = "0.14" bevy_ecs = "0.14"
bevy_core_pipeline = "0.14" bevy_core_pipeline = "0.14"

View File

@@ -52,7 +52,6 @@ bincode = "1"
md5 = "0.7" md5 = "0.7"
Inflector = "0.11" Inflector = "0.11"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
bevy_egui = "0.29" bevy_egui = "0.29"
bevy_ecs = "0.14" bevy_ecs = "0.14"
bevy_core_pipeline = "0.14" bevy_core_pipeline = "0.14"

View File

@@ -53,7 +53,6 @@ bincode = "1"
md5 = "0.7" md5 = "0.7"
Inflector = "0.11" Inflector = "0.11"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
bevy_egui = "0.29" bevy_egui = "0.29"
bevy_ecs = "0.14" bevy_ecs = "0.14"
bevy_core_pipeline = "0.14" bevy_core_pipeline = "0.14"

View File

@@ -1,4 +1,4 @@
use rapier3d::prelude::*; use rapier3d::{control::KinematicCharacterController, prelude::*};
use rapier_testbed3d::Testbed; use rapier_testbed3d::Testbed;
pub fn init_world(testbed: &mut Testbed) { pub fn init_world(testbed: &mut Testbed) {
@@ -41,7 +41,7 @@ pub fn init_world(testbed: &mut Testbed) {
* Character we will control manually. * Character we will control manually.
*/ */
let rigid_body = let rigid_body =
RigidBodyBuilder::kinematic_position_based().translation(vector![-3.0, 5.0, 0.0] * scale); RigidBodyBuilder::kinematic_position_based().translation(vector![0.0, 0.5, 0.0] * scale);
let character_handle = bodies.insert(rigid_body); let character_handle = bodies.insert(rigid_body);
let collider = ColliderBuilder::capsule_y(0.3 * scale, 0.15 * scale); // 0.15, 0.3, 0.15); let collider = ColliderBuilder::capsule_y(0.3 * scale, 0.15 * scale); // 0.15, 0.3, 0.15);
colliders.insert_with_parent(collider, character_handle, &mut bodies); colliders.insert_with_parent(collider, character_handle, &mut bodies);
@@ -95,19 +95,15 @@ pub fn init_world(testbed: &mut Testbed) {
*/ */
let slope_angle = 0.2; let slope_angle = 0.2;
let slope_size = 2.0; let slope_size = 2.0;
let collider = ColliderBuilder::cuboid( let collider = ColliderBuilder::cuboid(slope_size, ground_height, slope_size)
slope_size * scale, .translation(vector![0.1 + slope_size, -ground_height + 0.4, 0.0])
ground_height * scale, .rotation(Vector::z() * slope_angle);
slope_size * scale,
)
.translation(vector![ground_size + slope_size, -ground_height + 0.4, 0.0] * scale)
.rotation(Vector::z() * slope_angle);
colliders.insert(collider); colliders.insert(collider);
/* /*
* Create a slope we cant climb. * Create a slope we cant climb.
*/ */
let impossible_slope_angle = 0.9; let impossible_slope_angle = 0.6;
let impossible_slope_size = 2.0; let impossible_slope_size = 2.0;
let collider = ColliderBuilder::cuboid( let collider = ColliderBuilder::cuboid(
slope_size * scale, slope_size * scale,
@@ -116,8 +112,8 @@ pub fn init_world(testbed: &mut Testbed) {
) )
.translation( .translation(
vector![ vector![
ground_size + slope_size * 2.0 + impossible_slope_size - 0.9, 0.1 + slope_size * 2.0 + impossible_slope_size - 0.9,
-ground_height + 2.3, -ground_height + 1.7,
0.0 0.0
] * scale, ] * scale,
) )
@@ -185,5 +181,11 @@ pub fn init_world(testbed: &mut Testbed) {
*/ */
testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.set_world(bodies, colliders, impulse_joints, multibody_joints);
testbed.set_character_body(character_handle); testbed.set_character_body(character_handle);
testbed.set_character_controller(Some(KinematicCharacterController {
max_slope_climb_angle: impossible_slope_angle - 0.02,
min_slope_slide_angle: impossible_slope_angle - 0.02,
slide: true,
..Default::default()
}));
testbed.look_at(point!(10.0, 10.0, 10.0), Point::origin()); testbed.look_at(point!(10.0, 10.0, 10.0), Point::origin());
} }

View File

@@ -37,7 +37,7 @@ pub fn init_world(testbed: &mut Testbed) {
colliders.insert_with_parent(collider, handle, &mut bodies); colliders.insert_with_parent(collider, handle, &mut bodies);
let rigid_body = RigidBodyBuilder::dynamic() let rigid_body = RigidBodyBuilder::dynamic()
.translation(vector![0.0, 0.5, 0.0]) .translation(vector![-3.0, 5.0, 0.0])
.linvel(vector![0.0, -4.0, 20.0]) .linvel(vector![0.0, -4.0, 20.0])
.can_sleep(false); .can_sleep(false);
let handle = bodies.insert(rigid_body); let handle = bodies.insert(rigid_body);

View File

@@ -169,6 +169,7 @@ impl Default for KinematicCharacterController {
} }
/// The effective movement computed by the character controller. /// The effective movement computed by the character controller.
#[derive(Debug)]
pub struct EffectiveCharacterMovement { pub struct EffectiveCharacterMovement {
/// The movement to apply. /// The movement to apply.
pub translation: Vector<Real>, pub translation: Vector<Real>,
@@ -542,17 +543,17 @@ impl KinematicCharacterController {
) -> Vector<Real> { ) -> Vector<Real> {
let [_vertical_input, horizontal_input] = self.split_into_components(movement_input); let [_vertical_input, horizontal_input] = self.split_into_components(movement_input);
let horiz_input_decomp = self.decompose_hit(&horizontal_input, &hit.toi); let horiz_input_decomp = self.decompose_hit(&horizontal_input, &hit.toi);
let input_decomp = self.decompose_hit(movement_input, &hit.toi);
let decomp = self.decompose_hit(translation_remaining, &hit.toi); let decomp = self.decompose_hit(translation_remaining, &hit.toi);
// An object is trying to slip if the tangential movement induced by its vertical movement // An object is trying to slip if the tangential movement induced by its vertical movement
// points downward. // points downward.
let slipping_intent = self.up.dot(&horiz_input_decomp.vertical_tangent) < 0.0; let slipping_intent = self.up.dot(&horiz_input_decomp.vertical_tangent) < 0.0;
// An object is slipping if its vertical movement points downward.
let slipping = self.up.dot(&decomp.vertical_tangent) < 0.0; let slipping = self.up.dot(&decomp.vertical_tangent) < 0.0;
// An object is trying to climb if its indirect vertical motion points upward. // An object is trying to climb if its vertical input motion points upward.
let climbing_intent = self.up.dot(&input_decomp.vertical_tangent) > 0.0; let climbing_intent = self.up.dot(&_vertical_input) > 0.0;
// An object is climbing if the tangential movement induced by its vertical movement points upward.
let climbing = self.up.dot(&decomp.vertical_tangent) > 0.0; let climbing = self.up.dot(&decomp.vertical_tangent) > 0.0;
let allowed_movement = if hit.is_wall && climbing && !climbing_intent { let allowed_movement = if hit.is_wall && climbing && !climbing_intent {
@@ -904,3 +905,151 @@ fn subtract_hit(translation: Vector<Real>, hit: &ShapeCastHit) -> Vector<Real> {
let surface_correction = surface_correction * (1.0 + 1.0e-5); let surface_correction = surface_correction * (1.0 + 1.0e-5);
translation + *hit.normal1 * surface_correction translation + *hit.normal1 * surface_correction
} }
#[cfg(all(feature = "dim3", feature = "f32"))]
#[cfg(test)]
mod test {
use crate::{control::KinematicCharacterController, prelude::*};
#[test]
fn character_controller_climb_test() {
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 = BroadPhaseMultiSap::new();
let mut nf = NarrowPhase::new();
let mut islands = IslandManager::new();
let mut query_pipeline = QueryPipeline::new();
let mut bodies = RigidBodySet::new();
let gravity = Vector::y() * -9.81;
let ground_size = 100.0;
let ground_height = 0.1;
/*
* Create a flat ground
*/
let rigid_body = RigidBodyBuilder::fixed().translation(vector![0.0, -ground_height, 0.0]);
let floor_handle = bodies.insert(rigid_body);
let collider = ColliderBuilder::cuboid(ground_size, ground_height, ground_size);
colliders.insert_with_parent(collider, floor_handle, &mut bodies);
/*
* Create a slope we can climb.
*/
let slope_angle = 0.2;
let slope_size = 2.0;
let collider = ColliderBuilder::cuboid(slope_size, ground_height, slope_size)
.translation(vector![0.1 + slope_size, -ground_height + 0.4, 0.0])
.rotation(Vector::z() * slope_angle);
colliders.insert(collider);
/*
* Create a slope we cant climb.
*/
let impossible_slope_angle = 0.6;
let impossible_slope_size = 2.0;
let collider = ColliderBuilder::cuboid(slope_size, ground_height, ground_size)
.translation(vector![
0.1 + slope_size * 2.0 + impossible_slope_size - 0.9,
-ground_height + 1.7,
0.0
])
.rotation(Vector::z() * impossible_slope_angle);
colliders.insert(collider);
let integration_parameters = IntegrationParameters::default();
// Initialize character which can climb
let mut character_body_can_climb = RigidBodyBuilder::kinematic_position_based()
.additional_mass(1.0)
.build();
character_body_can_climb.set_translation(Vector::new(0.6, 0.5, 0.0), false);
let character_handle_can_climb = bodies.insert(character_body_can_climb);
let collider = ColliderBuilder::ball(0.5).build();
colliders.insert_with_parent(collider.clone(), character_handle_can_climb, &mut bodies);
// Initialize character which cannot climb
let mut character_body_cannot_climb = RigidBodyBuilder::kinematic_position_based()
.additional_mass(1.0)
.build();
character_body_cannot_climb.set_translation(Vector::new(-0.6, 0.5, 0.0), false);
let character_handle_cannot_climb = bodies.insert(character_body_cannot_climb);
let collider = ColliderBuilder::ball(0.5).build();
let character_shape = collider.shape();
colliders.insert_with_parent(collider.clone(), character_handle_cannot_climb, &mut bodies);
query_pipeline.update(&colliders);
for i in 0..200 {
let mut update_character_controller =
|controller: KinematicCharacterController, handle: RigidBodyHandle| {
let character_body = bodies.get(handle).unwrap();
// Use a closure to handle or collect the collisions while
// the character is being moved.
let mut collisions = vec![];
let filter_character_controller = QueryFilter::new().exclude_rigid_body(handle);
let effective_movement = controller.move_shape(
integration_parameters.dt,
&bodies,
&colliders,
&query_pipeline,
character_shape,
character_body.position(),
Vector::new(0.1, -0.1, 0.0),
filter_character_controller,
|collision| collisions.push(collision),
);
let character_body = bodies.get_mut(handle).unwrap();
let translation = character_body.translation();
assert_eq!(
effective_movement.grounded, true,
"movement should be grounded at all times for current setup (iter: {}), pos: {}.",
i, translation + effective_movement.translation
);
character_body.set_next_kinematic_translation(
translation + effective_movement.translation,
);
};
let character_controller_cannot_climb = KinematicCharacterController {
max_slope_climb_angle: impossible_slope_angle - 0.001,
..Default::default()
};
let character_controller_can_climb = KinematicCharacterController {
max_slope_climb_angle: impossible_slope_angle + 0.001,
..Default::default()
};
update_character_controller(
character_controller_cannot_climb,
character_handle_cannot_climb,
);
update_character_controller(character_controller_can_climb, character_handle_can_climb);
// Step once
pipeline.step(
&gravity,
&integration_parameters,
&mut islands,
&mut bf,
&mut nf,
&mut bodies,
&mut colliders,
&mut impulse_joints,
&mut multibody_joints,
&mut CCDSolver::new(),
Some(&mut query_pipeline),
&(),
&(),
);
}
let character_body = bodies.get(character_handle_can_climb).unwrap();
assert!(character_body.translation().x > 6.0);
assert!(character_body.translation().y > 3.0);
let character_body = bodies.get(character_handle_cannot_climb).unwrap();
assert!(character_body.translation().x < 4.0);
assert!(dbg!(character_body.translation().y) < 2.0);
}
}

View File

@@ -102,6 +102,7 @@ pub struct TestbedState {
pub draw_colls: bool, pub draw_colls: bool,
pub highlighted_body: Option<RigidBodyHandle>, pub highlighted_body: Option<RigidBodyHandle>,
pub character_body: Option<RigidBodyHandle>, pub character_body: Option<RigidBodyHandle>,
pub character_controller: Option<KinematicCharacterController>,
#[cfg(feature = "dim3")] #[cfg(feature = "dim3")]
pub vehicle_controller: Option<DynamicRayCastVehicleController>, pub vehicle_controller: Option<DynamicRayCastVehicleController>,
// pub grabbed_object: Option<DefaultBodyPartHandle>, // pub grabbed_object: Option<DefaultBodyPartHandle>,
@@ -186,6 +187,7 @@ impl TestbedApp {
draw_colls: false, draw_colls: false,
highlighted_body: None, highlighted_body: None,
character_body: None, character_body: None,
character_controller: None,
#[cfg(feature = "dim3")] #[cfg(feature = "dim3")]
vehicle_controller: None, vehicle_controller: None,
// grabbed_object: None, // grabbed_object: None,
@@ -530,6 +532,10 @@ impl<'a, 'b, 'c, 'd, 'e, 'f> Testbed<'a, 'b, 'c, 'd, 'e, 'f> {
self.state.character_body = Some(handle); self.state.character_body = Some(handle);
} }
pub fn set_character_controller(&mut self, controller: Option<KinematicCharacterController>) {
self.state.character_controller = controller;
}
#[cfg(feature = "dim3")] #[cfg(feature = "dim3")]
pub fn set_vehicle_controller(&mut self, controller: DynamicRayCastVehicleController) { pub fn set_vehicle_controller(&mut self, controller: DynamicRayCastVehicleController) {
self.state.vehicle_controller = Some(controller); self.state.vehicle_controller = Some(controller);
@@ -827,7 +833,7 @@ impl<'a, 'b, 'c, 'd, 'e, 'f> Testbed<'a, 'b, 'c, 'd, 'e, 'f> {
desired_movement *= speed; desired_movement *= speed;
desired_movement -= Vector::y() * speed; desired_movement -= Vector::y() * speed;
let controller = KinematicCharacterController::default(); let controller = self.state.character_controller.unwrap_or_default();
let phx = &mut self.harness.physics; let phx = &mut self.harness.physics;
let character_body = &phx.bodies[character_handle]; let character_body = &phx.bodies[character_handle];
let character_collider = &phx.colliders[character_body.colliders()[0]]; let character_collider = &phx.colliders[character_body.colliders()[0]];

View File

@@ -1,3 +1,4 @@
use rapier::control::CharacterLength;
use rapier::counters::Counters; use rapier::counters::Counters;
use rapier::math::Real; use rapier::math::Real;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
@@ -240,7 +241,40 @@ pub fn update_ui(
// .set(TestbedStateFlags::CONTACT_POINTS, contact_points); // .set(TestbedStateFlags::CONTACT_POINTS, contact_points);
// state.flags.set(TestbedStateFlags::WIREFRAME, wireframe); // state.flags.set(TestbedStateFlags::WIREFRAME, wireframe);
ui.separator(); ui.separator();
if let Some(character_controller) = &mut state.character_controller {
ui.label("Character controller");
ui.checkbox(&mut character_controller.slide, "slide").on_hover_text("Should the character try to slide against the floor if it hits it?");
#[allow(clippy::useless_conversion)]
{
ui.add(Slider::new(&mut character_controller.max_slope_climb_angle, 0.0..=std::f32::consts::TAU.into()).text("max_slope_climb_angle"))
.on_hover_text("The maximum angle (radians) between the floors normal and the `up` vector that the character is able to climb.");
ui.add(Slider::new(&mut character_controller.min_slope_slide_angle, 0.0..=std::f32::consts::FRAC_PI_2.into()).text("min_slope_slide_angle"))
.on_hover_text("The minimum angle (radians) between the floors normal and the `up` vector before the character starts to slide down automatically.");
}
let mut is_snapped = character_controller.snap_to_ground.is_some();
if ui.checkbox(&mut is_snapped, "snap_to_ground").changed {
match is_snapped {
true => {
character_controller.snap_to_ground = Some(CharacterLength::Relative(0.1));
},
false => {
character_controller.snap_to_ground = None;
},
}
}
if let Some(snapped) = &mut character_controller.snap_to_ground {
match snapped {
CharacterLength::Relative(val) => {
ui.add(Slider::new(val, 0.0..=10.0).text("Snapped Relative Character Length"));
},
CharacterLength::Absolute(val) => {
ui.add(Slider::new(val, 0.0..=10.0).text("Snapped Absolute Character Length"));
},
}
}
ui.separator();
}
let label = if state.running == RunMode::Stop { let label = if state.running == RunMode::Stop {
"Start (T)" "Start (T)"
} else { } else {