Skip to main content

bevy_tnua_rapier3d/
lib.rs

1//! # bevy_rapier3d Integration for bevy-tnua
2//!
3//! In addition to the instruction in bevy-tnua's documentation:
4//!
5//! * Add [`TnuaRapier3dPlugin`] to the Bevy app.
6//! * Optionally: Add [`TnuaRapier3dSensorShape`] to either entity of the character controller by
7//!   Tnua or to the to the sensor entities. If exists, the shape on the sensor entity overrides
8//!   the one on the character entity for that specific sensor.
9mod helpers;
10mod spatial_ext;
11
12use bevy::ecs::schedule::{InternedScheduleLabel, ScheduleLabel};
13use bevy::platform::collections::HashSet;
14use bevy::prelude::*;
15use bevy_rapier3d::rapier;
16use bevy_rapier3d::rapier::prelude::InteractionGroups;
17use bevy_rapier3d::{parry, prelude::*};
18
19use bevy_tnua_physics_integration_layer::TnuaPipelineSystems;
20use bevy_tnua_physics_integration_layer::TnuaSystems;
21use bevy_tnua_physics_integration_layer::data_for_backends::TnuaGravity;
22use bevy_tnua_physics_integration_layer::data_for_backends::TnuaToggle;
23use bevy_tnua_physics_integration_layer::data_for_backends::{TnuaGhostPlatform, TnuaNotPlatform};
24use bevy_tnua_physics_integration_layer::data_for_backends::{TnuaGhostSensor, TnuaSensorOf};
25use bevy_tnua_physics_integration_layer::data_for_backends::{
26    TnuaMotor, TnuaProximitySensor, TnuaProximitySensorOutput, TnuaRigidBodyTracker,
27};
28use bevy_tnua_physics_integration_layer::obstacle_radar::TnuaObstacleRadar;
29pub use spatial_ext::TnuaSpatialExtRapier3d;
30
31use self::helpers::PretendToBeRapierContext;
32
33pub mod prelude {
34    pub use crate::{TnuaRapier3dPlugin, TnuaRapier3dSensorShape, TnuaSpatialExtRapier3d};
35}
36
37/// Add this plugin to use bevy_rapier2d as a physics backend.
38///
39/// This plugin should be used in addition to `TnuaControllerPlugin`, and both plugins must use the
40/// same schedule - which should match the schedule Rapier runs in. By default, Rapier runs in
41/// [`PostUpdate`] - which means this plugin and `TnuaControllerPlugin` should run in [`Update`].
42pub struct TnuaRapier3dPlugin {
43    schedule: InternedScheduleLabel,
44}
45
46impl TnuaRapier3dPlugin {
47    pub fn new(schedule: impl ScheduleLabel) -> Self {
48        Self {
49            schedule: schedule.intern(),
50        }
51    }
52}
53
54impl Plugin for TnuaRapier3dPlugin {
55    fn build(&self, app: &mut App) {
56        app.register_required_components::<TnuaMotor, Velocity>()
57            .register_required_components::<TnuaMotor, ExternalForce>()
58            .register_required_components::<TnuaMotor, ReadMassProperties>()
59            .register_required_components_with::<TnuaGravity, GravityScale>(|| GravityScale(0.0));
60        app.configure_sets(
61            self.schedule,
62            TnuaSystems.before(PhysicsSet::SyncBackend).run_if(
63                |rapier_config: Single<&RapierConfiguration>| rapier_config.physics_pipeline_active,
64            ),
65        );
66        app.add_systems(
67            self.schedule,
68            (
69                update_rigid_body_trackers_system,
70                update_proximity_sensors_system,
71                update_obstacle_radars_system,
72            )
73                .in_set(TnuaPipelineSystems::Sensors),
74        );
75        app.add_systems(
76            self.schedule,
77            apply_motors_system.in_set(TnuaPipelineSystems::Motors),
78        );
79    }
80}
81
82/// Add this component to make [`TnuaProximitySensor`] cast a shape instead of a ray.
83///
84/// The [`SharedShape`](parry::shape::SharedShape) can be constructed using the re-exported
85/// [`bevy_rapier3d::parry`], or by constructing a [`Collider`] first and taking it's
86/// [`raw`](Collider::raw) field.
87#[derive(Component)]
88pub struct TnuaRapier3dSensorShape(pub parry::shape::SharedShape);
89
90#[allow(clippy::type_complexity)]
91fn update_rigid_body_trackers_system(
92    rapier_config: Single<&RapierConfiguration>,
93    mut query: Query<(
94        &GlobalTransform,
95        &Velocity,
96        &mut TnuaRigidBodyTracker,
97        Option<&TnuaToggle>,
98        Option<&TnuaGravity>,
99    )>,
100) {
101    for (transform, velocity, mut tracker, tnua_toggle, tnua_gravity) in query.iter_mut() {
102        match tnua_toggle.copied().unwrap_or_default() {
103            TnuaToggle::Disabled => continue,
104            TnuaToggle::SenseOnly => {}
105            TnuaToggle::Enabled => {}
106        }
107        let (_, rotation, translation) = transform.to_scale_rotation_translation();
108        *tracker = TnuaRigidBodyTracker {
109            translation,
110            rotation,
111            velocity: velocity.linvel,
112            angvel: velocity.angvel,
113            gravity: tnua_gravity.map(|g| g.0).unwrap_or(rapier_config.gravity),
114        };
115    }
116}
117
118pub(crate) fn get_collider(
119    rapier_colliders: &RapierContextColliders,
120    entity: Entity,
121) -> Option<&rapier::geometry::Collider> {
122    let collider_handle = rapier_colliders.entity2collider().get(&entity)?;
123    rapier_colliders.colliders.get(*collider_handle)
124}
125
126#[allow(clippy::type_complexity)]
127fn update_proximity_sensors_system(
128    rapier_context_query: Query<PretendToBeRapierContext>,
129    mut sensor_query: Query<(
130        &mut TnuaProximitySensor,
131        &TnuaSensorOf,
132        Option<&TnuaRapier3dSensorShape>,
133        Option<&mut TnuaGhostSensor>,
134    )>,
135    owner_query: Query<(
136        &RapierContextEntityLink,
137        &GlobalTransform,
138        Option<&TnuaRapier3dSensorShape>,
139        Option<&TnuaToggle>,
140    )>,
141    ghost_platforms_query: Query<(), With<TnuaGhostPlatform>>,
142    not_platform_query: Query<(), With<TnuaNotPlatform>>,
143    other_object_query: Query<(&GlobalTransform, &Velocity)>,
144) {
145    sensor_query.par_iter_mut().for_each(
146        |(mut sensor, &TnuaSensorOf(owner_entity), shape, mut ghost_sensor)| {
147            let Ok((rapier_context_entity_link, transform, owner_shape, tnua_toggle)) =
148                owner_query.get(owner_entity)
149            else {
150                return;
151            };
152            let shape = shape.or(owner_shape);
153            match tnua_toggle.copied().unwrap_or_default() {
154                TnuaToggle::Disabled => return,
155                TnuaToggle::SenseOnly => {}
156                TnuaToggle::Enabled => {}
157            }
158
159            let Ok(rapier_context) = rapier_context_query.get(rapier_context_entity_link.0) else {
160                return;
161            };
162
163            let cast_origin = transform.transform_point(sensor.cast_origin);
164            let cast_direction = sensor.cast_direction;
165
166            struct CastResult {
167                entity: Entity,
168                proximity: f32,
169                intersection_point: Vec3,
170                normal: Dir3,
171            }
172
173            let mut query_filter = QueryFilter::new().exclude_rigid_body(owner_entity);
174            let owner_solver_groups: InteractionGroups;
175
176            let owner_collider = get_collider(rapier_context.colliders, owner_entity);
177            if let Some(owner_collider) = owner_collider {
178                let collision_groups = owner_collider.collision_groups();
179                query_filter.groups = Some(CollisionGroups {
180                    memberships: Group::from_bits_truncate(collision_groups.memberships.bits()),
181                    filters: Group::from_bits_truncate(collision_groups.filter.bits()),
182                });
183                owner_solver_groups = owner_collider.solver_groups();
184            } else {
185                owner_solver_groups = InteractionGroups::all();
186            }
187
188            let mut already_visited_ghost_entities = HashSet::<Entity>::default();
189
190            let has_ghost_sensor = ghost_sensor.is_some();
191
192            let do_cast = |cast_range_skip: f32,
193                           already_visited_ghost_entities: &HashSet<Entity>|
194             -> Option<CastResult> {
195                let predicate = |other_entity: Entity| {
196                    if not_platform_query.contains(other_entity) {
197                        return false;
198                    }
199                    if let Some(other_collider) =
200                        get_collider(rapier_context.colliders, other_entity)
201                    {
202                        if !other_collider.solver_groups().test(owner_solver_groups) {
203                            if has_ghost_sensor && ghost_platforms_query.contains(other_entity) {
204                                if already_visited_ghost_entities.contains(&other_entity) {
205                                    return false;
206                                }
207                            } else {
208                                return false;
209                            }
210                        }
211                        if other_collider.is_sensor() {
212                            return false;
213                        }
214                    }
215                    true
216                };
217                let query_filter = query_filter.predicate(&predicate);
218                let cast_origin = cast_origin + cast_range_skip * *cast_direction;
219                let cast_range = sensor.cast_range - cast_range_skip;
220                rapier_context.with_query_pipeline(query_filter, |query_pipeline| {
221                    if let Some(TnuaRapier3dSensorShape(shape)) = shape {
222                        // TODO: can I bake `owner_rotation` into
223                        // `sensor.cast_shape_rotation`?
224                        let (_, owner_rotation, _) = transform.to_scale_rotation_translation();
225                        let owner_rotation = Quat::from_scaled_axis(
226                            owner_rotation.to_scaled_axis().dot(*cast_direction) * *cast_direction,
227                        );
228                        query_pipeline
229                            .cast_shape(
230                                cast_origin,
231                                owner_rotation.mul_quat(sensor.cast_shape_rotation),
232                                *cast_direction,
233                                shape.as_ref(),
234                                ShapeCastOptions {
235                                    max_time_of_impact: cast_range,
236                                    target_distance: 0.0,
237                                    stop_at_penetration: false,
238                                    compute_impact_geometry_on_penetration: false,
239                                },
240                            )
241                            .and_then(|(entity, hit)| {
242                                let details = hit.details?;
243                                Some(CastResult {
244                                    entity,
245                                    proximity: hit.time_of_impact,
246                                    intersection_point: details.witness1,
247                                    normal: Dir3::new(details.normal1)
248                                        .unwrap_or_else(|_| -cast_direction),
249                                })
250                            })
251                    } else {
252                        query_pipeline
253                            .cast_ray_and_get_normal(
254                                cast_origin,
255                                *cast_direction,
256                                cast_range,
257                                false,
258                            )
259                            .map(|(entity, hit)| CastResult {
260                                entity,
261                                proximity: hit.time_of_impact,
262                                intersection_point: hit.point,
263                                normal: Dir3::new(hit.normal).unwrap_or_else(|_| -cast_direction),
264                            })
265                    }
266                })
267            };
268
269            let mut cast_range_skip = 0.0;
270            if let Some(ghost_sensor) = ghost_sensor.as_mut() {
271                ghost_sensor.0.clear();
272            }
273            let isometry: rapier::na::Isometry3<f32> = {
274                let (_, rotation, translation) = transform.to_scale_rotation_translation();
275                (translation, rotation).into()
276            };
277            sensor.output = 'sensor_output: loop {
278                if let Some(CastResult {
279                    entity,
280                    proximity,
281                    intersection_point,
282                    normal,
283                }) = do_cast(cast_range_skip, &already_visited_ghost_entities)
284                {
285                    // Alternative fix for https://github.com/idanarye/bevy-tnua/issues/14 - one
286                    // that does not cause https://github.com/idanarye/bevy-tnua/issues/85
287                    // Note that this does not solve https://github.com/idanarye/bevy-tnua/issues/87
288                    if let Some(owner_collider) = owner_collider
289                        && owner_collider
290                            .shape()
291                            .contains_point(&isometry, &intersection_point.into())
292                    {
293                        // I hate having to do this so much, but without it it sometimes enters an
294                        // infinte loop...
295                        cast_range_skip = proximity
296                            + if sensor.cast_range.is_finite() && 0.0 < sensor.cast_range {
297                                0.1 * sensor.cast_range
298                            } else {
299                                0.1
300                            };
301                        continue;
302                    }
303
304                    let entity_linvel;
305                    let entity_angvel;
306                    if let Ok((entity_transform, entity_velocity)) = other_object_query.get(entity)
307                    {
308                        entity_angvel = entity_velocity.angvel;
309                        entity_linvel = entity_velocity.linvel
310                            + if 0.0 < entity_angvel.length_squared() {
311                                let relative_point =
312                                    intersection_point - entity_transform.translation();
313                                // NOTE: no need to project relative_point on the rotation plane, it will not
314                                // affect the cross product.
315                                entity_angvel.cross(relative_point)
316                            } else {
317                                Vec3::ZERO
318                            };
319                    } else {
320                        entity_angvel = Vec3::ZERO;
321                        entity_linvel = Vec3::ZERO;
322                    }
323                    let sensor_output = TnuaProximitySensorOutput {
324                        entity,
325                        proximity,
326                        normal,
327                        entity_linvel,
328                        entity_angvel,
329                    };
330                    if ghost_platforms_query.contains(entity) {
331                        cast_range_skip = proximity;
332                        already_visited_ghost_entities.insert(entity);
333                        if let Some(ghost_sensor) = ghost_sensor.as_mut() {
334                            ghost_sensor.0.push(sensor_output);
335                        }
336                    } else {
337                        break 'sensor_output Some(sensor_output);
338                    }
339                } else {
340                    break 'sensor_output None;
341                }
342            };
343        },
344    );
345}
346
347fn update_obstacle_radars_system(
348    rapier_world_query: Query<(PretendToBeRapierContext, &RapierConfiguration)>,
349    mut radars_query: Query<(
350        Entity,
351        &RapierContextEntityLink,
352        &mut TnuaObstacleRadar,
353        &GlobalTransform,
354    )>,
355) {
356    if radars_query.is_empty() {
357        return;
358    }
359    for (radar_owner_entity, rapier_context_entity_link, mut radar, radar_transform) in
360        radars_query.iter_mut()
361    {
362        let Ok((rapier_context, rapier_config)) =
363            rapier_world_query.get(rapier_context_entity_link.0)
364        else {
365            continue;
366        };
367        let (_radar_scale, radar_rotation, radar_translation) =
368            radar_transform.to_scale_rotation_translation();
369        radar.pre_marking_update(
370            radar_owner_entity,
371            radar_translation,
372            Dir3::new(rapier_config.gravity).unwrap_or(Dir3::Y),
373        );
374        rapier_context.with_query_pipeline(Default::default(), |query_pipeline| {
375            for obstacle_entity in query_pipeline.intersect_shape(
376                radar_translation,
377                radar_rotation,
378                &parry::shape::Cylinder::new(0.5 * radar.height, radar.radius),
379            ) {
380                if radar_owner_entity == obstacle_entity {
381                    continue;
382                }
383                radar.mark_seen(obstacle_entity);
384            }
385        });
386    }
387}
388
389#[allow(clippy::type_complexity)]
390fn apply_motors_system(
391    mut query: Query<(
392        &TnuaMotor,
393        &mut Velocity,
394        &ReadMassProperties,
395        &mut ExternalForce,
396        Option<&TnuaToggle>,
397        Option<&TnuaGravity>,
398    )>,
399) {
400    for (motor, mut velocity, mass_properties, mut external_force, tnua_toggle, tnua_gravity) in
401        query.iter_mut()
402    {
403        match tnua_toggle.copied().unwrap_or_default() {
404            TnuaToggle::Disabled | TnuaToggle::SenseOnly => {
405                *external_force = Default::default();
406                return;
407            }
408            TnuaToggle::Enabled => {}
409        }
410        if motor.lin.boost.is_finite() {
411            velocity.linvel += motor.lin.boost;
412        }
413        if motor.lin.acceleration.is_finite() {
414            external_force.force = motor.lin.acceleration * mass_properties.get().mass;
415        }
416        if motor.ang.boost.is_finite() {
417            velocity.angvel += motor.ang.boost;
418        }
419        if motor.ang.acceleration.is_finite() {
420            external_force.torque =
421                motor.ang.acceleration * mass_properties.get().principal_inertia;
422        }
423        if let Some(gravity) = tnua_gravity {
424            external_force.force += gravity.0 * mass_properties.get().mass;
425        }
426    }
427}