diff --git a/fyrox-impl/src/scene/graph/physics/character.rs b/fyrox-impl/src/scene/graph/physics/character.rs new file mode 100644 index 000000000..f67b0493e --- /dev/null +++ b/fyrox-impl/src/scene/graph/physics/character.rs @@ -0,0 +1,231 @@ +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +//! A kinematic character controller for player/NPC movement (walking, climbing, sliding). +//! See [`KinematicCharacterController`] docs for more info. + +use crate::scene::collider; +use crate::{ + core::{algebra::Vector3, num_traits::FloatConst, reflect::prelude::*, visitor::prelude::*}, + scene::graph::{ + physics::{filter_by_predicate, QueryFilter}, + Graph, + }, +}; +use fyrox_core::pool::Handle; +use fyrox_graph::SceneGraph; +use rapier3d::{ + geometry::{Collider, ColliderHandle, Shape}, + math::Pose, + na::Isometry3, +}; + +/// A length measure used for various options of a character controller. +#[derive(Visit, Reflect, Copy, Clone, Debug, PartialEq)] +#[reflect(type_uuid = "9b0843a3-dea4-46ed-91fa-7c4831302340")] +pub enum CharacterLength { + /// The length is specified relative to some of the character shape’s size. + /// + /// For example setting `CharacterAutostep::max_height` to `CharacterLength::Relative(0.1)` + /// for a shape with a height equal to 20.0 will result in a maximum step height + /// of `0.1 * 20.0 = 2.0`. + Relative(f32), + /// The length is specified as an absolute value, independent from the character shape’s size. + /// + /// For example setting `CharacterAutostep::max_height` to `CharacterLength::Relative(0.1)` + /// for a shape with a height equal to 20.0 will result in a maximum step height + /// of `0.1` (the shape height is ignored in for this value). + Absolute(f32), +} + +impl Default for CharacterLength { + fn default() -> Self { + Self::Absolute(1.0) + } +} + +impl Into for CharacterLength { + fn into(self) -> rapier3d::control::CharacterLength { + match self { + CharacterLength::Relative(v) => rapier3d::control::CharacterLength::Relative(v), + CharacterLength::Absolute(v) => rapier3d::control::CharacterLength::Absolute(v), + } + } +} + +/// Configuration for the auto-stepping character controller feature. +#[derive(Default, Visit, Reflect, Copy, Clone, Debug, PartialEq)] +#[reflect(type_uuid = "3b458b79-159c-4ab9-a01a-b0c866e997c3")] +pub struct CharacterAutostep { + /// The maximum step height a character can automatically step over. + pub max_height: CharacterLength, + /// The minimum width of free space that must be available after stepping on a stair. + pub min_width: CharacterLength, + /// Can the character automatically step over dynamic bodies too? + pub include_dynamic_bodies: bool, +} + +impl Into for CharacterAutostep { + fn into(self) -> rapier3d::control::CharacterAutostep { + rapier3d::control::CharacterAutostep { + max_height: self.max_height.into(), + min_width: self.min_width.into(), + include_dynamic_bodies: self.include_dynamic_bodies, + } + } +} + +/// A kinematic character controller for player/NPC movement (walking, climbing, sliding). +/// +/// This provides classic game character movement: walking on floors, sliding on slopes, +/// climbing stairs, and snapping to ground. It's kinematic (not physics-based), meaning +/// you control movement directly rather than applying forces. +/// +/// TODO: Example +#[derive(Visit, Reflect, Debug, Clone)] +#[reflect(type_uuid = "42995595-7e06-41b5-b8fc-8aa9b7feaf36", non_comparable)] +pub struct KinematicCharacterController { + /// The direction that goes "up". Used to determine where the floor is, and the floor’s angle. + pub up: Vector3, + /// A small gap to preserve between the character and its surroundings. + /// + /// This value should not be too large to avoid visual artifacts, but shouldn’t be too small + /// (must not be zero) to improve numerical stability of the character controller. + pub offset: CharacterLength, + /// Should the character try to slide against the floor if it hits it? + pub slide: bool, + /// Should the character automatically step over small obstacles? (disabled by default) + /// + /// Note that autostepping is currently a very computationally expensive feature, so it + /// is disabled by default. + pub autostep: Option, + /// The maximum angle (radians) between the floor’s normal and the `up` vector that the + /// character is able to climb. + pub max_slope_climb_angle: f32, + /// The minimum angle (radians) between the floor’s normal and the `up` vector before the + /// character starts to slide down automatically. + pub min_slope_slide_angle: f32, + /// Should the character be automatically snapped to the ground if the distance between + /// the ground and its feed are smaller than the specified threshold? + pub snap_to_ground: Option, + /// Increase this number if your character appears to get stuck when sliding against surfaces. + /// + /// This is a small distance applied to the movement toward the contact normals of shapes hit + /// by the character controller. This helps shape-casting not getting stuck in an always-penetrating + /// state during the sliding calculation. + /// + /// This value should remain fairly small since it can introduce artificial "bumps" when sliding + /// along a flat surface. + pub normal_nudge_factor: f32, +} + +/// The effective movement computed by the character controller. +#[derive(Debug)] +pub struct CharacterMovement { + /// The movement to apply. + pub translation: Vector3, + /// Is the character touching the ground after applying `EffectiveKinematicMovement::translation`? + pub grounded: bool, + /// Is the character sliding down a slope due to slope angle being larger than `min_slope_slide_angle`? + pub is_sliding_down_slope: bool, +} + +impl Default for KinematicCharacterController { + fn default() -> Self { + Self { + up: Vector3::y(), + offset: CharacterLength::Relative(0.01), + slide: true, + autostep: None, + max_slope_climb_angle: f32::FRAC_PI_4(), + min_slope_slide_angle: f32::FRAC_PI_4(), + snap_to_ground: Some(CharacterLength::Relative(0.2)), + normal_nudge_factor: 1.0e-4, + } + } +} + +impl KinematicCharacterController { + /// Computes the possible movement for a shape. + pub fn move_shape( + &self, + dt: f32, + shape: &dyn Shape, + position: Isometry3, + desired_translation: Vector3, + graph: &Graph, + filter: QueryFilter, + ) -> CharacterMovement { + let controller = rapier3d::control::KinematicCharacterController { + up: self.up.into(), + offset: self.offset.into(), + slide: self.slide, + autostep: self.autostep.map(Into::into), + max_slope_climb_angle: self.max_slope_climb_angle, + min_slope_slide_angle: self.min_slope_slide_angle, + snap_to_ground: self.snap_to_ground.map(Into::into), + normal_nudge_factor: self.normal_nudge_factor, + }; + + let predicate = |handle: ColliderHandle, _: &Collider| -> bool { + filter_by_predicate(filter.predicate, handle, graph, &graph.physics.colliders) + }; + let filter = filter.to_native(graph, &predicate); + let query_pipeline = graph.physics.query_pipeline(filter); + let movement = controller.move_shape( + dt, + &query_pipeline, + shape, + &Pose::from(position), + desired_translation.into(), + |_| {}, + ); + CharacterMovement { + translation: movement.translation.into(), + grounded: movement.grounded, + is_sliding_down_slope: movement.is_sliding_down_slope, + } + } + + /// Same as [`Self::move_shape`], but tries to use the shape of the specified collider. Can + /// return [`None`] if the `collider_handle` is invalid. + pub fn move_collider_shape( + &self, + dt: f32, + collider_handle: Handle, + position: Isometry3, + desired_translation: Vector3, + graph: &Graph, + filter: QueryFilter, + ) -> Option { + let collider = graph + .physics + .colliders + .get(graph.try_get(collider_handle).ok()?.native.get())?; + Some(self.move_shape( + dt, + collider.shape(), + position, + desired_translation, + graph, + filter, + )) + } +} diff --git a/fyrox-impl/src/scene/graph/physics.rs b/fyrox-impl/src/scene/graph/physics/mod.rs similarity index 97% rename from fyrox-impl/src/scene/graph/physics.rs rename to fyrox-impl/src/scene/graph/physics/mod.rs index 2850efb33..f48c755cf 100644 --- a/fyrox-impl/src/scene/graph/physics.rs +++ b/fyrox-impl/src/scene/graph/physics/mod.rs @@ -20,6 +20,8 @@ //! Scene physics module. +pub mod character; + use crate::{ core::{ algebra::{ @@ -53,7 +55,8 @@ use crate::{ }, utils::raw_mesh::{RawMeshBuilder, RawVertex}, }; -use rapier3d::geometry::Array2; +use fyrox_graph::{NodeWrapper, SceneGraph}; +pub use rapier3d::geometry; use rapier3d::{ dynamics::{ CCDSolver, GenericJoint, GenericJointBuilder, ImpulseJointHandle, ImpulseJointSet, @@ -61,12 +64,13 @@ use rapier3d::{ RigidBodyActivation, RigidBodyBuilder, RigidBodyHandle, RigidBodySet, RigidBodyType, }, geometry::{ - Collider, ColliderBuilder, ColliderHandle, ColliderSet, Cuboid, DefaultBroadPhase, + Array2, Collider, ColliderBuilder, ColliderHandle, ColliderSet, Cuboid, DefaultBroadPhase, InteractionGroups, NarrowPhase, Ray, SharedShape, }, - parry::{query::ShapeCastOptions, shape::HeightField}, - pipeline::{DebugRenderPipeline, EventHandler, PhysicsPipeline}, - prelude::{HeightFieldCellStatus, JointAxis, MassProperties}, + math::{Pose3, Vec3}, + parry::{query::DefaultQueryDispatcher, query::ShapeCastOptions, shape::HeightField}, + pipeline::{DebugRenderPipeline, EventHandler, PhysicsPipeline, QueryPipeline}, + prelude::{FrictionModel, HeightFieldCellStatus, JointAxis, MassProperties}, }; use std::{ cell::Cell, @@ -78,12 +82,6 @@ use std::{ }; use strum_macros::{AsRefStr, EnumString, VariantNames}; -use fyrox_graph::{NodeWrapper, SceneGraph}; -pub use rapier3d::geometry; -use rapier3d::math::{Pose3, Vec3}; -use rapier3d::parry::query::DefaultQueryDispatcher; -use rapier3d::prelude::FrictionModel; - /// Shape-dependent identifier. #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] pub enum FeatureId { @@ -1017,6 +1015,8 @@ fn u32_to_group(v: u32) -> rapier3d::geometry::Group { rapier3d::geometry::Group::from_bits(v).unwrap_or_else(rapier3d::geometry::Group::all) } +pub(crate) type FilterPredicate<'a> = Option<&'a dyn Fn(Handle, &collider::Collider) -> bool>; + /// A filter tha describes what collider should be included or excluded from a scene query. #[derive(Copy, Clone, Default)] #[allow(clippy::type_complexity)] @@ -1031,7 +1031,55 @@ pub struct QueryFilter<'a> { /// If set, any collider attached to this rigid-body will be excluded from the scene query. pub exclude_rigid_body: Option>, /// If set, any collider for which this closure returns false will be excluded from the scene query. - pub predicate: Option<&'a dyn Fn(Handle, &collider::Collider) -> bool>, + pub predicate: FilterPredicate<'a>, +} + +pub(crate) fn filter_by_predicate( + pred: FilterPredicate, + handle: ColliderHandle, + graph: &Graph, + colliders: &ColliderSet, +) -> bool { + if let Some(pred) = pred { + let h = Handle::decode_from_u128(colliders.get(handle).unwrap().user_data); + pred( + h, + graph + .node(h) + .self_or_field_ref::() + .unwrap(), + ) + } else { + true + } +} + +impl<'a> QueryFilter<'a> { + fn to_native( + self, + graph: &Graph, + predicate: &'a dyn Fn(ColliderHandle, &Collider) -> bool, + ) -> rapier3d::pipeline::QueryFilter<'a> { + rapier3d::pipeline::QueryFilter { + flags: rapier3d::pipeline::QueryFilterFlags::from_bits(self.flags.bits()).unwrap(), + groups: self.groups.map(|g| { + InteractionGroups::new( + u32_to_group(g.memberships.0), + u32_to_group(g.filter.0), + Default::default(), + ) + }), + exclude_collider: self + .exclude_collider + .and_then(|h| graph.try_get_of_type::(h).ok()) + .map(|c| c.native.get()), + exclude_rigid_body: self + .exclude_collider + .and_then(|h| graph.try_get_of_type::(h).ok()) + .map(|c| c.native.get()), + predicate: Some(predicate), + } + } } /// The result of a time-of-impact (TOI) computation. @@ -1218,20 +1266,29 @@ impl PhysicsWorld { ); } + pub(crate) fn query_pipeline<'a>( + &'a self, + filter: rapier3d::pipeline::QueryFilter<'a>, + ) -> QueryPipeline<'a> { + self.broad_phase.as_query_pipeline( + &DefaultQueryDispatcher, + &self.bodies, + &self.colliders, + filter, + ) + } + /// Casts a ray with given options. pub fn cast_ray(&self, opts: RayCastOptions, query_buffer: &mut S) { let time = instant::Instant::now(); - let query = self.broad_phase.as_query_pipeline( - &DefaultQueryDispatcher, - &self.bodies, - &self.colliders, - rapier3d::pipeline::QueryFilter::new().groups(InteractionGroups::new( + let query = self.query_pipeline(rapier3d::pipeline::QueryFilter::new().groups( + InteractionGroups::new( u32_to_group(opts.groups.memberships.0), u32_to_group(opts.groups.filter.0), Default::default(), - )), - ); + ), + )); query_buffer.clear(); let ray = Ray::new( @@ -1298,46 +1355,10 @@ impl PhysicsWorld { filter: QueryFilter, ) -> Option<(Handle, TOI)> { let predicate = |handle: ColliderHandle, _: &Collider| -> bool { - if let Some(pred) = filter.predicate { - let h = Handle::decode_from_u128(self.colliders.get(handle).unwrap().user_data); - pred( - h, - graph - .node(h) - .self_or_field_ref::() - .unwrap(), - ) - } else { - true - } + filter_by_predicate(filter.predicate, handle, graph, &self.colliders) }; - - let filter = rapier3d::pipeline::QueryFilter { - flags: rapier3d::pipeline::QueryFilterFlags::from_bits(filter.flags.bits()).unwrap(), - groups: filter.groups.map(|g| { - InteractionGroups::new( - u32_to_group(g.memberships.0), - u32_to_group(g.filter.0), - Default::default(), - ) - }), - exclude_collider: filter - .exclude_collider - .and_then(|h| graph.try_get_of_type::(h).ok()) - .map(|c| c.native.get()), - exclude_rigid_body: filter - .exclude_collider - .and_then(|h| graph.try_get_of_type::(h).ok()) - .map(|c| c.native.get()), - predicate: Some(&predicate), - }; - - let query = self.broad_phase.as_query_pipeline( - &DefaultQueryDispatcher, - &self.bodies, - &self.colliders, - filter, - ); + let filter = filter.to_native(graph, &predicate); + let query = self.query_pipeline(filter); let opts = ShapeCastOptions { max_time_of_impact: max_toi,