bevy_picking_xpbd/
lib.rs

1//! A raycasting backend for `bevy_mod_picking` that uses `xpbd` for raycasting.
2//!
3//! # Usage
4//!
5//! Pointers will automatically shoot rays into the xpbd scene and pick entities.
6//!
7//! To ignore an entity, you can add [`Pickable::IGNORE`] to it, and it will be ignored during
8//! raycasting.
9//!
10//! For fine-grained control, see the [`XpbdBackendSettings::require_markers`] setting.
11//!
12//! ## Limitations
13//!
14//! Because raycasting is expensive, only the closest intersection will be reported. This means that
15//! unlike some UI, you cannot hover multiple xpbd objects with a single pointer by configuring the
16//! [`Pickable`] component to not block lower elements but still emit events. As mentioned above,
17//! all that is supported is completely ignoring an entity with [`Pickable::IGNORE`].
18//!
19//! This is probably not a meaningful limitation, as the feature is usually only used in UI where
20//! you might want a pointer to be able to pick multiple elements that are on top of each other. If
21//! are trying to build a UI out of xpbd entities, beware, I suppose.
22
23#![allow(clippy::type_complexity)]
24#![allow(clippy::too_many_arguments)]
25#![deny(missing_docs)]
26
27use bevy_app::prelude::*;
28use bevy_ecs::prelude::*;
29use bevy_reflect::{std_traits::ReflectDefault, Reflect};
30use bevy_render::{prelude::*, view::RenderLayers};
31
32use bevy_picking_core::backend::prelude::*;
33use bevy_xpbd_3d::prelude::*;
34
35// Re-export for users who want this
36pub use bevy_xpbd_3d;
37
38/// Commonly used imports.
39pub mod prelude {
40    pub use crate::{XpbdBackend, XpbdBackendSettings, XpbdPickable};
41}
42
43/// Adds the `xpbd_3d` raycasting picking backend to your app.
44#[derive(Clone)]
45pub struct XpbdBackend;
46impl Plugin for XpbdBackend {
47    fn build(&self, app: &mut App) {
48        app.init_resource::<XpbdBackendSettings>()
49            .add_systems(PreUpdate, update_hits.in_set(PickSet::Backend))
50            .register_type::<XpbdBackendSettings>()
51            .register_type::<XpbdPickable>();
52    }
53}
54
55/// Runtime settings for the [`XpbdBackend`].
56#[derive(Resource, Default, Reflect)]
57#[reflect(Resource, Default)]
58pub struct XpbdBackendSettings {
59    /// When set to `true` raycasting will only happen between cameras and entities marked with
60    /// [`XpbdPickable`]. Off by default. This setting is provided to give you fine-grained
61    /// control over which cameras and entities should be used by the xpbd backend at runtime.
62    pub require_markers: bool,
63}
64
65/// Optional. Marks cameras and target entities that should be used in the xpbd picking backend.
66/// Only needed if [`XpbdBackendSettings::require_markers`] is set to true.
67#[derive(Debug, Clone, Default, Component, Reflect)]
68#[reflect(Component, Default)]
69pub struct XpbdPickable;
70
71/// Raycasts into the scene using [`XpbdBackendSettings`] and [`PointerLocation`]s, then outputs
72/// [`PointerHits`].
73pub fn update_hits(
74    picking_cameras: Query<(&Camera, Option<&XpbdPickable>, Option<&RenderLayers>)>,
75    ray_map: Res<RayMap>,
76    pickables: Query<&Pickable>,
77    marked_targets: Query<&XpbdPickable>,
78    layers: Query<&RenderLayers>,
79    backend_settings: Res<XpbdBackendSettings>,
80    spatial_query: Option<Res<SpatialQueryPipeline>>,
81    mut output_events: EventWriter<PointerHits>,
82) {
83    let Some(spatial_query) = spatial_query else {
84        return;
85    };
86
87    for (&ray_id, &ray) in ray_map.map().iter() {
88        let Ok((camera, cam_pickable, cam_layers)) = picking_cameras.get(ray_id.camera) else {
89            continue;
90        };
91        if backend_settings.require_markers && cam_pickable.is_none() || !camera.is_active {
92            continue;
93        }
94
95        let cam_layers = cam_layers.unwrap_or_default();
96
97        if let Some((entity, hit_data)) = spatial_query
98            .cast_ray_predicate(
99                ray.origin,
100                ray.direction,
101                f32::MAX,
102                true,
103                SpatialQueryFilter::default(),
104                &|entity| {
105                    let marker_requirement =
106                        !backend_settings.require_markers || marked_targets.get(entity).is_ok();
107
108                    // Other entities missing render layers are on the default layer 0
109                    let entity_layers = layers.get(entity).unwrap_or_default();
110                    let render_layers_match = cam_layers.intersects(entity_layers);
111
112                    let is_pickable = pickables
113                        .get(entity)
114                        .map(|p| *p != Pickable::IGNORE)
115                        .unwrap_or(true);
116
117                    marker_requirement && render_layers_match && is_pickable
118                },
119            )
120            .map(|ray_hit_data| {
121                let hit_data = HitData::new(
122                    ray_id.camera,
123                    ray_hit_data.time_of_impact,
124                    Some(ray.origin + (ray.direction * ray_hit_data.time_of_impact)),
125                    Some(ray_hit_data.normal),
126                );
127                (ray_hit_data.entity, hit_data)
128            })
129        {
130            output_events.send(PointerHits::new(
131                ray_id.pointer,
132                vec![(entity, hit_data)],
133                camera.order as f32,
134            ));
135        }
136    }
137}