Skip to content

Commit

Permalink
Introduce PhysicsPickingFilter (Jondolf#632)
Browse files Browse the repository at this point in the history
Co-authored-by: Joona Aalto <[email protected]>
  • Loading branch information
morgenthum and Jondolf authored Jan 30, 2025
1 parent fa9b898 commit 11e64a1
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 20 deletions.
2 changes: 2 additions & 0 deletions crates/avian3d/examples/picking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
//! By default, the [`PhysicsPickingPlugin`] will test intersections with the pointer against all colliders.
//! If you want physics picking to be opt-in, you can set [`PhysicsPickingSettings::require_markers`] to `true`
//! and add a [`PhysicsPickable`] component to the desired camera and target entities.
//!
//! Cameras can further filter which entities are pickable with the [`PhysicsPickingFilter`] component.
use std::f32::consts::PI;

Expand Down
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,9 @@ pub mod prelude {
#[cfg(feature = "debug-plugin")]
pub use crate::debug_render::*;
#[cfg(feature = "bevy_picking")]
pub use crate::picking::{PhysicsPickable, PhysicsPickingPlugin, PhysicsPickingSettings};
pub use crate::picking::{
PhysicsPickable, PhysicsPickingFilter, PhysicsPickingPlugin, PhysicsPickingSettings,
};
#[cfg(feature = "default-collider")]
pub(crate) use crate::position::RotationValue;
pub use crate::{
Expand Down
86 changes: 67 additions & 19 deletions src/picking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
//!
//! To make physics picking entirely opt-in, set [`PhysicsPickingSettings::require_markers`]
//! to `true` and add a [`PhysicsPickable`] component to the desired camera and target entities.
//!
//! Cameras can further filter which entities are pickable with the [`PhysicsPickingFilter`] component.
#![cfg_attr(
feature = "3d",
doc = "
Expand All @@ -15,12 +17,12 @@ Note that in 3D, only the closest intersection will be reported."

use crate::prelude::*;
use bevy::{
ecs::entity::EntityHashSet,
picking::{
backend::{ray::RayMap, HitData, PointerHits},
PickSet,
},
prelude::*,
render::view::RenderLayers,
};

/// Adds the physics picking backend to your app, enabling picking for [colliders](Collider).
Expand All @@ -31,15 +33,19 @@ impl Plugin for PhysicsPickingPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<PhysicsPickingSettings>()
.add_systems(PreUpdate, update_hits.in_set(PickSet::Backend))
.register_type::<(PhysicsPickingSettings, PhysicsPickable)>();
.register_type::<(
PhysicsPickingSettings,
PhysicsPickable,
PhysicsPickingFilter,
)>();
}
}

/// Runtime settings for the [`PhysicsPickingPlugin`].
#[derive(Resource, Default, Reflect)]
#[reflect(Resource, Default)]
pub struct PhysicsPickingSettings {
/// When set to `true` picking will only happen between cameras and entities marked with
/// When set to `true`, picking will only happen between cameras and entities marked with
/// [`PhysicsPickable`]. `false` by default.
///
/// This setting is provided to give you fine-grained control over which cameras and entities
Expand All @@ -53,48 +59,94 @@ pub struct PhysicsPickingSettings {
#[reflect(Component, Default)]
pub struct PhysicsPickable;

/// An optional component with a [`SpatialQueryFilter`] to determine
/// which physics entities a camera considers for [physics picking](crate::picking).
///
/// If not present on a camera, picking will consider all colliders.
#[derive(Component, Clone, Debug, Default, Reflect)]
#[reflect(Component, Debug, Default)]
pub struct PhysicsPickingFilter(pub SpatialQueryFilter);

impl PhysicsPickingFilter {
/// Creates a new [`PhysicsPickingFilter`] with the given [`LayerMask`] determining
/// which [collision layers] will be pickable.
///
/// [collision layers]: CollisionLayers
/// [spatial query]: crate::spatial_query
pub fn from_mask(mask: impl Into<LayerMask>) -> Self {
Self(SpatialQueryFilter::from_mask(mask))
}

/// Creates a new [`PhysicsPickingFilter`] with the given entities excluded from physics picking.
///
/// [spatial query]: crate::spatial_query
pub fn from_excluded_entities(entities: impl IntoIterator<Item = Entity>) -> Self {
Self(SpatialQueryFilter::from_excluded_entities(entities))
}

/// Sets the [`LayerMask`] of the filter configuration. Only colliders with the corresponding
/// [collision layer memberships] will be pickable.
///
/// [collision layer memberships]: CollisionLayers
/// [spatial query]: crate::spatial_query
pub fn with_mask(mut self, masks: impl Into<LayerMask>) -> Self {
self.0.mask = masks.into();
self
}

/// Excludes the given entities from physics picking.
pub fn with_excluded_entities(mut self, entities: impl IntoIterator<Item = Entity>) -> Self {
self.0.excluded_entities = EntityHashSet::from_iter(entities);
self
}
}

// Store a const reference to the default filter to avoid unnecessary allocations.
const DEFAULT_FILTER_REF: &PhysicsPickingFilter =
&PhysicsPickingFilter(SpatialQueryFilter::DEFAULT);

/// Queries for collider intersections with pointers using [`PhysicsPickingSettings`] and sends [`PointerHits`] events.
pub fn update_hits(
picking_cameras: Query<(&Camera, Option<&PhysicsPickable>, Option<&RenderLayers>)>,
picking_cameras: Query<(
&Camera,
Option<&PhysicsPickingFilter>,
Option<&PhysicsPickable>,
)>,
ray_map: Res<RayMap>,
pickables: Query<&PickingBehavior>,
marked_targets: Query<&PhysicsPickable>,
layers: Query<&RenderLayers>,
backend_settings: Res<PhysicsPickingSettings>,
spatial_query: SpatialQuery,
mut output_events: EventWriter<PointerHits>,
) {
for (&ray_id, &ray) in ray_map.map().iter() {
let Ok((camera, cam_pickable, cam_layers)) = picking_cameras.get(ray_id.camera) else {
let Ok((camera, picking_filter, cam_pickable)) = picking_cameras.get(ray_id.camera) else {
continue;
};

if backend_settings.require_markers && cam_pickable.is_none() || !camera.is_active {
continue;
}

let cam_layers = cam_layers.unwrap_or_default();
let filter = picking_filter.unwrap_or(DEFAULT_FILTER_REF);

#[cfg(feature = "2d")]
{
let mut hits: Vec<(Entity, HitData)> = vec![];

spatial_query.point_intersections_callback(
ray.origin.truncate().adjust_precision(),
&SpatialQueryFilter::default(),
&filter.0,
|entity| {
let marker_requirement =
!backend_settings.require_markers || marked_targets.get(entity).is_ok();

// Other entities missing render layers are on the default layer 0
let entity_layers = layers.get(entity).unwrap_or_default();
let render_layers_match = cam_layers.intersects(entity_layers);

let is_pickable = pickables
.get(entity)
.map(|p| *p != PickingBehavior::IGNORE)
.unwrap_or(true);

if marker_requirement && render_layers_match && is_pickable {
if marker_requirement && is_pickable {
hits.push((
entity,
HitData::new(ray_id.camera, 0.0, Some(ray.origin.f32()), None),
Expand All @@ -115,21 +167,17 @@ pub fn update_hits(
ray.direction,
Scalar::MAX,
true,
&SpatialQueryFilter::default(),
&filter.0,
&|entity| {
let marker_requirement =
!backend_settings.require_markers || marked_targets.get(entity).is_ok();

// Other entities missing render layers are on the default layer 0
let entity_layers = layers.get(entity).unwrap_or_default();
let render_layers_match = cam_layers.intersects(entity_layers);

let is_pickable = pickables
.get(entity)
.map(|p| *p != PickingBehavior::IGNORE)
.unwrap_or(true);

marker_requirement && render_layers_match && is_pickable
marker_requirement && is_pickable
},
)
.map(|ray_hit_data| {
Expand Down

0 comments on commit 11e64a1

Please sign in to comment.