66//!
77//! To make physics picking entirely opt-in, set [`PhysicsPickingSettings::require_markers`]
88//! to `true` and add a [`PhysicsPickable`] component to the desired camera and target entities.
9+ //!
10+ //! Cameras can further filter which entities are pickable with the [`PhysicsPickingFilter`] component.
911#.
@@ -31,15 +33,19 @@ impl Plugin for PhysicsPickingPlugin {
3133 fn build ( & self , app : & mut App ) {
3234 app. init_resource :: < PhysicsPickingSettings > ( )
3335 . add_systems ( PreUpdate , update_hits. in_set ( PickSet :: Backend ) )
34- . register_type :: < ( PhysicsPickingSettings , PhysicsPickable ) > ( ) ;
36+ . register_type :: < (
37+ PhysicsPickingSettings ,
38+ PhysicsPickable ,
39+ PhysicsPickingFilter ,
40+ ) > ( ) ;
3541 }
3642}
3743
3844/// Runtime settings for the [`PhysicsPickingPlugin`].
3945#[ derive( Resource , Default , Reflect ) ]
4046#[ reflect( Resource , Default ) ]
4147pub struct PhysicsPickingSettings {
42- /// When set to `true` picking will only happen between cameras and entities marked with
48+ /// When set to `true`, picking will only happen between cameras and entities marked with
4349 /// [`PhysicsPickable`]. `false` by default.
4450 ///
4551 /// This setting is provided to give you fine-grained control over which cameras and entities
@@ -53,48 +59,94 @@ pub struct PhysicsPickingSettings {
5359#[ reflect( Component , Default ) ]
5460pub struct PhysicsPickable ;
5561
62+ /// An optional component with a [`SpatialQueryFilter`] to determine
63+ /// which physics entities a camera considers for [physics picking](crate::picking).
64+ ///
65+ /// If not present on a camera, picking will consider all colliders.
66+ #[ derive( Component , Clone , Debug , Default , Reflect ) ]
67+ #[ reflect( Component , Debug , Default ) ]
68+ pub struct PhysicsPickingFilter ( pub SpatialQueryFilter ) ;
69+
70+ impl PhysicsPickingFilter {
71+ /// Creates a new [`PhysicsPickingFilter`] with the given [`LayerMask`] determining
72+ /// which [collision layers] will be pickable.
73+ ///
74+ /// [collision layers]: CollisionLayers
75+ /// [spatial query]: crate::spatial_query
76+ pub fn from_mask ( mask : impl Into < LayerMask > ) -> Self {
77+ Self ( SpatialQueryFilter :: from_mask ( mask) )
78+ }
79+
80+ /// Creates a new [`PhysicsPickingFilter`] with the given entities excluded from physics picking.
81+ ///
82+ /// [spatial query]: crate::spatial_query
83+ pub fn from_excluded_entities ( entities : impl IntoIterator < Item = Entity > ) -> Self {
84+ Self ( SpatialQueryFilter :: from_excluded_entities ( entities) )
85+ }
86+
87+ /// Sets the [`LayerMask`] of the filter configuration. Only colliders with the corresponding
88+ /// [collision layer memberships] will be pickable.
89+ ///
90+ /// [collision layer memberships]: CollisionLayers
91+ /// [spatial query]: crate::spatial_query
92+ pub fn with_mask ( mut self , masks : impl Into < LayerMask > ) -> Self {
93+ self . 0 . mask = masks. into ( ) ;
94+ self
95+ }
96+
97+ /// Excludes the given entities from physics picking.
98+ pub fn with_excluded_entities ( mut self , entities : impl IntoIterator < Item = Entity > ) -> Self {
99+ self . 0 . excluded_entities = EntityHashSet :: from_iter ( entities) ;
100+ self
101+ }
102+ }
103+
104+ // Store a const reference to the default filter to avoid unnecessary allocations.
105+ const DEFAULT_FILTER_REF : & PhysicsPickingFilter =
106+ & PhysicsPickingFilter ( SpatialQueryFilter :: DEFAULT ) ;
107+
56108/// Queries for collider intersections with pointers using [`PhysicsPickingSettings`] and sends [`PointerHits`] events.
57109pub fn update_hits (
58- picking_cameras : Query < ( & Camera , Option < & PhysicsPickable > , Option < & RenderLayers > ) > ,
110+ picking_cameras : Query < (
111+ & Camera ,
112+ Option < & PhysicsPickingFilter > ,
113+ Option < & PhysicsPickable > ,
114+ ) > ,
59115 ray_map : Res < RayMap > ,
60116 pickables : Query < & PickingBehavior > ,
61117 marked_targets : Query < & PhysicsPickable > ,
62- layers : Query < & RenderLayers > ,
63118 backend_settings : Res < PhysicsPickingSettings > ,
64119 spatial_query : SpatialQuery ,
65120 mut output_events : EventWriter < PointerHits > ,
66121) {
67122 for ( & ray_id, & ray) in ray_map. map ( ) . iter ( ) {
68- let Ok ( ( camera, cam_pickable , cam_layers ) ) = picking_cameras. get ( ray_id. camera ) else {
123+ let Ok ( ( camera, picking_filter , cam_pickable ) ) = picking_cameras. get ( ray_id. camera ) else {
69124 continue ;
70125 } ;
126+
71127 if backend_settings. require_markers && cam_pickable. is_none ( ) || !camera. is_active {
72128 continue ;
73129 }
74130
75- let cam_layers = cam_layers . unwrap_or_default ( ) ;
131+ let filter = picking_filter . unwrap_or ( DEFAULT_FILTER_REF ) ;
76132
77133 #[ cfg( feature = "2d" ) ]
78134 {
79135 let mut hits: Vec < ( Entity , HitData ) > = vec ! [ ] ;
80136
81137 spatial_query. point_intersections_callback (
82138 ray. origin . truncate ( ) . adjust_precision ( ) ,
83- & SpatialQueryFilter :: default ( ) ,
139+ & filter . 0 ,
84140 |entity| {
85141 let marker_requirement =
86142 !backend_settings. require_markers || marked_targets. get ( entity) . is_ok ( ) ;
87143
88- // Other entities missing render layers are on the default layer 0
89- let entity_layers = layers. get ( entity) . unwrap_or_default ( ) ;
90- let render_layers_match = cam_layers. intersects ( entity_layers) ;
91-
92144 let is_pickable = pickables
93145 . get ( entity)
94146 . map ( |p| * p != PickingBehavior :: IGNORE )
95147 . unwrap_or ( true ) ;
96148
97- if marker_requirement && render_layers_match && is_pickable {
149+ if marker_requirement && is_pickable {
98150 hits. push ( (
99151 entity,
100152 HitData :: new ( ray_id. camera , 0.0 , Some ( ray. origin . f32 ( ) ) , None ) ,
@@ -115,21 +167,17 @@ pub fn update_hits(
115167 ray. direction ,
116168 Scalar :: MAX ,
117169 true ,
118- & SpatialQueryFilter :: default ( ) ,
170+ & filter . 0 ,
119171 & |entity| {
120172 let marker_requirement =
121173 !backend_settings. require_markers || marked_targets. get ( entity) . is_ok ( ) ;
122174
123- // Other entities missing render layers are on the default layer 0
124- let entity_layers = layers. get ( entity) . unwrap_or_default ( ) ;
125- let render_layers_match = cam_layers. intersects ( entity_layers) ;
126-
127175 let is_pickable = pickables
128176 . get ( entity)
129177 . map ( |p| * p != PickingBehavior :: IGNORE )
130178 . unwrap_or ( true ) ;
131179
132- marker_requirement && render_layers_match && is_pickable
180+ marker_requirement && is_pickable
133181 } ,
134182 )
135183 . map ( |ray_hit_data| {
0 commit comments