1mod 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
37pub 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#[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 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 if let Some(owner_collider) = owner_collider
289 && owner_collider
290 .shape()
291 .contains_point(&isometry, &intersection_point.into())
292 {
293 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 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}